From ffb1db7d2d7e69a2018246aea5c279f53ed83b83 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 5 Aug 2026 11:55:58 +0200 Subject: [PATCH] conf: add provides, additional conditions a service supplies A block title is a service identity, so two variants of one service gated differently per platform cannot share a title. They do need to share the barrier condition downstream services wait for, which until now was spelled by the identity alone and so could not be shared: service syslogd:udev { if = "udevd" conditions = { "run/udevadm:5/success" } provides = "pid/syslogd" command = "-syslogd -F" } Any namespace is allowed, since the point is publishing a name that existing configurations already wait on. A claim on a condition that is already owned is dropped with a warning naming the owner, and the service still registers: the overlap is a configuration bug, and an init system is more useful degraded than refusing to boot. A real identity outranks a claim, pid/ is how Finit tracks its own services, so it is not up for grabs. Claims are dropped before each reload re-reads the .conf files. Doing it per service as it re-registers is not enough, since services are read in file order and one re-registering would lose to a claim another had not dropped yet, flipping the owner on every reload. initctl cond dump asked who owned a condition only for the pid/ namespace and printed 'static' for usr/, which now hides a provider. It asks first and falls back to what the namespace implies. Signed-off-by: Joachim Wiberg --- src/conf.c | 55 ++++++++++++++++ src/initctl.c | 24 +++---- src/service.c | 1 + test/Makefile.am | 2 + test/conf-provides.sh | 145 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 216 insertions(+), 11 deletions(-) create mode 100755 test/conf-provides.sh diff --git a/src/conf.c b/src/conf.c index f5e87d11..bf1fe475 100644 --- a/src/conf.c +++ b/src/conf.c @@ -288,6 +288,7 @@ static cfg_opt_t svc_opts[] = { 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_LIST("provides", NULL, CFGF_NODEFAULT), CFG_STR ("if", NULL, CFGF_NODEFAULT), CFG_STR ("tty", NULL, CFGF_NODEFAULT), /* @@ -1382,6 +1383,58 @@ static int if_translate(const char *str, char *buf, size_t len, char *file, cons return 0; } +/* + * provides names conditions this service asserts on top of its own + * pid/, so blocks with distinct titles can supply one barrier + * to everything downstream. Like the per-service directories it has + * no legacy token and is stored on the registered svc. + * + * A claim on a condition that is already owned is dropped, with the + * service itself left alone: the overlap is a configuration bug, and + * an init system is more useful degraded than refusing to boot. + */ +static void provides_translate(cfg_t *sec, svc_t *svc, char *file) +{ + unsigned int i, num; + + num = cfg_size(sec, "provides"); + for (i = 0; i < num; i++) { + const char *cond = cfg_getnstr(sec, "provides", i); + svc_t *owner; + + if (!strchr(cond, '/')) { + logit(LOG_ERR, "%s: %s: provides '%s' is not a condition," + " it needs a namespace, e.g. usr/%s, ignoring", + file, cfg_title(sec), cond, cond); + continue; + } + + if (strlen(cond) >= MAX_COND_LEN) { + logit(LOG_ERR, "%s: %s: provides '%s' is too long," + " ignoring", file, cfg_title(sec), cond); + continue; + } + + if (svc->num_provides >= MAX_NUM_PROVIDES) { + logit(LOG_WARNING, "%s: %s: too many provides, max %d," + " ignoring '%s'", file, cfg_title(sec), + MAX_NUM_PROVIDES, cond); + break; + } + + owner = svc_cond_owner(cond, svc); + if (owner) { + logit(LOG_WARNING, "%s: %s: provides condition <%s>" + " already registered by service %s, ignoring", + file, cfg_title(sec), cond, + svc_ident(owner, NULL, 0)); + continue; + } + + strlcpy(svc->provides[svc->num_provides++], cond, MAX_COND_LEN); + } +} + /* * These have no legacy token, they are validated by service_set_dir() * and stored directly on the registered svc. Empty means unset, @@ -1644,6 +1697,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi return; dirs_translate(sec, svc, file); + provides_translate(sec, svc, file); } /* @@ -2199,6 +2253,7 @@ int conf_reload(void) cgroup_mark_all(); svc_mark_dynamic(); conf_reset_env(); + svc_provides_reset(); /* * Reset global rlimit to bootstrap values from conf_init(). diff --git a/src/initctl.c b/src/initctl.c index 435e2da9..f0dbcf86 100644 --- a/src/initctl.c +++ b/src/initctl.c @@ -337,6 +337,7 @@ static int dump_one_cond(const char *fpath, const struct stat *sb, int tflag, st const char *cond, *asserted; char *nm = "init"; pid_t pid = 1; + svc_t *svc; if (tflag != FTW_F) return 0; @@ -350,17 +351,18 @@ static int dump_one_cond(const char *fpath, const struct stat *sb, int tflag, st if (dump_filter && dump_filter[0] && strncmp(cond, dump_filter, strlen(dump_filter))) return 0; - if (strncmp("pid/", cond, 4) == 0) { - svc_t *svc; - - svc = client_svc_find_by_cond(cond); - if (!svc) { - nm = "unknown"; - pid = 0; - } else { - nm = svc_ident(svc, NULL, 0); - pid = svc->pid; - } + /* + * Any namespace can be claimed with provides, so ask who owns + * the condition before falling back to what the namespace + * implies on its own. + */ + svc = client_svc_find_by_cond(cond); + if (svc) { + nm = svc_ident(svc, NULL, 0); + pid = svc->pid; + } else if (strncmp("pid/", cond, 4) == 0) { + nm = "unknown"; + pid = 0; } else if (strncmp("usr/", cond, 4) == 0) { nm = "static"; pid = 0; diff --git a/src/service.c b/src/service.c index fc3abd5c..55f3b858 100644 --- a/src/service.c +++ b/src/service.c @@ -2391,6 +2391,7 @@ svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file) svc->dir_mode[i] = 0755; } svc->dir_preserve = SVC_DIR_PRESERVE_NO; + svc->num_provides = 0; if (!svc_is_tty(svc) && ctty) { char *dev = ctty; diff --git a/test/Makefile.am b/test/Makefile.am index a87995b0..c46205f5 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-command.sh +EXTRA_DIST += conf-provides.sh EXTRA_DIST += conf-dup-title.sh EXTRA_DIST += conf-dirs.sh EXTRA_DIST += conf-if.sh @@ -84,6 +85,7 @@ TESTS += bootstrap-crash.sh TESTS += cond-start-task.sh TESTS += conf-format.sh TESTS += conf-command.sh +TESTS += conf-provides.sh TESTS += conf-dup-title.sh TESTS += conf-dirs.sh TESTS += conf-if.sh diff --git a/test/conf-provides.sh b/test/conf-provides.sh new file mode 100755 index 00000000..1012e57c --- /dev/null +++ b/test/conf-provides.sh @@ -0,0 +1,145 @@ +#!/bin/sh +# Verify 'provides': a block asserts conditions beyond its own +# pid/, so variants with distinct titles can supply one +# barrier. A claim on a condition somebody already owns is refused, +# with the service itself still registered. +set -eu + +TEST_DIR=$(dirname "$0") + +# shellcheck disable=SC2034 +# Only the udev variant qualifies, mdevd is not a known service here, +# so syslogd:udev is the one that gets to supply pid/syslogd. +BOOTSTRAP="service anchor { + runlevel = \"S12345\" + command = \"serv -np -i anchor\" +} +service syslogd:udev { + runlevel = \"S12345\" + if = \"anchor\" + provides = \"pid/syslogd\" + command = \"serv -np -i sysudev\" +} +service syslogd:mdev { + runlevel = \"S12345\" + if = \"mdevd\" + provides = \"pid/syslogd\" + command = \"serv -np -i sysmdev\" +} +service downstream { + runlevel = \"S12345\" + conditions = { \"pid/syslogd\" } + command = \"serv -np -i downstream\" +}" + +# 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" +} + +# The IDENT column of 'initctl cond dump' names the owner of a +# condition, which for a provided one is the service that claimed it. +assert_provider() +{ + assert "Condition $1 provided by $2" \ + "$(texec initctl cond dump | awk -v c="<$1>" '$4 == c {print $2}')" = "$2" +} + +test_teardown() +{ + say "Running test teardown." + run "rm -f $FINIT_CONF" +} + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +say 'The qualifying variant is loaded, the other is pruned by if' +retry 'assert_status syslogd:udev running' +assert_loaded syslogd:mdev 0 + +say 'It asserts the condition it provides, on top of its own' +retry 'assert_cond pid/syslogd' +assert_cond pid/syslogd:udev + +say 'A service waiting on the provided condition starts' +retry 'assert_status downstream running' + +say 'initctl cond dump names the provider, not "unknown"' +assert_provider pid/syslogd syslogd:udev + +say 'Stopping the provider clears what it provided' +run "initctl stop syslogd:udev" +retry 'assert_nocond pid/syslogd' + +run "initctl start syslogd:udev" +retry 'assert_cond pid/syslogd' + +# With a BOOTSTRAP the test is released in runlevel S, where a reload +# is ignored, so wait for the runlevel change before rewriting. +say 'Waiting for bootstrap to finish before rewriting the configuration' +retry "test \"\$(texec sh -c \"initctl runlevel | awk '{print \\\$2;}'\")\" = 2" 20 1 + +# The claim is refused, but the service is still registered and runs. +# Both blocks qualify here, which is the configuration bug the warning +# exists for. +say 'A second claim on the same condition is refused, the service still runs' +run "echo 'service first {' > $FINIT_CONF" +run "echo ' provides = \"usr/barrier\"' >> $FINIT_CONF" +run "echo ' command = \"serv -np -i first\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "echo 'service second {' >> $FINIT_CONF" +run "echo ' provides = \"usr/barrier\"' >> $FINIT_CONF" +run "echo ' command = \"serv -np -i second\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +retry 'assert_status first running' +assert_status second running +assert_provider usr/barrier first + +# A real service by that identity outranks a claim on it. +say 'An identity beats a claim on the same condition' +run "echo 'service realsvc {' > $FINIT_CONF" +run "echo ' command = \"serv -np -i realsvc\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "echo 'service pretender {' >> $FINIT_CONF" +run "echo ' provides = \"pid/realsvc\"' >> $FINIT_CONF" +run "echo ' command = \"serv -np -i pretender\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +retry 'assert_status pretender running' +assert_provider pid/realsvc realsvc + +say 'A value without a namespace is rejected, the service still runs' +run "echo 'service nonamespace {' > $FINIT_CONF" +run "echo ' provides = \"syslogd\"' >> $FINIT_CONF" +run "echo ' command = \"serv -np -i nonamespace\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" + +retry 'assert_status nonamespace running' + +# Claims are dropped before the .conf files are re-read, so a service +# re-registering cannot lose to a claim another one has not yet +# dropped. Without that, ownership flips on every reload. +say 'Ownership survives a reload, it does not flip between variants' +run "echo 'service keeper {' > $FINIT_CONF" +run "echo ' provides = \"usr/kept\"' >> $FINIT_CONF" +run "echo ' command = \"serv -np -i keeper\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "echo 'service loser {' >> $FINIT_CONF" +run "echo ' provides = \"usr/kept\"' >> $FINIT_CONF" +run "echo ' command = \"serv -np -i loser\"' >> $FINIT_CONF" +run "echo '}' >> $FINIT_CONF" +run "initctl reload" +retry 'assert_provider usr/kept keeper' + +run "initctl reload" +retry 'assert_provider usr/kept keeper' +run "initctl reload" +retry 'assert_provider usr/kept keeper'