/*********************************************************
 * Copyright (C) 2007-2016,2019 VMware, Inc. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation version 2.1 and no 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 Lesser GNU General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
 *
 *********************************************************/

/*
 * processPosix.c --
 *
 *      Implementation of the POSIX process wrapper.
 */

#include "imgcust-common/log.h"
#include "imgcust-common/process.h"
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <stdlib.h>

#include "util.h"

typedef struct _ProcessInternal {
   pid_t pid;
   int stdoutFd;
   int stderrFd;
   char* stdoutStr;
   char* stderrStr;
   int exitCode;
   char** args;
   LogFunction log;
} ProcessInternal;

typedef enum _ReadStatus {
   READSTATUS_UNDEFINED,
   READSTATUS_DONE,
   READSTATUS_PENDING,
   READSTATUS_ERROR
} ReadStatus;

static void
ProcessRead(ProcessInternal *p, ReadStatus *status, Bool out, Bool readToEof);


/*
 *------------------------------------------------------------------------------
 *
 * Process_Create --
 *
 *      Create and initialize a process object.
 *
 *------------------------------------------------------------------------------
 */

ProcessError
Process_Create(ProcessHandle *h, char *args[], void *logPtr)
{
   int i, numArgs;
   int err = -1;
   ProcessInternal *p;
   LogFunction log = (LogFunction)logPtr;
   log(log_info, "sizeof ProcessInternal is %d\n", sizeof(ProcessInternal));
   p = (ProcessInternal*) calloc(1, sizeof(ProcessInternal));
   if (p == NULL) {
      log(log_error, "Error allocating memory for process\n");
      goto error;
   }
   p->stdoutStr = malloc(sizeof(char));
   if (p->stdoutStr == NULL) {
      log(log_error, "Error allocating memory for process stdout\n");
      goto error;
   }
   p->stdoutStr[0] = '\0';
   p->stderrStr = malloc(sizeof(char));
   if (p->stderrStr == NULL) {
      log(log_error, "Error allocating memory for process stderr\n");
      goto error;
   }
   p->stderrStr[0] = '\0';

   p->stdoutFd = -1;
   p->stderrFd = -1;

   numArgs = 0;
   while (args[numArgs] != NULL) {
      numArgs++;
   }

   p->args = malloc((1 + numArgs) * sizeof(char*));
   if (p->args == NULL) {
      log(log_error, "Error allocating memory for process args\n");
      goto error;
   }
   for (i = 0; i < numArgs; i++) {
      p->args[i] = strdup(args[i]);
      if (p->args[i] == NULL) {
         log(log_error, "Error allocating memory for duplicate args\n");
         goto error;
      }
   }
   p->args[numArgs] = NULL;
   p->log = log;
   *h = (ProcessHandle)p;
   err = 0;

error:
   if (err != 0) {
      if (p != NULL) {
         Process_Destroy((ProcessHandle)p);
      }
      exit(1);
   }
   return PROCESS_SUCCESS;
}


/*
 *------------------------------------------------------------------------------
 *
 * Process_RunToComplete --
 *
 *      Runs a process until complete, collecting stdout and stderr into the
 *      process object.
 *
 *------------------------------------------------------------------------------
 */

