]> eyrie.org Git - kerberos/krb5-strength.git/blobdiff - tests/tap/process.c
Merge tag 'upstream/3.0' into debian
[kerberos/krb5-strength.git] / tests / tap / process.c
index b8d6ff96dce2c5d9dbdf19be3a389c5b3a7ccdfb..54e714aa999a8c5f03894e9853a3e38f7bc7dbdc 100644 (file)
@@ -7,12 +7,15 @@
  * runs a function in a subprocess and checks its output and exit status
  * against expected values.
  *
+ * Requires an Autoconf probe for sys/select.h and a replacement for a missing
+ * mkstemp.
+ *
  * The canonical version of this file is maintained in the rra-c-util package,
  * which can be found at <http://www.eyrie.org/~eagle/software/rra-c-util/>.
  *
  * Written by Russ Allbery <eagle@eyrie.org>
- * Copyright 2002, 2004, 2005 Russ Allbery <eagle@eyrie.org>
- * Copyright 2009, 2010, 2011, 2013
+ * Copyright 2002, 2004, 2005, 2013 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009, 2010, 2011, 2013, 2014
  *     The Board of Trustees of the Leland Stanford Junior University
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
 #include <config.h>
 #include <portable/system.h>
 
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#ifdef HAVE_SYS_SELECT_H
+# include <sys/select.h>
+#endif
+#include <sys/stat.h>
+#include <sys/time.h>
 #include <sys/wait.h>
 
 #include <tests/tap/basic.h>
 #include <tests/tap/process.h>
 #include <tests/tap/string.h>
 
