[vte] spawn: Allow passing file descriptors to the child process



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

    spawn: Allow passing file descriptors to the child process
    
    Add vte_pty_spawn_with_fds_async() and vte_terminal_spawn_with_fds_async()
    that take an array of file descriptors, and an array of integers specifying
    where to assign the file descriptors to in the child process.
    
    This also fixes the equivalent of gspawn/gsubprocess bug
    https://gitlab.gnome.org/GNOME/glib/-/issues/2097 when using
    these new functions (e.g. in gnome-terminal).

 doc/reference/vte-sections.txt |   2 +
 meson.build                    |   5 +-
 src/app/app.cc                 | 140 +++++++++++++++++++++++---
 src/libc-glue.hh               |  95 ++++++++++++++++++
 src/pty.cc                     |   7 +-
 src/pty.hh                     |   2 +-
 src/spawn.cc                   | 217 +++++++++++++++++++++++++++++------------
 src/spawn.hh                   |  48 ++++++++-
 src/vte/vtepty.h               |  17 ++++
 src/vte/vteterminal.h          |  19 ++++
 src/vtegtk.cc                  | 123 +++++++++++++++++------
 src/vtepty.cc                  | 120 ++++++++++++++++++-----
 src/vteptyinternal.hh          |   4 +-
 13 files changed, 663 insertions(+), 136 deletions(-)
---
diff --git a/doc/reference/vte-sections.txt b/doc/reference/vte-sections.txt
index 20067c88..482b2a98 100644
--- a/doc/reference/vte-sections.txt
+++ b/doc/reference/vte-sections.txt
@@ -100,6 +100,7 @@ vte_get_encoding_supported
 <SUBSECTION>
 VteTerminalSpawnAsyncCallback
 vte_terminal_spawn_async
+vte_terminal_spawn_with_fds_async
 vte_terminal_get_pty
 vte_terminal_set_pty
 vte_terminal_pty_new_sync
@@ -201,6 +202,7 @@ VTE_SPAWN_NO_PARENT_ENVV
 VTE_SPAWN_NO_SYSTEMD_SCOPE
 VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE
 vte_pty_spawn_async
+vte_pty_spawn_with_fds_async
 vte_pty_spawn_finish
 
 <SUBSECTION Standard>
diff --git a/meson.build b/meson.build
index e2200a75..67a3eb08 100644
--- a/meson.build
+++ b/meson.build
@@ -190,14 +190,11 @@ foreach func: check_functions_required
 endforeach
 
 check_functions = [
-  # Misc I/O routines.
   'explicit_bzero',
+  'fdwalk',
   'pread',
   'pwrite',
-  # Misc string routines.
   'strchrnul',
-  # for vtespawn
-  'fdwalk',
 ]
 
 foreach func: check_functions
diff --git a/src/app/app.cc b/src/app/app.cc
index c5d75ec2..14a6eea5 100644
--- a/src/app/app.cc
+++ b/src/app/app.cc
@@ -18,6 +18,7 @@
 
 #include "config.h"
 
+#include <fcntl.h>
 #include <string.h>
 #include <locale.h>
 #include <unistd.h>
@@ -34,9 +35,12 @@
 #include <cairo/cairo-gobject.h>
 #include <vte/vte.h>
 #include "vtepcre2.h"
-#include "glib-glue.hh"
 
 #include <algorithm>
+#include <vector>
+
+#include "glib-glue.hh"
+#include "libc-glue.hh"
 
 /* options */
 
@@ -121,8 +125,26 @@ public:
                 g_strfreev(environment);
         }
 
+        auto fds()
+        {
+                auto fds = std::vector<int>{};
+                fds.reserve(m_fds.size());
+                for (auto& fd : m_fds)
+                        fds.emplace_back(fd.get());
+
+                return fds;
+        }
+
+        auto map_fds()
+        {
+                return m_map_fds;
+        }
+
 private:
 
+        std::vector<vte::libc::FD> m_fds{};
+        std::vector<int> m_map_fds{};
+
         bool parse_enum(GType type,
                         char const* str,
                         int& value,
@@ -181,6 +203,89 @@ private:
                 return true;
         }
 
