mirror of
https://gitlab.gnome.org/GNOME/glib.git
synced 2025-10-06 13:40:06 +02:00
On Unix platforms, wait() and friends yield an integer that encodes how the process exited. Confusingly, this is usually not the same as the integer passed to exit() or returned from main(): conceptually it's an integer encoding of this tagged union: enum { EXITED, SIGNALLED, ... } tag; union { int exit_status; /* if EXITED */ struct { int terminating_signal; bool core_dumped; } terminating_signal; /* if SIGNALLED */ ... } detail; Meanwhile, on Windows, wait statuses and exit statuses are interchangeable. I find that it's clearer what is going on if we are consistent about referring to the result of wait() as a "wait status", and the value passed to exit() as an "exit status". GSubprocess already gets this right: g_subprocess_get_status() returns the wait status, while g_subprocess_get_exit_status() genuinely returns the exit status. However, the GSpawn family of APIs has tended to conflate the two. Confusingly, g_spawn_check_exit_status() has always checked a wait status, and it would not be correct to pass an exit status to it; so let's deprecate it in favour of g_spawn_check_wait_status(), which does the same thing that g_spawn_check_exit_status() always did. Code that needs backwards-compatibility with older GLib can use: #if !GLIB_CHECK_VERSION(2, 69, 0) #define g_spawn_check_wait_status(x) (g_spawn_check_exit_status (x)) #endif Signed-off-by: Simon McVittie <smcv@collabora.com>
538 lines
16 KiB
C
538 lines
16 KiB
C
/*
|
||
* Copyright (C) 2011 Red Hat, Inc.
|
||
*
|
||
* This work is provided "as is"; redistribution and modification
|
||
* in whole or in part, in any medium, physical or electronic is
|
||
* permitted without restriction.
|
||
*
|
||
* This work 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.
|
||
*
|
||
* In no event shall the authors or contributors be liable for any
|
||
* direct, indirect, incidental, special, exemplary, or consequential
|
||
* damages (including, but not limited to, procurement of substitute
|
||
* goods or services; loss of use, data, or profits; or business
|
||
* interruption) however caused and on any theory of liability, whether
|
||
* in contract, strict liability, or tort (including negligence or
|
||
* otherwise) arising in any way out of the use of this software, even
|
||
* if advised of the possibility of such damage.
|
||
*
|
||
* Author: Colin Walters <walters@verbum.org>
|
||
*/
|
||
|
||
#include "config.h"
|
||
|
||
#include <glib.h>
|
||
#include <locale.h>
|
||
#include <string.h>
|
||
#include <fcntl.h>
|
||
|
||
#ifdef G_OS_UNIX
|
||
#include <glib-unix.h>
|
||
#include <glib/gstdio.h>
|
||
#include <sys/types.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
#endif
|
||
|
||
#ifdef G_OS_WIN32
|
||
#include <io.h>
|
||
#define LINEEND "\r\n"
|
||
#else
|
||
#define LINEEND "\n"
|
||
#endif
|
||
|
||
/* MinGW builds are likely done using a BASH-style shell, so run the
|
||
* normal script there, as on non-Windows builds, as it is more likely
|
||
* that one will run 'make check' in such shells to test the code
|
||
*/
|
||
#if defined (G_OS_WIN32) && defined (_MSC_VER)
|
||
#define SCRIPT_EXT ".bat"
|
||
#else
|
||
#define SCRIPT_EXT
|
||
#endif
|
||
|
||
static char *echo_prog_path;
|
||
static char *echo_script_path;
|
||
|
||
typedef struct {
|
||
GMainLoop *loop;
|
||
gboolean child_exited;
|
||
gboolean stdout_done;
|
||
GString *stdout_buf;
|
||
} SpawnAsyncMultithreadedData;
|
||
|
||
static gboolean
|
||
on_child_exited (GPid pid,
|
||
gint status,
|
||
gpointer datap)
|
||
{
|
||
SpawnAsyncMultithreadedData *data = datap;
|
||
|
||
data->child_exited = TRUE;
|
||
if (data->child_exited && data->stdout_done)
|
||
g_main_loop_quit (data->loop);
|
||
|
||
return G_SOURCE_REMOVE;
|
||
}
|
||
|
||
static gboolean
|
||
on_child_stdout (GIOChannel *channel,
|
||
GIOCondition condition,
|
||
gpointer datap)
|
||
{
|
||
char buf[1024];
|
||
GError *error = NULL;
|
||
gsize bytes_read;
|
||
SpawnAsyncMultithreadedData *data = datap;
|
||
|
||
if (condition & G_IO_IN)
|
||
{
|
||
GIOStatus status;
|
||
status = g_io_channel_read_chars (channel, buf, sizeof (buf), &bytes_read, &error);
|
||
g_assert_no_error (error);
|
||
g_string_append_len (data->stdout_buf, buf, (gssize) bytes_read);
|
||
if (status == G_IO_STATUS_EOF)
|
||
data->stdout_done = TRUE;
|
||
}
|
||
if (condition & G_IO_HUP)
|
||
data->stdout_done = TRUE;
|
||
if (condition & G_IO_ERR)
|
||
g_error ("Error reading from child stdin");
|
||
|
||
if (data->child_exited && data->stdout_done)
|
||
g_main_loop_quit (data->loop);
|
||
|
||
return !data->stdout_done;
|
||
}
|
||
|
||
static void
|
||
test_spawn_async (void)
|
||
{
|
||
int tnum = 1;
|
||
GError *error = NULL;
|
||
GPtrArray *argv;
|
||
char *arg;
|
||
GPid pid;
|
||
GMainContext *context;
|
||
GMainLoop *loop;
|
||
GIOChannel *channel;
|
||
GSource *source;
|
||
int child_stdout_fd;
|
||
SpawnAsyncMultithreadedData data;
|
||
|
||
context = g_main_context_new ();
|
||
loop = g_main_loop_new (context, TRUE);
|
||
|
||
arg = g_strdup_printf ("thread %d", tnum);
|
||
|
||
argv = g_ptr_array_new ();
|
||
g_ptr_array_add (argv, echo_prog_path);
|
||
g_ptr_array_add (argv, arg);
|
||
g_ptr_array_add (argv, NULL);
|
||
|
||
g_spawn_async_with_pipes (NULL, (char**)argv->pdata, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid, NULL,
|
||
&child_stdout_fd, NULL, &error);
|
||
g_assert_no_error (error);
|
||
g_ptr_array_free (argv, TRUE);
|
||
|
||
data.loop = loop;
|
||
data.stdout_done = FALSE;
|
||
data.child_exited = FALSE;
|
||
data.stdout_buf = g_string_new (0);
|
||
|
||
source = g_child_watch_source_new (pid);
|
||
g_source_set_callback (source, (GSourceFunc)on_child_exited, &data, NULL);
|
||
g_source_attach (source, context);
|
||
g_source_unref (source);
|
||
|
||
channel = g_io_channel_unix_new (child_stdout_fd);
|
||
source = g_io_create_watch (channel, G_IO_IN | G_IO_HUP | G_IO_ERR);
|
||
g_source_set_callback (source, (GSourceFunc)on_child_stdout, &data, NULL);
|
||
g_source_attach (source, context);
|
||
g_source_unref (source);
|
||
|
||
g_main_loop_run (loop);
|
||
|
||
g_assert (data.child_exited);
|
||
g_assert (data.stdout_done);
|
||
g_assert_cmpstr (data.stdout_buf->str, ==, arg);
|
||
g_string_free (data.stdout_buf, TRUE);
|
||
|
||
g_io_channel_unref (channel);
|
||
g_main_context_unref (context);
|
||
g_main_loop_unref (loop);
|
||
|
||
g_free (arg);
|
||
}
|
||
|
||
/* Windows close() causes failure through the Invalid Parameter Handler
|
||
* Routine if the file descriptor does not exist.
|
||
*/
|
||
static void
|
||
safe_close (int fd)
|
||
{
|
||
if (fd >= 0)
|
||
close (fd);
|
||
}
|
||
|
||
/* Test g_spawn_async_with_fds() with a variety of different inputs */
|
||
static void
|
||
test_spawn_async_with_fds (void)
|
||
{
|
||
int tnum = 1;
|
||
GPtrArray *argv;
|
||
char *arg;
|
||
gsize i;
|
||
|
||
/* Each test has 3 variable parameters: stdin, stdout, stderr */
|
||
enum fd_type {
|
||
NO_FD, /* pass fd -1 (unset) */
|
||
FD_NEGATIVE, /* pass fd of negative value (equivalent to unset) */
|
||
PIPE, /* pass fd of new/unique pipe */
|
||
STDOUT_PIPE, /* pass the same pipe as stdout */
|
||
} tests[][3] = {
|
||
{ NO_FD, NO_FD, NO_FD }, /* Test with no fds passed */
|
||
{ NO_FD, FD_NEGATIVE, NO_FD }, /* Test another negative fd value */
|
||
{ PIPE, PIPE, PIPE }, /* Test with unique fds passed */
|
||
{ NO_FD, PIPE, STDOUT_PIPE }, /* Test the same fd for stdout + stderr */
|
||
};
|
||
|
||
arg = g_strdup_printf ("thread %d", tnum);
|
||
|
||
argv = g_ptr_array_new ();
|
||
g_ptr_array_add (argv, echo_prog_path);
|
||
g_ptr_array_add (argv, arg);
|
||
g_ptr_array_add (argv, NULL);
|
||
|
||
for (i = 0; i < G_N_ELEMENTS (tests); i++)
|
||
{
|
||
GError *error = NULL;
|
||
GPid pid;
|
||
GMainContext *context;
|
||
GMainLoop *loop;
|
||
GIOChannel *channel = NULL;
|
||
GSource *source;
|
||
SpawnAsyncMultithreadedData data;
|
||
enum fd_type *fd_info = tests[i];
|
||
gint test_pipe[3][2];
|
||
int j;
|
||
|
||
for (j = 0; j < 3; j++)
|
||
{
|
||
switch (fd_info[j])
|
||
{
|
||
case NO_FD:
|
||
test_pipe[j][0] = -1;
|
||
test_pipe[j][1] = -1;
|
||
break;
|
||
case FD_NEGATIVE:
|
||
test_pipe[j][0] = -5;
|
||
test_pipe[j][1] = -5;
|
||
break;
|
||
case PIPE:
|
||
#ifdef G_OS_UNIX
|
||
g_unix_open_pipe (test_pipe[j], FD_CLOEXEC, &error);
|
||
g_assert_no_error (error);
|
||
#else
|
||
g_assert_cmpint (_pipe (test_pipe[j], 4096, _O_BINARY), >=, 0);
|
||
#endif
|
||
break;
|
||
case STDOUT_PIPE:
|
||
g_assert_cmpint (j, ==, 2); /* only works for stderr */
|
||
test_pipe[j][0] = test_pipe[1][0];
|
||
test_pipe[j][1] = test_pipe[1][1];
|
||
break;
|
||
default:
|
||
g_assert_not_reached ();
|
||
}
|
||
}
|
||
|
||
context = g_main_context_new ();
|
||
loop = g_main_loop_new (context, TRUE);
|
||
|
||
g_spawn_async_with_fds (NULL, (char**)argv->pdata, NULL,
|
||
G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &pid,
|
||
test_pipe[0][0], test_pipe[1][1], test_pipe[2][1],
|
||
&error);
|
||
g_assert_no_error (error);
|
||
safe_close (test_pipe[0][0]);
|
||
safe_close (test_pipe[1][1]);
|
||
if (fd_info[2] != STDOUT_PIPE)
|
||
safe_close (test_pipe[2][1]);
|
||
|
||
data.loop = loop;
|
||
data.stdout_done = FALSE;
|
||
data.child_exited = FALSE;
|
||
data.stdout_buf = g_string_new (0);
|
||
|
||
source = g_child_watch_source_new (pid);
|
||
g_source_set_callback (source, (GSourceFunc)on_child_exited, &data, NULL);
|
||
g_source_attach (source, context);
|
||
g_source_unref (source);
|
||
|
||
if (test_pipe[1][0] >= 0)
|
||
{
|
||
channel = g_io_channel_unix_new (test_pipe[1][0]);
|
||
source = g_io_create_watch (channel, G_IO_IN | G_IO_HUP | G_IO_ERR);
|
||
g_source_set_callback (source, (GSourceFunc)on_child_stdout,
|
||
&data, NULL);
|
||
g_source_attach (source, context);
|
||
g_source_unref (source);
|
||
}
|
||
else
|
||
{
|
||
/* Don't check stdout data if we didn't pass a fd */
|
||
data.stdout_done = TRUE;
|
||
}
|
||
|
||
g_main_loop_run (loop);
|
||
|
||
g_assert_true (data.child_exited);
|
||
|
||
if (test_pipe[1][0] >= 0)
|
||
{
|
||
/* Check for echo on stdout */
|
||
g_assert_true (data.stdout_done);
|
||
g_assert_cmpstr (data.stdout_buf->str, ==, arg);
|
||
g_io_channel_unref (channel);
|
||
}
|
||
g_string_free (data.stdout_buf, TRUE);
|
||
|
||
g_main_context_unref (context);
|
||
g_main_loop_unref (loop);
|
||
safe_close (test_pipe[0][1]);
|
||
safe_close (test_pipe[1][0]);
|
||
if (fd_info[2] != STDOUT_PIPE)
|
||
safe_close (test_pipe[2][0]);
|
||
}
|
||
|
||
g_ptr_array_free (argv, TRUE);
|
||
g_free (arg);
|
||
}
|
||
|
||
static void
|
||
test_spawn_sync (void)
|
||
{
|
||
int tnum = 1;
|
||
GError *error = NULL;
|
||
char *arg = g_strdup_printf ("thread %d", tnum);
|
||
/* Include arguments with special symbols to test that they are correctly passed to child.
|
||
* This is tested on all platforms, but the most prone to failure is win32,
|
||
* where args are specially escaped during spawning.
|
||
*/
|
||
const char * const argv[] = {
|
||
echo_prog_path,
|
||
arg,
|
||
"doublequotes\\\"after\\\\\"\"backslashes", /* this would be special escaped on win32 */
|
||
"\\\"\"doublequotes spaced after backslashes\\\\\"", /* this would be special escaped on win32 */
|
||
"even$$dollars",
|
||
"even%%percents",
|
||
"even\"\"doublequotes",
|
||
"even''singlequotes",
|
||
"even\\\\backslashes",
|
||
"even//slashes",
|
||
"$odd spaced$dollars$",
|
||
"%odd spaced%spercents%",
|
||
"\"odd spaced\"doublequotes\"",
|
||
"'odd spaced'singlequotes'",
|
||
"\\odd spaced\\backslashes\\", /* this wasn't handled correctly on win32 in glib <=2.58 */
|
||
"/odd spaced/slashes/",
|
||
NULL
|
||
};
|
||
char *joined_args_str = g_strjoinv ("", (char**)argv + 1);
|
||
char *stdout_str;
|
||
int estatus;
|
||
|
||
g_spawn_sync (NULL, (char**)argv, NULL, 0, NULL, NULL, &stdout_str, NULL, &estatus, &error);
|
||
g_assert_no_error (error);
|
||
g_assert_cmpstr (joined_args_str, ==, stdout_str);
|
||
g_free (arg);
|
||
g_free (stdout_str);
|
||
g_free (joined_args_str);
|
||
}
|
||
|
||
/* Like test_spawn_sync but uses spawn flags that trigger the optimized
|
||
* posix_spawn codepath.
|
||
*/
|
||
static void
|
||
test_posix_spawn (void)
|
||
{
|
||
int tnum = 1;
|
||
GError *error = NULL;
|
||
GPtrArray *argv;
|
||
char *arg;
|
||
char *stdout_str;
|
||
int estatus;
|
||
GSpawnFlags flags = G_SPAWN_CLOEXEC_PIPES | G_SPAWN_LEAVE_DESCRIPTORS_OPEN;
|
||
|
||
arg = g_strdup_printf ("thread %d", tnum);
|
||
|
||
argv = g_ptr_array_new ();
|
||
g_ptr_array_add (argv, echo_prog_path);
|
||
g_ptr_array_add (argv, arg);
|
||
g_ptr_array_add (argv, NULL);
|
||
|
||
g_spawn_sync (NULL, (char**)argv->pdata, NULL, flags, NULL, NULL, &stdout_str, NULL, &estatus, &error);
|
||
g_assert_no_error (error);
|
||
g_assert_cmpstr (arg, ==, stdout_str);
|
||
g_free (arg);
|
||
g_free (stdout_str);
|
||
g_ptr_array_free (argv, TRUE);
|
||
}
|
||
|
||
static void
|
||
test_spawn_script (void)
|
||
{
|
||
GError *error = NULL;
|
||
GPtrArray *argv;
|
||
char *stdout_str;
|
||
int estatus;
|
||
|
||
argv = g_ptr_array_new ();
|
||
g_ptr_array_add (argv, echo_script_path);
|
||
g_ptr_array_add (argv, NULL);
|
||
|
||
g_spawn_sync (NULL, (char**)argv->pdata, NULL, 0, NULL, NULL, &stdout_str, NULL, &estatus, &error);
|
||
g_assert_no_error (error);
|
||
g_assert_cmpstr ("echo" LINEEND, ==, stdout_str);
|
||
g_free (stdout_str);
|
||
g_ptr_array_free (argv, TRUE);
|
||
}
|
||
|
||
/* Test that spawning a non-existent executable returns %G_SPAWN_ERROR_NOENT. */
|
||
static void
|
||
test_spawn_nonexistent (void)
|
||
{
|
||
GError *error = NULL;
|
||
GPtrArray *argv = NULL;
|
||
gchar *stdout_str = NULL;
|
||
gint wait_status = -1;
|
||
|
||
argv = g_ptr_array_new ();
|
||
g_ptr_array_add (argv, "this does not exist");
|
||
g_ptr_array_add (argv, NULL);
|
||
|
||
g_spawn_sync (NULL, (char**) argv->pdata, NULL, 0, NULL, NULL, &stdout_str,
|
||
NULL, &wait_status, &error);
|
||
g_assert_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT);
|
||
g_assert_null (stdout_str);
|
||
g_assert_cmpint (wait_status, ==, -1);
|
||
|
||
g_ptr_array_free (argv, TRUE);
|
||
|
||
g_clear_error (&error);
|
||
}
|
||
|
||
/* Test that FD assignments in a spawned process don’t overwrite and break the
|
||
* child_err_report_fd which is used to report error information back from the
|
||
* intermediate child process to the parent.
|
||
*
|
||
* https://gitlab.gnome.org/GNOME/glib/-/issues/2097 */
|
||
static void
|
||
test_spawn_fd_assignment_clash (void)
|
||
{
|
||
#if defined(G_OS_UNIX) && defined(F_DUPFD_CLOEXEC)
|
||
int tmp_fd;
|
||
guint i;
|
||
const guint n_fds = 10;
|
||
gint source_fds[n_fds];
|
||
gint target_fds[n_fds];
|
||
const gchar *argv[] = { "/nonexistent", NULL };
|
||
gboolean retval;
|
||
GError *local_error = NULL;
|
||
struct stat statbuf;
|
||
|
||
/* Open a temporary file and duplicate its FD several times so we have several
|
||
* FDs to remap in the child process. */
|
||
tmp_fd = g_file_open_tmp ("glib-spawn-test-XXXXXX", NULL, NULL);
|
||
g_assert_cmpint (tmp_fd, >=, 0);
|
||
|
||
for (i = 0; i < (n_fds - 1); ++i)
|
||
{
|
||
int source = fcntl (tmp_fd, F_DUPFD_CLOEXEC, 3);
|
||
g_assert_cmpint (source, >=, 0);
|
||
source_fds[i] = source;
|
||
target_fds[i] = source + n_fds;
|
||
}
|
||
|
||
source_fds[i] = tmp_fd;
|
||
target_fds[i] = tmp_fd + n_fds;
|
||
|
||
/* Print out the FD map. */
|
||
g_test_message ("FD map:");
|
||
for (i = 0; i < n_fds; i++)
|
||
g_test_message (" • %d → %d", source_fds[i], target_fds[i]);
|
||
|
||
/* Spawn the subprocess. This should fail because the executable doesn’t
|
||
* exist. */
|
||
retval = g_spawn_async_with_pipes_and_fds (NULL, argv, NULL, G_SPAWN_DEFAULT,
|
||
NULL, NULL, -1, -1, -1,
|
||
source_fds, target_fds, n_fds,
|
||
NULL, NULL, NULL, NULL,
|
||
&local_error);
|
||
g_assert_error (local_error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT);
|
||
g_assert_false (retval);
|
||
|
||
g_clear_error (&local_error);
|
||
|
||
/* Check nothing was written to the temporary file, as would happen if the FD
|
||
* mapping was messed up to conflict with the child process error reporting FD.
|
||
* See https://gitlab.gnome.org/GNOME/glib/-/issues/2097 */
|
||
g_assert_no_errno (fstat (tmp_fd, &statbuf));
|
||
g_assert_cmpuint (statbuf.st_size, ==, 0);
|
||
|
||
/* Clean up. */
|
||
for (i = 0; i < n_fds; i++)
|
||
g_close (source_fds[i], NULL);
|
||
#else /* !G_OS_UNIX */
|
||
g_test_skip ("FD redirection only supported on Unix with F_DUPFD_CLOEXEC");
|
||
#endif /* !G_OS_UNIX */
|
||
}
|
||
|
||
int
|
||
main (int argc,
|
||
char *argv[])
|
||
{
|
||
char *dirname;
|
||
int ret;
|
||
|
||
setlocale (LC_ALL, "");
|
||
|
||
g_test_init (&argc, &argv, NULL);
|
||
|
||
dirname = g_path_get_dirname (argv[0]);
|
||
echo_prog_path = g_build_filename (dirname, "test-spawn-echo" EXEEXT, NULL);
|
||
if (!g_file_test (echo_prog_path, G_FILE_TEST_EXISTS))
|
||
{
|
||
g_free (echo_prog_path);
|
||
echo_prog_path = g_build_filename (dirname, "lt-test-spawn-echo" EXEEXT, NULL);
|
||
}
|
||
echo_script_path = g_build_filename (dirname, "echo-script" SCRIPT_EXT, NULL);
|
||
if (!g_file_test (echo_script_path, G_FILE_TEST_EXISTS))
|
||
{
|
||
g_free (echo_script_path);
|
||
echo_script_path = g_test_build_filename (G_TEST_DIST, "echo-script" SCRIPT_EXT, NULL);
|
||
}
|
||
g_free (dirname);
|
||
|
||
g_assert (g_file_test (echo_prog_path, G_FILE_TEST_EXISTS));
|
||
g_assert (g_file_test (echo_script_path, G_FILE_TEST_EXISTS));
|
||
|
||
g_test_add_func ("/gthread/spawn-single-sync", test_spawn_sync);
|
||
g_test_add_func ("/gthread/spawn-single-async", test_spawn_async);
|
||
g_test_add_func ("/gthread/spawn-single-async-with-fds", test_spawn_async_with_fds);
|
||
g_test_add_func ("/gthread/spawn-script", test_spawn_script);
|
||
g_test_add_func ("/gthread/spawn/nonexistent", test_spawn_nonexistent);
|
||
g_test_add_func ("/gthread/spawn-posix-spawn", test_posix_spawn);
|
||
g_test_add_func ("/gthread/spawn/fd-assignment-clash", test_spawn_fd_assignment_clash);
|
||
|
||
ret = g_test_run();
|
||
|
||
g_free (echo_script_path);
|
||
g_free (echo_prog_path);
|
||
|
||
return ret;
|
||
}
|