diff --git a/src/adverb.c b/src/adverb.c index e8ee1c6249d0241891d75ede3c33d79591be6c62..815559af0ca7ebb956550faafdab901e3c88591a 100644 --- a/src/adverb.c +++ b/src/adverb.c @@ -43,19 +43,19 @@ #include "launcher.h" #include "utils.h" -#ifndef PR_SET_CHILD_SUBREAPER -#define PR_SET_CHILD_SUBREAPER 36 -#endif - static GPtrArray *global_locks = NULL; static GArray *global_pass_fds = NULL; static gboolean opt_create = FALSE; +static gboolean opt_exit_with_parent = FALSE; static gboolean opt_generate_locales = FALSE; static gboolean opt_subreaper = FALSE; +static double opt_terminate_idle_timeout = 0.0; +static double opt_terminate_timeout = -1.0; static gboolean opt_verbose = FALSE; static gboolean opt_version = FALSE; static gboolean opt_wait = FALSE; static gboolean opt_write = FALSE; +static GPid child_pid; typedef struct { @@ -67,7 +67,32 @@ static void child_setup_cb (gpointer user_data) { ChildSetupData *data = user_data; + sigset_t set; const int *iter; + int i; + + /* The adverb should wait for its child before it exits, but if it + * gets terminated prematurely, we want the child to terminate too. + * The child could reset this, but we assume it usually won't. + * This makes it exit even if we are killed by SIGKILL, unless it + * takes steps not to be. */ + if (opt_exit_with_parent + && prctl (PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0) != 0) + pv_async_signal_safe_error ("Failed to set up parent-death signal\n", + LAUNCH_EX_FAILED); + + /* Unblock all signals */ + sigemptyset (&set); + if (pthread_sigmask (SIG_SETMASK, &set, NULL) == -1) + pv_async_signal_safe_error ("Failed to unblock signals when starting child\n", + LAUNCH_EX_FAILED); + + /* Reset the handlers for all signals to their defaults. */ + for (i = 1; i < NSIG; i++) + { + if (i != SIGSTOP && i != SIGKILL) + signal (i, SIG_DFL); + } /* Put back the original stdout for the child process */ if (data != NULL && @@ -221,10 +246,17 @@ generate_locales (gchar **locpath_out, g_autofree gchar *pvlg = NULL; g_autofree gchar *this_path = NULL; g_autofree gchar *this_dir = NULL; + gboolean ret; + sigset_t mask; + sigset_t old_mask; g_return_val_if_fail (locpath_out == NULL || *locpath_out == NULL, FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + sigemptyset (&mask); + sigemptyset (&old_mask); + sigaddset (&mask, SIGCHLD); + temp_dir = g_dir_make_tmp ("pressure-vessel-locales-XXXXXX", error); if (temp_dir == NULL) @@ -247,17 +279,26 @@ generate_locales (gchar **locpath_out, NULL }; + /* Unblock SIGCHLD in case g_spawn_sync() needs it in some version */ + if (pthread_sigmask (SIG_UNBLOCK, &mask, &old_mask) != 0) + return glnx_throw_errno_prefix (error, "pthread_sigmask"); + /* We use LEAVE_DESCRIPTORS_OPEN to work around a deadlock in older GLib, * see flatpak_close_fds_workaround */ - if (!g_spawn_sync (NULL, /* cwd */ - (gchar **) locale_gen_argv, - NULL, /* environ */ - G_SPAWN_LEAVE_DESCRIPTORS_OPEN, - child_setup_cb, NULL, - &child_stdout, - &child_stderr, - &wait_status, - error)) + ret = g_spawn_sync (NULL, /* cwd */ + (gchar **) locale_gen_argv, + NULL, /* environ */ + G_SPAWN_LEAVE_DESCRIPTORS_OPEN, + child_setup_cb, NULL, + &child_stdout, + &child_stderr, + &wait_status, + error); + + if (pthread_sigmask (SIG_SETMASK, &old_mask, NULL) != 0) + return glnx_throw_errno_prefix (error, "pthread_sigmask"); + + if (!ret) { if (error != NULL) glnx_prefix_error (error, "Cannot run pressure-vessel-locale-gen"); @@ -298,6 +339,23 @@ generate_locales (gchar **locpath_out, return TRUE; } +/* Only do async-signal-safe things here: see signal-safety(7) */ +static void +terminate_child_cb (int signum) +{ + if (child_pid != 0) + { + /* pass it on to the child we're going to wait for */ + kill (child_pid, signum); + } + else + { + /* guess I'll just die, then */ + signal (signum, SIG_DFL); + raise (signum); + } +} + static GOptionEntry options[] = { { "fd", '\0', @@ -315,6 +373,16 @@ static GOptionEntry options[] = "Don't create subsequent nonexistent lock files [default].", NULL }, + { "exit-with-parent", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_exit_with_parent, + "Terminate child process and self with SIGTERM when parent process " + "exits.", + NULL }, + { "no-exit-with-parent", '\0', + G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_exit_with_parent, + "Don't do anything special when parent process exits [default].", + NULL }, + { "generate-locales", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_generate_locales, "Attempt to generate any missing locales.", NULL }, @@ -353,13 +421,27 @@ static GOptionEntry options[] = { "subreaper", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_subreaper, - "Do not exit until all child processes have exited.", + "Do not exit until all descendant processes have exited.", NULL }, { "no-subreaper", '\0', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_subreaper, "Only wait for a direct child process [default].", NULL }, + { "terminate-idle-timeout", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &opt_terminate_idle_timeout, + "If --terminate-timeout is used, wait this many seconds before " + "sending SIGTERM. [default: 0.0]", + "SECONDS" }, + { "terminate-timeout", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &opt_terminate_timeout, + "Send SIGTERM and SIGCONT to descendant processes that didn't " + "exit within --terminate-idle-timeout. If they don't all exit within " + "this many seconds, send SIGKILL and SIGCONT to survivors. If 0.0, " + "skip SIGTERM and use SIGKILL immediately. Implies --subreaper. " + "[Default: -1.0, meaning don't signal].", + "SECONDS" }, + { "verbose", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose, "Be more verbose.", NULL }, @@ -388,12 +470,25 @@ main (int argc, GError **error = &local_error; int ret = EX_USAGE; char **command_and_args; - GPid child_pid; int wait_status = -1; g_autofree gchar *locales_temp_dir = NULL; g_auto(GStrv) my_environ = NULL; g_autoptr(FILE) original_stdout = NULL; ChildSetupData child_setup_data = { -1, NULL }; + sigset_t mask; + struct sigaction terminate_child_action = {}; + + sigemptyset (&mask); + sigaddset (&mask, SIGCHLD); + + /* Must be called before we start any threads */ + if (pthread_sigmask (SIG_BLOCK, &mask, NULL) != 0) + { + ret = EX_UNAVAILABLE; + glnx_throw_errno_prefix (error, "pthread_sigmask"); + goto out; + } + setlocale (LC_ALL, ""); @@ -469,7 +564,19 @@ main (int argc, command_and_args = argv + 1; - if (opt_subreaper + if (opt_exit_with_parent) + { + g_debug ("Setting up to exit when parent does"); + + if (prctl (PR_SET_PDEATHSIG, SIGTERM, 0, 0, 0) != 0) + { + glnx_throw_errno_prefix (error, + "Unable to set parent death signal"); + goto out; + } + } + + if ((opt_subreaper || opt_terminate_timeout >= 0) && prctl (PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0) { glnx_throw_errno_prefix (error, @@ -500,6 +607,16 @@ main (int argc, } } + /* Respond to common termination signals by killing the child instead of + * ourselves */ + terminate_child_action.sa_handler = terminate_child_cb; + sigaction (SIGHUP, &terminate_child_action, NULL); + sigaction (SIGINT, &terminate_child_action, NULL); + sigaction (SIGQUIT, &terminate_child_action, NULL); + sigaction (SIGTERM, &terminate_child_action, NULL); + sigaction (SIGUSR1, &terminate_child_action, NULL); + sigaction (SIGUSR2, &terminate_child_action, NULL); + g_debug ("Launching child process..."); fflush (stdout); child_setup_data.original_stdout_fd = fileno (original_stdout); @@ -534,52 +651,44 @@ main (int argc, /* If the child writes to stdout and closes it, don't interfere */ g_clear_pointer (&original_stdout, fclose); - while (1) + /* Reap child processes until child_pid exits */ + if (!pv_wait_for_child_processes (child_pid, &wait_status, error)) + goto out; + + child_pid = 0; + + if (WIFEXITED (wait_status)) + { + ret = WEXITSTATUS (wait_status); + g_debug ("Command exited with status %d", ret); + } + else if (WIFSIGNALED (wait_status)) + { + ret = 128 + WTERMSIG (wait_status); + g_debug ("Command killed by signal %d", ret - 128); + } + else { - pid_t died = wait (&wait_status); + ret = EX_SOFTWARE; + g_debug ("Command terminated in an unknown way (wait status %d)", + wait_status); + } - if (died < 0) - { - if (errno == EINTR) - { - continue; - } - else if (errno == ECHILD) - { - g_debug ("No more child processes, exiting"); - break; - } - else - { - glnx_throw_errno_prefix (error, "wait"); - goto out; - } - } - else if (died == child_pid) - { - if (WIFEXITED (wait_status)) - { - ret = WEXITSTATUS (wait_status); - g_debug ("Command exited with status %d", ret); - } - else if (WIFSIGNALED (wait_status)) - { - ret = 128 + WTERMSIG (wait_status); - g_debug ("Command killed by signal %d", ret - 128); - } - else - { - ret = EX_SOFTWARE; - g_debug ("Command terminated in an unknown way (wait status %d)", - wait_status); - } - } - else - { - g_debug ("Indirect child %lld exited with wait status %d", - (long long) died, wait_status); - g_warn_if_fail (opt_subreaper); - } + if (opt_terminate_idle_timeout < 0.0) + opt_terminate_idle_timeout = 0.0; + + /* Wait for the other child processes, if any, possibly killing them */ + if (opt_terminate_timeout >= 0.0) + { + if (!pv_terminate_all_child_processes (opt_terminate_idle_timeout * G_TIME_SPAN_SECOND, + opt_terminate_timeout * G_TIME_SPAN_SECOND, + error)) + goto out; + } + else + { + if (!pv_wait_for_child_processes (0, NULL, error)) + goto out; } out: diff --git a/src/com.steampowered.PressureVessel.Launcher1.xml b/src/com.steampowered.PressureVessel.Launcher1.xml index 8313f578758a88b094741f76db55e3e49dcdc977..d064449a4a8d11325f9c0bdb3ed0d90a97cd92fd 100644 --- a/src/com.steampowered.PressureVessel.Launcher1.xml +++ b/src/com.steampowered.PressureVessel.Launcher1.xml @@ -80,8 +80,20 @@ Unknown (unsupported) flags are an error and will cause Launch() to fail. - No options are currently known. Unknown (unsupported) options - are ignored. + Unknown (unsupported) options are ignored. + The following options are supported: + + <variablelist> + <varlistentry> + <term>terminate-after b</term> + <listitem><para> + If present and true, the process groups of any remaining + launched commands will be terminated by SIGTERM immediately + after this command exits, regardless of whether it exits + successfully or unsuccessfully. + </para></listitem> + </varlistentry> + </variablelist> This method returns as soon as the process ID of the new process is known, and before the process exits. If you need to know when diff --git a/src/glib-backports.c b/src/glib-backports.c index 296d7895c0b2b44bff20e1b40d6aefcf029843fb..c7bfc1f9c8f460ac343a5e423f4c63a43cdb6984 100644 --- a/src/glib-backports.c +++ b/src/glib-backports.c @@ -29,6 +29,8 @@ #include <sys/wait.h> #include <unistd.h> +#include <glib-object.h> + /* We have no internationalization */ #define _(x) x @@ -257,39 +259,73 @@ my_g_dbus_address_escape_value (const gchar *string) #if !GLIB_CHECK_VERSION(2, 36, 0) /* - * Reimplement GUnixFDSourceFunc API in terms of GIOChannel + * Reimplement GUnixFDSourceFunc API in terms of older GLib versions */ typedef struct { - MyGUnixFDSourceFunc func; - gpointer user_data; - GDestroyNotify destroy; -} MyGUnixFDClosure; + GSource parent; + GPollFD pollfd; +} MyUnixFDSource; -static void -my_g_unix_fd_closure_free (gpointer p) +static gboolean +my_unix_fd_source_prepare (GSource *source, + int *timeout_out) { - MyGUnixFDClosure *closure = p; + if (timeout_out != NULL) + *timeout_out = -1; - if (closure->destroy != NULL) - closure->destroy (closure->user_data); + /* We don't know we're ready to be dispatched until we have polled. */ + return FALSE; +} - g_free (closure); +static gboolean +my_unix_fd_source_check (GSource *source) +{ + MyUnixFDSource *self = (MyUnixFDSource *) source; + + /* We're ready to be dispatched if an event occurred. */ + return (self->pollfd.revents != 0); } static gboolean -my_g_unix_fd_wrapper (GIOChannel *source, - GIOCondition condition, - gpointer user_data) +my_unix_fd_source_dispatch (GSource *source, + GSourceFunc callback, + gpointer user_data) +{ + MyUnixFDSource *self = (MyUnixFDSource *) source; + MyGUnixFDSourceFunc func = (MyGUnixFDSourceFunc) G_CALLBACK (callback); + + g_return_val_if_fail (func != NULL, G_SOURCE_REMOVE); + return func (self->pollfd.fd, self->pollfd.revents, user_data); +} + +static void +my_unix_fd_source_finalize (GSource *source) +{ +} + +static GSourceFuncs my_unix_fd_source_funcs = +{ + my_unix_fd_source_prepare, + my_unix_fd_source_check, + my_unix_fd_source_dispatch, + my_unix_fd_source_finalize +}; + +GSource * +my_g_unix_fd_source_new (int fd, + GIOCondition condition) { - MyGUnixFDClosure *closure = user_data; - int fd; + GSource *source = g_source_new (&my_unix_fd_source_funcs, + sizeof (MyUnixFDSource)); + MyUnixFDSource *self = (MyUnixFDSource *) source; - fd = g_io_channel_unix_get_fd (source); - g_return_val_if_fail (fd >= 0, G_SOURCE_REMOVE); + self->pollfd.fd = fd; + self->pollfd.events = condition; - return closure->func (fd, condition, closure->user_data); + g_source_add_poll (source, &self->pollfd); + return source; } guint @@ -300,33 +336,18 @@ my_g_unix_fd_add_full (int priority, gpointer user_data, GDestroyNotify destroy) { - GIOChannel *channel; - MyGUnixFDClosure *closure; + GSource *source; guint ret; g_return_val_if_fail (fd >= 0, 0); g_return_val_if_fail (func != NULL, 0); - closure = g_new0 (MyGUnixFDClosure, 1); - closure->func = func; - closure->user_data = user_data; - closure->destroy = destroy; - - /* POLLERR, POLLHUP and POLLNVAL are implicitly returned by poll() - * and hence by Unix fd watches, but GIOChannel applies its own - * filtering */ - condition |= (G_IO_ERR|G_IO_HUP|G_IO_NVAL); - - channel = g_io_channel_unix_new (fd); - /* Disable text recoding, treat it as a bytestream */ - g_io_channel_set_encoding (channel, NULL, NULL); - ret = g_io_add_watch_full (channel, - priority, - condition, - my_g_unix_fd_wrapper, - closure, - my_g_unix_fd_closure_free); - g_io_channel_unref (channel); + source = my_g_unix_fd_source_new (fd, condition); + g_source_set_callback (source, (GSourceFunc) G_CALLBACK (func), + user_data, destroy); + g_source_set_priority (source, priority); + ret = g_source_attach (source, NULL); + g_source_unref (source); return ret; } #endif diff --git a/src/glib-backports.h b/src/glib-backports.h index 035c026b4f2268928692fb6be9705b893a775c77..0e047412531ef71e032762e4e044c300657aeb10 100644 --- a/src/glib-backports.h +++ b/src/glib-backports.h @@ -78,6 +78,8 @@ gchar *my_g_dbus_address_escape_value (const gchar *string); #endif #if !GLIB_CHECK_VERSION(2, 36, 0) +#define g_unix_fd_source_new(fd, cond) \ + my_g_unix_fd_source_new (fd, cond) #define g_unix_fd_add(fd, cond, cb, ud) \ my_g_unix_fd_add_full (G_PRIORITY_DEFAULT, fd, cond, cb, ud, NULL) #define g_unix_fd_add_full(prio, fd, cond, cb, ud, destroy) \ @@ -89,4 +91,6 @@ guint my_g_unix_fd_add_full (int priority, MyGUnixFDSourceFunc func, gpointer user_data, GDestroyNotify destroy); +GSource *my_g_unix_fd_source_new (int fd, + GIOCondition condition); #endif diff --git a/src/launch.c b/src/launch.c index 9407d1fd10db774a9a4e66adac44f5819d1c9596..9a27c4c99930b4386dc0242e85b2054b905bced0 100644 --- a/src/launch.c +++ b/src/launch.c @@ -67,23 +67,23 @@ process_exited_cb (G_GNUC_UNUSED GDBusConnection *connection, { GMainLoop *loop = user_data; guint32 client_pid = 0; - guint32 exit_status = 0; + guint32 wait_status = 0; if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)"))) return; - g_variant_get (parameters, "(uu)", &client_pid, &exit_status); - g_debug ("child exited %d: %d", client_pid, exit_status); + g_variant_get (parameters, "(uu)", &client_pid, &wait_status); + g_debug ("child %d exited: wait status %d", client_pid, wait_status); if (child_pid == client_pid) { int exit_code = 0; - if (WIFEXITED (exit_status)) + if (WIFEXITED (wait_status)) { - exit_code = WEXITSTATUS (exit_status); + exit_code = WEXITSTATUS (wait_status); } - else if (WIFSIGNALED (exit_status)) + else if (WIFSIGNALED (wait_status)) { /* Smush the signal into an unsigned byte, as the shell does. This is * not quite right from the perspective of whatever ran flatpak-spawn @@ -91,7 +91,7 @@ process_exited_cb (G_GNUC_UNUSED GDBusConnection *connection, * alternative is to disconnect all signal() handlers then send this * signal to ourselves and hope it kills us. */ - exit_code = 128 + WTERMSIG (exit_status); + exit_code = 128 + WTERMSIG (wait_status); } else { @@ -100,7 +100,7 @@ process_exited_cb (G_GNUC_UNUSED GDBusConnection *connection, * of WIFEXITED() or WIFSIGNALED() will be true. */ g_warning ("exit status %d is neither WIFEXITED() nor WIFSIGNALED()", - exit_status); + wait_status); exit_code = LAUNCH_EX_CANNOT_REPORT; } @@ -205,7 +205,7 @@ forward_signal_handler (int sfd, } static guint -forward_signals (void) +forward_signals (GError **error) { static int forward[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGCONT, SIGTSTP, SIGUSR1, SIGUSR2 @@ -223,7 +223,7 @@ forward_signals (void) if (sfd < 0) { - g_warning ("Unable to watch signals: %s", g_strerror (errno)); + glnx_throw_errno_prefix (error, "Unable to watch signals"); return 0; } @@ -237,7 +237,11 @@ forward_signals (void) * of blocking them, they would no longer be pending by the time the * main loop wakes up and reads from the signalfd. */ - pthread_sigmask (SIG_BLOCK, &mask, NULL); + if (pthread_sigmask (SIG_BLOCK, &mask, NULL) != 0) + { + glnx_throw_errno_prefix (error, "Unable to block signals"); + return 0; + } return g_unix_fd_add (sfd, G_IO_IN, forward_signal_handler, NULL); } @@ -422,7 +426,8 @@ static const GOptionEntry options[] = "ABSPATH|@ABSTRACT" }, { "terminate", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_terminate, - "Terminate the Launcher server.", NULL }, + "Terminate the Launcher server after the COMMAND (if any) has run.", + NULL }, { "unset-env", '\0', G_OPTION_FLAG_FILENAME, G_OPTION_ARG_CALLBACK, opt_env_cb, "Unset environment variable, like env -u.", "VAR" }, @@ -454,7 +459,6 @@ main (int argc, GError **error = &local_error; g_autoptr(GPtrArray) env_prefix = NULL; char **command_and_args; - guint forward_signals_id = 0; g_autoptr(FILE) original_stdout = NULL; g_autoptr(GDBusConnection) session_bus = NULL; g_autoptr(GDBusConnection) peer_connection = NULL; @@ -474,7 +478,7 @@ main (int argc, setlocale (LC_ALL, ""); - g_set_prgname ("pressure-vessel-launcher"); + g_set_prgname ("pressure-vessel-launch"); g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE, @@ -516,14 +520,14 @@ main (int argc, goto out; } - if (!opt_terminate) + if (argc > 1) { /* We have to block the signals we want to forward before we start any * other thread, and in particular the GDBus worker thread, because * the signal mask is per-thread. We need all threads to have the same * mask, otherwise a thread that doesn't have the mask will receive * process-directed signals, causing the whole process to exit. */ - signal_source = forward_signals (); + signal_source = forward_signals (error); if (signal_source == 0) { @@ -546,23 +550,22 @@ main (int argc, argc--; } - if (opt_terminate) + if (argc < 2) { - if (argc != 1) + if (!opt_terminate) { - glnx_throw (error, "--terminate cannot be combined with a COMMAND"); + glnx_throw (error, "Usage: %s [OPTIONS] COMMAND [ARG...]", + g_get_prgname ()); goto out; } + + command_and_args = NULL; } - else if (argc < 2) + else { - glnx_throw (error, "Usage: %s [OPTIONS] COMMAND [ARG...]", - g_get_prgname ()); - goto out; + command_and_args = argv + 1; } - command_and_args = argv + 1; - launch_exit_status = LAUNCH_EX_FAILED; loop = g_main_loop_new (NULL, FALSE); @@ -653,8 +656,10 @@ main (int argc, g_assert (bus_or_peer_connection != NULL); - if (opt_terminate) + if (command_and_args == NULL) { + g_assert (opt_terminate); /* already checked */ + reply = g_dbus_connection_call_sync (bus_or_peer_connection, service_bus_name, service_obj_path, @@ -672,6 +677,7 @@ main (int argc, goto out; } + g_assert (command_and_args != NULL); g_dbus_connection_signal_subscribe (bus_or_peer_connection, service_bus_name, /* NULL if p2p */ service_iface, @@ -747,9 +753,12 @@ main (int argc, if (opt_clear_env) spawn_flags |= PV_LAUNCH_FLAGS_CLEAR_ENV; - /* There are no options yet, so just leave this empty */ g_variant_builder_init (&options_builder, G_VARIANT_TYPE ("a{sv}")); + if (opt_terminate) + g_variant_builder_add (&options_builder, "{s@v}", "terminate-after", + g_variant_new_variant (g_variant_new_boolean (TRUE))); + if (!opt_directory) { opt_directory = g_get_current_dir (); @@ -853,8 +862,8 @@ out: if (local_error != NULL) g_warning ("%s", local_error->message); - if (forward_signals_id > 0) - g_source_remove (forward_signals_id); + if (signal_source > 0) + g_source_remove (signal_source); g_strfreev (forward_fds); g_free (opt_directory); diff --git a/src/launcher.c b/src/launcher.c index baf0c663773fa035f13b7dae179859322ee208ac..7e840e1936f850ffb3b5b0d2d2887ee90f49b6cc 100644 --- a/src/launcher.c +++ b/src/launcher.c @@ -99,6 +99,7 @@ typedef struct GPid pid; gchar *client; guint child_watch; + gboolean terminate_after; } PidData; static void @@ -134,8 +135,9 @@ child_watch_died (GPid pid, { PidData *pid_data = user_data; g_autoptr(GVariant) signal_variant = NULL; + gboolean terminate_after = pid_data->terminate_after; - g_debug ("Client Pid %d died", pid_data->pid); + g_debug ("Child %d died: wait status %d", pid_data->pid, status); signal_variant = g_variant_ref_sink (g_variant_new ("(uu)", pid, status)); g_dbus_connection_emit_signal (pid_data->connection, @@ -148,6 +150,13 @@ child_watch_died (GPid pid, /* This frees the pid_data, so be careful */ g_hash_table_remove (client_pid_data_hash, GUINT_TO_POINTER (pid_data->pid)); + + if (terminate_after) + { + g_debug ("Main pid %d died, terminating...", pid); + terminate_children (SIGTERM); + unref_skeleton_in_timeout (); + } } typedef struct @@ -241,6 +250,7 @@ handle_launch (PvLauncher1 *object, g_autofree FdMapEntry *fd_map = NULL; g_auto(GStrv) env = NULL; gint32 max_fd; + gboolean terminate_after = FALSE; if (fd_list != NULL) fds = g_unix_fd_list_peek_fds (fd_list, &fds_len); @@ -263,6 +273,8 @@ handle_launch (PvLauncher1 *object, return TRUE; } + g_variant_lookup (arg_options, "terminate-after", "b", &terminate_after); + g_debug ("Running spawn command %s", arg_argv[0]); n_fds = 0; @@ -369,6 +381,7 @@ handle_launch (PvLauncher1 *object, pid_data->connection = g_object_ref (g_dbus_method_invocation_get_connection (invocation)); pid_data->pid = pid; pid_data->client = g_strdup (g_dbus_method_invocation_get_sender (invocation)); + pid_data->terminate_after = terminate_after; pid_data->child_watch = g_child_watch_add_full (G_PRIORITY_DEFAULT, pid, child_watch_died, diff --git a/src/meson.build b/src/meson.build index 6bf5901d0a668a71afc69d9e17c3e250440d42bf..0ac2f0a4f7d7db605188bb57fb6474967f4880c3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -58,6 +58,7 @@ pressure_vessel_utils = static_library( 'utils.h', ], dependencies : [ + threads, gio_unix, libelf, libglnx.get_variable('libglnx_dep'), @@ -83,6 +84,7 @@ executable( 'adverb.c', ], dependencies : [ + threads, gio_unix, libglnx.get_variable('libglnx_dep'), ], diff --git a/src/runtime.c b/src/runtime.c index 183d323e36c733889231007d4d7766b5c765a992..c7dd569f74ef3ac08d9d19c6415fb93976c4694b 100644 --- a/src/runtime.c +++ b/src/runtime.c @@ -888,26 +888,19 @@ pv_runtime_new (const char *source_files, * and rely on bubblewrap's init process for this, but we currently * can't do that without breaking gameoverlayrender.so's assumptions, * and we want -adverb for its locale functionality anyway. */ -void -pv_runtime_append_adverbs (PvRuntime *self, - FlatpakBwrap *bwrap) +gchar * +pv_runtime_get_adverb (PvRuntime *self, + FlatpakBwrap *bwrap) { - g_return_if_fail (PV_IS_RUNTIME (self)); - g_return_if_fail (!pv_bwrap_was_finished (bwrap)); + g_return_val_if_fail (PV_IS_RUNTIME (self), NULL); /* This will be true if pv_runtime_bind() was successfully called. */ - g_return_if_fail (self->adverb_in_container != NULL); - - flatpak_bwrap_add_args (bwrap, - self->adverb_in_container, - "--subreaper", - NULL); + g_return_val_if_fail (self->adverb_in_container != NULL, NULL); + g_return_val_if_fail (bwrap != NULL, NULL); + g_return_val_if_fail (!pv_bwrap_was_finished (bwrap), NULL); if (self->flags & PV_RUNTIME_FLAGS_GENERATE_LOCALES) flatpak_bwrap_add_args (bwrap, "--generate-locales", NULL); - if (self->flags & PV_RUNTIME_FLAGS_VERBOSE) - flatpak_bwrap_add_args (bwrap, "--verbose", NULL); - if (pv_bwrap_lock_is_ofd (self->runtime_lock)) { int fd = pv_bwrap_lock_steal_fd (self->runtime_lock); @@ -942,9 +935,7 @@ pv_runtime_append_adverbs (PvRuntime *self, NULL); } - flatpak_bwrap_add_args (bwrap, - "--", - NULL); + return g_strdup (self->adverb_in_container); } /* @@ -2974,6 +2965,12 @@ pv_runtime_bind (PvRuntime *self, if (!pv_cheap_tree_copy (pressure_vessel_prefix, dest, error)) return FALSE; + flatpak_bwrap_add_args (bwrap, + "--symlink", + "/usr/lib/pressure-vessel/from-host", + "/run/pressure-vessel/pv-from-host", + NULL); + self->adverb_in_container = "/usr/lib/pressure-vessel/from-host/bin/pressure-vessel-adverb"; } else diff --git a/src/runtime.h b/src/runtime.h index 93ec89c5d1476fa720c1e10d9184ee8ff4d9b5bf..c3d9cdbf0377d975906dd9ffb445aac6e3f8031f 100644 --- a/src/runtime.h +++ b/src/runtime.h @@ -72,8 +72,8 @@ PvRuntime *pv_runtime_new (const char *source_files, PvRuntimeFlags flags, GError **error); -void pv_runtime_append_adverbs (PvRuntime *self, - FlatpakBwrap *bwrap); +gchar *pv_runtime_get_adverb (PvRuntime *self, + FlatpakBwrap *adverb_args); gboolean pv_runtime_bind (PvRuntime *self, FlatpakBwrap *bwrap, GError **error); diff --git a/src/utils.c b/src/utils.c index 2fc26db88dbe2717f1d7eb7cf03f2b0630c6e9f4..95f3bf80358d4362ecbbe29a5a88230f15409f3d 100644 --- a/src/utils.c +++ b/src/utils.c @@ -23,8 +23,13 @@ #include "utils.h" #include <ftw.h> +#include <sys/prctl.h> +#include <sys/signalfd.h> +#include <sys/types.h> +#include <sys/wait.h> #include <glib.h> +#include <glib-unix.h> #include <glib/gstdio.h> #include <gio/gio.h> @@ -575,3 +580,486 @@ pv_get_random_uuid (GError **error) return g_steal_pointer (&contents); } + +/** + * pv_wait_for_child_processes: + * @main_process: process for which we will report wait-status; + * zero or negative if there is no main process + * @wait_status_out: (out): Used to store the wait status of `@main_process`, + * as if from `wait()`, on success + * @error: Used to raise an error on failure + * + * Wait for child processes of this process to exit, until the @main_process + * has exited. If there is no main process, wait until there are no child + * processes at all. + * + * If the process is a subreaper (`PR_SET_CHILD_SUBREAPER`), + * indirect child processes whose parents have exited will be reparented + * to it, so this will have the effect of waiting for all descendants. + * + * If @main_process is positive, return when @main_process has exited. + * Child processes that exited before @main_process will also have been + * "reaped", but child processes that exit after @main_process will not + * (use `pv_wait_for_child_processes (0, &error)` to resume waiting). + * + * If @main_process is zero or negative, wait for all child processes + * to exit. + * + * This function cannot be called in a process that is using + * g_child_watch_source_new() or similar functions, because it waits + * for all child processes regardless of their process IDs, and that is + * incompatible with waiting for individual child processes. + * + * Returns: %TRUE when @main_process has exited, or if @main_process + * is zero or negative and all child processes have exited + */ +gboolean +pv_wait_for_child_processes (pid_t main_process, + int *wait_status_out, + GError **error) +{ + if (wait_status_out != NULL) + *wait_status_out = -1; + + while (1) + { + int wait_status = -1; + pid_t died = wait (&wait_status); + + if (died < 0) + { + if (errno == EINTR) + { + continue; + } + else if (errno == ECHILD) + { + g_debug ("No more child processes"); + break; + } + else + { + return glnx_throw_errno_prefix (error, "wait"); + } + } + + g_debug ("Child %lld exited with wait status %d", + (long long) died, wait_status); + + if (died == main_process) + { + if (wait_status_out != NULL) + *wait_status_out = wait_status; + + return TRUE; + } + } + + if (main_process > 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Process %lld was not seen to exit", + (long long) main_process); + return FALSE; + } + + return TRUE; +} + +typedef struct +{ + GError *error; + GMainContext *context; + GSource *wait_source; + GSource *kill_source; + GSource *sigchld_source; + gchar *children_file; + /* GINT_TO_POINTER (pid) => arbitrary */ + GHashTable *sent_sigterm; + /* GINT_TO_POINTER (pid) => arbitrary */ + GHashTable *sent_sigkill; + /* Scratch space to build temporary strings */ + GString *buffer; + /* 0, SIGTERM or SIGKILL */ + int sending_signal; + /* Nonzero if wait_source has been attached to context */ + guint wait_source_id; + guint kill_source_id; + guint sigchld_source_id; + /* TRUE if we reach a point where we have no more child processes. */ + gboolean finished; +} TerminationData; + +/* + * Free everything in @data. + */ +static void +termination_data_clear (TerminationData *data) +{ + if (data->wait_source_id != 0) + { + g_source_destroy (data->wait_source); + data->wait_source_id = 0; + } + + if (data->kill_source_id != 0) + { + g_source_destroy (data->kill_source); + data->kill_source_id = 0; + } + + if (data->sigchld_source_id != 0) + { + g_source_destroy (data->sigchld_source); + data->sigchld_source_id = 0; + } + + if (data->wait_source != NULL) + g_source_unref (g_steal_pointer (&data->wait_source)); + + if (data->kill_source != NULL) + g_source_unref (g_steal_pointer (&data->kill_source)); + + if (data->sigchld_source != NULL) + g_source_unref (g_steal_pointer (&data->sigchld_source)); + + g_clear_pointer (&data->children_file, g_free); + g_clear_pointer (&data->context, g_main_context_unref); + g_clear_error (&data->error); + g_clear_pointer (&data->sent_sigterm, g_hash_table_unref); + g_clear_pointer (&data->sent_sigkill, g_hash_table_unref); + + if (data->buffer != NULL) + g_string_free (g_steal_pointer (&data->buffer), TRUE); +} + +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (TerminationData, termination_data_clear) + +/* + * Do whatever the next step for pv_terminate_all_child_processes() is. + * + * First, reap child processes that already exited, without blocking. + * + * Then, act according to the phase we are in: + * - before wait_period: do nothing + * - after wait_period but before grace_period: send SIGTERM + * - after wait_period and grace_period: send SIGKILL + */ +static void +termination_data_refresh (TerminationData *data) +{ + g_autofree gchar *contents = NULL; + gboolean has_child = FALSE; + const char *p; + char *endptr; + + if (data->error != NULL) + return; + + g_debug ("Checking for child processes"); + + while (1) + { + int wait_status = -1; + pid_t died = waitpid (-1, &wait_status, WNOHANG); + + if (died < 0) + { + if (errno == EINTR) + { + continue; + } + else if (errno == ECHILD) + { + /* No child processes at all. We'll double-check this + * a bit later. */ + break; + } + else + { + glnx_throw_errno_prefix (&data->error, "wait"); + return; + } + } + else if (died == 0) + { + /* No more child processes have exited, but at least one is + * still running. */ + break; + } + + /* This process has gone away, so remove any record that we have + * sent it signals. If the pid is reused, we'll want to send + * the same signals again. */ + g_debug ("Process %d exited", died); + g_hash_table_remove (data->sent_sigkill, GINT_TO_POINTER (died)); + g_hash_table_remove (data->sent_sigterm, GINT_TO_POINTER (died)); + } + + /* See whether we have any remaining children. These could be direct + * child processes, or they could be children we adopted because + * their parent was one of our descendants and has exited, leaving the + * child to be reparented to us (their (great)*grandparent) because we + * are a subreaper. */ + if (!g_file_get_contents (data->children_file, &contents, NULL, &data->error)) + return; + + g_debug ("Child tasks: %s", contents); + + for (p = contents; + p != NULL && *p != '\0'; + p = endptr) + { + guint64 maybe_child; + int child; + GHashTable *already; + + while (*p != '\0' && g_ascii_isspace (*p)) + p++; + + if (*p == '\0') + break; + + maybe_child = g_ascii_strtoull (p, &endptr, 10); + + if (*endptr != '\0' && !g_ascii_isspace (*endptr)) + { + glnx_throw (&data->error, "Non-numeric string found in %s: %s", + data->children_file, p); + return; + } + + if (maybe_child > G_MAXINT) + { + glnx_throw (&data->error, "Out-of-range number found in %s: %s", + data->children_file, p); + return; + } + + child = (int) maybe_child; + g_string_printf (data->buffer, "/proc/%d", child); + + /* If the task is just a thread, it won't have a /proc/%d directory + * in its own right. We don't kill threads, only processes. */ + if (!g_file_test (data->buffer->str, G_FILE_TEST_IS_DIR)) + { + g_debug ("Task %d is a thread, not a process", child); + continue; + } + + has_child = TRUE; + + if (data->sending_signal == 0) + break; + else if (data->sending_signal == SIGKILL) + already = data->sent_sigkill; + else + already = data->sent_sigterm; + + if (!g_hash_table_contains (already, GINT_TO_POINTER (child))) + { + g_debug ("Sending signal %d to process %d", + data->sending_signal, child); + g_hash_table_add (already, GINT_TO_POINTER (child)); + + if (kill (child, data->sending_signal) < 0) + g_warning ("Unable to send signal %d to process %d: %s", + data->sending_signal, child, g_strerror (errno)); + + /* In case the child is stopped, wake it up to receive the signal */ + if (kill (child, SIGCONT) < 0) + g_warning ("Unable to send SIGCONT to process %d: %s", + child, g_strerror (errno)); + + /* When the child terminates, we will get SIGCHLD and come + * back to here. */ + } + } + + if (!has_child) + data->finished = TRUE; +} + +/* + * Move from wait period to grace period: start sending SIGTERM to + * child processes. + */ +static gboolean +start_sending_sigterm (gpointer user_data) +{ + TerminationData *data = user_data; + + g_debug ("Wait period finished, starting to send SIGTERM..."); + + if (data->sending_signal == 0) + data->sending_signal = SIGTERM; + + termination_data_refresh (data); + + data->wait_source_id = 0; + return G_SOURCE_REMOVE; +} + +/* + * End of grace period: start sending SIGKILL to child processes. + */ +static gboolean +start_sending_sigkill (gpointer user_data) +{ + TerminationData *data = user_data; + + g_debug ("Grace period finished, starting to send SIGKILL..."); + + data->sending_signal = SIGKILL; + termination_data_refresh (data); + + data->kill_source_id = 0; + return G_SOURCE_REMOVE; +} + +/* + * Called when at least one child process has exited, resulting in + * SIGCHLD to this process. + */ +static gboolean +sigchld_cb (int sfd, + G_GNUC_UNUSED GIOCondition condition, + gpointer user_data) +{ + TerminationData *data = user_data; + struct signalfd_siginfo info; + ssize_t size; + + size = read (sfd, &info, sizeof (info)); + + if (size < 0) + { + if (errno != EINTR && errno != EAGAIN) + g_warning ("Unable to read struct signalfd_siginfo: %s", + g_strerror (errno)); + } + else if (size != sizeof (info)) + { + g_warning ("Expected struct signalfd_siginfo of size %" + G_GSIZE_FORMAT ", got %" G_GSSIZE_FORMAT, + sizeof (info), size); + } + + g_debug ("One or more child processes exited"); + termination_data_refresh (data); + return G_SOURCE_CONTINUE; +} + +/** + * pv_terminate_all_child_processes: + * @wait_period: If greater than 0, wait this many microseconds before + * sending `SIGTERM` + * @grace_period: If greater than 0, after @wait_period plus this many + * microseconds, use `SIGKILL` instead of `SIGTERM`. If 0, proceed + * directly to sending `SIGKILL`. + * @error: Used to raise an error on failure + * + * Make sure all child processes are terminated. + * + * If a child process catches `SIGTERM` but does not exit promptly and + * does not pass the signal on to its descendants, note that its + * descendant processes are not guaranteed to be terminated gracefully + * with `SIGTERM`; they might only receive `SIGKILL`. + * + * Return when all child processes have exited or when an error has + * occurred. + * + * This function cannot be called in a process that is using + * g_child_watch_source_new() or similar functions. + * + * The process must be a subreaper, and must have `SIGCHLD` blocked. + * + * Returns: %TRUE on success. + */ +gboolean +pv_terminate_all_child_processes (GTimeSpan wait_period, + GTimeSpan grace_period, + GError **error) +{ + g_auto(TerminationData) data = {}; + sigset_t mask; + int is_subreaper = -1; + glnx_autofd int sfd = -1; + + if (prctl (PR_GET_CHILD_SUBREAPER, (unsigned long) &is_subreaper, 0, 0, 0) != 0) + return glnx_throw_errno_prefix (error, "prctl PR_GET_CHILD_SUBREAPER"); + + if (is_subreaper != 1) + return glnx_throw (error, "Process is not a subreaper"); + + sigemptyset (&mask); + + if (pthread_sigmask (SIG_BLOCK, NULL, &mask) != 0) + return glnx_throw_errno_prefix (error, "pthread_sigmask"); + + if (!sigismember (&mask, SIGCHLD)) + return glnx_throw (error, "Process has not blocked SIGCHLD"); + + data.children_file = g_strdup_printf ("/proc/%d/task/%d/children", + getpid (), getpid ()); + data.context = g_main_context_new (); + data.sent_sigterm = g_hash_table_new (NULL, NULL); + data.sent_sigkill = g_hash_table_new (NULL, NULL); + data.buffer = g_string_new ("/proc/2345678901"); + + if (wait_period > 0 && grace_period > 0) + { + data.wait_source = g_timeout_source_new (wait_period / G_TIME_SPAN_MILLISECOND); + + g_source_set_callback (data.wait_source, start_sending_sigterm, + &data, NULL); + data.wait_source_id = g_source_attach (data.wait_source, data.context); + } + else if (grace_period > 0) + { + start_sending_sigterm (&data); + } + + if (wait_period + grace_period > 0) + { + data.kill_source = g_timeout_source_new ((wait_period + grace_period) / G_TIME_SPAN_MILLISECOND); + + g_source_set_callback (data.kill_source, start_sending_sigkill, + &data, NULL); + data.kill_source_id = g_source_attach (data.kill_source, data.context); + } + else + { + start_sending_sigkill (&data); + } + + sigemptyset (&mask); + sigaddset (&mask, SIGCHLD); + + sfd = signalfd (-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); + + if (sfd < 0) + return glnx_throw_errno_prefix (error, "signalfd"); + + data.sigchld_source = g_unix_fd_source_new (sfd, G_IO_IN); + g_source_set_callback (data.sigchld_source, + (GSourceFunc) G_CALLBACK (sigchld_cb), &data, NULL); + data.sigchld_source_id = g_source_attach (data.sigchld_source, data.context); + + termination_data_refresh (&data); + + while (data.error == NULL + && !data.finished + && (data.wait_source_id != 0 + || data.kill_source_id != 0 + || data.sigchld_source_id != 0)) + g_main_context_iteration (data.context, TRUE); + + if (data.error != NULL) + { + g_propagate_error (error, g_steal_pointer (&data.error)); + return FALSE; + } + + return TRUE; +} diff --git a/src/utils.h b/src/utils.h index d4215d0f6688dac03a78a2caa80840764ccc69b7..3d89b7cedce569deca2ad6d61b2053e38a08e7d0 100644 --- a/src/utils.h +++ b/src/utils.h @@ -23,9 +23,18 @@ #pragma once #include <stdio.h> +#include <sys/types.h> #include <glib.h> +#ifndef PR_GET_CHILD_SUBREAPER +#define PR_GET_CHILD_SUBREAPER 37 +#endif + +#ifndef PR_SET_CHILD_SUBREAPER +#define PR_SET_CHILD_SUBREAPER 36 +#endif + void pv_avoid_gvfs (void); int pv_envp_cmp (const void *p1, @@ -60,3 +69,11 @@ void pv_async_signal_safe_error (const char *message, int exit_status) G_GNUC_NORETURN; gchar *pv_get_random_uuid (GError **error); + +gboolean pv_wait_for_child_processes (pid_t main_process, + int *wait_status_out, + GError **error); + +gboolean pv_terminate_all_child_processes (GTimeSpan wait_period, + GTimeSpan grace_period, + GError **error); diff --git a/src/wrap.c b/src/wrap.c index 1812256b27426ba1ae36a3635b1ad99aff2def1a..d5a201dd9835a697379fcfae4ea65e3fdd5abe31 100644 --- a/src/wrap.c +++ b/src/wrap.c @@ -374,6 +374,8 @@ static char *opt_runtime_base = NULL; static char *opt_runtime = NULL; static Tristate opt_share_home = TRISTATE_MAYBE; static gboolean opt_share_pid = TRUE; +static double opt_terminate_idle_timeout = 0.0; +static double opt_terminate_timeout = -1.0; static gboolean opt_verbose = FALSE; static gboolean opt_version = FALSE; static gboolean opt_version_only = FALSE; @@ -673,6 +675,19 @@ static GOptionEntry options[] = { "xterm", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_terminal_cb, "Equivalent to --terminal=xterm", NULL }, + { "terminate-idle-timeout", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &opt_terminate_idle_timeout, + "If --terminate-timeout is used, wait this many seconds before " + "sending SIGTERM. [default: 0.0]" + "SECONDS" }, + { "terminate-timeout", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_DOUBLE, &opt_terminate_timeout, + "Send SIGTERM and SIGCONT to descendant processes that didn't " + "exit within --terminate-idle-timeout. If they don't all exit within " + "this many seconds, send SIGKILL and SIGCONT to survivors. If 0.0, " + "skip SIGTERM and use SIGKILL immediately. Implies --subreaper. " + "[Default: -1.0, meaning don't signal].", + "SECONDS" }, { "verbose", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose, "Be more verbose.", NULL }, @@ -741,6 +756,8 @@ main (int argc, g_auto(GStrv) original_argv = NULL; int original_argc = argc; g_autoptr(FlatpakBwrap) bwrap = NULL; + g_autoptr(FlatpakBwrap) adverb_args = NULL; + g_autofree gchar *adverb_in_container = NULL; g_autoptr(FlatpakBwrap) wrapped_command = NULL; g_autofree gchar *bwrap_executable = NULL; g_autoptr(GString) adjusted_ld_preload = g_string_new (""); @@ -1430,8 +1447,41 @@ main (int argc, if (!flatpak_bwrap_bundle_args (bwrap, 1, -1, FALSE, error)) goto out; + adverb_args = flatpak_bwrap_new (NULL); + if (runtime != NULL) - pv_runtime_append_adverbs (runtime, bwrap); + adverb_in_container = pv_runtime_get_adverb (runtime, adverb_args); + + if (opt_terminate_timeout >= 0.0) + { + if (opt_terminate_idle_timeout > 0.0) + flatpak_bwrap_add_arg_printf (adverb_args, + "--terminate-idle-timeout=%f", + opt_terminate_idle_timeout); + + flatpak_bwrap_add_arg_printf (adverb_args, + "--terminate-timeout=%f", + opt_terminate_timeout); + } + + /* If not using a runtime, the adverb in the container has the + * same path as outside */ + if (adverb_in_container == NULL) + adverb_in_container = g_build_filename (tools_dir, + "pressure-vessel-adverb", + NULL); + + flatpak_bwrap_add_args (bwrap, + adverb_in_container, + "--exit-with-parent", + "--subreaper", + NULL); + + if (opt_verbose) + flatpak_bwrap_add_arg (bwrap, "--verbose"); + + flatpak_bwrap_append_bwrap (bwrap, adverb_args); + flatpak_bwrap_add_arg (bwrap, "--"); g_debug ("Adding wrapped command..."); flatpak_bwrap_append_args (bwrap, wrapped_command->argv); diff --git a/tests/adverb.py b/tests/adverb.py index 09d9d6a06d37c4d4249eea66d3035c983a1d4f76..e1ee267a13bda3093d48599d755b62ad4896b665 100755 --- a/tests/adverb.py +++ b/tests/adverb.py @@ -8,7 +8,6 @@ import os import signal import subprocess import sys -import tempfile try: diff --git a/tests/launcher.py b/tests/launcher.py index be470e2578928a658fe97b792b5dcd5345b8e424..eca7f80090fd8555d73fc9c888c43460feaf4939 100755 --- a/tests/launcher.py +++ b/tests/launcher.py @@ -57,8 +57,11 @@ class TestLauncher(BaseTest): def test_socket_directory(self) -> None: with tempfile.TemporaryDirectory(prefix='test-') as temp: + need_terminate = True printf_symlink = os.path.join(temp, 'printf=symlink') - os.symlink(shutil.which('printf'), printf_symlink) + printf = shutil.which('printf') + assert printf is not None + os.symlink(printf, printf_symlink) proc = subprocess.Popen( [ @@ -262,8 +265,24 @@ class TestLauncher(BaseTest): launch.wait(), (128 + signal.SIGINT, -signal.SIGINT), ) + + completed = run_subprocess( + self.launch + [ + '--socket', socket, + '--terminate', + '--', + 'sh', '-euc', 'echo Goodbye', + ], + check=True, + stdout=subprocess.PIPE, + stderr=2, + ) + need_terminate = False + self.assertEqual(completed.stdout, b'Goodbye\n') finally: - proc.terminate() + if need_terminate: + proc.terminate() + proc.wait() self.assertEqual(proc.returncode, 0) @@ -561,10 +580,12 @@ class TestLauncher(BaseTest): stderr=2, universal_newlines=True, ) + stdin = proc.stdin + assert stdin is not None os.close(read_end) if use_stdin: - proc.stdin.close() + stdin.close() else: os.close(write_end) diff --git a/tests/meson.build b/tests/meson.build index 146d9b851c57f5a2af1dc013d2764549141b4adb..34d42008754fe57f8e93dca0fe7a19961288a657 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -47,6 +47,7 @@ tests = [ compiled_tests = [ 'bwrap-lock', 'resolve-in-sysroot', + 'wait-for-child-processes', 'utils', ] diff --git a/tests/utils.py b/tests/utils.py index 99c3cc4cca57995dad933fe2b83dd6c3dc9a740c..988fcb37f07d4d08737b067a2af30e9d7cc1dc90 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -6,7 +6,6 @@ import os import subprocess import sys -import tempfile try: diff --git a/tests/wait-for-child-processes.c b/tests/wait-for-child-processes.c new file mode 100644 index 0000000000000000000000000000000000000000..f00c88c241fe267d1f267d805bbc18c66771b64d --- /dev/null +++ b/tests/wait-for-child-processes.c @@ -0,0 +1,349 @@ +/* + * Copyright © 2019-2020 Collabora Ltd. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include <stdlib.h> +#include <sys/prctl.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <glib.h> +#include <glib/gstdio.h> + +#include "glib-backports.h" +#include "libglnx/libglnx.h" + +#include "test-utils.h" +#include "utils.h" + +typedef struct +{ + int unused; +} Fixture; + +typedef struct +{ + int unused; +} Config; + +static void +setup (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + + /* Assert we initially have no child processes */ + g_assert_cmpint (waitpid (-1, NULL, WNOHANG) >= 0 ? 0 : errno, ==, ECHILD); +} + +static void +teardown (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + + /* Assert we end up with no child processes */ + g_assert_cmpint (waitpid (-1, NULL, WNOHANG) >= 0 ? 0 : errno, ==, ECHILD); +} + +#define SPAWN_FLAGS \ + (G_SPAWN_SEARCH_PATH \ + | G_SPAWN_LEAVE_DESCRIPTORS_OPEN \ + | G_SPAWN_DO_NOT_REAP_CHILD) + +static void +test_terminate_nothing (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + gboolean ret; + + ret = pv_terminate_all_child_processes (0, 0, &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = pv_terminate_all_child_processes (0, 100 * G_TIME_SPAN_MILLISECOND, &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = pv_terminate_all_child_processes (100 * G_TIME_SPAN_MILLISECOND, 0, &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = pv_terminate_all_child_processes (100 * G_TIME_SPAN_MILLISECOND, + 100 * G_TIME_SPAN_MILLISECOND, &error); + g_assert_no_error (error); + g_assert_true (ret); +} + +static void +test_terminate_sigterm (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + gboolean ret; + const char * const argv[] = { "sh", "-c", "sleep 3600", NULL }; + + ret = g_spawn_async (NULL, /* cwd */ + (char **) argv, + NULL, /* envp */ + SPAWN_FLAGS, + NULL, NULL, /* child setup */ + NULL, /* pid */ + &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = pv_terminate_all_child_processes (0, 60 * G_TIME_SPAN_SECOND, &error); + g_assert_no_error (error); + g_assert_true (ret); +} + +static void +test_terminate_sigkill (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + gboolean ret; + const char * const argv[] = + { + "sh", "-c", "trap 'echo Ignoring SIGTERM >&2' TERM; sleep 3600", NULL + }; + + ret = g_spawn_async (NULL, /* cwd */ + (char **) argv, + NULL, /* envp */ + SPAWN_FLAGS, + NULL, NULL, /* child setup */ + NULL, /* pid */ + &error); + g_assert_no_error (error); + g_assert_true (ret); + + /* We give it 100ms before SIGTERM to let it put the trap in place */ + ret = pv_terminate_all_child_processes (100 * G_TIME_SPAN_MILLISECOND, + 100 * G_TIME_SPAN_MILLISECOND, &error); + g_assert_no_error (error); + g_assert_true (ret); +} + +static void +test_terminate_sigkill_immediately (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + gboolean ret; + const char * const argv[] = + { + "sh", "-c", "trap 'echo Ignoring SIGTERM >&2' TERM; sleep 3600", NULL + }; + + ret = g_spawn_async (NULL, /* cwd */ + (char **) argv, + NULL, /* envp */ + SPAWN_FLAGS, + NULL, NULL, /* child setup */ + NULL, /* pid */ + &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = pv_terminate_all_child_processes (0, 0, &error); + g_assert_no_error (error); + g_assert_true (ret); +} + +static void +test_wait_for_all (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + int wstat; + gboolean ret; + const char * const argv[] = { "sh", "-c", "exit 42", NULL }; + + ret = g_spawn_async (NULL, /* cwd */ + (char **) argv, + NULL, /* envp */ + SPAWN_FLAGS, + NULL, NULL, /* child setup */ + NULL, /* pid */ + &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = pv_wait_for_child_processes (0, &wstat, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (wstat, ==, -1); +} + +static void +test_wait_for_main (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + int wstat; + gboolean ret; + GPid main_pid; + const char * const argv[] = { "sh", "-c", "exit 42", NULL }; + + ret = g_spawn_async (NULL, /* cwd */ + (char **) argv, + NULL, /* envp */ + SPAWN_FLAGS, + NULL, NULL, /* child setup */ + &main_pid, + &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = pv_wait_for_child_processes (main_pid, &wstat, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_true (WIFEXITED (wstat)); + g_assert_cmpint (WEXITSTATUS (wstat), ==, 42); +} + +static void +test_wait_for_main_plus (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + int wstat; + gboolean ret; + GPid main_pid; + const char * const before_argv[] = { "sh", "-c", "exit 0", NULL }; + const char * const main_argv[] = { "sh", "-c", "sleep 1; kill -TERM $$", NULL }; + const char * const after_argv[] = { "sh", "-c", "sleep 2", NULL }; + + ret = g_spawn_async (NULL, /* cwd */ + (char **) before_argv, + NULL, /* envp */ + SPAWN_FLAGS, + NULL, NULL, /* child setup */ + NULL, /* PID */ + &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = g_spawn_async (NULL, /* cwd */ + (char **) main_argv, + NULL, /* envp */ + SPAWN_FLAGS, + NULL, NULL, /* child setup */ + &main_pid, + &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = g_spawn_async (NULL, /* cwd */ + (char **) after_argv, + NULL, /* envp */ + SPAWN_FLAGS, + NULL, NULL, /* child setup */ + NULL, /* PID */ + &error); + g_assert_no_error (error); + g_assert_true (ret); + + ret = pv_wait_for_child_processes (main_pid, &wstat, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_true (WIFSIGNALED (wstat)); + g_assert_cmpint (WTERMSIG (wstat), ==, SIGTERM); + + /* Don't leak the other processes, if any (probably after_argv) */ + ret = pv_wait_for_child_processes (0, &wstat, &error); + g_assert_no_error (error); + g_assert_true (ret); +} + +static void +test_wait_for_nothing (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + int wstat; + gboolean ret; + + ret = pv_wait_for_child_processes (0, &wstat, &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (wstat, ==, -1); +} + +static void +test_wait_for_wrong_main (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + int wstat; + gboolean ret; + + ret = pv_wait_for_child_processes (getpid (), &wstat, &error); + g_assert_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND); + g_assert_false (ret); + g_assert_cmpint (wstat, ==, -1); +} + +int +main (int argc, + char **argv) +{ + sigset_t mask; + + sigemptyset (&mask); + sigaddset (&mask, SIGCHLD); + + /* Must be called before we start any threads */ + if (pthread_sigmask (SIG_BLOCK, &mask, NULL) != 0) + g_error ("pthread_sigmask: %s", g_strerror (errno)); + + prctl (PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0); + pv_avoid_gvfs (); + + g_test_init (&argc, &argv, NULL); + g_test_add ("/terminate/nothing", Fixture, NULL, + setup, test_terminate_nothing, teardown); + g_test_add ("/terminate/sigterm", Fixture, NULL, + setup, test_terminate_sigterm, teardown); + g_test_add ("/terminate/sigkill", Fixture, NULL, + setup, test_terminate_sigkill, teardown); + g_test_add ("/terminate/sigkill-immediately", Fixture, NULL, + setup, test_terminate_sigkill_immediately, teardown); + g_test_add ("/wait/for-all", Fixture, NULL, + setup, test_wait_for_all, teardown); + g_test_add ("/wait/for-main", Fixture, NULL, + setup, test_wait_for_main, teardown); + g_test_add ("/wait/for-main-plus", Fixture, NULL, + setup, test_wait_for_main_plus, teardown); + g_test_add ("/wait/for-nothing", Fixture, NULL, + setup, test_wait_for_nothing, teardown); + g_test_add ("/wait/for-wrong-main", Fixture, NULL, + setup, test_wait_for_wrong_main, teardown); + + return g_test_run (); +}