diff --git a/README.md b/README.md index c204f96..54700df 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,22 @@ as PID 1. and isn't registered as a subreaper. If you don't see a warning, you're fine.* +### Remapping exit codes ### + +Tini will reuse the child's exit code when exiting, but occasionally, this may +not be exactly what you want (e.g. if your child exits with 143 after receiving +SIGTERM). Notably, this can be an issue with Java apps. + +In this case, you can use the `-e` flag to remap an arbitrary exit code to 0. +You can pass the flag multiple times if needed. + +For example: + +``` +tini -e 143 -- ... +``` + + ### Process group killing ### By default, Tini only kills its immediate child process. This can be diff --git a/ci/run_build.sh b/ci/run_build.sh index d0e05e0..d71c232 100755 --- a/ci/run_build.sh +++ b/ci/run_build.sh @@ -37,6 +37,7 @@ fi echo "CC=${CC}" echo "CFLAGS=${CFLAGS}" +echo "MINIMAL=${MINIMAL-}" echo "ARCH_SUFFIX=${ARCH_SUFFIX-}" echo "ARCH_NATIVE=${ARCH_NATIVE-}" @@ -64,7 +65,6 @@ popd pkg_version="$(cat "${BUILD_DIR}/VERSION")" - if [[ -n "${ARCH_NATIVE-}" ]]; then echo "Built native package (ARCH_NATIVE=${ARCH_NATIVE-})" echo "Running smoke and internal tests" diff --git a/src/tini.c b/src/tini.c index b6d4fce..1faf75c 100644 --- a/src/tini.c +++ b/src/tini.c @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -33,7 +34,15 @@ #define DEFAULT_VERBOSITY 1 #endif -#define ARRAY_LEN(x) (sizeof(x) / sizeof(x[0])) +#define ARRAY_LEN(x) (sizeof(x) / sizeof((x)[0])) + +#define INT32_BITFIELD_SET(F, i) ( F[(i / 32)] |= (1 << (i % 32)) ) +#define INT32_BITFIELD_CLEAR(F, i) ( F[(i / 32)] &= ~(1 << (i % 32)) ) +#define INT32_BITFIELD_TEST(F, i) ( F[(i / 32)] & (1 << (i % 32)) ) +#define INT32_BITFIELD_CHECK_BOUNDS(F, i) do { assert(i >= 0); assert(ARRAY_LEN(F) > (uint) (i / 32)); } while(0) + +#define STATUS_MAX 255 +#define STATUS_MIN 0 typedef struct { sigset_t* const sigmask_ptr; @@ -43,13 +52,15 @@ typedef struct { static unsigned int verbosity = DEFAULT_VERBOSITY; +static int32_t expect_status[(STATUS_MAX - STATUS_MIN + 1) / 32]; + #ifdef PR_SET_CHILD_SUBREAPER #define HAS_SUBREAPER 1 -#define OPT_STRING "hsvwgl" +#define OPT_STRING "hvwgle:s" #define SUBREAPER_ENV_VAR "TINI_SUBREAPER" #else #define HAS_SUBREAPER 0 -#define OPT_STRING "hvwgl" +#define OPT_STRING "hvwgle:" #endif #define VERBOSITY_ENV_VAR "TINI_VERBOSITY" @@ -199,6 +210,7 @@ void print_usage(char* const name, FILE* const file) { fprintf(file, " -v: Generate more verbose output. Repeat up to 3 times.\n"); fprintf(file, " -w: Print a warning when processes are getting reaped.\n"); fprintf(file, " -g: Send signals to the child's process group.\n"); + fprintf(file, " -e EXIT_CODE: Remap EXIT_CODE (from 0 to 255) to 0.\n"); fprintf(file, " -l: Show license and exit.\n"); #endif @@ -223,6 +235,24 @@ void print_license(FILE* const file) { } } +int add_expect_status(char* arg) { + long status = 0; + char* endptr = NULL; + status = strtol(arg, &endptr, 10); + + if ((endptr == NULL) || (*endptr != 0)) { + return 1; + } + + if ((status < STATUS_MIN) || (status > STATUS_MAX)) { + return 1; + } + + INT32_BITFIELD_CHECK_BOUNDS(expect_status, status); + INT32_BITFIELD_SET(expect_status, status); + return 0; +} + int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[], int* const parse_fail_exitcode_ptr) { char* name = argv[0]; @@ -258,6 +288,14 @@ int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[ kill_process_group++; break; + case 'e': + if (add_expect_status(optarg)) { + PRINT_FATAL("Not a valid option for -e: %s", optarg); + *parse_fail_exitcode_ptr = 1; + return 1; + } + break; + case 'l': print_license(stdout); *parse_fail_exitcode_ptr = 0; @@ -477,6 +515,15 @@ int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) { PRINT_FATAL("Main child exited for unknown reason"); return 1; } + + // Be safe, ensure the status code is indeed between 0 and 255. + *child_exitcode_ptr = *child_exitcode_ptr % (STATUS_MAX - STATUS_MIN + 1); + + // If this exitcode was remapped, then set it to 0. + INT32_BITFIELD_CHECK_BOUNDS(expect_status, *child_exitcode_ptr); + if (INT32_BITFIELD_TEST(expect_status, *child_exitcode_ptr)) { + *child_exitcode_ptr = 0; + } } else if (warn_on_reap > 0) { PRINT_WARNING("Reaped zombie process with pid=%i", current_pid); } diff --git a/test/run_inner_tests.py b/test/run_inner_tests.py index 9fb99c8..47e1a10 100755 --- a/test/run_inner_tests.py +++ b/test/run_inner_tests.py @@ -8,7 +8,9 @@ import psutil import bitmap import re +import itertools +DEVNULL = open(os.devnull, 'wb') SIGNUM_TO_SIGNAME = dict((v, k) for k,v in signal.__dict__.items() if re.match("^SIG[A-Z]+$", k)) @@ -36,6 +38,29 @@ def main(): subreaper_support = bool(int(os.environ["FORCE_SUBREAPER"])) + # Run the exit code test. We use POSIXLY_CORRECT here to not need -- + # until that's the default in Tini anyways. + if not args_disabled: + print "Running exit code test for {0}".format(tini) + for code in range(0, 256): + p = subprocess.Popen( + [tini, '-e', str(code), '--', 'sh', '-c', "exit {0}".format(code)], + stdout=DEVNULL, stderr=DEVNULL + ) + ret = p.wait() + assert ret == 0, "Inclusive exit code test failed for %s, exit: %s" % (code, ret) + + other_codes = [x for x in range(0, 256) if x != code] + args = list(itertools.chain(*[['-e', str(x)] for x in other_codes])) + + p = subprocess.Popen( + [tini] + args + ['sh', '-c', "exit {0}".format(code)], + env=dict(os.environ, POSIXLY_CORRECT="1"), + stdout=DEVNULL, stderr=DEVNULL + ) + ret = p.wait() + assert ret == code, "Exclusive exit code test failed for %s, exit: %s" % (code, ret) + tests = [([proxy, tini], {}),] if subreaper_support: diff --git a/tpl/README.md.in b/tpl/README.md.in index a53a409..6e0ce2d 100644 --- a/tpl/README.md.in +++ b/tpl/README.md.in @@ -141,6 +141,22 @@ as PID 1. and isn't registered as a subreaper. If you don't see a warning, you're fine.* +### Remapping exit codes ### + +Tini will reuse the child's exit code when exiting, but occasionally, this may +not be exactly what you want (e.g. if your child exits with 143 after receiving +SIGTERM). Notably, this can be an issue with Java apps. + +In this case, you can use the `-e` flag to remap an arbitrary exit code to 0. +You can pass the flag multiple times if needed. + +For example: + +``` +tini -e 143 -- ... +``` + + ### Process group killing ### By default, Tini only kills its immediate child process. This can be