+        int parse_fd_arg(char const* arg,
+                         char** end_ptr,
+                         GError** error)
+        {
+                errno = 0;
+                char* end = nullptr;
+                auto const v = g_ascii_strtoll(arg, &end, 10);
+                if (errno || end == arg || v < G_MININT || v > G_MAXINT) {
+                        g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
+                                     "Failed to parse \"%s\" as file descriptor number", arg);
+                        return -1;
+                }
+                if (v == -1) {
+                        g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
+                                     "\"%s\" is not a valid file descriptor number", arg);
+                        return -1;
+                }
+
+                if (end_ptr) {
+                        *end_ptr = end;
+                } else if (*end) {
+                        g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
+                                     "Extra characters after number in \"%s\"", arg);
+                        return -1;
+                }
+
+                return int(v);
+        }
+
+        bool parse_fd_arg(char const* str,
+                          GError** error)
+        {
+                char *end = nullptr;
+                auto fd = parse_fd_arg(str, &end, error);
+                if (fd == -1)
+                        return FALSE;
+
+                auto map_to = int{};
+                if (*end == '=' || *end == ':') {
+                        map_to = parse_fd_arg(end + 1, nullptr, error);
+                        if (map_to == -1)
+                                return false;
+
+                        if (map_to == STDIN_FILENO ||
+                            map_to == STDOUT_FILENO ||
+                            map_to == STDERR_FILENO) {
+                                g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
+                                             "Cannot map file descriptor to %d (reserved)", map_to);
+                                return false;
+                        }
+                } else if (*end) {
+                        g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
+                                     "Failed to parse \"%s\" as file descriptor assignment", str);
+                        return false;
+                } else {
+                        map_to = fd;
+                }
+
+                /* N:M assigns, N=M assigns a dup of N. Always dup stdin/out/err since
+                 * we need to output messages ourself there, too.
+                 */
+                auto new_fd = int{};
+                if (*end == '=' || fd < 3) {
+                        new_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+                        if (new_fd == -1) {
+                                g_set_error (error, G_IO_ERROR, g_io_error_from_errno(errno),
+                                             "Failed to duplicate file descriptor %d: %m", fd);
+                                return false;
+                        }
+                } else {
+                        new_fd = fd;
+                        if (vte::libc::fd_set_cloexec(fd) == -1) {
+                                g_set_error (error, G_IO_ERROR, g_io_error_from_errno(errno),
+                                             "Failed to set cloexec on file descriptor %d: %m", fd);
+                                return false;
+                        }
+                }
+
+                m_fds.emplace_back(new_fd);
+                m_map_fds.emplace_back(map_to);
+                return true;
+        }
+
         bool parse_geometry(char const* str,
                             GError** error)
         {
@@ -246,6 +351,13 @@ private:
                 return that->parse_color(value, &that->bg_color, &set, error);
         }
 
+        static gboolean
+        parse_fd(char const* option, char const* value, void* data, GError** error)
+        {
+                Options* that = static_cast<Options*>(data);
+                return that->parse_fd_arg(value, error);
+        }
+
         static gboolean
         parse_fg_color(char const* option, char const* value, void* data, GError** error)
         {
@@ -380,6 +492,8 @@ public:
                           "Add environment variable to the child\'s environment", "VAR=VALUE" },
                         { "extra-margin", 0, 0, G_OPTION_ARG_INT, &extra_margin,
                           "Add extra margin around the terminal widget", "MARGIN" },
+                        { "fd", 0, 0, G_OPTION_ARG_CALLBACK, (void*)parse_fd,
+                          "Pass file descriptor N (as M) to the child process", "N[=M]" },
                         { "feed-stdin", 'B', 0, G_OPTION_ARG_NONE, &feed_stdin,
                           "Feed input to the terminal", nullptr },
                         { "font", 'f', 0, G_OPTION_ARG_STRING, &font_string,
@@ -1251,16 +1365,20 @@ vteapp_window_launch_argv(VteappWindow* window,
         auto const spawn_flags = GSpawnFlags(G_SPAWN_SEARCH_PATH_FROM_ENVP |
                                              (options.no_systemd_scope ? VTE_SPAWN_NO_SYSTEMD_SCOPE : 0) |
                                              (options.require_systemd_scope ? 
VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE : 0));
-        vte_terminal_spawn_async(window->terminal,
-                                 VTE_PTY_DEFAULT,
-                                 options.working_directory,
-                                 argv,
-                                 options.environment,
-                                 spawn_flags,
-                                 nullptr, nullptr, nullptr, /* child setup, data and destroy */
-                                 -1 /* default timeout of 30s */,
-                                 nullptr /* cancellable */,
-                                 window_spawn_cb, window);
+        auto fds = options.fds();
+        auto map_fds = options.map_fds();
+        vte_terminal_spawn_with_fds_async(window->terminal,
+                                          VTE_PTY_DEFAULT,
+                                          options.working_directory,
+                                          argv,
+                                          options.environment,
+                                          fds.data(), fds.size(),
+                                          map_fds.data(), map_fds.size(),
+                                          spawn_flags,
+                                          nullptr, nullptr, nullptr, /* child setup, data and destroy */
+                                          -1 /* default timeout of 30s */,
+                                          nullptr /* cancellable */,
+                                          window_spawn_cb, window);
         return true;
 }
 
diff --git a/src/libc-glue.hh b/src/libc-glue.hh
index bb497a7e..d2a3271b 100644
--- a/src/libc-glue.hh
+++ b/src/libc-glue.hh
@@ -21,6 +21,7 @@
 #include <cerrno>
 
 #include <unistd.h>
+#include <fcntl.h>
 
 namespace vte::libc {
 
@@ -98,4 +99,98 @@ constexpr bool operator==(FD const& lhs, int rhs) { return lhs.get() == rhs; }
 constexpr bool operator!=(FD const& lhs, FD const& rhs) { return !(lhs == rhs); }
 constexpr bool operator!=(FD const& lhs, int rhs) { return !(lhs == rhs); }
 
+/* FD convenience functions */
+
+static inline int
+fd_dupfd_cloexec(int oldfd,
+                 int newfd)
+{
+        auto fd = int{};
+        do {
+                fd = fcntl(F_DUPFD_CLOEXEC, oldfd, newfd);
+        } while (fd == -1 && errno == EINTR);
+
+        return fd;
+}
+
+static inline int
+fd_get_descriptor_flags(int fd)
+{
+        auto flags = int{};
+        do {
+                flags = fcntl(fd, F_GETFD);
+        } while (flags == -1 && errno == EINTR);
+
+        return flags;
+}
+
+static inline int
+fd_set_descriptor_flags(int fd,
+                        int flags)
+{
+        auto r = int{};
+        do {
+                r = fcntl(fd, F_SETFD, flags);
+        } while (r == -1 && errno == EINTR);
+
+        return r;
+}
+
+static inline int
+fd_change_descriptor_flags(int fd,
+                           int set_flags,
+                           int unset_flags)
+{
+        auto const flags = fd_get_descriptor_flags(fd);
+        if (flags == -1)
+                return -1;
+
+        auto const new_flags = (flags | set_flags) & ~unset_flags;
+        if (new_flags == flags)
+                return 0;
+
+        return fd_set_descriptor_flags(fd, new_flags);
+}
+
+static inline bool
+fd_get_cloexec(int fd)
+{
+        auto const r = fd_get_descriptor_flags(fd);
+        return r != -1 && (r & FD_CLOEXEC) != 0;
+}
+
+static inline int
+fd_set_cloexec(int fd)
+{
+        return fd_change_descriptor_flags(fd, FD_CLOEXEC, 0);
+}
+
+static inline int
+fd_unset_cloexec(int fd)
+{
+        return fd_change_descriptor_flags(fd, 0, FD_CLOEXEC);
+}
+
+static inline int
+fd_dup_cloexec(int oldfd,
+               int newfd)
+{
+        auto r = int{};
+        do {
+                r = fcntl(oldfd, F_DUPFD_CLOEXEC, newfd);
+        } while (r == -1 && errno == EINTR);
+        return r;
+}
+
+static inline int
+fd_dup2(int oldfd,
+        int newfd)
+{
+        auto r = int{};
+        do {
+                r = dup2(oldfd, newfd);
+        } while (r == -1 && errno == EINTR);
+        return r;
+}
+
 } // namespace vte::libc
diff --git a/src/pty.cc b/src/pty.cc
index 6bdec813..2516a32c 100644
--- a/src/pty.cc
+++ b/src/pty.cc
@@ -95,7 +95,7 @@ Pty::unref() noexcept
 }
 
 int
