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'