Merge branch 'ckuehl_python_package'

This commit is contained in:
Chris Kuehl 2015-08-11 15:29:22 -07:00
commit 6f6814e91b
17 changed files with 328 additions and 124 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
dumb-init dumb-init
dist/ dist/
*.deb *.deb
*.egg-info
.tox

26
.pre-commit-config.yaml Normal file
View file

@ -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

View file

@ -2,9 +2,9 @@ FROM debian:jessie
MAINTAINER Chris Kuehl <ckuehl@yelp.com> MAINTAINER Chris Kuehl <ckuehl@yelp.com>
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
build-essential devscripts equivs && \ build-essential devscripts equivs && \
apt-get clean apt-get clean
WORKDIR /mnt WORKDIR /mnt
ENTRYPOINT mk-build-deps -i && make builddeb ENTRYPOINT mk-build-deps -i && make builddeb

1
MANIFEST.in Normal file
View file

@ -0,0 +1 @@
include dumb-init.c

View file

@ -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 .PHONY: build
build: build:
$(CC) -static -Wall -Werror -o dumb-init dumb-init.c $(CC) -static -Wall -Werror -o dumb-init dumb-init.c
.PHONY: clean .PHONY: clean
clean: clean: clean-tox
rm -rf dumb-init dist/ *.deb rm -rf dumb-init dist/ *.deb
.PHONY: clean-tox
clean-tox:
rm -rf .tox
.PHONY: builddeb .PHONY: builddeb
builddeb: builddeb:
debuild -us -uc -b debuild -us -uc -b
@ -22,24 +40,31 @@ builddeb-docker: docker-image
docker-image: docker-image:
docker build -t dumb-init-build . 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 .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: itest_lucid itest_precise itest_trusty itest_wheezy itest_jessie itest_stretch
itest_lucid: builddeb-docker itest_lucid: _itest-ubuntu-lucid
docker run -v $(PWD):/mnt:ro ubuntu:lucid \ itest_precise: _itest-ubuntu-precise
sh -ec "apt-get -y install timeout; $(DOCKER_TEST)" itest_trusty: _itest-ubuntu-trusty
itest_wheezy: _itest-debian-wheezy
itest_jessie: _itest-debian-jessie
itest_stretch: _itest-debian-stretch
itest_precise: builddeb-docker _itest-%: _itest_deb-% _itest_python-%
docker run -v $(PWD):/mnt:ro ubuntu:precise $(DOCKER_TEST) @true
itest_trusty: builddeb-docker _itest_python-%:
docker run -v $(PWD):/mnt:ro ubuntu:trusty $(DOCKER_TEST) $(eval DOCKER_IMG := $(shell echo $@ | cut -d- -f2 | sed 's/-/:/'))
$(DOCKER_RUN_TEST) $(DOCKER_IMG) $(DOCKER_PYTHON_TEST)
itest_wheezy: builddeb-docker _itest_deb-%:
docker run -v $(PWD):/mnt:ro debian:wheezy $(DOCKER_TEST) $(eval DOCKER_IMG := $(shell echo $@ | cut -d- -f2 | sed 's/-/:/'))
$(DOCKER_RUN_TEST) $(DOCKER_IMG) $(DOCKER_DEB_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)

View file

