Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Post Processing Commands #129

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,35 @@ tini -p SIGTERM -- ...
*NOTE: See [this PR discussion][12] to learn more about the parent death signal
and use cases.*

### Post Processing Commands ###

Tini can be instructed to execute a sequence of commands after the primary
child terminates. These post processing commands can be used to perform cleanup
actions, or remap exit codes. To add a post processing command, use the `-P`
flag. The flag will consume arguments until a lone `;` is encountered. In
addition, an argument of `{}` will be replaced with the numeric exit code of
the last process tini ran. The exit code of the post processing command will be
the new exit code for tini.

```
$ echo -e '#!/bin/sh\nexit $(expr $1 + 1)' > test.sh
$ tini -P test.sh {} ; -- /bin/false
$ exit $?
2
```

*NOTE: The `;` terminator may need to be escaped if executed from a shell*

Multiple post processing command may be specified on the command line. Each one
will be executed in the order listed, and each can be passed the exit code
from the previous one via `{}`.

Post processing commands inherit their signal mask from tini itself, unlike the
child command which gets the default signal mask. This makes them suitable for
unconditionally running uninterruptable commands after the primary command
exits. The post processing commands can always unmask signals if they desire to
be interruptable again.


More
----
Expand Down
12 changes: 12 additions & 0 deletions ci/run_build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ if [[ -n "${ARCH_NATIVE-}" ]]; then
if "${tini}" -vvv -- -- true; then
exit 1
fi

echo "Testing ${tini} missing post command terminator (should fail)"
# Missing terminator
if "$tini" -P true -- true; then
exit 1
fi

echo "Testing ${tini} missing post command with only a terminator (should fail)"
# Only a terminator
if "$tini" -P \; -- true; then
exit 1
fi
fi

echo "Testing ${tini} supports TINI_VERBOSITY"
Expand Down
137 changes: 111 additions & 26 deletions src/tini.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <signal.h>
#include <string.h>
#include <time.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
Expand Down Expand Up @@ -84,24 +86,35 @@ static const struct {
{ "SIGSYS", SIGSYS },
};

#define CHILD_CHECK_EXPECT 1
#define CHILD_KEEP_SIGMASK 2

struct child_process_t {
struct child_process_t* next;
int flags;
char exit_code_buffer[4];
char* argv[];
};

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 "p:hvwgle:s"
#define OPT_STRING "p:hvwgle:sP:"
#define SUBREAPER_ENV_VAR "TINI_SUBREAPER"
#else
#define HAS_SUBREAPER 0
#define OPT_STRING "p:hvwgle:"
#define OPT_STRING "p:hvwgle:P:"
#endif

#define VERBOSITY_ENV_VAR "TINI_VERBOSITY"
#define KILL_PROCESS_GROUP_GROUP_ENV_VAR "TINI_KILL_PROCESS_GROUP"

#define TINI_VERSION_STRING "tini version " TINI_VERSION TINI_GIT

#define POST_COMMAND_TERMINATOR ";"

#if HAS_SUBREAPER
static unsigned int subreaper = 0;
Expand Down Expand Up @@ -178,7 +191,7 @@ int isolate_child() {
}


