glib/gio/tests/gsubprocess-testprog.c
Simon McVittie 76148fc6db tests: Don't assume that sh optimizes simple commands into exec
Depending on the operating system, /bin/sh might either be bash (for
example on Fedora or Arch) or dash (for example on Debian or Ubuntu)
or some other POSIX shell.

When bash is asked to run a simple command with no shell keywords or
metacharacters, like this one, it replaces itself with the program
via execve(), but dash does not have that optimization and treats it
like any other program invocation in a larger script: it will fork,
exec the program in the child, and wait for the child in the parent.

This seems like it conflicts with sleep_and_kill() assuming that it can
use the subprocess's process ID as the sleep(1) process ID. Specifically,
if it sends SIGKILL, it will go to the sh(1) process and not the sleep(1)
child, which could result in the sh(1) process being terminated and
its sleep(1) child being leaked.

To get the bash-like behaviour portably, explicitly use the exec builtin
to instruct the shell to replace itself with sleep(1), so that the
process ID previously used for the shell becomes the process ID of the
sleep process.

This appears to resolve an intermittent hang and test timeout on Debian
machines (especially slower ones), although I'm not 100% clear on the
mechanics of how it happens.

Resolves: https://gitlab.gnome.org/GNOME/glib/-/issues/3157
Signed-off-by: Simon McVittie <smcv@collabora.com>
2023-11-01 17:56:48 +00:00

374 lines
8.0 KiB
C

