[vte] spawn: Rework spawning



commit 80a583842f9d013f63d80c469c4b7eace2c03414
Author: Christian Persch <chpe src gnome org>
Date:   Mon Apr 27 20:49:04 2020 +0200

    spawn: Rework spawning
    
    The old spawning code was copied from glib, and used a thread to
    make the synchronous variant into an asynchronous one. This had the
    problem that fork(3p) was called from the worker thread, not the
    calling (main) thread.
    
    The new code replaces this with a two-stage approach where the
    fork takes place on the calling thread, and only the async wait
    for the child error is done in a worker thread.
    
    This also unifies the sync and async spawn variants to use
    the exact same code, only called in one or two stages.
    
    This also removes calls to setenv/unsetenv from the child setup
    code, which are not safe to do there. Instead, the environment
    is prepared in advance and used via execve(2).
    
    Fixes: https://gitlab.gnome.org/GNOME/vte/-/issues/118

 src/app/app.cc        |   7 +-
 src/glib-glue.hh      |  63 ++++++-
 src/libc-glue.hh      |  13 +-
 src/meson.build       |   3 +
 src/missing.hh        |  25 +++
 src/pty.cc            | 304 +++----------------------------
 src/pty.hh            |  22 +--
 src/refptr.hh         |   7 +
 src/spawn.cc          | 486 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/spawn.hh          | 213 ++++++++++++++++++++++
 src/vte.cc            |   4 +-
 src/vte/vteterminal.h |   2 +-
 src/vtedefines.hh     |   4 +
 src/vtegtk.cc         | 106 +++++------
 src/vtepty.cc         | 308 +++++++++++++++-----------------
 src/vteptyinternal.hh |  23 +--
 src/vtespawn.cc       | 386 ++++++---------------------------------
 src/vtespawn.hh       |  32 ++--
 18 files changed, 1104 insertions(+), 904 deletions(-)
---
diff --git a/src/app/app.cc b/src/app/app.cc
index d8603b71..c5d75ec2 100644
--- a/src/app/app.cc
+++ b/src/app/app.cc
@@ -1235,7 +1235,10 @@ window_spawn_cb(VteTerminal* terminal,
         if (error != nullptr) {
                 verbose_printerr("Spawning failed: %s\n", error->message);
 
-                if (!options.keep)
+                auto msg = vte::glib::take_string(g_strdup_printf("Spawning failed: %s", error->message));
+                if (options.keep)
+                        vte_terminal_feed(window->terminal, msg.get(), -1);
+                else
                         gtk_widget_destroy(GTK_WIDGET(window));
         }
 }
@@ -1255,7 +1258,7 @@ vteapp_window_launch_argv(VteappWindow* window,
                                  options.environment,
                                  spawn_flags,
                                  nullptr, nullptr, nullptr, /* child setup, data and destroy */
-                                 30 * 1000 /* 30s timeout */,
+                                 -1 /* default timeout of 30s */,
                                  nullptr /* cancellable */,
                                  window_spawn_cb, window);
         return true;
diff --git a/src/glib-glue.hh b/src/glib-glue.hh
index c2bc373a..53b6c348 100644
--- a/src/glib-glue.hh
+++ b/src/glib-glue.hh
@@ -26,24 +26,54 @@
 
 namespace vte::glib {
 
+template <typename T, typename D, D d>
+class FreeablePtr : public std::unique_ptr<T, D>
+{
+private:
+        using base_type = std::unique_ptr<T, D>;
+
+public:
+        FreeablePtr(T* ptr = nullptr) : base_type{ptr, d} { }
+};
+
 template<typename T>
-using free_ptr = std::unique_ptr<T, decltype(&g_free)>;
+using FreePtr = FreeablePtr<T, decltype(&g_free), &g_free>;
 
 template<typename T>
-free_ptr<T>
+FreePtr<T>
 take_free_ptr(T* ptr)
 {
-        return {ptr, &g_free};
+        return {ptr};
 }
 
-using string_ptr = free_ptr<char>;
+using StringPtr = FreePtr<char>;
 
-inline string_ptr
+inline StringPtr
 take_string(char* str)
 {
         return take_free_ptr(str);
 }
 
+inline StringPtr
+dup_string(char const* str)
+{
+        return take_string(g_strdup(str));
+}
+
+using StrvPtr = FreeablePtr<char*, decltype(&g_strfreev), &g_strfreev>;
+
+inline StrvPtr
+take_strv(char** strv)
+{
+        return {strv};
+}
+
+inline StrvPtr
+dup_strv(char const* const* strv)
+{
+        return take_strv(g_strdupv((char**)strv));
+}
+
 class Error {
 public:
         Error() = default;
@@ -64,6 +94,25 @@ public:
 
         void assert_no_error() const noexcept { g_assert_no_error(m_error); }
 
+        G_GNUC_PRINTF(4, 5)
+        void set(GQuark domain,
+                 int code,
+                 char const* format,
+                 ...)
+        {
+                va_list args;
+                va_start(args, format);
+                g_propagate_error(&m_error, g_error_new_valist(domain, code, format, args));
+                va_end(args);
+        }
+
+        void set_literal(GQuark domain,
+                         int code,
+                         char const* msg)
+        {
+                g_propagate_error(&m_error, g_error_new_literal(domain, code, msg));
+        }
+
         bool matches(GQuark domain, int code) const noexcept
         {
                 return error() && g_error_matches(m_error, domain, code);
@@ -71,7 +120,9 @@ public:
 
         void reset() noexcept { g_clear_error(&m_error); }
 
-        bool propagate(GError** error) noexcept { g_propagate_error(error, m_error); m_error = nullptr; 
return false; }
+        GError* release() noexcept { auto err = m_error; m_error = nullptr; return err; }
+
+        bool propagate(GError** error) noexcept { g_propagate_error(error, release()); return false; }
 
 private:
         GError* m_error{nullptr};
diff --git a/src/libc-glue.hh b/src/libc-glue.hh
index 34de20d6..bb497a7e 100644
--- a/src/libc-glue.hh
+++ b/src/libc-glue.hh
@@ -36,6 +36,8 @@ public:
 
         inline constexpr operator int () const noexcept { return m_errsv; }
 
+        inline void reset() noexcept { m_errsv = 0; }
+
 private:
         int m_errsv;
 }; // class ErrnoSaver
@@ -59,17 +61,6 @@ public:
 
         FD& operator=(FD& rhs) = delete;
 
-#if 0
-        FD& operator=(FD& rhs) noexcept
-        {
-                if (&rhs != this) {
-                        reset();
-                        m_fd = rhs.release();
-                }
-                return *this;
-        }
-#endif
-
         FD& operator=(FD&& rhs) noexcept
         {
                 reset();
diff --git a/src/meson.build b/src/meson.build
index 1c5cad90..5347f125 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -105,6 +105,7 @@ libvte_common_sources = debug_sources + glib_glue_sources + libc_glue_sources +
   'color-triple.hh',
   'keymap.cc',
   'keymap.h',
+  'missing.hh',
   'reaper.cc',
   'reaper.hh',
   'refptr.hh',
@@ -112,6 +113,8 @@ libvte_common_sources = debug_sources + glib_glue_sources + libc_glue_sources +
   'ring.hh',
   'ringview.cc',
   'ringview.hh',
+  'spawn.cc',
+  'spawn.hh',
   'utf8.cc',
   'utf8.hh',
   'vte.cc',
diff --git a/src/missing.hh b/src/missing.hh
new file mode 100644
index 00000000..0bff44a4
--- /dev/null
+++ b/src/missing.hh
@@ -0,0 +1,25 @@
+/*
+ * Copyright © 2020 Christian Persch
+ *
+ * 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 3 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 General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+/* NOTE: This file must be included *after all other includes*. */
+
+/* NSIG isn't in POSIX, so if it doesn't exist use this here. See bug #759196 */
+#ifndef NSIG
+#define NSIG (8 * sizeof(sigset_t))
+#endif
diff --git a/src/pty.cc b/src/pty.cc
index e80d7941..aa295c1f 100644
--- a/src/pty.cc
+++ b/src/pty.cc
@@ -26,7 +26,7 @@
  * pseudo-terminals and to resize pseudo-terminals.
  */
 
-#include <config.h>
+#include "config.h"
 
 #include "pty.hh"
 
@@ -34,7 +34,6 @@
 
 #include <vte/vte.h>
 #include "vteptyinternal.hh"
-#include "vtespawn.hh"
 
 #include <assert.h>
 #include <sys/types.h>
@@ -75,18 +74,9 @@
 
 #include "glib-glue.hh"
 
-#ifdef WITH_SYSTEMD
-#include "systemd.hh"
-#endif
-
-/* NSIG isn't in POSIX, so if it doesn't exist use this here. See bug #759196 */
-#ifndef NSIG
-#define NSIG (8 * sizeof(sigset_t))
-#endif
+#include "vtedefines.hh"
 
-#define VTE_VERSION_NUMERIC ((VTE_MAJOR_VERSION) * 10000 + (VTE_MINOR_VERSION) * 100 + (VTE_MICRO_VERSION))
-
-#define VTE_TERMINFO_NAME "xterm-256color"
+#include "missing.hh"
 
 namespace vte::base {
 
@@ -104,20 +94,20 @@ Pty::unref() noexcept
                 delete this;
 }
 
-vte::libc::FD
+int
 Pty::get_peer() const noexcept
 {
         if (!m_pty_fd)
-                return {};
+                return -1;
 
         if (grantpt(m_pty_fd.get()) != 0) {
                 _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "grantpt");
-                return {};
+                return -1;
         }
 
         if (unlockpt(m_pty_fd.get()) != 0) {
                 _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "unlockpt");
-                return {};
+                return -1;
         }
 
         /* FIXME? else if (m_flags & VTE_PTY_NO_CTTTY)
@@ -142,7 +132,7 @@ Pty::get_peer() const noexcept
             errno != EINVAL &&
             errno != ENOTTY) {
                 _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "ioctl(TIOCGPTPEER)");
-                return {};
+                return -1;
         }
 
         /* Fall back to ptsname + open */
@@ -152,7 +142,7 @@ Pty::get_peer() const noexcept
                 auto const name = ptsname(m_pty_fd.get());
                 if (name == nullptr) {
                         _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "ptsname");
-                        return {};
+                        return -1;
                 }
 
                 _vte_debug_print (VTE_DEBUG_PTY,
@@ -162,7 +152,7 @@ Pty::get_peer() const noexcept
                 peer_fd = ::open(name, fd_flags);
                 if (!peer_fd) {
                         _vte_debug_print (VTE_DEBUG_PTY, "Failed to open PTY: %m\n");
-                        return {};
+                        return -1;
                 }
         }
 
@@ -174,22 +164,22 @@ Pty::get_peer() const noexcept
                 /* https://illumos.org/man/7m/ptem */
                 if ((ioctl(peer_fd.get(), I_FIND, "ptem") == 0) &&
                     (ioctl(peer_fd.get(), I_PUSH, "ptem") == -1)) {
-                        return {};
+                        return -1;
                 }
                 /* https://illumos.org/man/7m/ldterm */
                 if ((ioctl(peer_fd.get(), I_FIND, "ldterm") == 0) &&
                     (ioctl(peer_fd.get(), I_PUSH, "ldterm") == -1)) {
-                        return {};
+                        return -1;
                 }
                 /* https://illumos.org/man/7m/ttcompat */
                 if ((ioctl(peer_fd.get(), I_FIND, "ttcompat") == 0) &&
                     (ioctl(peer_fd.get(), I_PUSH, "ttcompat") == -1)) {
-                        return {};
+                        return -1;
                 }
         }
 #endif
 
-        return peer_fd;
+        return peer_fd.release();
 }
 
 void
