/* Finit service monitor, task starter and generic API for managing svc_t * * Copyright (c) 2008-2010 Claudio Matsuoka * Copyright (c) 2008-2022 Joachim Wiberg * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include "config.h" /* Generated by configure script */ #include /* isblank() */ #include /* sched_yield() */ #include #include #include #include #include #include #ifdef _LIBITE_LITE # include #else # include #endif #include #include "cgroup.h" #include "client.h" #include "conf.h" #include "cond.h" #include "devmon.h" #include "finit.h" #include "helpers.h" #include "pid.h" #include "private.h" #include "sig.h" #include "service.h" #include "sm.h" #include "tty.h" #include "util.h" #include "utmp-api.h" #include "schedule.h" static struct wq work = { .cb = service_worker, }; int service_interval = SERVICE_INTERVAL_DEFAULT; static void svc_set_state(svc_t *svc, svc_state_t new_state); /** * service_timeout_cb - libuev callback wrapper for service timeouts * @w: Watcher * @arg: Callback argument, from init * @events: Error, or ready to read/write (N/A for relative timers) * * Run callback registered when calling service_timeout_after(). */ static void service_timeout_cb(uev_t *w, void *arg, int events) { svc_t *svc = arg; /* Ignore any UEV_ERROR, we're a one-shot cb so just run it. */ if (svc->timer_cb) svc->timer_cb(svc); } /** * service_timeout_after - Call a function after some time has elapsed * @svc: Service to use as argument to the callback * @timeout: Timeout, in milliseconds * @cb: Callback function * * After @timeout milliseconds has elapsed, call @cb() with @svc as the * argument. * * Returns: * POSIX OK(0) on success, non-zero on error. */ int service_timeout_after(svc_t *svc, int timeout, void (*cb)(svc_t *svc)) { if (svc->timer_cb) return -EBUSY; svc->timer_cb = cb; return uev_timer_init(ctx, &svc->timer, service_timeout_cb, svc, timeout, 0); } /** * service_timeout_cancel - Cancel timeout associated with service * @svc: Service whose timeout to cancel * * If a timeout is associated with @svc, cancel it. * * Returns: * POSIX OK(0) on success, non-zero on error. */ int service_timeout_cancel(svc_t *svc) { int err; if (!svc->timer_cb) return 0; err = uev_timer_stop(&svc->timer); svc->timer_cb = NULL; return err; } struct assoc { TAILQ_ENTRY(assoc) link; pid_t pid; /* script pid */ svc_t *svc; /* associated svc_t */ }; static TAILQ_HEAD(, assoc) svc_assoc_list = TAILQ_HEAD_INITIALIZER(svc_assoc_list); static void service_script_kill(svc_t *svc) { struct assoc *ptr, *next; TAILQ_FOREACH_SAFE(ptr, &svc_assoc_list, link, next) { if (ptr->svc != svc || ptr->pid <= 1) continue; dbg("Killing service %s script PID %d.", svc_ident(svc, NULL, 0), ptr->pid); kill(-ptr->pid, SIGKILL); TAILQ_REMOVE(&svc_assoc_list, ptr, link); free(ptr); } } static int service_script_add(svc_t *svc, pid_t pid) { struct assoc *ptr = malloc(sizeof(*ptr)); if (!ptr) { err(1, "Failed starting service script timer"); return 1; } ptr->svc = svc; ptr->pid = pid; TAILQ_INSERT_TAIL(&svc_assoc_list, ptr, link); service_timeout_after(svc, svc->killdelay, service_script_kill); return 0; } static int service_script_del(pid_t pid) { struct assoc *ptr, *next; TAILQ_FOREACH_SAFE(ptr, &svc_assoc_list, link, next) { if (ptr->pid != pid) continue; dbg("Collected service %s script PID %d.", svc_ident(ptr->svc, NULL, 0), pid); service_timeout_cancel(ptr->svc); kill(-ptr->pid, SIGKILL); TAILQ_REMOVE(&svc_assoc_list, ptr, link); free(ptr); return 0; } return 1; } /* * Redirect stdin to /dev/null => all reads by process = EOF * https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Logging%20and%20Standard%20Input/Output */ static int stdin_redirect(void) { int fd; fd = open("/dev/null", O_RDONLY | O_APPEND); if (-1 != fd) { dup2(fd, STDIN_FILENO); return close(fd); } return -1; } /* * Redirect output to a file, e.g., /dev/null, or /dev/console */ static int fredirect(const char *file) { int fd; fd = open(file, O_WRONLY | O_APPEND | O_NOCTTY); if (-1 != fd) { dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); return close(fd); } return -1; } /* * Fallback in case we don't even have logger on the system. * XXX: we should parse 'prio' here to get facility.level */ static void fallback_logger(char *ident, char *prio) { int facility = LOG_DAEMON; int level = LOG_NOTICE; char buf[256]; prctl(PR_SET_NAME, "finitlog", 0, 0, 0); openlog(ident, LOG_NOWAIT | LOG_PID, facility); while ((fgets(buf, sizeof(buf), stdin))) syslog(level, "%s", buf); closelog(); } /* * Redirect output to syslog using the command line logit tool */ static int lredirect(svc_t *svc) { static int have_sysklogd = -1; pid_t pid; int fd; /* * Open PTY to connect to logger. A pty isn't buffered * like a pipe, and it eats newlines so they aren't logged */ fd = posix_openpt(O_RDWR); if (fd == -1) { svc->log.enabled = 0; return -1; } if (grantpt(fd) == -1 || unlockpt(fd) == -1) { close(fd); svc->log.enabled = 0; return -1; } /* * First time, check if we have sysklogd logger tool. * It supports logging the actual PID of the service. */ if (have_sysklogd == -1) { FILE *pp; pp = popen("logger -h 2>/dev/null", "r"); if (pp) { char buf[128]; have_sysklogd = 0; while (fgets(buf, sizeof(buf), pp)) { if (strstr(buf, "-I PID")) { have_sysklogd = 1; break; } } pclose(pp); } else { have_sysklogd = 0; } } pid = fork(); if (pid == 0) { char *prio = "daemon.info"; char buf[MAX_IDENT_LEN]; char *tag; int fds; sched_yield(); fds = open(ptsname(fd), O_RDONLY); close(fd); if (fds == -1) _exit(0); dup2(fds, STDIN_FILENO); /* Reset signals */ sig_unblock(); /* Default syslog identity name[:id] */ tag = svc_ident(svc, buf, sizeof(buf)); if (!have_sysklogd && !whichp(_PATH_LOGIT)) { logit(LOG_INFO, _PATH_LOGIT " missing, using syslog for %s instead", svc->name); fallback_logger(tag, prio); _exit(0); } if (svc->log.file[0] == '/') { if (have_sysklogd) { char rot[25], pid[16]; snprintf(rot, sizeof(rot), "%d:%d", logfile_size_max, logfile_count_max); snprintf(pid, sizeof(pid), "%d", getppid()); execlp("logger", "logger", "-f", svc->log.file, "-b", "-t", tag, "-p", prio, "-I", pid, "-r", rot, NULL); } else { char sz[20], num[3]; snprintf(sz, sizeof(sz), "%d", logfile_size_max); snprintf(num, sizeof(num), "%d", logfile_count_max); execlp(_PATH_LOGIT, "logit", "-f", svc->log.file, "-n", sz, "-r", num, NULL); } _exit(1); } if (svc->log.ident[0]) tag = svc->log.ident; if (svc->log.prio[0]) prio = svc->log.prio; if (have_sysklogd) { char pid[16]; snprintf(pid, sizeof(pid), "%d", getppid()); execlp("logger", "logger", "-t", tag, "-p", prio, "-I", pid, NULL); } else { execlp(_PATH_LOGIT, "logit", "-t", tag, "-p", prio, NULL); } _exit(1); } dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); return close(fd); } /* * Handle redirection of process output, if enabled */ static int redirect(svc_t *svc) { stdin_redirect(); if (svc->log.enabled) { if (svc->log.null) return fredirect("/dev/null"); if (svc->log.console) return fredirect(console()); return lredirect(svc); } else if (debug) return fredirect(console()); #ifdef REDIRECT_OUTPUT else return fredirect("/dev/null"); #endif return 0; } /* * Source environment file, if it exists * Note: must be called from privsepped child */ static void source_env(svc_t *svc) { char buf[LINE_SIZE]; char *line; size_t len; FILE *fp; char *fn; fn = svc_getenv(svc); if (!fn) return; /* Warning in service_start() after svc_checkenv() */ fp = fopen(fn, "r"); if (!fp) return; line = buf; len = sizeof(buf); while (fgets(line, len, fp)) { char *key = chomp(line); char *value, *end; /* skip any leading whitespace */ while (isspace(*key)) key++; /* skip comments */ if (*key == '#' || *key == ';') continue; /* find end of line */ end = key; while (*end) end++; /* strip trailing whitespace */ if (end > key) { end--; while (isspace(*end)) *end-- = 0; } value = strchr(key, '='); if (!value) continue; *value++ = 0; /* strip leading whitespace from value */ while (isspace(*value)) value++; /* unquote value, if quoted */ if (value[0] == '"' || value[0] == '\'') { char q = value[0]; if (*end == q) { value = &value[1]; *end = 0; } } /* find end of key */ end = key; while (*end) end++; /* strip trailing whitespace */ if (end > key) { end--; while (isspace(*end)) *end-- = 0; } setenv(key, value, 1); } fclose(fp); } static int is_norespawn(void) { return fexist("/mnt/norespawn") || fexist("/tmp/norespawn"); } /* used for process group name, derived from originating filename, * so to group multiple services, place them in the same .conf */ static char *group_name(svc_t *svc, char *buf, size_t len) { char *ptr; if (!svc->file[0]) return svc_ident(svc, buf, len); ptr = strrchr(svc->file, '/'); if (ptr) ptr++; else ptr = svc->file; strlcpy(buf, ptr, len); ptr = strstr(buf, ".conf"); if (ptr) *ptr = 0; return buf; } static void compose_cmdline(svc_t *svc, char *buf, size_t len) { size_t i; strlcpy(buf, svc->cmd, len); for (i = 1; i < MAX_NUM_SVC_ARGS; i++) { if (!strlen(svc->args[i])) break; strlcat(buf, " ", len); strlcat(buf, svc->args[i], len); } } static pid_t service_fork(svc_t *svc) { pid_t pid; pid = fork(); if (pid == 0) { char *home = NULL; #ifdef ENABLE_STATIC int uid = 0; /* XXX: Fix better warning that dropprivs is disabled. */ int gid = 0; #else int uid = getuser(svc->username, &home); int gid = getgroup(svc->group); #endif sched_yield(); /* Set configured limits */ for (int i = 0; i < RLIMIT_NLIMITS; i++) { if (setrlimit(i, &svc->rlimit[i]) == -1) logit(LOG_WARNING, "%s: rlimit: failed setting %s", svc_ident(svc, NULL, 0), rlim2str(i)); } /* Set desired user+group */ if (gid >= 0) { if (setgid(gid)) err(1, "%s: failed setgid(%d)", svc_ident(svc, NULL, 0), gid); } if (uid >= 0) { if (setuid(uid)) err(1, "%s: failed setuid(%d)", svc_ident(svc, NULL, 0), uid); /* Set default path for regular users */ if (uid > 0) setenv("PATH", _PATH_DEFPATH, 1); if (home) { setenv("HOME", home, 1); if (chdir(home)) { if (chdir("/")) err(1, "%s: failed chdir(%s) and chdir(/)", svc_ident(svc, NULL, 0), home); } } } /* Source any environment from env:/path/to/file */ source_env(svc); } return pid; } /** * service_start - Start service * @svc: Service to start * * Returns: * 0 if the service was successfully started. Non-zero otherwise. */ static int service_start(svc_t *svc) { int result = 0, do_progress = 1; char cmdline[CMD_SIZE] = ""; sigset_t nmask, omask; char grnam[80]; pid_t pid; size_t i; if (!svc) return 1; /* Ignore if finit is SIGSTOP'ed */ if (is_norespawn()) return 1; /* Don't try and start service if it doesn't exist. */ if (!whichp(svc->cmd)) { logit(LOG_WARNING, "%s: missing %s or not in $PATH", svc_ident(svc, NULL, 0), svc->cmd); svc_missing(svc); return 1; } /* Unlike systemd we do not allow starting service if env is missing, unless - */ if (!svc_checkenv(svc)) { logit(LOG_WARNING, "%s: missing %s env file %s", svc_ident(svc, NULL, 0), svc->cmd, svc->env); svc_missing(svc); return 1; } if (svc_is_tty(svc) && !svc->notty) { char *dev = tty_canonicalize(svc->dev); if (!dev || !tty_exists(dev)) { dbg("TTY %s missing or invalid, halting service.", svc->dev); svc_missing(svc); return 1; } } compose_cmdline(svc, cmdline, sizeof(cmdline)); if (svc_is_sysv(svc)) logit(LOG_CONSOLE | LOG_NOTICE, "Calling '%s start' ...", cmdline); if (!svc->desc[0]) do_progress = 0; if (do_progress) { if (svc_is_daemon(svc) || svc_is_sysv(svc)) print_desc("Starting ", svc->desc); else print_desc("", svc->desc); } /* Declare we're waiting for svc to create its pidfile */ svc_starting(svc); /* Increment total restarts, unless first time or non-service */ if (svc_is_daemon(svc) || svc_is_sysv(svc)) { if (svc->restart_cnt || svc->restart_tot) svc->restart_tot++; } /* Block SIGCHLD while forking. */ sigemptyset(&nmask); sigaddset(&nmask, SIGCHLD); sigprocmask(SIG_BLOCK, &nmask, &omask); pid = service_fork(svc); if (pid < 0) { result = -1; goto fail; } if (pid > 1) { svc->pid = pid; svc->start_time = jiffies(); } else if (pid == 0) { char *args[MAX_NUM_SVC_ARGS + 1]; int status; if (!svc_is_tty(svc)) redirect(svc); if (!svc_is_sysv(svc)) { wordexp_t we = { 0 }; int rc; if (svc->notify) { if (client_command(INIT_CMD_NOTIFY_SOCKET)) { err(1, "%s: failed creating notify socket", svc_ident(svc, NULL, 0)); client_disconnect(); svc->notify = 0; /* does not change parent, restarting may work. */ } else { char val[20]; snprintf(val, sizeof(val), "%d", client_socket()); setenv("NOTIFY_SOCKET", val, 1); } } if ((rc = wordexp(svc->cmd, &we, 0))) { errx(1, "%s: failed wordexp(%s): %d", svc_ident(svc, NULL, 0), svc->cmd, rc); nomem: wordfree(&we); _exit(1); } for (i = 0; i < MAX_NUM_SVC_ARGS; i++) { char *arg = svc->args[i]; size_t len = strlen(arg); char str[len + 2]; char ch = *arg; if (len == 0) break; if (svc->notify) { char *ptr = strstr(arg, "%n"); if (ptr) { len = snprintf(str, sizeof(str), "%d", client_socket()); if (len > 0 && len <= 2) { ptr[0] = ' '; ptr[1] = ' '; memcpy(ptr, str, len); } } } /* * Escape forbidden characters in wordexp() * but allowed in Finit run/task stanzas, * * XXX: escapes only leading characters ... */ if (strchr("|<>&:", ch)) sprintf(str, "\\"); else str[0] = 0; strlcat(str, arg, sizeof(str)); if ((rc = wordexp(str, &we, WRDE_APPEND))) { errx(1, "%s: failed wordexp(%s): %d", svc_ident(svc, NULL, 0), str, rc); goto nomem; } } if (we.we_wordc > MAX_NUM_SVC_ARGS) { logit(LOG_ERR, "%s: too many args to %s after expansion.", svc_ident(svc, NULL, 0), svc->cmd); goto nomem; } for (i = 0; i < we.we_wordc; i++) { if (strlen(we.we_wordv[i]) >= sizeof(svc->args[i])) { logit(LOG_ERR, "%s: expanded %s arg. '%s' too long", svc_ident(svc, NULL, 0), svc->cmd, we.we_wordv[i]); rc = WRDE_NOSPACE; goto nomem; } /* overwrite the child's svc with expanded args */ strlcpy(svc->args[i], we.we_wordv[i], sizeof(svc->args[i])); args[i] = svc->args[i]; } wordfree(&we); } else { size_t j; i = 0; args[i++] = svc->cmd; /* this handles, e.g., bridge-stop br0 start */ for (j = 0; j < MAX_NUM_SVC_ARGS; j++) { if (!strlen(svc->args[j])) break; args[i++] = svc->args[j]; } args[i++] = "start"; } args[i] = NULL; #ifdef DEBUG_COMMAND_ARGS char buf[PATH_MAX] = ""; for (i = 0; args[i]; i++) { strlcat(buf, args[i], sizeof(buf)); strlcat(buf, " ", sizeof(buf)); } logit(LOG_DEBUG, "DEBUG starting %s", buf); buf[0] = 0; strlcat(buf, svc->cmd, sizeof(buf)); strlcat(buf, " ", sizeof(buf)); for (i = 1; i < MAX_NUM_SVC_ARGS; i++) { if (!strlen(svc->args[i])) break; strlcat(buf, svc->args[i], sizeof(buf)); strlcat(buf, " ", sizeof(buf)); } logit(LOG_DEBUG, "DEBUG starting args %s", buf); #endif /* * The setsid() call takes care to detach the process * from its controlling terminal, preventing daemons * from leaking to the console, and allowing us to run * such programs like `lxc-start -F` in the foreground * to properly monitor them. * * If you find yourself here wanting to fix the output * to the console at boot, for debugging or similar, * have a look at redirect() and log.console instead. */ pid = setsid(); if (pid < 1) logit(LOG_ERR, "failed setsid(), pid %d: %s", pid, strerror(errno)); sig_unblock(); if (svc_is_runtask(svc)) status = exec_runtask(args[0], &args[1]); else if (svc_is_tty(svc)) status = tty_exec(svc); else status = execvp(args[0], &args[1]); _exit(status); } else if (debug) { dbg("Starting PID %d: %s", svc->pid, cmdline); } if (svc_is_tty(svc)) cgroup_user("getty", pid); else cgroup_service(group_name(svc, grnam, sizeof(grnam)), pid, &svc->cgroup); if (!svc_is_sysv(svc)) logit(LOG_CONSOLE | LOG_NOTICE, "Starting %s[%d]", svc_ident(svc, NULL, 0), pid); switch (svc->type) { case SVC_TYPE_RUN: svc->status = complete(svc->cmd, pid); dbg("collected %s(%d), normal exit: %d, signaled: %d, exit code: %d", svc_ident(svc, NULL, 0), pid, WIFEXITED(svc->status), WIFSIGNALED(svc->status), WEXITSTATUS(svc->status)); if (WIFEXITED(svc->status) && !WEXITSTATUS(svc->status)) { svc->started = 1; result = 0; } else { svc->started = 0; result = 1; } svc->start_time = svc->pid = 0; svc->once++; svc_set_state(svc, SVC_STOPPING_STATE); break; case SVC_TYPE_SERVICE: pid_file_create(svc); break; default: break; } fail: sigprocmask(SIG_SETMASK, &omask, NULL); if (do_progress) print_result(result); return result; } /** * service_kill - Forcefully terminate a service * @param svc Service to kill * * Called when a service refuses to terminate gracefully. */ static void service_kill(svc_t *svc) { service_timeout_cancel(svc); if (svc->pid <= 1) { /* Avoid killing ourselves or all processes ... */ dbg("%s: Aborting SIGKILL, already terminated.", svc_ident(svc, NULL, 0)); return; } dbg("%s: Sending SIGKILL to pid:%d", pid_get_name(svc->pid, NULL, 0), svc->pid); logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], sending SIGKILL ...", svc_ident(svc, NULL, 0), svc->pid); if (runlevel != 1) print_desc("Killing ", svc->desc); kill(-svc->pid, SIGKILL); /* Let SIGKILLs stand out, show result as [WARN] */ if (runlevel != 1) print(2, NULL); } /* * Clean up any lingering state from dead/killed services */ static void service_cleanup(svc_t *svc) { char *fn; /* PID collected, cancel any pending SIGKILL */ service_timeout_cancel(svc); fn = pid_file(svc); if (fn && remove(fn) && errno != ENOENT) logit(LOG_CRIT, "Failed removing service %s pidfile %s", svc_ident(svc, NULL, 0), fn); /* Ensure we don't have any notify socket lingering */ if (svc->notify && svc->notify_watcher.fd > 0) { uev_io_stop(&svc->notify_watcher); close(svc->notify_watcher.fd); svc->notify_watcher.fd = 0; } /* No longer running, update books. */ if (svc_is_tty(svc) && svc->pid > 1) utmp_set_dead(svc->pid); /* Set DEAD_PROCESS UTMP entry */ svc->oldpid = svc->pid; svc->starting = svc->start_time = svc->pid = 0; } /** * service_stop - Stop service * @svc: Service to stop * * Called externally by initctl to perform stop/start (restart) of * services. Internally it is used to bring a run/task/service to * HALTED state. * * Returns: * 0 if the service was successfully stopped. Non-zero otherwise. */ int service_stop(svc_t *svc) { char cmdline[CMD_SIZE] = ""; int do_progress = 1; int rc = 0; if (!svc) return 1; if (svc->state <= SVC_STOPPING_STATE) return 0; service_timeout_cancel(svc); compose_cmdline(svc, cmdline, sizeof(cmdline)); if (!svc_is_sysv(svc)) { if (svc->pid <= 1) return 1; dbg("Sending %s to pid:%d name:%s", sig_name(svc->sighalt), svc->pid, pid_get_name(svc->pid, NULL, 0)); logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], sending %s ...", svc_ident(svc, NULL, 0), svc->pid, sig_name(svc->sighalt)); } else { logit(LOG_CONSOLE | LOG_NOTICE, "Calling '%s stop' ...", cmdline); } /* * Make sure we are no longer considering the service to be starting (if * that was the case). service_monitor() might get confused otherwise, and * leave the service in a lingering, "stopping", state. */ svc_started(svc); /* * Verify there's still something there before we send the reaper. */ if (svc->pid > 1 && !pid_alive(svc->pid)) { svc->pid = 0; return 0; } if (!svc->desc[0]) do_progress = 0; if (runlevel != 1 && do_progress) print_desc("Stopping ", svc->desc); if (!svc_is_sysv(svc)) { if (svc->pid > 1) { /* Kill all children in the same proess group, e.g. logit */ rc = kill(-svc->pid, svc->sighalt); dbg("kill(-%d, %d) => rc %d, errno %d", svc->pid, svc->sighalt, rc, errno); /* PID lost or forking process never really started */ if (rc == -1 && (errno == ESRCH || errno == ENOENT)) { service_cleanup(svc); svc_set_state(svc, SVC_HALTED_STATE); } else svc_set_state(svc, SVC_STOPPING_STATE); } else { service_cleanup(svc); svc_set_state(svc, SVC_HALTED_STATE); } } else { char *args[MAX_NUM_SVC_ARGS + 1]; size_t i = 0, j; pid_t pid; args[i++] = svc->cmd; /* this handles, e.g., bridge-stop br0 stop */ for (j = 0; j < MAX_NUM_SVC_ARGS - 2; j++) { if (!strlen(svc->args[j])) break; args[i++] = svc->args[j]; } args[i++] = "stop"; args[i] = NULL; pid = fork(); switch (pid) { case 0: setsid(); redirect(svc); exec_runtask(args[0], &args[1]); _exit(0); break; case -1: err(1, "Failed fork() to call sysv script '%s stop'", cmdline); rc = 1; break; default: svc_set_state(svc, SVC_STOPPING_STATE); break; } } if (runlevel != 1) print_result(rc); return rc; } /** * service_restart - Restart a service by sending %SIGHUP * @svc: Service to reload * * This function does some basic checks of the runtime state of Finit * and a sanity check of the @svc before sending %SIGHUP. * * Returns: * POSIX OK(0) or non-zero on error. */ static int service_restart(svc_t *svc) { int do_progress = 1; pid_t lost = 0; int rc; /* Ignore if finit is SIGSTOP'ed */ if (is_norespawn()) return 1; if (!svc || !svc->sighup) return 1; if (svc->pid <= 1) { dbg("%s: bad PID %d for %s, SIGHUP", svc_ident(svc, NULL, 0), svc->pid, svc->cmd); svc->start_time = svc->pid = 0; return 1; } /* Skip progress if desc disabled or bootstrap task */ if (!svc->desc[0] || svc_in_runlevel(svc, 0)) do_progress = 0; if (do_progress) print_desc("Restarting ", svc->desc); dbg("Sending SIGHUP to PID %d", svc->pid); logit(LOG_CONSOLE | LOG_NOTICE, "Restarting %s[%d], sending SIGHUP ...", svc_ident(svc, NULL, 0), svc->pid); rc = kill(svc->pid, SIGHUP); if (rc == -1 && (errno == ESRCH || errno == ENOENT)) { /* nobody home, reset internal state machine */ lost = svc->pid; } else { /* Declare we're waiting for svc to re-assert/touch its pidfile */ svc_starting(svc); /* Service does not maintain a PID file on its own */ if (svc_has_pidfile(svc)) { sched_yield(); touch(pid_file(svc)); } } if (do_progress) print_result(rc); if (lost) service_monitor(lost, 0); return rc; } /** * service_reload_dynamic - Called on SIGHUP, 'init q' or 'initctl reload' * * This function is called when Finit has received SIGHUP to reload * .conf files in /etc/finit.d. It is responsible for starting, * stopping and reloading (forwarding SIGHUP) to processes affected. */ void service_reload_dynamic(void) { sm_set_reload(&sm); sm_step(&sm); } /** * service_runlevel - Change to a new runlevel * @newlevel: New runlevel to activate * * Stops all services not in @newlevel and starts, or lets continue to run, * those in @newlevel. Also updates @prevlevel and active @runlevel. */ void service_runlevel(int newlevel) { if (!rescue && runlevel <= 1 && newlevel > 1) networking(1); sm_set_runlevel(&sm, newlevel); sm_step(&sm); if (!rescue && (runlevel <= 1 || runlevel == 6)) networking(0); } /* * log:/path/to/logfile,priority:facility.level,tag:ident */ static void parse_log(svc_t *svc, char *arg) { char *tok; tok = strtok(arg, ":, "); while (tok) { if (!strcmp(tok, "log")) svc->log.enabled = 1; else if (!strcmp(tok, "null") || !strcmp(tok, "/dev/null")) svc->log.null = 1; else if (!strcmp(tok, "console") || !strcmp(tok, "/dev/console")) svc->log.console = 1; else if (tok[0] == '/') strlcpy(svc->log.file, tok, sizeof(svc->log.file)); else if (!strcmp(tok, "priority") || !strcmp(tok, "prio")) strlcpy(svc->log.prio, strtok(NULL, ","), sizeof(svc->log.prio)); else if (!strcmp(tok, "tag") || !strcmp(tok, "identity") || !strcmp(tok, "ident")) strlcpy(svc->log.ident, strtok(NULL, ","), sizeof(svc->log.ident)); tok = strtok(NULL, ":=, "); } } static int parse_notify(char *arg) { if (!strcmp(arg, "systemd")) return 1; if (!strcmp(arg, "s6")) return 2; return 0; /* unsupported/disabled */ } static void parse_env(svc_t *svc, char *env) { if (!env) return; if (strlen(env) >= sizeof(svc->env)) { errx(1, "%s: env file is too long (>%zu chars)", svc_ident(svc, NULL, 0), sizeof(svc->env)); return; } strlcpy(svc->env, env, sizeof(svc->env)); } static void parse_cgroup(svc_t *svc, char *cgroup) { char *ptr = cgroup; if (!cgroup) return; if (cgroup[0] == '.') { ptr = strchr(cgroup, ':'); if (ptr) *ptr++ = 0; strlcpy(svc->cgroup.name, &cgroup[1], sizeof(svc->cgroup.name)); if (!ptr) return; } if (strlen(ptr) >= sizeof(svc->cgroup)) { errx(1, "%s: cgroup settings too long (>%zu chars)", svc_ident(svc, NULL, 0), sizeof(svc->cgroup)); return; } strlcpy(svc->cgroup.cfg, ptr, sizeof(svc->cgroup.cfg)); } static void parse_sighalt(svc_t *svc, char *arg) { int signo; signo = sig_num(arg); if (signo == -1) return; svc->sighalt = signo; } static void parse_killdelay(svc_t *svc, char *delay) { const char *errstr; long long sec; sec = strtonum(delay, 1, 60, &errstr); if (errstr) { errx(1, "%s: killdelay %s is %s (1-60)", svc_ident(svc, NULL, 0), delay, errstr); return; } /* convert to msec */ svc->killdelay = (int)(sec * 1000); } static void parse_script(char *type, char *script, char *buf, size_t len) { if (access(script, X_OK)) logit(LOG_WARNING, "%s:%s is missing or not executable, skipping.", type, script); else strlcpy(buf, script, len); } /* * 'name:' or derived from '/path/to/cmd args' */ static char *parse_name(char *cmd, char *arg) { char *name = NULL; if (arg && !strncasecmp(arg, "name:", 5)) { name = arg + 5; } else { char *ptr; ptr = strchr(cmd, ' '); if (!ptr) ptr = cmd + strlen(cmd); while (ptr > cmd) { if (*ptr == '/') { ptr++; break; } ptr--; } name = ptr; } return name; } /* * Update the command line args in the svc struct */ static void parse_cmdline_args(svc_t *svc, char *cmd, char **args) { char prev[MAX_ARG_LEN]; int diff = 0; char sep = 0; char *arg; int i = 0; if (strcmp(svc->args[i], cmd)) diff++; strlcpy(svc->args[i++], cmd, sizeof(svc->args[0])); /* * Copy supplied args. Stop at MAX_NUM_SVC_ARGS-1 to allow the args * array to be zero-terminated. */ while ((arg = strtok_r(NULL, " ", args)) && i < (MAX_NUM_SVC_ARGS - 1)) { char ch = arg[0]; size_t len; if (!sep) { strlcpy(prev, svc->args[i], sizeof(prev)); svc->args[i][0] = 0; } /* XXX: ugly string arg re-concatenation, fixme */ if (ch == '"' || ch == '\'') sep = ch; else if (sep) strlcat(svc->args[i], " ", sizeof(svc->args[0])); strlcat(svc->args[i], arg, sizeof(svc->args[0])); /* string arg contained already? */ len = strlen(arg); if (sep && len >= 1) { ch = arg[len - 1]; if (ch != sep) continue; } /* replace any @console arg with the expanded device name */ if (svc_is_tty(svc) && tty_isatcon(svc->args[i])) strlcpy(svc->args[i], svc->dev, sizeof(svc->args[i])); if (strcmp(svc->args[i], prev)) diff++; sep = 0; i++; } /* * Clear remaining args in case they were set earlier. * This also zero-terminates the args array. */ while (i < MAX_NUM_SVC_ARGS) { if (svc->args[i++][0]) { svc->args[i-1][0] = 0; diff++; } } /* * Check also for changes to /etc/default/foo, because this * also constitutes changes to command line args. */ diff += conf_changed(svc_getenv(svc)); if (diff) { char buf[256]; for (buf[0] = 0, i = 0; i < MAX_NUM_SVC_ARGS; i++) { if (!strlen(svc->args[i])) break; strlcat(buf, " ", sizeof(buf)); strlcat(buf, svc->args[i], sizeof(buf)); } dbg("Modified args for %s detected: %s", cmd, buf); } svc->args_dirty = (diff > 0); } /** * service_register - Register service, task or run commands * @type: %SVC_TYPE_SERVICE(0), %SVC_TYPE_TASK(1), %SVC_TYPE_RUN(2) * @cfg: Configuration, complete command, with -- for description text * @rlimit: Limits for this service/task/run, may be global limits * @file: The file name service was loaded from * * This function is used to register commands to be run on different * system runlevels with optional username. The @type argument details * if it's service to bo monitored/respawned (daemon), a one-shot task * or a command that must run in sequence and not in parallel, like * service and task commands do. * * The @line can optionally start with a username, denoted by an @ * character. Like this: * * service @username [!0-6,S] /path/to/daemon arg -- Description * task @username [!0-6,S] /path/to/task arg -- Description * run @username [!0-6,S] /path/to/cmd arg -- Description * * If the username is left out the command is started as root. The [] * brackets denote the allowed runlevels, if left out the default for a * service is set to [2-5]. Allowed runlevels mimic that of SysV init * with the addition of the 'S' runlevel, which is only run once at * startup. It can be seen as the system bootstrap. If a task or run * command is listed in more than the [S] runlevel they will be called * when changing runlevel. * * Services (daemons) also support an optional argument. * This is for services that depend on another service, e.g. Quagga ripd * depends on zebra, or require a system gateway or interface to be up * before they are started. Or restarted, or even SIGHUP'ed, when the * gateway changes or interfaces come and go. The special case when a * service is declared with means it does not support SIGHUP but * must be STOP/START'ed at system reconfiguration. * * Conditions can for example be: pid/NAME:ID for process dependencies, * net//up or net//exists. The condition handling is * further described in doc/conditions.md, but worth mentioning here is * that the condition a services *provides* can be modified using the * :ID and name:foo syntax. * * For multiple instances of the same command, e.g. multiple DHCP * clients, the user must enter an ID, using the :ID syntax. * * service :eth1 /sbin/udhcpc -i eth1 * service :eth2 /sbin/udhcpc -i eth2 * * Without the :ID syntax, Finit replaces the first service line with * the contents of the second. The :ID can be any string value and * defaults to "" (empty string). * * Returns: * POSIX OK(0) on success, or non-zero errno exit status on failure. */ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) { char *cmd, *desc, *runlevels = NULL, *cond = NULL; char *username = NULL, *log = NULL, *pid = NULL; char *name = NULL, *halt = NULL, *delay = NULL; char *id = NULL, *env = NULL, *cgroup = NULL; char *pre_script = NULL, *post_script = NULL; char *ready_script = NULL; char *notify = NULL; struct tty tty = { 0 }; char *dev = NULL; int respawn = 0; int levels = 0; int forking = 0, manual = 0; int restart_max = SVC_RESPAWN_MAX; int restart_tmo = 0; unsigned oncrash_action = SVC_ONCRASH_IGNORE; char *line, *args; svc_t *svc; if (!cfg) { errx(1, "Invalid input argument"); return errno = EINVAL; } line = strdupa(cfg); if (!line) return 1; desc = strstr(line, "-- "); if (desc) { *desc = 0; desc += 3; while (*desc && isblank(*desc)) desc++; } else { int pos; /* Find "--\n" to denote empty/no description */ pos = (int)strlen(line) - 2; if (pos > 0 && !strcmp(&line[pos], "--")) { line[pos] = 0; desc = &line[pos]; } } cmd = strtok_r(line, " \t", &args); if (!cmd) { incomplete: errx(1, "Incomplete service '%s', cannot register", cfg); return errno = ENOENT; } while (cmd) { char *arg; if (type == SVC_TYPE_TTY && cmd[0] == '@') break; /* @console */ if (cmd[0] == '@') /* @username[:group] */ username = &cmd[1]; else if (cmd[0] == '[') /* [runlevels] */ runlevels = &cmd[0]; else if (cmd[0] == '<') /* <[!][cond][,cond..]> */ cond = &cmd[1]; else if (cmd[0] == ':') /* :ID */ id = &cmd[1]; else if (MATCH_CMD(cmd, "log", arg)) log = cmd; else if (MATCH_CMD(cmd, "pid", arg)) pid = cmd; else if (MATCH_CMD(cmd, "name:", arg)) name = cmd; else if (MATCH_CMD(cmd, "notify:", arg)) notify = arg; else if (MATCH_CMD(cmd, "type:forking", arg)) forking = 1; else if (MATCH_CMD(cmd, "manual:yes", arg)) manual = 1; else if (MATCH_CMD(cmd, "restart:", arg)) { if (MATCH_CMD(arg, "always", arg)) restart_max = -1; else restart_max = atoi(arg); } else if (MATCH_CMD(cmd, "restarttmo:", arg)) /* compat alias */ restart_tmo = atoi(arg) * 1000; else if (MATCH_CMD(cmd, "restart_sec:", arg)) restart_tmo = atoi(arg) * 1000; else if (MATCH_CMD(cmd, "norestart", arg)) restart_max = 0; else if (MATCH_CMD(cmd, "oncrash:", arg)) { if (MATCH_CMD(arg, "reboot", arg)) oncrash_action = SVC_ONCRASH_REBOOT; if (MATCH_CMD(arg, "script", arg)) oncrash_action = SVC_ONCRASH_SCRIPT; } else if (MATCH_CMD(cmd, "respawn", arg)) respawn = 1; else if (MATCH_CMD(cmd, "halt:", arg)) halt = arg; else if (MATCH_CMD(cmd, "kill:", arg)) delay = arg; else if (MATCH_CMD(cmd, "pre:", arg)) pre_script = arg; else if (MATCH_CMD(cmd, "post:", arg)) post_script = arg; else if (MATCH_CMD(cmd, "ready:", arg)) ready_script = arg; else if (MATCH_CMD(cmd, "env:", arg)) env = arg; else if (MATCH_CMD(cmd, "cgroup:", arg)) cgroup = arg; /* only settings */ else if (MATCH_CMD(cmd, "cgroup.", arg)) cgroup = arg; /* with group */ else break; /* Check if valid command follows... */ cmd = strtok_r(NULL, " ", &args); if (!cmd) goto incomplete; } levels = conf_parse_runlevels(runlevels); if (runlevel > 0 && !ISOTHER(levels, 0)) { dbg("Skipping %s, bootstrap is completed.", cmd); return 0; } name = parse_name(cmd, name); if (!id) id = ""; if (type == SVC_TYPE_TTY) { size_t i, len = 0; if (tty_parse_args(&tty, cmd, &args)) return errno; if (tty.cmd) len += strlen(tty.cmd); else len += 3; len += tty.num + 1; for (i = 0; i < tty.num; i++) len += strlen(tty.args[i]) + 1; line = alloca(len); if (!line) return errno; snprintf(line, len, "%s", tty.cmd ? tty.cmd : "tty"); for (i = 0; i < tty.num; i++) { strlcat(line, " ", len); strlcat(line, tty.args[i], len); } cmd = strtok_r(line, " \t", &args); if (!cmd) return errno; if (tty_isatcon(tty.dev)) dev = tty_atcon(); else dev = tty.dev; /* tty's always respawn, never incr. restart_cnt */ respawn = 1; next: svc = svc_find_by_tty(dev); } else svc = svc_find(name, id); if (!svc) { dbg("Creating new svc for %s name %s id %s type %d", cmd, name, id, type); svc = svc_new(cmd, name, id, type); if (!svc) { errx(1, "Out of memory, cannot register service %s", cmd); return errno = ENOMEM; } if (manual) svc_stop(svc); } else { dbg("Found existing svc for %s name %s id %s type %d", cmd, name, id, type); /* update type, may have changed from service -> task */ svc->type = type; /* update path, may have changed on reload */ strlcpy(svc->cmd, cmd, sizeof(svc->cmd)); /* e.g., if missing cmd or env before */ if (!manual) svc_unblock(svc); } if (username) { char *ptr = strchr(username, ':'); if (ptr) { *ptr++ = 0; strlcpy(svc->group, ptr, sizeof(svc->group)); } strlcpy(svc->username, username, sizeof(svc->username)); } else { getcuser(svc->username, sizeof(svc->username)); getcgroup(svc->group, sizeof(svc->group)); } svc->runlevels = levels; dbg("Service %s runlevel 0x%02x", svc_ident(svc, NULL, 0), svc->runlevels); conf_parse_cond(svc, cond); if (type == SVC_TYPE_TTY) { char *ptr; if (dev) strlcpy(svc->dev, dev, sizeof(svc->dev)); if (tty.baud) strlcpy(svc->baud, tty.baud, sizeof(svc->baud)); if (tty.term) strlcpy(svc->term, tty.term, sizeof(svc->term)); svc->noclear = tty.noclear; svc->nowait = tty.nowait; svc->nologin = tty.nologin; svc->notty = tty.notty; svc->rescue = tty.rescue; /* TTYs cannot be redirected */ log = NULL; /* Create name:id tuple for identity, e.g., tty:S0 */ ptr = strrchr(svc->dev, '/'); if (ptr) ptr++; else ptr = svc->dev; if (!strncmp(ptr, "tty", 3)) ptr += 3; if (!id || id[0] == 0) id = ptr; strlcpy(svc->name, "tty", sizeof(svc->name)); strlcpy(svc->id, id, sizeof(svc->id)); } parse_cmdline_args(svc, cmd, &args); /* * Warn if svc generates same condition (based on name:id) * as an existing service. */ svc_validate(svc); if (halt) parse_sighalt(svc, halt); else svc->sighalt = svc_is_tty(svc) ? SIGHUP : SIGTERM; if (delay) parse_killdelay(svc, delay); else svc->killdelay = SVC_TERM_TIMEOUT; if (pre_script) parse_script("pre", pre_script, svc->pre_script, sizeof(svc->pre_script)); else memset(svc->pre_script, 0, sizeof(svc->pre_script)); if (post_script) parse_script("post", post_script, svc->post_script, sizeof(svc->post_script)); else memset(svc->post_script, 0, sizeof(svc->post_script)); if (ready_script) parse_script("ready", ready_script, svc->ready_script, sizeof(svc->ready_script)); else memset(svc->ready_script, 0, sizeof(svc->ready_script)); if (!svc_is_tty(svc)) { if (log) parse_log(svc, log); else svc->log.enabled = 0; } if (notify) { int type = parse_notify(notify); if (type <= 0) goto disable_notify; svc->notify = type; } else if (svc->notify) { disable_notify: /* XXX: close existing socket */ svc->notify = 0; } if (desc) strlcpy(svc->desc, desc, sizeof(svc->desc)); else if (type == SVC_TYPE_TTY) snprintf(svc->desc, sizeof(svc->desc), "Getty on %s", svc->dev); if (env) parse_env(svc, env); else memset(svc->env, 0, sizeof(svc->env)); if (file) strlcpy(svc->file, file, sizeof(svc->file)); else memset(svc->file, 0, sizeof(svc->file)); svc->manual = manual; svc->respawn = respawn; svc->forking = forking; svc->restart_max = restart_max; svc->restart_tmo = restart_tmo; svc->oncrash_action = oncrash_action; /* Decode any (optional) pid:/optional/path/to/file.pid */ if (svc_is_daemon(svc) || svc_is_sysv(svc)) { char tmp[sizeof(svc->name) + 6]; /* pid:! + svc->name */ /* no pid: set, figure out a default to track this svc */ if (!pid && forking) { snprintf(tmp, sizeof(tmp), "pid:!%s", svc->name); pid = tmp; logit(LOG_INFO, "%s: forking but no pid:!file set, guessing -> %s", svc->name, tmp); } if (pid && pid_file_parse(svc, pid)) logit(LOG_WARNING, "%s: service has invalid 'pid:' config: %s", svc->name, pid); /* only set forking based on pidfile if user supplied pid: option */ if (pid && svc->pidfile[0] == '!') svc->forking = 1; if (svc->restart_tmo == 0) { if (svc_is_forking(svc)) svc->restart_tmo = 2000; else svc->restart_tmo = 1; } } /* Set configured limits */ memcpy(svc->rlimit, rlimit, sizeof(svc->rlimit)); /* Seed with currently active group, may be empty */ strlcpy(svc->cgroup.name, cgroup_current, sizeof(svc->cgroup.name)); if (cgroup) parse_cgroup(svc, cgroup); /* New, recently modified or unchanged ... used on reload. */ if ((file && conf_changed(file)) || conf_changed(svc_getenv(svc)) || svc->args_dirty) svc_mark_dirty(svc); else svc_mark_clean(svc); svc_enable(svc); /* for finit native services only, e.g. plugins/hotplug.c */ if (!file) svc->protect = 1; /* continue expanding any 'tty @console ...' */ if (tty_isatcon(tty.dev)) { dev = tty_atcon(); if (dev) goto next; } return 0; } /* * This function is called when cleaning up lingering (stopped) services * after a .conf reload. * * We need to ensure we properly stop the service before removing it, * including stopping any pending restart or SIGKILL timers before we * proceed to free() the svc itself. */ void service_unregister(svc_t *svc) { char *c; if (!svc) return; service_stop(svc); for (c = strtok(svc->cond, ","); c; c = strtok(NULL, ",")) devmon_del_cond(c); svc_del(svc); } void service_monitor(pid_t lost, int status) { svc_t *svc; if (lost <= 1) return; svc = svc_find_by_pid(lost); if (!svc) { if (service_script_del(lost)) dbg("collected unknown PID %d", lost); return; } switch (svc->state) { case SVC_CLEANUP_STATE: case SVC_SETUP_STATE: dbg("collected script %s(%d), normal exit: %d, signaled: %d, exit code: %d", svc->state == SVC_CLEANUP_STATE ? svc->post_script : svc->pre_script, lost, WIFEXITED(status), WIFSIGNALED(status), WEXITSTATUS(status)); kill(-svc->pid, SIGKILL); goto done; default: dbg("collected %s(%d), normal exit: %d, signaled: %d, exit code: %d", svc_ident(svc, NULL, 0), lost, WIFEXITED(status), WIFSIGNALED(status), WEXITSTATUS(status)); svc->status = status; break; } /* Forking sysv/services declare themselves with pid:!/path/to/pid.file */ if (svc_is_forking(svc)) { /* Likely start script exiting */ if (svc_is_starting(svc)) { svc->pid = 0; /* Expect no more activity from this one */ goto cont; } logit(LOG_CONSOLE | LOG_NOTICE, "Stopped %s[%d]", svc_ident(svc, NULL, 0), lost); } /* Terminate any children in the same proess group, e.g. logit */ kill(-svc->pid, SIGKILL); /* Try removing PID file (in case service does not clean up after itself) */ if (svc_is_daemon(svc) || svc_is_tty(svc)) { service_cleanup(svc); } else if (svc_is_runtask(svc)) { /* run/task should run at least once per runlevel */ if (WIFEXITED(status) && !WEXITSTATUS(status)) svc->started = 1; else svc->started = 0; } done: /* No longer running, update books. */ svc->start_time = svc->pid = 0; cont: if (!service_step(svc)) { /* Clean out any bootstrap tasks, they've had their time in the sun. */ if (svc_clean_bootstrap(svc)) dbg("collected bootstrap task %s(%d), removing.", svc_ident(svc, NULL, 0), lost); } sm_step(&sm); } static void svc_mark_affected(char *cond) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (!svc_has_cond(svc)) continue; if (cond_affects(cond, svc->cond)) svc_mark_dirty(svc); } } /* * Called on conf_reload() to update service reverse dependencies. * E.g., if ospfd depends on zebra and the zebra Finit conf has * changed, we need to mark the ospfd Finit conf as changed too. */ void service_update_rdeps(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { char cond[MAX_COND_LEN]; if (!svc_is_changed(svc)) continue; svc_mark_affected(mkcond(svc, cond, sizeof(cond))); } } static void service_kill_script(svc_t *svc) { if (svc->pid <= 1) return; kill(-svc->pid, SIGKILL); } /* * Shared env vars for both pre: and post: scripts */ static void set_pre_post_envs(svc_t *svc, const char *type) { char buf[25 + 256] = "/etc/default:/etc/conf.d"; setenv("SERVICE_TYPE", svc_typestr(svc), 1); setenv("SERVICE_IDENT", svc_ident(svc, NULL, 0), 1); setenv("SERVICE_SCRIPT_TYPE", type, 1); #ifdef FINIT_SYSCONFIG strlcat(buf, ":", sizeof(buf)); strlcat(buf, FINIT_SYSCONFIG, sizeof(buf)); #endif setenv("SERVICE_CONF_DIR", buf, 1); } static void service_pre_script(svc_t *svc) { svc->pid = service_fork(svc); if (svc->pid < 0) { err(1, "Failed forking off %s pre:script %s", svc_ident(svc, NULL, 0), svc->pre_script); return; } if (svc->pid == 0) { char buf[CMD_SIZE]; char *argv[4] = { "sh", "-ac", buf, NULL }; char *env_file; env_file = svc_getenv(svc); if (env_file) snprintf(buf, sizeof(buf), ". %s; exec %s", env_file, svc->pre_script); else strlcpy(buf, svc->pre_script, sizeof(buf)); set_pre_post_envs(svc, "pre"); execvp(_PATH_BSHELL, argv); _exit(EX_OSERR); } dbg("%s: pre:script %s started as PID %d", svc_ident(svc, NULL, 0), svc->pre_script, svc->pid); /* Short hard-coded timeout to prevent locking up Finit */ service_timeout_after(svc, svc->killdelay, service_kill_script); } static void service_post_script(svc_t *svc) { svc->pid = service_fork(svc); if (svc->pid < 0) { err(1, "Failed forking off %s post:script %s", svc_ident(svc, NULL, 0), svc->post_script); return; } if (svc->pid == 0) { char buf[CMD_SIZE]; char *argv[4] = { "sh", "-ac", buf, NULL }; char *env_file; int rc, sig; rc = WEXITSTATUS(svc->status); sig = WTERMSIG(svc->status); env_file = svc_getenv(svc); if (env_file) snprintf(buf, sizeof(buf), ". %s; exec %s", env_file, svc->post_script); else strlcpy(buf, svc->post_script, sizeof(buf)); set_pre_post_envs(svc, "post"); if (WIFEXITED(svc->status)) { char val[4]; setenv("EXIT_CODE", "exited", 1); snprintf(val, sizeof(val), "%d", rc & 0xff); setenv("EXIT_STATUS", val, 1); } else if (WIFSIGNALED(svc->status)) { setenv("EXIT_CODE", "signal", 1); setenv("EXIT_STATUS", sig2str(sig), 1); } if (svc_is_crashing(svc)) setenv("EXIT_CODE", "crashed", 1); execvp(_PATH_BSHELL, argv); _exit(EX_OSERR); } dbg("%s: post:script %s started as PID %d", svc_ident(svc, NULL, 0), svc->post_script, svc->pid); /* Short hard-coded timeout to prevent locking up Finit */ service_timeout_after(svc, svc->killdelay, service_kill_script); } void service_ready_script(svc_t *svc) { pid_t pid; if (access(svc->ready_script, X_OK)) return; pid = service_fork(svc); if (pid < 0) { err(1, "Failed forking off %s ready-script %s", svc_ident(svc, NULL, 0), svc->ready_script); return; } if (pid == 0) { char buf[CMD_SIZE]; char *argv[4] = { "sh", "-ac", buf, NULL }; char *env_file; env_file = svc_getenv(svc); if (env_file) snprintf(buf, sizeof(buf), ". %s; exec %s", env_file, svc->ready_script); else strlcpy(buf, svc->ready_script, sizeof(buf)); set_pre_post_envs(svc, "ready"); execvp(_PATH_BSHELL, argv); _exit(EX_OSERR); } dbg("%s: ready:script %s started as PID %d", svc_ident(svc, NULL, 0), svc->ready_script, pid); /* Short hard-coded timeout to prevent locking up Finit */ service_script_add(svc, pid); } static void service_retry(svc_t *svc) { char *restart_cnt = (char *)&svc->restart_cnt; int timeout; service_timeout_cancel(svc); if (svc->respawn) { dbg("%s crashed/exited, respawning ...", svc_ident(svc, NULL, 0)); svc_unblock(svc); service_step(svc); return; } if (svc->state != SVC_HALTED_STATE || svc->block != SVC_BLOCK_RESTARTING) { svc->restart_tmo = svc->restart_saved; svc_set_state(svc, SVC_RUNNING_STATE); logit(LOG_CONSOLE | LOG_NOTICE, "Successfully restarted crashing service %s.", svc_ident(svc, NULL, 0)); return; } /* Peak instability index */ if (svc->restart_max != -1 && *restart_cnt >= svc->restart_max) { logit(LOG_CONSOLE | LOG_WARNING, "Service %s keeps crashing, not restarting.", svc_ident(svc, NULL, 0)); svc_crashing(svc); *restart_cnt = 0; svc->restart_tmo = svc->restart_saved; switch (svc->oncrash_action) { case SVC_ONCRASH_REBOOT: logit(LOG_ERR, "%s issuing reboot", svc_ident(svc, NULL, 0)); sync(); kill(1, SIGTERM); break; case SVC_ONCRASH_SCRIPT: if (svc_has_post(svc)) { dbg("calling post:script %s, crashing.", svc->post_script); service_post_script(svc); } break; default: break; } service_step(svc); return; } (*restart_cnt)++; dbg("%s crashed, trying to start it again, attempt %d", svc_ident(svc, NULL, 0), *restart_cnt); if ((*restart_cnt) == 1) svc->restart_saved = svc->restart_tmo; /* Wait 2s for the first 5 respawns, then back off to 5s */ timeout = ((*restart_cnt) <= (svc->restart_max / 2)) ? 2000 : 5000; /* If a longer timeout was specified in the conf, use that instead. */ svc->restart_tmo = max(svc->restart_tmo, timeout); logit(LOG_CONSOLE|LOG_WARNING, "Service %s[%d] died, restarting in %d msec (%d/%d)", svc_ident(svc, NULL, 0), svc->oldpid, svc->restart_tmo, *restart_cnt, svc->restart_max); svc_unblock(svc); service_step(svc); service_timeout_after(svc, svc->restart_tmo, service_retry); } static void svc_set_state(svc_t *svc, svc_state_t new_state) { svc_state_t *state = (svc_state_t *)&svc->state; /* if PID isn't collected within SVC_TERM_TIMEOUT msec, kill it! */ if (new_state == SVC_STOPPING_STATE) { dbg("%s is stopping, wait %d sec before sending SIGKILL ...", svc_ident(svc, NULL, 0), svc->killdelay / 1000); service_timeout_cancel(svc); service_timeout_after(svc, svc->killdelay, service_kill); } if (svc->state == new_state) return; *state = new_state; if (svc_is_runtask(svc)) { char success[MAX_COND_LEN], failure[MAX_COND_LEN]; snprintf(success, sizeof(success), "%s/%s/success", svc_typestr(svc), svc_ident(svc, NULL, 0)); snprintf(failure, sizeof(failure), "%s/%s/failure", svc_typestr(svc), svc_ident(svc, NULL, 0)); /* create success/failure condition when entering SVC_DONE_STATE. */ if (*state == SVC_DONE_STATE) { if (svc->started) cond_set_oneshot(success); else cond_set_oneshot(failure); } /* clear all conditions when entering SVC_HALTED_STATE. */ if (*state == SVC_HALTED_STATE) { cond_clear(success); cond_clear(failure); } } if (svc_is_daemon(svc)) { char cond[MAX_COND_LEN]; snprintf(cond, sizeof(cond), "service/%s/", svc_ident(svc, NULL, 0)); cond_clear(cond); switch (svc->state) { case SVC_HALTED_STATE: case SVC_RUNNING_STATE: strlcat(cond, svc_status(svc), sizeof(cond)); cond_set_oneshot(cond); break; default: break; } } } /* * Called by pidfile plugin for forking services that have successfully started. * We get here when the service_monitor() collects the PID, sets svc->pid to 0, * and the state machine prepares for the worst bu setting state to HALTED. */ void service_forked(svc_t *svc) { /* Stop forking_retry() timer */ service_timeout_cancel(svc); /* Not crashing, restore RUNNING state */ svc_unblock(svc); svc_set_state(svc, SVC_RUNNING_STATE); } /* Set service/foo/ready condition for services and call optional ready:script */ void service_ready(svc_t *svc) { char buf[MAX_COND_LEN]; if (!svc_is_daemon(svc)) return; snprintf(buf, sizeof(buf), "service/%s/ready", svc_ident(svc, NULL, 0)); cond_set(buf); if (svc_has_ready(svc)) service_ready_script(svc); } /* * Transition task/run/service * * Returns: non-zero if the @svc is no longer valid (removed) */ int service_step(svc_t *svc) { cond_state_t cond; svc_state_t old_state; svc_cmd_t enabled; char *restart_cnt = (char *)&svc->restart_cnt; int changed = 0; int err; restart: old_state = svc->state; enabled = svc_enabled(svc); dbg("%20s(%4d): %8s %3sabled/%-7s cond:%-4s", svc_ident(svc, NULL, 0), svc->pid, svc_status(svc), enabled ? "en" : "dis", svc_dirtystr(svc), condstr(cond_get_agg(svc->cond))); switch (svc->state) { case SVC_HALTED_STATE: if (enabled) svc_set_state(svc, SVC_WAITING_STATE); break; case SVC_DONE_STATE: if (svc_is_changed(svc)) svc_set_state(svc, SVC_HALTED_STATE); if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled) svc_set_state(svc, SVC_WAITING_STATE); break; case SVC_STOPPING_STATE: if (!svc->pid) { char condstr[MAX_COND_LEN]; dbg("%s: stopped, cleaning up timers and conditions ...", svc_ident(svc, NULL, 0)); if (svc->notify && svc->notify_watcher.fd > 0) { uev_io_stop(&svc->notify_watcher); close(svc->notify_watcher.fd); svc->notify_watcher.fd = 0; } service_timeout_cancel(svc); cond_clear(mkcond(svc, condstr, sizeof(condstr))); switch (svc->type) { case SVC_TYPE_SERVICE: case SVC_TYPE_SYSV: case SVC_TYPE_TTY: if (svc_has_post(svc)) { svc_set_state(svc, SVC_CLEANUP_STATE); service_post_script(svc); } else svc_set_state(svc, SVC_HALTED_STATE); break; case SVC_TYPE_TASK: case SVC_TYPE_RUN: if (svc->manual) svc_stop(svc); svc_set_state(svc, SVC_DONE_STATE); break; default: errx(1, "unknown service type %d", svc->type); break; } } break; case SVC_CLEANUP_STATE: if (!svc->pid) svc_set_state(svc, SVC_HALTED_STATE); break; case SVC_SETUP_STATE: if (!svc->pid) svc_set_state(svc, SVC_STARTING_STATE); break; case SVC_WAITING_STATE: if (!enabled) { svc_set_state(svc, SVC_HALTED_STATE); } else if (cond_get_agg(svc->cond) == COND_ON) { /* wait until all processes have been stopped before continuing... */ if (sm_is_in_teardown(&sm)) break; if (svc_has_pre(svc)) { svc_set_state(svc, SVC_SETUP_STATE); service_pre_script(svc); break; } svc_set_state(svc, SVC_STARTING_STATE); } break; case SVC_STARTING_STATE: if (!enabled) { svc_set_state(svc, SVC_HALTED_STATE); break; } err = service_start(svc); if (err) { if (svc_is_missing(svc)) { svc_set_state(svc, SVC_HALTED_STATE); break; } (*restart_cnt)++; break; } /* Everything went fine, clean and set state */ svc_mark_clean(svc); svc_set_state(svc, SVC_RUNNING_STATE); break; case SVC_RUNNING_STATE: if (!enabled) { service_stop(svc); break; } if (!svc->pid) { if (svc_is_daemon(svc) || svc_is_sysv(svc) || svc_is_tty(svc)) { svc_restarting(svc); /* BLOCK_RESTARTING */ svc_set_state(svc, SVC_HALTED_STATE); /* * Restart directly after the first crash, except for forking services * which we need to wait for the forked-off child to create its pid * file. In both cases, after that, retry after 2 sec */ if (!svc->respawn) { dbg("delayed restart of %s", svc_ident(svc, NULL, 0)); service_timeout_after(svc, svc->restart_tmo, service_retry); goto done; } dbg("respawning %s", svc_ident(svc, NULL, 0)); svc_unblock(svc); break; } if (svc_is_runtask(svc)) { /* only run/task, not sysv here */ svc_set_state(svc, SVC_STOPPING_STATE); svc->once++; break; } } service_timeout_cancel(svc); cond = cond_get_agg(svc->cond); switch (cond) { case COND_OFF: service_stop(svc); break; case COND_FLUX: kill(svc->pid, SIGSTOP); svc_set_state(svc, SVC_PAUSED_STATE); break; case COND_ON: if (svc_is_changed(svc)) { if (svc_nohup(svc)) service_stop(svc); else { /* * wait until all processes have been * stopped before continuing... */ if (sm_is_in_teardown(&sm)) break; service_restart(svc); } svc_mark_clean(svc); } break; } break; case SVC_PAUSED_STATE: if (!enabled) { kill(svc->pid, SIGCONT); service_stop(svc); break; } if (!svc->pid) { (*restart_cnt)++; svc_set_state(svc, SVC_WAITING_STATE); break; } cond = cond_get_agg(svc->cond); switch (cond) { case COND_ON: kill(svc->pid, SIGCONT); svc_set_state(svc, SVC_RUNNING_STATE); /* Reassert condition if we go from waiting and no change */ if (!svc_is_changed(svc)) { char name[MAX_COND_LEN]; mkcond(svc, name, sizeof(name)); dbg("Reassert condition %s", name); cond_set_path(cond_path(name), COND_ON); } break; case COND_OFF: dbg("Condition for %s is off, sending SIGCONT + SIGTERM", svc->name); kill(svc->pid, SIGCONT); service_stop(svc); break; case COND_FLUX: break; } break; } if (svc->state != old_state) { dbg("%20s(%4d): -> %8s", svc_ident(svc, NULL, 0), svc->pid, svc_status(svc)); changed++; goto restart; } done: /* * When a run/task/service changes state, e.g. transitioning from * waiting to running, other services may need to change state too. */ if (changed) schedule_work(&work); return 0; } void service_step_all(int types) { svc_foreach_type(types, service_step); } void service_worker(void *unused) { service_step_all(SVC_TYPE_RESPAWN | SVC_TYPE_RUNTASK); } /** * svc_clean_runtask - Clear once flag of runtasks * * XXX: runtasks should be stopped before calling this */ void service_runtask_clean(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (!svc_is_runtask(svc)) continue; svc->once = 0; if (svc->state == SVC_DONE_STATE) svc_set_state(svc, SVC_HALTED_STATE); } } /** * service_completed - Have run/task completed in current runlevel * * This function checks if all run/task have run once in the current * runlevel. E.g., at bootstrap we must wait for these scripts or * programs to complete their run before switching to the configured * runlevel. * * All tasks with %HOOK_SVC_UP, %HOOK_SYSTEM_UP set in their condition * mask are skipped. These tasks cannot run until finalize() * * Returns: * %TRUE(1) or %FALSE(0) */ int service_completed(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (!svc_is_runtask(svc)) continue; if (!svc_enabled(svc)) continue; if (strstr(svc->cond, plugin_hook_str(HOOK_SVC_UP)) || strstr(svc->cond, plugin_hook_str(HOOK_SYSTEM_UP))) { dbg("Skipping %s(%s), post-strap hook", svc->desc, svc_ident(svc, NULL, 0)); continue; } if (!svc->once) { dbg("%s has not yet completed ...", svc_ident(svc, NULL, 0)); return 0; } dbg("%s has completed ...", svc_ident(svc, NULL, 0)); } return 1; } /* * Called in SM_RELOAD_WAIT_STATE to update unmodified non-native * services' READY condition to the current generation. Similar * to the pidfile_reconf() function for native services. */ void service_notify_reconf(void) { svc_t *svc, *iter = NULL; for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (!svc->notify) continue; /* managed by pidfile plugin */ if (svc->state != SVC_RUNNING_STATE) continue; if (svc_is_changed(svc) || svc_is_starting(svc)) continue; service_ready(svc); } } /* * Called when a service sends readiness notification, or when * the service closes its end of the IPC connection. */ void service_notify_cb(uev_t *w, void *arg, int events) { svc_t *svc = (svc_t *)arg; char buf[32]; ssize_t len; if (UEV_ERROR == events) { warn("Spurious problem with %s notify callback, restarting.", svc_ident(svc, NULL, 0)); uev_io_start(w); return; } len = read(w->fd, buf, sizeof(buf) - 1); if (len == -1) { warn("Failed reading notification from %s", svc_ident(svc, NULL, 0)); return; } buf[len] = 0; /* systemd and s6, respectively. The latter then closes the socket */ if (!strcmp(buf, "READY=1\n") || !strcmp(buf, "\n")) { /* * native (pidfile) services are marked as started by * the pidfile plugin. */ svc_started(svc); /* * On reload, and this svc is unmodified, it is up to * the service_notify_reconf() function to step the * generation of the READY condition. */ service_ready(svc); /* s6 applications close their socket after notification */ if (svc->notify == 2) { uev_io_stop(w); close(w->fd); w->fd = 0; } } } /* * Every five¹ minutes we sweep over all services, skipping crashed or * otherwise no longer running ones. Decrement non-zero crash counters * to allow services that have started after an initial crash to slowly * prove themselves again as stable services. Previously this counter * was reset as soon as such services had stopped crashing at least once * per second. This new scheme allows us to catch those that rage-quit * immediately when we try to start them, but now also those that are * only slightly buggy -- when they reach their restart_max, they too * are marked 'crashed'. * * This does not affect the restart_tot counter, which you can see in * the output from 'initctl status foo', along with this instability * "index" in parethesis: total (cnt/max) */ static void service_interval_cb(uev_t *w, void *arg, int events) { svc_t *svc, *iter = NULL; (void)arg; if (UEV_ERROR == events) { uev_timer_start(w); return; } for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) { if (svc_is_daemon(svc) || svc_is_sysv(svc)) { char *restart_cnt = (char *)&svc->restart_cnt; if (!svc_is_running(svc)) continue; if (*restart_cnt > 0) { logit(LOG_CONSOLE | LOG_DEBUG, "Aging %s instability index (%d/%d)", svc_ident(svc, NULL, 0), svc->restart_cnt, svc->restart_max); (*restart_cnt)--; } } } service_init(); } /* * The service_interval may change (conf) between invocations, so we * periodically reset the one-shot timer instead of using a periodic. */ void service_init(void) { static int initialized = 0; static uev_t watcher; if (!initialized) { uev_timer_init(ctx, &watcher, service_interval_cb, NULL, service_interval, 0); initialized = 1; } else uev_timer_set(&watcher, service_interval, 0); } /** * Local Variables: * indent-tabs-mode: t * c-file-style: "linux" * End: */