Full Code of Yelp/dumb-init for AI

master 5ccca9cb0498 cached
48 files
76.6 KB
21.4k tokens
73 symbols
1 requests
Download .txt
Repository: Yelp/dumb-init
Branch: master
Commit: 5ccca9cb0498
Files: 48
Total size: 76.6 KB

Directory structure:
gitextract_n9en36u7/

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

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
.tox
.git


================================================
FILE: .github/workflows/ci.yaml
================================================
name: CI
on: push
jobs:
    build-and-test:
        runs-on: ubuntu-20.04
        strategy:
            fail-fast: false
            matrix:
                include:
                    - arch: amd64
                      manylinux_arch: x86_64
                      docker_image: debian:buster

                    - arch: arm64
                      manylinux_arch: aarch64
                      docker_image: arm64v8/debian:buster

                    - arch: ppc64le
                      manylinux_arch: ppc64le
                      docker_image: ppc64le/debian:buster

                    - arch: s390x
                      manylinux_arch: s390x
                      docker_image: s390x/debian:buster

        env:
            BASE_IMAGE: ${{ matrix.docker_image }}

        steps:
            - uses: actions/checkout@v2

            - name: Set up QEMU
              id: qemu
              uses: docker/setup-qemu-action@v1
              if: ${{ matrix.arch != 'amd64' }}
              with:
                image: tonistiigi/binfmt:latest

            - name: Build Docker image
              run: make docker-image

            - name: Run python tests
              run: docker run --rm -v $(pwd):/mnt:rw dumb-init-build /mnt/ci/docker-python-test

            - name: Build Debian package
              run: docker run --init --rm -v $(pwd):/mnt:rw dumb-init-build make -C /mnt builddeb

            - name: Test built Debian package
              # XXX: This uses the clean base image (not the build one) to make
              # sure it installs in a clean image without any hidden dependencies.
              run: docker run --rm -v $(pwd):/mnt:rw ${{ matrix.docker_image }} /mnt/ci/docker-deb-test

            - name: Build wheels
              run: sudo make python-dists-${{ matrix.manylinux_arch }}

            - name: Upload build artifacts
              uses: actions/upload-artifact@v2
              with:
                  name: ${{ matrix.arch }}
                  path: dist


================================================
FILE: .gitignore
================================================
*.deb
*.egg-info
*.gc*
*.o
*.py[cod]
.pytest_cache
.tox
__pycache__/
build/
dist/
dumb-init


================================================
FILE: .pre-commit-config.yaml
================================================
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    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: name-tests-test
    -   id: requirements-txt-fixer
    -   id: trailing-whitespace
-   repo: https://github.com/pre-commit/mirrors-autopep8
    rev: v2.0.0
    hooks:
    -   id: autopep8
-   repo: https://github.com/pycqa/flake8
    rev: 6.0.0
    hooks:
    -   id: flake8
-   repo: https://github.com/asottile/reorder_python_imports
    rev: v3.9.0
    hooks:
    -   id: reorder-python-imports
        args: ['--py3-plus']
-   repo: https://github.com/Lucas-C/pre-commit-hooks
    rev: v1.3.1
    hooks:
    -   id: remove-tabs
-   repo: https://github.com/asottile/pyupgrade
    rev: v3.3.0
    hooks:
    -   id: pyupgrade
        args: ['--py3-plus']
-   repo: https://github.com/asottile/add-trailing-comma
    rev: v2.3.0
    hooks:
    -   id: add-trailing-comma
        args: ['--py36-plus']


================================================
FILE: CONTRIBUTING.md
================================================
Contributing 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)


================================================
FILE: Dockerfile
================================================
ARG BASE_IMAGE=debian:buster
FROM $BASE_IMAGE

LABEL maintainer="Chris Kuehl <ckuehl@yelp.com>"

