├── .dockerignore ├── .gitignore ├── sign.key.enc ├── tpl ├── VERSION.in └── README.md.in ├── src ├── tiniConfig.h.in ├── tiniLicense.h └── tini.c ├── dtest.sh ├── run_tests.sh ├── test ├── signals │ └── test.py ├── reaping │ ├── stage_2.py │ └── stage_1.py ├── subreaper-proxy.py ├── pgroup │ └── stage_1.py ├── pdeathsignal │ ├── stage_2.py │ └── stage_1.py ├── sigconf │ └── sigconf-test.c ├── run_outer_tests.py └── run_inner_tests.py ├── Dockerfile ├── ci ├── util │ └── rpmbuild ├── install_deps.sh └── run_build.sh ├── ddist.sh ├── LICENSE ├── .travis.yml ├── CMakeLists.txt └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | ./dist 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | sign.key 3 | .env 4 | -------------------------------------------------------------------------------- /sign.key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NVDLI/tini/master/sign.key.enc -------------------------------------------------------------------------------- /tpl/VERSION.in: -------------------------------------------------------------------------------- 1 | @tini_VERSION_MAJOR@.@tini_VERSION_MINOR@.@tini_VERSION_PATCH@ 2 | -------------------------------------------------------------------------------- /src/tiniConfig.h.in: -------------------------------------------------------------------------------- 1 | #define TINI_VERSION "@tini_VERSION_MAJOR@.@tini_VERSION_MINOR@.@tini_VERSION_PATCH@" 2 | #define TINI_GIT "@tini_VERSION_GIT@" 3 | -------------------------------------------------------------------------------- /dtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | 5 | IMG="tini" 6 | 7 | if [[ "$#" != 1 ]]; then 8 | echo "Usage: $0 ARCH_SUFFIX" 9 | exit 1 10 | fi 11 | suffix="$1" 12 | 13 | IMG="tini-build-${suffix}" 14 | python test/run_outer_tests.py "${IMG}" 15 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | 5 | REL_HERE=$(dirname "${BASH_SOURCE}") 6 | HERE=$(cd "${REL_HERE}"; pwd) 7 | 8 | for i in $(seq 0 1); do 9 | export FORCE_SUBREAPER="${i}" 10 | echo "Testing with FORCE_SUBREAPER=${FORCE_SUBREAPER}" 11 | "${HERE}/ddist.sh" 12 | "${HERE}/dtest.sh" 13 | done 14 | -------------------------------------------------------------------------------- /test/signals/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import signal 3 | import os 4 | 5 | 6 | def main(): 7 | signal.signal(signal.SIGTERM, signal.SIG_DFL) 8 | signal.signal(signal.SIGUSR1, signal.SIG_DFL) 9 | signal.signal(signal.SIGUSR2, signal.SIG_DFL) 10 | os.system("sleep 100") 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /test/reaping/stage_2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import subprocess 4 | import os 5 | import random 6 | 7 | 8 | if __name__ == "__main__": 9 | # Spawn lots of process 10 | for i in range(0, 100): 11 | cmd = ["sleep", str(1 + i % 2 + random.random())] 12 | proc = subprocess.Popen(cmd) 13 | -------------------------------------------------------------------------------- /test/subreaper-proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | import os 4 | import sys 5 | 6 | import prctl 7 | 8 | 9 | def main(): 10 | args = sys.argv[1:] 11 | 12 | print "subreaper-proxy: running '%s'" % (" ".join(args)) 13 | 14 | prctl.set_child_subreaper(1) 15 | os.execv(args[0], args) 16 | 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /test/pgroup/stage_1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import subprocess 4 | import signal 5 | 6 | 7 | def reset_sig_handler(): 8 | signal.signal(signal.SIGUSR1, signal.SIG_DFL) 9 | 10 | 11 | if __name__ == "__main__": 12 | signal.signal(signal.SIGUSR1, signal.SIG_IGN) 13 | p = subprocess.Popen( 14 | ["sleep", "1000"], 15 | preexec_fn=reset_sig_handler 16 | ) 17 | p.wait() 18 | 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | 3 | ARG ARCH_SUFFIX 4 | 5 | COPY ci/install_deps.sh /install_deps.sh 6 | RUN /install_deps.sh 7 | 8 | # Pre-install those here for faster local builds. 9 | RUN CFLAGS="-DPR_SET_CHILD_SUBREAPER=36 -DPR_GET_CHILD_SUBREAPER=37" pip install psutil python-prctl bitmap 10 | 11 | ARG ARCH_NATIVE 12 | ARG CC 13 | 14 | # Persist ARGs into the image 15 | 16 | ENV ARCH_SUFFIX="$ARCH_SUFFIX" \ 17 | ARCH_NATIVE="$ARCH_NATIVE" \ 18 | CC="$CC" 19 | -------------------------------------------------------------------------------- /test/pdeathsignal/stage_2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import os 5 | import sys 6 | import signal 7 | import time 8 | 9 | 10 | def main(): 11 | ret = sys.argv[2] 12 | 13 | def handler(*args): 14 | with open(ret, "w") as f: 15 | f.write("ok") 16 | sys.exit(0) 17 | 18 | signal.signal(signal.SIGUSR1, handler) 19 | pid = int(sys.argv[1]) 20 | 21 | os.kill(pid, signal.SIGKILL) 22 | time.sleep(5) 23 | 24 | if __name__ == "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /test/pdeathsignal/stage_1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import os 5 | import sys 6 | import subprocess 7 | 8 | 9 | def main(): 10 | pid = os.getpid() 11 | 12 | tini = sys.argv[1] 13 | ret = sys.argv[2] 14 | stage_2 = os.path.join(os.path.dirname(__file__), "stage_2.py") 15 | 16 | cmd = [ 17 | tini, 18 | "-vvv", 19 | "-p", 20 | "SIGUSR1", 21 | "--", 22 | stage_2, 23 | str(pid), 24 | ret 25 | ] 26 | 27 | subprocess.Popen(cmd).wait() 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /ci/util/rpmbuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Wrapper for rpm build that first removes files that should be in there 3 | # We need this for compatibility with CMake <= 2.8.10 (which is the only version we have in Travis) 4 | # See: http://www.cmake.org/pipermail/cmake-commits/2013-April/014818.html 5 | set -o nounset 6 | set -o errexit 7 | set -o pipefail 8 | 9 | # Remove PATH hack so we can find the real rpmbuild 10 | export PATH="${REAL_PATH}" 11 | 12 | echo "Using local rpmbuild" 13 | 14 | specFile="${!#}" # Last argument 15 | 16 | for removeLine in '"/usr"' '"/usr/bin"'; do 17 | sed -i "s|${removeLine}||g" "${specFile}" 18 | done 19 | 20 | # Passthrough to rpmbuild 21 | exec rpmbuild "${@}" 22 | -------------------------------------------------------------------------------- /ci/install_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o xtrace 5 | 6 | DEPS=( 7 | build-essential git gdb valgrind cmake rpm file 8 | libcap-dev python-dev python-pip python-setuptools 9 | hardening-includes gnupg 10 | ) 11 | 12 | case "${ARCH_SUFFIX-}" in 13 | amd64|'') ;; 14 | arm64) DEPS+=(gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu libc6-dev-arm64-cross) ;; 15 | armel) DEPS+=(gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi libc6-dev-armel-cross) ;; 16 | armhf) DEPS+=(gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf libc6-dev-armhf-cross) ;; 17 | i386) DEPS+=(libc6-dev-i386 gcc-multilib) ;; 18 | muslc-amd64) DEPS+=(musl-tools) ;; 19 | ppc64el) DEPS+=(gcc-powerpc64le-linux-gnu binutils-powerpc64le-linux-gnu libc6-dev-ppc64el-cross) ;; 20 | s390x) DEPS+=(gcc-s390x-linux-gnu binutils-s390x-linux-gnu libc6-dev-s390x-cross) ;; 21 | *) echo "Unknown ARCH_SUFFIX=${ARCH_SUFFIX-}"; exit 1 ;; 22 | esac 23 | 24 | apt-get update 25 | apt-get install --no-install-recommends --yes "${DEPS[@]}" 26 | rm -rf /var/lib/apt/lists/* 27 | 28 | pip install virtualenv 29 | -------------------------------------------------------------------------------- /ddist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | 5 | REL_HERE=$(dirname "${BASH_SOURCE}") 6 | HERE=$(cd "${REL_HERE}"; pwd) 7 | 8 | IMG="tini-build" 9 | 10 | if [[ -n "${ARCH_SUFFIX-}" ]]; then 11 | IMG="${IMG}_${ARCH_SUFFIX}" 12 | fi 13 | 14 | if [[ -n "${ARCH_NATIVE-}" ]]; then 15 | IMG="${IMG}_native" 16 | fi 17 | 18 | if [[ -n "${CC-}" ]]; then 19 | IMG="${IMG}_${CC}" 20 | fi 21 | 22 | # Cleanup the build dir 23 | rm -f "${HERE}/dist"/* 24 | 25 | # Create the build image 26 | echo "build: ${IMG}" 27 | 28 | docker build \ 29 | --build-arg "ARCH_SUFFIX=${ARCH_SUFFIX-}" \ 30 | --build-arg "ARCH_NATIVE=${ARCH_NATIVE-}" \ 31 | --build-arg "CC=${CC-gcc}" \ 32 | -t "${IMG}" \ 33 | . 34 | 35 | # Build new Tini 36 | SRC="/tini" 37 | 38 | docker run -it --rm \ 39 | --volume="${HERE}:${SRC}" \ 40 | -e BUILD_DIR=/tmp/tini-build \ 41 | -e SOURCE_DIR="${SRC}" \ 42 | -e FORCE_SUBREAPER="${FORCE_SUBREAPER-1}" \ 43 | -e GPG_PASSPHRASE="${GPG_PASSPHRASE-}" \ 44 | -e CFLAGS="${CFLAGS-}" \ 45 | -e MINIMAL="${MINIMAL-}" \ 46 | -u "$(id -u):$(id -g)" \ 47 | "${IMG}" "${SRC}/ci/run_build.sh" 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thomas Orozco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/sigconf/sigconf-test.c: -------------------------------------------------------------------------------- 1 | /* 2 | Test program to: 3 | + Ignore a few signals 4 | + Block a few signals 5 | + Exec whatever the test runner asked for 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | int main(int argc, char *argv[]) { 14 | // Signals to ignore 15 | signal(SIGTTOU, SIG_IGN); // This one should still be in SigIgn (Tini touches it to ignore it, and should restore it) 16 | signal(SIGSEGV, SIG_IGN); // This one should still be in SigIgn (Tini shouldn't touch it) 17 | signal(SIGINT, SIG_IGN); // This one should still be in SigIgn (Tini should block it to forward it, and restore it) 18 | 19 | // Signals to block 20 | sigset_t set; 21 | sigemptyset(&set); 22 | sigaddset(&set, SIGTTIN); // This one should still be in SigIgn (Tini touches it to ignore it, and should restore it) 23 | sigaddset(&set, SIGILL); // This one should still be in SigIgn (Tini shouldn't touch it) 24 | sigaddset(&set, SIGTERM); // This one should still be in SigIgn (Tini should block it to forward it, and restore it) 25 | sigprocmask(SIG_BLOCK, &set, NULL); 26 | 27 | // Run whatever we were asked to run 28 | execvp(argv[1], argv+1); 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | 5 | language: generic 6 | 7 | env: 8 | matrix: 9 | - ARCH_SUFFIX= CC=gcc ARCH_NATIVE=1 MINIMAL= 10 | - ARCH_SUFFIX=amd64 CC=gcc ARCH_NATIVE=1 MINIMAL= 11 | - ARCH_SUFFIX=amd64 CC=gcc ARCH_NATIVE=1 MINIMAL=1 12 | - ARCH_SUFFIX=arm64 CC=aarch64-linux-gnu-gcc ARCH_NATIVE= MINIMAL= 13 | - ARCH_SUFFIX=armel CC=arm-linux-gnueabi-gcc ARCH_NATIVE= MINIMAL= 14 | - ARCH_SUFFIX=armhf CC=arm-linux-gnueabihf-gcc ARCH_NATIVE= MINIMAL= 15 | - ARCH_SUFFIX=i386 CFLAGS="-m32" ARCH_NATIVE= MINIMAL= 16 | - ARCH_SUFFIX=muslc-amd64 CC=musl-gcc ARCH_NATIVE=1 MINIMAL= 17 | - ARCH_SUFFIX=ppc64el CC=powerpc64le-linux-gnu-gcc ARCH_NATIVE= MINIMAL= 18 | - ARCH_SUFFIX=s390x CC=s390x-linux-gnu-gcc ARCH_NATIVE= MINIMAL= 19 | global: 20 | - secure: "RKF9Z9gLxp6k/xITqn7ma1E9HfpYcDXuJFf4862WeH9EMnK9lDq+TWnGsQfkIlqh8h9goe7U+BvRiTibj9MiD5u7eluLo3dlwsLxPpYtyswYeLeC1wKKdT5LPGAXbRKomvBalRYMI+dDnGIM4w96mHgGGvx2zZXGkiAQhm6fJ3k=" 21 | - DIST_DIR="${PWD}/dist" 22 | 23 | before_install: 24 | - openssl aes-256-cbc -K $encrypted_2893fd5649e7_key -iv $encrypted_2893fd5649e7_iv -in sign.key.enc -out sign.key -d || echo "Encrypted signing key unavailable" 25 | 26 | script: 27 | - ./ddist.sh "$ARCH_SUFFIX" 28 | - ls -lah "$DIST_DIR" 29 | - git diff --exit-code 30 | 31 | deploy: 32 | provider: releases 33 | api_key: 34 | secure: Yk90ANpSPv1iJy8QDXCPwfaSmEr/WIJ3bzhQ6X8JvZjfrwTosbh0HrUzQyeac3nyvNwj7YJRssolOFc21IBKPpCFTZqYxSkuLPU6ysG4HGHgN6YJhOMm4mG4KKJ6741q3DJendhZpalBhCEi+NcZK/PCSD97Vl4OqRjBUged0fs= 35 | file: "${DIST_DIR}/*" 36 | file_glob: true 37 | skip_cleanup: true 38 | on: 39 | repo: krallin/tini 40 | tags: true 41 | condition: '-z "$MINIMAL"' 42 | -------------------------------------------------------------------------------- /test/reaping/stage_1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import os 4 | import sys 5 | import subprocess 6 | import time 7 | 8 | import psutil 9 | 10 | 11 | def in_group_or_reaped(pid): 12 | try: 13 | return os.getpgid(pid) == os.getpgid(0) 14 | except OSError: 15 | return True 16 | 17 | 18 | def main(): 19 | stage_2 = os.path.join(os.path.dirname(__file__), "stage_2.py") 20 | subprocess.Popen([stage_2]).wait() 21 | 22 | # In tests, we assume this process is the direct child of init 23 | this_process = psutil.Process(os.getpid()) 24 | init_process = this_process.parent() 25 | 26 | print("Reaping test: stage_1 is pid{0}, init is pid{1}".format( 27 | this_process.pid, init_process.pid)) 28 | 29 | # The only child PID that should persist is this one. 30 | expected_pids = [this_process.pid] 31 | 32 | print("Expecting pids to remain: {0}".format( 33 | ", ".join(str(pid) for pid in expected_pids))) 34 | 35 | while 1: 36 | pids = [p.pid for p in init_process.children(recursive=True)] 37 | print("Has pids: {0}".format(", ".join(str(pid) for pid in pids))) 38 | for pid in pids: 39 | assert in_group_or_reaped(pid), "Child had unexpected pgid" 40 | if set(pids) == set(expected_pids): 41 | break 42 | time.sleep(1) 43 | 44 | # Now, check if there are any zombies. For each of the potential zombies, 45 | # we check that the pgid is ours. NOTE: We explicitly test that this test 46 | # fails if subreaping is disabled, so we can be confident this doesn't turn 47 | # a failure into a success. 48 | for process in psutil.process_iter(): 49 | if process.pid == this_process.pid: 50 | continue 51 | if not in_group_or_reaped(process.pid): 52 | continue 53 | print("Not reaped: pid {0}: {1}".format(process.pid, process.name())) 54 | sys.exit(1) 55 | 56 | sys.exit(0) 57 | 58 | 59 | if __name__ == "__main__": 60 | main() 61 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.8.0) 2 | project (tini C) 3 | 4 | # Config 5 | set (tini_VERSION_MAJOR 0) 6 | set (tini_VERSION_MINOR 18) 7 | set (tini_VERSION_PATCH 0) 8 | 9 | # Build options 10 | option(MINIMAL "Disable argument parsing and verbose output" OFF) 11 | 12 | if(MINIMAL) 13 | add_definitions(-DTINI_MINIMAL=1) 14 | endif() 15 | 16 | # Extract git version and dirty-ness 17 | execute_process ( 18 | COMMAND git --git-dir "${PROJECT_SOURCE_DIR}/.git" --work-tree "${PROJECT_SOURCE_DIR}" log -n 1 --date=local --pretty=format:%h 19 | WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" 20 | RESULT_VARIABLE git_version_check_ret 21 | OUTPUT_VARIABLE tini_VERSION_GIT 22 | ) 23 | 24 | execute_process( 25 | COMMAND git --git-dir "${PROJECT_SOURCE_DIR}/.git" --work-tree "${PROJECT_SOURCE_DIR}" status --porcelain --untracked-files=no 26 | WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" 27 | OUTPUT_VARIABLE git_dirty_check_out 28 | ) 29 | 30 | if("${git_version_check_ret}" EQUAL 0) 31 | set(tini_VERSION_GIT " - git.${tini_VERSION_GIT}") 32 | if(NOT "${git_dirty_check_out}" STREQUAL "") 33 | set(tini_VERSION_GIT "${tini_VERSION_GIT}-dirty") 34 | endif() 35 | else() 36 | set(tini_VERSION_GIT "") 37 | endif() 38 | 39 | # Flags 40 | include(CheckCSourceCompiles) 41 | 42 | check_c_source_compiles(" 43 | #ifndef _FORTIFY_SOURCE 44 | #error \"Not defined: _FORTIFY_SOURCE\" 45 | #endif 46 | int main(void) { 47 | return 0; 48 | } 49 | " HAS_BUILTIN_FORTIFY) 50 | 51 | # Flags 52 | if(NOT HAS_BUILTIN_FORTIFY) 53 | add_definitions(-D_FORTIFY_SOURCE=2) 54 | endif() 55 | 56 | set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -Werror -Wextra -Wall -pedantic-errors -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat") 57 | set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-s") 58 | 59 | # Build 60 | 61 | configure_file ( 62 | "${PROJECT_SOURCE_DIR}/src/tiniConfig.h.in" 63 | "${PROJECT_BINARY_DIR}/tiniConfig.h" 64 | @ONLY 65 | ) 66 | 67 | configure_file ( 68 | "${PROJECT_SOURCE_DIR}/tpl/README.md.in" 69 | "${PROJECT_SOURCE_DIR}/README.md" 70 | @ONLY 71 | ) 72 | 73 | configure_file ( 74 | "${PROJECT_SOURCE_DIR}/tpl/VERSION.in" 75 | "${PROJECT_BINARY_DIR}/VERSION" 76 | @ONLY 77 | ) 78 | 79 | 80 | include_directories ("${PROJECT_BINARY_DIR}") 81 | 82 | add_executable (tini src/tini.c) 83 | 84 | add_executable (tini-static src/tini.c) 85 | set_target_properties (tini-static PROPERTIES LINK_FLAGS "-Wl,--no-export-dynamic -static") 86 | 87 | # Installation 88 | install (TARGETS tini DESTINATION bin) 89 | install (TARGETS tini-static DESTINATION bin) 90 | 91 | # Packaging 92 | include (InstallRequiredSystemLibraries) 93 | set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "A tiny but valid init process for containers") 94 | set (CPACK_PACKAGE_VENDOR "Thomas Orozco") 95 | set (CPACK_PACKAGE_CONTACT "thomas@orozco.fr") 96 | set (CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README.md") 97 | set (CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") 98 | set (CPACK_PACKAGE_VERSION_MAJOR "${tini_VERSION_MAJOR}") 99 | set (CPACK_PACKAGE_VERSION_MINOR "${tini_VERSION_MINOR}") 100 | set (CPACK_PACKAGE_VERSION_PATCH "${tini_VERSION_PATCH}") 101 | set (CPACK_PACKAGE_EXECUTABLES "${CMAKE_PROJECT_NAME}") 102 | set (CPACK_PACKAGE_NAME "${CMAKE_PROJECT_NAME}") 103 | set (CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}_${tini_VERSION_MAJOR}.${tini_VERSION_MINOR}.${tini_VERSION_PATCH}") 104 | set (CPACK_PACKAGE_VERSION "${tini_VERSION_MAJOR}.${tini_VERSION_MINOR}.${tini_VERSION_PATCH}") 105 | 106 | set (CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") # TODO 107 | set (CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.3.4)") 108 | 109 | set (CPACK_RPM_PACKAGE_ARCHITECTURE "x86_64") 110 | 111 | set (CPACK_GENERATOR "DEB" "RPM") 112 | 113 | include (CPack) 114 | -------------------------------------------------------------------------------- /test/run_outer_tests.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | import os 3 | import sys 4 | import time 5 | import pipes 6 | import subprocess 7 | import threading 8 | import pexpect 9 | import signal 10 | 11 | 12 | class ReturnContainer(): 13 | def __init__(self): 14 | self.value = None 15 | 16 | 17 | class Command(object): 18 | def __init__(self, cmd, fail_cmd, post_cmd=None, post_delay=0): 19 | self.cmd = cmd 20 | self.fail_cmd = fail_cmd 21 | self.post_cmd = post_cmd 22 | self.post_delay = post_delay 23 | self.proc = None 24 | 25 | def run(self, timeout=None, retcode=0): 26 | print "Testing '{0}'...".format(" ".join(pipes.quote(s) for s in self.cmd)), 27 | sys.stdout.flush() 28 | 29 | err = None 30 | pipe_kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "stdin": subprocess.PIPE} 31 | 32 | def target(): 33 | self.proc = subprocess.Popen(self.cmd, **pipe_kwargs) 34 | self.stdout, self.stderr = self.proc.communicate() 35 | 36 | thread = threading.Thread(target=target) 37 | thread.daemon = True 38 | 39 | thread.start() 40 | 41 | if self.post_cmd is not None: 42 | time.sleep(self.post_delay) 43 | subprocess.check_call(self.post_cmd, **pipe_kwargs) 44 | 45 | thread.join(timeout - self.post_delay if timeout is not None else timeout) 46 | 47 | # Checks 48 | if thread.is_alive(): 49 | subprocess.check_call(self.fail_cmd, **pipe_kwargs) 50 | err = Exception("Test failed with timeout!") 51 | 52 | elif self.proc.returncode != retcode: 53 | err = Exception("Test failed with unexpected returncode (expected {0}, got {1})".format(retcode, self.proc.returncode)) 54 | 55 | if err is not None: 56 | print "FAIL" 57 | print "--- STDOUT ---" 58 | print getattr(self, "stdout", "no stdout") 59 | print "--- STDERR ---" 60 | print getattr(self, "stderr", "no stderr") 61 | print "--- ... ---" 62 | raise err 63 | else: 64 | print "OK" 65 | 66 | 67 | def attach_and_type_exit_0(name): 68 | print "Attaching to {0} to exit 0".format(name) 69 | p = pexpect.spawn("docker attach {0}".format(name)) 70 | p.sendline('') 71 | p.sendline('exit 0') 72 | p.close() 73 | 74 | 75 | def attach_and_issue_ctrl_c(name): 76 | print "Attaching to {0} to CTRL+C".format(name) 77 | p = pexpect.spawn("docker attach {0}".format(name)) 78 | p.expect_exact('#') 79 | p.sendintr() 80 | p.close() 81 | 82 | 83 | def test_tty_handling(img, name, base_cmd, fail_cmd, container_command, exit_function, expect_exit_code): 84 | print "Testing TTY handling (using container command '{0}' and exit function '{1}')".format(container_command, exit_function.__name__) 85 | rc = ReturnContainer() 86 | 87 | shell_ready_event = threading.Event() 88 | 89 | def spawn(): 90 | cmd = base_cmd + ["--tty", "--interactive", img, "/tini/dist/tini"] 91 | if os.environ.get("MINIMAL") is None: 92 | cmd.append("--") 93 | cmd.append(container_command) 94 | p = pexpect.spawn(" ".join(cmd)) 95 | p.expect_exact("#") 96 | shell_ready_event.set() 97 | rc.value = p.wait() 98 | 99 | thread = threading.Thread(target=spawn) 100 | thread.daemon = True 101 | 102 | thread.start() 103 | 104 | if not shell_ready_event.wait(2): 105 | raise Exception("Timeout waiting for shell to spawn") 106 | 107 | exit_function(name) 108 | 109 | thread.join(timeout=2) 110 | 111 | if thread.is_alive(): 112 | subprocess.check_call(fail_cmd) 113 | raise Exception("Timeout waiting for container to exit!") 114 | 115 | if rc.value != expect_exit_code: 116 | raise Exception("Return code is: {0} (expected {1})".format(rc.value, expect_exit_code)) 117 | 118 | 119 | 120 | def main(): 121 | img = sys.argv[1] 122 | name = "{0}-test".format(img) 123 | args_disabled = os.environ.get("MINIMAL") 124 | 125 | root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 126 | 127 | base_cmd = [ 128 | "docker", 129 | "run", 130 | "--rm", 131 | "--volume={0}:/tini".format(root), 132 | "--name={0}".format(name), 133 | ] 134 | 135 | fail_cmd = ["docker", "kill", "-s", "KILL", name] 136 | 137 | # Funtional tests 138 | for entrypoint in ["/tini/dist/tini", "/tini/dist/tini-static"]: 139 | functional_base_cmd = base_cmd + [ 140 | "--entrypoint={0}".format(entrypoint), 141 | "-e", "TINI_VERBOSITY=3", 142 | img, 143 | ] 144 | 145 | # Reaping test 146 | Command(functional_base_cmd + ["/tini/test/reaping/stage_1.py"], fail_cmd).run(timeout=10) 147 | 148 | # Signals test 149 | for sig, retcode in [("TERM", 143), ("USR1", 138), ("USR2", 140)]: 150 | Command( 151 | functional_base_cmd + ["/tini/test/signals/test.py"], 152 | fail_cmd, 153 | ["docker", "kill", "-s", sig, name], 154 | 2 155 | ).run(timeout=10, retcode=retcode) 156 | 157 | # Exit code test 158 | Command(functional_base_cmd + ["-z"], fail_cmd).run(retcode=127 if args_disabled else 1) 159 | Command(functional_base_cmd + ["-h"], fail_cmd).run(retcode=127 if args_disabled else 0) 160 | Command(functional_base_cmd + ["zzzz"], fail_cmd).run(retcode=127) 161 | Command(functional_base_cmd + ["-w"], fail_cmd).run(retcode=127 if args_disabled else 0) 162 | 163 | # Valgrind test (we only run this on the dynamic version, because otherwise Valgrind may bring up plenty of errors that are 164 | # actually from libc) 165 | Command(base_cmd + [img, "valgrind", "--leak-check=full", "--error-exitcode=1", "/tini/dist/tini", "ls"], fail_cmd).run() 166 | 167 | # Test tty handling 168 | test_tty_handling(img, name, base_cmd, fail_cmd, "dash", attach_and_type_exit_0, 0) 169 | test_tty_handling(img, name, base_cmd, fail_cmd, "dash -c 'while true; do echo \#; sleep 0.1; done'", attach_and_issue_ctrl_c, 128 + signal.SIGINT) 170 | 171 | # Installation tests (sh -c is used for globbing and &&) 172 | for image, pkg_manager, extension in [ 173 | ["ubuntu:precise", "dpkg", "deb"], 174 | ["ubuntu:trusty", "dpkg", "deb"], 175 | ["centos:6", "rpm", "rpm"], 176 | ["centos:7", "rpm", "rpm"], 177 | ]: 178 | Command(base_cmd + [image, "sh", "-c", "{0} -i /tini/dist/*.{1} && /usr/bin/tini true".format(pkg_manager, extension)], fail_cmd).run() 179 | 180 | 181 | if __name__ == "__main__": 182 | main() 183 | -------------------------------------------------------------------------------- /src/tiniLicense.h: -------------------------------------------------------------------------------- 1 | unsigned char LICENSE[] = { 2 | 0x54, 0x68, 0x65, 0x20, 0x4d, 0x49, 0x54, 0x20, 0x4c, 0x69, 0x63, 0x65, 3 | 0x6e, 0x73, 0x65, 0x20, 0x28, 0x4d, 0x49, 0x54, 0x29, 0x0a, 0x0a, 0x43, 4 | 0x6f, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x28, 0x63, 0x29, 5 | 0x20, 0x32, 0x30, 0x31, 0x35, 0x20, 0x54, 0x68, 0x6f, 0x6d, 0x61, 0x73, 6 | 0x20, 0x4f, 0x72, 0x6f, 0x7a, 0x63, 0x6f, 0x20, 0x3c, 0x74, 0x68, 0x6f, 7 | 0x6d, 0x61, 0x73, 0x40, 0x6f, 0x72, 0x6f, 0x7a, 0x63, 0x6f, 0x2e, 0x66, 8 | 0x72, 0x3e, 0x0a, 0x0a, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 9 | 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x68, 0x65, 0x72, 0x65, 0x62, 0x79, 10 | 0x20, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x2c, 0x20, 0x66, 0x72, 11 | 0x65, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 12 | 0x2c, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x6e, 0x79, 0x20, 0x70, 0x65, 0x72, 13 | 0x73, 0x6f, 0x6e, 0x20, 0x6f, 0x62, 0x74, 0x61, 0x69, 0x6e, 0x69, 0x6e, 14 | 0x67, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x70, 0x79, 0x0a, 0x6f, 0x66, 0x20, 15 | 0x74, 0x68, 0x69, 0x73, 0x20, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 16 | 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 17 | 0x61, 0x74, 0x65, 0x64, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 18 | 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x73, 19 | 0x20, 0x28, 0x74, 0x68, 0x65, 0x20, 0x22, 0x53, 0x6f, 0x66, 0x74, 0x77, 20 | 0x61, 0x72, 0x65, 0x22, 0x29, 0x2c, 0x20, 0x74, 0x6f, 0x20, 0x64, 0x65, 21 | 0x61, 0x6c, 0x0a, 0x69, 0x6e, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, 0x6f, 22 | 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, 23 | 0x75, 0x74, 0x20, 0x72, 0x65, 0x73, 0x74, 0x72, 0x69, 0x63, 0x74, 0x69, 24 | 0x6f, 0x6e, 0x2c, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x69, 0x6e, 25 | 0x67, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x20, 0x6c, 0x69, 26 | 0x6d, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x68, 0x65, 27 | 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x73, 0x0a, 0x74, 0x6f, 0x20, 0x75, 28 | 0x73, 0x65, 0x2c, 0x20, 0x63, 0x6f, 0x70, 0x79, 0x2c, 0x20, 0x6d, 0x6f, 29 | 0x64, 0x69, 0x66, 0x79, 0x2c, 0x20, 0x6d, 0x65, 0x72, 0x67, 0x65, 0x2c, 30 | 0x20, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x2c, 0x20, 0x64, 0x69, 31 | 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x2c, 0x20, 0x73, 0x75, 32 | 0x62, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x2c, 0x20, 0x61, 0x6e, 33 | 0x64, 0x2f, 0x6f, 0x72, 0x20, 0x73, 0x65, 0x6c, 0x6c, 0x0a, 0x63, 0x6f, 34 | 0x70, 0x69, 0x65, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 35 | 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x2c, 0x20, 0x61, 0x6e, 36 | 0x64, 0x20, 0x74, 0x6f, 0x20, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x74, 0x20, 37 | 0x70, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x73, 0x20, 0x74, 0x6f, 0x20, 0x77, 38 | 0x68, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x53, 0x6f, 0x66, 0x74, 39 | 0x77, 0x61, 0x72, 0x65, 0x20, 0x69, 0x73, 0x0a, 0x66, 0x75, 0x72, 0x6e, 40 | 0x69, 0x73, 0x68, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x64, 0x6f, 0x20, 41 | 0x73, 0x6f, 0x2c, 0x20, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x20, 42 | 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 43 | 0x77, 0x69, 0x6e, 0x67, 0x20, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 44 | 0x6f, 0x6e, 0x73, 0x3a, 0x0a, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x61, 0x62, 45 | 0x6f, 0x76, 0x65, 0x20, 0x63, 0x6f, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 46 | 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x20, 0x61, 0x6e, 0x64, 47 | 0x20, 0x74, 0x68, 0x69, 0x73, 0x20, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 48 | 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x20, 49 | 0x73, 0x68, 0x61, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x69, 0x6e, 0x63, 50 | 0x6c, 0x75, 0x64, 0x65, 0x64, 0x20, 0x69, 0x6e, 0x0a, 0x61, 0x6c, 0x6c, 51 | 0x20, 0x63, 0x6f, 0x70, 0x69, 0x65, 0x73, 0x20, 0x6f, 0x72, 0x20, 0x73, 52 | 0x75, 0x62, 0x73, 0x74, 0x61, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x20, 0x70, 53 | 0x6f, 0x72, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x74, 54 | 0x68, 0x65, 0x20, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x2e, 55 | 0x0a, 0x0a, 0x54, 0x48, 0x45, 0x20, 0x53, 0x4f, 0x46, 0x54, 0x57, 0x41, 56 | 0x52, 0x45, 0x20, 0x49, 0x53, 0x20, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 57 | 0x45, 0x44, 0x20, 0x22, 0x41, 0x53, 0x20, 0x49, 0x53, 0x22, 0x2c, 0x20, 58 | 0x57, 0x49, 0x54, 0x48, 0x4f, 0x55, 0x54, 0x20, 0x57, 0x41, 0x52, 0x52, 59 | 0x41, 0x4e, 0x54, 0x59, 0x20, 0x4f, 0x46, 0x20, 0x41, 0x4e, 0x59, 0x20, 60 | 0x4b, 0x49, 0x4e, 0x44, 0x2c, 0x20, 0x45, 0x58, 0x50, 0x52, 0x45, 0x53, 61 | 0x53, 0x20, 0x4f, 0x52, 0x0a, 0x49, 0x4d, 0x50, 0x4c, 0x49, 0x45, 0x44, 62 | 0x2c, 0x20, 0x49, 0x4e, 0x43, 0x4c, 0x55, 0x44, 0x49, 0x4e, 0x47, 0x20, 63 | 0x42, 0x55, 0x54, 0x20, 0x4e, 0x4f, 0x54, 0x20, 0x4c, 0x49, 0x4d, 0x49, 64 | 0x54, 0x45, 0x44, 0x20, 0x54, 0x4f, 0x20, 0x54, 0x48, 0x45, 0x20, 0x57, 65 | 0x41, 0x52, 0x52, 0x41, 0x4e, 0x54, 0x49, 0x45, 0x53, 0x20, 0x4f, 0x46, 66 | 0x20, 0x4d, 0x45, 0x52, 0x43, 0x48, 0x41, 0x4e, 0x54, 0x41, 0x42, 0x49, 67 | 0x4c, 0x49, 0x54, 0x59, 0x2c, 0x0a, 0x46, 0x49, 0x54, 0x4e, 0x45, 0x53, 68 | 0x53, 0x20, 0x46, 0x4f, 0x52, 0x20, 0x41, 0x20, 0x50, 0x41, 0x52, 0x54, 69 | 0x49, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x20, 0x50, 0x55, 0x52, 0x50, 0x4f, 70 | 0x53, 0x45, 0x20, 0x41, 0x4e, 0x44, 0x20, 0x4e, 0x4f, 0x4e, 0x49, 0x4e, 71 | 0x46, 0x52, 0x49, 0x4e, 0x47, 0x45, 0x4d, 0x45, 0x4e, 0x54, 0x2e, 0x20, 72 | 0x49, 0x4e, 0x20, 0x4e, 0x4f, 0x20, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x20, 73 | 0x53, 0x48, 0x41, 0x4c, 0x4c, 0x20, 0x54, 0x48, 0x45, 0x0a, 0x41, 0x55, 74 | 0x54, 0x48, 0x4f, 0x52, 0x53, 0x20, 0x4f, 0x52, 0x20, 0x43, 0x4f, 0x50, 75 | 0x59, 0x52, 0x49, 0x47, 0x48, 0x54, 0x20, 0x48, 0x4f, 0x4c, 0x44, 0x45, 76 | 0x52, 0x53, 0x20, 0x42, 0x45, 0x20, 0x4c, 0x49, 0x41, 0x42, 0x4c, 0x45, 77 | 0x20, 0x46, 0x4f, 0x52, 0x20, 0x41, 0x4e, 0x59, 0x20, 0x43, 0x4c, 0x41, 78 | 0x49, 0x4d, 0x2c, 0x20, 0x44, 0x41, 0x4d, 0x41, 0x47, 0x45, 0x53, 0x20, 79 | 0x4f, 0x52, 0x20, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x0a, 0x4c, 0x49, 0x41, 80 | 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x2c, 0x20, 0x57, 0x48, 0x45, 0x54, 81 | 0x48, 0x45, 0x52, 0x20, 0x49, 0x4e, 0x20, 0x41, 0x4e, 0x20, 0x41, 0x43, 82 | 0x54, 0x49, 0x4f, 0x4e, 0x20, 0x4f, 0x46, 0x20, 0x43, 0x4f, 0x4e, 0x54, 83 | 0x52, 0x41, 0x43, 0x54, 0x2c, 0x20, 0x54, 0x4f, 0x52, 0x54, 0x20, 0x4f, 84 | 0x52, 0x20, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x57, 0x49, 0x53, 0x45, 0x2c, 85 | 0x20, 0x41, 0x52, 0x49, 0x53, 0x49, 0x4e, 0x47, 0x20, 0x46, 0x52, 0x4f, 86 | 0x4d, 0x2c, 0x0a, 0x4f, 0x55, 0x54, 0x20, 0x4f, 0x46, 0x20, 0x4f, 0x52, 87 | 0x20, 0x49, 0x4e, 0x20, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x49, 88 | 0x4f, 0x4e, 0x20, 0x57, 0x49, 0x54, 0x48, 0x20, 0x54, 0x48, 0x45, 0x20, 89 | 0x53, 0x4f, 0x46, 0x54, 0x57, 0x41, 0x52, 0x45, 0x20, 0x4f, 0x52, 0x20, 90 | 0x54, 0x48, 0x45, 0x20, 0x55, 0x53, 0x45, 0x20, 0x4f, 0x52, 0x20, 0x4f, 91 | 0x54, 0x48, 0x45, 0x52, 0x20, 0x44, 0x45, 0x41, 0x4c, 0x49, 0x4e, 0x47, 92 | 0x53, 0x20, 0x49, 0x4e, 0x0a, 0x54, 0x48, 0x45, 0x20, 0x53, 0x4f, 0x46, 93 | 0x54, 0x57, 0x41, 0x52, 0x45, 0x2e, 0x0a 94 | }; 95 | unsigned int LICENSE_len = 1099; 96 | -------------------------------------------------------------------------------- /test/run_inner_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | import os 4 | import sys 5 | import signal 6 | import subprocess 7 | import time 8 | import psutil 9 | import bitmap 10 | import re 11 | import itertools 12 | import tempfile 13 | 14 | DEVNULL = open(os.devnull, 'wb') 15 | 16 | SIGNUM_TO_SIGNAME = dict((v, k) for k,v in signal.__dict__.items() if re.match("^SIG[A-Z]+$", k)) 17 | 18 | 19 | def busy_wait(condition_callable, timeout): 20 | checks = 100 21 | increment = float(timeout) / checks 22 | 23 | for _ in xrange(checks): 24 | if condition_callable(): 25 | return 26 | time.sleep(increment) 27 | 28 | assert False, "Condition was never met" 29 | 30 | 31 | def main(): 32 | src = os.environ["SOURCE_DIR"] 33 | build = os.environ["BUILD_DIR"] 34 | 35 | args_disabled = os.environ.get("MINIMAL") 36 | 37 | proxy = os.path.join(src, "test", "subreaper-proxy.py") 38 | tini = os.path.join(build, "tini") 39 | 40 | subreaper_support = bool(int(os.environ["FORCE_SUBREAPER"])) 41 | 42 | # Run the exit code test. We use POSIXLY_CORRECT here to not need -- 43 | # until that's the default in Tini anyways. 44 | if not args_disabled: 45 | print "Running exit code test for {0}".format(tini) 46 | for code in range(0, 256): 47 | p = subprocess.Popen( 48 | [tini, '-e', str(code), '--', 'sh', '-c', "exit {0}".format(code)], 49 | stdout=DEVNULL, stderr=DEVNULL 50 | ) 51 | ret = p.wait() 52 | assert ret == 0, "Inclusive exit code test failed for %s, exit: %s" % (code, ret) 53 | 54 | other_codes = [x for x in range(0, 256) if x != code] 55 | args = list(itertools.chain(*[['-e', str(x)] for x in other_codes])) 56 | 57 | p = subprocess.Popen( 58 | [tini] + args + ['sh', '-c', "exit {0}".format(code)], 59 | env=dict(os.environ, POSIXLY_CORRECT="1"), 60 | stdout=DEVNULL, stderr=DEVNULL 61 | ) 62 | ret = p.wait() 63 | assert ret == code, "Exclusive exit code test failed for %s, exit: %s" % (code, ret) 64 | 65 | tests = [([proxy, tini], {}),] 66 | 67 | if subreaper_support: 68 | if not args_disabled: 69 | tests.append(([tini, "-s"], {})) 70 | tests.append(([tini], {"TINI_SUBREAPER": ""})) 71 | 72 | for target, env in tests: 73 | # Run the reaping test 74 | print "Running reaping test ({0} with env {1})".format(" ".join(target), env) 75 | p = subprocess.Popen(target + [os.path.join(src, "test", "reaping", "stage_1.py")], 76 | env=dict(os.environ, **env), 77 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 78 | 79 | out, err = p.communicate() 80 | 81 | if subreaper_support: 82 | # If subreaper support sin't available, Tini won't looku p its subreaper bit 83 | # and will output the error message here. 84 | assert "zombie reaping won't work" not in err, "Warning message was output!" 85 | ret = p.wait() 86 | assert "Reaped zombie process with pid=" not in err, "Warning message was output!" 87 | assert ret == 0, "Reaping test failed!\nOUT: %s\nERR: %s" % (out, err) 88 | 89 | 90 | if not args_disabled: 91 | print "Running reaping display test ({0} with env {1})".format(" ".join(target), env) 92 | p = subprocess.Popen(target + ["-w", os.path.join(src, "test", "reaping", "stage_1.py")], 93 | env=dict(os.environ, **env), 94 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 95 | 96 | out, err = p.communicate() 97 | ret = p.wait() 98 | assert "Reaped zombie process with pid=" in err, "Warning message was output!" 99 | assert ret == 0, "Reaping display test failed!\nOUT: %s\nERR: %s" % (out, err) 100 | 101 | 102 | # Run the signals test 103 | for signum in [signal.SIGTERM, signal.SIGUSR1, signal.SIGUSR2]: 104 | print "running signal test for: {0} ({1} with env {2})".format(signum, " ".join(target), env) 105 | p = subprocess.Popen(target + [os.path.join(src, "test", "signals", "test.py")], env=dict(os.environ, **env)) 106 | busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) > 1, 10) 107 | p.send_signal(signum) 108 | ret = p.wait() 109 | assert ret == 128 + signum, "Signals test failed (ret was {0}, expected {1})".format(ret, 128 + signum) 110 | 111 | 112 | # Run the process group test 113 | # This test has Tini spawn a process that ignores SIGUSR1 and spawns a child that doesn't (and waits on the child) 114 | # We send SIGUSR1 to Tini, and expect the grand-child to terminate, then the child, and then Tini. 115 | if not args_disabled: 116 | print "Running process group test (arguments)" 117 | p = subprocess.Popen([tini, '-g', os.path.join(src, "test", "pgroup", "stage_1.py")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 118 | 119 | busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 2, 10) 120 | p.send_signal(signal.SIGUSR1) 121 | busy_wait(lambda: p.poll() is not None, 10) 122 | 123 | print "Running process group test (environment variable)" 124 | p = subprocess.Popen( 125 | [tini, os.path.join(src, "test", "pgroup", "stage_1.py")], 126 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 127 | env=dict(os.environ, TINI_KILL_PROCESS_GROUP="1") 128 | ) 129 | 130 | busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 2, 10) 131 | p.send_signal(signal.SIGUSR1) 132 | busy_wait(lambda: p.poll() is not None, 10) 133 | 134 | # Run failing test. Force verbosity to 1 so we see the subreaper warning 135 | # regardless of whether MINIMAL is set. 136 | print "Running zombie reaping failure test (Tini should warn)" 137 | p = subprocess.Popen( 138 | [tini, os.path.join(src, "test", "reaping", "stage_1.py")], 139 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 140 | env={'TINI_VERBOSITY': '1'} 141 | ) 142 | out, err = p.communicate() 143 | assert "zombie reaping won't work" in err, "No warning message was output!" 144 | ret = p.wait() 145 | assert ret == 1, "Reaping test succeeded (it should have failed)!" 146 | 147 | # Test that the signals are properly in place here. 148 | print "Running signal configuration test" 149 | 150 | p = subprocess.Popen([os.path.join(build, "sigconf-test"), tini, "cat", "/proc/self/status"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 151 | out, err = p.communicate() 152 | 153 | # Extract the signal properties, and add a zero at the end. 154 | props = [line.split(":") for line in out.splitlines()] 155 | props = [(k.strip(), v.strip()) for (k, v) in props] 156 | props = [(k, bitmap.BitMap.fromstring(bin(int(v, 16))[2:].zfill(32))) for (k, v) in props if k in ["SigBlk", "SigIgn", "SigCgt"]] 157 | props = dict(props) 158 | 159 | # Print actual handling configuration 160 | for k, bmp in props.items(): 161 | print "{0}: {1}".format(k, ", ".join(["{0} ({1})".format(SIGNUM_TO_SIGNAME[n+1], n+1) for n in bmp.nonzero()])) 162 | 163 | for signal_set_name, signals_to_test_for in [ 164 | ("SigIgn", [signal.SIGTTOU, signal.SIGSEGV, signal.SIGINT,]), 165 | ("SigBlk", [signal.SIGTTIN, signal.SIGILL, signal.SIGTERM,]), 166 | ]: 167 | for signum in signals_to_test_for: 168 | # Use signum - 1 because the bitmap is 0-indexed but represents signals strting at 1 169 | assert (signum - 1) in props[signal_set_name].nonzero(), "{0} ({1}) is missing in {2}!".format(SIGNUM_TO_SIGNAME[signum], signum, signal_set_name) 170 | 171 | # Test parent death signal handling. 172 | if not args_disabled: 173 | print "Running parent death signal test" 174 | f = tempfile.NamedTemporaryFile() 175 | try: 176 | p = subprocess.Popen( 177 | [os.path.join(src, "test", "pdeathsignal", "stage_1.py"), tini, f.name], 178 | stdout=DEVNULL, stderr=DEVNULL 179 | ) 180 | p.wait() 181 | 182 | busy_wait(lambda: open(f.name).read() == "ok", 10) 183 | finally: 184 | f.close() 185 | 186 | print "---------------------------" 187 | print "All done, tests as expected" 188 | print "---------------------------" 189 | 190 | 191 | if __name__ == "__main__": 192 | main() 193 | -------------------------------------------------------------------------------- /ci/run_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Should be run from the root dir, or SOURCE_DIR should be set. 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | # Default compiler 8 | : ${CC:="gcc"} 9 | 10 | 11 | # Paths 12 | : ${SOURCE_DIR:="."} 13 | : ${DIST_DIR:="${SOURCE_DIR}/dist"} 14 | : ${BUILD_DIR:="/tmp/build"} 15 | 16 | # GPG Configuration 17 | : ${GPG_PASSPHRASE:=""} 18 | 19 | 20 | # Make those paths absolute, and export them for the Python tests to consume. 21 | export SOURCE_DIR="$(readlink -f "${SOURCE_DIR}")" 22 | export DIST_DIR="$(readlink -f "${DIST_DIR}")" 23 | export BUILD_DIR="$(readlink -f "${BUILD_DIR}")" 24 | 25 | # Configuration 26 | : ${FORCE_SUBREAPER:="1"} 27 | export FORCE_SUBREAPER 28 | 29 | 30 | # Our build platform doesn't have those newer Linux flags, but we want Tini to have subreaper support 31 | # We also use those in our tests 32 | CFLAGS="${CFLAGS-} -DPR_SET_CHILD_SUBREAPER=36 -DPR_GET_CHILD_SUBREAPER=37" 33 | if [[ "${FORCE_SUBREAPER}" -eq 1 ]]; then 34 | # If FORCE_SUBREAPER is requested, then we set those CFLAGS for the Tini build 35 | export CFLAGS 36 | fi 37 | 38 | echo "CC=${CC}" 39 | echo "CFLAGS=${CFLAGS}" 40 | echo "MINIMAL=${MINIMAL-}" 41 | echo "ARCH_SUFFIX=${ARCH_SUFFIX-}" 42 | echo "ARCH_NATIVE=${ARCH_NATIVE-}" 43 | 44 | # Ensure Python output is not buffered (to make tests output clearer) 45 | export PYTHONUNBUFFERED=1 46 | 47 | # Set path to prioritize our utils 48 | export REAL_PATH="${PATH}" 49 | export PATH="${SOURCE_DIR}/ci/util:${PATH}" 50 | 51 | # Build 52 | CMAKE_ARGS=(-B"${BUILD_DIR}" -H"${SOURCE_DIR}") 53 | if [[ -n "${MINIMAL:-}" ]]; then 54 | CMAKE_ARGS+=(-DMINIMAL=ON) 55 | fi 56 | cmake "${CMAKE_ARGS[@]}" 57 | 58 | pushd "${BUILD_DIR}" 59 | make clean 60 | make 61 | if [[ -n "${ARCH_NATIVE-}" ]]; then 62 | make package 63 | fi 64 | popd 65 | 66 | pkg_version="$(cat "${BUILD_DIR}/VERSION")" 67 | 68 | if [[ -n "${ARCH_NATIVE-}" ]]; then 69 | echo "Built native package (ARCH_NATIVE=${ARCH_NATIVE-})" 70 | echo "Running smoke and internal tests" 71 | 72 | BIN_TEST_DIR="${BUILD_DIR}/bin-test" 73 | mkdir -p "$BIN_TEST_DIR" 74 | export PATH="${BIN_TEST_DIR}:${PATH}" 75 | 76 | # Smoke tests (actual tests need Docker to run; they don't run within the CI environment) 77 | for tini in "${BUILD_DIR}/tini" "${BUILD_DIR}/tini-static"; do 78 | echo "Smoke test for ${tini}" 79 | "$tini" --version 80 | 81 | echo "Testing ${tini} --version" 82 | "$tini" --version | grep -q "tini version" 83 | 84 | echo "Testing ${tini} without arguments exits with 1" 85 | ! "$tini" 2>/dev/null 86 | 87 | echo "Testing ${tini} shows help message" 88 | { 89 | ! "$tini" 2>&1 90 | } | grep -q "supervision of a valid init process" 91 | 92 | if [[ -n "${MINIMAL:-}" ]]; then 93 | echo "Testing $tini with: true" 94 | "${tini}" true 95 | 96 | echo "Testing $tini with: false" 97 | if "${tini}" false; then 98 | exit 1 99 | fi 100 | 101 | echo "Testing ${tini} does not reference options that don't exist" 102 | ! { 103 | ! "$tini" 2>&1 104 | } | grep -q "more verbose" 105 | 106 | # We try running binaries named after flags (both valid and invalid 107 | # flags) and test that they run. 108 | for flag in h s w x; do 109 | bin="-${flag}" 110 | echo "Testing $tini can run binary: ${bin}" 111 | cp "$(which true)" "${BIN_TEST_DIR}/${bin}" 112 | "$tini" "$bin" 113 | done 114 | 115 | echo "Testing $tini can run binary --version if args are given" 116 | cp "$(which true)" "${BIN_TEST_DIR}/--version" 117 | if "$tini" "--version" --foo | grep -q "tini version"; then 118 | exit 1 119 | fi 120 | else 121 | echo "Testing ${tini} -h" 122 | "${tini}" -h 123 | 124 | echo "Testing $tini for license" 125 | "$tini" -l | diff - "${SOURCE_DIR}/LICENSE" 126 | 127 | echo "Testing $tini with: true" 128 | "${tini}" -vvv true 129 | 130 | echo "Testing $tini with: false" 131 | if "${tini}" -vvv false; then 132 | exit 1 133 | fi 134 | 135 | echo "Testing ${tini} references options that exist" 136 | { 137 | ! "$tini" 2>&1 138 | } | grep -q "more verbose" 139 | 140 | echo "Testing $tini with: -- true (should succeed)" 141 | "${tini}" -vvv -- true 142 | 143 | echo "Testing $tini with: -- -- true (should fail)" 144 | if "${tini}" -vvv -- -- true; then 145 | exit 1 146 | fi 147 | fi 148 | 149 | echo "Testing ${tini} supports TINI_VERBOSITY" 150 | TINI_VERBOSITY=3 "$tini" true 2>&1 | grep -q 'Received SIGCHLD' 151 | 152 | echo "Testing ${tini} exits with 127 if the command does not exist" 153 | "$tini" foobar123 && rc="$?" || rc="$?" 154 | if [[ "$rc" != 127 ]]; then 155 | echo "Exit code was: ${rc}" 156 | exit 1 157 | fi 158 | 159 | echo "Testing ${tini} exits with 126 if the command is not executable" 160 | "$tini" /etc && rc="$?" || rc="$?" 161 | if [[ "$rc" != 126 ]]; then 162 | echo "Exit code was: ${rc}" 163 | exit 1 164 | fi 165 | 166 | # Test stdin / stdout are handed over to child 167 | echo "Testing ${tini} does not break pipes" 168 | echo "exit 0" | "${tini}" sh 169 | if [[ ! "$?" -eq "0" ]]; then 170 | echo "Pipe test failed" 171 | exit 1 172 | fi 173 | 174 | echo "Checking hardening on $tini" 175 | hardening_skip=(--nopie --nostackprotector --nobindnow) 176 | if [[ "$CC" == "musl-gcc" ]]; then 177 | # FORTIFY_SOURCE is a glibc thing 178 | hardening_skip=("${hardening_skip[@]}" --nofortify) 179 | fi 180 | hardening-check "${hardening_skip[@]}" "${tini}" 181 | done 182 | 183 | # Quick package audit 184 | if which rpm >/dev/null; then 185 | echo "Contents for RPM:" 186 | rpm -qlp "${BUILD_DIR}/tini_${pkg_version}.rpm" 187 | echo "--" 188 | fi 189 | 190 | if which dpkg >/dev/null; then 191 | echo "Contents for DEB:" 192 | dpkg --contents "${BUILD_DIR}/tini_${pkg_version}.deb" 193 | echo "--" 194 | fi 195 | 196 | # Compile test code 197 | "${CC}" -o "${BUILD_DIR}/sigconf-test" "${SOURCE_DIR}/test/sigconf/sigconf-test.c" 198 | 199 | # Create virtual environment to run tests. 200 | # Accept system site packages for faster local builds. 201 | VENV="${BUILD_DIR}/venv" 202 | virtualenv --system-site-packages "${VENV}" 203 | 204 | # Don't use activate because it does not play nice with nounset 205 | export PATH="${VENV}/bin:${PATH}" 206 | export CFLAGS # We need them to build our test suite, regardless of FORCE_SUBREAPER 207 | 208 | # Install test dependencies 209 | CC=gcc pip install psutil python-prctl bitmap 210 | 211 | # Run tests 212 | python "${SOURCE_DIR}/test/run_inner_tests.py" 213 | else 214 | if [[ ! -n "${ARCH_SUFFIX-}" ]]; then 215 | echo "Built cross package, but $ARCH_SUFFIX is empty!" 216 | exit 1 217 | fi 218 | echo "Built cross package (ARCH_SUFFIX=${ARCH_SUFFIX})" 219 | echo "Skipping smoke and internal tests" 220 | fi 221 | 222 | # Now, copy over files to DIST_DIR, with appropriate names depending on the 223 | # architecture. 224 | # Handle the DEB / RPM 225 | mkdir -p "${DIST_DIR}" 226 | 227 | TINIS=() 228 | 229 | for tini in tini tini-static; do 230 | if [[ -n "${ARCH_SUFFIX-}" ]]; then 231 | to="${DIST_DIR}/${tini}-${ARCH_SUFFIX}" 232 | TINIS+=("$to") 233 | cp "${BUILD_DIR}/${tini}" "$to" 234 | else 235 | to="${DIST_DIR}/${tini}" 236 | TINIS+=("$to") 237 | cp "${BUILD_DIR}/${tini}" "$to" 238 | fi 239 | done 240 | 241 | if [[ -n "${ARCH_NATIVE-}" ]]; then 242 | for pkg_format in deb rpm; do 243 | src="${BUILD_DIR}/tini_${pkg_version}.${pkg_format}" 244 | 245 | if [[ -n "${ARCH_SUFFIX-}" ]]; then 246 | to="${DIST_DIR}/tini_${pkg_version}-${ARCH_SUFFIX}.${pkg_format}" 247 | TINIS+=("$to") 248 | cp "$src" "$to" 249 | else 250 | to="${DIST_DIR}/tini_${pkg_version}.${pkg_format}" 251 | TINIS+=("$to") 252 | cp "$src" "$to" 253 | fi 254 | done 255 | fi 256 | 257 | echo "Tinis: ${TINIS[*]}" 258 | 259 | for tini in "${TINIS[@]}"; do 260 | echo "${tini}:" 261 | sha1sum "$tini" 262 | sha256sum "$tini" 263 | file "$tini" 264 | echo "--" 265 | done 266 | 267 | # If a signing key and passphrase are made available, then use it to sign the 268 | # binaries 269 | if [[ -n "$GPG_PASSPHRASE" ]] && [[ -f "${SOURCE_DIR}/sign.key" ]]; then 270 | echo "Signing tinis" 271 | GPG_SIGN_HOMEDIR="${BUILD_DIR}/gpg-sign" 272 | GPG_VERIFY_HOMEDIR="${BUILD_DIR}/gpg-verify" 273 | PGP_KEY_FINGERPRINT="595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7" 274 | PGP_KEYSERVER="ha.pool.sks-keyservers.net" 275 | 276 | mkdir "${GPG_SIGN_HOMEDIR}" "${GPG_VERIFY_HOMEDIR}" 277 | chmod 700 "${GPG_SIGN_HOMEDIR}" "${GPG_VERIFY_HOMEDIR}" 278 | 279 | gpg --homedir "${GPG_SIGN_HOMEDIR}" --import "${SOURCE_DIR}/sign.key" 280 | gpg --homedir "${GPG_VERIFY_HOMEDIR}" --keyserver "$PGP_KEYSERVER" --recv-keys "$PGP_KEY_FINGERPRINT" 281 | 282 | for tini in "${TINIS[@]}"; do 283 | echo "${GPG_PASSPHRASE}" | gpg --homedir "${GPG_SIGN_HOMEDIR}" --passphrase-fd 0 --armor --detach-sign "${tini}" 284 | gpg --homedir "${GPG_VERIFY_HOMEDIR}" --verify "${tini}.asc" 285 | done 286 | fi 287 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | Tini - A tiny but valid `init` for containers 12 | ============================================= 13 | 14 | [![Build Status](https://travis-ci.org/krallin/tini.svg?branch=master)](https://travis-ci.org/krallin/tini) 15 | 16 | Tini is the simplest `init` you could think of. 17 | 18 | All Tini does is spawn a single child (Tini is meant to be run in a container), 19 | and wait for it to exit all the while reaping zombies and performing 20 | signal forwarding. 21 | 22 | 23 | Why Tini? 24 | --------- 25 | 26 | Using Tini has several benefits: 27 | 28 | - It protects you from software that accidentally creates zombie processes, 29 | which can (over time!) starve your entire system for PIDs (and make it 30 | unusable). 31 | - It ensures that the *default signal handlers* work for the software you run 32 | in your Docker image. For example, with Tini, `SIGTERM` properly terminates 33 | your process even if you didn't explicitly install a signal handler for it. 34 | - It does so completely transparently! Docker images that work without Tini 35 | will work with Tini without any changes. 36 | 37 | If you'd like more detail on why this is useful, review this issue discussion: 38 | [What is advantage of Tini?][0]. 39 | 40 | 41 | Using Tini 42 | ---------- 43 | 44 | *NOTE: If you are using Docker 1.13 or greater, Tini is included in Docker 45 | itself. This includes all versions of Docker CE. To enable Tini, just [pass the 46 | `--init` flag to `docker run`][5].* 47 | 48 | *NOTE: There are [pre-built Docker images available for Tini][10]. If 49 | you're currently using an Ubuntu or CentOS image as your base, you can use 50 | one of those as a drop-in replacement.* 51 | 52 | *NOTE: There are Tini packages for Alpine Linux and NixOS. See below for 53 | installation instructions.* 54 | 55 | Add Tini to your container, and make it executable. Then, just invoke Tini 56 | and pass your program and its arguments as arguments to Tini. 57 | 58 | In Docker, you will want to use an entrypoint so you don't have to remember 59 | to manually invoke Tini: 60 | 61 | # Add Tini 62 | ENV TINI_VERSION v0.18.0 63 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 64 | RUN chmod +x /tini 65 | ENTRYPOINT ["/tini", "--"] 66 | 67 | # Run your program under Tini 68 | CMD ["/your/program", "-and", "-its", "arguments"] 69 | # or docker run your-image /your/program ... 70 | 71 | Note that you *can* skip the `--` under certain conditions, but you might 72 | as well always include it to be safe. If you see an error message that 73 | looks like `tini: invalid option -- 'c'`, then you *need* to add the `--`. 74 | 75 | Arguments for Tini itself should be passed like `-v` in the following example: 76 | `/tini -v -- /your/program`. 77 | 78 | *NOTE: The binary linked above is a 64-bit dynamically-linked binary.* 79 | 80 | 81 | ### Signed binaries ### 82 | 83 | The `tini` and `tini-static` binaries are signed using the key `595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7`. 84 | 85 | You can verify their signatures using `gpg` (which you may install using 86 | your package manager): 87 | 88 | ENV TINI_VERSION v0.18.0 89 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 90 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini.asc /tini.asc 91 | RUN gpg --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \ 92 | && gpg --verify /tini.asc 93 | 94 | 95 | ### Alpine Linux Package ### 96 | 97 | On Alpine Linux, you can use the following command to install Tini: 98 | 99 | RUN apk add --no-cache tini 100 | # Tini is now available at /sbin/tini 101 | ENTRYPOINT ["/sbin/tini", "--"] 102 | 103 | 104 | ### NixOS ### 105 | 106 | Using Nix, you can use the following command to install Tini: 107 | 108 | nix-env --install tini 109 | 110 | ### Other Platforms ### 111 | 112 | ARM and 32-bit binaries are available! You can find the complete list of 113 | available binaries under [the releases tab][11]. 114 | 115 | 116 | Options 117 | ------- 118 | 119 | ### Verbosity ### 120 | 121 | The `-v` argument can be used for extra verbose output (you can pass it up to 122 | 3 times, e.g. `-vvv`). 123 | 124 | 125 | ### Subreaping ### 126 | 127 | By default, Tini needs to run as PID 1 so that it can reap zombies (by 128 | running as PID 1, zombies get re-parented to Tini). 129 | 130 | If for some reason, you cannot run Tini as PID 1, you should register Tini as 131 | a process subreaper instead (only in Linux >= 3.4), by either: 132 | 133 | + Passing the `-s` argument to Tini (`tini -s -- ...`) 134 | + Setting the environment variable `TINI_SUBREAPER` 135 | (e.g. `export TINI_SUBREAPER=`). 136 | 137 | This will ensure that zombies get re-parented to Tini despite Tini not running 138 | as PID 1. 139 | 140 | *NOTE: Tini will issue a warning if it detects that it isn't running as PID 1 141 | and isn't registered as a subreaper. If you don't see a warning, you're fine.* 142 | 143 | 144 | ### Remapping exit codes ### 145 | 146 | Tini will reuse the child's exit code when exiting, but occasionally, this may 147 | not be exactly what you want (e.g. if your child exits with 143 after receiving 148 | SIGTERM). Notably, this can be an issue with Java apps. 149 | 150 | In this case, you can use the `-e` flag to remap an arbitrary exit code to 0. 151 | You can pass the flag multiple times if needed. 152 | 153 | For example: 154 | 155 | ``` 156 | tini -e 143 -- ... 157 | ``` 158 | 159 | 160 | ### Process group killing ### 161 | 162 | By default, Tini only kills its immediate child process. This can be 163 | inconvenient if sending a signal to that process does not have the desired 164 | effect. For example, if you do 165 | 166 | docker run krallin/ubuntu-tini sh -c 'sleep 10' 167 | 168 | and ctrl-C it, nothing happens: SIGINT is sent to the 'sh' process, 169 | but that shell won't react to it while it is waiting for the 'sleep' 170 | to finish. 171 | 172 | With the `-g` option, Tini kills the child process group , so that 173 | every process in the group gets the signal. This corresponds more 174 | closely to what happens when you do ctrl-C etc. in a terminal: The 175 | signal is sent to the foreground process group. 176 | 177 | 178 | ### Parent Death Signal ### 179 | 180 | Tini can set its parent death signal, which is the signal Tini should receive 181 | when *its* parent exits. To set the parent death signal, use the `-p` flag with 182 | the name of the signal Tini should receive when its parent exits: 183 | 184 | ``` 185 | tini -p SIGTERM -- ... 186 | ``` 187 | 188 | *NOTE: See [this PR discussion][12] to learn more about the parent death signal 189 | and use cases.* 190 | 191 | 192 | More 193 | ---- 194 | 195 | ### Existing Entrypoint ### 196 | 197 | Tini can also be used with an existing entrypoint in your container! 198 | 199 | Assuming your entrypoint was `/docker-entrypoint.sh`, then you would use: 200 | 201 | ENTRYPOINT ["/tini", "--", "/docker-entrypoint.sh"] 202 | 203 | 204 | ### Statically-Linked Version ### 205 | 206 | Tini has very few dependencies (it only depends on libc), but if your 207 | container fails to start, you might want to consider using the statically-built 208 | version instead: 209 | 210 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /tini 211 | 212 | 213 | ### Size Considerations ### 214 | 215 | Tini is a very small file (in the 10KB range), so it doesn't add much weight 216 | to your container. 217 | 218 | The statically-linked version is bigger, but still < 1M. 219 | 220 | 221 | Building Tini 222 | ------------- 223 | 224 | If you'd rather not download the binary, you can build Tini by running 225 | `cmake . && make`. 226 | 227 | Before building, you probably also want to run: 228 | 229 | export CFLAGS="-DPR_SET_CHILD_SUBREAPER=36 -DPR_GET_CHILD_SUBREAPER=37" 230 | 231 | This ensure that even if you're building on a system that has old Linux Kernel 232 | headers (< 3.4), Tini will be built with child subreaper support. This is 233 | usually what you want if you're going to use Tini with Docker (if your host 234 | Kernel supports Docker, it should also support child subreapers). 235 | 236 | 237 | Understanding Tini 238 | ------------------ 239 | 240 | After spawning your process, Tini will wait for signals and forward those 241 | to the child process, and periodically reap zombie processes that may be 242 | created within your container. 243 | 244 | When the "first" child process exits (`/your/program` in the examples above), 245 | Tini exits as well, with the exit code of the child process (so you can 246 | check your container's exit code to know whether the child exited 247 | successfully). 248 | 249 | 250 | Debugging 251 | --------- 252 | 253 | If something isn't working just like you expect, consider increasing the 254 | verbosity level (up to 3): 255 | 256 | tini -v -- bash -c 'exit 1' 257 | tini -vv -- true 258 | tini -vvv -- pwd 259 | 260 | 261 | Authors 262 | ======= 263 | 264 | Maintainer: 265 | 266 | + [Thomas Orozco][20] 267 | 268 | Contributors: 269 | 270 | + [Tianon Gravi][30] 271 | + [David Wragg][31] 272 | + [Michael Crosby][32] 273 | + [Wyatt Preul][33] 274 | + [Patrick Steinhardt][34] 275 | 276 | Special thanks to: 277 | 278 | + [Danilo Bürger][40] for packaging Tini for Alpine 279 | + [Asko Soukka][41] for packaging Tini for Nix 280 | 281 | 282 | [0]: https://github.com/krallin/tini/issues/8 283 | [5]: https://docs.docker.com/engine/reference/commandline/run/ 284 | [10]: https://github.com/krallin/tini-images 285 | [11]: https://github.com/krallin/tini/releases 286 | [12]: https://github.com/krallin/tini/pull/114 287 | [20]: https://github.com/krallin/ 288 | [30]: https://github.com/tianon 289 | [31]: https://github.com/dpw 290 | [32]: https://github.com/crosbymichael 291 | [33]: https://github.com/geek 292 | [34]: https://github.com/pks-t 293 | [40]: https://github.com/danilobuerger 294 | [41]: https://github.com/datakurre 295 | -------------------------------------------------------------------------------- /tpl/README.md.in: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | Tini - A tiny but valid `init` for containers 12 | ============================================= 13 | 14 | [![Build Status](https://travis-ci.org/krallin/tini.svg?branch=master)](https://travis-ci.org/krallin/tini) 15 | 16 | Tini is the simplest `init` you could think of. 17 | 18 | All Tini does is spawn a single child (Tini is meant to be run in a container), 19 | and wait for it to exit all the while reaping zombies and performing 20 | signal forwarding. 21 | 22 | 23 | Why Tini? 24 | --------- 25 | 26 | Using Tini has several benefits: 27 | 28 | - It protects you from software that accidentally creates zombie processes, 29 | which can (over time!) starve your entire system for PIDs (and make it 30 | unusable). 31 | - It ensures that the *default signal handlers* work for the software you run 32 | in your Docker image. For example, with Tini, `SIGTERM` properly terminates 33 | your process even if you didn't explicitly install a signal handler for it. 34 | - It does so completely transparently! Docker images that work without Tini 35 | will work with Tini without any changes. 36 | 37 | If you'd like more detail on why this is useful, review this issue discussion: 38 | [What is advantage of Tini?][0]. 39 | 40 | 41 | Using Tini 42 | ---------- 43 | 44 | *NOTE: If you are using Docker 1.13 or greater, Tini is included in Docker 45 | itself. This includes all versions of Docker CE. To enable Tini, just [pass the 46 | `--init` flag to `docker run`][5].* 47 | 48 | *NOTE: There are [pre-built Docker images available for Tini][10]. If 49 | you're currently using an Ubuntu or CentOS image as your base, you can use 50 | one of those as a drop-in replacement.* 51 | 52 | *NOTE: There are Tini packages for Alpine Linux and NixOS. See below for 53 | installation instructions.* 54 | 55 | Add Tini to your container, and make it executable. Then, just invoke Tini 56 | and pass your program and its arguments as arguments to Tini. 57 | 58 | In Docker, you will want to use an entrypoint so you don't have to remember 59 | to manually invoke Tini: 60 | 61 | # Add Tini 62 | ENV TINI_VERSION v@tini_VERSION_MAJOR@.@tini_VERSION_MINOR@.@tini_VERSION_PATCH@ 63 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 64 | RUN chmod +x /tini 65 | ENTRYPOINT ["/tini", "--"] 66 | 67 | # Run your program under Tini 68 | CMD ["/your/program", "-and", "-its", "arguments"] 69 | # or docker run your-image /your/program ... 70 | 71 | Note that you *can* skip the `--` under certain conditions, but you might 72 | as well always include it to be safe. If you see an error message that 73 | looks like `tini: invalid option -- 'c'`, then you *need* to add the `--`. 74 | 75 | Arguments for Tini itself should be passed like `-v` in the following example: 76 | `/tini -v -- /your/program`. 77 | 78 | *NOTE: The binary linked above is a 64-bit dynamically-linked binary.* 79 | 80 | 81 | ### Signed binaries ### 82 | 83 | The `tini` and `tini-static` binaries are signed using the key `595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7`. 84 | 85 | You can verify their signatures using `gpg` (which you may install using 86 | your package manager): 87 | 88 | ENV TINI_VERSION v@tini_VERSION_MAJOR@.@tini_VERSION_MINOR@.@tini_VERSION_PATCH@ 89 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 90 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini.asc /tini.asc 91 | RUN gpg --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \ 92 | && gpg --verify /tini.asc 93 | 94 | 95 | ### Alpine Linux Package ### 96 | 97 | On Alpine Linux, you can use the following command to install Tini: 98 | 99 | RUN apk add --no-cache tini 100 | # Tini is now available at /sbin/tini 101 | ENTRYPOINT ["/sbin/tini", "--"] 102 | 103 | 104 | ### NixOS ### 105 | 106 | Using Nix, you can use the following command to install Tini: 107 | 108 | nix-env --install tini 109 | 110 | ### Other Platforms ### 111 | 112 | ARM and 32-bit binaries are available! You can find the complete list of 113 | available binaries under [the releases tab][11]. 114 | 115 | 116 | Options 117 | ------- 118 | 119 | ### Verbosity ### 120 | 121 | The `-v` argument can be used for extra verbose output (you can pass it up to 122 | 3 times, e.g. `-vvv`). 123 | 124 | 125 | ### Subreaping ### 126 | 127 | By default, Tini needs to run as PID 1 so that it can reap zombies (by 128 | running as PID 1, zombies get re-parented to Tini). 129 | 130 | If for some reason, you cannot run Tini as PID 1, you should register Tini as 131 | a process subreaper instead (only in Linux >= 3.4), by either: 132 | 133 | + Passing the `-s` argument to Tini (`tini -s -- ...`) 134 | + Setting the environment variable `TINI_SUBREAPER` 135 | (e.g. `export TINI_SUBREAPER=`). 136 | 137 | This will ensure that zombies get re-parented to Tini despite Tini not running 138 | as PID 1. 139 | 140 | *NOTE: Tini will issue a warning if it detects that it isn't running as PID 1 141 | and isn't registered as a subreaper. If you don't see a warning, you're fine.* 142 | 143 | 144 | ### Remapping exit codes ### 145 | 146 | Tini will reuse the child's exit code when exiting, but occasionally, this may 147 | not be exactly what you want (e.g. if your child exits with 143 after receiving 148 | SIGTERM). Notably, this can be an issue with Java apps. 149 | 150 | In this case, you can use the `-e` flag to remap an arbitrary exit code to 0. 151 | You can pass the flag multiple times if needed. 152 | 153 | For example: 154 | 155 | ``` 156 | tini -e 143 -- ... 157 | ``` 158 | 159 | 160 | ### Process group killing ### 161 | 162 | By default, Tini only kills its immediate child process. This can be 163 | inconvenient if sending a signal to that process does not have the desired 164 | effect. For example, if you do 165 | 166 | docker run krallin/ubuntu-tini sh -c 'sleep 10' 167 | 168 | and ctrl-C it, nothing happens: SIGINT is sent to the 'sh' process, 169 | but that shell won't react to it while it is waiting for the 'sleep' 170 | to finish. 171 | 172 | With the `-g` option, Tini kills the child process group , so that 173 | every process in the group gets the signal. This corresponds more 174 | closely to what happens when you do ctrl-C etc. in a terminal: The 175 | signal is sent to the foreground process group. 176 | 177 | 178 | ### Parent Death Signal ### 179 | 180 | Tini can set its parent death signal, which is the signal Tini should receive 181 | when *its* parent exits. To set the parent death signal, use the `-p` flag with 182 | the name of the signal Tini should receive when its parent exits: 183 | 184 | ``` 185 | tini -p SIGTERM -- ... 186 | ``` 187 | 188 | *NOTE: See [this PR discussion][12] to learn more about the parent death signal 189 | and use cases.* 190 | 191 | 192 | More 193 | ---- 194 | 195 | ### Existing Entrypoint ### 196 | 197 | Tini can also be used with an existing entrypoint in your container! 198 | 199 | Assuming your entrypoint was `/docker-entrypoint.sh`, then you would use: 200 | 201 | ENTRYPOINT ["/tini", "--", "/docker-entrypoint.sh"] 202 | 203 | 204 | ### Statically-Linked Version ### 205 | 206 | Tini has very few dependencies (it only depends on libc), but if your 207 | container fails to start, you might want to consider using the statically-built 208 | version instead: 209 | 210 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /tini 211 | 212 | 213 | ### Size Considerations ### 214 | 215 | Tini is a very small file (in the 10KB range), so it doesn't add much weight 216 | to your container. 217 | 218 | The statically-linked version is bigger, but still < 1M. 219 | 220 | 221 | Building Tini 222 | ------------- 223 | 224 | If you'd rather not download the binary, you can build Tini by running 225 | `cmake . && make`. 226 | 227 | Before building, you probably also want to run: 228 | 229 | export CFLAGS="-DPR_SET_CHILD_SUBREAPER=36 -DPR_GET_CHILD_SUBREAPER=37" 230 | 231 | This ensure that even if you're building on a system that has old Linux Kernel 232 | headers (< 3.4), Tini will be built with child subreaper support. This is 233 | usually what you want if you're going to use Tini with Docker (if your host 234 | Kernel supports Docker, it should also support child subreapers). 235 | 236 | 237 | Understanding Tini 238 | ------------------ 239 | 240 | After spawning your process, Tini will wait for signals and forward those 241 | to the child process, and periodically reap zombie processes that may be 242 | created within your container. 243 | 244 | When the "first" child process exits (`/your/program` in the examples above), 245 | Tini exits as well, with the exit code of the child process (so you can 246 | check your container's exit code to know whether the child exited 247 | successfully). 248 | 249 | 250 | Debugging 251 | --------- 252 | 253 | If something isn't working just like you expect, consider increasing the 254 | verbosity level (up to 3): 255 | 256 | tini -v -- bash -c 'exit 1' 257 | tini -vv -- true 258 | tini -vvv -- pwd 259 | 260 | 261 | Authors 262 | ======= 263 | 264 | Maintainer: 265 | 266 | + [Thomas Orozco][20] 267 | 268 | Contributors: 269 | 270 | + [Tianon Gravi][30] 271 | + [David Wragg][31] 272 | + [Michael Crosby][32] 273 | + [Wyatt Preul][33] 274 | + [Patrick Steinhardt][34] 275 | 276 | Special thanks to: 277 | 278 | + [Danilo Bürger][40] for packaging Tini for Alpine 279 | + [Asko Soukka][41] for packaging Tini for Nix 280 | 281 | 282 | [0]: https://github.com/krallin/tini/issues/8 283 | [5]: https://docs.docker.com/engine/reference/commandline/run/ 284 | [10]: https://github.com/krallin/tini-images 285 | [11]: https://github.com/krallin/tini/releases 286 | [12]: https://github.com/krallin/tini/pull/114 287 | [20]: https://github.com/krallin/ 288 | [30]: https://github.com/tianon 289 | [31]: https://github.com/dpw 290 | [32]: https://github.com/crosbymichael 291 | [33]: https://github.com/geek 292 | [34]: https://github.com/pks-t 293 | [40]: https://github.com/danilobuerger 294 | [41]: https://github.com/datakurre 295 | -------------------------------------------------------------------------------- /src/tini.c: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for copyright and license details. */ 2 | #define _GNU_SOURCE 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "tiniConfig.h" 19 | #include "tiniLicense.h" 20 | 21 | #if TINI_MINIMAL 22 | #define PRINT_FATAL(...) fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); 23 | #define PRINT_WARNING(...) if (verbosity > 0) { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } 24 | #define PRINT_INFO(...) if (verbosity > 1) { fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } 25 | #define PRINT_DEBUG(...) if (verbosity > 2) { fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } 26 | #define PRINT_TRACE(...) if (verbosity > 3) { fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } 27 | #define DEFAULT_VERBOSITY 0 28 | #else 29 | #define PRINT_FATAL(...) fprintf(stderr, "[FATAL tini (%i)] ", getpid()); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); 30 | #define PRINT_WARNING(...) if (verbosity > 0) { fprintf(stderr, "[WARN tini (%i)] ", getpid()); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } 31 | #define PRINT_INFO(...) if (verbosity > 1) { fprintf(stdout, "[INFO tini (%i)] ", getpid()); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } 32 | #define PRINT_DEBUG(...) if (verbosity > 2) { fprintf(stdout, "[DEBUG tini (%i)] ", getpid()); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } 33 | #define PRINT_TRACE(...) if (verbosity > 3) { fprintf(stdout, "[TRACE tini (%i)] ", getpid()); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } 34 | #define DEFAULT_VERBOSITY 1 35 | #endif 36 | 37 | #define ARRAY_LEN(x) (sizeof(x) / sizeof((x)[0])) 38 | 39 | #define INT32_BITFIELD_SET(F, i) ( F[(i / 32)] |= (1 << (i % 32)) ) 40 | #define INT32_BITFIELD_CLEAR(F, i) ( F[(i / 32)] &= ~(1 << (i % 32)) ) 41 | #define INT32_BITFIELD_TEST(F, i) ( F[(i / 32)] & (1 << (i % 32)) ) 42 | #define INT32_BITFIELD_CHECK_BOUNDS(F, i) do { assert(i >= 0); assert(ARRAY_LEN(F) > (uint) (i / 32)); } while(0) 43 | 44 | #define STATUS_MAX 255 45 | #define STATUS_MIN 0 46 | 47 | typedef struct { 48 | sigset_t* const sigmask_ptr; 49 | struct sigaction* const sigttin_action_ptr; 50 | struct sigaction* const sigttou_action_ptr; 51 | } signal_configuration_t; 52 | 53 | static const struct { 54 | char *const name; 55 | int number; 56 | } signal_names[] = { 57 | { "SIGHUP", SIGHUP }, 58 | { "SIGINT", SIGINT }, 59 | { "SIGQUIT", SIGQUIT }, 60 | { "SIGILL", SIGILL }, 61 | { "SIGTRAP", SIGTRAP }, 62 | { "SIGABRT", SIGABRT }, 63 | { "SIGBUS", SIGBUS }, 64 | { "SIGFPE", SIGFPE }, 65 | { "SIGKILL", SIGKILL }, 66 | { "SIGUSR1", SIGUSR1 }, 67 | { "SIGSEGV", SIGSEGV }, 68 | { "SIGUSR2", SIGUSR2 }, 69 | { "SIGPIPE", SIGPIPE }, 70 | { "SIGALRM", SIGALRM }, 71 | { "SIGTERM", SIGTERM }, 72 | { "SIGCHLD", SIGCHLD }, 73 | { "SIGCONT", SIGCONT }, 74 | { "SIGSTOP", SIGSTOP }, 75 | { "SIGTSTP", SIGTSTP }, 76 | { "SIGTTIN", SIGTTIN }, 77 | { "SIGTTOU", SIGTTOU }, 78 | { "SIGURG", SIGURG }, 79 | { "SIGXCPU", SIGXCPU }, 80 | { "SIGXFSZ", SIGXFSZ }, 81 | { "SIGVTALRM", SIGVTALRM }, 82 | { "SIGPROF", SIGPROF }, 83 | { "SIGWINCH", SIGWINCH }, 84 | { "SIGSYS", SIGSYS }, 85 | }; 86 | 87 | static unsigned int verbosity = DEFAULT_VERBOSITY; 88 | 89 | static int32_t expect_status[(STATUS_MAX - STATUS_MIN + 1) / 32]; 90 | 91 | #ifdef PR_SET_CHILD_SUBREAPER 92 | #define HAS_SUBREAPER 1 93 | #define OPT_STRING "p:hvwgle:s" 94 | #define SUBREAPER_ENV_VAR "TINI_SUBREAPER" 95 | #else 96 | #define HAS_SUBREAPER 0 97 | #define OPT_STRING "p:hvwgle:" 98 | #endif 99 | 100 | #define VERBOSITY_ENV_VAR "TINI_VERBOSITY" 101 | #define KILL_PROCESS_GROUP_GROUP_ENV_VAR "TINI_KILL_PROCESS_GROUP" 102 | 103 | #define TINI_VERSION_STRING "tini version " TINI_VERSION TINI_GIT 104 | 105 | 106 | #if HAS_SUBREAPER 107 | static unsigned int subreaper = 0; 108 | #endif 109 | static unsigned int parent_death_signal = 0; 110 | static unsigned int kill_process_group = 0; 111 | 112 | static unsigned int warn_on_reap = 0; 113 | 114 | static struct timespec ts = { .tv_sec = 1, .tv_nsec = 0 }; 115 | 116 | static const char reaper_warning[] = "Tini is not running as PID 1 " 117 | #if HAS_SUBREAPER 118 | "and isn't registered as a child subreaper" 119 | #endif 120 | ".\n\ 121 | Zombie processes will not be re-parented to Tini, so zombie reaping won't work.\n\ 122 | To fix the problem, " 123 | #if HAS_SUBREAPER 124 | #ifndef TINI_MINIMAL 125 | "use the -s option " 126 | #endif 127 | "or set the environment variable " SUBREAPER_ENV_VAR " to register Tini as a child subreaper, or " 128 | #endif 129 | "run Tini as PID 1."; 130 | 131 | int restore_signals(const signal_configuration_t* const sigconf_ptr) { 132 | if (sigprocmask(SIG_SETMASK, sigconf_ptr->sigmask_ptr, NULL)) { 133 | PRINT_FATAL("Restoring child signal mask failed: '%s'", strerror(errno)); 134 | return 1; 135 | } 136 | 137 | if (sigaction(SIGTTIN, sigconf_ptr->sigttin_action_ptr, NULL)) { 138 | PRINT_FATAL("Restoring SIGTTIN handler failed: '%s'", strerror((errno))); 139 | return 1; 140 | } 141 | 142 | if (sigaction(SIGTTOU, sigconf_ptr->sigttou_action_ptr, NULL)) { 143 | PRINT_FATAL("Restoring SIGTTOU handler failed: '%s'", strerror((errno))); 144 | return 1; 145 | } 146 | 147 | return 0; 148 | } 149 | 150 | int isolate_child() { 151 | // Put the child into a new process group. 152 | if (setpgid(0, 0) < 0) { 153 | PRINT_FATAL("setpgid failed: %s", strerror(errno)); 154 | return 1; 155 | } 156 | 157 | // If there is a tty, allocate it to this new process group. We 158 | // can do this in the child process because we're blocking 159 | // SIGTTIN / SIGTTOU. 160 | 161 | // Doing it in the child process avoids a race condition scenario 162 | // if Tini is calling Tini (in which case the grandparent may make the 163 | // parent the foreground process group, and the actual child ends up... 164 | // in the background!) 165 | if (tcsetpgrp(STDIN_FILENO, getpgrp())) { 166 | if (errno == ENOTTY) { 167 | PRINT_DEBUG("tcsetpgrp failed: no tty (ok to proceed)"); 168 | } else if (errno == ENXIO) { 169 | // can occur on lx-branded zones 170 | PRINT_DEBUG("tcsetpgrp failed: no such device (ok to proceed)"); 171 | } else { 172 | PRINT_FATAL("tcsetpgrp failed: %s", strerror(errno)); 173 | return 1; 174 | } 175 | } 176 | 177 | return 0; 178 | } 179 | 180 | 181 | int spawn(const signal_configuration_t* const sigconf_ptr, char* const argv[], int* const child_pid_ptr) { 182 | pid_t pid; 183 | 184 | // TODO: check if tini was a foreground process to begin with (it's not OK to "steal" the foreground!") 185 | 186 | pid = fork(); 187 | if (pid < 0) { 188 | PRINT_FATAL("fork failed: %s", strerror(errno)); 189 | return 1; 190 | } else if (pid == 0) { 191 | 192 | // Put the child in a process group and make it the foreground process if there is a tty. 193 | if (isolate_child()) { 194 | return 1; 195 | } 196 | 197 | // Restore all signal handlers to the way they were before we touched them. 198 | if (restore_signals(sigconf_ptr)) { 199 | return 1; 200 | } 201 | 202 | execvp(argv[0], argv); 203 | 204 | // execvp will only return on an error so make sure that we check the errno 205 | // and exit with the correct return status for the error that we encountered 206 | // See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF 207 | int status = 1; 208 | switch (errno) { 209 | case ENOENT: 210 | status = 127; 211 | break; 212 | case EACCES: 213 | status = 126; 214 | break; 215 | } 216 | PRINT_FATAL("exec %s failed: %s", argv[0], strerror(errno)); 217 | return status; 218 | } else { 219 | // Parent 220 | PRINT_INFO("Spawned child process '%s' with pid '%i'", argv[0], pid); 221 | *child_pid_ptr = pid; 222 | return 0; 223 | } 224 | } 225 | 226 | void print_usage(char* const name, FILE* const file) { 227 | fprintf(file, "%s (%s)\n", basename(name), TINI_VERSION_STRING); 228 | 229 | #if TINI_MINIMAL 230 | fprintf(file, "Usage: %s PROGRAM [ARGS] | --version\n\n", basename(name)); 231 | #else 232 | fprintf(file, "Usage: %s [OPTIONS] PROGRAM -- [ARGS] | --version\n\n", basename(name)); 233 | #endif 234 | fprintf(file, "Execute a program under the supervision of a valid init process (%s)\n\n", basename(name)); 235 | 236 | fprintf(file, "Command line options:\n\n"); 237 | 238 | fprintf(file, " --version: Show version and exit.\n"); 239 | 240 | #if TINI_MINIMAL 241 | #else 242 | fprintf(file, " -h: Show this help message and exit.\n"); 243 | #if HAS_SUBREAPER 244 | fprintf(file, " -s: Register as a process subreaper (requires Linux >= 3.4).\n"); 245 | #endif 246 | fprintf(file, " -p SIGNAL: Trigger SIGNAL when parent dies, e.g. \"-p SIGKILL\".\n"); 247 | fprintf(file, " -v: Generate more verbose output. Repeat up to 3 times.\n"); 248 | fprintf(file, " -w: Print a warning when processes are getting reaped.\n"); 249 | fprintf(file, " -g: Send signals to the child's process group.\n"); 250 | fprintf(file, " -e EXIT_CODE: Remap EXIT_CODE (from 0 to 255) to 0.\n"); 251 | fprintf(file, " -l: Show license and exit.\n"); 252 | #endif 253 | 254 | fprintf(file, "\n"); 255 | 256 | fprintf(file, "Environment variables:\n\n"); 257 | #if HAS_SUBREAPER 258 | fprintf(file, " %s: Register as a process subreaper (requires Linux >= 3.4).\n", SUBREAPER_ENV_VAR); 259 | #endif 260 | fprintf(file, " %s: Set the verbosity level (default: %d).\n", VERBOSITY_ENV_VAR, DEFAULT_VERBOSITY); 261 | fprintf(file, " %s: Send signals to the child's process group.\n", KILL_PROCESS_GROUP_GROUP_ENV_VAR); 262 | 263 | fprintf(file, "\n"); 264 | } 265 | 266 | void print_license(FILE* const file) { 267 | if(LICENSE_len > fwrite(LICENSE, sizeof(char), LICENSE_len, file)) { 268 | // Don't handle this error for now, since parse_args won't care 269 | // about the return value. We do need to check it to compile with 270 | // older glibc, though. 271 | // See: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=25509 272 | // See: http://sourceware.org/bugzilla/show_bug.cgi?id=11959 273 | } 274 | } 275 | 276 | int set_pdeathsig(char* const arg) { 277 | size_t i; 278 | 279 | for (i = 0; i < ARRAY_LEN(signal_names); i++) { 280 | if (strcmp(signal_names[i].name, arg) == 0) { 281 | /* Signals start at value "1" */ 282 | parent_death_signal = signal_names[i].number; 283 | return 0; 284 | } 285 | } 286 | 287 | return 1; 288 | } 289 | 290 | int add_expect_status(char* arg) { 291 | long status = 0; 292 | char* endptr = NULL; 293 | status = strtol(arg, &endptr, 10); 294 | 295 | if ((endptr == NULL) || (*endptr != 0)) { 296 | return 1; 297 | } 298 | 299 | if ((status < STATUS_MIN) || (status > STATUS_MAX)) { 300 | return 1; 301 | } 302 | 303 | INT32_BITFIELD_CHECK_BOUNDS(expect_status, status); 304 | INT32_BITFIELD_SET(expect_status, status); 305 | return 0; 306 | } 307 | 308 | int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[], int* const parse_fail_exitcode_ptr) { 309 | char* name = argv[0]; 310 | 311 | // We handle --version if it's the *only* argument provided. 312 | if (argc == 2 && strcmp("--version", argv[1]) == 0) { 313 | *parse_fail_exitcode_ptr = 0; 314 | fprintf(stdout, "%s\n", TINI_VERSION_STRING); 315 | return 1; 316 | } 317 | 318 | #ifndef TINI_MINIMAL 319 | int c; 320 | while ((c = getopt(argc, argv, OPT_STRING)) != -1) { 321 | switch (c) { 322 | case 'h': 323 | print_usage(name, stdout); 324 | *parse_fail_exitcode_ptr = 0; 325 | return 1; 326 | #if HAS_SUBREAPER 327 | case 's': 328 | subreaper++; 329 | break; 330 | #endif 331 | case 'p': 332 | if (set_pdeathsig(optarg)) { 333 | PRINT_FATAL("Not a valid option for -p: %s", optarg); 334 | *parse_fail_exitcode_ptr = 1; 335 | return 1; 336 | } 337 | break; 338 | 339 | case 'v': 340 | verbosity++; 341 | break; 342 | 343 | case 'w': 344 | warn_on_reap++; 345 | break; 346 | 347 | case 'g': 348 | kill_process_group++; 349 | break; 350 | 351 | case 'e': 352 | if (add_expect_status(optarg)) { 353 | PRINT_FATAL("Not a valid option for -e: %s", optarg); 354 | *parse_fail_exitcode_ptr = 1; 355 | return 1; 356 | } 357 | break; 358 | 359 | case 'l': 360 | print_license(stdout); 361 | *parse_fail_exitcode_ptr = 0; 362 | return 1; 363 | 364 | case '?': 365 | print_usage(name, stderr); 366 | return 1; 367 | default: 368 | /* Should never happen */ 369 | return 1; 370 | } 371 | } 372 | #endif 373 | 374 | *child_args_ptr_ptr = calloc(argc-optind+1, sizeof(char*)); 375 | if (*child_args_ptr_ptr == NULL) { 376 | PRINT_FATAL("Failed to allocate memory for child args: '%s'", strerror(errno)); 377 | return 1; 378 | } 379 | 380 | int i; 381 | for (i = 0; i < argc - optind; i++) { 382 | (**child_args_ptr_ptr)[i] = argv[optind+i]; 383 | } 384 | (**child_args_ptr_ptr)[i] = NULL; 385 | 386 | if (i == 0) { 387 | /* User forgot to provide args! */ 388 | print_usage(name, stderr); 389 | return 1; 390 | } 391 | 392 | return 0; 393 | } 394 | 395 | int parse_env() { 396 | #if HAS_SUBREAPER 397 | if (getenv(SUBREAPER_ENV_VAR) != NULL) { 398 | subreaper++; 399 | } 400 | #endif 401 | 402 | if (getenv(KILL_PROCESS_GROUP_GROUP_ENV_VAR) != NULL) { 403 | kill_process_group++; 404 | } 405 | 406 | char* env_verbosity = getenv(VERBOSITY_ENV_VAR); 407 | if (env_verbosity != NULL) { 408 | verbosity = atoi(env_verbosity); 409 | } 410 | 411 | return 0; 412 | } 413 | 414 | 415 | #if HAS_SUBREAPER 416 | int register_subreaper () { 417 | if (subreaper > 0) { 418 | if (prctl(PR_SET_CHILD_SUBREAPER, 1)) { 419 | if (errno == EINVAL) { 420 | PRINT_FATAL("PR_SET_CHILD_SUBREAPER is unavailable on this platform. Are you using Linux >= 3.4?") 421 | } else { 422 | PRINT_FATAL("Failed to register as child subreaper: %s", strerror(errno)) 423 | } 424 | return 1; 425 | } else { 426 | PRINT_TRACE("Registered as child subreaper"); 427 | } 428 | } 429 | return 0; 430 | } 431 | #endif 432 | 433 | 434 | void reaper_check () { 435 | /* Check that we can properly reap zombies */ 436 | #if HAS_SUBREAPER 437 | int bit = 0; 438 | #endif 439 | 440 | if (getpid() == 1) { 441 | return; 442 | } 443 | 444 | #if HAS_SUBREAPER 445 | if (prctl(PR_GET_CHILD_SUBREAPER, &bit)) { 446 | PRINT_DEBUG("Failed to read child subreaper attribute: %s", strerror(errno)); 447 | } else if (bit == 1) { 448 | return; 449 | } 450 | #endif 451 | 452 | PRINT_WARNING(reaper_warning); 453 | } 454 | 455 | 456 | int configure_signals(sigset_t* const parent_sigset_ptr, const signal_configuration_t* const sigconf_ptr) { 457 | /* Block all signals that are meant to be collected by the main loop */ 458 | if (sigfillset(parent_sigset_ptr)) { 459 | PRINT_FATAL("sigfillset failed: '%s'", strerror(errno)); 460 | return 1; 461 | } 462 | 463 | // These ones shouldn't be collected by the main loop 464 | uint i; 465 | int signals_for_tini[] = {SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS, SIGTTIN, SIGTTOU}; 466 | for (i = 0; i < ARRAY_LEN(signals_for_tini); i++) { 467 | if (sigdelset(parent_sigset_ptr, signals_for_tini[i])) { 468 | PRINT_FATAL("sigdelset failed: '%i'", signals_for_tini[i]); 469 | return 1; 470 | } 471 | } 472 | 473 | if (sigprocmask(SIG_SETMASK, parent_sigset_ptr, sigconf_ptr->sigmask_ptr)) { 474 | PRINT_FATAL("sigprocmask failed: '%s'", strerror(errno)); 475 | return 1; 476 | } 477 | 478 | // Handle SIGTTIN and SIGTTOU separately. Since Tini makes the child process group 479 | // the foreground process group, there's a chance Tini can end up not controlling the tty. 480 | // If TOSTOP is set on the tty, this could block Tini on writing debug messages. We don't 481 | // want that. Ignore those signals. 482 | struct sigaction ign_action; 483 | memset(&ign_action, 0, sizeof ign_action); 484 | 485 | ign_action.sa_handler = SIG_IGN; 486 | sigemptyset(&ign_action.sa_mask); 487 | 488 | if (sigaction(SIGTTIN, &ign_action, sigconf_ptr->sigttin_action_ptr)) { 489 | PRINT_FATAL("Failed to ignore SIGTTIN"); 490 | return 1; 491 | } 492 | 493 | if (sigaction(SIGTTOU, &ign_action, sigconf_ptr->sigttou_action_ptr)) { 494 | PRINT_FATAL("Failed to ignore SIGTTOU"); 495 | return 1; 496 | } 497 | 498 | return 0; 499 | } 500 | 501 | int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const child_pid) { 502 | siginfo_t sig; 503 | 504 | if (sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1) { 505 | switch (errno) { 506 | case EAGAIN: 507 | break; 508 | case EINTR: 509 | break; 510 | default: 511 | PRINT_FATAL("Unexpected error in sigtimedwait: '%s'", strerror(errno)); 512 | return 1; 513 | } 514 | } else { 515 | /* There is a signal to handle here */ 516 | switch (sig.si_signo) { 517 | case SIGCHLD: 518 | /* Special-cased, as we don't forward SIGCHLD. Instead, we'll 519 | * fallthrough to reaping processes. 520 | */ 521 | PRINT_DEBUG("Received SIGCHLD"); 522 | break; 523 | default: 524 | PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo)); 525 | /* Forward anything else */ 526 | if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo)) { 527 | if (errno == ESRCH) { 528 | PRINT_WARNING("Child was dead when forwarding signal"); 529 | } else { 530 | PRINT_FATAL("Unexpected error when forwarding signal: '%s'", strerror(errno)); 531 | return 1; 532 | } 533 | } 534 | break; 535 | } 536 | } 537 | 538 | return 0; 539 | } 540 | 541 | int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) { 542 | pid_t current_pid; 543 | int current_status; 544 | 545 | while (1) { 546 | current_pid = waitpid(-1, ¤t_status, WNOHANG); 547 | 548 | switch (current_pid) { 549 | 550 | case -1: 551 | if (errno == ECHILD) { 552 | PRINT_TRACE("No child to wait"); 553 | break; 554 | } 555 | PRINT_FATAL("Error while waiting for pids: '%s'", strerror(errno)); 556 | return 1; 557 | 558 | case 0: 559 | PRINT_TRACE("No child to reap"); 560 | break; 561 | 562 | default: 563 | /* A child was reaped. Check whether it's the main one. If it is, then 564 | * set the exit_code, which will cause us to exit once we've reaped everyone else. 565 | */ 566 | PRINT_DEBUG("Reaped child with pid: '%i'", current_pid); 567 | if (current_pid == child_pid) { 568 | if (WIFEXITED(current_status)) { 569 | /* Our process exited normally. */ 570 | PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status)); 571 | *child_exitcode_ptr = WEXITSTATUS(current_status); 572 | } else if (WIFSIGNALED(current_status)) { 573 | /* Our process was terminated. Emulate what sh / bash 574 | * would do, which is to return 128 + signal number. 575 | */ 576 | PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status))); 577 | *child_exitcode_ptr = 128 + WTERMSIG(current_status); 578 | } else { 579 | PRINT_FATAL("Main child exited for unknown reason"); 580 | return 1; 581 | } 582 | 583 | // Be safe, ensure the status code is indeed between 0 and 255. 584 | *child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1); 585 | 586 | // If this exitcode was remapped, then set it to 0. 587 | INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr); 588 | if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) { 589 | *child_exitcode_ptr = 0; 590 | } 591 | } else if (warn_on_reap > 0) { 592 | PRINT_WARNING("Reaped zombie process with pid=%i", current_pid); 593 | } 594 | 595 | // Check if other childs have been reaped. 596 | continue; 597 | } 598 | 599 | /* If we make it here, that's because we did not continue in the switch case. */ 600 | break; 601 | } 602 | 603 | return 0; 604 | } 605 | 606 | 607 | int main(int argc, char *argv[]) { 608 | pid_t child_pid; 609 | 610 | // Those are passed to functions to get an exitcode back. 611 | int child_exitcode = -1; // This isn't a valid exitcode, and lets us tell whether the child has exited. 612 | int parse_exitcode = 1; // By default, we exit with 1 if parsing fails. 613 | 614 | /* Parse command line arguments */ 615 | char* (*child_args_ptr)[]; 616 | int parse_args_ret = parse_args(argc, argv, &child_args_ptr, &parse_exitcode); 617 | if (parse_args_ret) { 618 | return parse_exitcode; 619 | } 620 | 621 | /* Parse environment */ 622 | if (parse_env()) { 623 | return 1; 624 | } 625 | 626 | /* Configure signals */ 627 | sigset_t parent_sigset, child_sigset; 628 | struct sigaction sigttin_action, sigttou_action; 629 | memset(&sigttin_action, 0, sizeof sigttin_action); 630 | memset(&sigttou_action, 0, sizeof sigttou_action); 631 | 632 | signal_configuration_t child_sigconf = { 633 | .sigmask_ptr = &child_sigset, 634 | .sigttin_action_ptr = &sigttin_action, 635 | .sigttou_action_ptr = &sigttou_action, 636 | }; 637 | 638 | if (configure_signals(&parent_sigset, &child_sigconf)) { 639 | return 1; 640 | } 641 | 642 | /* Trigger signal on this process when the parent process exits. */ 643 | if (parent_death_signal && prctl(PR_SET_PDEATHSIG, parent_death_signal)) { 644 | PRINT_FATAL("Failed to set up parent death signal"); 645 | return 1; 646 | } 647 | 648 | #if HAS_SUBREAPER 649 | /* If available and requested, register as a subreaper */ 650 | if (register_subreaper()) { 651 | return 1; 652 | }; 653 | #endif 654 | 655 | /* Are we going to reap zombies properly? If not, warn. */ 656 | reaper_check(); 657 | 658 | /* Go on */ 659 | int spawn_ret = spawn(&child_sigconf, *child_args_ptr, &child_pid); 660 | if (spawn_ret) { 661 | return spawn_ret; 662 | } 663 | free(child_args_ptr); 664 | 665 | while (1) { 666 | /* Wait for one signal, and forward it */ 667 | if (wait_and_forward_signal(&parent_sigset, child_pid)) { 668 | return 1; 669 | } 670 | 671 | /* Now, reap zombies */ 672 | if (reap_zombies(child_pid, &child_exitcode)) { 673 | return 1; 674 | } 675 | 676 | if (child_exitcode != -1) { 677 | PRINT_TRACE("Exiting: child has exited"); 678 | return child_exitcode; 679 | } 680 | } 681 | } 682 | --------------------------------------------------------------------------------