Files
finit/src/conf.c
T
Joachim Wiberg 2c6547635a conf: drop stale draft of the pidfile translation comment
An earlier revision of the comment survived right above its final
form.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-01 11:25:40 +02:00

2595 lines
67 KiB
C

/* Configuration frontend: file discovery, format detection, libconfuse parser
*
* Finit reads two configuration formats from /etc/finit.conf and the
* finit.d/ hierarchy, both using the .conf file extension:
*
* - the new libconfuse block format, parsed here, where all new
* features land, and
* - the legacy one-liner format, frozen at the Finit 4.x feature
* set, parsed by legacy.c
*
* The format is detected per file by content. Try-parse with
* libconfuse first; on success the file is in the new format. On a
* parse error, re-parse leniently, accepting any unknown key, to
* tell the two failure modes apart: if the lenient parse succeeds
* the file is in the new format but has a typo, which is reported
* as-is, never fed to the legacy parser. If it fails too, no block
* grammar fits and the file goes to the legacy parser.
*
* Each parsed block is translated to the canonical legacy one-liner
* and registered through the same code paths as the legacy format,
* so the two cannot drift apart.
*
* Copyright (c) 2012-2026 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 <stdarg.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/resource.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
# include <libite/queue.h> /* BSD sys/queue.h API */
#else
# include <lite/lite.h>
# include <lite/queue.h> /* BSD sys/queue.h API */
#endif
#include <confuse.h>
#include <time.h>
#include <glob.h>
#include "finit.h"
#include "cond.h"
#include "conf.h"
#include "devmon.h"
#include "iwatch.h"
#include "legacy.h"
#include "private.h"
#include "service.h"
#include "helpers.h"
#include "util.h"
int runlevel = INIT_LEVEL; /* Bootstrap 'S' */
int cfglevel = RUNLEVEL; /* Fallback if no configured runlevel */
int cmdlevel = 0; /* runlevel override from cmdline */
int prevlevel = -1;
int debug = 0; /* debug mode from kernel cmdline */
int rescue = 0; /* rescue mode from kernel cmdline */
int single = 0; /* single user mode from kernel cmdline */
int bootstrap = 1; /* set while bootstrapping (for TTYs) */
int kerndebug = 0; /* set if /proc/sys/kernel/printk > 7 */
int syncsec = 0; /* reboot delay */
int wdtreboot = 0; /* reboot via watchdog, default: SOC */
int readiness = SVC_NOTIFY_PID;
char *finit_conf= NULL;
char *finit_rcsd= NULL;
char *fstab = NULL;
char *sdown = NULL;
char *network = NULL;
char *hostname = NULL;
char *osheading = NULL;
char *runparts = NULL;
int runparts_progress;
int runparts_sysv;
char cgroup_current[CGROUP_NAME_SIZE]; /* cgroup.NAME sets current cgroup for a set of services */
char cgroup_settings_current[CGROUP_SETTINGS_SIZE]; /* cgroup.system,cpu.weight:500 - cgroup settings */
int cgroup_delegate_current; /* cgroup.system,delegate - delegation flag */
struct rlimit initial_rlimit[RLIMIT_NLIMITS];
struct rlimit global_rlimit[RLIMIT_NLIMITS];
struct conf_change {
TAILQ_ENTRY(conf_change) link;
char *name;
};
struct env_entry {
TAILQ_ENTRY(env_entry) link;
char *name;
};
static char cfg_errmsg[256];
static struct iwatch iw_conf;
static int iwatch_fd;
static uev_t etcw;
static TAILQ_HEAD(, env_entry) env_list = TAILQ_HEAD_INITIALIZER(env_list);
static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list);
/*
* libconfuse schema -- the new block format
*
* Long-form keys are canonical, short-form aliases are accepted
* systematically. The translators below merge them, canonical key
* wins if both are set.
*/
/*
* Common cgroup v2 keys, declared: typed and warning-free. The open-
* ended tail (hugetlb.<size>.max, rdma.<dev>.*, newer kernels, ...)
* is caught by CFGF_KEYSTRVAL on the section and passed through to
* the kernel verbatim, like the legacy parser always did.
*
* XXX: Workaround for libConfuse <3.4, which logs a spurious "no such
* option" per tail key, despite parsing it correctly. Harmless,
* cfg_error_cb() buffers it and only a failed parse is reported.
*/
static cfg_opt_t cgroup_opts[] = {
/*
* Not cgroupfs files but arguments to parse_cgroup(): the leaf
* directory to place the service in, and whether to hand the
* subtree over to it. Only meaningful inside a service block.
*/
CFG_STR ("name", NULL, CFGF_NODEFAULT),
CFG_BOOL("delegate", cfg_false, CFGF_NODEFAULT),
CFG_STR("cpu.weight", NULL, CFGF_NODEFAULT),
CFG_STR("cpu.weight.nice", NULL, CFGF_NODEFAULT),
CFG_STR("cpu.max", NULL, CFGF_NODEFAULT),
CFG_STR("cpu.max.burst", NULL, CFGF_NODEFAULT),
CFG_STR("cpu.idle", NULL, CFGF_NODEFAULT),
CFG_STR("cpuset.cpus", NULL, CFGF_NODEFAULT),
CFG_STR("cpuset.mems", NULL, CFGF_NODEFAULT),
CFG_STR("memory.min", NULL, CFGF_NODEFAULT),
CFG_STR("memory.low", NULL, CFGF_NODEFAULT),
CFG_STR("memory.high", NULL, CFGF_NODEFAULT),
CFG_STR("memory.max", NULL, CFGF_NODEFAULT),
CFG_STR("memory.swap.high", NULL, CFGF_NODEFAULT),
CFG_STR("memory.swap.max", NULL, CFGF_NODEFAULT),
CFG_STR("memory.oom.group", NULL, CFGF_NODEFAULT),
CFG_STR("memory.zswap.max", NULL, CFGF_NODEFAULT),
CFG_STR("io.weight", NULL, CFGF_NODEFAULT),
CFG_STR("io.max", NULL, CFGF_NODEFAULT),
CFG_STR("io.latency", NULL, CFGF_NODEFAULT),
CFG_STR("pids.max", NULL, CFGF_NODEFAULT),
CFG_END()
};
/*
* Resource limits are a closed set (kernel ABI), so the schema is
* fully declared: a typo is a hard parse error. Bare resource name
* sets both soft and hard limit, like the legacy two-token form.
*/
#define RLIMIT_OPTS(pfx) \
CFG_STR(pfx "as", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "core", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "cpu", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "data", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "fsize", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "locks", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "memlock", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "msgqueue", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "nice", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "nofile", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "nproc", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "rss", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "rtprio", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "rttime", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "sigpending", NULL, CFGF_NODEFAULT), \
CFG_STR(pfx "stack", NULL, CFGF_NODEFAULT)
static cfg_opt_t rlimit_opts[] = {
RLIMIT_OPTS(""),
RLIMIT_OPTS("soft."),
RLIMIT_OPTS("hard."),
CFG_END()
};
/* environment { KEY = "value" } -- global environment variables */
static cfg_opt_t env_opts[] = {
CFG_END()
};
/*
* log { file = "/var/log/foo" ... } -- where a service's output goes.
* /dev/null and /dev/console are spelled as paths, so there is one way
* to say it. An empty block means syslog with defaults; `log` cannot
* also be a scalar, libconfuse rejects a name declared as both.
*/
static cfg_opt_t svclog_opts[] = {
CFG_STR("file", NULL, CFGF_NODEFAULT),
CFG_STR("priority", NULL, CFGF_NODEFAULT),
CFG_STR("identity", NULL, CFGF_NODEFAULT),
CFG_END()
};
/* log { size = 200k count = 5 } -- Finit's own log rotation */
static cfg_opt_t log_opts[] = {
CFG_STR("size", NULL, CFGF_NODEFAULT),
CFG_INT("count", 0, CFGF_NODEFAULT),
CFG_END()
};
/* a lifecycle script and the bound on how long it may run */
#define EXEC_OPTS(nm) \
CFG_STR(nm, NULL, CFGF_NODEFAULT), \
CFG_INT(nm "-timeout", 0, CFGF_NODEFAULT)
/* service NAME[:ID] { ... }, shared with task/run/sysv */
static cfg_opt_t svc_opts[] = {
CFG_STR ("description", NULL, CFGF_NODEFAULT),
CFG_STR ("desc", NULL, CFGF_NODEFAULT), /* alias */
CFG_STR ("command", NULL, CFGF_NODEFAULT),
CFG_STR ("runlevel", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("conditions", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("cond", NULL, CFGF_NODEFAULT), /* alias */
CFG_STR ("user", NULL, CFGF_NODEFAULT),
CFG_STR ("group", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("extra-groups", NULL, CFGF_NODEFAULT),
CFG_STR ("envfile", NULL, CFGF_NODEFAULT),
CFG_STR ("env", NULL, CFGF_NODEFAULT), /* alias */
CFG_STR ("pidfile", NULL, CFGF_NODEFAULT),
CFG_BOOL ("pidfile-create", cfg_false, CFGF_NODEFAULT),
CFG_STR ("notify", NULL, CFGF_NODEFAULT),
CFG_STR ("type", NULL, CFGF_NODEFAULT),
CFG_BOOL ("manual-start", cfg_false, CFGF_NODEFAULT),
CFG_BOOL ("manual", cfg_false, CFGF_NODEFAULT), /* alias */
CFG_BOOL ("remain-after-exit", cfg_false, CFGF_NODEFAULT),
CFG_BOOL ("remain", cfg_false, CFGF_NODEFAULT), /* alias */
CFG_BOOL ("respawn", cfg_false, CFGF_NODEFAULT),
CFG_STR ("restart", NULL, CFGF_NODEFAULT),
CFG_INT ("restart-max", 0, CFGF_NODEFAULT),
CFG_INT ("restart-sec", 0, CFGF_NODEFAULT),
CFG_STR ("oncrash", NULL, CFGF_NODEFAULT),
CFG_STR ("reload-signal", NULL, CFGF_NODEFAULT),
CFG_BOOL ("required", cfg_true, CFGF_NODEFAULT),
CFG_STR ("stop-signal", NULL, CFGF_NODEFAULT),
CFG_STR ("halt", NULL, CFGF_NODEFAULT), /* alias */
CFG_INT ("stop-timeout", 0, CFGF_NODEFAULT),
CFG_INT ("kill", 0, CFGF_NODEFAULT), /* alias */
/* lifecycle scripts, each with its own bound */
EXEC_OPTS("exec-start-pre"),
EXEC_OPTS("exec-start-ready"),
EXEC_OPTS("exec-stop"),
EXEC_OPTS("exec-stop-post"),
EXEC_OPTS("exec-reload"),
EXEC_OPTS("exec-cleanup"),
CFG_STR_LIST("capabilities", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("caps", NULL, CFGF_NODEFAULT), /* alias */
CFG_STR ("runtime-dir", NULL, CFGF_NODEFAULT),
CFG_STR ("state-dir", NULL, CFGF_NODEFAULT),
CFG_STR ("cache-dir", NULL, CFGF_NODEFAULT),
CFG_STR ("logs-dir", NULL, CFGF_NODEFAULT),
CFG_STR ("config-dir", NULL, CFGF_NODEFAULT),
CFG_INT ("runtime-dir-mode", 0, CFGF_NODEFAULT),
CFG_INT ("state-dir-mode", 0, CFGF_NODEFAULT),
CFG_INT ("cache-dir-mode", 0, CFGF_NODEFAULT),
CFG_INT ("logs-dir-mode", 0, CFGF_NODEFAULT),
CFG_INT ("config-dir-mode", 0, CFGF_NODEFAULT),
CFG_STR ("runtime-dir-preserve", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("conflicts", NULL, CFGF_NODEFAULT),
CFG_STR ("if", NULL, CFGF_NODEFAULT),
CFG_STR ("tty", NULL, CFGF_NODEFAULT),
/*
* MULTI is not about repetition here, it is the only way to tell
* `log {}` from no log at all: libconfuse instantiates every
* non-MULTI section, so cfg_size() would always answer 1.
*/
CFG_SEC ("log", svclog_opts, CFGF_MULTI),
CFG_SEC ("cgroup", cgroup_opts, CFGF_MULTI | CFGF_TITLE | CFGF_KEYSTRVAL),
CFG_SEC ("rlimit", rlimit_opts, CFGF_NONE),
CFG_END()
};
/* tty NAME { ... } -- all three legacy variants */
static cfg_opt_t tty_opts[] = {
CFG_STR ("runlevel", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("conditions", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("cond", NULL, CFGF_NODEFAULT), /* alias */
CFG_STR ("device", NULL, CFGF_NODEFAULT),
CFG_INT ("baud", 0, CFGF_NODEFAULT),
CFG_STR ("term", NULL, CFGF_NODEFAULT),
CFG_BOOL ("noclear", cfg_false, CFGF_NODEFAULT),
CFG_BOOL ("nowait", cfg_false, CFGF_NODEFAULT),
CFG_BOOL ("nologin", cfg_false, CFGF_NODEFAULT),
CFG_STR ("command", NULL, CFGF_NODEFAULT),
CFG_BOOL ("notty", cfg_false, CFGF_NODEFAULT),
CFG_BOOL ("rescue", cfg_false, CFGF_NODEFAULT),
CFG_END()
};
static cfg_opt_t conf_opts[] = {
CFG_SEC ("service", svc_opts, CFGF_MULTI | CFGF_TITLE),
CFG_SEC ("task", svc_opts, CFGF_MULTI | CFGF_TITLE),
CFG_SEC ("run", svc_opts, CFGF_MULTI | CFGF_TITLE),
CFG_SEC ("sysv", svc_opts, CFGF_MULTI | CFGF_TITLE),
CFG_SEC ("tty", tty_opts, CFGF_MULTI | CFGF_TITLE),
CFG_SEC ("cgroup", cgroup_opts, CFGF_MULTI | CFGF_TITLE | CFGF_KEYSTRVAL),
CFG_SEC ("rlimit", rlimit_opts, CFGF_NONE),
CFG_SEC ("environment", env_opts, CFGF_KEYSTRVAL),
CFG_SEC ("env", env_opts, CFGF_KEYSTRVAL), /* alias */
CFG_SEC ("log", log_opts, CFGF_NONE),
/* static/bootstrap directives */
CFG_INT ("runlevel", 0, CFGF_NODEFAULT),
CFG_STR ("hostname", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("modules", NULL, CFGF_NODEFAULT),
CFG_STR_LIST("mod", NULL, CFGF_NODEFAULT), /* alias */
CFG_STR_LIST("mknod", NULL, CFGF_NODEFAULT),
CFG_STR ("network", NULL, CFGF_NODEFAULT),
CFG_STR ("rcsd", NULL, CFGF_NODEFAULT),
CFG_STR ("runparts", NULL, CFGF_NODEFAULT),
CFG_BOOL ("runparts-progress", cfg_false, CFGF_NODEFAULT),
CFG_BOOL ("runparts-sysv", cfg_false, CFGF_NODEFAULT),
CFG_STR ("readiness", NULL, CFGF_NODEFAULT),
CFG_INT ("reboot-delay", 0, CFGF_NODEFAULT),
CFG_BOOL ("reboot-watchdog", cfg_false, CFGF_NODEFAULT),
CFG_INT ("service-interval", 0, CFGF_NODEFAULT),
CFG_STR ("shutdown", NULL, CFGF_NODEFAULT),
CFG_FUNC ("include", cfg_include),
CFG_END()
};
static void drop_changes(void);
static int validate_arg(const char *arg, const char *opt)
{
if (!arg) {
errx(1, "option %s missing argument, skipping.", opt);
return 1;
}
return 0;
}
/*
* finit.cond = foo (=> <boot/foo>)
* finit.config = /path/to/etc/alt-finit.conf
* finit.debug = [on,off]
* finit.fstab = /path/to/etc/fstab.aternative
* finit.status = [on,off] (compat finit.show_status)
* finit.status_style = [old,classic,modern]
*/
static void parse_finit_opts(char *opt)
{
char *arg;
arg = strchr(opt, '=');
if (arg)
*arg++ = 0;
if (string_compare(opt, "cond")) {
if (validate_arg(arg, "finit.cond"))
return;
cond_boot_parse(arg);
return;
}
if (string_compare(opt, "config")) {
if (validate_arg(arg, "finit.config"))
return;
if (finit_conf)
free(finit_conf);
finit_conf = strdup(arg);
return;
}
if (string_compare(opt, "debug")) {
debug = istrue(arg, 1);
return;
}
if (string_compare(opt, "fstab")) {
if (validate_arg(arg, "finit.fstab"))
return;
if (fstab)
free(fstab);
fstab = strdup(arg);
return;
}
if (string_compare(opt, "status_style")) {
if (validate_arg(arg, "finit.status_style"))
return;
if (string_compare(arg, "old") || string_compare(arg, "classic"))
show_progress(PROGRESS_CLASSIC);
else
show_progress(PROGRESS_MODERN);
return;
}
if (string_compare(opt, "status") || string_compare(opt, "show_status")) {
show_progress(istrue(arg, 1) ? PROGRESS_DEFAULT : PROGRESS_SILENT);
return;
}
}
static void parse_fsck_opts(char *opt)
{
char *arg;
arg = strchr(opt, '=');
if (arg)
*arg++ = 0;
if (string_compare(opt, "mode")) {
if (validate_arg(arg, "fsck.mode"))
return;
if (!strcmp(arg, "skip"))
fsck_mode = NULL;
if (!strcmp(arg, "auto"))
fsck_mode = "";
if (!strcmp(arg, "force"))
fsck_mode = "-f";
return;
}
if (string_compare(opt, "repair")) {
if (validate_arg(arg, "fsck.repair"))
return;
if (!strcmp(arg, "no"))
fsck_repair = "-n";
if (!strcmp(arg, "preen"))
fsck_repair = "-p";
if (!strcmp(arg, "yes"))
fsck_repair = "-y";
return;
}
}
/*
* When runlevel (single integer) is given on the command line,
* it overrides the runlevel in finit.conf and the built-in
* default (from configure). It do however have to pass the
* same sanity checks.
*/
static int parse_runlevel(char *arg)
{
const char *err = NULL;
char *ptr = arg;
long long num;
/* Sanity check the token is just digit(s) */
while (*ptr) {
if (!isdigit(*ptr++))
return 0;
}
num = strtonum(arg, 1, 9, &err);
if (err || num == 6) {
dbg("Not a valid runlevel (%s), valid levels are [1-9], excluding 6, skipping.", arg);
return 0;
}
return (int)num;
}
static void parse_arg(char *arg)
{
if (!strncmp(arg, "finit.", 6)) {
parse_finit_opts(&arg[6]);
return;
}
if (!strncmp(arg, "fsck.", 5)) {
parse_fsck_opts(&arg[5]);
return;
}
#ifdef RESCUE_MODE
if (string_compare(arg, "rescue") || string_compare(arg, "recover")) {
rescue = 1;
return;
}
#endif
if (string_compare(arg, "single") || string_compare(arg, "S")) {
single = 1;
return;
}
/* Put any new command line options before this line. */
cmdlevel = parse_runlevel(arg);
}
#ifdef KERNEL_CMDLINE
/*
* Parse /proc/cmdline to find args for init. Don't use this!
*
* Instead, rely on the kernel to give Finit its arguments as
* regular argc + argv[]. Only use this if the system you run
* on has a broken initramfs system that cannot forward args
* to Finit properly.
*/
static void parse_kernel_cmdline(void)
{
char line[LINE_SIZE], *cmdline, *tok;
FILE *fp;
fp = fopen("/proc/cmdline", "r");
if (!fp)
return;
if (!fgets(line, sizeof(line), fp)) {
fclose(fp);
return;
}
cmdline = chomp(line);
dbg("%s", cmdline);
while ((tok = strtok(cmdline, " \t"))) {
cmdline = NULL;
parse_arg(tok);
}
fclose(fp);
}
#else
#define parse_kernel_cmdline()
#endif
static void parse_kernel_loglevel(void)
{
char line[LINE_SIZE], *ptr;
FILE *fp;
int val;
fp = fopen("/proc/sys/kernel/printk", "r");
if (!fp)
return;
if (!fgets(line, sizeof(line), fp)) {
fclose(fp);
return;
}
fclose(fp);
ptr = chomp(line);
dbg("%s", ptr);
val = atoi(ptr);
if (val >= 7)
kerndebug = 1;
}
/*
* Kernel gives us all non-kernel options on our cmdline
*/
void conf_parse_cmdline(int argc, char *argv[])
{
const char *ptr;
/* Set up defaults */
if ((ptr = FINIT_FSTAB))
fstab = strdup(ptr);
finit_conf = strdup(FINIT_CONF);
finit_rcsd = strdup(FINIT_RCSD);
for (int i = 1; i < argc; i++)
parse_arg(argv[i]);
parse_kernel_cmdline();
parse_kernel_loglevel();
}
/*
* Sourced mainly by initctl and other Finit helper tools
*/
void conf_saverc(void)
{
FILE *fp;
mkpath(_PATH_VARRUN "finit", 0755);
fp = fopen(_PATH_VARRUN "finit/.initrc", "w");
if (!fp) {
err(1, "failed creating .finitrc");
return;
}
fprintf(fp, "FINIT_CONF=%s\n", finit_conf);
fprintf(fp, "FINIT_RCSD=%s\n", finit_rcsd);
fprintf(fp, "FINIT_CGPATH=%s\n", FINIT_CGPATH);
fprintf(fp, "INIT_SOCKET=%s\n", INIT_SOCKET);
fprintf(fp, "INIT_MAGIC=0x%08x\n", INIT_MAGIC);
fclose(fp);
}
/*
* Called at bootstrap to log execution order (for debug)
*/
void conf_save_exec_order(svc_t *svc, char *cmdline, int result)
{
const char *fn = _PATH_VARRUN "finit/exec.order";
const char *ststr = !result ? "[ OK ]" : "[FAIL]";
static char *prepared = NULL;
int first = !fexist(fn);
FILE *fp;
fp = fopen(fn, "a+");
if (!fp) {
err(1, "failed writing to %s", fn);
return;
}
if (first) {
fprintf(fp, "# Execution order of run/task/services at bootstrap\n");
fprintf(fp, "# ST TYPE COMMAND LINE (DESC)\n");
}
if (result == -1) {
int len;
len = snprintf(NULL, 0, " %-7s %-64s (%s)", svc_typestr(svc), cmdline, svc->desc);
if (len > 0) {
if (prepared)
free(prepared);
prepared = malloc(++len);
if (!prepared)
goto done;
snprintf(prepared, len, " %-7s %-64s (%s)", svc_typestr(svc), cmdline, svc->desc);
}
} else {
if (prepared) {
memcpy(prepared, ststr, strlen(ststr));
fprintf(fp, "%s\n", prepared);
free(prepared);
prepared = NULL;
} else {
fprintf(fp, "%s %-7s %-64s (%s)\n", ststr, svc_typestr(svc), cmdline, svc->desc);
}
}
done:
fclose(fp);
}
/*
* Called by plugins and similar that dynamically generate system
* services. @body holds the block contents, one indented line per
* setting.
*/
void conf_save_service(int type, const char *name, char *file, const char *fmt, ...)
{
char fn[256];
va_list ap;
svc_t foo;
FILE *fp;
mkpath(FINIT_RUNPATH_, 0755);
snprintf(fn, sizeof(fn), "%s/%s", FINIT_RUNPATH_, file);
if (fexist(fn))
warnx("File %s already exists, overwriting.", fn);
fp = fopen(fn, "w");
if (!fp) {
err(1, "Failed creating %s", fn);
return;
}
foo.type = type;
fprintf(fp, "# Generated by finit:%s()\n", __func__);
fprintf(fp, "%s %s {\n", svc_typestr(&foo), name);
va_start(ap, fmt);
vfprintf(fp, fmt, ap);
va_end(ap);
fprintf(fp, "}\n");
fclose(fp);
}
/*
* Clear all environment variables read in parse_env(), they may be
* removed now so let the next call to parse_env() restore them.
*/
void conf_reset_env(void)
{
struct env_entry *node, *tmp;
TAILQ_FOREACH_SAFE(node, &env_list, link, tmp) {
TAILQ_REMOVE(&env_list, node, link);
if (node->name) {
unsetenv(node->name);
free(node->name);
}
free(node);
}
if (!getenv("PATH"))
setenv("PATH", _PATH_STDPATH, 1);
if (!getenv("SHELL"))
setenv("SHELL", _PATH_BSHELL, 1);
setenv("LOGNAME", "root", 1);
setenv("USER", "root", 1);
}
/*
* Sets, and makes a note of, all KEY=VALUE lines in a given .conf line
* from finit.conf, or other .conf file. Note, PATH is always reset in
* the conf_reset_env() function.
*/
void conf_set_env(char *line)
{
struct env_entry *node;
char *key, *val;
key = conf_parse_env(line, &val);
if (!key)
return;
dbg("Global env '%s'='%s'", key, val);
setenv(key, val, 1);
node = malloc(sizeof(*node));
if (!node) {
nomem:
err(1, "Out of memory cannot track env vars");
return;
}
node->name = strdup(key);
if (!node->name) {
free(node);
goto nomem;
}
TAILQ_INSERT_HEAD(&env_list, node, link);
}
/**
* conf_parse_env - Parse a key=value line
* @line: Line buffer without newline
* @value: Whitespace trimmed value
*
* Used by service.c:source_env() and
*
* Returns:
* %NULL on error, otherwise a whitespace trimmed key.
*/
char *conf_parse_env(char *line, char **value)
{
char *key, *val, *end;
/* skip any leading whitespace */
key = line;
while (isspace(*key))
key++;
/* find end of line */
end = key;
while (*end)
end++;
/* strip trailing whitespace */
if (end > key) {
end--;
while (isspace(*end))
*end-- = 0;
}
val = strchr(key, '=');
if (!val)
return NULL;
*val++ = 0;
/* strip leading whitespace from value */
while (isspace(*val))
val++;
/* unquote value, if quoted */
unquote(&val, end);
*value = val;
/* find end of key */
end = key;
while (*end)
end++;
/* strip trailing whitespace */
if (end > key) {
end--;
while (isspace(*end))
*end-- = 0;
}
/* strip any leading 'set ' */
end = key;
if (!strncmp(key, "set", 3))
end += 3;
/* check key, no spaces allowed */
while (*end && isspace(*end))
end++;
key = end;
while (*end && !isspace(*end))
end++;
if (*end != 0) {
warnx("'%s=%s': not a valid identifier", key, val);
return NULL; /* invalid key */
}
return key;
}
/* First form: `rlimit <hard|soft> RESOURCE LIMIT` */
void conf_parse_rlimit(char *line, struct rlimit arr[])
{
char *level, *limit, *val;
int resource = -1;
rlim_t cfg;
level = strtok(line, " \t");
if (!level)
goto error;
limit = strtok(NULL, " \t");
if (!limit)
goto error;
val = strtok(NULL, " \t");
if (!val) {
/* Second form: `rlimit RESOURCE LIMIT` */
val = limit;
limit = level;
level = "both";
}
resource = str2rlim(limit);
if (resource < 0 || resource > RLIMIT_NLIMITS)
goto error;
/* Official keyword from v3.1 is `unlimited`, from prlimit(1) */
if (!strcmp(val, "unlimited") || !strcmp(val, "infinity")) {
cfg = RLIM_INFINITY;
} else {
const char *err = NULL;
cfg = strtonum(val, 0, (long long)2 << 31, &err);
if (err) {
logit(LOG_WARNING, "rlimit: invalid %s value: %s",
rlim2str(resource), val);
return;
}
}
if (!strcmp(level, "soft"))
arr[resource].rlim_cur = cfg;
else if (!strcmp(level, "hard"))
arr[resource].rlim_max = cfg;
else if (!strcmp(level, "both"))
arr[resource].rlim_max = arr[resource].rlim_cur = cfg;
else
goto error;
return;
error:
logit(LOG_WARNING, "rlimit: parse error");
}
/* Convert optional "[!123456789S]" string into a bitmask */
int conf_parse_runlevels(const char *runlevels)
{
int i, not = 0, bitmask = 0;
if (!runlevels)
runlevels = "[234]";
i = 1;
while (i) {
int level;
char lvl = runlevels[i++];
if (']' == lvl || 0 == lvl)
break;
if ('!' == lvl) {
not = 1;
bitmask = 0x7FE;
continue;
}
if ('s' == lvl || 'S' == lvl)
level = INIT_LEVEL;
else
level = lvl - '0';
if (level > INIT_LEVEL || level < 0)
continue;
if (not)
CLRBIT(bitmask, level);
else
SETBIT(bitmask, level);
}
return bitmask;
}
void conf_parse_cond(svc_t *svc, char *cond)
{
size_t i = 0;
char *ptr;
char *c;
if (!svc) {
errx(1, "Invalid service pointer");
return;
}
/* By default we assume UNIX daemons support SIGHUP */
if (svc_is_daemon(svc))
svc->sighup = 1;
if (!cond) {
memset(svc->cond, 0, sizeof(svc->cond));
return;
}
/*
* First character must be '!' if:
* - service: SIGHUP is not supported
* - run/task: Do not block bootstrap
*/
ptr = cond;
if (ptr[i] == '!') {
ptr++;
if (svc_is_runtask(svc)) {
/* see service_runtask_clean() */
svc->sighup = 1;
svc->once = 1;
} else {
svc->sighup = 0;
}
}
while (ptr[i] != '>' && ptr[i] != 0)
i++;
ptr[i] = 0;
if (i >= sizeof(svc->cond)) {
logit(LOG_WARNING, "%s: too long list of conditions: %s", svc_ident(svc, NULL, 0), ptr);
return;
}
/*
* The '~' prefix means a reload of the upstream service is
* propagated to this service -- it will be reloaded (SIGHUP)
* or restarted (noreload), not just paused and resumed.
* Syntax: <!~pid/foo,~pid/bar> or <~pid/foo>
*
* Strip '~' from each condition and set the service-level
* flag. This allows '~' on any condition in the list.
*/
svc->cond[0] = 0;
for (i = 0, c = strtok(ptr, ","); c; c = strtok(NULL, ","), i++) {
if (c[0] == '~') {
c++;
svc->flux_reload = 1;
}
devmon_add_cond(c);
if (i)
strlcat(svc->cond, ",", sizeof(svc->cond));
strlcat(svc->cond, c, sizeof(svc->cond));
}
}
/*
* Try-parse error capture. Diagnostics are buffered, not printed:
* on fallback to the legacy parser they are only debug logged. The
* last message wins -- the parser stops at the first fatal error, so
* the last callback before failure is the fatal one.
*/
static void cfg_error_cb(cfg_t *cfg, const char *fmt, va_list ap)
{
char msg[128];
vsnprintf(msg, sizeof(msg), fmt, ap);
if (cfg && cfg->filename && cfg->line)
snprintf(cfg_errmsg, sizeof(cfg_errmsg), "%s:%d: %s",
cfg->filename, cfg->line, msg);
else if (cfg && cfg->filename)
snprintf(cfg_errmsg, sizeof(cfg_errmsg), "%s: %s",
cfg->filename, msg);
else
strlcpy(cfg_errmsg, msg, sizeof(cfg_errmsg));
}
/* discard diagnostics, the lenient try-parse only needs its verdict */
static void cfg_error_quiet(cfg_t *cfg, const char *fmt, va_list ap)
{
}
/*
* Alias helpers: canonical key wins, alias accepted.
*/
/* which of KEY or ALIAS the user actually set, canonical wins */
static const char *sec_key(cfg_t *sec, const char *key, const char *alias)
{
if (cfg_size(sec, key))
return key;
if (alias && cfg_size(sec, alias))
return alias;
return NULL;
}
static const char *sec_getstr(cfg_t *sec, const char *key, const char *alias)
{
key = sec_key(sec, key, alias);
return key ? cfg_getstr(sec, key) : NULL;
}
static int sec_getint(cfg_t *sec, const char *key, const char *alias, long *val)
{
key = sec_key(sec, key, alias);
if (!key)
return 0;
*val = cfg_getint(sec, key);
return 1;
}
static char *sec_getlist(cfg_t *sec, const char *key, const char *alias, char *buf, size_t len)
{
unsigned int i, num;
if (!cfg_size(sec, key)) {
if (!alias || !cfg_size(sec, alias))
return NULL;
key = alias;
}
buf[0] = 0;
num = cfg_size(sec, key);
for (i = 0; i < num; i++) {
if (i)
strlcat(buf, ",", len);
strlcat(buf, cfg_getnstr(sec, key, i), len);
}
return buf;
}
static int sec_getbool(cfg_t *sec, const char *key, const char *alias)
{
key = sec_key(sec, key, alias);
if (!key)
return 0;
return cfg_getbool(sec, key) == cfg_true;
}
/*
* Append token to the one-liner being built, space separated.
*/
static void addtok(char *line, size_t len, const char *fmt, ...)
{
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (line[0])
strlcat(line, " ", len);
strlcat(line, buf, len);
}
/* like addtok() but quotes values containing whitespace */
static void addopt(char *line, size_t len, const char *opt, const char *val)
{
if (strpbrk(val, " \t"))
addtok(line, len, "%s\"%s\"", opt, val);
else
addtok(line, len, "%s%s", opt, val);
}
/*
* cgroup NAME { key = val ... } -> "NAME" + "key:val,key:val"
* Free-form (KEYSTRVAL) and declared options enumerate the same way.
*/
static char *cgroup_settings(cfg_t *cg, char *buf, size_t len, char *file, int svc)
{
cfg_opt_t *opt;
buf[0] = 0;
/*
* delegate is a flag, so it carries no value, and a top-level
* group has nothing to delegate to.
*/
if (cfg_size(cg, "delegate")) {
if (!svc)
logit(LOG_WARNING, "%s: cgroup %s: delegate only applies"
" inside a service, ignoring", file, cfg_title(cg));
else if (cfg_getbool(cg, "delegate") == cfg_true)
strlcat(buf, "delegate", len);
}
if (!svc && cfg_size(cg, "name"))
logit(LOG_WARNING, "%s: cgroup %s: name only applies inside a"
" service, ignoring", file, cfg_title(cg));
for (opt = cg->opts; opt && opt->name; opt++) {
const char *val;
if (opt->type != CFGT_STR || !cfg_opt_size(opt))
continue;
val = cfg_opt_getnstr(opt, 0);
if (!val)
continue;
if (!svc && !strcmp(opt->name, "name"))
continue;
if (buf[0])
strlcat(buf, ",", len);
strlcat(buf, opt->name, len);
strlcat(buf, ":", len);
strlcat(buf, val, len);
}
return buf;
}
/*
* rlimit { nofile = 1024 hard.core = unlimited } -> conf_parse_rlimit()
* Bare resource name sets both soft and hard limit.
*/
static void rlimit_translate(cfg_t *sec, struct rlimit arr[])
{
cfg_opt_t *opt;
for (opt = sec->opts; opt && opt->name; opt++) {
const char *name = opt->name, *level = "both";
char line[64];
if (opt->type != CFGT_STR || !cfg_opt_size(opt))
continue;
if (!strncmp(name, "soft.", 5)) {
level = "soft";
name += 5;
} else if (!strncmp(name, "hard.", 5)) {
level = "hard";
name += 5;
}
snprintf(line, sizeof(line), "%s %s %s", level, name,
cfg_opt_getnstr(opt, 0));
conf_parse_rlimit(line, arr);
}
}
/*
* exec-NAME [+ exec-NAME-timeout] -> legacy "tok:[SEC,]script"
*/
static void addscript(char *line, size_t len, cfg_t *sec, const char *key,
const char *tok)
{
char tmo[64];
const char *str;
char buf[512];
str = sec_getstr(sec, key, NULL);
if (!str)
return;
snprintf(tmo, sizeof(tmo), "%s-timeout", key);
if (cfg_size(sec, tmo))
snprintf(buf, sizeof(buf), "%ld,%s", cfg_getint(sec, tmo), str);
else
strlcpy(buf, str, sizeof(buf));
addopt(line, len, tok, buf);
}
/*
* log { file = "..." priority = "..." identity = "..." } -> legacy
* "log" or "log:file,prio:P,tag:I". An empty block is syslog with
* defaults; /dev/null and /dev/console arrive here as a file.
*/
static void addlog(char *line, size_t len, cfg_t *sec, char *file)
{
unsigned int num = cfg_size(sec, "log");
char buf[512] = "";
const char *str;
cfg_t *log;
if (!num)
return;
log = cfg_getnsec(sec, "log", num - 1);
if (!log)
return;
if (num > 1)
logit(LOG_WARNING, "%s: %s declares %u log blocks, using the last",
file, cfg_title(sec), num);
if ((str = sec_getstr(log, "file", NULL)))
strlcpy(buf, str, sizeof(buf));
if ((str = sec_getstr(log, "priority", NULL))) {
if (buf[0])
strlcat(buf, ",", sizeof(buf));
strlcat(buf, "prio:", sizeof(buf));
strlcat(buf, str, sizeof(buf));
}
if ((str = sec_getstr(log, "identity", NULL))) {
if (buf[0])
strlcat(buf, ",", sizeof(buf));
strlcat(buf, "tag:", sizeof(buf));
strlcat(buf, str, sizeof(buf));
}
if (buf[0])
addopt(line, len, "log:", buf);
else
addtok(line, len, "log");
}
/*
* Translate service/task/run/sysv section to the canonical legacy
* one-liner and register through the same path as legacy files.
*/
/*
* if = "udevd" asks whether a service by that name is known, answered
* when the .conf is read, while if = "usr/foo" tests a condition,
* answered at runtime. The namespace separator tells the two apart,
* so the legacy angle brackets are not needed. A statement must be
* all of one kind; the two are not checked at the same time, so they
* cannot be combined.
*/
static int if_translate(const char *str, char *buf, size_t len, char *file, const char *ident)
{
char tmp[MAX_IDENT_LEN];
char *op, *save;
int cond = -1;
if (strpbrk(str, "<>")) {
logit(LOG_ERR, "%s: %s: if: angle brackets are not used in this"
" format, got '%s', skipping", file, ident, str);
return -1;
}
if (strlcpy(tmp, str, sizeof(tmp)) >= sizeof(tmp)) {
logit(LOG_ERR, "%s: %s: if: '%s' is too long, skipping",
file, ident, str);
return -1;
}
for (op = strtok_r(tmp, ",", &save); op; op = strtok_r(NULL, ",", &save)) {
int is_cond;
if (op[0] == '!')
op++;
if (!op[0])
continue;
is_cond = strchr(op, '/') != NULL;
if (cond == -1)
cond = is_cond;
else if (cond != is_cond) {
logit(LOG_ERR, "%s: %s: if: cannot mix a service name with a"
" condition in '%s', a statement must be all of one"
" kind, skipping", file, ident, str);
return -1;
}
}
if (cond == -1) {
logit(LOG_ERR, "%s: %s: if: no condition or service name in '%s',"
" skipping", file, ident, str);
return -1;
}
if (cond)
snprintf(buf, len, "<%s>", str);
else
strlcpy(buf, str, len);
return 0;
}
/*
* These have no legacy token, they are validated by service_set_dir()
* and stored directly on the registered svc. Empty means unset,
* service_register() has already cleared the fields.
*/
static void dirs_translate(cfg_t *sec, svc_t *svc, char *file)
{
const char *str;
int i;
for (i = 0; i < NUM_SVCDIRS; i++) {
char key[32];
long num;
str = sec_getstr(sec, svcdirs[i].key, NULL);
if (str && str[0]) {
if (service_set_dir(svc, &svcdirs[i], str))
logit(LOG_ERR, "%s: %s: %s '%s' %s, ignoring",
file, cfg_title(sec), svcdirs[i].key, str,
errno == ENAMETOOLONG ? "is too long"
: "must be relative, no '..'");
}
snprintf(key, sizeof(key), "%s-mode", svcdirs[i].key);
if (sec_getint(sec, key, NULL, &num)) {
if (num & ~07777L)
logit(LOG_ERR, "%s: %s: %s %04lo is not a valid"
" mode, ignoring. Modes are octal,"
" with the leading zero", file,
cfg_title(sec), key, num);
else
svc->dir_mode[i] = (mode_t)num;
}
}
if ((str = sec_getstr(sec, "runtime-dir-preserve", NULL))) {
if (!strcmp(str, "no") || !strcmp(str, "false"))
svc->dir_preserve = SVC_DIR_PRESERVE_NO;
else if (!strcmp(str, "restart"))
svc->dir_preserve = SVC_DIR_PRESERVE_RESTART;
else if (!strcmp(str, "yes") || !strcmp(str, "true"))
svc->dir_preserve = SVC_DIR_PRESERVE_YES;
else
logit(LOG_ERR, "%s: %s: runtime-dir-preserve '%s' is not"
" no, restart, or yes, using no", file,
cfg_title(sec), str);
}
}
static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *file)
{
struct rlimit local_rlimit[RLIMIT_NLIMITS];
char line[LINE_SIZE] = "";
const char *str, *cmd, *grp;
int nowarn, own, bang;
unsigned int cnt;
long num;
char buf[512];
char nm[80];
svc_t *svc;
char *id;
cmd = sec_getstr(sec, "command", NULL);
if (!cmd) {
logit(LOG_ERR, "%s: section '%s' missing command, skipping",
file, cfg_title(sec));
return;
}
/* a leading - tolerates a missing binary, cf. systemd ExecStart=- */
nowarn = cmd[0] == '-';
if (nowarn)
cmd++;
if ((str = sec_getstr(sec, "runlevel", NULL)))
addtok(line, sizeof(line), "[%s]", str);
/*
* The ! leading a legacy condition list is not a condition at
* all, and it means two unrelated things depending on the block
* it sits in. Each has its own key here, valid only where the
* meaning applies, and both translate back to that same !.
*/
bang = 0;
if (type == SVC_TYPE_RUN || type == SVC_TYPE_TASK) {
if (cfg_size(sec, "required") && !sec_getbool(sec, "required", NULL))
bang = 1; /* do not hold up bootstrap */
if (cfg_size(sec, "reload-signal"))
logit(LOG_WARNING, "%s: %s: reload-signal does not apply"
" to run or task, ignoring", file, cfg_title(sec));
} else {
if ((str = sec_getstr(sec, "reload-signal", NULL))) {
/*
* Only SIGHUP and none survive the trip through a
* legacy line, which carries the flag and not the
* signal.
*/
if (!strcasecmp(str, "none"))
bang = 1;
else if (str2sig((char *)str) != SIGHUP)
logit(LOG_WARNING, "%s: %s: reload-signal is SIGHUP"
" or none, ignoring '%s'", file,
cfg_title(sec), str);
}
if (cfg_size(sec, "required"))
logit(LOG_WARNING, "%s: %s: required only applies to run"
" or task, ignoring", file, cfg_title(sec));
}
if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf))) {
if (buf[0] == '!')
logit(LOG_WARNING, "%s: %s: '!' in conditions is not"
" supported, use reload-signal or required",
file, cfg_title(sec));
addtok(line, sizeof(line), "<%s%s>", bang ? "!" : "", buf);
} else if (bang)
addtok(line, sizeof(line), "<!>");
/* section title is the identity: NAME[:ID], %i in templates */
strlcpy(nm, cfg_title(sec), sizeof(nm));
id = strchr(nm, ':');
if (id)
*id++ = 0;
addtok(line, sizeof(line), "name:%s", nm);
if (id && *id)
addtok(line, sizeof(line), ":%s", id);
/* @user[:group[,extra,...]] */
str = sec_getstr(sec, "user", NULL);
grp = sec_getstr(sec, "group", NULL);
if (sec_getlist(sec, "extra-groups", NULL, buf, sizeof(buf)))
addtok(line, sizeof(line), "@%s:%s,%s", str ? str : "root",
grp ? grp : "root", buf);
else if (grp)
addtok(line, sizeof(line), "@%s:%s", str ? str : "root", grp);
else if (str)
addtok(line, sizeof(line), "@%s", str);
if ((str = sec_getstr(sec, "envfile", "env")))
addopt(line, sizeof(line), "env:", str);
addlog(line, sizeof(line), sec, file);
/*
* The daemon owns its pidfile unless told otherwise, so the
* legacy ! is the default here and pidfile-create drops it. A
* daemon-owned default path needs no token, that is what the
* pidfile plugin discovers on its own.
*/
str = sec_getstr(sec, "pidfile", NULL);
own = sec_getbool(sec, "pidfile-create", NULL);
if (str && strcmp(str, "true") && strcmp(str, "false"))
addopt(line, sizeof(line), own ? "pid:" : "pid:!", str);
else if (own)
addtok(line, sizeof(line), "pid");
if ((str = sec_getstr(sec, "notify", NULL)))
addtok(line, sizeof(line), "notify:%s", str);
if ((str = sec_getstr(sec, "type", NULL))) {
if (!strcmp(str, "forking"))
addtok(line, sizeof(line), "type:forking");
else
logit(LOG_WARNING, "%s: unknown type '%s', ignoring",
file, str);
}
if (sec_getbool(sec, "manual-start", "manual"))
addtok(line, sizeof(line), "manual:yes");
if (sec_getbool(sec, "remain-after-exit", "remain"))
addtok(line, sizeof(line), "remain:yes");
if (sec_getbool(sec, "respawn", NULL))
addtok(line, sizeof(line), "respawn");
if (nowarn)
addtok(line, sizeof(line), "nowarn");
/*
* restart is the policy, restart-max the count. true is the
* default policy and needs no token unless a count came with it.
*/
str = sec_getstr(sec, "restart", NULL);
if (!str)
str = "true";
if (!strcmp(str, "never") || !strcmp(str, "false"))
addtok(line, sizeof(line), "norestart");
else if (!strcmp(str, "always"))
addtok(line, sizeof(line), "restart:always");
else {
if (strcmp(str, "true"))
logit(LOG_WARNING, "%s: unknown restart '%s', assuming true",
file, str);
if (sec_getint(sec, "restart-max", NULL, &num))
addtok(line, sizeof(line), "restart:%ld", num);
}
if (sec_getint(sec, "restart-sec", NULL, &num))
addtok(line, sizeof(line), "restart_sec:%ld", num);
if ((str = sec_getstr(sec, "oncrash", NULL)))
addtok(line, sizeof(line), "oncrash:%s", str);
if ((str = sec_getstr(sec, "stop-signal", "halt")))
addtok(line, sizeof(line), "halt:%s", str);
if (sec_getint(sec, "stop-timeout", "kill", &num))
addtok(line, sizeof(line), "kill:%ld", num);
addscript(line, sizeof(line), sec, "exec-start-pre", "pre:");
addscript(line, sizeof(line), sec, "exec-start-ready", "ready:");
addscript(line, sizeof(line), sec, "exec-stop", "stop:");
addscript(line, sizeof(line), sec, "exec-stop-post", "post:");
addscript(line, sizeof(line), sec, "exec-reload", "reload:");
addscript(line, sizeof(line), sec, "exec-cleanup", "cleanup:");
if (sec_getlist(sec, "capabilities", "caps", buf, sizeof(buf)))
addtok(line, sizeof(line), "caps:%s", buf);
if (sec_getlist(sec, "conflicts", NULL, buf, sizeof(buf)))
addtok(line, sizeof(line), "conflict:%s", buf);
if ((str = sec_getstr(sec, "if", NULL))) {
char ifbuf[MAX_IDENT_LEN + 2];
if (if_translate(str, ifbuf, sizeof(ifbuf), file, cfg_title(sec)))
return;
addopt(line, sizeof(line), "if:", ifbuf);
}
if ((str = sec_getstr(sec, "tty", NULL)))
addtok(line, sizeof(line), "tty:%s", str);
/*
* cgroup NAME {} to join group, settings become overrides. A
* service joins exactly one group, so on duplicates the last
* one wins, like a repeated token in a legacy one-liner.
*/
cnt = cfg_size(sec, "cgroup");
if (cnt) {
cfg_t *cg = cfg_getnsec(sec, "cgroup", cnt - 1);
if (cnt > 1)
logit(LOG_WARNING, "%s: %s declares %u cgroup blocks,"
" using '%s'", file, cfg_title(sec), cnt,
cfg_title(cg));
cgroup_settings(cg, buf, sizeof(buf), file, 1);
if (buf[0])
addtok(line, sizeof(line), "cgroup.%s,%s", cfg_title(cg), buf);
else
addtok(line, sizeof(line), "cgroup.%s", cfg_title(cg));
}
/* per-service rlimit {} on top of the file/global limits */
if (cfg_size(sec, "rlimit")) {
memcpy(local_rlimit, rlimit, sizeof(local_rlimit));
rlimit_translate(cfg_getsec(sec, "rlimit"), local_rlimit);
rlimit = local_rlimit;
}
/* the command and its arguments, verbatim */
addtok(line, sizeof(line), "%s", cmd);
if ((str = sec_getstr(sec, "description", "desc")))
addtok(line, sizeof(line), "-- %s", str);
dbg("translated: %s", line);
svc = service_register(type, line, rlimit, file);
if (!svc)
return;
dirs_translate(sec, svc, file);
}
/*
* Translate tty section, covering all three legacy variants:
* device (built-in getty), command (external getty), notty/rescue.
*/
static void tty_translate(cfg_t *sec, struct rlimit rlimit[], char *file)
{
char line[LINE_SIZE] = "";
const char *str, *dev, *cmd;
char buf[512];
if ((str = sec_getstr(sec, "runlevel", NULL)))
addtok(line, sizeof(line), "[%s]", str);
if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf)))
addtok(line, sizeof(line), "<%s>", buf);
dev = sec_getstr(sec, "device", NULL);
cmd = sec_getstr(sec, "command", NULL);
if (cmd && cmd[0] == '-') {
addtok(line, sizeof(line), "nowarn");
cmd++;
}
if (dev) {
addtok(line, sizeof(line), "%s", dev);
if (cfg_size(sec, "baud"))
addtok(line, sizeof(line), "%ld", cfg_getint(sec, "baud"));
} else if (cmd) {
addtok(line, sizeof(line), "%s", cmd);
} else {
if (sec_getbool(sec, "notty", NULL))
addtok(line, sizeof(line), "notty");
if (sec_getbool(sec, "rescue", NULL))
addtok(line, sizeof(line), "rescue");
if (!line[0]) {
logit(LOG_ERR, "%s: tty '%s' missing device, command,"
" or notty/rescue, skipping", file, cfg_title(sec));
return;
}
}
if (sec_getbool(sec, "noclear", NULL))
addtok(line, sizeof(line), "noclear");
if (sec_getbool(sec, "nowait", NULL))
addtok(line, sizeof(line), "nowait");
if (sec_getbool(sec, "nologin", NULL))
addtok(line, sizeof(line), "nologin");
if (dev && (str = sec_getstr(sec, "term", NULL)))
addtok(line, sizeof(line), "%s", str);
dbg("translated: tty %s", line);
service_register(SVC_TYPE_TTY, line, rlimit, file);
}
/*
* environment { KEY = "val" } -> conf_set_env()
*/
static void env_translate(cfg_t *cfg, const char *section)
{
cfg_t *env = cfg_getsec(cfg, section);
cfg_opt_t *opt;
if (!env)
return;
for (opt = env->opts; opt && opt->name; opt++) {
char buf[LINE_SIZE];
if (opt->type != CFGT_STR || !cfg_opt_size(opt))
continue;
snprintf(buf, sizeof(buf), "%s=%s", opt->name,
cfg_opt_getnstr(opt, 0));
conf_set_env(buf);
}
}
/*
* Static/bootstrap directives, same gating as the legacy parser:
* most are only honored while bootstrapping (runlevel S).
*/
static void conf_parse_statics(cfg_t *cfg)
{
const char *str, *key;
unsigned int i;
if (BOOTSTRAP) {
if ((str = sec_getstr(cfg, "hostname", NULL))) {
if (hostname)
free(hostname);
hostname = strdup(str);
}
for (i = 0; i < cfg_size(cfg, "mknod"); i++) {
char *dev = cfg_getnstr(cfg, "mknod", i);
char cmd[CMD_SIZE];
strlcpy(cmd, "mknod ", sizeof(cmd));
strlcat(cmd, dev, sizeof(cmd));
run_interactive(cmd, "Creating device node %s", dev);
}
key = sec_key(cfg, "modules", "mod");
for (i = 0; key && i < cfg_size(cfg, key); i++)
kmod_load(cfg_getnstr(cfg, key, i));
if ((str = sec_getstr(cfg, "network", NULL))) {
if (network)
free(network);
network = strdup(str);
}
if ((str = sec_getstr(cfg, "rcsd", NULL))) {
if (finit_rcsd)
free(finit_rcsd);
finit_rcsd = strdup(str);
}
if ((str = sec_getstr(cfg, "runparts", NULL))) {
if (runparts)
free(runparts);
runparts = strdup(str);
runparts_progress = sec_getbool(cfg, "runparts-progress", NULL);
runparts_sysv = sec_getbool(cfg, "runparts-sysv", NULL);
}
if (cfg_size(cfg, "runlevel")) {
long lvl = cfg_getint(cfg, "runlevel");
if (lvl < 1 || lvl > 9 || lvl == 6)
cfglevel = 2; /* Fallback */
else
cfglevel = (int)lvl;
}
if ((str = sec_getstr(cfg, "readiness", NULL))) {
if (!strcmp(str, "none"))
readiness = SVC_NOTIFY_NONE;
}
}
/*
* Global environment variables, environment {} or env {}. Read
* on every reload, not just at bootstrap: conf_reset_env() has
* just cleared them, so gating here would drop every variable on
* the way into the configured runlevel. See doc/config/env.md
*/
env_translate(cfg, "environment");
env_translate(cfg, "env");
if (cfg_size(cfg, "log")) {
cfg_t *log = cfg_getsec(cfg, "log");
int val;
if ((str = sec_getstr(log, "size", NULL))) {
val = strtobytes((char *)str);
if (val >= 0)
logfile_size_max = val;
}
if (cfg_size(log, "count")) {
val = (int)cfg_getint(log, "count");
if (val >= 0)
logfile_count_max = val;
}
}
if ((str = sec_getstr(cfg, "shutdown", NULL))) {
if (sdown)
free(sdown);
sdown = strdup(str);
}
if (cfg_size(cfg, "reboot-delay")) {
long val = cfg_getint(cfg, "reboot-delay");
if (val >= 0 && val <= 60)
syncsec = (int)val;
}
if (cfg_size(cfg, "reboot-watchdog"))
wdtreboot = sec_getbool(cfg, "reboot-watchdog", NULL);
if (cfg_size(cfg, "service-interval")) {
long val = cfg_getint(cfg, "service-interval");
/* 0 min to 1 day, should check at least daily */
if (val >= 0 && val <= 1440) {
int disabled = !service_interval;
/* milliseconds for libuEv timer */
service_interval = (int)val * 1000;
if (disabled)
service_init(NULL);
}
}
}
/*
* Sections registered in file order: cfg_t tracks the source line of
* each parsed section, sort on it so run/task/service execution order
* matches the order of declaration, like the legacy format.
*/
struct secref {
unsigned int line;
int type;
cfg_t *sec;
};
static int secref_cmp(const void *a, const void *b)
{
const struct secref *sa = a, *sb = b;
return (int)sa->line - (int)sb->line;
}
static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd)
{
static const struct { const char *name; int type; } map[] = {
{ "service", SVC_TYPE_SERVICE },
{ "task", SVC_TYPE_TASK },
{ "run", SVC_TYPE_RUN },
{ "sysv", SVC_TYPE_SYSV },
{ "tty", SVC_TYPE_TTY },
};
struct rlimit rlimit[RLIMIT_NLIMITS];
struct secref *refs;
struct rlimit *arr;
unsigned int i, j, num = 0;
/* Same per-file defaults as the legacy parser */
if (is_rcsd) {
memcpy(rlimit, global_rlimit, sizeof(rlimit));
cgroup_current[0] = 0;
arr = rlimit;
} else
arr = global_rlimit;
conf_parse_statics(cfg);
/* top-level cgroup definitions */
for (i = 0; i < cfg_size(cfg, "cgroup"); i++) {
cfg_t *cg = cfg_getnsec(cfg, "cgroup", i);
char buf[512];
cgroup_add((char *)cfg_title(cg),
cgroup_settings(cg, buf, sizeof(buf), file, 0), 0);
}
/* file-scope resource limits, applies to all services in file */
if (cfg_size(cfg, "rlimit"))
rlimit_translate(cfg_getsec(cfg, "rlimit"), arr);
for (i = 0; i < NELEMS(map); i++)
num += cfg_size(cfg, map[i].name);
if (!num)
return 0;
refs = calloc(num, sizeof(*refs));
if (!refs) {
err(1, "failed alloc in %s()", __func__);
return 1;
}
for (i = 0, num = 0; i < NELEMS(map); i++) {
for (j = 0; j < cfg_size(cfg, map[i].name); j++) {
cfg_t *sec = cfg_getnsec(cfg, map[i].name, j);
refs[num].line = sec->line;
refs[num].type = map[i].type;
refs[num].sec = sec;
num++;
}
}
qsort(refs, num, sizeof(*refs), secref_cmp);
for (i = 0; i < num; i++) {
if (refs[i].type == SVC_TYPE_TTY)
tty_translate(refs[i].sec, arr, file);
else
svc_translate(refs[i].sec, refs[i].type, arr, file);
}
free(refs);
return 0;
}
/*
* Substitute every %i in LINE with NAME, for template instantiation.
*
* Very simple and crude implementation, only supports '%i'
*/
static char *conf_instantiate(char *line, char *name)
{
char *ptr, *end = strchr(line, 0);
char *pos = line;
size_t num = 0;
if (!name[0] || !end)
return line;
while ((ptr = strchr(pos, '%'))) {
num++;
pos = ptr + 1;
}
ptr = realloc(line, strlen(line) + num * strlen(name) + 1);
if (!ptr)
return line;
pos = line = ptr;
while ((ptr = strchr(pos, '%'))) {
if (!strncmp(ptr, "%i", 2)) {
char *rest = &ptr[2];
char *next = ptr + strlen(name);
memmove(next, rest, strlen(rest) + 1);
memcpy(ptr, name, strlen(name));
pos += strlen(name);
} else
pos++;
}
return line;
}
static int conf_is_template(const char *file, char *name, size_t len)
{
char *ptr, *nm;
size_t i = 0;
/* the @ convention names the file, a directory may contain one */
ptr = strchr(basenm((char *)file), '@');
if (!ptr)
return 0; /* not a template */
nm = ptr + 1;
ptr = strstr(nm, ".conf");
if (!strcmp(nm, ".conf") || !ptr)
return 1; /* template itself or invalid */
if (!name)
return 1;
while (nm < ptr && i < len - 1)
name[i++] = *nm++;
name[i] = 0;
return 1; /* instantiated template */
}
/*
* Parse from FILE, or from BUF when it is set, i.e. for a template
* whose %i has already been substituted.
*
* XXX: Workaround for libConfuse <3.4, whose cfg_parse_buf() replaces
* cfg->filename with "[buf]", so every diagnostic from a template
* would lose the file name. cfg_parse_fp() keeps a name the
* caller has already set, so go through fmemopen() instead. With
* a 3.4 floor this is just cfg_parse_buf(cfg, buf).
*/
static int conf_parse_any(cfg_t *cfg, char *file, char *buf)
{
FILE *fp;
int rc;
if (!buf)
return cfg_parse(cfg, file);
/* fmemopen() rejects a zero length on older GLIBC */
if (!buf[0])
return CFG_SUCCESS;
fp = fmemopen(buf, strlen(buf), "r");
if (!fp)
return CFG_FILE_ERROR;
/* cfg_parse_fp() only names the stream when we have not */
cfg->filename = strdup(file);
rc = cfg_parse_fp(cfg, fp);
fclose(fp);
return rc;
}
/*
* Is this a new-format file, or a legacy one?
*
* Called only after a strict parse has already failed, to tell a
* new-format file with a typo from a legacy one-liner file. The
* lenient re-parse accepts any unknown key, so it succeeds on the
* former and still fails on the latter -- no legacy directive can
* satisfy the block grammar, they all lack the '=' or the '{'.
*
* Only the verdict is used, the parsed data is discarded: with
* CFGF_IGNORE_UNKNOWN libconfuse drops unknown keys instead of
* creating them, so a lenient tree is missing every set{} variable
* and every free-form cgroup key.
*/
static int is_new_format(char *file, char *buf)
{
cfg_t *cfg;
int rc;
cfg = cfg_init(conf_opts, CFGF_IGNORE_UNKNOWN);
if (!cfg)
return 0;
cfg_set_error_function(cfg, cfg_error_quiet);
rc = conf_parse_any(cfg, file, buf);
cfg_free(cfg);
return rc == CFG_SUCCESS;
}
/*
* Read FILE and substitute %i with NAME, for template instantiation.
* Returns a malloc()'ed buffer the caller frees.
*/
static char *conf_read_template(char *file, char *name)
{
char *buf;
buf = fslurp(NULL, "%s", file);
if (!buf)
return NULL;
return conf_instantiate(buf, name);
}
/*
* Parse one Finit .conf file, in either format. The legacy .conf
* include directive routes back through this so included files are
* format-detected too.
*/
int conf_parse_file(char *file, int is_rcsd)
{
char name[MAX_ID_LEN] = { 0 };
char *buf = NULL;
cfg_t *cfg;
int rc;
/*
* Template files (name@.conf, name@id.conf). A bare name@.conf
* is the template itself, there is nothing to instantiate from
* it. Otherwise %i is substituted over the whole file up front,
* so both the format detection below and, on fallback, the
* legacy parser see the finished text.
*/
if (conf_is_template(file, name, sizeof(name))) {
if (!name[0]) {
dbg("*** Skipping template file %s", file);
return 0;
}
dbg("*** instantiating %s from %s ...", name, file);
buf = conf_read_template(file, name);
if (!buf)
return 1;
}
cfg = cfg_init(conf_opts, CFGF_NONE);
if (!cfg) {
rc = legacy_parse_conf(file, buf, is_rcsd);
goto done;
}
cfg_set_error_function(cfg, cfg_error_cb);
rc = conf_parse_any(cfg, file, buf);
if (rc == CFG_SUCCESS) {
dbg("*** Parsing %s (new format)", file);
rc = conf_parse_cfg(cfg, file, is_rcsd);
cfg_free(cfg);
goto done;
}
cfg_free(cfg);
if (rc == CFG_FILE_ERROR) {
rc = 1; /* like legacy fopen() failure */
goto done;
}
if (is_new_format(file, buf)) {
logit(LOG_ERR, "parse error: %s", cfg_errmsg);
rc = 1;
goto done;
}
dbg("not in new format (%s), falling back to legacy parser", cfg_errmsg);
rc = legacy_parse_conf(file, buf, is_rcsd);
done:
free(buf);
return rc;
}
static void glob_append(glob_t *gl, int append, const char *fmt, ...)
{
va_list ap;
size_t len;
char *path;
va_start(ap, fmt);
len = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
path = alloca(++len);
if (!path) {
warn("failed alloca() in glob_append()");
return;
}
va_start(ap, fmt);
vsnprintf(path, len, fmt, ap);
va_end(ap);
dbg("conf_reload(): glob %s ...", path);
glob(path, append ? GLOB_APPEND : 0, NULL, gl);
}
/*
* Reload /etc/finit.conf and all *.conf in /etc/finit.d/
*/
int conf_reload(void)
{
glob_t gl;
size_t i;
/* Set time according to current time zone */
tzset();
dbg("Set time daylight: %d timezone: %ld tzname: %s %s",
daylight, timezone, tzname[0], tzname[1]);
/* Mark and sweep */
cgroup_mark_all();
svc_mark_dynamic();
conf_reset_env();
/*
* Reset global rlimit to bootstrap values from conf_init().
*/
memcpy(global_rlimit, initial_rlimit, sizeof(global_rlimit));
/*
* When built with --disable-rescue mode many other 'if (rescue)'
* code paths are #ifdeffed out. This one, and the ones in the
* plugins, are not because a hook or plugin can still trigger
* the alternative rescue.conf instead of finit.conf. This for
* very advanced use-cases where files on /etc are generated at
* bootstrap, including users and passwords, from other sources.
*/
if (rescue) {
int rc;
char line[80] = "tty [12345789] rescue";
/* If rescue.conf is missing, fall back to a root shell */
rc = conf_parse_file(RESCUE_CONF, 0);
if (rc)
service_register(SVC_TYPE_TTY, line, global_rlimit, NULL);
print(rc, "Entering rescue mode");
goto done;
}
/* First, read /etc/finit.conf */
conf_parse_file(finit_conf, 0);
/* Set global limits */
for (int i = 0; i < RLIMIT_NLIMITS; i++) {
if (setrlimit(i, &global_rlimit[i]) == -1)
logit(LOG_WARNING, "rlimit: Failed setting %s: %s",
rlim2str(i), lim2str(&global_rlimit[i]));
}
/*
* Next, read all *.conf in /lib/finit/system and /etc/finit.d/
* The system files were previously created at runtime by plugins
* but are now regular files that can be overridden by files in
* /etc/finit.d -- similar to how tmfiles.d(5) work. E.g., add
* an override .conf, or an ignore by symlinking to /dev/null
*
* The .conf files (and run/task/service stanzas) are parsed and
* started in order. Each directory is sorted alphanumerically
* and then the result is appended to the overall order:
*
* /lib/finit/system/10-hotplug.conf
* /lib/finit/system/90-testserv.conf
* /run/finit/system/dbus.conf
* /run/finit/system/tty.conf
* /etc/finit.d/10-abc.conf
* /etc/finit.d/20-abc.conf
* /etc/finit.d/enabled/1-aaa.conf
* /etc/finit.d/enabled/1-abc.conf
* /etc/finit.d/enabled/2-aaa.conf
*/
glob_append(&gl, 0, "%s/*.conf", FINIT_SYSPATH_);
glob_append(&gl, 1, "%s/*.conf", FINIT_RUNPATH_);
glob_append(&gl, 1, "%s/*.conf", finit_rcsd);
glob_append(&gl, 1, "%s/enabled/*.conf", finit_rcsd);
if (bootstrap) {
const char *fn = _PATH_VARRUN "finit/conf.order";
FILE *fp;
fp = fopen(fn, "w");
if (!fp) {
err(1, "failed creating %s", fn);
} else {
fprintf(fp, "# Evaluation & execution order of .conf files\n");
for (i = 0; i < gl.gl_pathc; i++)
fprintf(fp, "%s\n", gl.gl_pathv[i]);
fclose(fp);
}
}
for (i = 0; i < gl.gl_pathc; i++) {
char *path = gl.gl_pathv[i];
char *rp = NULL;
struct stat st;
size_t j, len;
/* check for FINIT_SYSPATH_ or FINIT_RUNPATH_ overrides */
for (j = i + 1; j < gl.gl_pathc; j++) {
if (strncmp(path, FINIT_SYSPATH_, strlen(FINIT_SYSPATH_)) &&
strncmp(path, FINIT_RUNPATH_, strlen(FINIT_RUNPATH_)))
continue;
if (strcmp(basenm(path), basenm(gl.gl_pathv[j])))
continue;
path = NULL; /* replacement later in list, skip this */
break;
}
if (!path)
continue; /* skip, override exists */
/* Check that it's an actual file ... beyond any symlinks */
if (lstat(path, &st)) {
dbg("Skipping %s, cannot access: %s", path, strerror(errno));
continue;
}
/* Skip directories */
if (S_ISDIR(st.st_mode)) {
dbg("Skipping directory %s", path);
continue;
}
/* Check for dangling symlinks */
if (S_ISLNK(st.st_mode)) {
rp = realpath(path, NULL);
if (!rp) {
logit(LOG_WARNING, "Skipping %s, dangling symlink: %s", path, strerror(errno));
continue;
}
}
/* Check that file ends with '.conf' */
len = strlen(path);
if (len < 6 || strcmp(&path[len - 5], ".conf"))
dbg("Skipping %s, not a Finit .conf file ... ", path);
else
conf_parse_file(path, 1);
if (rp)
free(rp);
}
globfree(&gl);
/* Mark any reverse deps as chenaged. */
service_update_rdeps();
/* Prune according to if:[!]ident or if:<[!]cond> */
service_mark_unavail();
/* Set up top-level cgroups */
cgroup_config();
done:
/* Remove all unused top-level cgroups */
cgroup_cleanup();
/* Drop record of all .conf changes */
drop_changes();
if (bootstrap)
wdog = svc_find("watchdog", "finit");
/* Override configured runlevel, user said 'S' on /proc/cmdline */
if (BOOTSTRAP && single)
cfglevel = 1;
/*
* Set host name, from %DEFHOST, *.conf or /etc/hostname. The
* latter wins, if neither exists we default to "noname"
*/
set_hostname(&hostname);
return 0;
}
static struct conf_change *conf_find(char *file)
{
struct conf_change *node, *tmp;
TAILQ_FOREACH_SAFE(node, &conf_change_list, link, tmp) {
if (string_compare(node->name, file))
return node;
}
return NULL;
}
static void drop_change(struct conf_change *node)
{
if (!node)
return;
TAILQ_REMOVE(&conf_change_list, node, link);
free(node->name);
free(node);
}
static void drop_changes(void)
{
struct conf_change *node, *tmp;
TAILQ_FOREACH_SAFE(node, &conf_change_list, link, tmp)
drop_change(node);
}
static int conf_change_act(char *dir, char *name, uint32_t mask)
{
char fn[strlen(dir) + strlen(name) + 2];
struct conf_change *node;
char *rp = NULL;
/* Check for actual printable characters, sometimes STX */
if (name[0] && name[0] >= ' ')
paste(fn, sizeof(fn), dir, name);
else
strlcpy(fn, dir, sizeof(fn));
dbg("path: %s mask: %08x", fn, mask);
if (conf_is_template(name, NULL, 0)) {
/* Skip realpath for templates */
rp = strdup(fn);
} else {
/* Handle disabling/removal of service */
rp = realpath(fn, NULL);
if (!rp) {
if (errno != ENOENT)
goto fail;
rp = strdup(fn);
}
}
if (!rp)
goto fail;
node = conf_find(rp);
if (node) {
dbg("event already registered for %s ...", name);
free(rp);
return 0;
}
node = malloc(sizeof(*node));
if (!node) {
free(rp);
goto fail;
}
node->name = rp;
TAILQ_INSERT_HEAD(&conf_change_list, node, link);
dbg("event registered for %s, mask 0x%x", rp, mask);
return 0;
fail:
warn("failed registering %s event mask %08x", fn, mask);
return 1;
}
int conf_any_change(void)
{
if (TAILQ_EMPTY(&conf_change_list))
return 0;
return 1;
}
int conf_changed(char *file)
{
int rc = 0;
char *rp;
if (!file)
return 0;
if (conf_is_template(file, NULL, 0))
rp = strdup(file);
else
rp = realpath(file, NULL);
if (!rp)
return 0;
if (conf_find(rp))
rc = 1;
free(rp);
return rc;
}
static int conf_iwatch_read(int fd)
{
static char ev_buf[8 *(sizeof(struct inotify_event) + NAME_MAX + 1) + 1];
struct inotify_event *ev;
ssize_t sz;
size_t off;
sz = read(fd, ev_buf, sizeof(ev_buf) - 1);
if (sz <= 0)
return -1;
ev_buf[sz] = 0;
for (off = 0; off < (size_t)sz; off += sizeof(*ev) + ev->len) {
struct iwatch_path *iwp;
if (off + sizeof(*ev) > (size_t)sz)
break;
ev = (struct inotify_event *)&ev_buf[off];
if (off + sizeof(*ev) + ev->len > (size_t)sz)
break;
if (!ev->mask)
continue;
dbg("name %s, event: 0x%08x", ev->name, ev->mask);
/* Find base path for this event */
iwp = iwatch_find_by_wd(&iw_conf, ev->wd);
if (!iwp)
continue;
if (conf_change_act(iwp->path, ev->name, ev->mask))
break;
}
return 0;
}
static void conf_cb(uev_t *w, void *arg, int events)
{
(void)arg;
if (UEV_ERROR == events) {
dbg("%s(): iwatch socket %d invalid.", __func__, w->fd);
return;
}
if (conf_iwatch_read(w->fd)) {
err(1, "invalid inotify event");
return;
}
#ifdef AUTO_RELOAD
if (conf_any_change())
sm_reload();
#endif
}
void conf_flush_events(void)
{
while (!conf_iwatch_read(iwatch_fd))
dbg("emptying inotify queue ...");
}
/*
* Set up inotify watcher and load all *.conf in /etc/finit.d/
*/
int conf_monitor(void)
{
char path[strlen(finit_rcsd) + 16];
int rc = 0;
/*
* If only one watcher fails, that's OK. A user may have only
* one of /etc/finit.conf or /etc/finit.d in use, and may also
* have or not have symlinks in place. We need to monitor for
* changes to either symlink or target.
*/
rc |= iwatch_add(&iw_conf, finit_rcsd, IN_ONLYDIR);
snprintf(path, sizeof(path), "%s/available/", finit_rcsd);
rc |= iwatch_add(&iw_conf, path, IN_ONLYDIR | IN_DONT_FOLLOW);
snprintf(path, sizeof(path), "%s/enabled/", finit_rcsd);
rc |= iwatch_add(&iw_conf, path, IN_ONLYDIR | IN_DONT_FOLLOW);
rc |= iwatch_add(&iw_conf, finit_conf, 0);
/*
* Systems with /etc/default, /etc/conf.d, or similar, can also
* monitor changes in env files sourced by .conf files (above)
* define your own with --with-sysconfig=/path/to/envfiles
*/
rc |= iwatch_add(&iw_conf, "/etc/default/", IN_ONLYDIR);
rc |= iwatch_add(&iw_conf, "/etc/conf.d/", IN_ONLYDIR);
#ifdef FINIT_SYSCONFIG
rc |= iwatch_add(&iw_conf, FINIT_SYSCONFIG, IN_ONLYDIR);
#endif
rc |= conf_reload();
return rc;
}
/*
* Prepare .conf parser and load /etc/finit.conf for global settings
*/
int conf_init(uev_ctx_t *ctx)
{
/* default hostname */
hostname = strdup(DEFHOST);
/*
* Get current global limits, which may be overridden from both
* finit.conf, for Finit and its services like getty+watchdogd,
* and *.conf in finit.d/, for each service(s) listed there.
*/
for (int i = 0; i < RLIMIT_NLIMITS; i++) {
if (getrlimit(i, &initial_rlimit[i]))
logit(LOG_WARNING, "rlimit: Failed reading setting %s: %s",
rlim2str(i), strerror(errno));
}
/* Initialize global rlimits, e.g. for built-in services */
memcpy(global_rlimit, initial_rlimit, sizeof(global_rlimit));
/*
* Start built-in watchdogd as soon as possible, if enabled
*/
#ifdef WDT_DEVNODE
if (whichp(FINIT_EXECPATH_ "/watchdogd") && fexist(WDT_DEVNODE)) {
conf_save_service(SVC_TYPE_SERVICE, "watchdog:finit", "watchdogd.conf",
"\tdescription = \"Finit watchdog daemon\"\n"
"\trunlevel = \"S0123456789\"\n"
"\tnotify = \"none\"\n"
"\tcgroup init {}\n"
"\tcommand = \"" FINIT_EXECPATH_ "/watchdogd\"\n");
}
#endif
/*
* Start kernel event daemon as soon as possible, if enabled
*/
if (whichp(FINIT_EXECPATH_ "/keventd"))
conf_save_service(SVC_TYPE_SERVICE, "keventd", "keventd.conf",
"\tdescription = \"Finit kernel event daemon\"\n"
"\trunlevel = \"S12345789\"\n"
"\tnotify = \"none\"\n"
"\tcgroup init {}\n"
"\tcommand = \"" FINIT_EXECPATH_ "/keventd\"\n");
dbg("Allow plugins to register early runlevel 1 run/task/services ...");
plugin_run_hooks(HOOK_SVC_PLUGIN);
/* Read global rlimits and global cgroup setup from /etc/finit.conf */
conf_parse_file(finit_conf, 0);
/* prepare /etc watcher */
iwatch_fd = iwatch_init(&iw_conf);
if (iwatch_fd < 0)
return 1;
if (uev_io_init(ctx, &etcw, conf_cb, NULL, iwatch_fd, UEV_READ)) {
err(1, "Failed setting up I/O callback for /etc watcher");
close(iwatch_fd);
return 1;
}
/*
* Background startup scripts in the runparts directory, if any.
*/
if (runparts && fisdir(runparts) && !rescue) {
char args[10] = { 0 };
if (debug)
strlcat(args, "-d ", sizeof(args));
if (runparts_progress)
strlcat(args, "-p ", sizeof(args));
if (runparts_sysv)
strlcat(args, "-s ", sizeof(args));
conf_save_service(SVC_TYPE_TASK, "runparts", "runparts.conf",
"\tdescription = \"Calling runparts %s in the background\"\n"
"\trunlevel = \"S\"\n"
"\tconditions = { \"int/bootstrap\" }\n"
"\tnotify = \"none\"\n"
"\tlog { file = \"/dev/console\" }\n"
"\tcommand = \"%s %s%s\"\n",
runparts, _PATH_RUNPARTS, args, runparts);
}
return 0;
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/