ProcessError
Process_RunToComplete(ProcessHandle h, unsigned long timeoutSec)
{
   int stdout[2];
   int stderr[2];
   int flags;
   ProcessInternal* p;
   // poll for the process to complete and read the output
   const unsigned int OneSecMicroSec = 1000000;
   const unsigned int LoopSleepMicrosec = OneSecMicroSec / 10;
   const unsigned long timeoutLoopSleeps =
      timeoutSec * (OneSecMicroSec / LoopSleepMicrosec);
   unsigned long elapsedTimeLoopSleeps;

   ReadStatus res_stdout = READSTATUS_UNDEFINED;
   ReadStatus res_stderr = READSTATUS_UNDEFINED;

   p = (ProcessInternal*)h;

   stdout[0] = stdout[1] = 0;
   if (pipe(stdout) < 0) {
      p->log(log_error, "Failed to create pipe for stdout:%s", strerror(errno));
      return PROCESS_FAILED;
   }

   stderr[0] = stderr[1] = 0;
   if (pipe(stderr) < 0) {
      p->log(log_error, "Failed to create pipe for stderr,%s", strerror(errno));
      close(stdout[0]);
      close(stdout[1]);
      return PROCESS_FAILED;
   }

   p->pid = fork();
   if (p->pid == -1) {
      p->log(log_error, "Failed to fork: %s", strerror(errno));
      close(stdout[0]);
      close(stdout[1]);
      close(stderr[0]);
      close(stderr[1]);
      return PROCESS_FAILED;
   } else if (p->pid == 0) {
      // we're in the child. close the read ends of the pipes and exec
      close(stdout[0]);
      close(stderr[0]);
      dup2(stdout[1], STDOUT_FILENO);
      dup2(stderr[1], STDERR_FILENO);
      execv(p->args[0], p->args);

      // exec failed
      exit(127);
   }

   // close write ends of pipes and make reads nonblocking
   close(stdout[1]);
   close(stderr[1]);

   p->stdoutFd = stdout[0];
   flags = fcntl(p->stdoutFd, F_GETFL);
   if (fcntl(p->stdoutFd, F_SETFL, flags | O_NONBLOCK) == -1) {
      p->log(log_warning, "Failed to set stdoutFd status flags, (%s)",
             strerror(errno));
   }

   p->stderrFd = stderr[0];
   flags = fcntl(p->stderrFd, F_GETFL);
   if (fcntl(p->stderrFd, F_SETFL, flags | O_NONBLOCK) == -1) {
      p->log(log_warning, "Failed to set stderrFd status flags, (%s)",
             strerror(errno));
   }

   elapsedTimeLoopSleeps = 0;

   while (1) {
      int processStatus;
      int processFinished = (waitpid(p->pid, &processStatus, WNOHANG) > 0);

      if (processFinished) {
         if (WIFEXITED(processStatus)) {
            p->exitCode = WEXITSTATUS(processStatus);
            p->log(log_info,
                   "Process exited normally after %d seconds, returned %d",
                   elapsedTimeLoopSleeps * LoopSleepMicrosec / OneSecMicroSec,
                   p->exitCode);
         } else if (WIFSIGNALED(processStatus)) {
            p->exitCode = 127;
            p->log(log_error,
                   "Process exited abnormally after %d sec, uncaught signal %d",
                   elapsedTimeLoopSleeps * LoopSleepMicrosec / OneSecMicroSec,
                   WTERMSIG(processStatus));
         }

         break;
      } else {
         if (timeoutLoopSleeps == elapsedTimeLoopSleeps) {
            p->log(log_error, "Timed out waiting for process exit, killing...");
            kill(p->pid, SIGKILL);
         }

         // Empty the pipes.
         ProcessRead(p, &res_stdout, TRUE, FALSE);
         if (res_stdout == READSTATUS_ERROR) {
            p->log(log_error, "Error while reading process output, killing...");
            kill(p->pid, SIGKILL);
         }

         ProcessRead(p, &res_stderr, FALSE, FALSE);
         if (res_stderr == READSTATUS_ERROR) {
            p->log(log_error, "Error while reading process output, killing...");
            kill(p->pid, SIGKILL);
         }

         usleep(LoopSleepMicrosec);
         elapsedTimeLoopSleeps++;
      }
   }

   // Process completed. Now read all the output to EOF.
   ProcessRead(p, &res_stdout, TRUE, TRUE);
   if (res_stdout == READSTATUS_ERROR) {
      p->log(log_error, "Error while reading process output, killing...");
   }

   ProcessRead(p, &res_stderr, FALSE, TRUE);
   if (res_stderr == READSTATUS_ERROR) {
      p->log(log_error, "Error while reading process output, killing...");
   }

   close(stdout[1]);
   close(stderr[1]);
   return PROCESS_SUCCESS;
}


