/* 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 "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 = realloc(services.ptr, sizeof(*services.ptr) * cap); 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; 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 serviceStatus(struct Service *service) { 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 now, timeleft; clock_gettime(CLOCK_MONOTONIC, &now); timespecsub(&service->restartDeadline, &now, &timeleft); syslog( LOG_NOTICE, "%s[] is restarting in %lds", service->name, (long)timeleft.tv_sec ); } else if (service->state == Stop && service->intent == Restart) { syslog(LOG_NOTICE, "%s[] is restarting", service->name); } else if (service->state == Start && service->intent == Start) { syslog(LOG_NOTICE, "%s[%d] is started", service->name, service->pid); } 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: %m"); 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); error = setgid(serviceGID); if (error) err(ExitNoExec, "setgid"); 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]; 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; } snprintf(&command[len], sizeof(command) - len, "exec %s", service->command); execle( _PATH_BSHELL, _PATH_BSHELL, "-c", command, service->name, NULL, serviceEnviron ); err(ExitNotFound, "%s", _PATH_BSHELL); } void serviceSignal(struct Service *service, int signal) { if (service->state != Start) return; int error = killpg(service->pid, signal); if (error) { syslog( LOG_WARNING, "killpg(%s[%d], %s): %m", service->name, service->pid, sys_signame[signal] ); } } void serviceStop(struct Service *service) { 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: %m"); const char *err; while (NULL != (err = lineRead(&service->errLine, service->errPipe[0]))) { serviceLog(service, err); } if (errno != EAGAIN) syslog(LOG_WARNING, "read: %m"); } 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 %s", service->name, pid, sys_signame[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 %lds", service->name, pid, (long)service->restartInterval.tv_sec ); } else { syslog(LOG_NOTICE, "%s[%d] stopped", service->name, pid); } if (service->intent == Restart) { serviceStart(service); } }