diff --git a/configure.ac b/configure.ac index 83346228..90b35c47 100644 --- a/configure.ac +++ b/configure.ac @@ -50,6 +50,10 @@ PKG_PROG_PKG_CONFIG # Check for required libraries PKG_CHECK_MODULES([uev], [libuev >= 2.4.1]) PKG_CHECK_MODULES([lite], [libite >= 2.6.1]) +# 3.3 is the floor: CFGF_KEYSTRVAL, which set {} and the free-form +# cgroup keys are built on, does not exist before it. 3.3 parses both +# correctly, see the XXX in src/conf.c before raising this to 3.4. +PKG_CHECK_MODULES([confuse], [libconfuse >= 3.3]) # Check for configured Finit features AC_ARG_ENABLE(auto_reload, diff --git a/src/Makefile.am b/src/Makefile.am index 93f6e300..df8a8dfd 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -58,7 +58,8 @@ finit_SOURCES = api.c cgroup.c cgroup.h \ client.c client.h \ clone3.c clone3.h \ cond.c cond-w.c cond.h \ - legacy.c conf.h \ + conf.c conf.h \ + legacy.c legacy.h \ devmon.c devmon.h \ exec.c finit.c finit.h \ stty.c \ @@ -86,8 +87,8 @@ pkginclude_HEADERS = cgroup.h cond.h conf.h finit.h helpers.h log.h \ finit_CPPFLAGS = $(AM_CPPFLAGS) -D__FINIT__ finit_CFLAGS = -W -Wall -Wextra -Wno-unused-parameter -std=gnu99 -finit_CFLAGS += $(lite_CFLAGS) $(uev_CFLAGS) -finit_LDADD = $(lite_LIBS) $(uev_LIBS) +finit_CFLAGS += $(lite_CFLAGS) $(uev_CFLAGS) $(confuse_CFLAGS) +finit_LDADD = $(lite_LIBS) $(uev_LIBS) $(confuse_LIBS) if STATIC finit_LDADD += ../plugins/libplug.la else diff --git a/src/conf.c b/src/conf.c new file mode 100644 index 00000000..3bf9f4e5 --- /dev/null +++ b/src/conf.c @@ -0,0 +1,1430 @@ +/* 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 + * + * 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 +#include +#include +#ifdef _LIBITE_LITE +# include +# include /* BSD sys/queue.h API */ +#else +# include +# include /* BSD sys/queue.h API */ +#endif +#include +#include +#include + +#include "finit.h" +#include "conf.h" +#include "iwatch.h" +#include "legacy.h" +#include "private.h" +#include "service.h" +#include "helpers.h" +#include "util.h" + +#define BOOTSTRAP (runlevel == INIT_LEVEL) + +struct conf_change { + TAILQ_ENTRY(conf_change) link; + char *name; +}; + +static struct iwatch iw_conf; +static int iwatch_fd; +static uev_t etcw; + +static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list); + +static void drop_changes(void); + +/* + * 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..max, rdma..*, 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[] = { + 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 { 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() +}; + +/* 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 ("exec", NULL, CFGF_NODEFAULT), /* alias */ + CFG_STR ("runlevel", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("condition", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("cond", NULL, CFGF_NODEFAULT), /* alias */ + CFG_STR ("user", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("group", NULL, CFGF_NODEFAULT), + CFG_STR ("environment", NULL, CFGF_NODEFAULT), + CFG_STR ("env", NULL, CFGF_NODEFAULT), /* alias */ + CFG_STR ("pid", NULL, CFGF_NODEFAULT), + CFG_STR ("log", NULL, CFGF_NODEFAULT), + CFG_STR ("notify", NULL, CFGF_NODEFAULT), + CFG_STR ("type", NULL, CFGF_NODEFAULT), + CFG_BOOL ("manual", cfg_false, CFGF_NODEFAULT), + CFG_BOOL ("remain", cfg_false, CFGF_NODEFAULT), + CFG_BOOL ("respawn", cfg_false, CFGF_NODEFAULT), + CFG_BOOL ("nowarn", cfg_false, CFGF_NODEFAULT), + CFG_STR ("restart", NULL, CFGF_NODEFAULT), + CFG_INT ("restart_sec", 0, CFGF_NODEFAULT), + CFG_STR ("oncrash", NULL, CFGF_NODEFAULT), + CFG_STR ("halt", NULL, CFGF_NODEFAULT), + CFG_INT ("kill", 0, CFGF_NODEFAULT), + CFG_STR ("pre", NULL, CFGF_NODEFAULT), + CFG_STR ("post", NULL, CFGF_NODEFAULT), + CFG_STR ("ready", NULL, CFGF_NODEFAULT), + CFG_STR ("cleanup", NULL, CFGF_NODEFAULT), + CFG_STR ("reload", NULL, CFGF_NODEFAULT), + CFG_STR ("stop", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("capabilities", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("caps", NULL, CFGF_NODEFAULT), /* alias */ + CFG_STR_LIST("conflict", NULL, CFGF_NODEFAULT), + CFG_STR ("if", NULL, CFGF_NODEFAULT), + CFG_STR ("tty", NULL, CFGF_NODEFAULT), + 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("condition", 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_STR ("exec", NULL, CFGF_NODEFAULT), /* alias */ + 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 ("host", NULL, CFGF_NODEFAULT), /* alias */ + CFG_STR_LIST("module", NULL, CFGF_NODEFAULT), + 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() +}; + +/* + * 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 char cfg_errmsg[256]; + +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. + */ +static const char *sec_getstr(cfg_t *sec, const char *key, const char *alias) +{ + if (cfg_size(sec, key)) + return cfg_getstr(sec, key); + if (alias && cfg_size(sec, alias)) + return cfg_getstr(sec, alias); + return NULL; +} + +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) +{ + if (!cfg_size(sec, 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) +{ + cfg_opt_t *opt; + + buf[0] = 0; + 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 (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); + } +} + +/* + * Translate service/task/run/sysv section to the canonical legacy + * one-liner and register through the same path as legacy files. + */ +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; + unsigned int num; + char buf[512]; + char nm[80]; + char *id; + + cmd = sec_getstr(sec, "command", "exec"); + if (!cmd) { + logit(LOG_ERR, "%s: section '%s' missing command, skipping", + file, cfg_title(sec)); + return; + } + + if ((str = sec_getstr(sec, "runlevel", NULL))) + addtok(line, sizeof(line), "[%s]", str); + + if (sec_getlist(sec, "condition", "cond", buf, sizeof(buf))) + addtok(line, sizeof(line), "<%s>", buf); + + /* 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[,supplementary,...]], first group is primary */ + str = sec_getstr(sec, "user", NULL); + if (sec_getlist(sec, "group", NULL, buf, sizeof(buf))) + addtok(line, sizeof(line), "@%s:%s", str ? str : "root", buf); + else if (str) + addtok(line, sizeof(line), "@%s", str); + + if ((str = sec_getstr(sec, "environment", "env"))) + addopt(line, sizeof(line), "env:", str); + + if ((str = sec_getstr(sec, "log", NULL))) { + if (!strcmp(str, "true")) + addtok(line, sizeof(line), "log"); + else if (strcmp(str, "false")) + addopt(line, sizeof(line), "log:", str); + } + + if ((str = sec_getstr(sec, "pid", NULL))) { + if (!strcmp(str, "true")) + addtok(line, sizeof(line), "pid"); + else if (strcmp(str, "false")) + addopt(line, sizeof(line), "pid:", str); + } + + 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")) + addtok(line, sizeof(line), "manual:yes"); + if (sec_getbool(sec, "remain")) + addtok(line, sizeof(line), "remain:yes"); + if (sec_getbool(sec, "respawn")) + addtok(line, sizeof(line), "respawn"); + if (sec_getbool(sec, "nowarn")) + addtok(line, sizeof(line), "nowarn"); + + if ((str = sec_getstr(sec, "restart", NULL))) { + if (!strcmp(str, "never")) + addtok(line, sizeof(line), "norestart"); + else + addtok(line, sizeof(line), "restart:%s", str); + } + if (cfg_size(sec, "restart_sec")) + addtok(line, sizeof(line), "restart_sec:%ld", cfg_getint(sec, "restart_sec")); + + if ((str = sec_getstr(sec, "oncrash", NULL))) + addtok(line, sizeof(line), "oncrash:%s", str); + if ((str = sec_getstr(sec, "halt", NULL))) + addtok(line, sizeof(line), "halt:%s", str); + if (cfg_size(sec, "kill")) + addtok(line, sizeof(line), "kill:%ld", cfg_getint(sec, "kill")); + + if ((str = sec_getstr(sec, "pre", NULL))) + addopt(line, sizeof(line), "pre:", str); + if ((str = sec_getstr(sec, "post", NULL))) + addopt(line, sizeof(line), "post:", str); + if ((str = sec_getstr(sec, "ready", NULL))) + addopt(line, sizeof(line), "ready:", str); + if ((str = sec_getstr(sec, "cleanup", NULL))) + addopt(line, sizeof(line), "cleanup:", str); + if ((str = sec_getstr(sec, "reload", NULL))) + addopt(line, sizeof(line), "reload:", str); + if ((str = sec_getstr(sec, "stop", NULL))) + addopt(line, sizeof(line), "stop:", str); + + if (sec_getlist(sec, "capabilities", "caps", buf, sizeof(buf))) + addtok(line, sizeof(line), "caps:%s", buf); + if (sec_getlist(sec, "conflict", NULL, buf, sizeof(buf))) + addtok(line, sizeof(line), "conflict:%s", buf); + + if ((str = sec_getstr(sec, "if", NULL))) + addopt(line, sizeof(line), "if:", str); + 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. + */ + num = cfg_size(sec, "cgroup"); + if (num) { + cfg_t *cg = cfg_getnsec(sec, "cgroup", num - 1); + + if (num > 1) + logit(LOG_WARNING, "%s: %s declares %u cgroup blocks," + " using '%s'", file, cfg_title(sec), num, + cfg_title(cg)); + + cgroup_settings(cg, buf, sizeof(buf)); + 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); + service_register(type, line, rlimit, 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, "condition", "cond", buf, sizeof(buf))) + addtok(line, sizeof(line), "<%s>", buf); + + dev = sec_getstr(sec, "device", NULL); + cmd = sec_getstr(sec, "command", "exec"); + + 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")) + addtok(line, sizeof(line), "notty"); + if (sec_getbool(sec, "rescue")) + 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")) + addtok(line, sizeof(line), "noclear"); + if (sec_getbool(sec, "nowait")) + addtok(line, sizeof(line), "nowait"); + if (sec_getbool(sec, "nologin")) + 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)); + legacy_parse_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) +{ + unsigned int i; + const char *str; + + if (BOOTSTRAP) { + if ((str = sec_getstr(cfg, "hostname", "host"))) { + 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); + } + + for (i = 0; i < cfg_size(cfg, "module"); i++) + kmod_load(cfg_getnstr(cfg, "module", 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"); + runparts_sysv = sec_getbool(cfg, "runparts-sysv"); + } + + 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"); + + if (cfg_size(cfg, "service-interval")) { + long val = cfg_getint(cfg, "service-interval"); + + if (val >= 0 && val <= 1440) { + int disabled = !service_interval; + + 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)), 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; +} + +/* + * 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) +{ + 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 = cfg_parse(cfg, file); + cfg_free(cfg); + + return rc == CFG_SUCCESS; +} + +/* + * Parse one Finit .conf file, in either format, see top of file. + */ +int conf_parse_file(char *file, int is_rcsd) +{ + cfg_t *cfg; + int rc; + + /* + * Template files (name@.conf, name@id.conf): %i instantiation + * for the new format is not yet supported, so these bypass + * detection entirely and go to the legacy parser, which handles + * templates per line. See issue #148. + */ + if (strchr(basenm(file), '@')) + return legacy_parse_conf(file, is_rcsd); + + cfg = cfg_init(conf_opts, CFGF_NONE); + if (!cfg) + return legacy_parse_conf(file, is_rcsd); + + cfg_set_error_function(cfg, cfg_error_cb); + rc = cfg_parse(cfg, file); + if (rc == CFG_SUCCESS) { + dbg("*** Parsing %s (new format)", file); + rc = conf_parse_cfg(cfg, file, is_rcsd); + cfg_free(cfg); + return rc; + } + cfg_free(cfg); + + if (rc == CFG_FILE_ERROR) + return 1; /* like legacy fopen() failure */ + + if (is_new_format(file)) { + logit(LOG_ERR, "parse error: %s", cfg_errmsg); + return 1; + } + + dbg("not in new format (%s), falling back to legacy parser", cfg_errmsg); + + return legacy_parse_conf(file, is_rcsd); +} + +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 (strchr(name, '@')) { + /* 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 (strchr(file, '@')) + 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, "[S0123456789] cgroup.init notify:none name:watchdog :finit " + FINIT_EXECPATH_ "/watchdogd -- Finit watchdog daemon", "watchdogd.conf"); + } +#endif + /* + * Start kernel event daemon as soon as possible, if enabled + */ + if (whichp(FINIT_EXECPATH_ "/keventd")) + conf_save_service(SVC_TYPE_SERVICE, "[S12345789] cgroup.init notify:none " + FINIT_EXECPATH_ "/keventd -- Finit kernel event daemon", "keventd.conf"); + + 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 conf[sizeof(_PATH_RUNPARTS) + strlen(runparts) + 100]; + 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)); + + snprintf(conf, sizeof(conf), "[S] notify:none log:console %s %s %s" + " -- Calling runparts %s in the background", + _PATH_RUNPARTS, args, runparts, runparts); + conf_save_service(SVC_TYPE_TASK, conf, "runparts.conf"); + } + + return 0; +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/src/conf.h b/src/conf.h index a86d60f4..6141dd21 100644 --- a/src/conf.h +++ b/src/conf.h @@ -49,7 +49,7 @@ extern char *osheading; extern int logfile_size_max; extern int logfile_count_max; -extern struct rlimit global_rlimit[]; +extern struct rlimit global_rlimit[RLIMIT_NLIMITS]; extern char cgroup_current[]; extern char cgroup_settings_current[]; extern int cgroup_delegate_current; @@ -58,7 +58,7 @@ int str2rlim(char *str); char *rlim2str(int rlim); int conf_init (uev_ctx_t *ctx); -void conf_reload (void); +int conf_reload (void); int conf_any_change (void); int conf_changed (char *file); int conf_monitor (void); diff --git a/src/legacy.c b/src/legacy.c index 6e8ec834..1feb5ca3 100644 --- a/src/legacy.c +++ b/src/legacy.c @@ -1,4 +1,9 @@ -/* Parser for /etc/finit.conf and /etc/finit.d/.conf +/* Legacy one-liner parser for /etc/finit.conf and the finit.d hierarchy + * + * NOTE: this parser is frozen at the Finit 4.x feature set. It is + * kept, indefinitely, for backwards compatibility -- existing + * configurations must keep working. All new directives and + * options land in the libconfuse-based format, see conf.c * * Copyright (c) 2012-2025 Joachim Wiberg * @@ -26,7 +31,6 @@ #include #include #include -#include #include #ifdef _LIBITE_LITE # include @@ -35,13 +39,10 @@ # include # include /* BSD sys/queue.h API */ #endif -#include -#include - #include "finit.h" #include "cond.h" #include "devmon.h" -#include "iwatch.h" +#include "legacy.h" #include "private.h" #include "service.h" #include "tty.h" @@ -110,20 +111,6 @@ char cgroup_current[16]; /* cgroup.NAME sets current cgroup for a set char cgroup_settings_current[128]; /* cgroup.system,cpu.weight:500 - cgroup settings */ int cgroup_delegate_current; /* cgroup.system,delegate - delegation flag */ -struct conf_change { - TAILQ_ENTRY(conf_change) link; - char *name; -}; - -static struct iwatch iw_conf; -static int iwatch_fd; -static uev_t etcw; - -static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list); - -static int parse_conf(char *file, int is_rcsd); -static void drop_changes(void); - static int get_bool(char *arg, int default_value) { if (!arg) @@ -581,7 +568,7 @@ char *conf_parse_env(char *line, char **value) * from finit.conf, or other .conf file. Note, PATH is always reset in * the conf_reset_env() function. */ -static void parse_env(char *line) +void legacy_parse_env(char *line) { struct env_entry *node; char *key, *val; @@ -630,7 +617,7 @@ static int kmod_exists(char *mod) return found; } -static void kmod_load(char *mod) +void kmod_load(char *mod) { char module[64] = { 0 }; char cmd[CMD_SIZE]; @@ -963,7 +950,7 @@ static int parse_static(char *line, int is_rcsd) } if (BOOTSTRAP && MATCH_CMD(line, "set ", x)) { - parse_env(x); + legacy_parse_env(x); return 0; } @@ -976,7 +963,7 @@ static int parse_static(char *line, int is_rcsd) return 1; } - return parse_conf(cmd, is_rcsd); + return conf_parse_file(cmd, is_rcsd); } if (MATCH_CMD(line, "log ", x)) { @@ -1214,7 +1201,7 @@ static int is_template(const char *file, char *name, size_t len) return 1; /* instantiated template */ } -static int parse_conf(char *file, int is_rcsd) +int legacy_parse_conf(char *file, int is_rcsd) { struct rlimit rlimit[RLIMIT_NLIMITS]; char name[65] = { 0 }; @@ -1256,7 +1243,7 @@ static int parse_conf(char *file, int is_rcsd) else if (!parse_dynamic(line, is_rcsd ? rlimit : global_rlimit, file)) ; else - parse_env(line); + legacy_parse_env(line); free(line); } @@ -1266,503 +1253,6 @@ static int parse_conf(char *file, int is_rcsd) return 0; } -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 = parse_conf(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 */ - parse_conf(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 - parse_conf(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 (strchr(name, '@')) { - /* 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 (strchr(file, '@')) - 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, "[S0123456789] cgroup.init notify:none name:watchdog :finit " - FINIT_EXECPATH_ "/watchdogd -- Finit watchdog daemon", "watchdogd.conf"); - } -#endif - /* - * Start kernel event daemon as soon as possible, if enabled - */ - if (whichp(FINIT_EXECPATH_ "/keventd")) - conf_save_service(SVC_TYPE_SERVICE, "[S12345789] cgroup.init notify:none " - FINIT_EXECPATH_ "/keventd -- Finit kernel event daemon", "keventd.conf"); - - 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 */ - parse_conf(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 conf[sizeof(_PATH_RUNPARTS) + strlen(runparts) + 100]; - 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)); - - snprintf(conf, sizeof(conf), "[S] notify:none log:console %s %s %s" - " -- Calling runparts %s in the background", - _PATH_RUNPARTS, args, runparts, runparts); - conf_save_service(SVC_TYPE_TASK, conf, "runparts.conf"); - } - - return 0; -} - /** * Local Variables: * indent-tabs-mode: t diff --git a/src/legacy.h b/src/legacy.h new file mode 100644 index 00000000..083018f0 --- /dev/null +++ b/src/legacy.h @@ -0,0 +1,56 @@ +/* Internal interface between conf.c and the legacy one-liner parser + * + * Copyright (c) 2012-2026 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef FINIT_LEGACY_H_ +#define FINIT_LEGACY_H_ + +#include + +extern struct rlimit initial_rlimit[RLIMIT_NLIMITS]; +extern int runparts_progress; +extern int runparts_sysv; + +/* + * legacy.c -- the frozen legacy one-liner parser. No new features + * land here, only in the libconfuse format frontend in conf.c + */ +int legacy_parse_conf (char *file, int is_rcsd); +void legacy_parse_env (char *line); +void kmod_load (char *mod); +void conf_parse_rlimit (char *line, struct rlimit arr[]); +char *lim2str (struct rlimit *rlim); + +/* + * conf.c -- format-detecting frontend. The legacy include directive + * routes back through this so included files are format-detected too. + */ +int conf_parse_file (char *file, int is_rcsd); + +#endif /* FINIT_LEGACY_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/test/Makefile.am b/test/Makefile.am index da6eb905..9edcca9f 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -31,6 +31,7 @@ EXTRA_DIST += add-remove-dynamic-service.sh EXTRA_DIST += add-remove-dynamic-service-sub-config.sh EXTRA_DIST += bootstrap-crash.sh EXTRA_DIST += cond-start-task.sh +EXTRA_DIST += conf-format.sh EXTRA_DIST += crashing.sh EXTRA_DIST += dep-chain-reload.sh EXTRA_DIST += depserv.sh @@ -75,6 +76,7 @@ TESTS += add-remove-dynamic-service.sh TESTS += add-remove-dynamic-service-sub-config.sh TESTS += bootstrap-crash.sh TESTS += cond-start-task.sh +TESTS += conf-format.sh TESTS += crashing.sh TESTS += dep-chain-reload.sh TESTS += depserv.sh diff --git a/test/conf-format.sh b/test/conf-format.sh new file mode 100755 index 00000000..605c3170 --- /dev/null +++ b/test/conf-format.sh @@ -0,0 +1,133 @@ +#!/bin/sh +# Verify the new libconfuse config format: block -> legacy translation, +# per-file format detection (legacy files keep working alongside), and +# that a typo in a new-format file is rejected with an error instead of +# being fed to the legacy parser. +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck disable=SC2034 +# A /etc/finit.conf entirely in the new format, seeded before Finit +# boots. Mixing the two formats in one file is not supported, so the +# probe that reads the variable back is a block too. +BOOTSTRAP="environment {\n\ + CONF_FORMAT_VAR = \"blockfmt\"\n\ +}\n\ +run envprobe {\n\ + runlevel = \"S\"\n\ + description = \"Probe\"\n\ + command = \"/bin/touch /tmp/envprobe-\$CONF_FORMAT_VAR\"\n\ +}" + +test_teardown() +{ + say "Running test teardown." + run "rm -f $FINIT_CONF /tmp/envprobe-*" + run "rm -f $FINIT_RCSD/legacy-side.conf" +} + +# Write a service block, $1 is the description key, to exercise both +# the canonical spelling and a typo. $2, if given, is prepended as a +# root-level line. +write_svc() +{ + run "echo '${2:-}service service.sh {' > $FINIT_CONF" + run "echo ' $1 = \"Test service\"' >> $FINIT_CONF" + run "echo ' command = \"service.sh\"' >> $FINIT_CONF" + run "echo '}' >> $FINIT_CONF" +} + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +say 'A new-format /etc/finit.conf booted the system, environment {} applied' +retry 'assert_file_exists /tmp/envprobe-blockfmt' + +# Reaching runlevel 2 reloads the .conf files, and conf_reset_env() +# clears every tracked variable first. Gating environment {} on +# bootstrap would therefore drop it here, which is what serv -e +# catches: it exits unless the variable still holds the value. +say 'Global env survives the runlevel change and a reload, via the env {} alias' +run "echo 'env {' > $FINIT_CONF" +run "echo ' CONF_FORMAT_VAR = \"blockfmt\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "echo 'service serv {' >> $FINIT_CONF" +run "echo ' description = \"Verify env\"' >> $FINIT_CONF" +run "echo ' command = \"serv -np -e CONF_FORMAT_VAR:blockfmt\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +retry 'assert_num_children 1 serv' +assert_desc "Verify env" serv + +say "Add new-format service block in $FINIT_CONF" +run "echo 'service service.sh {' > $FINIT_CONF" +run "echo ' description = \"Test service\"' >> $FINIT_CONF" +run "echo ' runlevel = \"2345\"' >> $FINIT_CONF" +run "echo ' kill = 20' >> $FINIT_CONF" +run "echo ' log = true' >> $FINIT_CONF" +# Exercises the cgroup translation path. Whether the group is really +# joined cannot be asserted here, cgroup_avail() is false inside the +# test namespace, so initctl reports no cgroup at all. +run "echo ' cgroup user {}' >> $FINIT_CONF" +run "echo ' command = \"service.sh\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" + +say 'Reload Finit' +run "initctl reload" + +retry 'assert_num_children 1 service.sh' +assert_desc "Test service" service.sh + +say 'Stop the service' +run "initctl stop service.sh" + +retry 'assert_num_children 0 service.sh' + +say 'Start the service again' +run "initctl start service.sh" + +retry 'assert_num_children 1 service.sh' + +say 'Both formats side by side: legacy file in finit.d/, block file in finit.conf' +run "echo 'task [2345] name:legacyside /bin/touch /tmp/legacyside -- Legacy side' > $FINIT_RCSD/legacy-side.conf" +run "initctl reload" + +retry 'assert_file_exists /tmp/legacyside' +retry 'assert_num_children 1 service.sh' +assert_desc "Legacy side" legacyside + +run "rm -f $FINIT_RCSD/legacy-side.conf /tmp/legacyside" +run "initctl reload" + +say 'Round-trip: switch to the legacy one-liner equivalent' +run "echo 'service [2345] name:service.sh kill:20 log service.sh -- Test service' > $FINIT_CONF" +run "initctl reload" + +retry 'assert_num_children 1 service.sh' +assert_desc "Test service" service.sh + +# A rejected file must not reach the legacy parser, which registers a +# bogus unstartable service per line. assert_num_children cannot see +# that, the bogus service has no children either. +say 'Typo inside a block must be rejected, not fed to legacy parser' +write_svc descriptoin +run "initctl reload" + +retry 'assert_num_children 0 service.sh' +assert_num_services 0 service.sh + +say 'Typo at root level must be rejected too, same as inside a block' +write_svc description 'hostnam = \"typo\"\n' +run "initctl reload" + +retry 'assert_num_children 0 service.sh' +assert_num_services 0 service.sh + +say 'A clean new-format file still loads after the rejected ones' +write_svc description +run "initctl reload" + +retry 'assert_num_children 1 service.sh' +assert_desc "Test service" service.sh diff --git a/test/lib/setup.sh b/test/lib/setup.sh index fb3334ba..1512a09b 100755 --- a/test/lib/setup.sh +++ b/test/lib/setup.sh @@ -26,6 +26,11 @@ assert_file_contains() assert "File $1 contains the string $2" "$(texec grep "$2" "$1")" } +assert_file_exists() +{ + assert "File $1 exists" "$(texec ls "$1")" +} + assert_num_children() { assert "$1 services are running" "$(texec pgrep -P 1 "$2" | wc -l)" -eq "$1"