Commit 5b48dc40 authored by Colin Walters's avatar Colin Walters Committed by Allison Karlitskaya

GSubprocess: New class for spawning child processes

There are a number of nice things this class brings:

0) Has a race-free termination API on all platforms (on UNIX, calls to
   kill() and waitpid() are coordinated as not to cause problems).
1) Operates in terms of G{Input,Output}Stream, not file descriptors
2) Standard GIO-style async API for wait() with cancellation
3) Makes some simple cases easy, like synchronously spawning a
   process with an argument list
4) Makes hard cases possible, like asynchronously running a process
   with stdout/stderr merged, output directly to a file path

Much rewriting and code review from Ryan Lortie <desrt@desrt.ca>

https://bugzilla.gnome.org/show_bug.cgi?id=672102
parent e30bbca6
......@@ -108,6 +108,10 @@
<xi:include href="xml/ginitable.xml"/>
<xi:include href="xml/gasyncinitable.xml"/>
</chapter>
<chapter id="subprocesses">
<title>Subprocesses</title>
<xi:include href="xml/gsubprocess.xml"/>
</chapter>
<chapter id="networking">
<title>Low-level network support</title>
<xi:include href="xml/gsocket.xml"/>
......
......@@ -4077,4 +4077,37 @@ G_SIMPLE_PROXY_RESOLVER_CLASS
G_IS_SIMPLE_PROXY_RESOLVER_CLASS
G_SIMPLE_PROXY_RESOLVER_GET_CLASS
g_simple_proxy_resolver_get_type
<FILE>gsubprocess</FILE>
<TITLE>GSubprocess</TITLE>
GSubprocess
g_subprocess_new
g_subprocess_newv
<SUBSECTION IO>
g_subprocess_get_stdin_pipe
g_subprocess_get_stdout_pipe
g_subprocess_get_stderr_pipe
<SUBSECTION Waiting>
g_subprocess_wait
g_subprocess_wait_sync
g_subprocess_wait_finish
g_subprocess_wait_check
g_subprocess_wait_check_sync
g_subprocess_wait_check_finish
<SUBSECTION Status>
g_subprocess_get_successful
g_subprocess_get_if_exited
g_subprocess_get_exit_status
g_subprocess_get_if_signaled
g_subprocess_get_term_sig
g_subprocess_get_status
<SUBSECTION Control>
g_subprocess_send_signal
g_subprocess_force_exit
<SUBSECTION Standard>
G_IS_SUBPROCESS
G_TYPE_SUBPROCESS
G_SUBPROCESS
<SUBSECTION Private>
g_subprocess_get_type
</SECTION>
......@@ -137,3 +137,4 @@ g_test_dbus_get_type
g_test_dbus_flags_get_type
g_task_get_type
g_simple_proxy_resolver_get_type
g_subprocess_get_type
......@@ -428,6 +428,9 @@ libgio_2_0_la_SOURCES = \
gsocketlistener.c \
gsocketoutputstream.c \
gsocketoutputstream.h \
gsubprocesslauncher.c \
gsubprocess.c \
gsubprocesslauncher-private.h \
gsocketservice.c \
gsrvtarget.c \
gsimpleproxyresolver.c \
......@@ -591,6 +594,8 @@ gio_headers = \
gsrvtarget.h \
gsimpleproxyresolver.h \
gtask.h \
gsubprocess.h \
gsubprocesslauncher.h \
gtcpconnection.h \
gtcpwrapperconnection.h \
gthreadedsocketservice.h\
......
......@@ -126,6 +126,8 @@
#include <gio/gsrvtarget.h>
#include <gio/gsimpleproxyresolver.h>
#include <gio/gtask.h>
#include <gio/gsubprocess.h>
#include <gio/gsubprocesslauncher.h>
#include <gio/gtcpconnection.h>
#include <gio/gtcpwrapperconnection.h>
#include <gio/gtestdbus.h>
......
......@@ -1696,6 +1696,56 @@ typedef enum /*< flags >*/ {
G_TEST_DBUS_NONE = 0
} GTestDBusFlags;
/**
* GSubprocessFlags:
* @G_SUBPROCESS_FLAGS_NONE: No flags.
* @G_SUBPROCESS_FLAGS_STDIN_PIPE: create a pipe for the stdin of the
* spawned process that can be accessed with
* g_subprocess_get_stdin_pipe().
* @G_SUBPROCESS_FLAGS_STDIN_INHERIT: stdin is inherited from the
* calling process.
* @G_SUBPROCESS_FLAGS_STDOUT_PIPE: create a pipe for the stdout of the
* spawned process that can be accessed with
* g_subprocess_get_stdout_pipe().
* @G_SUBPROCESS_FLAGS_STDOUT_SILENCE: silence the stdout of the spawned
* process (ie: redirect to /dev/null).
* @G_SUBPROCESS_FLAGS_STDERR_PIPE: create a pipe for the stderr of the
* spawned process that can be accessed with
* g_subprocess_get_stderr_pipe().
* @G_SUBPROCESS_FLAGS_STDERR_SILENCE: silence the stderr of the spawned
* process (ie: redirect to /dev/null).
* @G_SUBPROCESS_FLAGS_STDERR_MERGE: merge the stderr of the spawned
* process with whatever the stdout happens to be. This is a good way
* of directing both streams to a common log file, for example.
* @G_SUBPROCESS_FLAGS_INHERIT_FDS: spawned processes will inherit the
* file descriptors of their parent, unless those descriptors have
* been explicitly marked as close-on-exec. This flag has no effect
* over the "standard" file descriptors (stdin, stdout, stderr).
*
* Flags to define the behaviour of a #GSubprocess.
*
* Note that the default for stdin is to redirect from /dev/null. For
* stdout and stderr the default are for them to inherit the
* corresponding descriptor from the calling process.
*
* Note that it is a programmer error to mix 'incompatible' flags. For
* example, you may not request both %G_SUBPROCESS_FLAGS_STDOUT_PIPE and
* %G_SUBPROCESS_FLAGS_STDOUT_SILENCE.
*
* Since: 2.36
**/
typedef enum {
G_SUBPROCESS_FLAGS_NONE = 0,
G_SUBPROCESS_FLAGS_STDIN_PIPE = (1u << 0),
G_SUBPROCESS_FLAGS_STDIN_INHERIT = (1u << 1),
G_SUBPROCESS_FLAGS_STDOUT_PIPE = (1u << 2),
G_SUBPROCESS_FLAGS_STDOUT_SILENCE = (1u << 3),
G_SUBPROCESS_FLAGS_STDERR_PIPE = (1u << 4),
G_SUBPROCESS_FLAGS_STDERR_SILENCE = (1u << 5),
G_SUBPROCESS_FLAGS_STDERR_MERGE = (1u << 6),
G_SUBPROCESS_FLAGS_INHERIT_FDS = (1u << 7)
} GSubprocessFlags;
G_END_DECLS
#endif /* __GIO_ENUMS_H__ */
......@@ -137,6 +137,7 @@ typedef struct _GIOStream GIOStream;
typedef struct _GPollableInputStream GPollableInputStream; /* Dummy typedef */
typedef struct _GPollableOutputStream GPollableOutputStream; /* Dummy typedef */
typedef struct _GResolver GResolver;
/**
* GResource:
*
......@@ -513,6 +514,23 @@ typedef GType (*GDBusProxyTypeFunc) (GDBusObjectManagerClient *manager,
typedef struct _GTestDBus GTestDBus;
/**
* GSubprocess:
*
* A child process.
*
* Since: 2.36
*/
typedef struct _GSubprocess GSubprocess;
/**
* GSubprocessLauncher:
*
* Options for launching a child process.
*
* Since: 2.36
*/
typedef struct _GSubprocessLauncher GSubprocessLauncher;
G_END_DECLS
#endif /* __GIO_TYPES_H__ */
......@@ -297,9 +297,8 @@ end_element (GMarkupParseContext *context,
if (xml_stripblanks && xmllint != NULL)
{
gchar *argv[8];
int status, fd, argc;
gchar *stderr_child = NULL;
int fd;
GSubprocess *proc;
tmp_file = g_strdup ("resource-XXXXXXXX");
if ((fd = g_mkstemp (tmp_file)) == -1)
......@@ -315,43 +314,29 @@ end_element (GMarkupParseContext *context,
}
close (fd);
argc = 0;
argv[argc++] = (gchar *) xmllint;
argv[argc++] = "--nonet";
argv[argc++] = "--noblanks";
argv[argc++] = "--output";
argv[argc++] = tmp_file;
argv[argc++] = real_file;
argv[argc++] = NULL;
g_assert (argc <= G_N_ELEMENTS (argv));
if (!g_spawn_sync (NULL /* cwd */, argv, NULL /* envv */,
G_SPAWN_STDOUT_TO_DEV_NULL,
NULL, NULL, NULL, &stderr_child, &status, &my_error))
{
g_propagate_error (error, my_error);
goto cleanup;
}
/* Ugly...we shoud probably just let stderr be inherited */
if (!g_spawn_check_exit_status (status, NULL))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Error processing input file with xmllint:\n%s"), stderr_child);
g_free (stderr_child);
proc = g_subprocess_new (G_SUBPROCESS_FLAGS_STDOUT_SILENCE, error,
xmllint, "--nonet", "--noblanks", "--output", tmp_file, real_file, NULL);
g_free (real_file);
real_file = NULL;
if (!proc)
goto cleanup;
if (!g_subprocess_wait_check (proc, NULL, error))
{
g_object_unref (proc);
goto cleanup;
}
g_free (stderr_child);
g_free (real_file);
g_object_unref (proc);
real_file = g_strdup (tmp_file);
}
if (to_pixdata)
{
gchar *argv[4];
gchar *stderr_child = NULL;
int status, fd, argc;
int fd;
GSubprocess *proc;
if (gdk_pixbuf_pixdata == NULL)
{
......@@ -375,31 +360,19 @@ end_element (GMarkupParseContext *context,
}
close (fd);
argc = 0;
argv[argc++] = (gchar *) gdk_pixbuf_pixdata;
argv[argc++] = real_file;
argv[argc++] = tmp_file2;
argv[argc++] = NULL;
g_assert (argc <= G_N_ELEMENTS (argv));
proc = g_subprocess_new (G_SUBPROCESS_FLAGS_STDOUT_SILENCE, error,
gdk_pixbuf_pixdata, real_file, tmp_file2, NULL);
g_free (real_file);
real_file = NULL;
if (!g_spawn_sync (NULL /* cwd */, argv, NULL /* envv */,
G_SPAWN_STDOUT_TO_DEV_NULL,
NULL, NULL, NULL, &stderr_child, &status, &my_error))
{
g_propagate_error (error, my_error);
goto cleanup;
}
if (!g_spawn_check_exit_status (status, NULL))
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
_("Error processing input file with to-pixdata:\n%s"), stderr_child);
g_free (stderr_child);
if (!g_subprocess_wait_check (proc, NULL, error))
{
g_object_unref (proc);
goto cleanup;
}
}
g_object_unref (proc);
g_free (stderr_child);
g_free (real_file);
real_file = g_strdup (tmp_file2);
}
}
......
/* GIO - GLib Input, Output and Streaming Library
*
* Copyright © 2012 Red Hat, Inc.
* Copyright © 2012-2013 Canonical Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2 of the licence or (at
* your option) any later version.
*
* See the included COPYING file for more information.
*
* Authors: Colin Walters <walters@verbum.org>
* Ryan Lortie <desrt@desrt.ca>
*/
/**
* SECTION:gsubprocess
* @title: GSubprocess
* @short_description: Child processes
* @see_also: #GSubprocessLauncher
*
* #GSubprocess allows the creation of and interaction with child
* processes.
*
* Processes can be communicated with using standard GIO-style APIs (ie:
* #GInputStream, #GOutputStream). There are GIO-style APIs to wait for
* process termination (ie: cancellable and with an asynchronous
* variant).
*
* There is an API to force a process to terminate, as well as a
* race-free API for sending UNIX signals to a subprocess.
*
* One major advantage that GIO brings over the core GLib library is
* comprehensive API for asynchronous I/O, such
* g_output_stream_splice_async(). This makes GSubprocess
* significantly more powerful and flexible than equivalent APIs in
* some other languages such as the <literal>subprocess.py</literal>
* included with Python. For example, using #GSubprocess one could
* create two child processes, reading standard output from the first,
* processing it, and writing to the input stream of the second, all
* without blocking the main loop.
*
* A powerful g_subprocess_communicate() API is provided similar to the
* <literal>communicate()</literal> method of
* <literal>subprocess.py</literal>. This enables very easy interaction
* with a subprocess that has been opened with pipes.
*
* #GSubprocess defaults to tight control over the file descriptors open
* in the child process, avoiding dangling-fd issues that are caused by
* a simple fork()/exec(). The only open file descriptors in the
* spawned process are ones that were explicitly specified by the
* #GSubprocess API (unless %G_SUBPROCESS_FLAGS_INHERIT_FDS was
* specified).
*
* #GSubprocess will quickly reap all child processes as they exit,
* avoiding "zombie processes" remaining around for long periods of
* time. g_subprocess_wait() can be used to wait for this to happen,
* but it will happen even without the call being explicitly made.
*
* As a matter of principle, #GSubprocess has no API that accepts
* shell-style space-separated strings. It will, however, match the
* typical shell behaviour of searching the PATH for executables that do
* not contain a directory separator in their name.
*
* #GSubprocess attempts to have a very simple API for most uses (ie:
* spawning a subprocess with arguments and support for most typical
* kinds of input and output redirection). See g_subprocess_new(). The
* #GSubprocessLauncher API is provided for more complicated cases
* (advanced types of redirection, environment variable manipulation,
* change of working directory, child setup functions, etc).
*
* A typical use of #GSubprocess will involve calling
* g_subprocess_new(), followed by g_subprocess_wait() or
* g_subprocess_wait_sync(). After the process exits, the status can be
* checked using functions such as g_subprocess_get_if_exited() (which
* are similar to the familiar WIFEXITED-style POSIX macros).
*
* Since: 2.36
**/
#include "config.h"
#include "gsubprocess.h"
#include "gsubprocesslauncher-private.h"
#include "gasyncresult.h"
#include "giostream.h"
#include "gmemoryinputstream.h"
#include "glibintl.h"
#include "glib-private.h"
#include <string.h>
#ifdef G_OS_UNIX
#include <gio/gunixoutputstream.h>
#include <gio/gfiledescriptorbased.h>
#include <gio/gunixinputstream.h>
#include <gstdio.h>
#include <glib-unix.h>
#include <fcntl.h>
#endif
#ifdef G_OS_WIN32
#define _WIN32_WINNT 0x0500
#include <windows.h>
#include "giowin32-priv.h"
#endif
#ifndef O_BINARY
#define O_BINARY 0
#endif
#define COMMUNICATE_READ_SIZE 4096
/* A GSubprocess can have two possible states: running and not.
*
* These two states are reflected by the value of 'pid'. If it is
* non-zero then the process is running, with that pid.
*
* When a GSubprocess is first created with g_object_new() it is not
* running. When it is finalized, it is also not running.
*
* During initable_init(), if the g_spawn() is successful then we
* immediately register a child watch and take an extra ref on the
* subprocess. That reference doesn't drop until the child has quit,
* which is why finalize can only happen in the non-running state. In
* the event that the g_spawn() failed we will still be finalizing a
* non-running GSubprocess (before returning from g_subprocess_new())
* with NULL.
*
* We make extensive use of the glib worker thread to guarantee
* race-free operation. As with all child watches, glib calls waitpid()
* in the worker thread. It reports the child exiting to us via the
* worker thread (which means that we can do synchronous waits without
* running a separate loop). We also send signals to the child process
* via the worker thread so that we don't race with waitpid() and
* accidentally send a signal to an already-reaped child.
*/
static void initable_iface_init (GInitableIface *initable_iface);
typedef GObjectClass GSubprocessClass;
struct _GSubprocess
{
GObject parent;
/* only used during construction */
GSubprocessLauncher *launcher;
GSubprocessFlags flags;
gchar **argv;
/* state tracking variables */
gchar identifier[24];
int status;
GPid pid;
/* list of GTask */
GMutex pending_waits_lock;
GSList *pending_waits;
/* These are the streams created if a pipe is requested via flags. */
GOutputStream *stdin_pipe;
GInputStream *stdout_pipe;
GInputStream *stderr_pipe;
};
G_DEFINE_TYPE_WITH_CODE (GSubprocess, g_subprocess, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init));
enum
{
PROP_0,
PROP_FLAGS,
PROP_ARGV,
N_PROPS
};
typedef struct
{
gint fds[3];
GSpawnChildSetupFunc child_setup_func;
gpointer child_setup_data;
GArray *basic_fd_assignments;
GArray *needdup_fd_assignments;
} ChildData;
static void
unset_cloexec (int fd)
{
int flags;
int result;
flags = fcntl (fd, F_GETFD, 0);
if (flags != -1)
{
flags &= (~FD_CLOEXEC);
do
result = fcntl (fd, F_SETFD, flags);
while (result == -1 && errno == EINTR);
}
}
/**
* Based on code derived from
* gnome-terminal:src/terminal-screen.c:terminal_screen_child_setup(),
* used under the LGPLv2+ with permission from author.
*/
static void
child_setup (gpointer user_data)
{
ChildData *child_data = user_data;
gint i;
gint result;
/* We're on the child side now. "Rename" the file descriptors in
* child_data.fds[] to stdin/stdout/stderr.
*
* We don't close the originals. It's possible that the originals
* should not be closed and if they should be closed then they should
* have been created O_CLOEXEC.
*/
for (i = 0; i < 3; i++)
if (child_data->fds[i] != -1 && child_data->fds[i] != i)
{
do
result = dup2 (child_data->fds[i], i);
while (result == -1 && errno == EINTR);
}
/* Basic fd assignments we can just unset FD_CLOEXEC */
if (child_data->basic_fd_assignments)
{
for (i = 0; i < child_data->basic_fd_assignments->len; i++)
{
gint fd = g_array_index (child_data->basic_fd_assignments, int, i);
unset_cloexec (fd);
}
}
/* If we're doing remapping fd assignments, we need to handle
* the case where the user has specified e.g.:
* 5 -> 4, 4 -> 6
*
* We do this by duping the source fds temporarily.
*/
if (child_data->needdup_fd_assignments)
{
for (i = 0; i < child_data->needdup_fd_assignments->len; i += 2)
{
gint parent_fd = g_array_index (child_data->needdup_fd_assignments, int, i);
gint new_parent_fd;
do
new_parent_fd = fcntl (parent_fd, F_DUPFD_CLOEXEC, 3);
while (parent_fd == -1 && errno == EINTR);
g_array_index (child_data->needdup_fd_assignments, int, i) = new_parent_fd;
}
for (i = 0; i < child_data->needdup_fd_assignments->len; i += 2)
{
gint parent_fd = g_array_index (child_data->needdup_fd_assignments, int, i);
gint child_fd = g_array_index (child_data->needdup_fd_assignments, int, i+1);
if (parent_fd == child_fd)
{
unset_cloexec (parent_fd);
}
else
{
do
result = dup2 (parent_fd, child_fd);
while (result == -1 && errno == EINTR);
(void) close (parent_fd);
}
}
}
if (child_data->child_setup_func)
child_data->child_setup_func (child_data->child_setup_data);
}
static GInputStream *
platform_input_stream_from_spawn_fd (gint fd)
{
if (fd < 0)
return NULL;
#ifdef G_OS_UNIX
return g_unix_input_stream_new (fd, TRUE);
#else
return g_win32_input_stream_new_from_fd (fd, TRUE);
#endif
}
static GOutputStream *
platform_output_stream_from_spawn_fd (gint fd)
{
if (fd < 0)
return NULL;
#ifdef G_OS_UNIX
return g_unix_output_stream_new (fd, TRUE);
#else
return g_win32_output_stream_new_from_fd (fd, TRUE);
#endif
}
#ifdef G_OS_UNIX
static gint
unix_open_file (const char *filename,
gint mode,
GError **error)
{
gint my_fd;
my_fd = g_open (filename, mode | O_BINARY | O_CLOEXEC, 0666);
/* If we return -1 we should also set the error */
if (my_fd < 0)
{
gint saved_errno = errno;
char *display_name;
display_name = g_filename_display_name (filename);
g_set_error (error, G_IO_ERROR, g_io_error_from_errno (saved_errno),
_("Error opening file '%s': %s"), display_name,
g_strerror (saved_errno));
g_free (display_name);
/* fall through... */
}
return my_fd;
}
#endif
static void
g_subprocess_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GSubprocess *self = G_SUBPROCESS (object);
switch (prop_id)
{
case PROP_FLAGS:
self->flags = g_value_get_flags (value);
break;
case PROP_ARGV:
self->argv = g_value_dup_boxed (value);
break;
default:
g_assert_not_reached ();
}
}
static gboolean
g_subprocess_exited (GPid pid,
gint status,
gpointer user_data)
{
GSubprocess *self = user_data;
GSList *tasks;
g_assert (self->pid == pid);
g_mutex_lock (&self->pending_waits_lock);
self->status = status;
tasks = self->pending_waits;
self->pending_waits = NULL;
self->pid = 0;
g_mutex_unlock (&self->pending_waits_lock);
/* Signal anyone in g_subprocess_wait_async() to wake up now */
while (tasks)
{
g_task_return_boolean (tasks->data, TRUE);
tasks = g_slist_delete_link (tasks, tasks);
}
g_spawn_close_pid (pid);
return FALSE;
}
static gboolean
initable_init (GInitable *initable,
GCancellable *cancellable,
GError **error)
{
GSubprocess *self = G_SUBPROCESS (initable);
ChildData child_data = { { -1, -1, -1 }, 0 };
gint *pipe_ptrs[3] = { NULL, NULL, NULL };
gint pipe_fds[3] = { -1, -1, -1 };
gint close_fds[3] = { -1, -1, -1 };
GSpawnFlags spawn_flags = 0;
gboolean success = FALSE;
gint i;
/* this is a programmer error */
if (!self->argv || !self->argv[0] || !self->argv[0][0])
return FALSE;
if (g_cancellable_set_error_if_cancelled (cancellable, error))
return FALSE;
/* We must setup the three fds that will end up in the child as stdin,
* stdout and stderr.
*
* First, stdin.
*/
if (self->flags & G_SUBPROCESS_FLAGS_STDIN_INHERIT)
spawn_flags |= G_SPAWN_CHILD_INHERITS_STDIN;
else if (self->flags & G_SUBPROCESS_FLAGS_STDIN_PIPE)
pipe_ptrs[0] = &pipe_fds[0];
#ifdef G_OS_UNIX
else if (self->launcher)
{
if (self->launcher->stdin_fd != -1)
child_data.fds[0] = self->launcher->stdin_fd;
else if (self->launcher->stdin_path != NULL)
{
child_data.fds[0] = close_fds[0] = unix_open_file (self->launcher->stdin_path, O_RDONLY, error);
if (child_data.fds[0] == -1)
goto out;
}
}
#endif
/* Next, stdout. */
if (self->flags & G_SUBPROCESS_FLAGS_STDOUT_SILENCE)
spawn_flags |= G_SPAWN_STDOUT_TO_DEV_NULL;
else if (self->flags & G_SUBPROCESS_FLAGS_STDOUT_PIPE)
pipe_ptrs[1] = &pipe_fds[1];
#ifdef G_OS_UNIX
else if (self->launcher)
{
if (self->launcher->stdout_fd != -1)
child_data.fds[1] = self->launcher->stdout_fd;
else if (self->launcher->stdout_path != NULL)
{
child_data.fds[1] = close_fds[1] = unix_open_file (self->launcher->stdout_path, O_CREAT | O_WRONLY, error);
if (child_data.fds[1] == -1)
goto out;
}
}
#endif
/* Finally, stderr. */
if (self->flags & G_SUBPROCESS_FLAGS_STDERR_SILENCE)
spawn_flags |= G_SPAWN_STDERR_TO_DEV_NULL;
else if (self->flags & G_SUBPROCESS_FLAGS_STDERR_PIPE)
pipe_ptrs[2] = &pipe_fds[2];
else if (self->flags & G_SUBPROCESS_FLAGS_STDERR_MERGE)
/* This will work because stderr gets setup after stdout. */
child_data.fds[2] = 1;
#ifdef G_OS_UNIX
else if (self->launcher)
{
if (self->launcher->stderr_fd != -1)
child_data.fds[2]