diff --git a/Dockerfile b/Dockerfile index 66f9904..f2ff7fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,8 @@ FROM ubuntu -RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install --no-install-recommends -y build-essential gdb && rm -rf /var/lib/apt/lists/* ADD . /tini RUN cd /tini && make + +ENTRYPOINT ["/tini/tini"] diff --git a/Makefile b/Makefile index eded780..fc3aa34 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ $(BIN): $(OBJ) $(OBJ): check: + docker build -t tini . python test/test.py install: all diff --git a/README.md b/README.md index 5cb1320..3944585 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,9 @@ Debugging --------- If something isn't working just like you expect, consider increasing the -verbosity level (up to 3): +verbosity level (up to 4): - tini -v -- bash -c 'exit 1' - tini -vv -- true - tini -vvv -- pwd + tini -v -- bash -c 'exit 1' + tini -vv -- true + tini -vvv -- pwd + tini -vvvv -- ls diff --git a/test/test.py b/test/test.py index 1045576..79ad437 100644 --- a/test/test.py +++ b/test/test.py @@ -1,33 +1,57 @@ #coding:utf-8 import os +import sys import time import subprocess import threading class Command(object): - def __init__(self, cmd, post_cmd=None, post_delay=None): + def __init__(self, cmd, fail_cmd, post_cmd=None, post_delay=0): self.cmd = cmd + self.fail_cmd = fail_cmd self.post_cmd = post_cmd self.post_delay = post_delay - self._process = None + self.proc = None + + def run(self, timeout=None, retcode=None): + print "Testing...", self.cmd, + sys.stdout.flush() + + err = None + pipe_kwargs = {"stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "stdin": subprocess.PIPE} - def run(self, timeout): def target(): - self._process = subprocess.Popen(self.cmd) - self._process.communicate() + self.proc = subprocess.Popen(self.cmd, **pipe_kwargs) + self.proc.communicate() thread = threading.Thread(target=target) thread.start() if self.post_cmd is not None: - if self.post_delay is not None: - time.sleep(self.post_delay) - subprocess.check_call(self.post_cmd) + time.sleep(self.post_delay) + subprocess.check_call(self.post_cmd, **pipe_kwargs) - thread.join(timeout) + thread.join(timeout - self.post_delay if timeout is not None else timeout) + + # Checks if thread.is_alive(): - raise Exception("Test failed!") + subprocess.check_call(self.fail_cmd, **pipe_kwargs) + err = Exception("Test failed with timeout!") + + elif retcode is not None and self.proc.returncode != retcode: + err = Exception("Test failed with unexpected returncode (expected {0}, got {1})".format(retcode, self.proc.returncode)) + + if err is not None: + print "FAIL" + print "--- STDOUT ---" + print out + print "--- STDERR ---" + print err + print "--- ... ---" + raise err + else: + print "OK" if __name__ == "__main__": @@ -37,24 +61,27 @@ if __name__ == "__main__": base_cmd = [ "docker", "run", - "-it", "--rm", "--name=tini-test", - "-v", - "{0}:{0}".format(root), - "ubuntu", - "{0}/tini".format(root), - "-vvv", - "--", + "tini", + "-vvvv", ] + fail_cmd = ["docker", "kill", "tini-test"] + # Reaping test - Command(base_cmd + ["/Users/thomas/dev/tini/test/reaping/stage_1.py"]).run(timeout=10) + Command(base_cmd + ["/tini/test/reaping/stage_1.py"], fail_cmd).run(timeout=10) # Signals test - for sig in ["INT", "TERM"]: + for sig, retcode in [("INT", 1), ("TERM", 143)]: Command( - base_cmd + ["/Users/thomas/dev/tini/test/signals/test.py"], + base_cmd + ["--", "/tini/test/signals/test.py"], + fail_cmd, ["docker", "kill", "-s", sig, "tini-test"], 2 - ).run(timeout=10) + ).run(timeout=10, retcode=retcode) + + # Exit code test + c = Command(base_cmd + ["-z"], fail_cmd).run(retcode=1) + c = Command(base_cmd + ["zzzz"], fail_cmd).run(retcode=1) + c = Command(base_cmd + ["-h"], fail_cmd).run(retcode=0) diff --git a/tini.c b/tini.c index 9b6415a..e80faed 100644 --- a/tini.c +++ b/tini.c @@ -9,19 +9,23 @@ #include #include #include +#include + #define PRINT_FATAL(...) fprintf(stderr, "[FATAL] "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); #define PRINT_WARNING(...) if (verbosity > 0) { fprintf(stderr, "[WARN ] "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } #define PRINT_INFO(...) if (verbosity > 1) { fprintf(stderr, "[INFO ] "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } #define PRINT_DEBUG(...) if (verbosity > 2) { fprintf(stderr, "[DEBUG] "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } +#define PRINT_TRACE(...) if (verbosity > 3) { fprintf(stderr, "[TRACE] "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } #define ARRAY_LEN(x) (sizeof(x) / sizeof(x[0])) static int verbosity = 0; +static struct timespec ts = { .tv_sec = 1, .tv_nsec = 0 }; -pid_t spawn(sigset_t *child_sigset_ptr, char (*argv[])) { +pid_t spawn(const sigset_t* const child_sigset_ptr, char (*argv[])) { /* TODO - Don't exit here! */ pid_t pid; @@ -36,35 +40,41 @@ pid_t spawn(sigset_t *child_sigset_ptr, char (*argv[])) { _exit(1); } execvp(argv[0], argv); - PRINT_FATAL("Executing child process failed: '%s'", strerror(errno)); + PRINT_FATAL("Executing child process '%s' failed: '%s'", argv[0], strerror(errno)); _exit(1); } else { // Parent - PRINT_INFO("Spawned child process '%s' with pid '%d'", argv[0], pid); + PRINT_INFO("Spawned child process '%s' with pid '%i'", argv[0], pid); return pid; } } -void print_usage(const char *name, FILE *file, const int status) { +void print_usage(char* const name, FILE* const file) { fprintf(file, "Usage: %s [-h | program arg1 arg2]\n", name); } -int parse_args(int argc, char *argv[], char* (**child_args_ptr_ptr)[]) { +int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[], int* const parse_fail_exitcode_ptr) { + /* + * Returns with 0 to indicate success, a positive value to indicate the process + * should exit with success, and -1 to indicate it should exit with a failure. + */ char* name = argv[0]; int c; while ((c = getopt (argc, argv, "hv")) != -1) { switch (c) { case 'h': - print_usage(name, stdout, 0); + /* TODO - Shouldn't cause exit with -1 ..*/ + print_usage(name, stdout); + *parse_fail_exitcode_ptr = 0; return 1; case 'v': verbosity++; - return 1; + break; case '?': - print_usage(name, stderr, 1); + print_usage(name, stderr); return 1; default: /* Should never happen */ @@ -86,25 +96,25 @@ int parse_args(int argc, char *argv[], char* (**child_args_ptr_ptr)[]) { if (i == 0) { /* User forgot to provide args! */ - print_usage(name, stdout, 1); + print_usage(name, stderr); return 1; } return 0; } -int prepare_sigmask(sigset_t *parent_sigset_ptr, sigset_t *child_sigset_ptr) { +int prepare_sigmask(sigset_t* const parent_sigset_ptr, sigset_t* const child_sigset_ptr) { /* Prepare signals to block; make sure we don't block program error signals. */ if (sigfillset(parent_sigset_ptr)) { PRINT_FATAL("sigfillset failed: '%s'", strerror(errno)); return 1; } - int i; - int ignore_signals[] = {SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGIOT, SIGTRAP, SIGEMT, SIGSYS} ; + uint i; + int ignore_signals[] = {SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS} ; for (i = 0; i < ARRAY_LEN(ignore_signals); i++) { if (sigdelset(parent_sigset_ptr, ignore_signals[i])) { - PRINT_FATAL("sigdelset failed: '%d'", ignore_signals[i]); + PRINT_FATAL("sigdelset failed: '%i'", ignore_signals[i]); return 1; } } @@ -117,20 +127,104 @@ int prepare_sigmask(sigset_t *parent_sigset_ptr, sigset_t *child_sigset_ptr) { return 0; } - -int main(int argc, char *argv[]) { +int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const child_pid) { siginfo_t sig; - pid_t child_pid; + if (sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1) { + switch (errno) { + case EAGAIN: + break; + case EINTR: + break; + case EINVAL: + PRINT_FATAL("EINVAL on sigtimedwait!"); + return -1; + } + } else { + /* There is a signal to handle here */ + switch (sig.si_signo) { + case SIGCHLD: + /* Special-cased, as we don't forward SIGCHLD. Instead, we'll + * fallthrough to reaping processes. + */ + PRINT_DEBUG("Received SIGCHLD"); + break; + default: + PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo)); + /* Forward anything else */ + kill(child_pid, sig.si_signo); // TODO - Check retcode! + break; + } + } + return 0; +} + +int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) { + /* + * Returns: + * + = 0: The iteration completed successfully, but the child is still alive. + * + > 0: The iteration completed successfully, and the child was reaped. + * + < 0: An error occured + */ pid_t current_pid; int current_status; - int exit_code = -1; + while (1) { + current_pid = waitpid(-1, ¤t_status, WNOHANG); - struct timespec ts; - ts.tv_sec = 1; - ts.tv_nsec = 0; + switch (current_pid) { + + case -1: + if (errno == ECHILD) { + PRINT_TRACE("No child to wait."); + break; + } + PRINT_FATAL("Error while waiting for pids: '%s'", strerror(errno)); + return -1; + + case 0: + PRINT_TRACE("No child to reap."); + break; + + default: + /* A child was reaped. Check whether it's the main one. If it is, then + * set the exit_code, which will cause us to exit once we've reaped everyone else. + */ + PRINT_DEBUG("Reaped child with pid: '%i'", current_pid); + if (current_pid == child_pid) { + if (WIFEXITED(current_status)) { + /* Our process exited normally. */ + PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status)); + *child_exitcode_ptr = WEXITSTATUS(current_status); + } else if (WIFSIGNALED(current_status)) { + /* Our process was terminated. Emulate what sh / bash + * would do, which is to return 128 + signal number. + */ + PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status))); + *child_exitcode_ptr = 128 + WTERMSIG(current_status); + } else { + PRINT_FATAL("Main child exited for unknown reason!"); + return -1; + } + } + + // Check if other childs have been reaped. + continue; + } + + /* If we make it here, that's because we did not continue in the switch case. */ + break; + } + + return 0; +} + + +int main(int argc, char *argv[]) { + pid_t child_pid; + int child_exitcode = -1; + int parse_exitcode = 1; // By default, we exit with 1 if parsing fails /* Prepare sigmask */ sigset_t parent_sigset; @@ -139,96 +233,31 @@ int main(int argc, char *argv[]) { return 1; } - /* Spawn the main command */ + /* Parse command line arguments */ char* (*child_args_ptr)[]; - if (parse_args(argc, argv, &child_args_ptr)) { - return 1; + int parse_args_ret = parse_args(argc, argv, &child_args_ptr, &parse_exitcode); + if (parse_args_ret) { + return parse_exitcode; } + child_pid = spawn(&child_sigset, *child_args_ptr); free(child_args_ptr); - /* Loop forever: - * - Reap zombies - * - Forward signals - */ while (1) { - if (sigtimedwait(&parent_sigset, &sig, &ts) == -1) { - switch (errno) { - case EAGAIN: - break; - case EINTR: - break; - case EINVAL: - PRINT_FATAL("EINVAL on sigtimedwait!"); - return 2; - } - } else { - /* There is a signal to handle here */ - switch (sig.si_signo) { - case SIGCHLD: - /* Special-cased, as we don't forward SIGCHLD. Instead, we'll - * fallthrough to reaping processes. - */ - PRINT_DEBUG("Received SIGCHLD"); - break; - default: - PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo)); - /* Forward anything else */ - kill(child_pid, sig.si_signo); - break; - } + /* Wait for one signal, and forward it */ + if (wait_and_forward_signal(&parent_sigset, child_pid)) { + return 1; } /* Now, reap zombies */ - while (1) { - current_pid = waitpid(-1, ¤t_status, WNOHANG); - switch (current_pid) { - case -1: - if (errno == ECHILD) { - // No childs to wait. Let's break out of the loop. - break; - } - /* An unknown error occured. Print it and exit. */ - PRINT_FATAL("Error while waiting for pids: '%s'", strerror(errno)); - return 1; - case 0: - /* No child to reap. We'll break out of the loop here. */ - break; - default: - /* A child was reaped. Check whether it's the main one. If it is, then - * set the exit_code, which will cause us to exit once we've reaped everyone else. - */ - PRINT_DEBUG("Reaped child with pid: '%i'", current_pid); - if (current_pid == child_pid) { - if (WIFEXITED(current_status)) { - /* Our process exited normally. */ - PRINT_INFO("Main child exited normally (with status '%i')", WEXITSTATUS(current_status)); - exit_code = WEXITSTATUS(current_status); - } else if (WIFSIGNALED(current_status)) { - /* Our process was terminated. Emulate what sh / bash - * would do, which is to return 128 + signal number. - */ - PRINT_INFO("Main child exited with signal (with signal '%s')", strsignal(WTERMSIG(current_status))); - exit_code = 128 + WTERMSIG(current_status); - } else { - PRINT_FATAL("Main child exited for unknown reason!"); - return 1; - } - } - continue; - } - - /* If exit_code is not equal to -1, then we're exiting because our main child has exited */ - if (exit_code != -1 ) { - return exit_code; - } - - /* If we make it here, that's because we did not continue in the switch case. */ - break; + if (reap_zombies(child_pid, &child_exitcode)) { + // Oops! + return 1; } + if (child_exitcode != -1) { + PRINT_TRACE("Child has exited. Exiting"); + return child_exitcode; + } } - /* not reachable */ - return 0; } -