-Pty::get_peer() const noexcept
+Pty::get_peer(bool cloexec) const noexcept
 {
         if (!m_pty_fd)
                 return -1;
@@ -106,8 +106,9 @@ Pty::get_peer() const noexcept
          */
 
         /* Now open the PTY peer. Note that this also makes the PTY our controlling TTY. */
-        /* Note: *not* O_CLOEXEC! */
-        auto const fd_flags = int{O_RDWR | ((m_flags & VTE_PTY_NO_CTTY) ? O_NOCTTY : 0)};
+        auto const fd_flags = int{O_RDWR |
+                                  ((m_flags & VTE_PTY_NO_CTTY) ? O_NOCTTY : 0) |
+                                  (cloexec ? O_CLOEXEC : 0)};
 
         auto peer_fd = vte::libc::FD{};
 
diff --git a/src/pty.hh b/src/pty.hh
index 75b3076d..a0764e25 100644
--- a/src/pty.hh
+++ b/src/pty.hh
@@ -52,7 +52,7 @@ public:
         inline constexpr int fd() const noexcept { return m_pty_fd.get(); }
         inline constexpr auto flags() const noexcept { return m_flags; }
 
-        int get_peer() const noexcept;
+        int get_peer(bool cloexec = false) const noexcept;
 
         void child_setup() const noexcept;
 
diff --git a/src/spawn.cc b/src/spawn.cc
index 844f6245..67dea189 100644
--- a/src/spawn.cc
+++ b/src/spawn.cc
@@ -138,8 +138,8 @@ SpawnContext::prepare_environ()
         m_envv = vte::glib::take_strv(merge_environ(m_envv.release(), m_cwd.get(), inherit_environ()));
 }
 
