/* Subprocesses with pipes.

   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc.

   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 3 of the License, or
   (at your option) 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, see <http://www.gnu.org/licenses/>.  */

/* Written by Juan Manuel Guerrero <juan.guerrero@gmx.de>. */


#include <config.h>

#include "subpipe.h"

#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <process.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include "xalloc.h"


#ifndef STDIN_FILENO
# define STDIN_FILENO 0
#endif
#ifndef STDOUT_FILENO
# define STDOUT_FILENO 1
#endif


#include "error.h"

#include "gettext.h"
#define _(Msgid)  gettext (Msgid)


/* Initialize this module. */


static int old_stdin;
static int old_stdout;
static char **arguments;
static char tmp_file_name[2][L_tmpnam];
static int spawn_status;


#define remove_tmp_file(fd, name)                                       \
  do {                                                                  \
    if (!close((fd)))                                                   \
    {                                                                   \
      if (unlink ((name)))                                              \
        error (EXIT_FAILURE, 0, _("removing of `%s' failed"), (name));  \
      (name)[0] = '\0';                                                 \
    }                                                                   \
  } while (0)


static void
remove_remaining_tmp_files(void)
{
  /*
   *  On MSDOS/Windows it is always necessary to close()
   *  a file before remove() or unlink() it.  The tmp files
   *  used to simulate the pipe are always connected to STDIN
   *  and STDOUT.
   */
  close(STDIN_FILENO);
  close(STDOUT_FILENO);
  unlink(tmp_file_name[0]);
  unlink(tmp_file_name[1]);
}


void
init_subpipe(void)
{
  char *tmpdir;
  int fd;

  tmpdir = getenv("TMPDIR");
  if (tmpdir == NULL)
    tmpdir = getenv("TMP");
  if (tmpdir == NULL)
    tmpdir = getenv("TEMP");
  if (access(tmpdir, D_OK))
    tmpdir = ".";

  strcpy(tmp_file_name[0], tmpdir);
  strcat(tmp_file_name[0], "/bnXXXXXX");
  fd = mkstemp(tmp_file_name[0]);
  if (fd < 0)
    error(EXIT_FAILURE, 0, _("creation of a temporary file failed"));
  close (fd);

  strcpy(tmp_file_name[1], tmpdir);
  strcat(tmp_file_name[1], "/bnXXXXXX");
  fd = mkstemp(tmp_file_name[1]);
  if (fd < 0)
    error(EXIT_FAILURE, 0, _("creation of a temporary file failed"));
  close (fd);

  atexit(remove_remaining_tmp_files);
}


/* Create a subprocess that is run as a filter.  ARGV is the
   NULL-terminated argument vector for the subprocess.  Store read and
   write file descriptors for communication with the subprocess into
   FD[0] and FD[1]: input meant for the process can be written into
   FD[0], and output from the process can be read from FD[1].  Return
   the subprocess id.

   Because DOS has neither fork nor pipe functionality to run the subprocess
   as a filter, the filter is reproduced using temporary files. First bison's
   stdout is redirected to a temporary file. After bison has produced all of
   is output, this file is closed and connected to m4's stdin. All m4's output
   is redirected from m4's stdout to a second temporary file and reopened as
   bison's stdin. */

pid_t
create_subpipe(char const *const *argv, int fd[2])
{
  int argc;
  int from_in_fd;  /* pipe from bison to m4. */
  pid_t pid;


  pid = getpid();

  /*
   *  Save original stdin and stdout
   *  for later restauration.
   */
  old_stdin = dup(STDIN_FILENO);
  if (old_stdin < 0)
    error(EXIT_FAILURE, 0, _("saving stdin failed"));

  old_stdout = dup(STDOUT_FILENO);
  if (old_stdout < 0)
    error(EXIT_FAILURE, 0, _("saving stdout failed"));

  /*
   *  Save argv for later use.
   */
  for (argc = 0; argv[argc]; argc++)
    ;
  argc++;
  arguments = xmalloc(argc * sizeof(arguments[0]));
  for (argc = 0; argv[argc]; argc++)
  {
    arguments[argc] = xmalloc((strlen(argv[argc]) + 1) * sizeof(arguments[0][0]));
    strcpy(arguments[argc], argv[argc]);
  }
  arguments[argc] = NULL;

  /*
   *  All bison's output will be gathered in this temporary file
   *  and will be redirected to m4's stdin.
   */
  from_in_fd = open(tmp_file_name[0], O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR);
  if (from_in_fd < 0)
    error(EXIT_FAILURE, 0, _("opening of tmpfile failed"));
  if (dup2(from_in_fd, STDOUT_FILENO) < 0)
  {
    remove_tmp_file(from_in_fd, tmp_file_name[0]);
    error(EXIT_FAILURE, 0, _("redirecting bison's stdout to the temporary file failed"));
  }
  close(from_in_fd);


  fd[0] = STDOUT_FILENO;
  return pid;
}


/* A signal handler that just records that a signal has happened. */
static int child_interrupted;

static void
signal_catcher(int signo)
{
  child_interrupted++;
}