@@ -204,7 +194,8 @@ Pty::child_setup() const noexcept
         }
 
         /* Reset the handlers for all signals to their defaults.  The parent
-         * (or one of the libraries it links to) may have changed one to be ignored. */
+         * (or one of the libraries it links to) may have changed one to be ignored.
+         */
         for (int n = 1; n < NSIG; n++) {
                 if (n == SIGSTOP || n == SIGKILL)
                         continue;
@@ -224,7 +215,7 @@ Pty::child_setup() const noexcept
         }
 
         auto peer_fd = get_peer();
-        if (!peer_fd)
+        if (peer_fd == -1)
                 _exit(127);
 
 #ifdef TIOCSCTTY
@@ -233,7 +224,7 @@ Pty::child_setup() const noexcept
          * on *BSD, that doesn't happen, so we need this explicit ioctl here.
          */
         if (!(m_flags & VTE_PTY_NO_CTTY)) {
-                if (ioctl(peer_fd.get(), TIOCSCTTY, peer_fd.get()) != 0) {
+                if (ioctl(peer_fd, TIOCSCTTY, peer_fd) != 0) {
                         _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "ioctl(TIOCSCTTY)");
                         _exit(127);
                 }
@@ -242,17 +233,17 @@ Pty::child_setup() const noexcept
 
        /* now setup child I/O through the tty */
        if (peer_fd != STDIN_FILENO) {
-               if (dup2(peer_fd.get(), STDIN_FILENO) != STDIN_FILENO){
+               if (dup2(peer_fd, STDIN_FILENO) != STDIN_FILENO){
                        _exit (127);
                }
        }
        if (peer_fd != STDOUT_FILENO) {
-               if (dup2(peer_fd.get(), STDOUT_FILENO) != STDOUT_FILENO){
+               if (dup2(peer_fd, STDOUT_FILENO) != STDOUT_FILENO){
                        _exit (127);
                }
        }
        if (peer_fd != STDERR_FILENO) {
-               if (dup2(peer_fd.get(), STDERR_FILENO) != STDERR_FILENO){
+               if (dup2(peer_fd, STDERR_FILENO) != STDERR_FILENO){
                        _exit (127);
                }
        }
@@ -260,10 +251,10 @@ Pty::child_setup() const noexcept
        /* If the peer FD has not been consumed above as one of the stdio descriptors,
          * need to close it now so that it doesn't leak to the child.
          */
-       if (peer_fd == STDIN_FILENO  ||
-            peer_fd == STDOUT_FILENO ||
-            peer_fd == STDERR_FILENO) {
-                (void)peer_fd.release();
+       if (peer_fd != STDIN_FILENO  &&
+            peer_fd != STDOUT_FILENO &&
+            peer_fd != STDERR_FILENO) {
+                close(peer_fd);
        }
 
         /* Now set the TERM environment variable */
@@ -274,249 +265,6 @@ Pty::child_setup() const noexcept
         char version[7];
         g_snprintf (version, sizeof (version), "%u", VTE_VERSION_NUMERIC);
         g_setenv ("VTE_VERSION", version, TRUE);
-
-       /* Finally call an extra child setup */
-       if (m_extra_child_setup.func) {
-               m_extra_child_setup.func(m_extra_child_setup.data);
-       }
-}
-
-/* TODO: clean up the spawning
- * - replace current env rather than adding!
- */
-
-/*
- * __vte_pty_merge_environ:
- * @envp: environment vector
- * @inherit: whether to use the parent environment
- *
- * Merges @envp to the parent environment, and returns a new environment vector.
- *
- * Returns: a newly allocated string array. Free using g_strfreev()
- */
-static gchar **
-__vte_pty_merge_environ (char **envp,
-                         const char *directory,
-                         gboolean inherit)
-{
-       GHashTable *table;
-        GHashTableIter iter;
-        char *name, *value;
-       gchar **parent_environ;
-       GPtrArray *array;
-       gint i;
-
-       table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
-
-       if (inherit) {
-               parent_environ = g_listenv ();
-               for (i = 0; parent_environ[i] != NULL; i++) {
-                       g_hash_table_replace (table,
-                                             g_strdup (parent_environ[i]),
-                                             g_strdup (g_getenv (parent_environ[i])));
-               }
-               g_strfreev (parent_environ);
-       }
-
-        /* Make sure the one in envp overrides the default. */
-        g_hash_table_replace (table, g_strdup ("TERM"), g_strdup (VTE_TERMINFO_NAME));
-
-       if (envp != NULL) {
-               for (i = 0; envp[i] != NULL; i++) {
-                       name = g_strdup (envp[i]);
-                       value = strchr (name, '=');
-                       if (value) {
-                               *value = '\0';
-                               value = g_strdup (value + 1);
-                       }
-                       g_hash_table_replace (table, name, value);
-               }
-       }
-
-        g_hash_table_replace (table, g_strdup ("VTE_VERSION"), g_strdup_printf ("%u", VTE_VERSION_NUMERIC));
-
-       /* Always set this ourself, not allowing replacing from envp */
-       g_hash_table_replace(table, g_strdup("COLORTERM"), g_strdup("truecolor"));
-
-        /* We need to put the working directory also in PWD, so that
-         * e.g. bash starts in the right directory if @directory is a symlink.
-         * See bug #502146 and #758452.
-         */
-        if (directory)
-                g_hash_table_replace(table, g_strdup("PWD"), g_strdup(directory));
-
-       array = g_ptr_array_sized_new (g_hash_table_size (table) + 1);
-        g_hash_table_iter_init(&iter, table);
-        while (g_hash_table_iter_next(&iter, (void**) &name, (void**) &value)) {
-                g_ptr_array_add (array, g_strconcat (name, "=", value, nullptr));
-        }
-        g_assert(g_hash_table_size(table) == array->len);
-       g_hash_table_destroy (table);
-       g_ptr_array_add (array, NULL);
-
-       return (gchar **) g_ptr_array_free (array, FALSE);
-}
-
-static void
-pty_child_setup_cb(void* data)
-{
-        auto pty = reinterpret_cast<vte::base::Pty*>(data);
-        pty->child_setup();
-}
-
-/*
- * Pty::spawn:
- * @directory: the name of a directory the command should start in, or %nullptr
- *   to use the cwd
- * @argv: child's argument vector
- * @envv: a list of environment variables to be added to the environment before
- *   starting the process, or %nullptr
- * @spawn_flags: flags from #GSpawnFlags
- * @child_setup: function to run in the child just before exec()
- * @child_setup_data: user data for @child_setup
- * @child_pid: a location to store the child PID, or %nullptr
- * @timeout: a timeout value in ms, or %nullptr
- * @cancellable: a #GCancellable, or %nullptr
- * @error: return location for a #GError, or %nullptr
- *
- * Uses g_spawn_async() to spawn the command in @argv. The child's environment will
- * be the parent environment with the variables in @envv set afterwards.
- *
- * Enforces the vte_terminal_watch_child() requirements by adding
- * %G_SPAWN_DO_NOT_REAP_CHILD to @spawn_flags.
- *
- * Note that the %G_SPAWN_LEAVE_DESCRIPTORS_OPEN flag is not supported;
- * it will be cleared!
- *
- * If spawning the command in @working_directory fails because the child
- * is unable to chdir() to it, falls back trying to spawn the command
- * in the parent's working directory.
- *
- * Returns: %TRUE on success, or %FALSE on failure with @error filled in
- */
-bool
-Pty::spawn(char const* directory,
-           char** argv,
-           char** envv,
-           GSpawnFlags spawn_flags_,
-           GSpawnChildSetupFunc child_setup_func,
-           gpointer child_setup_data,
-           GPid* child_pid /* out */,
-           int timeout,
-           GCancellable* cancellable,
-           GError** error) noexcept
-{
-        guint spawn_flags = (guint) spawn_flags_;
-        bool ret{true};
-        bool inherit_envv;
-        char** envp2;
-        int i;
-        GPollFD pollfd;
-
-#ifndef WITH_SYSTEMD
-        if (spawn_flags & VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE) {
-                g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
-                            "systemd not available");
-                return false;
-        }
-#endif
-
-        if (cancellable && !g_cancellable_make_pollfd(cancellable, &pollfd)) {
-                auto errsv = vte::libc::ErrnoSaver{};
-                g_set_error(error,
-                            G_IO_ERROR,
-                            g_io_error_from_errno(errsv),
-                            "Failed to make cancellable pollfd: %s",
-                            g_strerror(errsv));
-                return false;
-        }
-
-        inherit_envv = (spawn_flags & VTE_SPAWN_NO_PARENT_ENVV) == 0;
-        spawn_flags &= ~(VTE_SPAWN_NO_PARENT_ENVV | VTE_SPAWN_NO_SYSTEMD_SCOPE | 
VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE);
-
-        /* add the given environment to the childs */
-        envp2 = __vte_pty_merge_environ (envv, directory, inherit_envv);
-
-        _VTE_DEBUG_IF (VTE_DEBUG_MISC) {
-                g_printerr ("Spawning command:\n");
-                for (i = 0; argv[i] != NULL; i++) {
-                        g_printerr ("    argv[%d] = %s\n", i, argv[i]);
-                }
-                for (i = 0; envp2[i] != NULL; i++) {
-                        g_printerr ("    env[%d] = %s\n", i, envp2[i]);
-                }
-                g_printerr ("    directory: %s\n",
-                            directory ? directory : "(none)");
-        }
-
-       m_extra_child_setup.func = child_setup_func;
-       m_extra_child_setup.data = child_setup_data;
-
-        auto pid = pid_t{-1};
-        auto err = vte::glib::Error{};
-        ret = vte_spawn_async_cancellable(directory,
-                                          argv, envp2,
-                                          (GSpawnFlags)spawn_flags,
-                                          (GSpawnChildSetupFunc)pty_child_setup_cb,
-                                          this,
-                                          &pid,
-                                          timeout,
-                                          cancellable ? &pollfd : nullptr,
-                                          err);
-        if (!ret &&
-            directory != nullptr &&
-            err.matches(G_SPAWN_ERROR, G_SPAWN_ERROR_CHDIR)) {
-                /* try spawning in our working directory */
-                err.reset();
-                ret = vte_spawn_async_cancellable(nullptr,
-                                                  argv, envp2,
-                                                  (GSpawnFlags)spawn_flags,
-                                                  (GSpawnChildSetupFunc)pty_child_setup_cb,
-                                                  this,
-                                                  &pid,
-                                                  timeout,
-                                                  cancellable ? &pollfd : nullptr,
-                                                  err);
-        }
-
-        g_strfreev (envp2);
-
-       m_extra_child_setup.func = nullptr;
-       m_extra_child_setup.data = nullptr;
-
-        if (cancellable)
-                g_cancellable_release_fd(cancellable);
-
-#ifdef WITH_SYSTEMD
-        if (ret &&
-            !(spawn_flags & VTE_SPAWN_NO_SYSTEMD_SCOPE) &&
-            !vte::systemd::create_scope_for_pid_sync(pid,
-                                                     timeout, // FIXME: recalc timeout
-                                                     cancellable,
-                                                     err)) {
-                if (spawn_flags & VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE) {
-                        auto pgrp = getpgid(pid);
-                        if (pgrp != -1) {
-                                kill(-pgrp, SIGHUP);
-                        }
-
-                        kill(pid, SIGHUP);
-
-                        ret = false;
-                } else {
-                        _vte_debug_print(VTE_DEBUG_PTY,
-                                         "Failed to create systemd scope: %s",
-                                         err.message());
-                        err.reset();
-                }
-        }
-#endif // WITH_SYSTEMD
-
-        if (!ret)
-                return err.propagate(error);
-
-        *child_pid = pid;
-        return true;
 }
 
 /*
@@ -709,7 +457,7 @@ _vte_pty_open_posix(void)
                 return {};
         }
 
-       _vte_debug_print(VTE_DEBUG_PTY, "Allocated pty on fd %d.\n", (int)fd);
+       _vte_debug_print(VTE_DEBUG_PTY, "Allocated pty on fd %d.\n", fd.get());
 
         return fd;
 }
diff --git a/src/pty.hh b/src/pty.hh
index ace59f3d..75b3076d 100644
--- a/src/pty.hh
+++ b/src/pty.hh
@@ -30,18 +30,9 @@ namespace vte::base {
 class Pty {
 private:
         mutable volatile int m_refcount{1};
-
-        mutable struct {
-                GSpawnChildSetupFunc func{nullptr};
-                void* data{nullptr};
-        } m_extra_child_setup;
-
         vte::libc::FD m_pty_fd{};
-
         VtePtyFlags m_flags{VTE_PTY_DEFAULT};
 
-        vte::libc::FD get_peer() const noexcept;
-
 public:
         constexpr Pty(vte::libc::FD&& fd,
                       VtePtyFlags flags = VTE_PTY_DEFAULT) noexcept
@@ -61,18 +52,9 @@ public:
         inline constexpr int fd() const noexcept { return m_pty_fd.get(); }
         inline constexpr auto flags() const noexcept { return m_flags; }
 
-        void child_setup() const noexcept;
+        int get_peer() const noexcept;
 
-        bool spawn(char const* directory,
-                   char** argv,
-                   char** envv,
-                   GSpawnFlags spawn_flags_,
-                   GSpawnChildSetupFunc child_setup,
-                   void* child_setup_data,
-                   GPid* child_pid /* out */,
-                   int timeout,
-                   GCancellable* cancellable,
-                   GError** error) noexcept;
+        void child_setup() const noexcept;
 
         bool set_size(int rows,
                       int columns) const noexcept;
diff --git a/src/refptr.hh b/src/refptr.hh
index 5d8e126a..3fcc89e6 100644
--- a/src/refptr.hh
+++ b/src/refptr.hh
@@ -59,6 +59,13 @@ take_ref(T* obj)
         return {obj};
 }
 
+template<typename T>
+RefPtr<T>
+acquire_ref(GWeakRef* wr)
+{
+        return take_ref(reinterpret_cast<T*>(g_weak_ref_get(wr)));
+}
+
 } // namespace glib
 
 namespace base {
diff --git a/src/spawn.cc b/src/spawn.cc
new file mode 100644
index 00000000..844f6245
--- /dev/null
+++ b/src/spawn.cc
@@ -0,0 +1,486 @@
+/*
+ * Copyright © 2001,2002 Red Hat, Inc.
+ * Copyright © 2009, 2010, 2019, 2020 Christian Persch
+ *
+ * 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 3 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
+ */
+
+#include "config.h"
+
+#include "spawn.hh"
+
+#include "libc-glue.hh"
+
+#include <vte/vte.h>
+#include "vteptyinternal.hh"
+#include "vtespawn.hh"
+#include "debug.h"
+#include "reaper.hh"
+
+#include <assert.h>
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <errno.h>
+#include <fcntl.h>
+
+#include <glib/gi18n-lib.h>
+#include <glib-unix.h>
+
+#include "glib-glue.hh"
+
+#ifdef WITH_SYSTEMD
+#include "systemd.hh"
+#endif
+
+#include "vtedefines.hh"
+
+namespace vte::base {
+
+static bool
+make_pipe(int flags,
+          vte::libc::FD& read_fd,
+          vte::libc::FD& write_fd,
+          vte::glib::Error& error)
+{
+        int fds[2] = { -1, -1 };
+        if (!g_unix_open_pipe(fds, flags, error))
+                return false;
+
+        read_fd = fds[0];
+        write_fd = fds[1];
+        return true;
+}
+
+static char**
+merge_environ(char** envp /* consumed */,
+              char const* cwd,
+              bool inherit)
+{
+        auto table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+
+        if (inherit) {
+                auto parent_environ = g_listenv();
+                if (parent_environ) {
+                        for (auto i = unsigned{0}; parent_environ[i] != NULL; ++i) {
+                                g_hash_table_replace(table,
+                                                     g_strdup(parent_environ[i]),
+                                                     g_strdup(g_getenv(parent_environ[i])));
+                        }
+                        g_strfreev(parent_environ);
+                }
+        }
+
+        /* Make sure the one in envp overrides the default. */
+        g_hash_table_replace(table, g_strdup("TERM"), g_strdup(VTE_TERMINFO_NAME));
+
+        if (envp) {
+                for (auto i = unsigned{0}; envp[i] != nullptr; ++i) {
+                        auto name = g_strdup(envp[i]);
+                        auto value = strchr(name, '=');
+                        if (value) {
+                                *value = '\0';
+                                value = g_strdup(value + 1);
+                        }
+                        g_hash_table_replace(table, name, value); /* takes ownership of name and value */
+                }
+
+                g_strfreev(envp);
+        }
+
+        /* Always set this ourself, not allowing replacing from envp */
+        g_hash_table_replace(table, g_strdup("VTE_VERSION"), g_strdup_printf("%u", VTE_VERSION_NUMERIC));
+        g_hash_table_replace(table, g_strdup("COLORTERM"), g_strdup("truecolor"));
+
+        /* We need to put the working directory also in PWD, so that
+         * e.g. bash starts in the right directory if @directory is a symlink.
+         * See bug #502146 and #758452.
+         *
+         * If chdir to cwd fails, and we fall back to the fallback cwd, PWD will
+         * be set to a directory != the actual working directory, but that's not
+         * a problem since PWD is only used when it's equal to the actual working
+         * directory.
+         */
+        if (cwd)
+                g_hash_table_replace(table, g_strdup("PWD"), g_strdup(cwd));
+        else
+                g_hash_table_remove(table, "PWD");
+
+        auto array = g_ptr_array_sized_new(g_hash_table_size(table) + 1);
+
+        auto iter = GHashTableIter{};
+        g_hash_table_iter_init(&iter, table);
+        char *name, *value;
+        while (g_hash_table_iter_next(&iter, (void**)&name, (void**)&value)) {
+                if (value)
+                        g_ptr_array_add(array, g_strconcat(name, "=", value, nullptr));
+        }
+        g_hash_table_destroy(table);
+        g_ptr_array_add(array, nullptr);
+
+        return reinterpret_cast<char**>(g_ptr_array_free(array, false));
+}
+
+void
+SpawnContext::prepare_environ()
+{
+        m_envv = vte::glib::take_strv(merge_environ(m_envv.release(), m_cwd.get(), inherit_environ()));
+}
+
+int
+SpawnContext::exec() const noexcept
+{
+        /* NOTE! This function must not rely on smart pointers to
+         * release their object, since the destructors are NOT run
+         * when the exec succeeds!
+         */
+
+        _VTE_DEBUG_IF(VTE_DEBUG_MISC) {
+                g_printerr ("Spawning command:\n");
+                auto argv = m_argv.get();
+                for (auto i = 0; argv[i] != NULL; i++) {
+                        g_printerr("    argv[%d] = %s\n", i, argv[i]);
+                }
+                if (m_envv) {
+                        auto envv = m_envv.get();
+                        for (auto i = 0; envv[i] != NULL; i++) {
+                                g_printerr("    env[%d] = %s\n", i, envv[i]);
+                        }
+                }
+                g_printerr("    directory: %s\n",
+                           m_cwd ? m_cwd.get() : "(none)");
+        }
+
+        /* Unblock all signals */
+        sigset_t set;
+        sigemptyset(&set);
+        if (pthread_sigmask(SIG_SETMASK, &set, nullptr) == -1) {
+                _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "pthread_sigmask");
+                return -1;
+        }
+
+        /* Reset the handlers for all signals to their defaults.  The parent
+         * (or one of the libraries it links to) may have changed one to be ignored.
+         * Esp. SIGPIPE since it ensures this process terminates when we write
+         * to child_err_report_pipe after the parent has exited.
+         */
+        for (auto n = int{1}; n < NSIG; ++n) {
+                if (n == SIGSTOP || n == SIGKILL)
+                        continue;
+
+                signal(n, SIG_DFL);
+        }
+
+        /* Close all file descriptors on exec. Note that this includes
+         * child_error_report_pipe_write, which keeps the parent from blocking
+         * forever on the other end of that pipe.
+         * (Note that stdin, stdout and stderr will be set by the child setup afterwards.)
+         */
+        // FIXMEchpe make sure child_error_report_pipe_write is != 0, 1, 2 !!! before setting up PTY below!!!
+        _vte_cloexec_from(3);
+
+        if (!(pty()->flags() & VTE_PTY_NO_SESSION)) {
+                /* This starts a new session; we become its process-group leader,
+                 * and lose our controlling TTY.
+                 */
+                _vte_debug_print(VTE_DEBUG_PTY, "Starting new session\n");
+                if (setsid() == -1) {
+                        _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "setsid");
+                        return -1;
+                }
+        }
+
+        /* Note: *not* FD_CLOEXEC! */
+        auto peer_fd = pty()->get_peer();
+        if (peer_fd == -1)
+                return -1;
+
+#ifdef TIOCSCTTY
+        /* On linux, opening the PTY peer above already made it our controlling TTY (since
+         * previously there was none, after the setsid() call). However, it appears that e.g.
+         * on *BSD, that doesn't happen, so we need this explicit ioctl here.
+         */
+        if (!(pty()->flags() & VTE_PTY_NO_CTTY)) {
+                if (ioctl(peer_fd, TIOCSCTTY, peer_fd) != 0) {
+                        _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "ioctl(TIOCSCTTY)");
+                        return -1;
+                }
+        }
+#endif
+
+        /* now setup child I/O through the tty */
+        if (peer_fd != STDIN_FILENO) {
+                if (dup2(peer_fd, STDIN_FILENO) != STDIN_FILENO)
+                        return -1;
+        }
+        if (peer_fd != STDOUT_FILENO) {
+                if (dup2(peer_fd, STDOUT_FILENO) != STDOUT_FILENO)
+                        return -1;
+        }
+        if (peer_fd != STDERR_FILENO) {
+                if (dup2(peer_fd, STDERR_FILENO) != STDERR_FILENO)
+                        return -1;
+        }
+
+        if (peer_fd != STDIN_FILENO  &&
+            peer_fd != STDOUT_FILENO &&
+            peer_fd != STDERR_FILENO) {
+                close(peer_fd);
+        }
+
+        if (m_cwd && chdir(m_cwd.get()) < 0) {
+                /* If the fallback fails too, make sure to return the errno
+                 * from the original cwd, not the fallback cwd.
+                 */
+                auto errsv = vte::libc::ErrnoSaver{};
+                if (m_fallback_cwd && chdir(m_fallback_cwd.get()) < 0)
+                        return G_SPAWN_ERROR_CHDIR;
+
+                errsv.reset();
+        }
+
+        /* Finally call an extra child setup */
+        if (m_child_setup)
+                m_child_setup(m_child_setup_data);
+
+        /* exec */
+        _vte_execute(arg0(),
+                     argv(),
+                     environ(),
+                     search_path(),
+                     search_path_from_envp());
+
+        /* If we get here, exec failed */
+        return G_SPAWN_ERROR_FAILED;
+}
+
+SpawnOperation::~SpawnOperation()
+{
+        if (m_cancellable && m_cancellable_pollfd.fd != -1)
+                g_cancellable_release_fd(m_cancellable.get());
+
+        if (m_pid != -1) {
+                /* Since we're not passing the PID back to the caller,
+                 * we need to kill and reap it ourself.
+                 */
+                if (m_kill_pid) {
+                        auto const pgrp = getpgid(m_pid);
+                        /* Make sure not to kill ourself, if the child died before
+                         * it could call setsid()!
+                         */
+                        if (pgrp != -1 && pgrp != getpgid(getpid()))
+                                kill(-pgrp, SIGHUP);
+
+                        kill(m_pid, SIGHUP);
+                }
+
+                vte_reaper_add_child(m_pid);
+        }
+}
+
+bool
+SpawnOperation::prepare(vte::glib::Error& error) noexcept
+{
+#ifndef WITH_SYSTEMD
+        if (context().systemd_scope()) {
+                error.set_literal(G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+                                  "systemd not available");
+                return false;
+        }
+#endif
+
+        if (m_cancellable &&
+            !g_cancellable_make_pollfd(m_cancellable.get(), &m_cancellable_pollfd)) {
+                auto errsv = vte::libc::ErrnoSaver{};
+                error.set(G_IO_ERROR,
+                          g_io_error_from_errno(errsv),
+                          "Failed to make cancellable pollfd: %s",
+                          g_strerror(errsv));
+                return false;
+        }
+
+        auto child_report_error_pipe_read = vte::libc::FD{};
+        auto child_report_error_pipe_write = vte::libc::FD{};
+        assert(!child_report_error_pipe_read);
+        assert(!child_report_error_pipe_write);
+        if (!make_pipe(FD_CLOEXEC,
+                       child_report_error_pipe_read,
+                       child_report_error_pipe_write,
+                       error))
+                return false;
+
+        assert(child_report_error_pipe_read);
+        assert(child_report_error_pipe_write);
+        auto const pid = fork();
+        if (pid < 0) {
+                auto errsv = vte::libc::ErrnoSaver{};
+                error.set(G_IO_ERROR,
+                          g_io_error_from_errno(errsv),
+                          "Failed to fork: %s",
+                          g_strerror(errsv));
+                return false;
+        }
+
+        if (pid == 0) {
+                /* Child */
+
+                child_report_error_pipe_read.reset();
+                assert(!child_report_error_pipe_read);
+
+                auto const err = context().exec();
+
+                /* If we get here, exec failed. Write the error to the pipe and exit */
+                assert(!child_report_error_pipe_read);
+                _vte_write_err(child_report_error_pipe_write.get(), err);
+                _exit(127);
+                return true;
+        }
+
+        /* Parent */
+        m_pid = pid;
+        m_child_report_error_pipe_read = std::move(child_report_error_pipe_read);
+        assert(m_child_report_error_pipe_read);
+
+        return true;
+}
+
+bool
+SpawnOperation::run(vte::glib::Error& error) noexcept
+{
+        int buf[2] = {G_SPAWN_ERROR_FAILED, ENOSYS};
+        auto n_read = int{0};
+
+        g_assert_cmpint(m_child_report_error_pipe_read.get(), !=, -1);
+        assert(m_child_report_error_pipe_read);
+
+        if (!_vte_read_ints(m_child_report_error_pipe_read.get(),
+                            buf, 2, &n_read,
+                            m_timeout,
+                            &m_cancellable_pollfd,
+                            error))
+                return false;
+
+        if (n_read >= 2) {
+                /* The process will have called _exit(127) already, no need to kill it */
+                m_kill_pid = false;
+
+                switch (buf[0]) {
+                case G_SPAWN_ERROR_CHDIR: {
+                        auto cwd = vte::glib::take_string(context().cwd() ? 
g_utf8_make_valid(context().cwd(), -1) : nullptr);
+                        error.set(G_IO_ERROR,
+                                  g_io_error_from_errno(buf[1]),
+                                  _("Failed to change to directory “%s”: %s"),
+                                  cwd.get(),
+                                  g_strerror(buf[1]));
+                        break;
+                }
+
+                case G_SPAWN_ERROR_FAILED: {
+                        auto arg = vte::glib::take_string(g_utf8_make_valid(context().argv()[0], -1));
+                        error.set(G_IO_ERROR,
+                                  g_io_error_from_errno(buf[1]),
+                                  _("Failed to execute child process “%s”: %s"),
+                                  arg.get(),
+                                  g_strerror(buf[1]));
+                        break;
+                }
+
+                default: {
+                        auto arg = vte::glib::take_string(g_utf8_make_valid(context().argv()[0], -1));
+                        error.set(G_IO_ERROR,
+                                  G_IO_ERROR_FAILED,
+                                  _("Unknown error executing child process “%s”"),
+                                  arg.get());
+                        break;
+                }
+                }
+
+                return false;
+        }
+
+        /* Spawn succeeded */
+
+#ifdef WITH_SYSTEMD
+        if (context().systemd_scope() &&
+            !vte::systemd::create_scope_for_pid_sync(m_pid,
+                                                     m_timeout, // FIXME: recalc timeout
+                                                     m_cancellable.get(),
+                                                     error)) {
+                if (context().require_systemd_scope())
+                        return false;
+
+                _vte_debug_print(VTE_DEBUG_PTY,
+                                 "Failed to create systemd scope: %s",
+                                 error.message());
+                error.reset();
+        }
+#endif // WITH_SYSTEMD
+
+        return true;
+}
+
+void
+SpawnOperation::run_in_thread(GTask* task) noexcept
+{
+        auto error = vte::glib::Error{};
+        if (run(error))
+                g_task_return_int(task, ssize_t{release_pid()});
+        else
+                g_task_return_error(task, error.release());
+}
+
+/* Note: this function takes ownership of @this */
+void
+SpawnOperation::run_async(void* source_tag,
+                          GAsyncReadyCallback callback,
+                          void* user_data)
+{
+        /* Create a GTask to run the user-provided callback, and transfers
+         * ownership of @this to the task, meaning that @this will be deleted after
+         * the task is completed.
+         */
+        auto task = vte::glib::take_ref(g_task_new(context().pty_wrapper(),
+                                                   m_cancellable.get(),
+                                                   callback,
+                                                   user_data));
+        g_task_set_source_tag(task.get(), source_tag);
+        g_task_set_task_data(task.get(), this, delete_cb);
+        // g_task_set_name(task.get(), "vte-spawn-async");
+
+        /* Spawning is split into the fork() phase, and waiting for the child to
+         * exec or report an error. This is done so that the fork is happening on
+         * the main thread; see issue vte#118.
+         */
+        auto error = vte::glib::Error{};
+        if (!prepare(error))
+                return g_task_return_error(task.get(), error.release());
+
+        /* Async read from the child */
+        g_task_run_in_thread(task.get(), run_in_thread_cb);
+}
+
+bool
+SpawnOperation::run_sync(GPid* pid,
+                         vte::glib::Error& error)
+{
+        auto rv = prepare(error) && run(error);
+        if (rv)
+                *pid = release_pid();
+        else
+                *pid = -1;
+
+        return rv;
+}
+
+} // namespace vte::base
diff --git a/src/spawn.hh b/src/spawn.hh
new file mode 100644
index 00000000..c6a5b1f3
--- /dev/null
+++ b/src/spawn.hh
@@ -0,0 +1,213 @@
+/*
+ * Copyright © 2018, 2019 Christian Persch
+ *
+ * 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 3 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 General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#include "vtepty.h"
+#include "vteptyinternal.hh"
+
+#include "glib-glue.hh"
+#include "libc-glue.hh"
+#include "refptr.hh"
+
+namespace vte::base {
+
+class SpawnContext {
+public:
+        using child_setup_type = void(*)(void*);
+
+private:
+
+        vte::glib::RefPtr<VtePty> m_pty{};
+
+        std::unique_ptr<char*, decltype(&g_strfreev)> m_foo{nullptr, &g_strfreev};
+
+        vte::glib::StringPtr m_cwd;
+        vte::glib::StringPtr m_fallback_cwd;
+        vte::glib::StringPtr m_arg0;
+        vte::glib::StrvPtr m_argv;
+        vte::glib::StrvPtr m_envv;
+
+        child_setup_type m_child_setup{(void(*)(void*))0};
+        void* m_child_setup_data{nullptr};
+        GDestroyNotify m_child_setup_data_destroy{nullptr};
+
+        bool m_inherit_environ{true};
+        bool m_systemd_scope{true};
+        bool m_require_systemd_scope{false};
+        bool m_search_path_from_envp{false};
+        bool m_search_path{false};
+
+public:
+        SpawnContext() = default;
+
+        ~SpawnContext()
+        {
+                if (m_child_setup_data)
+                        m_child_setup_data_destroy(m_child_setup_data);
+        }
+
+        SpawnContext(SpawnContext const&) = delete;
+        SpawnContext(SpawnContext&&) = default;
+        SpawnContext operator=(SpawnContext const&) = delete;
+        SpawnContext operator=(SpawnContext&&) = delete;
+
+        void set_cwd(char const* cwd)
+        {
+                m_cwd = vte::glib::dup_string(cwd);
+        }
+
+        void set_fallback_cwd(char const* cwd)
+        {
+                m_fallback_cwd = vte::glib::dup_string(cwd);
+        }
+
+        void set_argv(char const* arg0,
+                      char const* const* argv)
+        {
+                m_arg0 = vte::glib::dup_string(arg0);
+                m_argv = vte::glib::dup_strv(argv);
+        }
+
+        void set_environ(char const* const* envv)
+        {
+                m_envv = vte::glib::dup_strv(envv);
+        }
+
+        void setenv(char const* env,
+                    char const* value,
+                    bool overwrite = true)
+        {
+                m_envv = vte::glib::take_strv(g_environ_setenv(m_envv.release(), env, value, overwrite));
+        }
+
+        void unsetenv(char const* env)
+        {
+                m_envv = vte::glib::take_strv(g_environ_unsetenv(m_envv.release(), env));
+        }
+
+        void set_pty(vte::glib::RefPtr<VtePty>&& pty)
+        {
+                m_pty = std::move(pty);
+        }
+
+        void set_child_setup(child_setup_type func,
+                             void* data,
+                             GDestroyNotify destroy)
+        {
+                m_child_setup = func;
+                m_child_setup_data = data;
+                m_child_setup_data_destroy = destroy;
+        }
+
+        void set_no_inherit_environ()    noexcept { m_inherit_environ = false;      }
+        void set_no_systemd_scope()      noexcept { m_systemd_scope = false;        }
+        void set_require_systemd_scope() noexcept { m_require_systemd_scope = true; }
+        void set_search_path()           noexcept { m_search_path = true;           }
+        void set_search_path_from_envp() noexcept { m_search_path_from_envp = true; }
+
+        auto arg0()         const noexcept { return m_arg0.get(); }
+        auto argv()         const noexcept { return m_argv.get(); }
+        auto cwd()          const noexcept { return m_cwd.get();  }
+        auto fallback_cwd() const noexcept { return m_fallback_cwd.get(); }
+        auto environ()      const noexcept { return m_envv.get(); }
+
+        auto pty_wrapper() const noexcept { return m_pty.get();  }
+        auto pty() const noexcept { return _vte_pty_get_impl(pty_wrapper()); }
+
+        constexpr auto inherit_environ()       const noexcept { return m_inherit_environ;       }
+        constexpr auto systemd_scope()         const noexcept { return m_systemd_scope;         }
+        constexpr auto require_systemd_scope() const noexcept { return m_require_systemd_scope; }
+        constexpr auto search_path()           const noexcept { return m_search_path;           }
+        constexpr auto search_path_from_envp() const noexcept { return m_search_path_from_envp; }
+
+        void prepare_environ();
+
+        int exec() const noexcept;
+
+}; // class SpawnContext
+
+class SpawnOperation {
+private:
+        int const default_timeout = 30000; /* ms */
+
+        SpawnContext m_context{};
+        int m_timeout{default_timeout};
+        vte::glib::RefPtr<GCancellable> m_cancellable{};
+        GAsyncReadyCallback m_callback{nullptr};
+        void* m_callback_user_data{nullptr};
+
+        GPollFD m_cancellable_pollfd{-1, 0, 0};
+        vte::libc::FD m_child_report_error_pipe_read{};
+        pid_t m_pid{-1};
+        bool m_kill_pid{true};
+
+        auto& context() const noexcept { return m_context; }
+
+        bool prepare(vte::glib::Error& error);
+        bool run(vte::glib::Error& error) noexcept;
+
+        void run_in_thread(GTask* task) noexcept;
+
+        auto release_pid() noexcept { auto pid = m_pid; m_pid = -1; return pid; }
+
+        static void delete_cb(void* that)
+        {
+                delete reinterpret_cast<SpawnOperation*>(that);
+        }
+
+        static void run_in_thread_cb(GTask* task,
+                                     gpointer source_object,
+                                     gpointer that,
+                                     GCancellable* cancellable)
+        {
+                reinterpret_cast<SpawnOperation*>(that)->run_in_thread(task);
+        }
+
+public:
+        SpawnOperation(SpawnContext&& context,
+                       int timeout,
+                       GCancellable* cancellable)
+                : m_context{std::move(context)},
+                  m_timeout{timeout >= 0 ? timeout : default_timeout},
+                  m_cancellable{vte::glib::make_ref(cancellable)}
+        {
+                m_context.prepare_environ();
+        }
+
+        ~SpawnOperation();
+
+        SpawnOperation(SpawnOperation const&) = delete;
+        SpawnOperation(SpawnOperation&&) = delete;
+        SpawnOperation operator=(SpawnOperation const&) = delete;
+        SpawnOperation operator=(SpawnOperation&&) = delete;
+
+        void run_async(void *source_tag,
+                       GAsyncReadyCallback callback,
+                       void* user_data); /* takes ownership of @this ! */
+
+        bool run_sync(GPid* pid,
+                      vte::glib::Error& error);
+
+}; // class SpawnOperation
+
+} // namespace vte::base
diff --git a/src/vte.cc b/src/vte.cc
index 2e4b9e40..a27a58da 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -17,7 +17,7 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  */
 