#include <gio/gio.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#ifdef G_OS_UNIX
#include <unistd.h>
#else
#include <io.h>
#endif
static GOptionEntry options[] = {
G_OPTION_ENTRY_NULL
};
static void
write_all (int fd,
const guint8* buf,
gsize len)
{
while (len > 0)
{
gssize bytes_written = write (fd, buf, len);
int errsv = errno;
if (bytes_written < 0)
g_error ("Failed to write to fd %d: %s",
fd, g_strerror (errsv));
buf += bytes_written;
len -= bytes_written;
}
}
static int
echo_mode (int argc,
char **argv)
{
int i;
for (i = 2; i < argc; i++)
{
write_all (1, (guint8*)argv[i], strlen (argv[i]));
write_all (1, (guint8*)"\n", 1);
}
return 0;
}
static int
echo_stdout_and_stderr_mode (int argc,
char **argv)
{
int i;
for (i = 2; i < argc; i++)
{
write_all (1, (guint8*)argv[i], strlen (argv[i]));
write_all (1, (guint8*)"\n", 1);
write_all (2, (guint8*)argv[i], strlen (argv[i]));
write_all (2, (guint8*)"\n", 1);
}
return 0;
}
static int
cat_mode (int argc,
char **argv)
{
GIOChannel *chan_stdin;
GIOChannel *chan_stdout;
GIOStatus status;
char buf[1024];
gsize bytes_read, bytes_written;
GError *local_error = NULL;
GError **error = &local_error;
chan_stdin = g_io_channel_unix_new (0);
g_io_channel_set_encoding (chan_stdin, NULL, error);
g_assert_no_error (local_error);
chan_stdout = g_io_channel_unix_new (1);
g_io_channel_set_encoding (chan_stdout, NULL, error);
g_assert_no_error (local_error);
while (TRUE)
{
do
status = g_io_channel_read_chars (chan_stdin, buf, sizeof (buf),
&bytes_read, error);
while (status == G_IO_STATUS_AGAIN);
if (status == G_IO_STATUS_EOF || status == G_IO_STATUS_ERROR)
break;
do
status = g_io_channel_write_chars (chan_stdout, buf, bytes_read,
&bytes_written, error);
while (status == G_IO_STATUS_AGAIN);
if (status == G_IO_STATUS_EOF || status == G_IO_STATUS_ERROR)
break;
}
g_io_channel_unref (chan_stdin);
g_io_channel_unref (chan_stdout);
if (local_error)
{
g_printerr ("I/O error: %s\n", local_error->message);
g_clear_error (&local_error);
return 1;
}
return 0;
}
static gint
sleep_forever_mode (int argc,
char **argv)
{
GMainLoop *loop;
loop = g_main_loop_new (NULL, TRUE);
g_main_loop_run (loop);
return 0;
}
static int
write_to_fds (int argc, char **argv)
{
int i;
for (i = 2; i < argc; i++)
{
int fd = atoi (argv[i]);
FILE *f = fdopen (fd, "w");
const char buf[] = "hello world\n";
size_t bytes_written;
g_assert (f != NULL);
bytes_written = fwrite (buf, 1, sizeof (buf), f);
g_assert (bytes_written == sizeof (buf));
if (fclose (f) == -1)
g_assert_not_reached ();
}
return 0;
}
static int
read_from_fd (int argc, char **argv)
{
int fd;
const char expected_result[] = "Yay success!";
guint8 buf[sizeof (expected_result) + 1];
gsize bytes_read;
FILE *f;
if (argc != 3)
{
g_print ("Usage: %s read-from-fd FD\n", argv[0]);
return 1;
}
fd = atoi (argv[2]);
if (fd == 0)
{
g_warning ("Argument \"%s\" does not look like a valid nonzero file descriptor", argv[2]);
return 1;
}
f = fdopen (fd, "r");
if (f == NULL)
{
g_warning ("Failed to open fd %d: %s", fd, g_strerror (errno));
return 1;
}
bytes_read = fread (buf, 1, sizeof (buf), f);
if (bytes_read != sizeof (expected_result))
{
g_warning ("Read %zu bytes, but expected %zu", bytes_read, sizeof (expected_result));
return 1;
}
if (memcmp (expected_result, buf, sizeof (expected_result)) != 0)
{
buf[sizeof (expected_result)] = '\0';
g_warning ("Expected \"%s\" but read \"%s\"", expected_result, (char *)buf);
return 1;
}
if (fclose (f) == -1)
g_assert_not_reached ();
return 0;
}
static int
env_mode (int argc, char **argv)
{
char **env;
int i;
env = g_get_environ ();
for (i = 0; env[i]; i++)
g_print ("%s\n", env[i]);
g_strfreev (env);
return 0;
}
static int
cwd_mode (int argc, char **argv)
{
char *cwd;
cwd = g_get_current_dir ();
g_print ("%s\n", cwd);
g_free (cwd);
return 0;
}
static int
printenv_mode (int argc, char **argv)
{
gint i;
for (i = 2; i < argc; i++)
{
const gchar *value = g_getenv (argv[i]);
if (value != NULL)
g_print ("%s=%s\n", argv[i], value);
}
return 0;
}
#ifdef G_OS_UNIX
static void
on_sleep_exited (GObject *object,
GAsyncResult *result,
gpointer user_data)
{
GSubprocess *subprocess = G_SUBPROCESS (object);
gboolean *done = user_data;
GError *local_error = NULL;
gboolean ret;
ret = g_subprocess_wait_finish (subprocess, result, &local_error);
g_assert_no_error (local_error);
g_assert_true (ret);
*done = TRUE;
g_main_context_wakeup (NULL);
}
static int
sleep_and_kill (int argc, char **argv)
{
GPtrArray *args = NULL;
GSubprocessLauncher *launcher = NULL;
GSubprocess *proc = NULL;
GError *local_error = NULL;
pid_t sleep_pid;
gboolean done = FALSE;
args = g_ptr_array_new_with_free_func (g_free);
/* Run sleep "forever" in a shell; this will trigger PTRACE_EVENT_EXEC */
g_ptr_array_add (args, g_strdup ("sh"));
g_ptr_array_add (args, g_strdup ("-c"));
g_ptr_array_add (args, g_strdup ("exec sleep infinity"));
g_ptr_array_add (args, NULL);
launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
proc = g_subprocess_launcher_spawnv (launcher, (const gchar **) args->pdata, &local_error);
g_assert_no_error (local_error);
g_assert_nonnull (proc);
sleep_pid = atoi (g_subprocess_get_identifier (proc));
g_subprocess_wait_async (proc, NULL, on_sleep_exited, &done);
kill (sleep_pid, SIGKILL);
while (!done)
g_main_context_iteration (NULL, TRUE);
g_assert_false (g_subprocess_get_successful (proc));
g_clear_pointer (&args, g_ptr_array_unref);
g_clear_object (&launcher);
g_clear_object (&proc);
return EXIT_SUCCESS;
}
#endif
int
main (int argc, char **argv)
{
GOptionContext *context;
GError *error = NULL;
const char *mode;
gboolean ret;
context = g_option_context_new ("MODE - Test GSubprocess stuff");
g_option_context_add_main_entries (context, options, NULL);
ret = g_option_context_parse (context, &argc, &argv, &error);
g_option_context_free (context);
if (!ret)
{
g_printerr ("%s: %s\n", argv[0], error->message);
g_error_free (error);
return 1;
}
if (argc < 2)
{
g_printerr ("MODE argument required\n");
return 1;
}
g_log_writer_default_set_use_stderr (TRUE);
mode = argv[1];
if (strcmp (mode, "noop") == 0)
return 0;
else if (strcmp (mode, "exit1") == 0)
return 1;
else if (strcmp (mode, "assert-argv0") == 0)
{
if (strcmp (argv[0], "moocow") == 0)
return 0;
g_printerr ("argv0=%s != moocow\n", argv[0]);
return 1;
}
else if (strcmp (mode, "echo") == 0)
return echo_mode (argc, argv);
else if (strcmp (mode, "echo-stdout-and-stderr") == 0)
return echo_stdout_and_stderr_mode (argc, argv);
else if (strcmp (mode, "cat") == 0)
return cat_mode (argc, argv);
else if (strcmp (mode, "sleep-forever") == 0)
return sleep_forever_mode (argc, argv);
else if (strcmp (mode, "write-to-fds") == 0)
return write_to_fds (argc, argv);
else if (strcmp (mode, "read-from-fd") == 0)
return read_from_fd (argc, argv);
else if (strcmp (mode, "env") == 0)
return env_mode (argc, argv);
else if (strcmp (mode, "cwd") == 0)
return cwd_mode (argc, argv);
else if (strcmp (mode, "printenv") == 0)
return printenv_mode (argc, argv);
#ifdef G_OS_UNIX
else if (strcmp (mode, "sleep-and-kill") == 0)
return sleep_and_kill (argc, argv);
#endif
else
{
g_printerr ("Unknown MODE %s\n", argv[1]);
return 1;
}
return TRUE;
}