/* * Utility functions for tests that use subprocesses. * * Provides utility functions for subprocess manipulation. Specifically, * provides a function, run_setup, which runs a command and bails if it fails, * using its error message as the bail output, and is_function_output, which * 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 . * * Written by Russ Allbery * Copyright 2002, 2004, 2005, 2013 Russ Allbery * 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 * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #ifdef HAVE_SYS_SELECT_H # include #endif #include #ifdef HAVE_SYS_TIME_H # include #endif #include #include #include #include #include /* 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 * function in a subprocess, capturing stdout and stderr via a pipe, and * returns the function output in newly allocated memory. Also captures the * process exit status. */ static void run_child_function(test_function_type function, void *data, int *status, char **output) { int fds[2]; pid_t child; char *buf; ssize_t count, ret, buflen; int rval; /* Flush stdout before we start to avoid odd forking issues. */ fflush(stdout); /* Set up the pipe and call the function, collecting its output. */ if (pipe(fds) == -1) sysbail("can't create pipe"); child = fork(); if (child == (pid_t) -1) { sysbail("can't fork"); } else if (child == 0) { /* In child. Set up our stdout and stderr. */ close(fds[0]); if (dup2(fds[1], 1) == -1) _exit(255); if (dup2(fds[1], 2) == -1) _exit(255); /* Now, run the function and exit successfully if it returns. */ (*function)(data); fflush(stdout); _exit(0); } else { /* * In the parent; close the extra file descriptor, read the output if * any, and then collect the exit status. */ close(fds[1]); buflen = BUFSIZ; buf = bmalloc(buflen); count = 0; do { ret = read(fds[0], buf + count, buflen - count - 1); if (ret > 0) count += ret; if (count >= buflen - 1) { buflen += BUFSIZ; buf = brealloc(buf, buflen); } } while (ret > 0); buf[count < 0 ? 0 : count] = '\0'; if (waitpid(child, &rval, 0) == (pid_t) -1) sysbail("waitpid failed"); close(fds[0]); } /* Store the output and return. */ *status = rval; *output = buf; } /* * Given a function, data to pass to that function, an expected exit status, * and expected output, runs that function in a subprocess, capturing stdout * and stderr via a pipe, and compare the combination of stdout and stderr * with the expected output and the exit status with the expected status. * Expects the function to always exit (not die from a signal). */ void is_function_output(test_function_type function, void *data, int status, const char *output, const char *format, ...) { char *buf, *msg; int rval; va_list args; run_child_function(function, data, &rval, &buf); /* Now, check the results against what we expected. */ va_start(args, format); bvasprintf(&msg, format, args); va_end(args); ok(WIFEXITED(rval), "%s (exited)", msg); is_int(status, WEXITSTATUS(rval), "%s (status)", msg); is_string(output, buf, "%s (output)", msg); free(buf); free(msg); } /* * A helper function for run_setup. This is a function to run an external * command, suitable for passing into run_child_function. The expected * argument must be an argv array, with argv[0] being the command to run. */ static void exec_command(void *data) { char *const *argv = data; execvp(argv[0], argv); } /* * Given a command expressed as an argv struct, with argv[0] the name or path * to the command, run that command. If it exits with a non-zero status, use * the part of its output up to the first newline as the error message when * calling bail. */ void run_setup(const char *const argv[]) { char *output, *p; int status; run_child_function(exec_command, (void *) argv, &status, &output); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { p = strchr(output, '\n'); if (p != NULL) *p = '\0'; if (output[0] != '\0') bail("%s", output); else bail("setup command failed with no output"); } 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; /* Do nothing if called with a NULL argument. */ if (process == NULL) return; /* 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; 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++) ; fakeroot_argv = bcalloc(2 + i + 1, 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); }