-int
-SpawnContext::exec() const noexcept
+SpawnContext::ExecError
+SpawnContext::exec(vte::libc::FD& child_report_error_pipe_write) noexcept
 {
         /* NOTE! This function must not rely on smart pointers to
          * release their object, since the destructors are NOT run
@@ -167,7 +167,7 @@ SpawnContext::exec() const noexcept
         sigemptyset(&set);
         if (pthread_sigmask(SIG_SETMASK, &set, nullptr) == -1) {
                 _vte_debug_print(VTE_DEBUG_PTY, "%s failed: %m\n", "pthread_sigmask");
-                return -1;
+                return ExecError::SIGMASK;
         }
 
         /* Reset the handlers for all signals to their defaults.  The parent
@@ -185,11 +185,22 @@ SpawnContext::exec() const noexcept
         /* 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);
 
+        /* Working directory */
+        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 ExecError::CHDIR;
+
+                errsv.reset();
+        }
+
+        /* Session */
         if (!(pty()->flags() & VTE_PTY_NO_SESSION)) {
                 /* This starts a new session; we become its process-group leader,
                  * and lose our controlling TTY.
@@ -197,14 +208,14 @@ SpawnContext::exec() const noexcept
                 _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;
+                        return ExecError::SETSID;
                 }
         }
 
         /* Note: *not* FD_CLOEXEC! */
-        auto peer_fd = pty()->get_peer();
+        auto peer_fd = pty()->get_peer(true /* cloexec */);
         if (peer_fd == -1)
-                return -1;
+                return ExecError::GETPTPEER;
 
 #ifdef TIOCSCTTY
         /* On linux, opening the PTY peer above already made it our controlling TTY (since
@@ -214,40 +225,79 @@ SpawnContext::exec() const noexcept
         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;
+                        return ExecError::SCTTY;
                 }
         }
 #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;
-        }
+        /* Replace the placeholders with the FD assignment for the PTY */
+        m_fd_map[0].first = peer_fd;
+        m_fd_map[1].first = peer_fd;
+        m_fd_map[2].first = peer_fd;
 
-        if (peer_fd != STDIN_FILENO  &&
-            peer_fd != STDOUT_FILENO &&
-            peer_fd != STDERR_FILENO) {
-                close(peer_fd);
-        }
+        /* Assign FDs */
+        auto const n_fd_map = m_fd_map.size();
+        for (auto i = size_t{0}; i < n_fd_map; ++i) {
+                auto [source_fd, target_fd] = m_fd_map[i];
 
-        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.
+                /* -1 means the source_fd is only in the map so that it can
+                 * be checked for conflicts with other target FDs. It may be
+                 * re-assigned while relocating other FDs.
                  */
-                auto errsv = vte::libc::ErrnoSaver{};
-                if (m_fallback_cwd && chdir(m_fallback_cwd.get()) < 0)
-                        return G_SPAWN_ERROR_CHDIR;
+                if (target_fd == -1)
+                        continue;
 
-                errsv.reset();
+                /* We want to move source_fd to target_fd */
+
+                if (target_fd != source_fd) {
+
+                        /* Need to check if target_fd is an FDs in the FD list.
+                         * If so, need to re-assign the source FD(s) first.
+                         */
+                        for (auto j = size_t{0}; j < n_fd_map; ++j) {
+                                auto const [from_fd, to_fd] = m_fd_map[j];
+
+                                if (from_fd != target_fd)
+                                        continue;
+
+                                auto new_from_fd = vte::libc::fd_dup_cloexec(from_fd, target_fd + 1);
+                                if (new_from_fd == -1)
+                                        return ExecError::DUP;
+
+                                for (auto k = j; k < n_fd_map; ++k) {
+                                        if (m_fd_map[k].first == from_fd)
+                                                m_fd_map[k].first = new_from_fd;
+                                }
+
+                                /* Now that we have updated all references to the old
+                                 * source FD in the map, we can close the FD. (Not
+                                 * strictly necessary since it'll be dup2'd over
+                                 * anyway.)
+                                 */
+                                if (from_fd == child_report_error_pipe_write.get()) {
+                                        /* Need to report the new pipe write FD back to the caller. */
+                                        child_report_error_pipe_write = new_from_fd;
+                                } else {
+                                        (void)close(from_fd);
+                                }
+
+                                break;
+                        }
+                }
+
+                /* source_fd may have been changed by the loop above */
+                source_fd = m_fd_map[i].first;
+
+                if (target_fd == source_fd) {
+                        /* Already assigned correctly, but need to remove FD_CLOEXEC */
+                        if (vte::libc::fd_unset_cloexec(target_fd) == -1)
+                                return ExecError::UNSET_CLOEXEC;
+
+                } else {
+                        /* Now we know that target_fd can be safely overwritten. */
+                        if (vte::libc::fd_dup2(source_fd, target_fd) == -1)
+                                return ExecError::DUP2;
+                }
         }
 
         /* Finally call an extra child setup */
@@ -262,7 +312,7 @@ SpawnContext::exec() const noexcept
                      search_path_from_envp());
 
         /* If we get here, exec failed */
-        return G_SPAWN_ERROR_FAILED;
+        return ExecError::EXEC;
 }
 
 SpawnOperation::~SpawnOperation()
@@ -290,7 +340,7 @@ SpawnOperation::~SpawnOperation()
 }
 
 bool
-SpawnOperation::prepare(vte::glib::Error& error) noexcept
+SpawnOperation::prepare(vte::glib::Error& error)
 {
 #ifndef WITH_SYSTEMD
         if (context().systemd_scope()) {
@@ -320,6 +370,13 @@ SpawnOperation::prepare(vte::glib::Error& error) noexcept
                        error))
                 return false;
 
+        /* Need to add the write end of the pipe to the FD map, so
+         * that the FD re-arranging code knows it needs to preserve
+         * the FD and not dup2 over it.
+         * Target -1 means that no actual re-assignment will take place.
+         */
+        context().add_map_fd(child_report_error_pipe_write.get(), -1);
+
         assert(child_report_error_pipe_read);
         assert(child_report_error_pipe_write);
         auto const pid = fork();
@@ -336,13 +393,11 @@ SpawnOperation::prepare(vte::glib::Error& error) noexcept
                 /* Child */
 
                 child_report_error_pipe_read.reset();
-                assert(!child_report_error_pipe_read);
 
-                auto const err = context().exec();
+                auto const err = context().exec(child_report_error_pipe_write);
 
-                /* 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);
+                /* If we get here, exec failed. Write the error to the pipe and exit. */
+                _vte_write_err(child_report_error_pipe_write.get(), int(err));
                 _exit(127);
                 return true;
         }