+/* May be defined by the build system. */
+#ifndef PATH_FAKEROOT
+# define PATH_FAKEROOT ""
+#endif
+
+/* How long to wait for the process to start in seconds. */
+#define PROCESS_WAIT 10
+
+/*
+ * Used to store information about a background process.  This contains
+ * everything required to stop the process and clean up after it.
+ */
+struct process {
+    pid_t pid;                  /* PID of child process */
+    char *pidfile;              /* PID file to delete on process stop */
+    char *tmpdir;               /* Temporary directory for log file */
+    char *logfile;              /* Log file of process output */
+    bool is_child;              /* Whether we can waitpid for process */
+    struct process *next;       /* Next process in global list */
+};
+
+/*
+ * Global list of started processes, which will be cleaned up automatically on
+ * program exit if they haven't been explicitly stopped with process_stop
+ * prior to that point.
+ */
+static struct process *processes = NULL;
+
 
 /*
  * Given a function, an expected exit status, and expected output, runs that
@@ -178,3 +217,306 @@ run_setup(const char *const argv[])
     }
     free(output);
 }
+
+
+/*
+ * Free the resources associated with tracking a process, without doing
+ * anything to the process.  This is kept separate so that we can free
+ * resources during shutdown in a non-primary process.
+ */
+static void
+process_free(struct process *process)
+{
+    struct process **prev;
+
+    /* Remove the process from the global list. */
+    prev = &processes;
+    while (*prev != NULL && *prev != process)
+        prev = &(*prev)->next;
+    if (*prev == process)
+        *prev = process->next;
+
+    /* Free resources. */
+    free(process->pidfile);
+    free(process->logfile);
+    test_tmpdir_free(process->tmpdir);
+    free(process);
+}
+
+
+/*
+ * Kill a process and wait for it to exit.  Returns the status of the process.
+ * Calls bail on a system failure or a failure of the process to exit.
+ *
+ * We are quite aggressive with error reporting here because child processes
+ * that don't exit or that don't exist often indicate some form of test
+ * failure.
+ */
+static int
+process_kill(struct process *process)
+{
+    int result, i;
+    int status = -1;
+    struct timeval tv;
+    unsigned long pid = process->pid;
+
+    /* If the process is not a child, just kill it and hope. */
+    if (!process->is_child) {
+        if (kill(process->pid, SIGTERM) < 0 && errno != ESRCH)
+            sysbail("cannot send SIGTERM to process %lu", pid);
+        return 0;
+    }
+
+    /* Check if the process has already exited. */
+    result = waitpid(process->pid, &status, WNOHANG);
+    if (result < 0)
+        sysbail("cannot wait for child process %lu", pid);
+    else if (result > 0)
+        return status;
+
+    /*
+     * Kill the process and wait for it to exit.  I don't want to go to the
+     * work of setting up a SIGCHLD handler or a full event loop here, so we
+     * effectively poll every tenth of a second for process exit (and
+     * hopefully faster when it does since the SIGCHLD may interrupt our
+     * select, although we're racing with it.
+     */
+    if (kill(process->pid, SIGTERM) < 0 && errno != ESRCH)
+        sysbail("cannot send SIGTERM to child process %lu", pid);
+    for (i = 0; i < PROCESS_WAIT * 10; i++) {
+        tv.tv_sec = 0;
+        tv.tv_usec = 100000;
+        select(0, NULL, NULL, NULL, &tv);
+        result = waitpid(process->pid, &status, WNOHANG);
+        if (result < 0)
+            sysbail("cannot wait for child process %lu", pid);
+        else if (result > 0)
+            return status;
+    }
+
+    /* The process still hasn't exited.  Bail. */
+    bail("child process %lu did not exit on SIGTERM", pid);
+
+    /* Not reached, but some compilers may get confused. */
+    return status;
+}
+
+
+/*
+ * Stop a particular process given its process struct.  This kills the
+ * process, waits for it to exit if possible (giving it at most five seconds),
+ * and then removes it from the global processes struct so that it isn't
+ * stopped again during global shutdown.
+ */
+void
+process_stop(struct process *process)
+{
+    int status;
+    unsigned long pid = process->pid;
+
+    /* Stop the process. */
+    status = process_kill(process);
+
+    /* Call diag to flush logs as well as provide exit status. */
+    if (process->is_child)
+        diag("stopped process %lu (exit status %d)", pid, status);
+    else
+        diag("stopped process %lu", pid);
+
+    /* Remove the log and PID file. */
+    diag_file_remove(process->logfile);
+    unlink(process->pidfile);
+    unlink(process->logfile);
+
+    /* Free resources. */
+    process_free(process);
+}
+
+
+/*
+ * Stop all running processes.  This is called as a cleanup handler during
+ * process shutdown.  The first argument, which says whether the test was
+ * successful, is ignored, since the same actions should be performed
+ * regardless.  The second argument says whether this is the primary process,
+ * in which case we do the full shutdown.  Otherwise, we only free resources
+ * but don't stop the process.
+ */
+static void
+process_stop_all(int success UNUSED, int primary)
+{
+    while (processes != NULL) {
+        if (primary)
+            process_stop(processes);
+        else
+            process_free(processes);
+    }
+}
+
+
+/*
+ * Read the PID of a process from a file.  This is necessary when running
+ * under fakeroot to get the actual PID of the remctld process.
+ */
+static long
+read_pidfile(const char *path)
+{
+    FILE *file;
+    char buffer[BUFSIZ];
+    long pid;
+
+    file = fopen(path, "r");
+    if (file == NULL)
+        sysbail("cannot open %s", path);
+    if (fgets(buffer, sizeof(buffer), file) == NULL)
+        sysbail("cannot read from %s", path);
+    fclose(file);
+    pid = strtol(buffer, NULL, 10);
+    if (pid <= 0)
+        bail("cannot read PID from %s", path);
+    return pid;
+}
+
+
+/*
+ * Start a process and return its status information.  The status information
+ * is also stored in the global processes linked list so that it can be
+ * stopped automatically on program exit.
+ *
+ * The boolean argument says whether to start the process under fakeroot.  If
+ * true, PATH_FAKEROOT must be defined, generally by Autoconf.  If it's not
+ * found, call skip_all.
+ *
+ * This is a helper function for process_start and process_start_fakeroot.
+ */
+static struct process *
+process_start_internal(const char *const argv[], const char *pidfile,
+                       bool fakeroot)
+{
+    size_t i, size;
+    int log_fd;
+    const char *name;
+    struct timeval tv;
+    struct process *process;
+    const char **fakeroot_argv = NULL;
+    const char *path_fakeroot = PATH_FAKEROOT;
+
+    /* Check prerequisites. */
+    if (fakeroot && path_fakeroot[0] == '\0')
+        skip_all("fakeroot not found");
+
+    /* Create the process struct and log file. */
+    process = bcalloc(1, sizeof(struct process));
+    process->pidfile = bstrdup(pidfile);
+    process->tmpdir = test_tmpdir();
+    name = strrchr(argv[0], '/');
+    if (name != NULL)
+        name++;
+    else
+        name = argv[0];
+    basprintf(&process->logfile, "%s/%s.log.XXXXXX", process->tmpdir, name);
+    log_fd = mkstemp(process->logfile);
+    if (log_fd < 0)
+        sysbail("cannot create log file for %s", argv[0]);
+
+    /* If using fakeroot, rewrite argv accordingly. */
+    if (fakeroot) {
+        for (i = 0; argv[i] != NULL; i++)
+            ;
+        size = 2 + i + 1;
+        fakeroot_argv = bmalloc(size * sizeof(const char *));
+        fakeroot_argv[0] = path_fakeroot;
+        fakeroot_argv[1] = "--";
+        for (i = 0; argv[i] != NULL; i++)
+            fakeroot_argv[i + 2] = argv[i];
+        fakeroot_argv[i + 2] = NULL;
+        argv = fakeroot_argv;
+    }
+
+    /*
+     * Fork off the child process, redirect its standard output and standard
+     * error to the log file, and then exec the program.
+     */
+    process->pid = fork();
+    if (process->pid < 0)
+        sysbail("fork failed");
+    else if (process->pid == 0) {
+        if (dup2(log_fd, STDOUT_FILENO) < 0)
+            sysbail("cannot redirect standard output");
+        if (dup2(log_fd, STDERR_FILENO) < 0)
+            sysbail("cannot redirect standard error");
+        close(log_fd);
+        if (execv(argv[0], (char *const *) argv) < 0)
+            sysbail("exec of %s failed", argv[0]);
+    }
+    close(log_fd);
+    free(fakeroot_argv);
+
+    /*
+     * In the parent.  Wait for the child to start by watching for the PID
+     * file to appear in 100ms intervals.
+     */
+    for (i = 0; i < PROCESS_WAIT * 10 && access(pidfile, F_OK) != 0; i++) {
+        tv.tv_sec = 0;
+        tv.tv_usec = 100000;
+        select(0, NULL, NULL, NULL, &tv);
+    }
+
+    /*
+     * If the PID file still hasn't appeared after ten seconds, attempt to
+     * kill the process and then bail.
+     */
+    if (access(pidfile, F_OK) != 0) {
+        kill(process->pid, SIGTERM);
+        alarm(5);
+        waitpid(process->pid, NULL, 0);
+        alarm(0);
+        bail("cannot start %s", argv[0]);
+    }
+
+    /*
+     * Read the PID back from the PID file.  This usually isn't necessary for
+     * non-forking daemons, but always doing this makes this function general,
+     * and it's required when running under fakeroot.
+     */
+    if (fakeroot)
+        process->pid = read_pidfile(pidfile);
+    process->is_child = !fakeroot;
+
+    /* Register the log file as a source of diag messages. */
+    diag_file_add(process->logfile);
+
+    /*
+     * Add the process to our global list and set our cleanup handler if this
+     * is the first process we started.
+     */
+    if (processes == NULL)
+        test_cleanup_register(process_stop_all);
+    process->next = processes;
+    processes = process;
+
+    /* All done. */
+    return process;
+}
+
+
+/*
+ * Start a process and return the opaque process struct.  The process must
+ * create pidfile with its PID when startup is complete.
+ */
+struct process *
+process_start(const char *const argv[], const char *pidfile)
+{
+    return process_start_internal(argv, pidfile, false);
+}
+
+
+/*
+ * Start a process under fakeroot and return the opaque process struct.  If
+ * fakeroot is not available, calls skip_all.  The process must create pidfile
+ * with its PID when startup is complete.
+ */
+struct process *
+process_start_fakeroot(const char *const argv[], const char *pidfile)
+{
+    return process_start_internal(argv, pidfile, true);
+}