From 1c7f3e8fbcdb5bce103b13d404946149d78dea28 Mon Sep 17 00:00:00 2001 From: Kristof Mattei <864376+Kristof-Mattei@users.noreply.github.com> Date: Mon, 25 Oct 2021 00:20:03 -0700 Subject: [PATCH 1/2] Initial commit of rewrite to Rust --- .gitignore | 1 + .python-version | 1 + .vscode/launch.json | 62 ++++++ .vscode/tasks.json | 29 +++ Cargo.lock | 122 ++++++++++ Cargo.toml | 13 ++ Dockerfile | 5 +- MANIFEST.in | 5 +- Makefile | 17 +- README.md | 2 +- VERSION.h | 6 - ci/docker-python-test | 4 + ci/gcov-build | 8 +- dumb-init.c | 340 ---------------------------- requirements-dev.txt | 1 + setup.py | 79 +------ src/main.rs | 502 ++++++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 18 files changed, 765 insertions(+), 433 deletions(-) create mode 100644 .python-version create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 VERSION.h delete mode 100644 dumb-init.c create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index 5f2930c..c016d29 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ __pycache__/ build/ dist/ +target dumb-init diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..1edd89c --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +dumb-init diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..21284eb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,62 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'dumb-init'", + "cargo": { + "args": [ + "build", + "--bin=dumb-init", + "--package=dumb-init" + ], + "filter": { + "name": "dumb-init", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'dumb-init'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=dumb-init", + "--package=dumb-init" + ], + "filter": { + "name": "dumb-init", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "name": "Python: Setup.py", + "type": "python", + "request": "launch", + "program": "setup.py", + "console": "integratedTerminal", + "justMyCode": false, + "args": [ + "bdist" + ] + }, + { + "name": "Python: Attach using Process Id", + "type": "python", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..17fb698 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "command": "build", + "problemMatcher": [ + "$rustc" + ], + "group": "build", + "label": "rust: cargo build", + "presentation": { + "clear": true + } + }, + { + "type": "cargo", + "command": "check", + "problemMatcher": [ + "$rustc" + ], + "group": "build", + "label": "rust: cargo check", + "presentation": { + "clear": true + } + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9b9611d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,122 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dumb-init" +version = "1.2.5" +dependencies = [ + "getopts", + "ioctl-rs", + "libc", + "nix", + "regex", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "ioctl-rs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "607b0d5e3c8affe6744655ccd713c5d3763c09407e191cea94705f541fd45151" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..70c135d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dumb-init" +version = "1.2.5" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libc = "0.2.105" +ioctl-rs = "0.2.0" +nix = "0.23.0" +regex = "1.5.4" +getopts = "0.2.21" diff --git a/Dockerfile b/Dockerfile index c37efad..3cacd04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,8 @@ RUN : \ && apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ build-essential \ + curl \ + debcargo \ devscripts \ equivs \ lintian \ @@ -17,7 +19,8 @@ RUN : \ python3-setuptools \ python3-pip \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && curl https://sh.rustup.rs -sSf | sh -s -- -y WORKDIR /tmp/mnt COPY debian/control /control diff --git a/MANIFEST.in b/MANIFEST.in index 2c9dd09..14c62f4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ -include dumb-init.c +include Cargo.toml +include Cargo.lock +recursive-include src * include VERSION -include VERSION.h diff --git a/Makefile b/Makefile index 1ddbb37..19c79a5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ SHELL=bash -CFLAGS=-std=gnu99 -static -s -Wall -Werror -O3 +CARGOFLAGS=--release TEST_PACKAGE_DEPS := build-essential python python-pip procps python-dev python-setuptools @@ -7,17 +7,16 @@ 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 +build: cargo-toml-version + cargo build $(CARGOFLAGS) $(TARGET) -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: cargo-toml-version +cargo-toml-version: VERSION + sed -i -z 's/name = "dumb-init"\nversion = "[0-9]*\.[0-9]*\.[0-9]*"/name = "dumb-init"\nversion = "$(VERSION)"/' Cargo.toml .PHONY: clean clean: clean-tox - rm -rf dumb-init dist/ *.deb + rm -rf target/ dist/ *.deb .PHONY: clean-tox clean-tox: @@ -33,7 +32,7 @@ release: python-dists python-dists: python-dists-x86_64 python-dists-aarch64 python-dists-ppc64le python-dists-s390x .PHONY: python-dists-% -python-dists-%: VERSION.h +python-dists-%: cargo-toml-version python setup.py sdist docker run \ --user $$(id -u):$$(id -g) \ diff --git a/README.md b/README.md index 87d6cf0..41cf2f4 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ 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 + $ TARGET=x86_64-unknown-linux-musl make When statically compiled with musl the binary size is around 20KB. diff --git a/VERSION.h b/VERSION.h deleted file mode 100644 index 9ca66b2..0000000 --- a/VERSION.h +++ /dev/null @@ -1,6 +0,0 @@ -// 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; diff --git a/ci/docker-python-test b/ci/docker-python-test index ecac947..c3325cc 100755 --- a/ci/docker-python-test +++ b/ci/docker-python-test @@ -3,6 +3,10 @@ set -euo pipefail cd /mnt +PATH=~/.cargo/bin:$PATH + +# requirements first for setup.py. +pip3 install -r requirements-dev.txt python3 setup.py clean python3 setup.py sdist pip3 install -vv dist/*.tar.gz diff --git a/ci/gcov-build b/ci/gcov-build index 0940f22..c8c962d 100755 --- a/ci/gcov-build +++ b/ci/gcov-build @@ -1,5 +1,7 @@ #!/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 +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" +export RUSTDOCFLAGS="-Cpanic=abort" + +cargo +nightly build \ No newline at end of file diff --git a/dumb-init.c b/dumb-init.c deleted file mode 100644 index a97ab41..0000000 --- a/dumb-init.c +++ /dev/null @@ -1,340 +0,0 @@ -/* - * dumb-init is a simple wrapper program designed to run as PID 1 and pass - * signals to its children. - * - * Usage: - * ./dumb-init python -c 'while True: pass' - * - * To get debug output on stderr, run with '-v'. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "VERSION.h" - -#define PRINTERR(...) do { \ - fprintf(stderr, "[dumb-init] " __VA_ARGS__); \ -} while (0) - -#define DEBUG(...) do { \ - if (debug) { \ - PRINTERR(__VA_ARGS__); \ - } \ -} while (0) - -// Signals we care about are numbered from 1 to 31, inclusive. -// (32 and above are real-time signals.) -// TODO: this is likely not portable outside of Linux, or on strange architectures -#define MAXSIG 31 - -// Indices are one-indexed (signal 1 is at index 1). Index zero is unused. -// User-specified signal rewriting. -int signal_rewrite[MAXSIG + 1] = {[0 ... MAXSIG] = -1}; -// One-time ignores due to TTY quirks. 0 = no skip, 1 = skip the next-received signal. -char signal_temporary_ignores[MAXSIG + 1] = {[0 ... MAXSIG] = 0}; - -pid_t child_pid = -1; -char debug = 0; -char use_setsid = 1; - -int translate_signal(int signum) { - if (signum <= 0 || signum > MAXSIG) { - return signum; - } else { - int translated = signal_rewrite[signum]; - if (translated == -1) { - return signum; - } else { - DEBUG("Translating signal %d to %d.\n", signum, translated); - return translated; - } - } -} - -void forward_signal(int signum) { - signum = translate_signal(signum); - if (signum != 0) { - kill(use_setsid ? -child_pid : child_pid, signum); - DEBUG("Forwarded signal %d to children.\n", signum); - } else { - DEBUG("Not forwarding signal %d to children (ignored).\n", signum); - } -} - -/* - * The dumb-init signal handler. - * - * The main job of this signal handler is to forward signals along to our child - * process(es). In setsid mode, this means signaling the entire process group - * rooted at our child. In non-setsid mode, this is just signaling the primary - * child. - * - * In most cases, simply proxying the received signal is sufficient. If we - * receive a job control signal, however, we should not only forward it, but - * also sleep dumb-init itself. - * - * This allows users to run foreground processes using dumb-init and to - * control them using normal shell job control features (e.g. Ctrl-Z to - * generate a SIGTSTP and suspend the process). - * - * The libc manual is useful: - * https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html - * -*/ -void handle_signal(int signum) { - DEBUG("Received signal %d.\n", signum); - - if (signal_temporary_ignores[signum] == 1) { - DEBUG("Ignoring tty hand-off signal %d.\n", signum); - signal_temporary_ignores[signum] = 0; - } else if (signum == SIGCHLD) { - int status, exit_status; - pid_t killed_pid; - while ((killed_pid = waitpid(-1, &status, WNOHANG)) > 0) { - if (WIFEXITED(status)) { - exit_status = WEXITSTATUS(status); - DEBUG("A child with PID %d exited with exit status %d.\n", killed_pid, exit_status); - } else { - assert(WIFSIGNALED(status)); - exit_status = 128 + WTERMSIG(status); - DEBUG("A child with PID %d was terminated by signal %d.\n", killed_pid, exit_status - 128); - } - - if (killed_pid == child_pid) { - forward_signal(SIGTERM); // send SIGTERM to any remaining children - DEBUG("Child exited with status %d. Goodbye.\n", exit_status); - exit(exit_status); - } - } - } else { - forward_signal(signum); - if (signum == SIGTSTP || signum == SIGTTOU || signum == SIGTTIN) { - DEBUG("Suspending self due to TTY signal.\n"); - kill(getpid(), SIGSTOP); - } - } -} - -void print_help(char *argv[]) { - fprintf(stderr, - "dumb-init v%.*s" - "Usage: %s [option] command [[arg] ...]\n" - "\n" - "dumb-init is a simple process supervisor that forwards signals to children.\n" - "It is designed to run as PID1 in minimal container environments.\n" - "\n" - "Optional arguments:\n" - " -c, --single-child Run in single-child mode.\n" - " In this mode, signals are only proxied to the\n" - " direct child and not any of its descendants.\n" - " -r, --rewrite s:r Rewrite received signal s to new signal r before proxying.\n" - " To ignore (not proxy) a signal, rewrite it to 0.\n" - " This option can be specified multiple times.\n" - " -v, --verbose Print debugging information to stderr.\n" - " -h, --help Print this help message and exit.\n" - " -V, --version Print the current version and exit.\n" - "\n" - "Full help is available online at https://github.com/Yelp/dumb-init\n", - VERSION_len, VERSION, - argv[0] - ); -} - -void print_rewrite_signum_help() { - fprintf( - stderr, - "Usage: -r option takes :, where " - "is between 1 and %d.\n" - "This option can be specified multiple times.\n" - "Use --help for full usage.\n", - MAXSIG - ); - exit(1); -} - -void parse_rewrite_signum(char *arg) { - int signum, replacement; - if ( - sscanf(arg, "%d:%d", &signum, &replacement) == 2 && - (signum >= 1 && signum <= MAXSIG) && - (replacement >= 0 && replacement <= MAXSIG) - ) { - signal_rewrite[signum] = replacement; - } else { - print_rewrite_signum_help(); - } -} - -void set_rewrite_to_sigstop_if_not_defined(int signum) { - if (signal_rewrite[signum] == -1) { - signal_rewrite[signum] = SIGSTOP; - } -} - -char **parse_command(int argc, char *argv[]) { - int opt; - struct option long_options[] = { - {"help", no_argument, NULL, 'h'}, - {"single-child", no_argument, NULL, 'c'}, - {"rewrite", required_argument, NULL, 'r'}, - {"verbose", no_argument, NULL, 'v'}, - {"version", no_argument, NULL, 'V'}, - {NULL, 0, NULL, 0}, - }; - while ((opt = getopt_long(argc, argv, "+hvVcr:", long_options, NULL)) != -1) { - switch (opt) { - case 'h': - print_help(argv); - exit(0); - case 'v': - debug = 1; - break; - case 'V': - fprintf(stderr, "dumb-init v%.*s", VERSION_len, VERSION); - exit(0); - case 'c': - use_setsid = 0; - break; - case 'r': - parse_rewrite_signum(optarg); - break; - default: - exit(1); - } - } - - if (optind >= argc) { - fprintf( - stderr, - "Usage: %s [option] program [args]\n" - "Try %s --help for full usage.\n", - argv[0], argv[0] - ); - exit(1); - } - - char *debug_env = getenv("DUMB_INIT_DEBUG"); - if (debug_env && strcmp(debug_env, "1") == 0) { - debug = 1; - DEBUG("Running in debug mode.\n"); - } - - char *setsid_env = getenv("DUMB_INIT_SETSID"); - if (setsid_env && strcmp(setsid_env, "0") == 0) { - use_setsid = 0; - DEBUG("Not running in setsid mode.\n"); - } - - if (use_setsid) { - set_rewrite_to_sigstop_if_not_defined(SIGTSTP); - set_rewrite_to_sigstop_if_not_defined(SIGTTOU); - set_rewrite_to_sigstop_if_not_defined(SIGTTIN); - } - - return &argv[optind]; -} - -// A dummy signal handler used for signals we care about. -// On the FreeBSD kernel, ignored signals cannot be waited on by `sigwait` (but -// they can be on Linux). We must provide a dummy handler. -// https://lists.freebsd.org/pipermail/freebsd-ports/2009-October/057340.html -void dummy(int signum) {} - -int main(int argc, char *argv[]) { - char **cmd = parse_command(argc, argv); - sigset_t all_signals; - sigfillset(&all_signals); - sigprocmask(SIG_BLOCK, &all_signals, NULL); - - int i = 0; - for (i = 1; i <= MAXSIG; i++) { - signal(i, dummy); - } - - /* - * Detach dumb-init from controlling tty, so that the child's session can - * attach to it instead. - * - * We want the child to be able to be the session leader of the TTY so that - * it can do normal job control. - */ - if (use_setsid) { - if (ioctl(STDIN_FILENO, TIOCNOTTY) == -1) { - DEBUG( - "Unable to detach from controlling tty (errno=%d %s).\n", - errno, - strerror(errno) - ); - } else { - /* - * When the session leader detaches from its controlling tty via - * TIOCNOTTY, the kernel sends SIGHUP and SIGCONT to the process - * group. We need to be careful not to forward these on to the - * dumb-init child so that it doesn't receive a SIGHUP and - * terminate itself (#136). - */ - if (getsid(0) == getpid()) { - DEBUG("Detached from controlling tty, ignoring the first SIGHUP and SIGCONT we receive.\n"); - signal_temporary_ignores[SIGHUP] = 1; - signal_temporary_ignores[SIGCONT] = 1; - } else { - DEBUG("Detached from controlling tty, but was not session leader.\n"); - } - } - } - - child_pid = fork(); - if (child_pid < 0) { - PRINTERR("Unable to fork. Exiting.\n"); - return 1; - } else if (child_pid == 0) { - /* child */ - sigprocmask(SIG_UNBLOCK, &all_signals, NULL); - if (use_setsid) { - if (setsid() == -1) { - PRINTERR( - "Unable to setsid (errno=%d %s). Exiting.\n", - errno, - strerror(errno) - ); - exit(1); - } - - if (ioctl(STDIN_FILENO, TIOCSCTTY, 0) == -1) { - DEBUG( - "Unable to attach to controlling tty (errno=%d %s).\n", - errno, - strerror(errno) - ); - } - DEBUG("setsid complete.\n"); - } - execvp(cmd[0], &cmd[0]); - - // if this point is reached, exec failed, so we should exit nonzero - PRINTERR("%s: %s\n", cmd[0], strerror(errno)); - return 2; - } else { - /* parent */ - DEBUG("Child spawned with PID %d.\n", child_pid); - if (chdir("/") == -1) { - DEBUG("Unable to chdir(\"/\") (errno=%d %s)\n", - errno, - strerror(errno)); - } - for (;;) { - int signum; - sigwait(&all_signals, &signum); - handle_signal(signum); - } - } -} diff --git a/requirements-dev.txt b/requirements-dev.txt index 49f54fd..eb4e311 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ pre-commit>=0.5.0 +setuptools-rust>=0.12.1 # also in tox requirements as we need it as part of setup.py 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 diff --git a/setup.py b/setup.py index 0cb322d..64a768b 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ from setuptools import Extension from setuptools import setup from setuptools.command.install import install as orig_install +from setuptools_rust import Binding, RustExtension try: @@ -28,25 +29,14 @@ def get_tag(self): 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), + ('install_rexe', None), ] -class install_cexe(Command): - description = 'install C executables' +class install_rexe(Command): + description = 'install Rust executables' outfiles = () def initialize_options(self): @@ -54,68 +44,18 @@ def initialize_options(self): 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_lib', ('build_dir', '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', @@ -123,13 +63,10 @@ def get_outputs(self): author='Yelp', url='https://github.com/Yelp/dumb-init/', platforms='linux', - c_executables=[Extension('dumb-init', ['dumb-init.c'])], + rust_extensions=[RustExtension("dumb-init", binding=Binding.Exec)], cmdclass={ 'bdist_wheel': bdist_wheel, - 'build': build, - 'build_cexe': build_cexe, 'install': install, - 'install_cexe': install_cexe, - }, - distclass=ExeDistribution, + 'install_rexe': install_rexe, + } ) diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a1fa137 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,502 @@ +use std::fmt; +use std::process::exit; + +use getopts::Fail; +use getopts::ParsingStyle; +use ioctl_rs::TIOCNOTTY; +use ioctl_rs::TIOCSCTTY; +use nix::ioctl_none_bad; +use nix::ioctl_write_int_bad; +use regex::Regex; + +use std::ffi::CString; + +use nix::sys::wait::waitpid; +use nix::sys::wait::WaitPidFlag; +use nix::sys::wait::WaitStatus; + +use nix::sys::signal::kill; +use nix::sys::signal::signal; +use nix::sys::signal::sigprocmask; +use nix::sys::signal::SigHandler; +use nix::sys::signal::SigSet; +use nix::sys::signal::SigmaskHow; +use nix::sys::signal::Signal; + +use nix::unistd::getpid; +use nix::unistd::getsid; +use nix::unistd::ForkResult; +use nix::unistd::Pid; + +use getopts::Options; +use std::env; + +ioctl_none_bad!(ioctl_tiocnotty, TIOCNOTTY); +ioctl_write_int_bad!(ioctl_tiocsctty, TIOCSCTTY); + +// /* +// * 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'. +// */ +fn printerr(args: fmt::Arguments) { + eprintln!("[dumb-init] {}", args); +} + +fn debug(args: fmt::Arguments) { + if unsafe { DEBUG } { + printerr(args); + } +} + +// 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 +const MAXSIG: libc::c_int = 31; + +#[derive(Debug, Copy, Clone)] +enum RewriteSignal { + Drop, + Rewrite(Signal), +} + +// Indices are one-indexed (signal 1 is at index 1). Index zero is unused. +// User-specified signal rewriting. +static mut SIGNAL_REWRITE: [Option; MAXSIG as usize + 1] = + [None; MAXSIG as usize + 1]; + +// One-time ignores due to TTY quirks. 0 = no skip, 1 = skip the next-received signal. +static mut SIGNAL_TEMPORARY_IGNORES: [bool; MAXSIG as usize + 1] = [false; MAXSIG as usize + 1]; + +static mut CHILD_PID: Option = None; +static mut DEBUG: bool = false; +static mut USE_SETSID: bool = true; + +fn translate_signal(signum: Signal) -> Option { + if signum as i32 <= 0 || signum as i32 > MAXSIG { + Some(signum) + } else { + let translation = unsafe { SIGNAL_REWRITE[signum as usize] }; + + match translation { + // not present in our translation set + None => Some(signum), + Some(translated) => { + debug(format_args!( + "Translating signal {} to {:?}.", + signum, translated + )); + + if let RewriteSignal::Rewrite(signal) = translated { + Some(signal) + } else { + None + } + } + } + } +} + +fn forward_signal(signum: Signal) { + let translated: Option = translate_signal(signum); + match translated { + Some(signum) => { + let pid = unsafe { CHILD_PID.unwrap() }; + + let pid_to_kill = if unsafe { USE_SETSID } { + Pid::from_raw(-pid.as_raw()) + } else { + pid + }; + + let _ = kill(pid_to_kill, signum); + + debug(format_args!( + "Forwarded signal {} to children.", + signum as i32 + )); + } + None => { + debug(format_args!( + "Not forwarding signal {} to children (ignored).", + 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 + * +*/ +fn handle_signal(signum: Signal) { + debug(format_args!("Received signal {}.", signum as i32)); + + if unsafe { SIGNAL_TEMPORARY_IGNORES[signum as usize] } { + debug(format_args!( + "Ignoring tty hand-off signal {}.", + signum as i32 + )); + unsafe { SIGNAL_TEMPORARY_IGNORES[signum as usize] = false }; + } else if signum == Signal::SIGCHLD { + loop { + let exit_status: i32; + let killed_pid: Pid; + + let result = waitpid(Pid::from_raw(-1), Some(WaitPidFlag::WNOHANG)); + + if result.is_err() { + break; + } + + let result_unwrapped = result.unwrap(); + + match result_unwrapped { + WaitStatus::StillAlive => break, + WaitStatus::Exited(pid, status) => { + exit_status = status; + killed_pid = pid; + debug(format_args!( + "A child with PID {} exited with exit status {}.", + killed_pid, exit_status + )); + } + WaitStatus::Signaled(pid, status, _) => { + killed_pid = pid; + exit_status = 128 + status as i32; + debug(format_args!( + "A child with PID {} was terminated by signal {}.", + killed_pid, + exit_status - 128 + )); + } + _ => { + assert!(matches!(result_unwrapped, WaitStatus::Signaled(_, _, _))); + break; + } + } + + if unsafe { Some(killed_pid) == CHILD_PID } { + forward_signal(Signal::SIGTERM); // send SIGTERM to any remaining children + debug(format_args!( + "Child exited with status {}. Goodbye.", + exit_status + )); + + exit(exit_status); + } + } + } else { + forward_signal(signum); + if signum == Signal::SIGTSTP || signum == Signal::SIGTTOU || signum == Signal::SIGTTIN { + debug(format_args!("Suspending self due to TTY signal.")); + let _ = kill(getpid(), Signal::SIGSTOP); + } + } +} + +fn parse_rewrite_signum(arg: &str) { + let regex = Regex::new(r"^(\d{1,2}?):(-1|\d{1,2}?)$").unwrap(); + + match regex.captures(arg) { + Some(captures) if captures.len() == 3 => { + let signum = str::parse::(&captures[1]); + let replacement = str::parse::(&captures[2]); + + match (signum, replacement) { + (Ok(s), Ok(r)) if (1..=MAXSIG).contains(&s) && (0..=MAXSIG).contains(&r) => { + let rewrite = if r == 0 { + Some(RewriteSignal::Drop) + } else { + Some(RewriteSignal::Rewrite(Signal::try_from(r).unwrap())) + }; + + unsafe { + SIGNAL_REWRITE[s as usize] = rewrite; + } + + return; + } + _ => {} + } + } + _ => {} + } + + print_rewrite_signum_help(); +} + +fn print_help(version: &str, executable_name: &str) { + eprintln!( + "dumb-init v{} +Usage: {} [option] command [[arg] ...] + +dumb-init is a simple process supervisor that forwards signals to children. +It is designed to run as PID1 in minimal container environments. + +Optional arguments: + -c, --single-child Run in single-child mode. + In this mode, signals are only proxied to the + direct child and not any of its descendants. + -r, --rewrite s:r Rewrite received signal s to new signal r before proxying. + To ignore (not proxy) a signal, rewrite it to 0. + This option can be specified multiple times. + -v, --verbose Print debugging information to stderr. + -h, --help Print this help message and exit. + -V, --version Print the current version and exit. + +Full help is available online at https://github.com/Yelp/dumb-init", + version, executable_name + ); +} + +fn print_rewrite_signum_help() { + eprintln!("Usage: -r option takes :, where is between 1 and {}.\nThis option can be specified multiple times.\nUse --help for full usage.", MAXSIG ); + + exit(1); +} + +unsafe fn set_rewrite_to_sigstop_if_not_defined(signum: Signal) { + if SIGNAL_REWRITE[signum as usize].is_none() { + SIGNAL_REWRITE[signum as usize] = Some(RewriteSignal::Rewrite(Signal::SIGSTOP)); + } +} + +fn parse_command() -> Vec { + let args: Vec = env::args().collect(); + + let mut opts = Options::new(); + opts.parsing_style(ParsingStyle::StopAtFirstFree); + opts.optflag("c", "single-child", ""); + opts.optmulti("r", "rewrite", "", ""); + opts.optflag("v", "verbose", ""); + opts.optflag("h", "help", ""); + opts.optflag("V", "version", ""); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => match f { + Fail::UnrecognizedOption(option) => { + eprintln!("dumb-init: unrecognized option: {}", option); + exit(1) + } + Fail::ArgumentMissing(_) => todo!(), + Fail::OptionMissing(_) => todo!(), + Fail::OptionDuplicated(_) => todo!(), + Fail::UnexpectedArgument(_) => todo!(), + }, + }; + + if matches.opt_present("h") { + print_help(env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_NAME")); + exit(0); + } + if matches.opt_present("v") { + unsafe { + DEBUG = true; + } + } + if matches.opt_present("V") { + eprintln!("dumb-init v{}", env!("CARGO_PKG_VERSION")); + + exit(0); + } + if matches.opt_present("c") { + unsafe { + USE_SETSID = false; + } + } + + matches.opt_strs("r").iter().for_each(|value| { + parse_rewrite_signum(value); + }); + + let rest = matches.free; + + if rest.is_empty() { + eprintln!( + "Usage: {} [option] program [args]\nTry {} --help for full usage.", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME") + ); + + exit(1); + } + + let debug_env = std::env::var("DUMB_INIT_DEBUG"); + + if debug_env.unwrap_or_default() == *"1" { + unsafe { DEBUG = true }; + debug(format_args!("Running in debug mode.")); + } + + let setsid_env = std::env::var("DUMB_INIT_SETSID"); + + if setsid_env.unwrap_or_default() == *"0" { + unsafe { USE_SETSID = false }; + debug(format_args!("Not running in setsid mode.")); + } + + if unsafe { USE_SETSID } { + unsafe { + set_rewrite_to_sigstop_if_not_defined(Signal::SIGTSTP); + set_rewrite_to_sigstop_if_not_defined(Signal::SIGTTOU); + set_rewrite_to_sigstop_if_not_defined(Signal::SIGTTIN); + } + } + + rest +} + +// 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 +extern "C" fn dummy(_signum: libc::c_int) {} + +fn main() { + let remainder = parse_command(); + + let all_signals: SigSet = SigSet::all(); + + let _ = sigprocmask(SigmaskHow::SIG_BLOCK, Some(&all_signals), None); + + for i in 1..=MAXSIG { + unsafe { + let _ = signal(Signal::try_from(i).unwrap(), SigHandler::Handler(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 unsafe { USE_SETSID } { + let ioctl_result; + unsafe { ioctl_result = ioctl_tiocnotty(libc::STDIN_FILENO) } + if let Err(err) = ioctl_result { + debug(format_args!( + "Unable to detach from controlling tty (errno={} {}).", + err, + err.desc() + )); + } 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). + */ + + let sid = getsid(Some(Pid::from_raw(0))); + let pid = getpid(); + + match sid { + Ok(p) if p == pid => { + debug(format_args!("Detached from controlling tty, ignoring the first SIGHUP and SIGCONT we receive.")); + unsafe { + SIGNAL_TEMPORARY_IGNORES[Signal::SIGHUP as usize] = true; + SIGNAL_TEMPORARY_IGNORES[Signal::SIGCONT as usize] = true; + } + } + _ => { + debug(format_args!( + "Detached from controlling tty, but was not session leader." + )); + } + } + } + } + + match unsafe { nix::unistd::fork() } { + Err(_) => { + printerr(format_args!("Unable to fork. Exiting.")); + exit(1); + } + Ok(ForkResult::Child) => { + /* child */ + let _ = sigprocmask(SigmaskHow::SIG_UNBLOCK, Some(&all_signals), None); + + if unsafe { USE_SETSID } { + if let Err(errno) = nix::unistd::setsid() { + printerr(format_args!( + "Unable to setsid (errno={} {}). Exiting.", + errno as i32, + errno.desc() + )); + + exit(1) + } + + if let Err(errno) = unsafe { ioctl_tiocsctty(libc::STDIN_FILENO, 0) } { + debug(format_args!( + "Unable to attach to controlling tty (errno={} {}).", + errno as i32, + errno.desc() + )); + } + debug(format_args!("setsid complete.")); + } + + let mut for_vp: Vec = remainder + .into_iter() + .map(|f| std::ffi::CString::new(f).unwrap()) + .collect(); + + for_vp.shrink_to_fit(); + + let error = nix::unistd::execvp(&for_vp[0], &for_vp).unwrap_err(); + + // if this point is reached, exec failed, so we should exit nonzero + printerr(format_args!( + "{}: {}", + &for_vp[0].to_str().unwrap(), + error.desc() + )); + + exit(2); + } + Ok(ForkResult::Parent { child }) => { + unsafe { CHILD_PID = Some(child) }; + /* parent */ + debug(format_args!("Child spawned with PID {}.", child)); + + if let Err(err) = nix::unistd::chdir("/") { + debug(format_args!( + "Unable to chdir(\"/\") (errno={} {})", + err as i32, + err.desc() + )); + } + + loop { + let signum = all_signals.wait().unwrap(); + handle_signal(signum); + } + } + } +} diff --git a/tox.ini b/tox.ini index 5a86cc6..a859dce 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = py38,gcov +requires = setuptools_rust >= 0.12.1 [testenv] deps = -r{toxinidir}/requirements-dev.txt From 61515ee9d4fbc0d412accbaf6ef1f18182814c95 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Oct 2021 01:44:34 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .vscode/launch.json | 2 +- .vscode/tasks.json | 56 ++++++++++++++++++++++---------------------- ci/gcov-build | 2 +- requirements-dev.txt | 2 +- setup.py | 8 ++++--- 5 files changed, 36 insertions(+), 34 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 21284eb..cc16f34 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -59,4 +59,4 @@ "processId": "${command:pickProcess}" } ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 17fb698..c93f456 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,29 +1,29 @@ { - "version": "2.0.0", - "tasks": [ - { - "type": "cargo", - "command": "build", - "problemMatcher": [ - "$rustc" - ], - "group": "build", - "label": "rust: cargo build", - "presentation": { - "clear": true - } - }, - { - "type": "cargo", - "command": "check", - "problemMatcher": [ - "$rustc" - ], - "group": "build", - "label": "rust: cargo check", - "presentation": { - "clear": true - } - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "command": "build", + "problemMatcher": [ + "$rustc" + ], + "group": "build", + "label": "rust: cargo build", + "presentation": { + "clear": true + } + }, + { + "type": "cargo", + "command": "check", + "problemMatcher": [ + "$rustc" + ], + "group": "build", + "label": "rust: cargo check", + "presentation": { + "clear": true + } + } + ] +} diff --git a/ci/gcov-build b/ci/gcov-build index c8c962d..64a2fa5 100755 --- a/ci/gcov-build +++ b/ci/gcov-build @@ -4,4 +4,4 @@ export CARGO_INCREMENTAL=0 export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" export RUSTDOCFLAGS="-Cpanic=abort" -cargo +nightly build \ No newline at end of file +cargo +nightly build diff --git a/requirements-dev.txt b/requirements-dev.txt index eb4e311..4b868c4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,7 @@ pre-commit>=0.5.0 -setuptools-rust>=0.12.1 # also in tox requirements as we need it as part of setup.py 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 +setuptools-rust>=0.12.1 # also in tox requirements as we need it as part of setup.py diff --git a/setup.py b/setup.py index 64a768b..0da34fd 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,8 @@ from setuptools import Extension from setuptools import setup from setuptools.command.install import install as orig_install -from setuptools_rust import Binding, RustExtension +from setuptools_rust import Binding +from setuptools_rust import RustExtension try: @@ -29,6 +30,7 @@ def get_tag(self): except ImportError: bdist_wheel = None + class install(orig_install): sub_commands = orig_install.sub_commands + [ ('install_rexe', None), @@ -63,10 +65,10 @@ def get_outputs(self): author='Yelp', url='https://github.com/Yelp/dumb-init/', platforms='linux', - rust_extensions=[RustExtension("dumb-init", binding=Binding.Exec)], + rust_extensions=[RustExtension('dumb-init', binding=Binding.Exec)], cmdclass={ 'bdist_wheel': bdist_wheel, 'install': install, 'install_rexe': install_rexe, - } + }, )