Skip to content

Commit

Permalink
Merge pull request #16 from krallin/dpw-kill-process-group
Browse files Browse the repository at this point in the history
Support for kill process group, thanks @dpw!
  • Loading branch information
krallin committed Oct 27, 2015
2 parents 1bfde9c + e7bae98 commit cf1cb5c
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 4 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ as PID 1.
and isn't registered as a subreaper. If you don't see a warning, you're fine.*


### Process group killing ###

By default, Tini only kills its immediate child process. This can be
inconvenient if sending a signal to that process does have the desired
effect. For example, if you do

docker run krallin/ubuntu-tini sh -c 'sleep 10'

and ctrl-C it, nothing happens: SIGINT is sent to the 'sh' process,
but that shell won't react to it while it is waiting for the 'sleep'
to finish.

With the `-g` option, Tini kills the child process group , so that
every process in the group gets the signal. This corresponds more
closely to what happens when you do ctrl-C etc. in a terminal: The
signal is sent to the foreground process group.


More
----

Expand Down
20 changes: 17 additions & 3 deletions src/tini.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,19 @@

#ifdef PR_SET_CHILD_SUBREAPER
#define HAS_SUBREAPER 1
#define OPT_STRING "hsv"
#define OPT_STRING "hsvg"
#define SUBREAPER_ENV_VAR "TINI_SUBREAPER"
#else
#define HAS_SUBREAPER 0
#define OPT_STRING "hv"
#define OPT_STRING "hvg"
#endif


#if HAS_SUBREAPER
static int subreaper = 0;
#endif
static int verbosity = 1;
static int kill_process_group = 0;

static struct timespec ts = { .tv_sec = 1, .tv_nsec = 0 };

Expand Down Expand Up @@ -68,6 +69,13 @@ int spawn(const sigset_t* const child_sigset_ptr, char* const argv[], int* const
PRINT_FATAL("Setting child signal mask failed: '%s'", strerror(errno));
return 1;
}

// Put the child into a new process group
if (setpgid(0, 0) < 0) {
PRINT_FATAL("setpgid failed: '%s'", strerror(errno));
return 1;
}

execvp(argv[0], argv);
PRINT_FATAL("Executing child process '%s' failed: '%s'", argv[0], strerror(errno));
return 1;
Expand All @@ -89,6 +97,7 @@ void print_usage(char* const name, FILE* const file) {
fprintf(file, " -s: Register as a process subreaper (requires Linux >= 3.4).\n");
#endif
fprintf(file, " -v: Generate more verbose output. Repeat up to 3 times.\n");
fprintf(file, " -g: Send signals to the child's process group.\n");
fprintf(file, "\n");
}

Expand All @@ -112,6 +121,11 @@ int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[
case 'v':
verbosity++;
break;

case 'g':
kill_process_group++;
break;

case '?':
print_usage(name, stderr);
return 1;
Expand Down Expand Up @@ -242,7 +256,7 @@ int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const
default:
PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo));
/* Forward anything else */
if (kill(child_pid, sig.si_signo)) {
if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo)) {
if (errno == ESRCH) {
PRINT_WARNING("Child was dead when forwarding signal");
} else {
Expand Down
18 changes: 18 additions & 0 deletions test/pgroup/stage_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env python
import os
import subprocess
import signal


def reset_sig_handler():
signal.signal(signal.SIGUSR1, signal.SIG_DFL)


if __name__ == "__main__":
signal.signal(signal.SIGUSR1, signal.SIG_IGN)
p = subprocess.Popen(
["sleep", "1000"],
preexec_fn=reset_sig_handler
)
p.wait()

26 changes: 25 additions & 1 deletion test/run_inner_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@
import sys
import signal
import subprocess
import time
import psutil


def busy_wait(condition_callable, timeout):
checks = 100
increment = float(timeout) / checks

for _ in xrange(checks):
if condition_callable():
return
time.sleep(increment)

assert False, "Condition was never met"


def main():
Expand Down Expand Up @@ -47,7 +61,17 @@ def main():
sig = getattr(signal, signame)
p.send_signal(sig)
ret = p.wait()
assert ret == - sig, "Signals test failed!"
assert ret == -sig, "Signals test failed!"

# Run the process group test
# This test has Tini spawn a process that ignores SIGUSR1 and spawns a child that doesn't (and waits on the child)
# We send SIGUSR1 to Tini, and expect the grand-child to terminate, then the child, and then Tini.
print "Running process group test"
p = subprocess.Popen([tini, '-g', '--', os.path.join(src, "test", "pgroup", "stage_1.py")], env=dict(os.environ, **env))

busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 2, 10)
p.send_signal(signal.SIGUSR1)
busy_wait(lambda: p.poll() is not None, 10)

# Run failing test
print "Running failing test"
Expand Down
18 changes: 18 additions & 0 deletions tpl/README.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ as PID 1.
and isn't registered as a subreaper. If you don't see a warning, you're fine.*


### Process group killing ###

By default, Tini only kills its immediate child process. This can be
inconvenient if sending a signal to that process does have the desired
effect. For example, if you do

docker run krallin/ubuntu-tini sh -c 'sleep 10'

and ctrl-C it, nothing happens: SIGINT is sent to the 'sh' process,
but that shell won't react to it while it is waiting for the 'sleep'
to finish.

With the `-g` option, Tini kills the child process group , so that
every process in the group gets the signal. This corresponds more
closely to what happens when you do ctrl-C etc. in a terminal: The
signal is sent to the foreground process group.


More
----

Expand Down

0 comments on commit cf1cb5c

Please sign in to comment.