/* 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" #define WS " \t" struct Set256 stopExits; struct timespec restartInterval = { .tv_sec = 1 }; struct timespec resetInterval = { .tv_sec = 15 * 60 }; static volatile sig_atomic_t signals[NSIG]; static void signalHandler(int signal) { signals[signal] = 1; } static ssize_t getlinecont(char **line, size_t *lcap, FILE *file) { size_t cap = 0; char *buf = NULL; ssize_t llen = getline(line, lcap, file); while (llen > 1 && (*line)[llen - 1] == '\n' && (*line)[llen - 2] == '\\') { llen -= 2; ssize_t len = getline(&buf, &cap, file); if (len < 0) { llen = -1; break; } size_t req = llen + len + 1; if (req > *lcap) { char *ptr = realloc(*line, req); if (!ptr) { llen = -1; break; } *line = ptr; *lcap = req; } strlcpy(*line + llen, buf, *lcap - llen); llen += len; } free(buf); return llen; } static int parseConfig(const char *path) { int ret = -1; size_t cap = 0; char *buf = NULL; FILE *file = fopen(path, "r"); if (!file) { syslog(LOG_WARNING, "%s: %m", path); goto err; } prependClear(); int line = 1; for (ssize_t len; 0 <= (len = getlinecont(&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] == '%') { ptr++; int error = prependAdd(&ptr[strspn(ptr, WS)]); if (error) { syslog(LOG_WARNING, "cannot add prepend command: %m"); goto err; } } else { char *name = strsep(&ptr, WS); if (!ptr) { syslog( LOG_WARNING, "%s:%d: no command line for service %s", path, line, name ); goto err; } int error = serviceAdd(name, &ptr[strspn(ptr, WS)]); if (error) { syslog(LOG_WARNING, "cannot add service: %m"); goto err; } } } if (ferror(file)) { syslog(LOG_WARNING, "%s: %m", path); goto err; } ret = 0; err: free(buf); if (file) fclose(file); return ret; } 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; } bool drop = false; 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 if (!strcmp(action, "drop")) { drop = true; } else { for (int i = 1; i < NSIG; ++i) { if (strcasecmp(action, sys_signame[i])) continue; signal = i; break; } } if (!drop && !fn && !signal) { syslog(LOG_NOTICE, "unknown action or signal %s", action); return; } while (command) { bool found = false; char *pattern = strsep(&command, WS); for (size_t i = services.len - 1; i < services.len; --i) { struct Service *service = &services.ptr[i]; if (fnmatch(pattern, service->name, 0)) continue; if (drop) { serviceDrop(i); } else if (signal) { serviceSignal(service, signal); } else { fn(service); } found = true; } if (!found) syslog(LOG_NOTICE, "no services matching %s", pattern); } } 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(struct timespec *interval, const char *millis) { unsigned long ms = strtoul(millis, NULL, 10); interval->tv_sec = ms / 1000; interval->tv_nsec = 1000000 * (ms % 1000); } static void setTitle(void) { size_t started = 0; for (size_t i = 0; i < services.len; ++i) { if (services.ptr[i].state == Start) started++; } setproctitle("%zu/%zu services", started, services.len); } int main(int argc, char *argv[]) { int error; openlog(getprogname(), LOG_NDELAY | LOG_PID | LOG_PERROR, LOG_DAEMON); bool daemonize = true; setAdd(&stopExits, EX_USAGE); setAdd(&stopExits, EX_DATAERR); setAdd(&stopExits, EX_NOINPUT); setAdd(&stopExits, EX_OSFILE); setAdd(&stopExits, EX_CANTCREAT); const char *pidPath = NULL; const char *configPath = ETCDIR "/catsit.conf"; const char *fifoPath = RUNDIR "/catsitd.pipe"; const char *userName = NULL; const char *groupName = NULL; for (int opt; 0 < (opt = getopt(argc, argv, "C:c:df:g:p:r: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 'r': parseInterval(&resetInterval, optarg); break; case 's': parseExits(optarg); break; case 't': parseInterval(&restartInterval, optarg); break; case 'u': userName = optarg; break; default: return EX_USAGE; } } error = access(configPath, R_OK); if (error) err(EX_NOINPUT, "%s", configPath); 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); serviceUID = user->pw_uid; if (groupName) { errno = 0; struct group *group = getgrnam(groupName); if (errno) err(EX_OSFILE, "getgrnam"); if (!group) errx(EX_USAGE, "no such group %s", groupName); serviceGID = group->gr_gid; } else { serviceGID = user->pw_gid; } int 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_CLOEXEC, 0600); if (pidFile < 0) err(EX_CANTCREAT, "%s", pidPath); error = flock(pidFile, LOCK_EX | LOCK_NB); if (error && errno != EWOULDBLOCK) err(EX_IOERR, "%s", pidPath); if (error) errx(EX_CANTCREAT, "%s: file is locked", pidPath); error = ftruncate(pidFile, 0); if (error) err(EX_IOERR, "%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); } struct Line fifoLine = {0}; int fifo = open(fifoPath, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (fifo < 0) err(EX_CANTCREAT, "%s", fifoPath); // XXX: Keep a writer open so the reader never gets EOF. int writer = open(fifoPath, O_WRONLY | O_NONBLOCK | O_CLOEXEC); if (writer < 0) err(EX_CANTCREAT, "%s", fifoPath); if (daemonize) { error = daemon(0, 0); if (error) { syslog(LOG_WARNING, "daemon: %m"); return EX_OSERR; } } #ifdef __OpenBSD__ error = 0 || unveil(fifoPath, "c") || unveil(configPath, "r") || unveil(serviceDir, "r") || unveil(_PATH_BSHELL, "x"); if (error) err(EX_OSERR, "unveil"); if (pidPath) { error = unveil(pidPath, "c"); if (error) err(EX_OSERR, "unveil"); } error = pledge("stdio rpath cpath proc exec id", NULL); if (error) err(EX_OSERR, "pledge"); #endif 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); parseConfig(configPath); for (size_t i = 0; i < services.len; ++i) { serviceStart(&services.ptr[i]); } setTitle(); struct pollfd *fds = calloc(1 + 2 * services.len, sizeof(*fds)); if (!fds) { syslog(LOG_ERR, "calloc: %m"); goto shutdown; } fds[0].fd = fifo; fds[0].events = POLLIN; sigset_t mask; sigemptyset(&mask); for (;;) { 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"); setTitle(); signals[SIGCHLD] = 0; } if (signals[SIGINT] || signals[SIGTERM]) { break; } if (signals[SIGHUP]) { parseConfig(configPath); fds = reallocarray(fds, 1 + 2 * services.len, sizeof(*fds)); if (!fds) { syslog(LOG_ERR, "reallocarray: %m"); goto shutdown; } setTitle(); signals[SIGHUP] = 0; } if (signals[SIGINFO]) { char command[] = "status *"; parseControl(command); signals[SIGINFO] = 0; } 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"); setTitle(); } 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); } } setTitle(); } } shutdown: close(fifo); unlink(fifoPath); setproctitle("stopping"); 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); } }