chiark / gitweb /
boolean option
[chiark-utils.git] / cprogs / watershed.c
1 /*
2  * watershed - an auxiliary verb for optimising away
3  *             unnecessary runs of idempotent commands
4  *
5  * watershed is Copyright 2007 Canonical Ltd
6  * written by Ian Jackson <ian@davenant.greenend.org.uk>
7  * and this version now maintained as part of chiark-utils
8  *
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public
21  * License along with this file; if not, consult the Free Software
22  * Foundation's website at www.fsf.org, or the GNU Project website at
23  * www.gnu.org.
24  *
25  */
26 /*
27  *  NB a different fork of this code exists in Ubuntu's udev.
28  */
29 /*
30  *
31  * usage: watershed [<options>] <command> [<arg>...]
32  *
33  * options:
34  *   -d|--state-dir <state-dir>
35  *        default is /var/run/watershed for uid 0
36  *                   $HOME/.watershed for others
37  *   -i|--command-id <command-id>
38  *
39  * files used:
40  *    <state-dir>/<command-id>.lock            lockfile
41  *    <state-dir>/<command-id>.cohort          cohort
42  *
43  * default <command-id> is
44  *    hex(sha256(argv[0]+'\0' + argv[1]+'\0' ... argv[argc-1]+'\0')
45  *    '='
46  *    mangled argv[0] (all chars [^-+_0-9A-Za-z] replaced with ?
47  *                     and max 32 chars)
48  *
49  * exit status:
50  *  127      - something went wrong, or process died with some other signal
51  *  SIGPIPE  - process died with SIGPIPE
52  *  x        - process called _exit(x)
53  *
54  * stdin/stdout/stderr:
55  *
56  *  If watershed exits 127 due to some unexpected problem, a message
57  *  is printed to stderr explaining why (obviously).
58  *
59  *  If a watershed invocation ends up running the process, the process
60  *  simply inherits stdin/out/err.  Otherwise stdin/stdout are not used.
61  *
62  *  If the process run for us by another invocation of watershed exits
63  *  zero, or watershed die with the same signal as the process
64  *  (currently just SIGPIPE), nothing is printed to stderr.  Otherwise
65  *  (ie, failure of the actual process, in another invocation),
66  *  watershed prints a description of the wait status to stderr, much
67  *  as the shell might.
68  *
69  */
70 /*
71  * gcc -Wall -Wwrite-strings -Wmissing-prototypes watershed.c -o watershed /usr/lib/libnettle.a
72  */
73 /*
74  *
75  * Theory:
76  *
77  *  We consider only invocations with a specific command id (and state
78  *  directory), since other invocations are completely independent by
79  *  virtue of having different state file pathnames and thus different
80  *  state files.  Normally, a command id corresponds to invocations
81  *  with a particular set of command line arguments and a state
82  *  directory corresponds to a particular euid; environment variable
83  *  settings and other inherited process properties are disregarded.
84  *
85  *  A `cohort' is a set of invocations which can be coalesced into one
86  *  run of the command.  For each cohort there is a file, the cohort
87  *  file (which may not yet exist, may exist and have a name, or may
88  *  be unliked).
89  *
90  *  An `invocation' is an invocation of the `watershed' program.  A
91  *  `process' is an invocation of the requested command.
92  *
93  *  There is always one current cohort, in one of the following
94  *  two states:
95  *
96  *   * Empty
97  *     No invocations are in this cohort yet.
98  *     The cohort filename is ENOENT.
99  *     This is the initial state for a cohort, and the legal next
100  *     state is Accumulating.
101  *
102  *   * Accumulating
103  *     The process for this run has not yet started, so that new
104  *     invocations arriving would be satisfied if this cohort were to
105  *     run.
106  *     The cohort filename refers to this cohort's file.
107  *     The legal next state for the cohort is Ready.
108  *
109  *  Additionally, there may be older cohorts in the following states:
110  *
111  *   * Ready
112  *     The command for this cohort has not yet been run.
113  *     The cohort file has no name and is empty.
114  *     Only one cohort, the lockholder's, may be in this state.
115  *     The next legal states are Running, or exceptionally Forgotten
116  *     (if the lockholder crashes and is the only invocation in the
117  *     cohort).
118  *
119  *   * Running
120  *     The lockholder is running the command for this cohort.
121  *     This state is identical to Ready from the point of view
122  *     of all invocations except the lockholder.
123  *     The legal next states are Done (the usual case), or (if the
124  *     lockholder crashes) Ready or Forgotten.
125  *
126  *   * Done
127  *     The main process for this run has finished.
128  *     The cohort file has no name and contains sizeof(int)
129  *     bytes, the `status' value from waitpid.
130  *     The legal next state is Forgotten.
131  *
132  *   * Forgotten
133  *     All invocations have finished and the cohort file no longer
134  *     exists.  This is the final state.
135  *
136  *  Only the lockholder may move a cohort between states, except that
137  *  any invocation may make the current Empty cohort become
138  *  Accumulating, and that the kernel will automatically move a cohort
139  *  from Running to Ready or from Done to Forgotten, when appropriate.
140  *
141  * 
142  * Algorithm:
143  *
144  *   1. Open the cohort file (O_CREAT|O_RDWR)   so our cohort is
145  *                                                 Accumulating/Ready/
146  *                                                    Running/Done
147  *                              
148  *   2. Acquire lock (see below)                so lockholder's cohort is
149  *                                                 Accumulating/Ready/Done
150  *   3. fstat the open cohort file
151  *         If it is nonempty:                      Done
152  *          Read status from it and exit.
153  *         Otherwise, if nonzero link count:       Accumulating
154  *          Unlink the cohort filename
155  *         Otherwise:                              Ready
156  *
157  *   4. Fork and run the command                   Running
158  *       and wait for it
159  *
160  *   5. Write the wait status to the cohort file   Done
161  *
162  *                      
163  *   6. Release the lock                        so we are no longer lockholder
164  *                                              but our cohort is still
165  *                                                 Done
166  *
167  *   8. Exit                                       Done/Forgotten
168  *
169  *  If an invocation crashes (ie, if watershed itself fails, rather
170  *  than if the command does) then that invocation's caller will be
171  *  informed of the error.
172  *
173  *  If the lockholder crashes with the cohort in:
174  *
175  *     Accumulating:
176  *       The cohort remains in Accumulating and another invocation can
177  *       become the lockholder.  If there are never any other
178  *       invocations then the lockfile and cohort file will not be
179  *       cleaned up (see below).
180  *
181  *     Running/Ready:
182  *       The cohort goes from Running back to Ready (see above) and
183  *       another invocation in the same cohort will become the
184  *       lockholder and run it.  If there is no other invocation in
185  *       the cohort the cohort goes to Forgotten although the lockfile
186  *       will not be cleaned up - see below.
187  *
188  *     Done:
189  *       If there are no more invocations, the cohort is Forgotten but
190  *       the lockfile is not cleaned up.
191  *
192  * Lockfile:
193  *
194  *  There is one lock for all cohorts.  The lockholder is the
195  *  invocation which holds the fcntl lock on the file whose name is
196  *  the lockfile.  The lockholder (and no-one else) may unlink the
197  *  lockfile.
198  *
199  *  To acquire the lock:
200  *
201  *   1. Open the lockfile (O_CREAT|O_RDWR)
202  *   2. Acquire fcntl lock (F_SETLKW)
203  *   3. fstat the open lockfile and stat the lockfile filenmae
204  *      If inode numbers disagree, close lockfile and start
205  *      again from the beginning.
206  *
207  *  To release the lock, unlink the lockfile and then either close it
208  *  or exit.  Crashing will also release the lock but leave the
209  *  lockfile lying around (which is slightly untidy but not
210  *  incorrect); if this is a problem a cleanup task could periodically
211  *  acquire and release the lock for each lockfile found:
212  *
213  * Cleanup:
214  *
215  *  As described above and below, stale cohort files and lockfiles can
216  *  result from invocations which crashed if the same command is never
217  *  run again.  Such cohorts are always in Empty or Accumulating.
218  *
219  *  If it became necessary to clean up stale cohort files and
220  *  lockfiles resulting from crashes, the following algorithm should
221  *  be executed for each lockfile found, as a cleanup task:
222  *
223  *   1. Acquire the lock.
224  *      This makes us the lockholder.           and the current cohort is in
225  *                                                 Empty/Accumulating
226  *
227  *                                              so now that cohort is
228  *   2. Unlink the cohort file, ignoring ENOENT.   Ready/Forgotten
229  *   3. Release the lock.                          Ready/Forgotten
230  *   4. Exit.                                      Ready/Forgotten
231  *
232  *  This consists only of legal transitions, so if current cohort
233  *  wasn't stale, it will have been moved to Ready and some other
234  *  invocation in this cohort will become the lockholder and as normal
235  *  from step 4 of the main algorithm.  If the cohort was stale it
236  *  will go to Forgotten straight away.
237  *
238  *  A suitable cleanup script, on a system with with-lock-ex, is: */
239  //     #!/bin/sh
240  //     set -e
241  //     if [ $# != 1 ]; echo >&2 'usage: cleanup <statedir>'; exit 1; fi
242  //     cd "$1"
243  //     for f in ./*.lock; do
244  //       with-lock-ex -w rm -f "${f%.lock}.cohort"
245  //     done
246 /*
247  */
248
249 #define _GNU_SOURCE
250
251 #include <stdio.h>
252 #include <stdlib.h>
253 #include <string.h>
254 #include <errno.h>
255 #include <stdarg.h>
256 #include <ctype.h>
257 #include <assert.h>
258
259 #include <sys/types.h>
260 #include <sys/stat.h>
261 #include <sys/wait.h>
262 #include <unistd.h>
263 #include <fcntl.h>
264 #include <getopt.h>
265 #include <locale.h>
266 #include <libintl.h>
267
268 #include <nettle/sha.h>
269
270 static const struct option os[]= {
271   { "--state-dir", 1,0,'d' },
272   { "--command-id",1,0,'i' },
273   { 0 }
274 };
275
276 static const char *state_dir, *command_id, *command;
277 static const char *lock_path, *cohort_path;
278
279 static int cohort_fd, lock_fd;
280
281
282 #define _(x) gettext(x)
283
284 #define NOEINTR_TYPED(type,assign) do{                  \
285     while ((assign)==(type)-1 && errno==EINTR) {}       \
286   }while(0)
287
288 #define NOEINTR(assign) \
289     NOEINTR_TYPED(int,(assign))
290
291 #define CHECKED(value,what) do{                 \
292     NOEINTR(r= (value));                        \
293     if (r<0) diee((what));                      \
294   }while(0)
295
296
297 static void badusage(void) {
298   fputs(_("usage: watershed [<options>] <command>...\n"
299           "options: -d|--state-dir <directory>  -i|--command-id <id>\n"
300           "see /usr/share/doc/chiark-utils-bin/watershed.txt\n"),
301           stderr);
302   exit(127);
303 }
304 static void die(const char *m) {
305   fprintf(stderr,_("watershed: error: %s\n"), m);
306   exit(127);
307 }
308 static void diee(const char *m) {
309   fprintf(stderr,_("watershed: error: %s failed: %s\n"), m, strerror(errno));
310   exit(127);
311 }
312 static void dieep(const char *action, const char *path) {
313   fprintf(stderr,_("watershed: error: could not %s `%s': %s\n"),
314           action, path, strerror(errno));
315   exit(127);
316 }
317
318 static char *m_vasprintf(const char *fmt, va_list al) {
319   char *s;  int r;
320   r= vasprintf(&s,fmt,al);
321   if (r==-1) diee("vasprintf");
322   return s;
323 }
324 static char *m_asprintf(const char *fmt, ...) {
325   char *s;  va_list al;
326   va_start(al,fmt); s= m_vasprintf(fmt,al); va_end(al);
327   return s;
328 }
329
330 static void parse_args(int argc, char *const *argv) {
331   int o;
332   for (;;) {
333     o= getopt_long(argc, argv, "+d:i:", os,0);
334     if (o==-1) break;
335     switch (o) {
336     case 'd': state_dir= optarg; break;
337     case 'i': command_id= optarg; break;
338     default: badusage();
339     }
340   }
341   command= argv[optind];
342   if (!command) badusage();
343   if (!state_dir) state_dir= getenv("WATERSHED_STATEDIR");
344   if (!state_dir) {
345     uid_t u= geteuid();  if (u==(uid_t)-1) diee("getuid");
346     if (u) {
347       const char *home= getenv("HOME");
348       if (!home) die(_("HOME not set, no --state-dir option"
349                        " supplied, not root"));
350       state_dir= m_asprintf("%s/.watershed", home);
351     } else {
352       state_dir= "/var/run/watershed";
353     }
354   }
355   if (!command_id) {
356     char *const *ap;
357     struct sha256_ctx sc;
358     unsigned char dbuf[SHA256_DIGEST_SIZE], *p;
359     char *construct, *q;
360     int i, c;
361     
362     sha256_init(&sc);
363     for (ap= argv+optind; *ap; ap++) sha256_update(&sc,strlen(*ap)+1,*ap);
364     sha256_digest(&sc,sizeof(dbuf),dbuf);
365
366     construct= m_asprintf("%*s#%.32s", (int)sizeof(dbuf)*2,"", command);
367     for (i=sizeof(dbuf), p=dbuf, q=construct; i; i--,p++,q+=2)
368       sprintf(q,"%02x",*p);
369     *q++= '=';
370     while ((c=*q++)) {
371       if (!(c=='-' || c=='+' || c=='_' || isalnum((unsigned char)c)))
372         q[-1]= '?';
373     }
374     command_id= construct;
375   }
376
377   lock_path= m_asprintf("%s/%s.lock", state_dir, command_id);
378   cohort_path= m_asprintf("%s/%s.cohort", state_dir, command_id);
379 }
380
381 static void acquire_lock(void) {
382   struct stat current_stab, our_stab;
383   struct flock fl;
384   int r;
385
386   for (;;) {
387     NOEINTR( lock_fd= open(lock_path, O_CREAT|O_RDWR, 0600) );
388     if (lock_fd<0) diee("open lock");
389
390     memset(&fl,0,sizeof(fl));
391     fl.l_type= F_WRLCK;
392     fl.l_whence= SEEK_SET;
393     CHECKED( fcntl(lock_fd, F_SETLKW, &fl), "acquire lock" );
394     
395     CHECKED( fstat(lock_fd, &our_stab), "fstat our lock");
396
397     NOEINTR( r= stat(lock_path, &current_stab) );
398     if (!r &&
399         our_stab.st_ino == current_stab.st_ino &&
400         our_stab.st_dev == current_stab.st_dev) break;
401     if (r && errno!=ENOENT) diee("fstat current lock");
402     
403     close(lock_fd);
404   }
405 }
406 static void release_lock(void) {
407   int r;
408   CHECKED( unlink(lock_path), "unlink lock");
409 }
410
411 static void report(int status) {
412   int v;
413   if (WIFEXITED(status)) {
414     v= WEXITSTATUS(status);
415     if (v) fprintf(stderr,_("watershed: `%s' failed with error exit status %d"
416                             " (in another invocation)\n"), command, v);
417     exit(status);
418   }
419   if (WIFSIGNALED(status)) {
420     v= WTERMSIG(status); assert(v);
421     if (v == SIGPIPE) raise(v);
422     fprintf(stderr,
423             WCOREDUMP(status)
424             ? _("watershed: `%s' died due to fatal signal %s (core dumped)\n")
425             : _("watershed: `%s' died due to fatal signal %s\n"),
426             command, strsignal(v));
427   } else {
428     fprintf(stderr, _("watershed: `%s' failed with"
429                       " crazy wait status 0x%x\n"), command, status);
430   }
431   exit(127);
432 }
433
434 int main(int argc, char *const *argv) {
435   int status, r, dir_created=0, l;
436   unsigned char *p;
437   struct stat cohort_stab;
438   pid_t c, c2;
439   
440   setlocale(LC_MESSAGES,""); /* not LC_ALL, see use of isalnum below */
441   parse_args(argc,argv);
442
443   for (;;) {
444     NOEINTR( cohort_fd= open(cohort_path, O_CREAT|O_RDWR, 0644) );
445     if (cohort_fd>=0) break;
446     if (errno!=ENOENT) dieep(_("open/create cohort state file"), cohort_path);
447     if (dir_created++) die("open cohort state file still ENOENT after mkdir");
448     NOEINTR( r= mkdir(state_dir,0700) );
449     if (r && errno!=EEXIST) dieep(_("create state directory"), state_dir);
450   }
451
452   acquire_lock();
453
454   CHECKED( fstat(cohort_fd, &cohort_stab), "fstat our cohort");
455   if (cohort_stab.st_size) {
456     if (cohort_stab.st_size < sizeof(status))
457       die(_("cohort status file too short (disk full?)"));
458     else if (cohort_stab.st_size != sizeof(status))
459       die("cohort status file too long");
460     NOEINTR( r= read(cohort_fd,&status,sizeof(status)) );
461     if (r==-1) diee("read cohort");
462     if (r!=sizeof(status)) die("cohort file read wrong length");
463     release_lock(); report(status);
464   }
465
466   if (cohort_stab.st_nlink)
467     CHECKED( unlink(cohort_path), "unlink our cohort");
468
469   NOEINTR_TYPED(pid_t, c= fork() );  if (c==(pid_t)-1) diee("fork");
470   if (!c) {
471     close(cohort_fd); close(lock_fd);
472     execvp(command, argv+optind);
473     fprintf(stderr,_("watershed: failed to execute `%s': %s\n"),
474             command, strerror(errno));
475     exit(127);
476   }
477
478   NOEINTR( c2= waitpid(c, &status, 0) );
479   if (c2==(pid_t)-1) diee("waitpid");
480   if (c2!=c) die("waitpid gave wrong pid");
481
482   for (l=sizeof(status), p=(void*)&status; l>0; l-=r, p+=r)
483     CHECKED( write(cohort_fd,p,l), _("write result status"));
484
485   release_lock();
486   if (!WIFEXITED(status)) report(status);
487   exit(WEXITSTATUS(status));
488 }