├── .dockerignore ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── VERSION ├── VERSION.h ├── ci ├── artifact-upload ├── docker-deb-test ├── docker-python-test ├── gcov-build └── gcov-report ├── circle.yml ├── debian ├── .gitignore ├── changelog ├── clean ├── compat ├── control ├── copyright ├── docs ├── help2man ├── install ├── lintian-overrides ├── manpages ├── rules └── source │ └── format ├── dumb-init.c ├── pytest.ini ├── requirements-dev.txt ├── setup.py ├── testing ├── __init__.py └── print_signals.py ├── tests ├── __init__.py ├── child_processes_test.py ├── cli_test.py ├── conftest.py ├── cwd_test.py ├── exit_status_test.py ├── proxies_signals_test.py ├── shell_background_test.py ├── test-zombies └── tty_test.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | .tox 2 | .git 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build-and-test: 5 | runs-on: ubuntu-20.04 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | include: 10 | - arch: amd64 11 | manylinux_arch: x86_64 12 | docker_image: debian:buster 13 | 14 | - arch: arm64 15 | manylinux_arch: aarch64 16 | docker_image: arm64v8/debian:buster 17 | 18 | - arch: ppc64le 19 | manylinux_arch: ppc64le 20 | docker_image: ppc64le/debian:buster 21 | 22 | - arch: s390x 23 | manylinux_arch: s390x 24 | docker_image: s390x/debian:buster 25 | 26 | env: 27 | BASE_IMAGE: ${{ matrix.docker_image }} 28 | 29 | steps: 30 | - uses: actions/checkout@v2 31 | 32 | - name: Set up QEMU 33 | id: qemu 34 | uses: docker/setup-qemu-action@v1 35 | if: ${{ matrix.arch != 'amd64' }} 36 | with: 37 | image: tonistiigi/binfmt:latest 38 | 39 | - name: Build Docker image 40 | run: make docker-image 41 | 42 | - name: Run python tests 43 | run: docker run --rm -v $(pwd):/mnt:rw dumb-init-build /mnt/ci/docker-python-test 44 | 45 | - name: Build Debian package 46 | run: docker run --init --rm -v $(pwd):/mnt:rw dumb-init-build make -C /mnt builddeb 47 | 48 | - name: Test built Debian package 49 | # XXX: This uses the clean base image (not the build one) to make 50 | # sure it installs in a clean image without any hidden dependencies. 51 | run: docker run --rm -v $(pwd):/mnt:rw ${{ matrix.docker_image }} /mnt/ci/docker-deb-test 52 | 53 | - name: Build wheels 54 | run: sudo make python-dists-${{ matrix.manylinux_arch }} 55 | 56 | - name: Upload build artifacts 57 | uses: actions/upload-artifact@v2 58 | with: 59 | name: ${{ matrix.arch }} 60 | path: dist 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.deb 2 | *.egg-info 3 | *.gc* 4 | *.o 5 | *.py[cod] 6 | .pytest_cache 7 | .tox 8 | __pycache__/ 9 | build/ 10 | dist/ 11 | dumb-init 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-docstring-first 7 | - id: check-executables-have-shebangs 8 | - id: check-merge-conflict 9 | - id: check-yaml 10 | - id: debug-statements 11 | - id: detect-private-key 12 | - id: double-quote-string-fixer 13 | - id: end-of-file-fixer 14 | - id: name-tests-test 15 | - id: requirements-txt-fixer 16 | - id: trailing-whitespace 17 | - repo: https://github.com/pre-commit/mirrors-autopep8 18 | rev: v2.0.0 19 | hooks: 20 | - id: autopep8 21 | - repo: https://github.com/pycqa/flake8 22 | rev: 6.0.0 23 | hooks: 24 | - id: flake8 25 | - repo: https://github.com/asottile/reorder_python_imports 26 | rev: v3.9.0 27 | hooks: 28 | - id: reorder-python-imports 29 | args: ['--py3-plus'] 30 | - repo: https://github.com/Lucas-C/pre-commit-hooks 31 | rev: v1.3.1 32 | hooks: 33 | - id: remove-tabs 34 | - repo: https://github.com/asottile/pyupgrade 35 | rev: v3.3.0 36 | hooks: 37 | - id: pyupgrade 38 | args: ['--py3-plus'] 39 | - repo: https://github.com/asottile/add-trailing-comma 40 | rev: v2.3.0 41 | hooks: 42 | - id: add-trailing-comma 43 | args: ['--py36-plus'] 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to dumb-init 2 | ======== 3 | 4 | `dumb-init` is primarily developed by [Yelp](https://yelp.github.io/), but 5 | contributions are welcome from everyone! 6 | 7 | Code is reviewed using GitHub pull requests. To make a contribution, you should: 8 | 9 | 1. Fork the GitHub repository 10 | 2. Push code to a branch on your fork 11 | 3. Create a pull request and wait for it to be reviewed 12 | 13 | We aim to have all dumb-init behavior covered by tests. If you make a change in 14 | behavior, please add a test to ensure it doesn't regress. We're also happy to 15 | help with suggestions on testing! 16 | 17 | 18 | ## Releasing new versions 19 | 20 | `dumb-init` uses [semantic versioning](http://semver.org/). If you're making a 21 | contribution, please don't bump the version number yourself—we'll take care 22 | of that after merging! 23 | 24 | The process to release a new version is: 25 | 26 | 1. Update the version in `VERSION` and run `make VERSION.h` 27 | 2. Update the Debian changelog with `dch -v {new version}`. 28 | 3. Update the two `wget` urls in the README to point to the new version. 29 | 4. Commit the changes and tag the commit like `v1.0.0`. 30 | 5. `git push --tags origin master` 31 | 6. Wait for Travis to run, then find and download the binary and Debian 32 | packages for all architectures; there will be links printed at the 33 | end of the Travis output. Put these into your `dist` directory. 34 | 7. Run `make release` 35 | 8. Run `twine upload --skip-existing dist/*.tar.gz dist/*.whl` to upload the 36 | new version to PyPI 37 | 9. Upload the resulting Debian packages, binaries, and sha256sums file (all 38 | inside the `dist` directory) to a new [GitHub 39 | release](https://github.com/Yelp/dumb-init/releases) 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=debian:buster 2 | FROM $BASE_IMAGE 3 | 4 | LABEL maintainer="Chris Kuehl " 5 | 6 | # Install the bare minimum dependencies necessary for working with Debian 7 | # packages. Build dependencies should be added under "Build-Depends" inside 8 | # debian/control instead. 9 | RUN : \ 10 | && apt-get update \ 11 | && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 12 | build-essential \ 13 | devscripts \ 14 | equivs \ 15 | lintian \ 16 | python3-distutils \ 17 | python3-setuptools \ 18 | python3-pip \ 19 | && apt-get clean \ 20 | && rm -rf /var/lib/apt/lists/* 21 | WORKDIR /tmp/mnt 22 | 23 | COPY debian/control /control 24 | RUN : \ 25 | && apt-get update \ 26 | && mk-build-deps --install --tool 'apt-get -y --no-install-recommends' /control \ 27 | && apt-get clean \ 28 | && rm -rf /var/lib/apt/lists/* 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yelp, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include dumb-init.c 2 | include VERSION 3 | include VERSION.h 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=bash 2 | CFLAGS=-std=gnu99 -static -s -Wall -Werror -O3 3 | 4 | TEST_PACKAGE_DEPS := build-essential python python-pip procps python-dev python-setuptools 5 | 6 | DOCKER_RUN_TEST := docker run -v $(PWD):/mnt:ro 7 | VERSION = $(shell cat VERSION) 8 | 9 | .PHONY: build 10 | build: VERSION.h 11 | $(CC) $(CFLAGS) -o dumb-init dumb-init.c 12 | 13 | VERSION.h: VERSION 14 | echo '// THIS FILE IS AUTOMATICALLY GENERATED' > VERSION.h 15 | echo '// Run `make VERSION.h` to update it after modifying VERSION.' >> VERSION.h 16 | xxd -i VERSION >> VERSION.h 17 | 18 | .PHONY: clean 19 | clean: clean-tox 20 | rm -rf dumb-init dist/ *.deb 21 | 22 | .PHONY: clean-tox 23 | clean-tox: 24 | rm -rf .tox 25 | 26 | .PHONY: release 27 | release: python-dists 28 | cd dist && \ 29 | 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 \ 30 | > sha256sums 31 | 32 | .PHONY: python-dists 33 | python-dists: python-dists-x86_64 python-dists-aarch64 python-dists-ppc64le python-dists-s390x 34 | 35 | .PHONY: python-dists-% 36 | python-dists-%: VERSION.h 37 | python setup.py sdist 38 | docker run \ 39 | --user $$(id -u):$$(id -g) \ 40 | -v `pwd`/dist:/dist:rw \ 41 | quay.io/pypa/manylinux2014_$*:latest \ 42 | bash -exc ' \ 43 | /opt/python/cp38-cp38/bin/pip wheel --wheel-dir /tmp /dist/*.tar.gz && \ 44 | auditwheel repair --wheel-dir /dist /tmp/*.whl --wheel-dir /dist \ 45 | ' 46 | 47 | .PHONY: builddeb 48 | builddeb: 49 | debuild --set-envvar=CC=musl-gcc -us -uc -b 50 | mkdir -p dist 51 | mv ../dumb-init_*.deb dist/ 52 | # Extract the built binary from the Debian package 53 | dpkg-deb --fsys-tarfile dist/dumb-init_$(VERSION)_$(shell dpkg --print-architecture).deb | \ 54 | tar -C dist --strip=3 -xvf - ./usr/bin/dumb-init 55 | mv dist/dumb-init dist/dumb-init_$(VERSION)_$(shell uname -m) 56 | 57 | .PHONY: builddeb-docker 58 | builddeb-docker: docker-image 59 | mkdir -p dist 60 | docker run --init --user $$(id -u):$$(id -g) -v $(PWD):/tmp/mnt dumb-init-build make builddeb 61 | 62 | .PHONY: docker-image 63 | docker-image: 64 | docker build $(if $(BASE_IMAGE),--build-arg BASE_IMAGE=$(BASE_IMAGE)) -t dumb-init-build . 65 | 66 | .PHONY: test 67 | test: 68 | tox 69 | tox -e pre-commit 70 | 71 | .PHONY: install-hooks 72 | install-hooks: 73 | tox -e pre-commit -- install -f --install-hooks 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dumb-init 2 | ======== 3 | 4 | [![PyPI version](https://badge.fury.io/py/dumb-init.svg)](https://pypi.python.org/pypi/dumb-init) 5 | 6 | 7 | **dumb-init** is a simple process supervisor and init system designed to run as 8 | PID 1 inside minimal container environments (such as [Docker][docker]). It is 9 | deployed as a small, statically-linked binary written in C. 10 | 11 | Lightweight containers have popularized the idea of running a single process or 12 | service without normal init systems like [systemd][systemd] or 13 | [sysvinit][sysvinit]. However, omitting an init system often leads to incorrect 14 | handling of processes and signals, and can result in problems such as 15 | containers which can't be gracefully stopped, or leaking containers which 16 | should have been destroyed. 17 | 18 | `dumb-init` enables you to simply prefix your command with `dumb-init`. It acts 19 | as PID 1 and immediately spawns your command as a child process, taking care to 20 | properly handle and forward signals as they are received. 21 | 22 | 23 | ## Why you need an init system 24 | 25 | Normally, when you launch a Docker container, the process you're executing 26 | becomes PID 1, giving it the quirks and responsibilities that come with being 27 | the init system for the container. 28 | 29 | There are two common issues this presents: 30 | 31 | 1. In most cases, signals won't be handled properly. 32 | 33 | The Linux kernel applies special signal handling to processes which run as 34 | PID 1. 35 | 36 | When processes are sent a signal on a normal Linux system, the kernel will 37 | first check for any custom handlers the process has registered for that 38 | signal, and otherwise fall back to default behavior (for example, killing 39 | the process on `SIGTERM`). 40 | 41 | However, if the process receiving the signal is PID 1, it gets special 42 | treatment by the kernel; if it hasn't registered a handler for the signal, 43 | the kernel won't fall back to default behavior, and nothing happens. In 44 | other words, if your process doesn't explicitly handle these signals, 45 | sending it `SIGTERM` will have no effect at all. 46 | 47 | A common example is CI jobs that do `docker run my-container script`: sending 48 | `SIGTERM` to the `docker run` process will typically kill the `docker run` command, 49 | but leave the container running in the background. 50 | 51 | 2. Orphaned zombie processes aren't properly reaped. 52 | 53 | A process becomes a zombie when it exits, and remains a zombie until its 54 | parent calls some variation of the `wait()` system call on it. It remains in 55 | the process table as a "defunct" process. Typically, a parent process will 56 | call `wait()` immediately and avoid long-living zombies. 57 | 58 | If a parent exits before its child, the child is "orphaned", and is 59 | re-parented under PID 1. The init system is thus responsible for 60 | `wait()`-ing on orphaned zombie processes. 61 | 62 | Of course, most processes *won't* `wait()` on random processes that happen 63 | to become attached to them, so containers often end with dozens of zombies 64 | rooted at PID 1. 65 | 66 | 67 | ## What `dumb-init` does 68 | 69 | `dumb-init` runs as PID 1, acting like a simple init system. It launches a 70 | single process and then proxies all received signals to a session rooted at 71 | that child process. 72 | 73 | Since your actual process is no longer PID 1, when it receives signals from 74 | `dumb-init`, the default signal handlers will be applied, and your process will 75 | behave as you would expect. If your process dies, `dumb-init` will also die, 76 | taking care to clean up any other processes that might still remain. 77 | 78 | 79 | ### Session behavior 80 | 81 | In its default mode, `dumb-init` establishes a 82 | [session](http://man7.org/linux/man-pages/man2/setsid.2.html) rooted at the 83 | child, and sends signals to the entire process group. This is useful if you 84 | have a poorly-behaving child (such as a shell script) which won't normally 85 | signal its children before dying. 86 | 87 | This can actually be useful outside of Docker containers in regular process 88 | supervisors like [daemontools][daemontools] or [supervisord][supervisord] for 89 | supervising shell scripts. Normally, a signal like `SIGTERM` received by a 90 | shell isn't forwarded to subprocesses; instead, only the shell process dies. 91 | With dumb-init, you can just write shell scripts with dumb-init in the shebang: 92 | 93 | #!/usr/bin/dumb-init /bin/sh 94 | my-web-server & # launch a process in the background 95 | my-other-server # launch another process in the foreground 96 | 97 | Ordinarily, a `SIGTERM` sent to the shell would kill the shell but leave those 98 | processes running (both the background and foreground!). With dumb-init, your 99 | subprocesses will receive the same signals your shell does. 100 | 101 | If you'd like for signals to only be sent to the direct child, you can run with 102 | the `--single-child` argument, or set the environment variable 103 | `DUMB_INIT_SETSID=0` when running `dumb-init`. In this mode, dumb-init is 104 | completely transparent; you can even string multiple together (like `dumb-init 105 | dumb-init echo 'oh, hi'`). 106 | 107 | 108 | ### Signal rewriting 109 | 110 | dumb-init allows rewriting incoming signals before proxying them. This is 111 | useful in cases where you have a Docker supervisor (like Mesos or Kubernetes) 112 | which always sends a standard signal (e.g. SIGTERM). Some apps require a 113 | different stop signal in order to do graceful cleanup. 114 | 115 | For example, to rewrite the signal SIGTERM (number 15) to SIGQUIT (number 3), 116 | just add `--rewrite 15:3` on the command line. 117 | 118 | To drop a signal entirely, you can rewrite it to the special number `0`. 119 | 120 | 121 | #### Signal rewriting special case 122 | 123 | When running in setsid mode, it is not sufficient to forward 124 | `SIGTSTP`/`SIGTTIN`/`SIGTTOU` in most cases, since if the process has not added 125 | a custom signal handler for these signals, then the kernel will not apply 126 | default signal handling behavior (which would be suspending the process) since 127 | it is a member of an orphaned process group. For this reason, we set default 128 | rewrites to `SIGSTOP` from those three signals. You can opt out of this 129 | behavior by rewriting the signals back to their original values, if desired. 130 | 131 | One caveat with this feature: for job control signals (`SIGTSTP`, `SIGTTIN`, 132 | `SIGTTOU`), dumb-init will always suspend itself after receiving the signal, 133 | even if you rewrite it to something else. 134 | 135 | 136 | ## Installing inside Docker containers 137 | 138 | You have a few options for using `dumb-init`: 139 | 140 | 141 | ### Option 1: Installing from your distro's package repositories (Debian, Ubuntu, etc.) 142 | 143 | Many popular Linux distributions (including Debian (since `stretch`) and Debian 144 | derivatives such as Ubuntu (since `bionic`)) now contain dumb-init packages in 145 | their official repositories. 146 | 147 | On Debian-based distributions, you can run `apt install dumb-init` to install 148 | dumb-init, just like you'd install any other package. 149 | 150 | *Note:* Most distro-provided versions of dumb-init are not statically-linked, 151 | unlike the versions we provide (see the other options below). This is normally 152 | perfectly fine, but means that these versions of dumb-init generally won't work 153 | when copied to other Linux distros, unlike the statically-linked versions we 154 | provide. 155 | 156 | 157 | ### Option 2: Installing via an internal apt server (Debian/Ubuntu) 158 | 159 | If you have an internal apt server, uploading the `.deb` to your server is the 160 | recommended way to use `dumb-init`. In your Dockerfiles, you can simply 161 | `apt install dumb-init` and it will be available. 162 | 163 | Debian packages are available from the [GitHub Releases tab][gh-releases], or 164 | you can run `make builddeb` yourself. 165 | 166 | 167 | ### Option 3: Installing the `.deb` package manually (Debian/Ubuntu) 168 | 169 | If you don't have an internal apt server, you can use `dpkg -i` to install the 170 | `.deb` package. You can choose how you get the `.deb` onto your container 171 | (mounting a directory or `wget`-ing it are some options). 172 | 173 | One possibility is with the following commands in your Dockerfile: 174 | 175 | ```Dockerfile 176 | RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_amd64.deb 177 | RUN dpkg -i dumb-init_*.deb 178 | ``` 179 | 180 | 181 | ### Option 4: Downloading the binary directly 182 | 183 | Since dumb-init is released as a statically-linked binary, you can usually just 184 | plop it into your images. Here's an example of doing that in a Dockerfile: 185 | 186 | ```Dockerfile 187 | 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 188 | RUN chmod +x /usr/local/bin/dumb-init 189 | ``` 190 | 191 | 192 | ### Option 5: Installing from PyPI 193 | 194 | Though `dumb-init` is written entirely in C, we also provide a Python package 195 | which compiles and installs the binary. It can be installed [from 196 | PyPI](https://pypi.python.org/pypi/dumb-init) using `pip`. You'll want to first 197 | install a C compiler (on Debian/Ubuntu, `apt-get install gcc` is sufficient), 198 | then just `pip install dumb-init`. 199 | 200 | As of 1.2.0, the package at PyPI is available as a pre-built wheel archive and does not 201 | need to be compiled on common Linux distributions. 202 | 203 | 204 | ## Usage 205 | 206 | Once installed inside your Docker container, simply prefix your commands with 207 | `dumb-init` (and make sure that you're using [the recommended JSON 208 | syntax][docker-cmd-json]). 209 | 210 | Within a Dockerfile, it's a good practice to use dumb-init as your container's 211 | entrypoint. An "entrypoint" is a partial command that gets prepended to your 212 | `CMD` instruction, making it a great fit for dumb-init: 213 | 214 | ```Dockerfile 215 | # Runs "/usr/bin/dumb-init -- /my/script --with --args" 216 | ENTRYPOINT ["/usr/bin/dumb-init", "--"] 217 | 218 | # or if you use --rewrite or other cli flags 219 | # ENTRYPOINT ["dumb-init", "--rewrite", "2:3", "--"] 220 | 221 | CMD ["/my/script", "--with", "--args"] 222 | ``` 223 | 224 | If you declare an entrypoint in a base image, any images that descend from it 225 | don't need to also declare dumb-init. They can just set a `CMD` as usual. 226 | 227 | For interactive one-off usage, you can just prepend it manually: 228 | 229 | $ docker run my_container dumb-init python -c 'while True: pass' 230 | 231 | Running this same command without `dumb-init` would result in being unable to 232 | stop the container without `SIGKILL`, but with `dumb-init`, you can send it 233 | more humane signals like `SIGTERM`. 234 | 235 | It's important that you use [the JSON syntax][docker-cmd-json] for `CMD` and 236 | `ENTRYPOINT`. Otherwise, Docker invokes a shell to run your command, resulting 237 | in the shell as PID 1 instead of dumb-init. 238 | 239 | 240 | ### Using a shell for pre-start hooks 241 | 242 | Often containers want to do some pre-start work which can't be done during 243 | build time. For example, you might want to template out some config files based 244 | on environment variables. 245 | 246 | The best way to integrate that with dumb-init is like this: 247 | 248 | ```Dockerfile 249 | ENTRYPOINT ["/usr/bin/dumb-init", "--"] 250 | CMD ["bash", "-c", "do-some-pre-start-thing && exec my-server"] 251 | ``` 252 | 253 | By still using dumb-init as the entrypoint, you always have a proper init 254 | system in place. 255 | 256 | The `exec` portion of the bash command is important because it [replaces the 257 | bash process][exec] with your server, so that the shell only exists momentarily 258 | at start. 259 | 260 | 261 | ## Building dumb-init 262 | 263 | Building the dumb-init binary requires a working compiler and libc headers and 264 | defaults to glibc. 265 | 266 | $ make 267 | 268 | 269 | ### Building with musl 270 | 271 | Statically compiled dumb-init is over 700KB due to glibc, but musl is now an 272 | option. On Debian/Ubuntu `apt-get install musl-tools` to install the source and 273 | wrappers, then just: 274 | 275 | $ CC=musl-gcc make 276 | 277 | When statically compiled with musl the binary size is around 20KB. 278 | 279 | 280 | ### Building the Debian package 281 | 282 | We use the standard Debian conventions for specifying build dependencies (look 283 | in `debian/control`). An easy way to get started is to `apt-get install 284 | build-essential devscripts equivs`, and then `sudo mk-build-deps -i --remove` 285 | to install all of the missing build dependencies automatically. You can then 286 | use `make builddeb` to build dumb-init Debian packages. 287 | 288 | If you prefer an automated Debian package build using Docker, just run `make 289 | builddeb-docker`. This is easier, but requires you to have Docker running on 290 | your machine. 291 | 292 | 293 | ## See also 294 | 295 | * [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/) 296 | * [Trapping signals in Docker containers (@gchudnov)](https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86) 297 | * [tini](https://github.com/krallin/tini), an alternative to dumb-init 298 | * [pid1](https://github.com/fpco/pid1), an alternative to dumb-init, written in Haskell 299 | 300 | 301 | [daemontools]: http://cr.yp.to/daemontools.html 302 | [docker-cmd-json]: https://docs.docker.com/engine/reference/builder/#run 303 | [docker]: https://www.docker.com/ 304 | [exec]: https://en.wikipedia.org/wiki/Exec_(system_call) 305 | [gh-releases]: https://github.com/Yelp/dumb-init/releases 306 | [supervisord]: http://supervisord.org/ 307 | [systemd]: https://wiki.freedesktop.org/www/Software/systemd/ 308 | [sysvinit]: https://wiki.archlinux.org/index.php/SysVinit 309 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.2.5 2 | -------------------------------------------------------------------------------- /VERSION.h: -------------------------------------------------------------------------------- 1 | // THIS FILE IS AUTOMATICALLY GENERATED 2 | // Run `make VERSION.h` to update it after modifying VERSION. 3 | unsigned char VERSION[] = { 4 | 0x31, 0x2e, 0x32, 0x2e, 0x35, 0x0a 5 | }; 6 | unsigned int VERSION_len = 6; 7 | -------------------------------------------------------------------------------- /ci/artifact-upload: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | if [ -n "${ARTIFACTS_SECRET:-}" ]; then 5 | # Travis has built-in support for artifact uploading, but it's broken on ppc64le: 6 | # https://github.com/travis-ci/travis-ci/issues/9710 7 | pip install --user awscli 8 | 9 | ARTIFACTS_PATH="dumb-init/${TRAVIS_BUILD_NUMBER}/${ITEST_TARGET}-${TRAVIS_OS_NAME}" 10 | echo 'Uploading artifacts:' 11 | for f in dist/*; do 12 | AWS_ACCESS_KEY_ID=$ARTIFACTS_KEY AWS_SECRET_ACCESS_KEY=$ARTIFACTS_SECRET ~/.local/bin/aws \ 13 | s3 --region $ARTIFACTS_REGION \ 14 | cp "$f" s3://$ARTIFACTS_BUCKET/$ARTIFACTS_PATH/$(dirname "$f")/ 15 | echo "* https://${ARTIFACTS_BUCKET}.s3.amazonaws.com/$ARTIFACTS_PATH/$f" 16 | done 17 | fi 18 | -------------------------------------------------------------------------------- /ci/docker-deb-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | set -o pipefail 3 | 4 | apt-get update 5 | apt-get -y --no-install-recommends install python3-pip procps 6 | 7 | cd /mnt 8 | dpkg -i dist/*.deb 9 | pip3 install -r requirements-dev.txt 10 | pytest tests/ 11 | 12 | exec dumb-init /mnt/tests/test-zombies 13 | -------------------------------------------------------------------------------- /ci/docker-python-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | set -euo pipefail 3 | 4 | cd /mnt 5 | 6 | python3 setup.py clean 7 | python3 setup.py sdist 8 | pip3 install -vv dist/*.tar.gz 9 | pip3 install -r requirements-dev.txt 10 | pytest-3 -vv tests/ 11 | 12 | exec dumb-init /mnt/tests/test-zombies 13 | -------------------------------------------------------------------------------- /ci/gcov-build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | envbindir="$1" 4 | cc -c dumb-init.c -o dumb-init.o -g --coverage 5 | cc dumb-init.o -o "${envbindir}/dumb-init" -g --coverage 6 | -------------------------------------------------------------------------------- /ci/gcov-report: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | gcov -f dumb-init.c 4 | grep '#####' dumb-init.c.gcov | cut -d':' -f2 | xargs echo "Missing:" 5 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | services: 3 | - docker 4 | 5 | dependencies: 6 | # Without overriding, Circle CI infers that it should run `python setup.py 7 | # install` on the host, which we don't want (instead we run all our tests 8 | # in Docker containers). 9 | # 10 | # Overriding with an empty list or list with an empty string doesn't seem 11 | # to work, so we use a little hackery. 12 | override: 13 | - /bin/true 14 | 15 | test: 16 | override: 17 | - ci/circle: 18 | parallel: true 19 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | /*.substvars 3 | /files 4 | /substvars 5 | /dumb-init/ 6 | /dumb-init.1 7 | /debhelper-build-stamp 8 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | dumb-init (1.2.5) unstable; urgency=medium 2 | 3 | * Change the working directory in the parent process to "/" after forking. 4 | 5 | https://github.com/Yelp/dumb-init/pull/210 6 | 7 | Thanks to @Villemoes for the patch! 8 | 9 | -- Chris Kuehl Thu, 10 Dec 2020 10:54:47 -0800 10 | 11 | dumb-init (1.2.4) unstable; urgency=medium 12 | 13 | * Actually fix the bug that can cause `--help` or `--version` to crash in 14 | some scenarios. 15 | 16 | https://github.com/Yelp/dumb-init/pull/215 17 | 18 | Thanks to @suve for the patch! 19 | 20 | -- Chris Kuehl Mon, 07 Dec 2020 11:58:06 -0800 21 | 22 | dumb-init (1.2.3) unstable; urgency=medium 23 | 24 | * Fix a bug that can cause `--help` or `--version` to crash in some 25 | scenarios. 26 | 27 | https://github.com/Yelp/dumb-init/pull/213 28 | 29 | Thanks to @suve for the patch! 30 | 31 | -- Chris Kuehl Wed, 02 Dec 2020 10:43:02 -0800 32 | 33 | dumb-init (1.2.2) unstable; urgency=medium 34 | 35 | * Fix a race condition which can cause the child to receive SIGHUP and 36 | SIGCONT very shortly after start (#174). 37 | 38 | In general this was very rare, but some environments (especially some 39 | container and virtualization environments) appear to encounter it at a 40 | much higher rate, possibly due to scheduler quirks. 41 | 42 | -- Chris Kuehl Wed, 01 Aug 2018 16:36:22 -0700 43 | 44 | dumb-init (1.2.1) unstable; urgency=medium 45 | 46 | * Fix verbose debug logging for ignored signals. 47 | 48 | Before this patch, they were reported in the verbose log as "forwarded 49 | signal 0 to children" instead of "not forwarding signal to children". 50 | 51 | Since signal 0 is a noop, there is no actual behavior change here. 52 | 53 | Thanks @kpengboy for the patch! 54 | 55 | -- Chris Kuehl Fri, 01 Dec 2017 10:00:27 -0800 56 | 57 | dumb-init (1.2.0) unstable; urgency=medium 58 | 59 | * Hand the controlling TTY to the child process, if we have one (#122). 60 | 61 | This fixes warnings that are printed when running a typical command like: 62 | docker run -ti dumb-init bash 63 | ...as well as allowing you to use job control. 64 | 65 | Thanks to @ehlers for the patch, and @alhafoudh (and several others) for 66 | reporting the issue and providing details! 67 | 68 | -- Chris Kuehl Mon, 10 Oct 2016 14:09:57 -0700 69 | 70 | dumb-init (1.1.3) unstable; urgency=low 71 | 72 | * Add support for FreeBSD kernel. Thanks @onlyjob for bringing this to our 73 | attention. 74 | 75 | -- Chris Kuehl Tue, 02 Aug 2016 10:57:11 -0700 76 | 77 | dumb-init (1.1.2) unstable; urgency=low 78 | 79 | * Fix race when the child exits very quickly that leads to dumb-init not 80 | reaping the child. This should be pretty rare and most likely to happen 81 | when dumb-init's child fails to exec (such as when you try to run a file 82 | that doesn't exist). 83 | 84 | -- Chris Kuehl Mon, 25 Jul 2016 22:46:40 -0700 85 | 86 | dumb-init (1.1.1) unstable; urgency=medium 87 | 88 | * Fix segfault when passing unknown arguments (thanks @asottile for 89 | noticing!) (#88). 90 | 91 | -- Chris Kuehl Fri, 17 Jun 2016 12:11:22 -0700 92 | 93 | dumb-init (1.1.0) unstable; urgency=medium 94 | 95 | * Add ability to rewrite incoming signals before proxying via the --rewrite 96 | flag. Thanks @mcclurmc for the PR (#83)! 97 | * Add ability to not rewrite incoming "suspend" job control signals in 98 | setsid mode (#85). 99 | * Add ability to ignore (not proxy) incoming signals (#86). 100 | 101 | -- Chris Kuehl Tue, 14 Jun 2016 11:45:50 -0700 102 | 103 | dumb-init (1.0.3) unstable; urgency=medium 104 | 105 | * Fix incorrect error message when calling with bad arguments (e.g. a 106 | command which did not exist) and passing flags (#82). 107 | * Tag Python packages of dumb-init properly (#75). 108 | 109 | -- Chris Kuehl Wed, 01 Jun 2016 20:25:24 -0400 110 | 111 | dumb-init (1.0.2) unstable; urgency=low 112 | 113 | * Rewrite signal handling to process signals synchronously (#72). 114 | This shouldn't change behavior, but does eliminate some undefined behavior 115 | and potential edge cases (especially race conditions). 116 | Thanks @msavage20 for the bug report (again)! 117 | 118 | -- Chris Kuehl Mon, 02 May 2016 10:59:14 -0700 119 | 120 | dumb-init (1.0.1) unstable; urgency=low 121 | 122 | * Fix exit status for processes which exit due to signal (#59). 123 | Thanks @msavage20 for the bug report! 124 | 125 | -- Chris Kuehl Fri, 11 Mar 2016 14:05:58 -0800 126 | 127 | dumb-init (1.0.0) unstable; urgency=low 128 | 129 | * Compile dumb-init with musl and strip the binary of unnecessary symbols 130 | (thanks Konrad Scherer) 131 | * Fix some typos (thanks Anthony Sottile) 132 | * Add a manpage to the Debian package 133 | 134 | -- Chris Kuehl Thu, 07 Jan 2016 15:00:19 -0800 135 | 136 | dumb-init (0.5.0) unstable; urgency=low 137 | 138 | * Add command-line option parsing (supplements existing environment 139 | variables). 140 | * Prefix debug output with '[dumb-init]' 141 | 142 | -- Chris Kuehl Fri, 02 Oct 2015 18:12:51 -0700 143 | 144 | dumb-init (0.4.0) unstable; urgency=medium 145 | 146 | * Properly respond to job control signals (SIGTSTP, SIGTTIN, SIGTTOU). 147 | 148 | This makes it possible to suspend dumb-init (and its child process) by 149 | hitting ^Z in an interactive shell session. 150 | 151 | -- Chris Kuehl Tue, 29 Sep 2015 13:45:06 -0700 152 | 153 | dumb-init (0.3.1) unstable; urgency=medium 154 | 155 | * Exit nonzero if exec() fails (such as trying to run a nonexistent process) 156 | 157 | -- Chris Kuehl Tue, 29 Sep 2015 11:18:12 -0700 158 | 159 | dumb-init (0.3.0) unstable; urgency=low 160 | 161 | * Send TERM to all processes in the session when the primary child dies when 162 | running in setsid mode. 163 | 164 | -- Chris Kuehl Fri, 18 Sep 2015 11:08:05 -0700 165 | 166 | dumb-init (0.2.0) unstable; urgency=low 167 | 168 | * Use setsid for process-group behavior. This fixes tty interaction. 169 | * Properly reap zombie processes. 170 | 171 | -- Kent Wills Thu, 10 Sep 2015 13:33:25 -0700 172 | 173 | dumb-init (0.1.0) unstable; urgency=low 174 | 175 | * Add process group support 176 | 177 | -- Chris Kuehl Thu, 03 Sep 2015 17:55:44 -0700 178 | 179 | dumb-init (0.0.2) unstable; urgency=low 180 | 181 | * Exit with the same exit status as the process we call. 182 | * Print a more useful help message when called with no arguments. 183 | 184 | -- Chris Kuehl Thu, 06 Aug 2015 13:51:38 -0700 185 | 186 | dumb-init (0.0.1) unstable; urgency=low 187 | 188 | * Initial release. 189 | 190 | -- Chris Kuehl Thu, 06 Aug 2015 13:51:38 -0700 191 | -------------------------------------------------------------------------------- /debian/clean: -------------------------------------------------------------------------------- 1 | tests/*.pyc 2 | tests/*/*.pyc 3 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: dumb-init 2 | Section: utils 3 | Priority: extra 4 | Maintainer: Chris Kuehl 5 | Build-Depends: 6 | debhelper (>= 9), 7 | help2man, 8 | musl-tools, 9 | ## Tests: 10 | procps, 11 | python3, 12 | python3-pytest, 13 | Standards-Version: 3.9.7 14 | Homepage: https://github.com/Yelp/dumb-init 15 | Vcs-Browser: https://github.com/Yelp/dumb-init 16 | Vcs-Git: https://github.com/Yelp/dumb-init.git 17 | 18 | Package: dumb-init 19 | Architecture: any 20 | Depends: ${misc:Depends} 21 | Description: wrapper script which proxies signals to a child 22 | dumb-init is a simple process supervisor and init system designed to run 23 | as PID 1 inside minimal container environments (such as Docker). 24 | . 25 | Lightweight containers have popularized the idea of running a single 26 | process or service without normal init systems like systemd or sysvinit. 27 | However, omitting an init system often leads to incorrect handling of 28 | processes and signals, and can result in problems such as containers 29 | which can't be gracefully stopped, or leaking containers which should 30 | have been destroyed. 31 | . 32 | dumb-init acts as PID 1 and immediately spawns your command as a child 33 | process, taking care to properly handle and forward signals as they are 34 | received. 35 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: dumb-init 3 | Source: https://github.com/Yelp/dumb-init/ 4 | 5 | Files: * 6 | Copyright: 7 | 2015, 2016 Yelp, Inc. 8 | License: Expat 9 | 10 | Files: debian/* 11 | Copyright: 12 | 2015, 2016 Yelp, Inc. 13 | 2016 Dmitry Smirnov 14 | License: Expat 15 | 16 | License: Expat 17 | Permission is hereby granted, free of charge, to any person obtaining a 18 | copy of this software and associated documentation files (the "Software"), 19 | to deal in the Software without restriction, including without limitation 20 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 21 | and/or sell copies of the Software, and to permit persons to whom the 22 | Software is furnished to do so, subject to the following conditions: 23 | . 24 | The above copyright notice and this permission notice shall be included in 25 | all copies or substantial portions of the Software. 26 | . 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 30 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 33 | DEALINGS IN THE SOFTWARE. 34 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README* 2 | -------------------------------------------------------------------------------- /debian/help2man: -------------------------------------------------------------------------------- 1 | [authors] 2 | .B dumb-init 3 | was primarily developed at Yelp. 4 | .br 5 | .br 6 | For a full list of contributors, see 7 | https://github.com/Yelp/dumb-init/graphs/contributors 8 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | dumb-init /usr/bin/ 2 | -------------------------------------------------------------------------------- /debian/lintian-overrides: -------------------------------------------------------------------------------- 1 | dumb-init binary: statically-linked-binary 2 | -------------------------------------------------------------------------------- /debian/manpages: -------------------------------------------------------------------------------- 1 | debian/dumb-init.1 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | export DEB_BUILD_MAINT_OPTIONS = hardening=+all 3 | 4 | %: 5 | dh $@ 6 | 7 | MAN=debian/dumb-init.1 8 | 9 | override_dh_clean: 10 | $(RM) -rv .cache 11 | dh_clean $(MAN) 12 | 13 | override_dh_auto_clean: 14 | @true 15 | 16 | $(MAN): 17 | help2man --name 'a minimal init system for Linux containers' \ 18 | --no-discard-stderr \ 19 | --include debian/help2man \ 20 | --no-info \ 21 | ./dumb-init > $@ 22 | 23 | override_dh_installman: $(MAN) 24 | dh_installman 25 | 26 | override_dh_builddeb: 27 | # Use gzip instead of xz to support older Debian/Ubuntu releases which 28 | # might install our debs. 29 | dh_builddeb -- -Zgzip 30 | 31 | override_dh_auto_test: 32 | find . -name '*.pyc' -delete 33 | find . -name '__pycache__' -delete 34 | PATH=.:$$PATH timeout --signal=KILL 60 pytest-3 -vv tests/ 35 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /dumb-init.c: -------------------------------------------------------------------------------- 1 | /* 2 | * dumb-init is a simple wrapper program designed to run as PID 1 and pass 3 | * signals to its children. 4 | * 5 | * Usage: 6 | * ./dumb-init python -c 'while True: pass' 7 | * 8 | * To get debug output on stderr, run with '-v'. 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include "VERSION.h" 23 | 24 | #define PRINTERR(...) do { \ 25 | fprintf(stderr, "[dumb-init] " __VA_ARGS__); \ 26 | } while (0) 27 | 28 | #define DEBUG(...) do { \ 29 | if (debug) { \ 30 | PRINTERR(__VA_ARGS__); \ 31 | } \ 32 | } while (0) 33 | 34 | // Signals we care about are numbered from 1 to 31, inclusive. 35 | // (32 and above are real-time signals.) 36 | // TODO: this is likely not portable outside of Linux, or on strange architectures 37 | #define MAXSIG 31 38 | 39 | // Indices are one-indexed (signal 1 is at index 1). Index zero is unused. 40 | // User-specified signal rewriting. 41 | int signal_rewrite[MAXSIG + 1] = {[0 ... MAXSIG] = -1}; 42 | // One-time ignores due to TTY quirks. 0 = no skip, 1 = skip the next-received signal. 43 | char signal_temporary_ignores[MAXSIG + 1] = {[0 ... MAXSIG] = 0}; 44 | 45 | pid_t child_pid = -1; 46 | char debug = 0; 47 | char use_setsid = 1; 48 | 49 | int translate_signal(int signum) { 50 | if (signum <= 0 || signum > MAXSIG) { 51 | return signum; 52 | } else { 53 | int translated = signal_rewrite[signum]; 54 | if (translated == -1) { 55 | return signum; 56 | } else { 57 | DEBUG("Translating signal %d to %d.\n", signum, translated); 58 | return translated; 59 | } 60 | } 61 | } 62 | 63 | void forward_signal(int signum) { 64 | signum = translate_signal(signum); 65 | if (signum != 0) { 66 | kill(use_setsid ? -child_pid : child_pid, signum); 67 | DEBUG("Forwarded signal %d to children.\n", signum); 68 | } else { 69 | DEBUG("Not forwarding signal %d to children (ignored).\n", signum); 70 | } 71 | } 72 | 73 | /* 74 | * The dumb-init signal handler. 75 | * 76 | * The main job of this signal handler is to forward signals along to our child 77 | * process(es). In setsid mode, this means signaling the entire process group 78 | * rooted at our child. In non-setsid mode, this is just signaling the primary 79 | * child. 80 | * 81 | * In most cases, simply proxying the received signal is sufficient. If we 82 | * receive a job control signal, however, we should not only forward it, but 83 | * also sleep dumb-init itself. 84 | * 85 | * This allows users to run foreground processes using dumb-init and to 86 | * control them using normal shell job control features (e.g. Ctrl-Z to 87 | * generate a SIGTSTP and suspend the process). 88 | * 89 | * The libc manual is useful: 90 | * https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html 91 | * 92 | */ 93 | void handle_signal(int signum) { 94 | DEBUG("Received signal %d.\n", signum); 95 | 96 | if (signal_temporary_ignores[signum] == 1) { 97 | DEBUG("Ignoring tty hand-off signal %d.\n", signum); 98 | signal_temporary_ignores[signum] = 0; 99 | } else if (signum == SIGCHLD) { 100 | int status, exit_status; 101 | pid_t killed_pid; 102 | while ((killed_pid = waitpid(-1, &status, WNOHANG)) > 0) { 103 | if (WIFEXITED(status)) { 104 | exit_status = WEXITSTATUS(status); 105 | DEBUG("A child with PID %d exited with exit status %d.\n", killed_pid, exit_status); 106 | } else { 107 | assert(WIFSIGNALED(status)); 108 | exit_status = 128 + WTERMSIG(status); 109 | DEBUG("A child with PID %d was terminated by signal %d.\n", killed_pid, exit_status - 128); 110 | } 111 | 112 | if (killed_pid == child_pid) { 113 | forward_signal(SIGTERM); // send SIGTERM to any remaining children 114 | DEBUG("Child exited with status %d. Goodbye.\n", exit_status); 115 | exit(exit_status); 116 | } 117 | } 118 | } else { 119 | forward_signal(signum); 120 | if (signum == SIGTSTP || signum == SIGTTOU || signum == SIGTTIN) { 121 | DEBUG("Suspending self due to TTY signal.\n"); 122 | kill(getpid(), SIGSTOP); 123 | } 124 | } 125 | } 126 | 127 | void print_help(char *argv[]) { 128 | fprintf(stderr, 129 | "dumb-init v%.*s" 130 | "Usage: %s [option] command [[arg] ...]\n" 131 | "\n" 132 | "dumb-init is a simple process supervisor that forwards signals to children.\n" 133 | "It is designed to run as PID1 in minimal container environments.\n" 134 | "\n" 135 | "Optional arguments:\n" 136 | " -c, --single-child Run in single-child mode.\n" 137 | " In this mode, signals are only proxied to the\n" 138 | " direct child and not any of its descendants.\n" 139 | " -r, --rewrite s:r Rewrite received signal s to new signal r before proxying.\n" 140 | " To ignore (not proxy) a signal, rewrite it to 0.\n" 141 | " This option can be specified multiple times.\n" 142 | " -v, --verbose Print debugging information to stderr.\n" 143 | " -h, --help Print this help message and exit.\n" 144 | " -V, --version Print the current version and exit.\n" 145 | "\n" 146 | "Full help is available online at https://github.com/Yelp/dumb-init\n", 147 | VERSION_len, VERSION, 148 | argv[0] 149 | ); 150 | } 151 | 152 | void print_rewrite_signum_help() { 153 | fprintf( 154 | stderr, 155 | "Usage: -r option takes :, where " 156 | "is between 1 and %d.\n" 157 | "This option can be specified multiple times.\n" 158 | "Use --help for full usage.\n", 159 | MAXSIG 160 | ); 161 | exit(1); 162 | } 163 | 164 | void parse_rewrite_signum(char *arg) { 165 | int signum, replacement; 166 | if ( 167 | sscanf(arg, "%d:%d", &signum, &replacement) == 2 && 168 | (signum >= 1 && signum <= MAXSIG) && 169 | (replacement >= 0 && replacement <= MAXSIG) 170 | ) { 171 | signal_rewrite[signum] = replacement; 172 | } else { 173 | print_rewrite_signum_help(); 174 | } 175 | } 176 | 177 | void set_rewrite_to_sigstop_if_not_defined(int signum) { 178 | if (signal_rewrite[signum] == -1) { 179 | signal_rewrite[signum] = SIGSTOP; 180 | } 181 | } 182 | 183 | char **parse_command(int argc, char *argv[]) { 184 | int opt; 185 | struct option long_options[] = { 186 | {"help", no_argument, NULL, 'h'}, 187 | {"single-child", no_argument, NULL, 'c'}, 188 | {"rewrite", required_argument, NULL, 'r'}, 189 | {"verbose", no_argument, NULL, 'v'}, 190 | {"version", no_argument, NULL, 'V'}, 191 | {NULL, 0, NULL, 0}, 192 | }; 193 | while ((opt = getopt_long(argc, argv, "+hvVcr:", long_options, NULL)) != -1) { 194 | switch (opt) { 195 | case 'h': 196 | print_help(argv); 197 | exit(0); 198 | case 'v': 199 | debug = 1; 200 | break; 201 | case 'V': 202 | fprintf(stderr, "dumb-init v%.*s", VERSION_len, VERSION); 203 | exit(0); 204 | case 'c': 205 | use_setsid = 0; 206 | break; 207 | case 'r': 208 | parse_rewrite_signum(optarg); 209 | break; 210 | default: 211 | exit(1); 212 | } 213 | } 214 | 215 | if (optind >= argc) { 216 | fprintf( 217 | stderr, 218 | "Usage: %s [option] program [args]\n" 219 | "Try %s --help for full usage.\n", 220 | argv[0], argv[0] 221 | ); 222 | exit(1); 223 | } 224 | 225 | char *debug_env = getenv("DUMB_INIT_DEBUG"); 226 | if (debug_env && strcmp(debug_env, "1") == 0) { 227 | debug = 1; 228 | DEBUG("Running in debug mode.\n"); 229 | } 230 | 231 | char *setsid_env = getenv("DUMB_INIT_SETSID"); 232 | if (setsid_env && strcmp(setsid_env, "0") == 0) { 233 | use_setsid = 0; 234 | DEBUG("Not running in setsid mode.\n"); 235 | } 236 | 237 | if (use_setsid) { 238 | set_rewrite_to_sigstop_if_not_defined(SIGTSTP); 239 | set_rewrite_to_sigstop_if_not_defined(SIGTTOU); 240 | set_rewrite_to_sigstop_if_not_defined(SIGTTIN); 241 | } 242 | 243 | return &argv[optind]; 244 | } 245 | 246 | // A dummy signal handler used for signals we care about. 247 | // On the FreeBSD kernel, ignored signals cannot be waited on by `sigwait` (but 248 | // they can be on Linux). We must provide a dummy handler. 249 | // https://lists.freebsd.org/pipermail/freebsd-ports/2009-October/057340.html 250 | void dummy(int signum) {} 251 | 252 | int main(int argc, char *argv[]) { 253 | char **cmd = parse_command(argc, argv); 254 | sigset_t all_signals; 255 | sigfillset(&all_signals); 256 | sigprocmask(SIG_BLOCK, &all_signals, NULL); 257 | 258 | int i = 0; 259 | for (i = 1; i <= MAXSIG; i++) { 260 | signal(i, dummy); 261 | } 262 | 263 | /* 264 | * Detach dumb-init from controlling tty, so that the child's session can 265 | * attach to it instead. 266 | * 267 | * We want the child to be able to be the session leader of the TTY so that 268 | * it can do normal job control. 269 | */ 270 | if (use_setsid) { 271 | if (ioctl(STDIN_FILENO, TIOCNOTTY) == -1) { 272 | DEBUG( 273 | "Unable to detach from controlling tty (errno=%d %s).\n", 274 | errno, 275 | strerror(errno) 276 | ); 277 | } else { 278 | /* 279 | * When the session leader detaches from its controlling tty via 280 | * TIOCNOTTY, the kernel sends SIGHUP and SIGCONT to the process 281 | * group. We need to be careful not to forward these on to the 282 | * dumb-init child so that it doesn't receive a SIGHUP and 283 | * terminate itself (#136). 284 | */ 285 | if (getsid(0) == getpid()) { 286 | DEBUG("Detached from controlling tty, ignoring the first SIGHUP and SIGCONT we receive.\n"); 287 | signal_temporary_ignores[SIGHUP] = 1; 288 | signal_temporary_ignores[SIGCONT] = 1; 289 | } else { 290 | DEBUG("Detached from controlling tty, but was not session leader.\n"); 291 | } 292 | } 293 | } 294 | 295 | child_pid = fork(); 296 | if (child_pid < 0) { 297 | PRINTERR("Unable to fork. Exiting.\n"); 298 | return 1; 299 | } else if (child_pid == 0) { 300 | /* child */ 301 | sigprocmask(SIG_UNBLOCK, &all_signals, NULL); 302 | if (use_setsid) { 303 | if (setsid() == -1) { 304 | PRINTERR( 305 | "Unable to setsid (errno=%d %s). Exiting.\n", 306 | errno, 307 | strerror(errno) 308 | ); 309 | exit(1); 310 | } 311 | 312 | if (ioctl(STDIN_FILENO, TIOCSCTTY, 0) == -1) { 313 | DEBUG( 314 | "Unable to attach to controlling tty (errno=%d %s).\n", 315 | errno, 316 | strerror(errno) 317 | ); 318 | } 319 | DEBUG("setsid complete.\n"); 320 | } 321 | execvp(cmd[0], &cmd[0]); 322 | 323 | // if this point is reached, exec failed, so we should exit nonzero 324 | PRINTERR("%s: %s\n", cmd[0], strerror(errno)); 325 | return 2; 326 | } else { 327 | /* parent */ 328 | DEBUG("Child spawned with PID %d.\n", child_pid); 329 | if (chdir("/") == -1) { 330 | DEBUG("Unable to chdir(\"/\") (errno=%d %s)\n", 331 | errno, 332 | strerror(errno)); 333 | } 334 | for (;;) { 335 | int signum; 336 | sigwait(&all_signals, &signum); 337 | handle_signal(signum); 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | timeout = 20 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit>=0.5.0 2 | pytest 3 | # TODO: This pin is to work around an issue where the system pytest is too old. 4 | # We should fix this by not depending on the system pytest/python packages at 5 | # some point. 6 | pytest-timeout<2.0.0 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import subprocess 3 | import tempfile 4 | 5 | from distutils.command.build import build as orig_build 6 | from distutils.core import Command 7 | from setuptools import Distribution 8 | from setuptools import Extension 9 | from setuptools import setup 10 | from setuptools.command.install import install as orig_install 11 | 12 | 13 | try: 14 | from wheel.bdist_wheel import bdist_wheel as _bdist_wheel 15 | 16 | class bdist_wheel(_bdist_wheel): 17 | 18 | def finalize_options(self): 19 | _bdist_wheel.finalize_options(self) 20 | # Mark us as not a pure python package 21 | self.root_is_pure = False 22 | 23 | def get_tag(self): 24 | python, abi, plat = _bdist_wheel.get_tag(self) 25 | # We don't contain any python source 26 | python, abi = 'py2.py3', 'none' 27 | return python, abi, plat 28 | except ImportError: 29 | bdist_wheel = None 30 | 31 | 32 | class ExeDistribution(Distribution): 33 | c_executables = () 34 | 35 | 36 | class build(orig_build): 37 | sub_commands = orig_build.sub_commands + [ 38 | ('build_cexe', None), 39 | ] 40 | 41 | 42 | class install(orig_install): 43 | sub_commands = orig_install.sub_commands + [ 44 | ('install_cexe', None), 45 | ] 46 | 47 | 48 | class install_cexe(Command): 49 | description = 'install C executables' 50 | outfiles = () 51 | 52 | def initialize_options(self): 53 | self.build_dir = self.install_dir = None 54 | 55 | def finalize_options(self): 56 | # this initializes attributes based on other commands' attributes 57 | self.set_undefined_options('build', ('build_scripts', 'build_dir')) 58 | self.set_undefined_options( 59 | 'install', ('install_scripts', 'install_dir'), 60 | ) 61 | 62 | def run(self): 63 | 64 | self.outfiles = self.copy_tree(self.build_dir, self.install_dir) 65 | 66 | def get_outputs(self): 67 | return self.outfiles 68 | 69 | 70 | class build_cexe(Command): 71 | description = 'build C executables' 72 | 73 | def initialize_options(self): 74 | self.build_scripts = None 75 | self.build_temp = None 76 | 77 | def finalize_options(self): 78 | self.set_undefined_options( 79 | 'build', 80 | ('build_scripts', 'build_scripts'), 81 | ('build_temp', 'build_temp'), 82 | ) 83 | 84 | def run(self): 85 | # stolen and simplified from distutils.command.build_ext 86 | from distutils.ccompiler import new_compiler 87 | 88 | compiler = new_compiler(verbose=True) 89 | 90 | print('supports -static... ', end='') 91 | with tempfile.NamedTemporaryFile(mode='w', suffix='.c') as f: 92 | f.write('int main(void){}\n') 93 | f.flush() 94 | cmd = compiler.linker_exe + [f.name, '-static', '-o', os.devnull] 95 | with open(os.devnull, 'wb') as devnull: 96 | if not subprocess.call(cmd, stderr=devnull): 97 | print('yes') 98 | link_args = ['-static'] 99 | else: 100 | print('no') 101 | link_args = [] 102 | 103 | for exe in self.distribution.c_executables: 104 | objects = compiler.compile(exe.sources, output_dir=self.build_temp) 105 | compiler.link_executable( 106 | objects, 107 | exe.name, 108 | output_dir=self.build_scripts, 109 | extra_postargs=link_args, 110 | ) 111 | 112 | def get_outputs(self): 113 | return [ 114 | os.path.join(self.build_scripts, exe.name) 115 | for exe in self.distribution.c_executables 116 | ] 117 | 118 | 119 | setup( 120 | name='dumb-init', 121 | description='Simple wrapper script which proxies signals to a child', 122 | version=open('VERSION').read().strip(), 123 | author='Yelp', 124 | url='https://github.com/Yelp/dumb-init/', 125 | platforms='linux', 126 | packages=[], 127 | c_executables=[Extension('dumb-init', ['dumb-init.c'])], 128 | cmdclass={ 129 | 'bdist_wheel': bdist_wheel, 130 | 'build': build, 131 | 'build_cexe': build_cexe, 132 | 'install': install, 133 | 'install_cexe': install_cexe, 134 | }, 135 | distclass=ExeDistribution, 136 | ) 137 | -------------------------------------------------------------------------------- /testing/__init__.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import os 3 | import re 4 | import signal 5 | import sys 6 | import time 7 | from contextlib import contextmanager 8 | from subprocess import PIPE 9 | from subprocess import Popen 10 | 11 | # these signals cause dumb-init to suspend itself 12 | SUSPEND_SIGNALS = frozenset([ 13 | signal.SIGTSTP, 14 | signal.SIGTTOU, 15 | signal.SIGTTIN, 16 | ]) 17 | 18 | NORMAL_SIGNALS = frozenset( 19 | set(range(1, 32)) - 20 | {signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD} - 21 | SUSPEND_SIGNALS, 22 | ) 23 | 24 | 25 | @contextmanager 26 | def print_signals(args=()): 27 | """Start print_signals and yield dumb-init process and print_signals PID.""" 28 | proc = Popen( 29 | ( 30 | ('dumb-init',) + 31 | tuple(args) + 32 | (sys.executable, '-m', 'testing.print_signals') 33 | ), 34 | stdout=PIPE, 35 | ) 36 | line = proc.stdout.readline() 37 | m = re.match(b'^ready \\(pid: ([0-9]+)\\)\n$', line) 38 | assert m, line 39 | 40 | yield proc, m.group(1).decode('ascii') 41 | 42 | for pid in pid_tree(proc.pid): 43 | os.kill(pid, signal.SIGKILL) 44 | 45 | 46 | def child_pids(pid): 47 | """Return a list of direct child PIDs for the given PID.""" 48 | children = set() 49 | for p in os.listdir('/proc'): 50 | try: 51 | with open(os.path.join('/proc', p, 'stat')) as f: 52 | stat = f.read() 53 | m = re.match( 54 | r'^\d+ \(.+?\) ' 55 | # This field, state, is normally a single letter, but can be 56 | # "0" if there are some unusual security settings that prevent 57 | # reading the process state (happens under GitHub Actions with 58 | # QEMU for some reason). 59 | '[0a-zA-Z] ' 60 | r'(\d+) ', 61 | stat, 62 | ) 63 | assert m, stat 64 | ppid = int(m.group(1)) 65 | if ppid == pid: 66 | children.add(int(p)) 67 | except OSError: 68 | # Happens when the process exits after listing it, or between 69 | # opening stat and reading it. 70 | pass 71 | return children 72 | 73 | 74 | def pid_tree(pid): 75 | """Return a list of all descendant PIDs for the given PID.""" 76 | children = child_pids(pid) 77 | return { 78 | pid 79 | for child in children 80 | for pid in pid_tree(child) 81 | } | children 82 | 83 | 84 | def is_alive(pid): 85 | """Return whether a process is running with the given PID.""" 86 | return os.path.isdir(os.path.join('/proc', str(pid))) 87 | 88 | 89 | def process_state(pid): 90 | """Return a process' state, such as "stopped" or "running".""" 91 | with open(os.path.join('/proc', str(pid), 'status')) as f: 92 | status = f.read() 93 | m = re.search(r'^State:\s+[A-Z] \(([a-z]+)\)$', status, re.MULTILINE) 94 | return m.group(1) 95 | 96 | 97 | def sleep_until(fn, timeout=1.5): 98 | """Sleep until fn succeeds, or we time out.""" 99 | interval = 0.01 100 | so_far = 0 101 | while True: 102 | try: 103 | fn() 104 | except Exception: 105 | if so_far >= timeout: 106 | raise 107 | else: 108 | break 109 | time.sleep(interval) 110 | so_far += interval 111 | 112 | 113 | def kill_if_alive(pid, signum=signal.SIGKILL): 114 | """Kill a process, ignoring "no such process" errors.""" 115 | try: 116 | os.kill(pid, signum) 117 | except OSError as ex: 118 | if ex.errno != errno.ESRCH: # No such process 119 | raise 120 | -------------------------------------------------------------------------------- /testing/print_signals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Print received signals to stdout. 3 | 4 | Since all signals are printed and otherwise ignored, you'll need to send 5 | SIGKILL (kill -9) to this process to actually end it. 6 | """ 7 | import os 8 | import signal 9 | import sys 10 | import time 11 | 12 | 13 | CATCHABLE_SIGNALS = frozenset( 14 | set(range(1, 32)) - {signal.SIGKILL, signal.SIGSTOP, signal.SIGCHLD}, 15 | ) 16 | 17 | 18 | print_queue = [] 19 | last_signal = None 20 | 21 | 22 | def unbuffered_print(line): 23 | sys.stdout.write('{}\n'.format(line)) 24 | sys.stdout.flush() 25 | 26 | 27 | def print_signal(signum, _): 28 | print_queue.append(signum) 29 | 30 | 31 | if __name__ == '__main__': 32 | for signum in CATCHABLE_SIGNALS: 33 | signal.signal(signum, print_signal) 34 | 35 | unbuffered_print('ready (pid: {})'.format(os.getpid())) 36 | 37 | # loop forever just printing signals 38 | while True: 39 | if print_queue: 40 | signum = print_queue.pop() 41 | unbuffered_print(signum) 42 | 43 | if signum == signal.SIGINT and last_signal == signal.SIGINT: 44 | print('Received SIGINT twice, exiting.') 45 | exit(0) 46 | last_signal = signum 47 | 48 | time.sleep(0.01) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/dumb-init/5ccca9cb0498d8a4cb7579a43c8d3ee8c41469c5/tests/__init__.py -------------------------------------------------------------------------------- /tests/child_processes_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import signal 4 | import sys 5 | from subprocess import PIPE 6 | from subprocess import Popen 7 | 8 | import pytest 9 | 10 | from testing import is_alive 11 | from testing import kill_if_alive 12 | from testing import pid_tree 13 | from testing import sleep_until 14 | 15 | 16 | def spawn_and_kill_pipeline(): 17 | proc = Popen(( 18 | 'dumb-init', 19 | 'sh', '-c', 20 | "yes 'oh, hi' | tail & yes error | tail >&2", 21 | )) 22 | 23 | def assert_living_pids(): 24 | assert len(living_pids(pid_tree(os.getpid()))) == 6 25 | 26 | sleep_until(assert_living_pids) 27 | 28 | pids = pid_tree(os.getpid()) 29 | proc.send_signal(signal.SIGTERM) 30 | proc.wait() 31 | return pids 32 | 33 | 34 | def living_pids(pids): 35 | return {pid for pid in pids if is_alive(pid)} 36 | 37 | 38 | @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') 39 | def test_setsid_signals_entire_group(): 40 | """When dumb-init is running in setsid mode, it should signal the entire 41 | process group rooted at it. 42 | """ 43 | pids = spawn_and_kill_pipeline() 44 | 45 | def assert_no_living_pids(): 46 | assert len(living_pids(pids)) == 0 47 | 48 | sleep_until(assert_no_living_pids) 49 | 50 | 51 | @pytest.mark.usefixtures('both_debug_modes', 'setsid_disabled') 52 | def test_no_setsid_doesnt_signal_entire_group(): 53 | """When dumb-init is not running in setsid mode, it should only signal its 54 | immediate child. 55 | """ 56 | pids = spawn_and_kill_pipeline() 57 | 58 | def assert_four_living_pids(): 59 | assert len(living_pids(pids)) == 4 60 | 61 | sleep_until(assert_four_living_pids) 62 | 63 | for pid in living_pids(pids): 64 | kill_if_alive(pid) 65 | 66 | 67 | def spawn_process_which_dies_with_children(): 68 | """Spawn a process which spawns some children and then dies without 69 | signaling them, wrapped in dumb-init. 70 | 71 | Returns a tuple (child pid, child stdout pipe), where the child is 72 | print_signals. This is useful because you can signal the PID and see if 73 | anything gets printed onto the stdout pipe. 74 | """ 75 | proc = Popen( 76 | ( 77 | 'dumb-init', 78 | 'sh', '-c', 79 | 80 | # we need to sleep before the shell exits, or dumb-init might send 81 | # TERM to print_signals before it has had time to register custom 82 | # signal handlers 83 | '{python} -m testing.print_signals & sleep 1'.format( 84 | python=sys.executable, 85 | ), 86 | ), 87 | stdout=PIPE, 88 | ) 89 | proc.wait() 90 | assert proc.returncode == 0 91 | 92 | # read a line from print_signals, figure out its pid 93 | line = proc.stdout.readline() 94 | match = re.match(b'ready \\(pid: ([0-9]+)\\)\n', line) 95 | assert match, line 96 | child_pid = int(match.group(1)) 97 | 98 | # at this point, the shell and dumb-init have both exited, but 99 | # print_signals may or may not still be running (depending on whether 100 | # setsid mode is enabled) 101 | 102 | return child_pid, proc.stdout 103 | 104 | 105 | @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') 106 | def test_all_processes_receive_term_on_exit_if_setsid(): 107 | """If the child exits for some reason, dumb-init should send TERM to all 108 | processes in its session if setsid mode is enabled.""" 109 | child_pid, child_stdout = spawn_process_which_dies_with_children() 110 | 111 | # print_signals should have received TERM 112 | assert child_stdout.readline() == b'15\n' 113 | 114 | os.kill(child_pid, signal.SIGKILL) 115 | 116 | 117 | @pytest.mark.usefixtures('both_debug_modes', 'setsid_disabled') 118 | def test_processes_dont_receive_term_on_exit_if_no_setsid(): 119 | """If the child exits for some reason, dumb-init should not send TERM to 120 | any other processes if setsid mode is disabled.""" 121 | child_pid, child_stdout = spawn_process_which_dies_with_children() 122 | 123 | # print_signals should not have received TERM; to test this, we send it 124 | # some other signals and ensure they were received (and TERM wasn't) 125 | for signum in [1, 2, 3]: 126 | os.kill(child_pid, signum) 127 | assert child_stdout.readline() == str(signum).encode('ascii') + b'\n' 128 | 129 | os.kill(child_pid, signal.SIGKILL) 130 | 131 | 132 | @pytest.mark.parametrize( 133 | 'args', [ 134 | ('/doesnotexist',), 135 | ('--', '/doesnotexist'), 136 | ('-c', '/doesnotexist'), 137 | ('--single-child', '--', '/doesnotexist'), 138 | ], 139 | ) 140 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 141 | def test_fails_nonzero_with_bad_exec(args): 142 | """If dumb-init can't exec as requested, it should exit nonzero.""" 143 | proc = Popen(('dumb-init',) + args, stderr=PIPE) 144 | _, stderr = proc.communicate() 145 | assert proc.returncode != 0 146 | assert ( 147 | b'[dumb-init] /doesnotexist: No such file or directory\n' 148 | in stderr 149 | ) 150 | -------------------------------------------------------------------------------- /tests/cli_test.py: -------------------------------------------------------------------------------- 1 | """Sanity checks for command-line options.""" 2 | import re 3 | import signal 4 | from subprocess import PIPE 5 | from subprocess import Popen 6 | 7 | import pytest 8 | 9 | 10 | @pytest.fixture 11 | def current_version(): 12 | return open('VERSION').read().strip() 13 | 14 | 15 | def normalize_stderr(stderr): 16 | # dumb-init prints out argv[0] in its usage message. This should always be 17 | # just "dumb-init" under regular test scenarios here since that is how we 18 | # call it, but in CI the use of QEMU causes the argv[0] to be replaced with 19 | # the full path. 20 | # 21 | # This is supposed to be avoidable by: 22 | # 1) Setting the "P" flag in the binfmt register: 23 | # https://en.wikipedia.org/wiki/Binfmt_misc#Registration 24 | # This can be done by setting the QEMU_PRESERVE_PARENT env var when 25 | # calling binfmt. 26 | # 27 | # 2) Setting the "QEMU_ARGV0" env var to empty string for *all* 28 | # processes: 29 | # https://bugs.launchpad.net/qemu/+bug/1835839 30 | # 31 | # I can get it working properly in CI outside of Docker, but for some 32 | # reason not during Docker builds. This doesn't affect the built executable 33 | # so I decided to just punt on it. 34 | return re.sub(rb'(^|(?<=\s))[a-z/.]+/dumb-init', b'dumb-init', stderr) 35 | 36 | 37 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 38 | def test_no_arguments_prints_usage(): 39 | proc = Popen(('dumb-init'), stderr=PIPE) 40 | _, stderr = proc.communicate() 41 | assert proc.returncode != 0 42 | assert normalize_stderr(stderr) == ( 43 | b'Usage: dumb-init [option] program [args]\n' 44 | b'Try dumb-init --help for full usage.\n' 45 | ) 46 | 47 | 48 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 49 | def test_exits_invalid_with_invalid_args(): 50 | proc = Popen(('dumb-init', '--yolo', '/bin/true'), stderr=PIPE) 51 | _, stderr = proc.communicate() 52 | assert proc.returncode == 1 53 | assert normalize_stderr(stderr) in ( 54 | b"dumb-init: unrecognized option '--yolo'\n", # glibc 55 | b'dumb-init: unrecognized option: yolo\n', # musl 56 | ) 57 | 58 | 59 | @pytest.mark.parametrize('flag', ['-h', '--help']) 60 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 61 | def test_help_message(flag, current_version): 62 | """dumb-init should say something useful when called with the help flag, 63 | and exit zero. 64 | """ 65 | proc = Popen(('dumb-init', flag), stderr=PIPE) 66 | _, stderr = proc.communicate() 67 | assert proc.returncode == 0 68 | assert normalize_stderr(stderr) == ( 69 | b'dumb-init v' + current_version.encode('ascii') + b'\n' 70 | b'Usage: dumb-init [option] command [[arg] ...]\n' 71 | b'\n' 72 | b'dumb-init is a simple process supervisor that forwards signals to children.\n' 73 | b'It is designed to run as PID1 in minimal container environments.\n' 74 | b'\n' 75 | b'Optional arguments:\n' 76 | b' -c, --single-child Run in single-child mode.\n' 77 | b' In this mode, signals are only proxied to the\n' 78 | b' direct child and not any of its descendants.\n' 79 | b' -r, --rewrite s:r Rewrite received signal s to new signal r before proxying.\n' 80 | b' To ignore (not proxy) a signal, rewrite it to 0.\n' 81 | b' This option can be specified multiple times.\n' 82 | b' -v, --verbose Print debugging information to stderr.\n' 83 | b' -h, --help Print this help message and exit.\n' 84 | b' -V, --version Print the current version and exit.\n' 85 | b'\n' 86 | b'Full help is available online at https://github.com/Yelp/dumb-init\n' 87 | ) 88 | 89 | 90 | @pytest.mark.parametrize('flag', ['-V', '--version']) 91 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 92 | def test_version_message(flag, current_version): 93 | """dumb-init should print its version when asked to.""" 94 | 95 | proc = Popen(('dumb-init', flag), stderr=PIPE) 96 | _, stderr = proc.communicate() 97 | assert proc.returncode == 0 98 | assert stderr == b'dumb-init v' + current_version.encode('ascii') + b'\n' 99 | 100 | 101 | @pytest.mark.parametrize('flag', ['-v', '--verbose']) 102 | def test_verbose(flag): 103 | """dumb-init should print debug output when asked to.""" 104 | proc = Popen(('dumb-init', flag, 'echo', 'oh,', 'hi'), stdout=PIPE, stderr=PIPE) 105 | stdout, stderr = proc.communicate() 106 | assert proc.returncode == 0 107 | assert stdout == b'oh, hi\n' 108 | 109 | # child/parent race to print output after the fork(), can't guarantee exact order 110 | assert re.search(b'(^|\n)\\[dumb-init\\] setsid complete\\.\n', stderr), stderr # child 111 | assert re.search( # parent 112 | ( 113 | '(^|\n)\\[dumb-init\\] Child spawned with PID [0-9]+\\.\n' 114 | '.*' # child might print here 115 | '\\[dumb-init\\] Received signal {signal.SIGCHLD}\\.\n' 116 | '\\[dumb-init\\] A child with PID [0-9]+ exited with exit status 0.\n' 117 | '\\[dumb-init\\] Forwarded signal 15 to children\\.\n' 118 | '\\[dumb-init\\] Child exited with status 0\\. Goodbye\\.\n$' 119 | ).format(signal=signal).encode('utf8'), 120 | stderr, 121 | re.DOTALL, 122 | ), stderr 123 | 124 | 125 | @pytest.mark.parametrize('flag1', ['-v', '--verbose']) 126 | @pytest.mark.parametrize('flag2', ['-c', '--single-child']) 127 | def test_verbose_and_single_child(flag1, flag2): 128 | """dumb-init should print debug output when asked to.""" 129 | proc = Popen(('dumb-init', flag1, flag2, 'echo', 'oh,', 'hi'), stdout=PIPE, stderr=PIPE) 130 | stdout, stderr = proc.communicate() 131 | assert proc.returncode == 0 132 | assert stdout == b'oh, hi\n' 133 | assert re.match( 134 | ( 135 | '^\\[dumb-init\\] Child spawned with PID [0-9]+\\.\n' 136 | '\\[dumb-init\\] Received signal {signal.SIGCHLD}\\.\n' 137 | '\\[dumb-init\\] A child with PID [0-9]+ exited with exit status 0.\n' 138 | '\\[dumb-init\\] Forwarded signal 15 to children\\.\n' 139 | '\\[dumb-init\\] Child exited with status 0\\. Goodbye\\.\n$' 140 | ).format(signal=signal).encode('utf8'), 141 | stderr, 142 | ) 143 | 144 | 145 | @pytest.mark.parametrize( 146 | 'extra_args', [ 147 | ('-r',), 148 | ('-r', ''), 149 | ('-r', 'herp'), 150 | ('-r', 'herp:derp'), 151 | ('-r', '15'), 152 | ('-r', '15::12'), 153 | ('-r', '15:derp'), 154 | ('-r', '15:12', '-r'), 155 | ('-r', '15:12', '-r', '0'), 156 | ('-r', '15:12', '-r', '1:32'), 157 | ], 158 | ) 159 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 160 | def test_rewrite_errors(extra_args): 161 | proc = Popen( 162 | ('dumb-init',) + extra_args + ('echo', 'oh,', 'hi'), 163 | stdout=PIPE, stderr=PIPE, 164 | ) 165 | stdout, stderr = proc.communicate() 166 | assert proc.returncode == 1 167 | assert stderr == ( 168 | b'Usage: -r option takes :, where ' 169 | b'is between 1 and 31.\n' 170 | b'This option can be specified multiple times.\n' 171 | b'Use --help for full usage.\n' 172 | ) 173 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import mock 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture(autouse=True, scope='function') 8 | def clean_environment(): 9 | """Ensure all tests start with a clean environment. 10 | 11 | Even if tests properly clean up after themselves, we still need this in 12 | case the user runs tests with an already-polluted environment. 13 | """ 14 | with mock.patch.dict( 15 | os.environ, 16 | {'DUMB_INIT_DEBUG': '', 'DUMB_INIT_SETSID': ''}, 17 | ): 18 | yield 19 | 20 | 21 | @pytest.fixture(params=['1', '0']) 22 | def both_debug_modes(request): 23 | with mock.patch.dict(os.environ, {'DUMB_INIT_DEBUG': request.param}): 24 | yield 25 | 26 | 27 | @pytest.fixture 28 | def debug_disabled(): 29 | with mock.patch.dict(os.environ, {'DUMB_INIT_DEBUG': '0'}): 30 | yield 31 | 32 | 33 | @pytest.fixture(params=['1', '0']) 34 | def both_setsid_modes(request): 35 | with mock.patch.dict(os.environ, {'DUMB_INIT_SETSID': request.param}): 36 | yield 37 | 38 | 39 | @pytest.fixture 40 | def setsid_enabled(): 41 | with mock.patch.dict(os.environ, {'DUMB_INIT_SETSID': '1'}): 42 | yield 43 | 44 | 45 | @pytest.fixture 46 | def setsid_disabled(): 47 | with mock.patch.dict(os.environ, {'DUMB_INIT_SETSID': '0'}): 48 | yield 49 | -------------------------------------------------------------------------------- /tests/cwd_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from subprocess import PIPE 4 | from subprocess import run 5 | 6 | import pytest 7 | 8 | 9 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 10 | def test_working_directories(): 11 | """The child process must start in the working directory in which 12 | dumb-init was invoked, but dumb-init itself should not keep a 13 | reference to that.""" 14 | 15 | # We need absolute path to dumb-init since we pass cwd=/tmp to get 16 | # predictable output - so we can't rely on dumb-init being found 17 | # in the "." directory. 18 | dumb_init = os.path.realpath(shutil.which('dumb-init')) 19 | proc = run( 20 | ( 21 | dumb_init, 22 | 'sh', '-c', 'readlink /proc/$PPID/cwd && readlink /proc/$$/cwd', 23 | ), 24 | cwd='/tmp', stdout=PIPE, stderr=PIPE, 25 | ) 26 | 27 | assert proc.returncode == 0 28 | assert proc.stdout == b'/\n/tmp\n' 29 | -------------------------------------------------------------------------------- /tests/exit_status_test.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import sys 3 | from subprocess import Popen 4 | 5 | import pytest 6 | 7 | 8 | @pytest.mark.parametrize('exit_status', [0, 1, 2, 32, 64, 127, 254, 255]) 9 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 10 | def test_exit_status_regular_exit(exit_status): 11 | """dumb-init should exit with the same exit status as the process that it 12 | supervises when that process exits normally. 13 | """ 14 | proc = Popen(('dumb-init', 'sh', '-c', 'exit {}'.format(exit_status))) 15 | proc.wait() 16 | assert proc.returncode == exit_status 17 | 18 | 19 | @pytest.mark.parametrize( 20 | 'signal', [ 21 | signal.SIGTERM, 22 | signal.SIGHUP, 23 | signal.SIGQUIT, 24 | signal.SIGKILL, 25 | ], 26 | ) 27 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 28 | def test_exit_status_terminated_by_signal(signal): 29 | """dumb-init should exit with status 128 + signal when the child process is 30 | terminated by a signal. 31 | """ 32 | # We use Python because sh is "dash" on Debian and "bash" on others. 33 | # https://github.com/Yelp/dumb-init/issues/115 34 | proc = Popen(( 35 | 'dumb-init', sys.executable, '-c', 'import os; os.kill(os.getpid(), {})'.format( 36 | signal, 37 | ), 38 | )) 39 | proc.wait() 40 | assert proc.returncode == 128 + signal 41 | -------------------------------------------------------------------------------- /tests/proxies_signals_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | from itertools import chain 4 | 5 | import pytest 6 | 7 | from testing import NORMAL_SIGNALS 8 | from testing import print_signals 9 | from testing import process_state 10 | 11 | 12 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 13 | def test_proxies_signals(): 14 | """Ensure dumb-init proxies regular signals to its child.""" 15 | with print_signals() as (proc, _): 16 | for signum in NORMAL_SIGNALS: 17 | proc.send_signal(signum) 18 | assert proc.stdout.readline() == '{}\n'.format(signum).encode('ascii') 19 | 20 | 21 | def _rewrite_map_to_args(rewrite_map): 22 | return chain.from_iterable( 23 | ('-r', '{}:{}'.format(src, dst)) for src, dst in rewrite_map.items() 24 | ) 25 | 26 | 27 | @pytest.mark.parametrize( 28 | 'rewrite_map,sequence,expected', [ 29 | ( 30 | {}, 31 | [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], 32 | [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], 33 | ), 34 | 35 | ( 36 | {signal.SIGTERM: signal.SIGINT}, 37 | [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], 38 | [signal.SIGINT, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], 39 | ), 40 | 41 | ( 42 | { 43 | signal.SIGTERM: signal.SIGINT, 44 | signal.SIGINT: signal.SIGTERM, 45 | signal.SIGQUIT: signal.SIGQUIT, 46 | }, 47 | [signal.SIGTERM, signal.SIGQUIT, signal.SIGCONT, signal.SIGINT], 48 | [signal.SIGINT, signal.SIGQUIT, signal.SIGCONT, signal.SIGTERM], 49 | ), 50 | 51 | # Lowest possible and highest possible signals. 52 | ( 53 | {1: 31, 31: 1}, 54 | [1, 31], 55 | [31, 1], 56 | ), 57 | ], 58 | ) 59 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 60 | def test_proxies_signals_with_rewrite(rewrite_map, sequence, expected): 61 | """Ensure dumb-init can rewrite signals.""" 62 | with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _): 63 | for send, expect_receive in zip(sequence, expected): 64 | proc.send_signal(send) 65 | assert proc.stdout.readline() == '{}\n'.format(expect_receive).encode('ascii') 66 | 67 | 68 | @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') 69 | def test_default_rewrites_can_be_overriden_with_setsid_enabled(): 70 | """In setsid mode, dumb-init should allow overwriting the default 71 | rewrites (but still suspend itself). 72 | """ 73 | rewrite_map = { 74 | signal.SIGTTIN: signal.SIGTERM, 75 | signal.SIGTTOU: signal.SIGINT, 76 | signal.SIGTSTP: signal.SIGHUP, 77 | } 78 | with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _): 79 | for send, expect_receive in rewrite_map.items(): 80 | assert process_state(proc.pid) in ['running', 'sleeping'] 81 | proc.send_signal(send) 82 | 83 | assert proc.stdout.readline() == '{}\n'.format(expect_receive).encode('ascii') 84 | os.waitpid(proc.pid, os.WUNTRACED) 85 | assert process_state(proc.pid) == 'stopped' 86 | 87 | proc.send_signal(signal.SIGCONT) 88 | assert proc.stdout.readline() == '{}\n'.format(signal.SIGCONT).encode('ascii') 89 | assert process_state(proc.pid) in ['running', 'sleeping'] 90 | 91 | 92 | @pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes') 93 | def test_ignored_signals_are_not_proxied(): 94 | """Ensure dumb-init can ignore signals.""" 95 | rewrite_map = { 96 | signal.SIGTERM: signal.SIGQUIT, 97 | signal.SIGINT: 0, 98 | signal.SIGWINCH: 0, 99 | } 100 | with print_signals(_rewrite_map_to_args(rewrite_map)) as (proc, _): 101 | proc.send_signal(signal.SIGTERM) 102 | proc.send_signal(signal.SIGINT) 103 | assert proc.stdout.readline() == '{}\n'.format(signal.SIGQUIT).encode('ascii') 104 | 105 | proc.send_signal(signal.SIGWINCH) 106 | proc.send_signal(signal.SIGHUP) 107 | assert proc.stdout.readline() == '{}\n'.format(signal.SIGHUP).encode('ascii') 108 | -------------------------------------------------------------------------------- /tests/shell_background_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from signal import SIGCONT 3 | 4 | import pytest 5 | 6 | from testing import print_signals 7 | from testing import process_state 8 | from testing import sleep_until 9 | from testing import SUSPEND_SIGNALS 10 | 11 | 12 | @pytest.mark.usefixtures('both_debug_modes', 'setsid_enabled') 13 | def test_shell_background_support_setsid(): 14 | """In setsid mode, dumb-init should suspend itself and its children when it 15 | receives SIGTSTP, SIGTTOU, or SIGTTIN. 16 | """ 17 | with print_signals() as (proc, pid): 18 | for signum in SUSPEND_SIGNALS: 19 | # both dumb-init and print_signals should be running or sleeping 20 | assert process_state(pid) in ['running', 'sleeping'] 21 | assert process_state(proc.pid) in ['running', 'sleeping'] 22 | 23 | # both should now suspend 24 | proc.send_signal(signum) 25 | 26 | def assert_both_stopped(): 27 | assert process_state(proc.pid) == process_state(pid) == 'stopped' 28 | 29 | sleep_until(assert_both_stopped) 30 | 31 | # and then both wake up again 32 | proc.send_signal(SIGCONT) 33 | assert ( 34 | proc.stdout.readline() == '{}\n'.format(SIGCONT).encode('ascii') 35 | ) 36 | assert process_state(pid) in ['running', 'sleeping'] 37 | assert process_state(proc.pid) in ['running', 'sleeping'] 38 | 39 | 40 | @pytest.mark.usefixtures('both_debug_modes', 'setsid_disabled') 41 | def test_shell_background_support_without_setsid(): 42 | """In non-setsid mode, dumb-init should forward the signals SIGTSTP, 43 | SIGTTOU, and SIGTTIN, and then suspend itself. 44 | """ 45 | with print_signals() as (proc, _): 46 | for signum in SUSPEND_SIGNALS: 47 | assert process_state(proc.pid) in ['running', 'sleeping'] 48 | proc.send_signal(signum) 49 | assert proc.stdout.readline() == '{}\n'.format(signum).encode('ascii') 50 | os.waitpid(proc.pid, os.WUNTRACED) 51 | assert process_state(proc.pid) == 'stopped' 52 | 53 | proc.send_signal(SIGCONT) 54 | assert ( 55 | proc.stdout.readline() == '{}\n'.format(SIGCONT).encode('ascii') 56 | ) 57 | assert process_state(proc.pid) in ['running', 'sleeping'] 58 | -------------------------------------------------------------------------------- /tests/test-zombies: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | # Spawn a zombie process, and ensure it gets reaped. 3 | # This test is only useful when run on an empty container with 4 | # dumb-init as PID1. 5 | # 6 | # We run it as the last step of the integration tests inside our Docker 7 | # containers. Since dumb-init must run as PID 1, we don't use pytest and 8 | # instead write it in bash (which gets executed by PID1 dumb-init). 9 | set -o pipefail 10 | 11 | bash -euxc "bash -euxc 'echo i am a zombie' &" & 12 | 13 | sleep 1 14 | num_zombies=$(ps -A -o state | (grep 'Z' || true) | wc -l) 15 | 16 | if [ "$num_zombies" -ne 0 ]; then 17 | echo "Expected no zombies, but instead there were ${num_zombies}." 18 | exit 1 19 | fi 20 | -------------------------------------------------------------------------------- /tests/tty_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pty 3 | import re 4 | import signal 5 | import termios 6 | import time 7 | 8 | import pytest 9 | 10 | 11 | EOF = b'\x04' 12 | 13 | 14 | def ttyflags(fd): 15 | """normalize tty i/o for testing""" 16 | # see: 17 | # http://www.gnu.org/software/libc/manual/html_mono/libc.html#Output-Modes 18 | attrs = termios.tcgetattr(fd) 19 | attrs[1] &= ~termios.OPOST # don't munge output 20 | attrs[3] &= ~termios.ECHO # don't echo input 21 | termios.tcsetattr(fd, termios.TCSANOW, attrs) 22 | 23 | 24 | def readall(fd): 25 | """read until EOF""" 26 | result = b'' 27 | while True: 28 | try: 29 | chunk = os.read(fd, 1 << 10) 30 | except OSError as error: 31 | if error.errno == 5: # linux pty EOF 32 | return result 33 | else: 34 | raise 35 | if chunk == b'': 36 | return result 37 | else: 38 | result += chunk 39 | 40 | 41 | # disable debug output so it doesn't break our assertion 42 | @pytest.mark.usefixtures('debug_disabled') 43 | def test_tty(): 44 | """Ensure processes under dumb-init can write successfully, given a tty.""" 45 | pid, fd = pty.fork() 46 | if pid == 0: 47 | os.execvp('dumb-init', ('dumb-init', 'tac')) 48 | else: 49 | # write to tac via the pty and verify its output 50 | ttyflags(fd) 51 | assert os.write(fd, b'1\n2\n3\n') == 6 52 | assert os.write(fd, EOF * 2) == 2 53 | output = readall(fd) 54 | assert os.waitpid(pid, 0) == (pid, 0) 55 | 56 | assert output == b'3\n2\n1\n', repr(output) 57 | 58 | 59 | @pytest.mark.usefixtures('both_debug_modes') 60 | @pytest.mark.usefixtures('both_setsid_modes') 61 | def test_child_gets_controlling_tty_if_we_had_one(): 62 | """If dumb-init has a controlling TTY, it should give it to the child. 63 | 64 | To test this, we make a new TTY then exec "dumb-init bash" and ensure that 65 | the shell has working job control. 66 | """ 67 | pid, sfd = pty.fork() 68 | if pid == 0: 69 | os.execvp('dumb-init', ('dumb-init', 'bash', '-m')) 70 | else: 71 | ttyflags(sfd) 72 | 73 | # We might get lots of extra output from the shell, so print something 74 | # we can match on easily. 75 | assert os.write(sfd, b'echo "flags are: [[$-]]"\n') == 25 76 | assert os.write(sfd, b'exit 0\n') == 7 77 | output = readall(sfd) 78 | assert os.waitpid(pid, 0) == (pid, 0), output 79 | 80 | m = re.search(b'flags are: \\[\\[([a-zA-Z]+)\\]\\]\n', output) 81 | assert m, output 82 | 83 | # "m" is job control 84 | flags = m.group(1) 85 | assert b'm' in flags 86 | 87 | 88 | def test_sighup_sigcont_ignored_if_was_session_leader(): 89 | """The first SIGHUP/SIGCONT should be ignored if dumb-init is the session leader. 90 | 91 | Due to TTY quirks (#136), when dumb-init is the session leader and forks, 92 | it needs to avoid forwarding the first SIGHUP and SIGCONT to the child. 93 | Otherwise, the child might receive the SIGHUP post-exec and terminate 94 | itself. 95 | 96 | You can "force" this race by adding a `sleep(1)` before the signal handling 97 | loop in dumb-init's code, but it's hard to reproduce the race reliably in a 98 | test otherwise. Because of this, we're stuck just asserting debug messages. 99 | """ 100 | pid, fd = pty.fork() 101 | if pid == 0: 102 | # child 103 | os.execvp('dumb-init', ('dumb-init', '-v', 'sleep', '20')) 104 | else: 105 | # parent 106 | ttyflags(fd) 107 | 108 | # send another SIGCONT to make sure only the first is ignored 109 | time.sleep(0.5) 110 | os.kill(pid, signal.SIGHUP) 111 | 112 | output = readall(fd).decode('UTF-8') 113 | 114 | assert 'Ignoring tty hand-off signal {}.'.format(signal.SIGHUP) in output 115 | assert 'Ignoring tty hand-off signal {}.'.format(signal.SIGCONT) in output 116 | 117 | assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGHUP) in output 118 | assert '[dumb-init] Forwarded signal {} to children.'.format(signal.SIGCONT) not in output 119 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py38,gcov 3 | 4 | [testenv] 5 | deps = -r{toxinidir}/requirements-dev.txt 6 | commands = 7 | pytest 8 | 9 | [testenv:gcov] 10 | skip_install = True 11 | basepython = /usr/bin/python3.8 12 | commands = 13 | {toxinidir}/ci/gcov-build {envbindir} 14 | {[testenv]commands} 15 | {toxinidir}/ci/gcov-report 16 | 17 | [testenv:pre-commit] 18 | basepython = /usr/bin/python3.8 19 | commands = pre-commit {posargs:run --all-files} 20 | 21 | [flake8] 22 | max-line-length = 119 23 | 24 | [pep8] 25 | # autopep8 will rewrite lines to be shorter, even though we raised the length 26 | ignore = E501 27 | --------------------------------------------------------------------------------