diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac5fca5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dumb-init +dist/ +*.deb diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9fd9a2c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM debian:jessie + +MAINTAINER Chris Kuehl +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential devscripts equivs && \ + apt-get clean +WORKDIR /mnt + +ENTRYPOINT mk-build-deps -i && make builddeb diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9e5040f --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +DOCKER_TEST := sh -c 'dpkg -i /mnt/dist/*.deb && cd /mnt/test && ./test' + +.PHONY: build +build: + $(CC) -static -Wall -Werror -o dumb-init dumb-init.c + +.PHONY: clean +clean: + rm -rf dumb-init dist/ *.deb + +.PHONY: builddeb +builddeb: + debuild -us -uc -b + rm -rf dist && mkdir dist + mv ../dumb-init_*.deb dist/ + +.PHONY: builddeb-docker +builddeb-docker: docker-image + docker run -v $(PWD):/mnt dumb-init-build + +.PHONY: docker-image +docker-image: + docker build -t dumb-init-build . + +.PHONY: itest itest_lucid itest_precise itest_trusty itest_wheezy itest_jessie itest_stretch +itest: itest_lucid itest_precise itest_trusty itest_wheezy itest_jessie itest_stretch + +itest_lucid: builddeb-docker + docker run -v $(PWD):/mnt:ro ubuntu:lucid $(DOCKER_TEST) + +itest_precise: builddeb-docker + docker run -v $(PWD):/mnt:ro ubuntu:precise $(DOCKER_TEST) + +itest_trusty: builddeb-docker + docker run -v $(PWD):/mnt:ro ubuntu:trusty $(DOCKER_TEST) + +itest_wheezy: builddeb-docker + docker run -v $(PWD):/mnt:ro debian:wheezy $(DOCKER_TEST) + +itest_jessie: builddeb-docker + docker run -v $(PWD):/mnt:ro debian:jessie $(DOCKER_TEST) + +itest_stretch: builddeb-docker + docker run -v $(PWD):/mnt:ro debian:stretch $(DOCKER_TEST) diff --git a/debian/.gitignore b/debian/.gitignore new file mode 100644 index 0000000..5571ad5 --- /dev/null +++ b/debian/.gitignore @@ -0,0 +1,5 @@ +*.log +*.substvars +files +substvars +dumb-init/ diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..b4f41f5 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +dumb-init (0.0.1) unstable; urgency=low + + * Initial release. + + -- Chris Kuehl Wed, 29 Jul 2015 15:39:11 -0700 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..78b9cf5 --- /dev/null +++ b/debian/control @@ -0,0 +1,26 @@ +Source: dumb-init +Section: utils +Priority: extra +Maintainer: Chris Kuehl +Build-Depends: debhelper (>= 7), gcc, fakeroot +Standards-Version: 3.9.6 + +Package: dumb-init +Architecture: any +Depends: ${misc:Depends} +Description: Simple wrapper script which proxies signals to a child + Docker runs your processes as PID1. The kernel doesn't apply default signal + handling to PID1 processes, so if your process doesn't register a custom + signal handler, signals like TERM will just bounce off your process. + . + This can result in cases where sending signals to a `docker run` process + results in the run process exiting, but the container continuing in the + background. + . + A workaround is to wrap your script in this proxy, which runs as PID1. Your + process then runs as some other PID, and the kernel won't treat the signals + that are proxied to them specially. + . + The proxy dies when your process dies, so it must not double-fork or do other + weird things (this is basically a requirement for doing things sanely in + Docker anyway). diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..4343f09 --- /dev/null +++ b/debian/install @@ -0,0 +1 @@ +dumb-init /usr/bin/ diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..cbe925d --- /dev/null +++ b/debian/rules @@ -0,0 +1,3 @@ +#!/usr/bin/make -f +%: + dh $@ diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/dumb-init.c b/dumb-init.c new file mode 100644 index 0000000..1267199 --- /dev/null +++ b/dumb-init.c @@ -0,0 +1,80 @@ +/* + * dumb-init is a simple wrapper program designed to run as PID 1 and pass + * signals to its children. + * + * Usage: + * ./dumb-init python -c 'while True: pass' + * + * To get debug output on stderr, run with DUMB_INIT_DEBUG=1. + */ + +#include +#include +#include +#include +#include +#include +#include + +pid_t child = -1; +char debug = 0; + +void signal_handler(int signum) { + if (debug) + fprintf(stderr, "Received signal %d.\n", signum); + + if (child > 0) { + kill(child, signum); + + if (debug) + fprintf(stderr, "Forwarded signal to child.\n"); + } +} + +int main(int argc, char *argv[]) { + int signum; + char* debug_env; + + if (argc < 2) { + fprintf(stderr, "Try providing some arguments.\n"); + return 1; + } + + debug_env = getenv("DUMB_INIT_DEBUG"); + if (debug_env && strcmp(debug_env, "1") == 0) + debug = 1; + + + /* register signal handlers */ + for (signum = 1; signum < 32; signum++) { + if (signum == SIGKILL || signum == SIGSTOP || signum == SIGCHLD) + continue; + + if (signal(signum, signal_handler) == SIG_ERR) { + fprintf(stderr, "Error: Couldn't register signal handler for signal `%d`. Exiting.\n", signum); + return 1; + } + } + + /* launch our process */ + child = fork(); + + if (child < 0) { + fprintf(stderr, "Unable to fork. Exiting.\n"); + return 1; + } + + if (child == 0) { + execvp(argv[1], &argv[1]); + } else { + if (debug) + fprintf(stderr, "Child spawned with PID %d.\n", child); + + waitpid(child, NULL, 0); + + if (debug) + fprintf(stderr, "Child exited, goodbye.\n"); + } + + return 0; +} diff --git a/test/print-signals b/test/print-signals new file mode 100755 index 0000000..682dfd7 --- /dev/null +++ b/test/print-signals @@ -0,0 +1,14 @@ +#!/bin/sh -eu +# Print received signals into a file, one per line +file="$1" + +. ./testlib.sh + +for i in $(catchable_signals); do + trap "echo $i > \"$file\"" "$i" +done + +echo 'ready' > "$file" + +# loop forever +while :; do true; done diff --git a/test/test b/test/test new file mode 100755 index 0000000..8f148b5 --- /dev/null +++ b/test/test @@ -0,0 +1,37 @@ +#!/bin/sh -eum +# Try sending all signals via dumb-init to our `print-signals` script, ensure +# they were all received. +. ./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) +fifo=$(mktemp -u) +mkfifo -m 600 "$fifo" +dumb-init ./print-signals "$fifo" & +pid="$!" + +# Wait for `print-signals` to indicate it's ready. +head -n1 "$fifo" > /dev/null + +for expected in $(catchable_signals); do + kill -s "$expected" "$pid" + echo -n "Sent signal ${expected}... " + received=$(timeout 1 head -n1 "$fifo") || { + echo + echo "Error: Didn't receive signal within 1 second." + exit 1 + } + + echo "received signal ${received}." + + if [ "$expected" -ne "$received" ]; then + echo "Error: Received signal $received, but expected signal $expected." + exit 1 + fi +done + +# Turn off job monitoring so we don't get a spurious "[1] + Killed" printed +set +m + +kill -9 "$pid" +rm "$fifo" diff --git a/test/testlib.sh b/test/testlib.sh new file mode 100644 index 0000000..f20d67a --- /dev/null +++ b/test/testlib.sh @@ -0,0 +1,4 @@ +catchable_signals() { + # We can't handle the signals SIGKILL=9, SIGCHLD=17, SIGSTOP=19 + seq 1 31 | grep -vE '^(9|17|19)$' +}