/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/* dbus-run-session.c - run a child process in its own session
 *
 * Copyright © 2003-2006 Red Hat, Inc.
 * Copyright © 2006 Thiago Macieira <thiago@kde.org>
 * Copyright © 2011-2012 Nokia Corporation
 * Copyright © 2018, 2021 Ralf Habacker
 *
 * SPDX-License-Identifier: AFL-2.1 OR GPL-2.0-or-later
 *
 * Licensed under the Academic Free License version 2.1
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include <config.h>

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _MSC_VER
#include <unistd.h>
#endif

#include <sys/types.h>
#ifdef DBUS_UNIX
#include <sys/wait.h>
#include <signal.h>
#include <dbus/dbus-sysdeps-unix.h>
#else
#include <dbus/dbus-internals.h>
#include <dbus/dbus-sysdeps-win.h>
#endif
#include "dbus/dbus.h"
#include "dbus/dbus-internals.h"

#include "tool-common.h"

#define MAX_ADDR_LEN 512
#define PIPE_READ_END  0
#define PIPE_WRITE_END 1

/* PROCESSES
 *
 * If you are in a shell and run "dbus-run-session myapp", here is what
 * happens (compare and contrast with dbus-launch):
 *
 * shell
 *   \- dbus-run-session myapp
 *      \- dbus-daemon --nofork --print-address --session
 *      \- myapp
 *
 * All processes are long-running.
 *
 * When myapp exits, dbus-run-session kills dbus-daemon and terminates.
 *
 * If dbus-daemon exits, dbus-run-session warns and continues to run.
 *
 * PIPES
 *
 * dbus-daemon --print-address -> bus_address_pipe -> d-r-s
 */

static const char me[] = "dbus-run-session";

static void usage (int ecode) _DBUS_GNUC_NORETURN;

static void
usage (int ecode)
{
  fprintf (stderr,
      "%s [OPTIONS] [--] PROGRAM [ARGUMENTS]\n"
      "%s --version\n"
      "%s --help\n"
      "\n"
      "Options:\n"
      "--dbus-daemon=BINARY       run BINARY instead of dbus-daemon\n"
      "--config-file=FILENAME     pass to dbus-daemon instead of --session\n"
      "\n",
      me, me, me);
  exit (ecode);
}

static void version (void) _DBUS_GNUC_NORETURN;

static void
version (void)
{
  printf ("%s %s\n"
          "Copyright (C) 2003-2006 Red Hat, Inc.\n"
          "Copyright (C) 2006 Thiago Macieira\n"
          "Copyright © 2011-2012 Nokia Corporation\n"
          "Copyright © 2018, 2021 Ralf Habacker\n"
          "\n"
          "This is free software; see the source for copying conditions.\n"
          "There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n",
          me, VERSION);
  exit (0);
}

#ifndef DBUS_WIN
static void oom (void) _DBUS_GNUC_NORETURN;

static void
oom (void)
{
  fprintf (stderr, "%s: out of memory\n", me);
  exit (1);
}

typedef enum
{
  READ_STATUS_OK,    /**< Read succeeded */
  READ_STATUS_ERROR, /**< Some kind of error */
  READ_STATUS_EOF    /**< EOF returned */
} ReadStatus;

static ReadStatus
read_line (int        fd,
           char      *buf,
           size_t     maxlen)
{
  size_t bytes = 0;
  ReadStatus retval;

  memset (buf, '\0', maxlen);
  maxlen -= 1; /* ensure nul term */

  retval = READ_STATUS_OK;

  while (1)
    {
      ssize_t chunk;
      size_t to_read;

    again:
      to_read = maxlen - bytes;

      if (to_read == 0)
        break;

      chunk = read (fd,
                    buf + bytes,
                    to_read);
      if (chunk < 0 && errno == EINTR)
        goto again;

      if (chunk < 0)
        {
          retval = READ_STATUS_ERROR;
          break;
        }
      else if (chunk == 0)
        {
          retval = READ_STATUS_EOF;
          break; /* EOF */
        }
      else /* chunk > 0 */
        bytes += chunk;
    }

  if (retval == READ_STATUS_EOF &&
      bytes > 0)
    retval = READ_STATUS_OK;

  /* whack newline */
  if (retval != READ_STATUS_ERROR &&
      bytes > 0 &&
      buf[bytes-1] == '\n')
    buf[bytes-1] = '\0';

  return retval;
}