-#include <config.h>
+#include "config.h"
 
 #include <math.h>
 #include <search.h>
@@ -10088,7 +10088,7 @@ Terminal::terminate_child() noexcept
                 return false;
 
         auto pgrp = getpgid(m_pty_pid);
-        if (pgrp != -1) {
+        if (pgrp != -1 && pgrp != getpgid(getpid())) {
                 kill(-pgrp, SIGHUP);
         }
 
diff --git a/src/vte/vteterminal.h b/src/vte/vteterminal.h
index 31012518..93c07d11 100644
--- a/src/vte/vteterminal.h
+++ b/src/vte/vteterminal.h
@@ -151,7 +151,7 @@ void vte_terminal_spawn_async(VteTerminal *terminal,
                               const char *working_directory,
                               char **argv,
                               char **envv,
-                              GSpawnFlags spawn_flags_,
+                              GSpawnFlags spawn_flags,
                               GSpawnChildSetupFunc child_setup,
                               gpointer child_setup_data,
                               GDestroyNotify child_setup_data_destroy,
diff --git a/src/vtedefines.hh b/src/vtedefines.hh
index df45ecf6..8aa4d9dc 100644
--- a/src/vtedefines.hh
+++ b/src/vtedefines.hh
@@ -139,3 +139,7 @@
 
 /* Maximum length of a paragraph, in lines, that might get proper RingView (BiDi) treatment. */
 #define VTE_RINGVIEW_PARAGRAPH_LENGTH_MAX   500
+
+#define VTE_VERSION_NUMERIC ((VTE_MAJOR_VERSION) * 10000 + (VTE_MINOR_VERSION) * 100 + (VTE_MICRO_VERSION))
+
+#define VTE_TERMINFO_NAME "xterm-256color"
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index c91bb923..2e69616b 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -2645,8 +2645,8 @@ vte_terminal_pty_new_sync(VteTerminal *terminal,
         g_return_val_if_fail(VTE_IS_TERMINAL(terminal), NULL);
 
         VtePty *pty = vte_pty_new_sync(flags, cancellable, error);
-        if (pty == NULL)
-                return NULL;
+        if (pty == nullptr)
+                return nullptr;
 
         vte_pty_set_size(pty, IMPL(terminal)->m_row_count, IMPL(terminal)->m_column_count, NULL);
 
@@ -2747,28 +2747,24 @@ vte_terminal_spawn_sync(VteTerminal *terminal,
         g_return_val_if_fail(child_setup_data == NULL || child_setup, FALSE);
         g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
 
-        auto new_pty = vte_terminal_pty_new_sync(terminal, pty_flags, cancellable, error);
-        if (new_pty == nullptr)
+        auto new_pty = vte::glib::take_ref(vte_terminal_pty_new_sync(terminal, pty_flags, cancellable, 
error));
+        if (!new_pty)
                 return false;
 
         GPid pid;
-        if (!_vte_pty_spawn(new_pty,
-                            working_directory,
-                            argv,
-                            envv,
-                            spawn_flags,
-                            child_setup, child_setup_data,
-                            &pid,
-                            -1 /* no timeout */,
-                            cancellable,
-                            error)) {
-                g_object_unref(new_pty);
+        if (!_vte_pty_spawn_sync(new_pty.get(),
+                                 working_directory,
+                                 argv,
+                                 envv,
+                                 spawn_flags,
+                                 child_setup, child_setup_data, nullptr,
+                                 &pid,
+                                 -1 /* default timeout */,
+                                 cancellable,
+                                 error))
                 return false;
-        }
-
-        vte_terminal_set_pty(terminal, new_pty);
-        g_object_unref (new_pty);
 
+        vte_terminal_set_pty(terminal, new_pty.get());
         vte_terminal_watch_child(terminal, pid);
 
         if (child_pid)
@@ -2785,8 +2781,8 @@ typedef struct {
 
 static gpointer
 spawn_async_callback_data_new(VteTerminal *terminal,
-                     VteTerminalSpawnAsyncCallback callback,
-                     gpointer user_data)
+                              VteTerminalSpawnAsyncCallback callback,
+                              gpointer user_data)
 {
         SpawnAsyncCallbackData *data = g_new0 (SpawnAsyncCallbackData, 1);
 
@@ -2812,36 +2808,36 @@ spawn_async_cb (GObject *source,
         SpawnAsyncCallbackData *data = reinterpret_cast<SpawnAsyncCallbackData*>(user_data);
         VtePty *pty = VTE_PTY(source);
 
-        GPid pid = -1;
+        auto pid = pid_t{-1};
         auto error = vte::glib::Error{};
-        vte_pty_spawn_finish(pty, result, &pid, error);
+        if (source) {
+                vte_pty_spawn_finish(pty, result, &pid, error);
+        } else {
+                (void)g_task_propagate_int(G_TASK(result), error);
+                assert(error.error());
+        }
 
         /* Now get a ref to the terminal */
-        VteTerminal* terminal = (VteTerminal*)g_weak_ref_get(&data->wref);
+        auto terminal = vte::glib::acquire_ref<VteTerminal>(&data->wref);
 
-        /* Automatically watch the child */
-        if (terminal != nullptr) {
+        if (terminal) {
                 if (pid != -1) {
-                        vte_terminal_set_pty(terminal, pty);
-                        vte_terminal_watch_child(terminal, pid);
+                        vte_terminal_set_pty(terminal.get(), pty);
+                        vte_terminal_watch_child(terminal.get(), pid);
                 } else {
-                        vte_terminal_set_pty(terminal, nullptr);
-                }
-        } else {
-                if (pid != -1) {
-                        vte_reaper_add_child(pid);
+                        vte_terminal_set_pty(terminal.get(), nullptr);
                 }
         }
 
         if (data->callback)
-                data->callback(terminal, pid, error, data->user_data);
+                data->callback(terminal.get(), pid, error, data->user_data);
 
-        if (terminal == nullptr) {
+        if (!terminal) {
                 /* If the terminal was destroyed, we need to abort the child process, if any */
                 if (pid != -1) {
                         pid_t pgrp;
                         pgrp = getpgid(pid);
-                        if (pgrp != -1) {
+                        if (pgrp != -1 && pgrp != getpgid(getpid())) {
                                 kill(-pgrp, SIGHUP);
                         }
 
@@ -2850,12 +2846,8 @@ spawn_async_cb (GObject *source,
         }
 
         spawn_async_callback_data_free(data);
-
-        if (terminal != nullptr)
-                g_object_unref(terminal);
 }
 
-
 /**
  * VteTerminalSpawnAsyncCallback:
  * @terminal: the #VteTerminal
@@ -2881,11 +2873,11 @@ spawn_async_cb (GObject *source,
  * @argv: (array zero-terminated=1) (element-type filename): child's argument vector
  * @envv: (allow-none) (array zero-terminated=1) (element-type filename): a list of environment
  *   variables to be added to the environment before starting the process, or %NULL
- * @spawn_flags_: flags from #GSpawnFlags
+ * @spawn_flags: flags from #GSpawnFlags
  * @child_setup: (allow-none) (scope async): an extra child setup function to run in the child just before 
exec(), or %NULL
  * @child_setup_data: (closure child_setup): user data for @child_setup, or %NULL
  * @child_setup_data_destroy: (destroy child_setup_data): a #GDestroyNotify for @child_setup_data, or %NULL
- * @timeout: a timeout value in ms, or -1 to wait indefinitely
+ * @timeout: a timeout value in ms, -1 for the default timeout, or G_MAXINT to wait indefinitely
  * @cancellable: (allow-none): a #GCancellable, or %NULL
  * @callback: (scope async): a #VteTerminalSpawnAsyncCallback, or %NULL
  * @user_data: (closure callback): user data for @callback, or %NULL
@@ -2933,7 +2925,7 @@ vte_terminal_spawn_async(VteTerminal *terminal,
                          const char *working_directory,
                          char **argv,
                          char **envv,
-                         GSpawnFlags spawn_flags_,
+                         GSpawnFlags spawn_flags,
                          GSpawnChildSetupFunc child_setup,
                          gpointer child_setup_data,
                          GDestroyNotify child_setup_data_destroy,
@@ -2946,37 +2938,29 @@ vte_terminal_spawn_async(VteTerminal *terminal,
         g_return_if_fail(argv != nullptr);
         g_return_if_fail(!child_setup_data || child_setup);
         g_return_if_fail(!child_setup_data_destroy || child_setup_data);
+        g_return_if_fail(timeout >= -1);
         g_return_if_fail(cancellable == nullptr || G_IS_CANCELLABLE (cancellable));
 
         auto error = vte::glib::Error{};
-        auto pty = vte_terminal_pty_new_sync(terminal, pty_flags, cancellable, error);
-        if (pty == nullptr) {
-                if (child_setup_data_destroy)
-                        child_setup_data_destroy(child_setup_data);
-
-                callback(terminal, -1, error, user_data);
+        auto pty = vte::glib::take_ref(vte_terminal_pty_new_sync(terminal, pty_flags, cancellable, error));
+        if (!pty) {
+                auto task = vte::glib::take_ref(g_task_new(nullptr,
+                                                           cancellable,
+                                                           spawn_async_cb,
+                                                           spawn_async_callback_data_new(terminal, callback, 
user_data)));
+                g_task_return_error(task.get(), error.release());
                 return;
         }
 
-        guint spawn_flags = (guint)spawn_flags_;
-
-        /* We do NOT support this flag. If you want to have some FD open in the child
-         * process, simply use a child setup function that unsets the CLOEXEC flag
-         * on that FD.
-         */
-        g_warn_if_fail((spawn_flags & G_SPAWN_LEAVE_DESCRIPTORS_OPEN) == 0);
-        spawn_flags &= ~G_SPAWN_LEAVE_DESCRIPTORS_OPEN;
-
-        vte_pty_spawn_async(pty,
+        vte_pty_spawn_async(pty.get(),
                             working_directory,
                             argv,
                             envv,
-                            GSpawnFlags(spawn_flags),
+                            spawn_flags,
                             child_setup, child_setup_data, child_setup_data_destroy,
                             timeout, cancellable,
                             spawn_async_cb,
                             spawn_async_callback_data_new(terminal, callback, user_data));
-        g_object_unref(pty);
 }
 
 /**
diff --git a/src/vtepty.cc b/src/vtepty.cc
index a056ce25..013de1ab 100644
--- a/src/vtepty.cc
+++ b/src/vtepty.cc
@@ -39,7 +39,7 @@
 
 #include "libc-glue.hh"
 #include "pty.hh"
-#include "vtespawn.hh"
+#include "spawn.hh"
 
 #include "vteptyinternal.hh"
 
@@ -124,8 +124,6 @@ _vte_pty_get_impl(VtePty* pty)
 /**
  * vte_pty_child_setup:
  * @pty: a #VtePty
- *
- * FIXMEchpe
  */
 void
 vte_pty_child_setup (VtePty *pty)
@@ -137,71 +135,6 @@ vte_pty_child_setup (VtePty *pty)
         impl->child_setup();
 }
 
-/*
- * __vte_pty_spawn:
- * @pty: a #VtePty
- * @directory: the name of a directory the command should start in, or %NULL
- *   to use the cwd
- * @argv: child's argument vector
- * @envv: a list of environment variables to be added to the environment before
- *   starting the process, or %NULL
- * @spawn_flags: flags from #GSpawnFlags
- * @child_setup: function to run in the child just before exec()
- * @child_setup_data: user data for @child_setup
- * @child_pid: a location to store the child PID, or %NULL
- * @timeout: a timeout value in ms, or %NULL
- * @cancellable: a #GCancellable, or %NULL
- * @error: return location for a #GError, or %NULL
- *
- * Uses g_spawn_async() to spawn the command in @argv. The child's environment will
- * be the parent environment with the variables in @envv set afterwards.
- *
- * Enforces the vte_terminal_watch_child() requirements by adding
- * %G_SPAWN_DO_NOT_REAP_CHILD to @spawn_flags.
- *
- * Note that the %G_SPAWN_LEAVE_DESCRIPTORS_OPEN flag is not supported;
- * it will be cleared!
- *
- * Note also that %G_SPAWN_STDOUT_TO_DEV_NULL, %G_SPAWN_STDERR_TO_DEV_NULL,
- * and %G_SPAWN_CHILD_INHERITS_STDIN are not supported, since stdin, stdout
- * and stderr of the child process will always be connected to the PTY.
- *
- * If spawning the command in @working_directory fails because the child
- * is unable to chdir() to it, falls back trying to spawn the command
- * in the parent's working directory.
- *
- * Returns: %TRUE on success, or %FALSE on failure with @error filled in
- */
-gboolean
-_vte_pty_spawn(VtePty *pty,
-               const char *directory,
-               char **argv,
-               char **envv,
-               GSpawnFlags spawn_flags_,
-               GSpawnChildSetupFunc child_setup,
-               gpointer child_setup_data,
-               GPid *child_pid /* out */,
-               int timeout,
-               GCancellable *cancellable,
-               GError **error)
-{
-        g_return_val_if_fail(VTE_IS_PTY(pty), FALSE);
-
-        auto impl = IMPL(pty);
-        g_return_val_if_fail(impl != nullptr, FALSE);
-
-        return impl->spawn(directory,
-                           argv,
-                           envv,
-                           spawn_flags_,
-                           child_setup,
-                           child_setup_data,
-                           child_pid,
-                           timeout,
-                           cancellable,
-                           error);
-}
-
 /**
  * vte_pty_set_size:
  * @pty: a #VtePty
@@ -587,82 +520,115 @@ vte_pty_get_fd (VtePty *pty)
         return impl->fd();
 }
 
-typedef struct {
-        VtePty* m_pty;
-        char* m_working_directory;
-        char** m_argv;
-        char** m_envv;
-        GSpawnFlags m_spawn_flags;
-        GSpawnChildSetupFunc m_child_setup;
-        gpointer m_child_setup_data;
-        GDestroyNotify m_child_setup_data_destroy;
-        int m_timeout;
-} AsyncSpawnData;
-
-static AsyncSpawnData*
-async_spawn_data_new (VtePty* pty,
-                      char const* working_directory,
-                      char** argv,
-                      char** envv,
-                      GSpawnFlags spawn_flags,
-                      GSpawnChildSetupFunc child_setup,
-                      gpointer child_setup_data,
-                      GDestroyNotify child_setup_data_destroy,
-                      int timeout)
+static constexpr inline auto
+all_spawn_flags()
 {
-        auto data = g_new(AsyncSpawnData, 1);
-
-        data->m_pty = (VtePty*)g_object_ref(pty);
-        data->m_working_directory = g_strdup(working_directory);
-        data->m_argv = g_strdupv(argv);
-        data->m_envv = envv ? g_strdupv(envv) : nullptr;
-        data->m_spawn_flags = spawn_flags;
-        data->m_child_setup = child_setup;
-        data->m_child_setup_data = child_setup_data;
-        data->m_child_setup_data_destroy = child_setup_data_destroy;
-        data->m_timeout = timeout;
-
-        return data;
+        return GSpawnFlags(G_SPAWN_LEAVE_DESCRIPTORS_OPEN |
+                           G_SPAWN_DO_NOT_REAP_CHILD |
+                           G_SPAWN_SEARCH_PATH |
+                           G_SPAWN_STDOUT_TO_DEV_NULL |
+                           G_SPAWN_STDERR_TO_DEV_NULL |
+                           G_SPAWN_CHILD_INHERITS_STDIN |
+                           G_SPAWN_FILE_AND_ARGV_ZERO |
+                           G_SPAWN_SEARCH_PATH_FROM_ENVP |
+                           G_SPAWN_CLOEXEC_PIPES |
+                           VTE_SPAWN_NO_PARENT_ENVV |
+                           VTE_SPAWN_NO_SYSTEMD_SCOPE |
+                           VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE);
 }
 
-static void
-async_spawn_data_free(gpointer data_)
+static constexpr inline auto
+forbidden_spawn_flags()
 {
-        AsyncSpawnData *data = reinterpret_cast<AsyncSpawnData*>(data_);
-
-        g_free(data->m_working_directory);
-        g_strfreev(data->m_argv);
-        g_strfreev(data->m_envv);
-        if (data->m_child_setup_data && data->m_child_setup_data_destroy)
-                data->m_child_setup_data_destroy(data->m_child_setup_data);
-        g_object_unref(data->m_pty);
+        return GSpawnFlags(G_SPAWN_LEAVE_DESCRIPTORS_OPEN |
+                           G_SPAWN_STDOUT_TO_DEV_NULL |
+                           G_SPAWN_STDERR_TO_DEV_NULL |
+                           G_SPAWN_CHILD_INHERITS_STDIN);
+}
 
-        g_free(data);
+static constexpr inline auto
+ignored_spawn_flags()
+{
+        return GSpawnFlags(G_SPAWN_CLOEXEC_PIPES |
+                           G_SPAWN_DO_NOT_REAP_CHILD);
 }
 
-static void
-async_spawn_run_in_thread(GTask *task,
-                          gpointer object,
-                          gpointer data_,
-                          GCancellable *cancellable)
+static vte::base::SpawnContext
+spawn_context_from_args(VtePty* pty,
+                        char const* working_directory,
+                        char** argv,
+                        char** envv,
+                        GSpawnFlags spawn_flags,
+                        GSpawnChildSetupFunc child_setup,
+                        void* child_setup_data,
+                        GDestroyNotify child_setup_data_destroy)
 {
-        AsyncSpawnData *data = reinterpret_cast<AsyncSpawnData*>(data_);
-
-        GPid pid;
-        GError *error = NULL;
-        if (_vte_pty_spawn(data->m_pty,
-                            data->m_working_directory,
-                            data->m_argv,
-                            data->m_envv,
-                            (GSpawnFlags)data->m_spawn_flags,
-                            data->m_child_setup, data->m_child_setup_data,
-                            &pid,
-                            data->m_timeout,
-                            cancellable,
-                            &error))
-                g_task_return_pointer(task, g_memdup(&pid, sizeof(pid)), g_free);
+        auto context = vte::base::SpawnContext{};
+        context.set_pty(vte::glib::make_ref(pty));
+        context.set_cwd(working_directory);
+        context.set_fallback_cwd(g_get_home_dir());
+        context.set_child_setup(child_setup, child_setup_data, child_setup_data_destroy);
+
+        if (spawn_flags & G_SPAWN_SEARCH_PATH_FROM_ENVP)
+                context.set_search_path_from_envp();
+        if (spawn_flags & G_SPAWN_SEARCH_PATH)
+                context.set_search_path();
+
+        if (spawn_flags & G_SPAWN_FILE_AND_ARGV_ZERO)
+                context.set_argv(argv[0], argv + 1);
         else
-                g_task_return_error(task, error);
+                context.set_argv(argv[0], argv);
+
+        context.set_environ(envv);
+        if (spawn_flags & VTE_SPAWN_NO_PARENT_ENVV)
+                context.set_no_inherit_environ();
+
+        if (spawn_flags & VTE_SPAWN_NO_SYSTEMD_SCOPE)
+                context.set_no_systemd_scope();
+        if (spawn_flags & VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE)
+                context.set_require_systemd_scope();
+
+        return context;
+}
+
+bool
+_vte_pty_spawn_sync(VtePty* pty,
+                    char const* working_directory,
+                    char** argv,
+                    char** envv,
+                    GSpawnFlags spawn_flags,
+                    GSpawnChildSetupFunc child_setup,
+                    gpointer child_setup_data,
+                    GDestroyNotify child_setup_data_destroy,
+                    GPid* child_pid /* out */,
+                    int timeout,
+                    GCancellable* cancellable,
+                    GError** error)
+{
+        /* These are ignored or need not be passed since the behaviour is the default */
+        g_warn_if_fail((spawn_flags & ignored_spawn_flags()) == 0);
+
+        /* This may be upgraded to a g_return_if_fail in the future */
+        g_warn_if_fail((spawn_flags & forbidden_spawn_flags()) == 0);
+        spawn_flags = GSpawnFlags(spawn_flags & ~forbidden_spawn_flags());
+
+        auto op = vte::base::SpawnOperation{spawn_context_from_args(pty,
+                                                                    working_directory,
+                                                                    argv,
+                                                                    envv,
+                                                                    spawn_flags,
+                                                                    child_setup,
+                                                                    child_setup_data,
+                                                                    child_setup_data_destroy),
+                                            timeout,
+                                            cancellable};
+
+        auto err = vte::glib::Error{};
+        auto rv = op.run_sync(child_pid, err);
+        if (!rv)
+                err.propagate(error);
+
+        return rv;
 }
 
 /**
@@ -677,7 +643,7 @@ async_spawn_run_in_thread(GTask *task,
  * @child_setup: (allow-none) (scope async): an extra child setup function to run in the child just before 
exec(), or %NULL
  * @child_setup_data: (closure child_setup): user data for @child_setup, or %NULL
  * @child_setup_data_destroy: (destroy child_setup_data): a #GDestroyNotify for @child_setup_data, or %NULL
- * @timeout: a timeout value in ms, or -1 to wait indefinitely
+ * @timeout: a timeout value in ms, -1 for the default timeout, or G_MAXINT to wait indefinitely
  * @cancellable: (allow-none): a #GCancellable, or %NULL
  *
  * Starts the specified command under the pseudo-terminal @pty.
@@ -686,12 +652,11 @@ async_spawn_run_in_thread(GTask *task,
  * but can be overridden from @envv.
  * @pty_flags controls logging the session to the specified system log files.
  *
- * Note that %G_SPAWN_DO_NOT_REAP_CHILD will always be added to @spawn_flags.
- *
  * Note also that %G_SPAWN_STDOUT_TO_DEV_NULL, %G_SPAWN_STDERR_TO_DEV_NULL,
  * and %G_SPAWN_CHILD_INHERITS_STDIN are not supported in @spawn_flags, since
  * stdin, stdout and stderr of the child process will always be connected to
- * the PTY.
+ * the PTY. Also %G_SPAWN_LEAVE_DESCRIPTORS_OPEN is not supported; and
+ * %G_SPAWN_DO_NOT_REAP_CHILD will always be added to @spawn_flags.
  *
  * Note that all open file descriptors will be closed in the child. If you want
  * to keep some file descriptor open for use in the child process, you need to
@@ -725,22 +690,35 @@ vte_pty_spawn_async(VtePty *pty,
                     gpointer user_data)
 {
         g_return_if_fail(argv != nullptr);
+        g_return_if_fail((spawn_flags & ~all_spawn_flags()) == 0);
         g_return_if_fail(!child_setup_data || child_setup);
         g_return_if_fail(!child_setup_data_destroy || child_setup_data);
+        g_return_if_fail(timeout >= -1);
         g_return_if_fail(cancellable == nullptr || G_IS_CANCELLABLE (cancellable));
         g_return_if_fail(callback);
 
-        auto data = async_spawn_data_new(pty,
-                                         working_directory, argv, envv,
-                                         spawn_flags,
-                                         child_setup, child_setup_data, child_setup_data_destroy,
-                                         timeout);
-
-        auto task = g_task_new(pty, cancellable, callback, user_data);
-        g_task_set_source_tag(task, (void*)vte_pty_spawn_async);
-        g_task_set_task_data(task, data, async_spawn_data_free);
-        g_task_run_in_thread(task, async_spawn_run_in_thread);
-        g_object_unref(task);
+        /* These are ignored or need not be passed since the behaviour is the default */
+        g_warn_if_fail((spawn_flags & ignored_spawn_flags()) == 0);
+
+        /* This may be upgraded to a g_return_if_fail in the future */
+        g_warn_if_fail((spawn_flags & forbidden_spawn_flags()) == 0);
+        spawn_flags = GSpawnFlags(spawn_flags & ~forbidden_spawn_flags());
+
+        auto op = new vte::base::SpawnOperation{spawn_context_from_args(pty,
+                                                                        working_directory,
+                                                                        argv,
+                                                                        envv,
+                                                                        spawn_flags,
+                                                                        child_setup,
+                                                                        child_setup_data,
+                                                                        child_setup_data_destroy),
+                                                timeout,
+                                                cancellable};
+
+        /* takes ownership of @op */
+        op->run_async((void*)vte_pty_spawn_async, /* tag */
+                      callback,
+                      user_data);
 }
 
 /**
@@ -755,27 +733,19 @@ vte_pty_spawn_async(VtePty *pty,
  * Since: 0.48
  */
 gboolean
-vte_pty_spawn_finish(VtePty *pty,
-                     GAsyncResult *result,
-                     GPid *child_pid /* out */,
-                     GError **error)
+vte_pty_spawn_finish(VtePty* pty,
+                     GAsyncResult* result,
+                     GPid* child_pid /* out */,
+                     GError** error)
 {
-        g_return_val_if_fail (VTE_IS_PTY (pty), FALSE);
-        g_return_val_if_fail (G_IS_TASK (result), FALSE);
-        g_return_val_if_fail(error == nullptr || *error == nullptr, FALSE);
-
-        gpointer pidptr = g_task_propagate_pointer(G_TASK(result), error);
-        if (pidptr == nullptr) {
-                if (child_pid)
-                        *child_pid = -1;
-                return FALSE;
-        }
+        g_return_val_if_fail (VTE_IS_PTY(pty), false);
+        g_return_val_if_fail (G_IS_TASK(result), false);
+        g_return_val_if_fail (g_task_get_source_tag(G_TASK (result)) == vte_pty_spawn_async, false);
+        g_return_val_if_fail(error == nullptr || *error == nullptr, false);
 
+        auto pid = g_task_propagate_int(G_TASK(result), error);
         if (child_pid)
-                *child_pid = *(GPid*)pidptr;
-        if (error)
-                *error = nullptr;
+                *child_pid = pid;
 
-        g_free(pidptr);
-        return TRUE;
+        return pid != -1;
 }
diff --git a/src/vteptyinternal.hh b/src/vteptyinternal.hh
index 12dcf38c..a6d66091 100644
--- a/src/vteptyinternal.hh
+++ b/src/vteptyinternal.hh
@@ -22,14 +22,15 @@
 
 vte::base::Pty* _vte_pty_get_impl(VtePty* pty);
 
-gboolean _vte_pty_spawn(VtePty *pty,
-                        const char *working_directory,
-                        char **argv,
-                        char **envv,
-                        GSpawnFlags spawn_flags,
-                        GSpawnChildSetupFunc child_setup,
-                        gpointer child_setup_data,
-                        GPid *child_pid /* out */,
-                        int timeout,
-                        GCancellable *cancellable,
-                        GError **error);
+bool _vte_pty_spawn_sync(VtePty* pty,
+                         char const* working_directory,
+                         char** argv,
+                         char** envv,
+                         GSpawnFlags spawn_flags,
+                         GSpawnChildSetupFunc child_setup,
+                         gpointer child_setup_data,
+                         GDestroyNotify child_setup_data_destroy,
+                         GPid* child_pid /* out */,
+                         int timeout,
+                         GCancellable* cancellable,
+                         GError** error);
diff --git a/src/vtespawn.cc b/src/vtespawn.cc
index edb99fb2..645600b3 100644
--- a/src/vtespawn.cc
+++ b/src/vtespawn.cc
@@ -49,124 +49,10 @@
 
 #define _(s) g_dgettext("glib20", s)
 
-/*
- * SECTION:spawn
- * @Short_description: process launching
- * @Title: Spawning Processes
- *
- * GLib supports spawning of processes with an API that is more
- * convenient than the bare UNIX fork() and exec().
- *
- * The vte_spawn family of functions has synchronous (vte_spawn_sync())
- * and asynchronous variants (vte_spawn_async(), vte_spawn_async_with_pipes(),
- * vte_spawn_async_cancellable(), vte_spawn_async_with_pipes_cancellable),
- * as well as convenience variants that take a complete shell-like
- * commandline (vte_spawn_command_line_sync(), vte_spawn_command_line_async()).
- *
- * See #GSubprocess in GIO for a higher-level API that provides
- * stream interfaces for communication with child processes.
- */
-
-static gint g_execute (const gchar  *file,
-                       gchar **argv,
-                       gchar **envp,
-                       gboolean search_path,
-                       gboolean search_path_from_envp);
-
-static gboolean fork_exec (const gchar          *working_directory,
-                           gchar               **argv,
-                           gchar               **envp,
-                           gboolean              search_path,
-                           gboolean              search_path_from_envp,
-                           gboolean              file_and_argv_zero,
-                           gboolean              cloexec_pipes,
-                           GSpawnChildSetupFunc  child_setup,
-                           gpointer              user_data,
-                           GPid                 *child_pid,
-                           gint                  timeout,
-                           GPollFD              *pollfd,
-                           GError              **error);
-
-/* Avoids a danger in threaded situations (calling close()
- * on a file descriptor twice, and another thread has
- * re-opened it since the first close)
- */
-static void
-close_and_invalidate (gint *fd)
-{
-  if (*fd == -1)
-    return;
-  else
-    {
-      (void)close(*fd);
-      *fd = -1;
-    }
-}
-
-/*
- * vte_spawn_async_cancellable:
- * @working_directory: (type filename) (allow-none): child's current working directory, or %NULL to inherit 
parent's, in the GLib file name encoding
- * @argv: (array zero-terminated=1): child's argument vector, in the GLib file name encoding
- * @envp: (array zero-terminated=1) (allow-none): child's environment, or %NULL to inherit parent's, in the 
GLib file name encoding
- * @flags: flags from #GSpawnFlags
- * @child_setup: (scope async) (allow-none): function to run in the child just before exec()
- * @user_data: (closure): user data for @child_setup
- * @child_pid: (out) (allow-none): return location for child process ID, or %NULL
- * @timeout: a timeout value in ms, or -1 to wait indefinitely
- * @pollfd: (allow-none): a #GPollFD, or %NULL
- * @error: return location for error
- *
- * Like g_spawn_async_with_pipes(), but allows the spawning to be
- * aborted.
- *
- * If @timeout is not -1, then the spawning will be aborted if
- * the timeout is exceeded before spawning has completed.
- *
- * If @pollfd is not %NULL, then the spawning will be aborted if
- * the @pollfd.fd becomes readable. Usually, you want to create
- * this parameter with g_cancellable_make_pollfd().
- *
- * Returns: %TRUE on success, %FALSE if an error occurred or the
- *  spawning was aborted
- *
- * Since: 2.52
- */
-gboolean
-vte_spawn_async_cancellable (const gchar          *working_directory,
-                             gchar               **argv,
-                             gchar               **envp,
-                             GSpawnFlags           flags,
-                             GSpawnChildSetupFunc  child_setup,
-                             gpointer              user_data,
-                             GPid                 *child_pid,
-                             gint                  timeout,
-                             GPollFD              *pollfd,
-                             GError              **error)
-{
-  g_return_val_if_fail (argv != NULL, FALSE);
-  g_return_val_if_fail ((flags & (G_SPAWN_STDOUT_TO_DEV_NULL |
-                                  G_SPAWN_STDERR_TO_DEV_NULL |
-                                  G_SPAWN_CHILD_INHERITS_STDIN)) == 0, FALSE);
-
-  return fork_exec (working_directory,
-                    argv,
-                    envp,
-                    (flags & G_SPAWN_SEARCH_PATH) != 0,
-                    (flags & G_SPAWN_SEARCH_PATH_FROM_ENVP) != 0,
-                    (flags & G_SPAWN_FILE_AND_ARGV_ZERO) != 0,
-                    (flags & G_SPAWN_CLOEXEC_PIPES) != 0,
-                    child_setup,
-                    user_data,
-                    child_pid,
-                    timeout,
-                    pollfd,
-                    error);
-}
-
 static gssize
-write_all (gint fd, gconstpointer vbuf, gsize to_write)
+write_all (int fd, gconstpointer vbuf, gsize to_write)
 {
-  gchar *buf = (gchar *) vbuf;
+  char *buf = (char *) vbuf;
 
   while (to_write > 0)
     {
@@ -186,16 +72,13 @@ write_all (gint fd, gconstpointer vbuf, gsize to_write)
   return TRUE;
 }
 
-G_GNUC_NORETURN
-static void
-write_err_and_exit (gint fd, gint msg)
+void
+_vte_write_err (int fd,
+                int msg)
 {
-  gint en = errno;
-
-  write_all (fd, &msg, sizeof(msg));
-  write_all (fd, &en, sizeof(en));
+        int data[2] = {msg, errno};
 
-  _exit (1);
+        write_all(fd, data, sizeof(data));
 }
 
 static int
@@ -220,7 +103,7 @@ fd_set_nonblocking(int fd)
 }
 
 static int
-set_cloexec (void *data, gint fd)
+set_cloexec (void *data, int fd)
 {
   if (fd >= GPOINTER_TO_INT (data))
     fd_set_cloexec (fd);
@@ -247,7 +130,7 @@ struct linux_dirent64
   char           d_name[]; /* Filename (null-terminated) */
 };
 
-static gint
+static int
 filename_to_fd (const char *p)
 {
   char c;
@@ -276,16 +159,19 @@ filename_to_fd (const char *p)
 #endif
 
 #ifndef HAVE_FDWALK
-static int
+int
+fdwalk (int (*cb)(void *data, int fd), void *data);
+
+int
 fdwalk (int (*cb)(void *data, int fd), void *data)
 {
   /* Fallback implementation of fdwalk. It should be async-signal safe, but it
    * may be slow on non-Linux operating systems, especially on systems allowing
    * very high number of open file descriptors.
    */
-  gint open_max;
-  gint fd;
-  gint res = 0;
+  int open_max;
+  int fd;
+  int res = 0;
 
 #ifdef HAVE_SYS_RESOURCE_H
   struct rlimit rl;
@@ -340,68 +226,34 @@ fdwalk (int (*cb)(void *data, int fd), void *data)
 }
 #endif /* HAVE_FDWALK */
 
-enum
-{
-  CHILD_CHDIR_FAILED,
-  CHILD_EXEC_FAILED,
-};
-
-static G_GNUC_NORETURN void
-do_exec (gint                  child_err_report_fd,
-         const gchar          *working_directory,
-         gchar               **argv,
-         gchar               **envp,
-         gboolean              search_path,
-         gboolean              search_path_from_envp,
-         gboolean              file_and_argv_zero,
-         GSpawnChildSetupFunc  child_setup,
-         gpointer              user_data)
+void
+_vte_cloexec_from(int fd)
 {
-  if (working_directory && chdir (working_directory) < 0)
-    write_err_and_exit (child_err_report_fd,
-                        CHILD_CHDIR_FAILED);
-
-  /* Close all file descriptors before we exec. Note that this includes
-   * child_err_report_fd, which keeps the parent from blocking
-   * forever on the other end of that pipe.
-   * (Note that stdin, stdout and stderr will be set by the child setup afterwards.)
-   */
-  fdwalk (set_cloexec, GINT_TO_POINTER (3));
-
-  /* Call user function just before we exec */
-  child_setup (user_data);
-
-  g_execute (argv[0],
-             file_and_argv_zero ? argv + 1 : argv,
-             envp, search_path, search_path_from_envp);
-
-  /* Exec failed */
-  write_err_and_exit (child_err_report_fd,
-                      CHILD_EXEC_FAILED);
+        fdwalk(set_cloexec, GINT_TO_POINTER(fd));
 }
 
-static gboolean
-read_ints (int      fd,
-           gint*    buf,
-           gint     n_ints_in_buf,
-           gint    *n_ints_read,
-           gint     timeout,
-           GPollFD *cancellable_pollfd,
-           GError **error)
+bool
+_vte_read_ints(int      fd,
+               int*    buf,
+               int     n_ints_in_buf,
+               int    *n_ints_read,
+               int     timeout,
+               GPollFD *cancellable_pollfd,
+               GError **error)
 {
   gsize bytes = 0;
   GPollFD pollfds[2];
   guint n_pollfds;
-  gint64 start_time = 0;
+  int64_t start_time = 0;
 
-  if (timeout >= 0 || cancellable_pollfd != NULL)
+  if (timeout >= 0 || cancellable_pollfd != nullptr)
     {
       if (fd_set_nonblocking(fd) < 0)
         {
           int errsv = errno;
           g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv),
                        _("Failed to set pipe nonblocking: %s"), g_strerror (errsv));
-          return FALSE;
+          return false;
         }
 
       pollfds[0].fd = fd;
@@ -420,11 +272,11 @@ read_ints (int      fd,
   if (timeout >= 0)
     start_time = g_get_monotonic_time ();
 
-  while (TRUE)
+  while (true)
     {
       gssize chunk;
 
-      if (bytes >= sizeof(gint)*2)
+      if (bytes >= sizeof(int)*2)
         break; /* give up, who knows what happened, should not be
                 * possible.
                 */
@@ -453,13 +305,13 @@ read_ints (int      fd,
               int errsv = errno;
               g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv),
                            _("poll error: %s"), g_strerror (errsv));
-              return FALSE;
+              return false;
             }
           if (r == 0)
             {
               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT,
                                    _("Operation timed out"));
-              return FALSE;
+              return false;
             }
 
           /* If the passed-in poll FD becomes readable, that's the signal
@@ -469,15 +321,15 @@ read_ints (int      fd,
             {
               g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
                                    _("Operation was cancelled"));
-              return FALSE;
+              return false;
             }
 
           /* Now we know we can try to read from the child */
         }
 
       chunk = read (fd,
-                    ((gchar*)buf) + bytes,
-                    sizeof(gint) * n_ints_in_buf - bytes);
+                    ((char*)buf) + bytes,
+                    sizeof(int) * n_ints_in_buf - bytes);
       if (chunk < 0 && errno == EINTR)
         goto again;
 
@@ -490,7 +342,7 @@ read_ints (int      fd,
                        _("Failed to read from child pipe (%s)"),
                        g_strerror (errsv));
 
-          return FALSE;
+          return false;
         }
       else if (chunk == 0)
         break; /* EOF */
@@ -498,149 +350,17 @@ read_ints (int      fd,
        bytes += chunk;
     }
 
-  *n_ints_read = (gint)(bytes / sizeof(gint));
-
-  return TRUE;
-}
-
-static gboolean
-fork_exec (const gchar          *working_directory,
-           gchar               **argv,
-           gchar               **envp,
-           gboolean              search_path,
-           gboolean              search_path_from_envp,
-           gboolean              file_and_argv_zero,
-           gboolean              cloexec_pipes,
-           GSpawnChildSetupFunc  child_setup,
-           gpointer              user_data,
-           GPid                 *child_pid,
-           gint                  timeout,
-           GPollFD              *pollfd,
-           GError              **error)
-{
-  GPid pid = -1;
-  gint child_err_report_pipe[2] = { -1, -1 };
-  guint pipe_flags = cloexec_pipes ? FD_CLOEXEC : 0;
-
-  if (!g_unix_open_pipe (child_err_report_pipe, pipe_flags, error))
-    return FALSE;
-
-  pid = fork ();
-
-  if (pid < 0)
-    {
-      int errsv = errno;
-
-      g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errsv),
-                   _("Failed to fork (%s)"), g_strerror (errsv));
-
-      goto cleanup_and_fail;
-    }
-  else if (pid == 0)
-    {
-      /* Immediate child. This may or may not be the child that
-       * actually execs the new process.
-       */
-
-      /* Reset some signal handlers that we may use */
-      signal (SIGCHLD, SIG_DFL);
-      signal (SIGINT, SIG_DFL);
-      signal (SIGTERM, SIG_DFL);
-      signal (SIGHUP, SIG_DFL);
-
-      /* Be sure we crash if the parent exits
-       * and we write to the err_report_pipe
-       */
-      signal (SIGPIPE, SIG_DFL);
-
-      do_exec (child_err_report_pipe[1],
-               working_directory,
-               argv,
-               envp,
-               search_path,
-               search_path_from_envp,
-               file_and_argv_zero,
-               child_setup,
-               user_data);
-    }
-  else
-    {
-      /* Parent */
-
-      gint buf[2];
-      gint n_ints = 0;
-
-      /* Close the uncared-about ends of the pipes */
-      close_and_invalidate (&child_err_report_pipe[1]);
-
-      if (!read_ints (child_err_report_pipe[0],
-                      buf, 2, &n_ints,
-                      timeout, pollfd,
-                      error))
-        goto cleanup_and_fail;
-
-      if (n_ints >= 2)
-        {
-          /* Error from the child. */
-
-          switch (buf[0])
-            {
-            case CHILD_CHDIR_FAILED:
-              g_set_error (error, G_IO_ERROR, g_io_error_from_errno (buf[1]),
-                           _("Failed to change to directory “%s”: %s"),
-                           working_directory, g_strerror (buf[1]));
-
-              break;
-
-            case CHILD_EXEC_FAILED:
-              g_set_error (error, G_IO_ERROR, g_io_error_from_errno(buf[1]),
-                           _("Failed to execute child process “%s”: %s"),
-                           argv[0], g_strerror (buf[1]));
-
-              break;
-
-            default:
-              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
-                           _("Unknown error executing child process “%s”"),
-                           argv[0]);
-              break;
-            }
-
-          goto cleanup_and_fail;
-        }
-
-      /* Success against all odds! return the information */
-      close_and_invalidate (&child_err_report_pipe[0]);
-
-      if (child_pid)
-        *child_pid = pid;
-
-      return TRUE;
-    }
-
- cleanup_and_fail:
-
-  /* There was an error from the Child, reap the child to avoid it being
-     a zombie.
-   */
-
-  if (pid > 0)
-    {
-      vte_reaper_add_child(pid);
-     }
-
-  close_and_invalidate (&child_err_report_pipe[0]);
-  close_and_invalidate (&child_err_report_pipe[1]);
+  *n_ints_read = int(bytes / sizeof(int));
 
-  return FALSE;
+  return true;
 }
 
 /* Based on execvp from GNU C Library */
 
 static void
