Skip to content

Commit b9450aa

Browse files
author
Matthew Byng-Maddick
committed
Reload on file change handling for tini-watched processes
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".
1 parent 6ad9813 commit b9450aa

File tree

3 files changed

+150
-2
lines changed

3 files changed

+150
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
dist
22
sign.key
33
.env
4+
test/.file_test

src/tini.c

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <sys/types.h>
55
#include <sys/wait.h>
66
#include <sys/prctl.h>
7+
#include <sys/stat.h>
78

89
#include <errno.h>
910
#include <signal.h>
@@ -17,6 +18,8 @@
1718
#include "tiniConfig.h"
1819
#include "tiniLicense.h"
1920

21+
extern char *optarg;
22+
2023
#if TINI_MINIMAL
2124
#define PRINT_FATAL(...) fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n");
2225
#define PRINT_WARNING(...) if (verbosity > 0) { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); }
@@ -41,15 +44,25 @@ typedef struct {
4144
struct sigaction* const sigttou_action_ptr;
4245
} signal_configuration_t;
4346

47+
struct file_change_t {
48+
const char * filename;
49+
dev_t last_dev;
50+
ino_t last_ino;
51+
time_t last_mtime;
52+
time_t last_ctime;
53+
struct file_change_t *next;
54+
};
55+
typedef struct file_change_t file_change_t;
56+
4457
static unsigned int verbosity = DEFAULT_VERBOSITY;
4558

4659
#ifdef PR_SET_CHILD_SUBREAPER
4760
#define HAS_SUBREAPER 1
48-
#define OPT_STRING "hsvgl"
61+
#define OPT_STRING "hsvglS:F:"
4962
#define SUBREAPER_ENV_VAR "TINI_SUBREAPER"
5063
#else
5164
#define HAS_SUBREAPER 0
52-
#define OPT_STRING "hvgl"
65+
#define OPT_STRING "hvglS:F:"
5366
#endif
5467

5568
#define VERBOSITY_ENV_VAR "TINI_VERBOSITY"
@@ -62,6 +75,9 @@ static unsigned int subreaper = 0;
6275
#endif
6376
static unsigned int kill_process_group = 0;
6477

78+
static unsigned int file_change_signal = 1; /* HUP */
79+
static file_change_t *file_change_files = NULL;
80+
6581
static struct timespec ts = { .tv_sec = 1, .tv_nsec = 0 };
6682

6783
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) {
196212
#endif
197213
fprintf(file, " -v: Generate more verbose output. Repeat up to 3 times.\n");
198214
fprintf(file, " -g: Send signals to the child's process group.\n");
215+
fprintf(file, " -S signal: numeric signal to send to child if '-F' files\n"
216+
" change. (default: 1 => HUP)\n");
217+
fprintf(file, " -F path: file(s) to check for changes for reload signal\n"
218+
" (may be specified more than once).\n");
199219
fprintf(file, " -l: Show license and exit.\n");
200220
#endif
201221

@@ -220,8 +240,47 @@ void print_license(FILE* const file) {
220240
}
221241
}
222242