# 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 \
        python3-distutils \
        python3-setuptools \
        python3-pip \
    && 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/*


================================================
FILE: LICENSE
================================================
The 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.


================================================
FILE: MANIFEST.in
================================================
include dumb-init.c
include VERSION
include VERSION.h


================================================
FILE: Makefile
================================================
SHELL=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: python-dists-x86_64 python-dists-aarch64 python-dists-ppc64le python-dists-s390x

.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/manylinux2014_$*:latest \
		bash -exc ' \
			/opt/python/cp38-cp38/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 --init --user $$(id -u):$$(id -g) -v $(PWD):/tmp/mnt dumb-init-build make builddeb

.PHONY: docker-image
docker-image:
	docker build $(if $(BASE_IMAGE),--build-arg BASE_IMAGE=$(BASE_IMAGE)) -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


================================================
FILE: README.md
================================================
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.5/dumb-init_1.2.5_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.5/dumb-init_1.2.5_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


================================================
FILE: VERSION
================================================
1.2.5


================================================
FILE: VERSION.h
================================================
// 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;


================================================
FILE: ci/artifact-upload
================================================
#!/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


================================================
FILE: ci/docker-deb-test
================================================
#!/bin/bash -eux
set -o pipefail

apt-get update
apt-get -y --no-install-recommends install python3-pip procps

cd /mnt
dpkg -i dist/*.deb
pip3 install -r requirements-dev.txt
pytest tests/

exec dumb-init /mnt/tests/test-zombies


================================================
FILE: ci/docker-python-test
================================================
#!/bin/bash -eux
set -euo pipefail

cd /mnt

python3 setup.py clean
python3 setup.py sdist
pip3 install -vv dist/*.tar.gz
pip3 install -r requirements-dev.txt
pytest-3 -vv tests/

exec dumb-init /mnt/tests/test-zombies


================================================
FILE: ci/gcov-build
================================================
#!/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


================================================
FILE: ci/gcov-report
================================================
#!/usr/bin/env bash
set -euo pipefail
gcov -f dumb-init.c
grep '#####' dumb-init.c.gcov | cut -d':' -f2 | xargs echo "Missing:"


================================================
FILE: circle.yml
================================================
machine:
    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


================================================
FILE: debian/.gitignore
================================================
/*.log
/*.substvars
/files
/substvars
/dumb-init/
/dumb-init.1
/debhelper-build-stamp


================================================
FILE: debian/changelog
================================================
dumb-init (1.2.5) unstable; urgency=medium

  * Change the working directory in the parent process to "/" after forking.

    https://github.com/Yelp/dumb-init/pull/210

    Thanks to @Villemoes for the patch!

 -- Chris Kuehl <ckuehl@yelp.com>  Thu, 10 Dec 2020 10:54:47 -0800

dumb-init (1.2.4) unstable; urgency=medium

  * Actually fix the bug that can cause `--help` or `--version` to crash in
    some scenarios.

    https://github.com/Yelp/dumb-init/pull/215

    Thanks to @suve for the patch!

 -- Chris Kuehl <ckuehl@yelp.com>  Mon, 07 Dec 2020 11:58:06 -0800

dumb-init (1.2.3) unstable; urgency=medium

  * Fix a bug that can cause `--help` or `--version` to crash in some
    scenarios.

    https://github.com/Yelp/dumb-init/pull/213

    Thanks to @suve for the patch!

 -- Chris Kuehl <ckuehl@yelp.com>  Wed, 02 Dec 2020 10:43:02 -0800

dumb-init (1.2.2) unstable; urgency=medium

  * Fix a race condition which can cause the child to receive SIGHUP and
    SIGCONT very shortly after start (#174).

    In general this was very rare, but some environments (especially some
    container and virtualization environments) appear to encounter it at a
    much higher rate, possibly due to scheduler quirks.

 -- Chris Kuehl <ckuehl@yelp.com>  Wed, 01 Aug 2018 16:36:22 -0700

dumb-init (1.2.1) unstable; urgency=medium

  * Fix verbose debug logging for ignored signals.

    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 <ckuehl@yelp.com>  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 <image> 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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <ckuehl@ocf.berkeley.edu>  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 <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  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 <rkwills@yelp.com>  Thu, 10 Sep 2015 13:33:25 -0700

dumb-init (0.1.0) unstable; urgency=low

  * Add process group support

 -- Chris Kuehl <ckuehl@yelp.com>  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 <ckuehl@yelp.com>  Thu, 06 Aug 2015 13:51:38 -0700

dumb-init (0.0.1) unstable; urgency=low

  * Initial release.

 -- Chris Kuehl <ckuehl@yelp.com>  Thu, 06 Aug 2015 13:51:38 -0700


================================================
FILE: debian/clean
================================================
tests/*.pyc
tests/*/*.pyc


================================================
FILE: debian/compat
================================================
9


================================================
FILE: debian/control
================================================
Source: dumb-init
Section: utils
Priority: extra
Maintainer: Chris Kuehl <ckuehl@yelp.com>
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.


