/*
 * 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 <sys/types.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

#include <metaops.h>		/* for AVMSG_ASCII */
#include <subprocs.h>

#define DEBUGLEVEL 3
#include <debug.h>


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



syntax highlighted by Code2HTML, v. 0.9.1