From 868473de8606e4fdac7a6dafb44f94dcf2635e26 Mon Sep 17 00:00:00 2001 From: lijinzhe Date: Fri, 30 Oct 2026 17:31:27 +0800 Subject: [PATCH] conf: optionally load services from available The distro layout currently treats finit.d/available as a staging area only. A unit is unknown to Finit until its configuration is linked into enabled, so disabling autostart also removes the unit from runtime control. Using manual-start keeps a unit controllable, but requires changing the unit configuration instead of using the existing enable and disable interface. Add the top-level load-available setting. When enabled, configuration files in finit.d/available are read before the regular configuration paths and their runnable units are registered with an available-only source state. Such units remain visible to initctl and can be started and stopped explicitly, but are prevented from starting automatically. A later enabled symlink overrides that source state and restores the unit's own manual-start policy; disabling it again leaves a running instance alone while preventing future automatic starts. Keep loader source state separate from manual-start so an explicit manual-start setting continues to apply after a unit is enabled. Pass the source through both block and legacy parsers, including nested legacy includes, and apply it consistently to service, task, run, sysv, and tty units. Load available files first so normal configuration precedence and same-name overrides remain intact, while ignoring dangling replacement symlinks. Available files are passive unit definitions, not active system configuration. Ignore their top-level settings until enabled, while retaining file-local resource limits needed by their units. Also keep available-only units from satisfying configuration-time if:name checks or displacing providers from enabled units. Handle reload transitions without restarting completed tasks, losing a manual start waiting on conditions, or stopping a running unit merely because it was disabled. Guard retry paths against reviving stopped available-only services. A late pidfile from a forking service can otherwise undo an explicit stop during the same transition, so retain the stop request when adopting the discovered PID and let the state machine terminate it. Document the setting, its precedence and manual-start interaction, and add integration coverage for block and legacy formats, all runnable unit types, enable/disable transitions, conditions, providers, global setting isolation, dangling links, delayed pidfiles, and empty available directories. --- doc/config/files.md | 57 ++++- doc/config/service-opts.md | 6 + doc/config/tty.md | 3 +- doc/distro.md | 24 ++ man/finit.conf.5 | 22 +- plugins/modules-load.c | 3 +- plugins/pidfile.c | 18 +- src/conf.c | 216 +++++++++++++++--- src/legacy.c | 72 ++++-- src/legacy.h | 2 +- src/service.c | 63 ++++-- src/service.h | 5 +- src/svc.c | 23 ++ src/svc.h | 10 +- test/Makefile.am | 2 + test/available-load.sh | 449 +++++++++++++++++++++++++++++++++++++ 16 files changed, 889 insertions(+), 86 deletions(-) create mode 100755 test/available-load.sh diff --git a/doc/config/files.md b/doc/config/files.md index edab5c08..ff40a105 100644 --- a/doc/config/files.md +++ b/doc/config/files.md @@ -57,11 +57,20 @@ being where they are installed and if the `initctl` tool can manage them with the `enable` and `disable` commands. An administrator can always create files and symlinks manually. +By default, `available/` is only a staging area, its files are *not* +read by Finit. With the [`load-available`](#loading-available-services) +setting enabled, their unit stanzas are registered as *available-only*: +controllable with `initctl start/stop`, but never started automatically +until enabled. This loader state is separate from `manual-start`, and +top-level settings remain inactive. + At bootstrap, and `initctl reload`, all .conf files are read, starting with `finit.conf`, then `/run/finit/system/*.conf`, `/lib/finit/system/*.conf`, `finit.d/*.conf`, and finally all -`finit.d/enabled/*.conf` files. Each directory is a unique group, -where files within each group are sorted alphabetically. +`finit.d/enabled/*.conf` files. With `load-available` enabled, +`finit.d/available/*.conf` is read first of all, so a same-named file +in any of the other directories overrides it. Each directory is a +unique group, where files within each group are sorted alphabetically. **Example:** @@ -204,6 +213,50 @@ another set of .conf files, e.g.: > This directive is only available from the top-level bootstrap .conf > file, usually `/etc/finit.conf`. +Loading Available Services +-------------------------- + +**Syntax:** `load-available = true` + +By default Finit only reads `finit.d/enabled/*.conf`, so a service that +has been installed in `finit.d/available/` but not enabled is unknown to +Finit and `initctl start` fails with *no such task or service*. This +setting makes Finit read `finit.d/available/*.conf` as well, so such +services can be started and stopped at runtime without being enabled, +i.e., without starting automatically at boot: + + load-available = true + +Run, task, service, sysv, and tty stanzas from `available/` are marked +available-only by the loader. There is no configuration key for this +state, and includes inherit it. `manual-start` remains an independent +unit option: it does not enable an available-only unit, even when set to +`false`. + +Enabling a service (a symlink in `enabled/`, read last) makes its logical +source enabled. The unit then follows its own `manual-start` setting: +`true` remains manual, while `false` or an omitted setting permits normal +automatic start. Disabling it again leaves it loaded and startable with +`initctl start`; a running instance is not stopped merely by reload. + +Only units and their file-scope resource limits are loaded from an +available-only file. Top-level settings, such as `environment`, +`cgroup`, `modules`, `mknod`, `runlevel`, and `shutdown`, are ignored +with a warning until the file is enabled. Unit-local settings such as +`envfile`, `log`, `cgroup`, and `rlimit` remain active so the unit can be +started manually. + +Units restricted to the bootstrap `S` runlevel retain the normal Finit +lifecycle: they are discarded after bootstrap and cannot be started in +a regular runlevel. Enable them before the next boot to run them. + +> [!NOTE] +> Like `rcsd`, this directive is only available from the top-level +> bootstrap .conf file, usually `/etc/finit.conf`, and only in the +> block-based format. A legacy format `finit.conf` can enable the +> feature with `include /path/to.conf`, where the included block-based +> file sets it. + Including Finit Configs ------------------------ diff --git a/doc/config/service-opts.md b/doc/config/service-opts.md index b866012f..9e45ca09 100644 --- a/doc/config/service-opts.md +++ b/doc/config/service-opts.md @@ -48,6 +48,12 @@ It can then be started at any time with `initctl start `: command = "/usr/sbin/lldpd -d" } +> [!NOTE] +> With [`load-available`](files.md#loading-available-services) enabled, +> units in `finit.d/available/` are prevented from automatic start by +> their loader-derived available-only state. This is independent of +> `manual-start`, which takes effect normally after the unit is enabled. + Other run/task/service settings are: * `capabilities` -- see the [Linux Capabilities](capabilities.md) section diff --git a/doc/config/tty.md b/doc/config/tty.md index 8d5ee43f..0efbdfab 100644 --- a/doc/config/tty.md +++ b/doc/config/tty.md @@ -21,6 +21,7 @@ Settings common to all three: |---|---|---| | `runlevel` | | Runlevels to run in, e.g. `"12345"` | | `conditions` | `cond` | Conditions to wait for | +| `manual-start` | `manual` | Register without starting automatically | | `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 | @@ -164,4 +165,4 @@ connected to the TTY device. runlevel = "S" tty = "@console" command = "my-setup-script" - } \ No newline at end of file + } diff --git a/doc/distro.md b/doc/distro.md index 2baac028..b7e7f9c4 100644 --- a/doc/distro.md +++ b/doc/distro.md @@ -101,6 +101,30 @@ directory (e.g., `/etc/finit.d/`), `initctl` will ignore it. This allows you to create system-level overrides that cannot be accidentally disabled by package management tools. +### Runtime Control Without Enabling + +By default, a `.conf` that only exists in `available/` is not loaded at +all, so `initctl start` cannot control it -- enabling (load) and starting +at boot are one and the same switch. Products that need the systemd-like +separation between "starts at boot" and "can be started/stopped at +runtime" can enable the `load-available` setting in `/etc/finit.conf`: + +``` +load-available = true +``` + +With this setting, unit stanzas in `available/` are loaded with an +internal available-only source state: they show up in `initctl status` +and can be started and stopped freely, yet they do not start +automatically until enabled. This state is independent of the unit's +`manual-start` option. Top-level settings in an available-only file +remain inactive. Enabling the file activates those settings and makes +the unit follow `manual-start`; disabling it no longer makes a service +uncontrollable (a running instance is not stopped by `initctl reload`, +mirroring `systemctl disable`). See +[config/files.md](config/files.md#loading-available-services) for +details. + ## Customizing Directory Paths Distributions can customize the directory names and locations at build time diff --git a/man/finit.conf.5 b/man/finit.conf.5 index a1c2df19..d7eda5d7 100644 --- a/man/finit.conf.5 +++ b/man/finit.conf.5 @@ -2,7 +2,7 @@ .\" First parameter, NAME, should be all caps .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection .\" other parameters are allowed: see man(7), man(1) -.Dd July 10, 2025 +.Dd October 29, 2026 .Dt FINIT.CONF 5 SMM .Os Linux .Sh NAME @@ -62,6 +62,26 @@ file. Please use the .Cm initctl tool to manage these symlinks to ensure proper operation. .Pp +By default, files in +.Pa /etc/finit.d/available/ +are not loaded; a service must be enabled to be known to +.Nm Finit . +With the +.Cm load-available = true +setting in +.Pa /etc/finit.conf +(new block-based format only), unit stanzas in +.Pa available/ +are registered with an internal available-only source state and are +controllable with +.Cm initctl start/stop , +but are not started automatically until enabled. This state is separate +from the unit's +.Cm manual-start +option. Top-level settings in those files remain inactive until enabled. +See the Finit documentation for details and the bootstrap-only runlevel +limitation. +.Pp .Sy NOTE: Previous versions of .Nm Finit diff --git a/plugins/modules-load.c b/plugins/modules-load.c index e2e45c72..83f552de 100644 --- a/plugins/modules-load.c +++ b/plugins/modules-load.c @@ -169,7 +169,8 @@ static int modules_load(const char *file, int index) snprintf(cmd, sizeof(cmd), SERVICE_LINE, mod, index++, lvl, modprobe_path, mod, args); dbg("task %s", cmd); - service_register(SVC_TYPE_TASK, cmd, global_rlimit, NULL); + service_register(SVC_TYPE_TASK, cmd, global_rlimit, NULL, + SVC_SOURCE_ENABLED); num++; next: free(line); diff --git a/plugins/pidfile.c b/plugins/pidfile.c index 1ae4bed9..1641fb49 100644 --- a/plugins/pidfile.c +++ b/plugins/pidfile.c @@ -103,10 +103,6 @@ static void pidfile_update_conds(char *dir, char *name, uint32_t mask) nm = svc_ident(svc, NULL, 0); dbg("Found svc %s for %s with pid %d", nm, fn, svc->pid); - if (svc_is_stopping(svc)) { - dbg("Ignoring pidfile death rattles while service %s is %s", nm, svc_status(svc)); - goto sneaky_zebra; - } if (mask & (IN_CLOSE_WRITE | IN_ATTRIB | IN_MODIFY | IN_MOVED_TO)) { /* @@ -126,16 +122,13 @@ static void pidfile_update_conds(char *dir, char *name, uint32_t mask) pid_t pid; pid = pid_file_read(fn); - if (pid == -1) { - /* handle case when service dies immediately */ + if (pid <= 1) { + /* handle missing, empty, or otherwise invalid pidfiles */ if (!fexist(fn)) - cond_clear(cond); + cond_clear(cond); return; } - /* Service is not crashing, let's tell state machine! */ - service_forked(svc); - if (pid != svc->pid) { dbg("Forking service %s (cmd %s) changed PID from %d to %d", svc_ident(svc, NULL, 0), svc->cmd, svc->pid, pid); @@ -145,6 +138,10 @@ static void pidfile_update_conds(char *dir, char *name, uint32_t mask) logit(LOG_CONSOLE | LOG_NOTICE, "Started %s[%d]", svc_ident(svc, NULL, 0), pid); cgroup_move_svc(svc); } + + /* Service is not crashing, let's tell state machine! */ + if (!service_forked(svc)) + return; } if (svc->notify == SVC_NOTIFY_PID) @@ -152,7 +149,6 @@ static void pidfile_update_conds(char *dir, char *name, uint32_t mask) if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE) service_ready(svc, 1); } else if (mask & IN_DELETE) { - sneaky_zebra: svc_cond_clear(svc); if (svc->notify == SVC_NOTIFY_PID) service_ready(svc, 0); diff --git a/src/conf.c b/src/conf.c index 22e5e79d..95f7d0f4 100644 --- a/src/conf.c +++ b/src/conf.c @@ -94,6 +94,15 @@ char *runparts = NULL; int runparts_progress; int runparts_sysv; +/* + * Load units from available/ as available-only: they are registered and + * controllable with initctl, but source state prevents automatic start + * until enabled. Off by default, set with the top-level load-available + * setting in /etc/finit.conf and reset on every reload. + */ +static int load_available; +static int available_context; + 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 */ @@ -308,6 +317,8 @@ static cfg_opt_t tty_opts[] = { CFG_STR ("runlevel", NULL, CFGF_NODEFAULT), CFG_STR_LIST("conditions", NULL, CFGF_NODEFAULT), CFG_STR_LIST("cond", NULL, CFGF_NODEFAULT), /* alias */ + CFG_BOOL ("manual-start", cfg_false, CFGF_NODEFAULT), + CFG_BOOL ("manual", cfg_false, CFGF_NODEFAULT), /* alias */ CFG_STR ("device", NULL, CFGF_NODEFAULT), CFG_INT ("baud", 0, CFGF_NODEFAULT), CFG_STR ("term", NULL, CFGF_NODEFAULT), @@ -349,6 +360,7 @@ static cfg_opt_t conf_opts[] = { CFG_STR_LIST("mod", NULL, CFGF_NODEFAULT), /* alias */ CFG_STR_LIST("mknod", NULL, CFGF_NODEFAULT), CFG_STR ("network", NULL, CFGF_NODEFAULT), + CFG_BOOL ("load-available", cfg_false, CFGF_NODEFAULT), CFG_STR ("rcsd", NULL, CFGF_NODEFAULT), CFG_STR ("runparts", NULL, CFGF_NODEFAULT), CFG_BOOL ("runparts-progress", cfg_false, CFGF_NODEFAULT), @@ -1113,6 +1125,31 @@ static int sec_getbool(cfg_t *sec, const char *key, const char *alias) return cfg_getbool(sec, key) == cfg_true; } +/* + * True if FILE is in the available/ sub-directory of rcsd and the + * load-available setting is active. This selects the restricted parse + * context in conf_reload(); nested legacy includes inherit that context. + */ +static int conf_is_available(const char *file) +{ + char path[strlen(finit_rcsd) + 12]; + + if (!load_available || !file) + return 0; + + snprintf(path, sizeof(path), "%s/available/", finit_rcsd); + + return !strncmp(file, path, strlen(path)); +} + +/* Check that a configuration path can actually be parsed. */ +static int conf_is_file(const char *file) +{ + struct stat st; + + return !stat(file, &st) && !S_ISDIR(st.st_mode); +} + /* * Pick the command for a service. Several may be listed, in which * case they are candidates for the same service and the first one @@ -1425,6 +1462,9 @@ static void provides_translate(cfg_t *sec, svc_t *svc, char *file) } owner = svc_cond_owner(cond, svc); + if (owner && svc_is_available(owner) && !svc_is_available(svc) && + svc_provides_remove(owner, cond)) + owner = NULL; if (owner) { logit(LOG_WARNING, "%s: %s: provides condition <%s>" " already registered by service %s, ignoring", @@ -1521,7 +1561,8 @@ static void pam_translate(cfg_t *sec, svc_t *svc, char *file) file, cfg_title(sec)); } -static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *file) +static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *file, + int is_available) { struct rlimit local_rlimit[RLIMIT_NLIMITS]; char line[LINE_SIZE] = ""; @@ -1635,6 +1676,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi file, str); } + /* manual-start is independent of available/enabled source state. */ if (sec_getbool(sec, "manual-start", "manual")) addtok(line, sizeof(line), "manual:yes"); if (sec_getbool(sec, "remain-after-exit", "remain")) @@ -1729,7 +1771,7 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi addtok(line, sizeof(line), "-- %s", str); dbg("translated: %s", line); - svc = service_register(type, line, rlimit, file); + svc = service_register(type, line, rlimit, file, is_available ? SVC_SOURCE_AVAILABLE_ONLY : SVC_SOURCE_ENABLED); if (!svc) return; @@ -1742,7 +1784,8 @@ static void svc_translate(cfg_t *sec, int type, struct rlimit rlimit[], char *fi * 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) +static void tty_translate(cfg_t *sec, struct rlimit rlimit[], char *file, + int is_available) { char line[LINE_SIZE] = ""; const char *str, *dev, *cmd; @@ -1755,6 +1798,9 @@ static void tty_translate(cfg_t *sec, struct rlimit rlimit[], char *file) if (sec_getlist(sec, "conditions", "cond", buf, sizeof(buf))) addtok(line, sizeof(line), "<%s>", buf); + if (sec_getbool(sec, "manual-start", "manual")) + addtok(line, sizeof(line), "manual:yes"); + dev = sec_getstr(sec, "device", NULL); cmd = svc_command(sec, &nowarn); if (nowarn) @@ -1804,7 +1850,8 @@ static void tty_translate(cfg_t *sec, struct rlimit rlimit[], char *file) addtok(line, sizeof(line), "%s", str); dbg("translated: tty %s", line); - service_register(SVC_TYPE_TTY, line, rlimit, file); + service_register(SVC_TYPE_TTY, line, rlimit, file, + is_available ? SVC_SOURCE_AVAILABLE_ONLY : SVC_SOURCE_ENABLED); } /* @@ -1969,7 +2016,52 @@ static int secref_cmp(const void *a, const void *b) return (int)sa->line - (int)sb->line; } -static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd) +static int cfg_section_has_values(cfg_t *cfg, const char *name) +{ + cfg_t *sec = cfg_getsec(cfg, name); + cfg_opt_t *opt; + + if (!sec) + return 0; + + for (opt = sec->opts; opt && opt->name; opt++) { + if (cfg_opt_size(opt)) + return 1; + } + + return 0; +} + +static int cfg_has_global_settings(cfg_t *cfg) +{ + cfg_opt_t *opt; + + for (opt = cfg->opts; opt && opt->name; opt++) { + const char *name = opt->name; + + /* Unit sections and file-local limits are valid in available/. */ + if (!strcmp(name, "service") || !strcmp(name, "task") || + !strcmp(name, "run") || !strcmp(name, "sysv") || + !strcmp(name, "tty") || !strcmp(name, "rlimit") || + !strcmp(name, "include")) + continue; + + /* Non-MULTI sections exist even when none of their keys are set. */ + if (!strcmp(name, "environment") || !strcmp(name, "env") || + !strcmp(name, "log")) { + if (cfg_section_has_values(cfg, name)) + return 1; + continue; + } + + if (cfg_opt_size(opt)) + return 1; + } + + return 0; +} + +static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd, int is_available) { static const struct { const char *name; int type; } map[] = { { "service", SVC_TYPE_SERVICE }, @@ -1987,19 +2079,41 @@ static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd) if (is_rcsd) { memcpy(rlimit, global_rlimit, sizeof(rlimit)); cgroup_current[0] = 0; + cgroup_settings_current[0] = 0; + cgroup_delegate_current = 0; arr = rlimit; } else arr = global_rlimit; - conf_parse_statics(cfg); + /* + * Load .conf files in available/ as manual-start services? Only + * honored from /etc/finit.conf, which conf_reload() parses, after + * resetting this setting, before it globs the .conf directories. + */ + if (!is_rcsd && cfg_size(cfg, "load-available")) + load_available = sec_getbool(cfg, "load-available", NULL); + + /* + * available/ is a pool of startable units, not active system + * configuration. File-scope rlimits below are retained because + * they only affect units in this file; all global settings wait + * until the file is enabled and parsed through enabled/. + */ + if (is_available) { + if (cfg_has_global_settings(cfg)) + logit(LOG_WARNING, "%s: ignoring top-level settings from available/" + " until enabled", file); + } else { + 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]; + /* top-level cgroup definitions */ + for (i = 0; i < cfg_size(cfg, "cgroup"); i++) { + cfg_t *cg = cfg_getnsec(cfg, "cgroup", i); + char buf[512]; - cgroup_add((char *)cfg_title(cg), - cgroup_settings(cg, buf, sizeof(buf), file, 0), 0); + cgroup_add((char *)cfg_title(cg), + cgroup_settings(cg, buf, sizeof(buf), file, 0), 0); + } } /* file-scope resource limits, applies to all services in file */ @@ -2033,9 +2147,9 @@ static int conf_parse_cfg(cfg_t *cfg, char *file, int is_rcsd) for (i = 0; i < num; i++) { if (refs[i].type == SVC_TYPE_TTY) - tty_translate(refs[i].sec, arr, file); + tty_translate(refs[i].sec, arr, file, is_available); else - svc_translate(refs[i].sec, refs[i].type, arr, file); + svc_translate(refs[i].sec, refs[i].type, arr, file, is_available); } free(refs); @@ -2209,6 +2323,7 @@ int conf_parse_file(char *file, int is_rcsd) char *buf = NULL; cfg_t *cfg; int rc; + int is_available = available_context > 0; /* * Template files (name@.conf, name@id.conf). A bare name@.conf @@ -2231,7 +2346,7 @@ int conf_parse_file(char *file, int is_rcsd) cfg = cfg_init(conf_opts, CFGF_NONE); if (!cfg) { - rc = legacy_parse_conf(file, buf, is_rcsd); + rc = legacy_parse_conf(file, buf, is_rcsd, is_available); goto done; } @@ -2239,7 +2354,7 @@ int conf_parse_file(char *file, int is_rcsd) 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); + rc = conf_parse_cfg(cfg, file, is_rcsd, is_available); cfg_free(cfg); goto done; } @@ -2257,14 +2372,26 @@ int conf_parse_file(char *file, int is_rcsd) } dbg("not in new format (%s), falling back to legacy parser", cfg_errmsg); - rc = legacy_parse_conf(file, buf, is_rcsd); + rc = legacy_parse_conf(file, buf, is_rcsd, is_available); done: free(buf); return rc; } -static void glob_append(glob_t *gl, int append, const char *fmt, ...) +static int conf_parse_available(char *file, int is_rcsd) +{ + int rc; + + /* A counter lets nested legacy includes inherit the source policy. */ + available_context++; + rc = conf_parse_file(file, is_rcsd); + available_context--; + + return rc; +} + +static void glob_append(glob_t *gl, int *append, const char *fmt, ...) { va_list ap; size_t len; @@ -2285,7 +2412,8 @@ static void glob_append(glob_t *gl, int append, const char *fmt, ...) va_end(ap); dbg("conf_reload(): glob %s ...", path); - glob(path, append ? GLOB_APPEND : 0, NULL, gl); + if (!glob(path, *append ? GLOB_APPEND : 0, NULL, gl)) + *append = 1; } /* @@ -2293,7 +2421,8 @@ static void glob_append(glob_t *gl, int append, const char *fmt, ...) */ int conf_reload(void) { - glob_t gl; + glob_t gl = { 0 }; + int append = 0; size_t i; /* Set time according to current time zone */ @@ -2327,13 +2456,20 @@ int conf_reload(void) /* 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); + service_register(SVC_TYPE_TTY, line, global_rlimit, NULL, + SVC_SOURCE_ENABLED); print(rc, "Entering rescue mode"); goto done; } - /* First, read /etc/finit.conf */ + /* + * First, read /etc/finit.conf. Reset load-available first: it + * is only set from finit.conf, which may be missing, or in the + * legacy format that cannot express it, and then the default + * (off) applies again. + */ + load_available = 0; conf_parse_file(finit_conf, 0); /* Set global limits */ @@ -2353,16 +2489,28 @@ int conf_reload(void) * so /etc/finit.d, always read last, is how users override any * built-in or bundled configuration. * + * With load-available enabled, .conf files in available/ are + * read as well, first of all: it is a passive staging pool, so + * any same-named file in the other directories overrides it, + * both built-in services and enabled/ symlinks back into it. + * Unit stanzas from it are registered as available-only. This source + * state is separate from manual-start and prevents automatic start. + * Top-level settings are ignored until enabled, so an available-only + * file cannot alter global state. Enabling a service overrides the + * available/ copy and restores normal source handling. + * * The .conf files (and their run/task/service stanzas) are * parsed and started in order. Each directory is sorted * alphanumerically before being appended to the overall order, * recorded in /run/finit/conf.order at bootstrap. See * doc/config/files.md for an example. */ - glob_append(&gl, 0, "%s/*.conf", FINIT_RUNPATH_); - glob_append(&gl, 1, "%s/*.conf", FINIT_SYSPATH_); - glob_append(&gl, 1, "%s/*.conf", finit_rcsd); - glob_append(&gl, 1, "%s/enabled/*.conf", finit_rcsd); + if (load_available) + glob_append(&gl, &append, "%s/available/*.conf", finit_rcsd); + glob_append(&gl, &append, "%s/*.conf", FINIT_RUNPATH_); + glob_append(&gl, &append, "%s/*.conf", FINIT_SYSPATH_); + glob_append(&gl, &append, "%s/*.conf", finit_rcsd); + glob_append(&gl, &append, "%s/enabled/*.conf", finit_rcsd); if (bootstrap) { const char *fn = _PATH_VARRUN "finit/conf.order"; @@ -2385,14 +2533,20 @@ int conf_reload(void) struct stat st; size_t j, len; - /* check for FINIT_SYSPATH_ or FINIT_RUNPATH_ overrides */ + /* + * Check for FINIT_SYSPATH_, FINIT_RUNPATH_, or when + * load-available is enabled, available/ overrides: a + * same-named file in a later directory replaces this one. + */ for (j = i + 1; j < gl.gl_pathc; j++) { if (strncmp(path, FINIT_SYSPATH_, strlen(FINIT_SYSPATH_)) && - strncmp(path, FINIT_RUNPATH_, strlen(FINIT_RUNPATH_))) + strncmp(path, FINIT_RUNPATH_, strlen(FINIT_RUNPATH_)) && + !conf_is_available(path)) continue; - if (strcmp(basenm(path), basenm(gl.gl_pathv[j]))) + if (strcmp(basenm(path), basenm(gl.gl_pathv[j])) || + !conf_is_file(gl.gl_pathv[j])) continue; - path = NULL; /* replacement later in list, skip this */ + path = NULL; /* valid replacement later in list, skip this */ break; } @@ -2424,6 +2578,8 @@ int conf_reload(void) len = strlen(path); if (len < 6 || strcmp(&path[len - 5], ".conf")) dbg("Skipping %s, not a Finit .conf file ... ", path); + else if (conf_is_available(path)) + conf_parse_available(path, 1); else conf_parse_file(path, 1); diff --git a/src/legacy.c b/src/legacy.c index 8de09469..bb94d80b 100644 --- a/src/legacy.c +++ b/src/legacy.c @@ -72,11 +72,27 @@ static void conf_parse_cgroup(char *line) cgroup_add(name, config, 0); } -static int parse_static(char *line, int is_rcsd) +static int parse_static(char *line, int is_rcsd, int is_available) { char cmd[CMD_SIZE]; char *x; + /* Includes inherit the available/ parsing context from conf.c. */ + if (MATCH_CMD(line, "include ", x)) { + char *file = strip_line(x); + + strlcpy(cmd, file, sizeof(cmd)); + if (!fexist(cmd)) { + errx(1, "Cannot find include file %s, absolute path required!", x); + return 1; + } + + return conf_parse_file(cmd, is_rcsd); + } + + if (is_available) + return 1; + if (BOOTSTRAP && (MATCH_CMD(line, "host ", x) || MATCH_CMD(line, "hostname ", x))) { if (hostname) free(hostname); hostname = strdup(strip_line(x)); @@ -130,18 +146,6 @@ static int parse_static(char *line, int is_rcsd) return 0; } - if (MATCH_CMD(line, "include ", x)) { - char *file = strip_line(x); - - strlcpy(cmd, file, sizeof(cmd)); - if (!fexist(cmd)) { - errx(1, "Cannot find include file %s, absolute path required!", x); - return 1; - } - - return conf_parse_file(cmd, is_rcsd); - } - if (MATCH_CMD(line, "log ", x)) { char *tok; static int size = 200000, count = 5; @@ -232,31 +236,39 @@ static int parse_static(char *line, int is_rcsd) return 1; } -static int parse_dynamic(char *line, struct rlimit rlimit[], char *file) +/* Register the unit with its loader-derived source; this is not syntax. */ +static void svc_register_source(int type, char *line, struct rlimit rlimit[], char *file, + int is_available) +{ + service_register(type, line, rlimit, file, + is_available ? SVC_SOURCE_AVAILABLE_ONLY : SVC_SOURCE_ENABLED); +} + +static int parse_dynamic(char *line, struct rlimit rlimit[], char *file, int is_available) { char *x; /* Monitored daemon, will be respawned on exit */ if (MATCH_CMD(line, "service ", x)) { - service_register(SVC_TYPE_SERVICE, x, rlimit, file); + svc_register_source(SVC_TYPE_SERVICE, x, rlimit, file, is_available); return 0; } /* One-shot task, will not be respawned */ if (MATCH_CMD(line, "task ", x)) { - service_register(SVC_TYPE_TASK, x, rlimit, file); + svc_register_source(SVC_TYPE_TASK, x, rlimit, file, is_available); return 0; } /* Like task but waits for completion, useful w/ [S] */ if (MATCH_CMD(line, "run ", x)) { - service_register(SVC_TYPE_RUN, x, rlimit, file); + svc_register_source(SVC_TYPE_RUN, x, rlimit, file, is_available); return 0; } /* Similar to task but is treated like a SysV init script */ if (MATCH_CMD(line, "sysv ", x)) { - service_register(SVC_TYPE_SYSV, x, rlimit, file); + svc_register_source(SVC_TYPE_SYSV, x, rlimit, file, is_available); return 0; } @@ -268,6 +280,8 @@ static int parse_dynamic(char *line, struct rlimit rlimit[], char *file) /* Read control group limits */ if (MATCH_CMD(line, "cgroup ", x)) { + if (is_available) + return 1; conf_parse_cgroup(x); return 0; } @@ -275,6 +289,9 @@ static int parse_dynamic(char *line, struct rlimit rlimit[], char *file) /* Set current cgroup for the following services/run/tasks */ if (MATCH_CMD(line, "cgroup.", x)) { char *group, *token, *saveptr; + + if (is_available) + return 1; char settings[128] = {0}; /* Reset cgroup state */ @@ -311,7 +328,7 @@ static int parse_dynamic(char *line, struct rlimit rlimit[], char *file) /* Regular or serial TTYs to run getty */ if (MATCH_CMD(line, "tty ", x)) { - service_register(SVC_TYPE_TTY, strip_line(x), rlimit, file); + svc_register_source(SVC_TYPE_TTY, strip_line(x), rlimit, file, is_available); return 0; } @@ -323,9 +340,10 @@ static int parse_dynamic(char *line, struct rlimit rlimit[], char *file) * instantiated it, i.e. for a template. Either way the text arriving * here is final, no %i is left to substitute. */ -int legacy_parse_conf(char *file, char *buf, int is_rcsd) +int legacy_parse_conf(char *file, char *buf, int is_rcsd, int is_available) { struct rlimit rlimit[RLIMIT_NLIMITS]; + int warned = 0; FILE *fp; if (buf) @@ -339,6 +357,8 @@ int legacy_parse_conf(char *file, char *buf, int is_rcsd) if (is_rcsd) { memcpy(rlimit, global_rlimit, sizeof(rlimit)); cgroup_current[0] = 0; + cgroup_settings_current[0] = 0; + cgroup_delegate_current = 0; } dbg("*** Parsing %s", file); @@ -351,12 +371,18 @@ int legacy_parse_conf(char *file, char *buf, int is_rcsd) tabstospaces(line); - if (!parse_static(line, is_rcsd)) + if (!parse_static(line, is_rcsd, is_available)) ; - else if (!parse_dynamic(line, is_rcsd ? rlimit : global_rlimit, file)) + else if (!parse_dynamic(line, is_rcsd ? rlimit : global_rlimit, file, + is_available)) ; - else + else if (!is_available) conf_set_env(line); + else if (*strip_line(line) && !warned) { + logit(LOG_WARNING, "%s: ignoring top-level settings from available/" + " until enabled", file); + warned = 1; + } free(line); } diff --git a/src/legacy.h b/src/legacy.h index f623c743..7851e712 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, char *buf, int is_rcsd); +int legacy_parse_conf (char *file, char *buf, int is_rcsd, int is_available); void legacy_parse_env (char *line); #endif /* FINIT_LEGACY_H_ */ diff --git a/src/service.c b/src/service.c index 59c47c30..a09fc39f 100644 --- a/src/service.c +++ b/src/service.c @@ -1462,7 +1462,8 @@ int service_stop(svc_t *svc) * Verify there's still something there before we send the reaper. */ if (svc->pid > 1 && !pid_alive(svc->pid)) { - svc->pid = 0; + service_cleanup(svc); + svc_set_state(svc, SVC_HALTED_STATE); return 0; } @@ -2152,6 +2153,7 @@ static void parse_cmdline_args(svc_t *svc, char *cmd, char **args) * @cfg: Configuration, complete command, with -- for description text * @rlimit: Limits for this service/task/run, may be global limits * @file: The file name service was loaded from + * @source: Whether the unit is enabled or only loaded from available/ * * This function is used to register commands to be run on different * system runlevels with optional username. The @type argument details @@ -2207,7 +2209,8 @@ static void parse_cmdline_args(svc_t *svc, char *cmd, char **args) * skipped on purpose -- conditional loading, bootstrap over, nowarn -- * also returns %NULL, with @errno zero. */ -svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file) +svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file, + svc_source_t source) { char *cmd, *desc, *runlevels = NULL, *cond = NULL; char *username = NULL, *log = NULL, *pid = NULL; @@ -2459,9 +2462,11 @@ svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file) return NULL; } - if (manual) + if (manual || source == SVC_SOURCE_AVAILABLE_ONLY) svc_stop(svc); } else { + int was_available = svc_is_available(svc); + dbg("Found existing svc for %s name %s id %s type %d", cmd, name, id, type); /* update type, may have changed from service -> task */ @@ -2470,11 +2475,24 @@ svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file) /* update path, may have changed on reload */ strlcpy(svc->cmd, cmd, sizeof(svc->cmd)); - /* e.g., if missing cmd or env before */ - if (!manual) + /* + * Source controls enablement, manual controls start policy. + * Moving into available-only must persistently block future + * starts, even while a script or restart timer is active. A + * running instance is left running, like systemctl disable. + */ + if (source == SVC_SOURCE_AVAILABLE_ONLY) { + if (!was_available && !svc_is_running(svc)) + svc_stop(svc); + } else if (!manual) { svc_unblock(svc); + } else if (!was_available && !svc->manual && !svc_is_running(svc)) { + svc_stop(svc); + } } + svc->source = source; + if (username) { char *ptr = strchr(username, ':'); @@ -2690,14 +2708,10 @@ svc_t *service_register(int type, char *cfg, struct rlimit rlimit[], char *file) /* Set configured limits */ memcpy(svc->rlimit, rlimit, sizeof(svc->rlimit)); - /* Seed with currently active group, may be empty */ + /* Seed with currently active group and settings, both may be empty */ strlcpy(svc->cgroup.name, cgroup_current, sizeof(svc->cgroup.name)); - - /* Apply cgroup settings if specified */ - if (cgroup_settings_current[0]) - strlcpy(svc->cgroup.cfg, cgroup_settings_current, sizeof(svc->cgroup.cfg)); - - /* Apply delegation flag */ + svc->cgroup.leafname[0] = 0; + strlcpy(svc->cgroup.cfg, cgroup_settings_current, sizeof(svc->cgroup.cfg)); svc->cgroup.delegate = cgroup_delegate_current; if (cgroup) @@ -3139,6 +3153,9 @@ static void service_retry(svc_t *svc) service_timeout_cancel(svc); + if (svc_is_available(svc) && svc_is_stopped(svc)) + return; + if (is_norespawn()) return; @@ -3300,14 +3317,34 @@ static void svc_set_state(svc_t *svc, svc_state_t new_state) * We get here when the service_monitor() collects the PID, sets svc->pid to 0, * and the state machine prepares for the worst bu setting state to HALTED. */ -void service_forked(svc_t *svc) +int service_forked(svc_t *svc) { /* Stop forking_retry() timer */ service_timeout_cancel(svc); + /* + * A pidfile may arrive after the service has been disabled or stopped. + * Keep the stop request intact and let the state machine terminate the + * process now recorded by the pidfile plugin. + */ + if (svc_is_stopping(svc) || svc_is_stopped(svc)) { + svc_state_t *state = (svc_state_t *)&svc->state; + + /* + * service_stop() ignores an already stopping service. Re-arm it + * internally for the newly discovered PID, without publishing a + * transient RUNNING state or clearing the stop request. + */ + *state = SVC_RUNNING_STATE; + service_stop(svc); + return 0; + } + /* Not crashing, restore RUNNING state */ svc_unblock(svc); svc_set_state(svc, SVC_RUNNING_STATE); + + return 1; } /* Set or clear service/foo/ready condition for services and call optional ready:script */ diff --git a/src/service.h b/src/service.h index efea5a85..9b6d42f6 100644 --- a/src/service.h +++ b/src/service.h @@ -43,7 +43,8 @@ struct svcdir { 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); +svc_t *service_register (int type, char *line, struct rlimit rlimit[], char *file, + svc_source_t source); void service_unregister (svc_t *svc); void service_runtask_clean (void); @@ -56,7 +57,7 @@ int service_timeout_after (svc_t *svc, int timeout, void (*cb)(svc_t *svc int service_timeout_cancel (svc_t *svc); void compose_cmdline (svc_t *svc, char *buf, size_t len); -void service_forked (svc_t *svc); +int service_forked (svc_t *svc); void service_ready (svc_t *svc, int ready); int service_stop (svc_t *svc); diff --git a/src/svc.c b/src/svc.c index a1fe9c99..e99ba357 100644 --- a/src/svc.c +++ b/src/svc.c @@ -553,6 +553,27 @@ void svc_provides_reset(void) svc->num_provides = 0; } +int svc_provides_remove(svc_t *svc, const char *cond) +{ + int i; + + if (!svc || !cond) + return 0; + + for (i = 0; i < svc->num_provides; i++) { + if (!string_compare(svc->provides[i], cond)) + continue; + + if (i + 1 < svc->num_provides) + memmove(svc->provides[i], svc->provides[i + 1], + (size_t)(svc->num_provides - i - 1) * sizeof(svc->provides[0])); + svc->num_provides--; + return 1; + } + + return 0; +} + /** * svc_find_by_jobid - Find a service object by its JOB:ID * @job: Job n:o @@ -873,6 +894,8 @@ int svc_ifthen(int is_conf, const char *ident, char *stmt, int quiet) } svc = svc_find_by_str(stmt); + if (svc_is_available(svc) && svc_is_stopped(svc)) + svc = NULL; if (not && svc) { if (!quiet) logit(LOG_NOTICE, "skipping %s, %s already loaded.", ident, svc_ident(svc, NULL, 0)); diff --git a/src/svc.h b/src/svc.h index cdefda70..d61cafee 100644 --- a/src/svc.h +++ b/src/svc.h @@ -118,6 +118,11 @@ typedef enum { * * initctl service */ +typedef enum { + SVC_SOURCE_ENABLED, + SVC_SOURCE_AVAILABLE_ONLY, +} svc_source_t; + typedef struct svc { TAILQ_ENTRY(svc) link; @@ -139,7 +144,8 @@ typedef struct svc { const svc_state_t state; /* Paused, Reloading, Restart, Running, ... */ svc_type_t type; /* Service, run, task, ... */ char protect; /* Services like dbus-daemon & udev by Finit */ - char manual; /* run/task that require `initctl start foo` */ + char manual; /* Requires an explicit `initctl start foo` */ + svc_source_t source; /* Enabled config or available-only pool */ char remain; /* run/task: stay in DONE state, run post: on stop */ char nowarn; /* Skip or log warning if cmd missing or conflicts */ const int dirty; /* 0: unmodified, 1: modified */ @@ -275,6 +281,7 @@ svc_t *svc_find_by_cond (const char *cond); svc_t *svc_cond_owner (const char *cond, svc_t *skip); const char *svc_cond_nth (svc_t *svc, int i, char *buf, size_t len); void svc_provides_reset (void); +int svc_provides_remove (svc_t *svc, const char *cond); svc_t *svc_find_by_jobid (int job, char *id); svc_t *svc_find_by_tty (char *dev); svc_t *svc_find_by_pidfile (char *fn); @@ -305,6 +312,7 @@ int svc_ifthen (int is_conf, const char *ident, char *stmt, int svc_parse_jobstr (char *str, size_t len, void *user_data, int (*found)(svc_t *, void *), int (not_found)(char *, char *, void *)); static inline int svc_is_daemon (svc_t *svc) { return svc && (SVC_TYPE_DAEMON & svc->type); } +static inline int svc_is_available (svc_t *svc) { return svc && svc->source == SVC_SOURCE_AVAILABLE_ONLY; } static inline int svc_is_sysv (svc_t *svc) { return svc && SVC_TYPE_SYSV == svc->type; } static inline int svc_is_tty (svc_t *svc) { return svc && SVC_TYPE_TTY == svc->type; } static inline int svc_is_runtask (svc_t *svc) { return svc && (SVC_TYPE_RUNTASK & svc->type);} diff --git a/test/Makefile.am b/test/Makefile.am index b943e1ef..6a767975 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -30,6 +30,7 @@ EXTRA_DIST = skel/bin/busybox-x86_64.sha256 skel/sbin/service.sh skel/etc/env EXTRA_DIST += setup-sysroot.sh EXTRA_DIST += add-remove-dynamic-service.sh EXTRA_DIST += add-remove-dynamic-service-sub-config.sh +EXTRA_DIST += available-load.sh EXTRA_DIST += bootstrap-crash.sh EXTRA_DIST += cond-start-task.sh EXTRA_DIST += conf-format.sh @@ -97,6 +98,7 @@ TEST_EXTENSIONS = .sh TESTS = checkself.sh TESTS += add-remove-dynamic-service.sh TESTS += add-remove-dynamic-service-sub-config.sh +TESTS += available-load.sh TESTS += bootstrap-crash.sh TESTS += cond-start-task.sh TESTS += conf-format.sh diff --git a/test/available-load.sh b/test/available-load.sh new file mode 100755 index 00000000..2ed7a9cf --- /dev/null +++ b/test/available-load.sh @@ -0,0 +1,449 @@ +#!/bin/sh +# Verify the load-available setting: units in /etc/finit.d/available/ are +# registered with available-only source state and controllable with initctl, +# but not started automatically. Enabling a service (symlink in enabled/, +# read last) makes it follow its independent manual-start policy; disabling +# it again keeps it loaded and startable, and must not re-run a completed +# task. Files in the top-level +# finit.d directory are unaffected, and override available/ by name. +set -eu + +TEST_DIR=$(dirname "$0") + +SYSROOT="${SYSROOT:-$(pwd)/$TEST_DIR/sysroot}" +FINITD="$SYSROOT/etc/finit.d" + +test_teardown() +{ + say "Removing test files from sysroot." + rm -f "$FINITD/available/svcblk.conf" \ + "$FINITD/available/svcleg.conf" \ + "$FINITD/available/svcauto.conf" \ + "$FINITD/available/svcmanual.conf" \ + "$FINITD/available/svctask.conf" \ + "$FINITD/available/svcrun.conf" \ + "$FINITD/available/svcsysv.conf" \ + "$FINITD/available/svcwait.conf" \ + "$FINITD/available/svctty.conf" \ + "$FINITD/available/svcinclude.conf" \ + "$FINITD/available/global.conf" \ + "$FINITD/available/dup.conf" \ + "$FINITD/available/cond.conf" \ + "$FINITD/available/provider.conf" \ + "$FINITD/available/dangling.conf" \ + "$FINITD/available/svclate.conf" \ + "$FINITD/dup.conf" \ + "$FINITD/cond.conf" \ + "$FINITD/provider.conf" \ + "$FINITD/svctop.conf" \ + "$FINITD/avenvprobe.conf" \ + "$FINITD/enabled/svcblk.conf" \ + "$FINITD/enabled/svcauto.conf" \ + "$FINITD/enabled/svcmanual.conf" \ + "$FINITD/enabled/svctask.conf" \ + "$FINITD/enabled/dangling.conf" \ + "$FINITD/enabled/svclate.conf" \ + "$SYSROOT/etc/available-include.conf" \ + "$SYSROOT/etc/load-available.conf" \ + "$SYSROOT/sbin/avtask.sh" \ + "$SYSROOT/sbin/avenv.sh" \ + "$SYSROOT/sbin/avlate.sh" \ + "$SYSROOT/var/avtask.log" \ + "$SYSROOT/var/avenv.log" \ + "$SYSROOT/run/svclate.pid" +} + +# shellcheck disable=SC2034 +BOOTSTRAP="load-available = true" + +mkdir -p "$FINITD/available" "$FINITD/enabled" + +# Counts each execution, run as a task below +cat > "$SYSROOT/sbin/avtask.sh" <<'EOF' +#!/bin/sh +echo run >> /var/avtask.log +EOF +chmod +x "$SYSROOT/sbin/avtask.sh" + +# A top-level setting in available/ must not affect the system until +# that file is enabled. A regular task records what environment it saw. +cat > "$SYSROOT/sbin/avenv.sh" <<'EOF' +#!/bin/sh +echo "${AVAIL_GLOBAL-unset}" > /var/avenv.log +EOF +chmod +x "$SYSROOT/sbin/avenv.sh" + +cat > "$FINITD/available/global.conf" < "$FINITD/avenvprobe.conf" < "$FINITD/available/svcblk.conf" < "$FINITD/available/svcleg.conf" < "$FINITD/available/svcauto.conf" < "$FINITD/available/svcmanual.conf" < "$FINITD/available/svctask.conf" < "$FINITD/available/svcrun.conf" < "$FINITD/available/svcsysv.conf" < "$FINITD/available/svcwait.conf" < "$FINITD/available/svctty.conf" < "$SYSROOT/etc/available-include.conf" < "$FINITD/available/svcinclude.conf" < "$FINITD/available/dup.conf" < "$FINITD/dup.conf" < "$FINITD/available/dangling.conf" < "$SYSROOT/sbin/avlate.sh" <<'EOF' +#!/bin/sh +/bin/sh -c 'sleep 3; echo $$ > /run/svclate.pid; sleep 30' & +EOF +chmod +x "$SYSROOT/sbin/avlate.sh" +cat > "$FINITD/available/svclate.conf" < "$FINITD/available/cond.conf" < "$FINITD/cond.conf" < "$FINITD/available/provider.conf" < "$FINITD/provider.conf" < "$FINITD/svctop.conf" < $runs2)" "$runs1" -eq "$runs2" + +say 'A legacy format finit.conf falls back to the default (off)' +# A legacy one-liner: fails block-format probe, goes to the legacy +# parser, which cannot express load-available => setting must reset +run "echo 'task [2345] name:legacynoop /bin/true -- Legacy format probe' > $FINIT_CONF" +run "initctl reload" +retry 'assert_loaded svcleg 0' +if run "initctl start svcleg" 2>/dev/null; then + fail "svcleg startable after finit.conf turned legacy format" +fi +say 'available/ no longer loaded, as expected' + +say 'A legacy finit.conf can enable it through a block-format include' +run "echo 'load-available = true' > /etc/load-available.conf" +run "echo 'include /etc/load-available.conf' > $FINIT_CONF" +run "echo 'task [2345] name:legacynoop /bin/true -- Legacy format probe' >> $FINIT_CONF" +run "initctl reload" +retry 'assert_loaded svcleg 1' +run "initctl start svcleg" +retry 'assert_status svcleg running' + +say 'load-available = false unloads available-only services' +run "echo 'load-available = false' > $FINIT_CONF" +run "initctl reload" +retry 'assert_loaded svcleg 0' +if run "initctl start svcleg" 2>/dev/null; then + fail "svcleg startable with load-available = false" +fi +say 'initctl start refused, as expected' + +say 'An empty available/ directory does not break later config globs' +run "rm -f $FINIT_RCSD/available/*.conf" +run "echo 'load-available = true' > $FINIT_CONF" +run "initctl reload" +retry 'assert_loaded svcauto 0' +retry 'assert_loaded svctop 1' +retry 'assert_status svctop running'