diff --git a/.gitignore b/.gitignore index ac5fca5..e2cf2df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ dumb-init dist/ *.deb +*.egg-info +.tox diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e392923 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +- repo: https://github.com/pre-commit/pre-commit-hooks.git + sha: 003e43251aea1da33f2072f2365ec8b9ceaae070 + hooks: + - id: autopep8-wrapper + - id: check-added-large-files + - id: check-docstring-first + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: double-quote-string-fixer + - id: end-of-file-fixer + - id: flake8 + - id: name-tests-test + - id: requirements-txt-fixer + - id: trailing-whitespace +- repo: https://github.com/asottile/reorder_python_imports.git + sha: 3d86483455ab5bd06cc1069fdd5ac57be5463f10 + hooks: + - id: reorder-python-imports +- repo: https://github.com/Lucas-C/pre-commit-hooks.git + sha: 181a63c511691da58116fa19a7241956018660bc + hooks: + - id: remove-tabs diff --git a/Dockerfile b/Dockerfile index 9fd9a2c..5f138b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,9 @@ 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 + 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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..30a7054 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include dumb-init.c diff --git a/Makefile b/Makefile index 2a41cbf..0bf8703 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,31 @@ -DOCKER_TEST := sh -c 'dpkg -i /mnt/dist/*.deb && cd /mnt && ./test' +DOCKER_RUN_TEST := docker run -v $(PWD):/mnt:ro +DOCKER_DEB_TEST := sh -euxc ' \ + apt-get update \ + && apt-get install -y --no-install-recommends procps \ + && dpkg -i /mnt/dist/*.deb \ + && cd /mnt \ + && ./test \ +' +DOCKER_PYTHON_TEST := sh -uexc ' \ + apt-get update \ + && apt-get install -y --no-install-recommends python-pip build-essential procps \ + && pip install -vv /mnt \ + && cd /mnt \ + && ./test \ +' .PHONY: build build: $(CC) -static -Wall -Werror -o dumb-init dumb-init.c .PHONY: clean -clean: +clean: clean-tox rm -rf dumb-init dist/ *.deb +.PHONY: clean-tox +clean-tox: + rm -rf .tox + .PHONY: builddeb builddeb: debuild -us -uc -b @@ -22,24 +40,31 @@ builddeb-docker: docker-image docker-image: docker build -t dumb-init-build . +.PHONY: test +test: + tox + +.PHONY: install-hooks +install-hooks: + tox -e pre-commit -- install -f --install-hooks + .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 \ - sh -ec "apt-get -y install timeout; $(DOCKER_TEST)" +itest_lucid: _itest-ubuntu-lucid +itest_precise: _itest-ubuntu-precise +itest_trusty: _itest-ubuntu-trusty +itest_wheezy: _itest-debian-wheezy +itest_jessie: _itest-debian-jessie +itest_stretch: _itest-debian-stretch -itest_precise: builddeb-docker - docker run -v $(PWD):/mnt:ro ubuntu:precise $(DOCKER_TEST) +_itest-%: _itest_deb-% _itest_python-% + @true -itest_trusty: builddeb-docker - docker run -v $(PWD):/mnt:ro ubuntu:trusty $(DOCKER_TEST) +_itest_python-%: + $(eval DOCKER_IMG := $(shell echo $@ | cut -d- -f2 | sed 's/-/:/')) + $(DOCKER_RUN_TEST) $(DOCKER_IMG) $(DOCKER_PYTHON_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) +_itest_deb-%: + $(eval DOCKER_IMG := $(shell echo $@ | cut -d- -f2 | sed 's/-/:/')) + $(DOCKER_RUN_TEST) $(DOCKER_IMG) $(DOCKER_DEB_TEST) diff --git a/README.md b/README.md index 35c9711..5cb5b25 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ If you don't have an internal apt server, you can use `dpkg -i` to install the (mounting a directory or `wget`-ing it are some options). +### Option 3: Installing from PyPI + +dumb-init can be installed [from PyPI](https://pypi.python.org/pypi/dumb-init) +using pip. Since dumb-init is written in C, you'll want to first install gcc +(on Debian/Ubuntu, `apt-get install gcc` is sufficient), then just `pip install +dumb-init`. + + ## Usage Once installed inside your Docker container, simply prefix your commands with diff --git a/debian/control b/debian/control index 78b9cf5..3f9a3cc 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: dumb-init Section: utils Priority: extra Maintainer: Chris Kuehl -Build-Depends: debhelper (>= 7), gcc, fakeroot +Build-Depends: debhelper (>= 7), gcc, fakeroot, procps Standards-Version: 3.9.6 Package: dumb-init diff --git a/debian/rules b/debian/rules index 483d681..f3af0c2 100755 --- a/debian/rules +++ b/debian/rules @@ -4,3 +4,6 @@ override_dh_builddeb: dh_builddeb -- -Zgzip + +override_dh_auto_test: + ./test ./dumb-init diff --git a/dumb-init.c b/dumb-init.c index 97b7410..48bdee6 100644 --- a/dumb-init.c +++ b/dumb-init.c @@ -20,87 +20,87 @@ pid_t child = -1; char debug = 0; void signal_handler(int signum) { - if (debug) - fprintf(stderr, "Received signal %d.\n", signum); + if (debug) + fprintf(stderr, "Received signal %d.\n", signum); - if (child > 0) { - kill(child, signum); + if (child > 0) { + kill(child, signum); - if (debug) - fprintf(stderr, "Forwarded signal to child.\n"); - } + if (debug) + fprintf(stderr, "Forwarded signal to child.\n"); + } } 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] - ); + 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, exit_status, status = 0; - char *debug_env; + int signum, exit_status, status = 0; + char *debug_env; - if (argc < 2) { - print_help(argv); - return 1; - } + if (argc < 2) { + print_help(argv); + return 1; + } - debug_env = getenv("DUMB_INIT_DEBUG"); - if (debug_env && strcmp(debug_env, "1") == 0) - debug = 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; + /* 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; - } - } + 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(); + /* launch our process */ + child = fork(); - if (child < 0) { - fprintf(stderr, "Unable to fork. Exiting.\n"); - return 1; - } + 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); + if (child == 0) { + execvp(argv[1], &argv[1]); + } else { + if (debug) + fprintf(stderr, "Child spawned with PID %d.\n", child); - waitpid(child, &status, 0); - exit_status = WEXITSTATUS(status); + waitpid(child, &status, 0); + exit_status = WEXITSTATUS(status); - if (debug) - fprintf(stderr, "Child exited with status %d, goodbye.\n", exit_status); + if (debug) + fprintf(stderr, "Child exited with status %d, goodbye.\n", exit_status); - return exit_status; - } + return exit_status; + } - return 0; + return 0; } diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fdd32dc --- /dev/null +++ b/setup.py @@ -0,0 +1,107 @@ +import os.path +from distutils.command.build import build as orig_build +from distutils.core import Command + +from setuptools import Distribution +from setuptools import Extension +from setuptools import setup +from setuptools.command.install import install as orig_install + + +class ExeDistribution(Distribution): + c_executables = () + + +class build(orig_build): + sub_commands = orig_build.sub_commands + [ + ('build_cexe', None), + ] + + +class install(orig_install): + sub_commands = orig_install.sub_commands + [ + ('install_cexe', None), + ] + + +class install_cexe(Command): + description = 'install C executables' + outfiles = () + + def initialize_options(self): + self.build_dir = self.install_dir = None + + def finalize_options(self): + # this initializes attributes based on other commands' attributes + self.set_undefined_options('build', ('build_scripts', 'build_dir')) + self.set_undefined_options( + 'install', ('install_scripts', 'install_dir')) + + def run(self): + self.outfiles = self.copy_tree(self.build_dir, self.install_dir) + + def get_outputs(self): + return self.outfiles + + +class build_cexe(Command): + description = 'build C executables' + + def initialize_options(self): + self.build_scripts = None + self.build_temp = None + + def finalize_options(self): + self.set_undefined_options( + 'build', + ('build_scripts', 'build_scripts'), + ('build_temp', 'build_temp'), + ) + + def run(self): + # stolen and simplified from distutils.command.build_ext + from distutils.ccompiler import new_compiler + + compiler = new_compiler(verbose=True) + + for exe in self.distribution.c_executables: + objects = compiler.compile( + exe.sources, + output_dir=self.build_temp, + ) + compiler.link_executable( + objects, + exe.name, + output_dir=self.build_scripts, + extra_postargs=exe.extra_link_args, + ) + + def get_outputs(self): + return [ + os.path.join(self.build_scripts, exe.name) + for exe in self.distribution.c_executables + ] + + +setup( + name='dumb-init', + description='Simple wrapper script which proxies signals to a child', + version='0.0.2', + author='Yelp', + platforms='linux', + + c_executables=[ + Extension( + 'dumb-init', + ['dumb-init.c'], + extra_link_args=['-static'], + ), + ], + cmdclass={ + 'build': build, + 'build_cexe': build_cexe, + 'install': install, + 'install_cexe': install_cexe, + }, + distclass=ExeDistribution, +) diff --git a/test b/test index f599877..421b346 100755 --- a/test +++ b/test @@ -1,8 +1,19 @@ -#!/bin/sh -eux +#!/bin/bash -eux +if [ "$#" -eq 1 ]; then + dumb_init_bin=$(readlink -f "$1") +else + dumb_init_bin=$(which dumb-init) || { + echo "Couldn't find dumb-init on your path, exiting." + exit 1 + } +fi + +echo "Running with dumb-init at '$dumb_init_bin'" + run_tests() { - ./test-proxies-signals - ./test-exit-status - ./test-help-message + ./test-proxies-signals "$dumb_init_bin" + ./test-exit-status "$dumb_init_bin" + ./test-help-message "$dumb_init_bin" } cd tests diff --git a/tests/lib/print-signals b/tests/lib/print-signals index db024ba..cc4e6ce 100755 --- a/tests/lib/print-signals +++ b/tests/lib/print-signals @@ -1,14 +1,15 @@ -#!/bin/sh -eu +#!/bin/bash -eux # Print received signals into a file, one per line file="$1" . ./lib/testlib.sh for i in $(catchable_signals); do - trap "echo $i > \"$file\"" "$i" + trap "echo $i > \"$file\"" "$i" done echo 'ready' > "$file" -# loop forever +echo 'loop forever...' +set +x while :; do true; done diff --git a/tests/lib/testlib.sh b/tests/lib/testlib.sh index f20d67a..bdff385 100644 --- a/tests/lib/testlib.sh +++ b/tests/lib/testlib.sh @@ -1,4 +1,4 @@ catchable_signals() { - # We can't handle the signals SIGKILL=9, SIGCHLD=17, SIGSTOP=19 - seq 1 31 | grep -vE '^(9|17|19)$' + # We can't handle the signals SIGKILL=9, SIGCHLD=17, SIGSTOP=19 + seq 1 31 | grep -vE '^(9|17|19)$' } diff --git a/tests/test-exit-status b/tests/test-exit-status index 364515f..597b412 100755 --- a/tests/test-exit-status +++ b/tests/test-exit-status @@ -1,10 +1,12 @@ -#!/bin/sh -u +#!/bin/bash -eux +dumb_init="$1" + # 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 $?) + status=$($dumb_init sh -c "exit $i"; echo $?) - if [ "$status" -ne "$i" ]; then - echo "Error: Expected exit status $i, got $status." - exit 1 - fi + 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 index f08673d..b1c45a1 100755 --- a/tests/test-help-message +++ b/tests/test-help-message @@ -1,18 +1,20 @@ -#!/bin/sh -u +#!/bin/bash -eux # dumb-init should say something useful when called with no arguments, and exit # nonzero. -status=$(dumb-init > /dev/null 2>&1; echo $?) +dumb_init="$1" + +status=$($dumb_init > /dev/null 2>&1; echo $?) if [ "$status" -ne 0 ]; then - msg=$(dumb-init 2>&1 || true) - msg_len=${#msg} + 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 + 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 + echo "Error: Expected dumb-init with no arguments to return nonzero, but it returned ${status}." + exit 1 fi diff --git a/tests/test-proxies-signals b/tests/test-proxies-signals index 134af97..32bc10e 100755 --- a/tests/test-proxies-signals +++ b/tests/test-proxies-signals @@ -1,5 +1,6 @@ -#!/bin/sh -eum +#!/bin/bash -euxm # dumb-init should proxy all possible signals to the child process. +dumb_init="$1" # Try sending all signals via dumb-init to our `print-signals` script, ensure # they were all received. @@ -11,31 +12,35 @@ fifo=$(mktemp -u) mkfifo -m 600 "$fifo" read_cmd="timeout 1 head -n1 $fifo" -dumb-init ./lib/print-signals "$fifo" & +$dumb_init ./lib/print-signals "$fifo" & pid="$!" # Wait for `print-signals` to indicate it's ready. $read_cmd > /dev/null for expected in $(catchable_signals); do - kill -s "$expected" "$pid" - echo -n "Sent signal ${expected}... " - received=$($read_cmd) || { - echo - echo "Error: Didn't receive signal within 1 second." - exit 1 - } + kill -s "$expected" "$pid" + echo -n "Sent signal ${expected}... " + received=$($read_cmd) || { + echo + echo "Error: Didn't receive signal within 1 second." + exit 1 + } - echo "received signal ${received}." + echo "received signal ${received}." - if [ "$expected" -ne "$received" ]; then - echo "Error: Received signal $received, but expected signal $expected." - exit 1 - fi + 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" +# $pid is the PID of the dumb-init process. Since print-signals ignores all +# signals, we need to `kill -9` it. If we just `kill -9` the dumb-init process, +# `print-signals` will still be running. So we instead kill children of the +# dumb-init process. +pkill -9 -P "$pid" rm "$fifo" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..63d6312 --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +[tox] +envlist = py27,py34 + +[testenv] +deps = pre-commit>=0.5.0 +commands = + ./test + pre-commit run --all-files + +[testenv:pre-commit] +commands = pre-commit {posargs}