diff --git a/build-relocatable-install.py b/build-relocatable-install.py index c87f026929e8eea0ba969f8fbbf0707a09826265..51be981e594c9d8d0ddcebb141b78cf81f973e92 100755 --- a/build-relocatable-install.py +++ b/build-relocatable-install.py @@ -109,6 +109,8 @@ SCRIPTS = [ ] EXECUTABLES = [ 'pressure-vessel-adverb', + 'pressure-vessel-launch', + 'pressure-vessel-launcher', 'pressure-vessel-try-setlocale', 'pressure-vessel-wrap', ] diff --git a/man/launch.1.md b/man/launch.1.md new file mode 100644 index 0000000000000000000000000000000000000000..8d5115a592e22d9c375a7a95ae82afd04b70f03a --- /dev/null +++ b/man/launch.1.md @@ -0,0 +1,121 @@ +--- +title: pressure-vessel-launch +section: 1 +... + +# NAME + +pressure-vessel-launch - client to launch processes in a container + +# SYNOPSIS + +**pressure-vessel-launch** +[**--clear-env**] +[**--directory** *DIR*] +[**--env** _VAR_**=**_VALUE_] +[**--forward-fd** *FD*] +[**--verbose**] +{**--bus-name** *NAME*|**--dbus-address** *ADDRESS*|**--socket** *SOCKET*} +[**--**] +*COMMAND* [*ARGUMENTS...*] + +# DESCRIPTION + +**pressure-vessel-launch** connects to an `AF_UNIX` socket established +by **pressure-vessel-launcher**(1), and executes an arbitrary command +as a subprocess of **pressure-vessel-launcher**. + +# OPTIONS + +**--socket** *PATH*, **--socket** *@ABSTRACT* +: Connect to the given `AF_UNIX` socket, which can either be an + absolute path, or `@` followed by an arbitrary string. + An absolute path indicates a filesystem-based socket, which is + associated with the filesystem and can be shared between filesystem + namespaces by bind-mounting. + `@` indicates an an abstract socket, which is associated with a + network namespace, is shared between all containers that are in + the same network namespace, and cannot be shared across network + namespace boundaries. + +**--dbus-address** *ADDRESS* +: The same as **--socket**, but the socket is specified in the form + of a D-Bus address. + +**--bus-name** *NAME* +: Connect to the well-known D-Bus session bus and send commands to + the given *NAME*, which is assumed to be owned by + **pressure-vessel-launcher**. + +**--clear-env** +: The *COMMAND* runs in an empty environment, apart from any environment + variables set by **--env**. By default, it inherits environment + variables from **pressure-vessel-launcher**, with **--env** + overriding individual variables. + +**--directory** *DIR* +: Arrange for the *COMMAND* to run in *DIR*. + By default, it inherits the current working directory from + **pressure-vessel-launcher**. + +**--env** _VAR=VALUE_ +: Set environment variable _VAR_ to _VALUE_. + This is mostly equivalent to using + **env** _VAR=VALUE_ *COMMAND* *ARGUMENTS...* + as the command. + +**--forward-fd** *FD* +: Arrange for the *COMMAND* to receive file descriptor number *FD* + from outside the container. File descriptors 0, 1 and 2 + (standard input, standard output and standard error) are always + forwarded. + +**--verbose** +: Be more verbose. + +# OUTPUT + +The standard output from *COMMAND* is printed on standard output. + +The standard error from *COMMAND* is printed on standard error. +Diagnostic messages from **pressure-vessel-launch** may also be printed +on standard error. + +# EXIT STATUS + +The exit status is similar to **env**(1): + +0 +: The *COMMAND* exited successfully with status 0. + +125 +: Invalid arguments were given, or **pressure-vessel-launch** failed + to start. + +126 +: Reserved to indicate inability to launch the *COMMAND*. + This is not currently distinguished from exit status 125. + +127 +: Reserved to indicate that the *COMMAND* was not found. + This is not currently distinguished from exit status 125. + +128 +: The *COMMAND* was launched, but its exit status could not be + determined. This happens if the wait-status was neither + normal exit nor termination by a signal. It also happens if + **pressure-vessel-launch** was disconnected from the D-Bus + session bus or the **--socket** before the exit status could be + determined. + +Any value less than 128 +: The *COMMAND* exited unsuccessfully with the status indicated. + +128 + *n* +: The *COMMAND* was killed by signal *n*. + +# EXAMPLES + +See **pressure-vessel-launcher**(1). + +<!-- vim:set sw=4 sts=4 et: --> diff --git a/man/launcher.1.md b/man/launcher.1.md new file mode 100644 index 0000000000000000000000000000000000000000..f6c292ed6f94944c93a1e902dffb77960c037490 --- /dev/null +++ b/man/launcher.1.md @@ -0,0 +1,226 @@ +--- +title: pressure-vessel-launcher +section: 1 +... + +# NAME + +pressure-vessel-launcher - server to launch processes in a container + +# SYNOPSIS + +**pressure-vessel-launcher** +[**--replace**] +[**--verbose**] +{**--bus-name** *NAME*|**--socket** *SOCKET*|**--socket-directory** *PATH*} + +# DESCRIPTION + +**pressure-vessel-launcher** listens on an `AF_UNIX` socket or the +D-Bus session bus, and executes arbitrary commands as subprocesses. + +# OPTIONS + +**--socket** *PATH*, **--socket** *@ABSTRACT* +: Listen on the given `AF_UNIX` socket, which can either be an + absolute path, or `@` followed by an arbitrary string. In both cases, + the length is limited to 107 bytes and the value must not contain + ASCII control characters. + An absolute path indicates a filesystem-based socket, which is + associated with the filesystem and can be shared between filesystem + namespaces by bind-mounting. + The path must not exist before **pressure-vessel-launcher** is run. + `@` indicates an an abstract socket, which is associated with a + network namespace, is shared between all containers that are in + the same network namespace, and cannot be shared across network + namespace boundaries. + +**--socket-directory** *PATH* +: Create a filesystem-based socket with an arbitrary name in the + given directory and listen on that. Clients can use the information + printed on standard output to connect to the socket. Due to + limitations of `AF_UNIX` socket addresses, the absolute path to the + directory must be no more than 64 bytes. It must not contain ASCII + control characters. + +**--bus-name** *NAME* +: Connect to the well-known D-Bus session bus, request the given name + and listen for commands there. + +**--replace** +: When used with **--bus-name**, allow other + **pressure-vessel-launcher** processes to take over the bus name, + and exit with status 0 if that happens. This option is ignored + if **--bus-name** is not used. + +**--verbose** +: Be more verbose. + +# OUTPUT + +**pressure-vessel-launcher** prints zero or more lines of +structured text on standard output, and then closes standard output: + +**socket=**PATH +: The launcher is listening on *PATH*, and can be contacted by + **pressure-vessel-launch --socket** _PATH_ + or by connecting a peer-to-peer D-Bus client to a D-Bus address + corresponding to _PATH_. + +**dbus_address=**ADDRESS +: The launcher is listening on *ADDRESS*, and can be contacted by + **pressure-vessel-launch --dbus-address** _ADDRESS_, + or by connecting another peer-to-peer D-Bus client + (such as **dbus-send --peer=ADDRESS**) to _ADDRESS_. + +**bus_name=**NAME +: The launcher is listening on the well-known D-Bus session bus, + and can be contacted by + **pressure-vessel-launch --bus-name** *NAME*, + or by connecting an ordinary D-Bus client + (such as **dbus-send --session**) to the session bus and sending + method calls to _NAME_. + +Clients must wait until after standard output has been closed, or wait +for the bus name to appear, before connecting by bus name. + +Clients must wait until after standard output has been closed before +connecting by socket name. + +Unstructured diagnostic messages are printed on standard error, +which remains open throughout. + +# EXIT STATUS + +0 +: Terminated gracefully by a signal, by being disconnected from the + D-Bus session bus after having obtained *NAME* (with **--bus-name**), + or by *NAME* being replaced by another process + (with **--bus-name** and **--replace**). + +64 (**EX_USAGE** from **sysexits.h**) +: Invalid arguments were given. + +Other nonzero values +: Startup failed. + +# INTENDED USE IN STEAM + +## Once per game launch + +When a game is launched, run one **pressure-vessel-launcher** per game +(whatever that means in practice - probably one per app-ID) as an +asynchronous child process, wrapped in **pressure-vessel-wrap** +(either directly or by using **run-in-steamrt** or **run-in-scout**), +with the write end of a **pipe**(2) as its standard output. +Read from the read end of the pipe until EOF is reached, +then leave it running. + +Steam should choose a **--socket**, **--socket-directory** or +**--bus-name** according to the granularity required. + +If the **realpath**(3) of **~/.steam** is sufficiently short, then Steam +could use a subdirectory of that path as a **--socket-directory**, +but if the user's home directory is very long, that might not work +(the maximum length for a **--socket-directory** is only 64 bytes). + +Otherwise, if Steam itself is in a container (for example if +**/.flatpak-info** exists), then it must choose a way to communicate +that can be shared with other containers and the host system. Using the +D-Bus session bus and **--bus-name** is probably the most reliable option +in this case. + +If Steam is not in a container, then it can create a random subdirectory +of **/tmp**, for example with **mkdtemp**(3), and either use it as a +**--socket-directory** or create a **--socket** with a meaningful name +in that directory. + +In the case of a **--socket-directory**, the socket can be determined by +parsing the data read from standard output. + +When the game is to be terminated, Steam should send **SIGINT** or +**SIGTERM** to the child process (which by now will be the +**pressure-vessel-adverb** process that replaces **pressure-vessel-wrap**), +and to the process group that it leads (if any): + + assert (child_pid > 1); + kill (child_pid, SIGTERM); + kill (-child_pid, SIGTERM); + +It must not use **SIGKILL**, because that cannot be forwarded across +the IPC connection. + +## Once per command + +For each command that is to be run as part of the game, Steam must +run **pressure-vessel-launch**(1) with a **--socket**, **--dbus-address** +or **--bus-name** option that indicates how to communicate with the +launcher. Alternatively, it could do this itself by using D-Bus. + +If Steam needs to set environment variables for the commands that are +run as part of the game, it can do so in one of two ways: + +* If the environment variable is equally valid for all commands, it can + be part of the environment of **pressure-vessel-launcher**. +* If the environment variable is specific to one command, Steam can pass + **--env VAR=VALUE** to **pressure-vessel-launch**. + +If the command is to be terminated without affecting other commands, +Steam should send **SIGINT** to **pressure-vessel-launch**. +It must not use **SIGKILL**, because that cannot be forwarded across the +IPC connection. It should not use **SIGTERM** unless the entire game is +being terminated, because that's only sent to a single process and not +to an entire process group. + +# EXAMPLES + +Listen on the session bus, and run two commands, and exit: + + name="com.steampowered.PressureVessel.App${SteamAppId}" + pressure-vessel-wrap \ + ... \ + -- \ + pressure-vessel-launcher --bus-name "$name" & + launcher_pid="$!" + gdbus wait --session --bus-name "$name" + + pressure-vessel-launch \ + --bus-name "$name" \ + -- \ + ls -al + pressure-vessel-launch \ + --bus-name "$name" \ + -- \ + id + + kill -TERM "$launcher_pid" + wait "$launcher_pid" + +Listen on a socket in a temporary directory: + + tmpdir="$(mktemp -d)" + mkfifo "${tmpdir}/fifo" + pressure-vessel-wrap \ + --filesystem="${tmpdir}" \ + ... \ + -- \ + pressure-vessel-launcher \ + --socket="${tmpdir}/launcher" > "${tmpdir}/fifo" & + launcher_pid="$!" + # Wait for EOF so we know the socket is available + cat "${tmpdir}/fifo" > /dev/null + + pressure-vessel-launch \ + --socket="${tmpdir}/launcher" \ + -- \ + ls -al + pressure-vessel-launch \ + --socket="${tmpdir}/launcher" \ + -- \ + id + + kill -TERM "$launcher_pid" + wait "$launcher_pid" + rm -fr "$tmpdir" + +<!-- vim:set sw=4 sts=4 et: --> diff --git a/man/meson.build b/man/meson.build index 919b63e39eca260c135d60c85dbb7f6ce6974cac..0c19932021196115a8818f69bab9f551590893c9 100644 --- a/man/meson.build +++ b/man/meson.build @@ -33,7 +33,7 @@ else pandoc_markdown_nosmart = 'markdown' endif -foreach tool : ['locale-gen', 'try-setlocale'] +foreach tool : ['launch', 'launcher', 'locale-gen', 'try-setlocale'] custom_target( tool + '.1', build_by_default : true, diff --git a/meson.build b/meson.build index 887dcd8ee605be261c9daef98f5a897a63e3beb3..808a289f048277024ed3f9488236a516fef92665 100644 --- a/meson.build +++ b/meson.build @@ -129,6 +129,19 @@ c_compiler = meson.get_compiler('c') supported_warning_cflags = c_compiler.get_supported_arguments(warning_cflags) add_global_arguments(supported_warning_cflags, language : 'c') +# gdbus-codegen in scout generates code that has many warnings +silence_gdbus_codegen_warnings = c_compiler.get_supported_arguments([ + '-Wno-error', + '-Wno-error=discarded-qualifiers', + '-Wno-error=redundant-decls', + '-Wno-error=shadow', + '-Wno-error=write-strings', + '-Wno-discarded-qualifiers', + '-Wno-redundant-decls', + '-Wno-shadow', + '-Wno-write-strings', +]) + foreach flag : no_warning_cflags supported_no_warning_cflags = c_compiler.get_supported_arguments([ '-Wno-error=' + flag, @@ -142,6 +155,7 @@ add_project_arguments(supported_warning_cflags, language : 'c') libglnx = subproject('libglnx') +threads = dependency('threads') steam_runtime_tools = dependency( 'steam-runtime-tools-0', version : '>=0.20200331.1', diff --git a/src/com.steampowered.PressureVessel.Launcher1.xml b/src/com.steampowered.PressureVessel.Launcher1.xml new file mode 100644 index 0000000000000000000000000000000000000000..ee47e85ce995de5462f41ab9917c32c0d961c17b --- /dev/null +++ b/src/com.steampowered.PressureVessel.Launcher1.xml @@ -0,0 +1,143 @@ +<!DOCTYPE node PUBLIC +"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> + +<!-- + Copyright (C) 2018 Red Hat, Inc. + Copyright (C) 2020 Collabora Ltd. + + This library 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; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General + Public License along with this library; if not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + + Based on org.freedesktop.portal.Flatpak. + Author: Alexander Larsson <alexl@redhat.com> +--> + +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + <!-- + com.steampowered.PressureVessel.Launcher1: + @short_description: Interface for process-launching + + Methods on this interface can be invoked by processes outside + the game's container (and in particular Steam), to start and + control programs inside the container. + + This documentation describes version 0 of this interface. + --> + <interface name='com.steampowered.PressureVessel.Launcher1'> + + <property name="Version" type="u" access="read"/> + + <!-- + SupportedLaunchFlags: + + A bitmask containing all the flags that can be passed to + the Launch() method. + --> + <property name="SupportedLaunchFlags" type="u" access="read"/> + + <!-- + Launch: + @cwd_path: the working directory for the new process, resolved + in the container's filesystem namespace. The empty string means + use whatever is the launcher's current working directory. + @argv: the argv for the new process, starting with the executable to launch + @fds: an array of file descriptors to pass to the new process + @envs: an array of variable/value pairs for the environment of the new process + @flags: flags + @options: Vardict with optional further information + @pid: the PID of the new process inside pressure-vessel's container + + Start a new program in the container. + + The following flags values are supported: + + <variablelist> + <varlistentry> + <term>1</term> + <listitem><para> + Clear the environment. If set, the environment will be + exactly @envs, similar to `env - X=1 Y=2 COMMAND`. + If unset, @envs are used to override environment variables + and any environment variable that is not overridden is + left unchanged, similar to `env X=1 Y=2 COMMAND`. + </para></listitem> + </varlistentry> + </variablelist> + + Unknown (unsupported) flags are an error and will cause Launch() + to fail. + + No options are currently known. Unknown (unsupported) options + are ignored. + + This method returns as soon as the process ID of the new process + is known, and before the process exits. If you need to know when + the process exits, subscribe to the ProcessExited signal before + calling this method, and use the returned process ID to correlate + signals with method calls. + --> + <method name="Launch"> + <annotation name="org.gtk.GDBus.C.UnixFD" value="true"/> + <arg type='ay' name='cwd_path' direction='in'/> + <arg type='aay' name='argv' direction='in'/> + <arg type='a{uh}' name='fds' direction='in'/> + <arg type='a{ss}' name='envs' direction='in'/> + <arg type='u' name='flags' direction='in'/> + <arg type="a{sv}" name="options" direction="in"/> + <arg type='u' name='pid' direction='out'/> + </method> + + <!-- + SendSignal: + @pid: the PID inside the container to signal + @signal: the signal to send (see `signal(7)`) + @to_process_group: whether to send the signal to the process group + + This methods let you send a Unix signal to a process + that was started with com.steampowered.PressureVessel.Launcher1.Launch(). + The @pid argument here should be the PID that is returned + by the Launch() call: it is not necessarily valid in the caller's + PID namespace. + --> + <method name="SendSignal"> + <arg type='u' name='pid' direction='in'/> + <arg type='u' name='signal' direction='in'/> + <arg type='b' name='to_process_group' direction='in'/> + </method> + + <!-- + ProcessExited: + @pid: the PID of the process that has ended + @wait_status: the wait status (see waitpid(2)) + + Emitted when a process started by + com.steampowered.PressureVessel.Launcher1.Launch() exits. + Use g_spawn_check_exit_status(), or the macros such as + `WIFEXITED` documented in `waitpid(2)`, to interpret + the @wait_status. + + This signal is not emitted for processes that were not launched + directly by Launch(), for example if a process launched by + Launch() runs foreground or background child processes. + --> + <signal name="ProcessExited"> + <arg type='u' name='pid'/> + <arg type='u' name='wait_status'/> + </signal> + + </interface> + +</node> diff --git a/src/launch.c b/src/launch.c new file mode 100644 index 0000000000000000000000000000000000000000..1f0f91b449bbb9df4c4b0ff2094120e0b52dd731 --- /dev/null +++ b/src/launch.c @@ -0,0 +1,697 @@ +/* + * pressure-vessel-launch — send IPC requests to create child processes + * + * Copyright © 2018 Red Hat, Inc. + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Based on flatpak-spawn from the flatpak-xdg-utils package. + * Copyright © 2018 Red Hat, Inc. + * Authors: + * Alexander Larsson <alexl@redhat.com> + */ + +#include "config.h" +#include "subprojects/libglnx/config.h" + +#include <locale.h> +#include <sysexits.h> +#include <sys/signalfd.h> + +#include <glib.h> +#include <glib/gstdio.h> +#include <glib-unix.h> +#include <gio/gio.h> +#include <gio/gunixfdlist.h> + +#include "glib-backports.h" +#include "libglnx/libglnx.h" + +#include "launcher.h" +#include "utils.h" + +static const char *service_bus_name = NULL; +static const char *service_iface = LAUNCHER_IFACE; +static const char *service_obj_path = LAUNCHER_PATH; + +typedef GUnixFDList AutoUnixFDList; +G_DEFINE_AUTOPTR_CLEANUP_FUNC(AutoUnixFDList, g_object_unref) + +static GDBusConnection *bus_or_peer_connection = NULL; +static guint child_pid = 0; +static int launch_exit_status = LAUNCH_EX_USAGE; + +static void +process_exited_cb (G_GNUC_UNUSED GDBusConnection *connection, + G_GNUC_UNUSED const gchar *sender_name, + G_GNUC_UNUSED const gchar *object_path, + G_GNUC_UNUSED const gchar *interface_name, + G_GNUC_UNUSED const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GMainLoop *loop = user_data; + guint32 client_pid = 0; + guint32 exit_status = 0; + + if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)"))) + return; + + g_variant_get (parameters, "(uu)", &client_pid, &exit_status); + g_debug ("child exited %d: %d", client_pid, exit_status); + + if (child_pid == client_pid) + { + int exit_code = 0; + + if (WIFEXITED (exit_status)) + { + exit_code = WEXITSTATUS (exit_status); + } + else if (WIFSIGNALED (exit_status)) + { + /* Smush the signal into an unsigned byte, as the shell does. This is + * not quite right from the perspective of whatever ran flatpak-spawn + * — it will get WIFEXITED() not WIFSIGNALED() — but the + * alternative is to disconnect all signal() handlers then send this + * signal to ourselves and hope it kills us. + */ + exit_code = 128 + WTERMSIG (exit_status); + } + else + { + /* wait(3p) claims that if the waitpid() call that returned the exit + * code specified neither WUNTRACED nor WIFSIGNALED, then exactly one + * of WIFEXITED() or WIFSIGNALED() will be true. + */ + g_warning ("exit status %d is neither WIFEXITED() nor WIFSIGNALED()", + exit_status); + exit_code = LAUNCH_EX_CANNOT_REPORT; + } + + g_debug ("child exit code %d: %d", client_pid, exit_code); + launch_exit_status = exit_code; + g_main_loop_quit (loop); + } +} + +static void +forward_signal (int sig) +{ + g_autoptr(GVariant) reply = NULL; + gboolean to_process_group = FALSE; + g_autoptr(GError) error = NULL; + + if (child_pid == 0) + { + /* We are not monitoring a child yet, so let the signal act on + * this main process instead */ + if (sig == SIGTSTP || sig == SIGSTOP || sig == SIGTTIN || sig == SIGTTOU) + { + raise (SIGSTOP); + } + else if (sig != SIGCONT) + { + sigset_t mask; + + sigemptyset (&mask); + sigaddset (&mask, sig); + /* Unblock it, so that it will be delivered properly this time. + * Use pthread_sigmask instead of sigprocmask because the latter + * has unspecified behaviour in a multi-threaded process. */ + pthread_sigmask (SIG_UNBLOCK, &mask, NULL); + raise (sig); + } + + return; + } + + g_debug ("Forwarding signal: %d", sig); + + /* We forward stop requests as real stop, because the default doesn't + seem to be to stop for non-kernel sent TSTP??? */ + if (sig == SIGTSTP) + sig = SIGSTOP; + + /* ctrl-c/z is typically for the entire process group */ + if (sig == SIGINT || sig == SIGSTOP || sig == SIGCONT) + to_process_group = TRUE; + + reply = g_dbus_connection_call_sync (bus_or_peer_connection, + service_bus_name, /* NULL if p2p */ + service_obj_path, + service_iface, + "SendSignal", + g_variant_new ("(uub)", + child_pid, sig, + to_process_group), + G_VARIANT_TYPE ("()"), + G_DBUS_CALL_FLAGS_NONE, + -1, NULL, &error); + + if (error) + g_debug ("Failed to forward signal: %s", error->message); + + if (sig == SIGSTOP) + { + g_debug ("SIGSTOP:ing myself"); + raise (SIGSTOP); + } +} + +static gboolean +forward_signal_handler (int sfd, + G_GNUC_UNUSED GIOCondition condition, + G_GNUC_UNUSED gpointer data) +{ + struct signalfd_siginfo info; + ssize_t size; + + size = read (sfd, &info, sizeof (info)); + + if (size < 0) + { + if (errno != EINTR && errno != EAGAIN) + g_warning ("Unable to read struct signalfd_siginfo: %s", + g_strerror (errno)); + } + else if (size != sizeof (info)) + { + g_warning ("Expected struct signalfd_siginfo of size %" + G_GSIZE_FORMAT ", got %" G_GSSIZE_FORMAT, + sizeof (info), size); + } + else + { + forward_signal (info.ssi_signo); + } + + return G_SOURCE_CONTINUE; +} + +static guint +forward_signals (void) +{ + static int forward[] = { + SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGCONT, SIGTSTP, SIGUSR1, SIGUSR2 + }; + sigset_t mask; + guint i; + int sfd; + + sigemptyset (&mask); + + for (i = 0; i < G_N_ELEMENTS (forward); i++) + sigaddset (&mask, forward[i]); + + sfd = signalfd (-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); + + if (sfd < 0) + { + g_warning ("Unable to watch signals: %s", g_strerror (errno)); + return 0; + } + + /* + * We have to block the signals, for two reasons: + * - If we didn't, most of them would kill our process. + * Listening for a signal with a signalfd does not prevent the signal's + * default disposition from being acted on. + * - Reading from a signalfd only returns information about the signals + * that are still pending for the process. If we ignored them instead + * of blocking them, they would no longer be pending by the time the + * main loop wakes up and reads from the signalfd. + */ + pthread_sigmask (SIG_BLOCK, &mask, NULL); + + return g_unix_fd_add (sfd, G_IO_IN, forward_signal_handler, NULL); +} + +static void +name_owner_changed (G_GNUC_UNUSED GDBusConnection *connection, + G_GNUC_UNUSED const gchar *sender_name, + G_GNUC_UNUSED const gchar *object_path, + G_GNUC_UNUSED const gchar *interface_name, + G_GNUC_UNUSED const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GMainLoop *loop = user_data; + const char *name, *from, *to; + + g_variant_get (parameters, "(sss)", &name, &from, &to); + + /* Check if the service dies, then we exit, because we can't track it anymore */ + if (strcmp (name, service_bus_name) == 0 && + strcmp (to, "") == 0) + { + g_debug ("portal exited"); + + if (child_pid == 0) + launch_exit_status = LAUNCH_EX_FAILED; + else + launch_exit_status = LAUNCH_EX_CANNOT_REPORT; + + g_main_loop_quit (loop); + } +} + +static void +connection_closed_cb (G_GNUC_UNUSED GDBusConnection *conn, + G_GNUC_UNUSED gboolean remote_peer_vanished, + G_GNUC_UNUSED GError *error, + GMainLoop *loop) +{ + g_debug ("D-Bus connection closed, quitting"); + + if (child_pid == 0) + launch_exit_status = LAUNCH_EX_FAILED; + else + launch_exit_status = LAUNCH_EX_CANNOT_REPORT; + + g_main_loop_quit (loop); +} + +static gchar **forward_fds = NULL; +static gboolean opt_clear_env = FALSE; +static gchar *opt_dbus_address = NULL; +static gchar *opt_directory = NULL; +static gchar *opt_socket = NULL; +static gchar **opt_envs = NULL; +static gboolean opt_verbose = FALSE; +static gboolean opt_version = FALSE; + +static const GOptionEntry options[] = +{ + { "bus-name", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &service_bus_name, + "Connect to a Launcher service with this name on the session bus.", + "NAME" }, + { "dbus-address", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_dbus_address, + "Connect to a Launcher server listening on this D-Bus address.", + "ADDRESS" }, + { "clear-env", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_clear_env, + "Run with clean environment.", NULL }, + { "directory", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_directory, + "Working directory in which to run the command.", "DIR" }, + { "env", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &opt_envs, + "Set environment variable.", "VAR=VALUE" }, + { "forward-fd", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &forward_fds, + "Connect a file descriptor to the launched process. " + "fds 0, 1 and 2 are automatically forwarded.", + "FD" }, + { "socket", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_socket, + "Connect to a Launcher server listening on this AF_UNIX socket.", + "ABSPATH|@ABSTRACT" }, + { "verbose", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose, + "Be more verbose.", NULL }, + { "version", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_version, + "Print version number and exit.", NULL }, + { NULL } +}; + +static void +cli_log_func (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + g_printerr ("%s: %s\n", (const char *) user_data, message); +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr(GMainLoop) loop = NULL; + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + char **command_and_args; + guint forward_signals_id = 0; + g_autoptr(FILE) original_stdout = NULL; + g_autoptr(GDBusConnection) session_bus = NULL; + g_autoptr(GDBusConnection) peer_connection = NULL; + g_auto(GVariantBuilder) fd_builder = {}; + g_auto(GVariantBuilder) env_builder = {}; + g_auto(GVariantBuilder) options_builder = {}; + g_autoptr(AutoUnixFDList) fd_list = NULL; + g_autoptr(GVariant) reply = NULL; + gint stdin_handle = -1; + gint stdout_handle = -1; + gint stderr_handle = -1; + PvLaunchFlags spawn_flags = 0; + guint signal_source = 0; + gsize i; + + setlocale (LC_ALL, ""); + + g_set_prgname ("pressure-vessel-launcher"); + + g_log_set_handler (G_LOG_DOMAIN, + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE, + cli_log_func, (void *) g_get_prgname ()); + + context = g_option_context_new ("COMMAND [ARG...]"); + g_option_context_set_summary (context, + "Accept IPC requests to create child " + "processes."); + + g_option_context_add_main_entries (context, options, NULL); + opt_verbose = pv_boolean_environment ("PRESSURE_VESSEL_VERBOSE", FALSE); + + if (!g_option_context_parse (context, &argc, &argv, error)) + goto out; + + if (opt_version) + { + g_print ("%s:\n" + " Package: pressure-vessel\n" + " Version: %s\n", + g_get_prgname (), VERSION); + launch_exit_status = 0; + goto out; + } + + if (opt_verbose) + g_log_set_handler (G_LOG_DOMAIN, + G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO, + cli_log_func, (void *) g_get_prgname ()); + + original_stdout = pv_divert_stdout_to_stderr (error); + + if (original_stdout == NULL) + { + launch_exit_status = LAUNCH_EX_FAILED; + goto out; + } + + /* We have to block the signals we want to forward before we start any + * other thread, and in particular the GDBus worker thread, because + * the signal mask is per-thread. We need all threads to have the same + * mask, otherwise a thread that doesn't have the mask will receive + * process-directed signals, causing the whole process to exit. */ + signal_source = forward_signals (); + + if (signal_source == 0) + { + launch_exit_status = LAUNCH_EX_FAILED; + goto out; + } + + pv_avoid_gvfs (); + + if (service_bus_name != NULL && opt_socket != NULL) + { + glnx_throw (error, "--bus-name and --socket cannot both be used"); + goto out; + } + + if (argc >= 2 && strcmp (argv[1], "--") == 0) + { + argv++; + argc--; + } + + if (argc < 2) + { + glnx_throw (error, "Usage: %s [OPTIONS] COMMAND [ARG...]", + g_get_prgname ()); + goto out; + } + + command_and_args = argv + 1; + + launch_exit_status = LAUNCH_EX_FAILED; + loop = g_main_loop_new (NULL, FALSE); + + if (service_bus_name != NULL) + { + if (opt_dbus_address != NULL || opt_socket != NULL) + { + glnx_throw (error, + "--bus-name cannot be combined with " + "--dbus-address or --socket"); + launch_exit_status = LAUNCH_EX_USAGE; + goto out; + } + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error); + if (session_bus == NULL) + { + glnx_prefix_error (error, "Can't find session bus"); + goto out; + } + + bus_or_peer_connection = session_bus; + } + else if (opt_dbus_address != NULL) + { + if (opt_socket != NULL) + { + glnx_throw (error, + "--dbus-address cannot be combined with --socket"); + launch_exit_status = LAUNCH_EX_USAGE; + goto out; + } + + peer_connection = g_dbus_connection_new_for_address_sync (opt_dbus_address, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, + NULL, + NULL, + error); + if (peer_connection == NULL) + { + glnx_prefix_error (error, "Can't connect to peer address"); + goto out; + } + + bus_or_peer_connection = peer_connection; + } + else if (opt_socket != NULL) + { + g_autofree gchar *address = NULL; + g_autofree gchar *escaped = NULL; + + if (opt_socket[0] == '@') + { + escaped = g_dbus_address_escape_value (&opt_socket[1]); + address = g_strdup_printf ("unix:abstract=%s", escaped); + } + else if (opt_socket[0] == '/') + { + escaped = g_dbus_address_escape_value (opt_socket); + address = g_strdup_printf ("unix:path=%s", escaped); + } + else + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid socket address '%s'", opt_socket); + goto out; + } + + peer_connection = g_dbus_connection_new_for_address_sync (address, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT, + NULL, + NULL, + error); + if (peer_connection == NULL) + { + glnx_prefix_error (error, "Can't connect to peer socket"); + goto out; + } + + bus_or_peer_connection = peer_connection; + } + else + { + glnx_throw (error, "--bus-name or --dbus-address or --socket is required"); + launch_exit_status = LAUNCH_EX_USAGE; + goto out; + } + + g_assert (bus_or_peer_connection != NULL); + + g_dbus_connection_signal_subscribe (bus_or_peer_connection, + service_bus_name, /* NULL if p2p */ + service_iface, + "ProcessExited", + service_obj_path, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + process_exited_cb, + g_main_loop_ref (loop), + (GDestroyNotify) g_main_loop_unref); + + g_variant_builder_init (&fd_builder, G_VARIANT_TYPE ("a{uh}")); + g_variant_builder_init (&env_builder, G_VARIANT_TYPE ("a{ss}")); + fd_list = g_unix_fd_list_new (); + + stdin_handle = g_unix_fd_list_append (fd_list, 0, error); + if (stdin_handle < 0) + { + glnx_prefix_error (error, "Can't append fd 0"); + goto out; + } + /* Remember that our stdout is now a copy of our original stderr, + * so we need to bypass that and use our *original* stdout here. */ + stdout_handle = g_unix_fd_list_append (fd_list, + fileno (original_stdout), + error); + if (stdout_handle < 0) + { + glnx_prefix_error (error, "Can't append fd 1"); + goto out; + } + stderr_handle = g_unix_fd_list_append (fd_list, 2, error); + if (stderr_handle < 0) + { + glnx_prefix_error (error, "Can't append fd 2"); + goto out; + } + + g_variant_builder_add (&fd_builder, "{uh}", 0, stdin_handle); + g_variant_builder_add (&fd_builder, "{uh}", 1, stdout_handle); + g_variant_builder_add (&fd_builder, "{uh}", 2, stderr_handle); + + for (i = 0; forward_fds != NULL && forward_fds[i] != NULL; i++) + { + int fd = strtol (forward_fds[i], NULL, 10); + gint handle = -1; + + if (fd == 0) + { + glnx_throw (error, "Invalid fd '%s'", forward_fds[i]); + goto out; + } + + if (fd >= 0 && fd <= 2) + continue; // We always forward these + + handle = g_unix_fd_list_append (fd_list, fd, error); + if (handle == -1) + { + glnx_prefix_error (error, "Can't append fd"); + goto out; + } + g_variant_builder_add (&fd_builder, "{uh}", fd, handle); + } + + for (i = 0; opt_envs != NULL && opt_envs[i] != NULL; i++) + { + const char *opt_env = opt_envs[i]; + g_auto(GStrv) split = g_strsplit (opt_env, "=", 2); + + if (split == NULL || split[0] == NULL || split[0][0] == 0 || split[1] == NULL) + { + glnx_throw (error, "Invalid env format %s", opt_env); + goto out; + } + g_variant_builder_add (&env_builder, "{ss}", split[0], split[1]); + } + + spawn_flags = 0; + + if (opt_clear_env) + spawn_flags |= PV_LAUNCH_FLAGS_CLEAR_ENV; + + /* There are no options yet, so just leave this empty */ + g_variant_builder_init (&options_builder, G_VARIANT_TYPE ("a{sv}")); + + if (!opt_directory) + { + opt_directory = g_get_current_dir (); + } + + if (session_bus != NULL) + g_dbus_connection_signal_subscribe (session_bus, + DBUS_NAME_DBUS, + DBUS_INTERFACE_DBUS, + "NameOwnerChanged", + DBUS_PATH_DBUS, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + name_owner_changed, + g_main_loop_ref (loop), + (GDestroyNotify) g_main_loop_unref); + + { + g_autoptr(GVariant) fds = NULL; + g_autoptr(GVariant) env = NULL; + g_autoptr(GVariant) opts = NULL; + + fds = g_variant_ref_sink (g_variant_builder_end (&fd_builder)); + env = g_variant_ref_sink (g_variant_builder_end (&env_builder)); + opts = g_variant_ref_sink (g_variant_builder_end (&options_builder)); + + reply = g_dbus_connection_call_with_unix_fd_list_sync (bus_or_peer_connection, + service_bus_name, + service_obj_path, + service_iface, + "Launch", + g_variant_new ("(^ay^aay@a{uh}@a{ss}u@a{sv})", + opt_directory, + (const char * const *) command_and_args, + fds, + env, + spawn_flags, + opts), + G_VARIANT_TYPE ("(u)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + fd_list, + NULL, + NULL, error); + + if (reply == NULL) + { + g_dbus_error_strip_remote_error (local_error); + goto out; + } + + g_variant_get (reply, "(u)", &child_pid); + } + + g_debug ("child_pid: %d", child_pid); + + g_signal_connect (bus_or_peer_connection, "closed", + G_CALLBACK (connection_closed_cb), loop); + + g_main_loop_run (loop); + +out: + if (local_error != NULL) + g_warning ("%s", local_error->message); + + if (forward_signals_id > 0) + g_source_remove (forward_signals_id); + + g_strfreev (forward_fds); + g_free (opt_directory); + g_free (opt_socket); + g_strfreev (opt_envs); + + return launch_exit_status; +} diff --git a/src/launcher.c b/src/launcher.c new file mode 100644 index 0000000000000000000000000000000000000000..522797cba2f7b07506f222eab59a7542281267dc --- /dev/null +++ b/src/launcher.c @@ -0,0 +1,1170 @@ +/* + * pressure-vessel-launcher — accept IPC requests to create child processes + * + * Copyright © 2018 Red Hat, Inc. + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Based on xdg-desktop-portal, flatpak-portal and flatpak-spawn. + * Authors: + * Alexander Larsson <alexl@redhat.com> + */ + +#include "config.h" +#include "subprojects/libglnx/config.h" + +#include <errno.h> +#include <locale.h> +#include <sysexits.h> +#include <sys/ioctl.h> +#include <sys/signalfd.h> +#include <sys/socket.h> + +#include <glib.h> +#include <glib/gstdio.h> +#include <glib-unix.h> +#include <gio/gio.h> +#include <gio/gunixfdlist.h> + +#include "glib-backports.h" +#include "libglnx/libglnx.h" + +#include "flatpak-utils-base-private.h" +#include "launcher.h" +#include "utils.h" + +typedef GCredentials AutoCredentials; +G_DEFINE_AUTOPTR_CLEANUP_FUNC(AutoCredentials, g_object_unref) + +typedef GDBusAuthObserver AutoDBusAuthObserver; +G_DEFINE_AUTOPTR_CLEANUP_FUNC(AutoDBusAuthObserver, g_object_unref) + +typedef GDBusServer AutoDBusServer; +G_DEFINE_AUTOPTR_CLEANUP_FUNC(AutoDBusServer, g_object_unref) + +static FILE *original_stdout = NULL; +static GDBusConnection *session_bus = NULL; +static GHashTable *client_pid_data_hash = NULL; +static guint name_owner_id = 0; +static GMainLoop *main_loop; +static PvLauncher1 *launcher; + +static void +skeleton_died_cb (gpointer data) +{ + g_debug ("skeleton finalized, exiting"); + g_main_loop_quit (main_loop); +} + +static gboolean +unref_skeleton_in_timeout_cb (gpointer user_data) +{ + g_clear_object (&launcher); + return G_SOURCE_REMOVE; +} + +static void +unref_skeleton_in_timeout (void) +{ + if (name_owner_id) + g_bus_unown_name (name_owner_id); + + name_owner_id = 0; + + /* After we've lost the name we drop the main ref on the helper + so that we'll exit when it drops to zero. However, if there are + outstanding calls these will keep the refcount up during the + execution of them. We do the unref on a timeout to make sure + we're completely draining the queue of (stale) requests. */ + g_timeout_add (500, unref_skeleton_in_timeout_cb, NULL); +} + +typedef struct +{ + GDBusConnection *connection; + GPid pid; + gchar *client; + guint child_watch; +} PidData; + +static void +pid_data_free (PidData *data) +{ + g_clear_object (&data->connection); + g_free (data->client); + g_free (data); +} + +static void +terminate_children (int signum) +{ + GHashTableIter iter; + PidData *pid_data = NULL; + gpointer value = NULL; + + /* pass the signal on to each process group led by one of our + * child processes */ + g_hash_table_iter_init (&iter, client_pid_data_hash); + + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + pid_data = value; + killpg (pid_data->pid, signum); + } +} + +static void +child_watch_died (GPid pid, + gint status, + gpointer user_data) +{ + PidData *pid_data = user_data; + g_autoptr(GVariant) signal_variant = NULL; + + g_debug ("Client Pid %d died", pid_data->pid); + + signal_variant = g_variant_ref_sink (g_variant_new ("(uu)", pid, status)); + g_dbus_connection_emit_signal (pid_data->connection, + pid_data->client, + LAUNCHER_PATH, + LAUNCHER_IFACE, + "ProcessExited", + signal_variant, + NULL); + + /* This frees the pid_data, so be careful */ + g_hash_table_remove (client_pid_data_hash, GUINT_TO_POINTER (pid_data->pid)); +} + +typedef struct +{ + int from; + int to; + int final; +} FdMapEntry; + +typedef struct +{ + FdMapEntry *fd_map; + int fd_map_len; +} ChildSetupData; + +static void +drop_cloexec (int fd) +{ + fcntl (fd, F_SETFD, 0); +} + +static void +child_setup_func (gpointer user_data) +{ + ChildSetupData *data = (ChildSetupData *) user_data; + FdMapEntry *fd_map = data->fd_map; + sigset_t set; + int i; + + flatpak_close_fds_workaround (3); + + /* Unblock all signals */ + sigemptyset (&set); + if (pthread_sigmask (SIG_SETMASK, &set, NULL) == -1) + pv_async_signal_safe_error ("Failed to unblock signals when starting child\n", + LAUNCH_EX_FAILED); + + /* Reset the handlers for all signals to their defaults. */ + for (i = 1; i < NSIG; i++) + { + if (i != SIGSTOP && i != SIGKILL) + signal (i, SIG_DFL); + } + + for (i = 0; i < data->fd_map_len; i++) + { + if (fd_map[i].from != fd_map[i].to) + { + dup2 (fd_map[i].from, fd_map[i].to); + close (fd_map[i].from); + } + } + + /* Second pass in case we needed an in-between fd value to avoid conflicts */ + for (i = 0; i < data->fd_map_len; i++) + { + if (fd_map[i].to != fd_map[i].final) + { + dup2 (fd_map[i].to, fd_map[i].final); + close (fd_map[i].to); + } + + /* Ensure we inherit the final fd value */ + drop_cloexec (fd_map[i].final); + } + + /* We become our own session and process group, because it never makes sense + to share the flatpak-session-helper dbus activated process group */ + setsid (); + setpgid (0, 0); +} + +static gboolean +handle_launch (PvLauncher1 *object, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + const gchar *arg_cwd_path, + const gchar *const *arg_argv, + GVariant *arg_fds, + GVariant *arg_envs, + guint arg_flags, + GVariant *arg_options) +{ + g_autoptr(GError) error = NULL; + ChildSetupData child_setup_data = { NULL }; + GPid pid; + PidData *pid_data; + gsize i, j, n_fds, n_envs; + const gint *fds = NULL; + gint fds_len = 0; + g_autofree FdMapEntry *fd_map = NULL; + g_auto(GStrv) env = NULL; + gint32 max_fd; + + if (fd_list != NULL) + fds = g_unix_fd_list_peek_fds (fd_list, &fds_len); + + if (*arg_cwd_path == 0) + arg_cwd_path = NULL; + + if (arg_argv == NULL || *arg_argv == NULL) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "No command given"); + return TRUE; + } + + if ((arg_flags & ~PV_LAUNCH_FLAGS_MASK) != 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, + "Unsupported flags enabled: 0x%x", arg_flags & ~PV_LAUNCH_FLAGS_MASK); + return TRUE; + } + + g_debug ("Running spawn command %s", arg_argv[0]); + + n_fds = 0; + if (fds != NULL) + n_fds = g_variant_n_children (arg_fds); + fd_map = g_new0 (FdMapEntry, n_fds); + + child_setup_data.fd_map = fd_map; + child_setup_data.fd_map_len = n_fds; + + max_fd = -1; + for (i = 0; i < n_fds; i++) + { + gint32 handle, dest_fd; + int handle_fd; + + g_variant_get_child (arg_fds, i, "{uh}", &dest_fd, &handle); + if (handle >= fds_len) + continue; + handle_fd = fds[handle]; + + fd_map[i].to = dest_fd; + fd_map[i].from = handle_fd; + fd_map[i].final = fd_map[i].to; + + max_fd = MAX (max_fd, fd_map[i].to); + max_fd = MAX (max_fd, fd_map[i].from); + } + + /* We make a second pass over the fds to find if any "to" fd index + overlaps an already in use fd (i.e. one in the "from" category + that are allocated randomly). If a fd overlaps "to" fd then its + a caller issue and not our fault, so we ignore that. */ + for (i = 0; i < n_fds; i++) + { + int to_fd = fd_map[i].to; + gboolean conflict = FALSE; + + /* At this point we're fine with using "from" values for this + value (because we handle to==from in the code), or values + that are before "i" in the fd_map (because those will be + closed at this point when dup:ing). However, we can't + reuse a fd that is in "from" for j > i. */ + for (j = i + 1; j < n_fds; j++) + { + int from_fd = fd_map[j].from; + if (from_fd == to_fd) + { + conflict = TRUE; + break; + } + } + + if (conflict) + fd_map[i].to = ++max_fd; + } + + if (arg_flags & PV_LAUNCH_FLAGS_CLEAR_ENV) + { + char *empty[] = { NULL }; + env = g_strdupv (empty); + } + else + { + env = g_get_environ (); + } + + n_envs = g_variant_n_children (arg_envs); + for (i = 0; i < n_envs; i++) + { + const char *var = NULL; + const char *val = NULL; + g_variant_get_child (arg_envs, i, "{&s&s}", &var, &val); + + env = g_environ_setenv (env, var, val, TRUE); + } + + /* We use LEAVE_DESCRIPTORS_OPEN to work around dead-lock, see flatpak_close_fds_workaround */ + if (!g_spawn_async_with_pipes (arg_cwd_path, + (gchar **) arg_argv, + env, + G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_LEAVE_DESCRIPTORS_OPEN, + child_setup_func, &child_setup_data, + &pid, + NULL, + NULL, + NULL, + &error)) + { + gint code = G_DBUS_ERROR_FAILED; + + if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_ACCES)) + code = G_DBUS_ERROR_ACCESS_DENIED; + else if (g_error_matches (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) + code = G_DBUS_ERROR_FILE_NOT_FOUND; + + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, code, + "Failed to start command: %s", + error->message); + return TRUE; + } + + pid_data = g_new0 (PidData, 1); + pid_data->connection = g_object_ref (g_dbus_method_invocation_get_connection (invocation)); + pid_data->pid = pid; + pid_data->client = g_strdup (g_dbus_method_invocation_get_sender (invocation)); + pid_data->child_watch = g_child_watch_add_full (G_PRIORITY_DEFAULT, + pid, + child_watch_died, + pid_data, + NULL); + + g_debug ("Client Pid is %d", pid_data->pid); + + g_hash_table_replace (client_pid_data_hash, GUINT_TO_POINTER (pid_data->pid), + pid_data); + + pv_launcher1_complete_launch (object, invocation, NULL, pid); + return TRUE; +} + +static gboolean +handle_send_signal (PvLauncher1 *object, + GDBusMethodInvocation *invocation, + guint arg_pid, + guint arg_signal, + gboolean arg_to_process_group) +{ + PidData *pid_data = NULL; + + g_debug ("SendSignal(%d, %d)", arg_pid, arg_signal); + + pid_data = g_hash_table_lookup (client_pid_data_hash, GUINT_TO_POINTER (arg_pid)); + if (pid_data == NULL || + pid_data->connection != g_dbus_method_invocation_get_connection (invocation) || + g_strcmp0 (pid_data->client, g_dbus_method_invocation_get_sender (invocation)) != 0) + { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_UNIX_PROCESS_ID_UNKNOWN, + "No such pid"); + return TRUE; + } + + g_debug ("Sending signal %d to client pid %d", arg_signal, arg_pid); + + if (arg_to_process_group) + killpg (pid_data->pid, arg_signal); + else + kill (pid_data->pid, arg_signal); + + pv_launcher1_complete_send_signal (launcher, invocation); + + return TRUE; +} + +static void +name_owner_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + const char *name, *from, *to; + + g_variant_get (parameters, "(&s&s&s)", &name, &from, &to); + + if (name[0] == ':' && + strcmp (name, from) == 0 && + strcmp (to, "") == 0) + { + GHashTableIter iter; + PidData *pid_data = NULL; + gpointer value = NULL; + GList *list = NULL, *l; + + g_hash_table_iter_init (&iter, client_pid_data_hash); + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + pid_data = value; + + if (g_str_equal (pid_data->client, name)) + list = g_list_prepend (list, pid_data); + } + + for (l = list; l; l = l->next) + { + pid_data = l->data; + g_debug ("%s dropped off the bus, killing %d", pid_data->client, pid_data->pid); + killpg (pid_data->pid, SIGINT); + } + + g_list_free (list); + } +} + +static gboolean +export_launcher (GDBusConnection *connection, + GError **error) +{ + if (launcher == NULL) + { + launcher = pv_launcher1_skeleton_new (); + + g_object_set_data_full (G_OBJECT (launcher), "track-alive", + launcher, /* an arbitrary non-NULL pointer */ + skeleton_died_cb); + + pv_launcher1_set_version (PV_LAUNCHER1 (launcher), 0); + pv_launcher1_set_supported_launch_flags (PV_LAUNCHER1 (launcher), + PV_LAUNCH_FLAGS_MASK); + + g_signal_connect (launcher, "handle-launch", + G_CALLBACK (handle_launch), NULL); + g_signal_connect (launcher, "handle-send-signal", + G_CALLBACK (handle_send_signal), NULL); + } + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (launcher), + connection, + LAUNCHER_PATH, + error)) + return FALSE; + + return TRUE; +} + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + int *ret = user_data; + + g_debug ("Bus acquired, creating skeleton"); + + g_dbus_connection_set_exit_on_close (connection, FALSE); + g_dbus_connection_signal_subscribe (connection, + DBUS_NAME_DBUS, + DBUS_INTERFACE_DBUS, + "NameOwnerChanged", + DBUS_PATH_DBUS, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + name_owner_changed, + NULL, NULL); + + if (!export_launcher (connection, &error)) + { + g_warning ("Unable to export object: %s", error->message); + *ret = EX_SOFTWARE; + g_main_loop_quit (main_loop); + } +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + int *ret = user_data; + + g_debug ("Name acquired"); + + /* If exporting the launcher didn't fail, then we are now happy */ + if (*ret == EX_UNAVAILABLE) + { + *ret = 0; + g_assert (original_stdout != NULL); + fprintf (original_stdout, "bus_name=%s\n", name); + fflush (original_stdout); + g_clear_pointer (&original_stdout, fclose); + } +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_debug ("Name lost"); + unref_skeleton_in_timeout (); +} + +#if GLIB_CHECK_VERSION(2, 34, 0) +/* + * Callback for GDBusServer::allow-mechanism. + * Only allow the (most secure) EXTERNAL authentication mechanism, + * if possible. + */ +static gboolean +allow_external_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, + const char *mechanism, + G_GNUC_UNUSED gpointer user_data) +{ + return (g_strcmp0 (mechanism, "EXTERNAL") == 0); +} +#endif + +/* + * Callback for GDBusServer::authorize-authenticated-peer. + * Only allow D-Bus connections from a matching uid. + */ +static gboolean +authorize_authenticated_peer_cb (G_GNUC_UNUSED GDBusAuthObserver *observer, + G_GNUC_UNUSED GIOStream *stream, + GCredentials *credentials, + G_GNUC_UNUSED gpointer user_data) +{ + g_autoptr(AutoCredentials) retry_credentials = NULL; + g_autoptr(AutoCredentials) myself = NULL; + g_autofree gchar *credentials_str = NULL; + + if (g_credentials_get_unix_user (credentials, NULL) == (uid_t) -1) + { + /* Work around https://gitlab.gnome.org/GNOME/glib/issues/1831: + * older GLib versions might retrieve incomplete credentials. + * Make them try again. */ + credentials = NULL; + } + + if (credentials == NULL && G_IS_SOCKET_CONNECTION (stream)) + { + /* Continue to work around + * https://gitlab.gnome.org/GNOME/glib/issues/1831: + * older GLib versions might retrieve incomplete or no credentials. */ + GSocket *sock; + + sock = g_socket_connection_get_socket (G_SOCKET_CONNECTION (stream)); + retry_credentials = g_socket_get_credentials (sock, NULL); + credentials = retry_credentials; + } + + myself = g_credentials_new (); + + if (credentials != NULL && + g_credentials_is_same_user (credentials, myself, NULL)) + return TRUE; + + if (credentials == NULL) + credentials_str = g_strdup ("peer with unknown credentials"); + else + credentials_str = g_credentials_to_string (credentials); + + g_warning ("Rejecting connection from %s", credentials_str); + return FALSE; +} + +static GDBusAuthObserver * +observer_new (void) +{ + g_autoptr(AutoDBusAuthObserver) observer = g_dbus_auth_observer_new (); + +#if GLIB_CHECK_VERSION(2, 34, 0) + g_signal_connect (observer, "allow-mechanism", + G_CALLBACK (allow_external_cb), NULL); +#endif + g_signal_connect (observer, "authorize-authenticated-peer", + G_CALLBACK (authorize_authenticated_peer_cb), NULL); + + return g_steal_pointer (&observer); +} + +/* + * Callback for GDBusConnection::closed. + */ +static void +peer_connection_closed_cb (GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *error, + gpointer user_data) +{ + /* Paired with g_object_ref() in new_connection_cb() */ + g_object_unref (connection); +} + +/* + * Double-check credentials of a peer, since we are working with older + * versions of GLib that can't necessarily be completely relied on. + * We are willing to execute arbitrary code on behalf of an authenticated + * connection, so it seems worthwhile to be extra-careful. + */ +static gboolean +check_credentials (GDBusConnection *connection, + GError **error) +{ + struct ucred creds; + socklen_t len; + GIOStream *stream; + GSocket *sock; + int sockfd; + + stream = g_dbus_connection_get_stream (connection); + len = sizeof (creds); + + if (!G_IS_SOCKET_CONNECTION (stream)) + return glnx_throw (error, "Incoming D-Bus connection is not a socket?"); + + sock = g_socket_connection_get_socket (G_SOCKET_CONNECTION (stream)); + + if (sock == NULL) + return glnx_throw (error, + "Incoming D-Bus connection does not have a socket?"); + + sockfd = g_socket_get_fd (sock); + + if (getsockopt (sockfd, SOL_SOCKET, SO_PEERCRED, &creds, &len) < 0) + return glnx_throw_errno_prefix (error, "Unable to check credentials"); + + if (creds.uid != geteuid ()) + return glnx_throw (error, + "Connection from uid %ld != %ld should have been " + "rejected already", + (long) creds.uid, (long) geteuid ()); + + return TRUE; +} + +/* + * Callback for GDBusServer::new-connection. + * + * Returns: %TRUE if the new connection attempt was handled, even if + * unsuccessfully. + */ +static gboolean +new_connection_cb (GDBusServer *server, + GDBusConnection *connection, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + + if (!check_credentials (connection, &error)) + { + g_warning ("Credentials verification failed: %s", error->message); + g_dbus_connection_close (connection, NULL, NULL, NULL); + return TRUE; /* handled, unsuccessfully */ + } + + /* Paired with g_object_unref() in peer_connection_closed_cb() */ + g_object_ref (connection); + g_signal_connect (connection, "closed", + G_CALLBACK (peer_connection_closed_cb), NULL); + + if (!export_launcher (connection, &error)) + { + g_warning ("Unable to export object: %s", error->message); + g_dbus_connection_close (connection, NULL, NULL, NULL); + return TRUE; /* handled, unsuccessfully */ + } + + return TRUE; +} + +static GDBusServer * +listen_on_address (const char *address, + GError **error) +{ + g_autofree gchar *guid = NULL; + g_autoptr(AutoDBusAuthObserver) observer = NULL; + g_autoptr(AutoDBusServer) server = NULL; + + guid = g_dbus_generate_guid (); + + observer = observer_new (); + + server = g_dbus_server_new_sync (address, + G_DBUS_SERVER_FLAGS_NONE, + guid, + observer, + NULL, + error); + + if (server == NULL) + return NULL; + + g_signal_connect (server, "new-connection", + G_CALLBACK (new_connection_cb), NULL); + g_dbus_server_start (server); + + return g_steal_pointer (&server); +} + +static GDBusServer * +listen_on_socket (const char *name, + GError **error) +{ + g_autofree gchar *address = NULL; + g_autofree gchar *escaped = NULL; + + if (name[0] == '@') + { + escaped = g_dbus_address_escape_value (&name[1]); + address = g_strdup_printf ("unix:abstract=%s", escaped); + } + else if (name[0] == '/') + { + escaped = g_dbus_address_escape_value (name); + address = g_strdup_printf ("unix:path=%s", escaped); + } + else + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Invalid socket address '%s'", name); + return FALSE; + } + + return listen_on_address (address, error); +} + +static gboolean +signal_handler (int sfd, + G_GNUC_UNUSED GIOCondition condition, + G_GNUC_UNUSED gpointer data) +{ + struct signalfd_siginfo info; + ssize_t size; + + size = read (sfd, &info, sizeof (info)); + + if (size < 0) + { + if (errno != EINTR && errno != EAGAIN) + g_warning ("Unable to read struct signalfd_siginfo: %s", + g_strerror (errno)); + } + else if (size != sizeof (info)) + { + g_warning ("Expected struct signalfd_siginfo of size %" + G_GSIZE_FORMAT ", got %" G_GSSIZE_FORMAT, + sizeof (info), size); + } + else + { + terminate_children (info.ssi_signo); + g_main_loop_quit (main_loop); + } + + return G_SOURCE_CONTINUE; +} + +static guint +connect_to_signals (void) +{ + static int signals[] = { SIGHUP, SIGINT, SIGTERM }; + sigset_t mask; + guint i; + int sfd; + + sigemptyset (&mask); + + for (i = 0; i < G_N_ELEMENTS (signals); i++) + sigaddset (&mask, signals[i]); + + sfd = signalfd (-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); + + if (sfd < 0) + { + g_warning ("Unable to watch signals: %s", g_strerror (errno)); + return 0; + } + + /* + * We have to block the signals, for two reasons: + * - If we didn't, most of them would kill our process. + * Listening for a signal with a signalfd does not prevent the signal's + * default disposition from being acted on. + * - Reading from a signalfd only returns information about the signals + * that are still pending for the process. If we ignored them instead + * of blocking them, they would no longer be pending by the time the + * main loop wakes up and reads from the signalfd. + */ + pthread_sigmask (SIG_BLOCK, &mask, NULL); + + return g_unix_fd_add (sfd, G_IO_IN, signal_handler, NULL); +} + +static gchar *opt_bus_name = NULL; +static gboolean opt_replace = FALSE; +static gchar *opt_socket = NULL; +static gchar *opt_socket_directory = NULL; +static gboolean opt_verbose = FALSE; +static gboolean opt_version = FALSE; + +static GOptionEntry options[] = +{ + { "bus-name", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_bus_name, + "Use this well-known name on the D-Bus session bus.", + "NAME" }, + { "replace", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_replace, + "Replace a previous instance with the same bus name. " + "Ignored if --bus-name is not used.", + NULL }, + { "socket", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_socket, + "Listen on this AF_UNIX socket.", + "ABSPATH|@ABSTRACT" }, + { "socket-directory", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_socket_directory, + "Listen on an arbitrary AF_UNIX socket in this directory. " + "Print the filename (socket=/path/to/socket), the " + "D-Bus address (dbus_address=unix:...) and possibly other " + "fields on stdout, one per line.", + "PATH" }, + { "verbose", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose, + "Be more verbose.", NULL }, + { "version", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_version, + "Print version number and exit.", NULL }, + { NULL } +}; + +static void +cli_log_func (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + g_printerr ("%s: %s\n", (const char *) user_data, message); +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr(AutoDBusServer) server = NULL; + g_autoptr(GOptionContext) context = NULL; + guint signals_id = 0; + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + gsize i; + int ret = EX_USAGE; + + setlocale (LC_ALL, ""); + + g_set_prgname ("pressure-vessel-launcher"); + + g_log_set_handler (G_LOG_DOMAIN, + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE, + cli_log_func, (void *) g_get_prgname ()); + + context = g_option_context_new (""); + g_option_context_set_summary (context, + "Accept IPC requests to create child " + "processes."); + + g_option_context_add_main_entries (context, options, NULL); + opt_verbose = pv_boolean_environment ("PRESSURE_VESSEL_VERBOSE", FALSE); + + if (!g_option_context_parse (context, &argc, &argv, error)) + { + ret = EX_USAGE; + goto out; + } + + if (opt_version) + { + g_print ("%s:\n" + " Package: pressure-vessel\n" + " Version: %s\n", + g_get_prgname (), VERSION); + ret = 0; + goto out; + } + + if (opt_verbose) + g_log_set_handler (G_LOG_DOMAIN, + G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO, + cli_log_func, (void *) g_get_prgname ()); + + original_stdout = pv_divert_stdout_to_stderr (error); + + if (original_stdout == NULL) + { + ret = EX_OSERR; + goto out; + } + + /* We have to block the signals we want to forward before we start any + * other thread, and in particular the GDBus worker thread, because + * the signal mask is per-thread. We need all threads to have the same + * mask, otherwise a thread that doesn't have the mask will receive + * process-directed signals, causing the whole process to exit. */ + signals_id = connect_to_signals (); + + if (signals_id == 0) + { + ret = EX_OSERR; + goto out; + } + + pv_avoid_gvfs (); + + if (argc >= 2 && strcmp (argv[1], "--") == 0) + { + argv++; + argc--; + } + + if (argc != 1) + { + glnx_throw (error, "Usage: %s [OPTIONS]", g_get_prgname ()); + goto out; + } + + client_pid_data_hash = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) pid_data_free); + + /* --socket argument needs to be printable so we can print + * "socket=%s\n" without escaping */ + if (opt_socket != NULL) + { + for (i = 0; opt_socket[i] != '\0'; i++) + { + if (!g_ascii_isprint (opt_socket[i])) + { + glnx_throw (error, + "Non-printable characters not allowed in --socket"); + goto out; + } + } + } + + /* --socket-directory argument likewise */ + if (opt_socket_directory != NULL) + { + for (i = 0; opt_socket_directory[i] != '\0'; i++) + { + if (!g_ascii_isprint (opt_socket_directory[i])) + { + glnx_throw (error, + "Non-printable characters not allowed in " + "--socket-directory"); + goto out; + } + } + } + + /* Exit with this status until we know otherwise */ + ret = EX_SOFTWARE; + + if (opt_bus_name != NULL) + { + GBusNameOwnerFlags flags; + + if (opt_socket != NULL || opt_socket_directory != NULL) + { + glnx_throw (error, + "--bus-name cannot be combined with --socket or " + "--socket-directory"); + ret = EX_USAGE; + goto out; + } + + g_debug ("Connecting to D-Bus session bus..."); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error); + + if (session_bus == NULL) + { + glnx_prefix_error (error, "Can't find session bus"); + goto out; + } + + flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT; + if (opt_replace) + flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE; + + ret = EX_UNAVAILABLE; + g_debug ("Claiming bus name %s...", opt_bus_name); + name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + opt_bus_name, + flags, + on_bus_acquired, + on_name_acquired, + on_name_lost, + &ret, + NULL); + } + else if (opt_socket != NULL) + { + if (opt_socket_directory != NULL) + { + glnx_throw (error, + "--socket and --socket-directory cannot both be used"); + ret = EX_USAGE; + goto out; + } + + g_debug ("Listening on socket %s...", opt_socket); + server = listen_on_socket (opt_socket, error); + + if (server == NULL) + { + glnx_prefix_error (error, + "Unable to listen on socket \"%s\"", opt_socket); + goto out; + } + + ret = 0; + } + else if (opt_socket_directory != NULL) + { + g_autofree gchar *unique = NULL; + g_autofree gchar *dir = NULL; + + if (strlen (opt_socket_directory) > PV_MAX_SOCKET_DIRECTORY_LEN) + { + glnx_throw (error, "Socket directory path \"%s\" too long", + opt_socket_directory); + goto out; + } + + dir = realpath (opt_socket_directory, NULL); + + if (strlen (dir) > PV_MAX_SOCKET_DIRECTORY_LEN) + { + glnx_throw (error, "Socket directory path \"%s\" too long", dir); + goto out; + } + + g_debug ("Listening on a socket in %s...", opt_socket_directory); + + /* @unique is long and random, so we assume it is not guessable + * by an attacker seeking to deny service by using the name we + * intended to use; so we don't need a retry loop for alternative + * names in the same directory. */ + unique = pv_get_random_uuid (error); + + if (unique == NULL) + goto out; + + opt_socket = g_build_filename (dir, unique, NULL); + g_debug ("Chosen socket is %s", opt_socket); + server = listen_on_socket (opt_socket, error); + + if (server == NULL) + { + glnx_prefix_error (error, + "Unable to listen on socket \"%s\"", opt_socket); + goto out; + } + + ret = 0; + } + else + { + glnx_throw (error, + "--bus-name, --socket or --socket-directory is required"); + ret = EX_USAGE; + goto out; + } + + if (opt_socket != NULL) + fprintf (original_stdout, "socket=%s\n", opt_socket); + + if (server != NULL) + fprintf (original_stdout, "dbus_address=%s\n", + g_dbus_server_get_client_address (server)); + + if (opt_bus_name == NULL) + { + fflush (original_stdout); + g_clear_pointer (&original_stdout, fclose); + } + + g_debug ("Entering main loop"); + + main_loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (main_loop); + g_debug ("Exiting"); + +out: + if (local_error != NULL) + g_warning ("%s", local_error->message); + + if (signals_id > 0) + g_source_remove (signals_id); + + if (name_owner_id) + g_bus_unown_name (name_owner_id); + + if (server != NULL && opt_socket != NULL) + unlink (opt_socket); + + g_free (opt_bus_name); + g_free (opt_socket); + g_free (opt_socket_directory); + g_clear_object (&session_bus); + + if (local_error == NULL) + ret = 0; + else if (local_error->domain == G_OPTION_ERROR) + ret = EX_USAGE; + else + ret = EX_UNAVAILABLE; + + g_clear_error (&local_error); + g_clear_pointer (&original_stdout, fclose); + + return ret; +} diff --git a/src/launcher.h b/src/launcher.h new file mode 100644 index 0000000000000000000000000000000000000000..f85d94d6b4b2df024798871bbc87fc836a88c58f --- /dev/null +++ b/src/launcher.h @@ -0,0 +1,79 @@ + +/* + * pressure-vessel-launcher — accept IPC requests to create child processes + * + * Copyright © 2020 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * 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; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <sys/socket.h> +#include <sys/un.h> + +#include <glib.h> +#include <gio/gio.h> + +#include "glib-backports.h" +#include "libglnx/libglnx.h" + +#include "launcher1.h" + +#define DBUS_NAME_DBUS "org.freedesktop.DBus" +#define DBUS_INTERFACE_DBUS DBUS_NAME_DBUS +#define DBUS_PATH_DBUS "/org/freedesktop/DBus" + +#define LAUNCHER_IFACE "com.steampowered.PressureVessel.Launcher1" +#define LAUNCHER_PATH "/com/steampowered/PressureVessel/Launcher1" + +typedef enum +{ + PV_LAUNCH_FLAGS_CLEAR_ENV = (1 << 0), + PV_LAUNCH_FLAGS_NONE = 0, + PV_LAUNCH_FLAGS_MASK = ( + PV_LAUNCH_FLAGS_CLEAR_ENV | + PV_LAUNCH_FLAGS_NONE + ), +} PvLaunchFlags; + +#if !GLIB_CHECK_VERSION (2, 47, 92) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PvLauncher1Skeleton, g_object_unref) +#endif + +/* Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx */ +#define PV_UUID_STRLEN 36 + +#define PV_MAX_SOCKET_DIRECTORY_LEN 64 + +/* If ${socket_directory} is no longer than PV_MAX_SOCKET_DIRECTORY_LEN, + * then struct sockaddr_un.sun_path is long enough to contain + * "${socket_directory}/${uuid}\0" */ +G_STATIC_ASSERT (sizeof (struct sockaddr_un) >= + (G_STRUCT_OFFSET (struct sockaddr_un, sun_path) + + PV_MAX_SOCKET_DIRECTORY_LEN + + PV_UUID_STRLEN + + 2)); + +/* Chosen to be similar to env(1) */ +enum +{ + LAUNCH_EX_USAGE = 125, + LAUNCH_EX_FAILED = 125, + LAUNCH_EX_CANNOT_INVOKE = 126, + LAUNCH_EX_NOT_FOUND = 127, + LAUNCH_EX_CANNOT_REPORT = 128 +}; diff --git a/src/meson.build b/src/meson.build index 4e348724629e7f3a2fd1038ad6fd369f631570a6..6bf5901d0a668a71afc69d9e17c3e250440d42bf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -27,6 +27,13 @@ headers = [ 'runtime.h', ] +launcher1 = gnome.gdbus_codegen( + 'launcher1', + sources : 'com.steampowered.PressureVessel.Launcher1.xml', + interface_prefix : 'com.steampowered.PressureVessel.', + namespace : 'Pv', +) + enums = gnome.mkenums_simple( 'enumtypes', sources : headers, @@ -59,6 +66,17 @@ pressure_vessel_utils = static_library( install: false, ) +launcher_codegen = static_library( + 'pressure-vessel-launcher-codegen', + sources : launcher1, + dependencies : [ + gio_unix, + ], + c_args : silence_gdbus_codegen_warnings, + include_directories : project_include_dirs, + install: false, +) + executable( 'pressure-vessel-adverb', sources : [ @@ -78,6 +96,47 @@ executable( install_rpath : '${ORIGIN}/../' + get_option('libdir'), ) +executable( + 'pressure-vessel-launch', + sources: [ + 'launch.c', + ], + dependencies : [ + threads, + gio_unix, + libglnx.get_variable('libglnx_dep'), + ], + link_with : [ + pressure_vessel_utils, + ], + include_directories : project_include_dirs, + install : true, + install_dir : get_option('bindir'), + build_rpath : '${ORIGIN}/../' + get_option('libdir'), + install_rpath : '${ORIGIN}/../' + get_option('libdir'), +) + +executable( + 'pressure-vessel-launcher', + sources: [ + 'launcher.c', + ], + dependencies : [ + threads, + gio_unix, + libglnx.get_variable('libglnx_dep'), + ], + link_with : [ + launcher_codegen, + pressure_vessel_utils, + ], + include_directories : project_include_dirs, + install : true, + install_dir : get_option('bindir'), + build_rpath : '${ORIGIN}/../' + get_option('libdir'), + install_rpath : '${ORIGIN}/../' + get_option('libdir'), +) + executable( 'pressure-vessel-wrap', sources : [