/* This file is part of GNU Pies.
   Copyright (C) 2013-2019 Sergey Poznyakoff

   GNU Pies 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 3, or (at your option)
   any later version.

   GNU Pies 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 GNU Pies.  If not, see <http://www.gnu.org/licenses/>. */

#include "pies.h"
#include "prog.h"
#include <sys/ioctl.h>
#include <termios.h>

enum boot_state
  {
    sysinit,
    boot,
    single,
    normal,
  };

static char *boot_state_name[] = {
  [sysinit] = "sysinit",
  [boot]    = "boot",
  [single]  = "single",
  [normal]  = "normal"
};

#define max_boot_state sizeof(boot_state_name) / sizeof (boot_state_name[0])


static char boot_state_str[] = "#*s ";

static const char valid_runlevels[] = "0123456789S";

static int boot_trans_tab[max_boot_state][sizeof(valid_runlevels)-1]  = {
  /*                    0,       1,       2,       3,       4,             */
  /*                         5,       6,       7,       8,       9,      S */
  /* sysinit */ {    boot,    boot,    boot,    boot,    boot,
                          boot,    boot,    boot,    boot,    boot, single },
  /* boot    */ {  normal,  normal,  normal,  normal,  normal,
                        normal,  normal,  normal,  normal,  normal, normal },
  /* single  */ {    boot,    boot,    boot,    boot,    boot,
                          boot,    boot,    boot,    boot,    boot, single },
  /* normal  */ {  normal,  normal,  normal,  normal,  normal,
                        normal,  normal,  normal,  normal,  normal, normal },
};

enum boot_state boot_state;
int runlevel = 0;
int prevlevel = 'N';

int initdefault;               /* Default runlevel */
int dfl_level;
char *emergency_shell = EMERGENCY_SHELL;
int emergency;

static int inittrans (void);

int
console_open (int mode)
{
  int i, fd;

  for (i = 0; i < 5; i++)
    {
      fd = open (console_device, mode | O_NONBLOCK);
      if (fd >= 0)
        {
          fcntl (fd, F_SETFL, mode);
          return fd;
        }
    }
  return -1;
}

#ifndef CBAUDEX
# define CBAUDEX 0
#endif

void
console_stty (void)
{
  struct termios tty;
  int fd;

  if ((fd = console_open (O_RDWR|O_NOCTTY)) < 0)
    {
      logmsg (LOG_CRIT, "can't open %s", console_device);
      return;
    }

  tcgetattr (fd, &tty);

  tty.c_cflag &= CBAUD|CBAUDEX|CSIZE|CSTOPB|PARENB|PARODD;
  tty.c_cflag |= HUPCL|CLOCAL|CREAD;

  tty.c_cc[VINTR]  = 3;   /* ctrl('c') */
  tty.c_cc[VQUIT]  = 28;  /* ctrl('\\') */
  tty.c_cc[VERASE] = 127;
  tty.c_cc[VKILL]  = 24;  /* ctrl('x') */
  tty.c_cc[VEOF]   = 4;   /* ctrl('d') */
  tty.c_cc[VTIME]  = 0;
  tty.c_cc[VMIN]   = 1;
  tty.c_cc[VSTART] = 17;  /* ctrl('q') */
  tty.c_cc[VSTOP]  = 19;  /* ctrl('s') */
  tty.c_cc[VSUSP]  = 26;  /* ctrl('z') */

  /*
   *    Set pre and post processing
   */
  tty.c_iflag = IGNPAR|ICRNL|IXON|IXANY;
  tty.c_oflag = OPOST|ONLCR;
  tty.c_lflag = ISIG|ICANON|ECHO|ECHOCTL|ECHOPRT|ECHOKE;

  /*
   *    Now set the terminal line.
   *    We don't care about non-transmitted output data
   *    and non-read input data.
   */
  tcsetattr (fd, TCSANOW, &tty);
  tcflush(fd, TCIOFLUSH);
  close (fd);
}

