/* * SUBPROCS - Support for managing subprocesses and communicating with them * * Author: * Emile van Bergen, emile@evbergen.xs4all.nl * * Permission to redistribute an original or modified version of this program * in source, intermediate or object code form is hereby granted exclusively * under the terms of the GNU General Public License, version 2. Please see the * file COPYING for details, or refer to http://www.gnu.org/copyleft/gpl.html. * * History: * 2001/07/18 - EvB - Created all over based on test/newproctest.c * 2002/03/19 - EvB - Added support for custom working directory * 2002/03/20 - EvB - Removed bug that caused watchdog timeouts on subprocesses * in state XFERING not to be handled gracefully */ char subprocs_id[] = "SUBPROCS - Copyright (C) 2001 Emile van Bergen."; /* * INCLUDES & DEFINES */ #include #include #include #include #include #include #include #include #include #include /* for AVMSG_ASCII */ #include #define DEBUGLEVEL 3 #include /* * FUNCTIONS */ PROC *proc_new(char *cmdline, int cmdlinelen, int flags, int xfertimeout, struct chan *chan, char *basepath, char *progcwd) { PROC *ret; char *c; int len, argc; /* * Check args, allocate object and init it enough for cleanup with * proc_del. */ if (!cmdline || !cmdline[0]) return 0; if (!cmdlinelen) cmdlinelen = strlen(cmdline); ret = (PROC *)malloc(sizeof(PROC)); if (!ret) return 0; memset(ret, 0, sizeof(PROC)); ret->rfd = ret->wfd = -1; /* * Duplicate the command line into the cmd member, adding the * specified base path if it doesn't start with a '/'. */ if (cmdline[0] != '/' && basepath && basepath[0]) { len = strlen(basepath); ret->cmd = (char *)malloc(len + 1 + cmdlinelen + 1); if (!ret->cmd) { proc_del(ret); return 0; } memcpy(ret->cmd, basepath, len); ret->cmd[len++] = '/'; } else { len = 0; ret->cmd = (char *)malloc(cmdlinelen + 1); if (!ret->cmd) { proc_del(ret); return 0; } } memcpy(ret->cmd + len, cmdline, cmdlinelen); ret->cmd[cmdlinelen + len] = 0; /* * Split the copied command line in parts */ /* Count number of words in command line */ for(argc = 0, c = ret->cmd; *c; argc++) { c += strspn(c, " \t\n\r"); c += strcspn(c, " \t\n\r"); } /* Allocate argv array with words + 1 elements */ ret->argv = (char **)malloc((argc + 1) * sizeof(char *)); if (!ret->argv) { proc_del(ret); return 0; } /* Split command line into argument array and zero-terminate it */ for(argc = 0, c = ret->cmd; ; ) { c += strspn(c, " \t\n\r"); ret->argv[argc++] = c; c += strcspn(c, " \t\n\r"); if (!*c) break; *c++ = 0; } ret->argv[argc] = 0; /* Test if argv[0] is at least executable, to prevent simple errors */ if (access(ret->argv[0], X_OK) == -1) { msg(F_PROC, L_ERR, "proc_new: ERROR: Program '%s' is not " "executable: %s!\n", ret->argv[0], strerror(errno)); proc_del(ret); return 0; } /* * Create environment containing interface version and flags. In * the future we could perhaps use a META_AV list instead. */ /* Allocate array with PR_ENV_VARS elements */ ret->envp = (char **)malloc((PR_ENV_VARS + 1) * sizeof(char *)); if (!ret->envp) { proc_del(ret); return 0; } memset(ret->envp, 0, PR_ENV_VARS * sizeof(char *)); /* Set first element; the binary interface has been bumped to major * version 2 because of the different request magic. */ c = (char *)malloc((len = strlen(PR_ENV_IFACEVER)) + 1 + 1); if (!c) { proc_del(ret); return 0; } memcpy(c, PR_ENV_IFACEVER, len); c[len] = flags & AVMSG_ASCII ? '1' : '2'; c[len + 1] = 0; ret->envp[0] = c; /* Set second element */ c = (char *)malloc((len = strlen(PR_ENV_IFACEFLAGS)) + 8 + 1); if (!c) { proc_del(ret); return 0; } memcpy(c, PR_ENV_IFACEFLAGS, len); meta_ordtoa(c + len, 8, 8, 16, flags); c[len + 8] = 0; ret->envp[1] = c; /* Set third element */ ret->envp[2] = 0; /* * Initialise the other members */ if (progcwd) { c = (char *)malloc(len = (strlen(progcwd) + 1)); if (!c) { proc_del(ret); return 0; } memcpy(c, progcwd, len); ret->cwd = c; } ret->chan = chan; ret->r = ring_new(PR_RING_SIZE); if (!ret->r) {proc_del(ret); return 0;} ret->w = ring_new(PR_RING_SIZE); if (!ret->w) {proc_del(ret); return 0;} ret->rfd = ret->wfd = -1; ret->flags = flags; ret->xfertimeout = xfertimeout; ret->expectedlen = -1; ret->timer = 0; ret->state = PRS_WAITING; return ret; } void proc_del(PROC *p) { char **s; if (p) { /* Kill subprocess if it wasn't dead yet */ if (p->pid) kill(p->pid, SIGKILL); /* Close FDs if they aren't yet */ if (p->rfd != -1) close(p->rfd); if (p->wfd != -1) close(p->wfd); /* Free rings */ if (p->r) ring_del(p->r); if (p->w) ring_del(p->w); /* Free environment */ if (p->envp) { for(s = p->envp; *s; s++) free(*s); free(p->envp); } /* Free arguments */ if (p->argv) free(p->argv); if (p->cmd) free(p->cmd); /* Free working directory */ if (p->cwd) free(p->cwd); /* Free object itself */ free(p); } } /* Start the process. If it doesn't work out, we go to the STARTING state which will cause handle_timeout to try again each PRT_RETRYSTART seconds. */ int proc_start(PROC *p, time_t t) { int p2c[2] = {-1, -1}, c2p[2] = {-1, -1}; /* If we were already running, exit now. */ if (p->pid) { msg(F_PROC, L_NOTICE, "proc_run: WARNING: Already running, as " "pid %d!\n", p->pid); return p->pid; } /* Create pipes; set close on exec all ends and non-blocking on ours */ if (pipe(p2c) == -1 || fcntl(p2c[PIPE_R], F_SETFD, 1) == -1 || fcntl(p2c[PIPE_W], F_SETFD, 1) == -1 || fcntl(p2c[PIPE_W], F_SETFL, O_NONBLOCK) == -1 || pipe(c2p) == -1 || fcntl(c2p[PIPE_R], F_SETFD, 1) == -1 || fcntl(c2p[PIPE_W], F_SETFD, 1) == -1 || fcntl(c2p[PIPE_R], F_SETFL, O_NONBLOCK) == -1) { msg(F_PROC, L_ERR, "proc_run: ERROR: Could not create pipes: " "%s!\n", strerror(errno)); goto pr_err; } /* Save our ends of the pipes for use by ring_read and ring_write */ p->rfd = c2p[PIPE_R]; p->wfd = p2c[PIPE_W]; /* Fork and run */ p->pid = fork(); switch(p->pid) { case -1: msg(F_PROC, L_ERR, "proc_run: ERROR: Could not fork: %s!\n", strerror(errno)); goto pr_err; case 0: /* Set standard input and standard output to the pipes */ if (dup2(p2c[PIPE_R], 0) == -1 || dup2(c2p[PIPE_W], 1) == -1) { msg(F_PROC, L_ERR, "proc_run: ERROR: Could not dup2: " "%s!\n", strerror(errno)); _exit(125); } /* Set the working directory */ if (p->cwd && chdir(p->cwd) == -1) { msg(F_PROC, L_ERR, "proc_run: ERROR: Could not chdir " "to '%s': %s!\n", p->cwd, strerror(errno)); _exit(126); } /* Run the child */ execve(p->argv[0], p->argv, p->envp); /* Apparently it failed */ msg(F_PROC, L_ERR, "proc_run: ERROR: Could not run %s: %s!\n", p->argv[0], strerror(errno)); _exit(127); } msg(F_PROC, L_DEBUG, "proc_start: started pid %d to run %s\n", p->pid, p->argv[0]); /* Close the other ends of the pipe in the parent */ close(c2p[PIPE_W]); close(p2c[PIPE_R]); /* Go to next state and return */ p->state = PRS_IDLE; p->timer = 0; return p->pid; pr_err: if (p2c[PIPE_R] != -1) close(p2c[PIPE_R]); if (p2c[PIPE_W] != -1) close(p2c[PIPE_W]); if (c2p[PIPE_R] != -1) close(c2p[PIPE_R]); if (c2p[PIPE_W] != -1) close(c2p[PIPE_W]); p->state = PRS_STARTING; p->timer = t + PRT_RETRYSTART; p->pid = 0; return -1; } /* End the process. If you don't want it to get restarted, stop calling proc_handle_end, basically. Continue calling proc_handle_timeout though, to send the SIGKILL if it doesn't respond to the SIGTERM. */ void proc_stop(PROC *p, time_t t) { /* is it safer to empty rings here in addition to proc_handle_end? */ if (p->pid) { msg(F_PROC, L_DEBUG, "proc_stop: killing %d\n", p->pid); kill(p->pid, SIGTERM); p->state = PRS_KILLING; p->timer = t + PRT_END; } } /* Test if at least one full binary message is available in the ring; returns * message size if yes, -1 if no, and -2 if a framing error is detected. */ ssize_t have_binmsg_inring(RING *r, U_INT32_T magic) { char binhdr[8]; ssize_t inl, expl; inl = ring_maxget(r); if (inl < sizeof(binhdr)) return -1; ring_peekdata(r, binhdr, sizeof(binhdr)); if (getord(binhdr, 4) != magic) return -2; expl = getord(binhdr + 4, 4); if (expl < 8 || expl > C_MAX_MSGSIZE) return -2; if (inl < expl) return -1; return expl; } /* Test if at least one ASCII line is available in the ring; returns line * length (without terminating LF) if yes, -1 if no. */ ssize_t have_ascline_inring(RING *r) { ssize_t inl, n; inl = ring_maxget(r); n = ring_strcspn(r, "\n", 1); if (n < inl) return n; return -1; } /* * Event handlers */ /* To be called when select indicates our read pipe is ready for reading. */ void proc_handle_read(PROC *p, time_t t) { ssize_t n; /* See if we got any space left at all. If not, then apparently the child is sending us a bigger message than the ring can hold, which is enough to give it the death penalty. There's no alternative; as we're out of sync at this point. */ if (!ring_maxput(p->r)) { msg(F_PROC, L_ERR, "proc_handle_read: ERROR: Message from %d " "does not fit - restarting subprocess!\n", p->argv[0]); proc_stop(p, t); return; } /* Read as much as we can. */ ring_read(p->r, p->rfd, &n); if (!n) { msg(F_PROC, L_ERR, "proc_handle_read: Warning: EOF from %d - " "killing it to be sure\n", p->pid); proc_stop(p, t); return; } msg(F_PROC, L_DEBUG, "proc_handle_read: Got %ld from pid %d, %ld now " "in ring.\n", n, p->pid, ring_maxget(p->r)); } void proc_handle_write(PROC *p, time_t t) { ssize_t xfered, left; /* Write as much as we can. */ if (ring_write(p->w, p->wfd, &xfered, 0) == RING_IOERR) { msg(F_PROC, L_ERR, "proc_handle_write: Warning: write error from %d - killing it to be sure\n", p->pid); proc_stop(p, t); return; } /* If we transfered anything at all, add RECEIVING to our state */ p->state |= PRS_RECEIVING; /* See if we got anything left to send, if not, remove SENDING */ left = ring_maxget(p->w); if (!left) p->state &= ~PRS_SENDING; msg(F_PROC, L_DEBUG, "proc_handle_write: Sent %ld to pid %d, %ld still " "in ring.\n", xfered, p->pid, left); } /* Handles this process' timer expiry */ void proc_handle_timeout(PROC *p, time_t t) { msg(F_PROC, L_NOTICE, "proc_handle_timeout: Pid %d timed out in state " "%d.\n", p->pid, p->state); switch(p->state) { case PRS_STARTING: /* retrying to start, */ proc_start(p, t); /* or restarting */ break; case PRS_SENDING: /* started sending */ case PRS_RECEIVING: /* started receiving */ case PRS_XFERING: /* still sending, also receiving */ proc_stop(p, t); break; case PRS_KILLING: /* gave SIGTERM */ kill(p->pid, SIGKILL); p->timer = t + PRT_END; break; default: msg(F_PROC, L_ERR, "proc_handle_timeout: BUG: spurious timer " "expiry for proc '%s' in state %d!\n", p->argv[0], p->state); } } /* Handles a SIGCHLD from this process */ void proc_handle_end(PROC *p, time_t t, int exitcode) { msg(F_PROC, L_NOTICE, "proc_handle_end: Child %d exited, return code " "%d (%d).\n", p->pid, exitcode>>8, exitcode&0xff); close(p->rfd); close(p->wfd); p->rfd = p->wfd = -1; ring_discard(p->r, 0); ring_discard(p->w, 0); p->pid = 0; p->state = PRS_STARTING; p->timer = t + PRT_RESTART; }