pax_global_header00006660000000000000000000000064137644674560014540gustar00rootroot0000000000000052 comment=89c1502b9d40b5cb4a844498b14d74ba1dd559bf dumb-init-1.2.5/000077500000000000000000000000001376446745600134355ustar00rootroot00000000000000dumb-init-1.2.5/.dockerignore000066400000000000000000000000121376446745600161020ustar00rootroot00000000000000.tox .git dumb-init-1.2.5/.gitignore000066400000000000000000000001341376446745600154230ustar00rootroot00000000000000*.deb *.egg-info *.gc* *.o *.py[cod] .pytest_cache .tox __pycache__/ build/ dist/ dumb-init dumb-init-1.2.5/.pre-commit-config.yaml000066400000000000000000000022541376446745600177210ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.2.3 hooks: - id: check-added-large-files - id: check-docstring-first - id: check-executables-have-shebangs - id: check-merge-conflict - 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 - id: fix-encoding-pragma args: ['--remove'] - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v1.4.4 hooks: - id: autopep8 - repo: https://github.com/asottile/reorder_python_imports rev: v1.6.0 hooks: - id: reorder-python-imports args: ['--py3-plus'] - repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.1.6 hooks: - id: remove-tabs - repo: https://github.com/asottile/pyupgrade rev: v1.21.0 hooks: - id: pyupgrade args: ['--py3-plus'] - repo: https://github.com/asottile/add-trailing-comma rev: v1.4.1 hooks: - id: add-trailing-comma args: ['--py36-plus'] dumb-init-1.2.5/.travis.yml000066400000000000000000000012151376446745600155450ustar00rootroot00000000000000language: c services: - docker matrix: include: - env: ITEST_TARGET=itest_focal - env: ITEST_TARGET=itest_buster - env: ITEST_TARGET=itest_tox - arch: ppc64le env: ITEST_TARGET=itest_buster - arch: arm64 env: ITEST_TARGET=itest_buster - arch: s390x env: ITEST_TARGET=itest_buster allow_failures: - arch: ppc64le env: ITEST_TARGET=itest_buster - arch: arm64 env: ITEST_TARGET=itest_buster - arch: s390x env: ITEST_TARGET=itest_buster script: - make "$ITEST_TARGET" after_script: - ci/artifact-upload dumb-init-1.2.5/CONTRIBUTING.md000066400000000000000000000031001376446745600156600ustar00rootroot00000000000000Contributing to dumb-init ======== `dumb-init` is primarily developed by [Yelp](https://yelp.github.io/), but contributions are welcome from everyone! Code is reviewed using GitHub pull requests. To make a contribution, you should: 1. Fork the GitHub repository 2. Push code to a branch on your fork 3. Create a pull request and wait for it to be reviewed We aim to have all dumb-init behavior covered by tests. If you make a change in behavior, please add a test to ensure it doesn't regress. We're also happy to help with suggestions on testing! ## Releasing new versions `dumb-init` uses [semantic versioning](http://semver.org/). If you're making a contribution, please don't bump the version number yourself—we'll take care of that after merging! The process to release a new version is: 1. Update the version in `VERSION` and run `make VERSION.h` 2. Update the Debian changelog with `dch -v {new version}`. 3. Update the two `wget` urls in the README to point to the new version. 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. 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 release](https://github.com/Yelp/dumb-init/releases) dumb-init-1.2.5/Dockerfile000066400000000000000000000016341376446745600154330ustar00rootroot00000000000000FROM debian:buster LABEL maintainer="Chris Kuehl " # 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 \ build-essential \ devscripts \ equivs \ lintian \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* WORKDIR /tmp/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 make builddeb dumb-init-1.2.5/LICENSE000066400000000000000000000020651376446745600144450ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Yelp, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dumb-init-1.2.5/MANIFEST.in000066400000000000000000000000661376446745600151750ustar00rootroot00000000000000include dumb-init.c include VERSION include VERSION.h dumb-init-1.2.5/Makefile000066400000000000000000000046731376446745600151070ustar00rootroot00000000000000SHELL=bash CFLAGS=-std=gnu99 -static -s -Wall -Werror -O3 TEST_PACKAGE_DEPS := build-essential python python-pip procps python-dev python-setuptools DOCKER_RUN_TEST := docker run -v $(PWD):/mnt:ro VERSION = $(shell cat VERSION) .PHONY: build build: VERSION.h $(CC) $(CFLAGS) -o dumb-init dumb-init.c VERSION.h: VERSION echo '// THIS FILE IS AUTOMATICALLY GENERATED' > VERSION.h echo '// Run `make VERSION.h` to update it after modifying VERSION.' >> VERSION.h xxd -i VERSION >> VERSION.h .PHONY: clean clean: clean-tox rm -rf dumb-init dist/ *.deb .PHONY: clean-tox clean-tox: rm -rf .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 \ > sha256sums .PHONY: python-dists python-dists: VERSION.h python setup.py sdist docker run \ --user $$(id -u):$$(id -g) \ -v $(PWD)/dist:/dist:rw \ quay.io/pypa/manylinux1_x86_64:latest \ bash -exc ' \ /opt/python/cp35-cp35m/bin/pip wheel --wheel-dir /tmp /dist/*.tar.gz && \ auditwheel repair --wheel-dir /dist /tmp/*.whl --wheel-dir /dist \ ' .PHONY: builddeb builddeb: debuild --set-envvar=CC=musl-gcc -us -uc -b mkdir -p dist mv ../dumb-init_*.deb dist/ # 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) .PHONY: builddeb-docker builddeb-docker: docker-image mkdir -p dist docker run --user $$(id -u):$$(id -g) -v $(PWD):/tmp/mnt dumb-init-build .PHONY: docker-image docker-image: docker build -t dumb-init-build . .PHONY: test test: tox tox -e pre-commit .PHONY: install-hooks install-hooks: tox -e pre-commit -- install -f --install-hooks ITEST_TARGETS = itest_focal itest_buster .PHONY: itest $(ITEST_TARGETS) itest: $(ITEST_TARGETS) itest_focal: _itest-ubuntu-focal itest_buster: _itest-debian-buster itest_tox: $(DOCKER_RUN_TEST) debian:buster /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 dumb-init-1.2.5/README.md000066400000000000000000000306071376446745600147220ustar00rootroot00000000000000dumb-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) **dumb-init** is a simple process supervisor and init system designed to run as PID 1 inside minimal container environments (such as [Docker][docker]). It is deployed as a small, statically-linked binary written in C. Lightweight containers have popularized the idea of running a single process or service without normal init systems like [systemd][systemd] or [sysvinit][sysvinit]. However, omitting an init system often leads to incorrect handling of processes and signals, and can result in problems such as containers which can't be gracefully stopped, or leaking containers which should have been destroyed. `dumb-init` enables you to simply prefix your command with `dumb-init`. It acts as PID 1 and immediately spawns your command as a child process, taking care to properly handle and forward signals as they are received. ## Why you need an init system Normally, when you launch a Docker container, the process you're executing becomes PID 1, giving it the quirks and responsibilities that come with being the init system for the container. There are two common issues this presents: 1. In most cases, signals won't be handled properly. The Linux kernel applies special signal handling to processes which run as PID 1. When processes are sent a signal on a normal Linux system, the kernel will first check for any custom handlers the process has registered for that signal, and otherwise fall back to default behavior (for example, killing the process on `SIGTERM`). However, if the process receiving the signal is PID 1, it gets special treatment by the kernel; if it hasn't registered a handler for the signal, the kernel won't fall back to default behavior, and nothing happens. In other words, if your process doesn't explicitly handle these signals, sending it `SIGTERM` will have no effect at all. A common example is CI jobs that do `docker run my-container script`: sending `SIGTERM` to the `docker run` process will typically kill the `docker run` command, but leave the container running in the background. 2. Orphaned zombie processes aren't properly reaped. A process becomes a zombie when it exits, and remains a zombie until its parent calls some variation of the `wait()` system call on it. It remains in the process table as a "defunct" process. Typically, a parent process will call `wait()` immediately and avoid long-living zombies. If a parent exits before its child, the child is "orphaned", and is re-parented under PID 1. The init system is thus responsible for `wait()`-ing on orphaned zombie processes. Of course, most processes *won't* `wait()` on random processes that happen to become attached to them, so containers often end with dozens of zombies rooted at PID 1. ## What `dumb-init` does `dumb-init` runs as PID 1, acting like a simple init system. It launches a single process and then proxies all received signals to a session rooted at that child process. Since your actual process is no longer PID 1, when it receives signals from `dumb-init`, the default signal handlers will be applied, and your process will behave as you would expect. If your process dies, `dumb-init` will also die, taking care to clean up any other processes that might still remain. ### Session behavior In its default mode, `dumb-init` establishes a [session](http://man7.org/linux/man-pages/man2/setsid.2.html) rooted at the child, and sends signals to the entire process group. This is useful if you have a poorly-behaving child (such as a shell script) which won't normally signal its children before dying. This can actually be useful outside of Docker containers in regular process supervisors like [daemontools][daemontools] or [supervisord][supervisord] for supervising shell scripts. Normally, a signal like `SIGTERM` received by a shell isn't forwarded to subprocesses; instead, only the shell process dies. With dumb-init, you can just write shell scripts with dumb-init in the shebang: #!/usr/bin/dumb-init /bin/sh my-web-server & # launch a process in the background my-other-server # launch another process in the foreground Ordinarily, a `SIGTERM` sent to the shell would kill the shell but leave those processes running (both the background and foreground!). With dumb-init, your subprocesses will receive the same signals your shell does. If you'd like for signals to only be sent to the direct child, you can run with the `--single-child` argument, or set the environment variable `DUMB_INIT_SETSID=0` when running `dumb-init`. In this mode, dumb-init is completely transparent; you can even string multiple together (like `dumb-init dumb-init echo 'oh, hi'`). ### Signal rewriting dumb-init allows rewriting incoming signals before proxying them. This is useful in cases where you have a Docker supervisor (like Mesos or Kubernetes) which always sends a standard signal (e.g. SIGTERM). Some apps require a different stop signal in order to do graceful cleanup. For example, to rewrite the signal SIGTERM (number 15) to SIGQUIT (number 3), just add `--rewrite 15:3` on the command line. To drop a signal entirely, you can rewrite it to the special number `0`. #### Signal rewriting special case When running in setsid mode, it is not sufficient to forward `SIGTSTP`/`SIGTTIN`/`SIGTTOU` in most cases, since if the process has not added a custom signal handler for these signals, then the kernel will not apply default signal handling behavior (which would be suspending the process) since it is a member of an orphaned process group. For this reason, we set default rewrites to `SIGSTOP` from those three signals. You can opt out of this behavior by rewriting the signals back to their original values, if desired. One caveat with this feature: for job control signals (`SIGTSTP`, `SIGTTIN`, `SIGTTOU`), dumb-init will always suspend itself after receiving the signal, even if you rewrite it to something else. ## Installing inside Docker containers You have a few options for using `dumb-init`: ### Option 1: Installing from your distro's package repositories (Debian, Ubuntu, etc.) Many popular Linux distributions (including Debian (since `stretch`) and Debian derivatives such as Ubuntu (since `bionic`)) now contain dumb-init packages in their official repositories. On Debian-based distributions, you can run `apt install dumb-init` to install dumb-init, just like you'd install any other package. *Note:* Most distro-provided versions of dumb-init are not statically-linked, unlike the versions we provide (see the other options below). This is normally perfectly fine, but means that these versions of dumb-init generally won't work when copied to other Linux distros, unlike the statically-linked versions we provide. ### Option 2: Installing via an internal apt server (Debian/Ubuntu) If you have an internal apt server, uploading the `.deb` to your server is the recommended way to use `dumb-init`. In your Dockerfiles, you can simply `apt install dumb-init` and it will be available. Debian packages are available from the [GitHub Releases tab][gh-releases], or you can run `make builddeb` yourself. ### Option 3: Installing the `.deb` package manually (Debian/Ubuntu) If you don't have an internal apt server, you can use `dpkg -i` to install the `.deb` package. You can choose how you get the `.deb` onto your container (mounting a directory or `wget`-ing it are some options). One possibility is with the following commands in your Dockerfile: ```Dockerfile RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.4/dumb-init_1.2.4_amd64.deb RUN dpkg -i dumb-init_*.deb ``` ### Option 4: Downloading the binary directly 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.4/dumb-init_1.2.4_x86_64 RUN chmod +x /usr/local/bin/dumb-init ``` ### Option 5: Installing from PyPI Though `dumb-init` is written entirely in C, we also provide a Python package which compiles and installs the binary. It can be installed [from PyPI](https://pypi.python.org/pypi/dumb-init) using `pip`. You'll want to first install a C compiler (on Debian/Ubuntu, `apt-get install gcc` is sufficient), then just `pip install dumb-init`. As of 1.2.0, the package at PyPI is available as a pre-built wheel archive and does not need to be compiled on common Linux distributions. ## Usage Once installed inside your Docker container, simply prefix your commands with `dumb-init` (and make sure that you're using [the recommended JSON syntax][docker-cmd-json]). Within a Dockerfile, it's a good practice to use dumb-init as your container's entrypoint. An "entrypoint" is a partial command that gets prepended to your `CMD` instruction, making it a great fit for dumb-init: ```Dockerfile # Runs "/usr/bin/dumb-init -- /my/script --with --args" ENTRYPOINT ["/usr/bin/dumb-init", "--"] # or if you use --rewrite or other cli flags # ENTRYPOINT ["dumb-init", "--rewrite", "2:3", "--"] CMD ["/my/script", "--with", "--args"] ``` If you declare an entrypoint in a base image, any images that descend from it don't need to also declare dumb-init. They can just set a `CMD` as usual. For interactive one-off usage, you can just prepend it manually: $ docker run my_container dumb-init python -c 'while True: pass' Running this same command without `dumb-init` would result in being unable to stop the container without `SIGKILL`, but with `dumb-init`, you can send it more humane signals like `SIGTERM`. It's important that you use [the JSON syntax][docker-cmd-json] for `CMD` and `ENTRYPOINT`. Otherwise, Docker invokes a shell to run your command, resulting in the shell as PID 1 instead of dumb-init. ### Using a shell for pre-start hooks Often containers want to do some pre-start work which can't be done during build time. For example, you might want to template out some config files based on environment variables. The best way to integrate that with dumb-init is like this: ```Dockerfile ENTRYPOINT ["/usr/bin/dumb-init", "--"] CMD ["bash", "-c", "do-some-pre-start-thing && exec my-server"] ``` By still using dumb-init as the entrypoint, you always have a proper init system in place. The `exec` portion of the bash command is important because it [replaces the bash process][exec] with your server, so that the shell only exists momentarily at start. ## Building dumb-init Building the dumb-init binary requires a working compiler and libc headers and defaults to glibc. $ make ### Building with musl Statically compiled dumb-init is over 700KB due to glibc, but musl is now an option. On Debian/Ubuntu `apt-get install musl-tools` to install the source and wrappers, then just: $ CC=musl-gcc make When statically compiled with musl the binary size is around 20KB. ### Building the Debian package We use the standard Debian conventions for specifying build dependencies (look in `debian/control`). An easy way to get started is to `apt-get install build-essential devscripts equivs`, and then `sudo mk-build-deps -i --remove` to install all of the missing build dependencies automatically. You can then use `make builddeb` to build dumb-init Debian packages. If you prefer an automated Debian package build using Docker, just run `make builddeb-docker`. This is easier, but requires you to have Docker running on your machine. ## See also * [Docker and the PID 1 zombie reaping problem (Phusion Blog)](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/) * [Trapping signals in Docker containers (@gchudnov)](https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86) * [tini](https://github.com/krallin/tini), an alternative to dumb-init * [pid1](https://github.com/fpco/pid1), an alternative to dumb-init, written in Haskell [daemontools]: http://cr.yp.to/daemontools.html [docker-cmd-json]: https://docs.docker.com/engine/reference/builder/#run [docker]: https://www.docker.com/ [exec]: https://en.wikipedia.org/wiki/Exec_(system_call) [gh-releases]: https://github.com/Yelp/dumb-init/releases [supervisord]: http://supervisord.org/ [systemd]: https://wiki.freedesktop.org/www/Software/systemd/ [sysvinit]: https://wiki.archlinux.org/index.php/SysVinit dumb-init-1.2.5/VERSION000066400000000000000000000000061376446745600145010ustar00rootroot000000000000001.2.5 dumb-init-1.2.5/VERSION.h000066400000000000000000000003101376446745600147250ustar00rootroot00000000000000// THIS FILE IS AUTOMATICALLY GENERATED // Run `make VERSION.h` to update it after modifying VERSION. unsigned char VERSION[] = { 0x31, 0x2e, 0x32, 0x2e, 0x35, 0x0a }; unsigned int VERSION_len = 6; dumb-init-1.2.5/ci/000077500000000000000000000000001376446745600140305ustar00rootroot00000000000000dumb-init-1.2.5/ci/artifact-upload000077500000000000000000000012751376446745600170420ustar00rootroot00000000000000#!/bin/bash set -euo pipefail if [ -n "${ARTIFACTS_SECRET:-}" ]; then # Travis has built-in support for artifact uploading, but it's broken on ppc64le: # https://github.com/travis-ci/travis-ci/issues/9710 pip install --user awscli ARTIFACTS_PATH="dumb-init/${TRAVIS_BUILD_NUMBER}/${ITEST_TARGET}-${TRAVIS_OS_NAME}" echo 'Uploading artifacts:' for f in dist/*; do AWS_ACCESS_KEY_ID=$ARTIFACTS_KEY AWS_SECRET_ACCESS_KEY=$ARTIFACTS_SECRET ~/.local/bin/aws \ s3 --region $ARTIFACTS_REGION \ cp "$f" s3://$ARTIFACTS_BUCKET/$ARTIFACTS_PATH/$(dirname "$f")/ echo "* https://${ARTIFACTS_BUCKET}.s3.amazonaws.com/$ARTIFACTS_PATH/$f" done fi dumb-init-1.2.5/ci/docker000066400000000000000000000005741376446745600152300ustar00rootroot00000000000000# 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 \ python3 \ python3-dev \ python3-pip \ python3-setuptools cp -r /mnt/ /test cd /test # vim: ft=sh dumb-init-1.2.5/ci/docker-deb-test000077500000000000000000000002421376446745600167300ustar00rootroot00000000000000#!/bin/bash -eux set -o pipefail . /mnt/ci/docker dpkg -i dist/*.deb pip3 install -r requirements-dev.txt pytest tests/ exec dumb-init /mnt/tests/test-zombies dumb-init-1.2.5/ci/docker-python-test000077500000000000000000000003341376446745600175210ustar00rootroot00000000000000#!/bin/bash -eux set -o pipefail . /mnt/ci/docker python3 setup.py clean python3 setup.py sdist pip3 install -vv dist/*.tar.gz pip3 install -r requirements-dev.txt pytest tests/ exec dumb-init /mnt/tests/test-zombies dumb-init-1.2.5/ci/docker-tox-test000077500000000000000000000002331376446745600170100ustar00rootroot00000000000000#!/bin/bash -eux set -o pipefail . /mnt/ci/docker apt-get update apt-get install -y --no-install-recommends \ git \ python3.7-dev \ tox tox dumb-init-1.2.5/ci/gcov-build000077500000000000000000000002351376446745600160110ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail envbindir="$1" cc -c dumb-init.c -o dumb-init.o -g --coverage cc dumb-init.o -o "${envbindir}/dumb-init" -g --coverage dumb-init-1.2.5/ci/gcov-report000077500000000000000000000002001376446745600162150ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail gcov -f dumb-init.c grep '#####' dumb-init.c.gcov | cut -d':' -f2 | xargs echo "Missing:" dumb-init-1.2.5/circle.yml000066400000000000000000000007271376446745600154270ustar00rootroot00000000000000machine: services: - docker dependencies: # Without overriding, Circle CI infers that it should run `python setup.py # install` on the host, which we don't want (instead we run all our tests # in Docker containers). # # Overriding with an empty list or list with an empty string doesn't seem # to work, so we use a little hackery. override: - /bin/true test: override: - ci/circle: parallel: true dumb-init-1.2.5/debian/000077500000000000000000000000001376446745600146575ustar00rootroot00000000000000dumb-init-1.2.5/debian/.gitignore000066400000000000000000000001261376446745600166460ustar00rootroot00000000000000/*.log /*.substvars /files /substvars /dumb-init/ /dumb-init.1 /debhelper-build-stamp dumb-init-1.2.5/debian/changelog000066400000000000000000000141321376446745600165320ustar00rootroot00000000000000dumb-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 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 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 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 Wed, 01 Aug 2018 16:36:22 -0700 dumb-init (1.2.1) unstable; urgency=medium * Fix verbose debug logging for ignored signals. Before this patch, they were reported in the verbose log as "forwarded signal 0 to children" instead of "not forwarding signal to children". Since signal 0 is a noop, there is no actual behavior change here. Thanks @kpengboy for the patch! -- Chris Kuehl Fri, 01 Dec 2017 10:00:27 -0800 dumb-init (1.2.0) unstable; urgency=medium * Hand the controlling TTY to the child process, if we have one (#122). This fixes warnings that are printed when running a typical command like: docker run -ti dumb-init bash ...as well as allowing you to use job control. Thanks to @ehlers for the patch, and @alhafoudh (and several others) for reporting the issue and providing details! -- Chris Kuehl Mon, 10 Oct 2016 14:09:57 -0700 dumb-init (1.1.3) unstable; urgency=low * Add support for FreeBSD kernel. Thanks @onlyjob for bringing this to our attention. -- Chris Kuehl Tue, 02 Aug 2016 10:57:11 -0700 dumb-init (1.1.2) unstable; urgency=low * Fix race when the child exits very quickly that leads to dumb-init not reaping the child. This should be pretty rare and most likely to happen when dumb-init's child fails to exec (such as when you try to run a file that doesn't exist). -- Chris Kuehl Mon, 25 Jul 2016 22:46:40 -0700 dumb-init (1.1.1) unstable; urgency=medium * Fix segfault when passing unknown arguments (thanks @asottile for noticing!) (#88). -- Chris Kuehl Fri, 17 Jun 2016 12:11:22 -0700 dumb-init (1.1.0) unstable; urgency=medium * Add ability to rewrite incoming signals before proxying via the --rewrite flag. Thanks @mcclurmc for the PR (#83)! * Add ability to not rewrite incoming "suspend" job control signals in setsid mode (#85). * Add ability to ignore (not proxy) incoming signals (#86). -- Chris Kuehl Tue, 14 Jun 2016 11:45:50 -0700 dumb-init (1.0.3) unstable; urgency=medium * Fix incorrect error message when calling with bad arguments (e.g. a command which did not exist) and passing flags (#82). * Tag Python packages of dumb-init properly (#75). -- Chris Kuehl Wed, 01 Jun 2016 20:25:24 -0400 dumb-init (1.0.2) unstable; urgency=low * Rewrite signal handling to process signals synchronously (#72). This shouldn't change behavior, but does eliminate some undefined behavior and potential edge cases (especially race conditions). Thanks @msavage20 for the bug report (again)! -- Chris Kuehl Mon, 02 May 2016 10:59:14 -0700 dumb-init (1.0.1) unstable; urgency=low * Fix exit status for processes which exit due to signal (#59). Thanks @msavage20 for the bug report! -- Chris Kuehl Fri, 11 Mar 2016 14:05:58 -0800 dumb-init (1.0.0) unstable; urgency=low * Compile dumb-init with musl and strip the binary of unnecessary symbols (thanks Konrad Scherer) * Fix some typos (thanks Anthony Sottile) * Add a manpage to the Debian package -- Chris Kuehl Thu, 07 Jan 2016 15:00:19 -0800 dumb-init (0.5.0) unstable; urgency=low * Add command-line option parsing (supplements existing environment variables). * Prefix debug output with '[dumb-init]' -- Chris Kuehl Fri, 02 Oct 2015 18:12:51 -0700 dumb-init (0.4.0) unstable; urgency=medium * Properly respond to job control signals (SIGTSTP, SIGTTIN, SIGTTOU). This makes it possible to suspend dumb-init (and its child process) by hitting ^Z in an interactive shell session. -- Chris Kuehl Tue, 29 Sep 2015 13:45:06 -0700 dumb-init (0.3.1) unstable; urgency=medium * Exit nonzero if exec() fails (such as trying to run a nonexistent process) -- Chris Kuehl Tue, 29 Sep 2015 11:18:12 -0700 dumb-init (0.3.0) unstable; urgency=low * Send TERM to all processes in the session when the primary child dies when running in setsid mode. -- Chris Kuehl Fri, 18 Sep 2015 11:08:05 -0700 dumb-init (0.2.0) unstable; urgency=low * Use setsid for process-group behavior. This fixes tty interaction. * Properly reap zombie processes. -- Kent Wills Thu, 10 Sep 2015 13:33:25 -0700 dumb-init (0.1.0) unstable; urgency=low * Add process group support -- Chris Kuehl Thu, 03 Sep 2015 17:55:44 -0700 dumb-init (0.0.2) unstable; urgency=low * Exit with the same exit status as the process we call. * Print a more useful help message when called with no arguments. -- Chris Kuehl Thu, 06 Aug 2015 13:51:38 -0700 dumb-init (0.0.1) unstable; urgency=low * Initial release. -- Chris Kuehl Thu, 06 Aug 2015 13:51:38 -0700 dumb-init-1.2.5/debian/clean000066400000000000000000000000321376446745600156570ustar00rootroot00000000000000tests/*.pyc tests/*/*.pyc dumb-init-1.2.5/debian/compat000066400000000000000000000000021376446745600160550ustar00rootroot000000000000009 dumb-init-1.2.5/debian/control000066400000000000000000000022121376446745600162570ustar00rootroot00000000000000Source: dumb-init Section: utils Priority: extra Maintainer: Chris Kuehl Build-Depends: debhelper (>= 9), help2man, musl-tools, ## Tests: procps, python3, python3-pytest, Standards-Version: 3.9.7 Homepage: https://github.com/Yelp/dumb-init Vcs-Browser: https://github.com/Yelp/dumb-init Vcs-Git: https://github.com/Yelp/dumb-init.git Package: dumb-init Architecture: any Depends: ${misc:Depends} Description: wrapper script which proxies signals to a child dumb-init is a simple process supervisor and init system designed to run as PID 1 inside minimal container environments (such as Docker). . Lightweight containers have popularized the idea of running a single process or service without normal init systems like systemd or sysvinit. However, omitting an init system often leads to incorrect handling of processes and signals, and can result in problems such as containers which can't be gracefully stopped, or leaking containers which should have been destroyed. . dumb-init acts as PID 1 and immediately spawns your command as a child process, taking care to properly handle and forward signals as they are received. dumb-init-1.2.5/debian/copyright000066400000000000000000000025401376446745600166130ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: dumb-init Source: https://github.com/Yelp/dumb-init/ Files: * Copyright: 2015, 2016 Yelp, Inc. License: Expat Files: debian/* Copyright: 2015, 2016 Yelp, Inc. 2016 Dmitry Smirnov License: Expat License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dumb-init-1.2.5/debian/docs000066400000000000000000000000101376446745600155210ustar00rootroot00000000000000README* dumb-init-1.2.5/debian/help2man000066400000000000000000000002331376446745600163060ustar00rootroot00000000000000[authors] .B dumb-init was primarily developed at Yelp. .br .br For a full list of contributors, see https://github.com/Yelp/dumb-init/graphs/contributors dumb-init-1.2.5/debian/install000066400000000000000000000000241376446745600162440ustar00rootroot00000000000000dumb-init /usr/bin/ dumb-init-1.2.5/debian/lintian-overrides000066400000000000000000000000531376446745600202360ustar00rootroot00000000000000dumb-init binary: statically-linked-binary dumb-init-1.2.5/debian/manpages000066400000000000000000000000231376446745600163700ustar00rootroot00000000000000debian/dumb-init.1 dumb-init-1.2.5/debian/rules000077500000000000000000000012631376446745600157410ustar00rootroot00000000000000#!/usr/bin/make -f export DEB_BUILD_MAINT_OPTIONS = hardening=+all %: dh $@ MAN=debian/dumb-init.1 override_dh_clean: $(RM) -rv .cache dh_clean $(MAN) override_dh_auto_clean: @true $(MAN): help2man --name 'a minimal init system for Linux containers' \ --no-discard-stderr \ --include debian/help2man \ --no-info \ ./dumb-init > $@ override_dh_installman: $(MAN) dh_installman override_dh_builddeb: # Use gzip instead of xz to support older Debian/Ubuntu releases which # might install our debs. dh_builddeb -- -Zgzip override_dh_auto_test: find . -name '*.pyc' -delete find . -name '__pycache__' -delete PATH=.:$$PATH timeout --signal=KILL 60 pytest-3 -vv tests/ dumb-init-1.2.5/debian/source/000077500000000000000000000000001376446745600161575ustar00rootroot00000000000000dumb-init-1.2.5/debian/source/format000066400000000000000000000000151376446745600173660ustar00rootroot000000000000003.0 (native) dumb-init-1.2.5/dumb-init.c000066400000000000000000000255511376446745600155010ustar00rootroot00000000000000/* * 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 '-v'. */ #include #include #include #include #include #include #include #include #include #include #include #include "VERSION.h" #define PRINTERR(...) do { \ fprintf(stderr, "[dumb-init] " __VA_ARGS__); \ } while (0) #define DEBUG(...) do { \ if (debug) { \ PRINTERR(__VA_ARGS__); \ } \ } while (0) // Signals we care about are numbered from 1 to 31, inclusive. // (32 and above are real-time signals.) // TODO: this is likely not portable outside of Linux, or on strange architectures #define MAXSIG 31 // Indices are one-indexed (signal 1 is at index 1). Index zero is unused. // User-specified signal rewriting. int signal_rewrite[MAXSIG + 1] = {[0 ... MAXSIG] = -1}; // One-time ignores due to TTY quirks. 0 = no skip, 1 = skip the next-received signal. char signal_temporary_ignores[MAXSIG + 1] = {[0 ... MAXSIG] = 0}; pid_t child_pid = -1; char debug = 0; char use_setsid = 1; int translate_signal(int signum) { if (signum <= 0 || signum > MAXSIG) { return signum; } else { int translated = signal_rewrite[signum]; if (translated == -1) { return signum; } else { DEBUG("Translating signal %d to %d.\n", signum, translated); return translated; } } } void forward_signal(int signum) { signum = translate_signal(signum); if (signum != 0) { kill(use_setsid ? -child_pid : child_pid, signum); DEBUG("Forwarded signal %d to children.\n", signum); } else { DEBUG("Not forwarding signal %d to children (ignored).\n", signum); } } /* * The dumb-init signal handler. * * The main job of this signal handler is to forward signals along to our child * process(es). In setsid mode, this means signaling the entire process group * rooted at our child. In non-setsid mode, this is just signaling the primary * child. * * In most cases, simply proxying the received signal is sufficient. If we * receive a job control signal, however, we should not only forward it, but * also sleep dumb-init itself. * * This allows users to run foreground processes using dumb-init and to * control them using normal shell job control features (e.g. Ctrl-Z to * generate a SIGTSTP and suspend the process). * * The libc manual is useful: * https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html * */ 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); signal_temporary_ignores[signum] = 0; } else if (signum == SIGCHLD) { int status, exit_status; pid_t killed_pid; while ((killed_pid = waitpid(-1, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) { exit_status = WEXITSTATUS(status); DEBUG("A child with PID %d exited with exit status %d.\n", killed_pid, exit_status); } else { assert(WIFSIGNALED(status)); exit_status = 128 + WTERMSIG(status); DEBUG("A child with PID %d was terminated by signal %d.\n", killed_pid, exit_status - 128); } if (killed_pid == child_pid) { forward_signal(SIGTERM); // send SIGTERM to any remaining children DEBUG("Child exited with status %d. Goodbye.\n", exit_status); exit(exit_status); } } } else { forward_signal(signum); if (signum == SIGTSTP || signum == SIGTTOU || signum == SIGTTIN) { DEBUG("Suspending self due to TTY signal.\n"); kill(getpid(), SIGSTOP); } } } void print_help(char *argv[]) { fprintf(stderr, "dumb-init v%.*s" "Usage: %s [option] command [[arg] ...]\n" "\n" "dumb-init is a simple process supervisor that forwards signals to children.\n" "It is designed to run as PID1 in minimal container environments.\n" "\n" "Optional arguments:\n" " -c, --single-child Run in single-child mode.\n" " In this mode, signals are only proxied to the\n" " direct child and not any of its descendants.\n" " -r, --rewrite s:r Rewrite received signal s to new signal r before proxying.\n" " To ignore (not proxy) a signal, rewrite it to 0.\n" " This option can be specified multiple times.\n" " -v, --verbose Print debugging information to stderr.\n" " -h, --help Print this help message and exit.\n" " -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, argv[0] ); } void print_rewrite_signum_help() { fprintf( stderr, "Usage: -r option takes :, where " "is between 1 and %d.\n" "This option can be specified multiple times.\n" "Use --help for full usage.\n", MAXSIG ); exit(1); } void parse_rewrite_signum(char *arg) { int signum, replacement; if ( sscanf(arg, "%d:%d", &signum, &replacement) == 2 && (signum >= 1 && signum <= MAXSIG) && (replacement >= 0 && replacement <= MAXSIG) ) { signal_rewrite[signum] = replacement; } else { print_rewrite_signum_help(); } } void set_rewrite_to_sigstop_if_not_defined(int signum) { if (signal_rewrite[signum] == -1) { signal_rewrite[signum] = SIGSTOP; } } char **parse_command(int argc, char *argv[]) { int opt; struct option long_options[] = { {"help", no_argument, NULL, 'h'}, {"single-child", no_argument, NULL, 'c'}, {"rewrite", required_argument, NULL, 'r'}, {"verbose", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'V'}, {NULL, 0, NULL, 0}, }; while ((opt = getopt_long(argc, argv, "+hvVcr:", long_options, NULL)) != -1) { switch (opt) { case 'h': print_help(argv); exit(0); case 'v': debug = 1; break; case 'V': fprintf(stderr, "dumb-init v%.*s", VERSION_len, VERSION); exit(0); case 'c': use_setsid = 0; break; case 'r': parse_rewrite_signum(optarg); break; default: exit(1); } } if (optind >= argc) { fprintf( stderr, "Usage: %s [option] program [args]\n" "Try %s --help for full usage.\n", argv[0], argv[0] ); exit(1); } char *debug_env = getenv("DUMB_INIT_DEBUG"); if (debug_env && strcmp(debug_env, "1") == 0) { debug = 1; DEBUG("Running in debug mode.\n"); } char *setsid_env = getenv("DUMB_INIT_SETSID"); if (setsid_env && strcmp(setsid_env, "0") == 0) { use_setsid = 0; DEBUG("Not running in setsid mode.\n"); } if (use_setsid) { set_rewrite_to_sigstop_if_not_defined(SIGTSTP); set_rewrite_to_sigstop_if_not_defined(SIGTTOU); set_rewrite_to_sigstop_if_not_defined(SIGTTIN); } return &argv[optind]; } // A dummy signal handler used for signals we care about. // On the FreeBSD kernel, ignored signals cannot be waited on by `sigwait` (but // they can be on Linux). We must provide a dummy handler. // https://lists.freebsd.org/pipermail/freebsd-ports/2009-October/057340.html void dummy(int signum) {} int main(int argc, char *argv[]) { char **cmd = parse_command(argc, argv); sigset_t all_signals; sigfillset(&all_signals); sigprocmask(SIG_BLOCK, &all_signals, NULL); int i = 0; for (i = 1; i <= MAXSIG; i++) { signal(i, dummy); } /* * Detach dumb-init from controlling tty, so that the child's session can * attach to it instead. * * We want the child to be able to be the session leader of the TTY so that * it can do normal job control. */ if (use_setsid) { if (ioctl(STDIN_FILENO, TIOCNOTTY) == -1) { DEBUG( "Unable to detach from controlling tty (errno=%d %s).\n", errno, strerror(errno) ); } else { /* * When the session leader detaches from its controlling tty via * TIOCNOTTY, the kernel sends SIGHUP and SIGCONT to the process * group. We need to be careful not to forward these on to the * dumb-init child so that it doesn't receive a SIGHUP and * terminate itself (#136). */ if (getsid(0) == getpid()) { DEBUG("Detached from controlling tty, ignoring the first SIGHUP and SIGCONT we receive.\n"); signal_temporary_ignores[SIGHUP] = 1; signal_temporary_ignores[SIGCONT] = 1; } else { DEBUG("Detached from controlling tty, but was not session leader.\n"); } } } child_pid = fork(); if (child_pid < 0) { PRINTERR("Unable to fork. Exiting.\n"); return 1; } else if (child_pid == 0) { /* child */ sigprocmask(SIG_UNBLOCK, &all_signals, NULL); if (use_setsid) { if (setsid() == -1) { PRINTERR( "Unable to setsid (errno=%d %s). Exiting.\n", errno, strerror(errno) ); exit(1); } if (ioctl(STDIN_FILENO, TIOCSCTTY, 0) == -1) { DEBUG( "Unable to attach to controlling tty (errno=%d %s).\n", errno, strerror(errno) ); } DEBUG("setsid complete.\n"); } execvp(cmd[0], &cmd[0]); // if this point is reached, exec failed, so we should exit nonzero PRINTERR("%s: %s\n", cmd[0], strerror(errno)); return 2; } 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); handle_signal(signum); } } } dumb-init-1.2.5/pytest.ini000066400000000000000000000000251376446745600154630ustar00rootroot00000000000000[pytest] timeout = 5 dumb-init-1.2.5/requirements-dev.txt000066400000000000000000000000501376446745600174700ustar00rootroot00000000000000pre-commit>=0.5.0 pytest pytest-timeout dumb-init-1.2.5/setup.py000066400000000000000000000074371376446745600151620ustar00rootroot00000000000000from __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 from setuptools.command.install import install as orig_install try: from wheel.bdist_wheel import bdist_wheel as _bdist_wheel class bdist_wheel(_bdist_wheel): def finalize_options(self): _bdist_wheel.finalize_options(self) # Mark us as not a pure python package self.root_is_pure = False def get_tag(self): python, abi, plat = _bdist_wheel.get_tag(self) # We don't contain any python source python, abi = 'py2.py3', 'none' return python, abi, plat except ImportError: bdist_wheel = None 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) print('supports -static... ', end='') with tempfile.NamedTemporaryFile(mode='w', suffix='.c') as f: f.write('int main(void){}\n') f.flush() cmd = compiler.linker_exe + [f.name, '-static', '-o', os.devnull] with open(os.devnull, 'wb') as devnull: if not subprocess.call(cmd, stderr=devnull): print('yes') link_args = ['-static'] else: print('no') link_args = [] 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=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=open('VERSION').read().strip(), author='Yelp', url='https://github.com/Yelp/dumb-init/', platforms='linux', c_executables=[Extension('dumb-init', ['dumb-init.c'])], cmdclass={ 'bdist_wheel': bdist_wheel, 'build': build, 'build_cexe': build_cexe, 'install': install, 'install_cexe': install_cexe, }, distclass=ExeDistribution, ) dumb-init-1.2.5/testing/000077500000000000000000000000001376446745600151125ustar00rootroot00000000000000dumb-init-1.2.5/testing/__init__.py000066400000000000000000000053761376446745600172360ustar00rootroot00000000000000import errno import os import re import signal import sys import time 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, signal.SIGTTOU, signal.SIGTTIN, ]) NORMAL_SIGNALS = frozenset( set(range(1, 32)) - {signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD} - SUSPEND_SIGNALS, ) @contextmanager def print_signals(args=()): """Start print_signals and yield dumb-init process and print_signals PID.""" proc = Popen( ( ('dumb-init',) + tuple(args) + (sys.executable, '-m', 'testing.print_signals') ), stdout=PIPE, ) line = proc.stdout.readline() m = re.match(b'^ready \\(pid: ([0-9]+)\\)\n$', line) assert m, line yield proc, m.group(1).decode('ascii') for pid in pid_tree(proc.pid): os.kill(pid, signal.SIGKILL) def child_pids(pid): """Return a list of direct child PIDs for the given PID.""" children = set() for p in LocalPath('/proc').listdir(): try: stat = open(p.join('stat').strpath).read() m = re.match(r'^\d+ \(.+?\) [a-zA-Z] (\d+) ', stat) assert m, stat ppid = int(m.group(1)) if ppid == pid: children.add(int(p.basename)) except OSError: # Happens when the process exits after listing it, or between # opening stat and reading it. pass return children def pid_tree(pid): """Return a list of all descendant PIDs for the given PID.""" children = child_pids(pid) return { pid for child in children for pid in pid_tree(child) } | children def is_alive(pid): """Return whether a process is running with the given PID.""" return LocalPath('/proc').join(str(pid)).isdir() def process_state(pid): """Return a process' state, such as "stopped" or "running".""" status = LocalPath('/proc').join(str(pid), 'status').read() m = re.search(r'^State:\s+[A-Z] \(([a-z]+)\)$', status, re.MULTILINE) return m.group(1) def sleep_until(fn, timeout=1.5): """Sleep until fn succeeds, or we time out.""" interval = 0.01 so_far = 0 while True: try: fn() except Exception: if so_far >= timeout: raise else: break time.sleep(interval) so_far += interval def kill_if_alive(pid, signum=signal.SIGKILL): """Kill a process, ignoring "no such process" errors.""" try: os.kill(pid, signum) except OSError as ex: if ex.errno != errno.ESRCH: # No such process raise dumb-init-1.2.5/testing/print_signals.py000077500000000000000000000021441376446745600203440ustar00rootroot00000000000000#!/usr/bin/env python """Print received signals to stdout. 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 import time CATCHABLE_SIGNALS = frozenset( set(range(1, 32)) - {signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD}, ) print_queue = [] last_signal = None def unbuffered_print(line): sys.stdout.write('{}\n'.format(line)) sys.stdout.flush() def print_signal(signum, _): print_queue.append(signum) if __name__ == '__main__': for signum in CATCHABLE_SIGNALS: signal.signal(signum, print_signal) unbuffered_print('ready (pid: {})'.format(os.getpid())) # loop forever just printing signals while True: if print_queue: signum = print_queue.pop() unbuffered_print(signum) if signum == signal.SIGINT and last_signal == signal.SIGINT: print('Received SIGINT twice, exiting.') exit(0) last_signal = signum time.sleep(0.01) dumb-init-1.2.5/tests/000077500000000000000000000000001376446745600145775ustar00rootroot00000000000000dumb-init-1.2.5/tests/__init__.py000066400000000000000000000000001376446745600166760ustar00rootroot00000000000000dumb-init-1.2.5/tests/child_processes_test.py000066400000000000000000000106241376446745600213640ustar00rootroot00000000000000import os import re import signal import sys from subprocess import PIPE from subprocess import Popen import pytest from testing import is_alive from testing import kill_if_alive from testing import pid_tree from testing import sleep_until def spawn_and_kill_pipeline(): proc = Popen(( 'dumb-init', 'sh', '-c', "yes 'oh, hi' | tail & yes error | tail >&2", )) def assert_living_pids(): assert len(living_pids(pid_tree(os.getpid()))) == 6 sleep_until(assert_living_pids) pids = pid_tree(os.getpid()) proc.send_signal(signal.SIGTERM) proc.wait() return pids def living_pids(pids): return {pid for pid in pids if is_alive(pid)} @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') def test_setsid_signals_entire_group(): """When dumb-init is running in setsid mode, it should signal the entire process group rooted at it. """ pids = spawn_and_kill_pipeline() def assert_no_living_pids(): assert len(living_pids(pids)) == 0 sleep_until(assert_no_living_pids) @pytest.mark.usefixtures('both_debug_modes', 'setsid_disabled') def test_no_setsid_doesnt_signal_entire_group(): """When dumb-init is not running in setsid mode, it should only signal its immediate child. """ pids = spawn_and_kill_pipeline() def assert_four_living_pids(): assert len(living_pids(pids)) == 4 sleep_until(assert_four_living_pids) for pid in living_pids(pids): kill_if_alive(pid) def spawn_process_which_dies_with_children(): """Spawn a process which spawns some children and then dies without signaling them, wrapped in dumb-init. Returns a tuple (child pid, child stdout pipe), where the child is print_signals. This is useful because you can signal the PID and see if anything gets printed onto the stdout pipe. """ proc = Popen( ( 'dumb-init', 'sh', '-c', # 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=sys.executable, ), ), stdout=PIPE, ) proc.wait() assert proc.returncode == 0 # read a line from print_signals, figure out its pid line = proc.stdout.readline() match = re.match(b'ready \\(pid: ([0-9]+)\\)\n', line) assert match, line child_pid = int(match.group(1)) # at this point, the shell and dumb-init have both exited, but # print_signals may or may not still be running (depending on whether # setsid mode is enabled) return child_pid, proc.stdout @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') def test_all_processes_receive_term_on_exit_if_setsid(): """If the child exits for some reason, dumb-init should send TERM to all processes in its session if setsid mode is enabled.""" child_pid, child_stdout = spawn_process_which_dies_with_children() # print_signals should have received TERM assert child_stdout.readline() == b'15\n' os.kill(child_pid, signal.SIGKILL) @pytest.mark.usefixtures('both_debug_modes', 'setsid_disabled') def test_processes_dont_receive_term_on_exit_if_no_setsid(): """If the child exits for some reason, dumb-init should not send TERM to any other processes if setsid mode is disabled.""" child_pid, child_stdout = spawn_process_which_dies_with_children() # print_signals should not have received TERM; to test this, we send it # some other signals and ensure they were received (and TERM wasn't) for signum in [1, 2, 3]: os.kill(child_pid, signum) assert child_stdout.readline() == str(signum).encode('ascii') + b'\n' os.kill(child_pid, signal.SIGKILL) @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.""" proc = Popen(('dumb-init',) + args, stderr=PIPE) _, stderr = proc.communicate() assert proc.returncode != 0 assert ( b'[dumb-init] /doesnotexist: No such file or directory\n' in stderr ) dumb-init-1.2.5/tests/cli_test.py000066400000000000000000000132101376446745600167540ustar00rootroot00000000000000"""Sanity checks for command-line options.""" import re import signal from subprocess import PIPE from subprocess import Popen import pytest @pytest.fixture def current_version(): return open('VERSION').read().strip() @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 stderr == ( b'Usage: dumb-init [option] program [args]\n' b'Try dumb-init --help for full usage.\n' ) @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') def test_exits_invalid_with_invalid_args(): proc = Popen(('dumb-init', '--yolo', '/bin/true'), stderr=PIPE) _, stderr = proc.communicate() assert proc.returncode == 1 assert stderr in ( b"dumb-init: unrecognized option '--yolo'\n", # glibc b'dumb-init: unrecognized option: yolo\n', # musl ) @pytest.mark.parametrize('flag', ['-h', '--help']) @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') def test_help_message(flag, current_version): """dumb-init should say something useful when called with the help flag, and exit zero. """ proc = Popen(('dumb-init', flag), stderr=PIPE) _, stderr = proc.communicate() assert proc.returncode == 0 assert stderr == ( b'dumb-init v' + current_version.encode('ascii') + b'\n' b'Usage: dumb-init [option] command [[arg] ...]\n' b'\n' b'dumb-init is a simple process supervisor that forwards signals to children.\n' b'It is designed to run as PID1 in minimal container environments.\n' b'\n' b'Optional arguments:\n' b' -c, --single-child Run in single-child mode.\n' b' In this mode, signals are only proxied to the\n' b' direct child and not any of its descendants.\n' b' -r, --rewrite s:r Rewrite received signal s to new signal r before proxying.\n' b' To ignore (not proxy) a signal, rewrite it to 0.\n' b' This option can be specified multiple times.\n' b' -v, --verbose Print debugging information to stderr.\n' b' -h, --help Print this help message and exit.\n' b' -V, --version Print the current version and exit.\n' b'\n' b'Full help is available online at https://github.com/Yelp/dumb-init\n' ) @pytest.mark.parametrize('flag', ['-V', '--version']) @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') def test_version_message(flag, current_version): """dumb-init should print its version when asked to.""" proc = Popen(('dumb-init', flag), stderr=PIPE) _, stderr = proc.communicate() assert proc.returncode == 0 assert stderr == b'dumb-init v' + current_version.encode('ascii') + b'\n' @pytest.mark.parametrize('flag', ['-v', '--verbose']) def test_verbose(flag): """dumb-init should print debug output when asked to.""" proc = Popen(('dumb-init', flag, 'echo', 'oh,', 'hi'), stdout=PIPE, stderr=PIPE) stdout, stderr = proc.communicate() assert proc.returncode == 0 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( # parent ( '(^|\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$' ).format(signal=signal).encode('utf8'), stderr, re.DOTALL, ), stderr @pytest.mark.parametrize('flag1', ['-v', '--verbose']) @pytest.mark.parametrize('flag2', ['-c', '--single-child']) def test_verbose_and_single_child(flag1, flag2): """dumb-init should print debug output when asked to.""" proc = Popen(('dumb-init', flag1, flag2, 'echo', 'oh,', 'hi'), stdout=PIPE, stderr=PIPE) stdout, stderr = proc.communicate() assert proc.returncode == 0 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$' ).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.usefixtures('both_debug_modes', 'both_setsid_modes') def test_rewrite_errors(extra_args): proc = Popen( ('dumb-init',) + extra_args + ('echo', 'oh,', 'hi'), stdout=PIPE, stderr=PIPE, ) stdout, stderr = proc.communicate() assert proc.returncode == 1 assert stderr == ( b'Usage: -r option takes :, where ' b'is between 1 and 31.\n' b'This option can be specified multiple times.\n' b'Use --help for full usage.\n' ) dumb-init-1.2.5/tests/conftest.py000066400000000000000000000021511376446745600167750ustar00rootroot00000000000000import os from unittest import mock import pytest @pytest.fixture(autouse=True, scope='function') def clean_environment(): """Ensure all tests start with a clean environment. Even if tests properly clean up after themselves, we still need this in case the user runs tests with an already-polluted environment. """ with mock.patch.dict( os.environ, {'DUMB_INIT_DEBUG': '', 'DUMB_INIT_SETSID': ''}, ): yield @pytest.fixture(params=['1', '0']) def both_debug_modes(request): with mock.patch.dict(os.environ, {'DUMB_INIT_DEBUG': request.param}): yield @pytest.fixture def debug_disabled(): with mock.patch.dict(os.environ, {'DUMB_INIT_DEBUG': '0'}): yield @pytest.fixture(params=['1', '0']) def both_setsid_modes(request): with mock.patch.dict(os.environ, {'DUMB_INIT_SETSID': request.param}): yield @pytest.fixture def setsid_enabled(): with mock.patch.dict(os.environ, {'DUMB_INIT_SETSID': '1'}): yield @pytest.fixture def setsid_disabled(): with mock.patch.dict(os.environ, {'DUMB_INIT_SETSID': '0'}): yield dumb-init-1.2.5/tests/cwd_test.py000066400000000000000000000014341376446745600167670ustar00rootroot00000000000000import os import shutil from subprocess import run, PIPE 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' dumb-init-1.2.5/tests/exit_status_test.py000066400000000000000000000023251376446745600205660ustar00rootroot00000000000000import signal import sys from subprocess import Popen import pytest @pytest.mark.parametrize('exit_status', [0, 1, 2, 32, 64, 127, 254, 255]) @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 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.wait() assert proc.returncode == exit_status @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 terminated by a 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.wait() assert proc.returncode == 128 + signal dumb-init-1.2.5/tests/proxies_signals_test.py000066400000000000000000000073661376446745600214350ustar00rootroot00000000000000import os import signal from itertools import chain import pytest from testing import NORMAL_SIGNALS from testing import print_signals from testing import process_state @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') def test_proxies_signals(): """Ensure dumb-init proxies regular signals to its child.""" with print_signals() as (proc, _): for signum in NORMAL_SIGNALS: proc.send_signal(signum) assert proc.stdout.readline() == '{}\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() ) @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.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], ), ], ) @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') @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') def test_default_rewrites_can_be_overriden_with_setsid_enabled(): """In setsid mode, dumb-init should allow overwriting the default rewrites (but still suspend itself). """ rewrite_map = { signal.SIGTTIN: signal.SIGTERM, signal.SIGTTOU: signal.SIGINT, signal.SIGTSTP: signal.SIGHUP, } with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _): for send, expect_receive in rewrite_map.items(): assert process_state(proc.pid) in ['running', 'sleeping'] proc.send_signal(send) assert proc.stdout.readline() == '{}\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 process_state(proc.pid) in ['running', 'sleeping'] @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') def test_ignored_signals_are_not_proxied(): """Ensure dumb-init can ignore signals.""" rewrite_map = { signal.SIGTERM: signal.SIGQUIT, signal.SIGINT: 0, signal.SIGWINCH: 0, } 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') proc.send_signal(signal.SIGWINCH) proc.send_signal(signal.SIGHUP) assert proc.stdout.readline() == '{}\n'.format(signal.SIGHUP).encode('ascii') dumb-init-1.2.5/tests/shell_background_test.py000066400000000000000000000041371376446745600215230ustar00rootroot00000000000000import os from signal import SIGCONT import pytest from testing import print_signals from testing import process_state from testing import sleep_until from testing import SUSPEND_SIGNALS @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') def test_shell_background_support_setsid(): """In setsid mode, dumb-init should suspend itself and its children when it receives SIGTSTP, SIGTTOU, or SIGTTIN. """ with print_signals() as (proc, pid): for signum in SUSPEND_SIGNALS: # both dumb-init and print_signals should be running or sleeping assert process_state(pid) in ['running', 'sleeping'] assert process_state(proc.pid) in ['running', 'sleeping'] # both should now suspend proc.send_signal(signum) def assert_both_stopped(): assert process_state(proc.pid) == process_state(pid) == 'stopped' sleep_until(assert_both_stopped) # and then both wake up again proc.send_signal(SIGCONT) assert ( proc.stdout.readline() == '{}\n'.format(SIGCONT).encode('ascii') ) assert process_state(pid) in ['running', 'sleeping'] assert process_state(proc.pid) in ['running', 'sleeping'] @pytest.mark.usefixtures('both_debug_modes', 'setsid_disabled') def test_shell_background_support_without_setsid(): """In non-setsid mode, dumb-init should forward the signals SIGTSTP, SIGTTOU, and SIGTTIN, and then suspend itself. """ with print_signals() as (proc, _): 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') 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') ) assert process_state(proc.pid) in ['running', 'sleeping'] dumb-init-1.2.5/tests/test-zombies000077500000000000000000000011571376446745600171560ustar00rootroot00000000000000#!/bin/bash -eux # Spawn a zombie process, and ensure it gets reaped. # This test is only useful when run on an empty container with # dumb-init as PID1. # # We run it as the last step of the integration tests inside our Docker # containers. Since dumb-init must run as PID 1, we don't use pytest and # instead write it in bash (which gets executed by PID1 dumb-init). set -o pipefail bash -euxc "bash -euxc 'echo i am a zombie' &" & sleep 1 num_zombies=$(ps -A -o state | (grep 'Z' || true) | wc -l) if [ "$num_zombies" -ne 0 ]; then echo "Expected no zombies, but instead there were ${num_zombies}." exit 1 fi dumb-init-1.2.5/tests/tty_test.py000066400000000000000000000072311376446745600170330ustar00rootroot00000000000000import os import pty import re import signal import termios import time import pytest EOF = b'\x04' def ttyflags(fd): """normalize tty i/o for testing""" # see: # http://www.gnu.org/software/libc/manual/html_mono/libc.html#Output-Modes attrs = termios.tcgetattr(fd) attrs[1] &= ~termios.OPOST # don't munge output attrs[3] &= ~termios.ECHO # don't echo input termios.tcsetattr(fd, termios.TCSANOW, attrs) def readall(fd): """read until EOF""" result = b'' while True: try: chunk = os.read(fd, 1 << 10) except OSError as error: if error.errno == 5: # linux pty EOF return result else: raise if chunk == b'': return result else: result += chunk # disable debug output so it doesn't break our assertion @pytest.mark.usefixtures('debug_disabled') def test_tty(): """Ensure processes under dumb-init can write successfully, given a tty.""" pid, fd = pty.fork() if pid == 0: os.execvp('dumb-init', ('dumb-init', 'tac')) else: # write to tac via the pty and verify its output ttyflags(fd) assert os.write(fd, b'1\n2\n3\n') == 6 assert os.write(fd, EOF * 2) == 2 output = readall(fd) assert os.waitpid(pid, 0) == (pid, 0) assert output == b'3\n2\n1\n', repr(output) @pytest.mark.usefixtures('both_debug_modes') @pytest.mark.usefixtures('both_setsid_modes') def test_child_gets_controlling_tty_if_we_had_one(): """If dumb-init has a controlling TTY, it should give it to the child. To test this, we make a new TTY then exec "dumb-init bash" and ensure that the shell has working job control. """ pid, sfd = pty.fork() if pid == 0: os.execvp('dumb-init', ('dumb-init', 'bash', '-m')) else: ttyflags(sfd) # We might get lots of extra output from the shell, so print something # we can match on easily. assert os.write(sfd, b'echo "flags are: [[$-]]"\n') == 25 assert os.write(sfd, b'exit 0\n') == 7 output = readall(sfd) assert os.waitpid(pid, 0) == (pid, 0), output m = re.search(b'flags are: \\[\\[([a-zA-Z]+)\\]\\]\n', output) assert m, output # "m" is job control flags = m.group(1) assert b'm' in flags def test_sighup_sigcont_ignored_if_was_session_leader(): """The first SIGHUP/SIGCONT should be ignored if dumb-init is the session leader. Due to TTY quirks (#136), when dumb-init is the session leader and forks, it needs to avoid forwarding the first SIGHUP and SIGCONT to the child. Otherwise, the child might receive the SIGHUP post-exec and terminate itself. You can "force" this race by adding a `sleep(1)` before the signal handling loop in dumb-init's code, but it's hard to reproduce the race reliably in a test otherwise. Because of this, we're stuck just asserting debug messages. """ pid, fd = pty.fork() if pid == 0: # child os.execvp('dumb-init', ('dumb-init', '-v', 'sleep', '20')) else: # parent ttyflags(fd) # send another SIGCONT to make sure only the first is ignored time.sleep(0.5) os.kill(pid, signal.SIGHUP) 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 '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGHUP) in output assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGCONT) not in output dumb-init-1.2.5/tox.ini000066400000000000000000000007771376446745600147630ustar00rootroot00000000000000[tox] envlist = py37,gcov [testenv] deps = -r{toxinidir}/requirements-dev.txt commands = pytest [testenv:gcov] skip_install = True basepython = /usr/bin/python3.7 commands = {toxinidir}/ci/gcov-build {envbindir} {[testenv]commands} {toxinidir}/ci/gcov-report [testenv:pre-commit] basepython = /usr/bin/python3.7 commands = pre-commit {posargs:run --all-files} [flake8] max-line-length = 119 [pep8] # autopep8 will rewrite lines to be shorter, even though we raised the length ignore = E501