/*
* 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);
}