Compare commits

..

1 commit

Author SHA1 Message Date
Chris Kuehl
b2203fd5b1 WIP fix race 2018-08-01 14:23:29 -07:00
31 changed files with 247 additions and 377 deletions

View file

@ -1,60 +0,0 @@
name: CI
on: push
jobs:
build-and-test:
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
manylinux_arch: x86_64
docker_image: debian:buster
- arch: arm64
manylinux_arch: aarch64
docker_image: arm64v8/debian:buster
- arch: ppc64le
manylinux_arch: ppc64le
docker_image: ppc64le/debian:buster
- arch: s390x
manylinux_arch: s390x
docker_image: s390x/debian:buster
env:
BASE_IMAGE: ${{ matrix.docker_image }}
steps:
- uses: actions/checkout@v2
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
if: ${{ matrix.arch != 'amd64' }}
with:
image: tonistiigi/binfmt:latest
- name: Build Docker image
run: make docker-image
- name: Run python tests
run: docker run --rm -v $(pwd):/mnt:rw dumb-init-build /mnt/ci/docker-python-test
- name: Build Debian package
run: docker run --init --rm -v $(pwd):/mnt:rw dumb-init-build make -C /mnt builddeb
- name: Test built Debian package
# XXX: This uses the clean base image (not the build one) to make
# sure it installs in a clean image without any hidden dependencies.
run: docker run --rm -v $(pwd):/mnt:rw ${{ matrix.docker_image }} /mnt/ci/docker-deb-test
- name: Build wheels
run: sudo make python-dists-${{ matrix.manylinux_arch }}
- name: Upload build artifacts
uses: actions/upload-artifact@v2
with:
name: ${{ matrix.arch }}
path: dist

View file

@ -1,7 +1,8 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v1.2.3
hooks:
- id: autopep8-wrapper
- id: check-added-large-files
- id: check-docstring-first
- id: check-executables-have-shebangs
@ -11,33 +12,15 @@ repos:
- 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/pre-commit/mirrors-autopep8
rev: v2.0.0
hooks:
- id: autopep8
- repo: https://github.com/pycqa/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0
rev: v1.0.1
hooks:
- id: reorder-python-imports
args: ['--py3-plus']
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.3.1
rev: v1.1.5
hooks:
- id: remove-tabs
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.0
hooks:
- id: pyupgrade
args: ['--py3-plus']
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.3.0
hooks:
- id: add-trailing-comma
args: ['--py36-plus']

19
.travis.yml Normal file
View file

@ -0,0 +1,19 @@
language: c
services:
- docker
matrix:
include:
- env: ITEST_TARGET=itest_trusty
- env: ITEST_TARGET=itest_xenial
- env: ITEST_TARGET=itest_bionic
- env: ITEST_TARGET=itest_stretch
- env: ITEST_TARGET=itest_tox
- os: linux-ppc64le
env: ITEST_TARGET=itest_stretch
script:
- make "$ITEST_TARGET"
after_script:
- ci/artifact-upload

View file

