From 3b6e9f256f012284de9fa19bda142686531c2d55 Mon Sep 17 00:00:00 2001 From: Mike McClurg Date: Fri, 3 Jun 2016 20:09:37 -0600 Subject: [PATCH 1/3] Add '--signal' option to replace SIGTERM Many servers respond to other signals than SIGTERM for their "soft shutdown" option, such as Unicorn which requires SIGQUIT to wait on outstanding connections. The 'docker stop' command sends the SIGTERM signal to the container, and provides no option for modifying this behavior. The 'docker kill' command has an '-s' option which allows one to modify the signal sent to the container, but orchestration frameworks such as Mesos don't provide a way to use this functionality. This commit adds the '-s/--signal' option to dumb-init, which provides a replacement signal for SIGTERM. For instance, running dumb-init like so: dumb-init -s 3 Will send SIGQUIT (3) to the process it spawns when it receives a SIGTERM. This allows Docker image writers the freedom to specify how SIGTERM will behave in their containers. A further improvement to this option could be to provide an arbirary mapping from signal to signal, but that would greatly complicate the code for a probably minor use case. --- dumb-init.c | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/dumb-init.c b/dumb-init.c index 560c7a9..87eb933 100644 --- a/dumb-init.c +++ b/dumb-init.c @@ -33,8 +33,20 @@ pid_t child_pid = -1; char debug = 0; char use_setsid = 1; +int sigterm_replacement = 15; + + +int translate_signal(int signum) { + switch (signum) { + case SIGTERM: + return sigterm_replacement; + default: + return signum; + } +} void forward_signal(int signum) { + signum = translate_signal(signum); kill(use_setsid ? -child_pid : child_pid, signum); DEBUG("Forwarded signal %d to children.\n", signum); } @@ -125,6 +137,7 @@ void print_help(char *argv[]) { " -c, --single-child Run in single-child mode.\n" " In this mode, signals are only proxied to the\n" " direct child and not any of its descendants.\n" + " -s, --signal Signal to use in place of SIGTERM.\n" " -v, --verbose Print debugging information to stderr.\n" " -h, --help Print this help message and exit.\n" " -V, --version Print the current version and exit.\n" @@ -136,15 +149,21 @@ void print_help(char *argv[]) { } +void write_sigterm_replacement(char *arg) { + sigterm_replacement = strtol(arg, NULL, 10); +} + + char **parse_command(int argc, char *argv[]) { int opt; struct option long_options[] = { - {"help", no_argument, NULL, 'h'}, - {"single-child", no_argument, NULL, 'c'}, - {"verbose", no_argument, NULL, 'v'}, - {"version", no_argument, NULL, 'V'}, + {"help", no_argument, NULL, 'h'}, + {"single-child", no_argument, NULL, 'c'}, + {"signal", required_argument, NULL, 's'}, + {"verbose", no_argument, NULL, 'v'}, + {"version", no_argument, NULL, 'V'}, }; - while ((opt = getopt_long(argc, argv, "+hvVc", long_options, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "+hvVcs:", long_options, NULL)) != -1) { switch (opt) { case 'h': print_help(argv); @@ -158,6 +177,9 @@ char **parse_command(int argc, char *argv[]) { case 'c': use_setsid = 0; break; + case 's': + write_sigterm_replacement(optarg); + break; default: exit(1); } From 6f6b51f8697892b3fc26870e3c53d6c6cf5b223f Mon Sep 17 00:00:00 2001 From: Mike McClurg Date: Thu, 9 Jun 2016 16:05:27 -0600 Subject: [PATCH 2/3] Rewrite arbitrary signals, and update tests We've decided to allow arbitrary signal rewriting, not just SIGTERM rewriting. To use, call dumb-init with the '-r s:r' option, which will rewrite signum s to signum r. Only signals 1-31 are allowed to be rewritten, which should cover all the signals we need to cover. Note that this commit does not add new tests, it only fixes the existing broken test. --- dumb-init.c | 61 +++++++++++++++++++++++++++++++++++------------ tests/cli_test.py | 1 + 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/dumb-init.c b/dumb-init.c index 87eb933..d73fb37 100644 --- a/dumb-init.c +++ b/dumb-init.c @@ -30,18 +30,24 @@ } \ } while (0) +#define MAXSIG 32 + +int signal_rewrite[MAXSIG] = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31 +}; + pid_t child_pid = -1; char debug = 0; char use_setsid = 1; -int sigterm_replacement = 15; - int translate_signal(int signum) { - switch (signum) { - case SIGTERM: - return sigterm_replacement; - default: - return signum; + if (signum < 0 || signum >= MAXSIG) { + return signum; + } else { + return signal_rewrite[signum]; } } @@ -137,7 +143,7 @@ void print_help(char *argv[]) { " -c, --single-child Run in single-child mode.\n" " In this mode, signals are only proxied to the\n" " direct child and not any of its descendants.\n" - " -s, --signal Signal to use in place of SIGTERM.\n" + " -r, --rewrite s:r Rewrite signum s to signum r before proxying.\n" " -v, --verbose Print debugging information to stderr.\n" " -h, --help Print this help message and exit.\n" " -V, --version Print the current version and exit.\n" @@ -148,22 +154,47 @@ void print_help(char *argv[]) { ); } - -void write_sigterm_replacement(char *arg) { - sigterm_replacement = strtol(arg, NULL, 10); +void print_rewrite_signum_help() { + fprintf( + stderr, + "Usage: -r option takes :, where " + "is between 1 and %d.\n" + "Use --help for full usage.\n", + MAXSIG + ); + exit(1); } +void rewrite_signum(char *arg) { + char *rest; + int signum, replacement; + + signum = strtol(arg, &rest, 10); + + if (*rest != ':') { + print_rewrite_signum_help(); + } + + replacement = strtol(++rest, NULL, 10);; + + if (signum <= 0 || signum > MAXSIG || + replacement <= 0 || replacement > MAXSIG) { + print_rewrite_signum_help(); + } + + signal_rewrite[signum] = replacement; +} char **parse_command(int argc, char *argv[]) { int opt; struct option long_options[] = { {"help", no_argument, NULL, 'h'}, {"single-child", no_argument, NULL, 'c'}, - {"signal", required_argument, NULL, 's'}, + {"rewrite", required_argument, NULL, 'r'}, {"verbose", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'V'}, }; - while ((opt = getopt_long(argc, argv, "+hvVcs:", long_options, NULL)) != -1) { + while ((opt = getopt_long(argc, argv, "+hvVcr:", long_options, NULL)) != -1) { switch (opt) { case 'h': print_help(argv); @@ -177,8 +208,8 @@ char **parse_command(int argc, char *argv[]) { case 'c': use_setsid = 0; break; - case 's': - write_sigterm_replacement(optarg); + case 'r': + rewrite_signum(optarg); break; default: exit(1); diff --git a/tests/cli_test.py b/tests/cli_test.py index 010c41c..f9c5526 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -40,6 +40,7 @@ def test_help_message(flag, both_debug_modes, both_setsid_modes, current_version b' -c, --single-child Run in single-child mode.\n' b' In this mode, signals are only proxied to the\n' b' direct child and not any of its descendants.\n' + b' -r, --rewrite s:r Rewrite signum s to signum r before proxying.\n' b' -v, --verbose Print debugging information to stderr.\n' b' -h, --help Print this help message and exit.\n' b' -V, --version Print the current version and exit.\n' From 9e456addb780dd14d8ae71ea9e06b90b6c14f24c Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Mon, 13 Jun 2016 14:28:57 -0700 Subject: [PATCH 3/3] Add signal translation tests --- README.md | 17 ++++++++ dumb-init.c | 45 +++++++++------------ tests/cli_test.py | 30 +++++++++++++- tests/proxies_signals_test.py | 73 +++++++++++++++++++++++++++++++---- 4 files changed, 131 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 454f185..31ed956 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,23 @@ completely transparent; you can even string multiple together (like `dumb-init dumb-init echo 'oh, hi'`). +### Signal rewriting + +dumb-init allows rewriting incoming signals before proxying them. This is +useful in cases where you have a Docker supervisor (like Mesos or Kubernates) +which always sends a standard signal (e.g. SIGTERM). Some apps require a +different stop signal in order to do graceful cleanup. + +For example, to rewrite the signal SIGTERM (number 15) to SIGQUIT (number 3), +just add `--rewrite 15:3` on the command line. + +One caveat with this feature: for job control signals (SIGTSTP, SIGTTIN, +SIGTTOU), dumb-init will always suspend itself after receiving the signal, even +if you rewrite it to something else. Additionally, if in setsid mode, dumb-init +will always forward SIGSTOP instead, since the original signals have no effect +unless the child has handlers for them. + + ## Installing inside Docker containers You have a few options for using `dumb-init`: diff --git a/dumb-init.c b/dumb-init.c index d73fb37..b4a3685 100644 --- a/dumb-init.c +++ b/dumb-init.c @@ -30,24 +30,23 @@ } \ } while (0) -#define MAXSIG 32 +// Signals we care about are numbered from 1 to 31, inclusive. +// (32 and above are real-time signals.) +#define MAXSIG 31 -int signal_rewrite[MAXSIG] = { - 0, 1, 2, 3, 4, 5, 6, 7, - 8, 9, 10, 11, 12, 13, 14, 15, - 16, 17, 18, 19, 20, 21, 22, 23, - 24, 25, 26, 27, 28, 29, 30, 31 -}; +// Indices are one-indexed (signal 1 is at index 1). Index zero is unused. +int signal_rewrite[MAXSIG + 1] = {0}; pid_t child_pid = -1; char debug = 0; char use_setsid = 1; int translate_signal(int signum) { - if (signum < 0 || signum >= MAXSIG) { + if (signum <= 0 || signum > MAXSIG) { return signum; } else { - return signal_rewrite[signum]; + int translated = signal_rewrite[signum]; + return translated == 0 ? signum : translated; } } @@ -143,7 +142,7 @@ void print_help(char *argv[]) { " -c, --single-child Run in single-child mode.\n" " In this mode, signals are only proxied to the\n" " direct child and not any of its descendants.\n" - " -r, --rewrite s:r Rewrite signum s to signum r before proxying.\n" + " -r, --rewrite s:r Rewrite received signal s to new signal r before proxying.\n" " -v, --verbose Print debugging information to stderr.\n" " -h, --help Print this help message and exit.\n" " -V, --version Print the current version and exit.\n" @@ -159,30 +158,24 @@ void print_rewrite_signum_help() { stderr, "Usage: -r option takes :, where " "is between 1 and %d.\n" + "This option can be specified multiple times.\n" "Use --help for full usage.\n", MAXSIG ); exit(1); } -void rewrite_signum(char *arg) { - char *rest; +void parse_rewrite_signum(char *arg) { int signum, replacement; - - signum = strtol(arg, &rest, 10); - - if (*rest != ':') { - print_rewrite_signum_help(); - } - - replacement = strtol(++rest, NULL, 10);; - - if (signum <= 0 || signum > MAXSIG || - replacement <= 0 || replacement > MAXSIG) { + if ( + sscanf(arg, "%d:%d", &signum, &replacement) == 2 && + (signum >= 1 && signum <= MAXSIG) && + (replacement >= 1 && replacement <= MAXSIG) + ) { + signal_rewrite[signum] = replacement; + } else { print_rewrite_signum_help(); } - - signal_rewrite[signum] = replacement; } char **parse_command(int argc, char *argv[]) { @@ -209,7 +202,7 @@ char **parse_command(int argc, char *argv[]) { use_setsid = 0; break; case 'r': - rewrite_signum(optarg); + parse_rewrite_signum(optarg); break; default: exit(1); diff --git a/tests/cli_test.py b/tests/cli_test.py index f9c5526..1bc7a19 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -40,7 +40,7 @@ def test_help_message(flag, both_debug_modes, both_setsid_modes, current_version b' -c, --single-child Run in single-child mode.\n' b' In this mode, signals are only proxied to the\n' b' direct child and not any of its descendants.\n' - b' -r, --rewrite s:r Rewrite signum s to signum r before proxying.\n' + b' -r, --rewrite s:r Rewrite received signal s to new signal r before proxying.\n' b' -v, --verbose Print debugging information to stderr.\n' b' -h, --help Print this help message and exit.\n' b' -V, --version Print the current version and exit.\n' @@ -97,3 +97,31 @@ def test_verbose_and_single_child(flag1, flag2): ), stderr, ) + + +@pytest.mark.parametrize('extra_args', [ + ('-r',), + ('-r', ''), + ('-r', 'herp'), + ('-r', 'herp:derp'), + ('-r', '15'), + ('-r', '15::12'), + ('-r', '15:derp'), + ('-r', '15:12', '-r'), + ('-r', '15:12', '-r', '0'), + ('-r', '15:12', '-r', '1:32'), +]) +@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') +def test_rewrite_errors(extra_args): + proc = Popen( + ('dumb-init',) + extra_args + ('echo', 'oh,', 'hi'), + stdout=PIPE, stderr=PIPE, + ) + stdout, stderr = proc.communicate() + assert proc.returncode == 1 + assert stderr == ( + b'Usage: -r option takes :, where ' + b'is between 1 and 31.\n' + b'This option can be specified multiple times.\n' + b'Use --help for full usage.\n' + ) diff --git a/tests/proxies_signals_test.py b/tests/proxies_signals_test.py index 8436a58..57955d2 100644 --- a/tests/proxies_signals_test.py +++ b/tests/proxies_signals_test.py @@ -2,25 +2,84 @@ import re import signal import sys +from contextlib import contextmanager +from itertools import chain from subprocess import PIPE from subprocess import Popen +import pytest + from tests.lib.testing import NORMAL_SIGNALS from tests.lib.testing import pid_tree -def test_proxies_signals(both_debug_modes, both_setsid_modes): - """Ensure dumb-init proxies regular signals to its child.""" +@contextmanager +def _print_signals(args=()): + """Start print_signals and return dumb-init process.""" proc = Popen( - ('dumb-init', sys.executable, '-m', 'tests.lib.print_signals'), + ( + ('dumb-init',) + + tuple(args) + + (sys.executable, '-m', 'tests.lib.print_signals') + ), stdout=PIPE, ) - assert re.match(b'^ready \(pid: (?:[0-9]+)\)\n$', proc.stdout.readline()) - for signum in NORMAL_SIGNALS: - proc.send_signal(signum) - assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii') + yield proc for pid in pid_tree(proc.pid): os.kill(pid, signal.SIGKILL) + + +@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') +def test_proxies_signals(): + """Ensure dumb-init proxies regular signals to its child.""" + with _print_signals() as proc: + for signum in NORMAL_SIGNALS: + proc.send_signal(signum) + assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii') + + +def _rewrite_map_to_args(rewrite_map): + return chain.from_iterable( + ('-r', '{0}:{1}'.format(src, dst)) for src, dst in rewrite_map.items() + ) + + +@pytest.mark.parametrize('rewrite_map,sequence,expected', [ + ( + {}, + [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], + [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], + ), + + ( + {signal.SIGTERM: signal.SIGINT}, + [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], + [signal.SIGINT, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], + ), + + ( + { + signal.SIGTERM: signal.SIGINT, + signal.SIGINT: signal.SIGTERM, + signal.SIGQUIT: signal.SIGQUIT, + }, + [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], + [signal.SIGINT, signal.SIGQUIT, signal.SIGCONT, signal.SIGTERM], + ), + + ( + {1: 31, 31: 1}, + [1, 31], + [31, 1], + ), +]) +@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') +def test_proxies_signals_with_rewrite(rewrite_map, sequence, expected): + """Ensure dumb-init can rewrite signals.""" + with _print_signals(_rewrite_map_to_args(rewrite_map)) as proc: + for send, expect_receive in zip(sequence, expected): + proc.send_signal(send) + assert proc.stdout.readline() == '{0}\n'.format(expect_receive).encode('ascii')