static void
exec_dbus_daemon (const char *dbus_daemon,
                  int         bus_address_pipe[2],
                  const char *config_file)
{
  /* Child process, which execs dbus-daemon or dies trying */
#define MAX_FD_LEN 64
  char write_address_fd_as_string[MAX_FD_LEN];

  close (bus_address_pipe[PIPE_READ_END]);

  /* Set all fds >= 3 close-on-execute, except for the one that can't be.
   * We don't want dbus-daemon to inherit random fds we might have
   * inherited from our caller. (Note that we *do* let the wrapped process
   * inherit them in exec_app(), in an attempt to be as close as possible
   * to being a transparent wrapper.) */
  _dbus_fd_set_all_close_on_exec ();
  _dbus_fd_clear_close_on_exec (bus_address_pipe[PIPE_WRITE_END]);

  sprintf (write_address_fd_as_string, "%d", bus_address_pipe[PIPE_WRITE_END]);

  execlp (dbus_daemon,
          dbus_daemon,
          "--nofork",
          "--print-address", write_address_fd_as_string,
          config_file ? "--config-file" : "--session",
          config_file, /* has to be last in this varargs list */
          NULL);

  fprintf (stderr, "%s: failed to execute message bus daemon '%s': %s\n",
           me, dbus_daemon, strerror (errno));
}

static void exec_app (int prog_arg, char **argv) _DBUS_GNUC_NORETURN;

static void
exec_app (int prog_arg, char **argv)
{
  execvp (argv[prog_arg], argv + prog_arg);

  fprintf (stderr, "%s: failed to exec '%s': %s\n", me, argv[prog_arg],
           strerror (errno));
  exit (1);
}

