/* Copyright © Triad National Security, LLC, and others. */

#define _GNU_SOURCE
#include "config.h"

#include <pwd.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include "all.h"


/** External variables **/

/* Path to host temporary directory. Set during command line processing. */
char *host_tmp = NULL;

/* Username of invoking user. Set during early initialization. */
char *username = NULL;


/** Functions **/

#undef execvpe
noreturn execvpe_ch(const char *path, char *const argv[], char *const envp[])
{
   execvpe(path, argv, envp);  // only returns on error
   if (errno == ENOEXEC)
      ERROR (errno, "can't exec: bad format: wrong architecture?: %s", path);
   else
      ERROR (errno, "can't exec: %s", path);
   exit(EXIT_ERR_CMD);
}
#define execvpe FN_BLOCKED

/** Run a command, wait for it to exit, then return. If the command fails,
    exit with a fatal error. Similar to Python’s @c subprocess.run().

    @param cmd    Command to run. If a path contains a slash, run that file
                  directly; otherwise, search in @c $PATH; i.e. the same
                  behavior as @c execlp(3).

    @param pathx  Colon-separated string suitable for @c $PATH to append to
                  the existing @c $PATH (e.g. “@c /foo:/bar”)., or @c NULL if
                  none. Neither the child nor the current process” @c $PATH is
                  modified. May not begin/end with colon.

    @param args2  Null-terminated array of arguments for the command, like
                  @c execv(3).

    @param args1  Null-terminated variadic arguments for the command, like
                  @c execl(3). If not used, caller must provide @c NULL.

    @warning
    Both @p args1 and @p args2 can be non-NULL simultaneously. In this case,
    @p args1 precedes @p args2 on the @p cmd command line, which is the
    opposite order from the function call.

    Design decisions:

    #. Manual @c fork(2) / @c exec(2) rather than @c posix_spawn(3) for better
       error reporting.

    #. @c fork(2) rather than @c vfork(2) or @c clone(2) because I didn’t feel
       like thinking hard enough to figure out whether one of the latter two
       would be better.

    #. @c fork(2) rather than @c fork_ch() because we don’t want garbage
       collection overhead. */
void run_wait(char *cmd, char *pathx, char **args2, ...)
{
   pid_t child;

   T__ (pathx && pathx[0] != '\0');
   T__ (pathx[0] != ':' && pathx[strlen(pathx)-1] != ':');

#undef fork
   Tfe (0 <= (child = fork()), "can't fork: %s", cmd);
#define fork FN_BLOCKED
   if (child) {  // parent
      int ws;
      DEBUG("run_wait: waiting ...");
      T_e (-1 != waitpid(child, &ws, 0));
      if (WIFEXITED(ws)) {
         Zf_ (WEXITSTATUS(ws), "%s: failed with %d", cmd, WEXITSTATUS(ws));
      } else {
         // no WUNTRACED so assume WIFSIGNALED
         Tf_ (0, "%s: killed by signal %d", cmd, WTERMSIG(ws));
      }
   } else {      // child
      char **args, **env_saved;
      char *arg, *path;
      va_list ap;

      // append pathx for execvpe_ch()
      env_saved = list_copy(environ, sizeof(environ[0]));
      path = cats(3, getenv("PATH"), (pathx == NULL ? "" : ":"), pathx);
      setenv("PATH", path, true);
      DEBUG("run_wait: PATH=%s", path);

      // prepare command arguments
      args = NULL;
      list_append((void **)&args, &cmd, sizeof(cmd));
      va_start(ap, args2);
      while ((arg = va_arg(ap, char *)))
         list_append((void **)&args, &arg, sizeof(arg));
      va_end(ap);
      list_cat((void **)&args, args2, sizeof(char *));

      // replace self with command
      T__ (streq(cmd, args[0]));
      DEBUG("run_wait: $ %s", argv_to_string(args));
      execvpe_ch(cmd, args, env_saved);
   }
}

/* Set the username global by looking up EUID in the password database. Logic
   must match ch.user(). Formerly, we used $USER, but that’s not reliably set.
   See #1162. This approach does require that EUID *have* a corresponding
   username. */
void username_set(void)
{
   struct passwd *pw;

   errno = 0;
   Tfe (pw = getpwuid(geteuid()), "can't get username for EUID %d", geteuid());
   username = pw->pw_name;
}

/* Report the version number. */
void version(void)
{
   fprintf(stderr, "%s\n", VERSION);
}
