/*
 * nbexec.c  -  Fork and execute a program
 *
 * Copyright (C) 2004-2007 Gero Kuhlmann <gero@gkminix.han.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: nbexec.c,v 1.7 2007/02/01 12:09:24 gkminix Exp $
 */

#define NEED_TIME
#include <common.h>
#include <nblib.h>
#include "privlib.h"



/*
 * Define subprocess status macros
 */
#ifdef HAVE_SYS_WAIT_H
# include <sys/wait.h>
#endif
#ifndef WEXITSTATUS
# define WEXITSTATUS(stat_val)	((unsigned int)(stat_val) >> 8)
#endif
#ifndef WIFEXITED
# define WIFEXITED(stat_val)	(((stat_val) & 255) == 0)
#endif



/*
 * select/poll system call
 */
#ifdef HAVE_SYS_SELECT_H
# include <sys/select.h>
#else
# ifdef HAVE_SYS_POLL_H
#  include <sys/poll.h>
# endif
#endif



/*
 * Number of seconds to wait for select / poll
 */
#define SELECT_TIMEOUT	1



/*
 * Read line from pipe file descriptor
 */
static void readline __F((fd, loglevel), int fd AND int loglevel)
{
  char linebuf[256];
  char *cp;
  size_t rdsize;
  unsigned int i;
  int neednl = FALSE;

  do {
	if ((rdsize = read(fd, linebuf, 255)) > 0) {
		linebuf[rdsize] = '\0';
		for (i = 0, cp = linebuf; i < rdsize; i++)
			if (linebuf[i] == '\n') {
				linebuf[i] = '\0';
				prnlog(loglevel, "%s\n", cp);
				cp = &(linebuf[i + 1]);
				neednl = FALSE;
			}
		if (cp < &(linebuf[rdsize])) {
			prnlog(loglevel, "%s", cp);
			neednl = TRUE;
		}
	}
  } while (rdsize == 255);
  if (neednl)
	prnlog(loglevel, "\n");
}



/*
 * Check if we have any work on a read file descriptor
 */
static int checkread __F((fd1, fd2), int fd1 AND int fd2)
{
  int retval = 0;

#ifdef HAVE_SELECT
  fd_set rfds;
  struct timeval tv;

  /* Use select() to determine if we have anything to read */
  FD_ZERO(&rfds);
  FD_SET(fd1, &rfds);
  FD_SET(fd2, &rfds);
  tv.tv_sec = SELECT_TIMEOUT;
  tv.tv_usec = 0;
  if (select(MAX(fd1, fd2) + 1, &rfds, NULL, NULL, &tv) > 0) {
	if (FD_ISSET(fd1, &rfds))
		retval |= 1;
	if (FD_ISSET(fd2, &rfds))
		retval |= 2;
  }

#else
# ifdef HAVE_POLL
  struct pollfd fds[2];

  /* Use poll() to determine if we have anything to read */
  fds[0].fd = fd1;
  fds[0].events = POLLIN;
  fds[0].revents = 0;
  fds[1].fd = fd2;
  fds[1].events = POLLIN;
  fds[1].revents = 0;
  if (poll(fds, 2, SELECT_TIMEOUT * 1000) > 0) {
	if (fds[0].revents == POLLIN)
		retval |= 1;
	if (fds[1].revents == POLLIN)
		retval |= 2;
  }

# endif
#endif

  return(retval);
}



/*
 * Fork and execute a program. This routine waits until the called program
 * terminates, and then returns the exit code.
 */
