Files
Joachim Wiberg 96c5b4b031 doc: document the provides setting
The migration guide told anyone holding the repeated-stanza idiom for a
per-platform service to split the variants across files or stay on the
line-based format, because a block title is an identity and the
variants have to share one barrier.  provides is the answer, so the
guide converts that shape now instead of routing around it, and the
header of 10-hotplug.conf.in no longer points at the workaround.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-05 17:59:09 +02:00

357 lines
13 KiB
Markdown

Services
========
**Syntax:** `service NAME { command = "/path/to/daemon ARGS" }`
Service, or daemon, to be monitored and automatically restarted if it
exits prematurely. Finit tries to restart services that die, by default
10 times before giving up and marking them as *crashed*. After which
they have to be manually restarted with `initctl restart NAME`. The
limits controlling this are configurable, see
[Service Options](service-opts.md).
> [!TIP]
> To allow endless restarts, see [`respawn`](service-opts.md#restarting)
For daemons that support it, we recommend appending `--foreground`,
`--no-background`, `-n`, `-F`, or similar command line argument to
prevent them from forking off a sub-process in the background. This is
the most reliable way to monitor a service.
However, not all daemons support running in the foreground, or they may
start logging to the foreground as well, these are forking daemons and
are supported using the same syntax as forking `sysv` services, by
naming the file to watch with `pidfile`. There is an alternative that
may be more intuitive, where Finit can also guess the PID file based on
the daemon's command name:
service ntpd {
description = "NTP daemon"
type = "forking"
command = "ntpd"
}
This example lets BusyBox `ntpd` daemonize itself. Finit uses the
basename of the binary to guess the PID file to watch for the PID:
`/var/run/ntpd.pid`. If Finit guesses wrong, name the file yourself
with `pidfile = "/path/to/file.pid"`.
The file belongs to the service: Finit reads it but does not create or
remove it. That is the default, and `pidfile-create = true` is what
asks Finit to write the file instead. The one exception is *stale*
cleanup — if the service dies without removing its own pidfile
(SIGKILL, OOM, segfault), and the file still names the just-reaped
PID, Finit removes it before the next retry. This prevents daemons
that refuse to start on an existing pidfile (e.g. `dbus-daemon`)
from getting stuck in a crash-restart loop.
**Example:**
In the case of `ospfd` (below), we omit the `-d` flag (daemonize) to
prevent it from forking to the background:
service ospfd {
description = "OSPF daemon"
runlevel = "2345"
conditions = { "pid/zebra" }
command = "/sbin/ospfd"
}
`runlevel` denotes the runlevels `ospfd` is allowed to run in, it is
optional and defaults to level 2-4 if omitted.
`conditions` lists what must be asserted before starting `ospfd`. In
this example Finit waits for another service, `zebra`, to have created
its PID file in `/var/run/quagga/zebra.pid`. Finit watches *all* files
in `/var/run`, for each file named `*.pid`, or `*/pid`, Finit opens it
and finds the matching `NAME:ID` using the PID.
A condition may be prefixed with `~` to propagate a reload of the
upstream service to this one, rather than merely pausing and resuming
it:
conditions = { "~pid/zebra" }
If `ospfd` cannot be reloaded with `SIGHUP` at all, that is a property
of `ospfd` and not of the condition, so it is said directly:
reload-signal = "none"
The legacy format spells that second one as a `!` leading the condition
list, which is not accepted here. For details, see the
[Finit Conditions](../conditions.md) document.
Some services do not maintain a PID file and rather than patching each
application Finit provides a workaround. With `pidfile-create` Finit
creates the file when starting and removes it when stopping. The path
comes from `pidfile`, which takes three forms:
pidfile = true # /var/run/<command basename>.pid
pidfile = "bar" # a bare name, /var/run/bar.pid
pidfile = "/run/bar.pid" # an explicit path
Such a file is also used by the Finit condition subsystem, so another
service, run or task can depend on `pid/bar`. Here foo is not started
until bar has:
service bar {
description = "Bar Service"
pidfile = "/run/bar.pid"
pidfile-create = true
command = "bar"
}
service foo {
description = "Foo Service"
conditions = { "pid/bar" }
command = "foo"
}
Needless to say, it is better if `bar` creates its own PID file when it
has completed starting up and is ready for service.
As an alternative "readiness" notification, Finit supports both systemd
and s6 style notification. This is enabled with the `notify` key:
* `notify = "systemd"` -- tells Finit the service uses the `sd_notify()`
API to signal PID 1 when it has completed its startup and is ready
to service events. The [sd_notify()][] API expects `NOTIFY_SOCKET`
to be set to the socket where the application can send `"READY=1\n"`
when it is starting up or has processed a `SIGHUP`.
* `notify = "s6"` -- puts Finit in s6 compatibility mode. Compared to the
systemd notification, [s6 expect][] compliant daemons to send `"\n"`
and then close their socket. Finit takes care of "hard-wiring" the
READY state as long as the application is running, events across any
`SIGHUP`. Since s6 can give its applications the descriptor number
(must be >3) on then command line, Finit provides the following
syntax (`%n` is replaced by Finit with then descriptor number):
service mdevd {
runlevel = "S12345789"
notify = "s6"
command = "mdevd -O 4 -D %n"
}
[sd_notify()]: https://www.freedesktop.org/software/systemd/man/sd_notify.html
[s6 expect]: https://skarnet.org/software/s6/notifywhenup.html
When a service is ready, either by Finit detecting its PID file, or
their respective readiness mechanism has been triggered, Finit creates
then service's ready condition which other services can depend on:
$ initctl -v cond get service/mdevd/ready
on
This can be used to synchronize the start of another run/task/service:
task mdevd-coldplug {
runlevel = "S"
conditions = { "service/mdevd/ready" }
user = "root"
group = "root"
command = "mdevd-coldplug"
}
Finit waits for `mdevd` to notify it, before starting `mdevd-coldplug`.
Notice how both start in runlevel S, and the coldplug task only runs in
S. When the system moves to runlevel 2 (the default), coldplug is no
longer part of the running configuration (`initctl show`), this is to
ensure that coldplug is not called more than once.
> For a detailed description of conditions, and how to debug them,
> see the [Finit Conditions](../conditions.md) document.
Non-privileged Services
-----------------------
Every `run`, `task`, or `service` can also list the privileges the
command should be executed with, using `user`, `group` and
`extra-groups`, all optional:
run hello {
runlevel = "2345"
user = "joe"
group = "users"
command = "logger \"Hello world\""
}
Finit reads the user's supplementary group membership from `/etc/group`
automatically. Any groups the user belongs to will be inherited by
the service.
To specify additional supplementary groups beyond those in
`/etc/group`, list them in `extra-groups`:
service caddy {
user = "caddy"
group = "caddy"
extra-groups = { "ssl-cert" }
command = "/usr/bin/caddy run"
}
This runs the `caddy` service as user `caddy`, with primary group
`caddy`, inheriting any groups `caddy` is a member of in `/etc/group`,
plus the additional `ssl-cert` group. This is useful when a service
needs access to resources owned by groups not listed in `/etc/group`.
For multiple instances of the same command, e.g. a DHCP client or
multiple web servers, add `:ID` to the block title, like this:
service httpd:80 {
description = "Web server"
runlevel = "2345"
command = "httpd -f -h /http -p 80"
}
service httpd:8080 {
description = "Old web server"
runlevel = "2345"
command = "httpd -f -h /http -p 8080"
}
Without the `:ID` the latter will overwrite the former and only the old
web server would be started and supervised.
> [!NOTE]
> The line-based format also accepts a bare ID, `service :80 ...`,
> deriving the name from the command. There is no block equivalent:
> the title carries both name and ID.
Conditional Loading
-------------------
Finit supports conditional loading of blocks. The following example is
taken from the `system/10-hotplug.conf` file in the Finit distribution.
Here we only show a simplified subset.
Starting with the udev daemon, which goes by two names depending on
how it was built. Both are candidates for the same service:
service udevd {
pidfile = "udevd"
command = { "/lib/systemd/systemd-udevd", "-udevd" }
}
When loading the .conf file Finit looks for
`/lib/systemd/systemd-udevd`, and if that is not there it moves on to
`udevd`. A candidate that is not installed is expected, so no warning
is logged for the ones that are skipped. The leading `-` on the last
one says it is also fine if none of them are found, in which case the
block is dropped quietly and no service named `udevd` exists.
> [!NOTE]
> This needs to be one block. The title is the service identity, so
> two blocks titled `udevd` in the same file are two declarations of
> one service, which Finit rejects. See [Duplicate
> titles](service-opts.md#duplicate-titles).
run udevadm:1 {
runlevel = "S"
if = "udevd"
conditions = { "pid/udevd" }
command = "-udevadm settle -t 0"
}
This block is only loaded if we know of a service named `udevd`. Again,
we do not warn if `udevadm` is not found, execution will also stop here
until the PID condition is asserted, i.e., Finit detecting udevd has
started.
run mdev {
description = "Populating device tree"
runlevel = "S"
conflicts = { "udevd" }
command = "-mdev -s"
}
If `udevd` is not available, we try to run `mdev`, but if that is not
found, again we do not warn.
Conditional loading can also be negated, so the previous block can be
written as:
run mdev {
description = "Populating device tree"
runlevel = "S"
if = "!udevd"
command = "-mdev -s"
}
The reason for using `conflicts` in this example is that a conflict can
be resolved. Blocks naming a conflict are rechecked at runtime.
Conditional Execution
---------------------
Similar to conditional loading of blocks there is conditional runtime
execution. This can be confusing at first, since Finit already has a
condition subsystem, but this is more akin to the qualification to a
runlevel. E.g., a task with `runlevel = "123"` is qualified to run
only in runlevel 1, 2, and 3. It is not considered for other
runlevels.
Conditional execution qualify a run/task/service based on a condition.
Consider this (simplified) example from the Infix operating system:
run startup {
runlevel = "S"
conditions = { "pid/sysrepo" }
command = "confd -b --load startup-config"
}
run failure {
runlevel = "S"
if = "usr/fail-startup"
conditions = { "pid/sysrepo" }
command = "confd --load failure-config"
}
The two run blocks reside in the same .conf file so Finit runs them in
true sequence. If loading the file `startup-config` fails confd sets
the condition `usr/fail-startup`, thus allowing the next one to load
`failure-config`.
Notice the critical difference between the `conditions` list and `if`.
The former is a condition for starting; the latter is a condition to
check whether a run/task/service is qualified to even be considered.
`if` has a negation of its own, `!`, which is unrelated to anything in
the `conditions` list.
What `if` compares against depends on the value. A namespace
separator makes it a condition, anything else is a service name:
| `if` | Asks |
|---|---|
| `"udevd"` | is a service by this name known? |
| `"usr/foo"` | was this condition set? |
Both are questions about whether the block belongs in the running
configuration at all, usually answered from what bootstrap established.
A statement is all of one kind or the other, so the block is rejected
if you mix them.
> [!NOTE]
> `if` qualifies, it does not track. A condition asserted or cleared
> later does not start or stop the service by itself -- that is what
> the `conditions` list is for.
Conditional execution can also be negated, so provided the file loaded
did the opposite, i.e., set a condition on success, the previous block
can be written as:
run failure {
runlevel = "S"
if = "!usr/startup-ok"
conditions = { "pid/sysrepo" }
command = "confd ..."
}
Variants of one service are often qualified this way, one per platform,
and they usually have to supply the same barrier to whatever waits for
them. Each variant needs its own title, since a title is an identity,
and the shared barrier is named with `provides`. See [Provided
Conditions](service-opts.md#provided-conditions).