From ad750a13c3c2eb6e0df5233a7a635dc0f0fadbaf Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 30 Jul 2026 15:21:05 +0200 Subject: [PATCH 01/28] Bump version for 5.0 release cycle Signed-off-by: Joachim Wiberg --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 7b0943ac..83346228 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -AC_INIT([Finit], [4.17], [https://github.com/finit-project/finit/issues], +AC_INIT([Finit], [5.0-dev], [https://github.com/finit-project/finit/issues], [finit], [https://http://finit-project.github.io/]) AC_CONFIG_AUX_DIR(aux) AM_INIT_AUTOMAKE([1.11 foreign subdir-objects]) From ee5038e7aff42d3b600360b6d674c88d9d0a363e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 01:03:39 +0200 Subject: [PATCH 02/28] test: reap the watchdog's sleep in wdkill() Every test left a stray `sleep 300` behind, reparented to PID 1, where it lingered for up to five minutes after the test had finished. wdstart() runs the watchdog in a subshell, so $! is the pid of the subshell, not of the sleep it forks. wdkill() killed the subshell and orphaned the sleep. Kill the child first, killing the subshell puts the sleep beyond the reach of pkill -P. Neither kill is sure to match, and wdkill() runs from the EXIT trap under set -e, so both must tolerate failure. Also return early when wdpid is unset, for failures before wdstart() runs. Signed-off-by: Joachim Wiberg --- test/lib/setup.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/lib/setup.sh b/test/lib/setup.sh index 09a57fd1..fb3334ba 100755 --- a/test/lib/setup.sh +++ b/test/lib/setup.sh @@ -255,8 +255,12 @@ wdstart() wdkill() { + [ -n "${wdpid:-}" ] || return 0 + say "Stopping test watchdog, pid $wdpid" - kill -KILL $wdpid + # Reap the sleep first, killing its subshell orphans it to PID 1. + pkill -KILL -P "$wdpid" 2>/dev/null || true + kill -KILL "$wdpid" 2>/dev/null || true } teardown() From fc74f8798684a3ee5f6a12e123c00cadecff2263 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 01:20:25 +0200 Subject: [PATCH 03/28] conf: rename conf.c to legacy.c The one-liner parser is about to be joined by a second, block-based format. Give it a name that says which of the two it implements, before any content changes make the diff hard to follow. No functional change. Signed-off-by: Joachim Wiberg --- src/Makefile.am | 2 +- src/{conf.c => legacy.c} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{conf.c => legacy.c} (100%) diff --git a/src/Makefile.am b/src/Makefile.am index bd126600..93f6e300 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -58,7 +58,7 @@ finit_SOURCES = api.c cgroup.c cgroup.h \ client.c client.h \ clone3.c clone3.h \ cond.c cond-w.c cond.h \ - conf.c conf.h \ + legacy.c conf.h \ devmon.c devmon.h \ exec.c finit.c finit.h \ stty.c \ diff --git a/src/conf.c b/src/legacy.c similarity index 100% rename from src/conf.c rename to src/legacy.c From 3b866c95e06ab84b3698c8c683893259fda65a29 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 01:21:23 +0200 Subject: [PATCH 04/28] 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" From c20d64587cf468664bb8544ae29ab7bad371b8ec Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 01:53:32 +0200 Subject: [PATCH 05/28] conf: relocate global state from legacy parser Relocate process-wide global variables from legacy parser that ended up there because it used to be conf.c, but which is now now frozen at the 4.x feature set. Each variable is moved to their respective "owner". Give cgroup_current[] and cgroup_settings_current[] named bounds. Their extern declarations were unsized, so sizeof() on them stopped compiling once the definitions moved to another translation unit. Signed-off-by: Joachim Wiberg --- src/conf.c | 31 +++++++++++++++++++++++++++++ src/conf.h | 7 +++++-- src/finit.c | 19 ++++++++++++++++++ src/legacy.c | 55 +-------------------------------------------------- src/service.c | 2 ++ 5 files changed, 58 insertions(+), 56 deletions(-) diff --git a/src/conf.c b/src/conf.c index 3bf9f4e5..8699c628 100644 --- a/src/conf.c +++ b/src/conf.c @@ -68,6 +68,37 @@ #define BOOTSTRAP (runlevel == INIT_LEVEL) +int runlevel = INIT_LEVEL; /* Bootstrap 'S' */ +int cfglevel = RUNLEVEL; /* Fallback if no configured runlevel */ +int cmdlevel = 0; /* runlevel override from cmdline */ +int prevlevel = -1; +int debug = 0; /* debug mode from kernel cmdline */ +int rescue = 0; /* rescue mode from kernel cmdline */ +int single = 0; /* single user mode from kernel cmdline */ +int bootstrap = 1; /* set while bootstrapping (for TTYs) */ +int kerndebug = 0; /* set if /proc/sys/kernel/printk > 7 */ +int syncsec = 0; /* reboot delay */ +int wdtreboot = 0; /* reboot via watchdog, default: SOC */ +int readiness = SVC_NOTIFY_PID; +char *finit_conf= NULL; +char *finit_rcsd= NULL; +char *fstab = NULL; +char *sdown = NULL; +char *network = NULL; +char *hostname = NULL; +char *osheading = NULL; + +char *runparts = NULL; +int runparts_progress; +int runparts_sysv; + +char cgroup_current[CGROUP_NAME_SIZE]; /* cgroup.NAME sets current cgroup for a set of services */ +char cgroup_settings_current[CGROUP_SETTINGS_SIZE]; /* cgroup.system,cpu.weight:500 - cgroup settings */ +int cgroup_delegate_current; /* cgroup.system,delegate - delegation flag */ + +struct rlimit initial_rlimit[RLIMIT_NLIMITS]; +struct rlimit global_rlimit[RLIMIT_NLIMITS]; + struct conf_change { TAILQ_ENTRY(conf_change) link; char *name; diff --git a/src/conf.h b/src/conf.h index 6141dd21..80491e7b 100644 --- a/src/conf.h +++ b/src/conf.h @@ -49,9 +49,12 @@ extern char *osheading; extern int logfile_size_max; extern int logfile_count_max; +#define CGROUP_NAME_SIZE 16 +#define CGROUP_SETTINGS_SIZE 128 + extern struct rlimit global_rlimit[RLIMIT_NLIMITS]; -extern char cgroup_current[]; -extern char cgroup_settings_current[]; +extern char cgroup_current[CGROUP_NAME_SIZE]; +extern char cgroup_settings_current[CGROUP_SETTINGS_SIZE]; extern int cgroup_delegate_current; int str2rlim(char *str); diff --git a/src/finit.c b/src/finit.c index 63df48a1..96061dd7 100644 --- a/src/finit.c +++ b/src/finit.c @@ -65,6 +65,25 @@ uev_ctx_t *ctx; /* Main loop context */ svc_t *wdog; /* No watchdog by default */ char *arg0; /* Saved for setprocnm() */ +/* + * --enable-fastboot => fsck_mode: NULL => no fsck by default + * --enable-fsckfix => fsck_mode: "-f" + fsck_repair: "y" + */ +#ifdef FSCK_FIX +# ifdef FAST_BOOT +char *fsck_mode = NULL; +# else +char *fsck_mode = "-f"; +# endif +char *fsck_repair = "-y"; +#else +# ifdef FAST_BOOT +char *fsck_mode = NULL; +# else +char *fsck_mode = ""; +# endif +char *fsck_repair = "-p"; +#endif /* * Show user configured banner before service bootstrap progress diff --git a/src/legacy.c b/src/legacy.c index 1feb5ca3..d32c9e16 100644 --- a/src/legacy.c +++ b/src/legacy.c @@ -41,6 +41,7 @@ #endif #include "finit.h" #include "cond.h" +#include "conf.h" #include "devmon.h" #include "legacy.h" #include "private.h" @@ -51,66 +52,12 @@ #define BOOTSTRAP (runlevel == INIT_LEVEL) -int runlevel = INIT_LEVEL; /* Bootstrap 'S' */ -int cfglevel = RUNLEVEL; /* Fallback if no configured runlevel */ -int cmdlevel = 0; /* runlevel override from cmdline */ -int prevlevel = -1; -int debug = 0; /* debug mode from kernel cmdline */ -int rescue = 0; /* rescue mode from kernel cmdline */ -int single = 0; /* single user mode from kernel cmdline */ -int bootstrap = 1; /* set while bootstrapping (for TTYs) */ -int kerndebug = 0; /* set if /proc/sys/kernel/printk > 7 */ -int syncsec = 0; /* reboot delay */ -int wdtreboot = 0; /* reboot via watchdog, default: SOC */ -int readiness = SVC_NOTIFY_PID; -char *finit_conf= NULL; -char *finit_rcsd= NULL; -char *fstab = NULL; -char *sdown = NULL; -char *network = NULL; -char *hostname = NULL; -char *osheading = NULL; - -int logfile_size_max = 200000; /* 200 kB */ -int logfile_count_max = 5; - struct env_entry { TAILQ_ENTRY(env_entry) link; char *name; }; static TAILQ_HEAD(, env_entry) env_list = TAILQ_HEAD_INITIALIZER(env_list); -struct rlimit initial_rlimit[RLIMIT_NLIMITS]; -struct rlimit global_rlimit[RLIMIT_NLIMITS]; - -/* - * --enable-fastboot => fsck_mode: NULL => no fsck by default - * --enable-fsckfix => fsck_mode: "-f" + fsck_repair: "y" - */ -#ifdef FSCK_FIX -# ifdef FAST_BOOT -char *fsck_mode = NULL; -# else -char *fsck_mode = "-f"; -# endif -char *fsck_repair = "-y"; -#else -# ifdef FAST_BOOT -char *fsck_mode = NULL; -# else -char *fsck_mode = ""; -# endif -char *fsck_repair = "-p"; -#endif - -char *runparts = NULL; -int runparts_progress; -int runparts_sysv; - -char cgroup_current[16]; /* cgroup.NAME sets current cgroup for a set of services */ -char cgroup_settings_current[128]; /* cgroup.system,cpu.weight:500 - cgroup settings */ -int cgroup_delegate_current; /* cgroup.system,delegate - delegation flag */ - static int get_bool(char *arg, int default_value) { if (!arg) diff --git a/src/service.c b/src/service.c index 127e0099..1c51cdc0 100644 --- a/src/service.c +++ b/src/service.c @@ -66,6 +66,8 @@ #define NOTIFY_PATH "@run/finit/notify/%d" +int logfile_size_max = 200000; /* 200 kB */ +int logfile_count_max = 5; /* * run tasks block other tasks/services from starting, we track the From 47a18140c1804cba939f311d70a9c4733cfd92c9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 11:50:35 +0200 Subject: [PATCH 06/28] conf: cleanup and relocation of common parsing functions and helpers - legacy.[ch]: strictly legacy .conf parser boilerplate only - conf.[ch]: .conf parser and generic configuration functionality Signed-off-by: Joachim Wiberg --- src/cgroup.h | 3 + src/conf.c | 661 +++++++++++++++++++++++++++++++++++++++++- src/conf.h | 52 ++-- src/helpers.c | 46 +++ src/helpers.h | 14 + src/legacy.c | 777 +------------------------------------------------- src/legacy.h | 23 +- src/util.c | 75 ++++- src/util.h | 6 + 9 files changed, 831 insertions(+), 826 deletions(-) diff --git a/src/cgroup.h b/src/cgroup.h index 7c792bde..b7b1c66d 100644 --- a/src/cgroup.h +++ b/src/cgroup.h @@ -26,6 +26,9 @@ #include +#define CGROUP_NAME_SIZE 16 +#define CGROUP_SETTINGS_SIZE 128 + /* Forward declaration */ typedef struct svc svc_t; diff --git a/src/conf.c b/src/conf.c index 8699c628..34d87458 100644 --- a/src/conf.c +++ b/src/conf.c @@ -58,7 +58,9 @@ #include #include "finit.h" +#include "cond.h" #include "conf.h" +#include "devmon.h" #include "iwatch.h" #include "legacy.h" #include "private.h" @@ -66,8 +68,6 @@ #include "helpers.h" #include "util.h" -#define BOOTSTRAP (runlevel == INIT_LEVEL) - int runlevel = INIT_LEVEL; /* Bootstrap 'S' */ int cfglevel = RUNLEVEL; /* Fallback if no configured runlevel */ int cmdlevel = 0; /* runlevel override from cmdline */ @@ -104,14 +104,19 @@ struct conf_change { char *name; }; +struct env_entry { + TAILQ_ENTRY(env_entry) link; + char *name; +}; + +static char cfg_errmsg[256]; static struct iwatch iw_conf; static int iwatch_fd; static uev_t etcw; +static TAILQ_HEAD(, env_entry) env_list = TAILQ_HEAD_INITIALIZER(env_list); static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list); -static void drop_changes(void); - /* * libconfuse schema -- the new block format * @@ -288,14 +293,650 @@ static cfg_opt_t conf_opts[] = { CFG_END() }; +static void drop_changes(void); + + +static int validate_arg(const char *arg, const char *opt) +{ + if (!arg) { + errx(1, "option %s missing argument, skipping.", opt); + return 1; + } + + return 0; +} + +/* + * finit.cond = foo (=> ) + * finit.config = /path/to/etc/alt-finit.conf + * finit.debug = [on,off] + * finit.fstab = /path/to/etc/fstab.aternative + * finit.status = [on,off] (compat finit.show_status) + * finit.status_style = [old,classic,modern] + */ +static void parse_finit_opts(char *opt) +{ + char *arg; + + arg = strchr(opt, '='); + if (arg) + *arg++ = 0; + + if (string_compare(opt, "cond")) { + if (validate_arg(arg, "finit.cond")) + return; + cond_boot_parse(arg); + return; + } + + if (string_compare(opt, "config")) { + if (validate_arg(arg, "finit.config")) + return; + if (finit_conf) + free(finit_conf); + finit_conf = strdup(arg); + return; + } + + if (string_compare(opt, "debug")) { + debug = istrue(arg, 1); + return; + } + + if (string_compare(opt, "fstab")) { + if (validate_arg(arg, "finit.fstab")) + return; + if (fstab) + free(fstab); + fstab = strdup(arg); + return; + } + + if (string_compare(opt, "status_style")) { + if (validate_arg(arg, "finit.status_style")) + return; + + if (string_compare(arg, "old") || string_compare(arg, "classic")) + show_progress(PROGRESS_CLASSIC); + else + show_progress(PROGRESS_MODERN); + return; + } + + if (string_compare(opt, "status") || string_compare(opt, "show_status")) { + show_progress(istrue(arg, 1) ? PROGRESS_DEFAULT : PROGRESS_SILENT); + return; + } +} + +static void parse_fsck_opts(char *opt) +{ + char *arg; + + arg = strchr(opt, '='); + if (arg) + *arg++ = 0; + + if (string_compare(opt, "mode")) { + if (validate_arg(arg, "fsck.mode")) + return; + + if (!strcmp(arg, "skip")) + fsck_mode = NULL; + if (!strcmp(arg, "auto")) + fsck_mode = ""; + if (!strcmp(arg, "force")) + fsck_mode = "-f"; + + return; + } + + if (string_compare(opt, "repair")) { + if (validate_arg(arg, "fsck.repair")) + return; + + if (!strcmp(arg, "no")) + fsck_repair = "-n"; + if (!strcmp(arg, "preen")) + fsck_repair = "-p"; + if (!strcmp(arg, "yes")) + fsck_repair = "-y"; + + return; + } +} + +/* + * When runlevel (single integer) is given on the command line, + * it overrides the runlevel in finit.conf and the built-in + * default (from configure). It do however have to pass the + * same sanity checks. + */ +static int parse_runlevel(char *arg) +{ + const char *err = NULL; + char *ptr = arg; + long long num; + + /* Sanity check the token is just digit(s) */ + while (*ptr) { + if (!isdigit(*ptr++)) + return 0; + } + + num = strtonum(arg, 1, 9, &err); + if (err || num == 6) { + dbg("Not a valid runlevel (%s), valid levels are [1-9], excluding 6, skipping.", arg); + return 0; + } + + return (int)num; +} + +static void parse_arg(char *arg) +{ + if (!strncmp(arg, "finit.", 6)) { + parse_finit_opts(&arg[6]); + return; + } + + if (!strncmp(arg, "fsck.", 5)) { + parse_fsck_opts(&arg[5]); + return; + } + +#ifdef RESCUE_MODE + if (string_compare(arg, "rescue") || string_compare(arg, "recover")) { + rescue = 1; + return; + } +#endif + + if (string_compare(arg, "single") || string_compare(arg, "S")) { + single = 1; + return; + } + + /* Put any new command line options before this line. */ + + cmdlevel = parse_runlevel(arg); +} + +#ifdef KERNEL_CMDLINE +/* + * Parse /proc/cmdline to find args for init. Don't use this! + * + * Instead, rely on the kernel to give Finit its arguments as + * regular argc + argv[]. Only use this if the system you run + * on has a broken initramfs system that cannot forward args + * to Finit properly. + */ +static void parse_kernel_cmdline(void) +{ + char line[LINE_SIZE], *cmdline, *tok; + FILE *fp; + + fp = fopen("/proc/cmdline", "r"); + if (!fp) + return; + + if (!fgets(line, sizeof(line), fp)) { + fclose(fp); + return; + } + + cmdline = chomp(line); + dbg("%s", cmdline); + + while ((tok = strtok(cmdline, " \t"))) { + cmdline = NULL; + parse_arg(tok); + } + fclose(fp); +} +#else +#define parse_kernel_cmdline() +#endif + +static void parse_kernel_loglevel(void) +{ + char line[LINE_SIZE], *ptr; + FILE *fp; + int val; + + fp = fopen("/proc/sys/kernel/printk", "r"); + if (!fp) + return; + + if (!fgets(line, sizeof(line), fp)) { + fclose(fp); + return; + } + fclose(fp); + + ptr = chomp(line); + dbg("%s", ptr); + val = atoi(ptr); + if (val >= 7) + kerndebug = 1; +} + +/* + * Kernel gives us all non-kernel options on our cmdline + */ +void conf_parse_cmdline(int argc, char *argv[]) +{ + const char *ptr; + + /* Set up defaults */ + if ((ptr = FINIT_FSTAB)) + fstab = strdup(ptr); + finit_conf = strdup(FINIT_CONF); + finit_rcsd = strdup(FINIT_RCSD); + + for (int i = 1; i < argc; i++) + parse_arg(argv[i]); + + parse_kernel_cmdline(); + parse_kernel_loglevel(); +} + +/* + * Sourced mainly by initctl and other Finit helper tools + */ +void conf_saverc(void) +{ + FILE *fp; + + mkpath(_PATH_VARRUN "finit", 0755); + fp = fopen(_PATH_VARRUN "finit/.initrc", "w"); + if (!fp) { + err(1, "failed creating .finitrc"); + return; + } + + fprintf(fp, "FINIT_CONF=%s\n", finit_conf); + fprintf(fp, "FINIT_RCSD=%s\n", finit_rcsd); + fprintf(fp, "FINIT_CGPATH=%s\n", FINIT_CGPATH); + fprintf(fp, "INIT_SOCKET=%s\n", INIT_SOCKET); + fprintf(fp, "INIT_MAGIC=0x%08x\n", INIT_MAGIC); + + fclose(fp); +} + +/* + * Called at bootstrap to log execution order (for debug) + */ +void conf_save_exec_order(svc_t *svc, char *cmdline, int result) +{ + const char *fn = _PATH_VARRUN "finit/exec.order"; + const char *ststr = !result ? "[ OK ]" : "[FAIL]"; + static char *prepared = NULL; + int first = !fexist(fn); + FILE *fp; + + fp = fopen(fn, "a+"); + if (!fp) { + err(1, "failed writing to %s", fn); + return; + } + + if (first) { + fprintf(fp, "# Execution order of run/task/services at bootstrap\n"); + fprintf(fp, "# ST TYPE COMMAND LINE (DESC)\n"); + } + + if (result == -1) { + int len; + + len = snprintf(NULL, 0, " %-7s %-64s (%s)", svc_typestr(svc), cmdline, svc->desc); + if (len > 0) { + if (prepared) + free(prepared); + prepared = malloc(++len); + if (!prepared) + goto done; + snprintf(prepared, len, " %-7s %-64s (%s)", svc_typestr(svc), cmdline, svc->desc); + } + } else { + if (prepared) { + memcpy(prepared, ststr, strlen(ststr)); + fprintf(fp, "%s\n", prepared); + free(prepared); + prepared = NULL; + } else { + fprintf(fp, "%s %-7s %-64s (%s)\n", ststr, svc_typestr(svc), cmdline, svc->desc); + } + } +done: + fclose(fp); +} + +/* + * Called by plugins and similar that dynamically generate system services + */ +void conf_save_service(int type, char *cfg, char *file) +{ + char fn[256]; + svc_t foo; + FILE *fp; + + mkpath(FINIT_RUNPATH_, 0755); + snprintf(fn, sizeof(fn), "%s/%s", FINIT_RUNPATH_, file); + if (fexist(fn)) + warnx("File %s already exists, overwriting.", fn); + fp = fopen(fn, "w"); + if (!fp) { + err(1, "Failed creating %s", fn); + return; + } + + foo.type = type; + fprintf(fp, "# Generated by finit:%s()\n", __func__); + fprintf(fp, "%s %s\n", svc_typestr(&foo), cfg); + fclose(fp); +} + +/* + * Clear all environment variables read in parse_env(), they may be + * removed now so let the next call to parse_env() restore them. + */ +void conf_reset_env(void) +{ + struct env_entry *node, *tmp; + + TAILQ_FOREACH_SAFE(node, &env_list, link, tmp) { + TAILQ_REMOVE(&env_list, node, link); + if (node->name) { + unsetenv(node->name); + free(node->name); + } + free(node); + } + + if (!getenv("PATH")) + setenv("PATH", _PATH_STDPATH, 1); + if (!getenv("SHELL")) + setenv("SHELL", _PATH_BSHELL, 1); + setenv("LOGNAME", "root", 1); + setenv("USER", "root", 1); +} + +/* + * Sets, and makes a note of, all KEY=VALUE lines in a given .conf line + * from finit.conf, or other .conf file. Note, PATH is always reset in + * the conf_reset_env() function. + */ +void conf_set_env(char *line) +{ + struct env_entry *node; + char *key, *val; + + key = conf_parse_env(line, &val); + if (!key) + return; + + dbg("Global env '%s'='%s'", key, val); + setenv(key, val, 1); + + node = malloc(sizeof(*node)); + if (!node) { + nomem: + err(1, "Out of memory cannot track env vars"); + return; + } + + node->name = strdup(key); + if (!node->name) { + free(node); + goto nomem; + } + + TAILQ_INSERT_HEAD(&env_list, node, link); +} + +/** + * conf_parse_env - Parse a key=value line + * @line: Line buffer without newline + * @value: Whitespace trimmed value + * + * Used by service.c:source_env() and + * + * Returns: + * %NULL on error, otherwise a whitespace trimmed key. + */ +char *conf_parse_env(char *line, char **value) +{ + char *key, *val, *end; + + /* skip any leading whitespace */ + key = line; + while (isspace(*key)) + key++; + + /* find end of line */ + end = key; + while (*end) + end++; + + /* strip trailing whitespace */ + if (end > key) { + end--; + while (isspace(*end)) + *end-- = 0; + } + + val = strchr(key, '='); + if (!val) + return NULL; + *val++ = 0; + + /* strip leading whitespace from value */ + while (isspace(*val)) + val++; + + /* unquote value, if quoted */ + unquote(&val, end); + *value = val; + + /* find end of key */ + end = key; + while (*end) + end++; + + /* strip trailing whitespace */ + if (end > key) { + end--; + while (isspace(*end)) + *end-- = 0; + } + + /* strip any leading 'set ' */ + end = key; + if (!strncmp(key, "set", 3)) + end += 3; + + /* check key, no spaces allowed */ + while (*end && isspace(*end)) + end++; + key = end; + while (*end && !isspace(*end)) + end++; + if (*end != 0) { + warnx("'%s=%s': not a valid identifier", key, val); + return NULL; /* invalid key */ + } + + return key; +} + +/* First form: `rlimit RESOURCE LIMIT` */ +void conf_parse_rlimit(char *line, struct rlimit arr[]) +{ + char *level, *limit, *val; + int resource = -1; + rlim_t cfg; + + level = strtok(line, " \t"); + if (!level) + goto error; + + limit = strtok(NULL, " \t"); + if (!limit) + goto error; + + val = strtok(NULL, " \t"); + if (!val) { + /* Second form: `rlimit RESOURCE LIMIT` */ + val = limit; + limit = level; + level = "both"; + } + + resource = str2rlim(limit); + if (resource < 0 || resource > RLIMIT_NLIMITS) + goto error; + + /* Official keyword from v3.1 is `unlimited`, from prlimit(1) */ + if (!strcmp(val, "unlimited") || !strcmp(val, "infinity")) { + cfg = RLIM_INFINITY; + } else { + const char *err = NULL; + + cfg = strtonum(val, 0, (long long)2 << 31, &err); + if (err) { + logit(LOG_WARNING, "rlimit: invalid %s value: %s", + rlim2str(resource), val); + return; + } + } + + if (!strcmp(level, "soft")) + arr[resource].rlim_cur = cfg; + else if (!strcmp(level, "hard")) + arr[resource].rlim_max = cfg; + else if (!strcmp(level, "both")) + arr[resource].rlim_max = arr[resource].rlim_cur = cfg; + else + goto error; + + return; +error: + logit(LOG_WARNING, "rlimit: parse error"); +} + +/* Convert optional "[!123456789S]" string into a bitmask */ +int conf_parse_runlevels(const char *runlevels) +{ + int i, not = 0, bitmask = 0; + + if (!runlevels) + runlevels = "[234]"; + i = 1; + while (i) { + int level; + char lvl = runlevels[i++]; + + if (']' == lvl || 0 == lvl) + break; + if ('!' == lvl) { + not = 1; + bitmask = 0x7FE; + continue; + } + + if ('s' == lvl || 'S' == lvl) + level = INIT_LEVEL; + else + level = lvl - '0'; + + if (level > INIT_LEVEL || level < 0) + continue; + + if (not) + CLRBIT(bitmask, level); + else + SETBIT(bitmask, level); + } + + return bitmask; +} + +void conf_parse_cond(svc_t *svc, char *cond) +{ + size_t i = 0; + char *ptr; + char *c; + + if (!svc) { + errx(1, "Invalid service pointer"); + return; + } + + /* By default we assume UNIX daemons support SIGHUP */ + if (svc_is_daemon(svc)) + svc->sighup = 1; + + if (!cond) { + memset(svc->cond, 0, sizeof(svc->cond)); + return; + } + + /* + * First character must be '!' if: + * - service: SIGHUP is not supported + * - run/task: Do not block bootstrap + */ + ptr = cond; + if (ptr[i] == '!') { + ptr++; + + if (svc_is_runtask(svc)) { + /* see service_runtask_clean() */ + svc->sighup = 1; + svc->once = 1; + } else { + svc->sighup = 0; + } + } + + while (ptr[i] != '>' && ptr[i] != 0) + i++; + ptr[i] = 0; + + if (i >= sizeof(svc->cond)) { + logit(LOG_WARNING, "%s: too long list of conditions: %s", svc_ident(svc, NULL, 0), ptr); + return; + } + + /* + * The '~' prefix means a reload of the upstream service is + * propagated to this service -- it will be reloaded (SIGHUP) + * or restarted (noreload), not just paused and resumed. + * Syntax: or <~pid/foo> + * + * Strip '~' from each condition and set the service-level + * flag. This allows '~' on any condition in the list. + */ + svc->cond[0] = 0; + for (i = 0, c = strtok(ptr, ","); c; c = strtok(NULL, ","), i++) { + if (c[0] == '~') { + c++; + svc->flux_reload = 1; + } + devmon_add_cond(c); + if (i) + strlcat(svc->cond, ",", sizeof(svc->cond)); + strlcat(svc->cond, c, sizeof(svc->cond)); + } +} + /* * Try-parse error capture. Diagnostics are buffered, not printed: * on fallback to the legacy parser they are only debug logged. The * last message wins -- the parser stops at the first fatal error, so * the last callback before failure is the fatal one. */ -static char cfg_errmsg[256]; - static void cfg_error_cb(cfg_t *cfg, const char *fmt, va_list ap) { char msg[128]; @@ -669,7 +1310,7 @@ static void env_translate(cfg_t *cfg, const char *section) snprintf(buf, sizeof(buf), "%s=%s", opt->name, cfg_opt_getnstr(opt, 0)); - legacy_parse_env(buf); + conf_set_env(buf); } } @@ -780,9 +1421,11 @@ static void conf_parse_statics(cfg_t *cfg) if (cfg_size(cfg, "service-interval")) { long val = cfg_getint(cfg, "service-interval"); + /* 0 min to 1 day, should check at least daily */ if (val >= 0 && val <= 1440) { int disabled = !service_interval; + /* milliseconds for libuEv timer */ service_interval = (int)val * 1000; if (disabled) service_init(NULL); @@ -913,7 +1556,9 @@ static int is_new_format(char *file) } /* - * Parse one Finit .conf file, in either format, see top of file. + * Parse one Finit .conf file, in either format. The legacy .conf + * include directive routes back through this so included files are + * format-detected too. */ int conf_parse_file(char *file, int is_rcsd) { diff --git a/src/conf.h b/src/conf.h index 80491e7b..b2576ff8 100644 --- a/src/conf.h +++ b/src/conf.h @@ -27,6 +27,10 @@ #include "cgroup.h" #include "svc.h" +#include + +#define BOOTSTRAP (runlevel == INIT_LEVEL) + extern int runlevel; extern int cfglevel; extern int cmdlevel; @@ -46,34 +50,38 @@ extern char *hostname; extern char *runparts; extern char *osheading; -extern int logfile_size_max; -extern int logfile_count_max; +extern int runparts_progress; +extern int runparts_sysv; -#define CGROUP_NAME_SIZE 16 -#define CGROUP_SETTINGS_SIZE 128 +extern int logfile_size_max; +extern int logfile_count_max; +extern char cgroup_current[CGROUP_NAME_SIZE]; +extern char cgroup_settings_current[CGROUP_SETTINGS_SIZE]; +extern int cgroup_delegate_current; + +extern struct rlimit initial_rlimit[RLIMIT_NLIMITS]; extern struct rlimit global_rlimit[RLIMIT_NLIMITS]; -extern char cgroup_current[CGROUP_NAME_SIZE]; -extern char cgroup_settings_current[CGROUP_SETTINGS_SIZE]; -extern int cgroup_delegate_current; -int str2rlim(char *str); -char *rlim2str(int rlim); +int conf_init (uev_ctx_t *ctx); +int conf_reload (void); +int conf_any_change (void); +int conf_changed (char *file); +int conf_monitor (void); + +void conf_saverc (void); +void conf_save_exec_order (svc_t *svc, char *cmdline, int result); +void conf_save_service (int type, char *cfg, char *file); +void conf_parse_cmdline (int argc, char *argv[]); -int conf_init (uev_ctx_t *ctx); -int conf_reload (void); -int conf_any_change (void); -int conf_changed (char *file); -int conf_monitor (void); +void conf_reset_env (void); +void conf_set_env (char *line); +char *conf_parse_env (char *line, char **val); -void conf_reset_env (void); -void conf_saverc (void); -void conf_save_exec_order (svc_t *svc, char *cmdline, int result); -void conf_save_service (int type, char *cfg, char *file); -void conf_parse_cmdline (int argc, char *argv[]); -char *conf_parse_env (char *line, char **val); -int conf_parse_runlevels (char *runlevels); -void conf_parse_cond (svc_t *svc, char *cond); +void conf_parse_rlimit (char *line, struct rlimit arr[]); +int conf_parse_runlevels (const char *runlevels); +void conf_parse_cond (svc_t *svc, char *cond); +int conf_parse_file (char *file, int is_rcsd); #endif /* FINIT_CONF_H_ */ diff --git a/src/helpers.c b/src/helpers.c index 42a03f89..3b81c1d2 100644 --- a/src/helpers.c +++ b/src/helpers.c @@ -470,6 +470,52 @@ void set_hostname(char **hostname) } } +static int kmod_exists(const char *mod) +{ + char buf[256]; + int found = 0; + FILE *fp; + + fp = fopen("/proc/modules", "r"); + if (!fp) + return 0; + + while (!found && fgets(buf, sizeof(buf), fp)) { + const char *kmod = strtok(buf, " \t"); + + if (kmod && !strcmp(kmod, mod)) + found = 1; + } + fclose(fp); + + return found; +} + +/* + * Load a kernel module during bootstrap + */ +void kmod_load(char *mod) +{ + char module[64] = { 0 }; + char cmd[CMD_SIZE]; + + if (runlevel != INIT_LEVEL) + return; + + /* Strip args for progress below and kmod_exists() */ + strlcpy(module, mod, sizeof(module)); + if (!strtok(module, " \t")) + return; + + if (kmod_exists(module)) + return; + + strcpy(cmd, "modprobe "); + strlcat(cmd, mod, sizeof(cmd)); + + run_interactive(cmd, "Loading kernel module %s", module); +} + /* * Bring up networking, but only if not single-user or rescue mode */ diff --git a/src/helpers.h b/src/helpers.h index 442d75dc..babff4f0 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -71,6 +71,7 @@ int print_result (int fail); void print_exit (void); void set_hostname (char **hostname); +void kmod_load (char *mod); void networking (int updown); int in_container (void); void setprocnm (const char *name); @@ -113,6 +114,19 @@ static inline int dprint(int fd, const char *s, size_t len) return rc; } +static inline int istrue(char *arg, int defval) +{ + if (!arg) + goto fallback; + + if (string_compare(arg, "true") || string_compare(arg, "on") || string_compare(arg, "1")) + return 1; + if (string_compare(arg, "false") || string_compare(arg, "off") || string_compare(arg, "0")) + return 0; +fallback: + return defval; +} + static inline char *fgetval(const char *line, const char *key, char *sep) { char *ptr, *str, *copy; diff --git a/src/legacy.c b/src/legacy.c index d32c9e16..c817886e 100644 --- a/src/legacy.c +++ b/src/legacy.c @@ -40,9 +40,7 @@ # include /* BSD sys/queue.h API */ #endif #include "finit.h" -#include "cond.h" #include "conf.h" -#include "devmon.h" #include "legacy.h" #include "private.h" #include "service.h" @@ -50,775 +48,6 @@ #include "helpers.h" #include "util.h" -#define BOOTSTRAP (runlevel == INIT_LEVEL) - -struct env_entry { - TAILQ_ENTRY(env_entry) link; - char *name; -}; -static TAILQ_HEAD(, env_entry) env_list = TAILQ_HEAD_INITIALIZER(env_list); - -static int get_bool(char *arg, int default_value) -{ - if (!arg) - goto fallback; - - if (string_compare(arg, "true") || string_compare(arg, "on") || string_compare(arg, "1")) - return 1; - if (string_compare(arg, "false") || string_compare(arg, "off") || string_compare(arg, "0")) - return 0; -fallback: - return default_value; -} - -static int validate_arg(char *arg, const char *opt) -{ - if (!arg) { - errx(1, "option %s missing argument, skipping.", opt); - return 1; - } - - return 0; -} - -/* - * finit.cond = foo (=> ) - * finit.config = /path/to/etc/alt-finit.conf - * finit.debug = [on,off] - * finit.fstab = /path/to/etc/fstab.aternative - * finit.status = [on,off] (compat finit.show_status) - * finit.status_style = [old,classic,modern] - */ -static void parse_finit_opts(char *opt) -{ - char *arg; - - arg = strchr(opt, '='); - if (arg) - *arg++ = 0; - - if (string_compare(opt, "cond")) { - if (validate_arg(arg, "finit.cond")) - return; - cond_boot_parse(arg); - return; - } - - if (string_compare(opt, "config")) { - if (validate_arg(arg, "finit.config")) - return; - if (finit_conf) - free(finit_conf); - finit_conf = strdup(arg); - return; - } - - if (string_compare(opt, "debug")) { - debug = get_bool(arg, 1); - return; - } - - if (string_compare(opt, "fstab")) { - if (validate_arg(arg, "finit.fstab")) - return; - if (fstab) - free(fstab); - fstab = strdup(arg); - return; - } - - if (string_compare(opt, "status_style")) { - if (validate_arg(arg, "finit.status_style")) - return; - - if (string_compare(arg, "old") || string_compare(arg, "classic")) - show_progress(PROGRESS_CLASSIC); - else - show_progress(PROGRESS_MODERN); - return; - } - - if (string_compare(opt, "status") || string_compare(opt, "show_status")) { - show_progress(get_bool(arg, 1) ? PROGRESS_DEFAULT : PROGRESS_SILENT); - return; - } -} - -static void parse_fsck_opts(char *opt) -{ - char *arg; - - arg = strchr(opt, '='); - if (arg) - *arg++ = 0; - - if (string_compare(opt, "mode")) { - if (validate_arg(arg, "fsck.mode")) - return; - - if (!strcmp(arg, "skip")) - fsck_mode = NULL; - if (!strcmp(arg, "auto")) - fsck_mode = ""; - if (!strcmp(arg, "force")) - fsck_mode = "-f"; - - return; - } - - if (string_compare(opt, "repair")) { - if (validate_arg(arg, "fsck.repair")) - return; - - if (!strcmp(arg, "no")) - fsck_repair = "-n"; - if (!strcmp(arg, "preen")) - fsck_repair = "-p"; - if (!strcmp(arg, "yes")) - fsck_repair = "-y"; - - return; - } -} - -/* - * When runlevel (single integer) is given on the command line, - * it overrides the runlevel in finit.conf and the built-in - * default (from configure). It do however have to pass the - * same sanity checks. - */ -static int parse_runlevel(char *arg) -{ - const char *err = NULL; - char *ptr = arg; - long long num; - - /* Sanity check the token is just digit(s) */ - while (*ptr) { - if (!isdigit(*ptr++)) - return 0; - } - - num = strtonum(arg, 1, 9, &err); - if (err || num == 6) { - dbg("Not a valid runlevel (%s), valid levels are [1-9], excluding 6, skipping.", arg); - return 0; - } - - return (int)num; -} - -static void parse_arg(char *arg) -{ - if (!strncmp(arg, "finit.", 6)) { - parse_finit_opts(&arg[6]); - return; - } - - if (!strncmp(arg, "fsck.", 5)) { - parse_fsck_opts(&arg[5]); - return; - } - -#ifdef RESCUE_MODE - if (string_compare(arg, "rescue") || string_compare(arg, "recover")) { - rescue = 1; - return; - } -#endif - - if (string_compare(arg, "single") || string_compare(arg, "S")) { - single = 1; - return; - } - - /* Put any new command line options before this line. */ - - cmdlevel = parse_runlevel(arg); -} - -#ifdef KERNEL_CMDLINE -/* - * Parse /proc/cmdline to find args for init. Don't use this! - * - * Instead, rely on the kernel to give Finit its arguments as - * regular argc + argv[]. Only use this if the system you run - * on has a broken initramfs system that cannot forward args - * to Finit properly. - */ -static void parse_kernel_cmdline(void) -{ - char line[LINE_SIZE], *cmdline, *tok; - FILE *fp; - - fp = fopen("/proc/cmdline", "r"); - if (!fp) - return; - - if (!fgets(line, sizeof(line), fp)) { - fclose(fp); - return; - } - - cmdline = chomp(line); - dbg("%s", cmdline); - - while ((tok = strtok(cmdline, " \t"))) { - cmdline = NULL; - parse_arg(tok); - } - fclose(fp); -} -#else -#define parse_kernel_cmdline() -#endif - -static void parse_kernel_loglevel(void) -{ - char line[LINE_SIZE], *ptr; - FILE *fp; - int val; - - fp = fopen("/proc/sys/kernel/printk", "r"); - if (!fp) - return; - - if (!fgets(line, sizeof(line), fp)) { - fclose(fp); - return; - } - fclose(fp); - - ptr = chomp(line); - dbg("%s", ptr); - val = atoi(ptr); - if (val >= 7) - kerndebug = 1; -} - -/* - * Kernel gives us all non-kernel options on our cmdline - */ -void conf_parse_cmdline(int argc, char *argv[]) -{ - const char *ptr; - - /* Set up defaults */ - if ((ptr = FINIT_FSTAB)) - fstab = strdup(ptr); - finit_conf = strdup(FINIT_CONF); - finit_rcsd = strdup(FINIT_RCSD); - - for (int i = 1; i < argc; i++) - parse_arg(argv[i]); - - parse_kernel_cmdline(); - parse_kernel_loglevel(); -} - -/* - * Clear all environment variables read in parse_env(), they may be - * removed now so let the next call to parse_env() restore them. - */ -void conf_reset_env(void) -{ - struct env_entry *node, *tmp; - - TAILQ_FOREACH_SAFE(node, &env_list, link, tmp) { - TAILQ_REMOVE(&env_list, node, link); - if (node->name) { - unsetenv(node->name); - free(node->name); - } - free(node); - } - - if (!getenv("PATH")) - setenv("PATH", _PATH_STDPATH, 1); - if (!getenv("SHELL")) - setenv("SHELL", _PATH_BSHELL, 1); - setenv("LOGNAME", "root", 1); - setenv("USER", "root", 1); -} - -/* - * Sourced mainly by initctl and other Finit helper tools - */ -void conf_saverc(void) -{ - FILE *fp; - - mkpath(_PATH_VARRUN "finit", 0755); - fp = fopen(_PATH_VARRUN "finit/.initrc", "w"); - if (!fp) { - err(1, "failed creating .finitrc"); - return; - } - - fprintf(fp, "FINIT_CONF=%s\n", finit_conf); - fprintf(fp, "FINIT_RCSD=%s\n", finit_rcsd); - fprintf(fp, "FINIT_CGPATH=%s\n", FINIT_CGPATH); - fprintf(fp, "INIT_SOCKET=%s\n", INIT_SOCKET); - fprintf(fp, "INIT_MAGIC=0x%08x\n", INIT_MAGIC); - - fclose(fp); -} - -/* - * Called at bootstrap to log execution order (for debug) - */ -void conf_save_exec_order(svc_t *svc, char *cmdline, int result) -{ - const char *fn = _PATH_VARRUN "finit/exec.order"; - const char *ststr = !result ? "[ OK ]" : "[FAIL]"; - static char *prepared = NULL; - int first = !fexist(fn); - FILE *fp; - - fp = fopen(fn, "a+"); - if (!fp) { - err(1, "failed writing to %s", fn); - return; - } - - if (first) { - fprintf(fp, "# Execution order of run/task/services at bootstrap\n"); - fprintf(fp, "# ST TYPE COMMAND LINE (DESC)\n"); - } - - if (result == -1) { - int len; - - len = snprintf(NULL, 0, " %-7s %-64s (%s)", svc_typestr(svc), cmdline, svc->desc); - if (len > 0) { - if (prepared) - free(prepared); - prepared = malloc(++len); - if (!prepared) - goto done; - snprintf(prepared, len, " %-7s %-64s (%s)", svc_typestr(svc), cmdline, svc->desc); - } - } else { - if (prepared) { - memcpy(prepared, ststr, strlen(ststr)); - fprintf(fp, "%s\n", prepared); - free(prepared); - prepared = NULL; - } else { - fprintf(fp, "%s %-7s %-64s (%s)\n", ststr, svc_typestr(svc), cmdline, svc->desc); - } - } -done: - fclose(fp); -} - -/* - * Called by plugins and similar that dynamically generate system services - */ -void conf_save_service(int type, char *cfg, char *file) -{ - char fn[256]; - svc_t foo; - FILE *fp; - - mkpath(FINIT_RUNPATH_, 0755); - snprintf(fn, sizeof(fn), "%s/%s", FINIT_RUNPATH_, file); - if (fexist(fn)) - warnx("File %s already exists, overwriting.", fn); - fp = fopen(fn, "w"); - if (!fp) { - err(1, "Failed creating %s", fn); - return; - } - - foo.type = type; - fprintf(fp, "# Generated by finit:%s()\n", __func__); - fprintf(fp, "%s %s\n", svc_typestr(&foo), cfg); - fclose(fp); -} - -/** - * conf_parse_env - Parse a key=value line - * @line: Line buffer without newline - * @value: Whitespace trimmed value - * - * Returns: - * %NULL on error, otherwise a whitespace trimmed key. - */ -char *conf_parse_env(char *line, char **value) -{ - char *key, *val, *end; - - /* skip any leading whitespace */ - key = line; - while (isspace(*key)) - key++; - - /* find end of line */ - end = key; - while (*end) - end++; - - /* strip trailing whitespace */ - if (end > key) { - end--; - while (isspace(*end)) - *end-- = 0; - } - - val = strchr(key, '='); - if (!val) - return NULL; - *val++ = 0; - - /* strip leading whitespace from value */ - while (isspace(*val)) - val++; - - /* unquote value, if quoted */ - unquote(&val, end); - *value = val; - - /* find end of key */ - end = key; - while (*end) - end++; - - /* strip trailing whitespace */ - if (end > key) { - end--; - while (isspace(*end)) - *end-- = 0; - } - - /* strip any leading 'set ' */ - end = key; - if (!strncmp(key, "set", 3)) - end += 3; - - /* check key, no spaces allowed */ - while (*end && isspace(*end)) - end++; - key = end; - while (*end && !isspace(*end)) - end++; - if (*end != 0) { - warnx("'%s=%s': not a valid identifier", key, val); - return NULL; /* invalid key */ - } - - return key; -} - -/* - * Sets, and makes a note of, all KEY=VALUE lines in a given .conf line - * from finit.conf, or other .conf file. Note, PATH is always reset in - * the conf_reset_env() function. - */ -void legacy_parse_env(char *line) -{ - struct env_entry *node; - char *key, *val; - - key = conf_parse_env(line, &val); - if (!key) - return; - - dbg("Global env '%s'='%s'", key, val); - setenv(key, val, 1); - - node = malloc(sizeof(*node)); - if (!node) { - nomem: - err(1, "Out of memory cannot track env vars"); - return; - } - - node->name = strdup(key); - if (!node->name) { - free(node); - goto nomem; - } - - TAILQ_INSERT_HEAD(&env_list, node, link); -} - -static int kmod_exists(char *mod) -{ - char buf[256]; - int found = 0; - FILE *fp; - - fp = fopen("/proc/modules", "r"); - if (!fp) - return 0; - - while (!found && fgets(buf, sizeof(buf), fp)) { - char *kmod = strtok(buf, " \t"); - - if (kmod && !strcmp(kmod, mod)) - found = 1; - } - fclose(fp); - - return found; -} - -void kmod_load(char *mod) -{ - char module[64] = { 0 }; - char cmd[CMD_SIZE]; - - if (runlevel != INIT_LEVEL) - return; - - /* Strip args for progress below and kmod_exists() */ - strlcpy(module, mod, sizeof(module)); - if (!strtok(module, " \t")) - return; - - if (kmod_exists(module)) - return; - - strcpy(cmd, "modprobe "); - strlcat(cmd, mod, sizeof(cmd)); - - run_interactive(cmd, "Loading kernel module %s", module); -} - -/* Convert optional "[!123456789S]" string into a bitmask */ -int conf_parse_runlevels(char *runlevels) -{ - int i, not = 0, bitmask = 0; - - if (!runlevels) - runlevels = "[234]"; - i = 1; - while (i) { - int level; - char lvl = runlevels[i++]; - - if (']' == lvl || 0 == lvl) - break; - if ('!' == lvl) { - not = 1; - bitmask = 0x7FE; - continue; - } - - if ('s' == lvl || 'S' == lvl) - level = INIT_LEVEL; - else - level = lvl - '0'; - - if (level > INIT_LEVEL || level < 0) - continue; - - if (not) - CLRBIT(bitmask, level); - else - SETBIT(bitmask, level); - } - - return bitmask; -} - -void conf_parse_cond(svc_t *svc, char *cond) -{ - size_t i = 0; - char *ptr; - char *c; - - if (!svc) { - errx(1, "Invalid service pointer"); - return; - } - - /* By default we assume UNIX daemons support SIGHUP */ - if (svc_is_daemon(svc)) - svc->sighup = 1; - - if (!cond) { - memset(svc->cond, 0, sizeof(svc->cond)); - return; - } - - /* - * First character must be '!' if: - * - service: SIGHUP is not supported - * - run/task: Do not block bootstrap - */ - ptr = cond; - if (ptr[i] == '!') { - ptr++; - - if (svc_is_runtask(svc)) { - /* see service_runtask_clean() */ - svc->sighup = 1; - svc->once = 1; - } else { - svc->sighup = 0; - } - } - - while (ptr[i] != '>' && ptr[i] != 0) - i++; - ptr[i] = 0; - - if (i >= sizeof(svc->cond)) { - logit(LOG_WARNING, "%s: too long list of conditions: %s", svc_ident(svc, NULL, 0), ptr); - return; - } - - /* - * The '~' prefix means a reload of the upstream service is - * propagated to this service -- it will be reloaded (SIGHUP) - * or restarted (noreload), not just paused and resumed. - * Syntax: or <~pid/foo> - * - * Strip '~' from each condition and set the service-level - * flag. This allows '~' on any condition in the list. - */ - svc->cond[0] = 0; - for (i = 0, c = strtok(ptr, ","); c; c = strtok(NULL, ","), i++) { - if (c[0] == '~') { - c++; - svc->flux_reload = 1; - } - devmon_add_cond(c); - if (i) - strlcat(svc->cond, ",", sizeof(svc->cond)); - strlcat(svc->cond, c, sizeof(svc->cond)); - } -} - -struct rlimit_name { - char *name; - int val; -}; - -static const struct rlimit_name rlimit_names[] = { - { "as", RLIMIT_AS }, - { "core", RLIMIT_CORE }, - { "cpu", RLIMIT_CPU }, - { "data", RLIMIT_DATA }, - { "fsize", RLIMIT_FSIZE }, - { "locks", RLIMIT_LOCKS }, - { "memlock", RLIMIT_MEMLOCK }, - { "msgqueue", RLIMIT_MSGQUEUE }, - { "nice", RLIMIT_NICE }, - { "nofile", RLIMIT_NOFILE }, - { "nproc", RLIMIT_NPROC }, - { "rss", RLIMIT_RSS }, - { "rtprio", RLIMIT_RTPRIO }, -#ifdef RLIMIT_RTTIME - { "rttime", RLIMIT_RTTIME }, -#endif - { "sigpending", RLIMIT_SIGPENDING }, - { "stack", RLIMIT_STACK }, - - { NULL, 0 } -}; - -int str2rlim(char *str) -{ - const struct rlimit_name *rn; - - for (rn = rlimit_names; rn->name; rn++) { - if (!strcmp(str, rn->name)) - return rn->val; - } - - return -1; -} - -char *rlim2str(int rlim) -{ - const struct rlimit_name *rn; - - for (rn = rlimit_names; rn->name; rn++) { - if (rn->val == rlim) - return rn->name; - } - - return "unknown"; -} - -char *lim2str(struct rlimit *rlim) -{ - char tmp[25]; - static char buf[42]; - - buf[0] = 0; - if (RLIM_INFINITY == rlim->rlim_cur) - snprintf(tmp, sizeof(tmp), "unlimited, "); - else - snprintf(tmp, sizeof(tmp), "%llu, ", (unsigned long long)rlim->rlim_cur); - strlcat(buf, tmp, sizeof(buf)); - - if (RLIM_INFINITY == rlim->rlim_max) - snprintf(tmp, sizeof(tmp), "unlimited, "); - else - snprintf(tmp, sizeof(tmp), "%llu, ", (unsigned long long)rlim->rlim_max); - strlcat(buf, tmp, sizeof(buf)); - - return buf; -} - -/* First form: `rlimit RESOURCE LIMIT` */ -void conf_parse_rlimit(char *line, struct rlimit arr[]) -{ - char *level, *limit, *val; - int resource = -1; - rlim_t cfg; - - level = strtok(line, " \t"); - if (!level) - goto error; - - limit = strtok(NULL, " \t"); - if (!limit) - goto error; - - val = strtok(NULL, " \t"); - if (!val) { - /* Second form: `rlimit RESOURCE LIMIT` */ - val = limit; - limit = level; - level = "both"; - } - - resource = str2rlim(limit); - if (resource < 0 || resource > RLIMIT_NLIMITS) - goto error; - - /* Official keyword from v3.1 is `unlimited`, from prlimit(1) */ - if (!strcmp(val, "unlimited") || !strcmp(val, "infinity")) { - cfg = RLIM_INFINITY; - } else { - const char *err = NULL; - - cfg = strtonum(val, 0, (long long)2 << 31, &err); - if (err) { - logit(LOG_WARNING, "rlimit: invalid %s value: %s", - rlim2str(resource), val); - return; - } - } - - if (!strcmp(level, "soft")) - arr[resource].rlim_cur = cfg; - else if (!strcmp(level, "hard")) - arr[resource].rlim_max = cfg; - else if (!strcmp(level, "both")) - arr[resource].rlim_max = arr[resource].rlim_cur = cfg; - else - goto error; - - return; -error: - logit(LOG_WARNING, "rlimit: parse error"); -} /* cgroup NAME ctrl.prop:value,ctrl.prop:value ... */ static void conf_parse_cgroup(char *line) @@ -897,7 +126,7 @@ static int parse_static(char *line, int is_rcsd) } if (BOOTSTRAP && MATCH_CMD(line, "set ", x)) { - legacy_parse_env(x); + conf_set_env(x); return 0; } @@ -976,7 +205,7 @@ static int parse_static(char *line, int is_rcsd) } if (MATCH_CMD(line, "reboot-watchdog ", x)) { - wdtreboot = get_bool(strip_line(x), 0); + wdtreboot = istrue(strip_line(x), 0); return 0; } @@ -1190,7 +419,7 @@ int legacy_parse_conf(char *file, int is_rcsd) else if (!parse_dynamic(line, is_rcsd ? rlimit : global_rlimit, file)) ; else - legacy_parse_env(line); + conf_set_env(line); free(line); } diff --git a/src/legacy.h b/src/legacy.h index 083018f0..da16c18d 100644 --- a/src/legacy.h +++ b/src/legacy.h @@ -24,27 +24,8 @@ #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); +int legacy_parse_conf (char *file, int is_rcsd); +void legacy_parse_env (char *line); #endif /* FINIT_LEGACY_H_ */ diff --git a/src/util.c b/src/util.c index 3d8a0b22..0dd7737d 100644 --- a/src/util.c +++ b/src/util.c @@ -43,8 +43,9 @@ #ifdef HAVE_SYS_IOCTL_H # include #endif +#include #include /* sysinfo() */ -#include /* statfs */ +#include /* statfs */ #include #ifdef _LIBITE_LITE # include @@ -108,6 +109,34 @@ static char *signames[] = { "SYS", }; +struct rlimit_name { + char *name; + int val; +}; + +static const struct rlimit_name rlimit_names[] = { + { "as", RLIMIT_AS }, + { "core", RLIMIT_CORE }, + { "cpu", RLIMIT_CPU }, + { "data", RLIMIT_DATA }, + { "fsize", RLIMIT_FSIZE }, + { "locks", RLIMIT_LOCKS }, + { "memlock", RLIMIT_MEMLOCK }, + { "msgqueue", RLIMIT_MSGQUEUE }, + { "nice", RLIMIT_NICE }, + { "nofile", RLIMIT_NOFILE }, + { "nproc", RLIMIT_NPROC }, + { "rss", RLIMIT_RSS }, + { "rtprio", RLIMIT_RTPRIO }, +#ifdef RLIMIT_RTTIME + { "rttime", RLIMIT_RTTIME }, +#endif + { "sigpending", RLIMIT_SIGPENDING }, + { "stack", RLIMIT_STACK }, + + { NULL, 0 } +}; + /* https://freedesktop.org/software/systemd/man/systemd.exec.html#id-1.20.8 */ static char *exitcodes[] = { "SUCCESS", /* 0: Std C exit OK */ @@ -483,6 +512,50 @@ char *code2str(int code) return exitcodes[code]; } +int str2rlim(char *str) +{ + const struct rlimit_name *rn; + + for (rn = rlimit_names; rn->name; rn++) { + if (!strcmp(str, rn->name)) + return rn->val; + } + + return -1; +} + +char *rlim2str(int rlim) +{ + const struct rlimit_name *rn; + + for (rn = rlimit_names; rn->name; rn++) { + if (rn->val == rlim) + return rn->name; + } + + return "unknown"; +} + +char *lim2str(struct rlimit *rlim) +{ + char tmp[25]; + static char buf[42]; + + buf[0] = 0; + if (RLIM_INFINITY == rlim->rlim_cur) + snprintf(tmp, sizeof(tmp), "unlimited, "); + else + snprintf(tmp, sizeof(tmp), "%llu, ", (unsigned long long)rlim->rlim_cur); + strlcat(buf, tmp, sizeof(buf)); + + if (RLIM_INFINITY == rlim->rlim_max) + snprintf(tmp, sizeof(tmp), "unlimited, "); + else + snprintf(tmp, sizeof(tmp), "%llu, ", (unsigned long long)rlim->rlim_max); + strlcat(buf, tmp, sizeof(buf)); + + return buf; +} void do_sleep(unsigned int sec) { diff --git a/src/util.h b/src/util.h index 7e8df0b2..2af4c9b5 100644 --- a/src/util.h +++ b/src/util.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #endif @@ -55,6 +56,7 @@ extern char *prognm; # include # include #endif + #include "log.h" char *progname (char *arg0); @@ -81,6 +83,10 @@ char *sig2str (int sig); int str2sig (char *sig); char *code2str (int code); +int str2rlim (char *str); +char *rlim2str (int rlim); +char *lim2str (struct rlimit *rlim); + void do_sleep (unsigned int sec); void do_usleep (unsigned int usec); long jiffies (void); From f1393ff8a1ecb5303ac19f056778ba376be5864b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 12:57:39 +0200 Subject: [PATCH 07/28] conf: instantiate %i templates for the block format A template in the block format registered garbage. conf_parse_file() routed every file with an '@' in its name straight to the legacy parser, which read the block line by line: the section header became a service whose command was the section title, and each key = value line below it became an environment variable. service serv:%i { ... } -> service 'serv:eth0' with argument '{' Substitute %i over the whole file before parsing instead, so format detection and both parsers see finished text. A bare name@.conf is still skipped, it is the template rather than an instance of one. The legacy parser no longer opens the file or substitutes per line, it is handed the instantiated buffer, so the template convention now has one implementation instead of two. conf_is_template() applies basenm(), a directory with an '@' in its name is not a template. libconfuse cannot name a buffer it parses before 3.4, so a typo in a template would be reported against "[buf]". Parse through fmemopen() with the file name preset until the floor moves. Signed-off-by: Joachim Wiberg --- configure.ac | 8 +- src/conf.c | 189 +++++++++++++++++++++++++++++++++++++----- src/conf.h | 1 + src/legacy.c | 79 ++---------------- src/legacy.h | 2 +- test/Makefile.am | 2 + test/conf-template.sh | 82 ++++++++++++++++++ 7 files changed, 269 insertions(+), 94 deletions(-) create mode 100755 test/conf-template.sh diff --git a/configure.ac b/configure.ac index 90b35c47..0b213b53 100644 --- a/configure.ac +++ b/configure.ac @@ -50,9 +50,11 @@ 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. +# 3.3 is the floor: CFGF_KEYSTRVAL, which environment {} and the +# free-form cgroup keys are built on, does not exist before it. 3.3 +# parses both correctly. Two workarounds hang off this number, the +# spurious KEYSTRVAL warning and cfg_parse_buf() losing the file name +# of a template; grep the XXX notes in src/conf.c before raising it. PKG_CHECK_MODULES([confuse], [libconfuse >= 3.3]) # Check for configured Finit features diff --git a/src/conf.c b/src/conf.c index 34d87458..70f349d6 100644 --- a/src/conf.c +++ b/src/conf.c @@ -1525,6 +1525,105 @@ static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd) return 0; } +/* + * Substitute every %i in LINE with NAME, for template instantiation. + * + * Very simple and crude implementation, only supports '%i' + */ +static char *conf_instantiate(char *line, char *name) +{ + char *ptr, *end = strchr(line, 0); + char *pos = line; + size_t num = 0; + + if (!name[0] || !end) + return line; + + while ((ptr = strchr(pos, '%'))) { + num++; + pos = ptr + 1; + } + + ptr = realloc(line, strlen(line) + num * strlen(name) + 1); + if (!ptr) + return line; + + pos = line = ptr; + while ((ptr = strchr(pos, '%'))) { + if (!strncmp(ptr, "%i", 2)) { + char *rest = &ptr[2]; + char *next = ptr + strlen(name); + + memmove(next, rest, strlen(rest) + 1); + memcpy(ptr, name, strlen(name)); + + pos += strlen(name); + } else + pos++; + } + + return line; +} + +static int conf_is_template(const char *file, char *name, size_t len) +{ + char *ptr, *nm; + size_t i = 0; + + /* the @ convention names the file, a directory may contain one */ + ptr = strchr(basenm((char *)file), '@'); + if (!ptr) + return 0; /* not a template */ + + nm = ptr + 1; + ptr = strstr(nm, ".conf"); + if (!strcmp(nm, ".conf") || !ptr) + return 1; /* template itself or invalid */ + + if (!name) + return 1; + + while (nm < ptr && i < len - 1) + name[i++] = *nm++; + name[i] = 0; + + return 1; /* instantiated template */ +} + +/* + * Parse from FILE, or from BUF when it is set, i.e. for a template + * whose %i has already been substituted. + * + * XXX: Workaround for libConfuse <3.4, whose cfg_parse_buf() replaces + * cfg->filename with "[buf]", so every diagnostic from a template + * would lose the file name. cfg_parse_fp() keeps a name the + * caller has already set, so go through fmemopen() instead. With + * a 3.4 floor this is just cfg_parse_buf(cfg, buf). + */ +static int conf_parse_any(cfg_t *cfg, char *file, char *buf) +{ + FILE *fp; + int rc; + + if (!buf) + return cfg_parse(cfg, file); + + /* fmemopen() rejects a zero length on older GLIBC */ + if (!buf[0]) + return CFG_SUCCESS; + + fp = fmemopen(buf, strlen(buf), "r"); + if (!fp) + return CFG_FILE_ERROR; + + /* cfg_parse_fp() only names the stream when we have not */ + cfg->filename = strdup(file); + rc = cfg_parse_fp(cfg, fp); + fclose(fp); + + return rc; +} + /* * Is this a new-format file, or a legacy one? * @@ -1539,7 +1638,7 @@ static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd) * creating them, so a lenient tree is missing every set{} variable * and every free-form cgroup key. */ -static int is_new_format(char *file) +static int is_new_format(char *file, char *buf) { cfg_t *cfg; int rc; @@ -1549,12 +1648,44 @@ static int is_new_format(char *file) return 0; cfg_set_error_function(cfg, cfg_error_quiet); - rc = cfg_parse(cfg, file); + rc = conf_parse_any(cfg, file, buf); cfg_free(cfg); return rc == CFG_SUCCESS; } +/* + * Read FILE and substitute %i with NAME, for template instantiation. + * Returns a malloc()'ed buffer the caller frees. + */ +static char *conf_read_template(char *file, char *name) +{ + struct stat st; + char *buf = NULL; + size_t len; + FILE *fp; + + fp = fopen(file, "r"); + if (!fp) + return NULL; + + /* fstat() the open fd, no window for the file to change under us */ + if (fstat(fileno(fp), &st) || st.st_size < 0) + goto out; + + buf = malloc((size_t)st.st_size + 1); + if (!buf) + goto out; + + len = fread(buf, 1, (size_t)st.st_size, fp); + buf[len] = 0; + buf = conf_instantiate(buf, name); +out: + fclose(fp); + + return buf; +} + /* * Parse one Finit .conf file, in either format. The legacy .conf * include directive routes back through this so included files are @@ -1562,43 +1693,63 @@ static int is_new_format(char *file) */ int conf_parse_file(char *file, int is_rcsd) { + char name[MAX_ID_LEN] = { 0 }; + char *buf = NULL; cfg_t *cfg; int rc; /* - * Template files (name@.conf, name@id.conf): %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. + * Template files (name@.conf, name@id.conf). A bare name@.conf + * is the template itself, there is nothing to instantiate from + * it. Otherwise %i is substituted over the whole file up front, + * so both the format detection below and, on fallback, the + * legacy parser see the finished text. */ - if (strchr(basenm(file), '@')) - return legacy_parse_conf(file, is_rcsd); + if (conf_is_template(file, name, sizeof(name))) { + if (!name[0]) { + dbg("*** Skipping template file %s", file); + return 0; + } + + dbg("*** instantiating %s from %s ...", name, file); + buf = conf_read_template(file, name); + if (!buf) + return 1; + } cfg = cfg_init(conf_opts, CFGF_NONE); - if (!cfg) - return legacy_parse_conf(file, is_rcsd); + if (!cfg) { + rc = legacy_parse_conf(file, buf, is_rcsd); + goto done; + } cfg_set_error_function(cfg, cfg_error_cb); - rc = cfg_parse(cfg, file); + rc = conf_parse_any(cfg, file, buf); if (rc == CFG_SUCCESS) { dbg("*** Parsing %s (new format)", file); rc = conf_parse_cfg(cfg, file, is_rcsd); cfg_free(cfg); - return rc; + goto done; } cfg_free(cfg); - if (rc == CFG_FILE_ERROR) - return 1; /* like legacy fopen() failure */ + if (rc == CFG_FILE_ERROR) { + rc = 1; /* like legacy fopen() failure */ + goto done; + } - if (is_new_format(file)) { + if (is_new_format(file, buf)) { logit(LOG_ERR, "parse error: %s", cfg_errmsg); - return 1; + rc = 1; + goto done; } dbg("not in new format (%s), falling back to legacy parser", cfg_errmsg); + rc = legacy_parse_conf(file, buf, is_rcsd); +done: + free(buf); - return legacy_parse_conf(file, is_rcsd); + return rc; } static void glob_append(glob_t *gl, int append, const char *fmt, ...) @@ -1849,7 +2000,7 @@ static int conf_change_act(char *dir, char *name, uint32_t mask) strlcpy(fn, dir, sizeof(fn)); dbg("path: %s mask: %08x", fn, mask); - if (strchr(name, '@')) { + if (conf_is_template(name, NULL, 0)) { /* Skip realpath for templates */ rp = strdup(fn); } else { @@ -1903,7 +2054,7 @@ int conf_changed(char *file) if (!file) return 0; - if (strchr(file, '@')) + if (conf_is_template(file, NULL, 0)) rp = strdup(file); else rp = realpath(file, NULL); diff --git a/src/conf.h b/src/conf.h index b2576ff8..aa565fca 100644 --- a/src/conf.h +++ b/src/conf.h @@ -83,6 +83,7 @@ int conf_parse_runlevels (const char *runlevels); void conf_parse_cond (svc_t *svc, char *cond); int conf_parse_file (char *file, int is_rcsd); + #endif /* FINIT_CONF_H_ */ /** diff --git a/src/legacy.c b/src/legacy.c index c817886e..8de09469 100644 --- a/src/legacy.c +++ b/src/legacy.c @@ -319,79 +319,19 @@ static int parse_dynamic(char *line, struct rlimit rlimit[], char *file) } /* - * Very simple and crude implementation, only supports '%i' + * Parse FILE, or BUF when the frontend has already read and + * instantiated it, i.e. for a template. Either way the text arriving + * here is final, no %i is left to substitute. */ -static char *instantiate(char *line, char *name) -{ - char *ptr, *end = strchr(line, 0); - char *pos = line; - size_t num = 0; - - if (!name[0] || !end) - return line; - - while ((ptr = strchr(pos, '%'))) { - num++; - pos++; - } - - ptr = realloc(line, strlen(line) + num * strlen(name) + 1); - if (!ptr) - return line; - - pos = line = ptr; - while ((ptr = strchr(pos, '%'))) { - if (!strncmp(ptr, "%i", 2)) { - char *rest = &ptr[2]; - char *next = ptr + strlen(name); - - memmove(next, rest, strlen(rest) + 1); - memcpy(ptr, name, strlen(name)); - - pos += strlen(name); - } else - pos++; - } - - return line; -} - -static int is_template(const char *file, char *name, size_t len) -{ - char *ptr, *nm; - size_t i = 0; - - ptr = strchr(file, '@'); - if (!ptr) - return 0; /* not a template */ - - nm = ptr + 1; - ptr = strstr(nm, ".conf"); - if (!strcmp(nm, ".conf") || !ptr) - return 1; /* template itself or invalid */ - - while (nm < ptr && i < len - 1) - name[i++] = *nm++; - name[i] = 0; - - return 1; /* instantiated template */ -} - -int legacy_parse_conf(char *file, int is_rcsd) +int legacy_parse_conf(char *file, char *buf, int is_rcsd) { struct rlimit rlimit[RLIMIT_NLIMITS]; - char name[65] = { 0 }; FILE *fp; - if (is_template(file, name, sizeof(name))) { - if (!name[0]) { - dbg("*** Skipping template file %s", file); - return 0; - } - dbg("*** instantiating %s from %s ...", name, file); - } - - fp = fopen(file, "r"); + if (buf) + fp = fmemopen(buf, strlen(buf), "r"); + else + fp = fopen(file, "r"); if (!fp) return 1; @@ -410,9 +350,6 @@ int legacy_parse_conf(char *file, int is_rcsd) continue; tabstospaces(line); -// dbg("raw: %s", line); - line = instantiate(line, name); -// dbg("ins: %s", line); if (!parse_static(line, is_rcsd)) ; diff --git a/src/legacy.h b/src/legacy.h index da16c18d..f623c743 100644 --- a/src/legacy.h +++ b/src/legacy.h @@ -24,7 +24,7 @@ #ifndef FINIT_LEGACY_H_ #define FINIT_LEGACY_H_ -int legacy_parse_conf (char *file, int is_rcsd); +int legacy_parse_conf (char *file, char *buf, int is_rcsd); void legacy_parse_env (char *line); #endif /* FINIT_LEGACY_H_ */ diff --git a/test/Makefile.am b/test/Makefile.am index 9edcca9f..38551d02 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -32,6 +32,7 @@ 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 += conf-template.sh EXTRA_DIST += crashing.sh EXTRA_DIST += dep-chain-reload.sh EXTRA_DIST += depserv.sh @@ -77,6 +78,7 @@ TESTS += add-remove-dynamic-service-sub-config.sh TESTS += bootstrap-crash.sh TESTS += cond-start-task.sh TESTS += conf-format.sh +TESTS += conf-template.sh TESTS += crashing.sh TESTS += dep-chain-reload.sh TESTS += depserv.sh diff --git a/test/conf-template.sh b/test/conf-template.sh new file mode 100755 index 00000000..344be694 --- /dev/null +++ b/test/conf-template.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# Verify %i template instantiation for both .conf formats: the block +# format substitutes over the whole file before parsing, the legacy +# one-liner format per line. A bare name@.conf registers nothing. +set -eu + +TEST_DIR=$(dirname "$0") + +test_teardown() +{ + say "Running test teardown." + run "rm -f $FINIT_RCSD/available/serv@.conf" + run "rm -f $FINIT_RCSD/enabled/serv@eth0.conf" + run "rm -f $FINIT_RCSD/enabled/serv@eth1.conf $FINIT_RCSD/enabled/serv@.conf" + run "rm -f /run/serv-eth0.pid /run/serv-eth1.pid" +} + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +# %i has to reach three different places: the section title, which +# becomes the instance identity, an ordinary value, and the command +# line. The per-instance PID file covers the last one, it can only +# appear if the command was instantiated. +say 'Install a block-format template' +run "echo 'service serv:%i {' > $FINIT_RCSD/available/serv@.conf" +run "echo ' description = \"Template for %i\"' >> $FINIT_RCSD/available/serv@.conf" +run "echo ' pid = \"/run/serv-%i.pid\"' >> $FINIT_RCSD/available/serv@.conf" +run "echo ' command = \"serv -n -p -P /run/serv-%i.pid\"' >> $FINIT_RCSD/available/serv@.conf" +run "echo '}' >> $FINIT_RCSD/available/serv@.conf" + +say 'Enable two instances' +run "initctl enable serv@eth0.conf" +run "initctl enable serv@eth1.conf" +run "initctl reload" + +retry 'assert_num_services 2 serv' +assert_desc "Template for eth0" serv:eth0 +assert_desc "Template for eth1" serv:eth1 + +say 'Both instances run, each with its own instantiated PID file' +retry 'assert_num_children 2 serv' +retry 'assert_file_exists /run/serv-eth0.pid' +retry 'assert_file_exists /run/serv-eth1.pid' + +# available/ is never globbed, so enabling the bare template is the +# only way the "skip the template itself" branch is reached at all. +say 'A bare template in enabled/ registers nothing' +run "ln -sf ../available/serv@.conf $FINIT_RCSD/enabled/serv@.conf" +run "initctl reload" + +retry 'assert_num_services 2 serv' +run "rm -f $FINIT_RCSD/enabled/serv@.conf" + +# assert_num_services cannot express "exactly one": initctl status +# prints a detail block for a single match and a table only for +# several, so the line count is 13, not 1. Check the survivor and the +# absence of the other instead. +say 'Disable one instance' +run "initctl disable serv@eth1.conf" +run "initctl reload" + +retry 'assert_num_children 1 serv' +assert_desc "Template for eth0" serv:eth0 +assert_num_services 0 serv:eth1 + +say 'Legacy one-liner templates still instantiate' +run "echo 'service :%i pid:/run/serv-%i.pid serv -n -p -P /run/serv-%i.pid -- Legacy template for %i' > $FINIT_RCSD/available/serv@.conf" +run "initctl reload" + +retry 'assert_num_children 1 serv' +assert_desc "Legacy template for eth0" serv:eth0 + +say 'A typo in a block template is rejected, and named after the instance' +run "echo 'service serv:%i {' > $FINIT_RCSD/available/serv@.conf" +run "echo ' descriptoin = \"Template for %i\"' >> $FINIT_RCSD/available/serv@.conf" +run "echo ' command = \"serv -n\"' >> $FINIT_RCSD/available/serv@.conf" +run "echo '}' >> $FINIT_RCSD/available/serv@.conf" +run "initctl reload" + +retry 'assert_num_children 0 serv' +assert_num_services 0 serv From 0721b0fec2fd3e262c1f03fd562aa0d9b7e06fd8 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 13:27:30 +0200 Subject: [PATCH 08/28] util: one whole-file reader for all tools Three private ones had grown: fnread() in util.c, flen() behind pid_cmdline()/pid_cgroup() in cgutil.c, and conf_read_template() in conf.c. Two of them were also wrong in ways the others were not. fnread() formatted the path into a char[256] and stat()ed it before opening, so a longer path was silently truncated and then read from whichever file the truncation happened to name, and the size could change between the look and the read. flen() existed because neither of those approaches works on procfs at all, where stat() reports zero and the only way to learn the size is to read to EOF. Add fslurp() to util.[ch], which every tool already links. It opens first and sizes the fd it holds, treats st_size as a hint, and reads until EOF, so procfs and regular files take the same path. Paths are formatted by libite's vfopenf(), which allocates to fit. Callers that need the byte count, /proc/PID/cmdline embeds NUL, ask for it. fnread() keeps its signature and becomes a bounded copy out of the result, so its one caller is unaffected. Signed-off-by: Joachim Wiberg --- src/cgutil.c | 69 +++++++------------------------- src/conf.c | 25 ++---------- src/util.c | 108 +++++++++++++++++++++++++++++++++++++++++++-------- src/util.h | 3 ++ 4 files changed, 114 insertions(+), 91 deletions(-) diff --git a/src/cgutil.c b/src/cgutil.c index a1d70a0c..9abd6bb5 100644 --- a/src/cgutil.c +++ b/src/cgutil.c @@ -34,6 +34,7 @@ #include "cgutil.h" #include "initctl.h" #include "log.h" +#include "util.h" #define CDIM plain ? "" : "\e[2m" #define CRST plain ? "" : "\e[0m" @@ -54,42 +55,18 @@ int cgroup_avail(void) return fismnt(FINIT_CGPATH); } -static size_t flen(FILE *fp) -{ - size_t total = 0, sz; - char buf[512]; - - while ((sz = fread(buf, 1, sizeof(buf), fp)) > 0) - total += sz; - rewind(fp); - - return total; -} - char *pid_cmdline(int pid) { size_t i, len; char *buf; - FILE *fp; - fp = fopenf("r", "/proc/%d/cmdline", pid); - if (!fp) + buf = fslurp(&len, "/proc/%d/cmdline", pid); + if (!buf) return strdup(""); /* regular process */ - len = flen(fp); if (len == 0) { - fail: - fclose(fp); - return NULL; /* kernel thread */ - } - - buf = calloc(len + 1, 1); - if (!buf) - goto fail; - - if (fread(buf, 1, len, fp) < len) { free(buf); - goto fail; + return NULL; /* kernel thread */ } /* replace all NUL chars with space */ @@ -98,46 +75,30 @@ char *pid_cmdline(int pid) buf[i] = ' '; } - fclose(fp); return buf; } char *pid_cgroup(int pid) { - char *buf, *ptr = NULL; - size_t len; - FILE *fp; - - fp = fopenf("r", "/proc/%d/cgroup", pid); - if (!fp) - return NULL; - - len = flen(fp); - if (len == 0) { - fclose(fp); - return NULL; - } - len++; + char *buf, *ptr; - buf = calloc(1, len); - if (!buf) { - fclose(fp); + buf = fslurp(NULL, "/proc/%d/cgroup", pid); + if (!buf) return NULL; - } - if (fgets(buf, len, fp)) - ptr = chomp(buf); - fclose(fp); + ptr = strchr(buf, '\n'); /* first line only */ + if (ptr) + *ptr = 0; + chomp(buf); + ptr = strchr(buf, '/'); if (ptr) { - ptr = strchr(ptr, '/'); - if (ptr) { - memmove(buf, ptr, strlen(ptr) + 1); - return buf; - } + memmove(buf, ptr, strlen(ptr) + 1); + return buf; } free(buf); + return NULL; } diff --git a/src/conf.c b/src/conf.c index 70f349d6..9ed81922 100644 --- a/src/conf.c +++ b/src/conf.c @@ -1660,30 +1660,13 @@ static int is_new_format(char *file, char *buf) */ static char *conf_read_template(char *file, char *name) { - struct stat st; - char *buf = NULL; - size_t len; - FILE *fp; - - fp = fopen(file, "r"); - if (!fp) - return NULL; + char *buf; - /* fstat() the open fd, no window for the file to change under us */ - if (fstat(fileno(fp), &st) || st.st_size < 0) - goto out; - - buf = malloc((size_t)st.st_size + 1); + buf = fslurp(NULL, "%s", file); if (!buf) - goto out; - - len = fread(buf, 1, (size_t)st.st_size, fp); - buf[len] = 0; - buf = conf_instantiate(buf, name); -out: - fclose(fp); + return NULL; - return buf; + return conf_instantiate(buf, name); } /* diff --git a/src/util.c b/src/util.c index 0dd7737d..4fe6a2db 100644 --- a/src/util.c +++ b/src/util.c @@ -33,6 +33,7 @@ #include #include #include +#include #include #ifdef HAVE_TERMIOS_H # include @@ -335,34 +336,109 @@ int mksubsys(const char *dir, mode_t mode, char *user, char *group) return rc; } -int fnread(char *buf, size_t len, char *fmt, ...) +/* + * Read an open stream to EOF into a malloc()'ed, NUL terminated buffer. + * + * st_size is only a hint here, and zero on procfs, so the read loop + * runs until EOF rather than trusting it. Sizing from the already + * open fd also leaves no window for the file to change between the + * look and the read. + */ +static char *slurp(FILE *fp, size_t *lenp) { - char path[256]; - va_list ap; + size_t len = 0, size = BUFSIZ; + struct stat st; + char *buf; + + if (!fstat(fileno(fp), &st) && st.st_size > 0 && (size_t)st.st_size > size) + size = (size_t)st.st_size; + + buf = malloc(size + 1); + if (!buf) + return NULL; + + while (1) { + char *ptr; + + len += fread(&buf[len], 1, size - len, fp); + if (len < size) + break; /* EOF, or error caught below */ + + ptr = realloc(buf, size * 2 + 1); + if (!ptr) { + free(buf); + return NULL; + } + buf = ptr; + size *= 2; + } + + if (ferror(fp)) { + free(buf); + return NULL; + } + + buf[len] = 0; + if (lenp) + *lenp = len; + + return buf; +} + +/* + * Read a whole file into a malloc()'ed, NUL terminated buffer, which + * the caller frees. @lenp, when given, returns the number of bytes + * read; the content may itself contain NUL, e.g. /proc/PID/cmdline. + */ +char *vfslurp(size_t *lenp, const char *fmt, va_list ap) +{ + char *buf; FILE *fp; + fp = vfopenf("r", fmt, ap); + if (!fp) + return NULL; + + buf = slurp(fp, lenp); + fclose(fp); + + return buf; +} + +char *fslurp(size_t *lenp, const char *fmt, ...) +{ + va_list ap; + char *buf; + va_start(ap, fmt); - vsnprintf(path, sizeof(path), fmt, ap); + buf = vfslurp(lenp, fmt, ap); va_end(ap); - if (!buf || !len) { - struct stat st; + return buf; +} - if (stat(path, &st)) - return -1; +int fnread(char *buf, size_t len, char *fmt, ...) +{ + size_t dlen; + va_list ap; + char *data; - return (ssize_t)st.st_size; - } + va_start(ap, fmt); + data = vfslurp(&dlen, fmt, ap); + va_end(ap); - fp = fopen(path, "r"); - if (!fp) + if (!data) return -1; - len = fread(buf, sizeof(char), len - 1, fp); - buf[len] = 0; - fclose(fp); + if (buf && len) { + if (dlen > len - 1) + dlen = len - 1; + memcpy(buf, data, dlen); + buf[dlen] = 0; + } + free(data); - return (int)len; + return (int)dlen; } int fnwrite(char *value, char *fmt, ...) diff --git a/src/util.h b/src/util.h index 2af4c9b5..b0ae8614 100644 --- a/src/util.h +++ b/src/util.h @@ -29,6 +29,7 @@ #include #ifdef HAVE_TERMIOS_H #include +#include #include #include #include @@ -72,6 +73,8 @@ int getcgroup (char *buf, size_t len); int mksubsys (const char *dir, mode_t mode, char *user, char *group); +char *fslurp (size_t *lenp, const char *fmt, ...) __attribute__ ((format (printf, 2, 3))); +char *vfslurp (size_t *lenp, const char *fmt, va_list ap); int fnread (char *buf, size_t len, char *fmt, ...) __attribute__ ((format (printf, 3, 4))); int fnwrite (char *value, char *fmt, ...) __attribute__ ((format (printf, 2, 3))); int fngetint (char *path, int *val); From 2a5190ecffebc09af9b9fbb2a580e1bb09624e38 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 22:14:09 +0200 Subject: [PATCH 09/28] service: do not let a script timeout take PID 1 with it A stop: or reload: script written with a timeout killed Finit at config load: service stop:5,/bin/true service.sh -- Boom parse_script() takes the timeout as a pointer and the caller decides whether it wants one. However, both stop: and reload: scripts so far have no timeout, i.e., NULL. Guard the branch that reads a leading number. Signed-off-by: Joachim Wiberg --- src/service.c | 3 ++- test/Makefile.am | 2 ++ test/script-timeout.sh | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100755 test/script-timeout.sh diff --git a/src/service.c b/src/service.c index 1c51cdc0..055985cd 100644 --- a/src/service.c +++ b/src/service.c @@ -1656,7 +1656,8 @@ static void parse_script(svc_t *svc, char *type, char *script, int *tmo, char *b script, errstr); goto err; } - *tmo = (int)(sec * 1000); + if (tmo) + *tmo = (int)(sec * 1000); } else { path = script; if (tmo) diff --git a/test/Makefile.am b/test/Makefile.am index 38551d02..0860d904 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -33,6 +33,7 @@ EXTRA_DIST += bootstrap-crash.sh EXTRA_DIST += cond-start-task.sh EXTRA_DIST += conf-format.sh EXTRA_DIST += conf-template.sh +EXTRA_DIST += script-timeout.sh EXTRA_DIST += crashing.sh EXTRA_DIST += dep-chain-reload.sh EXTRA_DIST += depserv.sh @@ -79,6 +80,7 @@ TESTS += bootstrap-crash.sh TESTS += cond-start-task.sh TESTS += conf-format.sh TESTS += conf-template.sh +TESTS += script-timeout.sh TESTS += crashing.sh TESTS += dep-chain-reload.sh TESTS += depserv.sh diff --git a/test/script-timeout.sh b/test/script-timeout.sh new file mode 100755 index 00000000..64b6debc --- /dev/null +++ b/test/script-timeout.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# A timeout on a stop: or reload: script must not take PID 1 with it. +# Those two hooks passed a NULL timeout pointer to parse_script(), +# which wrote through it whenever the script was prefixed with a +# valid number. Only a valid number reached the store, so +# 'stop:abc,/bin/true' was harmless while 'stop:5,/bin/true' was not. +set -eu + +TEST_DIR=$(dirname "$0") + +test_teardown() +{ + say "Running test teardown." + run "rm -f $FINIT_CONF" +} + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +# shellcheck disable=SC2154 +assert_alive() +{ + assert "Finit survived $1" "$(kill -0 "$finit_pid" 2>/dev/null && echo yes)" = "yes" +} + +for hook in stop reload post pre; do + say "Timeout on a $hook: script" + run "echo 'service $hook:5,/bin/true service.sh -- Timeout test' > $FINIT_CONF" + run "initctl reload" || true + assert_alive "$hook:5,/bin/true" +done + +say 'The service still runs afterwards' +run "echo 'service stop:5,/bin/true service.sh -- Timeout test' > $FINIT_CONF" +run "initctl reload" +retry 'assert_num_children 1 service.sh' From 53cb2b7bdb46fe899d4f6b84ef9812c6326b61f4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 22:14:32 +0200 Subject: [PATCH 10/28] service: give stop and reload scripts their own timeout Both were bounded by killdelay, the delay between the stop signal and SIGKILL, because service_run_script() had nothing else to reach for. That conflates two things: how long the daemon may take to die, and how long its stop script may run. Give each hook a timeout of its own, defaulting to killdelay when unset, so the existing behaviour is what you get until you ask for something else. parse_script() already falls back that way for the hooks that had one. Signed-off-by: Joachim Wiberg --- src/service.c | 12 ++++++------ src/svc.h | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/service.c b/src/service.c index 055985cd..34647923 100644 --- a/src/service.c +++ b/src/service.c @@ -1076,7 +1076,7 @@ static void service_kill(svc_t *svc) * Called by service_stop() and service_reload() when alternate mechanisms * for stopping and reloading have been specified by the user. */ -static int service_run_script(svc_t *svc, char *script) +static int service_run_script(svc_t *svc, char *script, int tmo) { const char *id = svc_ident(svc, NULL, 0); pid_t pid = service_fork(svc); @@ -1106,7 +1106,7 @@ static int service_run_script(svc_t *svc, char *script) } dbg("%s: script '%s' started as PID %d", id, script, pid); - return service_script_add(svc, pid, svc->killdelay); + return service_script_add(svc, pid, tmo ? tmo : svc->killdelay); } /* Ensure we don't have any notify socket lingering */ @@ -1268,7 +1268,7 @@ int service_stop(svc_t *svc) print_desc("Stopping ", svc->desc); if (svc->stop_script[0]) { - rc = service_run_script(svc, svc->stop_script); + rc = service_run_script(svc, svc->stop_script, svc->stop_tmo); } else if (!svc_is_sysv(svc)) { if (svc->pid > 1) { /* @@ -1360,7 +1360,7 @@ static int service_reload(svc_t *svc) if (svc->reload_script[0]) { logit(LOG_CONSOLE | LOG_NOTICE, "Reloading %s[%d], calling reload:%s ...", id, svc->pid, svc->reload_script); - rc = service_run_script(svc, svc->reload_script); + rc = service_run_script(svc, svc->reload_script, svc->reload_tmo); } else if (svc->sighup) { if (svc->pid <= 1) { dbg("%s[%d]: bad PID, cannot reload service", id, svc->pid); @@ -2211,12 +2211,12 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) memset(svc->cleanup_script, 0, sizeof(svc->cleanup_script)); if (reload_script) - parse_script(svc, "reload", reload_script, NULL, svc->reload_script, sizeof(svc->reload_script)); + parse_script(svc, "reload", reload_script, &svc->reload_tmo, svc->reload_script, sizeof(svc->reload_script)); else memset(svc->reload_script, 0, sizeof(svc->reload_script)); if (stop_script) - parse_script(svc, "stop", stop_script, NULL, svc->stop_script, sizeof(svc->stop_script)); + parse_script(svc, "stop", stop_script, &svc->stop_tmo, svc->stop_script, sizeof(svc->stop_script)); else memset(svc->stop_script, 0, sizeof(svc->stop_script)); diff --git a/src/svc.h b/src/svc.h index 1d4b46e3..59f4ed43 100644 --- a/src/svc.h +++ b/src/svc.h @@ -221,9 +221,11 @@ typedef struct svc { /* When set, used instead of SIGHUP or stop-start */ char reload_script[MAX_CMD_LEN]; + int reload_tmo; /* When set, used instead of SIGTERM or sysv 'stop' */ char stop_script[MAX_CMD_LEN]; + int stop_tmo; /* * Used to forcefully kill services that won't shutdown on From 8a3d55b416e475fe050448c5259eded50fc03cf2 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 22:15:03 +0200 Subject: [PATCH 11/28] test: refuse to run against a stale sysroot binary 'make check' refreshes the sysroot through the setup-chroot rule, but running a test script by hand does not, so the test exercises whichever finit was installed last and reports on code that is no longer there. Both a passing and a failing run are then meaningless, and nothing says so. Compare the built binary against the installed one at startup and fail with the command that fixes it. Signed-off-by: Joachim Wiberg --- test/lib/setup.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/lib/setup.sh b/test/lib/setup.sh index 1512a09b..3ffdfb60 100755 --- a/test/lib/setup.sh +++ b/test/lib/setup.sh @@ -314,6 +314,18 @@ trap teardown EXIT SYSROOT="${SYSROOT:-$(pwd)/${TEST_DIR}/sysroot}" export SYSROOT +# 'make check' refreshes the sysroot through the setup-chroot rule, but +# running a test script by hand does not, and then the test silently +# exercises whatever finit was installed last. Compare and refuse. +top_builddir="${top_builddir:-$TEST_DIR/..}" +sysroot_finit="$SYSROOT/sbin/finit" +built_finit="$top_builddir/src/finit" +[ -x "$top_builddir/src/.libs/finit" ] && built_finit="$top_builddir/src/.libs/finit" + +if [ -x "$built_finit" ] && [ -e "$sysroot_finit" ] && ! cmp -s "$built_finit" "$sysroot_finit"; then + fail "Stale $sysroot_finit, run 'make -C test setup-chroot' or use 'make check'" +fi + TEST_TIMEOUT=300 # shellcheck source=/dev/null From 8a541acd365aabad8f423d3f71c03c5c73afc085 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sun, 26 Jul 2026 22:15:03 +0200 Subject: [PATCH 12/28] conf: settle the block format's key names and value shapes The initial implementation was done using a naive translator of the legacy one-liner format key by key, so it inherited encodings that the block format exists to remove: a timeout packed into a script path, a small comma-and-colon language inside the log string, sigils standing in for booleans, and a log key (services) carrying three (!) types. Settled naming against systemd, OpenRC, FreeBSD rc.subr, s6 and SMF. Match systemd's semantics, not its naming. pid -> pidfile, plus pidfile-create for the rare case where Finit writes the file rather than the daemon environment -> envfile, since it names a file to source, and the top-level environment {} block sets variables pre/post/... -> exec-start-pre, exec-start-ready, exec-stop, exec-stop-post, exec-reload, exec-cleanup, each with its own -timeout instead of "SEC,script" halt, kill -> stop-signal, stop-timeout restart -> restart for the policy, restart-max for the count log -> a block with file, priority and identity, where /dev/null and /dev/console are spelled as paths group -> group and extra-groups, no longer positional nowarn -> a leading - on command, as on envfile List-valued keys take plural names. Aliases are desc, cond, mod, caps, env, halt and kill; an alias may abbreviate the canonical name or preserve a legacy spelling, nothing else. Signed-off-by: Joachim Wiberg --- src/conf.c | 303 +++++++++++++++++++++++++++++++----------- test/conf-format.sh | 57 +++++++- test/conf-template.sh | 2 +- 3 files changed, 277 insertions(+), 85 deletions(-) diff --git a/src/conf.c b/src/conf.c index 9ed81922..f4d99f1e 100644 --- a/src/conf.c +++ b/src/conf.c @@ -193,6 +193,19 @@ static cfg_opt_t env_opts[] = { CFG_END() }; +/* + * log { file = "/var/log/foo" ... } -- where a service's output goes. + * /dev/null and /dev/console are spelled as paths, so there is one way + * to say it. An empty block means syslog with defaults; `log` cannot + * also be a scalar, libconfuse rejects a name declared as both. + */ +static cfg_opt_t svclog_opts[] = { + CFG_STR("file", NULL, CFGF_NODEFAULT), + CFG_STR("priority", NULL, CFGF_NODEFAULT), + CFG_STR("identity", NULL, CFGF_NODEFAULT), + CFG_END() +}; + /* log { size = 200k count = 5 } -- Finit's own log rotation */ static cfg_opt_t log_opts[] = { CFG_STR("size", NULL, CFGF_NODEFAULT), @@ -200,43 +213,59 @@ static cfg_opt_t log_opts[] = { CFG_END() }; +/* a lifecycle script and the bound on how long it may run */ +#define EXEC_OPTS(nm) \ + CFG_STR(nm, NULL, CFGF_NODEFAULT), \ + CFG_INT(nm "-timeout", 0, CFGF_NODEFAULT) + /* service NAME[:ID] { ... }, shared with task/run/sysv */ static cfg_opt_t svc_opts[] = { CFG_STR ("description", NULL, CFGF_NODEFAULT), CFG_STR ("desc", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("command", NULL, CFGF_NODEFAULT), - CFG_STR ("exec", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("runlevel", NULL, CFGF_NODEFAULT), - CFG_STR_LIST("condition", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("conditions", NULL, CFGF_NODEFAULT), CFG_STR_LIST("cond", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("user", NULL, CFGF_NODEFAULT), - CFG_STR_LIST("group", NULL, CFGF_NODEFAULT), - CFG_STR ("environment", NULL, CFGF_NODEFAULT), + CFG_STR ("group", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("extra-groups", NULL, CFGF_NODEFAULT), + CFG_STR ("envfile", NULL, CFGF_NODEFAULT), CFG_STR ("env", NULL, CFGF_NODEFAULT), /* alias */ - CFG_STR ("pid", NULL, CFGF_NODEFAULT), - CFG_STR ("log", NULL, CFGF_NODEFAULT), + CFG_STR ("pidfile", NULL, CFGF_NODEFAULT), + CFG_BOOL ("pidfile-create", cfg_false, CFGF_NODEFAULT), CFG_STR ("notify", NULL, CFGF_NODEFAULT), CFG_STR ("type", NULL, CFGF_NODEFAULT), CFG_BOOL ("manual", 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_INT ("restart-max", 0, 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 ("stop-signal", NULL, CFGF_NODEFAULT), + CFG_STR ("halt", NULL, CFGF_NODEFAULT), /* alias */ + CFG_INT ("stop-timeout", 0, CFGF_NODEFAULT), + CFG_INT ("kill", 0, CFGF_NODEFAULT), /* alias */ + + /* lifecycle scripts, each with its own bound */ + EXEC_OPTS("exec-start-pre"), + EXEC_OPTS("exec-start-ready"), + EXEC_OPTS("exec-stop"), + EXEC_OPTS("exec-stop-post"), + EXEC_OPTS("exec-reload"), + EXEC_OPTS("exec-cleanup"), + CFG_STR_LIST("capabilities", NULL, CFGF_NODEFAULT), CFG_STR_LIST("caps", NULL, CFGF_NODEFAULT), /* alias */ - CFG_STR_LIST("conflict", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("conflicts", NULL, CFGF_NODEFAULT), CFG_STR ("if", NULL, CFGF_NODEFAULT), CFG_STR ("tty", NULL, CFGF_NODEFAULT), + /* + * MULTI is not about repetition here, it is the only way to tell + * `log {}` from no log at all: libconfuse instantiates every + * non-MULTI section, so cfg_size() would always answer 1. + */ + CFG_SEC ("log", svclog_opts, CFGF_MULTI), CFG_SEC ("cgroup", cgroup_opts, CFGF_MULTI | CFGF_TITLE | CFGF_KEYSTRVAL), CFG_SEC ("rlimit", rlimit_opts, CFGF_NONE), CFG_END() @@ -245,7 +274,7 @@ static cfg_opt_t svc_opts[] = { /* 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("conditions", NULL, CFGF_NODEFAULT), CFG_STR_LIST("cond", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR ("device", NULL, CFGF_NODEFAULT), CFG_INT ("baud", 0, CFGF_NODEFAULT), @@ -254,7 +283,6 @@ static cfg_opt_t tty_opts[] = { 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() @@ -275,8 +303,8 @@ static cfg_opt_t conf_opts[] = { /* 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("modules", NULL, CFGF_NODEFAULT), + CFG_STR_LIST("mod", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR_LIST("mknod", NULL, CFGF_NODEFAULT), CFG_STR ("network", NULL, CFGF_NODEFAULT), CFG_STR ("rcsd", NULL, CFGF_NODEFAULT), @@ -960,15 +988,33 @@ 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) +/* which of KEY or ALIAS the user actually set, canonical wins */ +static const char *sec_key(cfg_t *sec, const char *key, const char *alias) { if (cfg_size(sec, key)) - return cfg_getstr(sec, key); + return key; if (alias && cfg_size(sec, alias)) - return cfg_getstr(sec, alias); + return alias; return NULL; } +static const char *sec_getstr(cfg_t *sec, const char *key, const char *alias) +{ + key = sec_key(sec, key, alias); + + return key ? cfg_getstr(sec, key) : NULL; +} + +static int sec_getint(cfg_t *sec, const char *key, const char *alias, long *val) +{ + key = sec_key(sec, key, alias); + if (!key) + return 0; + *val = cfg_getint(sec, key); + + return 1; +} + static char *sec_getlist(cfg_t *sec, const char *key, const char *alias, char *buf, size_t len) { unsigned int i, num; @@ -1082,6 +1128,73 @@ static void rlimit_translate(cfg_t *sec, struct rlimit arr[]) } } +/* + * exec-NAME [+ exec-NAME-timeout] -> legacy "tok:[SEC,]script" + */ +static void addscript(char *line, size_t len, cfg_t *sec, const char *key, + const char *tok) +{ + char tmo[64]; + const char *str; + char buf[512]; + + str = sec_getstr(sec, key, NULL); + if (!str) + return; + + snprintf(tmo, sizeof(tmo), "%s-timeout", key); + if (cfg_size(sec, tmo)) + snprintf(buf, sizeof(buf), "%ld,%s", cfg_getint(sec, tmo), str); + else + strlcpy(buf, str, sizeof(buf)); + + addopt(line, len, tok, buf); +} + +/* + * log { file = "..." priority = "..." identity = "..." } -> legacy + * "log" or "log:file,prio:P,tag:I". An empty block is syslog with + * defaults; /dev/null and /dev/console arrive here as a file. + */ +static void addlog(char *line, size_t len, cfg_t *sec, char *file) +{ + unsigned int num = cfg_size(sec, "log"); + char buf[512] = ""; + const char *str; + cfg_t *log; + + if (!num) + return; + + log = cfg_getnsec(sec, "log", num - 1); + if (!log) + return; + + if (num > 1) + logit(LOG_WARNING, "%s: %s declares %u log blocks, using the last", + file, cfg_title(sec), num); + + if ((str = sec_getstr(log, "file", NULL))) + strlcpy(buf, str, sizeof(buf)); + if ((str = sec_getstr(log, "priority", NULL))) { + if (buf[0]) + strlcat(buf, ",", sizeof(buf)); + strlcat(buf, "prio:", sizeof(buf)); + strlcat(buf, str, sizeof(buf)); + } + if ((str = sec_getstr(log, "identity", NULL))) { + if (buf[0]) + strlcat(buf, ",", sizeof(buf)); + strlcat(buf, "tag:", sizeof(buf)); + strlcat(buf, str, sizeof(buf)); + } + + if (buf[0]) + addopt(line, len, "log:", buf); + else + addtok(line, len, "log"); +} + /* * Translate service/task/run/sysv section to the canonical legacy * one-liner and register through the same path as legacy files. @@ -1090,23 +1203,30 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi { struct rlimit local_rlimit[RLIMIT_NLIMITS]; char line[LINE_SIZE] = ""; - const char *str, *cmd; - unsigned int num; + const char *str, *cmd, *grp; + unsigned int cnt; + int nowarn, own; + long num; char buf[512]; char nm[80]; char *id; - cmd = sec_getstr(sec, "command", "exec"); + cmd = sec_getstr(sec, "command", NULL); if (!cmd) { logit(LOG_ERR, "%s: section '%s' missing command, skipping", file, cfg_title(sec)); return; } + /* a leading - tolerates a missing binary, cf. systemd ExecStart=- */ + nowarn = cmd[0] == '-'; + if (nowarn) + cmd++; + if ((str = sec_getstr(sec, "runlevel", NULL))) addtok(line, sizeof(line), "[%s]", str); - if (sec_getlist(sec, "condition", "cond", buf, sizeof(buf))) + if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf))) addtok(line, sizeof(line), "<%s>", buf); /* section title is the identity: NAME[:ID], %i in templates */ @@ -1118,29 +1238,39 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi if (id && *id) addtok(line, sizeof(line), ":%s", id); - /* @user[:group[,supplementary,...]], first group is primary */ + /* @user[:group[,extra,...]] */ 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); + grp = sec_getstr(sec, "group", NULL); + if (sec_getlist(sec, "extra-groups", NULL, buf, sizeof(buf))) + addtok(line, sizeof(line), "@%s:%s,%s", str ? str : "root", + grp ? grp : "root", buf); + else if (grp) + addtok(line, sizeof(line), "@%s:%s", str ? str : "root", grp); else if (str) addtok(line, sizeof(line), "@%s", str); - if ((str = sec_getstr(sec, "environment", "env"))) + if ((str = sec_getstr(sec, "envfile", "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); - } + addlog(line, sizeof(line), sec, file); - 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); - } + /* + * The daemon owns its pidfile unless told otherwise, so the + * legacy ! is the default here and pidfile-create drops it. + */ + /* + * The daemon owns its pidfile unless told otherwise, so the + * legacy ! is the default here and pidfile-create drops it. A + * daemon-owned default path needs no token, that is what the + * pidfile plugin discovers on its own. + */ + str = sec_getstr(sec, "pidfile", NULL); + own = sec_getbool(sec, "pidfile-create"); + + if (str && strcmp(str, "true") && strcmp(str, "false")) + addopt(line, sizeof(line), own ? "pid:" : "pid:!", str); + else if (own) + addtok(line, sizeof(line), "pid"); if ((str = sec_getstr(sec, "notify", NULL))) addtok(line, sizeof(line), "notify:%s", str); @@ -1159,41 +1289,47 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi addtok(line, sizeof(line), "remain:yes"); if (sec_getbool(sec, "respawn")) addtok(line, sizeof(line), "respawn"); - if (sec_getbool(sec, "nowarn")) + if (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); + /* + * restart is the policy, restart-max the count. true is the + * default policy and needs no token unless a count came with it. + */ + str = sec_getstr(sec, "restart", NULL); + if (!str) + str = "true"; + if (!strcmp(str, "never") || !strcmp(str, "false")) + addtok(line, sizeof(line), "norestart"); + else if (!strcmp(str, "always")) + addtok(line, sizeof(line), "restart:always"); + else { + if (strcmp(str, "true")) + logit(LOG_WARNING, "%s: unknown restart '%s', assuming true", + file, str); + if (sec_getint(sec, "restart-max", NULL, &num)) + addtok(line, sizeof(line), "restart:%ld", num); } - if (cfg_size(sec, "restart_sec")) - addtok(line, sizeof(line), "restart_sec:%ld", cfg_getint(sec, "restart_sec")); + + if (sec_getint(sec, "restart-sec", NULL, &num)) + addtok(line, sizeof(line), "restart_sec:%ld", num); if ((str = sec_getstr(sec, "oncrash", NULL))) addtok(line, sizeof(line), "oncrash:%s", str); - if ((str = sec_getstr(sec, "halt", NULL))) + if ((str = sec_getstr(sec, "stop-signal", "halt"))) 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_getint(sec, "stop-timeout", "kill", &num)) + addtok(line, sizeof(line), "kill:%ld", num); + + addscript(line, sizeof(line), sec, "exec-start-pre", "pre:"); + addscript(line, sizeof(line), sec, "exec-start-ready", "ready:"); + addscript(line, sizeof(line), sec, "exec-stop", "stop:"); + addscript(line, sizeof(line), sec, "exec-stop-post", "post:"); + addscript(line, sizeof(line), sec, "exec-reload", "reload:"); + addscript(line, sizeof(line), sec, "exec-cleanup", "cleanup:"); if (sec_getlist(sec, "capabilities", "caps", buf, sizeof(buf))) addtok(line, sizeof(line), "caps:%s", buf); - if (sec_getlist(sec, "conflict", NULL, buf, sizeof(buf))) + if (sec_getlist(sec, "conflicts", NULL, buf, sizeof(buf))) addtok(line, sizeof(line), "conflict:%s", buf); if ((str = sec_getstr(sec, "if", NULL))) @@ -1206,13 +1342,13 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi * 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); + cnt = cfg_size(sec, "cgroup"); + if (cnt) { + cfg_t *cg = cfg_getnsec(sec, "cgroup", cnt - 1); - if (num > 1) + if (cnt > 1) logit(LOG_WARNING, "%s: %s declares %u cgroup blocks," - " using '%s'", file, cfg_title(sec), num, + " using '%s'", file, cfg_title(sec), cnt, cfg_title(cg)); cgroup_settings(cg, buf, sizeof(buf)); @@ -1252,11 +1388,15 @@ static void tty_translate(cfg_t *sec, struct rlimit rlimit[], char *file) if ((str = sec_getstr(sec, "runlevel", NULL))) addtok(line, sizeof(line), "[%s]", str); - if (sec_getlist(sec, "condition", "cond", buf, sizeof(buf))) + if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf))) addtok(line, sizeof(line), "<%s>", buf); dev = sec_getstr(sec, "device", NULL); - cmd = sec_getstr(sec, "command", "exec"); + cmd = sec_getstr(sec, "command", NULL); + if (cmd && cmd[0] == '-') { + addtok(line, sizeof(line), "nowarn"); + cmd++; + } if (dev) { addtok(line, sizeof(line), "%s", dev); @@ -1320,11 +1460,11 @@ static void env_translate(cfg_t *cfg, const char *section) */ static void conf_parse_statics(cfg_t *cfg) { + const char *str, *key; unsigned int i; - const char *str; if (BOOTSTRAP) { - if ((str = sec_getstr(cfg, "hostname", "host"))) { + if ((str = sec_getstr(cfg, "hostname", NULL))) { if (hostname) free(hostname); hostname = strdup(str); @@ -1339,8 +1479,9 @@ static void conf_parse_statics(cfg_t *cfg) run_interactive(cmd, "Creating device node %s", dev); } - for (i = 0; i < cfg_size(cfg, "module"); i++) - kmod_load(cfg_getnstr(cfg, "module", i)); + key = sec_key(cfg, "modules", "mod"); + for (i = 0; key && i < cfg_size(cfg, key); i++) + kmod_load(cfg_getnstr(cfg, key, i)); if ((str = sec_getstr(cfg, "network", NULL))) { if (network) diff --git a/test/conf-format.sh b/test/conf-format.sh index 605c3170..d31154fd 100755 --- a/test/conf-format.sh +++ b/test/conf-format.sh @@ -23,7 +23,7 @@ run envprobe {\n\ test_teardown() { say "Running test teardown." - run "rm -f $FINIT_CONF /tmp/envprobe-*" + run "rm -f $FINIT_CONF /tmp/envprobe-* /tmp/pre /run/blockfmt.pid /run/notmine.pid" run "rm -f $FINIT_RCSD/legacy-side.conf" } @@ -65,8 +65,8 @@ 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" +run "echo ' stop-timeout = 20' >> $FINIT_CONF" +run "echo ' log { }' >> $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. @@ -131,3 +131,54 @@ run "initctl reload" retry 'assert_num_children 1 service.sh' assert_desc "Test service" service.sh + +# The renamed and split settings each have translation logic behind +# them, so exercise the ones with a visible effect. +# Only the warning differs, service_register() bails before svc_new() +# either way, so this asserts the stanza is skipped, not that the +# warning was suppressed. The emitted one-liner is where nowarn is +# visible, with finit.debug=on. +say 'A leading - on command tolerates a missing binary' +run "echo 'service ghost {' > $FINIT_CONF" +run "echo ' description = \"Ghost\"' >> $FINIT_CONF" +run "echo ' command = \"-/no/such/binary\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +assert_num_services 0 ghost + +say 'exec-start-pre runs before the service, pidfile-create makes Finit own the file' +run "echo 'service service.sh {' > $FINIT_CONF" +run "echo ' description = \"Test service\"' >> $FINIT_CONF" +run "echo ' conditions = { \"hook/svc/up\" }' >> $FINIT_CONF" +run "echo ' exec-start-pre = \"/bin/pre.sh\"' >> $FINIT_CONF" +run "echo ' exec-start-pre-timeout = 5' >> $FINIT_CONF" +run "echo ' pidfile = \"/run/blockfmt.pid\"' >> $FINIT_CONF" +run "echo ' pidfile-create = true' >> $FINIT_CONF" +run "echo ' restart = true' >> $FINIT_CONF" +run "echo ' restart-max = 3' >> $FINIT_CONF" +run "echo ' command = \"service.sh\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +retry 'assert_num_children 1 service.sh' +retry 'assert_file_exists /tmp/pre' +assert_restart_cnt 0 "0/3" service.sh + +# service.sh writes /run/service.pid, never /run/blockfmt.pid, so only +# Finit can have created this one. assert_is_pidfile cannot tell, it +# prints the path with any leading ! stripped. +retry 'assert_file_exists /run/blockfmt.pid' + +say 'Without pidfile-create the daemon owns the file, Finit does not make it' +run "echo 'service service.sh {' > $FINIT_CONF" +run "echo ' description = \"Test service\"' >> $FINIT_CONF" +run "echo ' pidfile = \"/run/notmine.pid\"' >> $FINIT_CONF" +run "echo ' command = \"service.sh\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "rm -f /run/notmine.pid" +run "initctl reload" + +retry 'assert_num_children 1 service.sh' +assert "daemon-owned pidfile is not created by Finit" \ + "$(texec ls /run/notmine.pid 2>/dev/null)" = "" diff --git a/test/conf-template.sh b/test/conf-template.sh index 344be694..5cbeae36 100755 --- a/test/conf-template.sh +++ b/test/conf-template.sh @@ -25,7 +25,7 @@ test_teardown() say 'Install a block-format template' run "echo 'service serv:%i {' > $FINIT_RCSD/available/serv@.conf" run "echo ' description = \"Template for %i\"' >> $FINIT_RCSD/available/serv@.conf" -run "echo ' pid = \"/run/serv-%i.pid\"' >> $FINIT_RCSD/available/serv@.conf" +run "echo ' pidfile = \"/run/serv-%i.pid\"' >> $FINIT_RCSD/available/serv@.conf" run "echo ' command = \"serv -n -p -P /run/serv-%i.pid\"' >> $FINIT_RCSD/available/serv@.conf" run "echo '}' >> $FINIT_RCSD/available/serv@.conf" From f48f8aacecc21065d294600abe644da04b4fad81 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 27 Jul 2026 11:27:54 +0200 Subject: [PATCH 13/28] conf: replace the '!' condition prefix with reload-signal and required A condition list could be led by '!', which is not a condition and not a negation. It is a flag on the block, and it means two unrelated things depending on which block it sits in: a service or sysv does not handle SIGHUP and must be restarted to reload, while a run or task must not hold up bootstrap. Writing '' with no condition at all is legal, which gives away that it was never an operator. Give each meaning its own key, valid only where it applies: service foo { reload-signal = "none" } # restart to reload task bar { required = false } # do not hold up bootstrap Using either on a block type it does not apply to warns, as does a '!' left in a conditions list. Both still translate to that same '!', which is all a legacy line can carry, so reload-signal takes SIGHUP or none for now; str2sig() already accepts any case and an optional SIG prefix. This also clears the way for the conditions list to grow real operators, '+' and '-' for asserted and deasserted, without '!' sitting among them meaning something else entirely. The '~' prefix stays. It belongs to the list: it marks a dependency whose reload should propagate here. Signed-off-by: Joachim Wiberg --- src/conf.c | 46 ++++++++++++++++++++++++++++++++++++++++++--- test/conf-format.sh | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/src/conf.c b/src/conf.c index f4d99f1e..4154b0fa 100644 --- a/src/conf.c +++ b/src/conf.c @@ -242,6 +242,8 @@ static cfg_opt_t svc_opts[] = { CFG_INT ("restart-max", 0, CFGF_NODEFAULT), CFG_INT ("restart-sec", 0, CFGF_NODEFAULT), CFG_STR ("oncrash", NULL, CFGF_NODEFAULT), + CFG_STR ("reload-signal", NULL, CFGF_NODEFAULT), + CFG_BOOL ("required", cfg_true, CFGF_NODEFAULT), CFG_STR ("stop-signal", NULL, CFGF_NODEFAULT), CFG_STR ("halt", NULL, CFGF_NODEFAULT), /* alias */ CFG_INT ("stop-timeout", 0, CFGF_NODEFAULT), @@ -1204,8 +1206,8 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi struct rlimit local_rlimit[RLIMIT_NLIMITS]; char line[LINE_SIZE] = ""; const char *str, *cmd, *grp; + int nowarn, own, bang; unsigned int cnt; - int nowarn, own; long num; char buf[512]; char nm[80]; @@ -1226,8 +1228,46 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi if ((str = sec_getstr(sec, "runlevel", NULL))) addtok(line, sizeof(line), "[%s]", str); - if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf))) - addtok(line, sizeof(line), "<%s>", buf); + /* + * The ! leading a legacy condition list is not a condition at + * all, and it means two unrelated things depending on the block + * it sits in. Each has its own key here, valid only where the + * meaning applies, and both translate back to that same !. + */ + bang = 0; + if (type == SVC_TYPE_RUN || type == SVC_TYPE_TASK) { + if (cfg_size(sec, "required") && !sec_getbool(sec, "required")) + bang = 1; /* do not hold up bootstrap */ + if (cfg_size(sec, "reload-signal")) + logit(LOG_WARNING, "%s: %s: reload-signal does not apply" + " to run or task, ignoring", file, cfg_title(sec)); + } else { + if ((str = sec_getstr(sec, "reload-signal", NULL))) { + /* + * Only SIGHUP and none survive the trip through a + * legacy line, which carries the flag and not the + * signal. + */ + if (!strcasecmp(str, "none")) + bang = 1; + else if (str2sig((char *)str) != SIGHUP) + logit(LOG_WARNING, "%s: %s: reload-signal is SIGHUP" + " or none, ignoring '%s'", file, + cfg_title(sec), str); + } + if (cfg_size(sec, "required")) + logit(LOG_WARNING, "%s: %s: required only applies to run" + " or task, ignoring", file, cfg_title(sec)); + } + + if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf))) { + if (buf[0] == '!') + logit(LOG_WARNING, "%s: %s: '!' in conditions is not" + " supported, use reload-signal or required", + file, cfg_title(sec)); + addtok(line, sizeof(line), "<%s%s>", bang ? "!" : "", buf); + } else if (bang) + addtok(line, sizeof(line), ""); /* section title is the identity: NAME[:ID], %i in templates */ strlcpy(nm, cfg_title(sec), sizeof(nm)); diff --git a/test/conf-format.sh b/test/conf-format.sh index d31154fd..6d72e6fc 100755 --- a/test/conf-format.sh +++ b/test/conf-format.sh @@ -182,3 +182,39 @@ run "initctl reload" retry 'assert_num_children 1 service.sh' assert "daemon-owned pidfile is not created by Finit" \ "$(texec ls /run/notmine.pid 2>/dev/null)" = "" + +say 'reload-signal = none emits the legacy noreload flag' +run "echo 'service service.sh {' > $FINIT_CONF" +run "echo ' description = \"Test service\"' >> $FINIT_CONF" +run "echo ' reload-signal = \"none\"' >> $FINIT_CONF" +run "echo ' command = \"service.sh\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +retry 'assert_num_children 1 service.sh' +assert_desc "Test service" service.sh + +say 'Case and short forms of SIGHUP are all the default, no flag' +for s in SIGHUP sighup HUP hup; do + run "echo 'service service.sh {' > $FINIT_CONF" + run "echo ' description = \"Test service\"' >> $FINIT_CONF" + run "echo \" reload-signal = '$s'\" >> $FINIT_CONF" + run "echo ' command = \"service.sh\"' >> $FINIT_CONF" + run "echo '}' >> $FINIT_CONF" + run "initctl reload" + retry 'assert_num_children 1 service.sh' +done + +# 'required' and 'reload-signal' both translate to the legacy ! that +# leads the condition list, but each is valid only for the block types +# where that ! carries its meaning. +say 'required = false on a task does not hold up bootstrap' +run "echo 'task pwrfail {' > $FINIT_CONF" +run "echo ' description = \"Power failure\"' >> $FINIT_CONF" +run "echo ' conditions = { \"sys/pwr/fail\" }' >> $FINIT_CONF" +run "echo ' required = false' >> $FINIT_CONF" +run "echo ' command = \"/bin/true\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +retry 'assert_desc "Power failure" pwrfail' From 13d58ace9f523ff8293c5296fd5d468c771b5db3 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 27 Jul 2026 12:53:33 +0200 Subject: [PATCH 14/28] conf: rename remain and manual for what they actually do Both keys prompt the question they should be answering. 'remain' decides whether a finished run or task keeps existing: without it the entry is pruned, so the work re-runs on every runlevel entry, initctl cannot see it, and its post script never fires. With it the entry stays, is not re-run, and gets a teardown when stopped or when it leaves its runlevels. That is systemd's RemainAfterExit, and 'remain' is that name with the informative half cut off. 'manual' says how a service is started but not that it is about starting at all. remain -> remain-after-exit manual -> manual-start Both keep their old spelling as an alias, which they qualify for twice over, as abbreviations of the canonical name and as the legacy spellings. While here, give sec_getbool() the alias argument its string and list counterparts already take. Signed-off-by: Joachim Wiberg --- src/conf.c | 37 ++++++++++++++++++++----------------- test/conf-format.sh | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/conf.c b/src/conf.c index 4154b0fa..947f1854 100644 --- a/src/conf.c +++ b/src/conf.c @@ -235,8 +235,10 @@ static cfg_opt_t svc_opts[] = { CFG_BOOL ("pidfile-create", cfg_false, 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 ("manual-start", cfg_false, CFGF_NODEFAULT), + CFG_BOOL ("manual", cfg_false, CFGF_NODEFAULT), /* alias */ + CFG_BOOL ("remain-after-exit", cfg_false, CFGF_NODEFAULT), + CFG_BOOL ("remain", cfg_false, CFGF_NODEFAULT), /* alias */ CFG_BOOL ("respawn", cfg_false, CFGF_NODEFAULT), CFG_STR ("restart", NULL, CFGF_NODEFAULT), CFG_INT ("restart-max", 0, CFGF_NODEFAULT), @@ -1038,9 +1040,10 @@ static char *sec_getlist(cfg_t *sec, const char *key, const char *alias, char *b return buf; } -static int sec_getbool(cfg_t *sec, const char *key) +static int sec_getbool(cfg_t *sec, const char *key, const char *alias) { - if (!cfg_size(sec, key)) + key = sec_key(sec, key, alias); + if (!key) return 0; return cfg_getbool(sec, key) == cfg_true; @@ -1236,7 +1239,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi */ bang = 0; if (type == SVC_TYPE_RUN || type == SVC_TYPE_TASK) { - if (cfg_size(sec, "required") && !sec_getbool(sec, "required")) + if (cfg_size(sec, "required") && !sec_getbool(sec, "required", NULL)) bang = 1; /* do not hold up bootstrap */ if (cfg_size(sec, "reload-signal")) logit(LOG_WARNING, "%s: %s: reload-signal does not apply" @@ -1305,7 +1308,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi * pidfile plugin discovers on its own. */ str = sec_getstr(sec, "pidfile", NULL); - own = sec_getbool(sec, "pidfile-create"); + own = sec_getbool(sec, "pidfile-create", NULL); if (str && strcmp(str, "true") && strcmp(str, "false")) addopt(line, sizeof(line), own ? "pid:" : "pid:!", str); @@ -1323,11 +1326,11 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi file, str); } - if (sec_getbool(sec, "manual")) + if (sec_getbool(sec, "manual-start", "manual")) addtok(line, sizeof(line), "manual:yes"); - if (sec_getbool(sec, "remain")) + if (sec_getbool(sec, "remain-after-exit", "remain")) addtok(line, sizeof(line), "remain:yes"); - if (sec_getbool(sec, "respawn")) + if (sec_getbool(sec, "respawn", NULL)) addtok(line, sizeof(line), "respawn"); if (nowarn) addtok(line, sizeof(line), "nowarn"); @@ -1445,9 +1448,9 @@ static void tty_translate(cfg_t *sec, struct rlimit rlimit[], char *file) } else if (cmd) { addtok(line, sizeof(line), "%s", cmd); } else { - if (sec_getbool(sec, "notty")) + if (sec_getbool(sec, "notty", NULL)) addtok(line, sizeof(line), "notty"); - if (sec_getbool(sec, "rescue")) + if (sec_getbool(sec, "rescue", NULL)) addtok(line, sizeof(line), "rescue"); if (!line[0]) { @@ -1457,11 +1460,11 @@ static void tty_translate(cfg_t *sec, struct rlimit rlimit[], char *file) } } - if (sec_getbool(sec, "noclear")) + if (sec_getbool(sec, "noclear", NULL)) addtok(line, sizeof(line), "noclear"); - if (sec_getbool(sec, "nowait")) + if (sec_getbool(sec, "nowait", NULL)) addtok(line, sizeof(line), "nowait"); - if (sec_getbool(sec, "nologin")) + if (sec_getbool(sec, "nologin", NULL)) addtok(line, sizeof(line), "nologin"); if (dev && (str = sec_getstr(sec, "term", NULL))) @@ -1539,8 +1542,8 @@ static void conf_parse_statics(cfg_t *cfg) if (runparts) free(runparts); runparts = strdup(str); - runparts_progress = sec_getbool(cfg, "runparts-progress"); - runparts_sysv = sec_getbool(cfg, "runparts-sysv"); + runparts_progress = sec_getbool(cfg, "runparts-progress", NULL); + runparts_sysv = sec_getbool(cfg, "runparts-sysv", NULL); } if (cfg_size(cfg, "runlevel")) { @@ -1597,7 +1600,7 @@ static void conf_parse_statics(cfg_t *cfg) } if (cfg_size(cfg, "reboot-watchdog")) - wdtreboot = sec_getbool(cfg, "reboot-watchdog"); + wdtreboot = sec_getbool(cfg, "reboot-watchdog", NULL); if (cfg_size(cfg, "service-interval")) { long val = cfg_getint(cfg, "service-interval"); diff --git a/test/conf-format.sh b/test/conf-format.sh index 6d72e6fc..76bbd1b1 100755 --- a/test/conf-format.sh +++ b/test/conf-format.sh @@ -218,3 +218,42 @@ run "echo '}' >> $FINIT_CONF" run "initctl reload" retry 'assert_desc "Power failure" pwrfail' + +# remain-after-exit keeps a completed task in the service list, so it +# is still visible and can be stopped. The alias must reach the same +# legacy token. +for key in remain-after-exit remain; do + say "$key keeps a completed task visible" + run "echo 'task setup {' > $FINIT_CONF" + run "echo ' description = \"Setup task\"' >> $FINIT_CONF" + run "echo ' runlevel = \"2345\"' >> $FINIT_CONF" + run "echo \" $key = true\" >> $FINIT_CONF" + run "echo ' command = \"/bin/true\"' >> $FINIT_CONF" + run "echo '}' >> $FINIT_CONF" + run "initctl reload" + + retry 'assert_desc "Setup task" setup' +done + +# manual-start registers the service but does not start it, so both +# spellings must reach the legacy manual:yes token. +for key in manual-start manual; do + say "$key leaves the service stopped until asked" + run "echo 'service service.sh {' > $FINIT_CONF" + run "echo ' description = \"Manual service\"' >> $FINIT_CONF" + run "echo \" $key = true\" >> $FINIT_CONF" + run "echo ' command = \"service.sh\"' >> $FINIT_CONF" + run "echo '}' >> $FINIT_CONF" + run "initctl reload" + + retry 'assert_desc "Manual service" service.sh' + assert_num_children 0 service.sh + + run "initctl start service.sh" + retry 'assert_num_children 1 service.sh' + + # a started service survives the next reload, so clear it before + # the alias pass repeats the "stopped until asked" check + run "initctl stop service.sh" + retry 'assert_num_children 0 service.sh' +done From ddaef0600f6a452ba2df21bbc375f6bec966f48e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Jul 2026 10:11:23 +0200 Subject: [PATCH 15/28] doc: rewrite sample.conf in the block format This is what 'initctl create' and 'initctl edit -c' put in front of a user writing their first .conf file, so it is also the whole of the "initctl emits the new format" work: neither command generates syntax, they copy this file and open an editor on it. The ASCII diagram naming eight positional fields goes with it. A block has no positions to explain. Signed-off-by: Joachim Wiberg --- src/sample.conf | 116 +++++++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 45 deletions(-) diff --git a/src/sample.conf b/src/sample.conf index e6c6ceab..b742a523 100644 --- a/src/sample.conf +++ b/src/sample.conf @@ -1,62 +1,88 @@ -# sample.conf: template cgroup/run/task/service stanza for finit +# sample.conf: template run/task/service stanza for finit # -# The top-level cgroups can be defined anywhere, but share the same -# namespace. It is up to the user to ensure groups are defined only -# once, otherwise the last read .conf wins. See below for assigning -# run/task/services to cgroups. The below example creates the cgroup -# NAME with the following cpu and memory settings: +# A .conf file is a series of blocks. Every setting is a key inside +# one, so nothing has to be memorised by position: # -# cgroup NAME cpu.weight:1000 mem.max:65 +# service NAME { +# description = "What it is" +# runlevel = "2345" # default: 234 +# conditions = { "net/lo/up" } # wait for these +# envfile = "-/etc/default/daemon" +# command = "/usr/sbin/daemon ARGS" +# } # -# The one-shot commands 'run' and 'task' are called only once per -# runlevel. Finit does not restart them when they exit. +# A leading '-' on a path means carry on if it is missing, so the +# envfile above is optional. Debian and Buildroot keep those files in +# /etc/default, Alpine in /etc/conf.d. # -# - run : wait for completion before continuing with next stanza -# - task : like run but started in background, parallel with other stanzas +# There are four kinds of stanza: +# +# - run : one-shot, wait for it before continuing with the next +# - task : one-shot, started in parallel with the next # - service : supervised daemon, run in foreground, restarted if it crashes +# - sysv : /etc/init.d style script, start/stop/restart +# +# The top-level cgroups can be defined anywhere, but share the same +# namespace. It is up to the user to ensure groups are defined only +# once, otherwise the last read .conf wins: +# +# cgroup NAME { +# cpu.weight = 1000 +# memory.max = 65M +# } +# +# A service joins one by naming it, and may override settings for +# itself alone. Here foo and bar share the group foo, baz has its own: # -# The env: is an optional path to a file with environment variables to -# adjust the behavior of daemons. Debian and Buildroot use /etc/default -# while Alpine use /etc/conf.d -- ensure your Finit is built correctly. -# The leading '-' determines if its OK to start the service even if the -# env file is missing. -# -# run [LVLS] log command ARGS -- Command -# task [LVLS] log command ARGS -- Command -# service [LVLS] log env:[-]/etc/default/daemon daemon ARGS -- Daemon daemon -# ^ ^ ^ ^ ^ ^ ^ ^ -# | | | | | | | `-- Optional description -# | | | | | | `----------- Daemon arguments -# | | | | | `-------------------------- Path to daemon -# | | | | `---------------------------------------------------- Optional env. file -# | | | `-------------------------------------------------------- Redirect output to log -# | | `--------------------------------------------------------------- Optional conditions -# | `---------------------------------------------------------------------- Optional Runlevels -# `------------------------------------------------------------------------------ Monitored application -# -# Each stanza can also hold a 'cgroup' argument, or be prefixed with -# cgroup.NAME to place all following stanzas in the same group. In -# the following example, foo and bar share the cgroup foo but baz -# runs in its own cgroup baz: -# -# cgroup.foo -# service cgroup:cpu.weight:250,mem.max:655350 foo args -- foo desc -# service cgroup:cpu.weight:150,mem.max:655350 bar args -- bar desc -# service cgroup.baz:cpu.weight:300 baz args -- baz desc +# service foo { cgroup foo {} command = "foo args" } +# service bar { cgroup foo { cpu.weight = 150 } command = "bar args" } +# service baz { cgroup baz { cpu.weight = 300 } command = "baz args" } # Debian GNU/Linux: start SSH daemon as soon as basic networking comes up -#service [2345] env:-/etc/default/ssh /usr/sbin/sshd -D $SSHD_OPTS -- OpenSSH daemon +#service sshd { +# description = "OpenSSH daemon" +# runlevel = "2345" +# conditions = { "net/lo/up" } +# envfile = "-/etc/default/ssh" +# command = "/usr/sbin/sshd -D $SSHD_OPTS" +#} # Alpine Linux: Oneshot task to run once at bootstrap, yes pipes are possible :) -#task [S] env:/etc/conf.d/loadkmap zcat $KEYMAP | loadkmap -- Loading keymap +#run kmap { +# description = "Loading keymap" +# runlevel = "S" +# envfile = "/etc/conf.d/loadkmap" +# command = "zcat $KEYMAP | loadkmap" +#} # Alpine Linux: start SSH daemon with $DROPBEAR_OPTS from /etc/conf.d -#service [2345] cgroup.user env:-/etc/conf.d/dropbear dropbear -R -F $DROPBEAR_OPTS -- Dropbear SSH daemon +#service dropbear { +# description = "Dropbear SSH daemon" +# runlevel = "2345" +# envfile = "-/etc/conf.d/dropbear" +# cgroup user {} +# command = "dropbear -R -F $DROPBEAR_OPTS" +#} # Handle PWR button to shutdown/reboot -- useful in Qemu (virt-manager) -# Depends on syslogd having started. Redirect any output to log. -#service [2345] cgroup:mem.max:32000,cpu.max:1000 log acpid -f -- ACPI daemon +# Depends on syslogd having started. Redirect any output to the log. +#service acpid { +# description = "ACPI daemon" +# runlevel = "2345" +# conditions = { "pid/syslogd" } +# cgroup system { +# memory.max = 32M +# cpu.max = 1000 +# } +# log { } +# command = "acpid -f" +#} # Start rsyslogd as soon as possible, should always run # Provides pid/syslogd condition -#service [S12345] name:syslogd env:-/etc/default/rsyslog rsyslogd -n $RSYSLOGD_OPTIONS -- Reliable syslog daemon +#service syslogd { +# description = "Reliable syslog daemon" +# runlevel = "S12345" +# envfile = "-/etc/default/rsyslog" +# command = "rsyslogd -n $RSYSLOGD_OPTIONS" +#} From ecac1e58e31c7cd9048dd87f98a805ba424cc2bb Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Jul 2026 11:39:37 +0200 Subject: [PATCH 16/28] conf: make cgroup delegate and leaf name first-class keys parse_cgroup() takes two arguments that are not cgroupfs files: the leaf directory to place the service in, and whether to hand the subtree over to it. The block format could express neither. 'name' happened to work, because a free-form key is emitted as name:VALUE and that is what the parser looks for, but 'delegate' came out as delegate:true and was filed as a cgroup setting, so it silently did nothing. Declare both, and emit delegate as the bare flag the parser expects. Neither means anything on a top-level group definition, so say so there rather than emitting something that would be written to cgroupfs. service podman { cgroup containers { name = "podman" delegate = true } ... } Signed-off-by: Joachim Wiberg --- src/conf.c | 32 +++++++++++++++++++++++++++++--- test/conf-format.sh | 17 +++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/conf.c b/src/conf.c index 947f1854..aef89a85 100644 --- a/src/conf.c +++ b/src/conf.c @@ -136,6 +136,14 @@ static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_ * cfg_error_cb() buffers it and only a failed parse is reported. */ static cfg_opt_t cgroup_opts[] = { + /* + * Not cgroupfs files but arguments to parse_cgroup(): the leaf + * directory to place the service in, and whether to hand the + * subtree over to it. Only meaningful inside a service block. + */ + CFG_STR ("name", NULL, CFGF_NODEFAULT), + CFG_BOOL("delegate", cfg_false, CFGF_NODEFAULT), + CFG_STR("cpu.weight", NULL, CFGF_NODEFAULT), CFG_STR("cpu.weight.nice", NULL, CFGF_NODEFAULT), CFG_STR("cpu.max", NULL, CFGF_NODEFAULT), @@ -1079,11 +1087,27 @@ static void addopt(char *line, size_t len, const char *opt, const char *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) +static char *cgroup_settings(cfg_t *cg, char *buf, size_t len, char *file, int svc) { cfg_opt_t *opt; buf[0] = 0; + + /* + * delegate is a flag, so it carries no value, and a top-level + * group has nothing to delegate to. + */ + if (cfg_size(cg, "delegate")) { + if (!svc) + logit(LOG_WARNING, "%s: cgroup %s: delegate only applies" + " inside a service, ignoring", file, cfg_title(cg)); + else if (cfg_getbool(cg, "delegate") == cfg_true) + strlcat(buf, "delegate", len); + } + if (!svc && cfg_size(cg, "name")) + logit(LOG_WARNING, "%s: cgroup %s: name only applies inside a" + " service, ignoring", file, cfg_title(cg)); + for (opt = cg->opts; opt && opt->name; opt++) { const char *val; @@ -1093,6 +1117,8 @@ static char *cgroup_settings(cfg_t *cg, char *buf, size_t len) val = cfg_opt_getnstr(opt, 0); if (!val) continue; + if (!svc && !strcmp(opt->name, "name")) + continue; if (buf[0]) strlcat(buf, ",", len); @@ -1394,7 +1420,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi " using '%s'", file, cfg_title(sec), cnt, cfg_title(cg)); - cgroup_settings(cg, buf, sizeof(buf)); + cgroup_settings(cg, buf, sizeof(buf), file, 1); if (buf[0]) addtok(line, sizeof(line), "cgroup.%s,%s", cfg_title(cg), buf); else @@ -1666,7 +1692,7 @@ static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd) char buf[512]; cgroup_add((char *)cfg_title(cg), - cgroup_settings(cg, buf, sizeof(buf)), 0); + cgroup_settings(cg, buf, sizeof(buf), file, 0), 0); } /* file-scope resource limits, applies to all services in file */ diff --git a/test/conf-format.sh b/test/conf-format.sh index 76bbd1b1..44da20a8 100755 --- a/test/conf-format.sh +++ b/test/conf-format.sh @@ -257,3 +257,20 @@ for key in manual-start manual; do run "initctl stop service.sh" retry 'assert_num_children 0 service.sh' done + +# delegate is a flag to parse_cgroup(), not a cgroupfs file, and the +# cgroup leaf name is an argument too. Both must come out of the +# translator in the comma-separated form that parser expects. +say 'cgroup delegate and name translate as parse_cgroup arguments' +run "echo 'service service.sh {' > $FINIT_CONF" +run "echo ' description = \"Delegated\"' >> $FINIT_CONF" +run "echo ' cgroup system {' >> $FINIT_CONF" +run "echo ' name = \"mysvc\"' >> $FINIT_CONF" +run "echo ' delegate = true' >> $FINIT_CONF" +run "echo ' cpu.weight = 250' >> $FINIT_CONF" +run "echo ' }' >> $FINIT_CONF" +run "echo ' command = \"service.sh\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +retry 'assert_desc "Delegated" service.sh' From b9ad9bcb21e9ebb5e66eb2948192a53e08f80a4a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Jul 2026 11:39:38 +0200 Subject: [PATCH 17/28] doc: convert the documentation to the block format The syntax overview no longer describes a line-based format, since that is not what the rest of the documentation shows. It now covers the grammar, the two naming conventions, the nine aliases, and the leading '-' on a path, and it says plainly that both formats are still read and told apart per file by content. Without that, a reader with an existing configuration is left wondering what happened to it. service-opts.md was a list of modifiers to place between a directive and its command, so it needed rewriting rather than translating: there are no positions left to describe. It is now grouped by what the settings do. conditions.md needed correcting. It presented '!' as a condition prefix alongside '~'. It is neither a condition nor a negation, it is a flag on the block that means one thing on a service and another on a run or task, so it is spelled reload-signal and required here, and the page maps the old form to both. Two things the pages claimed are not true. The kill delay range is 1-300, not 1-60, and stop and reload scripts are no longer run without a timeout. ChangeLog.md keeps its line-based examples. Those sit in historical release entries, and rewriting them in a syntax that did not exist at the time would misdate the format. Signed-off-by: Joachim Wiberg --- doc/build.md | 6 +- doc/conditions.md | 77 +++++++--- doc/config/cgroups.md | 193 +++++++++++++----------- doc/config/env.md | 13 +- doc/config/index.md | 109 ++++++++++++-- doc/config/logging.md | 17 ++- doc/config/rescue.md | 10 +- doc/config/runlevels.md | 12 +- doc/config/service-env.md | 15 +- doc/config/service-opts.md | 260 +++++++++++++++++++-------------- doc/config/service-sync.md | 19 ++- doc/config/service-wrappers.md | 7 +- doc/config/services.md | 234 ++++++++++++++++++++--------- doc/config/sysv.md | 14 +- doc/config/task-and-run.md | 36 +++-- doc/config/templating.md | 10 +- doc/config/tty.md | 52 ++++++- doc/example.md | 41 ++++-- doc/features.md | 14 +- doc/service.md | 41 ++++-- 20 files changed, 809 insertions(+), 371 deletions(-) diff --git a/doc/build.md b/doc/build.md index 6950fc8a..ae25f7b8 100644 --- a/doc/build.md +++ b/doc/build.md @@ -171,7 +171,11 @@ Notice the `--` separator. To debug startup issues, in particular issues with getty/login, add the following to your Finit .conf file: - tty [12345789] notty noclear + tty board { + runlevel = "12345789" + notty = true + noclear = true + } The `notty` option ensures reusing the stdin/stdout set up by the kernel. Remember, this is only for debugging and would leave your diff --git a/doc/conditions.md b/doc/conditions.md index aac0eadf..036278d6 100644 --- a/doc/conditions.md +++ b/doc/conditions.md @@ -14,12 +14,21 @@ specified separated by comma. Multiple conditions are logically AND'ed during evaluation, i.e. all conditions must be satisfied in order for a service to run. -Two special prefixes can be used inside the angle brackets: +One prefix can be used on a condition: - - `!` -- service does not support `SIGHUP` (noreload), or run/task - should not block runlevel changes (i.e., bootstrap) - `~` -- propagate reload from this dependency, see below +The line-based format also accepts a leading `!` on the list, which is +not a condition and not a negation. It is a flag on the block, and it +means two unrelated things depending on where it sits: a service does +not support `SIGHUP` (noreload), or a run/task should not block +runlevel changes, i.e. bootstrap. Each of those is its own key here: + +| Line-based | Block format | +|---|---| +| `` on a service or sysv | `reload-signal = "none"` | +| `` on a run or task | `required = false` | + Finit guarantees by default that all run/tasks run (at least) once per runlevel. For most tasks this is a good default, for example checking SSH host keys or loading keymap. However, for conditions @@ -27,7 +36,12 @@ that are unlikely to happen it is not. (See example below.) ### Example - service [2345] /sbin/netd -- Network monitor + service netd { + description = "Network monitor" + runlevel = "2345" + conditions = { "pid/setupd", "pid/zebra" } + command = "/sbin/netd" + } In this example the Network monitor daemon `netd` is not started until both the `pid/setupd` *and* `pid/zebra` conditions are satisfied. A @@ -42,15 +56,28 @@ being created, i.e., the service's default readiness notification. Another example is `dropbear`, it does not support `SIGHUP`, but we can also see optional sourcing of arguments from an environment file: - service [2345789] env:-/etc/default/dropbear dropbear -F -R $DROPBEAR_ARGS -- Dropbear SSH daemon + service dropbear { + description = "Dropbear SSH daemon" + runlevel = "2345789" + reload-signal = "none" + envfile = "-/etc/default/dropbear" + command = "dropbear -F -R $DROPBEAR_ARGS" + } Finally, the weird "block runlevel changes" example. Here we see what happens when Finit receives `SIGPWR`, sent from a power daemon like [powstatd(8)][]. A condition is asserted and a user can set up their own task to act on it. We do not want this task to block Finit from -moving to the next runlevel after bootstrap, so we set ``: +moving to the next runlevel after bootstrap, so we set +`required = false`: - task [S0123456789] name:pwrfail initctl poweroff -- Power failure, shutting down + task pwrfail { + description = "Power failure, shutting down" + runlevel = "S0123456789" + conditions = { "sys/pwr/fail" } + required = false + command = "initctl poweroff" + } Propagating Reload in Dependencies @@ -65,13 +92,23 @@ not care if it reloads its config. For services that need to react when their upstream reloads, the `~` prefix propagates the reload from the dependency: - service name:svc_b /sbin/svc_b -- Needs A (barrier) - service name:svc_c /sbin/svc_c -- Propagate reload from B + service svc_b { + description = "Needs A (barrier)" + conditions = { "pid/svc_a" } + command = "/sbin/svc_b" + } + + service svc_c { + description = "Propagate reload from B" + conditions = { "~pid/svc_b" } + reload-signal = "none" + command = "/sbin/svc_c" + } Here, `<~pid/svc_b>` means: propagate a reload of `svc_b` to `svc_c`. -When `svc_b` reloads, `svc_c` will be restarted (because of `!`, -noreload) instead of merely resumed. If `svc_c` supported `SIGHUP` -(no `!` prefix), it would be sent `SIGHUP` instead. +When `svc_b` reloads, `svc_c` will be restarted, because of +`reload-signal = "none"`, instead of merely resumed. If `svc_c` +supported `SIGHUP`, it would be sent `SIGHUP` instead. This is similar to systemd's `PropagatesReloadTo=` directive, but declared on the consumer side rather than the provider side. @@ -103,7 +140,12 @@ services, or run/task jobs, on external site-dependent stimuli. **Example:** - service [2345] alarm --arg foo -- Foo alarm + service alarm { + description = "Foo alarm" + runlevel = "2345" + conditions = { "usr/foo" } + command = "alarm --arg foo" + } For convenience, prefixing with `usr/` is allowed, but any other slashes or period characters are disallowed. E.g., to trigger the `Foo alarm`, @@ -126,8 +168,9 @@ Static (one-shot) conditions, like `usr/`, never enter the `flux` state. > "touch" or recreate, their PID file on `SIGHUP`. This can be done by > calling `utimensat()` on the PID file. Provided, of course, that the > service supports reloading on `SIGHUP`, otherwise it will be restarted -> by Finit when they instead exit on the signal. For such services, use -> `` to tell Finit the service does not support `SIGHUP`. +> by Finit when they instead exit on the signal. For such services, +> set `reload-signal = "none"` to tell Finit the service does not +> support `SIGHUP`. Built-in Conditions @@ -307,8 +350,8 @@ restarts that would otherwise occur because a depending service was sent Services with the `~` prefix are an exception to this rule: when their conditions return to `on` after being in `flux`, the reload is propagated --- the service is reloaded (SIGHUP) or restarted (noreload `!`) instead -of simply being resumed. +-- the service is reloaded (SIGHUP), or restarted if it has +`reload-signal = "none"`, instead of simply being resumed. Therefore, any plugin that supplies Finit with conditions must ensure that their state is updated after each reconfiguration. This can be diff --git a/doc/config/cgroups.md b/doc/config/cgroups.md index 89512799..6e96824f 100644 --- a/doc/config/cgroups.md +++ b/doc/config/cgroups.md @@ -23,77 +23,66 @@ Finit provides three different cgroup directives for controlling resource alloca Top-level Cgroup Definition ---------------------------- -**Syntax:** `cgroup NAME settings` +**Syntax:** `cgroup NAME { settings }` # Top-level cgroups and their default settings. All groups mandatory # but more can be added, max 8 groups in total currently. The cgroup # 'root' is also available, reserved for RT processes. Settings are # as-is, only one shorthand 'mem.' exists, other than that it's the # cgroup v2 controller default names. - cgroup init cpu.weight:100 - cgroup user cpu.weight:100 - cgroup system cpu.weight:9800 + cgroup init { cpu.weight = 100 } + cgroup user { cpu.weight = 100 } + cgroup system { cpu.weight = 9800 } Adding an extra cgroup `maint/` will require you to adjust the weight of the above three. We leave `init/` and `user/` as-is reducing weight of `system/` to 9700. - cgroup system cpu.weight:9700 + cgroup system { cpu.weight = 9700 } # Example extra cgroup 'maint' - cgroup maint cpu.weight:100 + cgroup maint { cpu.weight = 100 } By default, the `system/` cgroup is selected for almost everything. The `init/` cgroup is reserved for PID 1 itself and its closest relatives. The `user/` cgroup is for local TTY logins spawned by getty. -Global Cgroup Selector ----------------------- +Joining a Cgroup +---------------- -**Syntax:** `cgroup.NAME[,options]` (standalone directive) +**Syntax:** `cgroup NAME { settings }` (inside a service block) -To select a different top-level cgroup, e.g. `maint/`, for a group of -run/task/service directives in a `.conf` file, use the `cgroup.NAME` -directive as a standalone line: +Every block says which group it joins, by name. An empty block joins +without changing anything: - cgroup.maint - service [...] <...> /path/to/foo args -- description - service [...] <...> /path/to/bar args -- description + service foo { + cgroup maint {} + command = "/path/to/foo args" + } -Both services will run in the `maint/` cgroup. + service bar { + cgroup maint {} + command = "/path/to/bar args" + } -You can also include options with the global selector: +Both services run in the `maint/` cgroup. Settings inside the block +apply to that service alone: - cgroup.system,delegate - service [...] <...> /path/to/foo args -- description + service foo { + cgroup maint { + cpu.max = 10000 + memory.max = 655360 + } + command = "/path/to/foo args" + } -Per-Service Cgroup Option --------------------------- - -**Syntax:** `cgroup.NAME[,options]` or `cgroup:options` (service option) - -To override the cgroup for a specific service, use the `cgroup.NAME` -option within the service directive: - - service [...] <...> cgroup.maint /path/to/foo args -- description - -This form also allows per-service limits. Two syntaxes are supported: - -**New comma-separated syntax (recommended):** - - service [...] <...> cgroup.maint,cpu.max:10000,mem.max:655360 /path/to/foo args -- description - -**Old colon-separated syntax (legacy):** - - service [...] <...> cgroup.maint:cpu.max:10000,mem.max:655360 /path/to/foo args -- description - -You can also apply options to the current default cgroup (without changing it) -using the `cgroup:options` syntax: - - service [...] <...> cgroup:cpu.max:10000,mem.max:655360 /path/to/foo args -- description - -Both syntaxes work identically. The new comma-separated syntax is recommended -as it's more consistent with other option parsing in Finit. +> [!NOTE] +> The legacy format has a standalone `cgroup.NAME` line that selects a +> group for every stanza following it in the file, and a `cgroup:options` +> form that applies settings to whichever group is current without +> naming it. Neither has an equivalent here, by design: a block that +> joins a group says so itself, so the group cannot depend on what came +> earlier in the file. Note the `mem.` exception to the rule: every cgroup setting maps directly to cgroup v2 syntax. I.e., `cpu.max` maps to the file `/sys/fs/cgroup/maint/foo/cpu.max`. @@ -108,40 +97,66 @@ configuration filename (without the `.conf` extension). For example, a service defined in `system/10-hotplug.conf` would create a cgroup at `/sys/fs/cgroup/system/10-hotplug/` by default. -To use a more descriptive name (recommended for clarity), you can specify -`name:` in the cgroup directive: +To use a more descriptive name (recommended for clarity), set `name` +inside the cgroup block: - service [...] <...> cgroup.system,name:udevd /lib/systemd/systemd-udevd -- Device event daemon + service udevd { + description = "Device event daemon" + cgroup system { name = "udevd" } + command = "/lib/systemd/systemd-udevd" + } This creates the cgroup at `/sys/fs/cgroup/system/udevd/` instead. -The syntax supports combining the name override with other options: +`name` combines with any other setting: - service [...] <...> cgroup.system,name:udevd,cpu.max:10000 /lib/systemd/systemd-udevd -- Device event daemon + service udevd { + description = "Device event daemon" + cgroup system { + name = "udevd" + cpu.max = 10000 + } + command = "/lib/systemd/systemd-udevd" + } Or with delegation: - service [2345] @podman:podman \ - cgroup.containers,name:podman,delegate,mem.max:4G \ - /usr/bin/podman system service -- Podman API - -A daemon using `SCHED_RR` currently needs to run outside the default cgroups. - - service [...] <...> cgroup.root /path/to/daemon arg -- Real-Time process + service podman { + description = "Podman API" + runlevel = "2345" + user = "podman" + group = "podman" + cgroup containers { + name = "podman" + delegate = true + memory.max = 4G + } + command = "/usr/bin/podman system service" + } + +A daemon using `SCHED_RR` currently needs to run outside the default +cgroups. + + service rt { + description = "Real-Time process" + cgroup root {} + command = "/path/to/daemon arg" + } Cgroup Delegation ----------------- For services that need to create their own child cgroups (container runtimes -like Docker, Podman, systemd-nspawn, LXC), use the `delegate` option: +like Docker, Podman, systemd-nspawn, LXC), set `delegate`: - service [2345] @dockerd:dockerd \ - cgroup.system,delegate /usr/bin/dockerd -- Docker daemon - -Or with the old colon syntax: - - service [2345] @dockerd:dockerd \ - cgroup.system:delegate /usr/bin/dockerd -- Docker daemon + service dockerd { + description = "Docker daemon" + runlevel = "2345" + user = "dockerd" + group = "dockerd" + cgroup system { delegate = true } + command = "/usr/bin/dockerd" + } This allows the container runtime to: @@ -159,32 +174,46 @@ When delegation is enabled, Finit: **Requirements:** -- The service should specify `@user:group` for proper ownership +- The service should specify `user` and `group` for proper ownership - Controllers are delegated from the parent cgroup -**Example with additional config (new syntax):** - - service [2345] @podman:podman \ - cgroup.containers,delegate,mem.max:4G \ - /usr/bin/podman system service -- Podman API - -**Or with old syntax:** +**Example with additional config:** - service [2345] @podman:podman \ - cgroup.containers:delegate,mem.max:4G \ - /usr/bin/podman system service -- Podman API + service podman { + description = "Podman API" + runlevel = "2345" + user = "podman" + group = "podman" + cgroup containers { + delegate = true + memory.max = 4G + } + command = "/usr/bin/podman system service" + } -Both examples delegate the cgroup while also setting a 4GB memory limit. +This delegates the cgroup while also setting a 4GB memory limit. **Container template example:** Here's a real-world example from [Infix OS](https://github.com/kernelkit/infix) for running rootful podman container instances using delegation: - sysv log:prio:local1,tag:%i kill:30 pid:!/run/container:%i.pid \ - pre:0,/usr/sbin/container cleanup:0,/usr/sbin/container \ - cgroup.system,delegate \ - [2345] :%i container -n %i -- container %i + sysv container:%i { + description = "container %i" + runlevel = "2345" + reload-signal = "none" + pidfile = "/run/container:%i.pid" + stop-timeout = 30 + log { priority = "local1" identity = "%i" } + + exec-start-pre = "/usr/sbin/container" + exec-start-pre-timeout = 0 + exec-cleanup = "/usr/sbin/container" + exec-cleanup-timeout = 0 + + cgroup system { delegate = true } + command = "container -n %i" + } This template uses `sysv` type with delegation, demonstrating that cgroup delegation works with different service types, not just `service`. diff --git a/doc/config/env.md b/doc/config/env.md index 1868a976..b4064a0d 100644 --- a/doc/config/env.md +++ b/doc/config/env.md @@ -6,15 +6,16 @@ and any `*.conf`, was added. It is worth noting that these are global and *shared with all* services -- the only way to have a service-local environment is detailed in [Services Environment](service-env.md). -The syntax for global environment variables is straight forward. In -Finit v4.4 the `set` keyword was added for completeness, but the old -syntax (without the `set ` prefix) is still honored: +Global environment variables go in an `environment` block, `env` for +short: - set foo=bar - set baz="qux" + environment { + foo = "bar" + baz = "qux" + } On reload of .conf files, all tracked environment variables are cleared -so if `foo=bar` is removed from `finit.conf`, or any `finit.d/*.conf` +so if `foo` is removed from `finit.conf`, or any `finit.d/*.conf` file, it will no longer be used by Finit or any new (!) started run/tasks or services. The environment of already started processes can not be changed. diff --git a/doc/config/index.md b/doc/config/index.md index 86621d3a..7f11d84d 100644 --- a/doc/config/index.md +++ b/doc/config/index.md @@ -6,24 +6,109 @@ the navigation menu. Configuration File Syntax -------------------------- -The file format is line based, empty lines and comments, lines starting -with `#`, are ignored. A configuration directive starts with a keyword -followed by a space and the rest of the line is treated as the value. +A `.conf` file is a series of blocks. Every setting is a key inside +one, so nothing has to be remembered by position: -As of Finit v4.4, configuration directives can be broken up in multiple -lines using the continuation character `\`, and trailing comments are -also allowed. Example: +```aconf +service sysklogd { + description = "System log daemon" + runlevel = "S123456789" + envfile = "-/etc/default/sysklogd" + command = "syslogd -F $SYSLOGD_ARGS" +} +``` + +Values are quoted strings, bare words, or integers. Lists use braces, +and a block may carry a title, which becomes the service identity: + +```aconf +# shell comment +// C++ comment +/* C comment */ + +key = "value" # string +number = 20 # integer +flag = true # boolean +list = { "one", "two" } # string list +block title { key = "value" } # titled section + +path = "${HOME}/thing" # environment expansion +include("/etc/finit.d/extra.conf") # include another file +``` + +Bare words work wherever a string is expected, so `memory.max = 65M` +and `restart = always` need no quotes. + +### Two conventions + +Keys are kebab-case, never `snake_case` or `CamelCase`: `restart-sec`, +`stop-timeout`, `reboot-watchdog`. + +Keys that take a list are plural: `conditions`, `conflicts`, +`capabilities`, `modules`, `extra-groups`. Two imperatives keep their +singular form because they are verbs rather than nouns: `mknod` and +`include`. + +### Short forms + +Nine keys have an alias. An alias may abbreviate the canonical name, +or preserve the spelling the line-based format used; it never renames. + +| Canonical | Alias | Canonical | Alias | +|---|---|---|---| +| `description` | `desc` | `manual-start` | `manual` | +| `conditions` | `cond` | `remain-after-exit` | `remain` | +| `capabilities` | `caps` | `stop-signal` | `halt` | +| `modules` | `mod` | `stop-timeout` | `kill` | +| `envfile` | `env` | | | + +### Optional paths + +A leading `-` on a path means carry on if it is not there: ```aconf -# Escape \# chars if you want them literal in, e.g., descriptions -service name:sysklogd [S123456789] \ - env:-/etc/default/sysklogd \ - syslogd -F $SYSLOGD_ARGS \ - -- System log daemon \# 1 # Comments allowed +service foo { + envfile = "-/etc/default/foo" # skip the file if missing + command = "-/usr/sbin/foo" # skip the whole block if missing +} +``` + +That is the only mark meaning "optional". Two others mean something +else: `!` on `runlevel` inverts the set, e.g. `runlevel = "!12345"`, +and `~` on a condition propagates a reload from the service it names. +Both are covered where they apply. + + +Both Formats Are Read +--------------------- + +Finit also reads the line-based format it has always used, where a +stanza is a keyword followed by values whose meaning comes from their +position: + +```aconf +service [S123456789] env:-/etc/default/sysklogd syslogd -F $SYSLOGD_ARGS -- System log daemon +``` + +That format still works and is not going away. It is frozen at the +Finit 4.x feature set, so new settings appear only in the block format, +and the documentation is written in blocks throughout. + +There is no new file extension and no marker line. Every file is still +`*.conf`, and Finit decides which format a file is in by reading it: if +it parses as blocks it is a block file, otherwise it goes to the +line-based parser. A file is one format or the other, never a mix. + +Because the two are told apart before either parser commits, a typo +reports its own file and line instead of being mistaken for the other +format: + +``` +parse error: /etc/finit.d/foo.conf:3: no such option 'commnad' ``` The .conf files `/etc/finit.conf` and `/etc/finit.d/*` support many -directives. Some are restricted, e.g., only available at bootstrap, +settings. Some are restricted, e.g., only available at bootstrap, runlevel `S`. Read on in [Files & Layout](files.md) for more on how to structure your .conf files. diff --git a/doc/config/logging.md b/doc/config/logging.md index 5220a465..57427412 100644 --- a/doc/config/logging.md +++ b/doc/config/logging.md @@ -38,12 +38,19 @@ Log rotation is controlled using the global `log` setting. **Example:** - service log:prio:user.warn,tag:ntpd /sbin/ntpd pool.ntp.org -- NTP daemon + service ntpd { + description = "NTP daemon" + log { + priority = "user.warn" + identity = "ntpd" + } + command = "/sbin/ntpd pool.ntp.org" + } Output Buffering ---------------- -When using the `log` directive, Finit redirects the service's stdout and +When using the `log` block, Finit redirects the service's stdout and stderr to a pipe connected to a logger process. Programs detect this as non-interactive output (i.e., `isatty()` returns false) and typically switch from line-buffered to fully-buffered mode. @@ -53,7 +60,11 @@ directly, so this is rarely an issue. However, if a service's log messages appear delayed or batched, you can force line-buffered output by wrapping the command with `stdbuf`: - service log /usr/bin/stdbuf -oL /path/to/command -- My service + service myservice { + description = "My service" + log { } + command = "/usr/bin/stdbuf -oL /path/to/command" + } The `-oL` option forces line-buffered output, and `-o0` forces unbuffered output. See `stdbuf(1)` for details. diff --git a/doc/config/rescue.md b/doc/config/rescue.md index 4f70cff7..a690c150 100644 --- a/doc/config/rescue.md +++ b/doc/config/rescue.md @@ -48,7 +48,10 @@ system administrator. The bundled default `rescue.conf` contains nothing more than: runlevel 1 - tty [12345] rescue + tty rescue { + runlevel = "12345" + rescue = true + } The `tty` has the `rescue` option set, which works similar to the board bring-up tty option `notty`. The major difference being that `sulogin` @@ -57,6 +60,9 @@ is started to query for root/admin password. If `sulogin` is not found, If Finit cannot find `/lib/finit/rescue.conf` it defaults to: - tty [12345] rescue + tty rescue { + runlevel = "12345" + rescue = true + } There is no way to exit the *fallback* rescue mode. \ No newline at end of file diff --git a/doc/config/runlevels.md b/doc/config/runlevels.md index 5246fd45..16f95085 100644 --- a/doc/config/runlevels.md +++ b/doc/config/runlevels.md @@ -26,8 +26,16 @@ the lifetime of your system. Example: - task [S] /lib/console-setup/console-setup.sh - service [S12345] env:-/etc/default/rsyslog rsyslogd -n $RSYSLOGD_ARGS + task console-setup { + runlevel = "S" + command = "/lib/console-setup/console-setup.sh" + } + + service rsyslogd { + runlevel = "S12345" + envfile = "-/etc/default/rsyslog" + command = "rsyslogd -n $RSYSLOGD_ARGS" + } When bootstrap has completed, Finit moves to runlevel 2. This can be changed in `/etc/finit.conf` using the `runlevel N` directive, or by a diff --git a/doc/config/service-env.md b/doc/config/service-env.md index d298d400..6c7f0817 100644 --- a/doc/config/service-env.md +++ b/doc/config/service-env.md @@ -13,7 +13,12 @@ file `/etc/default/foo`. Like this: * `/etc/finit.conf`: - service [2345] env:-/etc/default/foo foo -n $FOO_OPTIONS -- Example foo daemon + service foo { + description = "Example foo daemon" + runlevel = "2345" + envfile = "-/etc/default/foo" + command = "foo -n $FOO_OPTIONS" + } Here the service `foo` is started with `-n`, to make sure it runs in the foreground, and the with the options found in the environment file. With @@ -22,6 +27,8 @@ the `ps` command we can see that the process is started with: foo -n --extra-arg=bar -s -x > [!NOTE] -> The leading `-` in `env:` determines if Finit should treat a missing -> environment file as blocking the start of the service or not. When -> `-` is used, a missing environment file does *not* block the start. +> The leading `-` on `envfile` determines if Finit should treat a +> missing environment file as blocking the start of the service or not. +> When `-` is used, a missing environment file does *not* block the +> start. The same mark works on `command`, where it means carry on if +> the binary is missing. diff --git a/doc/config/service-opts.md b/doc/config/service-opts.md index add707bb..e5bb5bfd 100644 --- a/doc/config/service-opts.md +++ b/doc/config/service-opts.md @@ -1,21 +1,22 @@ Service Options =============== -The run/task/tty/service/sysv directives take modifiers, or options, to -control their behavior. This section lists them with their limitations. -All modifiers must be placed between the directive and its command. +The `run`, `task`, `tty`, `service` and `sysv` blocks take settings as +keys. This section lists them with their limitations. -The name of a service, shown by the `initctl` tool, defaults to the -basename of the service executable. It can be changed with the `name:` -option: +The name of a service, shown by the `initctl` tool, is the block title. +It defaults to the basename of the executable if the title is omitted: - name: + service sshd { + command = "/usr/sbin/sshd -D" + } -For multiple instances of a service, with the same `name`, set the -identifier `:ID` to prevent Finit from replacing previous instances: +For multiple instances of a service, with the same name, add an +identifier after a colon to prevent Finit from replacing previous +instances: - service name:ssdp :eth1 ssdpd eth1 -- Windows discovery on eth1 - service name:ssdp :eth2 ssdpd eth2 -- Windows discovery on eth2 + service ssdp:eth1 { command = "ssdpd eth1" } + service ssdp:eth2 { command = "ssdpd eth2" } The [`initctl`](../initctl.md) tool will list these two services as: @@ -23,107 +24,140 @@ The [`initctl`](../initctl.md) tool will list these two services as: - ssdp:eth2 Conflicting services that must be prevented from starting, use the -`conflict:` option: - - service [S12345789] udevd -- Device event management daemon - run [S] conflict:udevd mdev -s -- Populating device tree - -Multiple conflicting services can be separated using `,`: - - service :1 abc - service :2 abc - service conflict:abc:1,abc:2 cde - -If a service should not be automatically started, it can be configured -as manual with the optional `manual` argument. The service can then be -started at any time by running `initctl start `. - - manual:yes - -Other run/task/service options are: - - * `caps:...` -- see the [Linux Capabilities](capabilities.md) section - * `cgroup.NAME[,opts]` or `cgroup:opts` -- see the [Cgroups](cgroups.md) section - * `env:[-]/path/to/env` -- see the [Service Environment](service-env.md) section - * `log:...` -- see [Redirecting Output](logging.md#redirecting-output) - * `tty:` -- see [Controlling TTY](tty.md#controlling-tty) - * `nowarn` -- see [Conditional Loading](services.md#conditional-loading) - * `notify:...` -- see [Service Synchronization](service-sync.md) - * `if:...` -- see [Conditional Execution](services.md#conditional-execution) - * `type:forking` -- see description of the [service](services.md) directive - -As mentioned previously, services are automatically started, restarted, -and stopped, depending on the configuration and conditions. Within the -confines of that the following options are available: - - * `restart:NUM` -- number of times Finit tries to restart a crashing - service, default: 10, max: 255. When this limit is reached the - service is marked *crashed* and must be restarted manually with - `initctl restart NAME` - * `restart_sec:SEC` -- number of seconds before Finit tries to restart - a crashing service, default: 2 seconds for the first five retries, - then back-off to 5 seconds. The maximum of this configured value - and the above (2 and 5) will be used - * `restart:always` -- no upper limit on the number of times Finit - tries to restart a crashing service. Same as `restart:-1` - * `norestart` -- dont restart on failures, same as `restart:0` - * `respawn` -- bypasses the `restart` mechanism completely, allows - endless restarts. Useful in many use-cases, but not what `service` - was originally designed for so not the default behavior - * `remain:yes` -- for `run` and `task` only. Prevents the task from - re-running on runlevel re-entry and ensures the `post:` script runs - when the task is explicitly stopped or leaves its valid runlevels. - Similar to systemd's `RemainAfterExit=yes`. See [Task and Run](task-and-run.md) - for more details - * `oncrash:reboot` -- when all retries have failed, and the service - has *crashed*, if this option is set the system is rebooted - * `oncrash:script` -- similarly, but instead of rebooting, call the - `post:script` action with exit code `crashed`, see below - * `reload:'script [args]'` -- some services do not support `SIGHUP` but - may have other ways to update the configuration of a running daemon. - When `reload:script` is defined it is preferred over `SIGHUP`. Like - systemd, Finit sets `$MAINPID` as a convenience to scripts, which in - effect also allow `reload:'kill -HUP $MAINPID'` - * `stop:'script [args]'` -- some services may require alternate methods - to be stopped. If a `stop:script` is defined it is preferred over - `SIGTERM` and `stop`, for `service` and `sysv`, respectively. - Similar to `reload:script`, Finit sets `$MAINPID` - -> [!CAUTION] -> Both `reload:script` and `stop:script` are called as PID 1, without -> any timeout! Meaning, it is up to you to ensure the script is not -> blocking for seconds at a time or never terminates. - -When stopping a service (run/task/sysv/service), either manually or when -moving to another runlevel, Finit starts by sending `SIGTERM`, to allow -the process to shut down gracefully (unless a `stop:'script'` is used). -However, if the process has not been collected within 3 seconds, Finit -will send `SIGKILL`. To stop the process using a different signal than -`SIGTERM`, use `halt:SIGNAL`, e.g., `halt:SIGPWR`. To change the delay -between the stop signal and KILL, use the option `kill:<1-60>`, e.g., -`kill:10` to wait 10 seconds before sending `SIGKILL`. - -Services, including the `sysv` variant, support pre/post/ready and -cleanup scripts: - - * `pre:[0-3600,]script` -- called before the sysv/service is stated - * `post:[0-3600,]script` -- called after the sysv/service has stopped - * `ready:[0-3600,]script` -- called when the sysv/service is ready - * `cleanup:[0-3600,]script` -- called when run/task/sysv/service is removed - -The optional number (0-3600) is the timeout before Finit kills the -script, it defaults to the kill delay value and can be disabled by -setting it to zero. These scripts run as the same `@USER:GROUP` as the -service itself, with any `env:file` sourced. The scripts are executed -from the `$HOME` of the given user. The scripts are not called with any -argument, but get a set of environment variables: +`conflicts` key: + + service udevd { command = "udevd" } + run mdev { + runlevel = "S" + conflicts = { "udevd" } + command = "mdev -s" + } + +The list may name several, and an instance is named the same way it is +declared: + + service abc:1 { command = "abc 1" } + service abc:2 { command = "abc 2" } + service cde { conflicts = { "abc:1", "abc:2" } command = "cde" } + +If a service should not be automatically started, set `manual-start`. +It can then be started at any time with `initctl start `: + + service lldpd { + manual-start = true + command = "/usr/sbin/lldpd -d" + } + +Other run/task/service settings are: + + * `capabilities` -- see the [Linux Capabilities](capabilities.md) section + * `cgroup NAME {}` -- see the [Cgroups](cgroups.md) section + * `envfile` -- see the [Service Environment](service-env.md) section + * `log {}` -- see [Redirecting Output](logging.md#redirecting-output) + * `tty` -- see [Controlling TTY](tty.md#controlling-tty) + * `notify` -- see [Service Synchronization](service-sync.md) + * `if` -- see [Conditional Execution](services.md#conditional-execution) + * `type = "forking"` -- see description of the [service](services.md) block + * a leading `-` on `command` -- see [Conditional Loading](services.md#conditional-loading) + +Restarting +---------- + +Services are automatically started, restarted, and stopped, depending +on the configuration and conditions. Within the confines of that the +following settings are available: + + * `restart-max = NUM` -- number of times Finit tries to restart a + crashing service, default: 10, max: 255. When this limit is + reached the service is marked *crashed* and must be restarted + manually with `initctl restart NAME` + * `restart-sec = SEC` -- number of seconds before Finit tries to + restart a crashing service, default: 2 seconds for the first five + retries, then back-off to 5 seconds. The maximum of this + configured value and the above (2 and 5) will be used + * `restart = "always"` -- no upper limit on the number of times Finit + tries to restart a crashing service + * `restart = "never"` -- do not restart on failures. `false` is + accepted as a synonym, and `true` selects the default policy + * `respawn = true` -- bypasses the `restart` mechanism completely, + allowing endless restarts. Exiting is treated as normal work + rather than failure, so the service is restarted at once instead of + being counted and delayed. This is how a `tty` behaves; it is not + what `service` was designed for, so it is not the default + * `remain-after-exit = true` -- for `run` and `task` only. Prevents + the task from re-running on runlevel re-entry and ensures the + `exec-stop-post` script runs when the task is explicitly stopped or + leaves its valid runlevels. This is systemd's `RemainAfterExit=`. + See [Task and Run](task-and-run.md) for more details + * `oncrash = "reboot"` -- when all retries have failed, and the + service has *crashed*, the system is rebooted + * `oncrash = "script"` -- similarly, but instead of rebooting, call + the `exec-stop-post` script with exit code `crashed`, see below + +Stopping and reloading +---------------------- + +When stopping a service, either manually or when moving to another +runlevel, Finit starts by sending `SIGTERM` to let the process shut +down gracefully. If it has not been collected within 3 seconds, +`SIGKILL` follows. Both ends of that are configurable: + + * `stop-signal = "SIGPWR"` -- send this instead of `SIGTERM` + * `stop-timeout = 10` -- seconds to wait before `SIGKILL`, 1-300 + +Some services need more than a signal: + + * `exec-stop = "script [args]"` -- run instead of sending the stop + signal, and instead of `stop` for `sysv` + * `exec-reload = "script [args]"` -- run instead of sending `SIGHUP`. + Like systemd, Finit sets `$MAINPID` as a convenience to scripts, + which in effect also allows `exec-reload = "kill -HUP $MAINPID"` + +If a daemon cannot be reloaded with a signal at all, say so and Finit +restarts it instead: + + service dropbear { + reload-signal = "none" + command = "/usr/sbin/dropbear -R -F" + } + +`SIGHUP` is the default and the only other accepted value, with or +without the `SIG` prefix and in any case. + +Scripts +------- + +Services, including the `sysv` variant, support six lifecycle scripts: + + * `exec-start-pre` -- called before the sysv/service is started + * `exec-start-ready` -- called when the sysv/service is ready + * `exec-stop` -- called instead of the stop signal + * `exec-stop-post` -- called after the sysv/service has stopped + * `exec-reload` -- called instead of `SIGHUP` + * `exec-cleanup` -- called when run/task/sysv/service is removed + +Each takes a matching `-timeout`, in seconds, 0-3600, after which Finit +kills the script. It defaults to the `stop-timeout` value and can be +disabled by setting it to zero: + + service foo { + exec-start-pre = "/etc/foo/pre.sh" + exec-start-pre-timeout = 10 + command = "/usr/sbin/foo" + } + +These scripts run as the same user and group as the service itself, +with any `envfile` sourced. They are executed from the `$HOME` of the +given user. The scripts are not called with any argument, but get a +set of environment variables: * `SERVICE_IDENT=foo:1` * `SERVICE_NAME=foo` * `SERVICE_ID=1` -The `post:script` is called with an additional set of environment -variables. Yes, the text is correct, the naming was an accident: +The `exec-stop-post` script is called with an additional set of +environment variables. Yes, the text is correct, the naming was an +accident: - `EXIT_CODE=[exited,signal,crashed]`: normal exit, signaled, or crashed @@ -131,11 +165,13 @@ variables. Yes, the text is correct, the naming was an accident: the program, if it exited normally, or the signal name (`HUP`, `TERM`, etc.) if it exited due to signal -When a run/task/sys/service is removed (disable + reload) it is first -stopped and then removed from the runlevel. The `post:script` always -runs when the process has stopped, and the `cleanup:script` runs when -the the stanza has been removed from the runlevel. +When a run/task/sysv/service is removed (disable + reload) it is first +stopped and then removed from the runlevel. The `exec-stop-post` +script always runs when the process has stopped, and `exec-cleanup` +runs when the stanza has been removed from the runlevel. > [!IMPORTANT] > These script actions are intended for setup, cleanup, and readiness -> notification. It is up to the user to ensure the scripts terminate. +> notification. A script that outlives its timeout is killed, so pick +> one that suits the work, or set it to zero to opt out and take +> responsibility for the script terminating. diff --git a/doc/config/service-sync.md b/doc/config/service-sync.md index deaa5059..46aabd15 100644 --- a/doc/config/service-sync.md +++ b/doc/config/service-sync.md @@ -38,10 +38,10 @@ This will be made the default in Finit 5.0. In this mode of operation, every service needs to explicitly declare their readiness notification, like this: - service notify:pid watchdogd - service notify:systemd foo - service notify:s6 bar - service notify:none qux + service watchdogd { notify = "pid" command = "watchdogd" } + service foo { notify = "systemd" command = "foo" } + service bar { notify = "s6" command = "bar" } + service qux { notify = "none" command = "qux" } The `notify:none` syntax is for completeness in systems which run in `readiness pid` mode (default). Services declared with `notify:none` @@ -50,8 +50,15 @@ will transition to ready as soon as Finit has started them, e.g., To synchronize two services the following condition can be used: - service notify:pid watchdogd - service stress-ng --cpu 8 + service watchdogd { + notify = "pid" + command = "watchdogd" + } + + service stress-ng { + conditions = { "service/watchdogd/ready" } + command = "stress-ng --cpu 8" + } For details on the syntax and options, see below. diff --git a/doc/config/service-wrappers.md b/doc/config/service-wrappers.md index 327fd756..f0d125a8 100644 --- a/doc/config/service-wrappers.md +++ b/doc/config/service-wrappers.md @@ -14,7 +14,12 @@ example employs a wrapper script in `/etc/start.d`. * `/etc/finit.d/available/program.conf`: - service [235] /etc/start.d/program -- Example Program + service program { + description = "Example Program" + runlevel = "235" + reload-signal = "none" + command = "/etc/start.d/program" + } * `/etc/start.d/program:` diff --git a/doc/config/services.md b/doc/config/services.md index a65b037c..7dbe49b7 100644 --- a/doc/config/services.md +++ b/doc/config/services.md @@ -19,20 +19,25 @@ the most reliable way to monitor a service. However, not all daemons support running in the foreground, or they may start logging to the foreground as well, these are forking daemons and -are supported using the same syntax as forking `sysv` services, using -the `pid:!/path/to/pidfile.pid` syntax. There is an alternative syntax -that may be more intuitive, where Finit can also guess the PID file -based on the daemon's command name: +are supported using the same syntax as forking `sysv` services, by +naming the file to watch with `pidfile`. There is an alternative that +may be more intuitive, where Finit can also guess the PID file based on +the daemon's command name: - service type:forking ntpd -- NTP daemon + service ntpd { + description = "NTP daemon" + type = "forking" + command = "ntpd" + } This example lets BusyBox `ntpd` daemonize itself. Finit uses the basename of the binary to guess the PID file to watch for the PID: -`/var/run/ntpd.pid`. If Finit guesses wrong, you have to submit the -full `pid:!/path/to/file.pid`. +`/var/run/ntpd.pid`. If Finit guesses wrong, name the file yourself +with `pidfile = "/path/to/file.pid"`. -With `pid:!/path`, the file belongs to the service: Finit reads it -but does not create or remove it. The one exception is *stale* +The file belongs to the service: Finit reads it but does not create or +remove it. That is the default, and `pidfile-create = true` is what +asks Finit to write the file instead. The one exception is *stale* cleanup — if the service dies without removing its own pidfile (SIGKILL, OOM, segfault), and the file still names the just-reaped PID, Finit removes it before the next retry. This prevents daemons @@ -44,56 +49,75 @@ from getting stuck in a crash-restart loop. In the case of `ospfd` (below), we omit the `-d` flag (daemonize) to prevent it from forking to the background: - service [2345] /sbin/ospfd -- OSPF daemon + service ospfd { + description = "OSPF daemon" + runlevel = "2345" + conditions = { "pid/zebra" } + command = "/sbin/ospfd" + } -`[2345]` denote the runlevels `ospfd` is allowed to run in, they are -optional and default to level 2-5 if omitted. - -`<...>` is the condition for starting `ospfd`. In this example Finit +`runlevel` denotes the runlevels `ospfd` is allowed to run in, it is +optional and defaults to level 2-4 if omitted. + +`conditions` lists what must be asserted before starting `ospfd`. In this example Finit waits for another service, `zebra`, to have created its PID file in `/var/run/quagga/zebra.pid` before starting `ospfd`. Finit watches *all* files in `/var/run`, for each file named `*.pid`, or `*/pid`, Finit opens it and find the matching `NAME:ID` using the PID. -The condition can be prefixed with `!` and/or `~`: +A condition may be prefixed with `~` to propagate a reload of the +upstream service to this one, rather than merely pausing and resuming +it: - - `` -- `ospfd` does not support `SIGHUP` (noreload) - - `<~pid/zebra>` -- propagate reload from `zebra` to `ospfd` - - `` -- both: noreload and propagate reload + conditions = { "~pid/zebra" } -For details, see the [Finit Conditions](../conditions.md) document. - -Some services do not maintain a PID file and rather than patching each -application Finit provides a workaround. A `pid` keyword can be set -to have Finit automatically create (when starting) and later remove -(when stopping) the PID file. The file is created in the `/var/run` -directory using the `basename(1)` of the service. The default can be -modified with an optional `pid:`-argument: +If `ospfd` cannot be reloaded with `SIGHUP` at all, that is a property +of `ospfd` and not of the condition, so it is said directly: - pid[:[/path/to/]filename[.pid]] + reload-signal = "none" -For example, by adding `pid:/run/bar.pid` to the service `/sbin/bar`, -that PID file will, not only be created and removed automatically, but -also be used by the Finit condition subsystem. So a service/run/task -can depend on ``, like this foo will not be started until bar -has started: +The legacy format spells that second one as a `!` leading the condition +list, which is not accepted here. For details, see the +[Finit Conditions](../conditions.md) document. - service pid:/run/bar.pid bar -- Bar Service - service foo -- Foo Service +Some services do not maintain a PID file and rather than patching each +application Finit provides a workaround. With `pidfile-create` Finit +creates the file when starting and removes it when stopping. The path +comes from `pidfile`, which takes three forms: + + pidfile = true # /var/run/.pid + pidfile = "bar" # a bare name, /var/run/bar.pid + pidfile = "/run/bar.pid" # an explicit path + +Such a file is also used by the Finit condition subsystem, so another +service, run or task can depend on `pid/bar`. Here foo is not started +until bar has: + + service bar { + description = "Bar Service" + pidfile = "/run/bar.pid" + pidfile-create = true + command = "bar" + } + + service foo { + description = "Foo Service" + conditions = { "pid/bar" } + command = "foo" + } Needless to say, it is better if `bar` creates its own PID file when it has completed starting up and is ready for service. As an alternative "readiness" notification, Finit supports both systemd -and s6 style notification. This can be enabled by using the `notify` -option: +and s6 style notification. This is enabled with the `notify` key: - * `notify:systemd` -- tells Finit the service uses the `sd_notify()` + * `notify = "systemd"` -- tells Finit the service uses the `sd_notify()` API to signal PID 1 when it has completed its startup and is ready to service events. The [sd_notify()][] API expects `NOTIFY_SOCKET` to be set to the socket where the application can send `"READY=1\n"` when it is starting up or has processed a `SIGHUP`. - * `notify:s6` -- puts Finit in s6 compatibility mode. Compared to the + * `notify = "s6"` -- puts Finit in s6 compatibility mode. Compared to the systemd notification, [s6 expect][] compliant daemons to send `"\n"` and then close their socket. Finit takes care of "hard-wiring" the READY state as long as the application is running, events across any @@ -101,7 +125,11 @@ option: (must be >3) on then command line, Finit provides the following syntax (`%n` is replaced by Finit with then descriptor number): - service [S12345789] notify:s6 mdevd -O 4 -D %n + service mdevd { + runlevel = "S12345789" + notify = "s6" + command = "mdevd -O 4 -D %n" + } [sd_notify()]: https://www.freedesktop.org/software/systemd/man/sd_notify.html [s6 expect]: https://skarnet.org/software/s6/notifywhenup.html @@ -115,7 +143,13 @@ then service's ready condition which other services can depend on: This can be used to synchronize the start of another run/task/service: - task [S] @root:root mdevd-coldplug + task mdevd-coldplug { + runlevel = "S" + conditions = { "service/mdevd/ready" } + user = "root" + group = "root" + command = "mdevd-coldplug" + } Finit waits for `mdevd` to notify it, before starting `mdevd-coldplug`. Notice how both start in runlevel S, and the coldplug task only runs in @@ -131,20 +165,29 @@ Non-privileged Services ----------------------- Every `run`, `task`, or `service` can also list the privileges the -`/path/to/cmd` should be executed with. Prefix the command with -`@USR[:GRP[,SUPP,...]]`, where group and supplementary groups are -optional, like this: +command should be executed with, using `user`, `group` and +`extra-groups`, all optional: - run [2345] @joe:users logger "Hello world" + run hello { + runlevel = "2345" + user = "joe" + group = "users" + command = "logger \"Hello world\"" + } Finit reads the user's supplementary group membership from `/etc/group` automatically. Any groups the user belongs to will be inherited by the service. -To specify additional supplementary groups beyond those in `/etc/group`, -append them after the primary group, separated by commas: +To specify additional supplementary groups beyond those in +`/etc/group`, list them in `extra-groups`: - service @caddy:caddy,ssl-cert /usr/bin/caddy run + service caddy { + user = "caddy" + group = "caddy" + extra-groups = { "ssl-cert" } + command = "/usr/bin/caddy run" + } This runs the `caddy` service as user `caddy`, with primary group `caddy`, inheriting any groups `caddy` is a member of in `/etc/group`, @@ -152,14 +195,22 @@ plus the additional `ssl-cert` group. This is useful when a service needs access to resources owned by groups not listed in `/etc/group`. For multiple instances of the same command, e.g. a DHCP client or -multiple web servers, add `:ID` somewhere between the `run`, `task`, -`service` keyword and the command, like this: +multiple web servers, add `:ID` to the block title, like this: + + service httpd:80 { + description = "Web server" + runlevel = "2345" + command = "httpd -f -h /http -p 80" + } - service :80 [2345] httpd -f -h /http -p 80 -- Web server - service :8080[2345] httpd -f -h /http -p 8080 -- Old web server + service httpd:8080 { + description = "Old web server" + runlevel = "2345" + command = "httpd -f -h /http -p 8080" + } -Without the `:ID` to the service the latter will overwrite the former -and only the old web server would be started and supervised. +Without the `:ID` the latter will overwrite the former and only the old +web server would be started and supervised. Conditional Loading @@ -169,35 +220,58 @@ Finit support conditional loading of stanzas. The following example is take from the `system/hotplug.conf` file in the Finit distribution. Here we only show a simplified subset. -Starting with the `nowarn` option. +Starting with the leading `-` on `command`. - service nowarn name:udevd pid:udevd /lib/systemd/systemd-udevd - service nowarn name:udevd pid:udevd udevd + service udevd { + pidfile = "udevd" + command = "-/lib/systemd/systemd-udevd" + } -When loading the .conf file Finit looks for `/lib/systemd/systemd-udevd` -if that is not found Finit automatically logs a warning. The `nowarn` -option disables this warning so that the second line can be evaluated, + service udevd { + pidfile = "udevd" + command = "-udevd" + } + +When loading the .conf file Finit looks for +`/lib/systemd/systemd-udevd`, and if that is not found it logs a +warning. The leading `-` says a missing binary is expected here, so +the stanza is skipped quietly and the second block can be evaluated, which also provides a service named `udevd`. - run nowarn if:udevd :1 udevadm settle -t 0 + run udevadm:1 { + runlevel = "S" + if = "udevd" + conditions = { "pid/udevd" } + command = "-udevadm settle -t 0" + } This line is only loaded if we know of a service named `udevd`. Again, we do not warn if `udevadm` is not found, execution will also stop here until the PID condition is asserted, i.e., Finit detecting udevd has started. - run nowarn conflict:udevd [S] mdev -s -- Populating device tree + run mdev { + description = "Populating device tree" + runlevel = "S" + conflicts = { "udevd" } + command = "-mdev -s" + } If `udevd` is not available, we try to run `mdev`, but if that is not found, again we do not warn. -Conditional loading statements can also be negated, so the previous stanza -can also be written as: +Conditional loading statements can also be negated, so the previous +stanza can also be written as: - run nowarn if:!udevd [S] mdev -s -- Populating device tree + run mdev { + description = "Populating device tree" + runlevel = "S" + if = "!udevd" + command = "-mdev -s" + } The reason for using `conflict` in this example is that a conflict can be -resolved. Stanzas marked with `conflict:foo` are rechecked at runtime. +resolved. Stanzas naming a conflict are rechecked at runtime. Conditional Execution @@ -212,21 +286,37 @@ runlevel. E.g., a `task [123]` is qualified to run only in runlevel 1, Conditional execution qualify a run/task/service based on a condition. Consider this (simplified) example from the Infix operating system: - run [S] name:startup confd -b --load startup-config - run [S] if: name:failure confd --load failure-config + run startup { + runlevel = "S" + conditions = { "pid/sysrepo" } + command = "confd -b --load startup-config" + } + + run failure { + runlevel = "S" + if = "" + conditions = { "pid/sysrepo" } + command = "confd --load failure-config" + } The two run statements reside in the same .conf file so Finit runs them in true sequence. If loading the file `startup-config` fails confd sets the condition `usr/fail-startup`, thus allowing the next run statement to load `failure-config`. -Notice the critical difference between the `` condition and -`if:`. The former is a condition for starting and the -latter is a condition to check if a run/task/service is qualified to -even be considered. +Notice the critical difference between the `conditions` list and `if`. +The former is a condition for starting; the latter is a condition to +check whether a run/task/service is qualified to even be considered. +`if` has a negation of its own, `!`, which is unrelated to anything in +the `conditions` list. Conditional execution statements can also be negated, so provided the file loaded did the opposite, i.e., set a condition on success, the previous stanza can also be written as: - run [S] if: name:failure confd ... + run failure { + runlevel = "S" + if = "" + conditions = { "pid/sysrepo" } + command = "confd ..." + } diff --git a/doc/config/sysv.md b/doc/config/sysv.md index 4167a500..e9378bfb 100644 --- a/doc/config/sysv.md +++ b/doc/config/sysv.md @@ -24,9 +24,10 @@ calls `init-script restart` on `initctl reload`. Similar to how `service` stanzas work. Forking services started with `sysv` scripts can be monitored by Finit -by declaring the PID file to look for: `pid:!/path/to/pidfile.pid`. -Notice the leading `!`, it signifies Finit should not try to create the -file, but rather watch that file for the resulting forked-off PID. This +by declaring the PID file to look for: `pidfile = "/path/to/file.pid"`. +Finit does not create that file, it watches it for the resulting +forked-off PID, which is the default; `pidfile-create = true` is what +asks Finit to write it instead. This syntax also works for forking daemons that do not have a command line option to run it in the foreground, more on this below in `service`. @@ -64,7 +65,12 @@ Start/Stop Scripts For syntax details, see [SysV Init Scripts](#sysv-init-scripts), above. Here follows an example taken from a Debian installation: - sysv [2345] /etc/init.d/openbsd-inetd -- OpenBSD inet daemon + sysv inetd { + description = "OpenBSD inet daemon" + runlevel = "2345" + conditions = { "pid/syslogd" } + command = "/etc/init.d/openbsd-inetd" + } The init script header could be parsed to extract `Default-Start:` and other parameters for the `sysv` command to Finit. There is currently no diff --git a/doc/config/task-and-run.md b/doc/config/task-and-run.md index 1ec06223..9f4994c6 100644 --- a/doc/config/task-and-run.md +++ b/doc/config/task-and-run.md @@ -40,27 +40,35 @@ One-shot like 'run', but starts in parallel with the next command. Both `run` and `task` commands are run in a shell, so basic pipes and redirects can be used: - task [s] echo "foo" | cat >/tmp/bar + task bar { + runlevel = "S" + command = "echo \"foo\" | cat >/tmp/bar" + } Please note, `;`, `&&`, `||`, and similar are *not supported*. Any non-trivial constructs are better placed in a separate shell script. -remain:yes ----------- +remain-after-exit +----------------- By default, a `run` or `task` will re-run each time its runlevel is -entered, and its `post:` script does not run on completion. +entered, and its `exec-stop-post` script does not run on completion. -With `remain:yes`, the task runs once and does not re-run on runlevel -re-entry: +With `remain-after-exit`, the task runs once and does not re-run on +runlevel re-entry: - task [2345] remain:yes /usr/sbin/setup-firewall -- Firewall setup + task firewall { + description = "Firewall setup" + runlevel = "2345" + remain-after-exit = true + command = "/usr/sbin/setup-firewall" + } This has the following effects: * The task does not re-run on runlevel re-entry - * The `post:` script runs when: + * The `exec-stop-post` script runs when: - The task is explicitly stopped (`initctl stop NAME`) - The task leaves its valid runlevels (e.g., runlevel change) @@ -72,12 +80,16 @@ This is useful for tasks that set up persistent state where: **Example:** Setting up firewall rules with cleanup on shutdown: ``` -task [2345] remain:yes \ - post:/usr/sbin/teardown-firewall \ - /usr/sbin/setup-firewall -- Firewall setup +task firewall { + description = "Firewall setup" + runlevel = "2345" + remain-after-exit = true + exec-stop-post = "/usr/sbin/teardown-firewall" + command = "/usr/sbin/setup-firewall" +} ``` -The firewall rules are created once. The `post:` script runs when +The firewall rules are created once. The `exec-stop-post` script runs when entering runlevel 0 (halt) or 6 (reboot), or on explicit stop. > [!NOTE] diff --git a/doc/config/templating.md b/doc/config/templating.md index f55a0335..4faed67f 100644 --- a/doc/config/templating.md +++ b/doc/config/templating.md @@ -5,15 +5,19 @@ Finit comes with rudimentary support for templating, similar to that of systemd. Best illustrated with an example: $ initctl show avahi-autoipd@ - service :%i avahi-autoipd --syslog %i -- ZeroConf for %i + service avahi-autoipd:%i { + description = "ZeroConf for %i" + command = "avahi-autoipd --syslog %i" + } To enable ZeroConf for, e.g., `eth0`, use $ initctl enable avahi-autoipd@eth0.conf The enabled symlink will be set up to `avahi-autoipd@.conf` and every -instance of `%i` will in the instantiated directive be replaced with -`eth0`. Inspect the resulting instantiated template with `initctl show +instance of `%i` will be replaced with `eth0` before the file is +parsed, so it works in the block title, in any value, and in the +command line alike. Inspect the resulting instantiated template with `initctl show avahi-autoipd:eth0` and check the status of a running instance with: ``` diff --git a/doc/config/tty.md b/doc/config/tty.md index 1ffaae07..fd0ee76a 100644 --- a/doc/config/tty.md +++ b/doc/config/tty.md @@ -19,7 +19,13 @@ The default baud rate is 0, i.e., keep kernel default. **Example:** - tty [12345] /dev/ttyAMA0 115200 noclear vt220 + tty ttyAMA0 { + runlevel = "12345" + device = "/dev/ttyAMA0" + baud = 115200 + term = "vt220" + noclear = true + } The second `tty` syntax variant is for using an external getty, like agetty or the BusyBox getty. @@ -38,8 +44,16 @@ the user to press enter before starting getty. **Example:** - tty [12345] /sbin/getty -L 115200 /dev/ttyAMA0 vt100 - tty [12345] /sbin/agetty -L ttyAMA0 115200 vt100 nowait + tty getty { + runlevel = "12345" + command = "/sbin/getty -L 115200 /dev/ttyAMA0 vt100" + } + + tty agetty { + runlevel = "12345" + command = "/sbin/agetty -L ttyAMA0 115200 vt100" + nowait = true + } The `noclear` option disables clearing the TTY after each session. Clearing the TTY when a user logs out is usually preferable. @@ -69,13 +83,21 @@ can be omitted to keep the kernel default. **Example:** - tty [12345] @console noclear vt220 + tty console { + runlevel = "12345" + device = "@console" + term = "vt220" + noclear = true + } On really bare bones systems, or for board bringup, Finit can give you a shell prompt as soon as bootstrap is done, without opening any device node: - tty [12345789] notty + tty board { + runlevel = "12345789" + notty = true + } This should of course not be enabled on production systems. Because it may give a user root access without having to log in. However, for @@ -83,7 +105,10 @@ board bringup and system debugging it can come in handy. One can also use the `service` stanza to start a stand-alone shell: - service [12345] /bin/sh -l + service shell { + runlevel = "12345" + command = "/bin/sh -l" + } Controlling TTY for Services ---------------------------- @@ -109,5 +134,16 @@ TTY device. **Example:** - service [2345] tty:/dev/ttyS0 /usr/sbin/foo -- Foo on serial console - task [S] tty:@console my-setup-script -- Board bringup on console \ No newline at end of file + service foo { + description = "Foo on serial console" + runlevel = "2345" + tty = "/dev/ttyS0" + command = "/usr/sbin/foo" + } + + task setup { + description = "Board bringup on console" + runlevel = "S" + tty = "@console" + command = "my-setup-script" + } \ No newline at end of file diff --git a/doc/example.md b/doc/example.md index 1708cb35..527961e5 100644 --- a/doc/example.md +++ b/doc/example.md @@ -98,29 +98,52 @@ Some components are optional: runlevel(s), condition(s) and description, making it easy to create simple start scripts and still possible for more advanced uses as well: - service /usr/sbin/sshd -D + service sshd { + command = "/usr/sbin/sshd -D" + } Dependencies are handled using [conditions](conditions.md). One of the most common conditions is to wait for basic networking to become available: - service nginx -- High performance HTTP server + service nginx { + description = "High performance HTTP server" + conditions = { "net/route/default" } + command = "nginx" + } Here is another example where we instruct Finit to not start BusyBox `ntpd` until `syslogd` has started properly. Finit waits for `syslogd` to create its PID file, by default `/var/run/syslogd.pid`. - service [2345] log ntpd -n -N -p pool.ntp.org - service [S12345] syslogd -n -- Syslog daemon - -Notice the `log` keyword, BusyBox `ntpd` uses `stderr` for logging when -run in the foreground. With `log` Finit redirects `stdout` + `stderr` -to the system log daemon using the command line `logger(1)` tool. + service ntpd { + runlevel = "2345" + conditions = { "pid/syslogd" } + reload-signal = "none" + log { } + command = "ntpd -n -N -p pool.ntp.org" + } + + service syslogd { + description = "Syslog daemon" + runlevel = "S12345" + command = "syslogd -n" + } + +Notice the empty `log` block, BusyBox `ntpd` uses `stderr` for logging +when run in the foreground. With it Finit redirects `stdout` + +`stderr` to the system log daemon using the command line `logger(1)` +tool. A service, or task, can have multiple dependencies listed. Here we wait for *both* `syslogd` to have started and basic networking to be up: - service [2345] log ntpd -n -N -p pool.ntp.org + service ntpd { + runlevel = "2345" + conditions = { "pid/syslogd", "net/route/default" } + log { } + command = "ntpd -n -N -p pool.ntp.org" + } If either condition fails, e.g. loss of networking, `ntpd` is stopped and as soon as it comes back up again `ntpd` is restarted automatically. diff --git a/doc/features.md b/doc/features.md index 82e5fe78..5f604a77 100644 --- a/doc/features.md +++ b/doc/features.md @@ -212,13 +212,17 @@ A fourth group also exists, the `root` group. It is also _reserved_ and primarily intended for RT tasks. If you have RT tasks they need to be declared as such in their service stanza like this: - service [...] <...> cgroup.root /path/to/foo args -- description + service foo { + cgroup root {} + command = "/path/to/foo args" + } -or +Every block names the group it joins, so a second RT task says so too: - cgroup.root - service [...] <...> /path/to/foo args -- description - service [...] <...> /path/to/bar args -- description + service bar { + cgroup root {} + command = "/path/to/bar args" + } See the [Cgroups](config/cgroups.md) section for more information, e.g., how to configure per-group limits. diff --git a/doc/service.md b/doc/service.md index 31235701..e4d37d44 100644 --- a/doc/service.md +++ b/doc/service.md @@ -27,13 +27,22 @@ Finit can *not* start and monitor a daemon that: ### Forks to bg w/ PID file -There are two syntax variants, type 1 and type 2. The former is the -traditional one used also for `sysv` start/stop scripts, and the latter -is inspired by systemd, with a twist -- it lets Finit guess the pifdile -to look for based on the standard path and the basename of the command. - - service pid:!/run/serv.pid serv -- Forking service, type 1 - service type:forking serv -- Forking service, type 2 +There are two variants. The former names the pidfile to watch, as for +`sysv` start/stop scripts, and the latter is inspired by systemd, with a +twist -- it lets Finit guess the pidfile based on the standard path and +the basename of the command. + + service serv { + description = "Forking service, type 1" + pidfile = "/run/serv.pid" + command = "serv" + } + + service serv { + description = "Forking service, type 2" + type = "forking" + command = "serv" + } In this example the resulting files to watch for are `/run/serv.pid` and `/var/run/serv.pid`, respectively. On most modern Linux systems this is @@ -41,17 +50,29 @@ the same directory (`/var/run` is a symlink to `../run`). ### Runs in fg w/ PID file - service serv -n -p -- Foreground service w/ PID file + service serv { + description = "Foreground service w/ PID file" + command = "serv -n -p" + } ### Runs in fg w/o PID file Same as previous, but we tell Finit to create the PID file, because we need it to synchronize start/stop of a dependent service. - service pid:/run/serv.pid serv -n -- Foreground service w/o PID file + service serv { + description = "Foreground service w/o PID file" + pidfile = "/run/serv.pid" + pidfile-create = true + command = "serv -n" + } ### Runs in fg w/ custom PID file - service pid:/run/servy.pid serv -n -p -P /run/servy.pid -- Foreground service w/ custom PID file + service serv { + description = "Foreground service w/ custom PID file" + pidfile = "/run/servy.pid" + command = "serv -n -p -P /run/servy.pid" + } [1]: config/service-sync.md From b44a5ff2f8c9823718bc96d57d74fe4d4fa85a61 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Jul 2026 12:14:09 +0200 Subject: [PATCH 18/28] contrib, system: convert the shipped .conf files to the block format The examples people copy from were still written in the line-based format, so the block format was documented but nowhere demonstrated. Two names in contrib were accidents of the old format, where the service name falls out of the command basename: the Alpine and Void keymap task was called zcat, and Debian's console/keyboard setup tasks carried a .sh suffix. They now carry the name their file implies. Nothing referenced the old names. The mdevd coldplug path keeps the name it has always had. Its legacy line spelled the name inside the cgroup argument, where it names the cgroup leaf and not the service, so the barrier condition really is and not the the comment above it promises. Converted as-is so boot ordering does not change; the discrepancy is now written down where it happens. A list may not contain comments, the lexer sees the entries after the '#' regardless: modules = { # "fbcon", "softdog" } so the commented-out module candidates sit above the list instead. setup-sysroot.sh removes 10-hotplug.conf from the test sysroot, so that file is covered by parsing only, not by make check. Signed-off-by: Joachim Wiberg --- contrib/alpine/finit.conf | 22 ++- contrib/alpine/finit.d/available/acpid.conf | 8 +- contrib/alpine/finit.d/available/crond.conf | 8 +- .../alpine/finit.d/available/dropbear.conf | 7 +- contrib/alpine/finit.d/available/getty.conf | 27 ++- contrib/alpine/finit.d/available/keymap.conf | 7 +- contrib/alpine/finit.d/available/lxdm.conf | 6 +- contrib/alpine/finit.d/available/modules.conf | 7 +- contrib/alpine/finit.d/available/ntpd.conf | 9 +- contrib/alpine/finit.d/available/syslogd.conf | 15 +- .../alpine/finit.d/available/watchdog.conf | 7 +- contrib/debian/finit.conf | 22 ++- contrib/debian/finit.d/available/acpid.conf | 9 +- contrib/debian/finit.d/available/anacron.conf | 9 +- .../debian/finit.d/available/apparmor.conf | 6 +- .../finit.d/available/avahi-daemon.conf | 7 +- .../debian/finit.d/available/bluetooth.conf | 8 +- .../finit.d/available/console-setup.conf | 6 +- contrib/debian/finit.d/available/cron.conf | 8 +- contrib/debian/finit.d/available/elogind.conf | 7 +- contrib/debian/finit.d/available/getty.conf | 43 ++++- .../finit.d/available/keyboard-setup.conf | 6 +- contrib/debian/finit.d/available/lightdm.conf | 6 +- contrib/debian/finit.d/available/modules.conf | 8 +- .../finit.d/available/plymouth-quit.conf | 6 +- .../debian/finit.d/available/rsyslogd.conf | 7 +- contrib/debian/finit.d/available/sntpd.conf | 8 +- contrib/debian/finit.d/available/sshd.conf | 8 +- contrib/debian/finit.d/available/syslogd.conf | 7 +- contrib/debian/finit.d/available/xdm.conf | 6 +- contrib/finit.conf | 154 +++++++++++++----- contrib/void/finit.conf | 22 ++- contrib/void/finit.d/available/acpid.conf | 6 +- contrib/void/finit.d/available/crond.conf | 7 +- contrib/void/finit.d/available/dhcpcd.conf | 6 +- contrib/void/finit.d/available/dmeventd.conf | 6 +- contrib/void/finit.d/available/getty.conf | 21 ++- contrib/void/finit.d/available/keymap.conf | 6 +- contrib/void/finit.d/available/lxdm.conf | 6 +- contrib/void/finit.d/available/modules.conf | 8 +- contrib/void/finit.d/available/ntpd.conf | 8 +- contrib/void/finit.d/available/sshd.conf | 6 +- contrib/void/finit.d/available/syslog.conf | 13 +- contrib/void/finit.d/available/uuidd.conf | 8 +- system/10-hotplug.conf.in | 148 ++++++++++++++--- system/90-testserv.conf | 12 +- 46 files changed, 589 insertions(+), 158 deletions(-) diff --git a/contrib/alpine/finit.conf b/contrib/alpine/finit.conf index 29432703..44dbe31a 100644 --- a/contrib/alpine/finit.conf +++ b/contrib/alpine/finit.conf @@ -3,24 +3,34 @@ ## Runlevel to start after bootstrap, runlevel 'S' # Default is 2 -#runlevel 2 +#runlevel = 2 # Top-level cgroups and their default settings. All groups mandatory # but more can be added, max 8 groups in total currently. The cgroup # 'root' is also available, reserved for RT processes. Settings are # as-is, only one shorthand 'mem.' exists, other than that it's the # cgroup v2 controller default names. -#cgroup init cpu.weight:100 -#cgroup user cpu.weight:100 -#cgroup system cpu.weight:9700 +#cgroup init { cpu.weight = 100 } +#cgroup user { cpu.weight = 100 } +#cgroup system { cpu.weight = 9700 } # Task to run if ctrl-alt-del is pressed. This condition is asserted by # Finit upon receiving SIGINT (from the kernel). -#task [12345789] initctl reboot -- Rebooting system +#task reboot { +# description = "Rebooting system" +# runlevel = "12345789" +# conditions = { "sys/key/ctrlaltdel" } +# command = "initctl reboot" +#} # Task to run if the kernel gets a power fail condition is pressed. # Asserted by Finit upon receiving SIGPWR (from the kernel). -#task [12345789] initctl poweroff -- Shutting down system +#task poweroff { +# description = "Shutting down system" +# runlevel = "12345789" +# conditions = { "sys/pwr/fail" } +# command = "initctl poweroff" +#} ## Bootstrap services and tasks # Can be listed here or in /etc/finit.d/*.conf diff --git a/contrib/alpine/finit.d/available/acpid.conf b/contrib/alpine/finit.d/available/acpid.conf index e00189e6..9bbcbbdf 100644 --- a/contrib/alpine/finit.d/available/acpid.conf +++ b/contrib/alpine/finit.d/available/acpid.conf @@ -1,3 +1,9 @@ # Handle PWR button to shutdown/reboot -- useful in Qemu (virt-manager) # Depends on syslogd having started. Redirect any output to log. -service [2345] log acpid -f -- ACPI daemon +service acpid { + description = "ACPI daemon" + runlevel = "2345" + conditions = { "pid/syslogd" } + log { } + command = "acpid -f" +} diff --git a/contrib/alpine/finit.d/available/crond.conf b/contrib/alpine/finit.d/available/crond.conf index 687dc403..434435ce 100644 --- a/contrib/alpine/finit.d/available/crond.conf +++ b/contrib/alpine/finit.d/available/crond.conf @@ -1 +1,7 @@ -service [2345] env:-/etc/conf.d/crond crond -f -S $CRON_OPTS -- Cron daemon +service crond { + description = "Cron daemon" + runlevel = "2345" + conditions = { "pid/syslogd" } + envfile = "-/etc/conf.d/crond" + command = "crond -f -S $CRON_OPTS" +} diff --git a/contrib/alpine/finit.d/available/dropbear.conf b/contrib/alpine/finit.d/available/dropbear.conf index 11db0b73..d4e16132 100644 --- a/contrib/alpine/finit.d/available/dropbear.conf +++ b/contrib/alpine/finit.d/available/dropbear.conf @@ -1,2 +1,7 @@ # Start SSH daemon as soon as basic networking comes up -service [2345] env:-/etc/conf.d/dropbear dropbear -R -F $DROPBEAR_OPTS -- Dropbear SSH daemon +service dropbear { + description = "Dropbear SSH daemon" + runlevel = "2345" + envfile = "-/etc/conf.d/dropbear" + command = "dropbear -R -F $DROPBEAR_OPTS" +} diff --git a/contrib/alpine/finit.d/available/getty.conf b/contrib/alpine/finit.d/available/getty.conf index f0aa0879..2ed77e5f 100644 --- a/contrib/alpine/finit.d/available/getty.conf +++ b/contrib/alpine/finit.d/available/getty.conf @@ -1,5 +1,24 @@ # Consoles to start getty on when system is up -tty [12345] /dev/ttyS0 noclear -tty [12345] /dev/tty1 noclear nowait -tty [2345] /dev/tty2 noclear nowait -tty [2345] /dev/tty3 noclear nowait +tty ttyS0 { + runlevel = "12345" + device = "/dev/ttyS0" + noclear = true +} +tty tty1 { + runlevel = "12345" + device = "/dev/tty1" + noclear = true + nowait = true +} +tty tty2 { + runlevel = "2345" + device = "/dev/tty2" + noclear = true + nowait = true +} +tty tty3 { + runlevel = "2345" + device = "/dev/tty3" + noclear = true + nowait = true +} diff --git a/contrib/alpine/finit.d/available/keymap.conf b/contrib/alpine/finit.d/available/keymap.conf index 55a294d9..498018f8 100644 --- a/contrib/alpine/finit.d/available/keymap.conf +++ b/contrib/alpine/finit.d/available/keymap.conf @@ -1,2 +1,7 @@ # Oneshot task to run once at bootstrap -task [S] env:/etc/conf.d/loadkmap zcat $KEYMAP | loadkmap -- Loading keymap +task keymap { + description = "Loading keymap" + runlevel = "S" + envfile = "/etc/conf.d/loadkmap" + command = "zcat $KEYMAP | loadkmap" +} diff --git a/contrib/alpine/finit.d/available/lxdm.conf b/contrib/alpine/finit.d/available/lxdm.conf index 51354ba5..ff4c2d96 100644 --- a/contrib/alpine/finit.d/available/lxdm.conf +++ b/contrib/alpine/finit.d/available/lxdm.conf @@ -1 +1,5 @@ -service [2345] lxdm -- Lightweight Login Manager +service lxdm { + description = "Lightweight Login Manager" + runlevel = "2345" + command = "lxdm" +} diff --git a/contrib/alpine/finit.d/available/modules.conf b/contrib/alpine/finit.d/available/modules.conf index 16345cc2..a9c5e2b1 100644 --- a/contrib/alpine/finit.d/available/modules.conf +++ b/contrib/alpine/finit.d/available/modules.conf @@ -1,7 +1,2 @@ ## Linux kernel modules to load -#module fbcon -#module button -#module evdev -#module mousedev -#module 8139cp -#module softdog +#modules = { "fbcon", "button", "evdev", "mousedev", "8139cp", "softdog" } diff --git a/contrib/alpine/finit.d/available/ntpd.conf b/contrib/alpine/finit.d/available/ntpd.conf index 185d1b03..95a6bcbf 100644 --- a/contrib/alpine/finit.d/available/ntpd.conf +++ b/contrib/alpine/finit.d/available/ntpd.conf @@ -1,2 +1,9 @@ # Busybox ntpd has no option to use syslog when running in foreground -service [2345] env:-/etc/conf.d/ntpd log ntpd -n $NTPD_OPTS -- NTP daemon +service ntpd { + description = "NTP daemon" + runlevel = "2345" + conditions = { "pid/syslogd" } + envfile = "-/etc/conf.d/ntpd" + log { } + command = "ntpd -n $NTPD_OPTS" +} diff --git a/contrib/alpine/finit.d/available/syslogd.conf b/contrib/alpine/finit.d/available/syslogd.conf index 8d24f896..9a28ff60 100644 --- a/contrib/alpine/finit.d/available/syslogd.conf +++ b/contrib/alpine/finit.d/available/syslogd.conf @@ -1,4 +1,15 @@ # Start syslogd as soon as possible, should always run # Start klogd as soon as possible, but after syslogd -service [S12345] env:-/etc/conf.d/syslog syslogd -n $SYSLOGD_OPTS -- Syslog daemon -service [S12345] env:-/etc/conf.d/klogd klogd -n $KLOGD_OPTS -- Kernel log daemon +service syslogd { + description = "Syslog daemon" + runlevel = "S12345" + envfile = "-/etc/conf.d/syslog" + command = "syslogd -n $SYSLOGD_OPTS" +} +service klogd { + description = "Kernel log daemon" + runlevel = "S12345" + conditions = { "pid/syslogd" } + envfile = "-/etc/conf.d/klogd" + command = "klogd -n $KLOGD_OPTS" +} diff --git a/contrib/alpine/finit.d/available/watchdog.conf b/contrib/alpine/finit.d/available/watchdog.conf index 269b711f..4a843c46 100644 --- a/contrib/alpine/finit.d/available/watchdog.conf +++ b/contrib/alpine/finit.d/available/watchdog.conf @@ -1 +1,6 @@ -service [S123456789] env:-/etc/conf.d/watchdog watchdog $WATCHDOG_OPTS $WATCHDOG_DEV -- System watchdog +service watchdog { + description = "System watchdog" + runlevel = "S123456789" + envfile = "-/etc/conf.d/watchdog" + command = "watchdog $WATCHDOG_OPTS $WATCHDOG_DEV" +} diff --git a/contrib/debian/finit.conf b/contrib/debian/finit.conf index 90b67870..d0c90246 100644 --- a/contrib/debian/finit.conf +++ b/contrib/debian/finit.conf @@ -3,24 +3,34 @@ ## Runlevel to start after bootstrap, runlevel 'S' # Default is 2 -#runlevel 2 +#runlevel = 2 # Top-level cgroups and their default settings. All groups mandatory # but more can be added, max 8 groups in total currently. The cgroup # 'root' is also available, reserved for RT processes. Settings are # as-is, only one shorthand 'mem.' exists, other than that it's the # cgroup v2 controller default names. -#cgroup init cpu.weight:100 -#cgroup user cpu.weight:100 -#cgroup system cpu.weight:9700 +#cgroup init { cpu.weight = 100 } +#cgroup user { cpu.weight = 100 } +#cgroup system { cpu.weight = 9700 } # Task to run if ctrl-alt-del is pressed. This condition is asserted by # Finit upon receiving SIGINT (from the kernel). -#task [12345789] initctl reboot -- Rebooting system +#task reboot { +# description = "Rebooting system" +# runlevel = "12345789" +# conditions = { "sys/key/ctrlaltdel" } +# command = "initctl reboot" +#} # Task to run if the kernel gets a power fail condition is pressed. # Asserted by Finit upon receiving SIGPWR (from the kernel). -#task [12345789] initctl poweroff -- Shutting down system +#task poweroff { +# description = "Shutting down system" +# runlevel = "12345789" +# conditions = { "sys/pwr/fail" } +# command = "initctl poweroff" +#} ## Bootstrap services and tasks # Can be listed here or in /etc/finit.d/*.conf diff --git a/contrib/debian/finit.d/available/acpid.conf b/contrib/debian/finit.d/available/acpid.conf index b23bf099..2c2412c7 100644 --- a/contrib/debian/finit.d/available/acpid.conf +++ b/contrib/debian/finit.d/available/acpid.conf @@ -1,3 +1,10 @@ # Handle PWR button to shutdown/reboot -- useful in Qemu (virt-manager) # Depends on a syslog daemon having started. Redirect any output to log. -service [2345] env:-/etc/default/acpid log acpid -f $OPTIONS -- ACPI daemon +service acpid { + description = "ACPI daemon" + runlevel = "2345" + conditions = { "pid/syslogd" } + envfile = "-/etc/default/acpid" + log { } + command = "acpid -f $OPTIONS" +} diff --git a/contrib/debian/finit.d/available/anacron.conf b/contrib/debian/finit.d/available/anacron.conf index 9a6f2942..f970f8e2 100644 --- a/contrib/debian/finit.d/available/anacron.conf +++ b/contrib/debian/finit.d/available/anacron.conf @@ -1 +1,8 @@ -task [2345] halt:usr1 env:-/etc/default/anacron anacron -d -q $ANACRON_ARGS -- Anacron jobs +task anacron { + description = "Anacron jobs" + runlevel = "2345" + conditions = { "sys/pwr/ac", "pid/syslogd" } + envfile = "-/etc/default/anacron" + stop-signal = "usr1" + command = "anacron -d -q $ANACRON_ARGS" +} diff --git a/contrib/debian/finit.d/available/apparmor.conf b/contrib/debian/finit.d/available/apparmor.conf index b746c7fb..92f46e84 100644 --- a/contrib/debian/finit.d/available/apparmor.conf +++ b/contrib/debian/finit.d/available/apparmor.conf @@ -1,2 +1,6 @@ -sysv [S] /etc/init.d/apparmor -- AppArmor initialization +sysv apparmor { + description = "AppArmor initialization" + runlevel = "S" + command = "/etc/init.d/apparmor" +} diff --git a/contrib/debian/finit.d/available/avahi-daemon.conf b/contrib/debian/finit.d/available/avahi-daemon.conf index f81f263a..38c17d3b 100644 --- a/contrib/debian/finit.d/available/avahi-daemon.conf +++ b/contrib/debian/finit.d/available/avahi-daemon.conf @@ -1 +1,6 @@ -service [2345] env:-/etc/default/avahi-daemon avahi-daemon -s $AVAHI_DAEMON_OPTS -- Avahi mDNS/DNS-SD Stack +service avahi-daemon { + description = "Avahi mDNS/DNS-SD Stack" + runlevel = "2345" + envfile = "-/etc/default/avahi-daemon" + command = "avahi-daemon -s $AVAHI_DAEMON_OPTS" +} diff --git a/contrib/debian/finit.d/available/bluetooth.conf b/contrib/debian/finit.d/available/bluetooth.conf index 23571602..4672bdce 100644 --- a/contrib/debian/finit.d/available/bluetooth.conf +++ b/contrib/debian/finit.d/available/bluetooth.conf @@ -1,3 +1,9 @@ # To ignore errors like "org.freedesktop.hostname1", update the env file with # "--noplugin=hostname", i.e. append to BLUETOOTHD_OPTS -service [2345] log:null env:-/etc/default/bluetooth /usr/libexec/bluetooth/bluetoothd -n $BLUETOOTHD_OPTS -- Bluetooth service +service bluetoothd { + description = "Bluetooth service" + runlevel = "2345" + envfile = "-/etc/default/bluetooth" + log { file = "/dev/null" } + command = "/usr/libexec/bluetooth/bluetoothd -n $BLUETOOTHD_OPTS" +} diff --git a/contrib/debian/finit.d/available/console-setup.conf b/contrib/debian/finit.d/available/console-setup.conf index 855e7ab1..5ec68635 100644 --- a/contrib/debian/finit.d/available/console-setup.conf +++ b/contrib/debian/finit.d/available/console-setup.conf @@ -1,2 +1,6 @@ # Oneshot task to run once at bootstrap -task [S] /lib/console-setup/console-setup.sh -- Setting up console font and keymap +task console-setup { + description = "Setting up console font and keymap" + runlevel = "S" + command = "/lib/console-setup/console-setup.sh" +} diff --git a/contrib/debian/finit.d/available/cron.conf b/contrib/debian/finit.d/available/cron.conf index 89fa6e48..35034991 100644 --- a/contrib/debian/finit.d/available/cron.conf +++ b/contrib/debian/finit.d/available/cron.conf @@ -1 +1,7 @@ -service [2345] env:-/etc/default/cron cron -f $EXTRA_OPTS -- Background job processing daemon +service cron { + description = "Background job processing daemon" + runlevel = "2345" + conditions = { "sys/pwr/ac", "pid/syslogd" } + envfile = "-/etc/default/cron" + command = "cron -f $EXTRA_OPTS" +} diff --git a/contrib/debian/finit.d/available/elogind.conf b/contrib/debian/finit.d/available/elogind.conf index c37095c0..8b7ccc4a 100644 --- a/contrib/debian/finit.d/available/elogind.conf +++ b/contrib/debian/finit.d/available/elogind.conf @@ -1,4 +1,9 @@ # On some systems, bootstrap can be really quick and cause elogind # to not start before a user has managed to log in. To prevent # this, add as condition to your TTYs in getty.conf -service [2345] /usr/libexec/elogind -- Login manager +service elogind { + description = "Login manager" + runlevel = "2345" + conditions = { "pid/syslogd" } + command = "/usr/libexec/elogind" +} diff --git a/contrib/debian/finit.d/available/getty.conf b/contrib/debian/finit.d/available/getty.conf index 12096163..c2a291fb 100644 --- a/contrib/debian/finit.d/available/getty.conf +++ b/contrib/debian/finit.d/available/getty.conf @@ -1,7 +1,38 @@ # Consoles to start getty on when system is up -tty [12345] /dev/tty1 linux noclear nowait -tty [2345] /dev/tty2 linux nowait -tty [2345] /dev/tty3 linux nowait -tty [2345] /dev/tty4 linux nowait -tty [2345] /dev/tty5 linux nowait -tty [2345] /dev/tty6 linux nowait +tty tty1 { + runlevel = "12345" + device = "/dev/tty1" + term = "linux" + noclear = true + nowait = true +} +tty tty2 { + runlevel = "2345" + device = "/dev/tty2" + term = "linux" + nowait = true +} +tty tty3 { + runlevel = "2345" + device = "/dev/tty3" + term = "linux" + nowait = true +} +tty tty4 { + runlevel = "2345" + device = "/dev/tty4" + term = "linux" + nowait = true +} +tty tty5 { + runlevel = "2345" + device = "/dev/tty5" + term = "linux" + nowait = true +} +tty tty6 { + runlevel = "2345" + device = "/dev/tty6" + term = "linux" + nowait = true +} diff --git a/contrib/debian/finit.d/available/keyboard-setup.conf b/contrib/debian/finit.d/available/keyboard-setup.conf index 15ef6990..856f8af2 100644 --- a/contrib/debian/finit.d/available/keyboard-setup.conf +++ b/contrib/debian/finit.d/available/keyboard-setup.conf @@ -1,2 +1,6 @@ # Oneshot task to run once at bootstrap -task [S] /lib/console-setup/keyboard-setup.sh -- Setting up console keyboard layout +task keyboard-setup { + description = "Setting up console keyboard layout" + runlevel = "S" + command = "/lib/console-setup/keyboard-setup.sh" +} diff --git a/contrib/debian/finit.d/available/lightdm.conf b/contrib/debian/finit.d/available/lightdm.conf index 98783e02..2486a55a 100644 --- a/contrib/debian/finit.d/available/lightdm.conf +++ b/contrib/debian/finit.d/available/lightdm.conf @@ -1 +1,5 @@ -service [3] lightdm -- Light Display Manager +service lightdm { + description = "Light Display Manager" + runlevel = "3" + command = "lightdm" +} diff --git a/contrib/debian/finit.d/available/modules.conf b/contrib/debian/finit.d/available/modules.conf index f45caec2..6c4bb5d9 100644 --- a/contrib/debian/finit.d/available/modules.conf +++ b/contrib/debian/finit.d/available/modules.conf @@ -1,8 +1,2 @@ ## Linux kernel modules to load -#module fbcon -#module button -#module evdev -#module loop -#module psmouse -#module mousedev -#module 8139cp +#modules = { "fbcon", "button", "evdev", "loop", "psmouse", "mousedev", "8139cp" } diff --git a/contrib/debian/finit.d/available/plymouth-quit.conf b/contrib/debian/finit.d/available/plymouth-quit.conf index cc72b451..9064aeef 100644 --- a/contrib/debian/finit.d/available/plymouth-quit.conf +++ b/contrib/debian/finit.d/available/plymouth-quit.conf @@ -1,2 +1,6 @@ # Need to stop plymouth, or it'll block stdin on tty1 -task [S] /usr/bin/plymouth quit -- Stopping Plymouth Boot Screen +task plymouth { + description = "Stopping Plymouth Boot Screen" + runlevel = "S" + command = "/usr/bin/plymouth quit" +} diff --git a/contrib/debian/finit.d/available/rsyslogd.conf b/contrib/debian/finit.d/available/rsyslogd.conf index 80e72cda..fee99c5a 100644 --- a/contrib/debian/finit.d/available/rsyslogd.conf +++ b/contrib/debian/finit.d/available/rsyslogd.conf @@ -1,3 +1,8 @@ # Start rsyslogd as soon as possible, should always run # Provides pid/syslogd condition -service [S12345] name:syslogd env:-/etc/default/rsyslog rsyslogd -n $RSYSLOGD_OPTIONS -- Reliable syslog daemon +service syslogd { + description = "Reliable syslog daemon" + runlevel = "S12345" + envfile = "-/etc/default/rsyslog" + command = "rsyslogd -n $RSYSLOGD_OPTIONS" +} diff --git a/contrib/debian/finit.d/available/sntpd.conf b/contrib/debian/finit.d/available/sntpd.conf index 7b5a9fa7..ccc3e392 100644 --- a/contrib/debian/finit.d/available/sntpd.conf +++ b/contrib/debian/finit.d/available/sntpd.conf @@ -1,2 +1,8 @@ # Simple NTP client (SNTP) -service [2345] env:-/etc/default/sntpd sntpd -n $SNTPD_OPTIONS -- SNTP client +service sntpd { + description = "SNTP client" + runlevel = "2345" + conditions = { "net/route/default" } + envfile = "-/etc/default/sntpd" + command = "sntpd -n $SNTPD_OPTIONS" +} diff --git a/contrib/debian/finit.d/available/sshd.conf b/contrib/debian/finit.d/available/sshd.conf index 62c310ab..a945eb90 100644 --- a/contrib/debian/finit.d/available/sshd.conf +++ b/contrib/debian/finit.d/available/sshd.conf @@ -1,2 +1,8 @@ # Start SSH daemon as soon as basic networking comes up -service [2345] env:-/etc/default/ssh /usr/sbin/sshd -D $SSHD_OPTS -- OpenSSH daemon +service sshd { + description = "OpenSSH daemon" + runlevel = "2345" + conditions = { "net/lo/up" } + envfile = "-/etc/default/ssh" + command = "/usr/sbin/sshd -D $SSHD_OPTS" +} diff --git a/contrib/debian/finit.d/available/syslogd.conf b/contrib/debian/finit.d/available/syslogd.conf index 6896ed10..afb95522 100644 --- a/contrib/debian/finit.d/available/syslogd.conf +++ b/contrib/debian/finit.d/available/syslogd.conf @@ -1,2 +1,7 @@ # Start sysklogd as soon as possible, should always run if enabled -service [S123456789] env:-/etc/default/syslogd syslogd -F $SYSLOGD_OPTIONS -- System logging daemon +service syslogd { + description = "System logging daemon" + runlevel = "S123456789" + envfile = "-/etc/default/syslogd" + command = "syslogd -F $SYSLOGD_OPTIONS" +} diff --git a/contrib/debian/finit.d/available/xdm.conf b/contrib/debian/finit.d/available/xdm.conf index 0883ba97..00f67503 100644 --- a/contrib/debian/finit.d/available/xdm.conf +++ b/contrib/debian/finit.d/available/xdm.conf @@ -1,4 +1,8 @@ # Here we use XDM since it has few dependencies (relatively speaking). # It is also an example of a service without a -- description. # service [3] lxdm -- Lightweight Login Manager -service [3] xdm -nodaemon -- X Display Manager +service xdm { + description = "X Display Manager" + runlevel = "3" + command = "xdm -nodaemon" +} diff --git a/contrib/finit.conf b/contrib/finit.conf index 3faf0928..41d0f390 100644 --- a/contrib/finit.conf +++ b/contrib/finit.conf @@ -1,85 +1,149 @@ # Sample finit.conf to demonstrate the syntax # -host testbed +hostname = "testbed" # Linux kernel modules to load -module button -module evdev -module loop -module psmouse +modules = { "button", "evdev", "loop", "psmouse" } # Top-level cgroups and their default settings. All groups mandatory # but more can be added, max 8 groups in total currently. The cgroup # 'root' is also available, reserved for RT processes. Settings are # as-is, only one shorthand 'mem.' exists, other than that it's the # cgroup v2 controller default names. -cgroup init cpu.weight:100 -cgroup user cpu.weight:100 -cgroup system cpu.weight:9700 cpu.max:50000 +cgroup init { cpu.weight = 100 } +cgroup user { cpu.weight = 100 } +cgroup system { cpu.weight = 9700 cpu.max = 50000 } # Example extra cgroup -cgroup maint cpu.weight:100 +cgroup maint { cpu.weight = 100 } # Runlevel to start after bootstrap, runlevel 'S' -runlevel 2 +runlevel = 2 # Network bringup script -network service networking start +network = "service networking start" # Max file size for each log file: 100 kiB, rotate max 4 copies: # log => log.1 => log.2.gz => log.3.gz => log.4.gz -log size:100k count:4 +log { + size = 100k + count = 4 +} # Virtual consoles to start getty on -tty /dev/tty1 -tty /dev/tty2 -tty /dev/tty3 +tty tty1 { device = "/dev/tty1" } +tty tty2 { device = "/dev/tty2" } +tty tty3 { device = "/dev/tty3" } # Task to run if ctrl-alt-del is pressed. This condition is asserted by # Finit upon receiving SIGINT (from the kernel). -task [12345789] initctl reboot -- Rebooting system +task reboot { + description = "Rebooting system" + runlevel = "12345789" + conditions = { "sys/key/ctrlaltdel" } + command = "initctl reboot" +} # Task to run if the kernel gets a power fail condition is pressed. # Asserted by Finit upon receiving SIGPWR (from the kernel). -task [12345789] initctl poweroff -- Shutting down system +task poweroff { + description = "Shutting down system" + runlevel = "12345789" + conditions = { "sys/pwr/fail" } + command = "initctl poweroff" +} # Alternative method instead of runparts -#task [S] /etc/init.d/keyboard-setup start -- Setting up preliminary keymap -#task [S] /etc/init.d/acpid start -- Starting ACPI Daemon -#task [S] /etc/init.d/kbd start -- Preparing console -#run [2] /etc/init.d/networking start -- Start networking +#task keyboard-setup { +# description = "Setting up preliminary keymap" +# runlevel = "S" +# command = "/etc/init.d/keyboard-setup start" +#} +#task acpid { +# description = "Starting ACPI Daemon" +# runlevel = "S" +# command = "/etc/init.d/acpid start" +#} +#task kbd { +# description = "Preparing console" +# runlevel = "S" +# command = "/etc/init.d/kbd start" +#} +#run networking { +# description = "Start networking" +# runlevel = "2" +# command = "/etc/init.d/networking start" +#} # Services to be monitored and respawned as needed -# klgod and syslogd are placed in the maint cgroup -cgroup.maint -service [2345] klogd -n -- Kernel logging server -service [2345] syslogd -n -- Syslog server - -# gdm in the system cgroup (default when read from it's own .conf file) -cgroup.system -service [3] cgroup:cpu.weight:250,mem.max:10M gdm -- GNOME Display Manager +# klogd and syslogd are placed in the maint cgroup. There is no +# "current cgroup" to switch in this format, every block names its own. +service klogd { + description = "Kernel logging server" + runlevel = "2345" + conditions = { "pid/syslogd" } + cgroup maint {} + command = "klogd -n" +} +service syslogd { + description = "Syslog server" + runlevel = "2345" + cgroup maint {} + command = "syslogd -n" +} + +# gdm gets its own limits inside the system group, which is where +# services end up when no cgroup block is given. +service gdm { + description = "GNOME Display Manager" + runlevel = "3" + cgroup system { + cpu.weight = 250 + memory.max = 10M + } + command = "gdm" +} # Start SSH daemon with opts from /etc/default/ssh (if available), move -# to user cgroup with cpu.weight:250. Default for services is system -# group. Default weight in user group is 100 (above). Max length of -# cgroup argument is (currently) 63 chars. -# -# Notice alternative syntax where current cgroup is only set for this -# particular service. -service [2345] cgroup.user:cpu.weight:250,mem.max:10M env:-/etc/default/ssh /usr/sbin/sshd -D $SSHD_OPTS -- OpenSSH daemon +# to user cgroup with cpu.weight 250. Default weight in the user group +# is 100 (above). +service sshd { + description = "OpenSSH daemon" + runlevel = "2345" + envfile = "-/etc/default/ssh" + cgroup user { + cpu.weight = 250 + memory.max = 10M + } + command = "/usr/sbin/sshd -D $SSHD_OPTS" +} # The BusyBox ntpd does not use syslog when running in the foreground # So we use this trick to redirect stdout/stderr to a log file. The # log file is rotated with the above settings. The condition declares -# a dependency on a system default route (gateway) to be set. A single -# at the beginning means ntpd does not respect SIGHUP for restart. -service [2345] log:/var/log/ntpd.log ntpd -n -l -I eth0 -- NTP daemon +# a dependency on a system default route (gateway) to be set. ntpd +# does not respect SIGHUP, so Finit restarts it on reload instead. +service ntpd { + description = "NTP daemon" + runlevel = "2345" + conditions = { "net/route/default" } + reload-signal = "none" + log { file = "/var/log/ntpd.log" } + command = "ntpd -n -l -I eth0" +} # Run start scripts from this directory -# runparts /etc/start.d - -# For multiple instances of the same service, add :ID somewhere between -# the service/run/task keyword and the command. -service :80 [2345] /sbin/httpd -f -h /http -p 80 -- Web server -service :8080 [2345] /sbin/httpd -f -h /http -p 8080 -- Old web server +#runparts = "/etc/start.d" + +# For multiple instances of the same service, add :ID to the block title. +service httpd:80 { + description = "Web server" + runlevel = "2345" + command = "/sbin/httpd -f -h /http -p 80" +} +service httpd:8080 { + description = "Old web server" + runlevel = "2345" + command = "/sbin/httpd -f -h /http -p 8080" +} diff --git a/contrib/void/finit.conf b/contrib/void/finit.conf index e16dec3e..e0830ae8 100644 --- a/contrib/void/finit.conf +++ b/contrib/void/finit.conf @@ -3,24 +3,34 @@ ## Runlevel to start after bootstrap, runlevel 'S' # Default is 2 -#runlevel 2 +#runlevel = 2 # Top-level cgroups and their default settings. All groups mandatory # but more can be added, max 8 groups in total currently. The cgroup # 'root' is also available, reserved for RT processes. Settings are # as-is, only one shorthand 'mem.' exists, other than that it's the # cgroup v2 controller default names. -#cgroup init cpu.weight:100 -#cgroup user cpu.weight:100 -#cgroup system cpu.weight:9700 +#cgroup init { cpu.weight = 100 } +#cgroup user { cpu.weight = 100 } +#cgroup system { cpu.weight = 9700 } # Task to run if ctrl-alt-del is pressed. This condition is asserted by # Finit upon receiving SIGINT (from the kernel). -#task [12345789] initctl reboot -- Rebooting system +#task reboot { +# description = "Rebooting system" +# runlevel = "12345789" +# conditions = { "sys/key/ctrlaltdel" } +# command = "initctl reboot" +#} # Task to run if the kernel gets a power fail condition is pressed. # Asserted by Finit upon receiving SIGPWR (from the kernel). -#task [12345789] initctl poweroff -- Shutting down system +#task poweroff { +# description = "Shutting down system" +# runlevel = "12345789" +# conditions = { "sys/pwr/fail" } +# command = "initctl poweroff" +#} ## Bootstrap services and tasks # Can be listed here or in /etc/finit.d/*.conf diff --git a/contrib/void/finit.d/available/acpid.conf b/contrib/void/finit.d/available/acpid.conf index 4cf422a5..f37354bf 100644 --- a/contrib/void/finit.d/available/acpid.conf +++ b/contrib/void/finit.d/available/acpid.conf @@ -1,3 +1,7 @@ # Handle PWR button to shutdown/reboot -- useful in Qemu (virt-manager) # Depends on syslogd having started. Redirect any output to log. -service [2345] acpid -f -- ACPI daemon +service acpid { + description = "ACPI daemon" + runlevel = "2345" + command = "acpid -f" +} diff --git a/contrib/void/finit.d/available/crond.conf b/contrib/void/finit.d/available/crond.conf index 1ee0538a..17174f61 100644 --- a/contrib/void/finit.d/available/crond.conf +++ b/contrib/void/finit.d/available/crond.conf @@ -1 +1,6 @@ -service [2345] crond -f -S -c /etc/crontabs -- Cron daemon +service crond { + description = "Cron daemon" + runlevel = "2345" + conditions = { "pid/syslogd" } + command = "crond -f -S -c /etc/crontabs" +} diff --git a/contrib/void/finit.d/available/dhcpcd.conf b/contrib/void/finit.d/available/dhcpcd.conf index 67a411e6..109a3ce8 100644 --- a/contrib/void/finit.d/available/dhcpcd.conf +++ b/contrib/void/finit.d/available/dhcpcd.conf @@ -1 +1,5 @@ -service [2345] dhcpcd -B -M -- DHCP client +service dhcpcd { + description = "DHCP client" + runlevel = "2345" + command = "dhcpcd -B -M" +} diff --git a/contrib/void/finit.d/available/dmeventd.conf b/contrib/void/finit.d/available/dmeventd.conf index 1d4fe6e0..3e3cafe0 100644 --- a/contrib/void/finit.d/available/dmeventd.conf +++ b/contrib/void/finit.d/available/dmeventd.conf @@ -1 +1,5 @@ -service [S12345] dmeventd -f -- Device mapper event daemon +service dmeventd { + description = "Device mapper event daemon" + runlevel = "S12345" + command = "dmeventd -f" +} diff --git a/contrib/void/finit.d/available/getty.conf b/contrib/void/finit.d/available/getty.conf index 6d31e47d..cf5cb613 100644 --- a/contrib/void/finit.d/available/getty.conf +++ b/contrib/void/finit.d/available/getty.conf @@ -1,4 +1,19 @@ # Consoles to start Finit built-in getty on when system is up -tty [12345] /dev/tty1 noclear nowait -tty [2345] /dev/tty2 noclear nowait -tty [2345] /dev/tty3 noclear nowait +tty tty1 { + runlevel = "12345" + device = "/dev/tty1" + noclear = true + nowait = true +} +tty tty2 { + runlevel = "2345" + device = "/dev/tty2" + noclear = true + nowait = true +} +tty tty3 { + runlevel = "2345" + device = "/dev/tty3" + noclear = true + nowait = true +} diff --git a/contrib/void/finit.d/available/keymap.conf b/contrib/void/finit.d/available/keymap.conf index 203253b3..e73dede8 100644 --- a/contrib/void/finit.d/available/keymap.conf +++ b/contrib/void/finit.d/available/keymap.conf @@ -1,2 +1,6 @@ # Oneshot task to run once at bootstrap -task [S] zcat /etc/keymap/se.bmap.gz | loadkmap -- Loading keymap +task keymap { + description = "Loading keymap" + runlevel = "S" + command = "zcat /etc/keymap/se.bmap.gz | loadkmap" +} diff --git a/contrib/void/finit.d/available/lxdm.conf b/contrib/void/finit.d/available/lxdm.conf index 51354ba5..ff4c2d96 100644 --- a/contrib/void/finit.d/available/lxdm.conf +++ b/contrib/void/finit.d/available/lxdm.conf @@ -1 +1,5 @@ -service [2345] lxdm -- Lightweight Login Manager +service lxdm { + description = "Lightweight Login Manager" + runlevel = "2345" + command = "lxdm" +} diff --git a/contrib/void/finit.d/available/modules.conf b/contrib/void/finit.d/available/modules.conf index 39aab7be..6fd5b180 100644 --- a/contrib/void/finit.d/available/modules.conf +++ b/contrib/void/finit.d/available/modules.conf @@ -1,7 +1,3 @@ ## Linux kernel modules to load -#module fbcon -#module button -#module evdev -#module mousedev -#module 8139cp -module softdog +# Other candidates: fbcon, button, evdev, mousedev, 8139cp +modules = { "softdog" } diff --git a/contrib/void/finit.d/available/ntpd.conf b/contrib/void/finit.d/available/ntpd.conf index ff443ab1..dea14b8e 100644 --- a/contrib/void/finit.d/available/ntpd.conf +++ b/contrib/void/finit.d/available/ntpd.conf @@ -1,2 +1,8 @@ # Silly Busybox ntpd has no option to use syslog when running in foreground -service [2345] log ntpd -n -N -p pool.ntp.org -- NTP daemon +service ntpd { + description = "NTP daemon" + runlevel = "2345" + conditions = { "pid/syslogd" } + log { } + command = "ntpd -n -N -p pool.ntp.org" +} diff --git a/contrib/void/finit.d/available/sshd.conf b/contrib/void/finit.d/available/sshd.conf index c85370e2..7739423c 100644 --- a/contrib/void/finit.d/available/sshd.conf +++ b/contrib/void/finit.d/available/sshd.conf @@ -1,2 +1,6 @@ # Start SSH daemon as soon as basic networking comes up -service [2345] /usr/sbin/sshd -D -- OpenSSH daemon +service sshd { + description = "OpenSSH daemon" + runlevel = "2345" + command = "/usr/sbin/sshd -D" +} diff --git a/contrib/void/finit.d/available/syslog.conf b/contrib/void/finit.d/available/syslog.conf index eb45f125..60423cff 100644 --- a/contrib/void/finit.d/available/syslog.conf +++ b/contrib/void/finit.d/available/syslog.conf @@ -1,4 +1,13 @@ # Start syslogd as soon as possible, should always run # Start klogd as soon as possible, but after syslogd -service [S12345] syslogd -n -- Syslog daemon -service [S12345] klogd -n -- Kernel log daemon +service syslogd { + description = "Syslog daemon" + runlevel = "S12345" + command = "syslogd -n" +} +service klogd { + description = "Kernel log daemon" + runlevel = "S12345" + conditions = { "pid/syslogd" } + command = "klogd -n" +} diff --git a/contrib/void/finit.d/available/uuidd.conf b/contrib/void/finit.d/available/uuidd.conf index 9841d338..587127f7 100644 --- a/contrib/void/finit.d/available/uuidd.conf +++ b/contrib/void/finit.d/available/uuidd.conf @@ -1 +1,7 @@ -service @uuidd:uuidd [2345] uuidd -F -- UUID generation daemon +service uuidd { + description = "UUID generation daemon" + runlevel = "2345" + user = "uuidd" + group = "uuidd" + command = "uuidd -F" +} diff --git a/system/10-hotplug.conf.in b/system/10-hotplug.conf.in index 85cb09ce..873cfe37 100644 --- a/system/10-hotplug.conf.in +++ b/system/10-hotplug.conf.in @@ -5,51 +5,151 @@ # often needs to be synchronized carefully, we suggest using your system log # daemon as the barrier for all other services. E.g.: # -# service if:udevd [S0123456789] syslogd -F -- System log daemon -# service if:mdev [S0123456789] syslogd -F -- System log daemon -# service if:mdevd [S0123456789] syslogd -F -- System log daemon +# service syslogd { +# description = "System log daemon" +# runlevel = "S0123456789" +# if = "udevd" +# conditions = { "run/udevadm:5/success" } +# command = "syslogd -F" +# } +# +# with `if = "mdev"` or `if = "mdevd"` and the matching coldplug condition +# for those variants. # # This provdes a condition that can act as a barrier for all -# other services. Notice the `if:` statement and the condition for starting +# other services. Notice the `if` setting: the condition for starting # syslogd is only considered if either udevd or mdev (service) is loaded and # is guaranteed to run after each respective run stanza have completed. # # Override this file by copying it to /etc/finit.d/, using the same name, then # change the contents any way you like, it can even be empty. +# +# The leading '-' on every command says a missing binary is expected here, so +# the block is skipped quietly and the next candidate gets its turn. # Check for systemd-udevd and eudev, if we find both, we opt for the latter. -service nowarn cgroup.system,name:udevd name:udevd notify:none env:-/etc/default/udevd pid:udevd log \ - [S12345789] /lib/systemd/systemd-udevd $UDEVD_ARGS -- Device event daemon (udev) -service nowarn cgroup.system,name:udevd name:udevd notify:none env:-/etc/default/udevd pid:udevd log \ - [S12345789] udevd $UDEVD_ARGS -- Device event daemon (udev) +service udevd { + description = "Device event daemon (udev)" + runlevel = "S12345789" + notify = "none" + envfile = "-/etc/default/udevd" + pidfile = "udevd" + pidfile-create = true + log { } + cgroup system { name = "udevd" } + command = "-/lib/systemd/systemd-udevd $UDEVD_ARGS" +} +service udevd { + description = "Device event daemon (udev)" + runlevel = "S12345789" + notify = "none" + envfile = "-/etc/default/udevd" + pidfile = "udevd" + pidfile-create = true + log { } + cgroup system { name = "udevd" } + command = "-udevd $UDEVD_ARGS" +} # Wait for udevd to start, then trigger coldplug events and module loading. # The last 'settle' call waits for it to finalize processing all uevents. -run nowarn if:udevd cgroup.init,name:udevadm :1 [S] log udevadm settle -t 0 -- -run nowarn if:udevd cgroup.init,name:udevadm :2 [S] log udevadm control --reload -- -run nowarn if:udevd cgroup.init,name:udevadm :3 [S] log udevadm trigger -c add -t devices -- Requesting device events -run nowarn if:udevd cgroup.init,name:udevadm :4 [S] log udevadm trigger -c add -t subsystems -- Requesting subsystem events -run nowarn if:udevd cgroup.init,name:udevadm :5 [S] log udevadm settle -t 30 -- Waiting for udev to finish +run udevadm:1 { + runlevel = "S" + if = "udevd" + conditions = { "service/udevd/ready" } + log { } + cgroup init { name = "udevadm" } + command = "-udevadm settle -t 0" +} +run udevadm:2 { + runlevel = "S" + if = "udevd" + conditions = { "service/udevd/ready" } + log { } + cgroup init { name = "udevadm" } + command = "-udevadm control --reload" +} +run udevadm:3 { + description = "Requesting device events" + runlevel = "S" + if = "udevd" + conditions = { "service/udevd/ready" } + log { } + cgroup init { name = "udevadm" } + command = "-udevadm trigger -c add -t devices" +} +run udevadm:4 { + description = "Requesting subsystem events" + runlevel = "S" + if = "udevd" + conditions = { "service/udevd/ready" } + log { } + cgroup init { name = "udevadm" } + command = "-udevadm trigger -c add -t subsystems" +} +run udevadm:5 { + description = "Waiting for udev to finish" + runlevel = "S" + if = "udevd" + conditions = { "service/udevd/ready" } + log { } + cgroup init { name = "udevadm" } + command = "-udevadm settle -t 30" +} # If udevd is not available, check for mdevd. Unlike plain 'mdev -df', the # mdevd-coldplug process exits when mdevd has completed. So the condition -# (same as for plain mdev below) can be relied on as a -# barrier for services that, e.g., need to probe for switchcore ports or PHYs -# that require firmware loading before they show up. -service nowarn conflict:udevd,mdev cgroup.init,name:mdevd notify:none if:!udevd \ - [S12345789] notify:s6 mdevd -O 4 -D %n -- Device event daemon (mdevd) -run nowarn conflict:udevd,mdev cgroup.init,name:coldplug \ - [S] mdevd-coldplug -- Cold plugging system +# can be relied on as a barrier for services +# that, e.g., need to probe for switchcore ports or PHYs that require +# firmware loading before they show up. +service mdevd { + description = "Device event daemon (mdevd)" + runlevel = "S12345789" + if = "!udevd" + conflicts = { "udevd", "mdev" } + notify = "s6" + cgroup init { name = "mdevd" } + command = "-mdevd -O 4 -D %n" +} + +# NOTE: this block is named for its command, so its barrier condition is +# , not the the +# plain mdev stanza below provides. The legacy line spelled the +# name inside the cgroup argument, where it names the cgroup leaf +# rather than the service, and the two paths have differed ever +# since. Left as it was so boot ordering does not change. +run mdevd-coldplug { + description = "Cold plugging system" + runlevel = "S" + conflicts = { "udevd", "mdev" } + conditions = { "service/mdevd/ready" } + cgroup init { name = "coldplug" } + command = "-mdevd-coldplug" +} # If neither udevd or mdevd are available, fall back to mdev. The new daemon # mode calls `mdev -s` internally but does not create a PID file when that is # done, so synchronization with coldplug is fragile. -service nowarn conflict:udevd,mdevd cgroup.init,name:mdev notify:none if:!udevd,!mdevd \ - [S12345789] mdev -df -S -- Device event daemon (mdev) +service mdev { + description = "Device event daemon (mdev)" + runlevel = "S12345789" + if = "!udevd,!mdevd" + conflicts = { "udevd", "mdevd" } + notify = "none" + cgroup init { name = "mdev" } + command = "-mdev -df -S" +} # Trigger a replay of all uevents from boot, before userspace was up and the # filesystem mounted. Compared to udevd, we don't know when 'mdev -df' has # finished processing all events, i.e., depending on is # not an indicator of all devices having been (re)discovered and fully set up. -run nowarn conflict:udevd,mdevd cgroup.init name:coldplug if:mdev \ - [S] @pkglibexecdir@/coldplug -- Cold plugging system +run coldplug { + description = "Cold plugging system" + runlevel = "S" + if = "mdev" + conflicts = { "udevd", "mdevd" } + conditions = { "service/mdev/running" } + cgroup init {} + command = "-@pkglibexecdir@/coldplug" +} diff --git a/system/90-testserv.conf b/system/90-testserv.conf index d165f88e..0d677e11 100644 --- a/system/90-testserv.conf +++ b/system/90-testserv.conf @@ -1,4 +1,10 @@ # Test service, only used for `make check`, not for public use -service [S123456789] name:testserv pid:!/run/testserv.pid \ - cgroup.system notify:none \ - /sbin/serv -n -p -i testserv -- Test serv daemon +service testserv { + description = "Test serv daemon" + runlevel = "S123456789" + conditions = { "boot/testserv" } + notify = "none" + pidfile = "/run/testserv.pid" + cgroup system {} + command = "/sbin/serv -n -p -i testserv" +} From 00794d41bc9de5d1e77722797769999b33e3356e Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Jul 2026 14:35:52 +0200 Subject: [PATCH 19/28] doc: convert the remaining legacy syntax The block conversion changed the bodies of the reference sections but left every "**Syntax:**" header spelling the line-based format, so each page opened by teaching the format it then stopped using. Six files were missed entirely: runparts, files, capabilities, requirements, runlevels, and switchroot. runparts had no block spelling written down anywhere, though the parser has read `runparts`, `runparts-progress`, and `runparts-sysv` all along. tty gains a table per variant. Its three syntax lines carried nine positional fields between them, which no longer describes anything the parser accepts. Fixes #148 Signed-off-by: Joachim Wiberg --- doc/conditions.md | 20 ++--- doc/config/capabilities.md | 72 +++++++++-------- doc/config/files.md | 6 +- doc/config/logging.md | 4 +- doc/config/runlevels.md | 39 +++++----- doc/config/runparts.md | 9 ++- doc/config/service-sync.md | 4 +- doc/config/services.md | 2 +- doc/config/sysv.md | 4 +- doc/config/task-and-run.md | 12 +-- doc/config/tty.md | 36 ++++++--- doc/example.md | 156 +++++++++++++++++++++++++------------ doc/features.md | 36 ++++++--- doc/plugins.md | 2 +- doc/requirements.md | 6 +- doc/runlevels.md | 49 ++++++++---- doc/switchroot.md | 66 +++++++++++++--- 17 files changed, 345 insertions(+), 178 deletions(-) diff --git a/doc/conditions.md b/doc/conditions.md index 036278d6..d16ebaa3 100644 --- a/doc/conditions.md +++ b/doc/conditions.md @@ -242,16 +242,16 @@ Composition ----------- The `pid/` conditions are generated by the Finit `pidfile.so` plugin and -composed from a service's `name:` and `:id`. By default the basename of -the daemon and the empty string. - -| **service** | **condition** | -|----------------------------------------------------|------------------| -| /sbin/foo | pid/foo | -| /sbin/bar -p /run/baz.pid | pid/bar | -| name:lxc :foo lxc-start -n foo -p /run/lxc/foo.pid | pid/lxc:foo | -| /usr/bin/dbus-daemon | pid/dbus-daemon | -| :222 dropbear -p 222 | pid/dropbear:222 | +composed from a service's block title and its `:id`. By default the +basename of the daemon and the empty string. + +| **service** | **condition** | +|-------------------------------------------------------------------|------------------| +| `service { command = "/sbin/foo" }` | pid/foo | +| `service { command = "/sbin/bar -p /run/baz.pid" }` | pid/bar | +| `service lxc:foo { command = "lxc-start -n foo -p /run/lxc/foo.pid" }` | pid/lxc:foo | +| `service { command = "/usr/bin/dbus-daemon" }` | pid/dbus-daemon | +| `service dropbear:222 { command = "dropbear -p 222" }` | pid/dropbear:222 | The condition is asserted when `pidfile.so` receives an inotify event for a file matching `/run/*.pid`, `/run/**/*.pid`, or `/run/**/pid`, diff --git a/doc/config/capabilities.md b/doc/config/capabilities.md index 8c89d392..5aa16c0a 100644 --- a/doc/config/capabilities.md +++ b/doc/config/capabilities.md @@ -16,14 +16,17 @@ which is the same approach used by other modern service managers like dinit. ## Basic Usage -Capabilities are specified using the `caps:` directive in service configuration: +Capabilities are specified with the `capabilities` key, alias `caps`: ```conf -service [2345] name:nginx \ - @www-data:www-data \ - caps:^cap_net_bind_service \ - /usr/sbin/nginx -g 'daemon off;' \ - -- Web server +service nginx { + description = "Web server" + runlevel = "2345" + user = "www-data" + group = "www-data" + capabilities = { "^cap_net_bind_service" } + command = "/usr/sbin/nginx -g 'daemon off;'" +} ``` This example allows nginx to bind to privileged ports (like 80 and 443) while @@ -52,7 +55,7 @@ with the following prefixes: Multiple capabilities can be specified as a comma-separated list: ```conf -caps:^cap_net_raw,^cap_net_admin,^cap_net_bind_service +capabilities = { "^cap_net_raw", "^cap_net_admin", "^cap_net_bind_service" } ``` ## Common Use Cases @@ -62,10 +65,13 @@ caps:^cap_net_raw,^cap_net_admin,^cap_net_bind_service Allow a web server to bind to ports 80 and 443 without running as root: ```conf -service [2345] name:webserver \ - @www-data:www-data \ - caps:^cap_net_bind_service \ - /usr/sbin/nginx -g 'daemon off;' +service webserver { + runlevel = "2345" + user = "www-data" + group = "www-data" + capabilities = { "^cap_net_bind_service" } + command = "/usr/sbin/nginx -g 'daemon off;'" +} ``` ### Network Monitoring (Raw Sockets) @@ -73,10 +79,12 @@ service [2345] name:webserver \ Allow packet capture without root privileges: ```conf -service [2345] name:tcpdump \ - @tcpdump \ - caps:^cap_net_raw,^cap_net_admin \ - /usr/sbin/tcpdump -i eth0 -w /var/log/capture.pcap +service tcpdump { + runlevel = "2345" + user = "tcpdump" + capabilities = { "^cap_net_raw", "^cap_net_admin" } + command = "/usr/sbin/tcpdump -i eth0 -w /var/log/capture.pcap" +} ``` ### NTP Daemon (System Time) @@ -84,10 +92,12 @@ service [2345] name:tcpdump \ Allow time synchronization without full root: ```conf -service [2345] name:ntpd \ - @ntp \ - caps:^cap_sys_time,^cap_sys_nice \ - /usr/sbin/ntpd -n +service ntpd { + runlevel = "2345" + user = "ntp" + capabilities = { "^cap_sys_time", "^cap_sys_nice" } + command = "/usr/sbin/ntpd -n" +} ``` ## Available Capabilities @@ -116,9 +126,9 @@ Common capabilities include (see `man 7 capabilities` for the complete list): - Don't grant `cap_sys_admin` unless absolutely necessary 2. **Specify a user (preferably non-root)** - - The `@user` directive is **required** for `caps:` to take effect - - For ambient capabilities (`^`), use a non-root user (not `@root`) - - Example: `@www-data`, `@nginx`, `@tcpdump` + - The `user` setting is **required** for `capabilities` to take effect + - For ambient capabilities (`^`), use a non-root user (not `"root"`) + - Example: `user = "www-data"`, `user = "nginx"`, `user = "tcpdump"` 3. **Use ambient capabilities (`^`)** - The `^` prefix ensures capabilities survive exec() @@ -163,17 +173,17 @@ ps -o user,pid,cmd -p $(pidof nginx) ## Limitations -- The `caps:` directive requires `@user` to be specified for it to take effect - - Without `@user`, the service runs as root with full capabilities and - the `caps:` configuration is silently ignored - - You can use `@root` with `caps:`, but see below about ambient capabilities +- `capabilities` requires `user` to be set for it to take effect + - Without `user`, the service runs as root with full capabilities and + the `capabilities` list is silently ignored + - You can use `user = "root"`, but see below about ambient capabilities - For ambient capabilities (`^`, recommended), the user **must be non-root** - - Using `@root` with `caps:^...` will not work effectively, as ambient + - Using `user = "root"` with `^` capabilities will not work effectively, as ambient capabilities are only added to the effective set when euid ≠ 0 - - Use inheritable (`%`) or bounding (`!`) capabilities with `@root` if needed -- Services without `caps:` use standard privilege dropping: - - Services with `@user` (non-root) have no special capabilities - - Services without `@user` run as root with full capabilities + - Use inheritable (`%`) or bounding (`!`) capabilities with `user = "root"` if needed +- Services without `capabilities` use standard privilege dropping: + - Services with a non-root `user` have no special capabilities + - Services without `user` run as root with full capabilities - Some very old binaries may not work correctly with ambient capabilities - File system capabilities are not managed by Finit (use `setcap` for that) diff --git a/doc/config/files.md b/doc/config/files.md index bc004e52..9e084d5c 100644 --- a/doc/config/files.md +++ b/doc/config/files.md @@ -180,7 +180,7 @@ document [Finit Services](../service.md). Alternate finit.d/ ------------------ -**Syntax:** `rcsd /path/to/finit.d` +**Syntax:** `rcsd = "/path/to/finit.d"` The Finit rcS.d directory is set at compile time with: @@ -194,7 +194,7 @@ configurations, starting with the kernel command line option: This file in turn can use the `rcsd` directive to tell Finit to use another set of .conf files, e.g.: - rcsd /etc/factory.d + rcsd = "/etc/factory.d" > [!NOTE] > This directive is only available from the top-level bootstrap .conf @@ -203,6 +203,6 @@ another set of .conf files, e.g.: Including Finit Configs ------------------------ -**Syntax:** `include ` +**Syntax:** `include("CONF")` Include another configuration file. Absolute path required. diff --git a/doc/config/logging.md b/doc/config/logging.md index 57427412..743fd34d 100644 --- a/doc/config/logging.md +++ b/doc/config/logging.md @@ -1,13 +1,13 @@ General Logging =============== -**Syntax:** `log size:200k count:5` +**Syntax:** `log { size = 200k count = 5 }` Log rotation for run/task/services using the `log` sub-option with redirection to a log file. Global setting, applies to all services. The size can be given as bytes, without a specifier, or in `k`, `M`, -or `G`, e.g. `size:10M`, or `size:3G`. A value of `size:0` disables +or `G`, e.g. `size = 10M`, or `size = 3G`. A value of `size = 0` disables log rotation. The default is `200k`. The count value is recommended to be between 1-5, with a default 5. diff --git a/doc/config/runlevels.md b/doc/config/runlevels.md index 16f95085..bb7d97cd 100644 --- a/doc/config/runlevels.md +++ b/doc/config/runlevels.md @@ -66,7 +66,7 @@ least the loopback interface is brought up. Runlevel Configuration ---------------------- -**Syntax:** `runlevel ` +**Syntax:** `runlevel = N` The system runlevel to go to after bootstrap (S) has completed. `N` is the runlevel number 0-9, where 6 is reserved for reboot and 0 for halt. @@ -84,7 +84,7 @@ Finit disables networking in this mode. Networking ---------- -**Syntax:** `network ` +**Syntax:** `network = "PATH"` Script or program to bring up networking, with optional arguments. @@ -98,7 +98,7 @@ Debian, Ubuntu, Linux Mint, or an embedded BusyBox system. System Hostname --------------- -**Syntax:** `host `, or `hostname ` +**Syntax:** `hostname = "NAME"` Set system hostname to NAME, unless `/etc/hostname` exists in which case the contents of that file is used. @@ -111,10 +111,16 @@ Deprecated. We recommend using `/etc/hostname` instead. Kernel Modules -------------- -**Syntax:** `module [ARGS]` +**Syntax:** `modules = { "MODULE [ARGS]", ... }`, alias `mod` -Load a kernel module, with optional arguments. Similar to `insmod` -command line tool. +Load kernel modules, each with optional arguments. Similar to the +`insmod` command line tool. + + modules = { "button", "evdev", "softdog" } + +> [!NOTE] +> A list cannot hold comments; the lexer reads the entries after a `#` +> regardless. Put commented-out candidates above the list. Deprecated, there is both a `modules-load.so` and a `modprobe.so` plugin that can handle module loading better. The former supports loading from @@ -130,9 +136,9 @@ BusyBox mdev tool, add to `/etc/mdev.conf`: Resource Limits --------------- -**Syntax:** `rlimit [hard|soft] RESOURCE ` +**Syntax:** `rlimit { RESOURCE = LIMIT }`, with `soft.` or `hard.` prefix -Set the hard or soft limit for a resource, or both if that argument is +Set the hard or soft limit for a resource, or both if the prefix is omitted. `RESOURCE` is the lower-case `RLIMIT_` string constants from `setrlimit(2)`, without prefix. E.g. to set `RLIMIT_CPU`, use `cpu`. @@ -142,14 +148,11 @@ or the kernel `/proc/PID/limits` file, for details. Finit versions before v3.1 used `infinity` for `unlimited`, which is still supported, albeit deprecated. - # No process is allowed more than 8MB of address space - rlimit hard as 8388608 - - # Core dumps may be arbitrarily large - rlimit soft core infinity - - # CPU limit for all services, soft & hard = 10 sec - rlimit cpu 10 + rlimit { + hard.as = 8388608 # no more than 8MB of address space + soft.core = unlimited # core dumps may be arbitrarily large + cpu = 10 # soft & hard = 10 sec + } `rlimit` can be set globally, in `/etc/finit.conf`, or locally per each `/etc/finit.d/*.conf` read. I.e., a set of task/run/service @@ -158,7 +161,7 @@ stanzas can share the same rlimits if they are in the same .conf. Miscellaneous Settings ---------------------- -**Syntax:** `reboot-delay <0-60>` +**Syntax:** `reboot-delay = 0-60` Optional delay at reboot (or shutdown or halt) to allow kernel filesystem threads to complete after calling `sync(2)` before @@ -176,7 +179,7 @@ sync(2) has been called, twice. > writing; it can actually take a short time before all the blocks are > finally written. -**Syntax:** `reboot-watchdog ` +**Syntax:** `reboot-watchdog = true|false` Controls whether the system should reboot via the watchdog timer (WDT) or directly via the SoC/kernel. When enabled, Finit will: diff --git a/doc/config/runparts.md b/doc/config/runparts.md index 0a074449..ba97a569 100644 --- a/doc/config/runparts.md +++ b/doc/config/runparts.md @@ -1,7 +1,7 @@ Run-parts Scripts ----------------- -**Syntax:** `runparts [progress] [sysv] ` +**Syntax:** `runparts = "DIR"` Call [run-parts(8)][] on `DIR` to run start scripts. All executable files in the directory are called, in alphabetic order. The scripts in @@ -14,9 +14,10 @@ whatever the next runlevel is set to be (default 2). E.g., generate a **Options:** - - `progress`: display the progress of each script being executed - - `sysv`: run only SysV style scripts, i.e., `SNNfoo`, or `KNNbar`, - where `NN` is a number (0-99). + - `runparts-progress = true`: display the progress of each script being + executed + - `runparts-sysv = true`: run only SysV style scripts, i.e., `SNNfoo`, + or `KNNbar`, where `NN` is a number (0-99). If global debug mode is enabled, the `runparts` program is also called with the debug flag. diff --git a/doc/config/service-sync.md b/doc/config/service-sync.md index 46aabd15..1cf05319 100644 --- a/doc/config/service-sync.md +++ b/doc/config/service-sync.md @@ -43,8 +43,8 @@ like this: service bar { notify = "s6" command = "bar" } service qux { notify = "none" command = "qux" } -The `notify:none` syntax is for completeness in systems which run in -`readiness pid` mode (default). Services declared with `notify:none` +The `notify = "none"` setting is for completeness in systems which run in +`readiness pid` mode (default). Services declared with `notify = "none"` will transition to ready as soon as Finit has started them, e.g., `service/qux/ready`. diff --git a/doc/config/services.md b/doc/config/services.md index 7dbe49b7..8070acbd 100644 --- a/doc/config/services.md +++ b/doc/config/services.md @@ -1,7 +1,7 @@ Services ======== -**Syntax:** `service [LVLS] /path/to/daemon ARGS -- Optional description` +**Syntax:** `service NAME { command = "/path/to/daemon ARGS" }` Service, or daemon, to be monitored and automatically restarted if it exits prematurely. Finit tries to restart services that die, by default diff --git a/doc/config/sysv.md b/doc/config/sysv.md index e9378bfb..2a59a7c8 100644 --- a/doc/config/sysv.md +++ b/doc/config/sysv.md @@ -9,7 +9,7 @@ from a serialized boot process. SysV Init Scripts ----------------- -**Syntax:** `sysv [LVLS] /path/to/init-script -- Optional description` +**Syntax:** `sysv NAME { command = "/path/to/init-script" }` > `` is described in the [Services](services.md) section. @@ -45,7 +45,7 @@ making it perfect for most scenarios. For syntax details, see the [Run-parts Scripts](runparts.md) section. Here is an example take from a Debian installation: - runparts /etc/rc2.d + runparts = "/etc/rc2.d" Files in these directories are usually named `SNNfoo` and `KNNfoo`, which Finit knows about and automatically appends the correct argument: diff --git a/doc/config/task-and-run.md b/doc/config/task-and-run.md index 9f4994c6..17f9c7f7 100644 --- a/doc/config/task-and-run.md +++ b/doc/config/task-and-run.md @@ -1,7 +1,7 @@ run (sequence) -------------- -**Syntax:** `run [LVLS] /path/to/cmd ARGS -- Optional description` +**Syntax:** `run NAME { command = "/path/to/cmd ARGS" }` > `` is described in the [Services](services.md) section. @@ -31,7 +31,7 @@ also the `--quiet` and `--batch` options. task (parallel) --------------- -**Syntax:** `task [LVLS] /path/to/cmd ARGS -- Optional description` +**Syntax:** `task NAME { command = "/path/to/cmd ARGS" }` > `` is described in the [Services](services.md) section. @@ -93,7 +93,7 @@ The firewall rules are created once. The `exec-stop-post` script runs when entering runlevel 0 (halt) or 6 (reboot), or on explicit stop. > [!NOTE] -> The `remain:yes` option is not supported for bootstrap-only tasks -> (tasks with only runlevel S). Bootstrap tasks are deleted immediately -> after completion, and their `post:` scripts never run. A warning is -> logged if `remain:yes` is used on such tasks. +> The `remain-after-exit` option is not supported for bootstrap-only +> tasks (tasks with only runlevel S). Bootstrap tasks are deleted +> immediately after completion, and their `exec-stop-post` scripts never +> run. A warning is logged if `remain-after-exit` is used on such tasks. diff --git a/doc/config/tty.md b/doc/config/tty.md index fd0ee76a..616e504b 100644 --- a/doc/config/tty.md +++ b/doc/config/tty.md @@ -1,16 +1,32 @@ TTYs and Consoles ================= -**Syntax:** `tty [LVLS] DEV [BAUD] [noclear] [nowait] [nologin] [TERM]` - `tty [LVLS] CMD [noclear] [nowait]` - `tty [LVLS] [notty] [rescue]` - -The first variant of this option uses the built-in getty on the given -TTY device DEV, in the given runlevels. DEV may be the special keyword -`@console`, which is expanded from `/sys/class/tty/console/active`, -useful on embedded systems. - -The default baud rate is 0, i.e., keep kernel default. +**Syntax:** `tty NAME { device = DEV }` -- built-in getty + `tty NAME { command = "CMD ARGS" }` -- external getty + `tty NAME { notty = true }`, or `{ rescue = true }` -- bare shell, no device + +The block title NAME is what `initctl` shows. The three variants differ +in what they open: `device` runs the built-in getty on that TTY, and DEV +may be the special keyword `@console`, expanded from +`/sys/class/tty/console/active`, useful on embedded systems. `command` +hands the TTY to an external getty. `notty` opens nothing at all. + +Settings common to all three: + +| Setting | Alias | Description | +|---|---|---| +| `runlevel` | | Runlevels to run in, e.g. `"12345"` | +| `conditions` | `cond` | Conditions to wait for | +| `noclear` | | Do not clear the TTY after each session | +| `nowait` | | Do not wait for Enter before the login prompt | +| `nologin` | | Skip login, give a shell straight away | + +The `device` variant takes two more: + +| Setting | Description | +|---|---| +| `baud` | Baud rate, default 0, i.e., keep kernel default | +| `term` | `$TERM` value, e.g. `"vt220"` | > The `tty` stanza inherits runlevel, condition (and other feature) > parsing from the `service` stanza. So TTYs can run in one or many diff --git a/doc/example.md b/doc/example.md index 527961e5..cd47fdb1 100644 --- a/doc/example.md +++ b/doc/example.md @@ -10,69 +10,126 @@ See the [contrib/][contrib] directory on GitHub for examples, or take a peek at systems using Finit, like [Infix OS][infix] and [myLinux][]. > [!TIP] -> As of Finit v4.4, `.conf` lines can be broken up using the standard UNIX -> continuation character (`\`), trailing comments are also supported. The -> latter means you must escape any hashes used in directives and descriptions -> (`\#`). For more on this and examples, see the [finit.conf(5)][] manual or -> the [Configuration](config/index.md) section. +> A block spans as many lines as it needs, so no continuation character is +> called for. For the full syntax, see the [finit.conf(5)][] manual or the +> [Configuration](config/index.md) section. ```ApacheConf # Fallback if /etc/hostname is missing -host default +hostname = "default" # Runlevel to start after bootstrap, 'S', default: 2 -#runlevel 2 +#runlevel = 2 -# Support for setting global environment variables, using foo=bar syntax -# be careful though with variables like PATH, SHELL, LOGNAME, etc. -#PATH=/usr/bin:/bin:/usr/sbin:/sbin +# Global environment variables, be careful though with variables like +# PATH, SHELL, LOGNAME, etc. +#environment { +# PATH = "/usr/bin:/bin:/usr/sbin:/sbin" +#} # Max file size for each log file: 100 kiB, rotate max 4 copies: # log => log.1 => log.2.gz => log.3.gz => log.4.gz -log size=100k count=4 +log { + size = 100k + count = 4 +} # Services to be monitored and respawned as needed -service [S12345] env:-/etc/conf.d/watchdog watchdog $WATCHDOG_OPTS $WATCHDOG_DEV -- System watchdog daemon -service [S12345] env:-/etc/conf.d/syslog syslogd -n $SYSLOGD_OPTS -- System log daemon -service [S12345] env:-/etc/conf.d/klogd klogd -n $KLOGD_OPTS -- Kernel log daemon -service [2345] env:-/etc/conf.d/lldpd lldpd -d $LLDPD_OPTS -- LLDP daemon (IEEE 802.1ab) +service watchdog { + description = "System watchdog daemon" + runlevel = "S12345" + envfile = "-/etc/conf.d/watchdog" + command = "watchdog $WATCHDOG_OPTS $WATCHDOG_DEV" +} +service syslogd { + description = "System log daemon" + runlevel = "S12345" + envfile = "-/etc/conf.d/syslog" + command = "syslogd -n $SYSLOGD_OPTS" +} +service klogd { + description = "Kernel log daemon" + runlevel = "S12345" + conditions = { "pid/syslogd" } + envfile = "-/etc/conf.d/klogd" + command = "klogd -n $KLOGD_OPTS" +} +service lldpd { + description = "LLDP daemon (IEEE 802.1ab)" + runlevel = "2345" + envfile = "-/etc/conf.d/lldpd" + command = "lldpd -d $LLDPD_OPTS" +} # The BusyBox ntpd does not use syslog when running in the foreground # So we use this trick to redirect stdout/stderr to a log file. The # log file is rotated with the above settings. The condition declares -# a dependency on a system default route (gateway) to be set. A single -# at the beginning means ntpd does not respect SIGHUP for restart. -service [2345] log:/var/log/ntpd.log ntpd -n -l -I eth0 -- NTP daemon - -# For multiple instances of the same service, add :ID somewhere between -# the service/run/task keyword and the command. -service :80 [2345] merecat -n -p 80 /var/www -- Web server -service :8080 [2345] merecat -n -p 8080 /var/www -- Old web server +# a dependency on a system default route (gateway) to be set. ntpd +# does not respect SIGHUP, so Finit restarts it on reload instead. +service ntpd { + description = "NTP daemon" + runlevel = "2345" + conditions = { "net/route/default" } + reload-signal = "none" + log { file = "/var/log/ntpd.log" } + command = "ntpd -n -l -I eth0" +} + +# For multiple instances of the same service, add :ID to the block title. +service merecat:80 { + description = "Web server" + runlevel = "2345" + command = "merecat -n -p 80 /var/www" +} +service merecat:8080 { + description = "Old web server" + runlevel = "2345" + command = "merecat -n -p 8080 /var/www" +} # Alternative method instead of below runparts, can also use /etc/rc.local -#sysv [S] /etc/init.d/keyboard-setup -- Setting up preliminary keymap -#sysv [S] /etc/init.d/acpid -- Starting ACPI Daemon -#task [S] /etc/init.d/kbd -- Preparing console - -# Hidden from boot progress, using empty `--` description -#sysv [S] /etc/init.d/keyboard-setup -- -#sysv [S] /etc/init.d/acpid -- -#task [S] /etc/init.d/kbd -- +#sysv keyboard-setup { +# description = "Setting up preliminary keymap" +# runlevel = "S" +# command = "/etc/init.d/keyboard-setup" +#} + +# Hidden from boot progress, using an empty description +#sysv acpid { +# description = "" +# runlevel = "S" +# command = "/etc/init.d/acpid" +#} # Run start scripts from this directory -# runparts /etc/start.d +#runparts = "/etc/start.d" # Virtual consoles run BusyBox getty, keep kernel default speed -tty [12345] /sbin/getty -L 0 /dev/tty1 linux nowait noclear -tty [2345] /sbin/getty -L 0 /dev/tty2 linux nowait noclear -tty [2345] /sbin/getty -L 0 /dev/tty3 linux nowait noclear +tty tty1 { + runlevel = "12345" + command = "/sbin/getty -L 0 /dev/tty1 linux" + nowait = true + noclear = true +} +tty tty2 { + runlevel = "2345" + command = "/sbin/getty -L 0 /dev/tty2 linux" + nowait = true + noclear = true +} +tty tty3 { + runlevel = "2345" + command = "/sbin/getty -L 0 /dev/tty3 linux" + nowait = true + noclear = true +} # Use built-in getty for serial port and USB serial -#tty [12345] /dev/ttyAMA0 noclear nowait -#tty [12345] /dev/ttyUSB0 noclear +#tty ttyAMA0 { runlevel = "12345" device = "/dev/ttyAMA0" noclear = true nowait = true } +#tty ttyUSB0 { runlevel = "12345" device = "/dev/ttyUSB0" noclear = true } # Just give me a shell, I need to debug this embedded system! -#tty [12345] console noclear nologin +#tty console { runlevel = "12345" device = "@console" noclear = true nologin = true } ``` The `service` stanza, as well as `task`, `run` and others are described in @@ -82,21 +139,18 @@ Here's a quick overview of some of the most common components needed to start a UNIX daemon: ``` -service [LVLS] log env:[-]/etc/default/daemon daemon ARGS -- Example daemon -^ ^ ^ ^ ^ ^ ^ ^ -| | | | | | | `---------- Optional description -| | | | | | `------------------ Daemon arguments -| | | | | `------------------------- Path to daemon -| | | | `---------------------------------------------------- Optional env. file -| | | `-------------------------------------------------------- Redirect output to log -| | `--------------------------------------------------------------- Optional conditions -| `---------------------------------------------------------------------- Optional Runlevels - `------------------------------------------------------------------------------ Supervised program (daemon) +service NAME { <-- Supervised program (daemon) + description = "Example daemon" <-- Optional description + runlevel = "2345" <-- Optional runlevels + conditions = { "net/route/default" } <-- Optional conditions + envfile = "-/etc/default/daemon" <-- Optional env. file + log { } <-- Redirect output to log + command = "daemon ARGS" <-- Path to daemon, and its arguments +} ``` -Some components are optional: runlevel(s), condition(s) and description, -making it easy to create simple start scripts and still possible for more -advanced uses as well: +Only `command` is required, which makes simple cases short while leaving +room for more advanced uses: service sshd { command = "/usr/sbin/sshd -D" diff --git a/doc/features.md b/doc/features.md index 5f604a77..564db42a 100644 --- a/doc/features.md +++ b/doc/features.md @@ -18,9 +18,9 @@ waits for user input before handing over to `/bin/login`, which is responsible for handling the actual authentication. ```conf -tty [12345] /dev/tty1 nowait linux -tty [12345] /dev/ttyAMA0 noclear vt100 -tty [12345] /sbin/getty -L /dev/ttyAMA0 vt100 +tty tty1 { runlevel = "12345" device = "/dev/tty1" term = "linux" nowait = true } +tty ttyAMA0 { runlevel = "12345" device = "/dev/ttyAMA0" term = "vt100" noclear = true } +tty getty { runlevel = "12345" command = "/sbin/getty -L /dev/ttyAMA0 vt100" } ``` Users of embedded systems may want to enable automatic serial console @@ -29,7 +29,7 @@ system uses `ttyS0`, `ttyAMA0`, `ttyMXC0`, or anything else. Finit figures it out by querying sysfs: `/sys/class/tty/console/active`. ```conf -tty [12345] @console linux noclear +tty console { runlevel = "12345" device = "@console" term = "linux" noclear = true } ``` Notice the optional `noclear`, `nowait`, and `nologin` flags. The @@ -86,8 +86,17 @@ the condition `` when starting other scripts. Here is an example: ``` -run [S] /path/to/ident -- -task [2] /path/to/foo-init -- Initializing Foo board +run ident { + description = "" + runlevel = "S" + command = "/path/to/ident" +} +task foo-init { + description = "Initializing Foo board" + runlevel = "2" + conditions = { "hw/model/foo" } + command = "/path/to/foo-init" +} ``` > [!TIP] @@ -141,17 +150,20 @@ required privileges instead of running as root. This improves security by following the principle of least privilege. ```conf -service [2345] name:nginx \ - www-data:www-data \ - caps:^cap_net_bind_service \ - /usr/sbin/nginx -g 'daemon off;' +service nginx { + runlevel = "2345" + user = "www-data" + group = "www-data" + capabilities = { "^cap_net_bind_service" } + command = "/usr/sbin/nginx -g 'daemon off;'" +} ``` In this example, nginx runs as the unprivileged `www-data` user but retains the ability to bind to privileged ports (80, 443) through the `cap_net_bind_service` capability. -The `caps:` directive uses the IAB (Inheritable, Ambient, Bounding) format: +The `capabilities` list uses the IAB (Inheritable, Ambient, Bounding) format: - `^` = Ambient (recommended) - capabilities survive exec() - `%` = Inheritable only - requires file capabilities - `!` = Bounding - block from acquiring capability @@ -159,7 +171,7 @@ The `caps:` directive uses the IAB (Inheritable, Ambient, Bounding) format: Multiple capabilities can be specified as comma-separated: ```conf -caps:^cap_net_raw,^cap_net_admin,!cap_sys_admin +capabilities = { "^cap_net_raw", "^cap_net_admin", "!cap_sys_admin" } ``` See the [Linux Capabilities](config/capabilities.md) section for detailed diff --git a/doc/plugins.md b/doc/plugins.md index 73bdd9a3..5a889677 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -71,7 +71,7 @@ For your convenience a set of *optional* plugins are available: comment character, `#`, or `;`, is skipped. Modules are by default loaded in runlevel `S` using the `task` stanza. - Each module gets a unique `name:modprobe.foo`, and optional`:ID`. The + Each module is named `modprobe.foo`, with an optional `:ID`. The runlevel can be changed per file using: set runlevel 2345 diff --git a/doc/requirements.md b/doc/requirements.md index 89e6f872..5b24f8a2 100644 --- a/doc/requirements.md +++ b/doc/requirements.md @@ -15,7 +15,11 @@ done slightly differently and on systems with udev you might want to add the following one-shot task early in your `/etc/finit.conf`: ```conf -run [S] udevadm settle --timeout=120 -- Waiting for udev +run udevadm { + description = "Waiting for udev" + runlevel = "S" + command = "udevadm settle --timeout=120" +} ``` Finit has a built-in Getty for TTYs, but requires a working `/bin/login` diff --git a/doc/runlevels.md b/doc/runlevels.md index f03fecac..e976cf15 100644 --- a/doc/runlevels.md +++ b/doc/runlevels.md @@ -13,20 +13,37 @@ more of a policy for the user to define. Normally only runlevels 1-6 are used, and even more commonly, only the default runlevel is used. To specify an allowed set of runlevels for a `service`, `run` command, -`task`, or `tty`, add `[NNN]` to your `/etc/finit.conf`, like this: +`task`, or `tty`, set `runlevel` in your `/etc/finit.conf`, like this: ``` -service [S12345] syslogd -n -x -- System log daemon -run [S] /etc/init.d/acpid start -- Starting ACPI Daemon -task [S] /etc/init.d/kbd start -- Preparing console -service [S12345] klogd -n -x -- Kernel log daemon +service syslogd { + description = "System log daemon" + runlevel = "S12345" + command = "syslogd -n -x" +} +run acpid { + description = "Starting ACPI Daemon" + runlevel = "S" + command = "/etc/init.d/acpid start" +} +task kbd { + description = "Preparing console" + runlevel = "S" + command = "/etc/init.d/kbd start" +} +service klogd { + description = "Kernel log daemon" + runlevel = "S12345" + conditions = { "pid/syslogd" } + command = "klogd -n -x" +} -tty [12345] /dev/tty1 -tty [2] /dev/tty2 -tty [2] /dev/tty3 -tty [2] /dev/tty4 -tty [2] /dev/tty5 -tty [2] /dev/tty6 +tty tty1 { runlevel = "12345" device = "/dev/tty1" } +tty tty2 { runlevel = "2" device = "/dev/tty2" } +tty tty3 { runlevel = "2" device = "/dev/tty3" } +tty tty4 { runlevel = "2" device = "/dev/tty4" } +tty tty5 { runlevel = "2" device = "/dev/tty5" } +tty tty6 { runlevel = "2" device = "/dev/tty6" } ``` In this example syslogd is first started, in parallel, and then acpid is @@ -46,8 +63,14 @@ are also removed when they have completed, `initctl show` will not list them. ``` -task [S] echo "foo" | cat >/tmp/bar -run [S] echo "$HOME" >/tmp/secret +task foo { + runlevel = "S" + command = "echo \"foo\" | cat >/tmp/bar" +} +run secret { + runlevel = "S" + command = "echo \"$HOME\" >/tmp/secret" +} ``` Switching between runlevels can be done by calling init with a single diff --git a/doc/switchroot.md b/doc/switchroot.md index c342b487..848c2e4c 100644 --- a/doc/switchroot.md +++ b/doc/switchroot.md @@ -51,27 +51,52 @@ Configuration file `/etc/finit.conf` in the initramfs: # /etc/finit.conf in initramfs # Mount the real root filesystem -run [S] name:mount-root /bin/mount /dev/sda1 /mnt/root -- Mounting root filesystem +run mount-root { + description = "Mounting root filesystem" + runlevel = "S" + command = "/bin/mount /dev/sda1 /mnt/root" +} # Switch to real root after mount completes -run [S] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root +run switch-root { + description = "Switching to real root" + runlevel = "S" + command = "/sbin/initctl switch-root /mnt/root" +} ``` For more complex setups (LUKS, LVM, etc.): ``` # Unlock LUKS volume -# The tty:@console stanza is required so cryptsetup can prompt for a passphrase -run [S] name:cryptsetup tty:@console /sbin/cryptsetup open /dev/sda2 cryptroot -- Unlocking encrypted root +# The tty setting is required so cryptsetup can prompt for a passphrase +run cryptsetup { + description = "Unlocking encrypted root" + runlevel = "S" + tty = "@console" + command = "/sbin/cryptsetup open /dev/sda2 cryptroot" +} # Activate LVM -run [S] name:lvm /sbin/lvm vgchange -ay -- Activating LVM volumes +run lvm { + description = "Activating LVM volumes" + runlevel = "S" + command = "/sbin/lvm vgchange -ay" +} # Mount root -run [S] name:mount-root /bin/mount /dev/vg0/root /mnt/root -- Mounting root +run mount-root { + description = "Mounting root" + runlevel = "S" + command = "/bin/mount /dev/vg0/root /mnt/root" +} # Switch root -run [S] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root +run switch-root { + description = "Switching to real root" + runlevel = "S" + command = "/sbin/initctl switch-root /mnt/root" +} ``` @@ -85,15 +110,34 @@ difficult in runlevel S, you can perform the switch-root in runlevel 1: # /etc/finit.conf in initramfs # Start mdevd for device handling -service [S] name:mdevd notify:s6 /sbin/mdevd -D %n -- Device event daemon -run [S] name:coldplug /sbin/mdevd-coldplug -- Coldplug devices +service mdevd { + description = "Device event daemon" + runlevel = "S" + notify = "s6" + command = "/sbin/mdevd -D %n" +} +run coldplug { + description = "Coldplug devices" + runlevel = "S" + conditions = { "service/mdevd/ready" } + command = "/sbin/mdevd-coldplug" +} # Mount the real root filesystem (after devices are ready) -run [S] name:mount-root /bin/mount /dev/sda1 /mnt/root -- Mounting root +run mount-root { + description = "Mounting root" + runlevel = "S" + conditions = { "run/coldplug/success" } + command = "/bin/mount /dev/sda1 /mnt/root" +} # Transition to runlevel 1 after all S tasks complete # The switch-root runs cleanly in runlevel 1 -run [1] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root +run switch-root { + description = "Switching to real root" + runlevel = "1" + command = "/sbin/initctl switch-root /mnt/root" +} ``` This approach separates the initramfs setup (runlevel S) from the From ddda9054879003d434e83c403c8b36555f7d3142 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 28 Jul 2026 17:55:34 +0200 Subject: [PATCH 20/28] service: warn when capabilities cannot take effect An ambient capability only reaches the effective set when euid is non-zero, so a service that pairs `capabilities = { "^cap_..." }` with a root user gets none of the restriction it asks for, and keeps the full root set instead. Finit read the list, applied it, and said nothing. A build without libcap dropped the list on the floor just as quietly. Both now warn, naming the service: nginx: ambient capabilities ('^') have no effect as root, use a non-root user, or '%' and '!' entries The ambient entries are read back from the parsed IAB value rather than matched in the text, so inheritable ('%') and bounding ('!') entries stay silent -- those work fine as root. The warning repeats when the .conf files are re-read on runlevel change, as parse warnings here already do. Signed-off-by: Joachim Wiberg --- doc/config/capabilities.md | 7 ++++++- src/service.c | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/doc/config/capabilities.md b/doc/config/capabilities.md index 5aa16c0a..9968b971 100644 --- a/doc/config/capabilities.md +++ b/doc/config/capabilities.md @@ -169,7 +169,8 @@ ps -o user,pid,cmd -p $(pidof nginx) - Linux kernel 4.3+ (for ambient capabilities support) - libcap library installed -- Finit built with `--enable-libcap` +- Finit built with `--enable-libcap`, otherwise a `capabilities` list is + ignored, with a warning ## Limitations @@ -181,6 +182,10 @@ ps -o user,pid,cmd -p $(pidof nginx) - Using `user = "root"` with `^` capabilities will not work effectively, as ambient capabilities are only added to the effective set when euid ≠ 0 - Use inheritable (`%`) or bounding (`!`) capabilities with `user = "root"` if needed + - Finit warns about this when reading the .conf file: + + nginx: ambient capabilities ('^') have no effect as root, use a + non-root user, or '%' and '!' entries - Services without `capabilities` use standard privilege dropping: - Services with a non-root `user` have no special capabilities - Services without `user` run as root with full capabilities diff --git a/src/service.c b/src/service.c index 34647923..4afd9f1e 100644 --- a/src/service.c +++ b/src/service.c @@ -1537,11 +1537,26 @@ static void parse_caps(svc_t *svc, char *caps) return; } + if (!strcmp(svc->username, "root")) { + cap_value_t cap; + + for (cap = 0; cap <= CAP_LAST_CAP; cap++) { + if (!cap_iab_get_vector(cap_iab, CAP_IAB_AMB, cap)) + continue; + + /* the ambient set only reaches effective when euid != 0 */ + logit(LOG_WARNING, "%s: ambient capabilities ('^') have no effect" + " as root, use a non-root user, or '%%' and '!' entries", + svc_ident(svc, NULL, 0)); + break; + } + } + cap_free(cap_iab); strlcpy(svc->capabilities, caps, sizeof(svc->capabilities)); #else - (void)svc; - (void)caps; + logit(LOG_WARNING, "%s: capabilities require Finit built with --enable-libcap," + " ignoring '%s'", svc_ident(svc, NULL, 0), caps); #endif } From c584795202cda6fe4bb8b8f1b28ce5fcf3c3b3a0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 29 Jul 2026 09:34:46 +0200 Subject: [PATCH 21/28] doc: fix back-references and stale claims left by the conversion Reference sections kept pointing at the line-based format they no longer document. `sysv` and `task` sent the reader to Services for "", the cgroups chapter opened by listing three legacy directives and then explained further down that only two of them exist here, and the logging chapter still gave "log:prio:facility.level,tag:ident" as the full syntax. Some claims were wrong independent of the format: - a sysv is a supervised daemon, grouped with service in SVC_TYPE_DAEMON, not a variation on task - restart-max has no upper bound of 255, or any other - the built-in rescue fallback runs in 12345789, not 12345 - conditional loading quotes system/10-hotplug.conf, not system/hotplug.conf - the key spells conflicts, not conflict - the built-in getty no longer wants TERM last, it is a key `if` takes either a service name or, in angle brackets, a condition, decided in svc_ifthen(). Only the examples showed this, so it is now said. Terminology follows the split index.md already draws: a block is the new format, a stanza the line-based one. src/rescue.conf was still line-based, missed because it sits in src/ rather than system/ or contrib/. Signed-off-by: Joachim Wiberg --- doc/cmdline.md | 7 +++-- doc/conditions.md | 21 ++++++------- doc/config/cgroups.md | 36 +++++++++++----------- doc/config/files.md | 10 +++--- doc/config/logging.md | 32 +++++++++++-------- doc/config/rescue.md | 11 ++++--- doc/config/runlevels.md | 10 +++--- doc/config/runparts.md | 6 ++-- doc/config/service-opts.md | 18 ++++++----- doc/config/service-sync.md | 14 ++++----- doc/config/service-wrappers.md | 8 ++--- doc/config/services.md | 56 +++++++++++++++++++--------------- doc/config/sysv.md | 31 ++++++++++--------- doc/config/task-and-run.md | 8 +++-- doc/config/templating.md | 4 +-- doc/config/tty.md | 55 ++++++++++++++++----------------- doc/example.md | 2 +- doc/features.md | 16 +++++----- doc/plugins.md | 2 +- doc/runparts.md | 8 ++--- src/rescue.conf | 7 +++-- 21 files changed, 191 insertions(+), 171 deletions(-) diff --git a/doc/cmdline.md b/doc/cmdline.md index bfc30a2c..2732a5cd 100644 --- a/doc/cmdline.md +++ b/doc/cmdline.md @@ -41,7 +41,7 @@ The `bool` setting is one of `on, off, true false, 1, 0`. Useful when starting up in various [rescue mode][rescue], factory, or production test setups. Use the top-level configuration file - directive `rcsd /path/to/finit.d` to override the default rcS.d + setting `rcsd = "/path/to/finit.d"` to override the default rcS.d directory. * `finit.debug[=bool]`: Enable finit debug. This is operated @@ -104,8 +104,9 @@ The `bool` setting is one of `on, off, true false, 1, 0`. * `single`, or `S`: Overrides the configured runlevel (default: 2) to go to after bootstrap by forcing it to runlevel 1, this is also known as single user mode. Useful to debug startup problems. All services - and TTYs in `[1]` will be started, so a `tty [1] @console nologin` - configuration presents you with a root console without login. + and TTYs in runlevel 1 will be started, so a `tty` block with + `runlevel = "1"`, `device = "@console"`, and `nologin = true` + presents you with a root console without login. * `1-9`, except `6`: override the configured `runlevel`. Like the `S` and `rescue`, giving a single number on the kernel command line tells diff --git a/doc/conditions.md b/doc/conditions.md index d16ebaa3..bb425c4e 100644 --- a/doc/conditions.md +++ b/doc/conditions.md @@ -7,12 +7,11 @@ mechanism for common synchronization problems. For example: - *"wait for service A to start before starting service B"*, or - *"wait for basic network access to be available"* -Conditions are similar in syntax to declaring runlevels per service. -They are specified within angle brackets `<>` and can be applied to any -of the `service`, `task`, or `run` stanza. Multiple conditions may be -specified separated by comma. Multiple conditions are logically AND'ed +A condition is named in the `conditions` list of a `service`, `task`, or +`run` block. The list may hold several, and they are logically AND'ed during evaluation, i.e. all conditions must be satisfied in order for a -service to run. +service to run. In running text, and in `initctl` output, a condition +is written inside angle brackets, ``. One prefix can be used on a condition: @@ -188,15 +187,15 @@ The `devmon` (built-in) plugin monitors `/dev` and `/dev/dir` for device nodes being created and removed. It is active only when a run, task, or service has declared a `` or `` condition. -The `pidfile` plugin (recursively) watches `/run/` (recursively) for PID -files created by the monitored services, and sets a corresponding -condition in the `pid/` namespace. +The `pidfile` plugin recursively watches `/run/` for PID files created +by the monitored services, and sets a corresponding condition in the +`pid/` namespace. Similarly, the `netlink` plugin provides basic conditions for when an interface is brought up/down and when a default route (gateway) is set, in the `net/` namespace. -The `sys` and `usr` plugins monitor are passive condition monitors where +The `sys` and `usr` plugins are passive condition monitors where the action is provided by `keventd`, signal handlers, and in the case of `usr`, the end-user via the `initctl` tool. @@ -266,8 +265,8 @@ its conditions are cleared and reasserted, ensuring dependent services are properly updated. Daemons that don't create PID files, or fail to touch them on reload, -can be worked around by using the `pid:/path/to/file.pid` syntax in -the service stanza for the daemon. It is far from optimal since any +can be worked around by setting `pidfile` and `pidfile-create` in the +service block for the daemon. It is far from optimal since any synchronization of depending services may fail due to the daemon not having reinitialized/created their IPC sockets, or similar. diff --git a/doc/config/cgroups.md b/doc/config/cgroups.md index 6e96824f..2e17d061 100644 --- a/doc/config/cgroups.md +++ b/doc/config/cgroups.md @@ -1,19 +1,17 @@ -Finit provides three different cgroup directives for controlling resource allocation: +Finit has two `cgroup` blocks for controlling resource allocation: - 1. **Top-level cgroup definition**: `cgroup NAME settings` - - Defines a top-level cgroup (e.g., `init`, `system`, `user`) with default settings - - Space-separated syntax - - Example: `cgroup system cpu.weight:9700` + 1. **Top-level definition**, at file scope: declares a group such as + `init`, `system`, or `user`, and its default settings. - 2. **Global cgroup selector**: `cgroup.NAME[,options]` (standalone directive) - - Sets the default cgroup for subsequent services in a `.conf` file - - Dot-separated with optional comma-separated options - - Example: `cgroup.maint` or `cgroup.system,delegate` + cgroup system { cpu.weight = 9700 } - 3. **Per-service cgroup option**: `cgroup.NAME[,options]` or `cgroup:options` - - Overrides the cgroup for a specific service - - Part of the service directive line - - Example: `service [...] cgroup.maint,mem.max:1G /path/to/cmd` + 2. **Joining a group**, inside a service block: names the group this + service runs in, and may override settings for itself alone. + + service foo { + cgroup maint { memory.max = 1G } + command = "/path/to/cmd" + } > [!NOTE] > Linux cgroups and details surrounding values are not explained in the @@ -84,11 +82,11 @@ apply to that service alone: > joins a group says so itself, so the group cannot depend on what came > earlier in the file. -Note the `mem.` exception to the rule: every cgroup setting maps directly to -cgroup v2 syntax. I.e., `cpu.max` maps to the file `/sys/fs/cgroup/maint/foo/cpu.max`. -There is no filtering, except for expanding the shorthand `mem.` to `memory.`. -If the file is not available, either the cgroup controller is not available -in your Linux kernel, or the name is misspelled. +Every cgroup setting maps directly to cgroup v2 syntax, so `cpu.max` +maps to the file `/sys/fs/cgroup/maint/foo/cpu.max`. There is no +filtering, the one exception being the shorthand `mem.`, which expands +to `memory.`. If the file is not available, either the controller is +missing from your Linux kernel, or the name is misspelled. ### Overriding Cgroup Leaf Names @@ -229,7 +227,7 @@ Initially, the service process runs directly in the cgroup root: Once the container runtime creates child cgroups (e.g., `libpod-*/`), cgroups v2 enforces the "no internal processes" rule. When Finit detects this (`EBUSY` error), -it automatically creates an `supervisor/` subdirectory and moves service-related +it automatically creates a `supervisor/` subdirectory and moves service-related processes there: /sys/fs/cgroup/system/container@web/ diff --git a/doc/config/files.md b/doc/config/files.md index 9e084d5c..22b01b1a 100644 --- a/doc/config/files.md +++ b/doc/config/files.md @@ -70,12 +70,12 @@ unique group, where files within each group are sorted alphabetically. /etc/finit.d/enabled/1-aaa.conf /etc/finit.d/enabled/1-abc.conf -The resulting combined configuration is read line by line, each `run`, +The resulting combined configuration is read in order, each `run`, `task`, and `service` added to an ordered list that ensures they are started in the same order. This is important because of the blocking -properties of the `run` statement. For an example on the relation of -`service` and `run` statements, and dependency handling between them, -see [Conditional Loading](services.md#conditional-loading), below. +properties of `run`. For an example on the relation of `service` and +`run`, and dependency handling between them, see +[Conditional Loading](services.md#conditional-loading), below. > [!NOTE] > The names `finit.conf` and `finit.d/` are only defaults. They can be @@ -84,7 +84,7 @@ see [Conditional Loading](services.md#conditional-loading), below. > > They can also be overridden from the [kernel command line](../cmdline.md) > using: `-- finit.config=/etc/bar.conf` and in that file use the -> top-level configuration directive `rcsd /path/to/finit.d`. +> top-level setting `rcsd = "/path/to/finit.d"`. Filesystem Layout ----------------- diff --git a/doc/config/logging.md b/doc/config/logging.md index 743fd34d..70959f09 100644 --- a/doc/config/logging.md +++ b/doc/config/logging.md @@ -3,8 +3,8 @@ General Logging **Syntax:** `log { size = 200k count = 5 }` -Log rotation for run/task/services using the `log` sub-option with -redirection to a log file. Global setting, applies to all services. +Log rotation for run/task/services that redirect output to a log file +with their own `log` block. Global setting, applies to all services. The size can be given as bytes, without a specifier, or in `k`, `M`, or `G`, e.g. `size = 10M`, or `size = 3G`. A value of `size = 0` disables @@ -17,22 +17,30 @@ size limit is reached. Redirecting Output ------------------ -The `run`, `task`, and `service` stanzas also allow the keyword `log` to -redirect `stderr` and `stdout` of the application to a file or syslog +The `run`, `task`, and `service` blocks take a `log` block of their own, +redirecting `stderr` and `stdout` of the application to a file or syslog using the native `logit` tool. This is useful for programs that do not support syslog on their own, which is sometimes the case when running in the foreground. -The full syntax is: +An empty block means syslog with the defaults, and three keys adjust it: - log:/path/to/file - log:prio:facility.level,tag:ident - log:console - log:null - log +| Setting | Description | +|---|---| +| `file` | Write to this path instead of syslog | +| `priority` | Syslog `facility.level`, default `daemon.info` | +| `identity` | Syslog tag, default the basename of the command | -Default `prio` is `daemon.info` and default `tag` is the basename of the -service or run/task command. +`/dev/console` and `/dev/null` are spelled as the paths they are: + + service foo { log { } command = "foo" } # syslog + service foo { log { file = "/var/log/foo" } command = "foo" } # a file + service foo { log { file = "/dev/console" } command = "foo" } # console + service foo { log { file = "/dev/null" } command = "foo" } # discard + +> [!NOTE] +> A `log` block at file scope is a different setting -- that one is the +> global rotation above, and it takes only `size` and `count`. Log rotation is controlled using the global `log` setting. diff --git a/doc/config/rescue.md b/doc/config/rescue.md index a690c150..0ce6d683 100644 --- a/doc/config/rescue.md +++ b/doc/config/rescue.md @@ -47,21 +47,22 @@ system administrator. The bundled default `rescue.conf` contains nothing more than: - runlevel 1 + runlevel = 1 tty rescue { runlevel = "12345" rescue = true } -The `tty` has the `rescue` option set, which works similar to the board -bring-up tty option `notty`. The major difference being that `sulogin` +The `tty` block has `rescue` set, which works similar to the board +bring-up setting `notty`. The major difference being that `sulogin` is started to query for root/admin password. If `sulogin` is not found, `rescue` behaves like `notty` and gives a plain root shell prompt. -If Finit cannot find `/lib/finit/rescue.conf` it defaults to: +If Finit cannot find `/lib/finit/rescue.conf` it falls back to a +built-in equivalent, which runs in every runlevel it can: tty rescue { - runlevel = "12345" + runlevel = "12345789" rescue = true } diff --git a/doc/config/runlevels.md b/doc/config/runlevels.md index bb7d97cd..905270ca 100644 --- a/doc/config/runlevels.md +++ b/doc/config/runlevels.md @@ -38,8 +38,8 @@ Example: } When bootstrap has completed, Finit moves to runlevel 2. This can be -changed in `/etc/finit.conf` using the `runlevel N` directive, or by a -script running in runlevel S that calls, e.g., `initctl runlevel 9`. +changed in `/etc/finit.conf` with `runlevel = N`, or by a script running +in runlevel S that calls, e.g., `initctl runlevel 9`. The latter is useful if startup scripts detect problems outside of Finit's control, e.g., critical services/devices missing or hardware problems. @@ -52,7 +52,7 @@ complete before proceeding to 2. Finit first stops everything that is not allowed to run in 2, and then brings up networking. Networking is expected to be available in all runlevels except: S, 1 (single user level), 6, and 0. Networking is -enabled either by the `network script` directive, or if you have an +enabled either by `network = "script"`, or if you have an `/etc/network/interfaces` file, Finit calls `ifup -a` -- at the very least the loopback interface is brought up. @@ -88,7 +88,7 @@ Networking Script or program to bring up networking, with optional arguments. -Deprecated. We recommend using dedicated task/run stanzas per runlevel, +Deprecated. We recommend using dedicated task/run blocks per runlevel, or `/etc/network/interfaces` if you have a system with `ifupdown`, like Debian, Ubuntu, Linux Mint, or an embedded BusyBox system. @@ -156,7 +156,7 @@ albeit deprecated. `rlimit` can be set globally, in `/etc/finit.conf`, or locally per each `/etc/finit.d/*.conf` read. I.e., a set of task/run/service -stanzas can share the same rlimits if they are in the same .conf. +blocks can share the same rlimits if they are in the same .conf. Miscellaneous Settings ---------------------- diff --git a/doc/config/runparts.md b/doc/config/runparts.md index ba97a569..3d57b662 100644 --- a/doc/config/runparts.md +++ b/doc/config/runparts.md @@ -25,7 +25,7 @@ with the debug flag. **Limitations:** Scripts called from `runparts`, or hook scripts (see below), are limited -in their interaction with Finit. Like the standalone `run` stanza and +in their interaction with Finit. Like a standalone `run` block and the `/etc/rc.local` shell script, Finit waits for their completion before continuing. None of them can issue commands to start, stop, or restart other services. Also, ensure all your services and programs @@ -40,13 +40,13 @@ either terminate or start in the background or you will block Finit. It can be beneficial to use `01-name`, `02-othername`, etc., to ensure the scripts are started in that order, e.g., if there is a dependency -order between scripts. Symlinks to existing daemons can talso be used, +order between scripts. Symlinks to existing daemons can also be used, but make sure they daemonize (background) themselves properly, otherwise Finit will lock up. If `S[0-9]foo` and `K[0-9]bar` style naming is used, the executable will be called with an extra argument, `start` and `stop`, respectively. E.g., `S01foo` will be called as `S01foo start`. Of course, `S01foo` -and `K01foo` may be a symlink to to `another/directory/foo`. +and `K01foo` may be a symlink to `another/directory/foo`. [run-parts(8)]: http://manpages.debian.org/cgi-bin/man.cgi?query=run-parts diff --git a/doc/config/service-opts.md b/doc/config/service-opts.md index e5bb5bfd..7c5ef7da 100644 --- a/doc/config/service-opts.md +++ b/doc/config/service-opts.md @@ -58,7 +58,8 @@ Other run/task/service settings are: * `notify` -- see [Service Synchronization](service-sync.md) * `if` -- see [Conditional Execution](services.md#conditional-execution) * `type = "forking"` -- see description of the [service](services.md) block - * a leading `-` on `command` -- see [Conditional Loading](services.md#conditional-loading) + * a leading `-` on `command` -- see + [Conditional Loading](services.md#conditional-loading) Restarting ---------- @@ -68,13 +69,14 @@ on the configuration and conditions. Within the confines of that the following settings are available: * `restart-max = NUM` -- number of times Finit tries to restart a - crashing service, default: 10, max: 255. When this limit is - reached the service is marked *crashed* and must be restarted - manually with `initctl restart NAME` + crashing service, default: 10. When this limit is reached the + service is marked *crashed* and must be restarted manually with + `initctl restart NAME` * `restart-sec = SEC` -- number of seconds before Finit tries to - restart a crashing service, default: 2 seconds for the first five - retries, then back-off to 5 seconds. The maximum of this - configured value and the above (2 and 5) will be used + restart a crashing service. The default is 2 seconds for the first + half of `restart-max` attempts, then a back-off to 5 seconds -- with + the default `restart-max` that is the first five retries. The + greater of this configured value and the back-off is used * `restart = "always"` -- no upper limit on the number of times Finit tries to restart a crashing service * `restart = "never"` -- do not restart on failures. `false` is @@ -168,7 +170,7 @@ accident: When a run/task/sysv/service is removed (disable + reload) it is first stopped and then removed from the runlevel. The `exec-stop-post` script always runs when the process has stopped, and `exec-cleanup` -runs when the stanza has been removed from the runlevel. +runs when the block has been removed from the runlevel. > [!IMPORTANT] > These script actions are intended for setup, cleanup, and readiness diff --git a/doc/config/service-sync.md b/doc/config/service-sync.md index 1cf05319..079dc0e2 100644 --- a/doc/config/service-sync.md +++ b/doc/config/service-sync.md @@ -32,7 +32,7 @@ notification is available, and the native PID file mode of operation is, as of Finit v4.6 optional, by default it is still enabled, but this can be changed in `finit.conf`: - readiness none + readiness = "none" This will be made the default in Finit 5.0. In this mode of operation, every service needs to explicitly declare their readiness notification, @@ -43,10 +43,10 @@ like this: service bar { notify = "s6" command = "bar" } service qux { notify = "none" command = "qux" } -The `notify = "none"` setting is for completeness in systems which run in -`readiness pid` mode (default). Services declared with `notify = "none"` -will transition to ready as soon as Finit has started them, e.g., -`service/qux/ready`. +The `notify = "none"` setting is for completeness in systems which run +in `readiness = "pid"` mode (default). Services declared with +`notify = "none"` will transition to ready as soon as Finit has started +them, e.g., `service/qux/ready`. To synchronize two services the following condition can be used: @@ -60,7 +60,7 @@ To synchronize two services the following condition can be used: command = "stress-ng --cpu 8" } -For details on the syntax and options, see below. +For the full list of conditions, see [Finit Conditions](../conditions.md). > [!NOTE] > On `initctl reload` conditions are set in "flux", while figuring out @@ -73,7 +73,7 @@ For details on the syntax and options, see below. > However, the s6 notify mode does not support this because in s6 you > are expected to close your notify descriptor after having written > `\n`. This means s6 style daemons currently must be stop-started. -> (Declare the service with `` in its condition statement.) +> (Declare the service with `reload-signal = "none"`.) > > For default, PID file style readiness notification, daemons are > expected to either create their PID files, or touch it using diff --git a/doc/config/service-wrappers.md b/doc/config/service-wrappers.md index f0d125a8..e4fb9798 100644 --- a/doc/config/service-wrappers.md +++ b/doc/config/service-wrappers.md @@ -7,7 +7,7 @@ use a wrapper shell script to start your service. The Finit service `.conf` file can be put into `/etc/finit.d/available`, so you can control the service using `initctl`. Then use the path to -the wrapper script in the Finit `.conf` service stanza. The following +the wrapper script in the Finit `.conf` service block. The following example employs a wrapper script in `/etc/start.d`. **Example:** @@ -31,6 +31,6 @@ example employs a wrapper script in `/etc/start.d`. exec /usr/bin/program $OPTIONS > [!NOTE] -> The example sets `` to denote that it doesn't support `SIGHUP`. -> That way Finit will stop/start the service instead of sending SIGHUP -> at restart/reload events. +> The example sets `reload-signal = "none"` to say the program does not +> support `SIGHUP`. Finit then stop/starts the service instead of +> signalling it at restart/reload events. diff --git a/doc/config/services.md b/doc/config/services.md index 8070acbd..857a335e 100644 --- a/doc/config/services.md +++ b/doc/config/services.md @@ -7,10 +7,11 @@ Service, or daemon, to be monitored and automatically restarted if it exits prematurely. Finit tries to restart services that die, by default 10 times before giving up and marking them as *crashed*. After which they have to be manually restarted with `initctl restart NAME`. The -limits controlling this are configurable, see the options below. +limits controlling this are configurable, see +[Service Options](service-opts.md). > [!TIP] -> To allow endless restarts, see the [`respawn` option](service-opts.md) +> To allow endless restarts, see [`respawn`](service-opts.md#restarting) For daemons that support it, we recommend appending `--foreground`, `--no-background`, `-n`, `-F`, or similar command line argument to @@ -59,11 +60,11 @@ prevent it from forking to the background: `runlevel` denotes the runlevels `ospfd` is allowed to run in, it is optional and defaults to level 2-4 if omitted. -`conditions` lists what must be asserted before starting `ospfd`. In this example Finit -waits for another service, `zebra`, to have created its PID file in -`/var/run/quagga/zebra.pid` before starting `ospfd`. Finit watches -*all* files in `/var/run`, for each file named `*.pid`, or `*/pid`, -Finit opens it and find the matching `NAME:ID` using the PID. +`conditions` lists what must be asserted before starting `ospfd`. In +this example Finit waits for another service, `zebra`, to have created +its PID file in `/var/run/quagga/zebra.pid`. Finit watches *all* files +in `/var/run`, for each file named `*.pid`, or `*/pid`, Finit opens it +and finds the matching `NAME:ID` using the PID. A condition may be prefixed with `~` to propagate a reload of the upstream service to this one, rather than merely pausing and resuming @@ -216,8 +217,8 @@ web server would be started and supervised. Conditional Loading ------------------- -Finit support conditional loading of stanzas. The following example is -take from the `system/hotplug.conf` file in the Finit distribution. +Finit supports conditional loading of blocks. The following example is +taken from the `system/10-hotplug.conf` file in the Finit distribution. Here we only show a simplified subset. Starting with the leading `-` on `command`. @@ -235,7 +236,7 @@ Starting with the leading `-` on `command`. When loading the .conf file Finit looks for `/lib/systemd/systemd-udevd`, and if that is not found it logs a warning. The leading `-` says a missing binary is expected here, so -the stanza is skipped quietly and the second block can be evaluated, +the block is skipped quietly and the second one can be evaluated, which also provides a service named `udevd`. run udevadm:1 { @@ -245,7 +246,7 @@ which also provides a service named `udevd`. command = "-udevadm settle -t 0" } -This line is only loaded if we know of a service named `udevd`. Again, +This block is only loaded if we know of a service named `udevd`. Again, we do not warn if `udevadm` is not found, execution will also stop here until the PID condition is asserted, i.e., Finit detecting udevd has started. @@ -260,8 +261,8 @@ started. If `udevd` is not available, we try to run `mdev`, but if that is not found, again we do not warn. -Conditional loading statements can also be negated, so the previous -stanza can also be written as: +Conditional loading can also be negated, so the previous block can be +written as: run mdev { description = "Populating device tree" @@ -270,18 +271,19 @@ stanza can also be written as: command = "-mdev -s" } -The reason for using `conflict` in this example is that a conflict can be -resolved. Stanzas naming a conflict are rechecked at runtime. +The reason for using `conflicts` in this example is that a conflict can +be resolved. Blocks naming a conflict are rechecked at runtime. Conditional Execution --------------------- -Similar to conditional loading of stanzas there is conditional runtime +Similar to conditional loading of blocks there is conditional runtime execution. This can be confusing at first, since Finit already has a condition subsystem, but this is more akin to the qualification to a -runlevel. E.g., a `task [123]` is qualified to run only in runlevel 1, -2, and 3. It is not considered for other runlevels. +runlevel. E.g., a task with `runlevel = "123"` is qualified to run +only in runlevel 1, 2, and 3. It is not considered for other +runlevels. Conditional execution qualify a run/task/service based on a condition. Consider this (simplified) example from the Infix operating system: @@ -299,10 +301,10 @@ Consider this (simplified) example from the Infix operating system: command = "confd --load failure-config" } -The two run statements reside in the same .conf file so Finit runs them -in true sequence. If loading the file `startup-config` fails confd sets -the condition `usr/fail-startup`, thus allowing the next run statement -to load `failure-config`. +The two run blocks reside in the same .conf file so Finit runs them in +true sequence. If loading the file `startup-config` fails confd sets +the condition `usr/fail-startup`, thus allowing the next one to load +`failure-config`. Notice the critical difference between the `conditions` list and `if`. The former is a condition for starting; the latter is a condition to @@ -310,9 +312,13 @@ check whether a run/task/service is qualified to even be considered. `if` has a negation of its own, `!`, which is unrelated to anything in the `conditions` list. -Conditional execution statements can also be negated, so provided the -file loaded did the opposite, i.e., set a condition on success, the -previous stanza can also be written as: +What `if` compares against depends on the angle brackets: `if = "udevd"` +asks whether a service by that name is known, decided when the .conf is +read, while `if = ""` tests a condition at runtime. + +Conditional execution can also be negated, so provided the file loaded +did the opposite, i.e., set a condition on success, the previous block +can be written as: run failure { runlevel = "S" diff --git a/doc/config/sysv.md b/doc/config/sysv.md index 2a59a7c8..4f844ed3 100644 --- a/doc/config/sysv.md +++ b/doc/config/sysv.md @@ -11,34 +11,36 @@ SysV Init Scripts **Syntax:** `sysv NAME { command = "/path/to/init-script" }` -> `` is described in the [Services](services.md) section. - -Similar to `task` is the `sysv` stanza, which can be used to call SysV -style scripts. The primary intention for this command is to be able to -reuse much of existing setup and init scripts in Linux distributions. +> [!NOTE] +> Conditions, runlevels, and the other settings a `sysv` block takes +> are described in [Service Options](service-opts.md). + +A `sysv` block is a supervised daemon, like `service`, but started and +stopped through a SysV style init script instead of a command line. The +intention is to reuse existing setup and init scripts from Linux +distributions. When entering an allowed runlevel, Finit calls `init-script start`, when entering a disallowed runlevel, Finit calls `init-script stop`, and if -the Finit .conf, where `sysv` stanza is declared, is modified, Finit +the Finit .conf, where the `sysv` block is declared, is modified, Finit calls `init-script restart` on `initctl reload`. Similar to how -`service` stanzas work. +`service` blocks work. Forking services started with `sysv` scripts can be monitored by Finit by declaring the PID file to look for: `pidfile = "/path/to/file.pid"`. Finit does not create that file, it watches it for the resulting -forked-off PID, which is the default; `pidfile-create = true` is what -asks Finit to write it instead. This -syntax also works for forking daemons that do not have a command line -option to run it in the foreground, more on this below in `service`. +forked-off PID. That is the default; `pidfile-create = true` asks Finit +to write it instead. The same applies to forking daemons with no way to +run in the foreground, see [Services](services.md). > [!TIP] > See also [SysV Init Compatibility](#sysv-init-compatibility). -`runparts DIRECTORY` --------------------- +Run-parts +--------- For a directory with traditional start/stop scripts that should run, in -order, at bootstrap, Finit provides the `runparts` directive. It runs +order, at bootstrap, Finit provides the `runparts` setting. It runs in runlevel S, at the very end of it (before calling `/etc/rc.local`) making it perfect for most scenarios. @@ -89,6 +91,7 @@ it exists, and is executable. It is called very late in the boot process when the system has left runlevel S, stopped all old and started all new services in the target runlevel (default 2). +> [!NOTE] > In Finit releases before v4.5 this script blocked Finit execution and > made it as good as impossible to call `initctl` during that time. diff --git a/doc/config/task-and-run.md b/doc/config/task-and-run.md index 17f9c7f7..a86d854e 100644 --- a/doc/config/task-and-run.md +++ b/doc/config/task-and-run.md @@ -3,7 +3,8 @@ run (sequence) **Syntax:** `run NAME { command = "/path/to/cmd ARGS" }` -> `` is described in the [Services](services.md) section. +> Conditions, runlevels, and the other settings a `run` block takes are +> described in [Service Options](service-opts.md). One-shot command to run in sequence when entering a runlevel, with optional arguments and description. `run` commands are guaranteed to be @@ -33,7 +34,8 @@ task (parallel) **Syntax:** `task NAME { command = "/path/to/cmd ARGS" }` -> `` is described in the [Services](services.md) section. +> A `task` block takes the same settings as `run`, see +> [Service Options](service-opts.md). One-shot like 'run', but starts in parallel with the next command. @@ -93,7 +95,7 @@ The firewall rules are created once. The `exec-stop-post` script runs when entering runlevel 0 (halt) or 6 (reboot), or on explicit stop. > [!NOTE] -> The `remain-after-exit` option is not supported for bootstrap-only +> The `remain-after-exit` setting is not supported for bootstrap-only > tasks (tasks with only runlevel S). Bootstrap tasks are deleted > immediately after completion, and their `exec-stop-post` scripts never > run. A warning is logged if `remain-after-exit` is used on such tasks. diff --git a/doc/config/templating.md b/doc/config/templating.md index 4faed67f..50a25de5 100644 --- a/doc/config/templating.md +++ b/doc/config/templating.md @@ -17,8 +17,8 @@ To enable ZeroConf for, e.g., `eth0`, use The enabled symlink will be set up to `avahi-autoipd@.conf` and every instance of `%i` will be replaced with `eth0` before the file is parsed, so it works in the block title, in any value, and in the -command line alike. Inspect the resulting instantiated template with `initctl show -avahi-autoipd:eth0` and check the status of a running instance with: +command line alike. Inspect the result with `initctl show +avahi-autoipd:eth0`, and check a running instance with: ``` $ initctl status avahi-autoipd:eth0 diff --git a/doc/config/tty.md b/doc/config/tty.md index 616e504b..945c5f2a 100644 --- a/doc/config/tty.md +++ b/doc/config/tty.md @@ -28,8 +28,8 @@ The `device` variant takes two more: | `baud` | Baud rate, default 0, i.e., keep kernel default | | `term` | `$TERM` value, e.g. `"vt220"` | -> The `tty` stanza inherits runlevel, condition (and other feature) -> parsing from the `service` stanza. So TTYs can run in one or many +> A `tty` block inherits runlevel, condition (and other feature) +> parsing from the `service` block. So TTYs can run in one or many > runlevels and depend on any condition supported by Finit. This is > useful e.g. to depend on `` before starting a TTY. @@ -47,13 +47,13 @@ The second `tty` syntax variant is for using an external getty, like agetty or the BusyBox getty. The third variant is for board bringup and the `rescue` boot mode. No -device node is required in this variant, the same output that the kernel -uses is reused for stdio. If the `rescue` option is omitted, a shell is -started (`nologin`, `noclear`, and `nowait` are implied), if the rescue -option is set the bundled `/libexec/finit/sulogin` is started to present -a bare-bones root login prompt. If the root (uid:0, gid:0) user does -not have a password set, no rescue is possible. For more information, -see the [Rescue Mode](rescue.md) section. +device node is required, the same output the kernel uses is reused for +stdio. With `notty` a shell is started (`nologin`, `noclear`, and +`nowait` are implied); with `rescue` the bundled +`/libexec/finit/sulogin` presents a bare-bones root login prompt. If +the root (uid:0, gid:0) user does not have a password set, no rescue is +possible. For more information, see the [Rescue Mode](rescue.md) +section. By default, the first two syntax variants *clear* the TTY and *wait* for the user to press enter before starting getty. @@ -71,27 +71,24 @@ the user to press enter before starting getty. nowait = true } -The `noclear` option disables clearing the TTY after each session. +The `noclear` setting disables clearing the TTY after each session. Clearing the TTY when a user logs out is usually preferable. -The `nowait` option disables the `press Enter to activate console` +The `nowait` setting disables the `press Enter to activate console` message before actually starting the getty program. On small and embedded systems running multiple unused getty wastes both memory and CPU cycles, so `wait` is the preferred default. -The `nologin` option disables getty and `/bin/login`, and gives the -user a root (login) shell on the given TTY `` immediately. +The `nologin` setting disables getty and `/bin/login`, and gives the +user a root (login) shell on the given TTY immediately. Needless to say, this is a rather insecure option, but can be very useful for developer builds, during board bringup, or similar. -Notice the ordering, the `TERM` option to the built-in getty must be -the last argument. - Embedded systems may want to enable automatic `DEV` by supplying the -special `@console` device. This works regardless weather the system +special `@console` device. This works regardless whether the system uses `ttyS0`, `ttyAMA0`, `ttyMXC0`, or anything else. Finit figures -it out by querying sysfs: `/sys/class/tty/console/active`. The speed -can be omitted to keep the kernel default. +it out by querying sysfs: `/sys/class/tty/console/active`. Leave +`baud` out to keep the kernel default. > Most systems get by fine by just using `console`, which will evaluate > to `/dev/console`. If you have to use `@console` to get any output, @@ -119,7 +116,7 @@ This should of course not be enabled on production systems. Because it may give a user root access without having to log in. However, for board bringup and system debugging it can come in handy. -One can also use the `service` stanza to start a stand-alone shell: +One can also use a `service` block to start a stand-alone shell: service shell { runlevel = "12345" @@ -129,23 +126,23 @@ One can also use the `service` stanza to start a stand-alone shell: Controlling TTY for Services ---------------------------- -The `tty:` option gives a `run`, `task`, or `service` a controlling +The `tty` setting gives a `run`, `task`, or `service` a controlling terminal on the given device. The device is opened, set as the controlling terminal for the session (after `setsid()`), and connected to the process's stdin, stdout, and stderr. A default `TERM` environment variable is set based on the device type: `vt102` for serial lines and `linux` for virtual terminals. -`` may be a device node like `/dev/ttyS0`, or the special keyword -`@console` (see above). Note that `@console` expands only to the -first console, not all. +The value may be a device node like `/dev/ttyS0`, or the special +keyword `@console` (see above). Note that `@console` expands only to +the first console, not all. -When `tty:` is combined with `log:`, stdout and stderr are redirected -to the log sink instead of the TTY, but stdin remains connected to the -TTY device. +When `tty` is combined with a `log` block, stdout and stderr are +redirected to the log sink instead of the TTY, but stdin remains +connected to the TTY device. -> The `tty:` option is for `run`, `task`, and `service` stanzas only. -> The `tty` directive itself (for getty/login) has its own syntax, see +> The `tty` setting is for `run`, `task`, and `service` blocks only. +> A `tty` block (for getty/login) is a different thing entirely, see > above. **Example:** diff --git a/doc/example.md b/doc/example.md index cd47fdb1..597d7208 100644 --- a/doc/example.md +++ b/doc/example.md @@ -132,7 +132,7 @@ tty tty3 { #tty console { runlevel = "12345" device = "@console" noclear = true nologin = true } ``` -The `service` stanza, as well as `task`, `run` and others are described in +The `service` block, as well as `task`, `run` and others are described in full in the [Services Syntax](config/services.md) section. Here's a quick overview of some of the most common components needed to start diff --git a/doc/features.md b/doc/features.md index 564db42a..48220525 100644 --- a/doc/features.md +++ b/doc/features.md @@ -40,21 +40,21 @@ see the [TTY and Consoles](config/tty.md) section. **Runlevels** Support for SysV init-style [runlevels][5] is available, in the same -minimal style as everything else in Finit. The `[2345]` syntax can be -applied to service, task, run, and TTY stanzas. +minimal style as everything else in Finit. The `runlevel` setting +applies to service, task, run, and tty blocks alike. Reserved runlevels are 0 and 6, halt and reboot, respectively just like SysV init. Runlevel 1 can be configured freely, but is recommended to be kept as the system single-user runlevel since Finit will not start -networking here. The configured `runlevel NUM` from `/etc/finit.conf` +networking here. The configured `runlevel` from `/etc/finit.conf` is what Finit changes to after bootstrap, unless 'single' (or 'S') is given on the kernel cmdline, in which case runlevel 1 is started. -All services in runlevel S) are started first, followed by the desired +All services in runlevel S are started first, followed by the desired run-time runlevel. Run tasks in runlevel S can be started in sequence -by using `run [S] cmd`. Changing runlevels at runtime is done like any -other init, e.g. init 4, but also using the more advanced -[`initctl`](initctl.md) tool. +by using a `run` block with `runlevel = "S"`. Changing runlevels at +runtime is done like any other init, e.g. init 4, but also +using the more advanced [`initctl`](initctl.md) tool. **Conditions** @@ -222,7 +222,7 @@ The name of each sub-group is taken from the username. A fourth group also exists, the `root` group. It is also _reserved_ and primarily intended for RT tasks. If you have RT tasks they need to be -declared as such in their service stanza like this: +declared as such in their service block like this: service foo { cgroup root {} diff --git a/doc/plugins.md b/doc/plugins.md index 5a889677..36b6ddde 100644 --- a/doc/plugins.md +++ b/doc/plugins.md @@ -70,7 +70,7 @@ For your convenience a set of *optional* plugins are available: name of the module to load. Any line starting with the standard UNIX comment character, `#`, or `;`, is skipped. - Modules are by default loaded in runlevel `S` using the `task` stanza. + Modules are by default loaded in runlevel `S` using a `task` block. Each module is named `modprobe.foo`, with an optional `:ID`. The runlevel can be changed per file using: diff --git a/doc/runparts.md b/doc/runparts.md index 94a9daaa..7d0605a5 100644 --- a/doc/runparts.md +++ b/doc/runparts.md @@ -3,18 +3,18 @@ Runparts & `/etc/rc.local` At the end of the boot, when all bootstrap (`S`) tasks and services have started, but not networking, Finit calls its built-in [run-parts(8)][] -command on any configured `runparts ` directory. This happens just +command on any configured `runparts = "DIR"` directory. This happens just before changing to the configured runlevel (default 2). (Networking is enabled just prior to changing from single user mode.) -```shell -runparts /etc/rc.d/ +```aconf +runparts = "/etc/rc.d/" ``` Right after the runlevel change when all services have started properly, `/etc/rc.local` is called. -No configuration stanza in `/etc/finit.conf` is required for `rc.local`. +No setting in `/etc/finit.conf` is required for `rc.local`. If it exists and is an executable shell script Finit calls it at the very end of the boot, before calling the `HOOK_SYSTEM_UP`. See more in the [Hook Scripts](plugins.md#hooks) section. diff --git a/src/rescue.conf b/src/rescue.conf index 69231099..c46f9569 100644 --- a/src/rescue.conf +++ b/src/rescue.conf @@ -1,2 +1,5 @@ -runlevel 1 -tty [12345] rescue +runlevel = 1 +tty rescue { + runlevel = "12345" + rescue = true +} From be28436c2d1c57f92d97ad6308997c7a3ca5952a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 29 Jul 2026 10:27:47 +0200 Subject: [PATCH 22/28] conf: drop the angle brackets from if, infer from the value The block format spells conditions as bare strings everywhere else, so requiring `if = ""` left one sigil behind, carried over from the line-based `if:` token. A namespace separator already tells the two apart: a value with a '/' is a condition, anything else is a service name. svc_ifthen() picks its mode from the start of the statement and applies it to the whole, so a statement naming both kinds cannot be evaluated. That is now an error, as are the old angle brackets, and either one skips the block: /etc/finit.conf: mixed: if: cannot mix a service name with a condition in 'anchor,usr/enable-me', a statement must be all of one kind, skipping Signed-off-by: Joachim Wiberg --- doc/config/services.md | 24 ++++++++--- src/conf.c | 68 +++++++++++++++++++++++++++++- test/Makefile.am | 2 + test/conf-if.sh | 95 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 7 deletions(-) create mode 100755 test/conf-if.sh diff --git a/doc/config/services.md b/doc/config/services.md index 857a335e..281106ee 100644 --- a/doc/config/services.md +++ b/doc/config/services.md @@ -296,7 +296,7 @@ Consider this (simplified) example from the Infix operating system: run failure { runlevel = "S" - if = "" + if = "usr/fail-startup" conditions = { "pid/sysrepo" } command = "confd --load failure-config" } @@ -312,9 +312,23 @@ check whether a run/task/service is qualified to even be considered. `if` has a negation of its own, `!`, which is unrelated to anything in the `conditions` list. -What `if` compares against depends on the angle brackets: `if = "udevd"` -asks whether a service by that name is known, decided when the .conf is -read, while `if = ""` tests a condition at runtime. +What `if` compares against depends on the value. A namespace +separator makes it a condition, anything else is a service name: + +| `if` | Asks | +|---|---| +| `"udevd"` | is a service by this name known? | +| `"usr/foo"` | was this condition set? | + +Both are questions about whether the block belongs in the running +configuration at all, usually answered from what bootstrap established. +A statement is all of one kind or the other, so the block is rejected +if you mix them. + +> [!NOTE] +> `if` qualifies, it does not track. A condition asserted or cleared +> later does not start or stop the service by itself -- that is what +> the `conditions` list is for. Conditional execution can also be negated, so provided the file loaded did the opposite, i.e., set a condition on success, the previous block @@ -322,7 +336,7 @@ can be written as: run failure { runlevel = "S" - if = "" + if = "!usr/startup-ok" conditions = { "pid/sysrepo" } command = "confd ..." } diff --git a/src/conf.c b/src/conf.c index aef89a85..752d84bc 100644 --- a/src/conf.c +++ b/src/conf.c @@ -1230,6 +1230,65 @@ static void addlog(char *line, size_t len, cfg_t *sec, char *file) * Translate service/task/run/sysv section to the canonical legacy * one-liner and register through the same path as legacy files. */ +/* + * if = "udevd" asks whether a service by that name is known, answered + * when the .conf is read, while if = "usr/foo" tests a condition, + * answered at runtime. The namespace separator tells the two apart, + * so the legacy angle brackets are not needed. A statement must be + * all of one kind; the two are not checked at the same time, so they + * cannot be combined. + */ +static int if_translate(const char *str, char *buf, size_t len, char *file, const char *ident) +{ + char tmp[MAX_IDENT_LEN]; + char *op, *save; + int cond = -1; + + if (strpbrk(str, "<>")) { + logit(LOG_ERR, "%s: %s: if: angle brackets are not used in this" + " format, got '%s', skipping", file, ident, str); + return -1; + } + + if (strlcpy(tmp, str, sizeof(tmp)) >= sizeof(tmp)) { + logit(LOG_ERR, "%s: %s: if: '%s' is too long, skipping", + file, ident, str); + return -1; + } + + for (op = strtok_r(tmp, ",", &save); op; op = strtok_r(NULL, ",", &save)) { + int is_cond; + + if (op[0] == '!') + op++; + if (!op[0]) + continue; + + is_cond = strchr(op, '/') != NULL; + if (cond == -1) + cond = is_cond; + else if (cond != is_cond) { + logit(LOG_ERR, "%s: %s: if: cannot mix a service name with a" + " condition in '%s', a statement must be all of one" + " kind, skipping", file, ident, str); + return -1; + } + } + + if (cond == -1) { + logit(LOG_ERR, "%s: %s: if: no condition or service name in '%s'," + " skipping", file, ident, str); + return -1; + } + + if (cond) + snprintf(buf, len, "<%s>", str); + else + strlcpy(buf, str, len); + + return 0; +} + static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *file) { struct rlimit local_rlimit[RLIMIT_NLIMITS]; @@ -1401,8 +1460,13 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi if (sec_getlist(sec, "conflicts", 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, "if", NULL))) { + char ifbuf[MAX_IDENT_LEN + 2]; + + if (if_translate(str, ifbuf, sizeof(ifbuf), file, cfg_title(sec))) + return; + addopt(line, sizeof(line), "if:", ifbuf); + } if ((str = sec_getstr(sec, "tty", NULL))) addtok(line, sizeof(line), "tty:%s", str); diff --git a/test/Makefile.am b/test/Makefile.am index 0860d904..392f845e 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -32,6 +32,7 @@ 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 += conf-if.sh EXTRA_DIST += conf-template.sh EXTRA_DIST += script-timeout.sh EXTRA_DIST += crashing.sh @@ -79,6 +80,7 @@ TESTS += add-remove-dynamic-service-sub-config.sh TESTS += bootstrap-crash.sh TESTS += cond-start-task.sh TESTS += conf-format.sh +TESTS += conf-if.sh TESTS += conf-template.sh TESTS += script-timeout.sh TESTS += crashing.sh diff --git a/test/conf-if.sh b/test/conf-if.sh new file mode 100755 index 00000000..3f9b08be --- /dev/null +++ b/test/conf-if.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# Verify the block format 'if' setting: a value with a namespace +# separator is a condition, checked at runtime, anything else is a +# service name, checked when the .conf is read. The two cannot be +# combined, and the legacy angle brackets are rejected. +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck disable=SC2034 +BOOTSTRAP="service anchor { + runlevel = \"S12345\" + command = \"serv -np -i anchor\" +} +service byname { + runlevel = \"S12345\" + if = \"anchor\" + command = \"serv -np -i byname\" +} +service noname { + runlevel = \"S12345\" + if = \"nosuchservice\" + command = \"serv -np -i noname\" +} +service negated { + runlevel = \"S12345\" + if = \"!anchor\" + command = \"serv -np -i negated\" +} +service bycond { + runlevel = \"S12345\" + if = \"usr/enable-me\" + command = \"serv -np -i bycond\" +} +service brackets { + runlevel = \"S12345\" + if = \"\" + command = \"serv -np -i brackets\" +} +service mixed { + runlevel = \"S12345\" + if = \"anchor,usr/enable-me\" + command = \"serv -np -i mixed\" +}" + +# initctl status prints a detail block for a single match and a table +# only for several, so count lines in the full listing instead. +assert_loaded() +{ + assert "Service $1 loaded: $2" \ + "$(texec initctl -t status | awk -v n="$1" '$2 == n' | wc -l)" -eq "$2" +} + +test_teardown() +{ + say "Running test teardown." + run "initctl cond clear enable-me" || true +} + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +say 'A bare value names a service, resolved when the .conf is read' +retry 'assert_loaded byname 1' + +say 'A service that is not known keeps the block out of the config' +assert_loaded noname 0 + +say 'Negation of a known service keeps the block out too' +assert_loaded negated 0 + +say 'A value with a namespace separator is a condition, so the block is' +say 'loaded, but held until the condition is asserted' +assert_loaded bycond 1 +assert_status bycond halted + +run "initctl cond set enable-me" +retry 'assert_status bycond running' + +# 'if' qualifies the block, it does not track the condition, so +# clearing it does not stop a running service. The qualification is +# re-read when the configuration is, i.e. on reload. +run "initctl cond clear enable-me" +run "initctl reload" +retry 'assert_status bycond halted' + +say 'Angle brackets belong to the line-based format, the block is rejected' +assert_loaded brackets 0 + +say 'A service name and a condition are checked at different times, so' +say 'mixing them in one statement is rejected' +assert_loaded mixed 0 + +say 'Rejecting a block leaves the rest of the file alone' +assert_loaded anchor 1 From 6615a248ee5657c75fe468528c2fbd268c3e60be Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 29 Jul 2026 17:58:38 +0200 Subject: [PATCH 23/28] service: return the svc from service_register() Settings that exist only in the block format have nowhere to go: the legacy line cannot carry them, and service_register() returned an errno that no caller ever read, so conf.c had no handle on the service it just created. Return the svc instead, NULL with errno set on failure, errno zero when a block is skipped on purpose. Signed-off-by: Joachim Wiberg --- src/service.c | 46 +++++++++++++++++++++++++++++----------------- src/service.h | 2 +- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/service.c b/src/service.c index 4afd9f1e..9e22ee9d 100644 --- a/src/service.c +++ b/src/service.c @@ -1875,9 +1875,11 @@ static void parse_cmdline_args(svc_t *svc, char *cmd, char **args) * defaults to "" (empty string). * * Returns: - * POSIX OK(0) on success, or non-zero errno exit status on failure. + * The registered svc, or %NULL with @errno set on failure. A block + * skipped on purpose -- conditional loading, bootstrap over, nowarn -- + * also returns %NULL, with @errno zero. */ -int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) +svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file) { char *cmd, *desc, *runlevels = NULL, *cond = NULL; char *username = NULL, *log = NULL, *pid = NULL; @@ -1905,12 +1907,15 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) if (!cfg) { errx(1, "Invalid input argument"); - return errno = EINVAL; + errno = EINVAL; + return NULL; } line = strdupa(cfg); - if (!line) - return 1; + if (!line) { + errno = ENOMEM; + return NULL; + } desc = strstr(line, "-- "); if (desc) { @@ -1935,7 +1940,8 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) if (!cmd) { incomplete: errx(1, "Incomplete service '%s', cannot register", cfg); - return errno = ENOENT; + errno = ENOENT; + return NULL; } while (cmd) { @@ -2035,14 +2041,17 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) strlcat(ident, id, sizeof(ident)); } - if (ifstmt && !svc_ifthen(1, ident, ifstmt, nowarn)) - return 0; + if (ifstmt && !svc_ifthen(1, ident, ifstmt, nowarn)) { + errno = 0; + return NULL; + } levels = conf_parse_runlevels(runlevels); if (runlevel != INIT_LEVEL && !ISOTHER(levels, INIT_LEVEL)) { dbg("Skipping %s%s%s, bootstrap is completed.", name, id[0] ? ":" : "", id[0] ? id : ""); - return 0; + errno = 0; + return NULL; } if (type == SVC_TYPE_TTY) { @@ -2050,7 +2059,7 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) char *ptr; if (tty_parse_args(&tty, cmd, &args)) - return errno; + return NULL; /* NOTE: this may result in dev == NULL! */ if (tty_isatcon(tty.dev)) @@ -2069,7 +2078,7 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) line = alloca(len); if (!line) - return errno; + return NULL; snprintf(line, len, "%s", tty.cmd ? tty.cmd : "tty"); for (i = 0; i < tty.num; i++) { @@ -2079,7 +2088,7 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) cmd = strtok_r(line, " \t", &args); if (!cmd) - return errno; + return NULL; /* tty's always respawn, never incr. restart_cnt */ respawn = 1; @@ -2104,11 +2113,13 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) svc = svc_find(name, id); if (!whichp(cmd)) { - if (nowarn) - return 0; + if (nowarn) { + errno = 0; + return NULL; + } warn("%s: skipping %s", file ? file : "static", cmd); - return errno; + return NULL; } if (!svc) { @@ -2116,7 +2127,8 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) svc = svc_new(cmd, name, id, type); if (!svc) { errx(1, "Out of memory, cannot register service %s", cmd); - return errno = ENOMEM; + errno = ENOMEM; + return NULL; } if (manual) @@ -2375,7 +2387,7 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file) } } - return 0; + return svc; } /* diff --git a/src/service.h b/src/service.h index 5efc2354..fd20b598 100644 --- a/src/service.h +++ b/src/service.h @@ -27,7 +27,7 @@ #include "svc.h" -int service_register (int type, char *line, struct rlimit rlimit[], char *file); +svc_t *service_register (int type, char *line, struct rlimit rlimit[], char *file); void service_unregister (svc_t *svc); void service_runtask_clean (void); From 7f8a64dd39fc1fe35889d803274290f33dabe8ab Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 29 Jul 2026 18:49:28 +0200 Subject: [PATCH 24/28] util: promote rmrf() from tmpfiles.c, fix silent mksubsys() skip rmrf() is needed outside tmpfiles.c. The move also deduplicates the nftw callback: the contents-only removal used by tmpfiles 'D' entries is now rmcontents(), sharing the callback with rmrf(). mksubsys() did nothing at all when the user could not be resolved, no directory and no message, and callers had no way to tell. Now the directory is always created, ownership is best effort, and an unknown user is warned about. Signed-off-by: Joachim Wiberg --- src/tmpfiles.c | 31 +------------------------------ src/util.c | 47 +++++++++++++++++++++++++++++++++++++++++++---- src/util.h | 2 ++ 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/tmpfiles.c b/src/tmpfiles.c index 4cf7ec07..4825ee6a 100644 --- a/src/tmpfiles.c +++ b/src/tmpfiles.c @@ -130,33 +130,6 @@ static int parse_mm(char *arg, int *major, int *minor) return 0; } -static int do_delete(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw) -{ - (void)sb; - (void)tflag; - - if (ftw->level == 0) - return 1; - - if (remove(fpath) && errno != EBUSY) - warn("Failed removing %s", fpath); - - return 0; - -} - -static int rmrf(const char *path) -{ - if (!fisdir(path)) - return 0; - - nftw(path, do_delete, 20, FTW_DEPTH | FTW_PHYS); - if (remove(path) && errno != ENOENT) - warn("Failed removing path %s", path); - - return 0; -} - static void mkparent(char *path, mode_t mode) { mkpath(dirname(strdupa(path)), mode); @@ -465,9 +438,7 @@ static void tmpfiles(char *line) case 'd': break; case 'D': - if (fisdir(path)) { - nftw(path, do_delete, 20, FTW_DEPTH | FTW_PHYS); - } + rmcontents(path); break; case 'e': case 'f': diff --git a/src/util.c b/src/util.c index 4fe6a2db..7cc8b095 100644 --- a/src/util.c +++ b/src/util.c @@ -45,6 +45,7 @@ # include #endif #include +#include /* rmrf() */ #include /* sysinfo() */ #include /* statfs */ #include @@ -310,6 +311,42 @@ int getcgroup(char *buf, size_t len) return 0; } +static int rmrf_cb(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw) +{ + (void)sb; + (void)tflag; + + if (ftw->level == 0) + return 0; + + if (remove(fpath) && errno != EBUSY) + warn("Failed removing %s", fpath); + + return 0; +} + +/* empty a directory but keep it, silently ignores a missing path */ +int rmcontents(const char *path) +{ + if (!fisdir(path)) + return 0; + + return nftw(path, rmrf_cb, 20, FTW_DEPTH | FTW_PHYS); +} + +/* rm -rf, silently ignores a missing path */ +int rmrf(const char *path) +{ + if (!fisdir(path)) + return 0; + + nftw(path, rmrf_cb, 20, FTW_DEPTH | FTW_PHYS); + if (remove(path) && errno != ENOENT) + warn("Failed removing path %s", path); + + return 0; +} + int mksubsys(const char *dir, mode_t mode, char *user, char *group) { mode_t omask; @@ -318,18 +355,20 @@ int mksubsys(const char *dir, mode_t mode, char *user, char *group) omask = umask(0); + rc = makedir(dir, mode); + if (rc && errno == EEXIST) + rc = chmod(dir, mode); + uid = getuser(user, NULL); if (uid >= 0) { gid = getgroup(group); if (gid < 0) gid = 0; - rc = makedir(dir, mode); - if (rc && errno == EEXIST) - rc = chmod(dir, mode); if (chown(dir, uid, gid)) err(1, "Failed chown(%s, %d, %d)", dir, uid, gid); - } + } else + warnx("Cannot find user %s, %s is owned by root", user, dir); umask(omask); diff --git a/src/util.h b/src/util.h index b0ae8614..53cfd84e 100644 --- a/src/util.h +++ b/src/util.h @@ -72,6 +72,8 @@ int getcuser (char *buf, size_t len); int getcgroup (char *buf, size_t len); int mksubsys (const char *dir, mode_t mode, char *user, char *group); +int rmcontents (const char *path); +int rmrf (const char *path); char *fslurp (size_t *lenp, const char *fmt, ...) __attribute__ ((format (printf, 2, 3))); char *vfslurp (size_t *lenp, const char *fmt, va_list ap); From f0d7257374d12be6241fec594ea4036148a392a2 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 29 Jul 2026 18:49:57 +0200 Subject: [PATCH 25/28] Fix #492: add per-service directories, systemd RuntimeDirectory style A service that drops privileges cannot create its own PID file in /run, root owns it. Finit can create the file with pidfile-create, but the daemon still cannot touch it to confirm a SIGHUP. Five new settings, block format only: runtime-dir, state-dir, cache-dir, logs-dir, and config-dir. The value is a directory name, resolved under /run, /var/lib, /var/cache, /var/log, and /etc, respectively. The directory is created before the service starts, mode 0755 owned by user/group, and the full path is exported to the process as RUNTIME_DIRECTORY, STATE_DIRECTORY, CACHE_DIRECTORY, LOGS_DIRECTORY, and CONFIGURATION_DIRECTORY. Mode and ownership are asserted at creation only, a daemon may tighten them afterwards. The runtime directory is removed when the unit stops, after any exec-stop-post script, like systemd with RuntimeDirectoryPreserve=no. A completed run/task counts as stopped unless remain-after-exit keeps it up. The other four persist across restarts. These are the first settings with no legacy token: they are validated by service_set_dir() and stored on the svc that service_register() now returns. systemd accepts a list of directories per setting; this is a single name for now, widening later is compatible since libconfuse accepts a bare value for a list option. The test sysroot gains libnss_files.so.2, which ldd cannot see, glibc dlopen()s it. Without it getpwnam() fails inside the chroot, so user/group settings never resolved and directory ownership could not be tested. Signed-off-by: Joachim Wiberg --- doc/config/service-opts.md | 39 +++++++++++++ src/conf.c | 36 +++++++++++- src/service.c | 113 +++++++++++++++++++++++++++++++++++++ src/service.h | 16 ++++++ src/svc.h | 8 +++ test/Makefile.am | 2 + test/conf-dirs.sh | 80 ++++++++++++++++++++++++++ test/lib/sysroot.mk | 7 ++- 8 files changed, 299 insertions(+), 2 deletions(-) create mode 100755 test/conf-dirs.sh diff --git a/doc/config/service-opts.md b/doc/config/service-opts.md index 7c5ef7da..19c7d5e8 100644 --- a/doc/config/service-opts.md +++ b/doc/config/service-opts.md @@ -96,6 +96,45 @@ following settings are available: * `oncrash = "script"` -- similarly, but instead of rebooting, call the `exec-stop-post` script with exit code `crashed`, see below +Service directories +------------------- + +Five settings ask Finit to create a directory for the service before it +starts, owned by its `user` and `group`, mode 0755. The value is a +directory name, resolved under a fixed base -- absolute paths and `..` +are refused: + +| Setting | Base | Environment variable | +|---|---|---| +| `runtime-dir` | `/run` | `RUNTIME_DIRECTORY` | +| `state-dir` | `/var/lib` | `STATE_DIRECTORY` | +| `cache-dir` | `/var/cache` | `CACHE_DIRECTORY` | +| `logs-dir` | `/var/log` | `LOGS_DIRECTORY` | +| `config-dir` | `/etc` | `CONFIGURATION_DIRECTORY` | + +Each resolved path is exported to the process environment under the +listed name, the same names systemd uses for `RuntimeDirectory=` and +friends. + +The runtime directory is removed again when the service stops, after +any `exec-stop-post` script has run; `/run` is a tmpfs so it would not +survive a reboot anyway. The other four persist. A completed `run` or +`task` counts as stopped, unless `remain-after-exit` keeps it alive +until stopped for real. + +This is what lets a service drop privileges and still create, and +later touch, its own PID file: + + service ntpd { + user = "ntp" + group = "ntp" + runtime-dir = "ntpd" + pidfile = "/run/ntpd/ntpd.pid" + command = "/usr/sbin/ntpd -n -p /run/ntpd/ntpd.pid" + } + +These settings exist only in the block format. + Stopping and reloading ---------------------- diff --git a/src/conf.c b/src/conf.c index 752d84bc..9ab273c7 100644 --- a/src/conf.c +++ b/src/conf.c @@ -269,6 +269,11 @@ static cfg_opt_t svc_opts[] = { CFG_STR_LIST("capabilities", NULL, CFGF_NODEFAULT), CFG_STR_LIST("caps", NULL, CFGF_NODEFAULT), /* alias */ + CFG_STR ("runtime-dir", NULL, CFGF_NODEFAULT), + CFG_STR ("state-dir", NULL, CFGF_NODEFAULT), + CFG_STR ("cache-dir", NULL, CFGF_NODEFAULT), + CFG_STR ("logs-dir", NULL, CFGF_NODEFAULT), + CFG_STR ("config-dir", NULL, CFGF_NODEFAULT), CFG_STR_LIST("conflicts", NULL, CFGF_NODEFAULT), CFG_STR ("if", NULL, CFGF_NODEFAULT), CFG_STR ("tty", NULL, CFGF_NODEFAULT), @@ -1289,6 +1294,30 @@ static int if_translate(const char *str, char *buf, size_t len, char *file, cons return 0; } +/* + * These have no legacy token, they are validated by service_set_dir() + * and stored directly on the registered svc. Empty means unset, + * service_register() has already cleared the fields. + */ +static void dirs_translate(cfg_t *sec, svc_t *svc, char *file) +{ + int i; + + for (i = 0; i < NUM_SVCDIRS; i++) { + const char *str; + + str = sec_getstr(sec, svcdirs[i].key, NULL); + if (!str || !str[0]) + continue; + + if (service_set_dir(svc, &svcdirs[i], str)) + logit(LOG_ERR, "%s: %s: %s '%s' %s, ignoring", + file, cfg_title(sec), svcdirs[i].key, str, + errno == ENAMETOOLONG ? "is too long" + : "must be relative, no '..'"); + } +} + static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *file) { struct rlimit local_rlimit[RLIMIT_NLIMITS]; @@ -1299,6 +1328,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi long num; char buf[512]; char nm[80]; + svc_t *svc; char *id; cmd = sec_getstr(sec, "command", NULL); @@ -1505,7 +1535,11 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi addtok(line, sizeof(line), "-- %s", str); dbg("translated: %s", line); - service_register(type, line, rlimit, file); + svc = service_register(type, line, rlimit, file); + if (!svc) + return; + + dirs_translate(sec, svc, file); } /* diff --git a/src/service.c b/src/service.c index 9e22ee9d..84be55b1 100644 --- a/src/service.c +++ b/src/service.c @@ -570,6 +570,101 @@ static void set_uid(uid_t uid, svc_t *svc) err(1, "%s: failed setuid(%d)", svc_ident(svc, NULL, 0), uid); } +/* + * systemd-style per-service directories: created before each start, + * owned by the service user, and exported to the environment. The + * runtime directory is removed again when the service stops, after + * any post: script has run. See issue #492. + */ +const struct svcdir svcdirs[NUM_SVCDIRS] = { + { "runtime-dir", "/run", "RUNTIME_DIRECTORY", offsetof(svc_t, runtime_dir) }, + { "state-dir", "/var/lib", "STATE_DIRECTORY", offsetof(svc_t, state_dir) }, + { "cache-dir", "/var/cache", "CACHE_DIRECTORY", offsetof(svc_t, cache_dir) }, + { "logs-dir", "/var/log", "LOGS_DIRECTORY", offsetof(svc_t, logs_dir) }, + { "config-dir", "/etc", "CONFIGURATION_DIRECTORY", offsetof(svc_t, config_dir) }, +}; + +static char *svcdir_path(svc_t *svc, const struct svcdir *sd, char *path, size_t len) +{ + char *name = (char *)svc + sd->off; + + if (!name[0]) + return NULL; + + paste(path, len, sd->base, name); + return path; +} + +/* + * Validate and set a per-service directory. The value is a name + * resolved under sd->base, so an absolute path or an escape is + * refused with -1 and errno set. + */ +int service_set_dir(svc_t *svc, const struct svcdir *sd, const char *name) +{ + char *dir = (char *)svc + sd->off; + + if (name[0] == '/' || strstr(name, "..")) { + errno = EINVAL; + return -1; + } + + if (strlcpy(dir, name, MAX_ARG_LEN) >= MAX_ARG_LEN) { + dir[0] = 0; + errno = ENAMETOOLONG; + return -1; + } + + return 0; +} + +static void service_mkdirs(svc_t *svc) +{ + char path[256]; + size_t i; + + for (i = 0; i < NELEMS(svcdirs); i++) { + char *ptr; + + ptr = svcdir_path(svc, &svcdirs[i], path, sizeof(path)); + if (!ptr) + continue; + + /* + * Script forks land here too, so an existing directory + * is left alone -- mode and ownership are asserted at + * creation only, a daemon may have tightened them. + */ + if (fisdir(ptr)) + continue; + + /* only the named directory is chowned, like systemd */ + mkpath(ptr, 0755); + if (mksubsys(ptr, 0755, svc->username, svc->group)) + logit(LOG_WARNING, "%s: failed creating %s", svc_ident(svc, NULL, 0), ptr); + } +} + +static void service_dir_env(svc_t *svc) +{ + char path[256]; + size_t i; + + for (i = 0; i < NELEMS(svcdirs); i++) { + if (svcdir_path(svc, &svcdirs[i], path, sizeof(path))) + setenv(svcdirs[i].env, path, 1); + } +} + +static void service_rmdirs(svc_t *svc) +{ + char path[256]; + + /* only the runtime directory, /run is tmpfs, the rest persist */ + if (svcdir_path(svc, &svcdirs[0], path, sizeof(path))) + rmrf(path); +} + static pid_t service_fork(svc_t *svc) { const char *cgnm; @@ -591,6 +686,8 @@ static pid_t service_fork(svc_t *svc) if (pid == 0) { char *home = NULL; + + service_mkdirs(svc); #ifdef ENABLE_STATIC int uid = 0; /* XXX: Fix better warning that dropprivs is disabled. */ int gid = 0; @@ -664,6 +761,8 @@ static pid_t service_fork(svc_t *svc) err(1, "%s: failed setgid(%d)", svc_ident(svc, NULL, 0), gid); } + service_dir_env(svc); + if (uid >= 0) { set_uid(uid, svc); @@ -2272,6 +2371,10 @@ svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file) else memset(svc->capabilities, 0, sizeof(svc->capabilities)); + /* block format only, set by conf.c after registration */ + for (int i = 0; i < NUM_SVCDIRS; i++) + memset((char *)svc + svcdirs[i].off, 0, MAX_ARG_LEN); + if (!svc_is_tty(svc) && ctty) { char *dev = ctty; @@ -2894,6 +2997,16 @@ static void svc_set_state(svc_t *svc, svc_state_t new_state) return; *state = new_state; + /* + * The unit has stopped: HALTED comes after any post:/cleanup: + * script, DONE is a completed run/task, where remain-after-exit + * keeps it alive until stopped for real. Same removal rules as + * systemd RuntimeDirectory with RuntimeDirectoryPreserve=no. + */ + if (new_state == SVC_HALTED_STATE || + (new_state == SVC_DONE_STATE && !svc_is_remain(svc))) + service_rmdirs(svc); + if (svc_is_runtask(svc)) { char success[MAX_COND_LEN], failure[MAX_COND_LEN]; diff --git a/src/service.h b/src/service.h index fd20b598..deaea552 100644 --- a/src/service.h +++ b/src/service.h @@ -27,6 +27,22 @@ #include "svc.h" +/* + * systemd-style per-service directories, block format only: the .conf + * key, the base the name resolves under, and the environment variable + * the path is exported as. Shared by service.c and conf.c so adding + * one means touching the table and svc.h alone. + */ +struct svcdir { + const char *key; + const char *base; + const char *env; + size_t off; /* offsetof() in svc_t */ +}; +#define NUM_SVCDIRS 5 +extern const struct svcdir svcdirs[NUM_SVCDIRS]; + +int service_set_dir (svc_t *svc, const struct svcdir *sd, const char *name); svc_t *service_register (int type, char *line, struct rlimit rlimit[], char *file); void service_unregister (svc_t *svc); diff --git a/src/svc.h b/src/svc.h index 59f4ed43..6aced5f0 100644 --- a/src/svc.h +++ b/src/svc.h @@ -199,6 +199,14 @@ typedef struct svc { int num_supgroups; char capabilities[MAX_CMD_LEN]; + /* Directories set up for the service, block format only, the + * name is resolved under a fixed base, e.g. /run/NAME */ + char runtime_dir[MAX_ARG_LEN]; + char state_dir[MAX_ARG_LEN]; + char cache_dir[MAX_ARG_LEN]; + char logs_dir[MAX_ARG_LEN]; + char config_dir[MAX_ARG_LEN]; + /* Command, arguments and service description */ char cmd[MAX_CMD_LEN]; char args[MAX_NUM_SVC_ARGS][MAX_CMD_LEN]; diff --git a/test/Makefile.am b/test/Makefile.am index 392f845e..6670cbc2 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -32,6 +32,7 @@ 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 += conf-dirs.sh EXTRA_DIST += conf-if.sh EXTRA_DIST += conf-template.sh EXTRA_DIST += script-timeout.sh @@ -80,6 +81,7 @@ TESTS += add-remove-dynamic-service-sub-config.sh TESTS += bootstrap-crash.sh TESTS += cond-start-task.sh TESTS += conf-format.sh +TESTS += conf-dirs.sh TESTS += conf-if.sh TESTS += conf-template.sh TESTS += script-timeout.sh diff --git a/test/conf-dirs.sh b/test/conf-dirs.sh new file mode 100755 index 00000000..79d0ac11 --- /dev/null +++ b/test/conf-dirs.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# Verify the per-service directory settings: runtime-dir, state-dir, +# cache-dir, logs-dir, and config-dir. The directory is created before +# the service starts, owned by the service user, and exported to the +# environment. The runtime directory is removed again when the service +# stops, the others persist. +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck disable=SC2034 +BOOTSTRAP="service owned { + runlevel = \"S12345\" + user = \"daemon\" + group = \"daemon\" + runtime-dir = \"owned\" + state-dir = \"owned\" + pidfile = \"/run/owned/serv.pid\" + command = \"/sbin/serv -np -P /run/owned/serv.pid -i owned -e STATE_DIRECTORY:/var/lib/owned\" +} +task probe { + runlevel = \"S12345\" + runtime-dir = \"probe\" + cache-dir = \"probe\" + command = \"/sbin/serv -h -e RUNTIME_DIRECTORY:/run/probe -e CACHE_DIRECTORY:/var/cache/probe\" +} +service escape { + runlevel = \"S12345\" + runtime-dir = \"../escape\" + command = \"serv -np -i escape\" +}" + +# ls output is empty for an empty directory, so test -d instead +assert_dir() +{ + assert "Directory $1 exists" "$(texec test -d "$1" && echo yes)" = "yes" +} + +assert_nodir() +{ + assert "Directory $1 removed" "$(texec test -d "$1" || echo gone)" = "gone" +} + +assert_owner() +{ + assert "$1 owned by $2" "$(texec stat -c %U:%G "$1")" = "$2" +} + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +say 'Directory created before start, owned by the service user' +retry 'assert_status owned running' +assert_dir /run/owned +assert_owner /run/owned daemon:daemon +assert_dir /var/lib/owned +assert_owner /var/lib/owned daemon:daemon + +say 'The paths are exported to the process environment; both commands' +say 'verify their own with serv -e, and refuse to run on a mismatch' + +say 'A completed task no longer holds its runtime directory' +retry 'assert_status probe done' +assert_nodir /run/probe +assert_dir /var/cache/probe + +say 'Stopping the service removes the runtime directory, state persists' +run "initctl stop owned" +retry 'assert_status owned stopped' +assert_nodir /run/owned +assert_dir /var/lib/owned + +say 'Starting again recreates it' +run "initctl start owned" +retry 'assert_status owned running' +assert_dir /run/owned + +say 'A path escaping the base directory is refused, service still runs' +retry 'assert_status escape running' +assert_nodir /escape diff --git a/test/lib/sysroot.mk b/test/lib/sysroot.mk index 00528da5..ea2d8c7c 100644 --- a/test/lib/sysroot.mk +++ b/test/lib/sysroot.mk @@ -34,7 +34,12 @@ BBBIN = busybox-$(ARCH) BBHOME ?= https://github.com/troglobit/busybox-builder/releases/download BBURL ?= $(BBHOME)/$(BBVER)/$(BBBIN) -_libs_src = $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') +# glibc dlopen()s NSS modules at runtime, so ldd does not list them, but +# without libnss_files getpwnam() cannot resolve users inside the chroot +_libs_nss = $(firstword $(wildcard /lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ + /usr/lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ + /lib64/libnss_files.so.2 /lib/libnss_files.so.2)) +_libs_src = $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') $(_libs_nss) libs = $(foreach path,$(_libs_src),$(abspath $(DEST))$(path)) all: $(libs) $(DEST)/bin/$(BBBIN) From 6b03a1ec8f07004f7291642d1fb549ddfd778161 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 29 Jul 2026 20:28:01 +0200 Subject: [PATCH 26/28] conf: adopt the systemd semantics for per-service directories Aaron Andersen points out in the #492 discussion that the *Directory settings carry more contract than create-and-chown: per-directory modes, specific ownership rules, and cleanup toggles. Without them config-dir was chowned to the service user, which systemd never does, an existing directory with drifted ownership was left wrong, and the runtime directory could not survive a restart. Now matching systemd.exec(5), and where the man page is vague, the code in setup_exec_directory(): - each directory takes a matching -mode key, octal with the leading zero, default 0755. The mode of the named directory is locked down again on every start, also when it already exists - config-dir is created but never chowned - the contents of an existing directory are left alone as long as the owner is right; on drift everything under it is chowned back - runtime-dir-preserve = no | restart | yes maps RuntimeDirectoryPreserve=. A service still qualified to run when the runtime directory would be removed is restarting, not stopping, which is what svc_enabled() answers The dir mechanics move to mksubsysd(), taking resolved ids, with mksubsys() reduced to a name-resolving wrapper for the dbus plugin. The child resolves uid/gid once for both directory setup and privilege drop. The symlink form, RuntimeDirectory=foo:bar, is not adopted. Signed-off-by: Joachim Wiberg --- doc/config/service-opts.md | 17 ++++++++-- src/conf.c | 47 +++++++++++++++++++++----- src/service.c | 53 +++++++++++++++++++---------- src/service.h | 2 +- src/svc.h | 6 ++++ src/util.c | 68 +++++++++++++++++++++++++++++--------- src/util.h | 2 ++ test/conf-dirs.sh | 40 ++++++++++++++++++++++ 8 files changed, 191 insertions(+), 44 deletions(-) diff --git a/doc/config/service-opts.md b/doc/config/service-opts.md index 19c7d5e8..d43429b1 100644 --- a/doc/config/service-opts.md +++ b/doc/config/service-opts.md @@ -114,13 +114,26 @@ are refused: Each resolved path is exported to the process environment under the listed name, the same names systemd uses for `RuntimeDirectory=` and -friends. +friends. As in systemd, `config-dir` is the odd one out: it is created +but never chowned. + +Each takes a matching `-mode`, e.g. `runtime-dir-mode = 0700`, default +0755. Modes are octal, with the leading zero. + +The mode of the named directory is locked down again on every start. +Its contents are left alone as long as the owner is right; if the owner +has drifted, everything under it is chowned back. The runtime directory is removed again when the service stops, after any `exec-stop-post` script has run; `/run` is a tmpfs so it would not survive a reboot anyway. The other four persist. A completed `run` or `task` counts as stopped, unless `remain-after-exit` keeps it alive -until stopped for real. +until stopped for real. `runtime-dir-preserve` adjusts this, same +values as systemd's `RuntimeDirectoryPreserve=`: + + * `"no"` -- removed when the service stops, the default + * `"restart"` -- kept across restarts, removed on a real stop + * `"yes"` -- never removed This is what lets a service drop privileges and still create, and later touch, its own PID file: diff --git a/src/conf.c b/src/conf.c index 9ab273c7..8ee3d159 100644 --- a/src/conf.c +++ b/src/conf.c @@ -274,6 +274,12 @@ static cfg_opt_t svc_opts[] = { CFG_STR ("cache-dir", NULL, CFGF_NODEFAULT), CFG_STR ("logs-dir", NULL, CFGF_NODEFAULT), CFG_STR ("config-dir", NULL, CFGF_NODEFAULT), + CFG_INT ("runtime-dir-mode", 0, CFGF_NODEFAULT), + CFG_INT ("state-dir-mode", 0, CFGF_NODEFAULT), + CFG_INT ("cache-dir-mode", 0, CFGF_NODEFAULT), + CFG_INT ("logs-dir-mode", 0, CFGF_NODEFAULT), + CFG_INT ("config-dir-mode", 0, CFGF_NODEFAULT), + CFG_STR ("runtime-dir-preserve", NULL, CFGF_NODEFAULT), CFG_STR_LIST("conflicts", NULL, CFGF_NODEFAULT), CFG_STR ("if", NULL, CFGF_NODEFAULT), CFG_STR ("tty", NULL, CFGF_NODEFAULT), @@ -1301,20 +1307,45 @@ static int if_translate(const char *str, char *buf, size_t len, char *file, cons */ static void dirs_translate(cfg_t *sec, svc_t *svc, char *file) { + const char *str; int i; for (i = 0; i < NUM_SVCDIRS; i++) { - const char *str; + char key[32]; + long num; str = sec_getstr(sec, svcdirs[i].key, NULL); - if (!str || !str[0]) - continue; + if (str && str[0]) { + if (service_set_dir(svc, &svcdirs[i], str)) + logit(LOG_ERR, "%s: %s: %s '%s' %s, ignoring", + file, cfg_title(sec), svcdirs[i].key, str, + errno == ENAMETOOLONG ? "is too long" + : "must be relative, no '..'"); + } + + snprintf(key, sizeof(key), "%s-mode", svcdirs[i].key); + if (sec_getint(sec, key, NULL, &num)) { + if (num & ~07777L) + logit(LOG_ERR, "%s: %s: %s %04lo is not a valid" + " mode, ignoring. Modes are octal," + " with the leading zero", file, + cfg_title(sec), key, num); + else + svc->dir_mode[i] = (mode_t)num; + } + } - if (service_set_dir(svc, &svcdirs[i], str)) - logit(LOG_ERR, "%s: %s: %s '%s' %s, ignoring", - file, cfg_title(sec), svcdirs[i].key, str, - errno == ENAMETOOLONG ? "is too long" - : "must be relative, no '..'"); + if ((str = sec_getstr(sec, "runtime-dir-preserve", NULL))) { + if (!strcmp(str, "no") || !strcmp(str, "false")) + svc->dir_preserve = SVC_DIR_PRESERVE_NO; + else if (!strcmp(str, "restart")) + svc->dir_preserve = SVC_DIR_PRESERVE_RESTART; + else if (!strcmp(str, "yes") || !strcmp(str, "true")) + svc->dir_preserve = SVC_DIR_PRESERVE_YES; + else + logit(LOG_ERR, "%s: %s: runtime-dir-preserve '%s' is not" + " no, restart, or yes, using no", file, + cfg_title(sec), str); } } diff --git a/src/service.c b/src/service.c index 84be55b1..903d6138 100644 --- a/src/service.c +++ b/src/service.c @@ -577,11 +577,11 @@ static void set_uid(uid_t uid, svc_t *svc) * any post: script has run. See issue #492. */ const struct svcdir svcdirs[NUM_SVCDIRS] = { - { "runtime-dir", "/run", "RUNTIME_DIRECTORY", offsetof(svc_t, runtime_dir) }, - { "state-dir", "/var/lib", "STATE_DIRECTORY", offsetof(svc_t, state_dir) }, - { "cache-dir", "/var/cache", "CACHE_DIRECTORY", offsetof(svc_t, cache_dir) }, - { "logs-dir", "/var/log", "LOGS_DIRECTORY", offsetof(svc_t, logs_dir) }, - { "config-dir", "/etc", "CONFIGURATION_DIRECTORY", offsetof(svc_t, config_dir) }, + { "runtime-dir", "/run", "RUNTIME_DIRECTORY", offsetof(svc_t, runtime_dir), 1 }, + { "state-dir", "/var/lib", "STATE_DIRECTORY", offsetof(svc_t, state_dir), 1 }, + { "cache-dir", "/var/cache", "CACHE_DIRECTORY", offsetof(svc_t, cache_dir), 1 }, + { "logs-dir", "/var/log", "LOGS_DIRECTORY", offsetof(svc_t, logs_dir), 1 }, + { "config-dir", "/etc", "CONFIGURATION_DIRECTORY", offsetof(svc_t, config_dir), 0 }, }; static char *svcdir_path(svc_t *svc, const struct svcdir *sd, char *path, size_t len) @@ -618,12 +618,15 @@ int service_set_dir(svc_t *svc, const struct svcdir *sd, const char *name) return 0; } -static void service_mkdirs(svc_t *svc) +static void service_mkdirs(svc_t *svc, uid_t uid, gid_t gid) { char path[256]; size_t i; for (i = 0; i < NELEMS(svcdirs); i++) { + uid_t u = svcdirs[i].chown ? uid : (uid_t)-1; + gid_t g = svcdirs[i].chown ? gid : 0; + struct stat st; char *ptr; ptr = svcdir_path(svc, &svcdirs[i], path, sizeof(path)); @@ -631,16 +634,21 @@ static void service_mkdirs(svc_t *svc) continue; /* - * Script forks land here too, so an existing directory - * is left alone -- mode and ownership are asserted at - * creation only, a daemon may have tightened them. + * Same rules as systemd: the named directory has its mode + * locked down again on every start, but its contents are + * only touched when the owner has drifted, then everything + * under it is chowned back. Script forks land here too, + * so this must be idempotent. */ - if (fisdir(ptr)) + if (stat(ptr, &st) == 0) { + chmod(ptr, svc->dir_mode[i]); + if (u != (uid_t)-1 && (st.st_uid != u || st.st_gid != g)) + chownr(ptr, u, g); continue; + } /* only the named directory is chowned, like systemd */ - mkpath(ptr, 0755); - if (mksubsys(ptr, 0755, svc->username, svc->group)) + if (mksubsysd(ptr, svc->dir_mode[i], u, g)) logit(LOG_WARNING, "%s: failed creating %s", svc_ident(svc, NULL, 0), ptr); } } @@ -661,8 +669,17 @@ static void service_rmdirs(svc_t *svc) char path[256]; /* only the runtime directory, /run is tmpfs, the rest persist */ - if (svcdir_path(svc, &svcdirs[0], path, sizeof(path))) - rmrf(path); + if (!svcdir_path(svc, &svcdirs[0], path, sizeof(path))) + return; + + if (svc->dir_preserve == SVC_DIR_PRESERVE_YES) + return; + + /* still qualified to run means this is a restart, not a stop */ + if (svc->dir_preserve == SVC_DIR_PRESERVE_RESTART && svc_enabled(svc)) + return; + + rmrf(path); } static pid_t service_fork(svc_t *svc) @@ -686,8 +703,6 @@ static pid_t service_fork(svc_t *svc) if (pid == 0) { char *home = NULL; - - service_mkdirs(svc); #ifdef ENABLE_STATIC int uid = 0; /* XXX: Fix better warning that dropprivs is disabled. */ int gid = 0; @@ -707,6 +722,7 @@ static pid_t service_fork(svc_t *svc) return -1; } #endif + service_mkdirs(svc, uid, gid); if (svc_is_tty(svc)) setprocnm("getty"); @@ -2372,8 +2388,11 @@ svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file) memset(svc->capabilities, 0, sizeof(svc->capabilities)); /* block format only, set by conf.c after registration */ - for (int i = 0; i < NUM_SVCDIRS; i++) + for (int i = 0; i < NUM_SVCDIRS; i++) { memset((char *)svc + svcdirs[i].off, 0, MAX_ARG_LEN); + svc->dir_mode[i] = 0755; + } + svc->dir_preserve = SVC_DIR_PRESERVE_NO; if (!svc_is_tty(svc) && ctty) { char *dev = ctty; diff --git a/src/service.h b/src/service.h index deaea552..f8b94fe5 100644 --- a/src/service.h +++ b/src/service.h @@ -38,8 +38,8 @@ struct svcdir { const char *base; const char *env; size_t off; /* offsetof() in svc_t */ + int chown; /* all but config-dir, like systemd */ }; -#define NUM_SVCDIRS 5 extern const struct svcdir svcdirs[NUM_SVCDIRS]; int service_set_dir (svc_t *svc, const struct svcdir *sd, const char *name); diff --git a/src/svc.h b/src/svc.h index 6aced5f0..8e91d6de 100644 --- a/src/svc.h +++ b/src/svc.h @@ -201,11 +201,17 @@ typedef struct svc { /* Directories set up for the service, block format only, the * name is resolved under a fixed base, e.g. /run/NAME */ +#define NUM_SVCDIRS 5 char runtime_dir[MAX_ARG_LEN]; char state_dir[MAX_ARG_LEN]; char cache_dir[MAX_ARG_LEN]; char logs_dir[MAX_ARG_LEN]; char config_dir[MAX_ARG_LEN]; + mode_t dir_mode[NUM_SVCDIRS]; +#define SVC_DIR_PRESERVE_NO 0 /* remove runtime-dir on stop */ +#define SVC_DIR_PRESERVE_RESTART 1 /* keep it across restarts */ +#define SVC_DIR_PRESERVE_YES 2 /* never remove it */ + char dir_preserve; /* Command, arguments and service description */ char cmd[MAX_CMD_LEN]; diff --git a/src/util.c b/src/util.c index 7cc8b095..8a5f3ee4 100644 --- a/src/util.c +++ b/src/util.c @@ -325,6 +325,33 @@ static int rmrf_cb(const char *fpath, const struct stat *sb, int tflag, struct F return 0; } +/* nftw() cannot pass user data, see also tmpfiles.c do_clean() */ +static uid_t chownr_uid; +static gid_t chownr_gid; + +static int chownr_cb(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw) +{ + (void)tflag; + (void)ftw; + + if (sb->st_uid == chownr_uid && sb->st_gid == chownr_gid) + return 0; + + if (lchown(fpath, chownr_uid, chownr_gid)) + warn("Failed chown(%s, %d, %d)", fpath, (int)chownr_uid, (int)chownr_gid); + + return 0; +} + +/* chown -R */ +int chownr(const char *path, uid_t uid, gid_t gid) +{ + chownr_uid = uid; + chownr_gid = gid; + + return nftw(path, chownr_cb, 20, FTW_PHYS); +} + /* empty a directory but keep it, silently ignores a missing path */ int rmcontents(const char *path) { @@ -347,34 +374,43 @@ int rmrf(const char *path) return 0; } -int mksubsys(const char *dir, mode_t mode, char *user, char *group) +/* + * Like mksubsys() but with the ids already resolved, uid -1 skips the + * chown. Parents are created 0755, only the leaf gets @mode. + */ +int mksubsysd(const char *dir, mode_t mode, uid_t uid, gid_t gid) { mode_t omask; - int uid, gid; - int rc = 0; + int rc; omask = umask(0); - rc = makedir(dir, mode); - if (rc && errno == EEXIST) + rc = mkpath(dir, 0755); + if (!rc) { rc = chmod(dir, mode); - - uid = getuser(user, NULL); - if (uid >= 0) { - gid = getgroup(group); - if (gid < 0) - gid = 0; - - if (chown(dir, uid, gid)) - err(1, "Failed chown(%s, %d, %d)", dir, uid, gid); - } else - warnx("Cannot find user %s, %s is owned by root", user, dir); + if (!rc && uid != (uid_t)-1 && chown(dir, uid, gid)) + err(1, "Failed chown(%s, %d, %d)", dir, (int)uid, (int)gid); + } umask(omask); return rc; } +int mksubsys(const char *dir, mode_t mode, char *user, char *group) +{ + int uid, gid; + + uid = getuser(user, NULL); + gid = getgroup(group); + if (gid < 0) + gid = 0; + if (uid < 0) + warnx("Cannot find user %s, %s is owned by root", user, dir); + + return mksubsysd(dir, mode, uid < 0 ? (uid_t)-1 : (uid_t)uid, (gid_t)gid); +} + /* * Read an open stream to EOF into a malloc()'ed, NUL terminated buffer. * diff --git a/src/util.h b/src/util.h index 53cfd84e..7e7a6aaf 100644 --- a/src/util.h +++ b/src/util.h @@ -72,6 +72,8 @@ int getcuser (char *buf, size_t len); int getcgroup (char *buf, size_t len); int mksubsys (const char *dir, mode_t mode, char *user, char *group); +int mksubsysd (const char *dir, mode_t mode, uid_t uid, gid_t gid); +int chownr (const char *path, uid_t uid, gid_t gid); int rmcontents (const char *path); int rmrf (const char *path); diff --git a/test/conf-dirs.sh b/test/conf-dirs.sh index 79d0ac11..369efd32 100755 --- a/test/conf-dirs.sh +++ b/test/conf-dirs.sh @@ -28,6 +28,15 @@ service escape { runlevel = \"S12345\" runtime-dir = \"../escape\" command = \"serv -np -i escape\" +} +service modes { + runlevel = \"S12345\" + user = \"daemon\" + runtime-dir = \"modes\" + runtime-dir-mode = 0700 + runtime-dir-preserve = \"restart\" + config-dir = \"modes\" + command = \"/sbin/serv -np -P /run/modes/serv.pid -i modes\" }" # ls output is empty for an empty directory, so test -d instead @@ -78,3 +87,34 @@ assert_dir /run/owned say 'A path escaping the base directory is refused, service still runs' retry 'assert_status escape running' assert_nodir /escape + +say 'runtime-dir-mode sets the mode, config-dir is never chowned' +retry 'assert_status modes running' +assert "mode is 0700" "$(texec stat -c %a /run/modes)" = "700" +assert_owner /run/modes daemon:root +assert_owner /etc/modes root:root + +say 'runtime-dir-preserve restart keeps the directory across a restart' +run "touch /run/modes/keepsake" || texec touch /run/modes/keepsake +run "initctl restart modes" +retry 'assert_status modes running' +assert_file_exists /run/modes/keepsake + +say 'but a real stop still removes it' +run "initctl stop modes" +retry 'assert_status modes stopped' +assert_nodir /run/modes + +say 'An existing directory with the wrong owner is chowned back, and' +say 'its mode is locked down again' +run "initctl stop owned" +retry 'assert_status owned stopped' +texec mkdir -p /var/lib/owned/sub +texec touch /var/lib/owned/sub/file +texec chown -R 0:0 /var/lib/owned +texec chmod 0700 /var/lib/owned +run "initctl start owned" +retry 'assert_status owned running' +assert "mode locked down to 0755" "$(texec stat -c %a /var/lib/owned)" = "755" +assert_owner /var/lib/owned daemon:daemon +assert_owner /var/lib/owned/sub/file daemon:daemon From 153f719e339b2c7902b927c5789a73247f532ead Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 30 Jul 2026 15:31:46 +0200 Subject: [PATCH 27/28] .github: install libconfuse-dev, mandatory since the new .conf format The workflows install libuev and libite from source and everything else from apt, but never libconfuse, so every build job on this branch dies in configure: checking for libconfuse >= 3.3... no Ubuntu ships libconfuse 3.3 with the static library included, which covers both the static and regular builds. Staying on 3.3 in CI is deliberate: it exercises the fallback paths marked "XXX: Workaround for libConfuse <3.4" that a from-source 3.4 would leave untested. Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 3 +-- .github/workflows/coverity.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/weekly.yml | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 460d378a..cec7aeab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,8 +34,7 @@ jobs: - name: Install dependencies run: | sudo apt-get -y update - sudo apt-get -y install pkg-config tree jq libcap-dev - sudo apt-get -y install pkg-config + sudo apt-get -y install pkg-config tree jq libcap-dev libconfuse-dev wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz tar xf libuev-2.4.1.tar.xz diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index 6b4add0b..f9b51781 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -55,7 +55,7 @@ jobs: - name: Install dependencies run: | sudo apt-get -y update - sudo apt-get -y install pkg-config libcap-dev + sudo apt-get -y install pkg-config libcap-dev libconfuse-dev wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz tar xf libuev-2.4.1.tar.xz diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f740e24..d0bfcbd4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Installing dependencies ... run: | sudo apt-get -y update - sudo apt-get -y install pkg-config jq libcap-dev + sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz tar xf libuev-2.4.1.tar.xz diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 56991c2f..d2d8cbe4 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | sudo apt-get -y update - sudo apt-get -y install pkg-config jq libcap-dev + sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz tar xf libuev-2.4.1.tar.xz From 301992b8d883a2ce13e8577a0a603535c9bed5d6 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 31 Jul 2026 11:55:38 +0200 Subject: [PATCH 28/28] doc: bare-ID services have no block equivalent The line-based format accepts `service :80 ...`, deriving the name from the command basename. The block format has no counterpart, the title carries both name and ID. Implied by the format description, but anyone converting such a line deserves to find it written down. Signed-off-by: Joachim Wiberg --- doc/config/services.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/config/services.md b/doc/config/services.md index 281106ee..570a94f7 100644 --- a/doc/config/services.md +++ b/doc/config/services.md @@ -213,6 +213,11 @@ multiple web servers, add `:ID` to the block title, like this: Without the `:ID` the latter will overwrite the former and only the old web server would be started and supervised. +> [!NOTE] +> The line-based format also accepts a bare ID, `service :80 ...`, +> deriving the name from the command. There is no block equivalent: +> the title carries both name and ID. + Conditional Loading -------------------