void
end_of_output_subpipe(pid_t pid, int fd[2])
{
  char *program;
  int from_out_fd = open(tmp_file_name[0], O_RDONLY, S_IRUSR);                   /* pipe from bison to m4. */
  int to_in_fd = open(tmp_file_name[1], O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR);  /* pipe from m4 to bison. */
  int status;
  void (*previous_handler)(int);


  program = strrchr(arguments[0], '/');
  if (program)
    program++;
  else
    program = arguments[0];

  /*
   *  Redirect bison's output to m4's stdin.
   */
  if (from_out_fd < 0)
    error(EXIT_FAILURE, 0, _("opening of tmpfile failed"));
  if (dup2(from_out_fd, STDIN_FILENO) < 0)
  {
    remove_tmp_file(from_out_fd, tmp_file_name[0]);
    error(EXIT_FAILURE, 0, _("redirecting m4's stdin from the temporary file failed"));
  }
  close(from_out_fd);

  /*
   *  All m4's output will be gathered in this temporary file
   *  and will be redirected to bison's stdin.
   */
  if (to_in_fd < 0)
  {
    remove_tmp_file(STDIN_FILENO, tmp_file_name[0]);
    error(EXIT_FAILURE, 0, _("opening of a temporary file failed"));
  }
  if (dup2(to_in_fd, STDOUT_FILENO) < 0)
  {
    remove_tmp_file(STDIN_FILENO, tmp_file_name[0]);
    remove_tmp_file(to_in_fd, tmp_file_name[1]);
    error(EXIT_FAILURE, 0, _("redirecting m4's stdout to a temporary file failed"));
  }
  close(to_in_fd);

  /*
   *  Run m4.
   */
  child_interrupted = 0;
  errno = 0;
  previous_handler = signal(SIGINT, signal_catcher);
  status = spawnvp(P_WAIT, program, arguments);
  signal(SIGINT, previous_handler);

  spawn_status = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
  if (child_interrupted)
  {
    remove_tmp_file(STDIN_FILENO, tmp_file_name[0]);
    remove_tmp_file(STDOUT_FILENO, tmp_file_name[1]);
    error(EXIT_FAILURE, 0, _("subsidiary program `%s' interrupted"), program);
  }
  if (spawn_status == -1)
  {
    /*
     *  - spawn returns -1 if m4 can not be invoked or executed.
     *  - If m4 could be executed, then spawn returns m4's exit
     *    code in the lower 8 bits of status.
     *  - m4's exit codes are at least: 0, 1 and 63.
     *
     *  If m4 returns with exit code greater than 0 bison will
     *  get the chance to examinate m4's output and terminated
     *  itself in reap_subpipe() if necessary.
     */
    remove_tmp_file(STDIN_FILENO, tmp_file_name[0]);
    remove_tmp_file(STDOUT_FILENO, tmp_file_name[1]);
    error(EXIT_FAILURE, 0, _(errno == ENOENT
                             ? "subsidiary program `%s' not found"
                             : "subsidiary program `%s' could not be invoked (status=%i, errno=%i)"), program, spawn_status, errno);
  }


  /*
   *  Redirect m4's output to bison's stdin.
   */
  if (dup2(old_stdout, STDOUT_FILENO) < 0)
    error(EXIT_FAILURE, 0, "restore of bison's stdout failed");
  close(old_stdout);
  to_in_fd = open(tmp_file_name[1], O_RDONLY, S_IRUSR);  /* pipe from m4 to bison. */
  if (to_in_fd < 0)
  {
    remove_tmp_file(STDIN_FILENO, tmp_file_name[0]);
    error(EXIT_FAILURE, 0, _("opening of tmpfile failed"));
  }
  if (dup2(to_in_fd, STDIN_FILENO) < 0)
  {
    remove_tmp_file(STDIN_FILENO, tmp_file_name[0]);
    remove_tmp_file(to_in_fd, tmp_file_name[1]);
    error(EXIT_FAILURE, -1, "dup2");
    error(EXIT_FAILURE, 0, _("redirecting bison's stdin from the temporary file failed"));
  }
  close(to_in_fd);


  fd[1] = STDIN_FILENO;
}


/* Free resources, unlink temporary files and restore stdin and stdout. */

void
reap_subpipe(pid_t pid, char const *program)
{
  if (spawn_status)
  {
    /*  m4 returned with exit code > 0.  Terminate bison.   */
    error(EXIT_FAILURE, 0, _("subsidiary program `%s' failed (status=%i, errno=%i)"), program, spawn_status, errno);
  }
  else
  {
    int argc;

    for (argc = 0; arguments[argc]; argc++)
      free(arguments[argc]);
    free(arguments);

    if (unlink(tmp_file_name[0]))
      error(EXIT_FAILURE, 0, _("removing of `%s' failed"), tmp_file_name[0]);
    if (unlink(tmp_file_name[1]))
      error(EXIT_FAILURE, 0, _("removing of `%s' failed"), tmp_file_name[1]);

    if (dup2(old_stdin, STDIN_FILENO) < 0)
      error(EXIT_FAILURE, 0, "restore of bison's stdin failed");
    close(old_stdin);
  }
}
