/* as-daemon.c: run a command as a daemon
 *
 ****************************************************************
 * Copyright (C) 2002  Tom Lord
 * 
 * See the file "COPYING" for further information about
 * the copyright and warranty status of this work.
 */


#include "config-options.h"
#include "hackerlab/cmd/main.h"
#include "hackerlab/os/errno.h"
#include "hackerlab/os/signal.h"
#include "hackerlab/os/fcntl.h"
#include "hackerlab/os/sys/types.h"
#include "hackerlab/os/sys/wait.h"
#include "shell-utils/log.h"

/* __STDC__ prototypes for static functions */
static void become_a_daemon (void);
static int open_anonymous_file (int flags, int mode);
static void send_notification_email (char * error_recipient,
				     char * error_subject,
				     int argc, char * command_argv[],
				     int exit_status,
				     int output_fd_1, int output_fd_2);
static void enter_critical (void);
static void enter_non_critical (void);
static void non_critical_handler ();
static void critical_handler ();
static void panic_handler ();



static t_uchar * program_name = "as-daemon";
static t_uchar * usage = "as-daemon [options] command ...";
static t_uchar * version_string = (cfg__std__package " from regexps.com\n"
				   "\n"
				   "Copyright 2002 Tom Lord\n"
				   "\n"
				   "This is free software; see the source for copying conditions.\n"
				   "There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n"
				   "PARTICULAR PURPOSE.\n"
				   "\n"
				   "Report bugs to " cfg__shell_utils_bug_mail ".\n"
				   "\n");

#define OPTS(OP, OP2) \
  OP (opt_help_msg, "h", "help", 0, \
      "Display a help message and exit.") \
  OP (opt_long_help, "H", 0, 0, \
      "Display a verbose help message and exit.") \
  OP (opt_version, "V", "version", 0, \
      "Display a release identifier string") \
  OP2 (opt_version, 0, 0, 0, "and exit.") \
  OP (opt_fg, 0, "fg", 0, \
      "Run in the foreground.") \
  OP (opt_log, "l", "log FILE", 1, \
      "Log significant events to FILE.\n") \
  OP (opt_null_input, 0, "null-input", 0, \
      "Redirect input from /dev/null.") \
  OP (opt_null_output, 0, "null-output", 0, \
      "Redirect stdout and stderr to /dev/null.") \
  OP (opt_null_stdout, 0, "null-stdout", 0, \
      "Redirect stdout to /dev/null.") \
  OP (opt_null_stderr, 0, "null-stderr", 0, \
      "Redirect output to /dev/null.") \
  OP (opt_mail_output, 0, "mail-output", 0, \
      "Combine stdout and stderr for email.") \
  OP (opt_mail_stdout, 0, "mail-stdout", 0, \
      "Redirect stdout for email.") \
  OP (opt_mail_stderr, 0, "mail-stderr", 0, \
      "Redirect stderr for email.") \
  OP (opt_mail_errors, 0, "mail-errors USER", 1, \
      "Send email if the command exits with") \
  OP2(opt_mail_errors, 0, 0, 0, \
      "non-0 status.") \
  OP (opt_mail_exit, 0, "mail USER", 1, \
      "Send email when the command exits.")

static t_uchar long_help[] = ("invoke a command as a daemon\n"
			      "Invoke \"COMMAND ...\" as a daemon.\n"
			      "\n"
			      "More specifically, COMMAND is invoked by or as an orphaned process, in\n"
			      "a new process group, with no controlling tty.\n"
			      "\n"
			      "Several of the options cause an orphaned \"as-daemon\"\n"
			      "process to persist and act as the parent process of COMMAND\n"
			      "in order to collect output and/or exit status.\n"
			      "\n"
			      "If a log file is specified (\"-l\" or \"--log\") a one line message is\n"
			      "appended to the log before COMMAND starts, and another message when\n"
			      "COMMAND exits.\n"
			      "\n"
			      "Other options control the disposition of standard input and output files\n");

enum options
{
  OPTS (OPT_ENUM, OPT_IGN)  
};

struct opt_desc opts[] = 
{
  OPTS (OPT_DESC, OPT_DESC)
    {-1, 0, 0, 0, 0}
};



static sigset_t all_signals;
static sigset_t inherited_signal_mask;
static sigset_t interesting_signal_mask;
static struct sigaction non_critical_action;
static struct sigaction critical_action;
static struct sigaction panic_action;

static char * log_file;
static int subproc;





