/* Copyright (C) 2020 C. McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "daemon.h" #ifndef RUNDIR #define RUNDIR "/var/run" #endif #ifndef ETCDIR #define ETCDIR "/usr/local/etc" #endif #define WS " \t" struct timespec restartInterval = { .tv_sec = 1 }; struct Set256 stopExits; static volatile sig_atomic_t signals[NSIG]; static void signalHandler(int signal) { signals[signal]++; } static void configerr(bool exit, const char *format, ...) { va_list ap; va_start(ap, format); if (exit) { verrx(EX_DATAERR, format, ap); } else { vsyslog(LOG_WARNING, format, ap); } va_end(ap); } static void parseConfig(bool exit, const char *path) { size_t cap = 0; char *buf = NULL; FILE *file = fopen(path, "r"); if (!file) { configerr(exit, "%s: %s", path, strerror(errno)); goto err; } prependClear(); int line = 1; for (ssize_t len; 0 <= (len = getline(&buf, &cap, file)); ++line) { if (buf[len - 1] == '\n') buf[len - 1] = '\0'; char *ptr = &buf[strspn(buf, WS)]; if (!ptr[0] || ptr[0] == '#') { continue; } else if (ptr[0] == '%') { int error = prependAdd(&ptr[1]); if (error) { configerr( exit, "cannot add prepend command: %s", strerror(errno) ); goto err; } } else { char *name = strsep(&ptr, WS); if (!ptr) { configerr( exit, "%s:%d: no command line for service %s", path, line, name ); goto err; } int error = serviceAdd(name, ptr); if (error) { configerr(exit, "cannot add service: %s", strerror(errno)); goto err; } } } if (ferror(file)) configerr(exit, "%s: %s", path, strerror(errno)); err: free(buf); fclose(file); } typedef void Action(struct Service *service); static void parseControl(char *command) { char *action = strsep(&command, WS); if (!command) { syslog(LOG_NOTICE, "no service names for %s", action); return; } Action *fn = NULL; int signal = 0; if (!strcmp(action, "start")) { fn = serviceStart; } else if (!strcmp(action, "stop")) { fn = serviceStop; } else if (!strcmp(action, "restart")) { fn = serviceRestart; } else if (!strcmp(action, "status")) { fn = serviceStatus; } else { for (int i = 1; i < NSIG; ++i) { if (strcasecmp(action, sys_signame[i])) continue; signal = i; break; } } if (!fn && !signal) { syslog(LOG_NOTICE, "unknown action or signal %s", action); return; } while (command) { char *pattern = strsep(&command, WS); for (size_t i = 0; i < services.len; ++i) { struct Service *service = &services.ptr[i]; if (fnmatch(pattern, service->name, 0)) continue; if (signal) { serviceSignal(service, signal); } else { fn(service); } } } } static void parseExits(char *list) { setClear(&stopExits); while (*list) { byte exit = strtoul(list, &list, 10); if (*list) { if (*list != ',') errx(EX_USAGE, "invalid exit status %s", list); list++; } setAdd(&stopExits, exit); } } static void parseInterval(const char *millis) { unsigned long ms = strtoul(millis, NULL, 10); restartInterval.tv_sec = ms / 1000; restartInterval.tv_nsec = 1000000 * (ms % 1000); } int main(int argc, char *argv[]) { setprogname(argv[0]); bool daemonize = true; setAdd(&stopExits, EX_USAGE); setAdd(&stopExits, EX_DATAERR); setAdd(&stopExits, EX_NOINPUT); setAdd(&stopExits, EX_OSFILE); setAdd(&stopExits, EX_CANTCREAT); setAdd(&stopExits, EX_CONFIG); const char *pidPath = NULL; const char *configPath = ETCDIR "/spawntab"; const char *fifoPath = RUNDIR "/spawnd.pipe"; const char *userName = NULL; const char *groupName = NULL; for (int opt; 0 < (opt = getopt(argc, argv, "C:c:df:g:p:s:t:u:"));) { switch (opt) { break; case 'C': serviceDir = optarg; break; case 'c': fifoPath = optarg; break; case 'd': daemonize = false; break; case 'f': configPath = optarg; break; case 'g': groupName = optarg; break; case 'p': pidPath = optarg; break; case 's': parseExits(optarg); break; case 't': parseInterval(optarg); break; case 'u': userName = optarg; break; default: return EX_USAGE; } } parseConfig(true, configPath); int error = access(serviceDir, X_OK); if (error) err(EX_NOINPUT, "%s", serviceDir); errno = 0; struct passwd *user = (userName ? getpwnam(userName) : getpwuid(getuid())); if (errno) err(EX_OSFILE, "getpwnam"); if (!user) errx(EX_USAGE, "no such user %s", userName); errno = 0; struct group *group = ( groupName ? getgrnam(groupName) : getgrgid(user->pw_gid) ); if (errno) err(EX_OSFILE, "getgrnam"); if (!group) errx(EX_USAGE, "no such group %s", groupName); serviceUID = user->pw_uid; serviceGID = group->gr_gid; int len = asprintf(&serviceEnviron[LOGNAME], "LOGNAME=%s", user->pw_name); if (len < 0) err(EX_OSERR, "asprintf"); len = asprintf(&serviceEnviron[USER], "USER=%s", user->pw_name); if (len < 0) err(EX_OSERR, "asprintf"); len = asprintf(&serviceEnviron[HOME], "HOME=%s", user->pw_dir); if (len < 0) err(EX_OSERR, "asprintf"); int pidFile = -1; if (pidPath) { pidFile = open( pidPath, O_WRONLY | O_CREAT | O_EXLOCK | O_CLOEXEC, 0600 ); if (pidFile < 0) err(EX_CANTCREAT, "%s", pidPath); } // We can't lock a named pipe, so just warn if it already exists. error = mkfifo(fifoPath, 0600); if (error) { if (errno != EEXIST) err(EX_CANTCREAT, "%s", fifoPath); warn("%s", fifoPath); } // XXX: Make sure there is always at least one writer open, otherwise we // get EOF continually. int fifo = open(fifoPath, O_RDWR | O_NONBLOCK | O_CLOEXEC); if (fifo < 0) err(EX_CANTCREAT, "%s", fifoPath); struct Line fifoLine = {0}; openlog(getprogname(), LOG_NDELAY | LOG_PID | LOG_PERROR, LOG_DAEMON); if (daemonize) { error = daemon(0, 0); if (error) { syslog(LOG_WARNING, "daemon: %m"); return EX_OSERR; } } if (pidPath) { int len = dprintf(pidFile, "%ju", (uintmax_t)getpid()); if (len < 0) syslog(LOG_WARNING, "%s: %m", pidPath); } signal(SIGHUP, signalHandler); signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); signal(SIGCHLD, signalHandler); signal(SIGINFO, signalHandler); for (size_t i = 0; i < services.len; ++i) { serviceStart(&services.ptr[i]); } // TODO: setproctitle to number of services currently running. sigset_t mask; sigemptyset(&mask); for (;;) { struct pollfd fds[1 + 2 * services.len]; fds[0].fd = fifo; fds[0].events = POLLIN; struct timespec deadline = {0}; for (size_t i = 0; i < services.len; ++i) { struct Service *service = &services.ptr[i]; fds[1 + 2 * i].fd = service->outPipe[0]; fds[2 + 2 * i].fd = service->errPipe[0]; fds[1 + 2 * i].events = POLLIN; fds[2 + 2 * i].events = POLLIN; if (service->intent != Start) continue; if (service->state == Start) continue; if ( !timespecisset(&deadline) || timespeccmp(&service->restartDeadline, &deadline, <) ) deadline = service->restartDeadline; } struct timespec now = {0}; struct timespec timeout = {0}; if (timespecisset(&deadline)) { clock_gettime(CLOCK_MONOTONIC, &now); timespecsub(&deadline, &now, &timeout); } if (timeout.tv_sec < 0 || timeout.tv_nsec < 0) { timespecclear(&timeout); } int nfds = ppoll( fds, 1 + 2 * services.len, (timespecisset(&deadline) ? &timeout : NULL), &mask ); if (nfds < 0 && errno != EINTR) { syslog(LOG_WARNING, "ppoll: %m"); continue; } if (nfds > 0 && fds[0].revents) { const char *line; while (NULL != (line = lineRead(&fifoLine, fifo))) { char buf[LineCap]; snprintf(buf, sizeof(buf), "%s", line); parseControl(buf); } if (errno != EAGAIN) syslog(LOG_WARNING, "read: %m"); } if (nfds > 0) { for (size_t i = 0; i < services.len; ++i) { if (fds[1 + 2 * i].revents || fds[2 + 2 * i].revents) { serviceRead(&services.ptr[i]); } } } if (timespecisset(&deadline)) { clock_gettime(CLOCK_MONOTONIC, &now); for (size_t i = 0; i < services.len; ++i) { struct Service *service = &services.ptr[i]; if (service->intent != Start) continue; if (service->state == Start) continue; if (timespeccmp(&service->restartDeadline, &now, <=)) { serviceStart(service); } } } if (signals[SIGCHLD]) { int status; pid_t pid; while (0 < (pid = waitpid(-1, &status, WNOHANG))) { serviceReap(pid, status); } if (pid < 0 && errno != ECHILD) syslog(LOG_WARNING, "waitpid: %m"); signals[SIGCHLD] = 0; } if (signals[SIGINT] || signals[SIGTERM]) { break; } if (signals[SIGHUP]) { parseConfig(false, configPath); signals[SIGHUP] = 0; } if (signals[SIGINFO]) { char command[] = "status *"; parseControl(command); signals[SIGINFO] = 0; } } close(fifo); unlink(fifoPath); size_t count = 0; for (size_t i = 0; i < services.len; ++i) { serviceStop(&services.ptr[i]); if (services.ptr[i].state == Start) count++; } while (count--) { int status; pid_t pid = wait(&status); if (pid < 0) { syslog(LOG_WARNING, "wait: %m"); continue; } serviceReap(pid, status); } if (pidPath) { close(pidFile); unlink(pidPath); } }