Skip to content
Snippets Groups Projects
Commit 3d336351 authored by Simon McVittie's avatar Simon McVittie
Browse files

utils: Add pv_terminate_all_child_processes()

parent b912cdee
Branches
Tags
No related merge requests found
......@@ -43,10 +43,6 @@
#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;
......
......@@ -58,6 +58,7 @@ pressure_vessel_utils = static_library(
'utils.h',
],
dependencies : [
threads,
gio_unix,
libelf,
libglnx.get_variable('libglnx_dep'),
......
......@@ -23,10 +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>
......@@ -662,3 +665,401 @@ pv_wait_for_child_processes (pid_t main_process,
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;
}
......@@ -27,6 +27,14 @@
#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,
......@@ -65,3 +73,7 @@ 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);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment