/* Subprocesses with pipes.

   Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012,2013, 2014, 2015 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 "cvs.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"


/* 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)                                      \
(__gnuc_extension__                                                    \
  ({                                                                   \
     if (!close((fd)))                                                 \
     {                                                                 \
       if (unlink((name)))                                             \
         error(EXIT_FAILURE, 0, "removing of \"%s\" failed", (name));  \
       (name)[0] = '\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_subprocess_with_pipe(void)
{
  char *tmpdir;
  int fd;

  tmpdir = getenv("TMPDIR");
  if (tmpdir == NULL)
    tmpdir = getenv("TMP");
  if (tmpdir == NULL)
    tmpdir = getenv("TEMP");
  if (!IS_TMPDIR(tmpdir))
    tmpdir = ".";

  strcpy(tmp_file_name[0], tmpdir);
  strcat(tmp_file_name[0], "/spXXXXXX");
  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], "/spXXXXXX");
  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 cvs'
   stdout is redirected to a temporary file.  After cvs has produced all
   of is output, this file is closed and connected to or ssh's stdin.
   All rsh's or ssh's output is redirected from rsh's or ssh's stdout
   to a second temporary file and reopened as cvs' stdin. */

pid_t
create_subprocess_with_pipe(char const *const *argv, int fd[2])
{
  int argc;
  int from_in_fd;  /* pipe from cvs to rsh or ssh. */
  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 cvs' output will be gathered in this temporary file
   *  and will be redirected to rsh's or ssh'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 cvs' 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_subprocess_with_pipe(pid_t pid, int fd[2])
{
  char *program;
  int from_out_fd = open(tmp_file_name[0], O_RDONLY, S_IRUSR);                   /* pipe from cvs to rsh or ssh. */
  int to_in_fd = open(tmp_file_name[1], O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR);  /* pipe from rsh or ssh to cvs. */
  int status;
  void (*previous_handler)(int);


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

  /*
   *  Redirect cvs' output to rsh's or ssh'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 rsh's or ssh's stdin from the temporary file failed");
  }
  close(from_out_fd);

  /*
   *  All rsh's or ssh's output will be gathered in this temporary file
   *  and will be redirected to cvs' 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 rsh's or ssh's stdout to a temporary file failed");
  }
  close(to_in_fd);

  /*
   *  Run rsh or ssh.
   */
  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 rsh or ssh can not be invoked or executed.
     *  - If rsh or ssh could be executed, then spawn returns rsh's or ssh's
     *    exit code in the lower 8 bits of status.
     *  - rsh's or ssh's exit codes are at least: 0, 1 and 63.
     *
     *  If rsh or ssh returns with exit code greater than 0 cvs will
     *  get the chance to examinate rsh's or ssh's output and terminated
     *  itself in reap_subprocess_with_pipe() 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 rsh's or ssh's output to cvs' stdin.
   */
  if (dup2(old_stdout, STDOUT_FILENO) < 0)
    error(EXIT_FAILURE, 0, "restore of cvs' stdout failed");
  close(old_stdout);
  to_in_fd = open(tmp_file_name[1], O_RDONLY, S_IRUSR);  /* pipe from rsh or ssh to cvs. */
  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 cvs' 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_subprocess_with_pipe(pid_t pid, char const *program)
{
  if (spawn_status)
  {
    /*  rsh or ssh returned with exit code > 0.  Terminate cvs.   */
    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 cvs' stdin failed");
    close(old_stdin);
  }
}