int
askrunlevel (void)
{
  int fd;
  char c, lvl = -1;
  enum { srl_init, srl_char, srl_r, srl_n, srl_skip } srl = srl_init;
  static const char prompt[] = "\nEnter runlevel: ";
  
  console_stty ();
  fd = console_open (O_RDWR|O_NOCTTY);
  if (fd == -1)
    return 'S';
  
  while (1)
    {
      if (srl == srl_init)
        {
          write (fd, prompt, sizeof (prompt) - 1);
          if (read (fd, &lvl, 1) != 1)
            return 'S';
          srl = srl_char;
        }
      else if (srl == srl_n)
        {
          if (is_valid_runlevel (lvl))
            break;
          srl = srl_init;
        }
      else
        {
          if (read (fd, &c, 1) != 1)
            {
              if (!is_valid_runlevel (lvl))
                lvl = 'S';
              break;
            }
          
          switch (srl)
            {
            case srl_char:
              if (c == '\r')
                srl = srl_r;
              else if (c == '\n')
                srl = srl_n;
              else
                srl = srl_skip;
              break;

            case srl_r:
              if (c == '\n')
                srl = srl_n;
              else
                srl = srl_skip;
              break;

            case srl_skip:
              if (c == '\n')
                srl = srl_init;
	    default:
	      break;
            }
        }
    }
  close (fd);
  return toupper (lvl);
}

static int
getinitdefault (void)
{
  return initdefault ? initdefault : askrunlevel ();
}

static int
runlevel_index (int n)
{
  char *p;

  if (n == 0)
    return -1;
  
  p = strchr (valid_runlevels, n);
  if (!p)
    return -1;
  return p - valid_runlevels;
}

struct enstate
{
  int mask;
};

static int
enablecomp (struct prog *prog, void *data)
{
  struct enstate *s = data;
  struct component *comp = prog->v.p.comp;

  switch (boot_state)
    {
    case sysinit:
      return comp->mode == pies_comp_sysinit;
          
    case boot:
      return comp->mode == pies_comp_boot || comp->mode == pies_comp_bootwait;
          
    case single:
    case normal:
      switch (comp->mode)
        {
        case pies_comp_sysinit:
        case pies_comp_boot:
        case pies_comp_bootwait:
          return 0;

	case pies_comp_ondemand:
	  /* Active flag persists: */
	  return prog->active;
	  
        case pies_comp_powerfail:
        case pies_comp_powerwait:
        case pies_comp_powerokwait:
        case pies_comp_ctrlaltdel:
        case pies_comp_powerfailnow:
        case pies_comp_kbrequest:
	  return s && (s->mask & PIES_COMP_MASK (comp->mode));

	default:
	  break;
        } 
    }

  if (!comp->runlevels)
    return -1;
  
  if (!strchr (comp->runlevels, runlevel))
    {
      if (prog->v.p.status == status_finished && comp->mode != pies_comp_once)
	prog->v.p.status = status_stopped;
      return 0;
    }
  
  if (prog->v.p.status == status_finished)
    return -1;
  
  return 1;
}

static int
runlevel_setup_prog (struct prog *prog, void *data)
{
  if (IS_COMPONENT (prog)
      && !(prog->v.p.comp->flags & CF_DISABLED)
      && is_sysvinit (prog->v.p.comp))
    {
      int rc = enablecomp (prog, data);
      if (rc < 0 || prog->active == rc)
        return 0;
      prog->active = rc;
      if (rc)
	prog->wait = is_comp_wait (prog->v.p.comp);
      else if (prog->v.p.status != status_stopped
	       && prog->v.p.status != status_finished)
	prog->stop = 1;
      debug (2, ("%s: %s%s", prog_tag (prog),
                 prog->active ?
                 "enabled" : "disabled",
		 prog->active && prog->wait ? " (wait)" : ""));
    }
  return 0;
}

void
sysvinit_runlevel_setup (int mask)
{
  struct enstate s;
  s.mask = mask;
  progman_foreach (runlevel_setup_prog, &s);
  pies_schedule_children (PIES_CHLD_GC);
}

static int
demand_prog (struct prog *prog, void *data)
{
  int *rl = data;
  struct component *comp = prog->v.p.comp;
  if (comp->mode == pies_comp_ondemand
      && comp->runlevels
      && strchr (comp->runlevels, *rl))
    {
      prog->active = 1;
      debug (1, ("%s: %s", prog_tag (prog), "enabled"));
    }
  return 0;
}
      
