diff --git a/man/launcher.1.md b/man/launcher.1.md
index f6c292ed6f94944c93a1e902dffb77960c037490..d2afb561a2d7107a4bc04d12a39868f2857de1df 100644
--- a/man/launcher.1.md
+++ b/man/launcher.1.md
@@ -47,6 +47,11 @@ D-Bus session bus, and executes arbitrary commands as subprocesses.
 :   Connect to the well-known D-Bus session bus, request the given name
     and listen for commands there.
 
+**--exit-on-readable** *FD*
+:   Exit when file descriptor *FD* (typically 0, for **stdin**) becomes
+    readable, meaning either data is available or end-of-file has been
+    reached.
+
 **--replace**
 :   When used with **--bus-name**, allow other
     **pressure-vessel-launcher** processes to take over the bus name,
@@ -196,19 +201,29 @@ Listen on the session bus, and run two commands, and exit:
     kill -TERM "$launcher_pid"
     wait "$launcher_pid"
 
-Listen on a socket in a temporary directory:
+Listen on a socket in a temporary directory, and use `--exit-with-fd`.
+Production code would be more likely to invoke **pressure-vessel-launch**
+from C, C++ or Python code rather than a shell script, and use pipes
+instead of fifos.
 
     tmpdir="$(mktemp -d)"
-    mkfifo "${tmpdir}/fifo"
+    mkfifo "${tmpdir}/exit-fifo"
+    mkfifo "${tmpdir}/ready-fifo"
     pressure-vessel-wrap \
         --filesystem="${tmpdir}" \
         ... \
         -- \
         pressure-vessel-launcher \
-            --socket="${tmpdir}/launcher" > "${tmpdir}/fifo" &
+            --exit-on-readable=0 \
+            --socket="${tmpdir}/launcher" \
+        < "${tmpdir}/exit-fifo" \
+        > "${tmpdir}/ready-fifo" \
+        &
     launcher_pid="$!"
+    sleep infinity > "${tmpdir}/exit-fifo" &
+    sleep_pid="$!"
     # Wait for EOF so we know the socket is available
-    cat "${tmpdir}/fifo" > /dev/null
+    cat "${tmpdir}/ready-fifo" > /dev/null
 
     pressure-vessel-launch \
         --socket="${tmpdir}/launcher" \
@@ -218,9 +233,17 @@ Listen on a socket in a temporary directory:
         --socket="${tmpdir}/launcher" \
         -- \
         id
-
-    kill -TERM "$launcher_pid"
-    wait "$launcher_pid"
+    pressure-vessel-launch \
+        --socket="${tmpdir}/launcher" \
+        -- \
+        sleep infinity &
+    launch_sleep_pid="$!"
+
+    # Make pressure-vessel-launcher's stdin become readable, which
+    # will make it exit due to --exit-on-readable
+    kill "$sleep_pid"
+    wait "$launcher_pid" || echo "launcher exit status: $?"
+    wait "$launch_sleep_pid" || echo "launched process exit status: $?"
     rm -fr "$tmpdir"
 
 <!-- vim:set sw=4 sts=4 et: -->
diff --git a/src/launcher.c b/src/launcher.c
index 2a98a3ebcb7eff1903c3b2c70d82124277bcea9f..baf0c663773fa035f13b7dae179859322ee208ac 100644
--- a/src/launcher.c
+++ b/src/launcher.c
@@ -853,7 +853,103 @@ connect_to_signals (void)
   return g_unix_fd_add (sfd, G_IO_IN, signal_handler, NULL);
 }
 
