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

Reload on file change handling for tini-watched processes #82

Open
wants to merge 2 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
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
121 changes: 118 additions & 3 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,6 +240,44 @@ 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];

Expand All @@ -231,7 +289,7 @@ int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[
}

#ifndef TINI_MINIMAL
int c;
int c, tmpi;
while ((c = getopt(argc, argv, OPT_STRING)) != -1) {
switch (c) {
case 'h':
Expand All @@ -251,6 +309,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 +555,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 +652,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