diff --git a/src/adverb.c b/src/adverb.c
index e8ee1c6249d0241891d75ede3c33d79591be6c62..815559af0ca7ebb956550faafdab901e3c88591a 100644
--- a/src/adverb.c
+++ b/src/adverb.c
@@ -43,19 +43,19 @@
 #include "launcher.h"
 #include "utils.h"
 
-#ifndef PR_SET_CHILD_SUBREAPER
-#define PR_SET_CHILD_SUBREAPER 36
-#endif
-
 static GPtrArray *global_locks = NULL;
 static GArray *global_pass_fds = NULL;
 static gboolean opt_create = FALSE;
+static gboolean opt_exit_with_parent = FALSE;
 static gboolean opt_generate_locales = FALSE;
 static gboolean opt_subreaper = FALSE;
+static double opt_terminate_idle_timeout = 0.0;
+static double opt_terminate_timeout = -1.0;
 static gboolean opt_verbose = FALSE;
 static gboolean opt_version = FALSE;
 static gboolean opt_wait = FALSE;
 static gboolean opt_write = FALSE;
+static GPid child_pid;
 
 typedef struct
 {
@@ -67,7 +67,32 @@ static void
 child_setup_cb (gpointer user_data)
 {
   ChildSetupData *data = user_data;
+  sigset_t set;
   const int *iter;
+  int i;
+
+  /* The adverb should wait for its child before it exits, but if it
+   * gets terminated prematurely, we want the child to terminate too.
+   * The child could reset this, but we assume it usually won't.
+   * This makes it exit even if we are killed by SIGKILL, unless it
+   * takes steps not to be. */
+  if (opt_exit_with_parent
+      && prctl (PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0) != 0)
+    pv_async_signal_safe_error ("Failed to set up parent-death signal\n",
+                                LAUNCH_EX_FAILED);
+
+  /* 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);
+    }
 
   /* Put back the original stdout for the child process */
   if (data != NULL &&
@@ -221,10 +246,17 @@ generate_locales (gchar **locpath_out,
   g_autofree gchar *pvlg = NULL;
   g_autofree gchar *this_path = NULL;
   g_autofree gchar *this_dir = NULL;
+  gboolean ret;
+  sigset_t mask;
+  sigset_t old_mask;
 
   g_return_val_if_fail (locpath_out == NULL || *locpath_out == NULL, FALSE);
   g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
 
+  sigemptyset (&mask);
+  sigemptyset (&old_mask);
+  sigaddset (&mask, SIGCHLD);
+
   temp_dir = g_dir_make_tmp ("pressure-vessel-locales-XXXXXX", error);
 
   if (temp_dir == NULL)
@@ -247,17 +279,26 @@ generate_locales (gchar **locpath_out,
     NULL
   };
 
+  /* Unblock SIGCHLD in case g_spawn_sync() needs it in some version */
+  if (pthread_sigmask (SIG_UNBLOCK, &mask, &old_mask) != 0)
+    return glnx_throw_errno_prefix (error, "pthread_sigmask");
+
   /* We use LEAVE_DESCRIPTORS_OPEN to work around a deadlock in older GLib,
    * see flatpak_close_fds_workaround */
-  if (!g_spawn_sync (NULL,  /* cwd */
-                     (gchar **) locale_gen_argv,
-                     NULL,  /* environ */
-                     G_SPAWN_LEAVE_DESCRIPTORS_OPEN,
-                     child_setup_cb, NULL,
-                     &child_stdout,
-                     &child_stderr,
-                     &wait_status,
-                     error))
+  ret = g_spawn_sync (NULL,  /* cwd */
+                      (gchar **) locale_gen_argv,
+                      NULL,  /* environ */
+                      G_SPAWN_LEAVE_DESCRIPTORS_OPEN,
+                      child_setup_cb, NULL,
+                      &child_stdout,
+                      &child_stderr,
+                      &wait_status,
+                      error);
+
+  if (pthread_sigmask (SIG_SETMASK, &old_mask, NULL) != 0)
+    return glnx_throw_errno_prefix (error, "pthread_sigmask");
+
+  if (!ret)
     {
       if (error != NULL)
         glnx_prefix_error (error, "Cannot run pressure-vessel-locale-gen");
@@ -298,6 +339,23 @@ generate_locales (gchar **locpath_out,
   return TRUE;
 }
 
+/* Only do async-signal-safe things here: see signal-safety(7) */
+static void
+terminate_child_cb (int signum)
+{
+  if (child_pid != 0)
+    {
+      /* pass it on to the child we're going to wait for */
+      kill (child_pid, signum);
+    }
+  else
+    {
+      /* guess I'll just die, then */
+      signal (signum, SIG_DFL);
+      raise (signum);
+    }
+}
+
 static GOptionEntry options[] =
 {
   { "fd", '\0',
@@ -315,6 +373,16 @@ static GOptionEntry options[] =
     "Don't create subsequent nonexistent lock files [default].",
     NULL },
 
+  { "exit-with-parent", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_exit_with_parent,
+    "Terminate child process and self with SIGTERM when parent process "
+    "exits.",
+    NULL },
+  { "no-exit-with-parent", '\0',
+    G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_exit_with_parent,
+    "Don't do anything special when parent process exits [default].",
+    NULL },
+
   { "generate-locales", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_generate_locales,
     "Attempt to generate any missing locales.", NULL },
@@ -353,13 +421,27 @@ static GOptionEntry options[] =
 
   { "subreaper", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_subreaper,
-    "Do not exit until all child processes have exited.",
+    "Do not exit until all descendant processes have exited.",
     NULL },
   { "no-subreaper", '\0',
     G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_subreaper,
     "Only wait for a direct child process [default].",
     NULL },
 
+  { "terminate-idle-timeout", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &opt_terminate_idle_timeout,
+    "If --terminate-timeout is used, wait this many seconds before "
+    "sending SIGTERM. [default: 0.0]",
+    "SECONDS" },
+  { "terminate-timeout", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &opt_terminate_timeout,
+    "Send SIGTERM and SIGCONT to descendant processes that didn't "
+    "exit within --terminate-idle-timeout. If they don't all exit within "
+    "this many seconds, send SIGKILL and SIGCONT to survivors. If 0.0, "
+    "skip SIGTERM and use SIGKILL immediately. Implies --subreaper. "
+    "[Default: -1.0, meaning don't signal].",
+    "SECONDS" },
+
   { "verbose", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose,
     "Be more verbose.", NULL },
@@ -388,12 +470,25 @@ main (int argc,
   GError **error = &local_error;
   int ret = EX_USAGE;
   char **command_and_args;
-  GPid child_pid;
   int wait_status = -1;
   g_autofree gchar *locales_temp_dir = NULL;
   g_auto(GStrv) my_environ = NULL;
   g_autoptr(FILE) original_stdout = NULL;
   ChildSetupData child_setup_data = { -1, NULL };
+  sigset_t mask;
+  struct sigaction terminate_child_action = {};
+
+  sigemptyset (&mask);
+  sigaddset (&mask, SIGCHLD);
+
+  /* Must be called before we start any threads */
+  if (pthread_sigmask (SIG_BLOCK, &mask, NULL) != 0)
+    {
+      ret = EX_UNAVAILABLE;
+      glnx_throw_errno_prefix (error, "pthread_sigmask");
+      goto out;
+    }
+
 
   setlocale (LC_ALL, "");
 
@@ -469,7 +564,19 @@ main (int argc,
 
   command_and_args = argv + 1;
 
-  if (opt_subreaper
+  if (opt_exit_with_parent)
+    {
+      g_debug ("Setting up to exit when parent does");
+
+      if (prctl (PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0) != 0)
+        {
+          glnx_throw_errno_prefix (error,
+                                   "Unable to set parent death signal");
+          goto out;
+        }
+    }
+
+  if ((opt_subreaper || opt_terminate_timeout >= 0)
       && prctl (PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0)
     {
       glnx_throw_errno_prefix (error,
@@ -500,6 +607,16 @@ main (int argc,
         }
     }
 
+  /* Respond to common termination signals by killing the child instead of
+   * ourselves */
+  terminate_child_action.sa_handler = terminate_child_cb;
+  sigaction (SIGHUP, &terminate_child_action, NULL);
+  sigaction (SIGINT, &terminate_child_action, NULL);
+  sigaction (SIGQUIT, &terminate_child_action, NULL);
+  sigaction (SIGTERM, &terminate_child_action, NULL);
+  sigaction (SIGUSR1, &terminate_child_action, NULL);
+  sigaction (SIGUSR2, &terminate_child_action, NULL);
+
   g_debug ("Launching child process...");
   fflush (stdout);
   child_setup_data.original_stdout_fd = fileno (original_stdout);
@@ -534,52 +651,44 @@ main (int argc,
   /* If the child writes to stdout and closes it, don't interfere */
   g_clear_pointer (&original_stdout, fclose);
 
-  while (1)
+  /* Reap child processes until child_pid exits */
+  if (!pv_wait_for_child_processes (child_pid, &wait_status, error))
+    goto out;
+
+  child_pid = 0;
+
+  if (WIFEXITED (wait_status))
+    {
+      ret = WEXITSTATUS (wait_status);
+      g_debug ("Command exited with status %d", ret);
+    }
+  else if (WIFSIGNALED (wait_status))
+    {
+      ret = 128 + WTERMSIG (wait_status);
+      g_debug ("Command killed by signal %d", ret - 128);
+    }
+  else
     {
-      pid_t died = wait (&wait_status);
+      ret = EX_SOFTWARE;
+      g_debug ("Command terminated in an unknown way (wait status %d)",
+               wait_status);
+    }
 
-      if (died < 0)
-        {
-          if (errno == EINTR)
-            {
-              continue;
-            }
-          else if (errno == ECHILD)
-            {
-              g_debug ("No more child processes, exiting");
-              break;
-            }
-          else
-            {
-              glnx_throw_errno_prefix (error, "wait");
-              goto out;
-            }
-        }
-      else if (died == child_pid)
-        {
-          if (WIFEXITED (wait_status))
-            {
-              ret = WEXITSTATUS (wait_status);
-              g_debug ("Command exited with status %d", ret);
-            }
-          else if (WIFSIGNALED (wait_status))
-            {
-              ret = 128 + WTERMSIG (wait_status);
-              g_debug ("Command killed by signal %d", ret - 128);
-            }
-          else
-            {
-              ret = EX_SOFTWARE;
-              g_debug ("Command terminated in an unknown way (wait status %d)",
-                       wait_status);
-            }
-        }
-      else
-        {
-          g_debug ("Indirect child %lld exited with wait status %d",
-                   (long long) died, wait_status);
-          g_warn_if_fail (opt_subreaper);
-        }
+  if (opt_terminate_idle_timeout < 0.0)
+    opt_terminate_idle_timeout = 0.0;
+
+  /* Wait for the other child processes, if any, possibly killing them */
+  if (opt_terminate_timeout >= 0.0)
+    {
+      if (!pv_terminate_all_child_processes (opt_terminate_idle_timeout * G_TIME_SPAN_SECOND,
+                                             opt_terminate_timeout * G_TIME_SPAN_SECOND,
+                                             error))
+        goto out;
+    }
+  else
+    {
+      if (!pv_wait_for_child_processes (0, NULL, error))
+        goto out;
     }
 
 out:
diff --git a/src/com.steampowered.PressureVessel.Launcher1.xml b/src/com.steampowered.PressureVessel.Launcher1.xml
index 8313f578758a88b094741f76db55e3e49dcdc977..d064449a4a8d11325f9c0bdb3ed0d90a97cd92fd 100644
--- a/src/com.steampowered.PressureVessel.Launcher1.xml
+++ b/src/com.steampowered.PressureVessel.Launcher1.xml
@@ -80,8 +80,20 @@
         Unknown (unsupported) flags are an error and will cause Launch()
         to fail.
 
-        No options are currently known. Unknown (unsupported) options
-        are ignored.
+        Unknown (unsupported) options are ignored.
+        The following options are supported:
+
+        <variablelist>
+          <varlistentry>
+            <term>terminate-after b</term>
+            <listitem><para>
+              If present and true, the process groups of any remaining
+              launched commands will be terminated by SIGTERM immediately
+              after this command exits, regardless of whether it exits
+              successfully or unsuccessfully.
+            </para></listitem>
+          </varlistentry>
+        </variablelist>
 
         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
diff --git a/src/glib-backports.c b/src/glib-backports.c
index 296d7895c0b2b44bff20e1b40d6aefcf029843fb..c7bfc1f9c8f460ac343a5e423f4c63a43cdb6984 100644
--- a/src/glib-backports.c
+++ b/src/glib-backports.c
@@ -29,6 +29,8 @@
 #include <sys/wait.h>
 #include <unistd.h>
 
+#include <glib-object.h>
+
 /* We have no internationalization */
 #define _(x) x
 
@@ -257,39 +259,73 @@ my_g_dbus_address_escape_value (const gchar *string)
 
 #if !GLIB_CHECK_VERSION(2, 36, 0)
 /*
- * Reimplement GUnixFDSourceFunc API in terms of GIOChannel
+ * Reimplement GUnixFDSourceFunc API in terms of older GLib versions
  */
 
 typedef struct
 {
-  MyGUnixFDSourceFunc func;
-  gpointer user_data;
-  GDestroyNotify destroy;
-} MyGUnixFDClosure;
+  GSource parent;
+  GPollFD pollfd;
+} MyUnixFDSource;
 
-static void
-my_g_unix_fd_closure_free (gpointer p)
+static gboolean
+my_unix_fd_source_prepare (GSource *source,
+                           int *timeout_out)
 {
-  MyGUnixFDClosure *closure = p;
+  if (timeout_out != NULL)
+    *timeout_out = -1;
 
-  if (closure->destroy != NULL)
-    closure->destroy (closure->user_data);
+  /* We don't know we're ready to be dispatched until we have polled. */
+  return FALSE;
+}
 
-  g_free (closure);
+static gboolean
+my_unix_fd_source_check (GSource *source)
+{
+  MyUnixFDSource *self = (MyUnixFDSource *) source;
+
+  /* We're ready to be dispatched if an event occurred. */
+  return (self->pollfd.revents != 0);
 }
 
 static gboolean
-my_g_unix_fd_wrapper (GIOChannel *source,
-                      GIOCondition condition,
-                      gpointer user_data)
+my_unix_fd_source_dispatch (GSource *source,
+                            GSourceFunc callback,
+                            gpointer user_data)
+{
+  MyUnixFDSource *self = (MyUnixFDSource *) source;
+  MyGUnixFDSourceFunc func = (MyGUnixFDSourceFunc) G_CALLBACK (callback);
+
+  g_return_val_if_fail (func != NULL, G_SOURCE_REMOVE);
+  return func (self->pollfd.fd, self->pollfd.revents, user_data);
+}
+
+static void
+my_unix_fd_source_finalize (GSource *source)
+{
+}
+
+static GSourceFuncs my_unix_fd_source_funcs =
+{
+  my_unix_fd_source_prepare,
+  my_unix_fd_source_check,
+  my_unix_fd_source_dispatch,
+  my_unix_fd_source_finalize
+};
+
+GSource *
+my_g_unix_fd_source_new (int fd,
+                         GIOCondition condition)
 {
-  MyGUnixFDClosure *closure = user_data;
-  int fd;
+  GSource *source = g_source_new (&my_unix_fd_source_funcs,
+                                  sizeof (MyUnixFDSource));
+  MyUnixFDSource *self = (MyUnixFDSource *) source;
 
-  fd = g_io_channel_unix_get_fd (source);
-  g_return_val_if_fail (fd >= 0, G_SOURCE_REMOVE);
+  self->pollfd.fd = fd;
+  self->pollfd.events = condition;
 
-  return closure->func (fd, condition, closure->user_data);
+  g_source_add_poll (source, &self->pollfd);
+  return source;
 }
 
 guint
@@ -300,33 +336,18 @@ my_g_unix_fd_add_full (int priority,
                        gpointer user_data,
                        GDestroyNotify destroy)
 {
-  GIOChannel *channel;
-  MyGUnixFDClosure *closure;
+  GSource *source;
   guint ret;
 
   g_return_val_if_fail (fd >= 0, 0);
   g_return_val_if_fail (func != NULL, 0);
 
-  closure = g_new0 (MyGUnixFDClosure, 1);
-  closure->func = func;
-  closure->user_data = user_data;
-  closure->destroy = destroy;
-
-  /* POLLERR, POLLHUP and POLLNVAL are implicitly returned by poll()
-   * and hence by Unix fd watches, but GIOChannel applies its own
-   * filtering */
-  condition |= (G_IO_ERR|G_IO_HUP|G_IO_NVAL);
-
-  channel = g_io_channel_unix_new (fd);
-  /* Disable text recoding, treat it as a bytestream */
-  g_io_channel_set_encoding (channel, NULL, NULL);
-  ret = g_io_add_watch_full (channel,
-                             priority,
-                             condition,
-                             my_g_unix_fd_wrapper,
-                             closure,
-                             my_g_unix_fd_closure_free);
-  g_io_channel_unref (channel);
+  source = my_g_unix_fd_source_new (fd, condition);
+  g_source_set_callback (source, (GSourceFunc) G_CALLBACK (func),
+                         user_data, destroy);
+  g_source_set_priority (source, priority);
+  ret = g_source_attach (source, NULL);
+  g_source_unref (source);
   return ret;
 }
 #endif
diff --git a/src/glib-backports.h b/src/glib-backports.h
index 035c026b4f2268928692fb6be9705b893a775c77..0e047412531ef71e032762e4e044c300657aeb10 100644
--- a/src/glib-backports.h
+++ b/src/glib-backports.h
@@ -78,6 +78,8 @@ gchar *my_g_dbus_address_escape_value (const gchar *string);
 #endif
 
 #if !GLIB_CHECK_VERSION(2, 36, 0)
+#define g_unix_fd_source_new(fd, cond) \
+  my_g_unix_fd_source_new (fd, cond)
 #define g_unix_fd_add(fd, cond, cb, ud) \
   my_g_unix_fd_add_full (G_PRIORITY_DEFAULT, fd, cond, cb, ud, NULL)
 #define g_unix_fd_add_full(prio, fd, cond, cb, ud, destroy) \
@@ -89,4 +91,6 @@ guint my_g_unix_fd_add_full (int priority,
                              MyGUnixFDSourceFunc func,
                              gpointer user_data,
                              GDestroyNotify destroy);
+GSource *my_g_unix_fd_source_new (int fd,
+                                  GIOCondition condition);
 #endif
diff --git a/src/launch.c b/src/launch.c
index 9407d1fd10db774a9a4e66adac44f5819d1c9596..9a27c4c99930b4386dc0242e85b2054b905bced0 100644
--- a/src/launch.c
+++ b/src/launch.c
@@ -67,23 +67,23 @@ process_exited_cb (G_GNUC_UNUSED GDBusConnection *connection,
 {
   GMainLoop *loop = user_data;
   guint32 client_pid = 0;
-  guint32 exit_status = 0;
+  guint32 wait_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);
+  g_variant_get (parameters, "(uu)", &client_pid, &wait_status);
+  g_debug ("child %d exited: wait status %d", client_pid, wait_status);
 
   if (child_pid == client_pid)
     {
       int exit_code = 0;
 
-      if (WIFEXITED (exit_status))
+      if (WIFEXITED (wait_status))
         {
-          exit_code = WEXITSTATUS (exit_status);
+          exit_code = WEXITSTATUS (wait_status);
         }
-      else if (WIFSIGNALED (exit_status))
+      else if (WIFSIGNALED (wait_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
@@ -91,7 +91,7 @@ process_exited_cb (G_GNUC_UNUSED GDBusConnection *connection,
            *  alternative is to disconnect all signal() handlers then send this
            *  signal to ourselves and hope it kills us.
            */
-          exit_code = 128 + WTERMSIG (exit_status);
+          exit_code = 128 + WTERMSIG (wait_status);
         }
       else
         {
@@ -100,7 +100,7 @@ process_exited_cb (G_GNUC_UNUSED GDBusConnection *connection,
            * of WIFEXITED() or WIFSIGNALED() will be true.
            */
           g_warning ("exit status %d is neither WIFEXITED() nor WIFSIGNALED()",
-                     exit_status);
+                     wait_status);
           exit_code = LAUNCH_EX_CANNOT_REPORT;
         }
 
@@ -205,7 +205,7 @@ forward_signal_handler (int sfd,
 }
 
 static guint
-forward_signals (void)
+forward_signals (GError **error)
 {
   static int forward[] = {
     SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGCONT, SIGTSTP, SIGUSR1, SIGUSR2
@@ -223,7 +223,7 @@ forward_signals (void)
 
   if (sfd < 0)
     {
-      g_warning ("Unable to watch signals: %s", g_strerror (errno));
+      glnx_throw_errno_prefix (error, "Unable to watch signals");
       return 0;
     }
 
@@ -237,7 +237,11 @@ forward_signals (void)
    *   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);
+  if (pthread_sigmask (SIG_BLOCK, &mask, NULL) != 0)
+    {
+      glnx_throw_errno_prefix (error, "Unable to block signals");
+      return 0;
+    }
 
   return g_unix_fd_add (sfd, G_IO_IN, forward_signal_handler, NULL);
 }
@@ -422,7 +426,8 @@ static const GOptionEntry options[] =
     "ABSPATH|@ABSTRACT" },
   { "terminate", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_terminate,
-    "Terminate the Launcher server.", NULL },
+    "Terminate the Launcher server after the COMMAND (if any) has run.",
+    NULL },
   { "unset-env", '\0',
     G_OPTION_FLAG_FILENAME, G_OPTION_ARG_CALLBACK, opt_env_cb,
     "Unset environment variable, like env -u.", "VAR" },
@@ -454,7 +459,6 @@ main (int argc,
   GError **error = &local_error;
   g_autoptr(GPtrArray) env_prefix = NULL;
   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;
@@ -474,7 +478,7 @@ main (int argc,
 
   setlocale (LC_ALL, "");
 
-  g_set_prgname ("pressure-vessel-launcher");
+  g_set_prgname ("pressure-vessel-launch");
 
   g_log_set_handler (G_LOG_DOMAIN,
                      G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE,
@@ -516,14 +520,14 @@ main (int argc,
       goto out;
     }
 
-  if (!opt_terminate)
+  if (argc > 1)
     {
       /* 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 ();
+      signal_source = forward_signals (error);
 
       if (signal_source == 0)
         {
@@ -546,23 +550,22 @@ main (int argc,
       argc--;
     }
 
-  if (opt_terminate)
+  if (argc < 2)
     {
-      if (argc != 1)
+      if (!opt_terminate)
         {
-          glnx_throw (error, "--terminate cannot be combined with a COMMAND");
+          glnx_throw (error, "Usage: %s [OPTIONS] COMMAND [ARG...]",
+                      g_get_prgname ());
           goto out;
         }
+
+      command_and_args = NULL;
     }
-  else if (argc < 2)
+  else
     {
-      glnx_throw (error, "Usage: %s [OPTIONS] COMMAND [ARG...]",
-                  g_get_prgname ());
-      goto out;
+      command_and_args = argv + 1;
     }
 
-  command_and_args = argv + 1;
-
   launch_exit_status = LAUNCH_EX_FAILED;
   loop = g_main_loop_new (NULL, FALSE);
 
@@ -653,8 +656,10 @@ main (int argc,
 
   g_assert (bus_or_peer_connection != NULL);
 
-  if (opt_terminate)
+  if (command_and_args == NULL)
     {
+      g_assert (opt_terminate);   /* already checked */
+
       reply = g_dbus_connection_call_sync (bus_or_peer_connection,
                                            service_bus_name,
                                            service_obj_path,
@@ -672,6 +677,7 @@ main (int argc,
       goto out;
     }
 
+  g_assert (command_and_args != NULL);
   g_dbus_connection_signal_subscribe (bus_or_peer_connection,
                                       service_bus_name,   /* NULL if p2p */
                                       service_iface,
@@ -747,9 +753,12 @@ main (int argc,
   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_terminate)
+    g_variant_builder_add (&options_builder, "{s@v}", "terminate-after",
+                           g_variant_new_variant (g_variant_new_boolean (TRUE)));
+
   if (!opt_directory)
     {
       opt_directory = g_get_current_dir ();
@@ -853,8 +862,8 @@ out:
   if (local_error != NULL)
     g_warning ("%s", local_error->message);
 
-  if (forward_signals_id > 0)
-    g_source_remove (forward_signals_id);
+  if (signal_source > 0)
+    g_source_remove (signal_source);
 
   g_strfreev (forward_fds);
   g_free (opt_directory);
diff --git a/src/launcher.c b/src/launcher.c
index baf0c663773fa035f13b7dae179859322ee208ac..7e840e1936f850ffb3b5b0d2d2887ee90f49b6cc 100644
--- a/src/launcher.c
+++ b/src/launcher.c
@@ -99,6 +99,7 @@ typedef struct
   GPid pid;
   gchar *client;
   guint child_watch;
+  gboolean terminate_after;
 } PidData;
 
 static void
@@ -134,8 +135,9 @@ child_watch_died (GPid     pid,
 {
   PidData *pid_data = user_data;
   g_autoptr(GVariant) signal_variant = NULL;
+  gboolean terminate_after = pid_data->terminate_after;
 
-  g_debug ("Client Pid %d died", pid_data->pid);
+  g_debug ("Child %d died: wait status %d", pid_data->pid, status);
 
   signal_variant = g_variant_ref_sink (g_variant_new ("(uu)", pid, status));
   g_dbus_connection_emit_signal (pid_data->connection,
@@ -148,6 +150,13 @@ child_watch_died (GPid     pid,
 
   /* This frees the pid_data, so be careful */
   g_hash_table_remove (client_pid_data_hash, GUINT_TO_POINTER (pid_data->pid));
+
+  if (terminate_after)
+    {
+      g_debug ("Main pid %d died, terminating...", pid);
+      terminate_children (SIGTERM);
+      unref_skeleton_in_timeout ();
+    }
 }
 
 typedef struct
@@ -241,6 +250,7 @@ handle_launch (PvLauncher1           *object,
   g_autofree FdMapEntry *fd_map = NULL;
   g_auto(GStrv) env = NULL;
   gint32 max_fd;
+  gboolean terminate_after = FALSE;
 
   if (fd_list != NULL)
     fds = g_unix_fd_list_peek_fds (fd_list, &fds_len);
@@ -263,6 +273,8 @@ handle_launch (PvLauncher1           *object,
       return TRUE;
     }
 
+  g_variant_lookup (arg_options, "terminate-after", "b", &terminate_after);
+
   g_debug ("Running spawn command %s", arg_argv[0]);
 
   n_fds = 0;
@@ -369,6 +381,7 @@ handle_launch (PvLauncher1           *object,
   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->terminate_after = terminate_after;
   pid_data->child_watch = g_child_watch_add_full (G_PRIORITY_DEFAULT,
                                                   pid,
                                                   child_watch_died,
diff --git a/src/meson.build b/src/meson.build
index 6bf5901d0a668a71afc69d9e17c3e250440d42bf..0ac2f0a4f7d7db605188bb57fb6474967f4880c3 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -58,6 +58,7 @@ pressure_vessel_utils = static_library(
     'utils.h',
   ],
   dependencies : [
+    threads,
     gio_unix,
     libelf,
     libglnx.get_variable('libglnx_dep'),
@@ -83,6 +84,7 @@ executable(
     'adverb.c',
   ],
   dependencies : [
+    threads,
     gio_unix,
     libglnx.get_variable('libglnx_dep'),
   ],
diff --git a/src/runtime.c b/src/runtime.c
index 183d323e36c733889231007d4d7766b5c765a992..c7dd569f74ef3ac08d9d19c6415fb93976c4694b 100644
--- a/src/runtime.c
+++ b/src/runtime.c
@@ -888,26 +888,19 @@ pv_runtime_new (const char *source_files,
  * and rely on bubblewrap's init process for this, but we currently
  * can't do that without breaking gameoverlayrender.so's assumptions,
  * and we want -adverb for its locale functionality anyway. */
-void
-pv_runtime_append_adverbs (PvRuntime *self,
-                           FlatpakBwrap *bwrap)
+gchar *
+pv_runtime_get_adverb (PvRuntime *self,
+                       FlatpakBwrap *bwrap)
 {
-  g_return_if_fail (PV_IS_RUNTIME (self));
-  g_return_if_fail (!pv_bwrap_was_finished (bwrap));
+  g_return_val_if_fail (PV_IS_RUNTIME (self), NULL);
   /* This will be true if pv_runtime_bind() was successfully called. */
-  g_return_if_fail (self->adverb_in_container != NULL);
-
-  flatpak_bwrap_add_args (bwrap,
-                          self->adverb_in_container,
-                          "--subreaper",
-                          NULL);
+  g_return_val_if_fail (self->adverb_in_container != NULL, NULL);
+  g_return_val_if_fail (bwrap != NULL, NULL);
+  g_return_val_if_fail (!pv_bwrap_was_finished (bwrap), NULL);
 
   if (self->flags & PV_RUNTIME_FLAGS_GENERATE_LOCALES)
     flatpak_bwrap_add_args (bwrap, "--generate-locales", NULL);
 
-  if (self->flags & PV_RUNTIME_FLAGS_VERBOSE)
-    flatpak_bwrap_add_args (bwrap, "--verbose", NULL);
-
   if (pv_bwrap_lock_is_ofd (self->runtime_lock))
     {
       int fd = pv_bwrap_lock_steal_fd (self->runtime_lock);
@@ -942,9 +935,7 @@ pv_runtime_append_adverbs (PvRuntime *self,
                               NULL);
     }
 
-  flatpak_bwrap_add_args (bwrap,
-                          "--",
-                          NULL);
+  return g_strdup (self->adverb_in_container);
 }
 
 /*
@@ -2974,6 +2965,12 @@ pv_runtime_bind (PvRuntime *self,
       if (!pv_cheap_tree_copy (pressure_vessel_prefix, dest, error))
         return FALSE;
 
+      flatpak_bwrap_add_args (bwrap,
+                              "--symlink",
+                              "/usr/lib/pressure-vessel/from-host",
+                              "/run/pressure-vessel/pv-from-host",
+                              NULL);
+
       self->adverb_in_container = "/usr/lib/pressure-vessel/from-host/bin/pressure-vessel-adverb";
     }
   else
diff --git a/src/runtime.h b/src/runtime.h
index 93ec89c5d1476fa720c1e10d9184ee8ff4d9b5bf..c3d9cdbf0377d975906dd9ffb445aac6e3f8031f 100644
--- a/src/runtime.h
+++ b/src/runtime.h
@@ -72,8 +72,8 @@ PvRuntime *pv_runtime_new (const char *source_files,
                            PvRuntimeFlags flags,
                            GError **error);
 
-void pv_runtime_append_adverbs (PvRuntime *self,
-                                FlatpakBwrap *bwrap);
+gchar *pv_runtime_get_adverb (PvRuntime *self,
+                              FlatpakBwrap *adverb_args);
 gboolean pv_runtime_bind (PvRuntime *self,
                           FlatpakBwrap *bwrap,
                           GError **error);
diff --git a/src/utils.c b/src/utils.c
index 2fc26db88dbe2717f1d7eb7cf03f2b0630c6e9f4..95f3bf80358d4362ecbbe29a5a88230f15409f3d 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -23,8 +23,13 @@
 #include "utils.h"
 
 #include <ftw.h>
+#include <sys/prctl.h>
+#include <sys/signalfd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
 
 #include <glib.h>
+#include <glib-unix.h>
 #include <glib/gstdio.h>
 #include <gio/gio.h>
 
@@ -575,3 +580,486 @@ pv_get_random_uuid (GError **error)
 
   return g_steal_pointer (&contents);
 }
+
+/**
+ * pv_wait_for_child_processes:
+ * @main_process: process for which we will report wait-status;
+ *  zero or negative if there is no main process
+ * @wait_status_out: (out): Used to store the wait status of `@main_process`,
+ *  as if from `wait()`, on success
+ * @error: Used to raise an error on failure
+ *
+ * Wait for child processes of this process to exit, until the @main_process
+ * has exited. If there is no main process, wait until there are no child
+ * processes at all.
+ *
+ * If the process is a subreaper (`PR_SET_CHILD_SUBREAPER`),
+ * indirect child processes whose parents have exited will be reparented
+ * to it, so this will have the effect of waiting for all descendants.
+ *
+ * If @main_process is positive, return when @main_process has exited.
+ * Child processes that exited before @main_process will also have been
+ * "reaped", but child processes that exit after @main_process will not
+ * (use `pv_wait_for_child_processes (0, &error)` to resume waiting).
+ *
+ * If @main_process is zero or negative, wait for all child processes
+ * to exit.
+ *
+ * This function cannot be called in a process that is using
+ * g_child_watch_source_new() or similar functions, because it waits
+ * for all child processes regardless of their process IDs, and that is
+ * incompatible with waiting for individual child processes.
+ *
+ * Returns: %TRUE when @main_process has exited, or if @main_process
+ *  is zero or negative and all child processes have exited
+ */
+gboolean
+pv_wait_for_child_processes (pid_t main_process,
+                             int *wait_status_out,
+                             GError **error)
+{
+  if (wait_status_out != NULL)
+    *wait_status_out = -1;
+
+  while (1)
+    {
+      int wait_status = -1;
+      pid_t died = wait (&wait_status);
+
+      if (died < 0)
+        {
+          if (errno == EINTR)
+            {
+              continue;
+            }
+          else if (errno == ECHILD)
+            {
+              g_debug ("No more child processes");
+              break;
+            }
+          else
+            {
+              return glnx_throw_errno_prefix (error, "wait");
+            }
+        }
+
+      g_debug ("Child %lld exited with wait status %d",
+               (long long) died, wait_status);
+
+      if (died == main_process)
+        {
+          if (wait_status_out != NULL)
+            *wait_status_out = wait_status;
+
+          return TRUE;
+        }
+    }
+
+  if (main_process > 0)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                   "Process %lld was not seen to exit",
+                   (long long) main_process);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+typedef struct
+{
+  GError *error;
+  GMainContext *context;
+  GSource *wait_source;
+  GSource *kill_source;
+  GSource *sigchld_source;
+  gchar *children_file;
+  /* GINT_TO_POINTER (pid) => arbitrary */
+  GHashTable *sent_sigterm;
+  /* GINT_TO_POINTER (pid) => arbitrary */
+  GHashTable *sent_sigkill;
+  /* Scratch space to build temporary strings */
+  GString *buffer;
+  /* 0, SIGTERM or SIGKILL */
+  int sending_signal;
+  /* Nonzero if wait_source has been attached to context */
+  guint wait_source_id;
+  guint kill_source_id;
+  guint sigchld_source_id;
+  /* TRUE if we reach a point where we have no more child processes. */
+  gboolean finished;
+} TerminationData;
+
+/*
+ * Free everything in @data.
+ */
+static void
+termination_data_clear (TerminationData *data)
+{
+  if (data->wait_source_id != 0)
+    {
+      g_source_destroy (data->wait_source);
+      data->wait_source_id = 0;
+    }
+
+  if (data->kill_source_id != 0)
+    {
+      g_source_destroy (data->kill_source);
+      data->kill_source_id = 0;
+    }
+
+  if (data->sigchld_source_id != 0)
+    {
+      g_source_destroy (data->sigchld_source);
+      data->sigchld_source_id = 0;
+    }
+
+  if (data->wait_source != NULL)
+    g_source_unref (g_steal_pointer (&data->wait_source));
+
+  if (data->kill_source != NULL)
+    g_source_unref (g_steal_pointer (&data->kill_source));
+
+  if (data->sigchld_source != NULL)
+    g_source_unref (g_steal_pointer (&data->sigchld_source));
+
+  g_clear_pointer (&data->children_file, g_free);
+  g_clear_pointer (&data->context, g_main_context_unref);
+  g_clear_error (&data->error);
+  g_clear_pointer (&data->sent_sigterm, g_hash_table_unref);
+  g_clear_pointer (&data->sent_sigkill, g_hash_table_unref);
+
+  if (data->buffer != NULL)
+    g_string_free (g_steal_pointer (&data->buffer), TRUE);
+}
+
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (TerminationData, termination_data_clear)
+
+/*
+ * Do whatever the next step for pv_terminate_all_child_processes() is.
+ *
+ * First, reap child processes that already exited, without blocking.
+ *
+ * Then, act according to the phase we are in:
+ * - before wait_period: do nothing
+ * - after wait_period but before grace_period: send SIGTERM
+ * - after wait_period and grace_period: send SIGKILL
+ */
+static void
+termination_data_refresh (TerminationData *data)
+{
+  g_autofree gchar *contents = NULL;
+  gboolean has_child = FALSE;
+  const char *p;
+  char *endptr;
+
+  if (data->error != NULL)
+    return;
+
+  g_debug ("Checking for child processes");
+
+  while (1)
+    {
+      int wait_status = -1;
+      pid_t died = waitpid (-1, &wait_status, WNOHANG);
+
+      if (died < 0)
+        {
+          if (errno == EINTR)
+            {
+              continue;
+            }
+          else if (errno == ECHILD)
+            {
+              /* No child processes at all. We'll double-check this
+               * a bit later. */
+              break;
+            }
+          else
+            {
+              glnx_throw_errno_prefix (&data->error, "wait");
+              return;
+            }
+        }
+      else if (died == 0)
+        {
+          /* No more child processes have exited, but at least one is
+           * still running. */
+          break;
+        }
+
+      /* This process has gone away, so remove any record that we have
+       * sent it signals. If the pid is reused, we'll want to send
+       * the same signals again. */
+      g_debug ("Process %d exited", died);
+      g_hash_table_remove (data->sent_sigkill, GINT_TO_POINTER (died));
+      g_hash_table_remove (data->sent_sigterm, GINT_TO_POINTER (died));
+    }
+
+  /* See whether we have any remaining children. These could be direct
+   * child processes, or they could be children we adopted because
+   * their parent was one of our descendants and has exited, leaving the
+   * child to be reparented to us (their (great)*grandparent) because we
+   * are a subreaper. */
+  if (!g_file_get_contents (data->children_file, &contents, NULL, &data->error))
+    return;
+
+  g_debug ("Child tasks: %s", contents);
+
+  for (p = contents;
+       p != NULL && *p != '\0';
+       p = endptr)
+    {
+      guint64 maybe_child;
+      int child;
+      GHashTable *already;
+
+      while (*p != '\0' && g_ascii_isspace (*p))
+        p++;
+
+      if (*p == '\0')
+        break;
+
+      maybe_child = g_ascii_strtoull (p, &endptr, 10);
+
+      if (*endptr != '\0' && !g_ascii_isspace (*endptr))
+        {
+          glnx_throw (&data->error, "Non-numeric string found in %s: %s",
+                      data->children_file, p);
+          return;
+        }
+
+      if (maybe_child > G_MAXINT)
+        {
+          glnx_throw (&data->error, "Out-of-range number found in %s: %s",
+                      data->children_file, p);
+          return;
+        }
+
+      child = (int) maybe_child;
+      g_string_printf (data->buffer, "/proc/%d", child);
+
+      /* If the task is just a thread, it won't have a /proc/%d directory
+       * in its own right. We don't kill threads, only processes. */
+      if (!g_file_test (data->buffer->str, G_FILE_TEST_IS_DIR))
+        {
+          g_debug ("Task %d is a thread, not a process", child);
+          continue;
+        }
+
+      has_child = TRUE;
+
+      if (data->sending_signal == 0)
+        break;
+      else if (data->sending_signal == SIGKILL)
+        already = data->sent_sigkill;
+      else
+        already = data->sent_sigterm;
+
+      if (!g_hash_table_contains (already, GINT_TO_POINTER (child)))
+        {
+          g_debug ("Sending signal %d to process %d",
+                   data->sending_signal, child);
+          g_hash_table_add (already, GINT_TO_POINTER (child));
+
+          if (kill (child, data->sending_signal) < 0)
+            g_warning ("Unable to send signal %d to process %d: %s",
+                       data->sending_signal, child, g_strerror (errno));
+
+          /* In case the child is stopped, wake it up to receive the signal */
+          if (kill (child, SIGCONT) < 0)
+            g_warning ("Unable to send SIGCONT to process %d: %s",
+                       child, g_strerror (errno));
+
+          /* When the child terminates, we will get SIGCHLD and come
+           * back to here. */
+        }
+    }
+
+  if (!has_child)
+    data->finished = TRUE;
+}
+
+/*
+ * Move from wait period to grace period: start sending SIGTERM to
+ * child processes.
+ */
+static gboolean
+start_sending_sigterm (gpointer user_data)
+{
+  TerminationData *data = user_data;
+
+  g_debug ("Wait period finished, starting to send SIGTERM...");
+
+  if (data->sending_signal == 0)
+    data->sending_signal = SIGTERM;
+
+  termination_data_refresh (data);
+
+  data->wait_source_id = 0;
+  return G_SOURCE_REMOVE;
+}
+
+/*
+ * End of grace period: start sending SIGKILL to child processes.
+ */
+static gboolean
+start_sending_sigkill (gpointer user_data)
+{
+  TerminationData *data = user_data;
+
+  g_debug ("Grace period finished, starting to send SIGKILL...");
+
+  data->sending_signal = SIGKILL;
+  termination_data_refresh (data);
+
+  data->kill_source_id = 0;
+  return G_SOURCE_REMOVE;
+}
+
+/*
+ * Called when at least one child process has exited, resulting in
+ * SIGCHLD to this process.
+ */
+static gboolean
+sigchld_cb (int sfd,
+            G_GNUC_UNUSED GIOCondition condition,
+            gpointer user_data)
+{
+  TerminationData *data = user_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);
+    }
+
+  g_debug ("One or more child processes exited");
+  termination_data_refresh (data);
+  return G_SOURCE_CONTINUE;
+}
+
+/**
+ * pv_terminate_all_child_processes:
+ * @wait_period: If greater than 0, wait this many microseconds before
+ *  sending `SIGTERM`
+ * @grace_period: If greater than 0, after @wait_period plus this many
+ *  microseconds, use `SIGKILL` instead of `SIGTERM`. If 0, proceed
+ *  directly to sending `SIGKILL`.
+ * @error: Used to raise an error on failure
+ *
+ * Make sure all child processes are terminated.
+ *
+ * If a child process catches `SIGTERM` but does not exit promptly and
+ * does not pass the signal on to its descendants, note that its
+ * descendant processes are not guaranteed to be terminated gracefully
+ * with `SIGTERM`; they might only receive `SIGKILL`.
+ *
+ * Return when all child processes have exited or when an error has
+ * occurred.
+ *
+ * This function cannot be called in a process that is using
+ * g_child_watch_source_new() or similar functions.
+ *
+ * The process must be a subreaper, and must have `SIGCHLD` blocked.
+ *
+ * Returns: %TRUE on success.
+ */
+gboolean
+pv_terminate_all_child_processes (GTimeSpan wait_period,
+                                  GTimeSpan grace_period,
+                                  GError **error)
+{
+  g_auto(TerminationData) data = {};
+  sigset_t mask;
+  int is_subreaper = -1;
+  glnx_autofd int sfd = -1;
+
+  if (prctl (PR_GET_CHILD_SUBREAPER, (unsigned long) &is_subreaper, 0, 0, 0) != 0)
+    return glnx_throw_errno_prefix (error, "prctl PR_GET_CHILD_SUBREAPER");
+
+  if (is_subreaper != 1)
+    return glnx_throw (error, "Process is not a subreaper");
+
+  sigemptyset (&mask);
+
+  if (pthread_sigmask (SIG_BLOCK, NULL, &mask) != 0)
+    return glnx_throw_errno_prefix (error, "pthread_sigmask");
+
+  if (!sigismember (&mask, SIGCHLD))
+    return glnx_throw (error, "Process has not blocked SIGCHLD");
+
+  data.children_file = g_strdup_printf ("/proc/%d/task/%d/children",
+                                        getpid (), getpid ());
+  data.context = g_main_context_new ();
+  data.sent_sigterm = g_hash_table_new (NULL, NULL);
+  data.sent_sigkill = g_hash_table_new (NULL, NULL);
+  data.buffer = g_string_new ("/proc/2345678901");
+
+  if (wait_period > 0 && grace_period > 0)
+    {
+      data.wait_source = g_timeout_source_new (wait_period / G_TIME_SPAN_MILLISECOND);
+
+      g_source_set_callback (data.wait_source, start_sending_sigterm,
+                             &data, NULL);
+      data.wait_source_id = g_source_attach (data.wait_source, data.context);
+    }
+  else if (grace_period > 0)
+    {
+      start_sending_sigterm (&data);
+    }
+
+  if (wait_period + grace_period > 0)
+    {
+      data.kill_source = g_timeout_source_new ((wait_period + grace_period) / G_TIME_SPAN_MILLISECOND);
+
+      g_source_set_callback (data.kill_source, start_sending_sigkill,
+                             &data, NULL);
+      data.kill_source_id = g_source_attach (data.kill_source, data.context);
+    }
+  else
+    {
+      start_sending_sigkill (&data);
+    }
+
+  sigemptyset (&mask);
+  sigaddset (&mask, SIGCHLD);
+
+  sfd = signalfd (-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
+
+  if (sfd < 0)
+    return glnx_throw_errno_prefix (error, "signalfd");
+
+  data.sigchld_source = g_unix_fd_source_new (sfd, G_IO_IN);
+  g_source_set_callback (data.sigchld_source,
+                         (GSourceFunc) G_CALLBACK (sigchld_cb), &data, NULL);
+  data.sigchld_source_id = g_source_attach (data.sigchld_source, data.context);
+
+  termination_data_refresh (&data);
+
+  while (data.error == NULL
+         && !data.finished
+         && (data.wait_source_id != 0
+             || data.kill_source_id != 0
+             || data.sigchld_source_id != 0))
+    g_main_context_iteration (data.context, TRUE);
+
+  if (data.error != NULL)
+    {
+      g_propagate_error (error, g_steal_pointer (&data.error));
+      return FALSE;
+    }
+
+  return TRUE;
+}
diff --git a/src/utils.h b/src/utils.h
index d4215d0f6688dac03a78a2caa80840764ccc69b7..3d89b7cedce569deca2ad6d61b2053e38a08e7d0 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -23,9 +23,18 @@
 #pragma once
 
 #include <stdio.h>
+#include <sys/types.h>
 
 #include <glib.h>
 
+#ifndef PR_GET_CHILD_SUBREAPER
+#define PR_GET_CHILD_SUBREAPER 37
+#endif
+
+#ifndef PR_SET_CHILD_SUBREAPER
+#define PR_SET_CHILD_SUBREAPER 36
+#endif
+
 void pv_avoid_gvfs (void);
 
 int pv_envp_cmp (const void *p1,
@@ -60,3 +69,11 @@ void pv_async_signal_safe_error (const char *message,
                                  int exit_status) G_GNUC_NORETURN;
 
 gchar *pv_get_random_uuid (GError **error);
+
+gboolean pv_wait_for_child_processes (pid_t main_process,
+                                      int *wait_status_out,
+                                      GError **error);
+
+gboolean pv_terminate_all_child_processes (GTimeSpan wait_period,
+                                           GTimeSpan grace_period,
+                                           GError **error);
diff --git a/src/wrap.c b/src/wrap.c
index 1812256b27426ba1ae36a3635b1ad99aff2def1a..d5a201dd9835a697379fcfae4ea65e3fdd5abe31 100644
--- a/src/wrap.c
+++ b/src/wrap.c
@@ -374,6 +374,8 @@ static char *opt_runtime_base = NULL;
 static char *opt_runtime = NULL;
 static Tristate opt_share_home = TRISTATE_MAYBE;
 static gboolean opt_share_pid = TRUE;
+static double opt_terminate_idle_timeout = 0.0;
+static double opt_terminate_timeout = -1.0;
 static gboolean opt_verbose = FALSE;
 static gboolean opt_version = FALSE;
 static gboolean opt_version_only = FALSE;
@@ -673,6 +675,19 @@ static GOptionEntry options[] =
   { "xterm", '\0',
     G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
     "Equivalent to --terminal=xterm", NULL },
+  { "terminate-idle-timeout", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &opt_terminate_idle_timeout,
+    "If --terminate-timeout is used, wait this many seconds before "
+    "sending SIGTERM. [default: 0.0]"
+    "SECONDS" },
+  { "terminate-timeout", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &opt_terminate_timeout,
+    "Send SIGTERM and SIGCONT to descendant processes that didn't "
+    "exit within --terminate-idle-timeout. If they don't all exit within "
+    "this many seconds, send SIGKILL and SIGCONT to survivors. If 0.0, "
+    "skip SIGTERM and use SIGKILL immediately. Implies --subreaper. "
+    "[Default: -1.0, meaning don't signal].",
+    "SECONDS" },
   { "verbose", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose,
     "Be more verbose.", NULL },
@@ -741,6 +756,8 @@ main (int argc,
   g_auto(GStrv) original_argv = NULL;
   int original_argc = argc;
   g_autoptr(FlatpakBwrap) bwrap = NULL;
+  g_autoptr(FlatpakBwrap) adverb_args = NULL;
+  g_autofree gchar *adverb_in_container = NULL;
   g_autoptr(FlatpakBwrap) wrapped_command = NULL;
   g_autofree gchar *bwrap_executable = NULL;
   g_autoptr(GString) adjusted_ld_preload = g_string_new ("");
@@ -1430,8 +1447,41 @@ main (int argc,
   if (!flatpak_bwrap_bundle_args (bwrap, 1, -1, FALSE, error))
     goto out;
 
+  adverb_args = flatpak_bwrap_new (NULL);
+
   if (runtime != NULL)
-    pv_runtime_append_adverbs (runtime, bwrap);
+    adverb_in_container = pv_runtime_get_adverb (runtime, adverb_args);
+
+  if (opt_terminate_timeout >= 0.0)
+    {
+      if (opt_terminate_idle_timeout > 0.0)
+        flatpak_bwrap_add_arg_printf (adverb_args,
+                                      "--terminate-idle-timeout=%f",
+                                      opt_terminate_idle_timeout);
+
+      flatpak_bwrap_add_arg_printf (adverb_args,
+                                    "--terminate-timeout=%f",
+                                    opt_terminate_timeout);
+    }
+
+  /* If not using a runtime, the adverb in the container has the
+   * same path as outside */
+  if (adverb_in_container == NULL)
+    adverb_in_container = g_build_filename (tools_dir,
+                                            "pressure-vessel-adverb",
+                                            NULL);
+
+  flatpak_bwrap_add_args (bwrap,
+                          adverb_in_container,
+                          "--exit-with-parent",
+                          "--subreaper",
+                          NULL);
+
+  if (opt_verbose)
+    flatpak_bwrap_add_arg (bwrap, "--verbose");
+
+  flatpak_bwrap_append_bwrap (bwrap, adverb_args);
+  flatpak_bwrap_add_arg (bwrap, "--");
 
   g_debug ("Adding wrapped command...");
   flatpak_bwrap_append_args (bwrap, wrapped_command->argv);
diff --git a/tests/adverb.py b/tests/adverb.py
index 09d9d6a06d37c4d4249eea66d3035c983a1d4f76..e1ee267a13bda3093d48599d755b62ad4896b665 100755
--- a/tests/adverb.py
+++ b/tests/adverb.py
@@ -8,7 +8,6 @@ import os
 import signal
 import subprocess
 import sys
-import tempfile
 
 
 try:
diff --git a/tests/launcher.py b/tests/launcher.py
index be470e2578928a658fe97b792b5dcd5345b8e424..eca7f80090fd8555d73fc9c888c43460feaf4939 100755
--- a/tests/launcher.py
+++ b/tests/launcher.py
@@ -57,8 +57,11 @@ class TestLauncher(BaseTest):
 
     def test_socket_directory(self) -> None:
         with tempfile.TemporaryDirectory(prefix='test-') as temp:
+            need_terminate = True
             printf_symlink = os.path.join(temp, 'printf=symlink')
-            os.symlink(shutil.which('printf'), printf_symlink)
+            printf = shutil.which('printf')
+            assert printf is not None
+            os.symlink(printf, printf_symlink)
 
             proc = subprocess.Popen(
                 [
@@ -262,8 +265,24 @@ class TestLauncher(BaseTest):
                     launch.wait(),
                     (128 + signal.SIGINT, -signal.SIGINT),
                 )
+
+                completed = run_subprocess(
+                    self.launch + [
+                        '--socket', socket,
+                        '--terminate',
+                        '--',
+                        'sh', '-euc', 'echo Goodbye',
+                    ],
+                    check=True,
+                    stdout=subprocess.PIPE,
+                    stderr=2,
+                )
+                need_terminate = False
+                self.assertEqual(completed.stdout, b'Goodbye\n')
             finally:
-                proc.terminate()
+                if need_terminate:
+                    proc.terminate()
+
                 proc.wait()
                 self.assertEqual(proc.returncode, 0)
 
@@ -561,10 +580,12 @@ class TestLauncher(BaseTest):
                 stderr=2,
                 universal_newlines=True,
             )
+            stdin = proc.stdin
+            assert stdin is not None
             os.close(read_end)
 
             if use_stdin:
-                proc.stdin.close()
+                stdin.close()
             else:
                 os.close(write_end)
 
diff --git a/tests/meson.build b/tests/meson.build
index 146d9b851c57f5a2af1dc013d2764549141b4adb..34d42008754fe57f8e93dca0fe7a19961288a657 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -47,6 +47,7 @@ tests = [
 compiled_tests = [
   'bwrap-lock',
   'resolve-in-sysroot',
+  'wait-for-child-processes',
   'utils',
 ]
 
diff --git a/tests/utils.py b/tests/utils.py
index 99c3cc4cca57995dad933fe2b83dd6c3dc9a740c..988fcb37f07d4d08737b067a2af30e9d7cc1dc90 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -6,7 +6,6 @@
 import os
 import subprocess
 import sys
-import tempfile
 
 
 try:
diff --git a/tests/wait-for-child-processes.c b/tests/wait-for-child-processes.c
new file mode 100644
index 0000000000000000000000000000000000000000..f00c88c241fe267d1f267d805bbc18c66771b64d
--- /dev/null
+++ b/tests/wait-for-child-processes.c
@@ -0,0 +1,349 @@
+/*
+ * Copyright © 2019-2020 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <sys/prctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include "glib-backports.h"
+#include "libglnx/libglnx.h"
+
+#include "test-utils.h"
+#include "utils.h"
+
+typedef struct
+{
+  int unused;
+} Fixture;
+
+typedef struct
+{
+  int unused;
+} Config;
+
+static void
+setup (Fixture *f,
+       gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+
+  /* Assert we initially have no child processes */
+  g_assert_cmpint (waitpid (-1, NULL, WNOHANG) >= 0 ? 0 : errno, ==, ECHILD);
+}
+
+static void
+teardown (Fixture *f,
+          gconstpointer context)
+{
+  G_GNUC_UNUSED const Config *config = context;
+
+  /* Assert we end up with no child processes */
+  g_assert_cmpint (waitpid (-1, NULL, WNOHANG) >= 0 ? 0 : errno, ==, ECHILD);
+}
+
+#define SPAWN_FLAGS \
+  (G_SPAWN_SEARCH_PATH \
+   | G_SPAWN_LEAVE_DESCRIPTORS_OPEN \
+   | G_SPAWN_DO_NOT_REAP_CHILD)
+
+static void
+test_terminate_nothing (Fixture *f,
+                        gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  gboolean ret;
+
+  ret = pv_terminate_all_child_processes (0, 0, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = pv_terminate_all_child_processes (0, 100 * G_TIME_SPAN_MILLISECOND, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = pv_terminate_all_child_processes (100 * G_TIME_SPAN_MILLISECOND, 0, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = pv_terminate_all_child_processes (100 * G_TIME_SPAN_MILLISECOND,
+                                          100 * G_TIME_SPAN_MILLISECOND, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+}
+
+static void
+test_terminate_sigterm (Fixture *f,
+                        gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  gboolean ret;
+  const char * const argv[] = { "sh", "-c", "sleep 3600", NULL };
+
+  ret = g_spawn_async (NULL,  /* cwd */
+                       (char **) argv,
+                       NULL,  /* envp */
+                       SPAWN_FLAGS,
+                       NULL, NULL,    /* child setup */
+                       NULL,  /* pid */
+                       &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = pv_terminate_all_child_processes (0, 60 * G_TIME_SPAN_SECOND, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+}
+
+static void
+test_terminate_sigkill (Fixture *f,
+                        gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  gboolean ret;
+  const char * const argv[] =
+  {
+    "sh", "-c", "trap 'echo Ignoring SIGTERM >&2' TERM; sleep 3600", NULL
+  };
+
+  ret = g_spawn_async (NULL,  /* cwd */
+                       (char **) argv,
+                       NULL,  /* envp */
+                       SPAWN_FLAGS,
+                       NULL, NULL,    /* child setup */
+                       NULL,  /* pid */
+                       &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  /* We give it 100ms before SIGTERM to let it put the trap in place */
+  ret = pv_terminate_all_child_processes (100 * G_TIME_SPAN_MILLISECOND,
+                                          100 * G_TIME_SPAN_MILLISECOND, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+}
+
+static void
+test_terminate_sigkill_immediately (Fixture *f,
+                                    gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  gboolean ret;
+  const char * const argv[] =
+  {
+    "sh", "-c", "trap 'echo Ignoring SIGTERM >&2' TERM; sleep 3600", NULL
+  };
+
+  ret = g_spawn_async (NULL,  /* cwd */
+                       (char **) argv,
+                       NULL,  /* envp */
+                       SPAWN_FLAGS,
+                       NULL, NULL,    /* child setup */
+                       NULL,  /* pid */
+                       &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = pv_terminate_all_child_processes (0, 0, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+}
+
+static void
+test_wait_for_all (Fixture *f,
+                   gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  int wstat;
+  gboolean ret;
+  const char * const argv[] = { "sh", "-c", "exit 42", NULL };
+
+  ret = g_spawn_async (NULL,  /* cwd */
+                       (char **) argv,
+                       NULL,  /* envp */
+                       SPAWN_FLAGS,
+                       NULL, NULL,    /* child setup */
+                       NULL,  /* pid */
+                       &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = pv_wait_for_child_processes (0, &wstat, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+  g_assert_cmpint (wstat, ==, -1);
+}
+
+static void
+test_wait_for_main (Fixture *f,
+                    gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  int wstat;
+  gboolean ret;
+  GPid main_pid;
+  const char * const argv[] = { "sh", "-c", "exit 42", NULL };
+
+  ret = g_spawn_async (NULL,  /* cwd */
+                       (char **) argv,
+                       NULL,  /* envp */
+                       SPAWN_FLAGS,
+                       NULL, NULL,    /* child setup */
+                       &main_pid,
+                       &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = pv_wait_for_child_processes (main_pid, &wstat, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+  g_assert_true (WIFEXITED (wstat));
+  g_assert_cmpint (WEXITSTATUS (wstat), ==, 42);
+}
+
+static void
+test_wait_for_main_plus (Fixture *f,
+                         gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  int wstat;
+  gboolean ret;
+  GPid main_pid;
+  const char * const before_argv[] = { "sh", "-c", "exit 0", NULL };
+  const char * const main_argv[] = { "sh", "-c", "sleep 1; kill -TERM $$", NULL };
+  const char * const after_argv[] = { "sh", "-c", "sleep 2", NULL };
+
+  ret = g_spawn_async (NULL,  /* cwd */
+                       (char **) before_argv,
+                       NULL,  /* envp */
+                       SPAWN_FLAGS,
+                       NULL, NULL,    /* child setup */
+                       NULL,          /* PID */
+                       &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = g_spawn_async (NULL,  /* cwd */
+                       (char **) main_argv,
+                       NULL,  /* envp */
+                       SPAWN_FLAGS,
+                       NULL, NULL,    /* child setup */
+                       &main_pid,
+                       &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = g_spawn_async (NULL,  /* cwd */
+                       (char **) after_argv,
+                       NULL,  /* envp */
+                       SPAWN_FLAGS,
+                       NULL, NULL,    /* child setup */
+                       NULL,          /* PID */
+                       &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  ret = pv_wait_for_child_processes (main_pid, &wstat, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+  g_assert_true (WIFSIGNALED (wstat));
+  g_assert_cmpint (WTERMSIG (wstat), ==, SIGTERM);
+
+  /* Don't leak the other processes, if any (probably after_argv) */
+  ret = pv_wait_for_child_processes (0, &wstat, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+}
+
+static void
+test_wait_for_nothing (Fixture *f,
+                       gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  int wstat;
+  gboolean ret;
+
+  ret = pv_wait_for_child_processes (0, &wstat, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+  g_assert_cmpint (wstat, ==, -1);
+}
+
+static void
+test_wait_for_wrong_main (Fixture *f,
+                          gconstpointer context)
+{
+  g_autoptr(GError) error = NULL;
+  int wstat;
+  gboolean ret;
+
+  ret = pv_wait_for_child_processes (getpid (), &wstat, &error);
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND);
+  g_assert_false (ret);
+  g_assert_cmpint (wstat, ==, -1);
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  sigset_t mask;
+
+  sigemptyset (&mask);
+  sigaddset (&mask, SIGCHLD);
+
+  /* Must be called before we start any threads */
+  if (pthread_sigmask (SIG_BLOCK, &mask, NULL) != 0)
+    g_error ("pthread_sigmask: %s", g_strerror (errno));
+
+  prctl (PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0);
+  pv_avoid_gvfs ();
+
+  g_test_init (&argc, &argv, NULL);
+  g_test_add ("/terminate/nothing", Fixture, NULL,
+              setup, test_terminate_nothing, teardown);
+  g_test_add ("/terminate/sigterm", Fixture, NULL,
+              setup, test_terminate_sigterm, teardown);
+  g_test_add ("/terminate/sigkill", Fixture, NULL,
+              setup, test_terminate_sigkill, teardown);
+  g_test_add ("/terminate/sigkill-immediately", Fixture, NULL,
+              setup, test_terminate_sigkill_immediately, teardown);
+  g_test_add ("/wait/for-all", Fixture, NULL,
+              setup, test_wait_for_all, teardown);
+  g_test_add ("/wait/for-main", Fixture, NULL,
+              setup, test_wait_for_main, teardown);
+  g_test_add ("/wait/for-main-plus", Fixture, NULL,
+              setup, test_wait_for_main_plus, teardown);
+  g_test_add ("/wait/for-nothing", Fixture, NULL,
+              setup, test_wait_for_nothing, teardown);
+  g_test_add ("/wait/for-wrong-main", Fixture, NULL,
+              setup, test_wait_for_wrong_main, teardown);
+
+  return g_test_run ();
+}