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
dist/
*.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>
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

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

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).
### 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

2
debian/control vendored
View file

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

3
debian/rules vendored
View file

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

View file

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

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() {
./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

View file

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

View file

@ -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)$'
}

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

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

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="$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"

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}