From 3b866c95e06ab84b3698c8c683893259fda65a29 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 01:21:23 +0200 Subject: [PATCH] conf: add libconfuse block format alongside the one-liner format The one-liner format has grown crowded and very wide, and every new service option makes it worse. Add a second, block-based format, parsed with libconfuse: service sshd { description = "OpenSSH daemon" runlevel = "2345" command = "/usr/sbin/sshd -D $SSHD_OPTS" } Both formats keep the .conf extension and are detected per file by content. Try-parse strictly with libconfuse; on a parse error, re-parse leniently to tell a block file with a typo from a one-liner file. Only a one-liner file reaches the legacy parser, a typo is reported with its file and line. Each block is translated to the canonical one-liner and registered through the existing entry points, so the two formats cannot drift. The one-liner parser is frozen at the 4.x feature set, new options land only in the block schema. libconfuse 3.3 or later is required, CFGF_KEYSTRVAL does not exist before it. Covers service, task, run, sysv and tty blocks, the static directives, and the cgroup, rlimit, set and log blocks. Templating and the documentation rewrite are still to come. The regression test covers translation of a service block to the one-liner, a block-format /etc/finit.conf booting with set {} applied at bootstrap, both formats side by side, and rejection of a typo at block and at root level. A rejected file must not fall through to the legacy parser, which registers a bogus unstartable service per line. assert_num_children cannot see that, the bogus service has no children either, so the check is assert_num_services. Signed-off-by: Joachim Wiberg --- configure.ac | 4 + src/Makefile.am | 7 +- src/conf.c | 1430 +++++++++++++++++++++++++++++++++++++++++++ src/conf.h | 4 +- src/legacy.c | 536 +--------------- src/legacy.h | 56 ++ test/Makefile.am | 2 + test/conf-format.sh | 133 ++++ test/lib/setup.sh | 5 + 9 files changed, 1649 insertions(+), 528 deletions(-) create mode 100644 src/conf.c create mode 100644 src/legacy.h create mode 100755 test/conf-format.sh 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"