int
main (int argc, char * argv[])
{
  int o;
  struct opt_parsed * option;
  char ** command_argv;
  int fg;

  int null_input;
  int null_stdout;
  int null_stderr;
  int mail_output;
  int mail_stdout;
  int mail_stderr;
  char * error_recipient;
  char * exit_recipient;
  char * error_subject;
  char * exit_subject;

  sigfillset (&all_signals);
  sigfillset (&interesting_signal_mask);
  sigdelset (&interesting_signal_mask, SIGHUP);
  sigdelset (&interesting_signal_mask, SIGINT);

  non_critical_action.sa_handler = non_critical_handler;
  non_critical_action.sa_mask = all_signals;

  critical_action.sa_handler = critical_handler;
  critical_action.sa_mask = all_signals;

  panic_action.sa_handler = panic_handler;
  panic_action.sa_mask = all_signals;

  safe_buffer_fd (1, 0, O_WRONLY, 0);

  fg = 0;
  log_file = 0;

  null_input = 0;
  null_stdout = 0;
  null_stderr = 0;

  mail_output = 0;
  mail_stdout = 0;
  mail_stderr = 0;

  error_recipient = 0;
  exit_recipient = 0;

  error_subject = "as-deamon process error report";
  exit_subject = "as-deamon process exit report";

  option = 0;

  while (1)
    {
      o = opt_standard (lim_use_must_malloc, &option, opts, &argc, argv, program_name, usage, version_string, long_help, opt_help_msg, opt_long_help, opt_version);
      if (o == opt_none)
	break;
      switch (o)
	{
	default:
	  safe_printfmt (2, "unhandled option `%s'\n", option->opt_string);
	  panic ("internal error parsing arguments");

	usage_error:
	  opt_usage (2, argv[0], program_name, usage, 1);
	  exit (1);

	/* bogus_arg: */
	  safe_printfmt (2, "ill-formed argument for `%s' (`%s')\n", option->opt_string, option->arg_string);
	  goto usage_error;

	case opt_fg:
	  fg = 1;
	  break;

	case opt_log:
	  log_file = str_save (0, option->arg_string);
	  break;

	case opt_null_input:
	  null_input = 1;
	  break;

	case opt_null_output:
	  null_stdout = 1;
	  null_stderr = 1;
	  mail_stderr = 0;
	  mail_stdout = 0;
	  mail_output = 0;
	  break;

	case opt_null_stdout:
	  null_stdout = 1;
	  mail_stdout = 0;
	  mail_output = 0;
	  break;

	case opt_null_stderr:
	  null_stderr = 1;
	  mail_stderr = 0;
	  mail_output = 0;
	  break;

	case opt_mail_output:
	  null_stdout = 0;
	  null_stderr = 0;
	  mail_stderr = 0;
	  mail_stdout = 0;
	  mail_output = 1;
	  break;

	case opt_mail_stdout:
	  null_stdout = 0;
	  mail_stdout = 1;
	  mail_output = 0;
	  break;

	case opt_mail_stderr:
	  null_stderr = 0;
	  mail_stderr = 1;
	  mail_output = 0;
	  break;

	case opt_mail_errors:
	  error_recipient = option->opt_string;
	  break;

	case opt_mail_exit:
	  exit_recipient = option->opt_string;
	  break;
	}
    }


  if (argc < 2)
    goto usage_error;

  command_argv = argv + 1;

  {
    char * dir_for_log;
    char * cmd_line_for_log;

    int null_fd;
    int stderr_fd;
    int stderr_reader_fd;
    int stdout_fd;
    int stdout_reader_fd;
    int combined_fd;
    int combined_reader_fd;

    null_fd = -1;
    stderr_fd = -1;
    stderr_reader_fd = -1;
    stdout_fd = -1;
    stdout_reader_fd = -1;
    combined_fd = -1;
    combined_reader_fd = -1;

    if (log_file)
      {
	size_t dir_size;
	int a;
	char * d;

	  
	dir_size = 4096;
	dir_for_log = lim_malloc (0, dir_size);

	while (!(d = getcwd (dir_for_log, dir_size)) && (errno == ERANGE) && (dir_size < (1 << 16)))
	  {
	    dir_size *= 2;
	    dir_for_log = lim_realloc (0, dir_for_log, dir_size);
	  }

	if (!d)
	  dir_for_log = "<no cwd>\n";
	  
	cmd_line_for_log = str_save (0, argv[1]);

	for (a = 2; a < argc; ++a)
	  {
	    cmd_line_for_log = str_alloc_cat (0, cmd_line_for_log, " ");
	    cmd_line_for_log = str_alloc_cat (0, cmd_line_for_log, argv[a]);
	  }
      }

    if (!fg)
      become_a_daemon ();

    if (null_stdout || null_stderr)
      {
	null_fd = safe_open ("/dev/null", O_WRONLY, 0);
      }

    if (mail_output)
      {
	combined_reader_fd = open_anonymous_file (O_RDWR, 0);
	combined_fd = safe_dup (combined_reader_fd);
	safe_buffer_fd (combined_reader_fd, 0, O_RDONLY, 0);
      }
    else 
      {
	if (mail_stderr)
	  {
	    stderr_reader_fd = open_anonymous_file (O_RDWR, 0);
	    stderr_fd = safe_dup (stderr_reader_fd);
	    safe_buffer_fd (stderr_reader_fd, 0, O_RDONLY, 0);
	  }

	if (mail_stdout)
	  {
	    stdout_reader_fd = open_anonymous_file (O_RDWR, 0);
	    stdout_fd = safe_dup (stdout_reader_fd);
	    safe_buffer_fd (stdout_reader_fd, 0, O_RDONLY, 0);
	  }
      }


    /* execute the command
     */
    {
      if (0 > sigprocmask (SIG_SETMASK, &all_signals, &inherited_signal_mask))
	{
	  panic ("sigprocmask");
	}

      subproc = fork ();

      if (subproc < 0)
	panic ("unable to fork");
      
      if (subproc == 0)
	{
	  if (0 > sigprocmask (SIG_SETMASK, &inherited_signal_mask, 0))
	    {
	      panic ("sigprocmask(2)");
	    }

	  {
	    /* perform redirections here
	     */

	    if (null_stdout)
	      {
		int fd;

		fd = safe_dup (null_fd);
		safe_move_fd (fd, 1);
	      }
	    else if (mail_output)
	      {
		int fd;
		fd = safe_dup (combined_fd);
		safe_move_fd (fd, 1);
		safe_move_fd (combined_fd, 2);
	      }
	    else if (mail_stdout)
	      {
		safe_move_fd (stdout_fd, 1);
	      }

	    if (null_stderr)
	      {
		int fd;

		fd = safe_dup (null_fd);
		safe_move_fd (fd, 1);
	      }
	    else if (mail_stderr)
	      {
		safe_move_fd (stderr_fd, 1);
	      }

	    if (null_fd > 0)
	      safe_close (null_fd);

	  }

	  /* exec the command
	   */
	  execvp (command_argv[0], command_argv);
	  safe_printfmt (2, "as-daemon: unable to exec %s\n", command_argv[0]);
	  exit (1);
	}
    }


    enter_non_critical ();
    
    if (0 > sigprocmask (SIG_SETMASK, &interesting_signal_mask, 0))
      {
	non_critical_handler();	/* should not return */
	panic ("sigprocmask(2)");
      }

    /* wait for the command
     */
    if (log_file)
      {
	enter_critical ();
	log_event (log_file, 0666, "as-daemon: start pid %d, cmd=%s, dir=%s\n", subproc, cmd_line_for_log, dir_for_log);
	enter_non_critical ();
      }

    {
      int status;
      int exit_status;
      int wait_pid;
      
      wait_pid = waitpid (subproc, &status, 0);

      if (wait_pid < 0)
	{
	  enter_critical ();
	  if (log_file)
	    log_event (log_file, 0666, "as-daemon: error in waitpid for pid %d\n", subproc);
	  kill (0, SIGKILL);
	  panic ("as-daemon: error waiting for subprocess");
	}

      if (wait_pid == 0)
	{
	  enter_critical ();
	  if (log_file)
	    log_event (log_file, 0666, "as-daemon: waitpid returned 0 for pid %d\n", subproc);
	  kill (0, SIGKILL);
	  panic ("as-daemon: received wait status for unrecognized process");
	}
      else if (!WIFEXITED (status))
	{
	  if (!WIFSIGNALED (status))
	    {
	      enter_critical ();
	      if (log_file)
		log_event (log_file, 0666, "as-daemon: waitpid returned for pid %d before it exitted!\n", subproc);
	      kill (0, SIGKILL);
	      panic ("as-daemon: waitpid returned for a non-exited process");
	    }
	  else
	    {
	      enter_critical ();
	      if (log_file)
		log_event (log_file, 0666, "as-daemon: pid %d killed by signal %d\n", subproc, WTERMSIG (status));
	      kill (0, SIGKILL);
	      panic ("as-daemon: subprocess killed by signal");
	    }
	}

      enter_critical ();
      exit_status = WEXITSTATUS (status);

      if (log_file)
	{
	  log_event (log_file, 0666, "as-daemon: pid %d exitted with status %d\n", subproc, exit_status);
	}

      /* send email
       */
      {
	if (error_recipient && exit_status)
	  {
	    int output_fd_1;
	    int output_fd_2;

	    output_fd_1 = -1;
	    output_fd_2 = -1;

	    if (mail_stdout)
	      {
		safe_lseek (stdout_reader_fd, 0, SEEK_SET);
		output_fd_1 = stdout_reader_fd;
	      }
	    if (mail_stderr)
	      {
		safe_lseek (stderr_reader_fd, 0, SEEK_SET);
		output_fd_2 = stderr_reader_fd;
	      }
	    if (mail_output)
	      {
		safe_lseek (combined_reader_fd, 0, SEEK_SET);
		output_fd_1 = combined_reader_fd;
	      }

	    send_notification_email (error_recipient, error_subject, argc, command_argv, exit_status, output_fd_1, output_fd_2);
	  }

	if (exit_recipient)
	  {
	    int output_fd_1;
	    int output_fd_2;

	    output_fd_1 = -1;
	    output_fd_2 = -1;

	    if (mail_stdout)
	      {
		safe_lseek (stdout_reader_fd, 0, SEEK_SET);
		output_fd_1 = stdout_reader_fd;
	      }
	    if (mail_stderr)
	      {
		safe_lseek (stderr_reader_fd, 0, SEEK_SET);
		output_fd_2 = stderr_reader_fd;
	      }
	    if (mail_output)
	      {
		safe_lseek (combined_reader_fd, 0, SEEK_SET);
		output_fd_1 = combined_reader_fd;
	      }

	    send_notification_email (exit_recipient, exit_subject, argc, command_argv, exit_status, output_fd_1, output_fd_2);
	  }
      }
    }

  }

  kill (0, SIGKILL);
  exit (0);
}