@ -29,11 +29,11 @@ The process to release a new version is:
4. Commit the changes and tag the commit like `v1.0.0`.
5. `git push --tags origin master`
6. Wait for Travis to run, then find and download the binary and Debian
packages for all architectures; there will be links printed at the
end of the Travis output. Put these into your `dist` directory.
packages for both amd64 and ppc64el; there will be links printed at the end
of the Travis output. Put these into your `dist` directory.
7. Run `make release`
8. Run `twine upload --skip-existing dist/*.tar.gz dist/*.whl` to upload the
new version to PyPI
9. Upload the resulting Debian packages, binaries, and sha256sums file (all
inside the `dist` directory) to a new [GitHub
9. Upload the resulting Debian package, binary (inside the `dist` directory),
and sha256sums file to a new [GitHub
release](https://github.com/Yelp/dumb-init/releases)

View file

@ -1,28 +1,24 @@
ARG BASE_IMAGE=debian:buster
FROM $BASE_IMAGE
FROM debian:stretch
LABEL maintainer="Chris Kuehl <ckuehl@yelp.com>"
# The default mirrors are too flaky to run reliably in CI.
RUN sed -E \
'/security\.debian/! s@http://[^/]+/@http://mirrors.kernel.org/@' \
-i /etc/apt/sources.list
# Install the bare minimum dependencies necessary for working with Debian
# packages. Build dependencies should be added under "Build-Depends" inside
# debian/control instead.
RUN : \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
devscripts \
equivs \
lintian \
python3-distutils \
python3-setuptools \
python3-pip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /tmp/mnt
&& rm -rf /var/lib/apt/lists/* && apt-get clean
WORKDIR /mnt
COPY debian/control /control
RUN : \
&& apt-get update \
&& mk-build-deps --install --tool 'apt-get -y --no-install-recommends' /control \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
ENTRYPOINT apt-get update && \
mk-build-deps -i --tool 'apt-get --no-install-recommends -y' && \
make builddeb

View file

@ -26,21 +26,18 @@ clean-tox:
.PHONY: release
release: python-dists
cd dist && \
sha256sum --binary dumb-init_$(VERSION)_amd64.deb dumb-init_$(VERSION)_x86_64 dumb-init_$(VERSION)_ppc64el.deb dumb-init_$(VERSION)_ppc64le dumb-init_$(VERSION)_s390x.deb dumb-init_$(VERSION)_s390x dumb-init_$(VERSION)_arm64.deb dumb-init_$(VERSION)_aarch64 \
sha256sum --binary dumb-init_$(VERSION)_amd64.deb dumb-init_$(VERSION)_amd64 dumb-init_$(VERSION)_ppc64el.deb dumb-init_$(VERSION)_ppc64el \
> sha256sums
.PHONY: python-dists
python-dists: python-dists-x86_64 python-dists-aarch64 python-dists-ppc64le python-dists-s390x
.PHONY: python-dists-%
python-dists-%: VERSION.h
python-dists: VERSION.h
python setup.py sdist
docker run \
--user $$(id -u):$$(id -g) \
-v `pwd`/dist:/dist:rw \
quay.io/pypa/manylinux2014_$*:latest \
-v $(PWD)/dist:/dist:rw \
quay.io/pypa/manylinux1_x86_64:latest \
bash -exc ' \
/opt/python/cp38-cp38/bin/pip wheel --wheel-dir /tmp /dist/*.tar.gz && \
/opt/python/cp35-cp35m/bin/pip wheel --wheel-dir /tmp /dist/*.tar.gz && \
auditwheel repair --wheel-dir /dist /tmp/*.whl --wheel-dir /dist \
'
@ -52,16 +49,16 @@ builddeb:
# Extract the built binary from the Debian package
dpkg-deb --fsys-tarfile dist/dumb-init_$(VERSION)_$(shell dpkg --print-architecture).deb | \
tar -C dist --strip=3 -xvf - ./usr/bin/dumb-init
mv dist/dumb-init dist/dumb-init_$(VERSION)_$(shell uname -m)
mv dist/dumb-init dist/dumb-init_$(VERSION)_$(shell dpkg --print-architecture)
.PHONY: builddeb-docker
builddeb-docker: docker-image
mkdir -p dist
docker run --init --user $$(id -u):$$(id -g) -v $(PWD):/tmp/mnt dumb-init-build make builddeb
docker run -v $(PWD):/mnt dumb-init-build
.PHONY: docker-image
docker-image:
docker build $(if $(BASE_IMAGE),--build-arg BASE_IMAGE=$(BASE_IMAGE)) -t dumb-init-build .
docker build -t dumb-init-build .
.PHONY: test
test:
@ -71,3 +68,25 @@ test:
.PHONY: install-hooks
install-hooks:
tox -e pre-commit -- install -f --install-hooks
ITEST_TARGETS = itest_trusty itest_xenial itest_bionic itest_stretch
.PHONY: itest $(ITEST_TARGETS)
itest: $(ITEST_TARGETS)
itest_trusty: _itest-ubuntu-trusty
itest_xenial: _itest-ubuntu-xenial
itest_bionic: _itest-ubuntu-bionic
itest_stretch: _itest-debian-stretch
itest_tox:
$(DOCKER_RUN_TEST) ubuntu:bionic /mnt/ci/docker-tox-test
_itest-%: _itest_deb-% _itest_python-%
@true
_itest_python-%:
$(DOCKER_RUN_TEST) $(shell sed 's/-/:/' <<< "$*") /mnt/ci/docker-python-test
_itest_deb-%: builddeb-docker
$(DOCKER_RUN_TEST) $(shell sed 's/-/:/' <<< "$*") /mnt/ci/docker-deb-test

View file

@ -1,6 +1,7 @@
dumb-init
========
[![Travis CI](https://travis-ci.org/Yelp/dumb-init.svg?branch=master)](https://travis-ci.org/Yelp/dumb-init/)
[![PyPI version](https://badge.fury.io/py/dumb-init.svg)](https://pypi.python.org/pypi/dumb-init)
@ -173,7 +174,7 @@ If you don't have an internal apt server, you can use `dpkg -i` to install the
One possibility is with the following commands in your Dockerfile:
```Dockerfile
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_amd64.deb
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64.deb
RUN dpkg -i dumb-init_*.deb
```
@ -184,7 +185,7 @@ Since dumb-init is released as a statically-linked binary, you can usually just
plop it into your images. Here's an example of doing that in a Dockerfile:
```Dockerfile
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.1/dumb-init_1.2.1_amd64
RUN chmod +x /usr/local/bin/dumb-init
```

View file

@ -1 +1 @@
1.2.5
1.2.1

View file

@ -1,6 +1,6 @@
// THIS FILE IS AUTOMATICALLY GENERATED
// Run `make VERSION.h` to update it after modifying VERSION.
unsigned char VERSION[] = {
0x31, 0x2e, 0x32, 0x2e, 0x35, 0x0a
0x31, 0x2e, 0x32, 0x2e, 0x31, 0x0a
};
unsigned int VERSION_len = 6;

18
ci/docker Normal file
View file

@ -0,0 +1,18 @@
# The default mirrors are too flaky to run reliably in CI.
sed -E \
'/security\.debian/! s@http://[^/]+/@http://mirrors.kernel.org/@' \
-i /etc/apt/sources.list
apt-get update
apt-get install -y --no-install-recommends \
build-essential \
procps \
python \
python-dev \
python-pip \
python-setuptools
cp -r /mnt/ /test
cd /test
# vim: ft=sh

View file

@ -1,12 +1,10 @@
#!/bin/bash -eux
set -o pipefail
apt-get update
apt-get -y --no-install-recommends install python3-pip procps
. /mnt/ci/docker
cd /mnt
dpkg -i dist/*.deb
pip3 install -r requirements-dev.txt
pytest tests/
pip install -r requirements-dev.txt
py.test tests/
exec dumb-init /mnt/tests/test-zombies

View file

@ -1,12 +1,12 @@
#!/bin/bash -eux
set -euo pipefail
set -o pipefail
cd /mnt
. /mnt/ci/docker
python3 setup.py clean
python3 setup.py sdist
pip3 install -vv dist/*.tar.gz
pip3 install -r requirements-dev.txt
pytest-3 -vv tests/
python setup.py clean
python setup.py sdist
pip install -vv dist/*.tar.gz
pip install -r requirements-dev.txt
py.test tests/
exec dumb-init /mnt/tests/test-zombies
exec dumb-init /mnt/tests/test-zombies \

13
ci/docker-tox-test Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash -eux
set -o pipefail
. /mnt/ci/docker
apt-get update
apt-get install -y --no-install-recommends \
git \
python2.7-dev \
python3.6-dev \
tox
tox

43
debian/changelog vendored
View file

@ -1,46 +1,3 @@
dumb-init (1.2.5) unstable; urgency=medium
* Change the working directory in the parent process to "/" after forking.
https://github.com/Yelp/dumb-init/pull/210
Thanks to @Villemoes for the patch!
-- Chris Kuehl <ckuehl@yelp.com> Thu, 10 Dec 2020 10:54:47 -0800
dumb-init (1.2.4) unstable; urgency=medium
* Actually fix the bug that can cause `--help` or `--version` to crash in
some scenarios.
https://github.com/Yelp/dumb-init/pull/215
Thanks to @suve for the patch!
-- Chris Kuehl <ckuehl@yelp.com> Mon, 07 Dec 2020 11:58:06 -0800
dumb-init (1.2.3) unstable; urgency=medium
* Fix a bug that can cause `--help` or `--version` to crash in some
scenarios.
https://github.com/Yelp/dumb-init/pull/213
Thanks to @suve for the patch!
-- Chris Kuehl <ckuehl@yelp.com> Wed, 02 Dec 2020 10:43:02 -0800
dumb-init (1.2.2) unstable; urgency=medium
* Fix a race condition which can cause the child to receive SIGHUP and
SIGCONT very shortly after start (#174).
In general this was very rare, but some environments (especially some
container and virtualization environments) appear to encounter it at a
much higher rate, possibly due to scheduler quirks.
-- Chris Kuehl <ckuehl@yelp.com> Wed, 01 Aug 2018 16:36:22 -0700
dumb-init (1.2.1) unstable; urgency=medium
* Fix verbose debug logging for ignored signals.

6
debian/control vendored
View file

@ -2,14 +2,16 @@ Source: dumb-init
Section: utils
Priority: extra
Maintainer: Chris Kuehl <ckuehl@yelp.com>
Uploaders: Kent Wills <rkwills@yelp.com>
Build-Depends:
debhelper (>= 9),
help2man,
musl-tools,
## Tests:
procps,
python3,
python3-pytest,
python,
python-mock,
python-pytest,
Standards-Version: 3.9.7
Homepage: https://github.com/Yelp/dumb-init
Vcs-Browser: https://github.com/Yelp/dumb-init

2
debian/rules vendored
View file

@ -31,4 +31,4 @@ override_dh_builddeb:
override_dh_auto_test:
find . -name '*.pyc' -delete
find . -name '__pycache__' -delete
PATH=.:$$PATH timeout --signal=KILL 60 pytest-3 -vv tests/
PATH=.:$$PATH timeout --signal=KILL 60 py.test -vv tests/

View file

@ -94,7 +94,7 @@ void handle_signal(int signum) {
DEBUG("Received signal %d.\n", signum);
if (signal_temporary_ignores[signum] == 1) {
DEBUG("Ignoring tty hand-off signal %d.\n", signum);
DEBUG("Ignoring signal %d during its first receive.\n", signum);
signal_temporary_ignores[signum] = 0;
} else if (signum == SIGCHLD) {
int status, exit_status;
@ -126,7 +126,7 @@ void handle_signal(int signum) {
void print_help(char *argv[]) {
fprintf(stderr,
"dumb-init v%.*s"
"dumb-init v%s"
"Usage: %s [option] command [[arg] ...]\n"
"\n"
"dumb-init is a simple process supervisor that forwards signals to children.\n"
@ -144,7 +144,7 @@ void print_help(char *argv[]) {
" -V, --version Print the current version and exit.\n"
"\n"
"Full help is available online at https://github.com/Yelp/dumb-init\n",
VERSION_len, VERSION,
VERSION,
argv[0]
);
}
@ -175,9 +175,8 @@ void parse_rewrite_signum(char *arg) {
}
void set_rewrite_to_sigstop_if_not_defined(int signum) {
if (signal_rewrite[signum] == -1) {
if (signal_rewrite[signum] == -1)
signal_rewrite[signum] = SIGSTOP;
}
}
char **parse_command(int argc, char *argv[]) {
@ -199,7 +198,7 @@ char **parse_command(int argc, char *argv[]) {
debug = 1;
break;
case 'V':
fprintf(stderr, "dumb-init v%.*s", VERSION_len, VERSION);
fprintf(stderr, "dumb-init v%s", VERSION);
exit(0);
case 'c':
use_setsid = 0;
@ -256,9 +255,8 @@ int main(int argc, char *argv[]) {
sigprocmask(SIG_BLOCK, &all_signals, NULL);
int i = 0;
for (i = 1; i <= MAXSIG; i++) {
for (i = 1; i <= MAXSIG; i++)
signal(i, dummy);
}
/*
* Detach dumb-init from controlling tty, so that the child's session can
@ -326,11 +324,6 @@ int main(int argc, char *argv[]) {
} else {
/* parent */
DEBUG("Child spawned with PID %d.\n", child_pid);
if (chdir("/") == -1) {
DEBUG("Unable to chdir(\"/\") (errno=%d %s)\n",
errno,
strerror(errno));
}
for (;;) {
int signum;
sigwait(&all_signals, &signum);

View file

@ -1,2 +1,2 @@
[pytest]
timeout = 20
timeout = 5

View file

@ -1,6 +1,4 @@
mock
pre-commit>=0.5.0
pytest
# TODO: This pin is to work around an issue where the system pytest is too old.
# We should fix this by not depending on the system pytest/python packages at
# some point.
pytest-timeout<2.0.0
pytest-timeout

View file

@ -1,9 +1,11 @@
from __future__ import print_function
import os.path
import subprocess
import tempfile
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
@ -56,8 +58,7 @@ class install_cexe(Command):
# 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'),
)
'install', ('install_scripts', 'install_dir'))
def run(self):
@ -123,7 +124,6 @@ setup(
author='Yelp',
url='https://github.com/Yelp/dumb-init/',
platforms='linux',
packages=[],
c_executables=[Extension('dumb-init', ['dumb-init.c'])],
cmdclass={
'bdist_wheel': bdist_wheel,

View file

@ -8,6 +8,9 @@ from contextlib import contextmanager
from subprocess import PIPE
from subprocess import Popen
from py._path.local import LocalPath
# these signals cause dumb-init to suspend itself
SUSPEND_SIGNALS = frozenset([
signal.SIGTSTP,
@ -17,8 +20,8 @@ SUSPEND_SIGNALS = frozenset([
NORMAL_SIGNALS = frozenset(
set(range(1, 32)) -
{signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD} -
SUSPEND_SIGNALS,
set([signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD]) -
SUSPEND_SIGNALS
)
@ -34,7 +37,7 @@ def print_signals(args=()):
stdout=PIPE,
)
line = proc.stdout.readline()
m = re.match(b'^ready \\(pid: ([0-9]+)\\)\n$', line)
m = re.match(b'^ready \(pid: ([0-9]+)\)\n$', line)
assert m, line
yield proc, m.group(1).decode('ascii')
@ -46,25 +49,15 @@ def print_signals(args=()):
def child_pids(pid):
"""Return a list of direct child PIDs for the given PID."""
children = set()
for p in os.listdir('/proc'):
for p in LocalPath('/proc').listdir():
try:
with open(os.path.join('/proc', p, 'stat')) as f:
stat = f.read()
m = re.match(
r'^\d+ \(.+?\) '
# This field, state, is normally a single letter, but can be
# "0" if there are some unusual security settings that prevent
# reading the process state (happens under GitHub Actions with
# QEMU for some reason).
'[0a-zA-Z] '
r'(\d+) ',
stat,
)
stat = open(p.join('stat').strpath).read()
m = re.match('^\d+ \(.+?\) [a-zA-Z] (\d+) ', stat)
assert m, stat
ppid = int(m.group(1))
if ppid == pid:
children.add(int(p))
except OSError:
children.add(int(p.basename))
except IOError:
# Happens when the process exits after listing it, or between
# opening stat and reading it.
pass
@ -74,23 +67,22 @@ def child_pids(pid):
def pid_tree(pid):
"""Return a list of all descendant PIDs for the given PID."""
children = child_pids(pid)
return {
return set(
pid
for child in children
for pid in pid_tree(child)
} | children
) | children
def is_alive(pid):
"""Return whether a process is running with the given PID."""
return os.path.isdir(os.path.join('/proc', str(pid)))
return LocalPath('/proc').join(str(pid)).isdir()
def process_state(pid):
"""Return a process' state, such as "stopped" or "running"."""
with open(os.path.join('/proc', str(pid), 'status')) as f:
status = f.read()
m = re.search(r'^State:\s+[A-Z] \(([a-z]+)\)$', status, re.MULTILINE)
status = LocalPath('/proc').join(str(pid), 'status').read()
m = re.search('^State:\s+[A-Z] \(([a-z]+)\)$', status, re.MULTILINE)
return m.group(1)

View file

@ -4,6 +4,8 @@
Since all signals are printed and otherwise ignored, you'll need to send
SIGKILL (kill -9) to this process to actually end it.
"""
from __future__ import print_function
import os
import signal
import sys
@ -11,7 +13,7 @@ import time
CATCHABLE_SIGNALS = frozenset(
set(range(1, 32)) - {signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD},
set(range(1, 32)) - set([signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD])
)
@ -20,7 +22,7 @@ last_signal = None
def unbuffered_print(line):
sys.stdout.write('{}\n'.format(line))
sys.stdout.write('{0}\n'.format(line))
sys.stdout.flush()
@ -32,7 +34,7 @@ if __name__ == '__main__':
for signum in CATCHABLE_SIGNALS:
signal.signal(signum, print_signal)
unbuffered_print('ready (pid: {})'.format(os.getpid()))
unbuffered_print('ready (pid: {0})'.format(os.getpid()))
# loop forever just printing signals
while True:

View file

@ -17,7 +17,7 @@ def spawn_and_kill_pipeline():
proc = Popen((
'dumb-init',
'sh', '-c',
"yes 'oh, hi' | tail & yes error | tail >&2",
"yes 'oh, hi' | tail & yes error | tail >&2"
))
def assert_living_pids():
@ -32,7 +32,7 @@ def spawn_and_kill_pipeline():
def living_pids(pids):
return {pid for pid in pids if is_alive(pid)}
return set(pid for pid in pids if is_alive(pid))
@pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled')
@ -80,7 +80,7 @@ def spawn_process_which_dies_with_children():
# we need to sleep before the shell exits, or dumb-init might send
# TERM to print_signals before it has had time to register custom
# signal handlers
'{python} -m testing.print_signals & sleep 1'.format(
'{python} -m testing.print_signals & sleep 0.1'.format(
python=sys.executable,
),
),
@ -91,7 +91,7 @@ def spawn_process_which_dies_with_children():
# read a line from print_signals, figure out its pid
line = proc.stdout.readline()
match = re.match(b'ready \\(pid: ([0-9]+)\\)\n', line)
match = re.match(b'ready \(pid: ([0-9]+)\)\n', line)
assert match, line
child_pid = int(match.group(1))
@ -129,14 +129,12 @@ def test_processes_dont_receive_term_on_exit_if_no_setsid():
os.kill(child_pid, signal.SIGKILL)
@pytest.mark.parametrize(
'args', [
('/doesnotexist',),
('--', '/doesnotexist'),
('-c', '/doesnotexist'),
('--single-child', '--', '/doesnotexist'),
],
)
@pytest.mark.parametrize('args', [
('/doesnotexist',),
('--', '/doesnotexist'),
('-c', '/doesnotexist'),
('--single-child', '--', '/doesnotexist'),
])
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_fails_nonzero_with_bad_exec(args):
"""If dumb-init can't exec as requested, it should exit nonzero."""

View file

@ -12,34 +12,12 @@ def current_version():
return open('VERSION').read().strip()
def normalize_stderr(stderr):
# dumb-init prints out argv[0] in its usage message. This should always be
# just "dumb-init" under regular test scenarios here since that is how we
# call it, but in CI the use of QEMU causes the argv[0] to be replaced with
# the full path.
#
# This is supposed to be avoidable by:
# 1) Setting the "P" flag in the binfmt register:
# https://en.wikipedia.org/wiki/Binfmt_misc#Registration
# This can be done by setting the QEMU_PRESERVE_PARENT env var when
# calling binfmt.
#
# 2) Setting the "QEMU_ARGV0" env var to empty string for *all*
# processes:
# https://bugs.launchpad.net/qemu/+bug/1835839
#
# I can get it working properly in CI outside of Docker, but for some
# reason not during Docker builds. This doesn't affect the built executable
# so I decided to just punt on it.
return re.sub(rb'(^|(?<=\s))[a-z/.]+/dumb-init', b'dumb-init', stderr)
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_no_arguments_prints_usage():
proc = Popen(('dumb-init'), stderr=PIPE)
_, stderr = proc.communicate()
assert proc.returncode != 0
assert normalize_stderr(stderr) == (
assert stderr == (
b'Usage: dumb-init [option] program [args]\n'
b'Try dumb-init --help for full usage.\n'
)
@ -50,7 +28,7 @@ def test_exits_invalid_with_invalid_args():
proc = Popen(('dumb-init', '--yolo', '/bin/true'), stderr=PIPE)
_, stderr = proc.communicate()
assert proc.returncode == 1
assert normalize_stderr(stderr) in (
assert stderr in (
b"dumb-init: unrecognized option '--yolo'\n", # glibc
b'dumb-init: unrecognized option: yolo\n', # musl
)
@ -65,7 +43,7 @@ def test_help_message(flag, current_version):
proc = Popen(('dumb-init', flag), stderr=PIPE)
_, stderr = proc.communicate()
assert proc.returncode == 0
assert normalize_stderr(stderr) == (
assert stderr == (
b'dumb-init v' + current_version.encode('ascii') + b'\n'
b'Usage: dumb-init [option] command [[arg] ...]\n'
b'\n'
@ -107,15 +85,15 @@ def test_verbose(flag):
assert stdout == b'oh, hi\n'
# child/parent race to print output after the fork(), can't guarantee exact order
assert re.search(b'(^|\n)\\[dumb-init\\] setsid complete\\.\n', stderr), stderr # child
assert re.search(b'(^|\n)\[dumb-init\] setsid complete\.\n', stderr), stderr # child
assert re.search( # parent
(
'(^|\n)\\[dumb-init\\] Child spawned with PID [0-9]+\\.\n'
'(^|\n)\[dumb-init\] Child spawned with PID [0-9]+\.\n'
'.*' # child might print here
'\\[dumb-init\\] Received signal {signal.SIGCHLD}\\.\n'
'\\[dumb-init\\] A child with PID [0-9]+ exited with exit status 0.\n'
'\\[dumb-init\\] Forwarded signal 15 to children\\.\n'
'\\[dumb-init\\] Child exited with status 0\\. Goodbye\\.\n$'
'\[dumb-init\] Received signal {signal.SIGCHLD}\.\n'
'\[dumb-init\] A child with PID [0-9]+ exited with exit status 0.\n'
'\[dumb-init\] Forwarded signal 15 to children\.\n'
'\[dumb-init\] Child exited with status 0\. Goodbye\.\n$'
).format(signal=signal).encode('utf8'),
stderr,
re.DOTALL,
@ -132,30 +110,28 @@ def test_verbose_and_single_child(flag1, flag2):
assert stdout == b'oh, hi\n'
assert re.match(
(
'^\\[dumb-init\\] Child spawned with PID [0-9]+\\.\n'
'\\[dumb-init\\] Received signal {signal.SIGCHLD}\\.\n'
'\\[dumb-init\\] A child with PID [0-9]+ exited with exit status 0.\n'
'\\[dumb-init\\] Forwarded signal 15 to children\\.\n'
'\\[dumb-init\\] Child exited with status 0\\. Goodbye\\.\n$'
'^\[dumb-init\] Child spawned with PID [0-9]+\.\n'
'\[dumb-init\] Received signal {signal.SIGCHLD}\.\n'
'\[dumb-init\] A child with PID [0-9]+ exited with exit status 0.\n'
'\[dumb-init\] Forwarded signal 15 to children\.\n'
'\[dumb-init\] Child exited with status 0\. Goodbye\.\n$'
).format(signal=signal).encode('utf8'),
stderr,
)
@pytest.mark.parametrize(
'extra_args', [
('-r',),
('-r', ''),
('-r', 'herp'),
('-r', 'herp:derp'),
('-r', '15'),
('-r', '15::12'),
('-r', '15:derp'),
('-r', '15:12', '-r'),
('-r', '15:12', '-r', '0'),
('-r', '15:12', '-r', '1:32'),
],
)
@pytest.mark.parametrize('extra_args', [
('-r',),
('-r', ''),
('-r', 'herp'),
('-r', 'herp:derp'),
('-r', '15'),
('-r', '15::12'),
('-r', '15:derp'),
('-r', '15:12', '-r'),
('-r', '15:12', '-r', '0'),
('-r', '15:12', '-r', '1:32'),
])
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_rewrite_errors(extra_args):
proc = Popen(

View file

@ -1,6 +1,6 @@
import os
from unittest import mock
import mock
import pytest

View file

@ -1,28 +0,0 @@
import os
import shutil
from subprocess import PIPE
from subprocess import run
import pytest
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_working_directories():
"""The child process must start in the working directory in which
dumb-init was invoked, but dumb-init itself should not keep a
reference to that."""
# We need absolute path to dumb-init since we pass cwd=/tmp to get
# predictable output - so we can't rely on dumb-init being found
# in the "." directory.
dumb_init = os.path.realpath(shutil.which('dumb-init'))
proc = run(
(
dumb_init,
'sh', '-c', 'readlink /proc/$PPID/cwd && readlink /proc/$$/cwd',
),
cwd='/tmp', stdout=PIPE, stderr=PIPE,
)
assert proc.returncode == 0
assert proc.stdout == b'/\n/tmp\n'

View file

@ -11,19 +11,17 @@ def test_exit_status_regular_exit(exit_status):
"""dumb-init should exit with the same exit status as the process that it
supervises when that process exits normally.
"""
proc = Popen(('dumb-init', 'sh', '-c', 'exit {}'.format(exit_status)))
proc = Popen(('dumb-init', 'sh', '-c', 'exit {0}'.format(exit_status)))
proc.wait()
assert proc.returncode == exit_status
@pytest.mark.parametrize(
'signal', [
signal.SIGTERM,
signal.SIGHUP,
signal.SIGQUIT,
signal.SIGKILL,
],
)
@pytest.mark.parametrize('signal', [
signal.SIGTERM,
signal.SIGHUP,
signal.SIGQUIT,
signal.SIGKILL,
])
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_exit_status_terminated_by_signal(signal):
"""dumb-init should exit with status 128 + signal when the child process is
@ -31,10 +29,8 @@ def test_exit_status_terminated_by_signal(signal):
"""
# We use Python because sh is "dash" on Debian and "bash" on others.
# https://github.com/Yelp/dumb-init/issues/115
proc = Popen((
'dumb-init', sys.executable, '-c', 'import os; os.kill(os.getpid(), {})'.format(
signal,
),
))
proc = Popen(('dumb-init', sys.executable, '-c', 'import os; os.kill(os.getpid(), {0})'.format(
signal,
)))
proc.wait()
assert proc.returncode == 128 + signal

View file

@ -15,54 +15,52 @@ def test_proxies_signals():
with print_signals() as (proc, _):
for signum in NORMAL_SIGNALS:
proc.send_signal(signum)
assert proc.stdout.readline() == '{}\n'.format(signum).encode('ascii')
assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii')
def _rewrite_map_to_args(rewrite_map):
return chain.from_iterable(
('-r', '{}:{}'.format(src, dst)) for src, dst in rewrite_map.items()
('-r', '{0}:{1}'.format(src, dst)) for src, dst in rewrite_map.items()
)
@pytest.mark.parametrize(
'rewrite_map,sequence,expected', [
(
{},
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
),
@pytest.mark.parametrize('rewrite_map,sequence,expected', [
(
{},
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
),
(
{signal.SIGTERM: signal.SIGINT},
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
[signal.SIGINT, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
),
(
{signal.SIGTERM: signal.SIGINT},
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
[signal.SIGINT, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
),
(
{
signal.SIGTERM: signal.SIGINT,
signal.SIGINT: signal.SIGTERM,
signal.SIGQUIT: signal.SIGQUIT,
},
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
[signal.SIGINT, signal.SIGQUIT, signal.SIGCONT, signal.SIGTERM],
),
(
{
signal.SIGTERM: signal.SIGINT,
signal.SIGINT: signal.SIGTERM,
signal.SIGQUIT: signal.SIGQUIT,
},
[signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT],
[signal.SIGINT, signal.SIGQUIT, signal.SIGCONT, signal.SIGTERM],
),
# Lowest possible and highest possible signals.
(
{1: 31, 31: 1},
[1, 31],
[31, 1],
),
],
)
# Lowest possible and highest possible signals.
(
{1: 31, 31: 1},
[1, 31],
[31, 1],
),
])
@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_proxies_signals_with_rewrite(rewrite_map, sequence, expected):
"""Ensure dumb-init can rewrite signals."""
with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _):
for send, expect_receive in zip(sequence, expected):
proc.send_signal(send)
assert proc.stdout.readline() == '{}\n'.format(expect_receive).encode('ascii')
assert proc.stdout.readline() == '{0}\n'.format(expect_receive).encode('ascii')
@pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled')
@ -80,12 +78,12 @@ def test_default_rewrites_can_be_overriden_with_setsid_enabled():
assert process_state(proc.pid) in ['running', 'sleeping']
proc.send_signal(send)
assert proc.stdout.readline() == '{}\n'.format(expect_receive).encode('ascii')
assert proc.stdout.readline() == '{0}\n'.format(expect_receive).encode('ascii')
os.waitpid(proc.pid, os.WUNTRACED)
assert process_state(proc.pid) == 'stopped'
proc.send_signal(signal.SIGCONT)
assert proc.stdout.readline() == '{}\n'.format(signal.SIGCONT).encode('ascii')
assert proc.stdout.readline() == '{0}\n'.format(signal.SIGCONT).encode('ascii')
assert process_state(proc.pid) in ['running', 'sleeping']
@ -100,8 +98,8 @@ def test_ignored_signals_are_not_proxied():
with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _):
proc.send_signal(signal.SIGTERM)
proc.send_signal(signal.SIGINT)
assert proc.stdout.readline() == '{}\n'.format(signal.SIGQUIT).encode('ascii')
assert proc.stdout.readline() == '{0}\n'.format(signal.SIGQUIT).encode('ascii')
proc.send_signal(signal.SIGWINCH)
proc.send_signal(signal.SIGHUP)
assert proc.stdout.readline() == '{}\n'.format(signal.SIGHUP).encode('ascii')
assert proc.stdout.readline() == '{0}\n'.format(signal.SIGHUP).encode('ascii')

View file

@ -31,7 +31,7 @@ def test_shell_background_support_setsid():
# and then both wake up again
proc.send_signal(SIGCONT)
assert (
proc.stdout.readline() == '{}\n'.format(SIGCONT).encode('ascii')
proc.stdout.readline() == '{0}\n'.format(SIGCONT).encode('ascii')
)
assert process_state(pid) in ['running', 'sleeping']
assert process_state(proc.pid) in ['running', 'sleeping']
@ -46,12 +46,12 @@ def test_shell_background_support_without_setsid():
for signum in SUSPEND_SIGNALS:
assert process_state(proc.pid) in ['running', 'sleeping']
proc.send_signal(signum)
assert proc.stdout.readline() == '{}\n'.format(signum).encode('ascii')
assert proc.stdout.readline() == '{0}\n'.format(signum).encode('ascii')
os.waitpid(proc.pid, os.WUNTRACED)
assert process_state(proc.pid) == 'stopped'
proc.send_signal(SIGCONT)
assert (
proc.stdout.readline() == '{}\n'.format(SIGCONT).encode('ascii')
proc.stdout.readline() == '{0}\n'.format(SIGCONT).encode('ascii')
)
assert process_state(proc.pid) in ['running', 'sleeping']

View file

@ -77,7 +77,7 @@ def test_child_gets_controlling_tty_if_we_had_one():
output = readall(sfd)
assert os.waitpid(pid, 0) == (pid, 0), output
m = re.search(b'flags are: \\[\\[([a-zA-Z]+)\\]\\]\n', output)
m = re.search(b'flags are: \[\[([a-zA-Z]+)\]\]\n', output)
assert m, output
# "m" is job control
@ -111,8 +111,8 @@ def test_sighup_sigcont_ignored_if_was_session_leader():
output = readall(fd).decode('UTF-8')
assert 'Ignoring tty hand-off signal {}.'.format(signal.SIGHUP) in output
assert 'Ignoring tty hand-off signal {}.'.format(signal.SIGCONT) in output
assert 'Ignoring signal {} during its first receive.'.format(signal.SIGHUP) in output
assert 'Ignoring signal {} during its first receive.'.format(signal.SIGCONT) in output
assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGHUP) in output
assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGCONT) not in output

View file

@ -1,21 +1,20 @@
[tox]
envlist = py38,gcov
envlist = py27,py36,gcov
[testenv]
deps = -r{toxinidir}/requirements-dev.txt
commands =
pytest
python -m pytest
[testenv:gcov]
skip_install = True
basepython = /usr/bin/python3.8
commands =
{toxinidir}/ci/gcov-build {envbindir}
{[testenv]commands}
{toxinidir}/ci/gcov-report
[testenv:pre-commit]
basepython = /usr/bin/python3.8
basepython = /usr/bin/python3.6
commands = pre-commit {posargs:run --all-files}
[flake8]