/* --------------- Moved here from job.c ---------------
   This file must be #included in job.c, as it accesses static functions.

Copyright (C) 1996-2013 Free Software Foundation, Inc.
This file is part of GNU Make.

GNU Make 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 of the License, or (at your option) any later
version.

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

#include <string.h>
#include <descrip.h>
#include <clidef.h>

char *vmsify (char *name, int type);

static int vms_jobsefnmask = 0;

/* Wait for nchildren children to terminate */
static void
vmsWaitForChildren(int *status)
{
  while (1)
    {
      if (!vms_jobsefnmask)
        {
          *status = 0;
          return;
        }

      *status = sys$wflor (32, vms_jobsefnmask);
    }
  return;
}

/* Set up IO redirection.  */

char *
vms_redirect (struct dsc$descriptor_s *desc, char *fname, char *ibuf)
{
  char *fptr;

  ibuf++;
  while (isspace ((unsigned char)*ibuf))
    ibuf++;
  fptr = ibuf;
  while (*ibuf && !isspace ((unsigned char)*ibuf))
    ibuf++;
  *ibuf = 0;
  if (strcmp (fptr, "/dev/null") != 0)
    {
      strcpy (fname, vmsify (fptr, 0));
      if (strchr (fname, '.') == 0)
        strcat (fname, ".");
    }
  desc->dsc$w_length = strlen (fname);
  desc->dsc$a_pointer = fname;
  desc->dsc$b_dtype = DSC$K_DTYPE_T;
  desc->dsc$b_class = DSC$K_CLASS_S;

  if (*fname == 0)
    printf (_("Warning: Empty redirection\n"));
  return ibuf;
}


/* found apostrophe at (p-1)
   inc p until after closing apostrophe.
*/

char *
vms_handle_apos (char *p)
{
  int alast;

#define SEPCHARS ",/()= "

  alast = 0;

  while (*p != 0)
    if (*p == '"')
      if (alast)
        {
          alast = 0;
          p++;
        }
      else
        {
          p++;
          if (strchr (SEPCHARS, *p))
            break;
          alast = 1;
        }
    else
      p++;

  return p;
}

static int ctrlYPressed= 0;
/* This is called at main or AST level. It is at AST level for DONTWAITFORCHILD
   and at main level otherwise. In any case it is called when a child process
   terminated. At AST level it won't get interrupted by anything except a
   inner mode level AST.
*/
int
vmsHandleChildTerm(struct child *child)
{
  int status;
  register struct child *lastc, *c;
  int child_failed;

  vms_jobsefnmask &= ~(1 << (child->efn - 32));

  lib$free_ef (&child->efn);
  if (child->comname)
    {
      if (!ISDB (DB_JOBS) && !ctrlYPressed)
        unlink (child->comname);
      free (child->comname);
    }

  (void) sigblock (fatal_signal_mask);

  child_failed = !(child->cstatus & 1 || ((child->cstatus & 7) == 0));

  /* Search for a child matching the deceased one.  */
  lastc = 0;
#if defined(RECURSIVEJOBS) /* I've had problems with recursive stuff and process handling */
  for (c = children; c != 0 && c != child; lastc = c, c = c->next)
    ;
#else
  c = child;
#endif

  if (child_failed && !c->noerror && !ignore_errors_flag)
    {
      /* The commands failed.  Write an error message,
         delete non-precious targets, and abort.  */
      child_error (c, c->cstatus, 0, 0, 0);
      c->file->update_status = us_failed;
      delete_child_targets (c);
    }
  else
    {
      if (child_failed)
        {
          /* The commands failed, but we don't care.  */
          child_error (c, c->cstatus, 0, 0, 1);
          child_failed = 0;
        }

#if defined(RECURSIVEJOBS) /* I've had problems with recursive stuff and process handling */
      /* If there are more commands to run, try to start them.  */
      start_job (c);

      switch (c->file->command_state)
        {
        case cs_running:
          /* Successfully started.  */
          break;

        case cs_finished:
          if (c->file->update_status != us_success)
            /* We failed to start the commands.  */
            delete_child_targets (c);
          break;

        default:
          error (NILF, _("internal error: '%s' command_state"),
                 c->file->name);
          abort ();
          break;
        }
#endif /* RECURSIVEJOBS */
    }

  /* Set the state flag to say the commands have finished.  */
  c->file->command_state = cs_finished;
  notice_finished_file (c->file);

#if defined(RECURSIVEJOBS) /* I've had problems with recursive stuff and process handling */
  /* Remove the child from the chain and free it.  */
  if (lastc == 0)
    children = c->next;
  else
    lastc->next = c->next;
  free_child (c);
#endif /* RECURSIVEJOBS */

  /* There is now another slot open.  */
  if (job_slots_used > 0)
    --job_slots_used;

  /* If the job failed, and the -k flag was not given, die.  */
  if (child_failed && !keep_going_flag)
    die (EXIT_FAILURE);

  (void) sigsetmask (sigblock (0) & ~(fatal_signal_mask));

  return 1;
}

