/* rund: daemon supervisor * Copyright (C) 2015-2016 Daniel Beer * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * Compile with: * * gcc -O1 -Wall -o rund rund.c -lrt * * Type "./rund --help" for options. * * Updated 21-Sep-2015: set GID and supplementary groups when dropping * privileges. Add support for kernels older than 2.6.27. * * Updated 23-Dec-2015: fix race in shutdown waitpid() checking (added * sig_run to enable super_exit to recheck on SIGCHLD delivery). * * Updated 11-Feb-2016: added periodic fsync(). * * Updated 25-Aug-2016: create a new session for the supervisee and kill * the entire process group when a stop is requested. * * Updated 17-Apr-2018: set supplementary groups from /etc/group. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /************************************************************************ * Option parser */ typedef int flags_t; static const flags_t FLAG_ONESHOT = 0x01; struct config { const char *log_file; off_t log_size; unsigned int log_count; const char *runas_user; const char *lock_file; const char *sock_path; unsigned int restart_delay; unsigned int sigkill_delay; unsigned int log_sync; int flags; const char *action; int argc; char **argv; }; static struct config cfg = { .log_size = 1048576, .log_count = 4, .sock_path = "rund-control", .restart_delay = 2000, .sigkill_delay = 30000, .log_sync = 10 }; /************************************************************************ * POSIX helpers */ static void exit_clean(int code); static int write_all(int fd, const uint8_t *data, size_t len) { while (len) { int ret = write(fd, data, len); if (ret < 0) { if (errno == EAGAIN || errno == EINTR) continue; return -1; } data += ret; len -= ret; } return 0; } static int read_all(int fd, uint8_t *data, size_t len) { while (len) { int ret = read(fd, data, len); if (!ret) { errno = EPIPE; return -1; } if (ret < 0) { if (errno == EAGAIN || errno == EINTR) continue; return -1; } data += ret; len -= ret; } return 0; } /************************************************************************ * Log */ struct log_state { int fd; off_t size; }; static struct log_state log = { .fd = -1 }; static void log_init(void) { if (!cfg.log_file) return; log.fd = open(cfg.log_file, O_WRONLY | O_NOCTTY | O_CREAT, 0600); if (log.fd < 0) { fprintf(stderr, "log_init: %s: open: %s\n", cfg.log_file, strerror(errno)); exit_clean(-1); } log.size = lseek(log.fd, 0, SEEK_END); if (log.size == (off_t)(-1)) { fprintf(stderr, "log_init: %s: open: %s\n", cfg.log_file, strerror(errno)); exit_clean(-1); } } static void log_close(void) { if (log.fd >= 0) { fsync(log.fd); close(log.fd); } } static void log_rotate(void) { unsigned int i; close(log.fd); log.fd = -1; log.size = 0; for (i = cfg.log_count; i > 0; i--) { char to[1024]; char from[1024]; if (i > 1) snprintf(from, sizeof(from), "%s.%d", cfg.log_file, i - 1); else snprintf(from, sizeof(from), "%s", cfg.log_file); snprintf(to, sizeof(to), "%s.%d", cfg.log_file, i); /* Rename, ignoring errors */ rename(from, to); } /* Reopen, ignoring errors */ log.fd = open(cfg.log_file, O_WRONLY | O_NOCTTY | O_CREAT, 0644); } static void log_printf(const char *fmt, ...) { time_t now; char tbuf[128]; char fbuf[1024]; char text[1024]; int len; va_list ap; if (log.fd < 0) return; va_start(ap, fmt); vsnprintf(fbuf, sizeof(fbuf), fmt, ap); va_end(ap); now = time(NULL); strftime(tbuf, sizeof(tbuf), "%a %d %b %Y %H:%M:%S %Z", localtime(&now)); snprintf(text, sizeof(text), "[%s] %s\n", tbuf, fbuf); len = strlen(text); if (!write_all(log.fd, (uint8_t *)text, len)) { log.size += len; if (log.size >= cfg.log_size) log_rotate(); } } /************************************************************************ * Clock */ typedef uint64_t ticks_t; static ticks_t clock_get(void) { struct timespec res; if (clock_gettime(CLOCK_MONOTONIC, &res) < 0) { perror("clock_gettime"); exit_clean(-1); } return res.tv_sec * 1000LL + res.tv_nsec / 1000000LL; } /************************************************************************ * Poller */ struct poller_state { fd_set rfds; ticks_t deadline; int max_fd; }; static struct poller_state poller; static void poller_init(void) { FD_ZERO(&poller.rfds); } static void poller_rfd(int fd) { FD_SET(fd, &poller.rfds); if (fd > poller.max_fd) poller.max_fd = fd; } static void poller_deadline(ticks_t when) { if (!poller.deadline || when < poller.deadline) poller.deadline = when; } static void poller_wait(void) { struct timeval tv; struct timeval *timeout = NULL; if (poller.deadline) { ticks_t now = clock_get(); ticks_t delay; if (now >= poller.deadline) goto done; delay = poller.deadline - now; tv.tv_sec = delay / 1000LL; tv.tv_usec = (delay % 1000LL) * 1000; timeout = &tv; } if (select(poller.max_fd + 1, &poller.rfds, NULL, NULL, timeout) < 0) log_printf("poller_wait: select: %s", strerror(errno)); done: FD_ZERO(&poller.rfds); poller.max_fd = 0; poller.deadline = 0; } /************************************************************************ * Log syncer */ static ticks_t lsync_last; static void lsync_init(void) { lsync_last = clock_get(); } static inline ticks_t lsync_next(void) { return lsync_last + cfg.log_sync * 1000; } static void lsync_run(void) { const ticks_t now = clock_get(); if (!cfg.log_sync || (log.fd < 0)) return; if (now >= lsync_next()) { lsync_last = now; fsync(log.fd); } poller_deadline(lsync_next()); } /************************************************************************ * Signal catcher */ static int sig_fd; static void sig_init(void) { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGCHLD); sigaddset(&mask, SIGTERM); if (sigprocmask(SIG_BLOCK, &mask, NULL)) { perror("sigprocmask"); exit_clean(-1); } sig_fd = signalfd(-1, &mask, 0); if (sig_fd < 0) { perror("signalfd"); exit_clean(-1); } fcntl(sig_fd, F_SETFL, fcntl(sig_fd, F_GETFL) | O_NONBLOCK); } static void sig_preexec(void) { sigset_t mask; sigemptyset(&mask); if (sigprocmask(SIG_SETMASK, &mask, NULL)) { perror("sigprocmask"); exit(-1); } close(sig_fd); } static int sig_run(void) { struct signalfd_siginfo info; int ret = 0; while (read(sig_fd, &info, sizeof(info)) > 0) { log_printf("Received signal %d", info.ssi_signo); if (info.ssi_signo == SIGTERM) ret = 1; } poller_rfd(sig_fd); return ret; } /************************************************************************ * Control socket command/reply */ typedef uint8_t ctrl_msg_t; static const ctrl_msg_t CTRL_CMD_STATUS = 0x00; static const ctrl_msg_t CTRL_CMD_STOP = 0x01; static const ctrl_msg_t CTRL_STATUS_RUNNING = 0x01; /************************************************************************ * Control socket client */ static int ctrl_client(ctrl_msg_t in) { int fd = socket(PF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr; uint8_t out = 0; if (fd < 0) { perror("ctrl_client: socket"); exit_clean(-1); } addr.sun_family = AF_UNIX; strncpy(addr.sun_path, cfg.sock_path, sizeof(addr.sun_path)); addr.sun_path[sizeof(addr.sun_path) - 1] = 0; if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { fprintf(stderr, "ctrl_client: %s: connect: %s\n", addr.sun_path, strerror(errno)); return -1; } /* Exchange data */ if (write_all(fd, &in, 1) < 0) { fprintf(stderr, "ctrl_client: %s: write: %s\n", addr.sun_path, strerror(errno)); exit_clean(-1); } if (read_all(fd, &out, 1) < 0) { fprintf(stderr, "ctrl_client: %s: read: %s\n", addr.sun_path, strerror(errno)); exit_clean(-1); } /* Wait for EOF */ for (;;) { char buf[4096]; int ret = read(fd, buf, sizeof(buf)); if (!ret) break; if (errno == EAGAIN || errno == EINTR) continue; fprintf(stderr, "ctrl_client: read: %s: %s\n", addr.sun_path, strerror(errno)); exit_clean(-1); } return out; } /************************************************************************ * Control socket server */ static int ctrl_fd = -1; static void ctrl_server_init(void) { struct sockaddr_un addr; ctrl_fd = socket(PF_UNIX, SOCK_STREAM, 0); if (ctrl_fd < 0) { perror("ctrl_server_init: socket"); exit_clean(-1); } addr.sun_family = AF_UNIX; strncpy(addr.sun_path, cfg.sock_path, sizeof(addr.sun_path)); addr.sun_path[sizeof(addr.sun_path) - 1] = 0; if (bind(ctrl_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { fprintf(stderr, "ctrl_server_init: %s: bind: %s\n", addr.sun_path, strerror(errno)); exit_clean(-1); } if (chmod(addr.sun_path, 0700) < 0) { fprintf(stderr, "ctrl_server_init: %s: fchmod: %s\n", addr.sun_path, strerror(errno)); exit_clean(-1); } if (listen(ctrl_fd, SOMAXCONN) < 0) { fprintf(stderr, "ctrl_server_init: %s: listen: %s\n", addr.sun_path, strerror(errno)); exit_clean(-1); } fcntl(ctrl_fd, F_SETFL, fcntl(ctrl_fd, F_GETFL) | O_NONBLOCK); } static void ctrl_server_preexec(void) { close(ctrl_fd); } static void ctrl_server_exit(void) { if (ctrl_fd >= 0) { close(ctrl_fd); unlink(cfg.sock_path); } } static int ctrl_server_run(ctrl_msg_t status) { for (;;) { struct sockaddr_un addr; socklen_t len = sizeof(addr); ctrl_msg_t request; int fd = accept(ctrl_fd, (struct sockaddr *)&addr, &len); if (fd < 0) break; if (read_all(fd, &request, 1) < 0) { close(fd); continue; } /* If stop requested, do not close the client socket */ if (request == CTRL_CMD_STOP) { log_printf("Stop command received"); write_all(fd, &status, 1); return 1; } if (request == CTRL_CMD_STATUS) write_all(fd, &status, 1); close(fd); } poller_rfd(ctrl_fd); return 0; } static int ctrl_server_check(void) { struct stat st; /* Check to see if the control socket already exists */ if (!stat(cfg.sock_path, &st) && S_ISSOCK(st.st_mode)) { /* Already running? */ if (ctrl_client(CTRL_CMD_STATUS) >= 0) return 1; fprintf(stderr, "warning: unlinking stale socket (%s)\n", cfg.sock_path); if (unlink(cfg.sock_path) < 0) { fprintf(stderr, "unlink: %s: %s\n", cfg.sock_path, strerror(errno)); exit_clean(-1); } } return 0; } /************************************************************************ * Daemon locker */ static int lock_fd = -1; static int lock_acquire(void) { if (!cfg.lock_file) return 0; lock_fd = open(cfg.lock_file, O_RDWR | O_NOCTTY | O_CREAT, 0600); if (lock_fd < 0) { fprintf(stderr, "lock_acquire: %s: open: %s\n", cfg.lock_file, strerror(errno)); exit_clean(-1); } if (flock(lock_fd, LOCK_EX | LOCK_NB) < 0) { if (errno == EWOULDBLOCK) return 1; fprintf(stderr, "lock_acquire: %s: flock: %s\n", cfg.lock_file, strerror(errno)); exit_clean(-1); } return 0; } static void lock_release(void) { if (lock_fd >= 0) close(lock_fd); } /************************************************************************ * Privilege dropping */ static void change_user(void) { struct passwd *pw; if (!cfg.runas_user) return; pw = getpwnam(cfg.runas_user); if (!pw) { fprintf(stderr, "Unknown user: %s\n", cfg.runas_user); exit(-1); } if (initgroups(cfg.runas_user, pw->pw_gid) < 0 || setgid(pw->pw_gid) < 0 || setuid(pw->pw_uid) < 0) { fprintf(stderr, "setuid: %s (%d/%d): %s\n", pw->pw_name, pw->pw_uid, pw->pw_gid, strerror(errno)); exit(-1); } } /************************************************************************ * Line reader */ struct reader { int fd; char text[1024]; size_t len; }; static void reader_start(struct reader *r, int fd) { r->fd = fd; r->len = 0; fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); } static int reader_run(struct reader *r, const char *name) { if (r->fd < 0) return 1; for (;;) { char buf[4096]; int ret = read(r->fd, buf, sizeof(buf)); int i; if (ret < 0) { if (errno == EAGAIN || errno == EINTR) break; log_printf("Stream %s error: %s", name, strerror(errno)); close(r->fd); r->fd = -1; return 1; } if (!ret) { log_printf("Stream %s EOF", name); close(r->fd); r->fd = -1; return 1; } for (i = 0; i < ret; i++) { char c = buf[i]; if (c == '\r') continue; if (c == '\n') { r->text[r->len] = 0; r->len = 0; log_printf("%s: %s", name, r->text); continue; } if (!(c >= 32 && c <= 126) || c == '\t') c = '?'; if (r->len + 1 < sizeof(r->text)) r->text[r->len++] = c; } } poller_rfd(r->fd); return 0; } /************************************************************************ * Process control */ struct proc_state { struct reader read_stdout; struct reader read_stderr; pid_t pid; int exited; }; static struct proc_state proc; static int proc_start(void) { int pout[2]; int perr[2]; if (proc.pid) return 0; if (pipe(pout) < 0) { log_printf("pipe: %s", strerror(errno)); return -1; } if (pipe(perr) < 0) { log_printf("pipe: %s", strerror(errno)); close(pout[0]); close(pout[1]); return -1; } proc.pid = fork(); if (proc.pid < 0) { log_printf("fork: %s", strerror(errno)); close(pout[0]); close(pout[1]); close(perr[0]); close(perr[1]); return -1; } if (!proc.pid) { setsid(); close(0); close(1); close(2); dup2(pout[1], 1); dup2(perr[1], 2); close(pout[0]); close(pout[1]); close(perr[0]); close(perr[1]); log_close(); sig_preexec(); ctrl_server_preexec(); lock_release(); change_user(); if (execvp(cfg.argv[0], cfg.argv) < 0) perror("execvp"); exit(1); } log_printf("Started process, PID = %d", proc.pid); close(pout[1]); close(perr[1]); proc.exited = 0; reader_start(&proc.read_stdout, pout[0]); reader_start(&proc.read_stderr, perr[0]); return 0; } static int proc_run(void) { int ret = 1; if (!proc.pid) return 1; if (!reader_run(&proc.read_stdout, "stdout")) ret = 0; if (!reader_run(&proc.read_stderr, "stderr")) ret = 0; if (!proc.exited) { int status; int r; r = waitpid(proc.pid, &status, WNOHANG); if (r < 0) { log_printf("waitpid: %s", strerror(errno)); } else if (r) { log_printf("Process %d exited with status %d", proc.pid, status); proc.exited = 1; } } if (!proc.exited) ret = 0; if (ret) proc.pid = 0; return ret; } /************************************************************************ * Daemon logic */ static void super_exit(void) { log_printf("Sending SIGTERM to child"); kill(-proc.pid, SIGTERM); if (cfg.sigkill_delay) { ticks_t deadline = clock_get() + cfg.sigkill_delay; while (clock_get() < deadline) { if (proc_run()) exit_clean(0); sig_run(); lsync_run(); poller_deadline(deadline); poller_wait(); } log_printf("Timed out, sending SIGKILL"); kill(-proc.pid, SIGKILL); } while (!proc_run()) { sig_run(); lsync_run(); poller_wait(); } exit_clean(0); } static void super_restart(void) { if (cfg.flags & FLAG_ONESHOT) { log_printf("One-shot execution requested. Exiting."); exit_clean(0); } if (cfg.restart_delay) { ticks_t deadline = clock_get() + cfg.restart_delay; log_printf("Waiting before restart"); while (clock_get() < deadline) { if (ctrl_server_run(0) || sig_run()) exit_clean(0); lsync_run(); poller_deadline(deadline); poller_wait(); } } } static void super_run(void) { proc_start(); for (;;) { if (ctrl_server_run(CTRL_STATUS_RUNNING) || sig_run()) super_exit(); if (proc_run()) break; lsync_run(); poller_wait(); } } /************************************************************************ * Actions */ static void exit_clean(int code) { ctrl_server_exit(); log_close(); lock_release(); exit(code); } static int action_start(void) { if (lock_acquire() || ctrl_server_check()) return 0; if (!cfg.argc) { fprintf(stderr, "No command has been given\n"); return -1; } poller_init(); log_init(); sig_init(); lsync_init(); ctrl_server_init(); if (daemon(1, 0) < 0) { perror("daemon"); exit(-1); } log_printf("Supervisor started"); for (;;) { super_run(); super_restart(); } return 0; } static int action_stop(void) { ctrl_client(CTRL_CMD_STOP); return 0; } static int action_status(void) { if (ctrl_client(CTRL_CMD_STATUS) < 0) return -1; return 0; } static int action_pstatus(void) { int s = ctrl_client(CTRL_CMD_STATUS); if (s < 0) return -1; if (s & CTRL_STATUS_RUNNING) return 0; return -1; } /************************************************************************ * Command-line parser */ static void usage(const char *progname) { printf("Usage: %s [options] [program [args ...]]\n" "\n" "Options may be any of the following:\n" " --help Show this text.\n" " --version Show program version.\n" " -l Log to the given file.\n" " --log-count Number of log history files (default 4).\n" " --log-size Maximum size of log file (default 1048576).\n" " -s Control socket (default rund-control).\n" " -r Restart delay, in milliseconds (default 2000).\n" " --one-shot Do not restart after failure.\n" " -u Run as user.\n" " --lock Lock on the given file.\n" " --kill-delay Wait milliseconds before trying SIGKILL\n" " (default 30000). Set this to 0 to disable use of\n" " SIGKILL.\n" " --log-sync Call fsync() on the log file every seconds.\n" " Default is 10. Set to 0 to disable.\n" "\n" "Action should be one of:\n" " stop Stop the process synchronously.\n" " status Check to see if the supervisor is running.\n" " pstatus Check to see if the supervised process is ok.\n" " start Start the process.\n" "\n" "If \"start\" is specified, a command and arguments must also be given.\n" "\n" "When started, a daemon is spawned to supervise and run the given program.\n" "If the program terminates early, then it will be restarted after a delay\n" "(unless one-shot operation is requested). All output from stdout/stderr is\n" "logged.\n" "\n" "The supervisor can be queried via a control socket to check to see whether\n" "it (or its supervisee) is running, and to shut it down. Daemon shutdown is\n" "synchronous -- the \"stop\" command doesn't finish until after both the\n" "supervisor and supervisee have exited and released resources.\n" "\n" "The supervisor also responds to SIGTERM for asynchronous shutdown. When\n" "shutting down, the supervisee is first sent SIGTERM. If it fails to exit\n" "within a certain time, SIGKILL is sent and the supervisor waits\n" "indefinitely.\n" "\n" "The \"start\" command tests to see if the supervisor is running and starts\n" "it if not. If you need to do be able to do this from multiple processes\n" "concurrently, use the --lock option to prevent races. The specified file\n" "will be locked exclusively by the supervisor daemon (or any process that\n" "attempts to become the supervisor). If multiple supervisors try to launch\n" "simultaneously, only one will succeed.\n", progname); } static void version(void) { printf("rund: daemon supervisor, 11 Feb 2016\n" "Copyright (C) 2015-2016 Daniel Beer \n"); } static void parse(int argc, char **argv) { enum { LOPT_HELP = 0x100, LOPT_VERSION, LOPT_LOG_COUNT, LOPT_LOG_SIZE, LOPT_ONE_SHOT, LOPT_LOCK, LOPT_KILL_DELAY, LOPT_LOG_SYNC }; static const struct option longopts[] = { {"help", 0, NULL, LOPT_HELP}, {"version", 0, NULL, LOPT_VERSION}, {"log-count", 1, NULL, LOPT_LOG_COUNT}, {"log-size", 1, NULL, LOPT_LOG_SIZE}, {"one-shot", 0, NULL, LOPT_ONE_SHOT}, {"lock", 1, NULL, LOPT_LOCK}, {"kill-delay", 1, NULL, LOPT_KILL_DELAY}, {"log-sync", 1, NULL, LOPT_LOG_SYNC}, {NULL, 0, NULL, 0} }; int opt; while ((opt = getopt_long(argc, argv, "+l:s:r:u:g:", longopts, NULL)) >= 0) switch (opt) { case LOPT_HELP: usage(argv[0]); exit(0); case LOPT_VERSION: version(); exit(0); case 'l': cfg.log_file = optarg; break; case LOPT_LOG_COUNT: cfg.log_count = atoi(optarg); break; case LOPT_LOG_SIZE: cfg.log_size = atoi(optarg); break; case LOPT_ONE_SHOT: cfg.flags |= FLAG_ONESHOT; break; case LOPT_LOCK: cfg.lock_file = optarg; break; case LOPT_KILL_DELAY: cfg.sigkill_delay = atoi(optarg); break; case LOPT_LOG_SYNC: cfg.log_sync = atoi(optarg); break; case 's': cfg.sock_path = optarg; break; case 'r': cfg.restart_delay = atoi(optarg); break; case 'u': cfg.runas_user = optarg; break; case '?': fprintf(stderr, "Unknown option. Try --help.\n"); exit(-1); } argc -= optind; argv += optind; if (!argc) { fprintf(stderr, "You need to specify an action. Try --help\n"); exit(-1); } cfg.action = *(argv++); argc--; cfg.argc = argc; cfg.argv = argv; } int main(int argc, char **argv) { parse(argc, argv); if (!strcmp(cfg.action, "start")) return action_start(); if (!strcmp(cfg.action, "status")) return action_status(); if (!strcmp(cfg.action, "pstatus")) return action_pstatus(); if (!strcmp(cfg.action, "stop")) return action_stop(); fprintf(stderr, "Unknown action: %s\n", cfg.action); return -1; }