243+
void add_file_change(char* const filename) {
244+
file_change_t *new = NULL, *curr;
245+
struct stat file_st;
246+
247+
// Here we add to a linked list of file_change_t structures
248+
// we layout each "structure" as <struct><filename><nul> in
249+
// the RAM. We expect only a very few - so a linked list here
250+
// is fine.
251+
new = malloc(sizeof(file_change_t) + 1 + strlen(filename));
252+
new->next = NULL;
253+
new->filename = (const char *)(new + 1);
254+
// we break the const once...
255+
strcpy((char *)(new->filename), filename);
256+
257+
if(stat(new->filename, &file_st) == 0) {
258+
new->last_dev = file_st.st_dev;
259+
new->last_ino = file_st.st_ino;
260+
new->last_mtime = file_st.st_mtime;
261+
new->last_ctime = file_st.st_ctime;
262+
} else {
263+
// we produce blank records for files that don't
264+
// exist or are otherwise not accessible to us,
265+
// this way, if they become accessible, we also
266+
// signal the reload.
267+
new->last_dev = 0;
268+
new->last_ino = 0;
269+
new->last_mtime = 0;
270+
new->last_ctime = 0;
271+
}
272+
273+
if(!file_change_files) {
274+
file_change_files = new;
275+
} else {
276+
for (curr = file_change_files; curr->next != NULL; curr = curr->next);
277+
curr->next = new;
278+
}
279+
}
280+
223281
int parse_args(const int argc, char* const argv[], char* (**child_args_ptr_ptr)[], int* const parse_fail_exitcode_ptr) {
224282
char* name = argv[0];
283+
int tmpi;
225284

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

313+
case 'S':
314+
tmpi = atoi(optarg);
315+
if(tmpi == 0) {
316+
print_usage(name, stderr);
317+
return 1;
318+
}
319+
file_change_signal = tmpi;
320+
break;
321+
322+
case 'F':
323+
add_file_change(optarg);
324+
break;
325+
254326
case 'l':
255327
print_license(stdout);
256328
*parse_fail_exitcode_ptr = 0;
@@ -484,6 +556,45 @@ int reap_zombies(const pid_t child_pid, int* const child_exitcode_ptr) {
484556
}
485557

486558

559+
int kill_on_change_files(pid_t child_pid) {
560+
file_change_t *curr;
561+
struct stat file_st;
562+
char changed = 0;
563+
564+
// We go through our linked list and figure out what changed
565+
for(curr = file_change_files; curr != NULL; curr = curr->next) {
566+
if(stat(curr->filename, &file_st) == 0) {
567+
if( curr->last_dev != file_st.st_dev ||
568+
curr->last_ino != file_st.st_ino ||
569+
curr->last_mtime != file_st.st_mtime ||
570+
curr->last_ctime != file_st.st_ctime ||
571+
0) {
572+
PRINT_DEBUG("Found new/changed file: %s", curr->filename);
573+
changed = 1;
574+
curr->last_dev = file_st.st_dev;
575+
curr->last_ino = file_st.st_ino;
576+
curr->last_mtime = file_st.st_mtime;
577+
curr->last_ctime = file_st.st_ctime;
578+
}
579+
} else if(curr->last_ctime != 0) {
580+
PRINT_DEBUG("Found deleted file: %s", curr->filename);
581+
changed = 1;
582+
curr->last_dev = 0;
583+
curr->last_ino = 0;
584+
curr->last_mtime = 0;
585+
curr->last_ctime = 0;
586+
}
587+
}
588+
589+
if(changed) {
590+
PRINT_INFO("files changed, killing %d with %d", child_pid, file_change_signal);
591+
kill(kill_process_group ? -child_pid : child_pid, file_change_signal);
592+
}
593+
594+
return 0;
595+
}
596+
597+
487598
int main(int argc, char *argv[]) {
488599
pid_t child_pid;
489600

@@ -542,6 +653,11 @@ int main(int argc, char *argv[]) {
542653
return 1;
543654
}
544655

656+
/* check the files for changes */
657+
if (file_change_files && kill_on_change_files(child_pid)) {
658+
return 1;
659+
}
660+
545661
/* Now, reap zombies */
546662
if (reap_zombies(child_pid, &child_exitcode)) {
547663
return 1;

test/run_inner_tests.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,37 @@ def main():
8080
p.send_signal(signal.SIGUSR1)
8181
busy_wait(lambda: p.poll() is not None, 10)
8282

83+
# Run a file-change check test
84+
# This test has Tini spawn a long sleep, similar to above, at which point, we briefly
85+
# sleep ourselves and touch a file underneath
86+
if not args_disabled:
87+
print "Running file-change tests"
88+
t_file = os.path.join(src, "test", ".file_test")
89+
try:
90+
os.unlink(t_file)
91+
except:
92+
pass
93+
94+
p = subprocess.Popen([tini, "-S", "{0}".format(signal.SIGUSR1), "-F", t_file, "sleep", "1000"],
95+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
96+
busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 1, 10)
97+
with open(t_file, 'w') as f:
98+
f.write('{}'.format(time.time()))
99+
busy_wait(lambda: p.poll() is not None, 10)
100+
101+
p = subprocess.Popen([tini, "-S", "{0}".format(signal.SIGUSR1), "-F", t_file, "sleep", "1000"],
102+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
103+
busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 1, 10)
104+
with open(t_file, 'w') as f:
105+
f.write('{}'.format(time.time()))
106+
busy_wait(lambda: p.poll() is not None, 10)
107+
108+
p = subprocess.Popen([tini, "-S", "{0}".format(signal.SIGUSR1), "-F", t_file, "sleep", "1000"],
109+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
110+
busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 1, 10)
111+
os.unlink(t_file)
112+
busy_wait(lambda: p.poll() is not None, 10)
113+
83114
# Run failing test. Force verbosity to 1 so we see the subreaper warning
84115
# regardless of whether MINIMAL is set.
85116
print "Running zombie reaping failure test (Tini should warn)"

0 commit comments

Comments
 (0)