@@ -375,37 +430,77 @@ SpawnOperation::run(vte::glib::Error& error) noexcept
                 /* 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 const err = buf[1];
+
+                switch (SpawnContext::ExecError(buf[0])) {
+                case SpawnContext::ExecError::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]),
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
                                   _("Failed to change to directory “%s”: %s"),
                                   cwd.get(),
-                                  g_strerror(buf[1]));
+                                  g_strerror(err));
                         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]));
+                case SpawnContext::ExecError::DUP:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Failed to duplicate file descriptor: %s",
+                                  g_strerror(err));
                         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());
+                case SpawnContext::ExecError::DUP2:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Failed to duplicate file descriptor (dup2): %s",
+                                  g_strerror(err));
+                        break;
+
+                case SpawnContext::ExecError::EXEC:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Failed to execve: %s",
+                                  g_strerror(err));
+                        break;
+
+                case SpawnContext::ExecError::GETPTPEER:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Failed to open PTY peer: %s",
+                                  g_strerror(err));
+                        break;
+
+                case SpawnContext::ExecError::SCTTY:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Failed to set controlling TTY: %s",
+                                  g_strerror(err));
+                        break;
+
+                case SpawnContext::ExecError::SETSID:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Failed to start session: %s",
+                                  g_strerror(err));
+                        break;
+
+                case SpawnContext::ExecError::SIGMASK:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Failed to set signal mask: %s",
+                                  g_strerror(err));
+                        break;
+
+                case SpawnContext::ExecError::UNSET_CLOEXEC:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Failed to make file descriptor not cloexec: %s",
+                                  g_strerror(err));
+                        break;
+
+                default:
+                        error.set(G_IO_ERROR, g_io_error_from_errno(err),
+                                  "Unknown error: %s",
+                                  g_strerror(err));
                         break;
-                }
                 }
 
+                auto arg0 = vte::glib::take_string(g_utf8_make_valid(context().argv()[0], -1));
+                g_prefix_error(error,
+                               _("Failed to execute child process “%s”"),
+                               arg0.get());
                 return false;
         }
 
diff --git a/src/spawn.hh b/src/spawn.hh
index c6a5b1f3..26b0ec0b 100644
--- a/src/spawn.hh
+++ b/src/spawn.hh
@@ -18,6 +18,7 @@
 #pragma once
 
 #include <memory>
+#include <vector>
 
 #include <glib.h>
 #include <gio/gio.h>
@@ -47,6 +48,13 @@ private:
         vte::glib::StrvPtr m_argv;
         vte::glib::StrvPtr m_envv;
 
+        std::vector<vte::libc::FD> m_fds{};
+
+        // these 3 are placeholder elements for the PTY peer fd being mapped to 0, 1, 2 later
+        // we preallocate this here so that the child setup function doesn't do any
+        // allocations
+        std::vector<std::pair<int,int>> m_fd_map{{-1, 0}, {-1, 1}, {-1, 2}};
+
         child_setup_type m_child_setup{(void(*)(void*))0};
         void* m_child_setup_data{nullptr};
         GDestroyNotify m_child_setup_data_destroy{nullptr};
@@ -119,6 +127,30 @@ public:
                 m_child_setup_data_destroy = destroy;
         }
 
+        void add_fds(int const* fds,
+                     int n_fds)
+        {
+                m_fds.reserve(m_fds.size() + n_fds);
+                for (auto i = int{0}; i < n_fds; ++i)
+                        m_fds.emplace_back(fds[i]);
+        }
+
+        void add_map_fds(int const* fds,
+                         int n_fds,
+                         int const* map_fds,
+                         int n_map_fds)
+        {
+                m_fd_map.reserve(m_fd_map.size() + n_fds);
+                for (auto i = int{0}; i < n_fds; ++i)
+                        m_fd_map.emplace_back(fds[i], i < n_map_fds ? map_fds[i] : -1);
+        }
+
+        void add_map_fd(int fd,
+                        int map_to)
+        {
+                add_map_fds(&fd, 1, &map_to, 1);
+        }
+
         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; }
@@ -142,7 +174,19 @@ public:
 
         void prepare_environ();
 
-        int exec() const noexcept;
+        enum class ExecError {
+                CHDIR,
+                DUP,
+                DUP2,
+                EXEC,
+                GETPTPEER,
+                SCTTY,
+                SETSID,
+                SIGMASK,
+                UNSET_CLOEXEC,
+        };
+
+        ExecError exec(vte::libc::FD& child_report_error_pipe_write) noexcept;
 
 }; // class SpawnContext
 
@@ -161,7 +205,7 @@ private:
         pid_t m_pid{-1};
         bool m_kill_pid{true};
 
-        auto& context() const noexcept { return m_context; }
+        auto& context() noexcept { return m_context; }
 
         bool prepare(vte::glib::Error& error);
         bool run(vte::glib::Error& error) noexcept;
diff --git a/src/vte/vtepty.h b/src/vte/vtepty.h
index 06822740..f811c5b2 100644
--- a/src/vte/vtepty.h
+++ b/src/vte/vtepty.h
@@ -110,6 +110,23 @@ void vte_pty_spawn_async(VtePty *pty,
                          gpointer user_data) _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(3);
 
 _VTE_PUBLIC
+void vte_pty_spawn_with_fds_async(VtePty *pty,
+                                  char const* working_directory,
+                                  char const* const* argv,
+                                  char const* const* envv,
+                                  int const* fds,
+                                  int n_fds,
+                                  int const* map_fds,
+                                  int n_map_fds,
+                                  GSpawnFlags spawn_flags,
+                                  GSpawnChildSetupFunc child_setup,
+                                  gpointer child_setup_data,
+                                  GDestroyNotify child_setup_data_destroy,
+                                  int timeout,
+                                  GCancellable *cancellable,
+                                  GAsyncReadyCallback callback,
+                                  gpointer user_data) _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(3);
+
 gboolean vte_pty_spawn_finish(VtePty *pty,
                               GAsyncResult *result,
                               GPid *child_pid /* out */,
