Skip to content

Commit

Permalink
Reload on file change handling for tini-watched processes
Browse files Browse the repository at this point in the history
Some container-based programs can do an inline reload when a
config file (particularly those mounted on a Volume) changes, but
need to be signalled. For a local docker, we can do docker kill
and let the signal pass through tini, but on a scheduler, like
Kubernetes, we either have to run commands in each container or
delete and recreate the containers (as per the replication-group
strategy).

With this patch, we can run something like prometheus or fluentd
in containers, and be able to use kubernetes to push updates,
with the relevant program auto-reloading, without the need for a
side-bar watcher, making tini a little bit more like a "supervisor".
  • Loading branch information
Matthew Byng-Maddick committed Apr 6, 2017
1 parent 6ad9813 commit b9450aa
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
sign.key
.env
test/.file_test
120 changes: 118 additions & 2 deletions src/tini.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <sys/stat.h>

#include <errno.h>
#include <signal.h>
Expand All @@ -17,6 +18,8 @@
#include "tiniConfig.h"
#include "tiniLicense.h"

extern char *optarg;

#if TINI_MINIMAL
#define PRINT_FATAL(...) fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n");
#define PRINT_WARNING(...) if (verbosity > 0) { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); }
Expand All @@ -41,15 +44,25 @@ typedef struct {
struct sigaction* const sigttou_action_ptr;
} signal_configuration_t;

struct file_change_t {
const char * filename;
dev_t last_dev;
ino_t last_ino;
time_t last_mtime;
time_t last_ctime;
struct file_change_t *next;
};
typedef struct file_change_t file_change_t;

static unsigned int verbosity = DEFAULT_VERBOSITY;

#ifdef PR_SET_CHILD_SUBREAPER
#define HAS_SUBREAPER 1
#define OPT_STRING "hsvgl"
#define OPT_STRING "hsvglS:F:"
#define SUBREAPER_ENV_VAR "TINI_SUBREAPER"
#else
#define HAS_SUBREAPER 0
#define OPT_STRING "hvgl"
#define OPT_STRING "hvglS:F:"
#endif

#define VERBOSITY_ENV_VAR "TINI_VERBOSITY"
Expand All @@ -62,6 +75,9 @@ static unsigned int subreaper = 0;
#endif
static unsigned int kill_process_group = 0;

static unsigned int file_change_signal = 1; /* HUP */
static file_change_t *file_change_files = NULL;

static struct timespec ts = { .tv_sec = 1, .tv_nsec = 0 };

static const char reaper_warning[] = "Tini is not running as PID 1 "
Expand Down Expand Up @@ -196,6 +212,10 @@ void print_usage(char* const name, FILE* const file) {
#endif
fprintf(file, " -v: Generate more verbose output. Repeat up to 3 times.\n");
fprintf(file, " -g: Send signals to the child's process group.\n");
fprintf(file, " -S signal: numeric signal to send to child if '-F' files\n"
" change. (default: 1 => HUP)\n");
fprintf(file, " -F path: file(s) to check for changes for reload signal\n"
" (may be specified more than once).\n");
fprintf(file, " -l: Show license and exit.\n");
#endif

Expand All @@ -220,8 +240,47 @@ void print_license(FILE* const file) {
}
}

void add_file_change(char* const filename) {
file_change_t *new = NULL, *curr;
struct stat file_st;

// Here we add to a linked list of file_change_t structures
// we layout each "structure" as <struct><filename><nul> in
// the RAM. We expect only a very few - so a linked list here
// is fine.
new = malloc(sizeof(file_change_t) + 1 + strlen(filename));
new->next = NULL;
new->filename = (const char *)(new + 1);
// we break the const once...
strcpy((char *)(new->filename), filename);

if(stat(new->filename, &file_st) == 0) {
new->last_dev = file_st.st_dev;
new->last_ino = file_st.st_ino;
new->last_mtime = file_st.st_mtime;
new->last_ctime = file_st.st_ctime;
} else {
// we produce blank records for files that don't
// exist or are otherwise not accessible to us,
// this way, if they become accessible, we also
// signal the reload.
new->last_dev = 0;
new->last_ino = 0;
new->last_mtime = 0;
new->last_ctime = 0;
}

if(!file_change_files) {
file_change_files = new;
} else {
for (curr = file_change_files; curr->next != NULL; curr = curr->next);
curr->next = new;
}
}

