Files
finit/src/service.c
T
Joachim Wiberg a61c2ceb79 Fix #310: service restart delay not working
Always use the configured restart delay for crashing services.  If no
delay is configured, we default to an initial 2000 msec for forking
daemons and start-stop scripts, and 1 msec for non-forking daemons.

We must track the increasing delay in the svc_t because processes can
fail quickly in differing ways.  E.g, the test daemon 'serv' behaves
extra evil by crashing right after having created its pidile (i.e.,
when it's signaled to Finit it is 'ready' and all is dandy ...).  If
we don't track the delay per svc_t we would restart 'serv' after only
1 msec every (other) time.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2022-11-06 13:54:56 +01:00

2498 lines
58 KiB
C

/* Finit service monitor, task starter and generic API for managing svc_t
*
* Copyright (c) 2008-2010 Claudio Matsuoka <cmatsuoka@gmail.com>
* Copyright (c) 2008-2022 Joachim Wiberg <troglobit@gmail.com>
*
* 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 <ctype.h> /* isblank() */
#include <sched.h> /* sched_yield() */
#include <string.h>
#include <sys/reboot.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <net/if.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include <wordexp.h>
#include "cgroup.h"
#include "client.h"
#include "conf.h"
#include "cond.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);
/**
* 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)
{
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;
}
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 (!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] == '/') {
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;
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;
/*
* 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);
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:<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
*
* strtok internal pointer must be positioned at first command line arg
* when this function is called.
*
* Side effect: strtok internal pointer will be modified.
*/
static void parse_cmdline_args(svc_t *svc, char *cmd)
{
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(NULL, " ")) && i < (MAX_NUM_SVC_ARGS - 1)) {
char prev[sizeof(svc->args[0])] = { 0 };
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)
dbg("Modified args for %s detected", cmd);
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] <!COND> /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 <!condition> 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/<IFNAME>/up or net/<IFNAME>/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;
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(line, " \t");
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(NULL, " ");
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(cmd, &tty))
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(line, " \t");
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);
/*
* Warn if svc generates same condition (based on name:id)
* as an existing service.
*/
svc_validate(svc);
if (halt)
parse_sighalt(svc, halt);
if (delay)
parse_killdelay(svc, delay);
if (pre_script)
parse_script("pre", pre_script, svc->pre_script, sizeof(svc->pre_script));
if (post_script)
parse_script("post", post_script, svc->post_script, sizeof(svc->post_script));
if (ready_script)
parse_script("ready", ready_script, svc->ready_script, sizeof(svc->ready_script));
if (log)
parse_log(svc, log);
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);
if (file)
strlcpy(svc->file, file, 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);
if (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_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)
{
if (!svc)
return;
service_stop(svc);
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)
{
svc_state_t *state = (svc_state_t *)&svc->state;
*state = new;
/* if PID isn't collected within SVC_TERM_TIMEOUT msec, kill it! */
if (*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_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;
}
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);
}
if (svc->notify == 2 && svc->notify_watcher.fd == 0)
service_ready(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 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")) {
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:
*/