chiark / gitweb /
with-lock-ex: Provide -t (timeout) option
[chiark-utils.git] / cprogs / with-lock-ex.c
1 /*
2  * File locker
3  *
4  * Usage:
5  *  with-lock-ex -<mode> [-t <secs>] <lockfile> <command> <args>...
6  *  with-lock-ex -l      <lockfile>
7  *
8  * modes are
9  *  w    wait for the lock
10  *  f    fail if the lock cannot be acquired
11  *  q    silently do nothing if the lock cannot be acquired
12  *  l    show who is waiting (print "none" or "read <pid>"
13  *         or "write <pid>"; lockfile opened for reading;
14  *         no command may be specified)
15  *
16  * if -t is specified, then with-lock-ex will wait for up to <secs>
17  * seconds to acquire the lock, and then fail or silently do nothing
18  * (depending on whether -f or -q is specified). You cannot specify
19  * a timeout for modes l or w
20  *
21  * with-lock-ex will open and lock the lockfile for writing and
22  * then feed the remainder of its arguments to exec(2); when
23  * that process terminates the fd will be closed and the file
24  * unlocked automatically by the kernel.
25  *
26  * If invoked as with-lock, behaves like with-lock-ex -f (for backward
27  * compatibility with an earlier version).
28  *
29  * This file written by me, Ian Jackson, in 1993, 1994, 1995, 1996,
30  * 1998, 1999, 2016.
31  *
32  * Copyright 1993-2016 Ian Jackson in some jurisdictions
33  * Copyright 2017      Ian Jackson in all jurisdictions
34  * Copyright 2017      Genome Research Ltd
35  *
36  * (MIT licence:)
37  *
38  * Permission is hereby granted, free of charge, to any person obtaining a
39  * copy of this software and associated documentation files (the "Software"),
40  * to deal in the Software without restriction, including without limitation
41  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
42  * and/or sell copies of the Software, and to permit persons to whom the
43  * Software is furnished to do so, subject to the following conditions:
44  *
45  * The above copyright notice and this permission notice shall be included in
46  * all copies or substantial portions of the Software.
47  *
48  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
49  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
50  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
51  * SOFTWARE IN THE PUBLIC INTEREST, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR
52  * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
53  * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
54  * DEALINGS IN THE SOFTWARE.
55  */
56
57 #include <errno.h>
58 #include <fcntl.h>
59 #include <stdio.h>
60 #include <stdlib.h>
61 #include <unistd.h>
62 #include <string.h>
63 #include <sys/stat.h>
64 #include <limits.h>
65 #include <signal.h>
66 #include <sys/time.h>
67
68 static const char *cmd;
69
70 static void fail(const char *why) __attribute__((noreturn));
71
72 static void fail(const char *why) {
73   fprintf(stderr,"with-lock-ex %s: %s: %s\n",cmd,why,strerror(errno));
74   exit(255);
75 }
76
77 static void badusage(void) __attribute__((noreturn));
78
79 static void badusage(void) {
80     fputs("usage: with-lock-ex -w|-q|-f [-t <secs>] <lockfile> <command> <args>...\n"
81           "       with-lock-ex -l       <lockfile>\n"
82           "       with-lock             <lockfile> <command> <args>...\n",
83           stderr);
84     exit(255);
85 }
86
87 volatile int gotlock = 0;
88 static int mode;
89
90 /* This signal handler uses unsafe functions, so MUST NOT be callable
91  * during an unsafe function, as that is Undefined Behaviour
92  */
93 static void alrm_handler(int signum) {
94   if (!gotlock) {
95     if (mode=='q') {
96       exit(0);
97     } else {
98       fprintf(stderr,
99               "with-lock-ex %s: timer expired while trying to acquire lock\n",
100               cmd);
101       exit(255);
102     }
103   }
104 }
105
106 int main(int argc, char **argv) {
107   int fd, um, c, r;
108   struct stat stab, fstab;
109   long cloexec, secs=0;
110   struct flock fl;
111   char *endptr;
112   sigset_t sigs, oldsigs;
113   struct sigaction siga;
114   struct itimerval itv;
115
116   mode='x';
117   while ((c = getopt(argc,argv,"+wfqlt:")) != -1)
118     switch(c) {
119     case 'l':
120     case 'w':
121     case 'f':
122     case 'q':
123       if (mode != 'x') badusage();
124       mode = c;
125       break;
126     case 't':
127       errno = 0;
128       secs = strtol(optarg, &endptr, 0);
129       if (*endptr || endptr==optarg || errno==ERANGE)
130         fail("parsing timeout value");
131       if (secs < 0) {
132         fprintf(stderr,"timeout value must be >=0\n");
133         exit(255);
134       }
135       break;
136     default:
137       badusage();
138     }
139
140   if (secs && (mode=='l' || mode=='w')) {
141     fputs("-t only allowed with -q or -f.\n", stderr);
142     exit(255);
143   }
144
145   argv += optind-1; argc -= optind-1;
146   if (argc < 2) badusage();
147
148   if (secs) {
149     if (sigemptyset(&sigs)) fail("Initialising signal set");
150     if (sigaddset(&sigs,SIGALRM)) fail("Adding SIGALRM to signal set");
151     if (sigprocmask(SIG_BLOCK,&sigs,&oldsigs)) fail("Blocking SIGALRM");
152     memset(&siga,0,sizeof(siga));
153     siga.sa_handler=alrm_handler;
154     if (sigaction(SIGALRM,&siga,NULL)) fail("Installing SIGALRM handler");
155     memset(&itv,0,sizeof(itv));
156     itv.it_value.tv_sec=secs;
157     if (setitimer(ITIMER_REAL,&itv,NULL)) fail("Setting timer");
158   }
159
160   cmd= argv[2];
161   um= umask(0777); if (um==-1) fail("find umask");
162   if (umask(um)==-1) fail("reset umask");
163
164   for (;;) {
165
166     int openmode = mode=='l' ? O_RDONLY : O_RDWR|O_CREAT;
167
168     fd= open(argv[1],openmode,0666&~(um|((um&0222)<<1)));
169     if (fd<0) fail(argv[1]);
170   
171     for (;;) {
172       fl.l_type= F_WRLCK;
173       fl.l_whence= SEEK_SET;
174       fl.l_start= 0;
175       fl.l_len= mode=='l' ? 0 : 1;
176       if (secs) sigprocmask(SIG_UNBLOCK,&sigs,NULL);
177       r = fcntl(fd,
178                 mode=='l' ? F_GETLK :
179                 mode=='w' || secs > 0 ? F_SETLKW :
180                 F_SETLK,
181                 &fl);
182       if (secs) sigprocmask(SIG_BLOCK,&sigs,NULL);
183       if (!r) {
184         gotlock=1;
185         break;
186       }
187       if (mode=='q' &&
188           (errno == EAGAIN || errno == EWOULDBLOCK || errno == EBUSY))
189         exit(0);
190       if (errno != EINTR) fail("could not acquire lock");
191     }
192     if (mode=='l') {
193       if (fl.l_pid) {
194         printf("%s %lu\n",
195                fl.l_type == F_WRLCK ? "write" :
196                fl.l_type == F_RDLCK ? "read" : "unknown",
197                (unsigned long)fl.l_pid);
198       } else {
199         printf("none\n");
200       }
201       if (ferror(stdout)) fail("print to stdout\n");
202       exit(0);
203     }
204     if (secs) {
205       itv.it_value.tv_sec=0;
206       if (setitimer(ITIMER_REAL,&itv,NULL)) fail("Clearing timer");
207       sigprocmask(SIG_SETMASK,&oldsigs,NULL);
208       siga.sa_handler=SIG_DFL;
209       sigaction(SIGALRM,&siga,NULL);
210     }
211
212     if (fstat(fd, &fstab)) fail("could not fstat lock fd");
213     if (stat(argv[1], &stab)) {
214       if (errno != ENOENT) fail("could not stat lockfile");
215     } else {
216       if (stab.st_dev == fstab.st_dev &&
217           stab.st_ino == fstab.st_ino) break;
218     }
219     close(fd);
220   }
221
222   cloexec= fcntl(fd, F_GETFD); if (cloexec==-1) fail("fcntl F_GETFD");
223   cloexec &= ~1;
224   if (fcntl(fd, F_SETFD, cloexec)==-1) fail("fcntl F_SETFD");
225
226   execvp(cmd,argv+2);
227   fail("unable to execute command");
228 }