int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[], int* const parse_fail_exitcode_ptr) {
char* name = argv[0];
int tmpi;

// We handle --version if it's the *only* argument provided.
if (argc == 2 && strcmp("--version", argv[1]) == 0) {
Expand Down Expand Up @@ -251,6 +310,19 @@ int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[
kill_process_group++;
break;

case 'S':
tmpi = atoi(optarg);
if(tmpi == 0) {
print_usage(name, stderr);
return 1;
}
file_change_signal = tmpi;
break;

case 'F':
add_file_change(optarg);
break;

case 'l':
print_license(stdout);
*parse_fail_exitcode_ptr = 0;
Expand Down Expand Up @@ -484,6 +556,45 @@ int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) {
}


int kill_on_change_files(pid_t child_pid) {
file_change_t *curr;
struct stat file_st;
char changed = 0;

// We go through our linked list and figure out what changed
for(curr = file_change_files; curr != NULL; curr = curr->next) {
if(stat(curr->filename, &file_st) == 0) {
if( curr->last_dev != file_st.st_dev ||
curr->last_ino != file_st.st_ino ||
curr->last_mtime != file_st.st_mtime ||
curr->last_ctime != file_st.st_ctime ||
0) {
PRINT_DEBUG("Found new/changed file: %s", curr->filename);
changed = 1;
curr->last_dev = file_st.st_dev;
curr->last_ino = file_st.st_ino;
curr->last_mtime = file_st.st_mtime;
curr->last_ctime = file_st.st_ctime;
}
} else if(curr->last_ctime != 0) {
PRINT_DEBUG("Found deleted file: %s", curr->filename);
changed = 1;
curr->last_dev = 0;
curr->last_ino = 0;
curr->last_mtime = 0;
curr->last_ctime = 0;
}
}

if(changed) {
PRINT_INFO("files changed, killing %d with %d", child_pid, file_change_signal);
kill(kill_process_group ? -child_pid : child_pid, file_change_signal);
}

return 0;
}


int main(int argc, char *argv[]) {
pid_t child_pid;

Expand Down Expand Up @@ -542,6 +653,11 @@ int main(int argc, char *argv[]) {
return 1;
}

/* check the files for changes */
if (file_change_files && kill_on_change_files(child_pid)) {
return 1;
}

/* Now, reap zombies */
if (reap_zombies(child_pid, &child_exitcode)) {
return 1;
Expand Down
31 changes: 31 additions & 0 deletions test/run_inner_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,37 @@ def main():
p.send_signal(signal.SIGUSR1)
busy_wait(lambda: p.poll() is not None, 10)

# Run a file-change check test
# This test has Tini spawn a long sleep, similar to above, at which point, we briefly
# sleep ourselves and touch a file underneath
if not args_disabled:
print "Running file-change tests"
t_file = os.path.join(src, "test", ".file_test")
try:
os.unlink(t_file)
except:
pass

p = subprocess.Popen([tini, "-S", "{0}".format(signal.SIGUSR1), "-F", t_file, "sleep", "1000"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 1, 10)
with open(t_file, 'w') as f:
f.write('{}'.format(time.time()))
busy_wait(lambda: p.poll() is not None, 10)

p = subprocess.Popen([tini, "-S", "{0}".format(signal.SIGUSR1), "-F", t_file, "sleep", "1000"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 1, 10)
with open(t_file, 'w') as f:
f.write('{}'.format(time.time()))
busy_wait(lambda: p.poll() is not None, 10)

p = subprocess.Popen([tini, "-S", "{0}".format(signal.SIGUSR1), "-F", t_file, "sleep", "1000"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 1, 10)
os.unlink(t_file)
busy_wait(lambda: p.poll() is not None, 10)

# Run failing test. Force verbosity to 1 so we see the subreaper warning
# regardless of whether MINIMAL is set.
print "Running zombie reaping failure test (Tini should warn)"
Expand Down

0 comments on commit b9450aa

Please sign in to comment.