static void
sysvinit_demand (int rl)
{
  progman_foreach (demand_prog, &rl);
}

static const char valid_runlevel_arg[] = "0123456789SsQqAaBbCcUu";

int
is_valid_runlevel (int c)
{
  return !!strchr (valid_runlevel_arg, c);
}

#define ENVAR_CONSOLE "CONSOLE="
#define ENVTMPL_CONSOLE "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

static char env_prevlevel[] = "PREVLEVEL=x";
static char env_runlevel[] = "RUNLEVEL=x";
static char env_console[] = ENVAR_CONSOLE ENVTMPL_CONSOLE;

#define NR_ENVHINT 32

char *sysvinit_environ_hint[NR_ENVHINT+1] = {
  env_prevlevel,
  env_runlevel,
  env_console,
  "INIT_VERSION=" PACKAGE_STRING,
  "PATH=/bin:/usr/bin:/sbin:/usr/sbin",
  NULL
};

#define ENVI_PREVLEVEL 0
#define ENVI_RUNLEVEL  1
#define ENVI_CONSOLE   2
#define ENVI_VERSION   3
#define ENVI_PATH      4
#define ENVI_AVAIL     5

static void
envsetup (void)
{
  int i;

  for (i = 0; i < ENVI_AVAIL; i++)
    {
      char *str = sysvinit_environ_hint[i];
      switch (i)
        {
        case ENVI_PREVLEVEL:
          str[strlen (str) - 1] = prevlevel;
          break;

        case ENVI_RUNLEVEL:
          str[strlen (str) - 1] =
            boot_state_str[boot_state] == ' ' ?
              (runlevel ? runlevel : '#') : boot_state_str[boot_state];
          break;

        case ENVI_CONSOLE:
          if (strlen (console_device) >= sizeof (ENVTMPL_CONSOLE))
            logmsg (LOG_ERR, "console device name too long");
          else
            strcpy (str + sizeof (ENVAR_CONSOLE) - 1, console_device);
        }
    }
}

int
sysvinit_envlocate (char const *name, char **value)
{
  int i, j;
  char *var;

  for (i = 0; (var = sysvinit_environ_hint[i]); i++)
    {
      for (j = 0; var[j] && name[j] == var[j]; j++)
	;
      if (!name[j] && var[j] == '=')
	{
	  if (value)
	    *value = var + j + 1;
	  return i;
	}
    }
  return -1;
}
	  
int
sysvinit_envupdate (char const *var)
{
  int i, j;
  size_t len;
  
  len = strcspn (var, "=");
  for (i = 0; i < NR_ENVHINT; i++)
    {
      char *s = sysvinit_environ_hint[i];
      if (s)
	{
	  for (j = 0; *s && j < len; j++, s++)
	    if (var[j] != *s) break;
	  if (*s != '=' || j != len)
	    continue;
	  if (i < ENVI_AVAIL)
	    return 1;
	  free (sysvinit_environ_hint[i]);
	}

      if (var[len] == '=')
	sysvinit_environ_hint[i] = grecs_strdup (var);
      else
	while ((sysvinit_environ_hint[i] = sysvinit_environ_hint[i+1]))
	  ++i;
      return 0;
    }
  return -1;
}

static void
sysvinit_setenv (char const *data, int size)
{
  while (size)
    {
      char const *var = data;
      size_t len = strlen (var) + 1;
      size -= len;
      if (size < 0)
	break;
      data += len;
      sysvinit_envupdate (var);
    }
}

int
sysvinit_set_runlevel (int newlevel)
{
  switch (newlevel)
    {
    case 'A':
    case 'B':
    case 'C':
      diagmsg (DIAG_ALL, LOG_INFO,
	       "Activating on-demand level '%c'", newlevel);
      sysvinit_demand (newlevel);
      break;
			  
    default:
      if (newlevel == runlevel)
	break;
      else if (runlevel_index (newlevel) == -1)
	return -1;
      else
	{
	  diagmsg (DIAG_ALL, LOG_INFO, "Switching to runlevel: %c", newlevel);
	  dfl_level = newlevel;
	  pies_schedule_children (PIES_CHLD_WAKEUP);
	}
    }
  return 0;
}


char *init_fifo = INIT_FIFO;