@ -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). (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 ## Usage
Once installed inside your Docker container, simply prefix your commands with Once installed inside your Docker container, simply prefix your commands with

2
debian/control vendored
View file

@ -2,7 +2,7 @@ Source: dumb-init
Section: utils Section: utils
Priority: extra Priority: extra
Maintainer: Chris Kuehl <ckuehl@yelp.com> Maintainer: Chris Kuehl <ckuehl@yelp.com>
Build-Depends: debhelper (>= 7), gcc, fakeroot Build-Depends: debhelper (>= 7), gcc, fakeroot, procps
Standards-Version: 3.9.6 Standards-Version: 3.9.6
Package: dumb-init Package: dumb-init

3
debian/rules vendored
View file

@ -4,3 +4,6 @@
override_dh_builddeb: override_dh_builddeb:
dh_builddeb -- -Zgzip dh_builddeb -- -Zgzip
override_dh_auto_test:
./test ./dumb-init

View file

@ -20,87 +20,87 @@ pid_t child = -1;
char debug = 0; char debug = 0;
void signal_handler(int signum) { void signal_handler(int signum) {
if (debug) if (debug)
fprintf(stderr, "Received signal %d.\n", signum); fprintf(stderr, "Received signal %d.\n", signum);
if (child > 0) { if (child > 0) {
kill(child, signum); kill(child, signum);
if (debug) if (debug)
fprintf(stderr, "Forwarded signal to child.\n"); fprintf(stderr, "Forwarded signal to child.\n");
} }
} }
void print_help(char *argv[]) { void print_help(char *argv[]) {
fprintf(stderr, fprintf(stderr,
"Usage: %s COMMAND [[ARG] ...]\n" "Usage: %s COMMAND [[ARG] ...]\n"
"\n" "\n"
"Docker runs your processes as PID1. The kernel doesn't apply default signal\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" "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" "signal handler, signals like TERM will just bounce off your process.\n"
"\n" "\n"
"This can result in cases where sending signals to a `docker run` process\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" "results in the run process exiting, but the container continuing in the\n"
"background.\n" "background.\n"
"\n" "\n"
"A workaround is to wrap your script in this proxy, which runs as PID1. Your\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" "process then runs as some other PID, and the kernel won't treat the signals\n"
"that are proxied to them specially.\n" "that are proxied to them specially.\n"
"\n" "\n"
"The proxy dies when your process dies, so it must not double-fork or do other\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" "weird things (this is basically a requirement for doing things sanely in\n"
"Docker anyway).\n", "Docker anyway).\n",
argv[0] argv[0]
); );
} }
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
int signum, exit_status, status = 0; int signum, exit_status, status = 0;
char *debug_env; char *debug_env;
if (argc < 2) { if (argc < 2) {
print_help(argv); print_help(argv);
return 1; return 1;
} }
debug_env = getenv("DUMB_INIT_DEBUG"); debug_env = getenv("DUMB_INIT_DEBUG");
if (debug_env && strcmp(debug_env, "1") == 0) if (debug_env && strcmp(debug_env, "1") == 0)
debug = 1; debug = 1;
/* register signal handlers */ /* register signal handlers */
for (signum = 1; signum < 32; signum++) { for (signum = 1; signum < 32; signum++) {
if (signum == SIGKILL || signum == SIGSTOP || signum == SIGCHLD) if (signum == SIGKILL || signum == SIGSTOP || signum == SIGCHLD)
continue; continue;
if (signal(signum, signal_handler) == SIG_ERR) { if (signal(signum, signal_handler) == SIG_ERR) {
fprintf(stderr, "Error: Couldn't register signal handler for signal `%d`. Exiting.\n", signum); fprintf(stderr, "Error: Couldn't register signal handler for signal `%d`. Exiting.\n", signum);
return 1; return 1;
} }
} }
/* launch our process */ /* launch our process */
child = fork(); child = fork();
if (child < 0) { if (child < 0) {
fprintf(stderr, "Unable to fork. Exiting.\n"); fprintf(stderr, "Unable to fork. Exiting.\n");
return 1; return 1;
} }
if (child == 0) { if (child == 0) {
execvp(argv[1], &argv[1]); execvp(argv[1], &argv[1]);
} else { } else {
if (debug) if (debug)
fprintf(stderr, "Child spawned with PID %d.\n", child); fprintf(stderr, "Child spawned with PID %d.\n", child);
waitpid(child, &status, 0); waitpid(child, &status, 0);
exit_status = WEXITSTATUS(status); exit_status = WEXITSTATUS(status);
if (debug) if (debug)
fprintf(stderr, "Child exited with status %d, goodbye.\n", exit_status); fprintf(stderr, "Child exited with status %d, goodbye.\n", exit_status);
return exit_status; return exit_status;
} }
return 0; return 0;
} }

107
setup.py Normal file
View file

@ -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,
)

19
test
View file

@ -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() { run_tests() {
./test-proxies-signals ./test-proxies-signals "$dumb_init_bin"
./test-exit-status ./test-exit-status "$dumb_init_bin"
./test-help-message ./test-help-message "$dumb_init_bin"
} }
cd tests cd tests