+/*
+ * If @fd is `stdin`, make `stdin` point to /dev/null and return a
+ * new fd that is a duplicate of the original `stdin`, so that the
+ * `stdin` inherited by child processes will not collide with the fd
+ * we are using for some other purpose.
+ */
+static int
+avoid_stdin (int fd,
+             GError **error)
+{
+  g_return_val_if_fail (fd >= 0, FALSE);
+
+  if (fd == STDIN_FILENO)
+    {
+      glnx_autofd int old_stdin = -1;
+      glnx_autofd int new_stdin = -1;
+      int fd_flags;
+
+      old_stdin = dup (STDIN_FILENO);
+
+      if (old_stdin < 0)
+        {
+          glnx_throw_errno_prefix (error,
+                                   "Unable to duplicate standard input");
+          return -1;
+        }
+
+      fd_flags = fcntl (old_stdin, F_GETFD);
+
+      if (fd_flags < 0 ||
+          fcntl (old_stdin, F_SETFD, fd_flags | FD_CLOEXEC) != 0)
+        {
+          glnx_throw_errno_prefix (error, "Unable to set flags on fd %d",
+                                   old_stdin);
+          return -1;
+        }
+
+      new_stdin = open ("/dev/null", O_RDONLY | O_CLOEXEC);
+
+      if (new_stdin < 0)
+        {
+          glnx_throw_errno_prefix (error, "Unable to open /dev/null");
+          return -1;
+        }
+
+      if (dup2 (new_stdin, STDIN_FILENO) != STDIN_FILENO)
+        {
+          glnx_throw_errno_prefix (error,
+                                   "Unable to make stdin point to /dev/null");
+          return -1;
+        }
+
+      fd = glnx_steal_fd (&old_stdin);
+    }
+
+  return fd;
+}
+
+static gboolean
+exit_on_readable_cb (int fd,
+                     GIOCondition condition,
+                     gpointer user_data)
+{
+  guint *id_p = user_data;
+
+  terminate_children (SIGTERM);
+  g_main_loop_quit (main_loop);
+  *id_p = 0;
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+set_up_exit_on_readable (int fd,
+                         guint *id_p,
+                         GError **error)
+{
+  g_return_val_if_fail (fd >= 0, FALSE);
+  g_return_val_if_fail (id_p != NULL, FALSE);
+  g_return_val_if_fail (*id_p == 0, FALSE);
+
+  if (fd == STDOUT_FILENO || fd == STDERR_FILENO)
+    {
+      return glnx_throw (error,
+                         "--exit-on-readable fd cannot be stdout or stderr");
+    }
+
+  fd = avoid_stdin (fd, error);
+
+  if (fd < 0)
+    return FALSE;
+
+  *id_p = g_unix_fd_add (fd, G_IO_IN|G_IO_ERR|G_IO_HUP, exit_on_readable_cb, id_p);
+  return TRUE;
+}
+
 static gchar *opt_bus_name = NULL;
+static gint opt_exit_on_readable_fd = -1;
 static gboolean opt_replace = FALSE;
 static gchar *opt_socket = NULL;
 static gchar *opt_socket_directory = NULL;
@@ -866,6 +962,11 @@ static GOptionEntry options[] =
     G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_bus_name,
     "Use this well-known name on the D-Bus session bus.",
     "NAME" },
+  { "exit-on-readable", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_INT, &opt_exit_on_readable_fd,
+    "Exit when data is available for reading or when end-of-file is "
+    "reached on this fd, usually 0 for stdin.",
+    "FD" },
   { "replace", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_replace,
     "Replace a previous instance with the same bus name. "
@@ -907,6 +1008,7 @@ main (int argc,
   g_autoptr(AutoDBusServer) server = NULL;
   g_autoptr(GOptionContext) context = NULL;
   guint signals_id = 0;
+  guint exit_on_readable_id = 0;
   g_autoptr(GError) local_error = NULL;
   GError **error = &local_error;
   gsize i;
@@ -957,6 +1059,16 @@ main (int argc,
       goto out;
     }
 
+  if (opt_exit_on_readable_fd >= 0)
+    {
+      if (!set_up_exit_on_readable (opt_exit_on_readable_fd,
+                                    &exit_on_readable_id, error))
+        {
+          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
@@ -1154,6 +1266,9 @@ out:
   if (local_error != NULL)
     g_warning ("%s", local_error->message);
 
+  if (exit_on_readable_id > 0)
+    g_source_remove (exit_on_readable_id);
+
   if (signals_id > 0)
     g_source_remove (signals_id);