Remove wheel submodule - will re-add later with new URL
[dmon:dmon.git] / dmon.c
1 /*
2  * dmon.c
3  * Copyright (C) 2010 Adrian Perez <aperez@igalia.com>
4  *
5  * Distributed under terms of the MIT license.
6  */
7
8 #define _BSD_SOURCE             /* getloadavg() */
9 #define _POSIX_C_SOURCE 199309L /* nanosleep()  */
10
11 #include "wheel.h"
12 #include "task.h"
13 #include "util.h"
14 #include <sys/types.h>
15 #include <sys/wait.h>
16 #include <stdlib.h>
17 #include <unistd.h>
18 #include <string.h>
19 #include <fcntl.h>
20 #include <errno.h>
21 #include <time.h>
22
23
24 #ifdef NO_MULTICALL
25 # define dmon_main main
26 #endif /* NO_MULTICALL */
27
28
29 static int           log_fds[2]   = { -1, -1 };
30 static w_io_t       *status_io    = NULL;
31 static task_t        cmd_task     = TASK;
32 static task_t        log_task     = TASK;
33 static float         load_low     = 0.0f;
34 static float         load_high    = 0.0f;
35 static wbool         success_exit = W_NO;
36 static wbool         log_signals  = W_NO;
37 static wbool         cmd_signals  = W_NO;
38 static unsigned long cmd_timeout  = 0;
39 static unsigned long cmd_interval = 0;
40 static int           check_child  = 0;
41 static int           running      = 1;
42 static int           paused       = 0;
43 static wbool         nodaemon     = W_NO;
44 static char         *status_path  = NULL;
45 static char         *pidfile_path = NULL;
46
47
48 static const struct {
49     const char *name;
50     int         code;
51 } forward_signals[] = {
52     { "CONT", SIGCONT },
53     { "ALRM", SIGALRM },
54     { "QUIT", SIGQUIT },
55     { "USR1", SIGUSR1 },
56     { "USR2", SIGUSR2 },
57     { "HUP" , SIGHUP  },
58     { "NONE", NO_SIGNAL },
59     { "STOP", SIGSTOP },
60     { "TERM", SIGTERM },
61     { "INT" , SIGINT  },
62     { "KILL", SIGKILL },
63     { NULL  , NO_SIGNAL },
64 };
65
66
67 #define almost_zerof(_v)  ((_v) < 0.000000001f)
68
69 #define log_enabled   (log_fds[0] != -1)
70 #define load_enabled  (!almost_zerof (load_high))
71
72
73 static inline void
74 _write_status (const char *fmt, ...)
75 {
76     va_list arg;
77
78     w_assert (status_io);
79
80     va_start (arg, fmt);
81     w_io_formatv (status_io, fmt, arg);
82     va_end (arg);
83 }
84
85 #define write_status(_args) \
86     if (status_io != NULL)    \
87         _write_status _args
88
89
90 #define task_action_dispatch_and_write_status(_what, _task)         \
91     do {                                                            \
92         int __pidafter__ = 0;                                       \
93         if (status_io != NULL) {                                    \
94             switch ((_task)->action) {                              \
95                 case A_NONE:                                        \
96                     break;                                          \
97                 case A_START:                                       \
98                     _write_status ("$s start ", (_what));           \
99                     __pidafter__ = 1;                               \
100                     break;                                          \
101                 case A_STOP:                                        \
102                     _write_status ("$s stop $L\n", (_what),         \
103                                    (unsigned long) ((_task)->pid)); \
104                     break;                                          \
105                 case A_SIGNAL:                                      \
106                     _write_status ("$s signal $L $i\n",             \
107                                    (unsigned long) ((_task)->pid),  \
108                                    (_task)->signal);                \
109                     break;                                          \
110             }                                                       \
111         }                                                           \
112         task_action_dispatch (_task);                               \
113         if (__pidafter__ && status_io != NULL)                      \
114             _write_status ("$L\n", (unsigned long) ((_task)->pid)); \
115     } while (0)
116
117
118 #ifdef _DEBUG_PRINT
119 const char*
120 signal_to_name (int signum)
121 {
122     static const char *unknown = "(unknown)";
123     int i = 0;
124
125     while (forward_signals[i].name != NULL) {
126         if (forward_signals[i].code == signum)
127             return forward_signals[i].name;
128         i++;
129     }
130     return unknown;
131 }
132 #endif /* _DEBUG_PRINT */
133
134
135 static int
136 reap_and_check (void)
137 {
138     int status;
139     pid_t pid;
140
141     w_debug (("waiting for a children to reap...\n"));
142
143     pid = waitpid (-1, &status, WNOHANG);
144
145     if (pid == cmd_task.pid) {
146         w_debug (("reaped cmd process $I\n", (unsigned) pid));
147
148         write_status (("cmd exit $L $i\n", (unsigned long) pid, status));
149
150         cmd_task.pid = NO_PID;
151
152         /*
153          * If exit-on-success was request AND the process exited ok,
154          * then we do not want to respawn, but to gracefully shutdown.
155          */
156         if (success_exit && WIFEXITED (status) && WEXITSTATUS (status) == 0) {
157             w_debug (("cmd process ended successfully, will exit\n"));
158             running = 0;
159         }
160         else {
161             cmd_task.action = A_START;
162         }
163         return status;
164     }
165     else if (log_enabled && pid == log_task.pid) {
166         w_debug (("reaped log process $I\n", (unsigned) pid));
167
168         write_status (("log exit $L $i\n", (unsigned long) pid, status));
169
170         log_task.action = A_START;
171         log_task.pid = NO_PID;
172     }
173     else {
174         w_debug (("reaped unknown process $I", (unsigned) pid));
175     }
176
177     /*
178      * For cases where a return status is not meaningful (PIDs other than
179      * that of the command being run) just return some invalid return code
180      * value.
181      */
182     return -1;
183 }
184
185
186 static void
187 handle_signal (int signum)
188 {
189     unsigned i = 0;
190
191     w_debug (("handle signal $i ($s)\n", signum, signal_to_name (signum)));
192
193     /* Receiving INT/TERM signal will stop gracefully */
194     if (signum == SIGINT || signum == SIGTERM) {
195         running = 0;
196         return;
197     }
198
199     /* Handle CHLD: check children */
200     if (signum == SIGCHLD) {
201         check_child = 1;
202         return;
203     }
204
205     /*
206      * If we have a maximum time to run the process, and we receive SIGALRM,
207      * then the timeout was reached. As per signal(7) it is safe to kill(2)
208      * the process from a signal handler, so we do that and then mark it for
209      * respawning.
210      */
211     if (cmd_timeout && signum == SIGALRM) {
212         write_status (("cmd timeout $L\n", (unsigned long) cmd_task.pid));
213         task_action (&cmd_task, A_STOP);
214         cmd_task.action = A_START;
215         alarm (cmd_timeout);
216         return;
217     }
218
219     while (forward_signals[i].code != NO_SIGNAL) {
220         if (signum == forward_signals[i++].code)
221             break;
222     }
223
224     if (signum != NO_SIGNAL) {
225         /* Try to forward signals */
226         if (cmd_signals) {
227             w_debug (("delayed signal $i for cmd process\n", signum));
228             cmd_task.action = A_SIGNAL;
229             cmd_task.signal = signum;
230         }
231         if (log_signals && log_enabled) {
232             w_debug (("delayed signal $i for log process\n", signum));
233             log_task.action = A_SIGNAL;
234             log_task.signal = signum;
235         }
236     }
237
238     return;
239 }
240
241
242
243 static void
244 setup_signals (void)
245 {
246     unsigned i = 0;
247     struct sigaction sa;
248
249     sa.sa_handler = handle_signal;
250     sa.sa_flags = SA_NOCLDSTOP;
251     sigfillset(&sa.sa_mask);
252
253     while (forward_signals[i].code!= NO_SIGNAL) {
254         safe_sigaction (forward_signals[i].name,
255                         forward_signals[i].code, &sa);
256         i++;
257     }
258
259     safe_sigaction ("CHLD", SIGCHLD, &sa);
260     safe_sigaction ("TERM", SIGTERM, &sa);
261     safe_sigaction ("INT" , SIGINT , &sa);
262 }
263
264
265 static w_opt_status_t
266 _environ_option (const w_opt_context_t *ctx)
267 {
268     char *equalsign;
269     char *varname;
270
271     w_assert (ctx);
272     w_assert (ctx->argument);
273     w_assert (ctx->argument[0]);
274
275     if ((equalsign = strchr (ctx->argument[0], '=')) == NULL) {
276         unsetenv (ctx->argument[0]);
277     }
278     else {
279         varname = w_str_dupl (ctx->argument[0],
280                               equalsign - ctx->argument[0]);
281         setenv (varname, equalsign + 1, 1);
282     }
283     return W_OPT_OK;
284 }
285
286
287 static w_opt_status_t
288 _rlimit_option (const w_opt_context_t *ctx)
289 {
290     long value;
291     int status;
292     int limit;
293
294     w_assert (ctx);
295     w_assert (ctx->argument);
296     w_assert (ctx->argument[0]);
297
298     status = parse_limit_arg (ctx->argument[0], &limit, &value);
299     if (status < 0)
300         return W_OPT_EXIT_OK;
301     if (status)
302         return W_OPT_BAD_ARG;
303
304     safe_setrlimit (limit, value);
305     return W_OPT_OK;
306 }
307
308
309 static w_opt_status_t
310 _store_uidgids_option (const w_opt_context_t *ctx)
311 {
312     w_assert (ctx);
313     w_assert (ctx->userdata);
314     w_assert (ctx->argument);
315     w_assert (ctx->argument[0]);
316
317     return (parse_uidgids (ctx->argument[0], ctx->option->extra))
318             ? W_OPT_BAD_ARG
319             : W_OPT_OK;
320 }
321
322
323 static w_opt_status_t
324 _config_option (const w_opt_context_t *ctx)
325 {
326     w_assert (ctx);
327     w_assert (ctx->argv);
328     w_assert (ctx->argv[0]);
329
330     w_io_format (w_stderr,
331                  "$s: Option --config/-C must be the first one specified\n",
332                  ctx->argv[0]);
333
334     return W_OPT_EXIT_FAIL;
335 }
336
337
338 static const w_opt_t dmon_options[] = {
339     { 1, 'C' | W_OPT_CLI_ONLY, "config", _config_option, NULL,
340         "Read options from the specified configuration file. If given, this option "
341         "must be the first one in the command line." },
342
343     { 1, 'I', "write-info", W_OPT_STRING, &status_path,
344         "Write information on process status to the given file. "
345         "Sockets and FIFOs may be used." },
346
347     { 1, 'p', "pid-file", W_OPT_STRING, &pidfile_path,
348         "Write PID to a file in the given path." },
349
350     { 0, 'n', "no-daemon", W_OPT_BOOL, &nodaemon,
351         "Do not daemonize, stay in foreground." },
352
353     { 0, 'e', "stderr-redir", W_OPT_BOOL, &cmd_task.redir_errfd,
354         "Redirect command's standard error stream to its standard "
355         "output stream." },
356
357     { 0, 's', "cmd-sigs", W_OPT_BOOL, &cmd_signals,
358         "Forward signals to command process." },
359
360     { 0, 'S', "log-sigs", W_OPT_BOOL, &log_signals,
361         "Forward signals to log process." },
362
363     { 1, 'E', "environ", _environ_option, NULL,
364         "Define an environment variable, or if no value is given, "
365         "delete it. This option can be specified multiple times." },
366
367     { 1, 'r', "limit", _rlimit_option, NULL,
368         "Sets a resource limit, given as 'name=value'. This option "
369         "can be specified multiple times. Use '-r help' for a list." },
370
371     { 1, 'u', "cmd-user", _store_uidgids_option, &cmd_task.user,
372         "User and (optionally) groups to run the command as. Format "
373         "is 'user[:group1[:group2[:...groupN]]]'." },
374
375     { 1, 'U', "log-user", _store_uidgids_option, &log_task.user,
376         "User and (optionally) groups to run the log process as. "
377         "Format is 'user[:group1[:group2[:...groupN]]]'." },
378
379     { 0, '1', "once", W_OPT_BOOL, &success_exit,
380         "Exit if command exits with a zero return code. The process "
381         "will be still respawned when it exits with a non-zero code." },
382
383     { 1, 't', "timeout", time_period_option, &cmd_timeout,
384         "If command execution takes longer than the time specified "
385         "the process will be killed and started again." },
386
387     { 1, 'i', "interval", time_period_option, &cmd_interval,
388         "Time to wait between successful command executions. When "
389         "exit code is non-zero, the interval is ignored and the "
390         "command is executed again as soon as possible." },
391
392     { 1, 'L', "load-high", W_OPT_FLOAT, &load_high,
393         "Stop process when system load surpasses the given value." },
394
395     { 1, 'l', "load-low", W_OPT_FLOAT, &load_low,
396         "Resume process execution when system load drops below the "
397         "given value. If not given, defaults to half the value passed "
398         "to '-l'." },
399
400     W_OPT_END
401 };
402
403
404 int
405 dmon_main (int argc, char **argv)
406 {
407         w_io_t *pidfile_io = NULL;
408         char *opts_env = NULL;
409         wbool success;
410         unsigned i, consumed;
411
412     /* Check for "-C configfile" given in the command line. */
413     if (argc > 2 && ((argv[1][0] == '-' &&
414                       argv[1][1] == 'C' &&
415                       argv[1][2] == '\0') ||
416                      !strcmp ("--config", argv[1])))
417
418     {
419         w_io_t *cfg_io = NULL;
420         char *err_msg = NULL;
421
422         if ((cfg_io = w_io_unix_open (argv[2], O_RDONLY, 0)) == NULL)
423             w_die ("$s: Could not open file '$s', $E\n", argv[0], argv[2]);
424
425         success = w_opt_parse_io (dmon_options, cfg_io, &err_msg);
426         w_obj_unref (cfg_io);
427
428         if (!success || err_msg)
429             w_die ("$s: Error parsing '$s' at line $s\n", argv[0], argv[2], err_msg);
430
431         replace_args_shift (2, &argc, &argv);
432     }
433
434     if ((opts_env = getenv ("DMON_OPTIONS")) != NULL)
435         replace_args_string (opts_env, &argc, &argv);
436
437     i = consumed = w_opt_parse (dmon_options, NULL, NULL,
438                                 "cmd [cmd-options] [ -- "
439                                 "log-cmd [log-cmd-options]]",
440                                 argc, argv);
441
442     w_debug (("w_opt_parse consumed $I arguments\n", consumed));
443
444     if (status_path) {
445         status_io = w_io_unix_open (status_path, O_WRONLY | O_CREAT | O_APPEND, 0666);
446         if (!status_io)
447             w_die ("$s: Cannot open '$s' for writing, $E\n", argv[0], status_path);
448     }
449
450     if (cmd_interval && success_exit)
451         w_die ("$s: Options '-i' and '-1' cannot be used together.\n", argv[0]);
452
453     if (load_enabled && almost_zerof (load_low))
454         load_low = load_high / 2.0f;
455
456         cmd_task.argv = argv + consumed;
457
458         /* Skip over until "--" is found */
459         while (i < (unsigned) argc && strcmp (argv[i], "--") != 0) {
460                 cmd_task.argc++;
461                 i++;
462         }
463
464         /* There is a log command */
465         if (i < (unsigned) argc && strcmp (argv[i], "--") == 0) {
466                 log_task.argc = argc - cmd_task.argc - consumed - 1;
467                 log_task.argv = argv + argc - log_task.argc;
468         log_task.argv[log_task.argc] = NULL;
469         }
470
471     cmd_task.argv[cmd_task.argc] = NULL;
472
473     if (log_task.argc > 0) {
474         if (pipe (log_fds) != 0) {
475             w_die ("$s: Cannot create pipe: $E\n", argv[0]);
476         }
477         w_debug (("pipe_read = $i, pipe_write = $i\n", log_fds[0], log_fds[1]));
478         fd_cloexec (log_fds[0]);
479         fd_cloexec (log_fds[1]);
480     }
481
482 #ifdef _DEBUG_PRINT
483     {
484         char **xxargv = cmd_task.argv;
485         w_io_format (w_stderr, "cmd:");
486         while (*xxargv) w_io_format (w_stderr, " $s", *xxargv++);
487         w_io_format (w_stderr, "\n");
488         if (log_enabled) {
489             char **xxargv = log_task.argv;
490             w_io_format (w_stderr, "log:");
491             while (*xxargv) w_io_format (w_stderr, " $c", *xxargv++);
492             w_io_format (w_stderr, "\n");
493         }
494     }
495 #endif /* _DEBUG_PRINT */
496
497     if (cmd_task.argc == 0)
498         w_die ("$s: No command to run given.\n", argv[0]);
499
500     if (pidfile_path) {
501         pidfile_io = w_io_unix_open (pidfile_path,
502                                      O_TRUNC | O_CREAT | O_WRONLY,
503                                      0666);
504         if (!pidfile_io) {
505             w_die ("$s: cannot open '$s' for writing: $E\n",
506                    argv[0], pidfile_path);
507         }
508     }
509
510     if (!nodaemon)
511         become_daemon ();
512
513     /* We have a valid file descriptor: write PID */
514     if (pidfile_io) {
515         w_io_format (pidfile_io, "$L\n", (unsigned long) getpid ());
516         w_obj_unref (pidfile_io);
517     }
518
519     setup_signals ();
520     alarm (cmd_timeout);
521
522     cmd_task.write_fd = log_fds[1];
523     log_task.read_fd  = log_fds[0];
524
525     while (running) {
526         w_debug ((">>> loop iteration\n"));
527         if (check_child) {
528             int retcode = reap_and_check ();
529
530             /*
531              * Wait the specified timeout but DO NOT use safe_sleep(): here
532              * we want an interruptible sleep-wait so reaction to signals is
533              * quick, which we definitely want for SIGINT/SIGTERM.
534              */
535             if (cmd_interval && !success_exit && retcode == 0) {
536                 int retval;
537                 struct timespec ts;
538                 ts.tv_sec = cmd_interval;
539                 ts.tv_nsec = 0;
540
541                 do {
542                     retval = nanosleep (&ts, &ts);
543                     w_debug (("nanosleep -> $i\n", retval));
544                 } while (retval == -1 && errno == EINTR && running);
545             }
546
547             /*
548              * Either handling signals which interrupt the previous loop,
549              * or reap_and_check() may request stopping on successful exit
550              */
551             if (!running) {
552                 cmd_task.action = A_NONE;
553                 break;
554             }
555         }
556
557         task_action_dispatch_and_write_status ("cmd", &cmd_task);
558         if (log_enabled)
559             task_action_dispatch_and_write_status ("log", &log_task);
560
561         if (load_enabled) {
562             double load_cur;
563
564             w_debug (("checking load after sleeping 1s\n"));
565             interruptible_sleep (1);
566
567             if (getloadavg (&load_cur, 1) == -1)
568                 w_die ("$s: Could not get load average!\n");
569
570             if (paused) {
571                 /* If the current load dropped below load_low -> resume */
572                 if (load_cur <= load_low) {
573                     w_debug (("resuming... "));
574                     task_signal (&cmd_task, SIGCONT);
575                     write_status (("cmd resume $L\n", (unsigned long) cmd_task.pid));
576                     paused = 0;
577                 }
578             }
579             else {
580                 /* If the load went above load_high -> pause */
581                 if (load_cur > load_high) {
582                     w_debug (("pausing... "));
583                     task_signal (&cmd_task, SIGSTOP);
584                     write_status (("cmd pause $L\n", (unsigned long) cmd_task.pid));
585                     paused = 1;
586                 }
587             }
588         }
589         else {
590             /* Wait for signals to arrive. */
591             w_debug (("waiting for signals to come...\n"));
592             pause ();
593         }
594     }
595
596     w_debug (("exiting gracefully...\n"));
597
598     if (cmd_task.pid != NO_PID) {
599         write_status (("cmd stop $L\n", (unsigned long) cmd_task.pid));
600         task_action (&cmd_task, A_STOP);
601     }
602     if (log_enabled && log_task.pid != NO_PID) {
603         write_status (("log stop $L\n", (unsigned long) log_task.pid));
604         task_action (&log_task, A_STOP);
605     }
606
607     if (status_io) {
608         w_obj_unref (status_io);
609         status_io = NULL;
610     }
611
612         exit (EXIT_SUCCESS);
613 }
614
615 /* vim: expandtab shiftwidth=4 tabstop=4
616  */