static void create_fifo (void);
static void powerfailcmd (int power_stat);

static int
sysvinit_fifo_handler (int fd, void *data)
{
  static size_t size;
  union
  {
    struct sysvinit_request req;
    char data[sizeof (struct sysvinit_request)];
  } buf;
  int rc;

  rc = read (fd, buf.data + size, sizeof (struct sysvinit_request) - size);
  if (rc == -1)
    {
      logmsg (LOG_ERR, _("error reading from %s: %s"), init_fifo,
              strerror (errno));
      size = 0;
      return 0;
    }
  if (rc == 0)
    {
      logmsg (LOG_ERR, _("end of file on %s: reopening"), init_fifo);
      size = 0;
      deregister_socket (fd);
      create_fifo ();
      return 0;
    }

  size += rc;

  if (size == sizeof (struct sysvinit_request))
    {
      if (buf.req.magic != INIT_MAGIC)
        logmsg (LOG_ERR, _("got invalid initreq"));
      else
        {
          debug (1, ("INITREQ: cmd=%d, runlevel=%d, sleeptime=%d",
                     buf.req.cmd, buf.req.runlevel, buf.req.sleeptime));
	  if (buf.req.sleeptime > 0)
	    shutdown_timeout = buf.req.sleeptime;
          switch (buf.req.cmd)
            {
            case INIT_CMD_RUNLVL:
              buf.req.runlevel = toupper (buf.req.runlevel);
	      switch (buf.req.runlevel)
		{
		case 'Q':	
		  pies_schedule_action (ACTION_RELOAD);
		  break;

		default:
		  sysvinit_set_runlevel (buf.req.runlevel);
		}
	      break;

	    case INIT_CMD_SETENV:
	      sysvinit_setenv (buf.req.data, sizeof (buf.req.data));
	      break;
	      
	    case INIT_CMD_POWERFAIL:
	      powerfailcmd (POWER_STAT_FAIL);
	      break;
	      
	    case INIT_CMD_POWERFAILNOW:
	      powerfailcmd (POWER_STAT_LOW);
	      break;
	      
	    case INIT_CMD_POWEROK:
	      powerfailcmd (POWER_STAT_OK);
	      break;

              /* FIXME: react on other commands */
            }
        }
      size = 0;
    }
  return 0;
}

static void
create_fifo (void)
{
  static int fd = -1;
  struct stat st, fst;

  if (stat (init_fifo, &st) < 0)
    {
      if (errno != ENOENT)
        {
          logmsg (LOG_ERR, "cannot stat fifo %s: %s", init_fifo,
                  strerror (errno));
          return;
        }
      else if (mkfifo (init_fifo, 0600))
        {
          logmsg (LOG_ERR, "cannot create fifo %s: %s", init_fifo,
                  strerror (errno));
          return;
        }
    }
  else
    {
      if (!S_ISFIFO (st.st_mode))
        {
          logmsg (LOG_ERR, "not a fifo: %s", init_fifo);
          return;
        }

      chmod (init_fifo, 0600);
    }

  if (fd != -1)
    {
      fstat (fd, &fst);
      if (fst.st_dev != st.st_dev || fst.st_ino != st.st_ino)
        deregister_socket (fd);
      debug (1, ("reopening %s", init_fifo));
    }
  
  /* Opening the socket in read-write mode ensures we won't get EOF
     on it when the caller party closes connection (at least on Linux).
     Nevertheless, the svinit_fifo_handler is prepared for that eventuality,
     too. */
  fd = open (init_fifo, O_RDWR|O_NONBLOCK);
  if (fd == -1)
    {
      logmsg (LOG_ERR, "cannot open %s: %s", init_fifo,
              strerror (errno));
      return;
    }
  register_socket (fd, sysvinit_fifo_handler, NULL, NULL, NULL);
}

static char *try_console[] = { NULL, "/dev/console", "/dev/tty0" };
char *console_device;

static void
set_console_dev (void)
{
  int i;
  for (i = 0; i < ARRAY_SIZE (try_console); i++)
    {
      if (try_console[i])
        {
          int fd = open (try_console[i], O_RDONLY|O_NONBLOCK);

          if (fd >= 0)
            {
              close (fd);
              console_device = try_console[i];
              return;
            }
        }
    }
  /* provide default */
  console_device = "/dev/null";
}