/* VMS:
   Spawn a process executing the command in ARGV and return its pid. */

#define MAXCMDLEN 200

/* local helpers to make ctrl+c and ctrl+y working, see below */
#include <iodef.h>
#include <libclidef.h>
#include <ssdef.h>

static int ctrlMask= LIB$M_CLI_CTRLY;
static int oldCtrlMask;
static int setupYAstTried= 0;
static unsigned short int chan= 0;

static void
reEnableAst(void)
{
  lib$enable_ctrl (&oldCtrlMask,0);
}

static int
astYHandler (void)
{
  struct child *c;
  for (c = children; c != 0; c = c->next)
    sys$delprc (&c->pid, 0, 0);
  ctrlYPressed= 1;
  kill (getpid(),SIGQUIT);
  return SS$_NORMAL;
}

static void
tryToSetupYAst(void)
{
  $DESCRIPTOR(inputDsc,"SYS$COMMAND");
  int     status;
  struct {
    short int       status, count;
    int     dvi;
  } iosb;
  unsigned short int loc_chan;

  setupYAstTried++;

  if (chan)
    loc_chan= chan;
  else
    {
      status= sys$assign(&inputDsc,&loc_chan,0,0);
      if (!(status&SS$_NORMAL))
        {
          lib$signal(status);
          return;
        }
    }
  status= sys$qiow (0, loc_chan, IO$_SETMODE|IO$M_CTRLYAST,&iosb,0,0,
                    astYHandler,0,0,0,0,0);
  if (status==SS$_NORMAL)
    status= iosb.status;
  if (status!=SS$_NORMAL)
    {
      if (!chan)
        sys$dassgn(loc_chan);
      if (status!=SS$_ILLIOFUNC && status!=SS$_NOPRIV)
        lib$signal(status);
      return;
    }

  /* called from AST handler ? */
  if (setupYAstTried>1)
    return;
  if (atexit(reEnableAst))
    fprintf (stderr,
             _("-warning, you may have to re-enable CTRL-Y handling from DCL.\n"));
  status= lib$disable_ctrl (&ctrlMask, &oldCtrlMask);
  if (!(status&SS$_NORMAL))
    {
      lib$signal(status);
      return;
    }
  if (!chan)
    chan = loc_chan;
}

