diff options
author | Stef Walter <stef@memberwebs.com> | 2009-10-11 17:52:54 +0000 |
---|---|---|
committer | Stef Walter <stef@memberwebs.com> | 2009-10-11 20:04:03 +0000 |
commit | 7efb2a3d9d07c31087925143975173e97d3ed8bc (patch) | |
tree | ed48555d70f4185e20c98c2570097b539f91e6f2 /egg | |
parent | 1286e3c553b3e03ca2b0fbb4cdcafa9cf06e6861 (diff) |
[egg] Add spawn with callback functionality.
Allows executing an application and getting callbacks when
stdin, stdout, or stderr need servicing.
Diffstat (limited to 'egg')
-rw-r--r-- | egg/Makefile.am | 1 | ||||
-rw-r--r-- | egg/egg-spawn.c | 378 | ||||
-rw-r--r-- | egg/egg-spawn.h | 65 | ||||
-rw-r--r-- | egg/tests/Makefile.am | 3 | ||||
-rw-r--r-- | egg/tests/test-data/echo-script.sh | 11 | ||||
-rw-r--r-- | egg/tests/unit-test-spawn.c | 274 |
6 files changed, 731 insertions, 1 deletions
diff --git a/egg/Makefile.am b/egg/Makefile.am index b42bc5f1..584df420 100644 --- a/egg/Makefile.am +++ b/egg/Makefile.am @@ -29,6 +29,7 @@ libegg_la_SOURCES = \ egg-openssl.c egg-openssl.h \ egg-unix-credentials.c egg-unix-credentials.h \ egg-secure-memory.c egg-secure-memory.h \ + egg-spawn.c egg-spawn.h \ egg-symkey.c egg-symkey.h \ $(BUILT_SOURCES) diff --git a/egg/egg-spawn.c b/egg/egg-spawn.c new file mode 100644 index 00000000..b4e242f1 --- /dev/null +++ b/egg/egg-spawn.c @@ -0,0 +1,378 @@ +/* + * gnome-keyring + * + * Copyright (C) 2009 Stefan Walter + * + * 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.1 of + * the License, or (at your option) any later version. + * + * This program 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 program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA + * 02111-1307, USA. + */ + +#include "config.h" + +#include "egg-spawn.h" + +#include <glib/gi18n-lib.h> + +#include <errno.h> +#include <string.h> +#include <unistd.h> + +#include <sys/select.h> +#include <sys/wait.h> + +typedef struct _CallbackSource { + GSource source; + EggSpawnCallbacks callbacks; + GPollFD polls[3]; +} CallbackSource; + +static void +close_fd (int *fd) +{ + g_assert (fd); + if (*fd >= 0) + close (*fd); + *fd = -1; +} + +static void +close_poll (GSource *source, GPollFD *poll) +{ + g_source_remove_poll (source, poll); + close_fd (&poll->fd); + poll->revents = 0; +} + +static gboolean +unused_callback (gpointer data) +{ + /* Never called */ + g_assert_not_reached (); + return FALSE; +} + +static gboolean +cb_source_prepare (GSource *source, gint *timeout_) +{ + CallbackSource *cb_source = (CallbackSource*)source; + gint i; + + for (i = 0; i < 3; ++i) { + if (cb_source->polls[i].fd >= 0) + return FALSE; + } + + /* If none of the FDs are valid, then process immediately */ + return TRUE; +} + +static gboolean +cb_source_check (GSource *source) +{ + CallbackSource *cb_source = (CallbackSource*)source; + gint i; + + for (i = 0; i < 3; ++i) { + if (cb_source->polls[i].fd >= 0 && cb_source->polls[i].revents != 0) + return TRUE; + } + return FALSE; +} + +static void +cb_source_finalize (GSource *source) +{ + CallbackSource *cb_source = (CallbackSource*)source; + gint i; + + for (i = 0; i < 3; ++i) + close_fd (&cb_source->polls[i].fd); +} + +static gboolean +cb_source_dispatch (GSource *source, GSourceFunc unused, gpointer user_data) +{ + CallbackSource *cb_source = (CallbackSource*)source; + GPollFD *poll; + gint i; + + /* Standard input */ + poll = &cb_source->polls[0]; + if (poll->fd >= 0 && poll->revents != 0) { + g_assert (cb_source->callbacks.standard_input); + if (!(cb_source->callbacks.standard_input) (poll->fd, user_data)) + close_poll (source, poll); + } + + /* Standard output */ + poll = &cb_source->polls[1]; + if (poll->fd >= 0 && poll->revents != 0) { + g_assert (cb_source->callbacks.standard_output); + if (!(cb_source->callbacks.standard_output) (poll->fd, user_data)) + close_poll (source, poll); + } + + /* Standard error */ + poll = &cb_source->polls[2]; + if (poll->fd >= 0 && poll->revents != 0) { + g_assert (cb_source->callbacks.standard_error); + if (!(cb_source->callbacks.standard_error) (poll->fd, user_data)) + close_poll (source, poll); + } + + for (i = 0; i < 3; ++i) { + if (cb_source->polls[i].fd >= 0) + return TRUE; + } + + /* All input and output is done */ + if (cb_source->callbacks.completed) + (cb_source->callbacks.completed) (user_data); + return FALSE; +} + +static GSourceFuncs cb_source_funcs = { + cb_source_prepare, + cb_source_check, + cb_source_dispatch, + cb_source_finalize, +}; + +guint +egg_spawn_async_with_callbacks (const gchar *working_directory, gchar **argv, + gchar **envp, GSpawnFlags flags, GPid *child_pid, + EggSpawnCallbacks *cbs, gpointer user_data, + GMainContext *context, GError **error) +{ + gint in_fd, out_fd, err_fd; + CallbackSource *cb_source; + GSource *source; + guint tag; + + g_return_val_if_fail (argv != NULL, FALSE); + g_return_val_if_fail ((cbs && cbs->standard_input == NULL) || + !(flags & G_SPAWN_CHILD_INHERITS_STDIN), 0); + g_return_val_if_fail ((cbs && cbs->standard_output == NULL) || + !(flags & G_SPAWN_STDOUT_TO_DEV_NULL), 0); + g_return_val_if_fail ((cbs && cbs->standard_error == NULL) || + !(flags & G_SPAWN_STDERR_TO_DEV_NULL), 0); + + in_fd = out_fd = err_fd = -1; + + if (!g_spawn_async_with_pipes (working_directory, argv, envp, flags, + cbs ? cbs->child_setup : NULL, + user_data, child_pid, + cbs && cbs->standard_input ? &in_fd : NULL, + cbs && cbs->standard_output ? &out_fd : NULL, + cbs && cbs->standard_error ? &err_fd : NULL, + error)) + return 0; + + source = g_source_new (&cb_source_funcs, sizeof (CallbackSource)); + + cb_source = (CallbackSource*)source; + if (cbs != NULL) + memcpy (&cb_source->callbacks, cbs, sizeof (EggSpawnCallbacks)); + + cb_source->polls[0].fd = in_fd; + if (in_fd >= 0) { + g_assert (cb_source->callbacks.standard_input); + cb_source->polls[0].events = G_IO_ERR | G_IO_OUT; + g_source_add_poll (source, &cb_source->polls[0]); + } + cb_source->polls[1].fd = out_fd; + if (out_fd >= 0) { + g_assert (cb_source->callbacks.standard_output); + cb_source->polls[1].events = G_IO_ERR | G_IO_HUP | G_IO_IN; + g_source_add_poll (source, &cb_source->polls[1]); + } + cb_source->polls[2].fd = err_fd; + if (err_fd >= 0) { + g_assert (cb_source->callbacks.standard_error); + cb_source->polls[2].events = G_IO_ERR | G_IO_HUP | G_IO_IN; + g_source_add_poll (source, &cb_source->polls[2]); + } + + if (context == NULL) + context = g_main_context_default (); + g_source_set_callback (source, unused_callback, user_data, + cbs ? cbs->finalize_func : NULL); + tag = g_source_attach (source, context); + g_source_unref (source); + + return tag; +} + +gboolean +egg_spawn_sync_with_callbacks (const gchar *working_directory, gchar **argv, + gchar **envp, GSpawnFlags flags, GPid *child_pid, + EggSpawnCallbacks *cbs, gpointer user_data, + gint *exit_status, GError **error) +{ + gint in_fd, out_fd, err_fd, max_fd; + fd_set read_fds, write_fds; + gboolean failed = FALSE; + gint status; + GPid pid; + gint ret; + + g_return_val_if_fail (argv != NULL, FALSE); + g_return_val_if_fail ((cbs && cbs->standard_input == NULL) || + !(flags & G_SPAWN_CHILD_INHERITS_STDIN), 0); + g_return_val_if_fail ((cbs && cbs->standard_output == NULL) || + !(flags & G_SPAWN_STDOUT_TO_DEV_NULL), 0); + g_return_val_if_fail ((cbs && cbs->standard_error == NULL) || + !(flags & G_SPAWN_STDERR_TO_DEV_NULL), 0); + + in_fd = out_fd = err_fd = -1; + + if (exit_status) + flags |= G_SPAWN_DO_NOT_REAP_CHILD; + + if (!g_spawn_async_with_pipes (working_directory, argv, envp, flags, + cbs ? cbs->child_setup : NULL, + user_data, &pid, + cbs && cbs->standard_input ? &in_fd : NULL, + cbs && cbs->standard_output ? &out_fd : NULL, + cbs && cbs->standard_error ? &err_fd : NULL, + error)) + return FALSE; + + if (child_pid) + *child_pid = pid; + + max_fd = MAX (in_fd, MAX (out_fd, err_fd)) + 1; + + while (in_fd >= 0 || out_fd >= 0 || err_fd >= 0) { + + FD_ZERO (&write_fds); + if (in_fd >= 0) + FD_SET (in_fd, &write_fds); + FD_ZERO (&read_fds); + if (out_fd >= 0) + FD_SET (out_fd, &read_fds); + if (err_fd >= 0) + FD_SET (err_fd, &read_fds); + + ret = select (max_fd, &read_fds, &write_fds, NULL, NULL); + if (ret < 0 && errno != EINTR) { + failed = TRUE; + g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_READ, + _("Unexpected error in select() reading data from a child process (%s)"), + g_strerror (errno)); + break; + } + + if (in_fd >= 0 && FD_ISSET (in_fd, &write_fds)) { + g_assert (cbs && cbs->standard_input); + if (!(cbs->standard_input) (in_fd, user_data)) + close_fd (&in_fd); + } + if (out_fd >= 0 && FD_ISSET (out_fd, &read_fds)) { + g_assert (cbs && cbs->standard_output); + if (!(cbs->standard_output) (out_fd, user_data)) + close_fd (&out_fd); + } + if (err_fd >= 0 && FD_ISSET (err_fd, &read_fds)) { + g_assert (cbs && cbs->standard_error); + if (!(cbs->standard_error) (err_fd, user_data)) + close_fd (&err_fd); + } + } + + if (in_fd >= 0) + close_fd (&in_fd); + if (out_fd >= 0) + close_fd (&out_fd); + if (err_fd >= 0) + close_fd (&err_fd); + + if (!failed) { + if (cbs && cbs->completed) + (cbs->completed) (user_data); + } + +again: + ret = waitpid (pid, &status, 0); + if (ret < 0) { + if (errno == EINTR) + goto again; + else if (errno == ECHILD) { + if (exit_status) + g_warning ("In call to g_spawn_sync(), exit status of a child process was requested but SIGCHLD action was set to SIG_IGN and ECHILD was received by waitpid(), so exit status can't be returned. This is a bug in the program calling g_spawn_sync(); either don't request the exit status, or don't set the SIGCHLD action."); + else + ; /* We don't need the exit status. */ + } else if (!failed) { /* avoid error pileups */ + failed = TRUE; + g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_READ, + _("Unexpected error in waitpid() (%s)"), + g_strerror (errno)); + } + } else { + if (exit_status) + *exit_status = status; + } + + if (!child_pid) + g_spawn_close_pid (pid); + + if (cbs && cbs->finalize_func) + (cbs->finalize_func) (user_data); + + return !failed; +} + +gssize +egg_spawn_write_input (int fd, gconstpointer data, gsize n_data) +{ + gssize result; + + g_return_val_if_fail (fd >= 0, -1); + + for (;;) { + result = write (fd, data, n_data); + if (result < 0) { + if (errno == EINTR) + continue; + else if (errno == EAGAIN) + result = 0; + } + break; + } + + return result; +} + +gssize +egg_spawn_read_output (int fd, gpointer data, gsize n_data) +{ + gssize result; + + g_return_val_if_fail (fd >= 0, -1); + + for (;;) { + result = read (fd, data, n_data); + if (result < 0) { + if (errno == EINTR) + continue; + else if (errno == EAGAIN) + result = 0; + } + break; + } + + return result; +} + diff --git a/egg/egg-spawn.h b/egg/egg-spawn.h new file mode 100644 index 00000000..c611d4e6 --- /dev/null +++ b/egg/egg-spawn.h @@ -0,0 +1,65 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* egg-sub-process.h - spawn a subprocess and perform IO + + Copyright (C) 2009 Stefan Walter + + Gnome keyring is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. + + Gnome keyring 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 + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + + Author: Stef Walter <stef@memberwebs.com> +*/ + +#ifndef EGG_SPAWN_H_ +#define EGG_SUB_PROCESS_H_ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct _EggSpawnCallbacks { + gboolean (*standard_input) (int fd, gpointer user_data); + gboolean (*standard_output) (int fd, gpointer user_data); + gboolean (*standard_error) (int fd, gpointer user_data); + void (*completed) (gpointer user_data); + GDestroyNotify finalize_func; + GSpawnChildSetupFunc child_setup; +} EggSpawnCallbacks; + +guint egg_spawn_async_with_callbacks (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GPid *child_pid, + EggSpawnCallbacks *callbacks, + gpointer user_data, + GMainContext *context, + GError **error); + +gboolean egg_spawn_sync_with_callbacks (const gchar *working_directory, + gchar **argv, + gchar **envp, + GSpawnFlags flags, + GPid *child_pid, + EggSpawnCallbacks *callbacks, + gpointer user_data, + gint *exit_status, + GError **error); + +gssize egg_spawn_write_input (int fd, gconstpointer data, gsize n_data); + +gssize egg_spawn_read_output (int fd, gpointer data, gsize n_data); + +G_END_DECLS + +#endif /*EGG_SPAWN_H_*/ diff --git a/egg/tests/Makefile.am b/egg/tests/Makefile.am index 50b2d4d7..e95b8132 100644 --- a/egg/tests/Makefile.am +++ b/egg/tests/Makefile.am @@ -10,7 +10,8 @@ UNIT_AUTO = \ unit-test-secmem.c \ unit-test-symkey.c \ unit-test-openssl.c \ - unit-test-dh.c + unit-test-dh.c \ + unit-test-spawn.c \ asn1-def-test.h UNIT_PROMPT = diff --git a/egg/tests/test-data/echo-script.sh b/egg/tests/test-data/echo-script.sh new file mode 100644 index 00000000..d3009fb7 --- /dev/null +++ b/egg/tests/test-data/echo-script.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +count=0 +output="" +while read line; do + output="$output $line" + count=`expr $count + 1` + echo "$count" >&2 +done +echo $output +exit 3 diff --git a/egg/tests/unit-test-spawn.c b/egg/tests/unit-test-spawn.c new file mode 100644 index 00000000..6b38d9b7 --- /dev/null +++ b/egg/tests/unit-test-spawn.c @@ -0,0 +1,274 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* unit-test-dh.c: Test egg-spawn.c + + Copyright (C) 2009 Stefan Walter + + The Gnome Keyring Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. + + The Gnome Keyring 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with the Gnome Library; see the file COPYING.LIB. If not, + write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. + + Author: Stef Walter <stef@memberwebs.com> +*/ + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +#include "run-auto-test.h" + +#include "egg-spawn.h" + +#include <sys/wait.h> + +typedef struct _EchoData { + gint index; + gchar *output; + gchar *error; + gboolean finalized; + gboolean completed; + gboolean is_async; + pid_t parent_pid; +} EchoData; + +static gboolean +want_input (gint fd, gpointer user_data) +{ + EchoData *data = user_data; + gchar *buffer; + + g_assert (data); + if (data->index == 85) + return FALSE; + + g_assert (data->index >= 80); + g_assert (data->index < 85); + + buffer = g_strdup_printf ("%d\n", data->index); + if (egg_spawn_write_input (fd, (guchar*)buffer, strlen (buffer)) < 0) + g_assert_not_reached (); + g_free (buffer); + ++data->index; + return TRUE; +} + +static gboolean +have_output (gint fd, gpointer user_data) +{ + EchoData *data = user_data; + gchar buffer[1024]; + gssize length; + gchar *output; + + g_assert (data); + + length = egg_spawn_read_output (fd, (guchar*)buffer, 1023); + g_assert (length >= 0); + + buffer[length] = 0; + output = g_strconcat (data->output ? data->output : "", buffer, NULL); + g_free (data->output); + data->output = output; + + return (length > 0); +} + +static gboolean +have_error (gint fd, gpointer user_data) +{ + EchoData *data = user_data; + gchar buffer[1024]; + gssize length; + gchar *error; + + g_assert (data); + + length = egg_spawn_read_output (fd, (guchar*)buffer, 1023); + g_assert (length >= 0); + + buffer[length] = 0; + error = g_strconcat (data->error ? data->error : "", buffer, NULL); + g_free (data->error); + data->error = error; + + return (length > 0); +} + +static void +completed_func (gpointer user_data) +{ + EchoData *data = user_data; + g_assert (data); + g_assert (!data->finalized); + g_assert (!data->completed); + data->completed = TRUE; + if (data->is_async) + test_mainloop_quit (); +} + +static void +finalize_func (gpointer user_data) +{ + EchoData *data = user_data; + g_assert (!data->finalized); + data->finalized = 1; +} + +static void +child_setup (gpointer user_data) +{ + EchoData *data = user_data; + g_assert (data->parent_pid != getpid ()); +} + +static EggSpawnCallbacks echo_callbacks = { + want_input, + have_output, + have_error, + completed_func, + finalize_func, + child_setup, +}; + +static char* echo_argv[] = { + "/bin/sh", + "./echo-script.sh", + NULL +}; + +static char* error_argv[] = { + "/nonexistent", + NULL +}; + +static EggSpawnCallbacks null_callbacks = { + NULL, + NULL, + NULL, + completed_func, + finalize_func, + child_setup, +}; + +DEFINE_TEST(test_spawn_sync) +{ + GError *error = NULL; + gboolean ret; + gint exit_status; + EchoData data; + GPid pid = 0; + + memset (&data, 0, sizeof (data)); + data.parent_pid = getpid(); + data.index = 80; + + ret = egg_spawn_sync_with_callbacks (test_dir_testdata (), + echo_argv, NULL, 0, &pid, + &echo_callbacks, &data, + &exit_status, &error); + g_assert (ret == TRUE); + g_assert (pid != 0); + g_assert (WIFEXITED (exit_status)); + g_assert (WEXITSTATUS(exit_status) == 3); + g_assert (error == NULL); + g_assert (data.finalized); + g_assert (data.completed); + g_assert_cmpstr (data.output, ==, "80 81 82 83 84\n"); + g_assert_cmpstr (data.error, ==, "1\n2\n3\n4\n5\n"); +} + +DEFINE_TEST(test_spawn_sync_error) +{ + GError *error = NULL; + gboolean ret; + + ret = egg_spawn_sync_with_callbacks (test_dir_testdata (), + error_argv, NULL, 0, NULL, + NULL, NULL, + NULL, &error); + g_assert (ret == FALSE); + g_assert (error != NULL); + g_clear_error (&error); +} + + +DEFINE_TEST(test_spawn_async) +{ + GError *error = NULL; + EchoData data; + guint ret; + GPid pid; + + memset (&data, 0, sizeof (data)); + data.parent_pid = getpid(); + data.index = 80; + data.is_async = TRUE; + + ret = egg_spawn_async_with_callbacks (test_dir_testdata (), + echo_argv, NULL, 0, &pid, + &echo_callbacks, &data, + NULL, &error); + g_assert (ret != 0); + g_assert (error == NULL); + g_assert (!data.finalized); + g_assert (!data.output); + g_assert (!data.completed); + + test_mainloop_run (2000); + + g_assert (data.finalized); + g_assert (data.completed); + g_assert_cmpstr (data.output, ==, "80 81 82 83 84\n"); + g_assert_cmpstr (data.error, ==, "1\n2\n3\n4\n5\n"); +} + +DEFINE_TEST(test_spawn_async_none) +{ + GError *error = NULL; + EchoData data; + guint ret; + + memset (&data, 0, sizeof (data)); + data.parent_pid = getpid(); + data.is_async = TRUE; + + ret = egg_spawn_async_with_callbacks (test_dir_testdata (), + echo_argv, NULL, 0, NULL, + &null_callbacks, &data, + NULL, &error); + g_assert (ret != 0); + g_assert (error == NULL); + g_assert (!data.finalized); + g_assert (!data.completed); + g_assert (!data.output); + + test_mainloop_run (2000); + + g_assert (data.finalized); + g_assert (data.completed); + g_assert (!data.output); +} + +DEFINE_TEST(test_spawn_async_error) +{ + GError *error = NULL; + guint ret; + + ret = egg_spawn_async_with_callbacks (test_dir_testdata (), + error_argv, NULL, 0, NULL, + NULL, NULL, + NULL, &error); + g_assert (ret == 0); + g_assert (error != NULL); + g_clear_error (&error); +} |