int
sysvinit_sigtrans (int sig, int *pact)
{
  switch (sig)
    {
    case SIGINT:
      *pact = ACTION_CTRLALTDEL;
      break;
    case SIGWINCH:
      *pact = ACTION_KBREQUEST;
      break;
    case SIGTERM:
    case SIGQUIT:
      /* Ignore these signals. */
      *pact = ACTION_CONT;
      break;
    case SIGPWR:
      *pact = ACTION_POWER;
      break;
    default:
      return 0;
    }
  return 1;
}

static void
unintr_sleep (unsigned n)
{
  struct timeval tv;

  tv.tv_sec = n;
  tv.tv_usec = 0;

  while (select (0, NULL, NULL, NULL, &tv) < 0 && errno == EINTR)
    ;
}

static void
start_shell (const char *shell)
{
  pid_t pid, rc;
  int st;
  struct sigaction act, old;

  act.sa_flags = SA_RESTART;
  act.sa_handler = SIG_DFL;
  sigemptyset (&act.sa_mask);
  sigaction (SIGCHLD, &act, &old);

  pid = fork ();
  if (pid == -1)
    {
      logmsg (LOG_CRIT, "cannot run `%s': fork failed: %s",
	      shell, strerror (errno));
      exit (1);
    }

  if (pid == 0)
    {
      int fd;

      signal_setup (SIG_DFL);
      setsid ();
      
      fd = console_open (O_RDWR|O_NOCTTY);
      if (fd < 0) 
	{
	  logmsg (LOG_CRIT, "open(%s): %s",
		  console_device, strerror (errno));
	  exit (1);
	}
      ioctl (fd, TIOCSCTTY, 1);
      dup (fd);
      dup (fd);

      execl (shell, shell, NULL);
      _exit (127);
    }
  
  while ((rc = wait (&st)) != pid)
    if (rc == (pid_t) -1 && errno == ECHILD)
      break;
  
  logmsg (LOG_CRIT, "shell finished");
  sigaction (SIGCHLD, &old, NULL);
}

/* Memory allocation functions for INIT.  They may not fail, therefore
   they just retry allocation until it eventually succeeds.
*/

static void *
sysvinit_malloc (size_t size)
{
  void *p;

  while (!(p = malloc (size)))
    {
      logmsg (LOG_ERR, _("out of memory"));
      unintr_sleep (5);
    }
  return p;
}

static void *
sysvinit_realloc (void *ptr, size_t size)
{
  void *p;

  while (!(p = realloc (ptr, size)))
    {
      logmsg (LOG_ERR, _("out of memory"));
      unintr_sleep (5);
    }
  return p;
}

static void
save_argv (void)
{
  int i;
  char **av;
  instance = grecs_strdup (instance);
  av = grecs_calloc (pies_master_argc + 1, sizeof (av[0]));
  for (i = 0; i < pies_master_argc; i++)
    av[i] = grecs_strdup (pies_master_argv[i]);
  av[i] = NULL;
  pies_master_argv = av;
}

void
sysvinit_begin (void)
{
  int sigv[] = {
    SIGINT,
    SIGPWR,
    SIGWINCH,
  };

  grecs_malloc_fun  = sysvinit_malloc;
  grecs_realloc_fun = sysvinit_realloc;
  
  close (0);
  close (1);
  close (2);
  set_console_dev ();
  console_stty ();
  setsid ();
  envsetup ();
  sysvinit_runlevel_setup (PIES_COMP_DEFAULT);
  add_extra_sigv (sigv, ARRAY_SIZE (sigv));
  sysvinit_sysdep_begin ();
  save_argv ();
  pies_set_hook (inittrans);
  if (emergency)
    start_shell (emergency_shell);
}

#define IS_RUNNING_DISABLED_PROG(prog)		       \
  (IS_COMPONENT (prog)				       \
   && prog->v.p.status == status_running	       \
   && !prog->active)

