2 * watershed - an auxiliary verb for optimising away
3 * unnecessary runs of idempotent commands
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
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.
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.
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
27 * NB a different fork of this code exists in Ubuntu's udev.
31 * usage: watershed [<options>] <command> [<arg>...]
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>
40 * <state-dir>/<command-id>.lock lockfile
41 * <state-dir>/<command-id>.cohort cohort
43 * default <command-id> is
44 * hex(sha256(argv[0]+'\0' + argv[1]+'\0' ... argv[argc-1]+'\0')
46 * mangled argv[0] (all chars [^-+_0-9A-Za-z] replaced with ?
50 * 127 - something went wrong, or process died with some other signal
51 * SIGPIPE - process died with SIGPIPE
52 * x - process called _exit(x)
54 * stdin/stdout/stderr:
56 * If watershed exits 127 due to some unexpected problem, a message
57 * is printed to stderr explaining why (obviously).
59 * If a watershed invocation ends up running the process, the process
60 * simply inherits stdin/out/err. Otherwise stdin/stdout are not used.
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
71 * gcc -Wall -Wwrite-strings -Wmissing-prototypes watershed.c -o watershed /usr/lib/libnettle.a
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.
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
90 * An `invocation' is an invocation of the `watershed' program. A
91 * `process' is an invocation of the requested command.
93 * There is always one current cohort, in one of the following
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.
103 * The process for this run has not yet started, so that new
104 * invocations arriving would be satisfied if this cohort were to
106 * The cohort filename refers to this cohort's file.
107 * The legal next state for the cohort is Ready.
109 * Additionally, there may be older cohorts in the following states:
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
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.
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.
133 * All invocations have finished and the cohort file no longer
134 * exists. This is the final state.
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.
144 * 1. Open the cohort file (O_CREAT|O_RDWR) so our cohort is
145 * Accumulating/Ready/
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
157 * 4. Fork and run the command Running
160 * 5. Write the wait status to the cohort file Done
163 * 6. Release the lock so we are no longer lockholder
164 * but our cohort is still
167 * 8. Exit Done/Forgotten
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.
173 * If the lockholder crashes with the cohort in:
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).
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.
189 * If there are no more invocations, the cohort is Forgotten but
190 * the lockfile is not cleaned up.
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
199 * To acquire the lock:
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.
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:
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.
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:
223 * 1. Acquire the lock.
224 * This makes us the lockholder. and the current cohort is in
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
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.
238 * A suitable cleanup script, on a system with with-lock-ex, is: */
241 // if [ $# != 1 ]; echo >&2 'usage: cleanup <statedir>'; exit 1; fi
243 // for f in ./*.lock; do
244 // with-lock-ex -w rm -f "${f%.lock}.cohort"
259 #include <sys/types.h>
260 #include <sys/stat.h>
261 #include <sys/wait.h>
268 #include <nettle/sha.h>
270 static const struct option os[]= {
271 { "--state-dir", 1,0,'d' },
272 { "--command-id",1,0,'i' },
276 static const char *state_dir, *command_id, *command;
277 static const char *lock_path, *cohort_path;
279 static int cohort_fd, lock_fd;
282 #define _(x) gettext(x)
284 #define NOEINTR_TYPED(type,assign) do{ \
285 while ((assign)==(type)-1 && errno==EINTR) {} \
288 #define NOEINTR(assign) \
289 NOEINTR_TYPED(int,(assign))
291 #define CHECKED(value,what) do{ \
292 NOEINTR(r= (value)); \
293 if (r<0) diee((what)); \
297 static void badusage(void) {
298 fputs(_("usage: watershed [<options>] <command>...\n"
299 "options: -d|--state-dir <directory> -i|--command-id <id>\n"),
303 static void die(const char *m) {
304 fprintf(stderr,_("watershed: error: %s\n"), m);
307 static void diee(const char *m) {
308 fprintf(stderr,_("watershed: error: %s failed: %s\n"), m, strerror(errno));
311 static void dieep(const char *action, const char *path) {
312 fprintf(stderr,_("watershed: error: could not %s `%s': %s\n"),
313 action, path, strerror(errno));
317 static char *m_vasprintf(const char *fmt, va_list al) {
319 r= vasprintf(&s,fmt,al);
320 if (r==-1) diee("vasprintf");
323 static char *m_asprintf(const char *fmt, ...) {
325 va_start(al,fmt); s= m_vasprintf(fmt,al); va_end(al);
329 static void parse_args(int argc, char *const *argv) {
332 o= getopt_long(argc, argv, "+d:i:", os,0);
335 case 'd': state_dir= optarg; break;
336 case 'i': command_id= optarg; break;
340 command= argv[optind];
341 if (!command) badusage();
342 if (!state_dir) state_dir= getenv("WATERSHED_STATEDIR");
344 uid_t u= geteuid(); if (u==(uid_t)-1) diee("getuid");
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);
351 state_dir= "/var/run/watershed";
356 struct sha256_ctx sc;
357 unsigned char dbuf[SHA256_DIGEST_SIZE], *p;
362 for (ap= argv+optind; *ap; ap++) sha256_update(&sc,strlen(*ap)+1,*ap);
363 sha256_digest(&sc,sizeof(dbuf),dbuf);
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);
370 if (!(c=='-' || c=='+' || c=='_' || isalnum((unsigned char)c)))
373 command_id= construct;
376 lock_path= m_asprintf("%s/%s.lock", state_dir, command_id);
377 cohort_path= m_asprintf("%s/%s.cohort", state_dir, command_id);
380 static void acquire_lock(void) {
381 struct stat current_stab, our_stab;
386 NOEINTR( lock_fd= open(lock_path, O_CREAT|O_RDWR, 0600) );
387 if (lock_fd<0) diee("open lock");
389 memset(&fl,0,sizeof(fl));
391 fl.l_whence= SEEK_SET;
392 CHECKED( fcntl(lock_fd, F_SETLKW, &fl), "acquire lock" );
394 CHECKED( fstat(lock_fd, &our_stab), "fstat our lock");
396 NOEINTR( r= stat(lock_path, ¤t_stab) );
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");
405 static void release_lock(void) {
407 CHECKED( unlink(lock_path), "unlink lock");
410 static void report(int status) {
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);
418 if (WIFSIGNALED(status)) {
419 v= WTERMSIG(status); assert(v);
420 if (v == SIGPIPE) raise(v);
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));
427 fprintf(stderr, _("watershed: `%s' failed with"
428 " crazy wait status 0x%x\n"), command, status);
433 int main(int argc, char *const *argv) {
434 int status, r, dir_created=0, l;
436 struct stat cohort_stab;
439 setlocale(LC_MESSAGES,""); /* not LC_ALL, see use of isalnum below */
440 parse_args(argc,argv);
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);
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);
465 if (cohort_stab.st_nlink)
466 CHECKED( unlink(cohort_path), "unlink our cohort");
468 NOEINTR_TYPED(pid_t, c= fork() ); if (c==(pid_t)-1) diee("fork");
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));
477 NOEINTR( c2= waitpid(c, &status, 0) );
478 if (c2==(pid_t)-1) diee("waitpid");
479 if (c2!=c) die("waitpid gave wrong pid");
481 for (l=sizeof(status), p=(void*)&status; l>0; l-=r, p+=r)
482 CHECKED( write(cohort_fd,p,l), _("write result status"));
485 if (!WIFEXITED(status)) report(status);
486 exit(WEXITSTATUS(status));