From 2c3b93a86e2b5878fc86516af907c975dc5db246 Mon Sep 17 00:00:00 2001 From: Chris Kuehl Date: Thu, 6 Aug 2015 14:24:27 -0700 Subject: [PATCH] Fix return code, add better help, add more tests --- Makefile | 2 +- README.md | 79 +++++++++++++++++++++++++ debian/changelog | 9 ++- dumb-init.c | 36 +++++++++-- test | 15 +++++ {test => tests/lib}/print-signals | 2 +- {test => tests/lib}/testlib.sh | 0 tests/test-exit-status | 10 ++++ tests/test-help-message | 18 ++++++ test/test => tests/test-proxies-signals | 6 +- 10 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 README.md create mode 100755 test rename {test => tests/lib}/print-signals (91%) rename {test => tests/lib}/testlib.sh (100%) create mode 100755 tests/test-exit-status create mode 100755 tests/test-help-message rename test/test => tests/test-proxies-signals (87%) diff --git a/Makefile b/Makefile index 6270dd5..2a41cbf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -DOCKER_TEST := sh -c 'dpkg -i /mnt/dist/*.deb && cd /mnt/test && ./test' +DOCKER_TEST := sh -c 'dpkg -i /mnt/dist/*.deb && cd /mnt && ./test' .PHONY: build build: diff --git a/README.md b/README.md new file mode 100644 index 0000000..35c9711 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +dumb-init +======== + +`dumb-init` is a simple process designed to run as PID 1 inside Docker +containers and proxy signals to a single child process. + +In Docker containers, a process typically runs as PID 1, which means that +signals like TERM will just bounce off your process unless it goes out of its +way to handle them (see the "Why" section below). This is a big problem with +scripts in languages like Python, Bash, or Ruby, and can lead to leaking Docker +containers if you're not careful. + + +## Why you need a signal proxy + +Normally, when processes are sent a signal like `TERM`, the Linux kernel will +try to trigger any custom handlers the process has registered for that signal. + +If the process hasn't registered custom handlers, the kernel will fall back to +default behavior for that signal (such as killing the process, in the case of +`TERM`). + +However, processes which run as PID 1 get special treatment by the kernel, and +default signal handlers won't be applied. If your process doesn't explicitly +handle these signals, a `TERM` will have no effect at all. + +For example, if you have Jenkins jobs that do `docker run my-container script`, +sending TERM to the `docker run` process will typically kill the `docker run` +command, but leave the container running in the background. + + +## What `dumb-init` does + +`dumb-init` runs as PID 1, acting like a simple init system. It launches a +single process, and then proxies all received signals to that child process. + +Since your actual process is no longer PID 1, when it receives signals from +`dumb-init`, the default signal handlers will be applied, and your process will +behave as you would expect. + +If your process dies, `dumb-init` will also die. + + +## Installing inside Docker containers + +You have a few options for using `dumb-init`: + + +### Option 1: Installing via an internal apt server + +If you have an internal apt server, uploading the `.deb` to your server is the +recommended way to use `dumb-init`. In your Dockerfiles, you can simply +`apt-get install dumb-init` and it will be available. + + +### Option 2: Installing the `.deb` package manually + +If you don't have an internal apt server, you can use `dpkg -i` to install the +`.deb` package. You can choose how you get the `.deb` onto your container +(mounting a directory or `wget`-ing it are some options). + + +## Usage + +Once installed inside your Docker container, simply prefix your commands with +`dumb-init`. For example: + + $ docker run my_container dumb-init python -c 'while True: pass' + +Running this same command without `dumb-init` would result in being unable to +stop the container without SIGKILL, but with `dumb-init`, you can send it more +humane signals like TERM. + + +## See also + +* [Docker and the PID 1 zombie reaping problem (Phusion Blog)](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/) +* [Trapping signals in Docker containers (@gchudnov)](https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86) +* [pgctl](https://github.com/Yelp/pgctl) diff --git a/debian/changelog b/debian/changelog index b4f41f5..b75f84b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,12 @@ +dumb-init (0.0.2) unstable; urgency=low + + * Exit with the same exit status as the process we call. + * Print a more useful help message when called with no arguments. + + -- Chris Kuehl Thu, 06 Aug 2015 13:51:38 -0700 + dumb-init (0.0.1) unstable; urgency=low * Initial release. - -- Chris Kuehl Wed, 29 Jul 2015 15:39:11 -0700 + -- Chris Kuehl Thu, 06 Aug 2015 13:51:38 -0700 diff --git a/dumb-init.c b/dumb-init.c index 1267199..97b7410 100644 --- a/dumb-init.c +++ b/dumb-init.c @@ -31,12 +31,35 @@ void signal_handler(int signum) { } } +void print_help(char *argv[]) { + fprintf(stderr, + "Usage: %s COMMAND [[ARG] ...]\n" + "\n" + "Docker runs your processes as PID1. The kernel doesn't apply default signal\n" + "handling to PID1 processes, so if your process doesn't register a custom\n" + "signal handler, signals like TERM will just bounce off your process.\n" + "\n" + "This can result in cases where sending signals to a `docker run` process\n" + "results in the run process exiting, but the container continuing in the\n" + "background.\n" + "\n" + "A workaround is to wrap your script in this proxy, which runs as PID1. Your\n" + "process then runs as some other PID, and the kernel won't treat the signals\n" + "that are proxied to them specially.\n" + "\n" + "The proxy dies when your process dies, so it must not double-fork or do other\n" + "weird things (this is basically a requirement for doing things sanely in\n" + "Docker anyway).\n", + argv[0] + ); +} + int main(int argc, char *argv[]) { - int signum; - char* debug_env; + int signum, exit_status, status = 0; + char *debug_env; if (argc < 2) { - fprintf(stderr, "Try providing some arguments.\n"); + print_help(argv); return 1; } @@ -70,10 +93,13 @@ int main(int argc, char *argv[]) { if (debug) fprintf(stderr, "Child spawned with PID %d.\n", child); - waitpid(child, NULL, 0); + waitpid(child, &status, 0); + exit_status = WEXITSTATUS(status); if (debug) - fprintf(stderr, "Child exited, goodbye.\n"); + fprintf(stderr, "Child exited with status %d, goodbye.\n", exit_status); + + return exit_status; } return 0; diff --git a/test b/test new file mode 100755 index 0000000..f599877 --- /dev/null +++ b/test @@ -0,0 +1,15 @@ +#!/bin/sh -eux +run_tests() { + ./test-proxies-signals + ./test-exit-status + ./test-help-message +} + +cd tests + +echo "Running tests in normal mode." +run_tests + +echo "Running tests in debug mode." +export DUMB_INIT_DEBUG=1 +run_tests diff --git a/test/print-signals b/tests/lib/print-signals similarity index 91% rename from test/print-signals rename to tests/lib/print-signals index 682dfd7..db024ba 100755 --- a/test/print-signals +++ b/tests/lib/print-signals @@ -2,7 +2,7 @@ # Print received signals into a file, one per line file="$1" -. ./testlib.sh +. ./lib/testlib.sh for i in $(catchable_signals); do trap "echo $i > \"$file\"" "$i" diff --git a/test/testlib.sh b/tests/lib/testlib.sh similarity index 100% rename from test/testlib.sh rename to tests/lib/testlib.sh diff --git a/tests/test-exit-status b/tests/test-exit-status new file mode 100755 index 0000000..364515f --- /dev/null +++ b/tests/test-exit-status @@ -0,0 +1,10 @@ +#!/bin/sh -u +# dumb-init should exit with the same exit status as the process it launches. +for i in $(seq 0 255); do + status=$(dumb-init sh -c "exit $i"; echo $?) + + if [ "$status" -ne "$i" ]; then + echo "Error: Expected exit status $i, got $status." + exit 1 + fi +done diff --git a/tests/test-help-message b/tests/test-help-message new file mode 100755 index 0000000..f08673d --- /dev/null +++ b/tests/test-help-message @@ -0,0 +1,18 @@ +#!/bin/sh -u +# dumb-init should say something useful when called with no arguments, and exit +# nonzero. + +status=$(dumb-init > /dev/null 2>&1; echo $?) + +if [ "$status" -ne 0 ]; then + msg=$(dumb-init 2>&1 || true) + msg_len=${#msg} + + if [ "$msg_len" -le 50 ]; then + echo "Error: Expected dumb-init with no arguments to print a useful message, but it was only ${msg_len} chars long." + exit 1 + fi +else + echo "Error: Expected dumb-init with no arguments to return nonzero, but it returned ${status}." + exit 1 +fi diff --git a/test/test b/tests/test-proxies-signals similarity index 87% rename from test/test rename to tests/test-proxies-signals index 7715df7..134af97 100755 --- a/test/test +++ b/tests/test-proxies-signals @@ -1,7 +1,9 @@ #!/bin/sh -eum +# dumb-init should proxy all possible signals to the child process. + # Try sending all signals via dumb-init to our `print-signals` script, ensure # they were all received. -. ./testlib.sh +. ./lib/testlib.sh # The easiest way to communicate with the background process is with a FIFO. # (piping spawns additional subshells and makes it hard to get the right PID) @@ -9,7 +11,7 @@ fifo=$(mktemp -u) mkfifo -m 600 "$fifo" read_cmd="timeout 1 head -n1 $fifo" -dumb-init ./print-signals "$fifo" & +dumb-init ./lib/print-signals "$fifo" & pid="$!" # Wait for `print-signals` to indicate it's ready.