New utility for the collection: 'buildrun', a rewrite of my previous
[sgt/utils] / buildrun / buildrun.c
diff --git a/buildrun/buildrun.c b/buildrun/buildrun.c
new file mode 100644 (file)
index 0000000..6b89e52
--- /dev/null
@@ -0,0 +1,361 @@
+/*
+ * buildrun.c: wait to run one command until an instance of another
+ * completes successfully
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+/*
+ * Implementation strategy
+ * -----------------------
+ *
+ * The basic plan for making buildrun -r wait for buildrun -w is that
+ * buildrun -w creates a named pipe, opens it for writing, and does
+ * not actually write to it; then buildrun -r opens the same named
+ * pipe for reading. When buildrun -w terminates, anything which was
+ * trying to read from the pipe receives EOF, so buildrun -r can wake
+ * up and look to see if the run was successful.
+ *
+ * buildrun -w's report of success or failure is done by the very
+ * simple approach of deleting the pipe file before terminating, if
+ * the run was successful. So after buildrun -r receives EOF on the
+ * pipe, it immediately tries to open it again - and if it gets
+ * ENOENT, that's the success condition. Otherwise it reopens the pipe
+ * and blocks again until another buildrun -w succeeds.
+ *
+ * If the run was unsuccessful, buildrun -r must block immediately and
+ * stay blocked until buildrun -w next runs. This is achieved
+ * automatically by the above strategy, since attempting to open a
+ * pipe for reading when nothing is trying to write it will cause the
+ * open(2) call to block until something opens the pipe for writing.
+ *
+ * Likewise, if the last buildrun -w was unsuccessful and no -w run is
+ * active at the moment buildrun -r starts in the first place,
+ * buildrun -r must block until one starts up - and again, this is
+ * done automatically by the attempt to open the pipe for reading.
+ *
+ * Of course, the blocking open behaviour of named pipes works both
+ * ways: if buildrun -w tried to open the pipe for writing when no
+ * buildrun -r is trying to read it, _it_ would block. For that
+ * reason, buildrun -w actually opens with O_RDWR, so it can't block.
+ *
+ * Just in case the user tries to run two buildrun -w instances on the
+ * same control file, we also flock(LOCK_EX) the named pipe once we've
+ * opened it, so that the second -w instance can cleanly report the
+ * error condition.
+ *
+ * Finally, there is a small race condition in the mechanism I've just
+ * described. Suppose one buildrun -w instance finishes and deletes
+ * its pipe file - but before buildrun -r manages to unblock and
+ * notice the file's absence, another buildrun -w starts up and
+ * recreates it. This is a more or less theoretical risk in the
+ * interactive environments where I expect this tool to be used, but
+ * even so, I like to solve these problems where I can; what I do is
+ * to create not just a named pipe, but a _subdirectory_ containing a
+ * named pipe. So a successful buildrun -w deletes the pipe and
+ * removes the containing directory; a subsequent buildrun -w will
+ * create a fresh directory and pipe. Then buildrun -r starts by
+ * setting its _cwd_ to the directory containing the pipe - which
+ * means that if one buildrun -w finishes and another one starts,
+ * buildrun -r will still have its cwd pointing at the orphaned
+ * directory from the first, and won't accidentally find the pipe file
+ * created by the second in its own directory.
+ */
+
+#define PIPEFILE "pipe"
+
+char *ctldir, *pipepath, **command;
+
+int writemode(void)
+{
+    pid_t pid;
+    int fd, status;
+
+    if (mkdir(ctldir, 0700) < 0 && errno != EEXIST) {
+        fprintf(stderr, "buildrun: %s: mkdir: %s\n", ctldir, strerror(errno));
+        return 1;
+    }
+    if (mknod(pipepath, S_IFIFO | 0600, 0) < 0 && errno != EEXIST) {
+        fprintf(stderr, "buildrun: %s: mknod: %s\n", pipepath,strerror(errno));
+        return 1;
+    }
+    fd = open(pipepath, O_RDWR);
+    if (fd < 0) {
+        fprintf(stderr, "buildrun: %s: open: %s\n", pipepath, strerror(errno));
+        return 1;
+    }
+    if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
+        if (errno == EAGAIN) {
+            fprintf(stderr, "buildrun: another write process "
+                    "is already active\n");
+        } else {
+            fprintf(stderr, "buildrun: %s: flock: %s\n",
+                    pipepath, strerror(errno));
+        }
+        return 1;
+    }
+
+    pid = fork();
+    if (pid < 0) {
+        fprintf(stderr, "buildrun: fork: %s\n", strerror(errno));
+        return 1;
+    } else if (pid == 0) {
+        close(fd);
+        execvp(command[0], command);
+        fprintf(stderr, "buildrun: %s: exec: %s\n",
+                command[0], strerror(errno));
+        return 127;
+    }
+
+    while (1) {
+        if (waitpid(pid, &status, 0) < 0) {
+            fprintf(stderr, "buildrun: wait: %s\n", strerror(errno));
+            return 1;
+        }
+        if (WIFEXITED(status)) {
+            status = WEXITSTATUS(status);
+            break;
+        } else if (WIFSIGNALED(status)) {
+            status = 128 | WTERMSIG(status);
+            break;
+        }
+    }
+
+    if (status == 0) {
+        if (remove(pipepath) < 0) {
+            fprintf(stderr, "buildrun: %s: remove: %s\n", pipepath,
+                    strerror(errno));
+            return 1;
+        }
+        if (rmdir(ctldir) < 0) {
+            fprintf(stderr, "buildrun: %s: rmdir: %s\n", ctldir,
+                    strerror(errno));
+            return 1;
+        }
+    }
+
+    return status;
+}
+
+int readmode()
+{
+    pid_t pid;
+    int fd, ret, status;
+    char buf[1];
+
+    /*
+     * Our strategy against race conditions relies on changing our cwd
+     * to the control dir, but that will confuse the user command if
+     * we exec one afterwards. So we do most of our work in a
+     * subprocess, and don't pollute our main process context.
+     */
+    pid = fork();
+    if (pid < 0) {
+        fprintf(stderr, "buildrun: fork: %s\n", strerror(errno));
+        return 1;
+    } else if (pid == 0) {
+        /*
+         * The main show.
+         */
+        if (chdir(ctldir) < 0) {
+            if (errno == ENOENT) {
+                /*
+                 * If the control directory doesn't exist at all, it
+                 * must be because the last -w run completed
+                 * successfully, so we immediately return success.
+                 */
+                return 0;
+            }
+            fprintf(stderr, "buildrun: %s: chdir: %s\n",
+                    ctldir, strerror(errno));
+            return 1;
+        }
+        while (1) {
+            fd = open(PIPEFILE, O_RDONLY);
+            if (fd < 0) {
+                if (errno == ENOENT)
+                    return 0;          /* success! */
+
+                fprintf(stderr, "buildrun: %s: open: %s\n",
+                        pipepath, strerror(errno));
+                return 1;
+            }
+
+            ret = read(fd, buf, 1);
+            if (ret < 0) {
+                fprintf(stderr, "buildrun: %s: read: %s\n",
+                        pipepath, strerror(errno));
+                return 1;
+            } else if (ret > 0) {
+                fprintf(stderr, "buildrun: %s: read: unexpectedly received "
+                        "some data!\n", pipepath, strerror(errno));
+                return 1;
+            }
+            close(fd);
+        }
+    }
+
+    while (1) {
+        if (waitpid(pid, &status, 0) < 0) {
+            fprintf(stderr, "buildrun: wait: %s\n", strerror(errno));
+            return 1;
+        }
+        if (WIFEXITED(status)) {
+            status = WEXITSTATUS(status);
+            break;
+        } else if (WIFSIGNALED(status)) {
+            status = 128 | WTERMSIG(status);
+            break;
+        }
+    }
+
+    if (status != 0)                   /* something went wrong in child */
+        return status;
+
+    /*
+     * In -r mode, running a command is optional; in the absence of
+     * one, we simply return true, so the user can invoke as 'buildrun
+     * -r file && complex shell command'.
+     */
+    if (!command)
+        return 0;
+
+    execvp(command[0], command);
+    fprintf(stderr, "buildrun: %s: exec: %s\n", command[0], strerror(errno));
+    return 127;
+}
+
+const char usagemsg[] =
+    "usage: buildrun -w <control_dir> <command> [<args>...]\n"
+    "   or: buildrun -r <control_dir> [<command> [<args>...]]\n"
+    "where: -w               write mode: run a build command\n"
+    "       -r               read mode: wait for a build command to succeed\n"
+    "       <control_dir>    directory to use for communication; suggest in /tmp\n"
+    " also: buildrun --version              report version number\n"
+    "       buildrun --help                 display this help text\n"
+    "       buildrun --licence              display the (MIT) licence text\n"
+    ;
+
+void usage(void) {
+    fputs(usagemsg, stdout);
+}
+
+const char licencemsg[] =
+    "buildrun is copyright 2012 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: 8227 $"
+    char rev[sizeof(SVN_REV)];
+    char *p, *q;
+
+    strcpy(rev, SVN_REV);
+
+    for (p = rev; *p && *p != ':'; p++);
+    if (*p) {
+        p++;
+        while (*p && isspace(*p)) p++;
+        for (q = p; *q && *q != '$'; q++);
+        if (*q) *q = '\0';
+        printf("buildrun revision %s\n", p);
+    } else {
+        printf("buildrun: unknown version\n");
+    }
+}
+
+int main(int argc, char **argv)
+{
+    enum { UNSPEC, READ, WRITE } mode = UNSPEC;
+    int doing_opts = 1;
+
+    ctldir = NULL;
+    command = NULL;
+
+    while (--argc > 0) {
+        char *p = *++argv;
+        if (doing_opts && *p == '-') {
+            if (!strcmp(p, "-w")) {
+                mode = WRITE;
+            } else if (!strcmp(p, "-r")) {
+                mode = READ;
+            } else if (!strcmp(p, "--")) {
+                doing_opts = 0;
+            } else if (!strcmp(p, "--version")) {
+                version();
+                return 0;
+            } else if (!strcmp(p, "--help")) {
+                usage();
+                return 0;
+            } else if (!strcmp(p, "--licence") || !strcmp(p, "--license")) {
+                licence();
+                return 0;
+            } else {
+                fprintf(stderr, "buildrun: unrecognised option '%s'\n", p);
+                return 1;
+            }
+        } else {
+            if (!ctldir) {
+                ctldir = p;
+            } else {
+                command = argv;
+                break;
+            }
+        }
+    }
+
+    if (mode == UNSPEC) {
+        fprintf(stderr, "buildrun: expected -w or -r\n");
+        return 1;
+    }
+
+    if (!ctldir) {
+        fprintf(stderr, "buildrun: expected a control directory name\n");
+        return 1;
+    }
+
+    pipepath = malloc(strlen(ctldir) + strlen(PIPEFILE) + 2);
+    if (!pipepath) {
+        fprintf(stderr, "buildrun: out of memory\n");
+        return 1;
+    }
+    sprintf(pipepath, "%s/%s", ctldir, PIPEFILE);
+
+    if (mode == WRITE) {
+        return writemode();
+    } else if (mode == READ) {
+        return readmode();
+    }
+}