View file

@ -1,14 +1,15 @@
#!/bin/sh -eu #!/bin/bash -eux
# Print received signals into a file, one per line # Print received signals into a file, one per line
file="$1" file="$1"
. ./lib/testlib.sh . ./lib/testlib.sh
for i in $(catchable_signals); do for i in $(catchable_signals); do
trap "echo $i > \"$file\"" "$i" trap "echo $i > \"$file\"" "$i"
done done
echo 'ready' > "$file" echo 'ready' > "$file"
# loop forever echo 'loop forever...'
set +x
while :; do true; done while :; do true; done

View file

@ -1,4 +1,4 @@
catchable_signals() { catchable_signals() {
# We can't handle the signals SIGKILL=9, SIGCHLD=17, SIGSTOP=19 # We can't handle the signals SIGKILL=9, SIGCHLD=17, SIGSTOP=19
seq 1 31 | grep -vE '^(9|17|19)$' seq 1 31 | grep -vE '^(9|17|19)$'
} }

View file

@ -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. # dumb-init should exit with the same exit status as the process it launches.
for i in $(seq 0 255); do 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 if [ "$status" -ne "$i" ]; then
echo "Error: Expected exit status $i, got $status." echo "Error: Expected exit status $i, got $status."
exit 1 exit 1
fi fi
done done

View file

@ -1,18 +1,20 @@
#!/bin/sh -u #!/bin/bash -eux
# dumb-init should say something useful when called with no arguments, and exit # dumb-init should say something useful when called with no arguments, and exit
# nonzero. # 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 if [ "$status" -ne 0 ]; then
msg=$(dumb-init 2>&1 || true) msg=$($dumb_init 2>&1 || true)
msg_len=${#msg} msg_len=${#msg}
if [ "$msg_len" -le 50 ]; then 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." echo "Error: Expected dumb-init with no arguments to print a useful message, but it was only ${msg_len} chars long."
exit 1 exit 1
fi fi
else else
echo "Error: Expected dumb-init with no arguments to return nonzero, but it returned ${status}." echo "Error: Expected dumb-init with no arguments to return nonzero, but it returned ${status}."
exit 1 exit 1
fi fi

View file

@ -1,5 +1,6 @@
#!/bin/sh -eum #!/bin/bash -euxm
# dumb-init should proxy all possible signals to the child process. # 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 # Try sending all signals via dumb-init to our `print-signals` script, ensure
# they were all received. # they were all received.
@ -11,31 +12,35 @@ fifo=$(mktemp -u)
mkfifo -m 600 "$fifo" mkfifo -m 600 "$fifo"
read_cmd="timeout 1 head -n1 $fifo" read_cmd="timeout 1 head -n1 $fifo"
dumb-init ./lib/print-signals "$fifo" & $dumb_init ./lib/print-signals "$fifo" &
pid="$!" pid="$!"
# Wait for `print-signals` to indicate it's ready. # Wait for `print-signals` to indicate it's ready.
$read_cmd > /dev/null $read_cmd > /dev/null
for expected in $(catchable_signals); do for expected in $(catchable_signals); do
kill -s "$expected" "$pid" kill -s "$expected" "$pid"
echo -n "Sent signal ${expected}... " echo -n "Sent signal ${expected}... "
received=$($read_cmd) || { received=$($read_cmd) || {
echo echo
echo "Error: Didn't receive signal within 1 second." echo "Error: Didn't receive signal within 1 second."
exit 1 exit 1
} }
echo "received signal ${received}." echo "received signal ${received}."
if [ "$expected" -ne "$received" ]; then if [ "$expected" -ne "$received" ]; then
echo "Error: Received signal $received, but expected signal $expected." echo "Error: Received signal $received, but expected signal $expected."
exit 1 exit 1
fi fi
done done
# Turn off job monitoring so we don't get a spurious "[1] + Killed" printed # Turn off job monitoring so we don't get a spurious "[1] + Killed" printed
set +m 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" rm "$fifo"

11
tox.ini Normal file
View file

@ -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}