static int
run_session (const char *dbus_daemon,
             const char *config_file,
             char       *bus_address,
             char      **argv,
             int         prog_arg)
{
  pid_t bus_pid;
  pid_t app_pid;
  int bus_address_pipe[2] = { 0, 0 };

  if (pipe (bus_address_pipe) < 0)
    {
      fprintf (stderr, "%s: failed to create pipe: %s\n", me, strerror (errno));
      return 127;
    }

  /* Make sure our output buffers aren't redundantly printed by both the
   * parent and the child */
  fflush (stdout);
  fflush (stderr);

  bus_pid = fork ();

  if (bus_pid < 0)
    {
      fprintf (stderr, "%s: failed to fork: %s\n", me, strerror (errno));
      return 127;
    }

  if (bus_pid == 0)
    {
      /* child */
      exec_dbus_daemon (dbus_daemon, bus_address_pipe, config_file);
      /* not reached */
      return 127;
    }

  close (bus_address_pipe[PIPE_WRITE_END]);

  switch (read_line (bus_address_pipe[PIPE_READ_END], bus_address, MAX_ADDR_LEN))
    {
    case READ_STATUS_OK:
      break;

    case READ_STATUS_EOF:
      fprintf (stderr, "%s: EOF reading address from bus daemon\n", me);
      return 127;
      break;

    case READ_STATUS_ERROR:
      fprintf (stderr, "%s: error reading address from bus daemon: %s\n",
               me, strerror (errno));
      return 127;
      break;

    default:
      _dbus_assert_not_reached ("invalid read result");
    }

  close (bus_address_pipe[PIPE_READ_END]);

  if (!dbus_setenv ("DBUS_SESSION_BUS_ADDRESS", bus_address) ||
      !dbus_setenv ("DBUS_SESSION_BUS_PID", NULL) ||
      !dbus_setenv ("DBUS_SESSION_BUS_WINDOWID", NULL) ||
      !dbus_setenv ("DBUS_STARTER_ADDRESS", NULL) ||
      !dbus_setenv ("DBUS_STARTER_BUS_TYPE", NULL))
    oom ();

  fflush (stdout);
  fflush (stderr);

  app_pid = fork ();

  if (app_pid < 0)
    {
      fprintf (stderr, "%s: failed to fork: %s\n", me, strerror (errno));
      return 127;
    }

  if (app_pid == 0)
    {
      /* child */
      exec_app (prog_arg, argv);
      /* not reached */
      return 127;
    }

  while (1)
    {
      int child_status;
      pid_t child_pid = waitpid (-1, &child_status, 0);

      if (child_pid == (pid_t) -1)
        {
          int errsv = errno;

          if (errsv == EINTR)
            continue;

          /* shouldn't happen: the only other documented errors are ECHILD,
           * which shouldn't happen because we terminate when all our children
           * have died, and EINVAL, which would indicate programming error */
          fprintf (stderr, "%s: waitpid() failed: %s\n", me, strerror (errsv));
          return 127;
        }
      else if (child_pid == bus_pid)
        {
          /* no need to kill it, now */
          bus_pid = 0;

          if (WIFEXITED (child_status))
            fprintf (stderr, "%s: dbus-daemon exited with code %d\n",
                me, WEXITSTATUS (child_status));
          else if (WIFSIGNALED (child_status))
            fprintf (stderr, "%s: dbus-daemon terminated by signal %d\n",
                me, WTERMSIG (child_status));
          else
            fprintf (stderr, "%s: dbus-daemon died or something\n", me);
        }
      else if (child_pid == app_pid)
        {
          if (bus_pid != 0)
            kill (bus_pid, SIGTERM);

          if (WIFEXITED (child_status))
            return WEXITSTATUS (child_status);

          /* if it died from a signal, behave like sh(1) */
          if (WIFSIGNALED (child_status))
            return 128 + WTERMSIG (child_status);

          /* I give up (this should never be reached) */
          fprintf (stderr, "%s: child process died or something\n", me);
          return 127;
        }
      else
        {
          fprintf (stderr, "%s: ignoring unknown child process %ld\n", me,
              (long) child_pid);
        }
    }

  return 0;
}
#else
static int
run_session (const char *dbus_daemon,
             const char *config_file,
             char       *bus_address,
             char      **argv,
             int         prog_arg)
{
  char *dbus_daemon_argv[5];
  int ret = 127;
  HANDLE server_handle = NULL;
  HANDLE app_handle = NULL;
  HANDLE ready_event_handle = NULL;
  DWORD exit_code;
  DBusString argv_strings[4];
  DBusString address;
  char **env = NULL;
  DBusHashTable *env_table = NULL;
  dbus_int64_t sec;
  long usec;
  dbus_bool_t result = TRUE;
  char *key = NULL;
  char *value = NULL;
  DBusError error;

  if (!_dbus_string_init (&argv_strings[0]))
    result = FALSE;
  if (!_dbus_string_init (&argv_strings[1]))
    result = FALSE;
  if (!_dbus_string_init (&argv_strings[2]))
    result = FALSE;
  if (!_dbus_string_init (&argv_strings[3]))
    result = FALSE;
  if (!_dbus_string_init (&address))
    result = FALSE;
  if (!result)
    goto out;

  /* The handle of this event is used by the dbus daemon
   * to signal that connections are ready. */
  dbus_error_init (&error);
  ready_event_handle = _dbus_win_event_create_inheritable (&error);
  if (ready_event_handle == NULL)
    goto out;

  /* run dbus daemon */
  _dbus_get_real_time (&sec, &usec);
  /* On Windows it's difficult to make use of --print-address to
   * convert a listenable address into a connectable address, so instead
   * we tell the temporary dbus-daemon to use the Windows autolaunch
   * mechanism, with a unique scope that is shared by this dbus-daemon,
   * the app process that defines its lifetime, and any other child
   * processes they might have. */
  _dbus_string_append_printf (&address, "autolaunch:scope=dbus-tmp-session-%" DBUS_INT64_MODIFIER "d%ld-" DBUS_PID_FORMAT, sec, usec, _dbus_getpid ());
  _dbus_string_append_printf (&argv_strings[0], "%s", dbus_daemon);
  if (config_file != NULL)
    _dbus_string_append_printf (&argv_strings[1], "--config-file=%s", config_file);
  else
    _dbus_string_append_printf (&argv_strings[1], "--session");
  _dbus_string_append_printf (&argv_strings[2], "--address=%s", _dbus_string_get_const_data (&address));
  _dbus_string_append_printf (&argv_strings[3], "--ready-event-handle=%p", ready_event_handle);
  dbus_daemon_argv[0] = _dbus_string_get_data (&argv_strings[0]);
  dbus_daemon_argv[1] = _dbus_string_get_data (&argv_strings[1]);
  dbus_daemon_argv[2] = _dbus_string_get_data (&argv_strings[2]);
  dbus_daemon_argv[3] = _dbus_string_get_data (&argv_strings[3]);
  dbus_daemon_argv[4] = NULL;

  server_handle = _dbus_spawn_program (dbus_daemon, dbus_daemon_argv, NULL, TRUE, &error);
  if (server_handle == NULL)
    goto out;

  /* wait until dbus-daemon is ready for connections */
  if (ready_event_handle != NULL)
    {
      DWORD status;
      HANDLE events[2];

      _dbus_verbose ("Wait until dbus-daemon is ready for connections (event handle %p)\n", ready_event_handle);

      events[0] = ready_event_handle;
      events[1] = server_handle;
      status = WaitForMultipleObjects (2, events, FALSE, 30000);

      switch (status)
        {
          case WAIT_OBJECT_0:
            /* ready event signalled, everything is okay */
            break;

          case WAIT_OBJECT_0 + 1:
            /* dbus-daemon process has exited */
            dbus_set_error (&error, DBUS_ERROR_SPAWN_CHILD_EXITED, "dbus-daemon exited before signalling ready");
            goto out;

          case WAIT_FAILED:
            _dbus_win_set_error_from_last_error (&error, "Unable to wait for server readiness (handle %p)", ready_event_handle);
            goto out;

          case WAIT_TIMEOUT:
            /* GetLastError() is not set */
            dbus_set_error (&error, DBUS_ERROR_TIMEOUT, "Timed out waiting for server readiness or exit (handle %p)", ready_event_handle);
            goto out;

          default:
            /* GetLastError() is probably not set? */
            dbus_set_error (&error, DBUS_ERROR_FAILED, "Unknown result '%lu' while waiting for server readiness (handle %p)", status, ready_event_handle);
            goto out;
        }
      _dbus_verbose ("Got signal that dbus-daemon is ready for connections\n");
    }

  /* run app */
  env = _dbus_get_environment ();
  env_table = _dbus_hash_table_new (DBUS_HASH_STRING,
                                    dbus_free,
                                    dbus_free);
  if (!_dbus_hash_table_from_array (env_table, env, '='))
    {
      goto out;
    }

  /* replace DBUS_SESSION_BUS_ADDRESS in environment */
  if (!_dbus_string_steal_data (&address, &value))
    goto out;

  key = _dbus_strdup ("DBUS_SESSION_BUS_ADDRESS");

  if (key == NULL)
    goto out;

  if (_dbus_hash_table_insert_string (env_table, key, value))
    {
      /* env_table took ownership, do not free separately */
      key = NULL;
      value = NULL;
    }
  else
    {
      /* we still own key and value, the cleanup code will free them */
      goto out;
    }

  _dbus_hash_table_remove_string (env_table, "DBUS_STARTER_ADDRESS");
  _dbus_hash_table_remove_string (env_table, "DBUS_STARTER_BUS_TYPE");
  _dbus_hash_table_remove_string (env_table, "DBUS_SESSION_BUS_PID");
  _dbus_hash_table_remove_string (env_table, "DBUS_SESSION_BUS_WINDOWID");

  dbus_free_string_array (env);
  env = _dbus_hash_table_to_array (env_table, '=');
  if (!env)
    goto out;

  app_handle = _dbus_spawn_program (argv[prog_arg], argv + prog_arg, env, FALSE, &error);
  if (app_handle == NULL)
    goto out;

  WaitForSingleObject (app_handle, INFINITE);
  if (!GetExitCodeProcess (app_handle, &exit_code))
    {
      _dbus_win_set_error_from_last_error (&error, "Could not fetch exit code");
      goto out;
    }
  ret = exit_code;

out:
  if (dbus_error_is_set (&error))
    tool_stderr_error (me, &error);
  dbus_error_free (&error);
  TerminateProcess (server_handle, 0);
  if (server_handle != NULL)
    CloseHandle (server_handle);
  if (app_handle != NULL)
    CloseHandle (app_handle);
  if (ready_event_handle != NULL)
    _dbus_win_event_free (ready_event_handle, NULL);
  _dbus_string_free (&argv_strings[0]);
  _dbus_string_free (&argv_strings[1]);
  _dbus_string_free (&argv_strings[2]);
  _dbus_string_free (&argv_strings[3]);
  _dbus_string_free (&address);
  dbus_free_string_array (env);
  if (env_table != NULL)
    _dbus_hash_table_unref (env_table);
  dbus_free (key);
  dbus_free (value);
  return ret;
}
#endif