diff --git a/src/vte/vteterminal.h b/src/vte/vteterminal.h
index 93c07d11..6bb2e0f7 100644
--- a/src/vte/vteterminal.h
+++ b/src/vte/vteterminal.h
@@ -160,6 +160,25 @@ void vte_terminal_spawn_async(VteTerminal *terminal,
                               VteTerminalSpawnAsyncCallback callback,
                               gpointer user_data) _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(4);
 
+_VTE_PUBLIC
+void vte_terminal_spawn_with_fds_async(VteTerminal* terminal,
+                                       VtePtyFlags pty_flags,
+                                       char const* working_directory,
+                                       char const* const* argv,
+                                       char const* const* envv,
+                                       int const* fds,
+                                       int n_fds,
+                                       int const* map_fds,
+                                       int n_map_fds,
+                                       GSpawnFlags spawn_flags,
+                                       GSpawnChildSetupFunc child_setup,
+                                       gpointer child_setup_data,
+                                       GDestroyNotify child_setup_data_destroy,
+                                       int timeout,
+                                       GCancellable* cancellable,
+                                       VteTerminalSpawnAsyncCallback callback,
+                                       gpointer user_data) _VTE_GNUC_NONNULL(1) _VTE_GNUC_NONNULL(4);
+
 /* Send data to the terminal to display, or to the terminal's forked command
  * to handle in some way.  If it's 'cat', they should be the same. */
 _VTE_PUBLIC
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index 2e69616b..b3ed2dd4 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -2865,7 +2865,7 @@ spawn_async_cb (GObject *source,
  */
 
 /**
- * vte_terminal_spawn_async:
+ * vte_terminal_spawn_with_fds_async:
  * @terminal: a #VteTerminal
  * @pty_flags: flags from #VtePtyFlags
  * @working_directory: (allow-none): the name of a directory the command should start
@@ -2873,6 +2873,10 @@ 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
+ * @fds: (nullable) (array length=n_fds) (transfer none) (scope call): an array of file descriptors, or %NULL
+ * @n_fds: the number of file descriptors in @fds, or 0 if @fds is %NULL
+ * @map_fds: (nullable) (array length=n_map_fds) (transfer none) (scope call): an array of integers, or %NULL
+ * @n_map_fds: the number of elements in @map_fds, or 0 if @map_fds is %NULL
  * @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
@@ -2883,7 +2887,7 @@ spawn_async_cb (GObject *source,
  * @user_data: (closure callback): user data for @callback, or %NULL
  *
  * A convenience function that wraps creating the #VtePty and spawning
- * the child process on it. See vte_pty_new_sync(), vte_pty_spawn_async(),
+ * the child process on it. See vte_pty_new_sync(), vte_pty_spawn_with_fds_async(),
  * and vte_pty_spawn_finish() for more information.
  *
  * When the operation is finished successfully, @callback will be called
@@ -2898,6 +2902,16 @@ spawn_async_cb (GObject *source,
  * stdin, stdout and stderr of the child process will always be connected to
  * the PTY.
  *
+ * If @fds is not %NULL, the child process will map the file descriptors from
+ * @fds according to @map_fds; @n_map_fds must be less or equal to @n_fds.
+ * This function will take ownership of the file descriptors in @fds;
+ * you must not use or close them after this call.
+ *
+ * Note that all  open file descriptors apart from those mapped as above
+ * will be closed in the child. (If you want to keep some other file descriptor
+ * open for use in the child process, you need to use a child setup function
+ * that unsets the FD_CLOEXEC flag on that file descriptor manually.)
+ *
  * Beginning with 0.60, and on linux only, and unless %VTE_SPAWN_NO_SYSTEMD_SCOPE is
  * passed in @spawn_flags, the newly created child process will be moved to its own
  * systemd user scope; and if %VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE is passed, and creation
@@ -2917,29 +2931,30 @@ spawn_async_cb (GObject *source,
  * The caller should also make sure that symlinks were preserved while constructing the value of 
@working_directory,
  * e.g. by using vte_terminal_get_current_directory_uri(), g_get_current_dir() or get_current_dir_name().
  *
- * Since: 0.48
+ * Since: 0.62
  */
 void
-vte_terminal_spawn_async(VteTerminal *terminal,
-                         VtePtyFlags pty_flags,
-                         const char *working_directory,
-                         char **argv,
-                         char **envv,
-                         GSpawnFlags spawn_flags,
-                         GSpawnChildSetupFunc child_setup,
-                         gpointer child_setup_data,
-                         GDestroyNotify child_setup_data_destroy,
-                         int timeout,
-                         GCancellable *cancellable,
-                         VteTerminalSpawnAsyncCallback callback,
-                         gpointer user_data)
+vte_terminal_spawn_with_fds_async(VteTerminal *terminal,
+                                  VtePtyFlags pty_flags,
+                                  const char *working_directory,
+                                  char const* const* argv,
+                                  char const* const* envv,
+                                  int const* fds,
+                                  int n_fds,
+                                  int const* fd_map_to,
+                                  int n_fd_map_to,
+                                  GSpawnFlags spawn_flags,
+                                  GSpawnChildSetupFunc child_setup,
+                                  gpointer child_setup_data,
+                                  GDestroyNotify child_setup_data_destroy,
+                                  int timeout,
+                                  GCancellable *cancellable,
+                                  VteTerminalSpawnAsyncCallback callback,
+                                  gpointer user_data)
 {
         g_return_if_fail(VTE_IS_TERMINAL(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));
+        g_return_if_fail(callback);
 
         auto error = vte::glib::Error{};
         auto pty = vte::glib::take_ref(vte_terminal_pty_new_sync(terminal, pty_flags, cancellable, error));
@@ -2952,15 +2967,65 @@ vte_terminal_spawn_async(VteTerminal *terminal,
                 return;
         }
 
-        vte_pty_spawn_async(pty.get(),
-                            working_directory,
-                            argv,
-                            envv,
-                            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));
+        vte_pty_spawn_with_fds_async(pty.get(),
+                                     working_directory,
+                                     argv,
+                                     envv,
+                                     fds, n_fds, fd_map_to, n_fd_map_to,
+                                     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));
+}
+
+/**
+ * vte_terminal_spawn_async:
+ * @terminal: a #VteTerminal
+ * @pty_flags: flags from #VtePtyFlags
+ * @working_directory: (allow-none): the name of a directory the command should start
+ *   in, or %NULL to use the current working directory
+ * @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
+ * @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, -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
+ *
+ * A convenience function that wraps creating the #VtePty and spawning
+ * the child process on it. Like vte_terminal_spawn_with_fds_async(),
+ * except that this function does not allow passing file descriptors to
+ * the child process. See vte_terminal_spawn_with_fds_async() for more
+ * information.
+ *
+ * Since: 0.48
+ */
+void
+vte_terminal_spawn_async(VteTerminal *terminal,
+                         VtePtyFlags pty_flags,
+                         const char *working_directory,
+                         char **argv,
+                         char **envv,
+                         GSpawnFlags spawn_flags,
+                         GSpawnChildSetupFunc child_setup,
+                         gpointer child_setup_data,
+                         GDestroyNotify child_setup_data_destroy,
+                         int timeout,
+                         GCancellable *cancellable,
+                         VteTerminalSpawnAsyncCallback callback,
+                         gpointer user_data)
+{
+        vte_terminal_spawn_with_fds_async(terminal, pty_flags, working_directory, argv, envv,
+                                          nullptr, 0, nullptr, 0,
+                                          spawn_flags,
+                                          child_setup, child_setup_data, child_setup_data_destroy,
+                                          timeout, cancellable,
+                                          callback, user_data);
 }
 
 /**
diff --git a/src/vtepty.cc b/src/vtepty.cc
index 013de1ab..b64da787 100644
--- a/src/vtepty.cc
+++ b/src/vtepty.cc
@@ -556,8 +556,12 @@ ignored_spawn_flags()
 static vte::base::SpawnContext
 spawn_context_from_args(VtePty* pty,
                         char const* working_directory,
-                        char** argv,
-                        char** envv,
+                        char const* const* argv,
+                        char const* const* envv,
+                        int const* fds,
+                        int n_fds,
+                        int const* fd_map_to,
+                        int n_fd_map_to,
                         GSpawnFlags spawn_flags,
                         GSpawnChildSetupFunc child_setup,
                         void* child_setup_data,
@@ -588,14 +592,17 @@ spawn_context_from_args(VtePty* pty,
         if (spawn_flags & VTE_SPAWN_REQUIRE_SYSTEMD_SCOPE)
                 context.set_require_systemd_scope();
 
+        context.add_fds(fds, n_fds);
+        context.add_map_fds(fds, n_fds, fd_map_to, n_fd_map_to);
+
         return context;
 }
 
 bool
 _vte_pty_spawn_sync(VtePty* pty,
                     char const* working_directory,
-                    char** argv,
-                    char** envv,
+                    char const* const* argv,
+                    char const* const* envv,
                     GSpawnFlags spawn_flags,
                     GSpawnChildSetupFunc child_setup,
                     gpointer child_setup_data,
@@ -616,6 +623,8 @@ _vte_pty_spawn_sync(VtePty* pty,
                                                                     working_directory,
                                                                     argv,
                                                                     envv,
+                                                                    nullptr, 0,
+                                                                    nullptr, 0,
                                                                     spawn_flags,
                                                                     child_setup,
                                                                     child_setup_data,
@@ -632,13 +641,17 @@ _vte_pty_spawn_sync(VtePty* pty,
 }
 
 /**
- * vte_pty_spawn_async:
+ * vte_pty_spawn_with_fds_async:
  * @pty: a #VtePty
  * @working_directory: (allow-none): the name of a directory the command should start
  *   in, or %NULL to use the current working directory
  * @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
+ * @fds: (nullable) (array length=n_fds) (transfer none) (scope call): an array of file descriptors, or %NULL
+ * @n_fds: the number of file descriptors in @fds, or 0 if @fds is %NULL
+ * @map_fds: (nullable) (array length=n_map_fds) (transfer none) (scope call): an array of integers, or %NULL
+ * @n_map_fds: the number of elements in @map_fds, or 0 if @map_fds is %NULL
  * @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
@@ -658,10 +671,17 @@ _vte_pty_spawn_sync(VtePty* 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
- * use a child setup function that unsets the FD_CLOEXEC flag on that file
- * descriptor.
+ * If @fds is not %NULL, the child process will map the file descriptors from
+ * @fds according to @map_fds; @n_map_fds must be less or equal to @n_fds.
+ * This function will take ownership of the file descriptors in @fds;
+ * you must not use or close them after this call. All file descriptors in @fds
+ * must have the FD_CLOEXEC flag set on them; it will be unset in the child process
+ * before calling exec.
+ *
+ * Note that all  open file descriptors apart from those mapped as above
+ * will be closed in the child. (If you want to keep some other file descriptor
+ * open for use in the child process, you need to use a child setup function
+ * that unsets the FD_CLOEXEC flag on that file descriptor manually.)
  *
  * Beginning with 0.60, and on linux only, and unless %VTE_SPAWN_NO_SYSTEMD_SCOPE is
  * passed in @spawn_flags, the newly created child process will be moved to its own
@@ -671,25 +691,34 @@ _vte_pty_spawn_sync(VtePty* pty,
  * providing a systemd override file for 'vte-spawn-.scope' unit. See man:systemd.unit(5)
  * for further information.
  *
- * See vte_pty_new(), g_spawn_async() and vte_terminal_watch_child() for more information.
+ * See vte_pty_new(), and vte_terminal_watch_child() for more information.
  *
- * Since: 0.48
+ * Since: 0.62
  */
 void
