New file format - #493
Conversation
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Every test left a stray `sleep 300` behind, reparented to PID 1, where it lingered for up to five minutes after the test had finished. wdstart() runs the watchdog in a subshell, so $! is the pid of the subshell, not of the sleep it forks. wdkill() killed the subshell and orphaned the sleep. Kill the child first, killing the subshell puts the sleep beyond the reach of pkill -P. Neither kill is sure to match, and wdkill() runs from the EXIT trap under set -e, so both must tolerate failure. Also return early when wdpid is unset, for failures before wdstart() runs. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The one-liner parser is about to be joined by a second, block-based format. Give it a name that says which of the two it implements, before any content changes make the diff hard to follow. No functional change. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The one-liner format has grown crowded and very wide, and every new
service option makes it worse.
Add a second, block-based format, parsed with libconfuse:
service sshd {
description = "OpenSSH daemon"
runlevel = "2345"
command = "/usr/sbin/sshd -D $SSHD_OPTS"
}
Both formats keep the .conf extension and are detected per file by
content. Try-parse strictly with libconfuse; on a parse error,
re-parse leniently to tell a block file with a typo from a one-liner
file. Only a one-liner file reaches the legacy parser, a typo is
reported with its file and line.
Each block is translated to the canonical one-liner and registered
through the existing entry points, so the two formats cannot drift.
The one-liner parser is frozen at the 4.x feature set, new options
land only in the block schema. libconfuse 3.3 or later is required,
CFGF_KEYSTRVAL does not exist before it.
Covers service, task, run, sysv and tty blocks, the static directives,
and the cgroup, rlimit, set and log blocks. Templating and the
documentation rewrite are still to come.
The regression test covers translation of a service block to the
one-liner, a block-format /etc/finit.conf booting with set {} applied
at bootstrap, both formats side by side, and rejection of a typo at
block and at root level.
A rejected file must not fall through to the legacy parser, which
registers a bogus unstartable service per line. assert_num_children
cannot see that, the bogus service has no children either, so the
check is assert_num_services.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Relocate process-wide global variables from legacy parser that ended up there because it used to be conf.c, but which is now now frozen at the 4.x feature set. Each variable is moved to their respective "owner". Give cgroup_current[] and cgroup_settings_current[] named bounds. Their extern declarations were unsized, so sizeof() on them stopped compiling once the definitions moved to another translation unit. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
- legacy.[ch]: strictly legacy .conf parser boilerplate only - conf.[ch]: .conf parser and generic configuration functionality Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
A template in the block format registered garbage. conf_parse_file()
routed every file with an '@' in its name straight to the legacy
parser, which read the block line by line: the section header became a
service whose command was the section title, and each key = value line
below it became an environment variable.
service serv:%i { ... } -> service 'serv:eth0' with argument '{'
Substitute %i over the whole file before parsing instead, so format
detection and both parsers see finished text. A bare name@.conf is
still skipped, it is the template rather than an instance of one.
The legacy parser no longer opens the file or substitutes per line, it
is handed the instantiated buffer, so the template convention now has
one implementation instead of two. conf_is_template() applies
basenm(), a directory with an '@' in its name is not a template.
libconfuse cannot name a buffer it parses before 3.4, so a typo in a
template would be reported against "[buf]". Parse through fmemopen()
with the file name preset until the floor moves.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Three private ones had grown: fnread() in util.c, flen() behind pid_cmdline()/pid_cgroup() in cgutil.c, and conf_read_template() in conf.c. Two of them were also wrong in ways the others were not. fnread() formatted the path into a char[256] and stat()ed it before opening, so a longer path was silently truncated and then read from whichever file the truncation happened to name, and the size could change between the look and the read. flen() existed because neither of those approaches works on procfs at all, where stat() reports zero and the only way to learn the size is to read to EOF. Add fslurp() to util.[ch], which every tool already links. It opens first and sizes the fd it holds, treats st_size as a hint, and reads until EOF, so procfs and regular files take the same path. Paths are formatted by libite's vfopenf(), which allocates to fit. Callers that need the byte count, /proc/PID/cmdline embeds NUL, ask for it. fnread() keeps its signature and becomes a bounded copy out of the result, so its one caller is unaffected. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
A stop: or reload: script written with a timeout killed Finit at
config load:
service stop:5,/bin/true service.sh -- Boom
parse_script() takes the timeout as a pointer and the caller decides
whether it wants one. However, both stop: and reload: scripts so far
have no timeout, i.e., NULL. Guard the branch that reads a leading
number.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Both were bounded by killdelay, the delay between the stop signal and SIGKILL, because service_run_script() had nothing else to reach for. That conflates two things: how long the daemon may take to die, and how long its stop script may run. Give each hook a timeout of its own, defaulting to killdelay when unset, so the existing behaviour is what you get until you ask for something else. parse_script() already falls back that way for the hooks that had one. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
'make check' refreshes the sysroot through the setup-chroot rule, but running a test script by hand does not, so the test exercises whichever finit was installed last and reports on code that is no longer there. Both a passing and a failing run are then meaningless, and nothing says so. Compare the built binary against the installed one at startup and fail with the command that fixes it. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The initial implementation was done using a naive translator of the
legacy one-liner format key by key, so it inherited encodings that the
block format exists to remove: a timeout packed into a script path, a
small comma-and-colon language inside the log string, sigils standing in
for booleans, and a log key (services) carrying three (!) types.
Settled naming against systemd, OpenRC, FreeBSD rc.subr, s6 and SMF.
Match systemd's semantics, not its naming.
pid -> pidfile, plus pidfile-create for the rare case
where Finit writes the file rather than the daemon
environment -> envfile, since it names a file to source, and the
top-level environment {} block sets variables
pre/post/... -> exec-start-pre, exec-start-ready, exec-stop,
exec-stop-post, exec-reload, exec-cleanup, each
with its own -timeout instead of "SEC,script"
halt, kill -> stop-signal, stop-timeout
restart -> restart for the policy, restart-max for the count
log -> a block with file, priority and identity, where
/dev/null and /dev/console are spelled as paths
group -> group and extra-groups, no longer positional
nowarn -> a leading - on command, as on envfile
List-valued keys take plural names. Aliases are desc, cond, mod,
caps, env, halt and kill; an alias may abbreviate the canonical name
or preserve a legacy spelling, nothing else.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
A condition list could be led by '!', which is not a condition and not
a negation. It is a flag on the block, and it means two unrelated
things depending on which block it sits in: a service or sysv does not
handle SIGHUP and must be restarted to reload, while a run or task
must not hold up bootstrap. Writing '<!>' with no condition at all is
legal, which gives away that it was never an operator.
Give each meaning its own key, valid only where it applies:
service foo { reload-signal = "none" } # restart to reload
task bar { required = false } # do not hold up bootstrap
Using either on a block type it does not apply to warns, as does a '!'
left in a conditions list. Both still translate to that same '!',
which is all a legacy line can carry, so reload-signal takes SIGHUP or
none for now; str2sig() already accepts any case and an optional SIG
prefix.
This also clears the way for the conditions list to grow real
operators, '+' and '-' for asserted and deasserted, without '!'
sitting among them meaning something else entirely.
The '~' prefix stays. It belongs to the list: it marks a dependency
whose reload should propagate here.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Both keys prompt the question they should be answering.
'remain' decides whether a finished run or task keeps existing: without
it the entry is pruned, so the work re-runs on every runlevel entry,
initctl cannot see it, and its post script never fires. With it the
entry stays, is not re-run, and gets a teardown when stopped or when it
leaves its runlevels. That is systemd's RemainAfterExit, and 'remain'
is that name with the informative half cut off.
'manual' says how a service is started but not that it is about
starting at all.
remain -> remain-after-exit
manual -> manual-start
Both keep their old spelling as an alias, which they qualify for twice
over, as abbreviations of the canonical name and as the legacy
spellings.
While here, give sec_getbool() the alias argument its string and list
counterparts already take.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This is what 'initctl create' and 'initctl edit -c' put in front of a user writing their first .conf file, so it is also the whole of the "initctl emits the new format" work: neither command generates syntax, they copy this file and open an editor on it. The ASCII diagram naming eight positional fields goes with it. A block has no positions to explain. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
parse_cgroup() takes two arguments that are not cgroupfs files: the
leaf directory to place the service in, and whether to hand the subtree
over to it. The block format could express neither. 'name' happened
to work, because a free-form key is emitted as name:VALUE and that is
what the parser looks for, but 'delegate' came out as delegate:true and
was filed as a cgroup setting, so it silently did nothing.
Declare both, and emit delegate as the bare flag the parser expects.
Neither means anything on a top-level group definition, so say so there
rather than emitting something that would be written to cgroupfs.
service podman {
cgroup containers { name = "podman" delegate = true }
...
}
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The syntax overview no longer describes a line-based format, since that is not what the rest of the documentation shows. It now covers the grammar, the two naming conventions, the nine aliases, and the leading '-' on a path, and it says plainly that both formats are still read and told apart per file by content. Without that, a reader with an existing configuration is left wondering what happened to it. service-opts.md was a list of modifiers to place between a directive and its command, so it needed rewriting rather than translating: there are no positions left to describe. It is now grouped by what the settings do. conditions.md needed correcting. It presented '!' as a condition prefix alongside '~'. It is neither a condition nor a negation, it is a flag on the block that means one thing on a service and another on a run or task, so it is spelled reload-signal and required here, and the page maps the old form to both. Two things the pages claimed are not true. The kill delay range is 1-300, not 1-60, and stop and reload scripts are no longer run without a timeout. ChangeLog.md keeps its line-based examples. Those sit in historical release entries, and rewriting them in a syntax that did not exist at the time would misdate the format. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The examples people copy from were still written in the line-based
format, so the block format was documented but nowhere demonstrated.
Two names in contrib were accidents of the old format, where the
service name falls out of the command basename: the Alpine and Void
keymap task was called zcat, and Debian's console/keyboard setup tasks
carried a .sh suffix. They now carry the name their file implies.
Nothing referenced the old names.
The mdevd coldplug path keeps the name it has always had. Its legacy
line spelled the name inside the cgroup argument, where it names the
cgroup leaf and not the service, so the barrier condition really is
<run/mdevd-coldplug/success> and not the <run/coldplug/success> the
comment above it promises. Converted as-is so boot ordering does not
change; the discrepancy is now written down where it happens.
A list may not contain comments, the lexer sees the entries after the
'#' regardless:
modules = {
# "fbcon",
"softdog"
}
so the commented-out module candidates sit above the list instead.
setup-sysroot.sh removes 10-hotplug.conf from the test sysroot, so that
file is covered by parsing only, not by make check.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The block conversion changed the bodies of the reference sections but left every "**Syntax:**" header spelling the line-based format, so each page opened by teaching the format it then stopped using. Six files were missed entirely: runparts, files, capabilities, requirements, runlevels, and switchroot. runparts had no block spelling written down anywhere, though the parser has read `runparts`, `runparts-progress`, and `runparts-sysv` all along. tty gains a table per variant. Its three syntax lines carried nine positional fields between them, which no longer describes anything the parser accepts. Fixes #148 Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
An ambient capability only reaches the effective set when euid is
non-zero, so a service that pairs `capabilities = { "^cap_..." }` with a
root user gets none of the restriction it asks for, and keeps the full
root set instead. Finit read the list, applied it, and said nothing. A
build without libcap dropped the list on the floor just as quietly.
Both now warn, naming the service:
nginx: ambient capabilities ('^') have no effect as root, use a
non-root user, or '%' and '!' entries
The ambient entries are read back from the parsed IAB value rather than
matched in the text, so inheritable ('%') and bounding ('!') entries stay
silent -- those work fine as root.
The warning repeats when the .conf files are re-read on runlevel change,
as parse warnings here already do.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Reference sections kept pointing at the line-based format they no longer
document. `sysv` and `task` sent the reader to Services for "<COND>",
the cgroups chapter opened by listing three legacy directives and then
explained further down that only two of them exist here, and the logging
chapter still gave "log:prio:facility.level,tag:ident" as the full
syntax.
Some claims were wrong independent of the format:
- a sysv is a supervised daemon, grouped with service in
SVC_TYPE_DAEMON, not a variation on task
- restart-max has no upper bound of 255, or any other
- the built-in rescue fallback runs in 12345789, not 12345
- conditional loading quotes system/10-hotplug.conf, not
system/hotplug.conf
- the key spells conflicts, not conflict
- the built-in getty no longer wants TERM last, it is a key
`if` takes either a service name or, in angle brackets, a condition,
decided in svc_ifthen(). Only the examples showed this, so it is now
said.
Terminology follows the split index.md already draws: a block is the new
format, a stanza the line-based one.
src/rescue.conf was still line-based, missed because it sits in src/
rather than system/ or contrib/.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The block format spells conditions as bare strings everywhere else, so
requiring `if = "<usr/foo>"` left one sigil behind, carried over from
the line-based `if:` token. A namespace separator already tells the two
apart: a value with a '/' is a condition, anything else is a service
name.
svc_ifthen() picks its mode from the start of the statement and applies
it to the whole, so a statement naming both kinds cannot be evaluated.
That is now an error, as are the old angle brackets, and either one
skips the block:
/etc/finit.conf: mixed: if: cannot mix a service name with a
condition in 'anchor,usr/enable-me', a statement must be all of
one kind, skipping
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Settings that exist only in the block format have nowhere to go: the legacy line cannot carry them, and service_register() returned an errno that no caller ever read, so conf.c had no handle on the service it just created. Return the svc instead, NULL with errno set on failure, errno zero when a block is skipped on purpose. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
rmrf() is needed outside tmpfiles.c. The move also deduplicates the nftw callback: the contents-only removal used by tmpfiles 'D' entries is now rmcontents(), sharing the callback with rmrf(). mksubsys() did nothing at all when the user could not be resolved, no directory and no message, and callers had no way to tell. Now the directory is always created, ownership is best effort, and an unknown user is warned about. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
A service that drops privileges cannot create its own PID file in /run, root owns it. Finit can create the file with pidfile-create, but the daemon still cannot touch it to confirm a SIGHUP. Five new settings, block format only: runtime-dir, state-dir, cache-dir, logs-dir, and config-dir. The value is a directory name, resolved under /run, /var/lib, /var/cache, /var/log, and /etc, respectively. The directory is created before the service starts, mode 0755 owned by user/group, and the full path is exported to the process as RUNTIME_DIRECTORY, STATE_DIRECTORY, CACHE_DIRECTORY, LOGS_DIRECTORY, and CONFIGURATION_DIRECTORY. Mode and ownership are asserted at creation only, a daemon may tighten them afterwards. The runtime directory is removed when the unit stops, after any exec-stop-post script, like systemd with RuntimeDirectoryPreserve=no. A completed run/task counts as stopped unless remain-after-exit keeps it up. The other four persist across restarts. These are the first settings with no legacy token: they are validated by service_set_dir() and stored on the svc that service_register() now returns. systemd accepts a list of directories per setting; this is a single name for now, widening later is compatible since libconfuse accepts a bare value for a list option. The test sysroot gains libnss_files.so.2, which ldd cannot see, glibc dlopen()s it. Without it getpwnam() fails inside the chroot, so user/group settings never resolved and directory ownership could not be tested. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
Aaron Andersen points out in the #492 discussion that the *Directory settings carry more contract than create-and-chown: per-directory modes, specific ownership rules, and cleanup toggles. Without them config-dir was chowned to the service user, which systemd never does, an existing directory with drifted ownership was left wrong, and the runtime directory could not survive a restart. Now matching systemd.exec(5), and where the man page is vague, the code in setup_exec_directory(): - each directory takes a matching -mode key, octal with the leading zero, default 0755. The mode of the named directory is locked down again on every start, also when it already exists - config-dir is created but never chowned - the contents of an existing directory are left alone as long as the owner is right; on drift everything under it is chowned back - runtime-dir-preserve = no | restart | yes maps RuntimeDirectoryPreserve=. A service still qualified to run when the runtime directory would be removed is restarting, not stopping, which is what svc_enabled() answers The dir mechanics move to mksubsysd(), taking resolved ids, with mksubsys() reduced to a name-resolving wrapper for the dbus plugin. The child resolves uid/gid once for both directory setup and privilege drop. The symlink form, RuntimeDirectory=foo:bar, is not adopted. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The workflows install libuev and libite from source and everything
else from apt, but never libconfuse, so every build job on this
branch dies in configure:
checking for libconfuse >= 3.3... no
Ubuntu ships libconfuse 3.3 with the static library included, which
covers both the static and regular builds. Staying on 3.3 in CI is
deliberate: it exercises the fallback paths marked
"XXX: Workaround for libConfuse <3.4" that a from-source 3.4 would
leave untested.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
The line-based format accepts `service :80 ...`, deriving the name from the command basename. The block format has no counterpart, the title carries both name and ID. Implied by the format description, but anyone converting such a line deserves to find it written down. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
@aanderse if you could spare a minute or so, skimming through the PR description at least, that would be very appreciated! 🙇♂️ |
aanderse
left a comment
There was a problem hiding this comment.
this is fantastic! i am really happy with what you did here 😁 and all these wonderful new options! somehow you keep adding so much great stuff while really hitting that perfect balance with simplicity 🤩
this format matches what i had with finix already so porting over wasn't that hard. happy to report my laptop is running fine on this branch as of tonight 🚀
PR description is fantastic and all i needed to migrate everything i had 👍
really looking forward to this being merged!
Thank you for those kind words, I really appreciate that! 😊
Wow, that was quick! 🤯
I'll put together a migration guide later on, thank you for the idea!
Going in now! |
New Finit .conf format
Reference for the libconfuse-based format in
src/conf.c, writtenfrom the schema and the actual translators.
Every setting is listed with what it does and a snippet you can paste
into a
.conffile.How a file is chosen and parsed
Both formats use
.conf. There is no marker line and no new extension;conf_parse_file()decides per file by trying to parse it:succeeds the file is block format with a typo, and the strict error
is reported with file and line. The file is not handed to the legacy
parser.
one-liner parser.
One format per file. Mixing an
environment { }block and a legacyrun [S] ...line in the same file makes the whole file fail bothparses, and it lands in the legacy parser, where the block lines are
junk.
Files are read from
/etc/finit.conffirst, then*.confin/lib/finit/system/,/run/finit/system/,/etc/finit.d/and/etc/finit.d/enabled/, in that order./etc/finit.d/available/isnever read; only what is enabled.
Grammar
Native libconfuse. Values are quoted strings, bare tokens, or integers;
lists use braces; sections may carry a title.
Bare tokens work where a string is expected, so
memory.max = 65Mandrestart = alwaysboth parse as strings.Two conventions
Keys are kebab-case, never snake_case or CamelCase:
restart-sec,stop-timeout,reboot-watchdog.List-valued keys are plural:
conditions,conflicts,capabilities,modules,extra-groups. Two imperatives keep their singular formbecause they are verbs rather than nouns:
mknodandinclude.Aliases
Nine, no more. An alias may abbreviate the canonical name, or preserve
a legacy spelling; it may not simply rename.
descriptiondescconditionscondcapabilitiescapsmodulesmodenvfileenvmanual-startmanualremain-after-exitremainstop-signalhaltstop-timeoutkillThe
-prefixA leading
-on a path means "carry on if it is not there", the samemark systemd uses on
EnvironmentFile=andExecStart=:It is the only sigil that means "optional". Two others exist and mean
something else:
!onrunlevelinverts the set, and~on acondition propagates a reload. Both are covered where they apply.
service, task, run, sysv
Four block types share one schema, so every key below parses in all
four. The schema does not enforce which keys are meaningful where:
remainonly does something forrunandtask,notifyismeaningless for
sysv, andexec-start-readyis a no-op inrunandtaskbecause readiness only applies to daemons. None are rejected.The section title is the identity, optionally
name:id:commandis the only required key. A block without it is skipped withan error. Blocks run in the order they appear in the file, recovered by
sorting on each section's source line.
Identity and selection
descriptiondescinitctl status, and during bootcommand-tolerates a missing binaryrunlevel"2345","S12345"; a leading!inverts the setconditionscondifconflictsrunlevel = "!12345"means every runlevel except those, the setstarts full and the listed levels are cleared. It is a set of
characters, not a range, so
Sand the digits, and!1-9does notwork.
Condition prefixes
~before a condition propagates a reload of the upstream service tothis one, instead of merely pausing and resuming it. Any condition in
the list may carry it. Note that it sets one flag for the whole block,
so writing it on a single condition still affects all of them.
There is no way to express "run when this condition is not
asserted". A
+and-pair for asserted and deasserted conditionshas been discussed, and the list is kept clear of anything that is not
a condition so those operators have room.
Porting note: a legacy condition list may be led by
!, which is nota condition and not a negation. It is a flag on the block, and it maps
to two different keys here depending on the block it was in.
<!...>on a service or sysvreload-signal = "none"<!...>on a run or taskrequired = falseA
!left in aconditionslist warns and is ignored, as does eitherkey on a block type it does not apply to.
Process environment
envfileenv-prefix makes it optionalusergroupextra-groupscapabilitiescapsttyuserwithoutgroupruns in the user's own group.groupwithoutuserimpliesroot. Note thatenvfilenames a file to source;inline variables are the top-level
environment {}block.Supervision and readiness
type"forking"is understood; anything else warns and is ignorednotifynone,pid,systemd,s6pidfiletruefor the derived default, a bare name, or a pathpidfile-createmanual-startmanualinitctl startonlyremain-after-exitremainrequiredfalsemeans it does not hold up bootstraprespawnThe daemon writes its own pidfile unless told otherwise, so the three
forms of
pidfilename a path and nothing more:pidfile-create = trueis the exception, for a foreground daemon thatwrites no pidfile of its own. It is the fragile path: Finit then
creates the file on start and removes it on stop. Naming the file and
choosing who writes it are separate decisions, so changing the path
never changes the owner.
Restart behaviour
restarttrue,false,alwaysorneverrestart-maxrestart-seconcrashrebootorscriptreload-signalnoneto restart it insteadstop-signalhaltSIGTERMstop-timeoutkillruntime-dir/run, created at start, owned byuser, removed on stopstate-dir/var/lib, persistscache-dir/var/cache, persistslogs-dir/var/log, persistsconfig-dir/etc, persistsFinit reloads a daemon by sending it
reload-signal, SIGHUP bydefault, unless
exec-reloadis set, which takes precedence. Withreload-signal = "none"there is no signal to send and the service isrestarted instead. Only
SIGHUPandnoneare accepted for now, inany case and with or without the
SIGprefix, because the legacy linethis translates into can carry nothing else.
remain-after-exitdecides whether a finished run or task keepsexisting. By default it does not: the entry is removed, so it re-runs
whenever its runlevel is entered again,
initctlcannot see it, andexec-stop-postnever fires. With the key set it stays in the list ina done state, is not re-run on runlevel re-entry, and gets a real
teardown when stopped or when it leaves its runlevels.
This is systemd's
RemainAfterExit=, and the two agree on the default.Finit is the unusual one in having a vanishing model at all: SMF's
duration = transient, s6-rc oneshots and OpenRC services all stayaround once run, so no other init system needs the option.
It is ignored, with a warning, for a task whose only runlevel is
S.Bootstrap tasks are pruned as soon as they finish, so there is nothing
left to remain, and their
exec-stop-postnever runs.required = falseneeds a word on what it does not do. Finit hastwo separate notions of blocking: a
runblocks the next stanza,which is what makes it a
runrather than atask, and separatelythe bootstrap barrier waits for every run and task to finish before
leaving runlevel S.
requiredspeaks only to the second. Arunwithrequired = falsestill runs in sequence; it simply stops holding upbootstrap. Run/tasks conditioned on
hook/svc/uporhook/system/upare exempt from that barrier already and need no key.
restartis the policy andrestart-maxthe count.trueis thedefault policy, restart up to
restart-max;falseandnevermeanno restarts;
alwaysis unlimited and ignoresrestart-max. Anunrecognised word warns and is treated as
true.respawn is not part of that family
respawntakes a different path through the state machine, and thedifference is not "forever" but why the process exited. For a
service, exiting is a failure: Finit counts it, waits
restart-sec,and gives up at
restart-max. For a respawn service, exiting isnormal work, so it restarts at once with no counter and no give-up.
That is the getty case, where a login ending is the expected outcome
and the next getty should appear immediately.
The two are not interchangeable.
restart = "always"still counts andstill waits, and
restart-sec = 0does not mean "immediately", itmeans the retry timer is never armed, so the service would not come
back at all.
systemd has no equivalent key. A getty there is
Restart=alwayswithRestartSec=0andStartLimitIntervalSec=0to switch the ratelimiter off, which is the same behaviour assembled from three general
settings rather than named as one.
Today a respawn service ignores
restart-maxandoncrashentirely,even when it exits non-zero every time, so a permanently broken getty
retries forever and never triggers
oncrash. The intended directionis for respawn to honour the restart settings on a failing exit
while keeping the immediate restart on a clean one: normal exits stay
normal, failures become failures. That split is also where
on-failureand smarter backoff would fit.Lifecycle scripts
Six hooks, each with a
-timeoutof its own, in seconds, 0-3600.Without one the service's
stop-timeoutis used.exec-start-preexec-start-readyexec-stopexec-stop-postexec-reloadexec-cleanupThe script must exist and be executable or it is skipped with a
warning. Mapped onto systemd,
exec-start-preisExecStartPre=,exec-start-readyisExecStartPost=, andexec-stop-postisExecStopPost=;exec-cleanuphas no counterpart.Service output
log {}says where the service's own output goes./dev/nulland/dev/consoleare spelled as paths, so there is one way to say it.file/dev/nullto discard, or/dev/consoleprioritydaemon.infoidentityThe empty block is how a service asks for syslog with defaults.
Careful when copying these: at file scope
log {}is the rotationblock below, which has no
file. Thereis no
log = true: libconfuse rejects a name declared as both a scalarand a section.
Per-service cgroup and rlimit
Both are sections, covered below.
tty
Three shapes, distinguished by which key is present. A
ttyblock withnone of
device,command,nottyorrescueis skipped with anerror.
device@consolebaudtermdeviceform onlycommand-tolerates a missing binarynottyrescuerunlevelconditionscondnoclearnowaitnologinloginBuilt-in getty, selected by
device:External getty, selected by
command:Board bring-up and rescue:
termandbaudapply only to thedeviceform and are dropped foran external getty. The booleans stay negative, matching agetty and
mingetty, which spell
--noclearexactly this way and carry a wholefamily of
noflags between them. Two are worth knowing precisely:nowaitskips the "press Enter to activate" prompt, which Finit showsby default where the gettys do not; and
nologinruns a shell directlyrather than
login, so it is stronger than agetty's--skip-login.cgroup
Valid at top level, where it defines a group, and inside a service
block, where it joins one.
Common cgroup v2 keys are declared in the schema, so they are typed and
warning-free. Anything else is passed to the kernel verbatim, which is
what the legacy parser always did; the kernel rejects bad writes at
runtime. Consequence: a typo in a common key degrades to passthrough
rather than being caught.
Declared keys:
cpu.weight,cpu.weight.nice,cpu.max,cpu.max.burst,cpu.idle,cpuset.cpus,cpuset.mems,memory.{min,low,high,max},memory.swap.{high,max},memory.oom.group,memory.zswap.max,io.{weight,max,latency},pids.max.The keys are the cgroupfs filenames, so the config reads the same as
the kernel documentation. Dots are literal characters, not nesting.
A service joins exactly one group. If a block declares more than one
cgroupsection the last wins and a warning names the one used.The legacy
cgroup.NAMEcontext-switch line, meaning "every followingstanza joins NAME", has no block equivalent. Each block says which
group it joins.
rlimit
Valid at top level, where it applies to every block in the file, and
inside a service block, where it applies to that block only, layered on
the file-scope values.
Unlike cgroup keys the resource set is closed, it is the kernel ABI, so
the schema declares all of them and a typo is a hard parse error. Each
resource can be written bare, or with a
soft.orhard.prefix. Baresets both.
Resources:
as,core,cpu,data,fsize,locks,memlock,msgqueue,nice,nofile,nproc,rss,rtprio,rttime,sigpending,stack.Values are a number, or
unlimited(infinityalso works).Templates
A file named
name@.confis a template. It is never started on itsown; enabling an instance creates
name@INSTANCE.confpointing at it,and every
%iis replaced with INSTANCE before the file is parsed.Substitution happens over the whole file, so
%iworks in the sectiontitle, in any value, and in the command line alike.
The instance name becomes the service
:ID, so it is bounded byMAX_ID_LEN. A barename@.confreaching the parser registersnothing. Diagnostics are reported against the instance file, so a typo
in the template above appears as
/etc/finit.d/enabled/avahi-autoipd@eth0.conf:2: ....environment
Global environment variables, free-form keys.
envis an acceptedalias, matching the
envfile/envpair on services.Read on every reload, not only at bootstrap. That matters because
conf_reset_env()clears all tracked variables at the start of eachreload, and entering the configured runlevel triggers one; gating the
block on bootstrap would drop every variable on the way out of runlevel
S. Matches
doc/config/env.md.Variables are global and shared by all services. For a service-local
environment use the per-service
envfilekey, which sources a file.log
Rotation of Finit's own log, not service output. Always evaluated, not
bootstrap-gated. Shares its name with the per-service
log {}blockdeliberately; the two sit at different levels and more global log
settings are expected here.
Top-level directives
Everything below sits at file scope, outside any block.
Evaluated only at bootstrap
hostname/etc/hostnamestill winsmodulesmodmknodnetworkrcsdfinit.ddirectoryrunpartsrunparts-progressrunparts-sysvrunlevelreadiness"none"is acted onAn out-of-range
runlevelsilently falls back to 2 rather thanerroring.
modulesentries go tokmod_load(), which skips already-loadedmodules and shells out to
modprobe; arguments after the module nameare passed along.
mknodentries are concatenated onto amknodcommand and run interactively.
Evaluated on every reload
shutdownreboot-delayreboot-watchdogservice-intervalOut-of-range values are ignored, leaving the previous value in place.
Worked example: a complete finit.conf
Per-service directories
The five
*-dirkeys mirror systemd'sRuntimeDirectory=family. Thevalue is a name resolved under a fixed base, absolute paths and
..are refused. The directory is created before each start, mode 0755,
chowned to
user/group, and its full path is exported to the processas
RUNTIME_DIRECTORY,STATE_DIRECTORY,CACHE_DIRECTORY,LOGS_DIRECTORY, orCONFIGURATION_DIRECTORY. Only the runtimedirectory is removed when the unit stops (after
exec-stop-post); acompleted non-remain run/task counts as stopped.
Each takes a
-modesuffix key (octal, leading zero, default 0755),and
runtime-dir-preserve = "no" | "restart" | "yes"maps systemd'sRuntimeDirectoryPreserve=.config-diris created but neverchowned, like systemd. An existing dir with the right owner is left
alone; on mismatch the tree is chowned back recursively.
These are the first block-only settings: no legacy token exists, the
values are set on the svc after
service_register()returns it.systemd accepts multiple directories per setting; these are scalar for
now, widening later is syntax-compatible since libconfuse accepts a
bare value for a list option.
Gaps worth knowing
initctl showdoes not print the translated line. It is an alias forcat, so for a block-format service it prints the block. The planassumed otherwise, which means the "declare the same service in both
formats and compare the output" check it describes cannot be done
today. The translated one-liner is visible only with
finit.debug=on,as a
translated:line.Emitted one-liners are verified by reading and by that debug output,
not by the test suite. Three have been wrong so far:
cgroup:NAME,which
parse_cgroup()reads as a settings string so the group wasnever joined;
cgroup.NAME:settings, whose separator has to be acomma; and a
nowarnthat landed on the wrong service after arefactor. All three are fixed, and the last one passed the whole test
suite before the debug output caught it.
Cgroups are untested.
cgroup_avail()is false inside the testnamespace, so the suite cannot assert that a group was joined.
On libconfuse 3.3 each free-form cgroup key logs one spurious warning.
Values are captured correctly, only the logging differs, and Finit's
error callback absorbs it. Gone in 3.4.
doc/config/service-opts.mdstill documentsstop:'script'without atimeout. Both
exec-stopandexec-reloadnow take one, so that pageis out of date.
Future
A
persistflag for run/tasks whose only runlevel isS. Those arepruned once bootstrap finishes, so a user in a normal runlevel cannot
see that they ran at all, and
remain-after-exitcannot help becausethere is nothing left by then.
persistwould keep the entry alivepast the prune purely so it stays inspectable.