int
main (int argc, char **argv)
{
  int prog_arg = 0;
  const char *config_file = NULL;
  const char *dbus_daemon = NULL;
  char bus_address[MAX_ADDR_LEN] = { 0 };
  const char *prev_arg = NULL;
  int i = 1;
  int requires_arg = 0;

  while (i < argc)
    {
      const char *arg = argv[i];

      if (requires_arg)
        {
          const char **arg_dest;

          assert (prev_arg != NULL);

          if (strcmp (prev_arg, "--config-file") == 0)
            {
              arg_dest = &config_file;
            }
          else if (strcmp (prev_arg, "--dbus-daemon") == 0)
            {
              arg_dest = &dbus_daemon;
            }
          else
            {
              /* shouldn't happen */
              fprintf (stderr, "%s: internal error: %s not fully implemented\n",
                       me, prev_arg);
              return 127;
            }

          if (*arg_dest != NULL)
            {
              fprintf (stderr, "%s: %s given twice\n", me, prev_arg);
              return 127;
            }

          *arg_dest = arg;
          requires_arg = 0;
          prev_arg = arg;
          ++i;
          continue;
        }

      if (strcmp (arg, "--help") == 0 ||
          strcmp (arg, "-h") == 0 ||
          strcmp (arg, "-?") == 0)
        {
          usage (0);
        }
      else if (strcmp (arg, "--version") == 0)
        {
          version ();
        }
      else if (strstr (arg, "--config-file=") == arg)
        {
          const char *file;

          if (config_file != NULL)
            {
              fprintf (stderr, "%s: --config-file given twice\n", me);
              return 127;
            }

          file = strchr (arg, '=');
          ++file;

          config_file = file;
        }
      else if (strstr (arg, "--dbus-daemon=") == arg)
        {
          const char *file;

          if (dbus_daemon != NULL)
            {
              fprintf (stderr, "%s: --dbus-daemon given twice\n", me);
              return 127;
            }

          file = strchr (arg, '=');
          ++file;

          dbus_daemon = file;
        }
      else if (strcmp (arg, "--config-file") == 0 ||
               strcmp (arg, "--dbus-daemon") == 0)
        {
          requires_arg = 1;
        }
      else if (arg[0] == '-')
        {
          if (strcmp (arg, "--") != 0)
            {
              fprintf (stderr, "%s: option '%s' is unknown\n", me, arg);
              return 127;
            }
          else
            {
              prog_arg = i + 1;
              break;
            }
        }
      else
        {
          prog_arg = i;
          break;
        }

      prev_arg = arg;
      ++i;
    }

  /* "dbus-run-session" and "dbus-run-session ... --" are not allowed:
   * there must be something to run */
  if (prog_arg < 1 || prog_arg >= argc)
    {
      fprintf (stderr, "%s: a non-option argument is required\n", me);
      return 127;
    }

  if (requires_arg)
    {
      fprintf (stderr, "%s: option '%s' requires an argument\n", me, prev_arg);
      return 127;
    }

  if (dbus_daemon == NULL)
    dbus_daemon = "dbus-daemon";

  return run_session (dbus_daemon, config_file, bus_address, argv, prog_arg);
}
