nagios4/lib/worker.c

876 lines
20 KiB
C

#include <unistd.h>
#include <stdio.h>
#include <stdarg.h>
#include <signal.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <syslog.h>
#include "libnagios.h"
#include "../include/config.h"
#define MSG_DELIM "\1\0\0" /**< message limiter - note this ends up being
\1\0\0\0 on the wire as "" strings null-terminate */
#define MSG_DELIM_LEN (sizeof(MSG_DELIM)) /**< message delimiter length - 4, not 3 */
#define PAIR_SEP 0 /**< pair separator for buf2kvvec() and kvvec2buf() */
#define KV_SEP '=' /**< key/value separator for buf2kvvec() and kvvec2buf() */
struct execution_information {
squeue_event *sq_event;
pid_t pid;
int state;
struct timeval start;
struct timeval stop;
float runtime;
/* 5DEPR: rusage is deprecated for Nagios, will be removed in 5.0.0 */
struct rusage rusage;
};
static iobroker_set *iobs;
static squeue_t *sq;
static unsigned int started, running_jobs, timeouts, reapable;
static int master_sd;
static int parent_pid;
static fanout_table *ptab;
static void exit_worker(int code, const char *msg)
{
child_process *cp;
int discard;
#ifdef HAVE_SIGACTION
struct sigaction sig_action;
#endif
if (msg) {
perror(msg);
}
/*
* We must kill our children, so let's embark on that
* large scale filicide. Each process should be in a
* process group of its own, so we can signal not only
* the plugin but also all of its children.
*/
#ifdef HAVE_SIGACTION
sig_action.sa_sigaction = NULL;
sig_action.sa_handler = SIG_IGN;
sigemptyset(&sig_action.sa_mask);
sig_action.sa_flags = 0;
sigaction(SIGTERM, &sig_action, NULL);
sigaction(SIGSEGV, &sig_action, NULL);
#else
signal(SIGTERM, SIG_IGN);
signal(SIGSEGV, SIG_IGN);
#endif
kill(0, SIGTERM);
while (waitpid(-1, &discard, WNOHANG) > 0)
; /* do nothing */
sleep(1);
while ((cp = (child_process *)squeue_pop(sq))) {
/* kill all processes in the child's process group */
(void)kill(-cp->ei->pid, SIGKILL);
}
sleep(1);
while (waitpid(-1, &discard, WNOHANG) > 0)
; /* do nothing */
exit(code);
}
/*
* write a log message to master.
* Note that this will break if we change delimiters someday,
* but avoids doing several extra malloc()+free() for this
* pretty simple case.
*/
__attribute__((__format__(__printf__, 1, 2)))
static void wlog(const char *fmt, ...)
{
#define LOG_KEY_LEN 4
static char lmsg[8192] = "log=";
int len;
va_list ap;
va_start(ap, fmt);
len = vsnprintf(lmsg + LOG_KEY_LEN, sizeof(lmsg) - LOG_KEY_LEN - MSG_DELIM_LEN, fmt, ap);
va_end(ap);
if (len < 0) {
/* We can't send what we can't print. */
return;
}
len += LOG_KEY_LEN; /* log= */
if (len > sizeof(lmsg) - MSG_DELIM_LEN - 1) {
/* A truncated log is better than no log or buffer overflows. */
len = sizeof(lmsg) - MSG_DELIM_LEN - 1;
}
/* Add the kv pair separator and the message delimiter. */
lmsg[len] = 0;
len++;
memcpy(lmsg + len, MSG_DELIM, MSG_DELIM_LEN);
len += MSG_DELIM_LEN;
if (write(master_sd, lmsg, len) < 0 && errno == EPIPE) {
/* Master has died or abandoned us, so exit. */
exit_worker(1, "Failed to write() to master");
}
}
__attribute__((__format__(__printf__, 3, 4)))
static void job_error(child_process *cp, struct kvvec *kvv, const char *fmt, ...)
{
char msg[4096];
int len;
va_list ap;
va_start(ap, fmt);
len = vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
if (len < 0) {
/* We can't send what we can't print. */
kvvec_destroy(kvv, 0);
return;
}
if (len > sizeof(msg) - 1) {
/* A truncated log is better than no log or buffer overflows. */
len = sizeof(msg) - 1;
}
msg[len] = 0;
if (cp) {
kvvec_addkv(kvv, "job_id", mkstr("%d", cp->id));
}
kvvec_addkv_wlen(kvv, "error_msg", 9, msg, len);
if (worker_send_kvvec(master_sd, kvv) < 0 && errno == EPIPE) {
/* Master has died or abandoned us, so exit. */
exit_worker(1, "Failed to send job error key/value vector to master");
}
kvvec_destroy(kvv, 0);
}
struct kvvec_buf *build_kvvec_buf(struct kvvec *kvv)
{
struct kvvec_buf *kvvb;
/*
* key=value, separated by PAIR_SEP and messages
* delimited by MSG_DELIM
*/
kvvb = kvvec2buf(kvv, KV_SEP, PAIR_SEP, MSG_DELIM_LEN);
if (!kvvb) {
return NULL;
}
memcpy(kvvb->buf + (kvvb->bufsize - MSG_DELIM_LEN), MSG_DELIM, MSG_DELIM_LEN);
return kvvb;
}
int worker_send_kvvec(int sd, struct kvvec *kvv)
{
int ret;
struct kvvec_buf *kvvb;
kvvb = build_kvvec_buf(kvv);
if (!kvvb)
return -1;
/* bufsize, not buflen, as it gets us the delimiter */
ret = nwrite(sd, kvvb->buf, kvvb->bufsize, NULL);
free(kvvb->buf);
free(kvvb);
return ret;
}
int send_kvvec(int sd, struct kvvec *kvv)
{
return worker_send_kvvec(sd, kvv);
}
char *worker_ioc2msg(iocache *ioc, unsigned long *size, int flags)
{
return iocache_use_delim(ioc, MSG_DELIM, MSG_DELIM_LEN, size);
}
int worker_buf2kvvec_prealloc(struct kvvec *kvv, char *buf, unsigned long len, int kvv_flags)
{
return buf2kvvec_prealloc(kvv, buf, len, KV_SEP, PAIR_SEP, kvv_flags);
}
#define kvvec_add_long(kvv, key, value) \
do { \
const char *buf = mkstr("%ld", value); \
kvvec_addkv_wlen(kvv, key, sizeof(key) - 1, buf, strlen(buf)); \
} while (0)
#define kvvec_add_tv(kvv, key, value) \
do { \
const char *buf = mkstr("%ld.%06ld", value.tv_sec, value.tv_usec); \
kvvec_addkv_wlen(kvv, key, sizeof(key) - 1, buf, strlen(buf)); \
} while (0)
/* forward declaration */
static int gather_output(child_process *cp, iobuf *io, int final);
static void destroy_job(child_process *cp)
{
/*
* we must remove the job's timeout ticker,
* or we'll end up accessing an already free()'d
* pointer, or the pointer to a different child.
*/
squeue_remove(sq, cp->ei->sq_event);
running_jobs--;
fanout_remove(ptab, cp->ei->pid);
if (cp->outstd.buf) {
free(cp->outstd.buf);
cp->outstd.buf = NULL;
}
if (cp->outerr.buf) {
free(cp->outerr.buf);
cp->outerr.buf = NULL;
}
if(NULL != cp->env) kvvec_destroy(cp->env, KVVEC_FREE_ALL);
kvvec_destroy(cp->request, KVVEC_FREE_ALL);
free(cp->cmd);
free(cp->ei);
free(cp);
}
#define strip_nul_bytes(io) \
do { \
char *nul; \
if (!io.buf || !*io.buf) \
io.len = 0; \
else if ((nul = memchr(io.buf, 0, io.len))) { \
io.len = (unsigned long)nul - (unsigned long)io.buf; \
} \
} while (0)
int finish_job(child_process *cp, int reason)
{
static struct kvvec resp = KVVEC_INITIALIZER;
int i, ret, rd;
/* get rid of still open filedescriptors */
if (cp->outstd.fd != -1) {
rd = 1;
while(rd > 0) {
rd = gather_output(cp, &cp->outstd, 0);
}
iobroker_close(iobs, cp->outstd.fd);
}
if (cp->outerr.fd != -1) {
rd = 1;
while(rd > 0) {
rd = gather_output(cp, &cp->outerr, 0);
}
iobroker_close(iobs, cp->outerr.fd);
}
/* Make sure network-supplied data doesn't contain nul bytes */
strip_nul_bytes(cp->outstd);
strip_nul_bytes(cp->outerr);
/* how many key/value pairs do we need? */
if (kvvec_init(&resp, 12 + cp->request->kv_pairs) == NULL) {
/* what the hell do we do now? */
exit_worker(1, "Failed to init response key/value vector");
}
gettimeofday(&cp->ei->stop, NULL);
if (running_jobs != squeue_size(sq)) {
wlog("running_jobs(%d) != squeue_size(sq) (%d)\n",
running_jobs, squeue_size(sq));
wlog("started: %d; running: %d; finished: %d\n",
started, running_jobs, started - running_jobs);
}
cp->ei->runtime = tv_delta_f(&cp->ei->start, &cp->ei->stop);
/*
* Now build the return message.
* First comes the request, minus environment variables
*/
for (i = 0; i < cp->request->kv_pairs; i++) {
struct key_value *kv = &cp->request->kv[i];
/* skip environment macros */
if (kv->key_len == 3 && !strcmp(kv->key, "env")) {
continue;
}
kvvec_addkv_wlen(&resp, kv->key, kv->key_len, kv->value, kv->value_len);
}
kvvec_addkv(&resp, "wait_status", mkstr("%d", cp->ret));
kvvec_add_tv(&resp, "start", cp->ei->start);
kvvec_add_tv(&resp, "stop", cp->ei->stop);
kvvec_addkv(&resp, "runtime", mkstr("%f", cp->ei->runtime));
if (!reason) {
/* child exited nicely */
kvvec_addkv(&resp, "exited_ok", "1");
} else {
/* some error happened */
kvvec_addkv(&resp, "exited_ok", "0");
kvvec_addkv(&resp, "error_code", mkstr("%d", reason));
}
kvvec_addkv_wlen(&resp, "outerr", 6, cp->outerr.buf, cp->outerr.len);
kvvec_addkv_wlen(&resp, "outstd", 6, cp->outstd.buf, cp->outstd.len);
ret = worker_send_kvvec(master_sd, &resp);
if (ret < 0 && errno == EPIPE)
exit_worker(1, "Failed to send kvvec struct to master");
return 0;
}
static int check_completion(child_process *cp, int flags)
{
int status;
pid_t result;
if (!cp || !cp->ei->pid) {
return 0;
}
/*
* we mustn't let EINTR interrupt us, since it could well
* be a SIGCHLD from the properly exiting process doing it
*/
do {
errno = 0;
result = waitpid(cp->ei->pid, &status, flags);
} while (result < 0 && errno == EINTR);
if (result == cp->ei->pid || (result < 0 && errno == ECHILD)) {
cp->ret = status;
finish_job(cp, 0);
destroy_job(cp);
return 0;
}
if (!result)
return -1;
return -errno;
}
/*
* "What can the harvest hope for, if not for the care
* of the Reaper Man?"
* -- Terry Pratchett, Reaper Man
*
* We end up here no matter if the job is stale (ie, the child is
* stuck in uninterruptable sleep) or if it's the first time we try
* to kill it.
* A job is considered reaped once we reap our direct child, in
* which case init will become parent of our grandchildren.
* It's also considered fully reaped if kill() results in ESRCH or
* EPERM, or if wait()ing for the process group results in ECHILD.
*/
static void kill_job(child_process *cp, int reason)
{
pid_t ret;
int status, reaped = 0;
int pid = cp ? cp->ei->pid : 0;
/*
* first attempt at reaping, so see if we just failed to
* notice that things were going wrong her
*/
if (reason == ETIME && !check_completion(cp, WNOHANG)) {
timeouts++;
wlog("job %d with pid %d reaped at timeout. timeouts=%u; started=%u", cp->id, pid, timeouts, started);
return;
}
/* brutal but efficient */
if (kill(-cp->ei->pid, SIGKILL) < 0) {
if (errno == ESRCH) {
reaped = 1;
} else {
wlog("kill(-%ld, SIGKILL) failed: %s\n", (long)cp->ei->pid, strerror(errno));
}
}
/*
* we must iterate at least once, in case kill() returns
* ESRCH when there's zombies
*/
do {
ret = waitpid(cp->ei->pid, &status, WNOHANG);
if (ret < 0 && errno == EINTR)
continue;
if (ret == cp->ei->pid || (ret < 0 && errno == ECHILD)) {
reaped = 1;
break;
}
if (!ret) {
struct timeval tv;
gettimeofday(&tv, NULL);
/*
* stale process (signal may not have been delivered, or
* the child can be stuck in uninterruptible sleep). We
* can't hang around forever, so just reschedule a new
* reap attempt later.
*/
if (reason == ESTALE) {
wlog("tv.tv_sec is currently %llu", (unsigned long long)tv.tv_sec);
tv.tv_sec += 5;
wlog("Failed to reap child with pid %ld. Next attempt @ %llu.%lu", (long)cp->ei->pid, (unsigned long long)tv.tv_sec, (unsigned long)tv.tv_usec);
} else {
tv.tv_usec += 250000;
if (tv.tv_usec > 1000000) {
tv.tv_usec -= 1000000;
tv.tv_sec += 1;
}
cp->ei->state = ESTALE;
finish_job(cp, reason);
}
squeue_remove(sq, cp->ei->sq_event);
cp->ei->sq_event = squeue_add_tv(sq, &tv, cp);
return;
}
} while (!reaped);
if (cp->ei->state != ESTALE)
finish_job(cp, reason);
else
wlog("job %d (pid=%ld): Dormant child reaped", cp->id, (long)cp->ei->pid);
destroy_job(cp);
}
static int gather_output(child_process *cp, iobuf *io, int final)
{
int retry = 5;
int rd;
for (;;) {
char buf[4096];
rd = read(io->fd, buf, sizeof(buf));
if (rd < 0) {
if (errno == EAGAIN && !final)
break;
if (errno == EINTR || errno == EAGAIN) {
char buf[1024];
if (--retry == 0) {
sprintf(buf, "job %d (pid=%ld): Failed to read(): %s", cp->id, (long)cp->ei->pid, strerror(errno));
break;
}
sprintf(buf, "job %d (pid=%ld): read() returned error %d", cp->id, (long)cp->ei->pid, errno);
syslog(LOG_NOTICE, "%s", buf);
sleep(1);
continue;
}
if (!final && errno != EAGAIN)
wlog("job %d (pid=%ld): Failed to read(): %s", cp->id, (long)cp->ei->pid, strerror(errno));
}
if (rd > 0) {
/* we read some data */
io->buf = realloc(io->buf, rd + io->len + 1);
memcpy(&io->buf[io->len], buf, rd);
io->len += rd;
io->buf[io->len] = '\0';
}
/*
* Close down on bad, zero and final reads (we don't get
* EAGAIN, so all errors are really unfixable)
*/
if (rd <= 0 || final) {
iobroker_close(iobs, io->fd);
io->fd = -1;
break;
}
break;
}
return rd;
}
static int stderr_handler(int fd, int events, void *cp_)
{
child_process *cp = (child_process *)cp_;
gather_output(cp, &cp->outerr, 0);
return 0;
}
static int stdout_handler(int fd, int events, void *cp_)
{
child_process *cp = (child_process *)cp_;
gather_output(cp, &cp->outstd, 0);
return 0;
}
static void sigchld_handler(int sig)
{
reapable++;
}
static void reap_jobs(void)
{
int reaped = 0;
do {
int pid, status;
pid = waitpid(-1, &status, WNOHANG);
if (pid > 0) {
struct child_process *cp;
reapable--;
if (!(cp = fanout_get(ptab, pid))) {
/* we reaped a lost child. Odd that */
continue;
}
cp->ret = status;
reaped++;
if (cp->ei->state != ESTALE)
finish_job(cp, cp->ei->state);
destroy_job(cp);
}
else if (!pid || (pid < 0 && errno == ECHILD)) {
reapable = 0;
}
} while (reapable);
}
void cmd_iobroker_register(int fdout, int fderr, void *arg) {
/* We must never block, even if plugins issue '_exit()' */
fcntl(fdout, F_SETFL, O_NONBLOCK);
fcntl(fderr, F_SETFL, O_NONBLOCK);
if( iobroker_register(iobs, fdout, arg, stdout_handler) < 0) {
wlog("Failed to register iobroker for stdout");
}
if( iobroker_register(iobs, fderr, arg, stderr_handler) < 0) {
wlog("Failed to register iobroker for stderr");
}
}
char **env_from_kvvec(struct kvvec *kvv_env) {
int i;
char **env;
if(NULL == kvv_env) return NULL;
env = calloc(kvv_env->kv_pairs*2+1, sizeof(char *));
for (i = 0; i < kvv_env->kv_pairs; i++) {
struct key_value *kv = &kvv_env->kv[i];
env[i*2] = kv->key;
env[i*2+1] = kv->value;
}
env[i*2] = NULL;
return env;
}
int start_cmd(child_process *cp)
{
int pfd[2] = {-1, -1}, pfderr[2] = {-1, -1};
char **env = env_from_kvvec(cp->env);
cp->outstd.fd = runcmd_open(cp->cmd, pfd, pfderr, env,
cmd_iobroker_register, cp);
my_free(env);
if (cp->outstd.fd < 0) {
return -1;
}
cp->outerr.fd = pfderr[0];
cp->ei->pid = runcmd_pid(cp->outstd.fd);
/* no pid means we somehow failed */
if (!cp->ei->pid) {
return -1;
}
fanout_add(ptab, cp->ei->pid, cp);
return 0;
}
static iocache *ioc;
static child_process *parse_command_kvvec(struct kvvec *kvv)
{
int i;
child_process *cp;
/* get this command's struct and insert it at the top of the list */
cp = calloc(1, sizeof(*cp));
if (!cp) {
wlog("Failed to calloc() a child_process struct");
return NULL;
}
cp->ei = calloc(1, sizeof(*cp->ei));
if (!cp->ei) {
wlog("Failed to calloc() a execution_information struct");
return NULL;
}
/*
* we must copy from the vector, since it points to data
* found in the iocache where we read the command, which will
* be overwritten when we receive one next
*/
for (i = 0; i < kvv->kv_pairs; i++) {
struct key_value *kv = &kvv->kv[i];
char *key = kv->key;
char *value = kv->value;
char *endptr;
if (!strcmp(key, "command")) {
cp->cmd = strdup(value);
continue;
}
if (!strcmp(key, "job_id")) {
cp->id = (unsigned int)strtoul(value, &endptr, 0);
continue;
}
if (!strcmp(key, "timeout")) {
cp->timeout = (unsigned int)strtoul(value, &endptr, 0);
continue;
}
if (!strcmp(key, "env")) {
cp->env = buf2kvvec(value, strlen(value), '=', '\n', KVVEC_COPY);
continue;
}
}
/* jobs without a timeout get a default of 60 seconds. */
if (!cp->timeout) {
cp->timeout = 60;
}
return cp;
}
static void spawn_job(struct kvvec *kvv, int(*cb)(child_process *))
{
int result;
child_process *cp;
if (!kvv) {
wlog("Received NULL command key/value vector. Bug in iocache.c or kvvec.c?");
return;
}
cp = parse_command_kvvec(kvv);
if (!cp) {
job_error(NULL, kvv, "Failed to parse worker-command");
return;
}
if (!cp->cmd) {
job_error(cp, kvv, "Failed to parse commandline. Ignoring job %u", cp->id);
return;
}
gettimeofday(&cp->ei->start, NULL);
cp->request = kvv;
cp->ei->sq_event = squeue_add(sq, cp->timeout + time(NULL), cp);
started++;
running_jobs++;
result = cb(cp);
if (result < 0) {
job_error(cp, kvv, "Failed to start child: %s: %s", runcmd_strerror(result), strerror(errno));
squeue_remove(sq, cp->ei->sq_event);
running_jobs--;
return;
}
}
static int receive_command(int sd, int events, void *arg)
{
int ioc_ret;
char *buf;
unsigned long size;
if (!ioc) {
ioc = iocache_create(512 * 1024);
}
ioc_ret = iocache_read(ioc, sd);
/* master closed the connection, so we exit */
if (ioc_ret == 0) {
iobroker_close(iobs, sd);
exit_worker(0, NULL);
}
if (ioc_ret < 0) {
/* XXX: handle this somehow */
}
#if 0
/* debug-volley */
buf = iocache_use_size(ioc, ioc_ret);
write(master_sd, buf, ioc_ret);
return 0;
#endif
/*
* now loop over all inbound messages in the iocache.
* Since KV_TERMINATOR is a nul-byte, they're separated by 3 nuls
*/
while ((buf = iocache_use_delim(ioc, MSG_DELIM, MSG_DELIM_LEN, &size))) {
struct kvvec *kvv;
/* we must copy vars here, as we preserve them for the response */
kvv = buf2kvvec(buf, (unsigned int)size, KV_SEP, PAIR_SEP, KVVEC_COPY);
if (kvv)
spawn_job(kvv, arg);
}
return 0;
}
int worker_set_sockopts(int sd, int bufsize)
{
int ret;
ret = fcntl(sd, F_SETFD, FD_CLOEXEC);
ret |= fcntl(sd, F_SETFL, O_NONBLOCK);
if (!bufsize)
return ret;
ret |= setsockopt(sd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(int));
ret |= setsockopt(sd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(int));
return ret;
}
/* XXX: deprecated */
int set_socket_options(int sd, int bufsize)
{
return worker_set_sockopts(sd, bufsize);
}
void enter_worker(int sd, int (*cb)(child_process*))
{
#ifdef HAVE_SIGACTION
struct sigaction sig_action;
#endif
/* created with socketpair(), usually */
master_sd = sd;
parent_pid = getppid();
(void)chdir("/tmp");
(void)chdir("nagios-workers");
ptab = fanout_create(4096);
if (setpgid(0, 0)) {
/* XXX: handle error somehow, or maybe just ignore it */
}
/* we need to catch child signals to mark jobs as reapable */
#ifdef HAVE_SIGACTION
sig_action.sa_sigaction = NULL;
sig_action.sa_handler = sigchld_handler;
sigfillset(&sig_action.sa_mask);
sig_action.sa_flags=SA_NOCLDSTOP;
sigaction(SIGCHLD, &sig_action, NULL);
#else
signal(SIGCHLD, sigchld_handler);
#endif
fcntl(fileno(stdout), F_SETFD, FD_CLOEXEC);
fcntl(fileno(stderr), F_SETFD, FD_CLOEXEC);
fcntl(master_sd, F_SETFD, FD_CLOEXEC);
iobs = iobroker_create();
if (!iobs) {
/* XXX: handle this a bit better */
exit_worker(EXIT_FAILURE, "Worker failed to create io broker socket set");
}
/*
* Create a modest scheduling queue that will be
* more than enough for our needs
*/
sq = squeue_create(1024);
worker_set_sockopts(master_sd, 256 * 1024);
iobroker_register(iobs, master_sd, cb, receive_command);
while (iobroker_get_num_fds(iobs) > 0) {
int poll_time = -1;
/* check for timed out jobs */
while (running_jobs) {
child_process *cp;
struct timeval now, tmo;
/* stop when scheduling queue is empty */
cp = (child_process *)squeue_peek(sq);
if (!cp)
break;
tmo.tv_usec = cp->ei->start.tv_usec;
tmo.tv_sec = cp->ei->start.tv_sec + cp->timeout;
gettimeofday(&now, NULL);
poll_time = tv_delta_msec(&now, &tmo);
/*
* A little extra takes care of rounding errors and
* ensures we never kill a job before it times out.
* 5 milliseconds is enough to take care of that.
*/
poll_time += 5;
if (poll_time > 0)
break;
if (cp->ei->state == ESTALE) {
if(cp->ei->sq_event &&
squeue_evt_when_is_after(cp->ei->sq_event, &now)) {
/* If the state is already stale, there is a already
a job to kill the child on the queue so don't
add another one here. */
kill_job(cp, ESTALE);
}
} else {
/* this job timed out, so kill it */
wlog("job %d (pid=%ld) timed out. Killing it", cp->id, (long)cp->ei->pid);
kill_job(cp, ETIME);
}
}
iobroker_poll(iobs, poll_time);
if (reapable)
reap_jobs();
}
/* we exit when the master shuts us down */
exit(EXIT_SUCCESS);
}
int spawn_named_helper(char *path, char **argv)
{
int ret, pid;
pid = fork();
if (pid < 0) {
return -1;
}
/* parent leaves early */
if (pid)
return pid;
ret = execvp(path, argv);
/* if execvp() fails, there's really nothing we can do */
exit(ret);
}
int spawn_helper(char **argv)
{
return spawn_named_helper(argv[0], argv);
}