-vte_pty_spawn_async(VtePty *pty,
-                    const char *working_directory,
-                    char **argv,
-                    char **envv,
-                    GSpawnFlags spawn_flags,
-                    GSpawnChildSetupFunc child_setup,
-                    gpointer child_setup_data,
-                    GDestroyNotify child_setup_data_destroy,
-                    int timeout,
-                    GCancellable *cancellable,
-                    GAsyncReadyCallback callback,
-                    gpointer user_data)
+vte_pty_spawn_with_fds_async(VtePty *pty,
+                             char const* working_directory,
+                             char const* const* argv,
+                             char const* const* envv,
+                             int const* fds,
+                             int n_fds,
+                             int const* fd_map_to,
+                             int n_fd_map_to,
+                             GSpawnFlags spawn_flags,
+                             GSpawnChildSetupFunc child_setup,
+                             gpointer child_setup_data,
+                             GDestroyNotify child_setup_data_destroy,
+                             int timeout,
+                             GCancellable *cancellable,
+                             GAsyncReadyCallback callback,
+                             gpointer user_data)
 {
         g_return_if_fail(argv != nullptr);
+        g_return_if_fail(n_fds == 0 || fds != nullptr);
+        for (auto i = int{0}; i < n_fds; ++i)
+                g_return_if_fail(vte::libc::fd_get_cloexec(fds[i]));
+        g_return_if_fail(n_fd_map_to == 0 || fd_map_to != nullptr);
+        g_return_if_fail(n_fds >= n_fd_map_to);
         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);