static void
become_a_daemon (void)
{
  int pid;

  pid = fork ();

  if (pid < 0)
    panic ("as-daemon: unable to fork");

  if (pid)
    {
      int p;
      int status;

      p = waitpid (pid, &status, WUNTRACED);
      if ((p != pid) || !WIFSTOPPED (status))
	{
	  if (!WIFEXITED (status) && !WIFSIGNALED (status))
	    kill (pid, SIGKILL);
	  panic ("as-daemon: subprocess failed");
	}
      if (0 > kill (pid, SIGCONT))
	{
	  panic ("as-daemon: couldn't SIGCONT subprocess");
	}
      exit (0);
    }

  if (setsid () < 0)
    {
      safe_printfmt (2, "as-daemon: setsid failure (%s)\n", errno_to_string (errno));
      exit (1);
    }

  if (0 > kill (getpid (), SIGSTOP))
    panic ("as-daemon: subprocess couldn't SIGSTOP itself");
}

static int
open_anonymous_file (int flags, int mode)
{
  panic ("as-daemon: email output not supported yet");
  return 0;
}

static void
send_notification_email (char * error_recipient,
			 char * error_subject,
			 int argc, char * command_argv[],
			 int exit_status,
			 int output_fd_1, int output_fd_2)
{
  panic ("as-daemon: email output not supported yet");
}




static void
enter_critical (void)
{
  sigaction (SIGHUP, &critical_action, 0);
  sigaction (SIGINT, &critical_action, 0);
}


static void
enter_non_critical (void)
{
  sigaction (SIGHUP, &non_critical_action, 0);
  sigaction (SIGINT, &non_critical_action, 0);
}


static void
non_critical_handler ()
{
  enter_critical ();

  if (0 > sigprocmask (SIG_SETMASK, &interesting_signal_mask, 0))
    {
      critical_handler();	/* should not return */
      panic ("sigprocmask(2)");
    }
  
  if (log_file)
    {
      log_event (log_file, 0666, "as-daemon: as-daemon killed by signal for pid %d\n", subproc);
    }

  kill (0, SIGKILL);		/* should not return */
  _exit (127);
}

static void
critical_handler ()
{
  kill (0, SIGKILL);		/* should not return */
  _exit (127);
}

static void
panic_handler ()
{
  kill (getpid (), SIGKILL);
  _exit (127);
}


/* tag: Tom Lord Sun Jan 20 15:40:31 2002 (as-daemon.c)
 */
