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 support for signal "observers" #207

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,23 @@ One caveat with this feature: for job control signals (`SIGTSTP`, `SIGTTIN`,
even if you rewrite it to something else.


### Signal "observing"

dumb-init also allows executing an "observer" when a signal is received. An
observer is nothing more than a script or executable that gets called as part of
the signal-handling process, whether or not the signal is forwarded to the child
process. You can provide a full path to an observer or let dumb-init search the
`PATH`. The executable should not require or expect command line arguments (it
will get none), but two environment variables will be provided,
`DUMB_INIT_SIGNUM` and `DUMB_INIT_REPLACEMENT_SIGNUM`. They will contain the
signal _numbers_, not names.

An observer is specified as an optional third parameter to `-r/--rewrite`:
`--rewrite 12:0:/path/to/observer`. To observe a signal while still passing it
to the child process, simply replace a signal with itself: `--rewrite
10:10:some_observer`.


## Installing inside Docker containers

You have a few options for using `dumb-init`:
Expand Down Expand Up @@ -217,7 +234,7 @@ entrypoint. An "entrypoint" is a partial command that gets prepended to your
ENTRYPOINT ["/usr/bin/dumb-init", "--"]

# or if you use --rewrite or other cli flags
# ENTRYPOINT ["dumb-init", "--rewrite", "2:3", "--"]
# ENTRYPOINT ["dumb-init", "--rewrite", "2:3", "--rewrite", "10:10:observer_script", "--"]

CMD ["/my/script", "--with", "--args"]
```
Expand Down
128 changes: 116 additions & 12 deletions dumb-init.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <assert.h>
#include <errno.h>
#include <getopt.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -39,6 +40,7 @@
// Indices are one-indexed (signal 1 is at index 1). Index zero is unused.
// User-specified signal rewriting.
int signal_rewrite[MAXSIG + 1] = {[0 ... MAXSIG] = -1};
char *signal_observers[MAXSIG + 1] = {[0 ... MAXSIG] = NULL};
// One-time ignores due to TTY quirks. 0 = no skip, 1 = skip the next-received signal.
char signal_temporary_ignores[MAXSIG + 1] = {[0 ... MAXSIG] = 0};

Expand All @@ -61,12 +63,41 @@ int translate_signal(int signum) {
}

void forward_signal(int signum) {
signum = translate_signal(signum);
if (signum != 0) {
kill(use_setsid ? -child_pid : child_pid, signum);
DEBUG("Forwarded signal %d to children.\n", signum);
int replacement = translate_signal(signum);
char *observer = signal_observers[signum];
char s[10];

if (observer) {
pid_t observer_pid = fork();

if (observer_pid < 0) {
PRINTERR("%s: unable to fork observer\n", observer);
} else if (observer_pid == 0) {
/* child */
sigset_t all_signals;

sigfillset(&all_signals);
sigprocmask(SIG_UNBLOCK, &all_signals, NULL);

snprintf(s, 10, "%d", signum);
setenv("DUMB_INIT_SIGNUM", s, 1);
snprintf(s, 10, "%d", replacement);
setenv("DUMB_INIT_REPLACEMENT_SIGNUM", s, 1);

execl(observer, observer, NULL);

PRINTERR("%s: %s\n", observer, strerror(errno));
} else {
/* parent */
DEBUG("%s: Observer spawned with PID %d.\n", observer, observer_pid);
}
}

if (replacement != 0) {
kill(use_setsid ? -child_pid : child_pid, replacement);
DEBUG("Forwarded signal %d to children.\n", replacement);
} else {
DEBUG("Not forwarding signal %d to children (ignored).\n", signum);
DEBUG("Not forwarding signal %d to children (ignored).\n", replacement);
}
}

Expand Down Expand Up @@ -136,9 +167,15 @@ 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"
" To ignore (not proxy) a signal, rewrite it to 0.\n"
" This option can be specified multiple times.\n"
" -r, --rewrite s:r[:observer]\n"
" Rewrite received signal s to new signal r before\n"
" proxying. To ignore (not proxy) a signal, rewrite it\n"
" to 0. The optional observer is a script or executable\n"
" to execute when signal s is received (regardless\n"
" of any rewriting). It must expect no arguments, but\n"
" the DUMB_INIT_SIGNUM and DUMB_INIT_REPLACEMENT_SIGNUM\n"
" environment variables will be set. This option can be\n"
" specified multiple times.\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 @@ -152,26 +189,93 @@ 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"
"Usage: -r option takes <signum>:<signum>[:<observer>], "
"where <signum> is between 1 and %d.\n"
"<observer> must be a path to an executable or an executable "
"that can be found in the PATH. It must expect no arguments.\n"
"This option can be specified multiple times.\n"
"Use --help for full usage.\n",
MAXSIG
);
exit(1);
}

char *find_path(const char *partial) {
static char **path_entries = NULL;
static int path_count = 0;

if (strchr(partial, '/')) {
return !access(partial, X_OK) ? strdup(partial) : NULL;
} else {
int i;
size_t plen;
char file[PATH_MAX];

if (!path_entries) {
char *path, *tokpath, *s;

path = getenv("PATH");

if (!(tokpath = strdup(path && strlen(path) ? path : "/bin:/usr/bin:/sbin:/usr/sbin"))) {
PRINTERR("cannot get PATH\n");
exit(1);
}

for (path_count = 1, s = tokpath; (s = strchr(s, ':')); path_count++, s++) {
;
}

if (!(path_entries = (char**)malloc(path_count * sizeof(char*)))) {
PRINTERR("cannot not create PATH entries\n");
exit(1);
}

for(i = 0, s = strtok(tokpath, ":"); s; s = strtok(NULL, ":"), i++) {
path_entries[i] = strdup(s);
}

free(tokpath);
}

for (plen = strlen(partial), i = 0; i < path_count; i++) {
if (plen + strlen(path_entries[i]) < (PATH_MAX - 2)) {
sprintf(file, "%s/%s", path_entries[i], partial);

if (!access(file, X_OK)) {
return strdup(file);
}
}
}
}

return NULL;
}

void parse_rewrite_signum(char *arg) {
int signum, replacement;
int signum, replacement, position;
size_t length;
char *observer, *path;

if (
sscanf(arg, "%d:%d", &signum, &replacement) == 2 &&
sscanf(arg, "%d:%d%n", &signum, &replacement, &position) == 2 &&
(signum >= 1 && signum <= MAXSIG) &&
(replacement >= 0 && replacement <= MAXSIG)
) {
signal_rewrite[signum] = replacement;
} else {
print_rewrite_signum_help();
}

observer = arg + position;

if ((*observer++ == ':') && (length = strlen(observer))) {
if (!(path = find_path(observer))) {
PRINTERR("%s: observer not found or not executable\n", observer);
exit(1);
}

signal_observers[signum] = path;
}
}

void set_rewrite_to_sigstop_if_not_defined(int signum) {
Expand Down
35 changes: 30 additions & 5 deletions tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@ def test_help_message(flag, 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' To ignore (not proxy) a signal, rewrite it to 0.\n'
b' This option can be specified multiple times.\n'
b' -r, --rewrite s:r[:observer]\n'
b' Rewrite received signal s to new signal r before\n'
b' proxying. To ignore (not proxy) a signal, rewrite it\n'
b' to 0. The optional observer is a script or executable\n'
b' to execute when signal s is received (regardless\n'
b' of any rewriting). It must expect no arguments, but\n'
b' the DUMB_INIT_SIGNUM and DUMB_INIT_REPLACEMENT_SIGNUM\n'
b' environment variables will be set. This option can be\n'
b' specified multiple times.\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 @@ -143,8 +149,27 @@ def test_rewrite_errors(extra_args):
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'Usage: -r option takes <signum>:<signum>[:<observer>], where <signum> is between 1 and 31.\n'
b'<observer> must be a path to an executable or an executable that can be found in the PATH. It must expect no arguments.\n'
b'This option can be specified multiple times.\n'
b'Use --help for full usage.\n'
)

@pytest.mark.parametrize(
'extra_args', [
('-r', '12:0:foo'),
('-r', '12:0:/bin/foo'),
],
)
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_observer_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 in (
b'[dumb-init] foo: observer not found or not executable\n',
b'[dumb-init] /bin/foo: observer not found or not executable\n',
)
18 changes: 18 additions & 0 deletions tests/execs_observers_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os

import pytest

from testing import print_signals


@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_execs_observers():
"""Ensure dumb-init executes observers."""
with print_signals(('-r', '10:0:/bin/pwd', '-r', '12:12:pwd',)) as (proc, _):
proc.send_signal(10)
assert proc.stdout.readline() == '{}\n'.format(os.getcwd()).encode('ascii')
proc.send_signal(12)
assert (proc.stdout.readline() + proc.stdout.readline()) in (
'12\n{}\n'.format(os.getcwd()).encode('ascii'),
'{}\n12\n'.format(os.getcwd()).encode('ascii'),
)