New, rather hacky, Unix utility: `after', to wait for the
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Wed, 20 Feb 2008 19:46:02 +0000 (19:46 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Wed, 20 Feb 2008 19:46:02 +0000 (19:46 +0000)
termination of an arbitrary process specified by pid.

git-svn-id: svn://svn.tartarus.org/sgt/utils@7863 cda61777-01e9-0310-a592-d414129be87e

Makefile
after/Makefile [new file with mode: 0644]
after/after.but [new file with mode: 0644]
after/after.c [new file with mode: 0644]

index bae793b..fc9b314 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-SUBDIRS = base64 beep cvt-utf8 lns multi nntpid reservoir xcopy
+SUBDIRS = after base64 beep cvt-utf8 lns multi nntpid reservoir xcopy
 
 # for `make html' and `make release'; should be a relative path
 DESTDIR = .
diff --git a/after/Makefile b/after/Makefile
new file mode 100644 (file)
index 0000000..071e219
--- /dev/null
@@ -0,0 +1,46 @@
+# for `make release' and `make html'
+DESTDIR = .
+
+# for `make install'
+PREFIX = /usr/local
+BINDIR = $(PREFIX)/bin
+SCRIPTDIR = $(PREFIX)/bin
+MANDIR = $(PREFIX)/man/man1
+INSTALL = install
+IPROG =#   flags for installing programs (default none)
+IDATA = -m 0644  # flags for installing data
+
+all: after.1 after
+man: after.1
+progs: after
+
+after: after.c
+       $(CC) $(CFLAGS) -o $@ $<
+
+%.1: %.but
+       halibut --man=$@ $<
+
+clean:
+       rm -f *.1 after *.html *.tar.gz
+
+html:
+       halibut --html=after.html after.but
+       mv after.html $(DESTDIR)
+
+release: after.1
+       mkdir -p reltmp/after
+       ln -s ../../after.c reltmp/after
+       ln -s ../../after.1 reltmp/after
+       ln -s ../../after.but reltmp/after
+       ln -s ../../Makefile reltmp/after
+       tar -C reltmp -chzf $(DESTDIR)/after.tar.gz after
+       rm -rf reltmp
+
+install: install-progs install-man
+
+install-progs: after
+       mkdir -p $(BINDIR)
+       $(INSTALL) $(IPROG) after $(BINDIR)/after
+install-man: after.1
+       mkdir -p $(MANDIR)
+       $(INSTALL) $(IDATA) after.1 $(MANDIR)/after.1
diff --git a/after/after.but b/after/after.but
new file mode 100644 (file)
index 0000000..2c8dfac
--- /dev/null
@@ -0,0 +1,83 @@
+\cfg{man-identity}{after}{1}{2008-02-20}{Simon Tatham}{Simon Tatham}
+
+\title Man page for \c{after}
+
+\U NAME
+
+\c{after} - wait until an unrelated process has terminated
+
+\U SYNOPSIS
+
+\c after [ -x | -z ] pid
+\e bbbbb   bb   bb   iii
+
+\U DESCRIPTION
+
+\c{after} lets you specify a process by its numeric PID, and then
+waits until that process has terminated. If possible, it returns the
+process's exit code as well.
+
+You might use \c{after} if you had started a long-running process in
+one window and then realised that you should have told the shell to
+do something else after it finished. For example, after typing
+\cq{make} to begin a long build run, you suddenly realise you should
+have typed \cq{make && ./runtests.sh}, so that you wouldn't have to
+be physically present to kick off the test run after the build
+completed, and so that you could take one long coffee break instead
+of two short ones. In this situation you could use \c{after} to
+solve your problem: use \cw{ps}(\e{1}) to look up the process ID of
+the \cw{make} process, and then type a command such as \cq{after
+34530 && ./runtests.sh} in a second terminal window.
+
+The operation of waiting for an arbitrary process is not an easy one
+on Unix, so the \c{after} command supports multiple methods of
+achieving it and will try them in order until one works. Some
+methods allow the exit code of the process to be retrieved, in which
+case \c{after} will return the same value; others do not.
+
+\U OPTIONS
+
+\dt \cw{-x}
+
+\dd Restricts \c{after} to only attempting methods which retrieve
+the process's exit code. These methods typically require \c{after}
+and the target process to be running under the same user ID. If no
+available method works, \c{after} will fail.
+
+\dt \cw{-z}
+
+\dd Causes \c{after} to always return 0 (success) after detecting
+that the process has terminated, whether or not its exit code was
+available.
+
+\U EXIT STATUS
+
+If it settles on a method which provides the target process's exit
+code, \c{after} will return the same code itself (unless you disable
+this with the \cw{-z} option). Hence, you can use the shell \cw{&&}
+operator to run other commands which are conditional on the target
+process succeeding (as in the example above).
+
+If it has to fall back to a method which does not provide the exit
+code, \c{after} will return 0 (success) when the process terminates.
+
+If something goes so badly wrong that \c{after} was unable to
+reliably wait for the process at all, it will return 127.
+
+\U BUGS
+
+There are not enough methods supported.
+
+The whole concept of \c{after} has an inherent bug in the form of a
+race condition: if you're not careful, the target process could
+terminate in between you looking up its PID and typing the \c{after}
+command. If your process looks as if it might terminate within the
+time it takes to type the command, it's probably better not to try
+using \c{after} at all.
+
+\U LICENCE
+
+\c{after} is free software, distributed under the MIT licence. Type
+\cq{after --licence} to see the full licence text.
+
+\versionid $Id$
diff --git a/after/after.c b/after/after.c
new file mode 100644 (file)
index 0000000..1e1c245
--- /dev/null
@@ -0,0 +1,266 @@
+/*
+ * after: wait for an unrelated process to terminate, returning
+ * its exit code if possible.
+ */
+
+/*
+ * Possible future extensions:
+ * 
+ *  - alternatives to ptrace on systems supporting different
+ *    process-debugging mechanisms
+ * 
+ *  - possibly an approach based on invoking ps?
+ *     * with a longish interval, to prevent system overload
+ *     * and we have to deal with the profusion of ps options. I
+ *      suspect going by SUS/POSIX is the best thing here: set
+ *      POSIXLY_CORRECT and expect ps to support the options
+ *      given in SUS.
+ *     * need to try to avoid race condition if the process ends
+ *      and another one of the same name starts up. Printing the
+ *      start time seems like the sensible thing, except that SUS
+ *      doesn't give a standard option to do that; we can only
+ *      print elapsed time since the process began, which means
+ *      we have to be alert for off-by-1s errors if we wait 30s
+ *      and the time only changes by 29 for some irritating
+ *      reason.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+char errbuf[4096];
+int errlen = 0;
+char *pname;
+
+#ifndef NO_PTRACE
+
+/*
+ * ptrace-based waiting method, which gets the process's exit
+ * code.
+ */
+
+#include <sys/ptrace.h>
+
+int after_ptrace(int pid)
+{
+    if (ptrace(PTRACE_ATTACH, pid, NULL, 0) < 0) {
+       /*
+        * If we can't attach to the process, that's not a fatal
+        * error; it just means we fall back to the next available
+        * method.
+        */
+       errlen += sprintf(errbuf+errlen,
+                         "%s: ptrace(PTRACE_ATTACH, %d): %.256s\n",
+                         pname, pid, strerror(errno));
+       return -1;
+    }
+
+    /*
+     * Having successfully attached to the process, however, any
+     * subsequent error is fatal.
+     */
+
+    while (1) {
+       int wpid, wstatus;
+       wpid = waitpid(pid, &wstatus, 0);
+       if (wpid < 0) {
+           perror("wait");
+           return 127;
+       } else if (WIFEXITED(wstatus)) {
+           return WEXITSTATUS(wstatus);
+       } else {
+           if (ptrace(PTRACE_CONT, pid, NULL, 0) < 0) {
+               perror("ptrace(PTRACE_CONT)");
+               return 127;
+           }
+       }
+    }
+}
+
+#endif
+
+#ifndef NO_PROC
+
+/*
+ * /proc-based waiting method.
+ */
+
+int after_proc(int pid)
+{
+    char buf[128];
+
+    sprintf(buf, "/proc/%d", pid);
+
+    if (chdir(buf) < 0) {
+       /*
+        * Fallback error.
+        */
+       errlen += sprintf(errbuf+errlen,
+                         "%s: chdir(\"%.32s\"): %.256s\n",
+                         pname, buf, strerror(errno));
+       return -1;
+    }
+
+    do {
+       sleep(1);
+    } while (getcwd(buf, sizeof(buf)));
+
+    return 0;
+}
+
+#endif
+
+#ifdef NO_PTRACE
+#define NO_EXITCODE
+#endif
+
+const char usagemsg[] =
+    "usage: after"
+#ifndef NO_EXITCODE
+    " [ -x | -z ]"
+#endif
+    " <pid>\n"
+#ifndef NO_EXITCODE
+    "where: -x     return an error if the process's exit code is unavailable\n"
+    "       -z     return 0 instead of the process's exit code\n"
+#endif
+    " also: after --version             report version number\n"
+    "       after --help                display this help text\n"
+    "       after --licence             display the (MIT) licence text\n"
+    ;
+
+void usage(void) {
+    fputs(usagemsg, stdout);
+}
+
+const char licencemsg[] =
+    "after is copyright 2008 Simon Tatham.\n"
+    "\n"
+    "Permission is hereby granted, free of charge, to any person\n"
+    "obtaining a copy of this software and associated documentation files\n"
+    "(the \"Software\"), to deal in the Software without restriction,\n"
+    "including without limitation the rights to use, copy, modify, merge,\n"
+    "publish, distribute, sublicense, and/or sell copies of the Software,\n"
+    "and to permit persons to whom the Software is furnished to do so,\n"
+    "subject to the following conditions:\n"
+    "\n"
+    "The above copyright notice and this permission notice shall be\n"
+    "included in all copies or substantial portions of the Software.\n"
+    "\n"
+    "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n"
+    "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n"
+    "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n"
+    "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\n"
+    "BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\n"
+    "ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\n"
+    "CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n"
+    "SOFTWARE.\n"
+    ;
+
+void licence(void) {
+    fputs(licencemsg, stdout);
+}
+
+void version(void) {
+#define SVN_REV "$Revision: 6566 $"
+    char rev[sizeof(SVN_REV)];
+    char *p, *q;
+
+    strcpy(rev, SVN_REV);
+
+    for (p = rev; *p && *p != ':'; p++);
+    if (*p) {
+        p++;
+        while (*p && isspace((unsigned char)*p)) p++;
+        for (q = p; *q && !isspace((unsigned char)*q) && *q != '$'; q++);
+        if (*q) *q = '\0';
+        printf("after revision %s", p);
+    } else {
+        printf("after: unknown version");
+    }
+    putchar('\n');
+}
+
+int main(int argc, char **argv)
+{
+    int pid = -1;
+    enum { UNWANTED, OPTIONAL, MANDATORY } exitcode = OPTIONAL;
+    int ret;
+
+    pname = argv[0];
+
+    /* parse the command line arguments */
+    while (--argc) {
+       char *p = *++argv;
+
+#ifndef NO_EXITCODE
+       if (!strcmp(p, "-x")) {
+            exitcode = MANDATORY;
+        } else if (!strcmp(p, "-z")) {
+            exitcode = UNWANTED;
+        } else
+#endif
+       if (!strcmp(p, "--help")) {
+           usage();
+           return 0;
+        } else if (!strcmp(p, "--version")) {
+            version();
+           return 0;
+        } else if (!strcmp(p, "--licence") || !strcmp(p, "--license")) {
+            licence();
+           return 0;
+       } else if (*p=='-') {
+           fprintf(stderr, "%s: unrecognised option `%s'\n", pname, p);
+           return 1;
+       } else {
+           if (pid >= 0) {
+               fprintf(stderr, "%s: parameter `%s' unexpected\n", pname, p);
+               return 1;
+           } else {
+               pid = atoi(p);
+           }
+       }
+    }
+
+    if (pid < 0) {
+       usage();
+       return 0;
+    }
+
+#ifndef NO_PTRACE
+    ret = after_ptrace(pid);
+    if (ret >= 0)
+       return (exitcode == UNWANTED ? 0 : ret);
+#endif
+
+    if (exitcode == MANDATORY) {
+       /*
+        * If we reach here, the user has demanded the process's
+        * return code, and we haven't been able to get it. Print
+        * the error messages we accrued while trying, and abandon
+        * ship.
+        */
+       fputs(errbuf, stderr);
+       return 127;
+    }
+
+#ifndef NO_PROC
+    ret = after_proc(pid);
+    if (ret >= 0)
+       return ret;
+#endif
+
+    /*
+     * If we reach here, we have run out of all our options. Print
+     * all our error messages, and return total failure.
+     */
+    fputs(errbuf, stderr);
+    return 127;
+}