int
inittrans (void)
{
  int n;
  int newlevel = 0;
  enum boot_state newstate;
  int trans = 0;
  int userchg = 0;
  
  if (progman_waiting_p ())
    /* Noting to do if there are processes left in the previous runlevel */
    return 0;
  
  if (runlevel == 0)
    n = runlevel_index (dfl_level ? dfl_level : getinitdefault ());
  else
    n = runlevel_index (runlevel);
  if (n == -1)
    n = runlevel_index ('S');
  
  newstate = boot_trans_tab[boot_state][n];
  if (newstate != boot_state)
    {
      debug (1, ("STATE TRANS: %s -> %s", boot_state_name[boot_state],
                 boot_state_name[newstate]));
      boot_state = newstate;
      trans = 1;
    }
  
  switch (boot_state)
    {
    case sysinit:
      break;
    case boot:
      sysvinit_acct (SYSV_ACCT_BOOT, "reboot", "~~", 0, "~");
      break;
    case single:
      newlevel = 'S';
      break;
    case normal:
      newlevel = dfl_level ? dfl_level : getinitdefault ();
      if (trans)
	{
	  /* boot -> normal */
	  create_fifo ();
	  ctl_open ();
	  diag_setup (DIAG_TO_SYSLOG | DIAG_REOPEN_LOG);
	}
      else
	userchg = 1;
    }
  if (newlevel && newlevel != runlevel)
    {
      prevlevel = runlevel ? runlevel : 'N';
      debug (1, ("RL TRANS: %c -> %c", prevlevel, newlevel));
      sysvinit_acct (SYSV_ACCT_RUNLEVEL, "runlevel", "~~",
                     newlevel + 256 * runlevel, "~");
      mf_proctitle_format ("init [%c]", newlevel);
      runlevel = newlevel;
      trans = 1;
    }

  if (trans)
    {
      envsetup ();
      
      if (userchg)
	pies_schedule_action (ACTION_RELOAD);
      else
	{
	  sysvinit_runlevel_setup (PIES_COMP_DEFAULT);
	  pies_schedule_children (PIES_CHLD_WAKEUP);
	}
    }
  return trans;
}

/* Return true if the progman should wait for the component to
   terminate. */
int
is_comp_wait (struct component *comp)
{
  switch (comp->mode)
    {
    case pies_comp_sysinit:
    case pies_comp_bootwait:
    case pies_comp_wait:
    case pies_comp_powerwait:
    case pies_comp_powerfailnow:
    case pies_comp_powerokwait:
    case pies_comp_ctrlaltdel:
      return 1;
    default:
      break;
    }
  return 0;
}

#include "telinit.h"

int
telinit (int argc, char **argv)
{
  int fd;
  struct sysvinit_request req;

  memset (&req, 0, sizeof (req));
  req.magic = INIT_MAGIC;

  telinit_parser (&req, argc, argv);

  signal (SIGALRM, SIG_DFL);
  alarm (5);
  fd = open (init_fifo, O_WRONLY);
  if (fd < 0)
    {
      logmsg (LOG_ERR, _("cannot open %s: %s"), init_fifo, strerror (errno));
      return EX_UNAVAILABLE;
    }
  if (write (fd, &req, sizeof (req)) != sizeof (req))
    {
      logmsg (LOG_ERR, _("error writing to %s: %s"),
              init_fifo, strerror (errno));
      return EX_UNAVAILABLE;
    }
  alarm (0);
  close (fd);
  return 0;
}

static char *
getfld (char *str, char **endp)
{
  char *p;

  if (str)
    {
      p = strchr (str, ':');
      if (p)
	*p++ = 0;
      *endp = p;
    }
  else
    *endp = NULL;
  return str;
}

struct action_parser
{
  const char *action;
  enum pies_comp_mode mode;
  int (*parser) (struct component *comp, const char *file, unsigned line);
};

static struct action_parser action_tab[] = {
  { "wait",         pies_comp_wait },
  { "once",         pies_comp_once },
  { "boot",         pies_comp_boot },
  { "bootwait",     pies_comp_bootwait },
  { "powerfail",    pies_comp_powerfail },
  { "powerwait",    pies_comp_powerwait },
  { "powerokwait",  pies_comp_powerokwait },
  { "ctrlaltdel",   pies_comp_ctrlaltdel },	
  { "ondemand",     pies_comp_ondemand },
  { "sysinit",      pies_comp_sysinit },
  { "powerfailnow", pies_comp_powerfailnow },
  { "kbrequest",    pies_comp_kbrequest },
  { "respawn",      pies_comp_respawn },
  { NULL }
};