int
child_execute_job (char *argv, struct child *child)
{
  int i;
  static struct dsc$descriptor_s cmddsc;
  static struct dsc$descriptor_s pnamedsc;
  static struct dsc$descriptor_s ifiledsc;
  static struct dsc$descriptor_s ofiledsc;
  static struct dsc$descriptor_s efiledsc;
  int have_redirection = 0;
  int have_append = 0;
  int have_newline = 0;

  int spflags = CLI$M_NOWAIT;
  int status;
  char *cmd = alloca (strlen (argv) + 512), *p, *q;
  char ifile[256], ofile[256], efile[256];
  int comnamelen;
  char procname[100];
  int in_string;

  /* Parse IO redirection.  */

  ifile[0] = 0;
  ofile[0] = 0;
  efile[0] = 0;
  child->comname = NULL;

  DB (DB_JOBS, ("child_execute_job (%s)\n", argv));

  while (isspace ((unsigned char)*argv))
    argv++;

  if (*argv == 0)
    return 0;

  sprintf (procname, "GMAKE_%05x", getpid () & 0xfffff);
  pnamedsc.dsc$w_length = strlen(procname);
  pnamedsc.dsc$a_pointer = procname;
  pnamedsc.dsc$b_dtype = DSC$K_DTYPE_T;
  pnamedsc.dsc$b_class = DSC$K_CLASS_S;

  in_string = 0;
  /* Handle comments and redirection. */
  for (p = argv, q = cmd; *p; p++, q++)
    {
      if (*p == '"')
        in_string = !in_string;
      if (in_string)
        {
          *q = *p;
          continue;
        }
      switch (*p)
        {
        case '#':
          *p-- = 0;
          *q-- = 0;
          break;
        case '\\':
          p++;
          if (*p == '\n')
            p++;
          if (isspace ((unsigned char)*p))
            {
              do { p++; } while (isspace ((unsigned char)*p));
              p--;
            }
          *q = *p;
          break;
        case '<':
          p = vms_redirect (&ifiledsc, ifile, p);
          *q = ' ';
          have_redirection = 1;
          break;
        case '>':
          have_redirection = 1;
          if (*(p-1) == '2')
            {
              q--;
              if (strncmp (p, ">&1", 3) == 0)
                {
                  p += 3;
                  strcpy (efile, "sys$output");
                  efiledsc.dsc$w_length = strlen(efile);
                  efiledsc.dsc$a_pointer = efile;
                  efiledsc.dsc$b_dtype = DSC$K_DTYPE_T;
                  efiledsc.dsc$b_class = DSC$K_CLASS_S;
                }
              else
                  p = vms_redirect (&efiledsc, efile, p);
            }
          else
            {
              if (*(p+1) == '>')
                {
                  have_append = 1;
                  p += 1;
                }
              p = vms_redirect (&ofiledsc, ofile, p);
            }
          *q = ' ';
          break;
        case '\n':
          have_newline = 1;
        default:
          *q = *p;
          break;
        }
    }
  *q = *p;
  while (isspace ((unsigned char)*--q))
    *q = '\0';

  if (strncmp (cmd, "builtin_", 8) == 0)
    {
      child->pid = 270163;
      child->efn = 0;
      child->cstatus = 1;

      DB (DB_JOBS, (_("BUILTIN [%s][%s]\n"), cmd, cmd+8));

      p = cmd + 8;

      if ((*(p) == 'c')
          && (*(p+1) == 'd')
          && ((*(p+2) == ' ') || (*(p+2) == '\t')))
        {
          p += 3;
          while ((*p == ' ') || (*p == '\t'))
            p++;
          DB (DB_JOBS, (_("BUILTIN CD %s\n"), p));
          if (chdir (p))
            return 0;
          else
            return 1;
        }
      else if ((*(p) == 'r')
               && (*(p+1) == 'm')
               && ((*(p+2) == ' ') || (*(p+2) == '\t')))
        {
          int in_arg;

          /* rm  */
          p += 3;
          while ((*p == ' ') || (*p == '\t'))
            p++;
          in_arg = 1;

          DB (DB_JOBS, (_("BUILTIN RM %s\n"), p));
          while (*p)
            {
              switch (*p)
                {
                case ' ':
                case '\t':
                  if (in_arg)
                    {
                      *p++ = ';';
                      in_arg = 0;
                    }
                  break;
                default:
                  break;
                }
              p++;
            }
        }
      else
        {
          printf (_("Unknown builtin command '%s'\n"), cmd);
          fflush (stdout);
          return 0;
        }
    }

  /* Create a *.com file if either the command is too long for
     lib$spawn, or the command contains a newline, or if redirection
     is desired. Forcing commands with newlines into DCLs allows to
     store search lists on user mode logicals.  */

  if (strlen (cmd) > MAXCMDLEN
      || (have_redirection != 0)
      || (have_newline != 0))
    {
      FILE *outfile;
      char c;
      char *sep;
      int alevel = 0;   /* apostrophe level */

      if (strlen (cmd) == 0)
        {
          printf (_("Error, empty command\n"));
          fflush (stdout);
          return 0;
        }

      outfile = output_tmpfile (&child->comname, "sys$scratch:CMDXXXXXX.COM");
      if (outfile == 0)
        pfatal_with_name (_("fopen (temporary file)"));
      comnamelen = strlen (child->comname);

      if (ifile[0])
        {
          fprintf (outfile, "$ assign/user %s sys$input\n", ifile);
          DB (DB_JOBS, (_("Redirected input from %s\n"), ifile));
          ifiledsc.dsc$w_length = 0;
        }

      if (efile[0])
        {
          fprintf (outfile, "$ define sys$error %s\n", efile);
          DB (DB_JOBS, (_("Redirected error to %s\n"), efile));
          efiledsc.dsc$w_length = 0;
        }

      if (ofile[0])
        if (have_append)
          {
            fprintf (outfile, "$ set noon\n");
            fprintf (outfile, "$ define sys$output %.*s\n", comnamelen-3, child->comname);
            DB (DB_JOBS, (_("Append output to %s\n"), ofile));
            ofiledsc.dsc$w_length = 0;
          }
        else
          {
            fprintf (outfile, "$ define sys$output %s\n", ofile);
            DB (DB_JOBS, (_("Redirected output to %s\n"), ofile));
            ofiledsc.dsc$w_length = 0;
          }

      p = sep = q = cmd;
      for (c = '\n'; c; c = *q++)
        {
          switch (c)
            {
            case '\n':
              /* At a newline, skip any whitespace around a leading $
                 from the command and issue exactly one $ into the DCL. */
              while (isspace ((unsigned char)*p))
                p++;
              if (*p == '$')
                p++;
              while (isspace ((unsigned char)*p))
                p++;
              fwrite (p, 1, q - p, outfile);
              fputc ('$', outfile);
              fputc (' ', outfile);
              /* Reset variables. */
              p = sep = q;
              break;

              /* Nice places for line breaks are after strings, after
                 comma or space and before slash. */
            case '"':
              q = vms_handle_apos (q);
              sep = q;
              break;
            case ',':
            case ' ':
              sep = q;
              break;
            case '/':
            case '\0':
              sep = q - 1;
              break;
            default:
              break;
            }
          if (sep - p > 78)
            {
              /* Enough stuff for a line. */
              fwrite (p, 1, sep - p, outfile);
              p = sep;
              if (*sep)
                {
                  /* The command continues.  */
                  fputc ('-', outfile);
                }
              fputc ('\n', outfile);
            }
        }

      if (*p)
        {
          fwrite (p, 1, --q - p, outfile);
          fputc ('\n', outfile);
        }

      if (have_append)
        {
          fprintf (outfile, "$ deassign sys$output ! 'f$verify(0)\n");
          fprintf (outfile, "$ append:=append\n");
          fprintf (outfile, "$ delete:=delete\n");
          fprintf (outfile, "$ append/new %.*s %s\n", comnamelen-3, child->comname, ofile);
          fprintf (outfile, "$ delete %.*s;*\n", comnamelen-3, child->comname);
          DB (DB_JOBS, (_("Append %.*s and cleanup\n"), comnamelen-3, child->comname));
        }

      fclose (outfile);

      sprintf (cmd, "$ @%s", child->comname);

      DB (DB_JOBS, (_("Executing %s instead\n"), cmd));
    }

  cmddsc.dsc$w_length = strlen(cmd);
  cmddsc.dsc$a_pointer = cmd;
  cmddsc.dsc$b_dtype = DSC$K_DTYPE_T;
  cmddsc.dsc$b_class = DSC$K_CLASS_S;

  child->efn = 0;
  while (child->efn < 32 || child->efn > 63)
    {
      status = lib$get_ef ((unsigned long *)&child->efn);
      if (!(status & 1))
        {
          if (child->comname)
            {
              if (!ISDB (DB_JOBS))
                unlink (child->comname);
              free (child->comname);
            }
          return 0;
        }
    }

  sys$clref (child->efn);

  vms_jobsefnmask |= (1 << (child->efn - 32));

  /*
    LIB$SPAWN  [command-string]
    [,input-file]
    [,output-file]
    [,flags]
    [,process-name]
    [,process-id] [,completion-status-address] [,byte-integer-event-flag-num]
    [,AST-address] [,varying-AST-argument]
    [,prompt-string] [,cli] [,table]
  */

#ifndef DONTWAITFORCHILD
  /*
   * Code to make ctrl+c and ctrl+y working.
   * The problem starts with the synchronous case where after lib$spawn is
   * called any input will go to the child. But with input re-directed,
   * both control characters won't make it to any of the programs, neither
   * the spawning nor to the spawned one. Hence the caller needs to spawn
   * with CLI$M_NOWAIT to NOT give up the input focus. A sys$waitfr
   * has to follow to simulate the wanted synchronous behaviour.
   * The next problem is ctrl+y which isn't caught by the crtl and
   * therefore isn't converted to SIGQUIT (for a signal handler which is
   * already established). The only way to catch ctrl+y, is an AST
   * assigned to the input channel. But ctrl+y handling of DCL needs to be
   * disabled, otherwise it will handle it. Not to mention the previous
   * ctrl+y handling of DCL needs to be re-established before make exits.
   * One more: At the time of LIB$SPAWN signals are blocked. SIGQUIT will
   * make it to the signal handler after the child "normally" terminates.
   * This isn't enough. It seems reasonable for simple command lines like
   * a 'cc foobar.c' spawned in a subprocess but it is unacceptable for
   * spawning make. Therefore we need to abort the process in the AST.
   *
   * Prior to the spawn it is checked if an AST is already set up for
   * ctrl+y, if not one is set up for a channel to SYS$COMMAND. In general
   * this will work except if make is run in a batch environment, but there
   * nobody can press ctrl+y. During the setup the DCL handling of ctrl+y
   * is disabled and an exit handler is established to re-enable it.
   * If the user interrupts with ctrl+y, the assigned AST will fire, force
   * an abort to the subprocess and signal SIGQUIT, which will be caught by
   * the already established handler and will bring us back to common code.
   * After the spawn (now /nowait) a sys$waitfr simulates the /wait and
   * enables the ctrl+y be delivered to this code. And the ctrl+c too,
   * which the crtl converts to SIGINT and which is caught by the common
   * signal handler. Because signals were blocked before entering this code
   * sys$waitfr will always complete and the SIGQUIT will be processed after
   * it (after termination of the current block, somewhere in common code).
   * And SIGINT too will be delayed. That is ctrl+c can only abort when the
   * current command completes. Anyway it's better than nothing :-)
   */

  if (!setupYAstTried)
    tryToSetupYAst();
  status = lib$spawn (&cmddsc,                                  /* cmd-string */
                      (ifiledsc.dsc$w_length == 0)?0:&ifiledsc, /* input-file */
                      (ofiledsc.dsc$w_length == 0)?0:&ofiledsc, /* output-file */
                      &spflags,                                 /* flags */
                      &pnamedsc,                                /* proc name */
                      &child->pid, &child->cstatus, &child->efn,
                      0, 0,
                      0, 0, 0);
  if (status & 1)
    {
      status= sys$waitfr (child->efn);
      vmsHandleChildTerm(child);
    }
#else
  status = lib$spawn (&cmddsc,
                      (ifiledsc.dsc$w_length == 0)?0:&ifiledsc,
                      (ofiledsc.dsc$w_length == 0)?0:&ofiledsc,
                      &spflags,
                      &pnamedsc,
                      &child->pid, &child->cstatus, &child->efn,
                      vmsHandleChildTerm, child,
                      0, 0, 0);
#endif

  if (!(status & 1))
    {
      printf (_("Error spawning, %d\n") ,status);
      fflush (stdout);
      switch (status)
        {
        case 0x1c:
          errno = EPROCLIM;
          break;
        default:
          errno = EFAIL;
        }
    }

  return (status & 1);
}