int nbexec __F((progfile, args), char *progfile AND char **args)
{
  int stdoutpipe[2];
  int stderrpipe[2];
  int retval, rdstat, waitstat;
  pid_t pid, checkpid;
  unsigned int i;
  char *fname;

  /* Check that we have anything to do at all */
  if (progfile == NULL)
	return(0);

  /* Check that the program is executable */
  fname = NULL;
  copystr(&fname, progfile);
  checkaccess(&fname, nblibdir, ACCESS_FILE_READ);
  if (fname == NULL) {
	nberror(EXIT_EXEC, "file '%s' is not executable", progfile);
	return(-1);
  }

  /* Now let the user know what we are going to do */
  prnlog(LOGLEVEL_DEBUG, "Executing: %s ", fname);
  for (i = 0; args[i] != NULL; i++)
	prnlog(LOGLEVEL_DEBUG, "%s ", args[i]);
  prnlog(LOGLEVEL_DEBUG, "\n");

  /* Create two pipes to receive stdout and stderr of the called program */
  if (pipe(stdoutpipe) != 0) {
	nberror(EXIT_EXEC, "unable to create stdout pipe");
	free(fname);
	return(-1);
  }
  if (pipe(stderrpipe) != 0) {
	nberror(EXIT_EXEC, "unable to create stderr pipe");
	(void)close(stdoutpipe[0]);
	(void)close(stdoutpipe[1]);
	free(fname);
	return(-1);
  }

  /* Reset SIGCHLD counter */
  nblib_sigchld = 0;

  /* Now do the fork */
  if ((pid = fork()) == -1) {
	nberror(EXIT_EXEC, "unable to fork");
	(void)close(stdoutpipe[0]);
	(void)close(stdoutpipe[1]);
	(void)close(stderrpipe[0]);
	(void)close(stderrpipe[1]);
	free(fname);
	return(-1);
  }

  /* Handle the child code */
  if (pid == 0) {
	int stdinhandle;

	/* Terminate any signal handling */
	nblib_restore_signal();

	/* Cleanup any open file handles */
	nblib_exit_cleanup();

	/* Redirect stdin to /dev/null */
	if ((stdinhandle = open("/dev/null", O_RDONLY)) != -1) {
		(void)close(STDIN_FILENO);
		(void)dup(stdinhandle);
		(void)close(stdinhandle);
	}

	/* Redirect stdout to pipe */
	(void)close(STDOUT_FILENO);
	(void)dup(stdoutpipe[1]);
	(void)close(stdoutpipe[0]);
	(void)close(stdoutpipe[1]);

	/* Redirect stderr to pipe */
	(void)close(STDERR_FILENO);
	(void)dup(stderrpipe[1]);
	(void)close(stderrpipe[0]);
	(void)close(stderrpipe[1]);

	/* Execute the program */
	(void)execv(fname, args);

	/* Oops, unable to execute program */
	fprintf(stderr, "%s: unable to execute '%s'\n", progname, fname);
	free(fname);
	nbexit(1);
  }

  /*
   * This is the code executed by the parent - it waits for the client to
   * terminate, and copies all output of the client into the log file.
   */

  /* We don't need the file name anymore */
  free(fname);
  fname = NULL;

  /* Close all unneeded pipe write handles */
  (void)close(stdoutpipe[1]);
  (void)close(stderrpipe[1]);

  /* Wait until the sub-process terminates */
  checkpid = 0;
  waitstat = -1;
  while (checkpid != pid) {

	/* Check for any output from sub-process */
	if ((rdstat = checkread(stdoutpipe[0], stderrpipe[0])) > 0) {
		if (rdstat & 1)
			readline(stdoutpipe[0], LOGLEVEL_DEBUG);
		if (rdstat & 2)
			readline(stderrpipe[0], LOGLEVEL_NOTICE);
	}

	/* Check if child process terminated */
	if (nblib_sigchld > 0) {
		nblib_sigchld = 0;
#ifdef HAVE_WAITPID
		if ((checkpid = waitpid(-1, &waitstat, WNOHANG)) < 0) {
#else
# ifdef HAVE_WAIT3
		if ((checkpid = wait3(&waitstat, WNOHANG, NULL)) < 0) {
# else
		/* No chance but to use wait() - let's hope it doesn't hang */
		if ((checkpid = wait(&waitstat)) < 0) {
# endif
#endif
			nberror(EXIT_EXEC, "unable to read child status");
			(void)close(stdoutpipe[0]);
			(void)close(stderrpipe[0]);
			return(-1);
		}
	}
  }

  /* Determine return value */
  if (WIFEXITED(waitstat))
	retval = (int)(WEXITSTATUS(waitstat));
  else
	retval = 1;

  /* Close any pipe file descriptors */
  (void)close(stdoutpipe[0]);
  (void)close(stderrpipe[0]);

  /* Let the user know */
  prnlog(LOGLEVEL_DEBUG, "Child exited with status %d\n", retval);
  return(retval);
}

