chiark / gitweb /
Bugfixes:
[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   { "--help",      0,0,'h' },
274   { 0 }
275 };
276
277 static const char *state_dir, *command_id, *command;
278 static const char *lock_path, *cohort_path;
279
280 static int cohort_fd, lock_fd;
281
282
283 #define _(x) gettext(x)
284
285 #define NOEINTR_TYPED(type,assign) do{                  \
286     while ((assign)==(type)-1 && errno==EINTR) {}       \
287   }while(0)
288
289 #define NOEINTR(assign) \
290     NOEINTR_TYPED(int,(assign))
291
292 #define CHECKED(value,what) do{                 \
293     NOEINTR(r= (value));                        \
294     if (r<0) diee((what));                      \
295   }while(0)
296
297
298 static void printusage(FILE *f) {
299   fputs(_("usage: watershed [<options>] <command>...\n"
300           "options:\n"
301           "   -d|--state-dir <directory>\n"
302           "   -i|--command-id <id>\n"
303           "   -h|--help\n"
304           "see /usr/share/doc/chiark-utils-bin/watershed.txt\n"),
305           f);
306 }
307 static void badusage(void) {
308   printusage(stderr);
309   exit(127);
310 }
311 static void die(const char *m) {
312   fprintf(stderr,_("watershed: error: %s\n"), m);
313   exit(127);
314 }
315 static void diee(const char *m) {
316   fprintf(stderr,_("watershed: error: %s failed: %s\n"), m, strerror(errno));
317   exit(127);
318 }
319 static void dieep(const char *action, const char *path) {
320   fprintf(stderr,_("watershed: error: could not %s `%s': %s\n"),
321           action, path, strerror(errno));
322   exit(127);
323 }
324
325 static char *m_vasprintf(const char *fmt, va_list al) {
326   char *s;  int r;
327   r= vasprintf(&s,fmt,al);
328   if (r==-1) diee("vasprintf");
329   return s;
330 }
331 static char *m_asprintf(const char *fmt, ...) {
332   char *s;  va_list al;
333   va_start(al,fmt); s= m_vasprintf(fmt,al); va_end(al);
334   return s;
335 }
336
337 static void parse_args(int argc, char *const *argv) {
338   int o;
339   for (;;) {
340     o= getopt_long(argc, argv, "+d:i:h", os,0);
341     if (o==-1) break;
342     switch (o) {
343     case 'd': state_dir= optarg; break;
344     case 'i': command_id= optarg; break;
345     case 'h': printusage(stdout); exit(0); break;
346     default: badusage();
347     }
348   }
349   command= argv[optind];
350   if (!command) badusage();
351   if (!state_dir) state_dir= getenv("WATERSHED_STATEDIR");
352   if (!state_dir) {
353     uid_t u= geteuid();  if (u==(uid_t)-1) diee("getuid");
354     if (u) {
355       const char *home= getenv("HOME");
356       if (!home) die(_("HOME not set, no --state-dir option"
357                        " supplied, not root"));
358       state_dir= m_asprintf("%s/.watershed", home);
359     } else {
360       state_dir= "/var/run/watershed";
361     }
362   }
363   if (!command_id) {
364     char *const *ap;
365     struct sha256_ctx sc;
366     unsigned char dbuf[SHA256_DIGEST_SIZE], *p;
367     char *construct, *q;
368     int i, c;
369     
370     sha256_init(&sc);
371     for (ap= argv+optind; *ap; ap++) sha256_update(&sc,strlen(*ap)+1,*ap);
372     sha256_digest(&sc,sizeof(dbuf),dbuf);
373
374     construct= m_asprintf("%*s#%.32s", (int)sizeof(dbuf)*2,"", command);
375     for (i=sizeof(dbuf), p=dbuf, q=construct; i; i--,p++,q+=2)
376       sprintf(q,"%02x",*p);
377     *q++= '=';
378     while ((c=*q++)) {
379       if (!(c=='-' || c=='+' || c=='_' || isalnum((unsigned char)c)))
380         q[-1]= '?';
381     }
382     command_id= construct;
383   }
384
385   lock_path= m_asprintf("%s/%s.lock", state_dir, command_id);
386   cohort_path= m_asprintf("%s/%s.cohort", state_dir, command_id);
387 }
388
389 static void acquire_lock(void) {
390   struct stat current_stab, our_stab;
391   struct flock fl;
392   int r;
393
394   for (;;) {
395     NOEINTR( lock_fd= open(lock_path, O_CREAT|O_RDWR, 0600) );
396     if (lock_fd<0) diee("open lock");
397
398     memset(&fl,0,sizeof(fl));
399     fl.l_type= F_WRLCK;
400     fl.l_whence= SEEK_SET;
401     CHECKED( fcntl(lock_fd, F_SETLKW, &fl), "acquire lock" );
402     
403     CHECKED( fstat(lock_fd, &our_stab), "fstat our lock");
404
405     NOEINTR( r= stat(lock_path, &current_stab) );
406     if (!r &&
407         our_stab.st_ino == current_stab.st_ino &&
408         our_stab.st_dev == current_stab.st_dev) break;
409     if (r && errno!=ENOENT) diee("fstat current lock");
410     
411     close(lock_fd);
412   }
413 }
414 static void release_lock(void) {
415   int r;
416   CHECKED( unlink(lock_path), "unlink lock");
417 }
418
419 static void report(int status) {
420   int v;
421   if (WIFEXITED(status)) {
422     v= WEXITSTATUS(status);
423     if (v) fprintf(stderr,_("watershed: `%s' failed with error exit status %d"
424                             " (in another invocation)\n"), command, v);
425     exit(status);
426   }
427   if (WIFSIGNALED(status)) {
428     v= WTERMSIG(status); assert(v);
429     if (v == SIGPIPE) raise(v);
430     fprintf(stderr,
431             WCOREDUMP(status)
432             ? _("watershed: `%s' died due to fatal signal %s (core dumped)\n")
433             : _("watershed: `%s' died due to fatal signal %s\n"),
434             command, strsignal(v));
435   } else {
436     fprintf(stderr, _("watershed: `%s' failed with"
437                       " crazy wait status 0x%x\n"), command, status);
438   }
439   exit(127);
440 }
441
442 int main(int argc, char *const *argv) {
443   int status, r, dir_created=0, l;
444   unsigned char *p;
445   struct stat cohort_stab;
446   pid_t c, c2;
447   
448   setlocale(LC_MESSAGES,""); /* not LC_ALL, see use of isalnum below */
449   parse_args(argc,argv);
450
451   for (;;) {
452     NOEINTR( cohort_fd= open(cohort_path, O_CREAT|O_RDWR, 0644) );
453     if (cohort_fd>=0) break;
454     if (errno!=ENOENT) dieep(_("open/create cohort state file"), cohort_path);
455     if (dir_created++) die("open cohort state file still ENOENT after mkdir");
456     NOEINTR( r= mkdir(state_dir,0700) );
457     if (r && errno!=EEXIST) dieep(_("create state directory"), state_dir);
458   }
459
460   acquire_lock();
461
462   CHECKED( fstat(cohort_fd, &cohort_stab), "fstat our cohort");
463   if (cohort_stab.st_size) {
464     if (cohort_stab.st_size < sizeof(status))
465       die(_("cohort status file too short (disk full?)"));
466     else if (cohort_stab.st_size != sizeof(status))
467       die("cohort status file too long");
468     NOEINTR( r= read(cohort_fd,&status,sizeof(status)) );
469     if (r==-1) diee("read cohort");
470     if (r!=sizeof(status)) die("cohort file read wrong length");
471     release_lock(); report(status);
472   }
473
474   if (cohort_stab.st_nlink)
475     CHECKED( unlink(cohort_path), "unlink our cohort");
476
477   NOEINTR_TYPED(pid_t, c= fork() );  if (c==(pid_t)-1) diee("fork");
478   if (!c) {
479     close(cohort_fd); close(lock_fd);
480     execvp(command, argv+optind);
481     fprintf(stderr,_("watershed: failed to execute `%s': %s\n"),
482             command, strerror(errno));
483     exit(127);
484   }
485
486   NOEINTR( c2= waitpid(c, &status, 0) );
487   if (c2==(pid_t)-1) diee("waitpid");
488   if (c2!=c) die("waitpid gave wrong pid");
489
490   for (l=sizeof(status), p=(void*)&status; l>0; l-=r, p+=r)
491     CHECKED( write(cohort_fd,p,l), _("write result status"));
492
493   release_lock();
494   if (!WIFEXITED(status)) report(status);
495   exit(WEXITSTATUS(status));
496 }