/*
 * adverb — Run a child process with various environmental adjustments.
 *
 * Copyright © 2020 Collabora Ltd.
 *
 * SPDX-License-Identifier: MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sysexits.h>

#include <glib.h>
#include <gio/gio.h>

#include "steam-runtime-tools/glib-backports-internal.h"

#include "steam-runtime-tools/file-lock-internal.h"
#include "steam-runtime-tools/utils-internal.h"

static gboolean opt_assert_no_children = FALSE;
static gchar *opt_exclusive_lock = NULL;
static gboolean opt_print_version = FALSE;
static gboolean opt_ignore_sigchld = FALSE;
static gboolean opt_unignore_sigchld = FALSE;
static gboolean opt_block_sigchld = FALSE;
static gboolean opt_unblock_sigchld = FALSE;
static gboolean opt_show_signal_dispositions = FALSE;

static GOptionEntry options[] =
{
  { "assert-no-children", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
    &opt_assert_no_children,
    "Assert that we have no child processes", NULL },
  { "exclusive-lock", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME,
    &opt_exclusive_lock,
    "Take an exclusive lock on PATH", "PATH" },
  { "show-signals", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
    &opt_show_signal_dispositions,
    "Show signal dispositions", NULL },
  { "ignore-sigchld", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
    &opt_ignore_sigchld,
    "Ignore SIGCHLD with SIG_IGN", NULL },
  { "unignore-sigchld", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
    &opt_unignore_sigchld,
    "Don't ignore SIGCHLD (restore default disposition SIG_DFL)", NULL },
  { "block-sigchld", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
    &opt_block_sigchld,
    "Block SIGCHLD with sigprocmask()", NULL },
  { "unblock-sigchld", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
    &opt_unblock_sigchld,
    "Unblock SIGCHLD with sigprocmask()", NULL },

  { "version", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_print_version,
    "Print version number and exit", NULL },
  { NULL }
};

static gboolean
put_back_original_stdout (FILE *original_stdout,
                          GError **error)
{
  int original_stdout_fd = fileno (original_stdout);

  if (original_stdout_fd < 0)
    return glnx_throw_errno_prefix (error,
                                    "Unable to get fileno of original stdout");

  if (dup2 (original_stdout_fd, STDOUT_FILENO) != STDOUT_FILENO)
    return glnx_throw_errno_prefix (error,
                                    "Unable to make fd %d a copy of fd %d",
                                    STDOUT_FILENO, original_stdout_fd);

  return TRUE;
}

int
main (int argc,
      char *argv[])
{
  g_autoptr(SrtFileLock) exclusive_lock = NULL;
  GError *local_error = NULL;
  FILE *original_stdout = NULL;
  int ret = EX_USAGE;
  char **command_and_args;
  int saved_errno;
  GOptionContext *context;

  context = g_option_context_new (
      "[COMMAND [ARG...]]\n"
      "Run COMMAND [ARG...] with environmental adjustments.\n"
      "If no COMMAND is given, just print current status.\n");

  g_option_context_add_main_entries (context, options, NULL);

  if (!g_option_context_parse (context, &argc, &argv, &local_error))
    {
      if (local_error->domain == G_OPTION_ERROR)
        ret = EX_USAGE;
      else
        ret = EX_UNAVAILABLE;

      goto out;
    }

  if (opt_print_version)
    {
      g_print (
          "%s:\n"
          " Package: steam-runtime-tools\n"
          " Version: %s\n",
          argv[0], VERSION);
      ret = 0;
      goto out;
    }

  original_stdout = _srt_divert_stdout_to_stderr (&local_error);

  if (original_stdout == NULL)
    {
      ret = EX_OSERR;
      goto out;
    }

  if (argc >= 2 && strcmp (argv[1], "--") == 0)
    {
      argv++;
      argc--;
    }

  if (argc < 2)
    command_and_args = NULL;
  else
    command_and_args = argv + 1;

  if (opt_block_sigchld && opt_unblock_sigchld)
    {
      g_printerr ("%s: Cannot both block and unblock SIGCHLD",
                  g_get_prgname ());
      goto out;
    }

  if (opt_ignore_sigchld && opt_unignore_sigchld)
    {
      g_printerr ("%s: Cannot both ignore and unignore SIGCHLD",
                  g_get_prgname ());
      goto out;
    }

  ret = EX_OSERR;

  if (opt_assert_no_children)
    {
      pid_t child;
      int wstatus;

      child = waitpid (-1, &wstatus, WNOHANG);
      saved_errno = errno;

      if (child == (pid_t) -1 && saved_errno == ECHILD)
        g_printerr ("# OK: no child processes\n");
      else if (child == (pid_t) -1)
        g_error ("%s", g_strerror (saved_errno));
      else
        g_error ("Should not have had any child processes! (%" G_PID_FORMAT ")",
                 child);
    }

  if (opt_block_sigchld || opt_unblock_sigchld)
    {
      int sig;
      int how;
      sigset_t old_set;
      sigset_t new_set;

      sigemptyset (&old_set);
      sigemptyset (&new_set);
      sigaddset (&new_set, SIGCHLD);

      if (opt_block_sigchld)
        {
          how = SIG_BLOCK;
          g_message ("Blocking SIGCHLD");
        }
      else
        {
          how = SIG_UNBLOCK;
          g_message ("Unblocking SIGCHLD");
        }

      if (sigprocmask (how, NULL, &old_set) != 0)
        {
          saved_errno = errno;
          g_set_error (&local_error, G_IO_ERROR,
                       g_io_error_from_errno (saved_errno),
                       "get sigprocmask: %s", g_strerror (saved_errno));
          goto out;
        }

      for (sig = 1; sig < 64; sig++)
        {
          /* sigismember returns -1 for non-signals, which we ignore */
          if (sigismember (&old_set, sig) == 1)
            g_message ("Before: signal %d (%s) blocked", sig, g_strsignal (sig));
        }

      if (sigprocmask (how, &new_set, &old_set) != 0)
        {
          saved_errno = errno;
          g_set_error (&local_error, G_IO_ERROR,
                       g_io_error_from_errno (saved_errno),
                       "set sigprocmask: %s", g_strerror (saved_errno));
          goto out;
        }

      if (sigprocmask (how, NULL, &new_set) != 0)
        {
          saved_errno = errno;
          g_set_error (&local_error, G_IO_ERROR,
                       g_io_error_from_errno (saved_errno),
                       "get sigprocmask: %s", g_strerror (saved_errno));
          goto out;
        }

      for (sig = 1; sig < 64; sig++)
        {
          if (sigismember (&new_set, sig) == 1)
            g_message ("After: signal %d (%s) blocked", sig, g_strsignal (sig));
        }
    }

  if (opt_ignore_sigchld || opt_unignore_sigchld)
    {
      struct sigaction action = { .sa_handler = SIG_DFL };
      struct sigaction old_action = { .sa_handler = SIG_DFL };

      if (opt_ignore_sigchld)
        {
          g_message ("Ignoring SIGCHLD");
          action.sa_handler = SIG_IGN;
        }
      else
        {
          g_message ("Unignoring SIGCHLD");
          action.sa_handler = SIG_DFL;
        }

      if (sigaction (SIGCHLD, &action, &old_action) != 0)
        {
          saved_errno = errno;
          g_set_error (&local_error, G_IO_ERROR,
                       g_io_error_from_errno (saved_errno),
                       "sigaction: %s", g_strerror (saved_errno));
          goto out;
        }

      g_message ("SIG_DFL:            %p", (void *) SIG_DFL);
      g_message ("SIG_IGN:            %p", (void *) SIG_IGN);
      g_message ("Old signal handler: %p", old_action.sa_handler);
      g_message ("Old flags:          0x%x", old_action.sa_flags);
      g_message ("New signal handler: %p", action.sa_handler);
      g_message ("New flags:          0x%x", action.sa_flags);
    }

  if (opt_show_signal_dispositions)
    {
      int sig;
      sigset_t set;

      sigemptyset (&set);

      if (sigprocmask (SIG_UNBLOCK, NULL, &set) != 0)
        {
          saved_errno = errno;
          g_set_error (&local_error, G_IO_ERROR,
                       g_io_error_from_errno (saved_errno),
                       "get sigprocmask: %s", g_strerror (saved_errno));
          goto out;
        }

      for (sig = 1; sig < 64; sig++)
        {
          if (sigismember (&set, sig) == 1)
            g_message ("Signal %s (%d) is blocked", g_strsignal (sig), sig);
        }

      sigfillset (&set);

      for (sig = 1; sig < 64; sig++)
        {
          if (sigismember (&set, sig) == 1)
            {
              int masked;
              struct sigaction action = { .sa_handler = SIG_DFL };

              if (sigaction (sig, NULL, &action) != 0)
                {
                  saved_errno = errno;
                  g_set_error (&local_error, G_IO_ERROR,
                               g_io_error_from_errno (saved_errno),
                               "get sigaction: %s", g_strerror (saved_errno));
                  goto out;
                }

              if (action.sa_handler != SIG_DFL || action.sa_flags != 0)
                {
                  g_message ("Signal %s (%d) handler: %p",
                             g_strsignal (sig), sig,
                             action.sa_handler);
                  g_message ("Signal %s (%d) flags: 0x%x",
                             g_strsignal (sig), sig,
                             action.sa_flags);
                }

              for (masked = 1; masked < 64; masked++)
                {
                  if (sigismember (&action.sa_mask, masked) == 1)
                    {
                      g_message ("Signal %s (%d) blocks signal %s (%d)",
                                 g_strsignal (sig), sig,
                                 g_strsignal (masked), masked);
                    }
                }
            }
        }
    }

  if (opt_exclusive_lock != NULL)
    {
      exclusive_lock = srt_file_lock_new (AT_FDCWD, opt_exclusive_lock,
                                          (SRT_FILE_LOCK_FLAGS_EXCLUSIVE
                                           | SRT_FILE_LOCK_FLAGS_WAIT),
                                          &local_error);

      if (exclusive_lock == NULL)
        goto out;
    }

  if (!put_back_original_stdout (original_stdout, &local_error))
    goto out;

  if (command_and_args == NULL)
    {
      ret = EX_OK;
    }
  else
    {
      /* Doesn't return if successful */
      execvp (command_and_args[0], command_and_args);

      saved_errno = errno;
      g_set_error (&local_error, G_IO_ERROR,
                   g_io_error_from_errno (saved_errno),
                   "execvp %s: %s",
                   command_and_args[0], g_strerror (saved_errno));
    }

out:
  g_option_context_free (context);

  if (local_error != NULL)
    g_warning ("%s", local_error->message);

  g_clear_error (&local_error);
  return ret;
}
