mirror of
https://github.com/troglobit/finit.git
synced 2026-10-09 16:50:59 +07:00
This refactor should have been done years ago when we first introduced the big state machine. The old functions service_runvel() and the oddly named service_reload_dynamic() are actually state machine control fns, so let relocate them. Also in this commit, remove the global variable 'sm' and rename a few of the critical functions to better names. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
3099 lines
74 KiB
C
3099 lines
74 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-2024 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/un.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 "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"
|
|
|
|
#define NOTIFY_PATH "@run/finit/notify/%d"
|
|
|
|
|
|
/*
|
|
* run tasks block other tasks/services from starting, we track the
|
|
* current run task here. For service_step() and service_start().
|
|
*/
|
|
static pid_t run_block_pid;
|
|
|
|
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);
|
|
static void service_notify_cb(uev_t *w, void *arg, int events);
|
|
|
|
|
|
/**
|
|
* 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;
|
|
|
|
if (UEV_ERROR == events) {
|
|
dbg("%s(): spurious problem, svc %s.", __func__, svc_ident(svc, NULL, 0));
|
|
uev_timer_start(w);
|
|
return;
|
|
}
|
|
|
|
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 (timeout == 0)
|
|
return 0; /* OK, not starting timer. */
|
|
|
|
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, int tmo)
|
|
{
|
|
struct assoc *ptr;
|
|
|
|
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, tmo, 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, killing process group.", 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);
|
|
if (fd == -1) {
|
|
warn("Failed opening /dev/null for stdin redirect");
|
|
return -1;
|
|
}
|
|
|
|
dup2(fd, STDIN_FILENO);
|
|
close(fd);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* 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];
|
|
|
|
strlcpy(buf, prio, sizeof(buf));
|
|
log_parse(buf, &facility, &level);
|
|
|
|
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 svc_pid = getpid();
|
|
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) {
|
|
dbg("Failed posix_openpt(), errno %d: %s", errno, strerror(errno));
|
|
svc->log.enabled = 0;
|
|
return -1;
|
|
}
|
|
if (grantpt(fd) == -1 || unlockpt(fd) == -1) {
|
|
dbg("Failed grantpt()|unlockpt(), errno %d: %s", errno, strerror(errno));
|
|
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;
|
|
|
|
have_sysklogd = 0;
|
|
|
|
pp = popen("logger -h 2>/dev/null", "r");
|
|
if (pp) {
|
|
char buf[128];
|
|
|
|
while (fgets(buf, sizeof(buf), pp)) {
|
|
if (strstr(buf, "-I PID")) {
|
|
have_sysklogd = 1;
|
|
break;
|
|
}
|
|
}
|
|
pclose(pp);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
logit(LOG_WARNING, "failed open() ptsname(%d), errno %d", fd, errno);
|
|
_exit(0);
|
|
}
|
|
dup2(fds, STDIN_FILENO);
|
|
|
|
/* Reset signals */
|
|
sig_unblock();
|
|
|
|
/* Default syslog identity name[:id] */
|
|
tag = svc_ident(svc, buf, sizeof(buf));
|
|
|
|
if (svc->log.ident[0])
|
|
tag = svc->log.ident;
|
|
if (svc->log.prio[0])
|
|
prio = svc->log.prio;
|
|
|
|
/* Neither sysklogd logger or native logit tool available */
|
|
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", svc_pid);
|
|
execlp("logger", "logger", "-f", svc->log.file, "-b", "-t", tag, "-p", prio, "-I", pid, "-r", rot, debug ? "-s" : NULL, 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, debug ? "-s" : NULL, NULL);
|
|
|
|
}
|
|
_exit(1);
|
|
}
|
|
|
|
/*
|
|
* For now, let systemd programs go via our native logit
|
|
* tool. It supports systemd logging defines for stderr
|
|
* parsing. The only real downside is that it cannot do
|
|
* PID faking, like sysklogd's logger tool.
|
|
*/
|
|
if (have_sysklogd && svc->notify != SVC_NOTIFY_SYSTEMD) {
|
|
char pid[16];
|
|
|
|
snprintf(pid, sizeof(pid), "%d", svc_pid);
|
|
execlp("logger", "logger", "-t", tag, "-p", prio, "-I", pid, debug ? "-s" : NULL, NULL);
|
|
} else {
|
|
execlp(_PATH_LOGIT, "logit", "-t", tag, "-p", prio, debug ? "-s" : NULL, 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, *val, *line, *fn;
|
|
FILE *fp;
|
|
|
|
fn = svc_getenv(svc);
|
|
if (!fn)
|
|
return;
|
|
|
|
/* Warning in service_start() after svc_checkenv() */
|
|
fp = fopen(fn, "r");
|
|
if (!fp)
|
|
return;
|
|
|
|
buf = alloca(LINE_SIZE);
|
|
val = alloca(LINE_SIZE);
|
|
if (!buf || !val) {
|
|
warn("Failed allocating temporary env buffer");
|
|
return;
|
|
}
|
|
|
|
line = buf;
|
|
while (fgets(line, LINE_SIZE, fp)) {
|
|
wordexp_t we = { 0 };
|
|
char *key, *value;
|
|
size_t i;
|
|
|
|
/* Trim newline */
|
|
key = chomp(line);
|
|
|
|
/* skip any leading whitespace */
|
|
while (isspace(*key))
|
|
key++;
|
|
|
|
/* skip comments */
|
|
if (*key == '#' || *key == ';')
|
|
continue;
|
|
|
|
key = conf_parse_env(key, &value);
|
|
if (!key)
|
|
continue;
|
|
|
|
if (wordexp(value, &we, 0)) {
|
|
setenv(key, value, 1);
|
|
} else {
|
|
for (i = 0, *val = 0; i < we.we_wordc; i++) {
|
|
if (i > 0)
|
|
strlcat(val, " ", LINE_SIZE);
|
|
strlcat(val, we.we_wordv[i], LINE_SIZE);
|
|
}
|
|
setenv(key, val, 1);
|
|
}
|
|
wordfree(&we);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
if (pid > 1) {
|
|
char grnam[80];
|
|
|
|
if (svc_is_tty(svc))
|
|
cgroup_user("getty", pid);
|
|
else
|
|
cgroup_service(group_name(svc, grnam, sizeof(grnam)), pid, &svc->cgroup);
|
|
}
|
|
|
|
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;
|
|
int pipefd[2];
|
|
int fd = -1;
|
|
int sd = -1;
|
|
pid_t pid;
|
|
size_t i;
|
|
|
|
if (!svc)
|
|
return 1;
|
|
|
|
/* Ignore if finit is SIGSTOP'ed */
|
|
if (is_norespawn())
|
|
return 1;
|
|
|
|
/* Waiting for a run task to complete */
|
|
if (run_block_pid)
|
|
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 (bootstrap)
|
|
conf_save_exec_order(svc, cmdline, -1);
|
|
|
|
if (svc_is_sysv(svc))
|
|
logit(LOG_CONSOLE | LOG_NOTICE, "Calling '%s start' ...", cmdline);
|
|
|
|
switch (svc->notify) {
|
|
case SVC_NOTIFY_S6:
|
|
if (pipe(pipefd) == -1) {
|
|
err(1, "%s: failed opening pipe for s6 notify", svc_ident(svc, NULL, 0));
|
|
svc_missing(svc);
|
|
return 1;
|
|
}
|
|
fd = pipefd[1];
|
|
sd = pipefd[0];
|
|
break;
|
|
|
|
case SVC_NOTIFY_SYSTEMD:
|
|
sd = socket(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
|
|
if (sd == -1) {
|
|
err(1, "%s: failed opening notify socket", svc_ident(svc, NULL, 0));
|
|
svc_missing(svc);
|
|
return 1;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!svc->desc[0])
|
|
do_progress = 0;
|
|
|
|
if (do_progress) {
|
|
if (svc_is_daemon(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)) {
|
|
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) {
|
|
if (sd != -1)
|
|
close(sd);
|
|
if (fd != -1)
|
|
close(fd);
|
|
result = -1;
|
|
goto fail;
|
|
}
|
|
if (pid > 1) {
|
|
struct sockaddr_un sun;
|
|
size_t len;
|
|
|
|
dbg("Starting %s as PID %d", svc_ident(svc, NULL, 0), pid);
|
|
svc->pid = pid;
|
|
svc->start_time = jiffies();
|
|
|
|
switch (svc->notify) {
|
|
case SVC_NOTIFY_SYSTEMD:
|
|
memset(&sun, 0, sizeof(sun));
|
|
sun.sun_family = AF_UNIX;
|
|
snprintf(sun.sun_path, sizeof(sun.sun_path), NOTIFY_PATH, pid);
|
|
len = strlen(sun.sun_path);
|
|
sun.sun_path[0] = 0;
|
|
result = bind(sd, (struct sockaddr *)&sun, offsetof(struct sockaddr_un, sun_path) + len);
|
|
if (result == -1) {
|
|
err(1, "%s: failed binding to notify socket", svc_ident(svc, NULL, 0));
|
|
close(sd);
|
|
break;
|
|
}
|
|
/* fallthrough */
|
|
case SVC_NOTIFY_S6:
|
|
close(fd); /* client-end of pipefd for s6 notify */
|
|
result = uev_io_init(ctx, &svc->notify_watcher, service_notify_cb, svc, sd, UEV_READ);
|
|
if (result < 0) {
|
|
err(1, "%s: failed setting up notify callback", svc_ident(svc, NULL, 0));
|
|
if (svc->notify == SVC_NOTIFY_S6)
|
|
close(fd);
|
|
close(sd);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
} else if (pid == 0) {
|
|
char str[strlen(NOTIFY_PATH) + 32];
|
|
char *args[MAX_NUM_SVC_ARGS + 1];
|
|
int status;
|
|
|
|
if (!svc_is_tty(svc))
|
|
redirect(svc);
|
|
|
|
switch (svc->notify) {
|
|
case SVC_NOTIFY_SYSTEMD:
|
|
snprintf(str, sizeof(str), NOTIFY_PATH, getpid());
|
|
setenv("NOTIFY_SOCKET", str, 1);
|
|
/* fallthrough */
|
|
case SVC_NOTIFY_S6:
|
|
close(sd);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!svc_is_sysv(svc)) {
|
|
wordexp_t we = { 0 };
|
|
int rc;
|
|
|
|
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 == SVC_NOTIFY_S6) {
|
|
char *ptr = strstr(arg, "%n");
|
|
|
|
if (ptr) {
|
|
len = snprintf(str, sizeof(str), "%d", fd);
|
|
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)
|
|
syslog(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]);
|
|
|
|
syslog(LOG_ERR, "failed starting %s, exit code %d: %s", svc_ident(svc, NULL, 0),
|
|
status, strerror(errno));
|
|
_exit(status);
|
|
} else if (debug) {
|
|
dbg("Starting PID %d: %s", svc->pid, cmdline);
|
|
}
|
|
|
|
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:
|
|
run_block_pid = pid;
|
|
break;
|
|
case SVC_TYPE_SERVICE:
|
|
pid_file_create(svc);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
fail:
|
|
sigprocmask(SIG_SETMASK, &omask, NULL);
|
|
if (do_progress && !run_block_pid)
|
|
print_result(result);
|
|
if (bootstrap && !run_block_pid)
|
|
conf_save_exec_order(svc, NULL, 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)
|
|
{
|
|
char *nm, *id = svc_ident(svc, NULL, 0);
|
|
|
|
service_timeout_cancel(svc);
|
|
|
|
if (svc->pid <= 1) {
|
|
/* Avoid killing ourselves or all processes ... */
|
|
dbg("%s: Aborting SIGKILL, already terminated.", id);
|
|
return;
|
|
}
|
|
|
|
nm = pid_get_name(svc->pid, NULL, 0);
|
|
if (!nm) {
|
|
/* PID possibly monitored by someone else? */
|
|
dbg("%s: Aborting SIGKILL, PID[%d] no longer exists.", id, svc->pid);
|
|
service_monitor(svc->pid, 0);
|
|
return;
|
|
}
|
|
|
|
dbg("%s: Sending SIGKILL to process %d", nm, svc->pid);
|
|
logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], sending SIGKILL ...", id, 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);
|
|
}
|
|
|
|
/* Ensure we don't have any notify socket lingering */
|
|
static void service_notify_stop(svc_t *svc)
|
|
{
|
|
if (svc->notify != SVC_NOTIFY_SYSTEMD && svc->notify != SVC_NOTIFY_S6)
|
|
return;
|
|
|
|
uev_io_stop(&svc->notify_watcher);
|
|
if (svc->notify_watcher.fd > 0) {
|
|
close(svc->notify_watcher.fd);
|
|
svc->notify_watcher.fd = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
|
|
service_notify_stop(svc);
|
|
|
|
/* 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)) {
|
|
char *nm = pid_get_name(svc->pid, NULL, 0);
|
|
const char *sig = sig_name(svc->sighalt);
|
|
char *id = svc_ident(svc, NULL, 0);
|
|
|
|
if (svc->pid <= 1)
|
|
return 1;
|
|
if (!nm) {
|
|
dbg("Skipping stop (%s) of %s, PID[%d] no longer exists.", sig, id, svc->pid);
|
|
service_cleanup(svc);
|
|
svc_set_state(svc, SVC_HALTED_STATE);
|
|
return 0;
|
|
}
|
|
|
|
dbg("Sending %s to pid:%d name:%s(%s)", sig, svc->pid, id, nm);
|
|
logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], sending %s ...", id, svc->pid, sig);
|
|
} 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;
|
|
|
|
/*
|
|
* Skip run/tasks in progress, would otherwise print silly stuff
|
|
* like: "Stopping Shutting down" ...
|
|
*/
|
|
if (runlevel != 1 && do_progress && svc_is_daemon(svc))
|
|
print_desc("Stopping ", svc->desc);
|
|
|
|
if (!svc_is_sysv(svc)) {
|
|
if (svc->pid > 1) {
|
|
/*
|
|
* Send SIGTERM to parent process of process group, not to the
|
|
* entire group. This gives the process time to properly stop
|
|
* and/or forward TERM to its children. If it does not respond
|
|
* in within a reasonable timeout we SIGKILL the entire group.
|
|
*/
|
|
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 && do_progress && svc_is_daemon(svc))
|
|
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, INIT_LEVEL))
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* Parse run/task/service arguments with support for quoted strings, both
|
|
* single and double quotes to allow arguments containing spaces.
|
|
*
|
|
* Returns next argument or NULL if end of line
|
|
* Updates *line to point past the parsed argument
|
|
*/
|
|
static char *parse_args(char **line)
|
|
{
|
|
char *start, *end = NULL, *arg;
|
|
char in_quote = 0;
|
|
int has_colon = 0;
|
|
|
|
if (!line || !*line)
|
|
return NULL;
|
|
|
|
/* Skip leading whitespace */
|
|
while (**line && (**line == ' ' || **line == '\t'))
|
|
(*line)++;
|
|
|
|
if (!**line)
|
|
return NULL;
|
|
|
|
start = *line;
|
|
|
|
/* Parse the token */
|
|
while (**line) {
|
|
if (in_quote) {
|
|
if (**line == in_quote) {
|
|
/* End quote found */
|
|
in_quote = 0;
|
|
}
|
|
} else {
|
|
if (**line == '\'' || **line == '"') {
|
|
/* Start quote */
|
|
in_quote = **line;
|
|
} else if (**line == ':') {
|
|
/* Found colon - this might be key:value format */
|
|
has_colon = 1;
|
|
} else if (**line == ' ' || **line == '\t') {
|
|
/* Whitespace - check if we're in key:value mode */
|
|
if (has_colon) {
|
|
/* Look ahead to see if there's a comma after whitespace */
|
|
char *lookahead = *line;
|
|
while (*lookahead && (*lookahead == ' ' || *lookahead == '\t'))
|
|
lookahead++;
|
|
if (*lookahead == ',') {
|
|
/* Continue parsing - this space is part of the token */
|
|
(*line)++;
|
|
continue;
|
|
}
|
|
}
|
|
/* End token at whitespace */
|
|
end = *line;
|
|
break;
|
|
}
|
|
}
|
|
(*line)++;
|
|
}
|
|
|
|
/* Set end if we reached end of string */
|
|
if (!end)
|
|
end = *line;
|
|
|
|
/* Null terminate the argument */
|
|
if (end > start) {
|
|
arg = start;
|
|
if (*end) {
|
|
*end = '\0';
|
|
*line = end + 1;
|
|
}
|
|
return arg;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* 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 svc_notify_t parse_notify(char *arg)
|
|
{
|
|
if (!strcmp(arg, "systemd"))
|
|
return SVC_NOTIFY_SYSTEMD;
|
|
if (!strcmp(arg, "s6"))
|
|
return SVC_NOTIFY_S6;
|
|
if (!strcmp(arg, "pid"))
|
|
return SVC_NOTIFY_PID;
|
|
return SVC_NOTIFY_NONE; /* unsupported/none */
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
/*
|
|
* the @cgroup argument can be, e.g., .system:mem.max:1234 or just the
|
|
* default group with some cfg, e.g., :mem.max:1234 as a side effect,
|
|
* cgroupinit also work, selecting group init.
|
|
*/
|
|
static void parse_cgroup(svc_t *svc, char *cgroup)
|
|
{
|
|
char *ptr = NULL;
|
|
|
|
if (!cgroup)
|
|
return;
|
|
|
|
if (cgroup[0] == '.') {
|
|
ptr = strchr(cgroup, ':');
|
|
if (ptr)
|
|
*ptr++ = 0;
|
|
group:
|
|
strlcpy(svc->cgroup.name, &cgroup[1], sizeof(svc->cgroup.name));
|
|
if (!ptr)
|
|
return;
|
|
} else if (cgroup[0] == ':') {
|
|
ptr = &cgroup[1];
|
|
} else
|
|
goto group;
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* pre:[0-3600,]/path/to/script
|
|
*/
|
|
static void parse_script(svc_t *svc, char *type, char *script, int *tmo, char *buf, size_t len)
|
|
{
|
|
char *found, *path;
|
|
|
|
path = strchr(script, ',');
|
|
if (path) {
|
|
const char *errstr;
|
|
long long sec;
|
|
|
|
*path++ = 0;
|
|
|
|
sec = strtonum(script, 0, 3600, &errstr);
|
|
if (errstr) {
|
|
errx(1, "%s: tmo %s is %s (0-3600)", svc_ident(svc, NULL, 0),
|
|
script, errstr);
|
|
return;
|
|
}
|
|
*tmo = (int)(sec * 1000);
|
|
} else {
|
|
path = script;
|
|
*tmo = svc->killdelay;
|
|
}
|
|
|
|
found = which(path);
|
|
if (!found) {
|
|
/* Not in $PATH, check if path exists and is executable */
|
|
if (access(path, X_OK)) {
|
|
logit(LOG_WARNING, "%s:%s is missing or not executable, skipping.",
|
|
type, path);
|
|
return;
|
|
}
|
|
} else
|
|
free(found);
|
|
|
|
strlcpy(buf, path, 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
|
|
*/
|
|
static void parse_cmdline_args(svc_t *svc, char *cmd, char **args)
|
|
{
|
|
char prev[MAX_CMD_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] <!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. For run/task a <!>
|
|
* means Finit can relax its promise to run at least once per runlevel.
|
|
* I.e., for run/task conditions that would otherwise block bootstrap:
|
|
*
|
|
* task [S0123456789] <!sys/pwr/fail> name:pwrfail initctl poweroff -- Power failure, shutting down
|
|
*
|
|
* 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, *conflict = NULL;
|
|
char *cleanup_script = NULL;
|
|
char ident[MAX_IDENT_LEN];
|
|
char *ifstmt = NULL;
|
|
char *notify = NULL;
|
|
struct tty tty = { 0 };
|
|
char *dev = NULL;
|
|
int respawn = 0;
|
|
int levels = 0;
|
|
int forking = 0, manual = 0, nowarn = 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];
|
|
}
|
|
}
|
|
|
|
args = line;
|
|
cmd = parse_args(&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, "nowarn", arg))
|
|
nowarn = 1;
|
|
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, "cleanup:", arg))
|
|
cleanup_script = arg;
|
|
else if (MATCH_CMD(cmd, "env:", arg))
|
|
env = arg;
|
|
/* catch both cgroup: and cgroup. handled in parse_cgroup() */
|
|
else if (MATCH_CMD(cmd, "cgroup", arg))
|
|
cgroup = arg;
|
|
else if (MATCH_CMD(cmd, "conflict:", arg))
|
|
conflict = arg;
|
|
else if (MATCH_CMD(cmd, "if:", arg))
|
|
ifstmt = arg;
|
|
else
|
|
break;
|
|
|
|
/* Check if valid command follows... */
|
|
cmd = parse_args(&args);
|
|
if (!cmd)
|
|
goto incomplete;
|
|
}
|
|
|
|
name = parse_name(cmd, name);
|
|
strlcpy(ident, name, sizeof(ident));
|
|
if (!id) {
|
|
id = "";
|
|
} else {
|
|
strlcat(ident, ":", sizeof(ident));
|
|
strlcat(ident, id, sizeof(ident));
|
|
}
|
|
|
|
if (ifstmt && !svc_ifthen(1, ident, ifstmt, nowarn))
|
|
return 0;
|
|
|
|
levels = conf_parse_runlevels(runlevels);
|
|
if (runlevel != INIT_LEVEL && !ISOTHER(levels, INIT_LEVEL)) {
|
|
dbg("Skipping %s%s%s, bootstrap is completed.",
|
|
name, id[0] ? ":" : "", id[0] ? id : "");
|
|
return 0;
|
|
}
|
|
|
|
if (type == SVC_TYPE_TTY) {
|
|
size_t i, len;
|
|
char *ptr;
|
|
|
|
if (tty_parse_args(&tty, cmd, &args))
|
|
return errno;
|
|
|
|
/* NOTE: this may result in dev == NULL! */
|
|
if (tty_isatcon(tty.dev))
|
|
dev = tty_atcon();
|
|
else
|
|
dev = tty.dev;
|
|
next:
|
|
len = 0;
|
|
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;
|
|
|
|
/* tty's always respawn, never incr. restart_cnt */
|
|
respawn = 1;
|
|
|
|
/* Create name:id tuple for identity, e.g., tty:S0 */
|
|
if (dev) {
|
|
ptr = strrchr(dev, '/');
|
|
if (ptr)
|
|
ptr++;
|
|
else
|
|
ptr = dev;
|
|
if (!strncmp(ptr, "tty", 3))
|
|
ptr += 3;
|
|
|
|
name = "tty";
|
|
if (!id || id[0] == 0)
|
|
id = ptr;
|
|
}
|
|
|
|
svc = svc_find_by_tty(dev);
|
|
} else
|
|
svc = svc_find(name, id);
|
|
|
|
if (!whichp(cmd)) {
|
|
if (nowarn)
|
|
return 0;
|
|
|
|
warn("%s: skipping %s", file ? file : "static", cmd);
|
|
return errno;
|
|
}
|
|
|
|
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) {
|
|
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;
|
|
}
|
|
|
|
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(svc, "pre", pre_script, &svc->pre_tmo, svc->pre_script, sizeof(svc->pre_script));
|
|
else
|
|
memset(svc->pre_script, 0, sizeof(svc->pre_script));
|
|
if (post_script)
|
|
parse_script(svc, "post", post_script, &svc->post_tmo, svc->post_script, sizeof(svc->post_script));
|
|
else
|
|
memset(svc->post_script, 0, sizeof(svc->post_script));
|
|
if (ready_script)
|
|
parse_script(svc, "ready", ready_script, &svc->ready_tmo, svc->ready_script, sizeof(svc->ready_script));
|
|
else
|
|
memset(svc->ready_script, 0, sizeof(svc->ready_script));
|
|
if (cleanup_script)
|
|
parse_script(svc, "cleanup", cleanup_script, &svc->cleanup_tmo, svc->cleanup_script, sizeof(svc->cleanup_script));
|
|
else
|
|
memset(svc->cleanup_script, 0, sizeof(svc->cleanup_script));
|
|
|
|
if (!svc_is_tty(svc)) {
|
|
if (log)
|
|
parse_log(svc, log);
|
|
else
|
|
svc->log.enabled = 0;
|
|
}
|
|
|
|
if (notify)
|
|
svc->notify = parse_notify(notify);
|
|
else
|
|
svc->notify = readiness;
|
|
|
|
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));
|
|
if (conflict)
|
|
strlcpy(svc->conflict, conflict, sizeof(svc->conflict));
|
|
else
|
|
memset(svc->conflict, 0, sizeof(svc->conflict));
|
|
if (ifstmt)
|
|
strlcpy(svc->ifstmt, ifstmt, sizeof(svc->ifstmt));
|
|
else
|
|
memset(svc->ifstmt, 0, sizeof(svc->ifstmt));
|
|
svc->manual = manual;
|
|
svc->nowarn = nowarn;
|
|
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)) {
|
|
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) {
|
|
id = NULL; /* reset for next tty:ID */
|
|
goto next;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* This function is called at the end of a runlevel change or reload
|
|
* command to delete all remnants of removed services.
|
|
*
|
|
* 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)
|
|
{
|
|
int sig = WIFSIGNALED(status);
|
|
int rc = WEXITSTATUS(status);
|
|
int ok = WIFEXITED(status);
|
|
svc_t *svc;
|
|
|
|
if (lost <= 1)
|
|
return;
|
|
|
|
/* main process as well as pre: and post: scripts use svc->pid */
|
|
svc = svc_find_by_pid(lost);
|
|
if (!svc) {
|
|
/* Check if ready: script in assoc list */
|
|
if (service_script_del(lost))
|
|
dbg("collected unknown PID %d", lost);
|
|
return;
|
|
}
|
|
|
|
switch (svc->state) {
|
|
case SVC_SETUP_STATE:
|
|
/* If the setup phase fails, drive svc to crashed. */
|
|
svc->status = status;
|
|
/* fallthrough */
|
|
case SVC_TEARDOWN_STATE:
|
|
case SVC_CLEANUP_STATE:
|
|
dbg("collected script %s(%d), normal exit: %d, signaled: %d, exit code: %d",
|
|
(svc->state == SVC_TEARDOWN_STATE
|
|
? svc->post_script
|
|
: (svc->state == SVC_CLEANUP_STATE
|
|
? svc->cleanup_script
|
|
: svc->pre_script)), lost, ok, sig, rc);
|
|
/* Kill all children in the same proess group, e.g. logit */
|
|
dbg("Killing lingering children in same process group ...");
|
|
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, ok, sig, rc);
|
|
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 */
|
|
dbg("Killing lingering children in same process group ...");
|
|
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 */
|
|
svc->started = ok;
|
|
}
|
|
|
|
done:
|
|
/* No longer running, update books. */
|
|
svc->start_time = svc->pid = 0;
|
|
cont:
|
|
if (lost == run_block_pid) {
|
|
int result = ok ? rc : 1;
|
|
|
|
svc_mark_clean(svc); /* done, regardless of exit status */
|
|
run_block_pid = 0;
|
|
if (svc->desc[0])
|
|
print_result(result);
|
|
if (bootstrap)
|
|
conf_save_exec_order(svc, NULL, result);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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.
|
|
*
|
|
* However, a daemon that depends on syslogd (sysklogd project), need
|
|
* not be reloeaded (SIGHUP'ed or stop/started) because syslogd support
|
|
* reloading its configuration file on SIGHUP.
|
|
*/
|
|
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;
|
|
|
|
/* Service supports reloading conf without stop/start */
|
|
if (!svc_is_nohup(svc))
|
|
continue; /* Yup, no need to stop start rdeps */
|
|
|
|
svc_mark_affected(mkcond(svc, cond, sizeof(cond)));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Drop services with 'if:otherserv' if otherserv does not exist.
|
|
* Drop services with 'if:!otherserv' if otherserv does exist.
|
|
*/
|
|
void service_mark_unavail(void)
|
|
{
|
|
svc_t *svc, *iter = NULL;
|
|
|
|
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
|
|
char buf[MAX_IDENT_LEN];
|
|
|
|
if (!svc->ifstmt[0])
|
|
continue;
|
|
|
|
if (!svc_ifthen(1, svc_ident(svc, buf, sizeof(buf)), svc->ifstmt, svc->nowarn))
|
|
svc_mark(svc);
|
|
}
|
|
}
|
|
|
|
static void service_kill_script(svc_t *svc)
|
|
{
|
|
if (svc->pid <= 1)
|
|
return;
|
|
|
|
dbg("Timeout, killing service %s script PID %d", svc_ident(svc, NULL, 0), svc->pid);
|
|
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_NAME", svc->name, 1);
|
|
if (svc->id[0])
|
|
setenv("SERVICE_ID", svc->id, 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;
|
|
|
|
redirect(svc);
|
|
|
|
/* Warning in service_start() after svc_checkenv() */
|
|
env_file = svc_getenv(svc);
|
|
if (env_file && fexist(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");
|
|
sig_unblock();
|
|
|
|
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);
|
|
service_timeout_after(svc, svc->pre_tmo, 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);
|
|
|
|
/* Warning in service_start() after svc_checkenv() */
|
|
env_file = svc_getenv(svc);
|
|
if (env_file && fexist(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");
|
|
sig_unblock();
|
|
|
|
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);
|
|
service_timeout_after(svc, svc->post_tmo, service_kill_script);
|
|
}
|
|
|
|
/*
|
|
* Unlike the pre: and post: scripts, the ready: script cannot reuse
|
|
* svc->pid, so instead we use an assoc. list for matching any scripts
|
|
* related to the svc, handled by service_script_add() below. When a
|
|
* ready: script is reaped the service_monitor() checks first for the
|
|
* svc, then calls service_script_del().
|
|
*/
|
|
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;
|
|
|
|
/* Warning in service_start() after svc_checkenv() */
|
|
env_file = svc_getenv(svc);
|
|
if (env_file && fexist(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");
|
|
sig_unblock();
|
|
|
|
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);
|
|
service_script_add(svc, pid, svc->ready_tmo);
|
|
}
|
|
|
|
static void service_cleanup_script(svc_t *svc)
|
|
{
|
|
svc->pid = service_fork(svc);
|
|
if (svc->pid < 0) {
|
|
err(1, "Failed forking off %s cleanup:script %s", svc_ident(svc, NULL, 0), svc->cleanup_script);
|
|
return;
|
|
}
|
|
|
|
if (svc->pid == 0) {
|
|
char buf[CMD_SIZE];
|
|
char *argv[4] = {
|
|
"sh",
|
|
"-ac",
|
|
buf,
|
|
NULL
|
|
};
|
|
char *env_file;
|
|
|
|
redirect(svc);
|
|
|
|
/* Warning in service_start() after svc_checkenv() */
|
|
env_file = svc_getenv(svc);
|
|
if (env_file && fexist(env_file))
|
|
snprintf(buf, sizeof(buf), ". %s; exec %s", env_file, svc->cleanup_script);
|
|
else
|
|
strlcpy(buf, svc->cleanup_script, sizeof(buf));
|
|
|
|
set_pre_post_envs(svc, "cleanup");
|
|
sig_unblock();
|
|
|
|
execvp(_PATH_BSHELL, argv);
|
|
_exit(EX_OSERR);
|
|
}
|
|
|
|
dbg("%s: cleanup:script %s started as PID %d", svc_ident(svc, NULL, 0), svc->cleanup_script, svc->pid);
|
|
service_timeout_after(svc, svc->cleanup_tmo, service_kill_script);
|
|
}
|
|
|
|
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 (retry in %d msec) (attempt: %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;
|
|
const svc_state_t old_state = 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 (new_state == SVC_DONE_STATE) {
|
|
if (svc->started && !WEXITSTATUS(svc->status))
|
|
cond_set_oneshot(success);
|
|
else
|
|
cond_set_oneshot(failure);
|
|
}
|
|
|
|
/* clear all conditions when entering SVC_HALTED_STATE. */
|
|
if (new_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));
|
|
|
|
if ((old_state == SVC_RUNNING_STATE && new_state == SVC_PAUSED_STATE) ||
|
|
(old_state == SVC_PAUSED_STATE && new_state == SVC_RUNNING_STATE))
|
|
; /* only paused during reload, don't clear conds. */
|
|
else if (sm_in_reload())
|
|
cond_clear_noupdate(cond);
|
|
else
|
|
cond_clear(cond);
|
|
|
|
switch (new_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 or clear service/foo/ready condition for services and call optional ready:script */
|
|
void service_ready(svc_t *svc, int ready)
|
|
{
|
|
char buf[MAX_COND_LEN];
|
|
|
|
if (!svc_is_daemon(svc))
|
|
return;
|
|
|
|
snprintf(buf, sizeof(buf), "service/%s/ready", svc_ident(svc, NULL, 0));
|
|
if (ready) {
|
|
cond_set(buf);
|
|
|
|
if (svc_has_ready(svc))
|
|
service_ready_script(svc);
|
|
} else
|
|
cond_clear(buf);
|
|
}
|
|
|
|
/*
|
|
* Transition task/run/service
|
|
*
|
|
* Returns: non-zero if the @svc is no longer valid (removed)
|
|
*/
|
|
int service_step(svc_t *svc)
|
|
{
|
|
char *restart_cnt = (char *)&svc->restart_cnt;
|
|
int changed = 0, waiting = 0;
|
|
svc_state_t old_state;
|
|
cond_state_t cond;
|
|
svc_cmd_t enabled;
|
|
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);
|
|
} else {
|
|
if (svc_is_removed(svc)) {
|
|
svc_set_state(svc, SVC_DEAD_STATE);
|
|
} else if (svc_is_conflict(svc)) {
|
|
#if 0
|
|
logit(svc->nowarn ? LOG_DEBUG : LOG_INFO,
|
|
"%s in conflict with %s, checking again ...",
|
|
svc_ident(svc, NULL, 0), svc->conflict);
|
|
#endif
|
|
if (!svc_conflicts(svc))
|
|
svc_unblock(svc);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SVC_TEARDOWN_STATE:
|
|
if (!svc->pid) {
|
|
dbg("%s: post script done.", svc_ident(svc, NULL, 0));
|
|
service_timeout_cancel(svc);
|
|
|
|
if (svc_is_removed(svc) && svc_has_cleanup(svc)) {
|
|
svc_set_state(svc, SVC_CLEANUP_STATE);
|
|
service_cleanup_script(svc);
|
|
} else
|
|
svc_set_state(svc, SVC_HALTED_STATE);
|
|
}
|
|
break;
|
|
|
|
case SVC_CLEANUP_STATE:
|
|
if (!svc->pid) {
|
|
dbg("%s: cleanup script done.", svc_ident(svc, NULL, 0));
|
|
svc_set_state(svc, SVC_HALTED_STATE);
|
|
}
|
|
break;
|
|
|
|
case SVC_DEAD_STATE:
|
|
/* End of the line ☠ */
|
|
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));
|
|
service_notify_stop(svc);
|
|
|
|
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_TEARDOWN_STATE);
|
|
service_post_script(svc);
|
|
} else if (svc_is_removed(svc) && svc_has_cleanup(svc)) {
|
|
svc_set_state(svc, SVC_CLEANUP_STATE);
|
|
service_cleanup_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_SETUP_STATE:
|
|
if (!enabled) {
|
|
service_stop(svc);
|
|
break;
|
|
}
|
|
|
|
if (!svc->pid) {
|
|
int rc = WEXITSTATUS(svc->status);
|
|
int ok = WIFEXITED(svc->status);
|
|
|
|
if (!ok || rc) {
|
|
const char *sig;
|
|
|
|
if (WIFSIGNALED(svc->status))
|
|
sig = sig_name(WTERMSIG(svc->status));
|
|
else
|
|
sig = "none";
|
|
|
|
logit(LOG_WARNING, "Service %s pre:%s failed, rc: %d sig:%s",
|
|
svc_ident(svc, NULL, 0), svc->pre_script, rc, sig);
|
|
|
|
svc_set_state(svc, SVC_STOPPING_STATE);
|
|
svc_crashing(svc);
|
|
} else {
|
|
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_in_reload())
|
|
break;
|
|
|
|
/* Don't start if it conflicts with something else already started */
|
|
if (svc_conflicts(svc)) {
|
|
logit(svc->nowarn ? LOG_DEBUG : LOG_INFO,
|
|
"Not starting %s, conflicts with %s",
|
|
svc_ident(svc, NULL, 0), svc->conflict);
|
|
svc_conflict(svc);
|
|
svc_set_state(svc, SVC_HALTED_STATE);
|
|
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) {
|
|
/* Busy, waiting for run task, try again later */
|
|
if (run_block_pid)
|
|
break;
|
|
|
|
if (svc_is_missing(svc)) {
|
|
svc_set_state(svc, SVC_HALTED_STATE);
|
|
break;
|
|
}
|
|
(*restart_cnt)++;
|
|
break;
|
|
}
|
|
|
|
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_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)) {
|
|
svc_set_state(svc, SVC_STOPPING_STATE);
|
|
svc->restart_tot++;
|
|
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) || !svc_is_daemon(svc)) {
|
|
service_stop(svc);
|
|
} else {
|
|
/*
|
|
* wait until all processes have been
|
|
* stopped before continuing...
|
|
*/
|
|
if (sm_in_reload())
|
|
break;
|
|
|
|
service_restart(svc);
|
|
}
|
|
|
|
svc_mark_clean(svc);
|
|
}
|
|
if (svc->notify == SVC_NOTIFY_NONE)
|
|
service_ready(svc, 1);
|
|
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)) {
|
|
if (svc->notify == SVC_NOTIFY_PID) {
|
|
char name[MAX_COND_LEN];
|
|
|
|
mkcond(svc, name, sizeof(name));
|
|
dbg("Reassert condition %s", name);
|
|
cond_set_path(cond_path(name), COND_ON);
|
|
}
|
|
|
|
dbg("Reassert %s ready condition", svc_ident(svc, NULL, 0));
|
|
service_ready(svc, 1);
|
|
}
|
|
break;
|
|
|
|
case COND_OFF:
|
|
dbg("Condition for %s is off, sending SIGCONT + SIGTERM", svc_ident(svc, NULL, 0));
|
|
kill(svc->pid, SIGCONT);
|
|
service_stop(svc);
|
|
break;
|
|
|
|
case COND_FLUX:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* Are we waiting for a run task to complete? */
|
|
waiting = run_block_pid;
|
|
|
|
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 || waiting)
|
|
schedule_work(&work);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void service_step_all(int types)
|
|
{
|
|
svc_foreach_type(types, service_step);
|
|
}
|
|
|
|
void service_worker(void *unused)
|
|
{
|
|
(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;
|
|
|
|
/* run/task declared with <!> */
|
|
if (svc->sighup)
|
|
svc->once = 1;
|
|
else
|
|
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
|
|
* @svcp: On %FALSE return, this points to the first incomplete svc
|
|
*
|
|
* 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(svc_t **svcp)
|
|
{
|
|
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 (svc_conflicts(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));
|
|
if (svcp)
|
|
*svcp = svc;
|
|
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 == SVC_NOTIFY_PID)
|
|
continue; /* managed by pidfile plugin */
|
|
|
|
if (svc->state != SVC_RUNNING_STATE)
|
|
continue;
|
|
|
|
if (svc->notify != SVC_NOTIFY_NONE && (svc_is_changed(svc) || svc_is_starting(svc)))
|
|
continue;
|
|
|
|
service_ready(svc, 1);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Called when a service sends readiness notification, or when
|
|
* the service closes its end of the IPC connection.
|
|
*/
|
|
static void service_notify_cb(uev_t *w, void *arg, int events)
|
|
{
|
|
svc_t *svc = (svc_t *)arg;
|
|
int ready = 0;
|
|
char buf[32];
|
|
ssize_t len;
|
|
|
|
if (UEV_ERROR == events) {
|
|
dbg("%s: spurious problem with notify callback, restarting.", svc_ident(svc, NULL, 0));
|
|
uev_io_start(w);
|
|
return;
|
|
}
|
|
|
|
len = read(w->fd, buf, sizeof(buf) - 1);
|
|
if (len <= 0) {
|
|
warn("Failed reading notification from %s", svc_ident(svc, NULL, 0));
|
|
return;
|
|
}
|
|
buf[len] = 0;
|
|
|
|
/* Check for systemd READY=1 or s6 newline termination */
|
|
if (svc->notify == SVC_NOTIFY_SYSTEMD) {
|
|
char *token = strtok(buf, "\n");
|
|
|
|
while (token) {
|
|
if (!strcmp(token, "READY=1")) {
|
|
ready = 1;
|
|
break;
|
|
}
|
|
token = strtok(NULL, "\n");
|
|
}
|
|
} else if (svc->notify == SVC_NOTIFY_S6 && buf[len - 1] == '\n') {
|
|
ready = 1;
|
|
}
|
|
|
|
if (ready) {
|
|
/*
|
|
* 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, 1);
|
|
|
|
/* s6 applications close their socket after notification */
|
|
if (svc->notify == SVC_NOTIFY_S6) {
|
|
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 parenthesis: 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) {
|
|
dbg("%s(): spurious problem, restarting.", __func__);
|
|
uev_timer_start(w);
|
|
return;
|
|
}
|
|
|
|
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
|
|
if (svc_is_daemon(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(NULL);
|
|
}
|
|
|
|
/*
|
|
* The service_interval may change (conf) between invocations, so we
|
|
* periodically reset the one-shot timer instead of using a periodic.
|
|
*/
|
|
void service_init(uev_ctx_t *ctx)
|
|
{
|
|
static int initialized = 0;
|
|
static uev_t watcher;
|
|
|
|
if (!initialized)
|
|
uev_timer_init(ctx, &watcher, service_interval_cb, NULL, service_interval, 0);
|
|
else
|
|
uev_timer_set(&watcher, service_interval, 0);
|
|
|
|
initialized = 1;
|
|
}
|
|
|
|
/**
|
|
* Local Variables:
|
|
* indent-tabs-mode: t
|
|
* c-file-style: "linux"
|
|
* End:
|
|
*/
|