/*
 *------------------------------------------------------------------------------
 *
 * ProcessRead --
 *
 *      Read redirected stdout or stderr.
 *
 * status - as IN  - holds the result from previous read operation.
 *          as OUT - returns the status from the read operation.
 *
 * There are two modes:
 * readToEof = TRUE - block until the read returns 0 (EOF). This signifies
 *                    that the process has exited and there's nothing left.
 * readToEof = FALSE- Just empty the pipe. This will return even if we get
 *             EAGAIN back from the read. Use this in the midst of the poll
 *             loop so the pipe doesn't fill up and block the process.
 *
 *------------------------------------------------------------------------------
 */

static void
ProcessRead(ProcessInternal *p, ReadStatus *status, Bool stdout, Bool readToEof)
{
   char buf[1024];
   size_t currSize, newSize;
   char** saveTo;
   int fd;
   char* stdstr = stdout ? "stdout" : "stderr";

   // which fd do we read and which pointer do we save to?
   fd = stdout ? p->stdoutFd : p->stderrFd;
   saveTo = stdout ? &p->stdoutStr : &p->stderrStr;

   // if there's output waiting, read it out. FDs should already be non-blocking
   do {
      ssize_t count = read(fd, buf, sizeof buf);

      if (count > 0) {
         // save output
         currSize = strlen(*saveTo);
         newSize = count + currSize;
         *saveTo = Util_SafeRealloc(*saveTo, newSize + 1);
         memcpy(*saveTo + currSize, buf, count);
         (*saveTo)[newSize] = '\0';
         p->log(log_info, "Saving output from %s", stdstr);
      } else if (count == 0) {
         if (*status != READSTATUS_DONE) {
            // we're done
            p->log(log_info, "No more output from %s", stdstr);
            *status = READSTATUS_DONE;
         }

         return;
      } else if (count < 0) {
         if (errno == EAGAIN && readToEof) {
            if (*status != READSTATUS_PENDING) {
               // waiting for more output, sleep briefly and try again
               p->log(log_info, "Pending output from %s, trying again", stdstr);
               *status = READSTATUS_PENDING;
            }

            usleep(1000);
         } else if (errno == EAGAIN && !readToEof) {
            if (*status != READSTATUS_PENDING) {
               // caller doesn't want to wait until EOF
               p->log(log_info, "Returning, pending output from %s", stdstr);
               *status = READSTATUS_PENDING;
            }

            return;
         } else {
            // error
            p->log(log_error, "Failed to read from %s: %s",
                   stdstr, strerror(errno));

            *status = READSTATUS_ERROR;
            return;
         }
      }
   } while (1);
}


/*
 *------------------------------------------------------------------------------
 *
 * Process_GetStdout --
 *
 *      Returns process's standard output.
 *
 *------------------------------------------------------------------------------
 */

const char *
Process_GetStdout(ProcessHandle h)
{
   ProcessInternal *p = (ProcessInternal *)h;
   return p->stdoutStr;
}


/*
 *------------------------------------------------------------------------------
 *
 * Process_GetStderr --
 *
 *      Returns process's standard error output.
 *
 *------------------------------------------------------------------------------
 */

const char *
Process_GetStderr(ProcessHandle h)
{
   ProcessInternal *p = (ProcessInternal *)h;
   return p->stderrStr;
}


/*
 *------------------------------------------------------------------------------
 *
 * Process_GetExitCode --
 *
 *      Returns process's exit code.
 *
 *------------------------------------------------------------------------------
 */

int
Process_GetExitCode(ProcessHandle h)
{
   ProcessInternal *p = (ProcessInternal *)h;
   return p->exitCode;
}


/*
 *------------------------------------------------------------------------------
 *
 * Process_Destroy --
 *
 *      Destroys the process and returns result of the operation.
 *
 *------------------------------------------------------------------------------
 */

ProcessError
Process_Destroy(ProcessHandle h)
{
   ProcessInternal* p;
   int i;
   p = (ProcessInternal*)h;
   if (p->stdoutFd >= 0) {
      close(p->stdoutFd);
   }
   if (p->stderrFd >= 0) {
      close(p->stderrFd);
   }
   free(p->stdoutStr);
   free(p->stderrStr);
   if (p->args != NULL) {
      for (i = 0; p->args[i] != NULL; i++) {
         free(p->args[i]);
      }
      free(p->args);
   }
   free(p);
   return PROCESS_SUCCESS;
}
