diff --git a/.gitignore b/.gitignore index fe84e5d..d3e7ba7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist sign.key .env +test/.file_test diff --git a/src/tini.c b/src/tini.c index 90ee3e8..1422f34 100644 --- a/src/tini.c +++ b/src/tini.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -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"); } @@ -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" @@ -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 " @@ -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 @@ -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 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]; @@ -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': @@ -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; @@ -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; @@ -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; diff --git a/test/run_inner_tests.py b/test/run_inner_tests.py index e79338e..fa3d78d 100755 --- a/test/run_inner_tests.py +++ b/test/run_inner_tests.py @@ -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)"