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