#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define LOGS(lvl, fmt) \
if ((lvl) >= LOG_LEVEL) fprintf(stderr, "dcrond: " fmt "\n")
#define LOGV(lvl, fmt, ...) \
if ((lvl) >= LOG_LEVEL) fprintf(stderr, "dcrond: " fmt "\n", __VA_ARGS__)
#define LOGS_ERRNO(lvl, fmt) LOGV(lvl, fmt " - %d: %s", errno, strerror(errno))
#define LOGV_ERRNO(lvl, fmt, ...) LOGV(lvl, fmt " - %d: %s", __VA_ARGS__, errno, strerror(errno))
static unsigned LOG_LEVEL = 0;
static volatile int CAUGHT_SIGNAL = 0;
static volatile int CAUGHT_ALARM = 0;
/**
* Set the first terminating signal caught to initiate shutdown.
* Also set that the next alarm call has expired.
*/
static void signal_handler(int sig) {
if (sig == SIGALRM) {
++CAUGHT_ALARM;
} else if (sig != SIGCHLD && CAUGHT_SIGNAL == 0) {
CAUGHT_SIGNAL = sig;
}
}
/**
* Register INT, QUIT, TERM and ALRM with exclusive handler mask and without restart.
*/
static int register_handlers() {
struct sigaction sa = {0};
sigemptyset(&sa.sa_mask);
sa.sa_handler = SIG_IGN;
sigaction(SIGHUP, &sa, NULL);
sigaction(SIGPIPE, &sa, NULL);
sigaddset(&sa.sa_mask, SIGINT);
sigaddset(&sa.sa_mask, SIGQUIT);
sigaddset(&sa.sa_mask, SIGTERM);
sigaddset(&sa.sa_mask, SIGALRM);
sa.sa_handler = &signal_handler;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGALRM, &sa, NULL);
return 0;
}
/**
* Waiting for any signal received in the meanwhile.
* This avoids the small race between checking for signals and entering a
* blocking sleep() call.
* Using signal- and pid-fd APIs or sigtimedwait as alternative solution is
* not very POSIX/portable.
* Passing the last checked signal stack generation allows to not block when
* there are already changes pending.
* Returning -1 in case of (not expected) errors or even integer overflows are
* not really a problem here.
*/
static int signal_wait(int old_alarm) {
sigset_t ss;
sigemptyset(&ss);
sigaddset(&ss, SIGINT);
sigaddset(&ss, SIGQUIT);
sigaddset(&ss, SIGTERM);
sigaddset(&ss, SIGALRM);
sigset_t ss_old;
if (sigprocmask(SIG_BLOCK, &ss, &ss_old) == -1) {
LOGS_ERRNO(1, "Cannot temporarily block signals");
return -1;
}
if (CAUGHT_SIGNAL || CAUGHT_ALARM != old_alarm) {
(void)sigprocmask(SIG_UNBLOCK, &ss, NULL);
return CAUGHT_ALARM;
}
if (sigsuspend(&ss_old) == -1 && errno != EINTR) {
LOGS_ERRNO(1, "Cannot suspend for signals");
return -1;
}
if (sigprocmask(SIG_UNBLOCK, &ss, NULL) == -1) {
LOGS_ERRNO(1, "Cannot unblock signals");
return -1;
}
return CAUGHT_ALARM;
}
/**
* Open /dev/null and dup() it to given file descriptor.
* Prevents for example hanging reads from stdin, and simply closing could lead
* to errors or confusion upon reassigning fd 0.
* Nonzero upon error.
*/
static int dup_null(int fd, int mode) {
const int nullfd = open("/dev/null", mode); // no CLOEXEC
if (nullfd == -1) {
LOGV_ERRNO(1, "Cannot open /dev/null for %d", fd);
return -1;
} else if (dup2(nullfd, fd) == -1) {
LOGV_ERRNO(1, "Cannot dup /dev/null to %d", fd);
return -1;
} else if (close(nullfd) == -1) {
LOGV_ERRNO(1, "Cannot close temporary /dev/null fd %d for %d", nullfd, fd);
return -1;
} else {
return 0;
}
}
/*
* Main loop running the given command in intervals.
*/
static int run(unsigned delay, unsigned interval, int ignore_errors, const char* const command) {
LOGV(0, "Starting up with %u+%us for command: %s", delay, interval, command);
int alarm_generation = 0;
if (delay > 0) {
alarm(delay);
alarm_generation = signal_wait(alarm_generation);
}
while (CAUGHT_SIGNAL == 0) {
const int rv = system(command); // NOLINT(cert-env33-c)
if (rv == -1) {
LOGS_ERRNO(1, "Command shell failed, stopping");
return 127;
} else if (WIFSIGNALED(rv)) {
if (ignore_errors) {
LOGV(1, "Command exited due to signal %d, ignoring", WTERMSIG(rv));
} else {
LOGV(1, "Command exited due to signal %d, stopping", WTERMSIG(rv));
return 128 + WTERMSIG(rv);
}
} else if (WIFEXITED(rv) && WEXITSTATUS(rv) != 0) {
if (ignore_errors) {
LOGV(1, "Command exited with status %d, ignoring", WEXITSTATUS(rv));
} else {
LOGV(1, "Command exited with status %d, stopping", WEXITSTATUS(rv));
return WEXITSTATUS(rv);
}
} else if (rv != 0) {
if (ignore_errors) {
LOGV(1, "Command failed with status %d, ignoring", rv);
} else {
LOGV(1, "Command failed with status %d, stopping", rv);
return rv < 255 ? rv : 255;
}
} else {
LOGV(0, "Success: %s", command);
}
if (interval > 0) {
alarm(interval);
alarm_generation = signal_wait(alarm_generation);
}
}
LOGV(0, "Received signal %d, stopping", CAUGHT_SIGNAL);
return 0;
}
static int usage(const char* name) {
LOGV(1, "Usage: %s [-q...] [-d delay] -i interval -- [-]commandline", name && *name ? name : "$0");
return 1;
}
/**
* Main, argument parsing.
*/
int main(int argc, char** argv) {
int delay = 0;
int interval = -1;
int opt;
while ((opt = getopt(argc, argv, "qd:i:")) != -1) {
switch (opt) {
case 'q':
LOG_LEVEL++;
break;
case 'd':
delay = atoi(optarg); // NOLINT(cert-err34-c)
if (delay == 0 && strcmp(optarg, "0") != 0) return usage(argv[0]);
break;
case 'i':
interval = atoi(optarg); // NOLINT(cert-err34-c)
if (interval == 0 && strcmp(optarg, "0") != 0) return usage(argv[0]);
break;
default:
return usage(argv[0]);
}
}
if (optind + 1 != argc) {
LOGS(1, "Expecting single commandline");
return usage(argv[0]);
}
const char* command = argv[argc - 1];
int ignore_errors = 0;
if (command[0] == '-') {
command++;
ignore_errors = 1;
}
if (dup_null(0, O_RDONLY) != 0) {
return 1;
}
if (LOG_LEVEL >= 3) {
if (dup_null(1, O_WRONLY) != 0 || dup_null(2, O_WRONLY) != 0) {
return 1;
}
}
if (register_handlers() != 0) {
return 1;
}
return run(delay >= 0 ? (unsigned)delay : 0, interval >= 0 ? (unsigned)interval : 0, ignore_errors, command);
}