/* -*- 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 * 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 #include #include #include #include #include #ifndef _MSC_VER #include #endif #include #ifdef DBUS_UNIX #include #include #include #else #include #include #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; long sec,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-%ld%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); }