================================================
FILE: debian/copyright
================================================
Format: 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 <onlyjob@debian.org>
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.


================================================
FILE: debian/docs
================================================
README*


================================================
FILE: debian/help2man
================================================
[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


================================================
FILE: debian/install
================================================
dumb-init /usr/bin/


================================================
FILE: debian/lintian-overrides
================================================
dumb-init binary: statically-linked-binary


================================================
FILE: debian/manpages
================================================
debian/dumb-init.1


================================================
FILE: debian/rules
================================================
#!/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/


================================================
FILE: debian/source/format
================================================
3.0 (native)


================================================
FILE: dumb-init.c
================================================
/*
 * 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 <assert.h>
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#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 <signum>:<signum>, where <signum> "
        "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);
        }
    }
}


================================================
FILE: pytest.ini
================================================
[pytest]
timeout = 20


================================================
FILE: requirements-dev.txt
================================================
pre-commit>=0.5.0
pytest
# TODO: This pin is to work around an issue where the system pytest is too old.
# We should fix this by not depending on the system pytest/python packages at
# some point.
pytest-timeout<2.0.0


================================================
FILE: setup.py
================================================
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',
    packages=[],
    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,
)


================================================
FILE: testing/__init__.py
================================================
import errno
import os
import re
import signal
import sys
import time
from contextlib import contextmanager
from subprocess import PIPE
from subprocess import Popen

# 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 os.listdir('/proc'):
        try:
            with open(os.path.join('/proc', p, 'stat')) as f:
                stat = f.read()
            m = re.match(
                r'^\d+ \(.+?\) '
                # This field, state, is normally a single letter, but can be
                # "0" if there are some unusual security settings that prevent
                # reading the process state (happens under GitHub Actions with
                # QEMU for some reason).
                '[0a-zA-Z] '
                r'(\d+) ',
                stat,
            )
            assert m, stat
            ppid = int(m.group(1))
            if ppid == pid:
                children.add(int(p))
        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 os.path.isdir(os.path.join('/proc', str(pid)))


def process_state(pid):
    """Return a process' state, such as "stopped" or "running"."""
    with open(os.path.join('/proc', str(pid), 'status')) as f:
        status = f.read()
    m = re.search(r'^State:\s+[A-Z] \(([a-z]+)\)$', status, re.MULTILINE)
    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


================================================
FILE: testing/print_signals.py
================================================
#!/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.
"""
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)


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/child_processes_test.py
================================================
import 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
    )


================================================
FILE: tests/cli_test.py
================================================
"""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()


def normalize_stderr(stderr):
    # dumb-init prints out argv[0] in its usage message. This should always be
    # just "dumb-init" under regular test scenarios here since that is how we
    # call it, but in CI the use of QEMU causes the argv[0] to be replaced with
    # the full path.
    #
    # This is supposed to be avoidable by:
    #   1) Setting the "P" flag in the binfmt register:
    #      https://en.wikipedia.org/wiki/Binfmt_misc#Registration
    #      This can be done by setting the QEMU_PRESERVE_PARENT env var when
    #      calling binfmt.
    #
    #   2) Setting the "QEMU_ARGV0" env var to empty string for *all*
    #      processes:
    #      https://bugs.launchpad.net/qemu/+bug/1835839
    #
    # I can get it working properly in CI outside of Docker, but for some
    # reason not during Docker builds. This doesn't affect the built executable
    # so I decided to just punt on it.
    return re.sub(rb'(^|(?<=\s))[a-z/.]+/dumb-init', b'dumb-init', stderr)


@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_no_arguments_prints_usage():
    proc = Popen(('dumb-init'), stderr=PIPE)
    _, stderr = proc.communicate()
    assert proc.returncode != 0
    assert normalize_stderr(stderr) == (
        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 normalize_stderr(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 normalize_stderr(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 <signum>:<signum>, where <signum> '
        b'is between 1 and 31.\n'
        b'This option can be specified multiple times.\n'
        b'Use --help for full usage.\n'
    )


================================================
FILE: tests/conftest.py
================================================
import 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


================================================
FILE: tests/cwd_test.py
================================================
import os
import shutil
from subprocess import PIPE
from subprocess import run

import pytest


@pytest.mark.usefixtures('both_debug_modes', 'both_setsid_modes')
def test_working_directories():
    """The child process must start in the working directory in which
    dumb-init was invoked, but dumb-init itself should not keep a
    reference to that."""

    # We need absolute path to dumb-init since we pass cwd=/tmp to get
    # predictable output - so we can't rely on dumb-init being found
    # in the "." directory.
    dumb_init = os.path.realpath(shutil.which('dumb-init'))
    proc = run(
        (
            dumb_init,
            'sh', '-c', 'readlink /proc/$PPID/cwd && readlink /proc/$$/cwd',
        ),
        cwd='/tmp', stdout=PIPE, stderr=PIPE,
    )

    assert proc.returncode == 0
    assert proc.stdout == b'/\n/tmp\n'


================================================
FILE: tests/exit_status_test.py
================================================
import 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


================================================
FILE: tests/proxies_signals_test.py
================================================
import 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')


================================================
FILE: tests/shell_background_test.py
================================================
import 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']


================================================
FILE: tests/test-zombies
================================================
#!/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


================================================
FILE: tests/tty_test.py
================================================
import 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


================================================
FILE: tox.ini
================================================
[tox]
envlist = py38,gcov

[testenv]
deps = -r{toxinidir}/requirements-dev.txt
commands =
    pytest

[testenv:gcov]
skip_install = True
basepython = /usr/bin/python3.8
commands =
    {toxinidir}/ci/gcov-build {envbindir}
    {[testenv]commands}
    {toxinidir}/ci/gcov-report

[testenv:pre-commit]
basepython = /usr/bin/python3.8
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
Download .txt
gitextract_n9en36u7/

├── .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
Download .txt
SYMBOL INDEX (73 symbols across 12 files)

FILE: dumb-init.c
  function translate_signal (line 49) | int translate_signal(int signum) {
  function forward_signal (line 63) | void forward_signal(int signum) {
  function handle_signal (line 93) | void handle_signal(int signum) {
  function print_help (line 127) | void print_help(char *argv[]) {
  function print_rewrite_signum_help (line 152) | void print_rewrite_signum_help() {
  function parse_rewrite_signum (line 164) | void parse_rewrite_signum(char *arg) {
  function set_rewrite_to_sigstop_if_not_defined (line 177) | void set_rewrite_to_sigstop_if_not_defined(int signum) {
  type option (line 185) | struct option
  function dummy (line 250) | void dummy(int signum) {}
  function main (line 252) | int main(int argc, char *argv[]) {

FILE: setup.py
  class bdist_wheel (line 16) | class bdist_wheel(_bdist_wheel):
    method finalize_options (line 18) | def finalize_options(self):
    method get_tag (line 23) | def get_tag(self):
  class ExeDistribution (line 32) | class ExeDistribution(Distribution):
  class build (line 36) | class build(orig_build):
  class install (line 42) | class install(orig_install):
  class install_cexe (line 48) | class install_cexe(Command):
    method initialize_options (line 52) | def initialize_options(self):
    method finalize_options (line 55) | def finalize_options(self):
    method run (line 62) | def run(self):
    method get_outputs (line 66) | def get_outputs(self):
  class build_cexe (line 70) | class build_cexe(Command):
    method initialize_options (line 73) | def initialize_options(self):
    method finalize_options (line 77) | def finalize_options(self):
    method run (line 84) | def run(self):
    method get_outputs (line 112) | def get_outputs(self):

FILE: testing/__init__.py
  function print_signals (line 26) | def print_signals(args=()):
  function child_pids (line 46) | def child_pids(pid):
  function pid_tree (line 74) | def pid_tree(pid):
  function is_alive (line 84) | def is_alive(pid):
  function process_state (line 89) | def process_state(pid):
  function sleep_until (line 97) | def sleep_until(fn, timeout=1.5):
  function kill_if_alive (line 113) | def kill_if_alive(pid, signum=signal.SIGKILL):

FILE: testing/print_signals.py
  function unbuffered_print (line 22) | def unbuffered_print(line):
  function print_signal (line 27) | def print_signal(signum, _):

FILE: tests/child_processes_test.py
  function spawn_and_kill_pipeline (line 16) | def spawn_and_kill_pipeline():
  function living_pids (line 34) | def living_pids(pids):
  function test_setsid_signals_entire_group (line 39) | def test_setsid_signals_entire_group():
  function test_no_setsid_doesnt_signal_entire_group (line 52) | def test_no_setsid_doesnt_signal_entire_group():
  function spawn_process_which_dies_with_children (line 67) | def spawn_process_which_dies_with_children():
  function test_all_processes_receive_term_on_exit_if_setsid (line 106) | def test_all_processes_receive_term_on_exit_if_setsid():
  function test_processes_dont_receive_term_on_exit_if_no_setsid (line 118) | def test_processes_dont_receive_term_on_exit_if_no_setsid():
  function test_fails_nonzero_with_bad_exec (line 141) | def test_fails_nonzero_with_bad_exec(args):

FILE: tests/cli_test.py
  function current_version (line 11) | def current_version():
  function normalize_stderr (line 15) | def normalize_stderr(stderr):
  function test_no_arguments_prints_usage (line 38) | def test_no_arguments_prints_usage():
  function test_exits_invalid_with_invalid_args (line 49) | def test_exits_invalid_with_invalid_args():
  function test_help_message (line 61) | def test_help_message(flag, current_version):
  function test_version_message (line 92) | def test_version_message(flag, current_version):
  function test_verbose (line 102) | def test_verbose(flag):
  function test_verbose_and_single_child (line 127) | def test_verbose_and_single_child(flag1, flag2):
  function test_rewrite_errors (line 160) | def test_rewrite_errors(extra_args):

FILE: tests/conftest.py
  function clean_environment (line 8) | def clean_environment():
  function both_debug_modes (line 22) | def both_debug_modes(request):
  function debug_disabled (line 28) | def debug_disabled():
  function both_setsid_modes (line 34) | def both_setsid_modes(request):
  function setsid_enabled (line 40) | def setsid_enabled():
  function setsid_disabled (line 46) | def setsid_disabled():

FILE: tests/cwd_test.py
  function test_working_directories (line 10) | def test_working_directories():

FILE: tests/exit_status_test.py
  function test_exit_status_regular_exit (line 10) | def test_exit_status_regular_exit(exit_status):
  function test_exit_status_terminated_by_signal (line 28) | def test_exit_status_terminated_by_signal(signal):

FILE: tests/proxies_signals_test.py
  function test_proxies_signals (line 13) | def test_proxies_signals():
  function _rewrite_map_to_args (line 21) | def _rewrite_map_to_args(rewrite_map):
  function test_proxies_signals_with_rewrite (line 60) | def test_proxies_signals_with_rewrite(rewrite_map, sequence, expected):
  function test_default_rewrites_can_be_overriden_with_setsid_enabled (line 69) | def test_default_rewrites_can_be_overriden_with_setsid_enabled():
  function test_ignored_signals_are_not_proxied (line 93) | def test_ignored_signals_are_not_proxied():

FILE: tests/shell_background_test.py
  function test_shell_background_support_setsid (line 13) | def test_shell_background_support_setsid():
  function test_shell_background_support_without_setsid (line 41) | def test_shell_background_support_without_setsid():

FILE: tests/tty_test.py
  function ttyflags (line 14) | def ttyflags(fd):
  function readall (line 24) | def readall(fd):
  function test_tty (line 43) | def test_tty():
  function test_child_gets_controlling_tty_if_we_had_one (line 61) | def test_child_gets_controlling_tty_if_we_had_one():
  function test_sighup_sigcont_ignored_if_was_session_leader (line 88) | def test_sighup_sigcont_ignored_if_was_session_leader():
Condensed preview — 48 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (84K chars).
[
  {
    "path": ".dockerignore",
    "chars": 10,
    "preview": ".tox\n.git\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "chars": 2004,
    "preview": "name: CI\non: push\njobs:\n    build-and-test:\n        runs-on: ubuntu-20.04\n        strategy:\n            fail-fast: false"
  },
  {
    "path": ".gitignore",
    "chars": 92,
    "preview": "*.deb\n*.egg-info\n*.gc*\n*.o\n*.py[cod]\n.pytest_cache\n.tox\n__pycache__/\nbuild/\ndist/\ndumb-init\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1204,
    "preview": "repos:\n-   repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.4.0\n    hooks:\n    -   id: check-added-large"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1598,
    "preview": "Contributing to dumb-init\n========\n\n`dumb-init` is primarily developed by [Yelp](https://yelp.github.io/), but\ncontribut"
  },
  {
    "path": "Dockerfile",
    "chars": 830,
    "preview": "ARG BASE_IMAGE=debian:buster\nFROM $BASE_IMAGE\n\nLABEL maintainer=\"Chris Kuehl <ckuehl@yelp.com>\"\n\n# Install the bare mini"
  },
  {
    "path": "LICENSE",
    "chars": 1077,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Yelp, Inc.\n\nPermission is hereby granted, free of charge, to any person obtain"
  },
  {
    "path": "MANIFEST.in",
    "chars": 54,
    "preview": "include dumb-init.c\ninclude VERSION\ninclude VERSION.h\n"
  },
  {
    "path": "Makefile",
    "chars": 2201,
    "preview": "SHELL=bash\nCFLAGS=-std=gnu99 -static -s -Wall -Werror -O3\n\nTEST_PACKAGE_DEPS := build-essential python python-pip procps"
  },
  {
    "path": "README.md",
    "chars": 12569,
    "preview": "dumb-init\n========\n\n[![PyPI version](https://badge.fury.io/py/dumb-init.svg)](https://pypi.python.org/pypi/dumb-init)\n\n\n"
  },
  {
    "path": "VERSION",
    "chars": 6,
    "preview": "1.2.5\n"
  },
  {
    "path": "VERSION.h",
    "chars": 200,
    "preview": "// THIS FILE IS AUTOMATICALLY GENERATED\n// Run `make VERSION.h` to update it after modifying VERSION.\nunsigned char VERS"
  },
  {
    "path": "ci/artifact-upload",
    "chars": 701,
    "preview": "#!/bin/bash\nset -euo pipefail\n\nif [ -n \"${ARTIFACTS_SECRET:-}\" ]; then\n    # Travis has built-in support for artifact up"
  },
  {
    "path": "ci/docker-deb-test",
    "chars": 230,
    "preview": "#!/bin/bash -eux\nset -o pipefail\n\napt-get update\napt-get -y --no-install-recommends install python3-pip procps\n\ncd /mnt\n"
  },
  {
    "path": "ci/docker-python-test",
    "chars": 219,
    "preview": "#!/bin/bash -eux\nset -euo pipefail\n\ncd /mnt\n\npython3 setup.py clean\npython3 setup.py sdist\npip3 install -vv dist/*.tar.g"
  },
  {
    "path": "ci/gcov-build",
    "chars": 157,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\nenvbindir=\"$1\"\ncc -c dumb-init.c -o dumb-init.o -g --coverage\ncc dumb-init.o -o \"$"
  },
  {
    "path": "ci/gcov-report",
    "chars": 128,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\ngcov -f dumb-init.c\ngrep '#####' dumb-init.c.gcov | cut -d':' -f2 | xargs echo \"Mi"
  },
  {
    "path": "circle.yml",
    "chars": 471,
    "preview": "machine:\n    services:\n        - docker\n\ndependencies:\n    # Without overriding, Circle CI infers that it should run `py"
  },
  {
    "path": "debian/.gitignore",
    "chars": 86,
    "preview": "/*.log\n/*.substvars\n/files\n/substvars\n/dumb-init/\n/dumb-init.1\n/debhelper-build-stamp\n"
  },
  {
    "path": "debian/changelog",
    "chars": 6234,
    "preview": "dumb-init (1.2.5) unstable; urgency=medium\n\n  * Change the working directory in the parent process to \"/\" after forking."
  },
  {
    "path": "debian/clean",
    "chars": 26,
    "preview": "tests/*.pyc\ntests/*/*.pyc\n"
  },
  {
    "path": "debian/compat",
    "chars": 2,
    "preview": "9\n"
  },
  {
    "path": "debian/control",
    "chars": 1162,
    "preview": "Source: dumb-init\nSection: utils\nPriority: extra\nMaintainer: Chris Kuehl <ckuehl@yelp.com>\nBuild-Depends:\n debhelper (>="
  },
  {
    "path": "debian/copyright",
    "chars": 1376,
    "preview": "Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: dumb-init\nSource: https://githu"
  },
  {
    "path": "debian/docs",
    "chars": 8,
    "preview": "README*\n"
  },
  {
    "path": "debian/help2man",
    "chars": 155,
    "preview": "[authors]\n.B dumb-init\nwas primarily developed at Yelp.\n.br\n.br\nFor a full list of contributors, see\nhttps://github.com/"
  },
  {
    "path": "debian/install",
    "chars": 20,
    "preview": "dumb-init /usr/bin/\n"
  },
  {
    "path": "debian/lintian-overrides",
    "chars": 43,
    "preview": "dumb-init binary: statically-linked-binary\n"
  },
  {
    "path": "debian/manpages",
    "chars": 19,
    "preview": "debian/dumb-init.1\n"
  },
  {
    "path": "debian/rules",
    "chars": 691,
    "preview": "#!/usr/bin/make -f\nexport DEB_BUILD_MAINT_OPTIONS = hardening=+all\n\n%:\n\tdh $@\n\nMAN=debian/dumb-init.1\n\noverride_dh_clean"
  },
  {
    "path": "debian/source/format",
    "chars": 13,
    "preview": "3.0 (native)\n"
  },
  {
    "path": "dumb-init.c",
    "chars": 11113,
    "preview": "/*\n * dumb-init is a simple wrapper program designed to run as PID 1 and pass\n * signals to its children.\n *\n * Usage:\n "
  },
  {
    "path": "pytest.ini",
    "chars": 22,
    "preview": "[pytest]\ntimeout = 20\n"
  },
  {
    "path": "requirements-dev.txt",
    "chars": 218,
    "preview": "pre-commit>=0.5.0\npytest\n# TODO: This pin is to work around an issue where the system pytest is too old.\n# We should fix"
  },
  {
    "path": "setup.py",
    "chars": 3849,
    "preview": "import os.path\nimport subprocess\nimport tempfile\n\nfrom distutils.command.build import build as orig_build\nfrom distutils"
  },
  {
    "path": "testing/__init__.py",
    "chars": 3189,
    "preview": "import errno\nimport os\nimport re\nimport signal\nimport sys\nimport time\nfrom contextlib import contextmanager\nfrom subproc"
  },
  {
    "path": "testing/print_signals.py",
    "chars": 1085,
    "preview": "#!/usr/bin/env python\n\"\"\"Print received signals to stdout.\n\nSince all signals are printed and otherwise ignored, you'll "
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/child_processes_test.py",
    "chars": 4500,
    "preview": "import os\nimport re\nimport signal\nimport sys\nfrom subprocess import PIPE\nfrom subprocess import Popen\n\nimport pytest\n\nfr"
  },
  {
    "path": "tests/cli_test.py",
    "chars": 6815,
    "preview": "\"\"\"Sanity checks for command-line options.\"\"\"\nimport re\nimport signal\nfrom subprocess import PIPE\nfrom subprocess import"
  },
  {
    "path": "tests/conftest.py",
    "chars": 1129,
    "preview": "import os\nfrom unittest import mock\n\nimport pytest\n\n\n@pytest.fixture(autouse=True, scope='function')\ndef clean_environme"
  },
  {
    "path": "tests/cwd_test.py",
    "chars": 846,
    "preview": "import os\nimport shutil\nfrom subprocess import PIPE\nfrom subprocess import run\n\nimport pytest\n\n\n@pytest.mark.usefixtures"
  },
  {
    "path": "tests/exit_status_test.py",
    "chars": 1237,
    "preview": "import signal\nimport sys\nfrom subprocess import Popen\n\nimport pytest\n\n\n@pytest.mark.parametrize('exit_status', [0, 1, 2,"
  },
  {
    "path": "tests/proxies_signals_test.py",
    "chars": 3830,
    "preview": "import os\nimport signal\nfrom itertools import chain\n\nimport pytest\n\nfrom testing import NORMAL_SIGNALS\nfrom testing impo"
  },
  {
    "path": "tests/shell_background_test.py",
    "chars": 2143,
    "preview": "import os\nfrom signal import SIGCONT\n\nimport pytest\n\nfrom testing import print_signals\nfrom testing import process_state"
  },
  {
    "path": "tests/test-zombies",
    "chars": 623,
    "preview": "#!/bin/bash -eux\n# Spawn a zombie process, and ensure it gets reaped.\n# This test is only useful when run on an empty co"
  },
  {
    "path": "tests/tty_test.py",
    "chars": 3737,
    "preview": "import os\nimport pty\nimport re\nimport signal\nimport termios\nimport time\n\nimport pytest\n\n\nEOF = b'\\x04'\n\n\ndef ttyflags(fd"
  },
  {
    "path": "tox.ini",
    "chars": 511,
    "preview": "[tox]\nenvlist = py38,gcov\n\n[testenv]\ndeps = -r{toxinidir}/requirements-dev.txt\ncommands =\n    pytest\n\n[testenv:gcov]\nski"
  }
]

About this extraction

This page contains the full source code of the Yelp/dumb-init GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 48 files (76.6 KB), approximately 21.4k tokens, and a symbol index with 73 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!