static struct action_parser *
find_action_parser (const char *action)
{
  struct action_parser *ap;

  for (ap = action_tab; ap->action; ap++)
    if (strcmp (ap->action, action) == 0)
      return ap;
  return NULL;
}

static char *
strupr (char *s)
{
  char *p;
  for (p = s; *p; p++)
    *p = toupper (*p);
  return s;
}

struct inittab_ctx
{
  char *buf;
  size_t size;
  char const *file;
  unsigned line_no;
};

enum inittab_status
  {
    inittab_ok,
    inittab_err,
    inittab_stop
  };

static enum inittab_status
inittab_parse_line (struct inittab_ctx *ctx)
{
  char *id, *runlevels, *action, *process, *p;
  struct action_parser *ap;
  struct component *comp;
  struct wordsplit ws;
      
  ctx->line_no++;
  for (p = ctx->buf; *p && c_isblank (*p); p++)
    ;

  if (!p || *p == '\n')
    return inittab_ok;
  
  if (*p == '#')
    {
      if (wordsplit (p+1, &ws, WRDSF_DEFFLAGS))
	{
	  logmsg (LOG_ERR, "%s:%u: wordsplit: %s", ctx->file, ctx->line_no,
		  strerror (errno));
	  return inittab_err;
	}
      /* pies pragma debug N */
      /* pies pragma next FORMAT FILE */
      /* pies pragma stop */
      if (ws.ws_wordc > 2 && strcmp (ws.ws_wordv[0], "pies") == 0 &&
	  strcmp (ws.ws_wordv[1], "pragma") == 0)
	{
	  if (strcmp (ws.ws_wordv[2], "debug") == 0)
	    {
	      if (ws.ws_wordc == 4)
		debug_level = atoi (ws.ws_wordv[3]);
	    }
	  else if (strcmp (ws.ws_wordv[2], "next") == 0)
	    {
	      if (ws.ws_wordc >= 5)
		{
		  struct config_syntax *synt =
		    str_to_config_syntax (ws.ws_wordv[3]);
		      
		  if (!synt)
		    logmsg (LOG_ERR, "%s:%u: %s",
			    ctx->file, ctx->line_no, _("unknown syntax type"));
		  else
		    config_file_add (synt, ws.ws_wordv[4]);
		}
	      else if (ws.ws_wordc == 4)
		config_file_add_type (CONF_PIES, ws.ws_wordv[3]);
	    }
	  else if (strcmp (ws.ws_wordv[2], "stop") == 0)
	    {
	      wordsplit_free (&ws);
	      return inittab_stop;
	    }
	}
      
      wordsplit_free (&ws);
      return inittab_ok;
    }    
      
  id = getfld (p, &p);
  runlevels = getfld (p, &p);
  action = getfld (p, &p);
  process = p;

  if (!id || !runlevels || !action || !process)
    {
      logmsg (LOG_ERR, "%s:%u: %s",
	      ctx->file, ctx->line_no, _("not enough fields"));
      return inittab_err;
    }

  if (strcmp (action, "initdefault") == 0)
    {
      if (!runlevels[0] || !is_valid_runlevel (runlevels[0]))
	{
	  logmsg (LOG_ERR, "%s:%u: %s",
		  ctx->file, ctx->line_no, _("invalid runlevel"));
	  return inittab_err;
	}
      else
	initdefault = toupper (runlevels[0]);
      return inittab_ok;
    }

  if (strcmp (action, "off") == 0)
    /* Ignore the entry */
    return inittab_ok;

  ap = find_action_parser (action);
  if (!ap)
    {
      logmsg (LOG_ERR, "%s:%u: %s", ctx->file, ctx->line_no,
	      _("unknown action"));
      return inittab_err;
    }
      
  comp = component_create (id);
  comp->mode = ap->mode;
  comp->runlevels = grecs_strdup (strupr (runlevels));

  if (wordsplit (process, &ws, WRDSF_DEFFLAGS))
    {
      component_free (comp);
      logmsg (LOG_ERR, "%s:%u: wordsplit: %s", ctx->file, ctx->line_no,
	      strerror (errno));
      return inittab_err;
    }
  wordsplit_get_words (&ws, &comp->argc, &comp->argv);
  comp->program = grecs_strdup (comp->argv[0]);
  wordsplit_free (&ws);

  comp->flags |= CF_SIGGROUP;
      
  if (ap->parser && ap->parser (comp, ctx->file, ctx->line_no))
    {
      component_free (comp);
      return inittab_err;
    }
  return inittab_ok;
}