-script_execute (const gchar *file,
-                gchar      **argv,
-                gchar      **envp)
+script_execute (const char *file,
+                char      **argv,
+                char      **envp)
 {
   /* Count the arguments.  */
   int argc = 0;
@@ -649,9 +369,9 @@ script_execute (const gchar *file,
 
   /* Construct an argument list for the shell.  */
   {
-    gchar **new_argv;
+    char **new_argv;
 
-    new_argv = g_new0 (gchar*, argc + 2); /* /bin/sh and NULL */
+    new_argv = g_new0 (char*, argc + 2); /* /bin/sh and NULL */
 
     new_argv[0] = (char *) "/bin/sh";
     new_argv[1] = (char *) file;
@@ -671,12 +391,12 @@ script_execute (const gchar *file,
   }
 }
 
-static gint
-g_execute (const gchar *file,
-           gchar      **argv,
-           gchar      **envp,
-           gboolean     search_path,
-           gboolean     search_path_from_envp)
+int
+_vte_execute (const char *file,
+              char      **argv,
+              char      **envp,
+              bool        search_path,
+              bool        search_path_from_envp)
 {
   if (*file == '\0')
     {
@@ -699,8 +419,8 @@ g_execute (const gchar *file,
   else
     {
       gboolean got_eacces = 0;
-      const gchar *path, *p;
-      gchar *name, *freeme;
+      const char *path, *p;
+      char *name, *freeme;
       gsize len;
       gsize pathlen;
 
diff --git a/src/vtespawn.hh b/src/vtespawn.hh
index 89c752be..855394e6 100644
--- a/src/vtespawn.hh
+++ b/src/vtespawn.hh
@@ -22,13 +22,25 @@
 
 #include <glib.h>
 
-gboolean vte_spawn_async_cancellable (const gchar          *working_directory,
-                                      gchar               **argv,
-                                      gchar               **envp,
-                                      GSpawnFlags           flags,
-                                      GSpawnChildSetupFunc  child_setup,
-                                      gpointer              user_data,
-                                      GPid                 *child_pid,
-                                      gint                  timeout,
-                                      GPollFD              *pollfd,
-                                      GError              **error);
+void _vte_cloexec_from(int fd);
+
+char** _vte_merge_envv(char** envp,
+                       char const* cwd,
+                       bool inherit);
+
+int _vte_execute(char const* file,
+                 char** argv,
+                 char** envp,
+                 bool search_path,
+                 bool search_path_from_envp);
+
+bool _vte_read_ints(int     fd,
+                    int*    buf,
+                    int     n_ints_in_buf,
+                    int    *n_ints_read,
+                    int     timeout,
+                    GPollFD *cancellable_pollfd,
+                    GError **error);
+
+void _vte_write_err (int fd,
+                     int msg);



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]