@@ -708,6 +737,8 @@ vte_pty_spawn_async(VtePty *pty,
                                                                         working_directory,
                                                                         argv,
                                                                         envv,
+                                                                        fds, n_fds,
+                                                                        fd_map_to, n_fd_map_to,
                                                                         spawn_flags,
                                                                         child_setup,
                                                                         child_setup_data,
@@ -721,6 +752,49 @@ vte_pty_spawn_async(VtePty *pty,
                       user_data);
 }
 
+/**
+ * vte_pty_spawn_async:
+ * @pty: a #VtePty
+ * @working_directory: (allow-none): the name of a directory the command should start
+ *   in, or %NULL to use the current working directory
+ * @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
+ * @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, -1 for the default timeout, or G_MAXINT to wait indefinitely
+ * @cancellable: (allow-none): a #GCancellable, or %NULL
+ *
+ * Like vte_pty_spawn_with_fds_async(), except that this function does not
+ * allow passing file descriptors to the child process. See vte_pty_spawn_with_fds_async()
+ * for more information.
+ *
+ * Since: 0.48
+ */
+void
+vte_pty_spawn_async(VtePty *pty,
+                    const char *working_directory,
+                    char **argv,
+                    char **envv,
+                    GSpawnFlags spawn_flags,
+                    GSpawnChildSetupFunc child_setup,
+                    gpointer child_setup_data,
+                    GDestroyNotify child_setup_data_destroy,
+                    int timeout,
+                    GCancellable *cancellable,
+                    GAsyncReadyCallback callback,
+                    gpointer user_data)
+{
+        vte_pty_spawn_with_fds_async(pty, working_directory, argv, envv,
+                                     nullptr, 0, nullptr, 0,
+                                     spawn_flags,
+                                     child_setup, child_setup_data, child_setup_data_destroy,
+                                     timeout, cancellable,
+                                     callback, user_data);
+}
+
 /**
  * vte_pty_spawn_finish:
  * @pty: a #VtePty
diff --git a/src/vteptyinternal.hh b/src/vteptyinternal.hh
index a6d66091..14321008 100644
--- a/src/vteptyinternal.hh
+++ b/src/vteptyinternal.hh
@@ -24,8 +24,8 @@ vte::base::Pty* _vte_pty_get_impl(VtePty* pty);
 
 bool _vte_pty_spawn_sync(VtePty* pty,
                          char const* working_directory,
-                         char** argv,
-                         char** envv,
+                         char const* const* argv,
+                         char const* const* envv,
                          GSpawnFlags spawn_flags,
                          GSpawnChildSetupFunc child_setup,
                          gpointer child_setup_data,


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