Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add '--signal' option to replace SIGTERM #83

Merged
merged 4 commits into from
Jun 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down
56 changes: 51 additions & 5 deletions dumb-init.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,28 @@
} \
} while (0)

// Signals we care about are numbered from 1 to 31, inclusive.
// (32 and above are real-time signals.)
#define MAXSIG 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) {
return signum;
} else {
int translated = signal_rewrite[signum];
return translated == 0 ? signum : translated;
}
}

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);
}
Expand Down Expand Up @@ -125,6 +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 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"
Expand All @@ -135,16 +153,41 @@ void print_help(char *argv[]) {
);
}

void print_rewrite_signum_help() {
fprintf(
stderr,
"Usage: -r option takes <signum>:<signum>, where <signum> "
"is between 1 and %d.\n"
"This option can be specified multiple times.\n"
"Use --help for full usage.\n",
MAXSIG
);
exit(1);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function is probably unnecessary and the line could just be put in the switch


void parse_rewrite_signum(char *arg) {
int signum, replacement;
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();
}
}

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'},
{"rewrite", required_argument, NULL, 'r'},
{"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, "+hvVcr:", long_options, NULL)) != -1) {
switch (opt) {
case 'h':
print_help(argv);
Expand All @@ -158,6 +201,9 @@ char **parse_command(int argc, char *argv[]) {
case 'c':
use_setsid = 0;
break;
case 'r':
parse_rewrite_signum(optarg);
break;
default:
exit(1);
}
Expand Down
29 changes: 29 additions & 0 deletions tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 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'
Expand Down Expand Up @@ -96,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 <signum>:<signum>, where <signum> '
b'is between 1 and 31.\n'
b'This option can be specified multiple times.\n'
b'Use --help for full usage.\n'
)
73 changes: 66 additions & 7 deletions tests/proxies_signals_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')