/* Return true if we hit a single-user component */
static int
is_single_user (struct component *comp, void *data)
{
  return is_sysvinit (comp) && comp->runlevels && strchr (comp->runlevels, 'S');
}

int
inittab_parse (const char *file)
{
  FILE *fp;
  struct inittab_ctx ctx;
  int err;
  
  ctx.size = 0;
  ctx.buf = NULL;
  ctx.file = file;
  ctx.line_no = 0;
  
  fp = fopen (file, "r");
  if (fp)
    {
      while (getline (&ctx.buf, &ctx.size, fp) >= 0)
	{
	  enum inittab_status st = inittab_parse_line (&ctx);
	  if (st == inittab_err)
	    err = 1;
	  else if (st == inittab_stop)
	    break;
	}
      fclose (fp);
    }
  else
    {
      logmsg (LOG_ERR,
	      _("cannot open configuration file %s: %s"),
	      file, strerror (errno));
      err = 1;
    }

  if (!component_foreach (is_single_user, 0))
    {
      /* Provide single-user entry */
      ctx.file = __FILE__;
      ctx.line_no = __LINE__;
      grecs_asprintf (&ctx.buf, &ctx.size, "~~:S:wait:%s\n", emergency_shell);
      inittab_parse_line (&ctx);
    }
  
  free (ctx.buf);
  return err;
}

char *power_stat_file = POWER_STAT_FILE; 

void
sysvinit_power (void)
{
  int power_stat = POWER_STAT_FAIL;
  int fd;

  fd = open (power_stat_file, O_RDONLY);
  if (fd >= 0)
    {
      char c;
      switch (read (fd, &c, 1))
	{
	case 1:
	  power_stat = c;
	  break;

	case 0:
	  logmsg (LOG_NOTICE, _("unexpected EOF on %s"), power_stat_file);
	  break;

	case -1:
	  logmsg (LOG_ERR, _("error reading from %s: %s"),
		  power_stat_file, strerror (errno));
	}
      close (fd);
      if (unlink (power_stat_file))
	logfuncall ("unlink", power_stat_file, errno);
    }
  else
    debug (1, (_("cannot open %s: %s"), power_stat_file, strerror (errno)));

  powerfailcmd (power_stat);
}

static void
powerfailcmd (int power_stat)
{
  int mask;
  
  switch (power_stat)
    {
    case POWER_STAT_OK: /* The power is OK */
      mask = PIES_COMP_MASK (pies_comp_powerokwait);
      break;
      
    case POWER_STAT_LOW: /* Low battery: shut down now */
      mask = PIES_COMP_MASK (pies_comp_powerfailnow);
      break;
      
    default: /* Power failure */
      mask = PIES_COMP_MASK (pies_comp_powerfail)
	     | PIES_COMP_MASK (pies_comp_powerwait);
    }
	
  sysvinit_runlevel_setup (mask);
}

void
sysvinit_report (struct json_value *obj)
{
  json_object_set_string (obj, "runlevel", "%c", runlevel);
  json_object_set_string (obj, "prevlevel", "%c", prevlevel);
  json_object_set_string (obj, "bootstate", "%s",
			  boot_state_name[boot_state]);
  if (initdefault)
    json_object_set_string (obj, "initdefault", "%c", initdefault);
}

void
sysvinit_parse_argv (int argc, char **argv)
{
  while (--argc)
    {
      int c;
      char *arg = *++argv;
      if (!strcmp (arg, "single") || !strcmp (arg, "-s"))
	dfl_level = 'S';
      else if (!strcmp (arg, "-b") || !strcmp (arg, "emergency"))
	emergency = 1;
      else if (!arg[1] && strchr (valid_runlevels, (c = toupper (arg[0]))))
	dfl_level = c;
    }	  
}
