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 : [