2 * watershed - an auxiliary verb for optimising away
3 * unnecessary runs of idempotent commands
5 * watershed is Copyright 2007 Canonical Ltd
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
22 * See the file XXXXX for a full list of credits information (often
28 * usage: watershed [<options>] <command> [<arg>...]
31 * -d|--state-dir <state-dir>
32 * default is /var/run/watershed for uid 0
33 * $HOME/.watershed for others
34 * -i|--command-id <command-id>
37 * <state-dir>/<command-id>.lock lockfile
38 * <state-dir>/<command-id>.cohort cohort
40 * default <command-id> is
41 * hex(sha256(argv[0]+'\0' + argv[1]+'\0' ... argv[argc-1]+'\0')
43 * mangled argv[0] (all chars [^-+_0-9A-Za-z] replaced with ?
47 * 127 - something went wrong, or process died with some other signal
48 * SIGPIPE - process died with SIGPIPE
49 * x - process called _exit(x)
51 * stdin/stdout/stderr:
53 * If watershed exits 127 due to some unexpected problem, a message
54 * is printed to stderr explaining why (obviously).
56 * If a watershed invocation ends up running the process, the process
57 * simply inherits stdin/out/err. Otherwise stdin/stdout are not used.
59 * If the process run for us by another invocation of watershed exits
60 * zero, or watershed die with the same signal as the process
61 * (currently just SIGPIPE), nothing is printed to stderr. Otherwise
62 * (ie, failure of the actual process, in another invocation),
63 * watershed prints a description of the wait status to stderr, much
68 * gcc -Wall -Wwrite-strings -Wmissing-prototypes watershed.c -o watershed /usr/lib/libnettle.a
74 * We consider only invocations with a specific command id (and state
75 * directory), since other invocations are completely independent by
76 * virtue of having different state file pathnames and thus different
77 * state files. Normally, a command id corresponds to invocations
78 * with a particular set of command line arguments and a state
79 * directory corresponds to a particular euid; environment variable
80 * settings and other inherited process properties are disregarded.
82 * A `cohort' is a set of invocations which can be coalesced into one
83 * run of the command. For each cohort there is a file, the cohort
84 * file (which may not yet exist, may exist and have a name, or may
87 * An `invocation' is an invocation of the `watershed' program. A
88 * `process' is an invocation of the requested command.
90 * There is always one current cohort, in one of the following
94 * No invocations are in this cohort yet.
95 * The cohort filename is ENOENT.
96 * This is the initial state for a cohort, and the legal next
97 * state is Accumulating.
100 * The process for this run has not yet started, so that new
101 * invocations arriving would be satisfied if this cohort were to
103 * The cohort filename refers to this cohort's file.
104 * The legal next state for the cohort is Ready.
106 * Additionally, there may be older cohorts in the following states:
109 * The command for this cohort has not yet been run.
110 * The cohort file has no name and is empty.
111 * Only one cohort, the lockholder's, may be in this state.
112 * The next legal states are Running, or exceptionally Forgotten
113 * (if the lockholder crashes and is the only invocation in the
117 * The lockholder is running the command for this cohort.
118 * This state is identical to Ready from the point of view
119 * of all invocations except the lockholder.
120 * The legal next states are Done (the usual case), or (if the
121 * lockholder crashes) Ready or Forgotten.
124 * The main process for this run has finished.
125 * The cohort file has no name and contains sizeof(int)
126 * bytes, the `status' value from waitpid.
127 * The legal next state is Forgotten.
130 * All invocations have finished and the cohort file no longer
131 * exists. This is the final state.
133 * Only the lockholder may move a cohort between states, except that
134 * any invocation may make the current Empty cohort become
135 * Accumulating, and that the kernel will automatically move a cohort
136 * from Running to Ready or from Done to Forgotten, when appropriate.
141 * 1. Open the cohort file (O_CREAT|O_RDWR) so our cohort is
142 * Accumulating/Ready/
145 * 2. Acquire lock (see below) so lockholder's cohort is
146 * Accumulating/Ready/Done
147 * 3. fstat the open cohort file
148 * If it is nonempty: Done
149 * Read status from it and exit.
150 * Otherwise, if nonzero link count: Accumulating
151 * Unlink the cohort filename
154 * 4. Fork and run the command Running
157 * 5. Write the wait status to the cohort file Done
160 * 6. Release the lock so we are no longer lockholder
161 * but our cohort is still
164 * 8. Exit Done/Forgotten
166 * If an invocation crashes (ie, if watershed itself fails, rather
167 * than if the command does) then that invocation's caller will be
168 * informed of the error.
170 * If the lockholder crashes with the cohort in:
173 * The cohort remains in Accumulating and another invocation can
174 * become the lockholder. If there are never any other
175 * invocations then the lockfile and cohort file will not be
176 * cleaned up (see below).
179 * The cohort goes from Running back to Ready (see above) and
180 * another invocation in the same cohort will become the
181 * lockholder and run it. If there is no other invocation in
182 * the cohort the cohort goes to Forgotten although the lockfile
183 * will not be cleaned up - see below.
186 * If there are no more invocations, the cohort is Forgotten but
187 * the lockfile is not cleaned up.
191 * There is one lock for all cohorts. The lockholder is the
192 * invocation which holds the fcntl lock on the file whose name is
193 * the lockfile. The lockholder (and no-one else) may unlink the
196 * To acquire the lock:
198 * 1. Open the lockfile (O_CREAT|O_RDWR)
199 * 2. Acquire fcntl lock (F_SETLKW)
200 * 3. fstat the open lockfile and stat the lockfile filenmae
201 * If inode numbers disagree, close lockfile and start
202 * again from the beginning.
204 * To release the lock, unlink the lockfile and then either close it
205 * or exit. Crashing will also release the lock but leave the
206 * lockfile lying around (which is slightly untidy but not
207 * incorrect); if this is a problem a cleanup task could periodically
208 * acquire and release the lock for each lockfile found:
212 * As described above and below, stale cohort files and lockfiles can
213 * result from invocations which crashed if the same command is never
214 * run again. Such cohorts are always in Empty or Accumulating.
216 * If it became necessary to clean up stale cohort files and
217 * lockfiles resulting from crashes, the following algorithm should
218 * be executed for each lockfile found, as a cleanup task:
220 * 1. Acquire the lock.
221 * This makes us the lockholder. and the current cohort is in
224 * so now that cohort is
225 * 2. Unlink the cohort file, ignoring ENOENT. Ready/Forgotten
226 * 3. Release the lock. Ready/Forgotten
227 * 4. Exit. Ready/Forgotten
229 * This consists only of legal transitions, so if current cohort
230 * wasn't stale, it will have been moved to Ready and some other
231 * invocation in this cohort will become the lockholder and as normal
232 * from step 4 of the main algorithm. If the cohort was stale it
233 * will go to Forgotten straight away.
235 * A suitable cleanup script, on a system with with-lock-ex, is: */
238 // if [ $# != 1 ]; echo >&2 'usage: cleanup <statedir>'; exit 1; fi
240 // for f in ./*.lock; do
241 // with-lock-ex -w rm -f "${f%.lock}.cohort"
256 #include <sys/types.h>
257 #include <sys/stat.h>
258 #include <sys/wait.h>
265 #include <nettle/sha.h>
267 static const struct option os[]= {
268 { "--state-dir", 1,0,'d' },
269 { "--command-id",1,0,'i' },
273 static const char *state_dir, *command_id, *command;
274 static const char *lock_path, *cohort_path;
276 static int cohort_fd, lock_fd;
279 #define _(x) gettext(x)
281 #define NOEINTR_TYPED(type,assign) do{ \
282 while ((assign)==(type)-1 && errno==EINTR) {} \
285 #define NOEINTR(assign) \
286 NOEINTR_TYPED(int,(assign))
288 #define CHECKED(value,what) do{ \
289 NOEINTR(r= (value)); \
290 if (r<0) diee((what)); \
294 static void badusage(void) {
295 fputs(_("usage: watershed [<options>] <command>...\n"
296 "options: -d|--state-dir <directory> -i|--command-id <id>\n"),
300 static void die(const char *m) {
301 fprintf(stderr,_("watershed: error: %s\n"), m);
304 static void diee(const char *m) {
305 fprintf(stderr,_("watershed: error: %s failed: %s\n"), m, strerror(errno));
308 static void dieep(const char *action, const char *path) {
309 fprintf(stderr,_("watershed: error: could not %s `%s': %s\n"),
310 action, path, strerror(errno));
314 static char *m_vasprintf(const char *fmt, va_list al) {
316 r= vasprintf(&s,fmt,al);
317 if (r==-1) diee("vasprintf");
320 static char *m_asprintf(const char *fmt, ...) {
322 va_start(al,fmt); s= m_vasprintf(fmt,al); va_end(al);
326 static void parse_args(int argc, char *const *argv) {
329 o= getopt_long(argc, argv, "+d:i:", os,0);
332 case 'd': state_dir= optarg; break;
333 case 'i': command_id= optarg; break;
337 command= argv[optind];
338 if (!command) badusage();
339 if (!state_dir) state_dir= getenv("WATERSHED_STATEDIR");
341 uid_t u= geteuid(); if (u==(uid_t)-1) diee("getuid");
343 const char *home= getenv("HOME");
344 if (!home) die(_("HOME not set, no --state-dir option"
345 " supplied, not root"));
346 state_dir= m_asprintf("%s/.watershed", home);
348 state_dir= "/var/run/watershed";
353 struct sha256_ctx sc;
354 unsigned char dbuf[SHA256_DIGEST_SIZE], *p;
359 for (ap= argv+optind; *ap; ap++) sha256_update(&sc,strlen(*ap)+1,*ap);
360 sha256_digest(&sc,sizeof(dbuf),dbuf);
362 construct= m_asprintf("%*s#%.32s", (int)sizeof(dbuf)*2,"", command);
363 for (i=sizeof(dbuf), p=dbuf, q=construct; i; i--,p++,q+=2)
364 sprintf(q,"%02x",*p);
367 if (!(c=='-' || c=='+' || c=='_' || isalnum((unsigned char)c)))
370 command_id= construct;
373 lock_path= m_asprintf("%s/%s.lock", state_dir, command_id);
374 cohort_path= m_asprintf("%s/%s.cohort", state_dir, command_id);
377 static void acquire_lock(void) {
378 struct stat current_stab, our_stab;
383 NOEINTR( lock_fd= open(lock_path, O_CREAT|O_RDWR, 0600) );
384 if (lock_fd<0) diee("open lock");
386 memset(&fl,0,sizeof(fl));
388 fl.l_whence= SEEK_SET;
389 CHECKED( fcntl(lock_fd, F_SETLKW, &fl), "acquire lock" );
391 CHECKED( fstat(lock_fd, &our_stab), "fstat our lock");
393 NOEINTR( r= stat(lock_path, ¤t_stab) );
395 our_stab.st_ino == current_stab.st_ino &&
396 our_stab.st_dev == current_stab.st_dev) break;
397 if (r && errno!=ENOENT) diee("fstat current lock");
402 static void release_lock(void) {
404 CHECKED( unlink(lock_path), "unlink lock");
407 static void report(int status) {
409 if (WIFEXITED(status)) {
410 v= WEXITSTATUS(status);
411 if (v) fprintf(stderr,_("watershed: `%s' failed with error exit status %d"
412 " (in another invocation)\n"), command, v);
415 if (WIFSIGNALED(status)) {
416 v= WTERMSIG(status); assert(v);
417 if (v == SIGPIPE) raise(v);
420 ? _("watershed: `%s' died due to fatal signal %s (core dumped)\n")
421 : _("watershed: `%s' died due to fatal signal %s\n"),
422 command, strsignal(v));
424 fprintf(stderr, _("watershed: `%s' failed with"
425 " crazy wait status 0x%x\n"), command, status);
430 int main(int argc, char *const *argv) {
431 int status, r, dir_created=0, l;
433 struct stat cohort_stab;
436 setlocale(LC_MESSAGES,""); /* not LC_ALL, see use of isalnum below */
437 parse_args(argc,argv);
440 NOEINTR( cohort_fd= open(cohort_path, O_CREAT|O_RDWR, 0644) );
441 if (cohort_fd>=0) break;
442 if (errno!=ENOENT) dieep(_("open/create cohort state file"), cohort_path);
443 if (dir_created++) die("open cohort state file still ENOENT after mkdir");
444 NOEINTR( r= mkdir(state_dir,0700) );
445 if (r && errno!=EEXIST) dieep(_("create state directory"), state_dir);
450 CHECKED( fstat(cohort_fd, &cohort_stab), "fstat our cohort");
451 if (cohort_stab.st_size) {
452 if (cohort_stab.st_size < sizeof(status))
453 die(_("cohort status file too short (disk full?)"));
454 else if (cohort_stab.st_size != sizeof(status))
455 die("cohort status file too long");
456 NOEINTR( r= read(cohort_fd,&status,sizeof(status)) );
457 if (r==-1) diee("read cohort");
458 if (r!=sizeof(status)) die("cohort file read wrong length");
459 release_lock(); report(status);
462 if (cohort_stab.st_nlink)
463 CHECKED( unlink(cohort_path), "unlink our cohort");
465 NOEINTR_TYPED(pid_t, c= fork() ); if (c==(pid_t)-1) diee("fork");
467 close(cohort_fd); close(lock_fd);
468 execvp(command, argv+optind);
469 fprintf(stderr,_("watershed: failed to execute `%s': %s\n"),
470 command, strerror(errno));
474 NOEINTR( c2= waitpid(c, &status, 0) );
475 if (c2==(pid_t)-1) diee("waitpid");
476 if (c2!=c) die("waitpid gave wrong pid");
478 for (l=sizeof(status), p=(void*)&status; l>0; l-=r, p+=r)
479 CHECKED( write(cohort_fd,p,l), _("write result status"));
482 if (!WIFEXITED(status)) report(status);
483 exit(WEXITSTATUS(status));