int spawn(const signal_configuration_t* const sigconf_ptr, char* const argv[], int* const child_pid_ptr) {
int spawn(const signal_configuration_t* const sigconf_ptr, const struct child_process_t* const child, int* const child_pid_ptr) {
pid_t pid;

// TODO: check if tini was a foreground process to begin with (it's not OK to "steal" the foreground!")
Expand All @@ -194,12 +207,14 @@ int spawn(const signal_configuration_t* const sigconf_ptr, char* const argv[], i
return 1;
}

// Restore all signal handlers to the way they were before we touched them.
if (restore_signals(sigconf_ptr)) {
return 1;
if (!(child->flags & CHILD_KEEP_SIGMASK)) {
// Restore all signal handlers to the way they were before we touched them.
if (restore_signals(sigconf_ptr)) {
return 1;
}
}

execvp(argv[0], argv);
execvp(child->argv[0], child->argv);

// execvp will only return on an error so make sure that we check the errno
// and exit with the correct return status for the error that we encountered
Expand All @@ -213,11 +228,11 @@ int spawn(const signal_configuration_t* const sigconf_ptr, char* const argv[], i
status = 126;
break;
}
PRINT_FATAL("exec %s failed: %s", argv[0], strerror(errno));
PRINT_FATAL("exec %s failed: %s", child->argv[0], strerror(errno));
return status;
} else {
// Parent
PRINT_INFO("Spawned child process '%s' with pid '%i'", argv[0], pid);
PRINT_INFO("Spawned child process '%s' with pid '%i'", child->argv[0], pid);
*child_pid_ptr = pid;
return 0;
}
Expand Down Expand Up @@ -249,6 +264,7 @@ void print_usage(char* const name, FILE* const file) {
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");
fprintf(file, " -P PROGRAM [ARGS] ;: Add post processing command, e.g. \"-P exit {};\"\n");
#endif

fprintf(file, "\n");
Expand Down Expand Up @@ -305,8 +321,9 @@ int add_expect_status(char* arg) {
return 0;
}

int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[], int* const parse_fail_exitcode_ptr) {
int parse_args(const int argc, char* const argv[], struct child_process_t** child_list_head_ptr, int* const parse_fail_exitcode_ptr) {
char* name = argv[0];
struct child_process_t* child;

// We handle --version if it's the *only* argument provided.
if (argc == 2 && strcmp("--version", argv[1]) == 0) {
Expand All @@ -316,6 +333,8 @@ int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[
}

#ifndef TINI_MINIMAL
struct child_process_t** child_list_tail_ptr = child_list_head_ptr;

int c;
while ((c = getopt(argc, argv, OPT_STRING)) != -1) {
switch (c) {
Expand Down Expand Up @@ -361,6 +380,51 @@ int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[
*parse_fail_exitcode_ptr = 0;
return 1;

case 'P': {
// Check for degenerate case (terminator in optarg)
if (strcmp(optarg, POST_COMMAND_TERMINATOR) == 0) {
PRINT_FATAL("Not a valid post command: %s", optarg);
*parse_fail_exitcode_ptr = 1;
return 1;
}

// Count arguments until a terminator is encountered
int arg_count = 0;
while (optind + arg_count < argc && strcmp(argv[optind + arg_count], POST_COMMAND_TERMINATOR) != 0) {
arg_count++;
}

if (optind + arg_count == argc) {
PRINT_FATAL("Post command must be terminated with '%s'", POST_COMMAND_TERMINATOR);
*parse_fail_exitcode_ptr = 1;
return 1;
}

child = calloc(1, offsetof(struct child_process_t, argv) + (arg_count + 2) * sizeof(char*));
if (child == NULL) {
PRINT_FATAL("Failed to allocate memory for child args: '%s'", strerror(errno));
return 1;
}

child->flags = CHILD_KEEP_SIGMASK;

child->argv[0] = optarg;
for (int i = 0; i < arg_count; i++) {
char* arg = argv[optind + i];
if (strcmp(arg, "{}") == 0) {
arg = child->exit_code_buffer;
}
child->argv[1 + i] = arg;
}
child->argv[arg_count + 1] = NULL;

// Skip consumed arguments
optind += arg_count + 1;

*child_list_tail_ptr = child;
child_list_tail_ptr = &child->next;
} break;

case '?':
print_usage(name, stderr);
return 1;
Expand All @@ -371,17 +435,21 @@ int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[
}
#endif

*child_args_ptr_ptr = calloc(argc-optind+1, sizeof(char*));
if (*child_args_ptr_ptr == NULL) {
child = calloc(1, offsetof(struct child_process_t, argv) + (argc-optind+1) * sizeof(char*));
if (child == NULL) {
PRINT_FATAL("Failed to allocate memory for child args: '%s'", strerror(errno));
return 1;
}

int i;
for (i = 0; i < argc - optind; i++) {
(**child_args_ptr_ptr)[i] = argv[optind+i];
child->argv[i] = argv[optind+i];
}
(**child_args_ptr_ptr)[i] = NULL;
child->argv[i] = NULL;

child->flags = CHILD_CHECK_EXPECT;
child->next = *child_list_head_ptr;
*child_list_head_ptr = child;

if (i == 0) {
/* User forgot to provide args! */
Expand Down Expand Up @@ -538,7 +606,7 @@ int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const
return 0;
}

int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) {
int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr, bool check_expect) {
pid_t current_pid;
int current_status;

Expand Down Expand Up @@ -583,10 +651,12 @@ int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) {
// 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;
if (check_expect) {
// 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);
Expand All @@ -612,8 +682,8 @@ int main(int argc, char *argv[]) {
int parse_exitcode = 1; // By default, we exit with 1 if parsing fails.

/* Parse command line arguments */
char* (*child_args_ptr)[];
int parse_args_ret = parse_args(argc, argv, &child_args_ptr, &parse_exitcode);
struct child_process_t* child_list_head = NULL;
int parse_args_ret = parse_args(argc, argv, &child_list_head, &parse_exitcode);
if (parse_args_ret) {
return parse_exitcode;
}
Expand Down Expand Up @@ -656,11 +726,10 @@ int main(int argc, char *argv[]) {
reaper_check();

/* Go on */
int spawn_ret = spawn(&child_sigconf, *child_args_ptr, &child_pid);
int spawn_ret = spawn(&child_sigconf, child_list_head, &child_pid);
if (spawn_ret) {
return spawn_ret;
}
free(child_args_ptr);

while (1) {
/* Wait for one signal, and forward it */
Expand All @@ -669,13 +738,29 @@ int main(int argc, char *argv[]) {
}

/* Now, reap zombies */
if (reap_zombies(child_pid, &child_exitcode)) {
if (reap_zombies(child_pid, &child_exitcode, !!(child_list_head->flags & CHILD_CHECK_EXPECT))) {
return 1;
}

if (child_exitcode != -1) {
PRINT_TRACE("Exiting: child has exited");
return child_exitcode;
PRINT_TRACE("Child %d has exited", child_pid);

struct child_process_t* child_tmp = child_list_head;
child_list_head = child_list_head->next;
free(child_tmp);

if (child_list_head == NULL) {
return child_exitcode;
}

snprintf(child_list_head->exit_code_buffer, sizeof(child_list_head->exit_code_buffer),
"%" PRIu8, child_exitcode);

child_exitcode = -1;
int spawn_ret = spawn(&child_sigconf, child_list_head, &child_pid);
if (spawn_ret) {
return spawn_ret;
}
}
}
}
12 changes: 12 additions & 0 deletions test/post/stage_1.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#! /bin/sh

case "$1" in
0)
exit 2
;;
1)
exit 3
;;
esac

exit 4
13 changes: 13 additions & 0 deletions test/post/stage_2.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#! /bin/sh

case "$1" in
2)
exit 5
;;
3)
exit 6
;;
esac

exit 4

19 changes: 19 additions & 0 deletions test/run_inner_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ def main():
ret = p.wait()
assert ret == code, "Exclusive exit code test failed for %s, exit: %s" % (code, ret)

print "Running post command test for {0}".format(tini)
for in_code, out_code in ((0, 2), (1, 3)):
p = subprocess.Popen([tini, '-P', os.path.join(src, "test", "post", "stage_1.sh"), '{}', ';', '--', 'sh', '-c', 'exit {0}'.format(in_code)],
stdout=DEVNULL, stderr=DEVNULL
)
ret = p.wait()
assert ret == out_code, "post command test failed for %s, exit: %s" % (in_code, ret)

print "Running post command chain test for {0}".format(tini)

for in_code, out_code in ((0, 5), (1, 6)):
post_cmd_1 = os.path.join(src, "test", "post", "stage_1.sh")
post_cmd_2 = os.path.join(src, "test", "post", "stage_2.sh")
p = subprocess.Popen([tini, '-P', post_cmd_1, '{}', ';', '-P', post_cmd_2, '{}', ';', '--', 'sh', '-c', 'exit {0}'.format(in_code)],
stdout=DEVNULL, stderr=DEVNULL
)
ret = p.wait()
assert ret == out_code, "post command test failed for %s, exit: %s" % (in_code, ret)

tests = [([proxy, tini], {}),]

if subreaper_support:
Expand Down
Loading