/* 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 . */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // horrible :-( #include #include #include "daemon.h" enum { ExitNotFound = 127, ExitNoExec = 126, }; const char *serviceDir = "/"; uid_t serviceUID; gid_t serviceGID; char *serviceEnviron[EnvironLen] = { [SHELL] = "SHELL=" _PATH_BSHELL, [PATH] = "PATH=" _PATH_DEFPATH, }; struct Prepend prepend; struct Services services; static void serviceFree(struct Service *service) { free(service->name); free(service->command); if (service->outPipe[0] >= 0) { close(service->outPipe[0]); close(service->outPipe[1]); } if (service->errPipe[0] >= 0) { close(service->errPipe[0]); close(service->errPipe[1]); } } int serviceAdd(const char *name, const char *command) { struct Service *service = NULL; for (size_t i = 0; i < services.len; ++i) { if (!strcmp(services.ptr[i].name, name)) { service = &services.ptr[i]; break; } } if (service) { char *dup = strdup(command); if (!dup) return -1; free(service->command); service->command = dup; return 0; } if (services.len == services.cap) { size_t cap = (services.cap ? services.cap * 2 : 8); void *ptr = reallocarray(services.ptr, cap, sizeof(*services.ptr)); if (!ptr) return -1; services.cap = cap; services.ptr = ptr; } service = &services.ptr[services.len]; *service = (struct Service) { .outPipe = { -1, -1 }, .errPipe = { -1, -1 }, .restartInterval = restartInterval, }; service->name = strdup(name); if (!service->name) goto err; service->command = strdup(command); if (!service->command) goto err; if (name[0] == '@') service->privileged = true; int error = pipe2(service->outPipe, O_CLOEXEC); if (error) goto err; error = pipe2(service->errPipe, O_CLOEXEC); if (error) goto err; fcntl(service->outPipe[0], F_SETFL, O_NONBLOCK); fcntl(service->errPipe[0], F_SETFL, O_NONBLOCK); services.len++; return 0; err: serviceFree(service); return -1; } void serviceDrop(size_t index) { struct Service *service = &services.ptr[index]; if (service->state != Stop) return; syslog(LOG_NOTICE, "%s[] dropped", service->name); serviceFree(service); services.ptr[index] = services.ptr[--services.len]; } static const char *humanize(struct timespec interval) { static char buf[256]; if (!interval.tv_sec) { snprintf(buf, sizeof(buf), "%dms", (int)(interval.tv_nsec / 1000000)); return buf; } int d = interval.tv_sec / (24 * 60 * 60); interval.tv_sec %= (24 * 60 * 60); int h = interval.tv_sec / (60 * 60); interval.tv_sec %= (60 * 60); int m = interval.tv_sec / 60; interval.tv_sec %= 60; int s = interval.tv_sec; if (d) { snprintf(buf, sizeof(buf), "%dd %dh %dm %ds", d, h, m, s); } else if (h) { snprintf(buf, sizeof(buf), "%dh %dm %ds", h, m, s); } else if (m) { snprintf(buf, sizeof(buf), "%dm %ds", m, s); } else { snprintf(buf, sizeof(buf), "%ds", s); } return buf; } void serviceStatus(struct Service *service) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); if (service->state == Stop && service->intent == Stop) { syslog(LOG_NOTICE, "%s[] is stopped", service->name); } else if (service->state == Stop && service->intent == Start) { struct timespec timeleft; timespecsub(&service->restartDeadline, &now, &timeleft); syslog( LOG_NOTICE, "%s[] is restarting in %s", service->name, humanize(timeleft) ); } else if (service->state == Stop && service->intent == Restart) { syslog(LOG_NOTICE, "%s[] is restarting", service->name); } else if (service->state == Start && service->intent == Start) { struct timespec uptime; timespecsub(&now, &service->startTime, &uptime); syslog( LOG_NOTICE, "%s[%d] is started (up %s)", service->name, service->pid, humanize(uptime) ); } else if (service->state == Start && service->intent == Stop) { syslog(LOG_NOTICE, "%s[%d] is stopping", service->name, service->pid); } else if (service->state == Start && service->intent == Restart) { syslog( LOG_NOTICE, "%s[%d] is stopping for restart", service->name, service->pid ); } } static void setDeadline(struct Service *service) { clock_gettime(CLOCK_MONOTONIC, &service->restartDeadline); timespecadd( &service->restartDeadline, &service->restartInterval, &service->restartDeadline ); } void serviceStart(struct Service *service) { if (service->state != Stop) return; if (service->intent == Start) { struct timespec backoff = service->restartInterval; timespecadd(&backoff, &backoff, &service->restartInterval); } else { service->restartInterval = restartInterval; } setDeadline(service); service->intent = Start; service->pid = fork(); if (service->pid < 0) { syslog(LOG_WARNING, "fork: %s", strerror(errno)); return; } if (service->pid) { service->state = Start; clock_gettime(CLOCK_MONOTONIC, &service->startTime); syslog(LOG_NOTICE, "%s[%d] started", service->name, service->pid); return; } setpgid(0, 0); dup2(service->outPipe[1], STDOUT_FILENO); dup2(service->errPipe[1], STDERR_FILENO); int error = chdir(serviceDir); if (error) err(ExitNoExec, "%s", serviceDir); if (!service->privileged) { printf("wops"); error = setgid(serviceGID); if (error) err(ExitNoExec, "setgid"); if (!getuid()) { error = setgroups(1, &serviceGID); if (error) err(ExitNoExec, "setgroups"); } error = setuid(serviceUID); if (error) err(ExitNoExec, "setuid"); } size_t len = 0; //char command[ARG_MAX]; char command[MAX_ARG_STRLEN]; for (size_t i = 0; i < prepend.len; ++i) { int n = snprintf( &command[len], sizeof(command) - len, "%s;", prepend.commands[i] ); assert(n > 0); len += n; if (len >= sizeof(command)) errx(ExitNoExec, "command truncated"); } int n = snprintf( &command[len], sizeof(command) - len, "%s%s", (service->command[strcspn(service->command, ";&|()")] ? "" : "exec "), service->command ); assert(n > 0); len += n; if (len >= sizeof(command)) errx(ExitNoExec, "command truncated"); execle( _PATH_BSHELL, _PATH_BSHELL, "-c", command, service->name, NULL, serviceEnviron ); err(ExitNoExec, "%s", _PATH_BSHELL); } void serviceSignal(struct Service *service, int signal) { syslog(LOG_NOTICE, "%s[%d] sending signal %d", service->name, service->pid, signal); if (service->state != Start) return; int error = killpg(service->pid, signal); if (error) { syslog( LOG_WARNING, "killpg(%s[%d], %d): %s", service->name, service->pid, signal, strerror(errno) ); } } void serviceStop(struct Service *service) { if (service->intent != Stop && service->state != Start) { syslog(LOG_NOTICE, "%s[] stopped", service->name); } service->intent = Stop; serviceSignal(service, SIGTERM); } void serviceRestart(struct Service *service) { service->intent = Restart; if (service->state == Start) { serviceSignal(service, SIGTERM); } else { serviceStart(service); } } static void serviceLog(struct Service *service, const char *log) { syslog(LOG_NOTICE, "%s[%d]: %s", service->name, service->pid, log); } void serviceRead(struct Service *service) { const char *out; while (NULL != (out = lineRead(&service->outLine, service->outPipe[0]))) { serviceLog(service, out); } if (errno != EAGAIN) syslog(LOG_WARNING, "read: %s", strerror(errno)); const char *err; while (NULL != (err = lineRead(&service->errLine, service->errPipe[0]))) { serviceLog(service, err); } if (errno != EAGAIN) syslog(LOG_WARNING, "read: %s", strerror(errno)); } static void serviceFlush(struct Service *service) { const char *out = lineFlush(&service->outLine); const char *err = lineFlush(&service->errLine); if (out) serviceLog(service, out); if (err) serviceLog(service, err); } void serviceReap(pid_t pid, int status) { struct Service *service = NULL; for (size_t i = 0; i < services.len; ++i) { if (services.ptr[i].state != Start) continue; if (services.ptr[i].pid != pid) continue; service = &services.ptr[i]; break; } if (!service) { syslog(LOG_WARNING, "reaping unknown child %d", pid); return; } serviceFlush(service); service->state = Stop; if (WIFEXITED(status)) { int exit = WEXITSTATUS(status); if ( exit == ExitNotFound || exit == ExitNoExec || setTest(&stopExits, exit) ) { service->intent = Stop; } if (exit) { syslog( LOG_WARNING, "%s[%d] exited %d", service->name, pid, exit ); } } else if (WIFSIGNALED(status) && WTERMSIG(status) != SIGTERM) { syslog( LOG_WARNING, "%s[%d] signaled %d", service->name, pid, WTERMSIG(status) ); } if (service->intent == Start) { struct timespec uptime; clock_gettime(CLOCK_MONOTONIC, &uptime); timespecsub(&uptime, &service->startTime, &uptime); if (timespeccmp(&uptime, &resetInterval, >=)) { service->restartInterval = restartInterval; } setDeadline(service); syslog( LOG_NOTICE, "%s[%d] restarting in %s", service->name, pid, humanize(service->restartInterval) ); } else { syslog(LOG_NOTICE, "%s[%d] stopped", service->name, pid); } if (service->intent == Restart) { serviceStart(service); } }