Compare commits

..
63 Commits
Author SHA1 Message Date
Joachim Wiberg c01faef99b service: clear condition before stopping rdeps on reload
When a service without SIGHUP reload support (noreload) is touched and
'initctl reload' is called, service_update_rdeps() correctly identifies
its reverse dependencies but only marks them dirty.  It does not clear
the service's condition, so when service_step_all() runs:

 - rdeps supporting SIGHUP hit the sm_in_reload() guard and break early,
   left running while their dependency is being killed.
 - rdeps without SIGHUP support may receive SIGTERM too late, after the
   dependency has already died and broken their connection, causing them
   to exit from RUNNING state and have their restart counter incremented.

Fix by calling cond_clear() on the service's condition immediately in
service_update_rdeps(), before service_step_all() runs.  cond_clear()
calls cond_update() which calls service_step() inline on all affected
services, which see COND_OFF and transition to STOPPING_STATE — all
before SIGTERM is ever sent to the dependency itself.

This mirrors the pattern already used in api.c:do_reload() for direct
'initctl reload <svc>' calls.

Fixes: avahi/mdns stop causing mdns-alias restart counter increment

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-03-19 06:37:50 +01:00
Joachim Wiberg 4867b7268b Merge pull request #482 from vazub/plugfix
Use explicit plugin names to prevent subtle macro processing bugs
2026-03-16 07:31:34 +01:00
bazub 662293e194 Use explicit plugin names to prevent subtle macro processing bugs 2026-03-06 20:36:23 +00:00
Joachim Wiberg 4ce810cc1f Merge pull request #481 from vazub/pathfix
Remove redundant global path var and fix memory corruption
2026-03-04 15:05:36 +01:00
bazub 184c079c08 Remove redundant global path var and fix memory corruption 2026-03-03 20:31:33 +00:00
Joachim Wiberg 17c6791c70 Update ChangeLog and bump version for v4.16 release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-27 10:35:32 +01:00
Joachim Wiberg 42f24f5af5 Silence shutdown logging by default
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-27 10:34:38 +01:00
Joachim Wiberg cef5049bf0 Update ChangeLog and bump version for v4.16-rc1
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-24 05:36:45 +01:00
Joachim Wiberg ec251f1ce1 Merge pull request #480 from aanderse/master 2026-02-24 05:05:05 +01:00
Aaron Andersen 98ec30d308 tmpfiles: add --exclude-prefix and -E flags
Add support for the --exclude-prefix=PATH option to skip rules whose
path starts with the specified prefix.  The option can be specified
multiple times to exclude multiple path prefixes.

The -E flag is a shortcut for:

    --exclude-prefix=/dev --exclude-prefix=/proc \
    --exclude-prefix=/run --exclude-prefix=/sys

This is useful to avoid creating files below virtual or memory-backed
file system mount points.
2026-02-23 15:39:44 +00:00
Joachim Wiberg e1fe870aaf Merge pull request #478 from mattiaswal/fix-initial-devmon
devmon: assert condition immediately if device already exists

  1. udev fires early, creates device nodes in /dev/                                                                                                           
  2. Config is parsed later, calling devmon_add_cond() for each dev/ condition
  3. At this point the device already exists but the inotify event was missed
  4. The PR's fexist() check catches this — new node is added to the TAILQ and condition is immediately asserted

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-23 08:09:12 +01:00
Mattias Walström f04fce544d devmon: assert condition immediately if device already exists
The config parsing happens after udev triggers the initial event,
make sure to assert the condition if the device node exists when adding
it from configuration.

Signed-off-by: Mattias Walström <lazzer@gmail.com>
2026-02-23 07:30:08 +01:00
Joachim Wiberg 7aa9ed5982 Merge pull request #474 from finit-project/multi-depend
Multi depend fix
2026-02-22 21:05:31 +01:00
Joachim Wiberg a1a92a04bd Update ChangeLog
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:51:07 +01:00
Joachim Wiberg 804f655d87 test: fix depserv.sh after regression in 0b182c06
A service that is reloaded should not trigger dependants to be reloaded
unless the new <~cond> is used.  Which is reserve for tightly coupled
services.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:36 +01:00
Joachim Wiberg 2c78e7429a Only remove managed pidfiles in service cleanup
For SysV services with pid:!/path, the pidfile belongs to the service
itself and Finit shouldn't delete it.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:36 +01:00
Joachim Wiberg d77f0127b4 Don't disrupt dependents on reload of SIGHUP-capable service
This fixes a real bug where `initctl reload syslogd` unconditionally
clears syslogd's pid condition, causing all dependent services (dbus,
dnsmasq, etc.) to be stopped even though syslogd handles SIGHUP
gracefully and its PID/pidfile persist.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:35 +01:00
Joachim Wiberg 4253d0de25 Run service stop: and reload: scripts as non-blocking processes
A reload: script, like 'frrinit.sh reload' could potentially take a
while to finish, during which Finit would be blocked.  This change
reuses the service_script_add(), used for ready: scripts, to track
these background helpers.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:35 +01:00
Joachim Wiberg d47181a4a7 Silence cgroup warnings for non-existing PIDs
When the kernel manages to reap a child process before we've moved it to
its proper cgroup it will return ESRCH (No such process), we can safely
ignore such errors for short-lived processes.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:34 +01:00
Joachim Wiberg 8642007d0e Debug shutdown hangs and guard with timer watchdog
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 20:36:34 +01:00
Joachim Wiberg 96c74ed48b Reformat signaling log messages for readability
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 15:51:25 +01:00
Joachim Wiberg 03a14bcaa5 Add <~cond> condition modifier for tightly coupled services
Conditions in Finit are dependencies: if A is asserted, service B is
allowed to run.  When A goes through FLUX (e.g., upstream reloads),
dependents are PAUSED and then simply resumed when the condition is
reasserted -- this is the correct behavior for barrier-style deps
like <pid/syslogd>.

However, some setups have tightly coupled services where dependents
must be reloaded/restarted when an upstream service reloads, not just
resumed.  E.g., the FRR routing stack on Infix OS:

    netd <pid/mgmtd> ← zebra <!pid/netd> ← {staticd,ripd} <!pid/zebra>

When netd reloads (SIGHUP), zebra and its dependents must be restarted
to pick up the new configuration.

The new '~' condition prefix marks a dependency as flux-sensitive:

    service <!~pid/netd> name:zebra ...

When the upstream condition goes FLUX and returns to ON, the dependent
is reloaded (SIGHUP) or restarted (noreload '!') instead of merely
resumed.  Transitivity follows naturally through the condition chain.

Closes #416
Closes #476

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-22 15:51:25 +01:00
Joachim Wiberg 4bb2c33859 Dependents not restarted after SIGHUP reload of service
When 'initctl reload' is called after marking a service in a dependency
chain dirty, Finit fails to restart (unfreeze) affected services.

This patch updates the pidfile plugin to watch for IN_ATTRIB changes,
e.g. when a process uses utimensat() to update its pidfile, and adds
service_step_all() at end of reload cycle to guarantee convergence
after conditions are reasserted.

Issue #476

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-17 07:08:30 +01:00
Joachim Wiberg ab81272083 test: new regression test to verify multi-chain deps
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-13 07:30:10 +01:00
Joachim Wiberg 1fbf03bc1e Clear pid condition on service collection to fix stale deps
In a setup like this, when 'netd' is marked dirty and subsequently is
reloaded, e.g., using 'initctl reload', zebra is properly restarted,
but staticd isn't:

mgmtd <!> ← netd <pid/mgmtd> ← zebra <!pid/netd> ← staticd <!pid/zebra>

Finit must invalidate the condition of zebra to trigger a restart also
of staticd.  This to guard against daemons like zebra that may fail to
clean up their pidfiles.

Fixes #475

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-13 07:30:00 +01:00
Joachim Wiberg 92a2861b1c Merge pull request #473 from mattiaswal/fix-devmon 2026-02-10 20:36:37 +01:00
Joachim Wiberg 922714780a Merge pull request #472 from finit-project/reload-foo 2026-02-10 20:33:50 +01:00
Mattias Walström b206c3d655 devmon: re-evaluate device conditions on reconf
Device conditions tracked by devmon were lost on `initctl reload`
because the reconf path did not re-assert them.  Add devmon_reconf()
to iterate all tracked device nodes and set or clear their conditions
based on current device presence.

Signed-off-by: Mattias Walström <lazzer@gmail.com>
2026-02-10 20:09:35 +01:00
Mattias Walström 109d8826bd devmon: Fix deletion of conditions
Only compare the beginning of the condition, not the whole
condition name.

Signed-off-by: Mattias Walström <lazzer@gmail.com>
2026-02-10 19:49:13 +01:00
Joachim Wiberg 60478106eb Update ChangeLog with latest changes and features
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 16:01:53 +01:00
Joachim Wiberg 0de9cfb020 doc: Note per-service reload behavior for conditions
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 16:01:47 +01:00
Joachim Wiberg 0b182c063e test: Extend depserv test with per-service reload, fix slay race
Verify that 'initctl reload foo' properly triggers dependent
services by checking that bar gets a new PID after the reload.
Also change the second test case from service/foo/running to
service/foo/ready which is the actual condition set by pidfile.so.

Fix a race in slay where the target process could exit between
the PID lookup and kill -9, causing spurious test failures in
tight kill loops (e.g., start-kill-service.sh).

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 16:00:57 +01:00
Joachim Wiberg f0914f6d32 Fix 'initctl reload NAME' not updating conditions for dependents
When reloading a specific service with 'initctl reload foo', the
pid/foo and service/foo/ready conditions were never cleared, so
dependent services were not notified of the reload.

Clear the service's pid condition and, for pid/none notify types,
the ready condition before reloading.  The conditions are then
reasserted by the pidfile inotify handler when the service touches
its PID file after processing SIGHUP.

For s6/systemd services the ready condition is left intact since
their readiness notification may not re-trigger on SIGHUP.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 15:59:49 +01:00
Joachim Wiberg 7090321ff3 Don't hide cursor when shutting down
Users starting Finit based systems using U-Boot or Barebox may otherwise
not get a visible cursor at their prompt.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-10 15:58:46 +01:00
Joachim Wiberg 266f1132a4 Merge pull request #471 from aanderse/remain-after-exit
Add remain:yes option for run/task oneshot commands

Fixes #457

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-02-08 08:37:25 +01:00
Aaron Andersen 5f7e8457af Add remain:yes option for run/task oneshot commands
Similar to systemd's RemainAfterExit=yes.  Prevents the task from
re-running on runlevel re-entry and ensures the post: script runs
when explicitly stopped or when leaving valid runlevels.

Useful for tasks that set up persistent state like firewall rules:

    task [2345] remain:yes \
         post:/usr/sbin/teardown-firewall \
         /usr/sbin/setup-firewall -- Firewall setup

Not supported for bootstrap-only tasks (runlevel S only) since these
are deleted immediately after completion.
2026-02-05 22:08:22 -05:00
Joachim Wiberg a215747355 Fix clone3 build failure with older toolchain kernel headers
Define __NR_clone3 (435) ourselves when not provided by the toolchain
headers.  The syscall number is stable kernel ABI and the same on all
architectures since Linux 5.3.

The existing runtime fallback to fork() handles older kernels that don't
support the syscall.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-29 13:11:23 +01:00
Joachim Wiberg dacda5ab5d Merge pull request #470 from aanderse/master 2026-01-20 07:18:46 +01:00
Aaron Andersen 39044adcf8 Create mount points in fs_init() if they don't exist
In containerized or virtualized environments, standard mount point
directories may not exist at boot.  Ensure they are created before
attempting to mount.
2026-01-19 15:20:43 -05:00
Joachim Wiberg 9396cd1d26 Merge pull request #469 from aanderse/tmpfiles
enhance tmpfiles implementation
2026-01-19 07:57:16 +01:00
Aaron Andersen c9fd4418e7 tmpfiles: fix f/F to apply ownership when writing content
When f or F types write content to a file, the mode and ownership
specified in the config should be applied. Previously, ownership was
only applied when create() was used (i.e., when no argument was
specified).

Now we explicitly apply mode and ownership after writing content to
the file.
2026-01-18 18:59:20 -05:00
Aaron Andersen 7206f745a2 tmpfiles: fix 'e' type to only adjust existing directories
According to tmpfiles.d(5), the 'e' type adjusts the mode and ownership
of existing paths but should not create them. Previously, mksubsys()
was used which could create directories.

Now we explicitly check if the path is an existing directory before
adjusting its permissions.
2026-01-18 18:59:20 -05:00
Aaron Andersen 25bcde12d4 tmpfiles: add support for numeric uid/gid in config files
Add parse_uid() and parse_gid() helper functions that support both
numeric IDs and name lookups. Update the d/D directory creation
handlers to use these new functions.

This allows config files to specify ownership using numeric UIDs and
GIDs instead of only usernames and group names, matching systemd-tmpfiles
behavior.
2026-01-18 18:59:20 -05:00
Aaron Andersen 7459ede806 tmpfiles: fix L+ to replace non-directory entries
The L+ type should replace existing entries with a symlink. Previously,
rmrf() was always called which is only appropriate for directories.
Now we check if the path is a directory first, and use erase() for
files and symlinks.
2026-01-18 18:59:20 -05:00
Aaron Andersen 6bf35513c3 tmpfiles: add support for config files on command line
Allow specifying one or more configuration files as command line
arguments instead of always processing all files in the standard
tmpfiles.d directories.

This enables targeted operations on specific config files:

    tmpfiles --create /etc/tmpfiles.d/myapp.conf
    tmpfiles --clean /tmp/test.conf /tmp/other.conf

When no config files are specified, the existing behavior of
processing all *.conf files in the standard directories is preserved.

Also refactors file processing into a helper function to reduce
code duplication.
2026-01-18 18:59:20 -05:00
Aaron Andersen 0b8d39329a tmpfiles: add --clean flag for age-based cleanup
Add support for the --clean (-C) flag to remove files and directories
older than the age specified in tmpfiles.d configuration entries.

The age field (6th column) in tmpfiles.d entries can now be used with
'd', 'D', and 'e' type entries to clean up old files.  Supported time
suffixes are: s (seconds), m (minutes), h (hours), d (days), w (weeks).

Example configuration:
    d /tmp/cache 0755 root root 10d

When run with --clean, files in /tmp/cache older than 10 days will be
removed.  The directory itself is preserved.

Uses a conservative cleanup approach matching systemd-tmpfiles:
 - Files: kept if ANY of atime, ctime, mtime is recent
 - Directories: kept if ANY of atime, mtime is recent (ctime excluded
   because cleanup itself updates directory ctime)

A value of "-" or "0" for age disables cleanup for that entry.

Note: x/X exclusion patterns are recognized but not yet implemented.
2026-01-18 18:59:20 -05:00
Aaron Andersen 84098dd8b7 tmpfiles: rename flags for clarity
Rename the command-line flag variables to be more descriptive:

  c_flag -> create_flag
  r_flag -> remove_flag

This improves code readability.
2026-01-18 18:59:20 -05:00
Joachim Wiberg c4463ec64f initctl: escape special characters in JSON output
Strings like command, description, and environment may contain characters
that need escaping for valid JSON, e.g., embedded quotes in command line
arguments like -V "NanoPi R2S".

Add json_escape() helper to handle quotes, backslashes, and control chars.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-18 23:26:37 +01:00
Joachim Wiberg 6f1808248e Merge pull request #468 from aanderse/master
netlink: enumerate existing interfaces at startup
2026-01-18 09:00:27 +01:00
Aaron Andersen 29029bb78f netlink: enumerate existing interfaces at startup
The netlink plugin only receives RTM_NEWLINK events for interfaces that
appear after the plugin starts.  Interfaces that already exist at boot
(e.g., virtio-net in QEMU) never generate events, so their conditions
like net/eth0/exist were never set.

Moving enumeration to PLUGIN_INIT doesn't work because it runs before
cond_init(), so the condition filesystem isn't ready yet.

Fix by registering an HOOK_SVC_PLUGIN callback that queries existing
interfaces and routes.  This hook runs during conf_init(), after the
condition system is initialized.
2026-01-14 15:58:22 -05:00
Joachim Wiberg bbe588ac16 Bump version for new release cycle and update ChangeLog
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-12 19:32:09 +01:00
Joachim Wiberg 34bf9a7776 Fix #467: TTY services stuck in restart state after non-zero exit
When a TTY exited with non-zero code (e.g., user with shell=/sbin/false),
it would enter restart state but never recover, requiring manual restart.

The throttling logic from commit f0032ab had two issues:

  1. Duplicate exit code check in service_retry() created infinite timer loop
  2. TTYs lacked default restart_tmo, causing timer to never start

Fix by removing duplicate check and ensuring TTYs get a 2-second default
restart_tmo for proper throttling.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-12 19:31:39 +01:00
Joachim Wiberg bb111f98fc Merge pull request #465 from aanderse/master
Set USER and LOGNAME environment variables when dropping privileges
2026-01-11 08:54:59 +01:00
Joachim Wiberg c7faf4f9e2 Merge pull request #466 from JackieMium/patch-1
Fix elogind path in elogind.conf
2026-01-11 08:53:29 +01:00
JackieMe 18774539c0 Fix elogind path in elogind.conf
On Debian and Debian-derived distro, the elogind is place at /usr/libexec/elogind
2026-01-11 00:00:06 -06:00
Aaron Andersen abaad560f0 Set USER and LOGNAME environment variables when dropping privileges
When a service is configured to run as a non-root user (@user), finit
correctly drops privileges via setuid() and sets HOME and PATH, but
does not set the USER and LOGNAME environment variables. They remain
set to "root" from boot time.

This causes problems for software that determines its identity from
the environment rather than getuid(). For example, rootless Podman
checks os.Getenv("USER") first when looking up subordinate UID/GID
ranges in /etc/subuid and /etc/subgid.

With USER=root but UID=1000, Podman looks up root's subuid entry
instead of the actual user's, causing applications like newuidmap
to fail. Setting USER and LOGNAME to match the actual user identity
follows POSIX conventions and matches the behavior of su, sudo, and
login.
2026-01-10 21:36:58 -05:00
Joachim Wiberg 0ef325d86c doc: improve cgroups documentation
- Grammar
 - Clarity
 - Examples

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-07 12:49:37 +01:00
Joachim Wiberg 2c94f4e45f doc: fix per-service cgroup syntax
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-07 12:49:13 +01:00
Joachim Wiberg da184e7922 doc: fix invalid user:group examples in cgroups.md
Introduced in 820b9a77 for v4.15.

Fixes #464

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-07 11:55:17 +01:00
Joachim Wiberg 1e93fc1671 Merge pull request #463 from aanderse/switch_root
Add switch_root support for initramfs to real root transitions
2026-01-03 00:49:07 +01:00
Aaron Andersen 8e7d1b7bb5 Refactor: drop do_ prefix from iterate_proc() and switch_root()
The do_ prefix is conventionally reserved for local helper functions.
Move switch_root() declaration to private.h alongside iterate_proc()
and remove the now-empty initramfs.h header.
2026-01-02 18:13:00 -05:00
Aaron Andersen 373738f3d1 Implement switch_root functionality allowing Finit to serve as the init
in an initramfs, then transition to the real root filesystem.  Useful
for systems requiring early boot tasks like LUKS unlock, LVM activation,
or network boot before mounting the real root.

Adds INIT_CMD_SWITCH_ROOT API command, `initctl switch-root` subcommand,
and HOOK_SWITCH_ROOT plugin hook point.  The implementation gracefully
stops services, moves virtual filesystems (/dev, /proc, /sys, /run) to
the new root, deletes initramfs contents to free memory, then execs the
new init as PID 1.

See GitHub Discussion #292 for background.
2026-01-01 19:10:24 -05:00
Aaron Andersen e6d3eb2526 Handle already-mounted cgroups in cgroup_init()
Add handling for EBUSY when mounting cgroup2 filesystem, which occurs
when cgroups are already mounted. This can happen after switch_root
when cgroups were moved from the initramfs, or in container environments.

Verify the existing mount is actually cgroup2 before proceeding, and
track whether we mounted to avoid unmounting on error if we didn't.
2026-01-01 16:02:36 -05:00
57 changed files with 1795 additions and 198 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
AC_INIT([Finit], [4.15], [https://github.com/finit-project/finit/issues],
AC_INIT([Finit], [4.16], [https://github.com/finit-project/finit/issues],
[finit], [https://http://finit-project.github.io/])
AC_CONFIG_AUX_DIR(aux)
AM_INIT_AUTOMAKE([1.11 foreign subdir-objects])
@@ -1,4 +1,4 @@
# On some systems, bootstrap can be really quick and cause elogind
# to not start before a user has managed to log in. To prevent
# this, add <pid/elogind> as condition to your TTYs in getty.conf
service [2345] <pid/syslogd> /lib/elogind/elogind -- Login manager
service [2345] <pid/syslogd> /usr/libexec/elogind -- Login manager
+56
View File
@@ -3,6 +3,61 @@ Change Log
All relevant changes are documented in this file.
[4.16][] - 2026-02-27
---------------------
### Changes
- Add `initctl switch-root` command and plugin hook point. Useful for
systems requiring early boot tasks like LUKS unlock, LVM activation,
or network boot before mounting the real root, by Aaron Andersen
- Set USER and LOGNAME environment variables when dropping privileges.
Fixes issues with software like rootless Podman that determines user
identity from environment variables, by Aaron Andersen
- Add `remain:yes` option for run/task oneshot commands, similar to the
systemd `RemainAfterExit=yes`, by Aaron Andersen
- Clear service conditions on `initctl reload NAME` to ensure dependent
services are properly updated
- Run service `stop:` and `reload:` scripts as non-blocking processes,
preventing Finit from stalling on long-running helper scripts
- Guard shutdown with timer watchdog to detect and debug shutdown hangs
- Add `~` condition prefix to propagate reload from a dependency to the
dependent service. E.g., `<!~pid/netd>` means not only a regular
condition, but when `netd` reloads, restart this service too. Similar
to systemd's directive `PropagatesReloadTo=`, but declared on the
consumer side. Issue #416
- `tmpfiles`: add support for `-E` and `--exclude-prefix` flags
contributed by Aaron Andersen
### Fixes
- Fix #464: invalid user:group examples in cgroups.md
- Fix #466: elogind path for Debian-based distros, by Jackie Liu
- Fix #467: TTY services stuck in restart state after non-zero exit.
Throttling logic introduced in v4.15 had duplicate checks causing
infinite timer loop, and TTYs lacked default restart timeout
- Fix #475: clear pid condition on service collection to fix stale
deps. When a service crashes (SIGKILL), the RUNNING → HALTED path
bypasses STOPPING where `cond_clear()` is normally called, leaving
dependents stuck
- Fix #476: dependents not restarted after SIGHUP reload of service in
dependency chain. Add `service_step_all()` at end of reload cycle to
guarantee convergence after conditions are reasserted. See also the
new `~` condition prefix (above) to propagate reload to dependents
- Fix #478: fix `<dev/foo>` conditions at boot, conditions that exist
before inotify was set up. Found and fixed by Mattias Walström
- Fix reload of SIGHUP-capable services incorrectly disrupting their
dependents. E.g., `initctl reload syslogd` would stop dbus, dnsmasq,
etc. even though syslogd handles SIGHUP gracefully and its PID persists
- Only remove managed pidfiles in service cleanup. For SysV services
with `pid:!/path`, the pidfile belongs to the service itself and Finit
should not delete it
- Silence spurious cgroup warnings for short-lived processes where the
kernel reaps the child before cgroup assignment completes
- Fix handling of already-mounted cgroups in `cgroup_init()`, can occur
after switch_root or in container environments
- Improve cgroups documentation clarity, grammar, and examples
[4.15][] - 2026-01-01
---------------------
@@ -1914,6 +1969,7 @@ Major bug fix release.
* Initial release
[UNRELEASED]: https://github.com/finit-project/finit/compare/4.15...HEAD
[4.16]: https://github.com/finit-project/finit/compare/4.15...4.16
[4.15]: https://github.com/finit-project/finit/compare/4.14...4.15
[4.14]: https://github.com/finit-project/finit/compare/4.13...4.14
[4.13]: https://github.com/troglobit/finit/compare/4.12...4.13
+37 -4
View File
@@ -14,11 +14,11 @@ specified separated by comma. Multiple conditions are logically AND'ed
during evaluation, i.e. all conditions must be satisfied in order for a
service to run.
A special syntax, using a leading `!` in run/task/service conditions,
denote if a:
Two special prefixes can be used inside the angle brackets:
- service does not support `SIGHUP`
- run/task should not block runlevel changes (i.e., bootstrap)
- `!` -- service does not support `SIGHUP` (noreload), or run/task
should not block runlevel changes (i.e., bootstrap)
- `~` -- propagate reload from this dependency, see below
Finit guarantees by default that all run/tasks run (at least) once
per runlevel. For most tasks this is a good default, for example
@@ -53,6 +53,30 @@ moving to the next runlevel after bootstrap, so we set `<!>`:
task [S0123456789] <!sys/pwr/fail> name:pwrfail initctl poweroff -- Power failure, shutting down
Propagating Reload in Dependencies
-----------------------------------
By default, when a service reloads during `initctl reload`, dependent
services are paused (`SIGSTOP`) and simply resumed (`SIGCONT`) when the
condition is reasserted. This is correct for barrier-style dependencies
like `<pid/syslogd>`, where dependents just need syslogd running and do
not care if it reloads its config.
For services that need to react when their upstream reloads, the `~`
prefix propagates the reload from the dependency:
service <pid/svc_a> name:svc_b /sbin/svc_b -- Needs A (barrier)
service <!~pid/svc_b> name:svc_c /sbin/svc_c -- Propagate reload from B
Here, `<~pid/svc_b>` means: propagate a reload of `svc_b` to `svc_c`.
When `svc_b` reloads, `svc_c` will be restarted (because of `!`,
noreload) instead of merely resumed. If `svc_c` supported `SIGHUP`
(no `!` prefix), it would be sent `SIGHUP` instead.
This is similar to systemd's `PropagatesReloadTo=` directive, but
declared on the consumer side rather than the provider side.
Triggering
----------
@@ -194,6 +218,10 @@ When Finit configuration files are changed and the `initctl reload`
command is called, it is expected of services to touch their PID files
for Finit to reassert their conditions.
Similarly, when a single service is reloaded with `initctl reload NAME`,
its conditions are cleared and reasserted, ensuring dependent services
are properly updated.
Daemons that don't create PID files, or fail to touch them on reload,
can be worked around by using the `pid:/path/to/file.pid` syntax in
the service stanza for the daemon. It is far from optimal since any
@@ -277,6 +305,11 @@ This STOP/CONT handling minimizes the number of unnecessary service
restarts that would otherwise occur because a depending service was sent
`SIGHUP` for example.
Services with the `~` prefix are an exception to this rule: when their
conditions return to `on` after being in `flux`, the reload is propagated
-- the service is reloaded (SIGHUP) or restarted (noreload `!`) instead
of simply being resumed.
Therefore, any plugin that supplies Finit with conditions must ensure
that their state is updated after each reconfiguration. This can be
done by binding to the `HOOK_SVC_RECONF` hook. For an example of how
+67 -21
View File
@@ -1,16 +1,29 @@
There are three major cgroup configuration directives:
Finit provides three different cgroup directives for controlling resource allocation:
1. Global top-level group: `init`, `system`, `user`, or a custom group
2. Selecting a top-level group for a set of run/task/services
3. Per run/task/service limits
1. **Top-level cgroup definition**: `cgroup NAME settings`
- Defines a top-level cgroup (e.g., `init`, `system`, `user`) with default settings
- Space-separated syntax
- Example: `cgroup system cpu.weight:9700`
2. **Global cgroup selector**: `cgroup.NAME[,options]` (standalone directive)
- Sets the default cgroup for subsequent services in a `.conf` file
- Dot-separated with optional comma-separated options
- Example: `cgroup.maint` or `cgroup.system,delegate`
3. **Per-service cgroup option**: `cgroup.NAME[,options]` or `cgroup:options`
- Overrides the cgroup for a specific service
- Part of the service directive line
- Example: `service [...] cgroup.maint,mem.max:1G /path/to/cmd`
> [!NOTE]
> Linux cgroups and details surrounding values are not explained in the
> Finit documentation. The Linux admin-guide cover this well:
> Finit documentation. The Linux admin-guide covers this well:
> <https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html>
Top-level Group Configuration
-----------------------------
Top-level Cgroup Definition
----------------------------
**Syntax:** `cgroup NAME settings`
# Top-level cgroups and their default settings. All groups mandatory
# but more can be added, max 8 groups in total currently. The cgroup
@@ -34,22 +47,37 @@ By default, the `system/` cgroup is selected for almost everything. The
`init/` cgroup is reserved for PID 1 itself and its closest relatives.
The `user/` cgroup is for local TTY logins spawned by getty.
Selecting Cgroups
------------------
Global Cgroup Selector
----------------------
To select a different top-level cgroup, e.g. `maint/`, one can either
define it for a group of run/task/service directives in a `.conf` or per
each stanza:
**Syntax:** `cgroup.NAME[,options]` (standalone directive)
To select a different top-level cgroup, e.g. `maint/`, for a group of
run/task/service directives in a `.conf` file, use the `cgroup.NAME`
directive as a standalone line:
cgroup.maint
service [...] <...> /path/to/foo args -- description
service [...] <...> /path/to/bar args -- description
or
Both services will run in the `maint/` cgroup.
You can also include options with the global selector:
cgroup.system,delegate
service [...] <...> /path/to/foo args -- description
Per-Service Cgroup Option
--------------------------
**Syntax:** `cgroup.NAME[,options]` or `cgroup:options` (service option)
To override the cgroup for a specific service, use the `cgroup.NAME`
option within the service directive:
service [...] <...> cgroup.maint /path/to/foo args -- description
The latter form also allows per-stanza limits. Two syntaxes are supported:
This form also allows per-service limits. Two syntaxes are supported:
**New comma-separated syntax (recommended):**
@@ -59,6 +87,11 @@ The latter form also allows per-stanza limits. Two syntaxes are supported:
service [...] <...> cgroup.maint:cpu.max:10000,mem.max:655360 /path/to/foo args -- description
You can also apply options to the current default cgroup (without changing it)
using the `cgroup:options` syntax:
service [...] <...> cgroup:cpu.max:10000,mem.max:655360 /path/to/foo args -- description
Both syntaxes work identically. The new comma-separated syntax is recommended
as it's more consistent with other option parsing in Finit.
@@ -88,11 +121,11 @@ The syntax supports combining the name override with other options:
Or with delegation:
service [2345] user:podman group:podman \
service [2345] @podman:podman \
cgroup.containers,name:podman,delegate,mem.max:4G \
/usr/bin/podman system service -- Podman API
A daemon using `SCHED_RR` currently need to run outside the default cgroups.
A daemon using `SCHED_RR` currently needs to run outside the default cgroups.
service [...] <...> cgroup.root /path/to/daemon arg -- Real-Time process
@@ -102,12 +135,12 @@ Cgroup Delegation
For services that need to create their own child cgroups (container runtimes
like Docker, Podman, systemd-nspawn, LXC), use the `delegate` option:
service [2345] user:dockerd group:dockerd \
service [2345] @dockerd:dockerd \
cgroup.system,delegate /usr/bin/dockerd -- Docker daemon
Or with the old colon syntax:
service [2345] user:dockerd group:dockerd \
service [2345] @dockerd:dockerd \
cgroup.system:delegate /usr/bin/dockerd -- Docker daemon
This allows the container runtime to:
@@ -126,23 +159,36 @@ When delegation is enabled, Finit:
**Requirements:**
- The service should specify `user:` and `group:` for proper ownership
- The service should specify `@user:group` for proper ownership
- Controllers are delegated from the parent cgroup
**Example with additional config (new syntax):**
service [2345] user:podman group:podman \
service [2345] @podman:podman \
cgroup.containers,delegate,mem.max:4G \
/usr/bin/podman system service -- Podman API
**Or with old syntax:**
service [2345] user:podman group:podman \
service [2345] @podman:podman \
cgroup.containers:delegate,mem.max:4G \
/usr/bin/podman system service -- Podman API
Both examples delegate the cgroup while also setting a 4GB memory limit.
**Container template example:**
Here's a real-world example from [Infix OS](https://github.com/kernelkit/infix)
for running rootful podman container instances using delegation:
sysv log:prio:local1,tag:%i kill:30 pid:!/run/container:%i.pid \
pre:0,/usr/sbin/container cleanup:0,/usr/sbin/container \
cgroup.system,delegate \
[2345] <!> :%i container -n %i -- container %i
This template uses `sysv` type with delegation, demonstrating that cgroup
delegation works with different service types, not just `service`.
**Cgroup structure with delegation:**
Initially, the service process runs directly in the cgroup root:
+6 -1
View File
@@ -43,7 +43,7 @@ started at any time by running `initctl start <service>`.
Other run/task/service options are:
* `caps:...` -- see the [Linux Capabilities](capabilities.md) section
* `cgroups:...` -- see the [Cgroups](cgroups.md) section
* `cgroup.NAME[,opts]` or `cgroup:opts` -- see the [Cgroups](cgroups.md) section
* `env:[-]/path/to/env` -- see the [Service Environment](service-env.md) section
* `log:...` -- see [Redirecting Output](logging.md#redirecting-output)
* `nowarn` -- see [Conditional Loading](services.md#conditional-loading)
@@ -69,6 +69,11 @@ confines of that the following options are available:
* `respawn` -- bypasses the `restart` mechanism completely, allows
endless restarts. Useful in many use-cases, but not what `service`
was originally designed for so not the default behavior
* `remain:yes` -- for `run` and `task` only. Prevents the task from
re-running on runlevel re-entry and ensures the `post:` script runs
when the task is explicitly stopped or leaves its valid runlevels.
Similar to systemd's `RemainAfterExit=yes`. See [Task and Run](task-and-run.md)
for more details
* `oncrash:reboot` -- when all retries have failed, and the service
has *crashed*, if this option is set the system is rebooted
* `oncrash:script` -- similarly, but instead of rebooting, call the
+8
View File
@@ -47,6 +47,14 @@ waits for another service, `zebra`, to have created its PID file in
*all* files in `/var/run`, for each file named `*.pid`, or `*/pid`,
Finit opens it and find the matching `NAME:ID` using the PID.
The condition can be prefixed with `!` and/or `~`:
- `<!pid/zebra>` -- `ospfd` does not support `SIGHUP` (noreload)
- `<~pid/zebra>` -- propagate reload from `zebra` to `ospfd`
- `<!~pid/zebra>` -- both: noreload and propagate reload
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. A `pid` keyword can be set
to have Finit automatically create (when starting) and later remove
+41
View File
@@ -44,3 +44,44 @@ redirects can be used:
Please note, `;`, `&&`, `||`, and similar are *not supported*. Any
non-trivial constructs are better placed in a separate shell script.
remain:yes
----------
By default, a `run` or `task` will re-run each time its runlevel is
entered, and its `post:` script does not run on completion.
With `remain:yes`, the task runs once and does not re-run on runlevel
re-entry:
task [2345] remain:yes /usr/sbin/setup-firewall -- Firewall setup
This has the following effects:
* The task does not re-run on runlevel re-entry
* The `post:` script runs when:
- The task is explicitly stopped (`initctl stop NAME`)
- The task leaves its valid runlevels (e.g., runlevel change)
This is useful for tasks that set up persistent state where:
* Cleanup should only happen on explicit stop or when leaving valid runlevels
* The setup should not be re-run on every runlevel entry
**Example:** Setting up firewall rules with cleanup on shutdown:
```
task [2345] remain:yes \
post:/usr/sbin/teardown-firewall \
/usr/sbin/setup-firewall -- Firewall setup
```
The firewall rules are created once. The `post:` script runs when
entering runlevel 0 (halt) or 6 (reboot), or on explicit stop.
> [!NOTE]
> The `remain:yes` option is not supported for bootstrap-only tasks
> (tasks with only runlevel S). Bootstrap tasks are deleted immediately
> after completion, and their `post:` scripts never run. A warning is
> logged if `remain:yes` is used on such tasks.
+23
View File
@@ -288,4 +288,27 @@ Rescue mode can be disabled at build time with `configure --without-rescue`.
See the [Rescue Mode](config/rescue.md) section for more information.
**Switch Root**
Finit supports switching from an initramfs to a real root filesystem using
the built-in `initctl switch-root` command. This allows Finit to serve as
the init system in an initramfs for early boot tasks (LUKS unlock, LVM
activation, network boot) before transitioning to the real root.
```bash
# In initramfs, after mounting the real root:
initctl switch-root /mnt/root
```
The switch-root operation:
1. Runs the `HOOK_SWITCH_ROOT` hook for cleanup
2. Stops all services gracefully
3. Moves virtual filesystems (`/dev`, `/proc`, `/sys`, `/run`) to the new root
4. Deletes initramfs contents to free memory
5. Pivots to the new root and execs the new init
See the [Switch Root](switchroot.md) section for complete documentation.
[5]: https://en.wikipedia.org/wiki/Runlevel
+1
View File
@@ -39,6 +39,7 @@ Features
* [Cgroups v2](config/cgroups.md), both configuration and monitoring in [`initctl top`](initctl.md)
* [Plugin support](plugins.md) for customization
* Proper [rescue mode](config/rescue.md) with bundled `sulogin` for protected maintenance shell
* [Switch root](switchroot.md) support for initramfs-to-real-root transitions
* Integration with [watchdogd][] for full system supervision
* [Logging](config/logging.md) to kernel ring buffer before `syslogd` has started, see the
recommended [sysklogd][] project for complete logging integration
+22
View File
@@ -65,6 +65,7 @@ Commands:
halt Halt system
poweroff Halt and power off system
suspend Suspend system
switch-root NEWROOT [INIT] Switch to new root filesystem (initramfs only)
utmp show Raw dump of UTMP/WTMP db
```
@@ -143,3 +144,24 @@ Apr 8 12:37:46 alpine authpriv.info dropbear[2300]: Exit (root) from <192.168.1
Apr 8 15:02:11 alpine authpriv.info dropbear[2634]: Child connection from 192.168.121.1:48576
Apr 8 15:02:12 alpine authpriv.notice dropbear[2634]: Password auth succeeded for 'root' from 192.168.121.1:48576
```
Switch Root
-----------
The `switch-root` command is used when running Finit in an initramfs to
transition to the real root filesystem. This is similar to the standalone
`switch_root(8)` utility but integrated into Finit.
```
initctl switch-root /mnt/root [/sbin/init]
```
Requirements:
- Must be run during runlevel S (bootstrap) or runlevel 1
- The new root must be a mount point (different device than /)
- Can only be used when Finit is running as PID 1 in an initramfs
For complete documentation and usage examples, see the dedicated
[Switch Root](switchroot.md) section.
+10
View File
@@ -205,6 +205,16 @@ hook points:
new runlevel have been been stopped. When the hook has completed,
Finit continues to start all services in the new runlevel.
### Switch Root Hooks
* `HOOK_SWITCH_ROOT`, `hook/sys/switchroot`: Called when
`initctl switch-root` is issued, before the transition begins. Use
this hook to save state, unmount initramfs-only filesystems, or perform
cleanup before switching to the new root. Only runs when Finit is
operating as PID 1 in an initramfs.
See the [Switch Root](switchroot.md) section for more information.
### Shutdown Hooks
* `HOOK_NETWORK_DN`, `hook/net/down`: Called right after having changed
+148
View File
@@ -0,0 +1,148 @@
Switch Root
===========
Finit supports switching from an initramfs to a real root filesystem
using the `initctl switch-root` command. This is useful for systems
that use an initramfs for early boot (LUKS, LVM, network boot, etc.)
and need to transition to the real root before starting services.
Usage
-----
```sh
initctl switch-root NEWROOT [INIT]
```
- `NEWROOT`: Path to the mounted new root filesystem (e.g., `/mnt/root`)
- `INIT`: Optional path to init on the new root (default: `/sbin/init`)
Requirements
------------
1. Must be run during runlevel S (bootstrap) or runlevel 1
2. `NEWROOT` must be a mount point (different device than /)
3. `INIT` must exist and be executable on the new root
4. Finit must be running as PID 1 (in initramfs)
How It Works
------------
1. Runs `HOOK_SWITCH_ROOT` for any cleanup scripts/plugins
2. Runs `HOOK_SHUTDOWN` to notify plugins
3. Stops all services and kills remaining processes
4. Exits all plugins gracefully
5. Moves `/dev`, `/proc`, `/sys`, `/run` to new root
6. Deletes initramfs contents (if on tmpfs/ramfs) to free memory
7. Moves new root mount to `/`
8. Chroots to new root
9. Reopens `/dev/console` for stdin/stdout/stderr
10. Execs new init as PID 1
Example: Initramfs finit.conf
-----------------------------
Configuration file `/etc/finit.conf` in the initramfs:
```
# /etc/finit.conf in initramfs
# Mount the real root filesystem
run [S] name:mount-root /bin/mount /dev/sda1 /mnt/root -- Mounting root filesystem
# Switch to real root after mount completes
run [S] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root
```
For more complex setups (LUKS, LVM, etc.):
```
# Unlock LUKS volume
run [S] name:cryptsetup /sbin/cryptsetup open /dev/sda2 cryptroot -- Unlocking encrypted root
# Activate LVM
run [S] name:lvm /sbin/lvm vgchange -ay -- Activating LVM volumes
# Mount root
run [S] name:mount-root /bin/mount /dev/vg0/root /mnt/root -- Mounting root
# Switch root
run [S] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root
```
Example: Using Runlevel 1 for Switch Root
-----------------------------------------
For more complex initramfs setups where ordering of tasks becomes
difficult in runlevel S, you can perform the switch-root in runlevel 1:
```
# /etc/finit.conf in initramfs
# Start mdevd for device handling
service [S] name:mdevd notify:s6 /sbin/mdevd -D %n -- Device event daemon
run [S] name:coldplug <service/mdevd/ready> /sbin/mdevd-coldplug -- Coldplug devices
# Mount the real root filesystem (after devices are ready)
run [S] name:mount-root <run/coldplug/success> /bin/mount /dev/sda1 /mnt/root -- Mounting root
# Transition to runlevel 1 after all S tasks complete
# The switch-root runs cleanly in runlevel 1
run [1] name:switch-root /sbin/initctl switch-root /mnt/root -- Switching to real root
```
This approach separates the initramfs setup (runlevel S) from the
switch-root operation (runlevel 1), making task ordering simpler.
Hooks
-----
The `HOOK_SWITCH_ROOT` hook runs before the switch begins. Use it for:
- Saving state to the new root
- Unmounting initramfs-only mounts
- Cleanup tasks
Plugins can register for `HOOK_SWITCH_ROOT` just like other hooks:
```c
static void my_switch_root_hook(void *arg)
{
/* Cleanup before switch_root */
}
static plugin_t plugin = {
.name = "my-plugin",
.hook[HOOK_SWITCH_ROOT] = {
.cb = my_switch_root_hook
}
};
PLUGIN_INIT(plugin_init)
{
plugin_register(&plugin);
}
```
Conditions
----------
After switch_root, the new finit instance starts fresh. No conditions
or state are preserved across the switch. The new finit will:
1. Re-read `/etc/finit.conf` from the new root
2. Re-initialize all conditions
3. Start services according to the new configuration
See Also
--------
- [switch_root(8)](https://man7.org/linux/man-pages/man8/switch_root.8.html) - util-linux switch_root utility
- [Kernel initramfs documentation](https://docs.kernel.org/filesystems/ramfs-rootfs-initramfs.html)
+14
View File
@@ -398,6 +398,20 @@ mechanism completely, allows endless restarts. Useful in many
use-cases, but not what
.Cm service
was originally designed for so not the default behavior.
.It Cm remain:yes
for
.Cm run
and
.Cm task
only. Prevents the task from re-running on runlevel re-entry and
ensures the
.Cm post:
script runs when the task is explicitly stopped or leaves its valid
runlevels. Similar to systemd's
.Cm RemainAfterExit=yes .
.Pp
.Sy Note:
not supported for bootstrap-only tasks (runlevels [S] only).
.It Cm oncrash:reboot
when all retries have failed, and the service
has
+1
View File
@@ -79,6 +79,7 @@ nav:
- Limitations: config/limitations.md
- Usage:
- Commands & Status: initctl.md
- Switch Root: switchroot.md
- Rebooting & Halting: commands.md
- Command Line Options: cmdline.md
- Managing Services: service.md
+3 -3
View File
@@ -38,7 +38,7 @@
static void save(void *arg)
{
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "alsa-utils");
return;
}
@@ -51,7 +51,7 @@ static void save(void *arg)
static void restore(void *arg)
{
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "alsa-utils");
return;
}
@@ -62,7 +62,7 @@ static void restore(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "alsa-utils",
.hook[HOOK_BASEFS_UP] = { .cb = restore },
.hook[HOOK_SHUTDOWN] = { .cb = save }
};
+1 -1
View File
@@ -172,7 +172,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "bootmisc",
.hook[HOOK_MOUNT_POST] = { .cb = clean },
.hook[HOOK_BASEFS_UP] = { .cb = setup },
.depends = { "pidfile" },
+2 -2
View File
@@ -106,7 +106,7 @@ static void setup(void *arg)
char *cmd;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "dbus");
return;
}
@@ -164,7 +164,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "dbus",
.hook[HOOK_SVC_PLUGIN] = { .cb = setup },
};
+1 -1
View File
@@ -79,7 +79,7 @@ static void hscript_shutdown(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "hook-scripts",
.hook[HOOK_BANNER] = { .cb = hscript_banner },
.hook[HOOK_ROOTFS_UP] = { .cb = hscript_rootfs_up },
.hook[HOOK_MOUNT_ERROR] = { .cb = hscript_mount_error },
+1 -1
View File
@@ -228,7 +228,7 @@ static void coldplug(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "modprobe",
.hook[HOOK_BASEFS_UP] = { .cb = coldplug },
.depends = { "bootmisc", }
};
+1 -1
View File
@@ -213,7 +213,7 @@ static void load(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "modules-load",
.hook[HOOK_SVC_PLUGIN] = { .cb = load },
};
+22 -2
View File
@@ -406,9 +406,29 @@ static void nl_reconf(void *arg)
cond_reassert("net/");
}
/*
* Initial enumeration of existing interfaces and routes. Called from
* HOOK_SVC_PLUGIN which runs after the condition system is initialized.
* Interfaces that exist before finit starts (e.g., virtio-net in QEMU)
* won't generate RTM_NEWLINK events, so we must query for them.
*/
static void nl_enumerate(void *arg)
{
int sd;
sd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
if (sd < 0)
return;
nl_resync_ifaces(sd, 0);
nl_resync_routes(sd, 1);
close(sd);
}
static plugin_t plugin = {
.name = __FILE__,
.hook[HOOK_SVC_RECONF] = { .cb = nl_reconf },
.name = "netlink",
.hook[HOOK_SVC_RECONF] = { .cb = nl_reconf },
.hook[HOOK_SVC_PLUGIN] = { .cb = nl_enumerate },
.io = {
.cb = nl_callback,
.flags = PLUGIN_IO_READ,
+3 -2
View File
@@ -59,7 +59,7 @@ static int pidfile_add_path(struct iwatch *iw, char *path)
}
}
return iwatch_add(iw, path, IN_ONLYDIR | IN_CLOSE_WRITE);
return iwatch_add(iw, path, IN_ONLYDIR | IN_CLOSE_WRITE | IN_ATTRIB);
}
static void pidfile_update_conds(char *dir, char *name, uint32_t mask)
@@ -276,6 +276,7 @@ static void pidfile_reconf(void *arg)
if (cond_get(cond) == COND_ON)
continue;
dbg("Reassert condition %s", cond);
cond_set_path(cond_path(cond), COND_ON);
}
@@ -334,7 +335,7 @@ static void pidfile_init(void *arg)
* SIGSTP:ed (in state PAUSED) waiting for <net/iface/lo>.
*/
static plugin_t plugin = {
.name = __FILE__,
.name = "pidfile",
.hook[HOOK_BASEFS_UP] = { .cb = pidfile_init },
.hook[HOOK_SVC_RECONF] = { .cb = pidfile_reconf },
.depends = { "netlink" }, /* bootmisc depends on us */
+2 -2
View File
@@ -43,7 +43,7 @@ static void setup(void *arg)
glob_t gl;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "procps");
return;
}
@@ -69,7 +69,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "procps",
.hook[HOOK_BASEFS_UP] = {
.cb = setup
},
+1 -1
View File
@@ -51,7 +51,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "resolvconf",
.hook[HOOK_BASEFS_UP] = {
.cb = setup
},
+3 -3
View File
@@ -236,7 +236,7 @@ static void rtc_save(void *arg)
int fd, rc = 0;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "rtc");
return;
}
@@ -266,7 +266,7 @@ static void rtc_restore(void *arg)
int fd, rc = 0;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "rtc");
return;
}
@@ -321,7 +321,7 @@ static void update(uev_t *w, void *arg, int events)
static plugin_t plugin = {
.name = __FILE__,
.name = "rtc",
.hook[HOOK_BASEFS_UP] = {
.cb = rtc_restore
},
+1 -1
View File
@@ -180,7 +180,7 @@ static void sys_init(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "sys",
.hook[HOOK_BASEFS_UP] = { .cb = sys_init },
.depends = { "bootmisc", },
};
+1
View File
@@ -43,6 +43,7 @@
static void tty_watcher(void *arg, int fd, int events);
static plugin_t plugin = {
.name = "tty",
.io = {
.cb = tty_watcher,
.flags = PLUGIN_IO_READ,
+3 -3
View File
@@ -87,7 +87,7 @@ static void setup(void *arg)
int fd, err;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "urandom");
return;
}
@@ -188,7 +188,7 @@ static void save(void *arg)
mode_t prev;
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "urandom");
return;
}
@@ -202,7 +202,7 @@ static void save(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "urandom",
.hook[HOOK_BASEFS_UP] = { .cb = setup },
.hook[HOOK_SHUTDOWN] = { .cb = save },
.depends = { "bootmisc", }
+1 -1
View File
@@ -104,7 +104,7 @@ static void usr_init(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "usr",
.hook[HOOK_BASEFS_UP] = { .cb = usr_init },
.depends = { "bootmisc", },
};
+2 -2
View File
@@ -39,7 +39,7 @@
static void setup(void *arg)
{
if (rescue) {
dbg("Skipping %s plugin in rescue mode.", __FILE__);
dbg("Skipping %s plugin in rescue mode.", "x11-common");
return;
}
@@ -48,7 +48,7 @@ static void setup(void *arg)
}
static plugin_t plugin = {
.name = __FILE__,
.name = "x11-common",
.hook[HOOK_SVC_PLUGIN] = { .cb = setup },
};
+1
View File
@@ -63,6 +63,7 @@ finit_SOURCES = api.c cgroup.c cgroup.h \
exec.c finit.c finit.h \
stty.c \
helpers.c helpers.h \
initramfs.c \
iwatch.c iwatch.h \
log.c log.h \
mdadm.c mount.c \
+74
View File
@@ -112,6 +112,8 @@ static int restart(svc_t *svc, void *user_data)
static int reload(svc_t *svc, void *user_data)
{
char cond[MAX_COND_LEN];
(void)user_data;
if (!svc)
@@ -122,6 +124,25 @@ static int reload(svc_t *svc, void *user_data)
else
service_timeout_cancel(svc);
/*
* Clear conditions before reload to ensure dependent services
* are properly updated. Only needed when the service does NOT
* support SIGHUP (noreload), because then it will be stopped
* and restarted, so conditions genuinely go away. When the
* service handles SIGHUP, its PID and pidfile persist, so the
* condition stays valid and dependents should not be disrupted.
*
* Note: only clear 'ready' for services where the pidfile
* inotify handler reasserts it (pid/none). For s6/systemd
* services readiness relies on their respective notification
* mechanism which may not re-trigger on SIGHUP.
*/
if (svc_is_noreload(svc)) {
cond_clear(mkcond(svc, cond, sizeof(cond)));
if (svc->notify == SVC_NOTIFY_PID || svc->notify == SVC_NOTIFY_NONE)
service_ready(svc, 0);
}
svc_mark_dirty(svc);
service_step(svc);
@@ -245,6 +266,46 @@ static void bypass_shutdown(void *unused)
do_shutdown(halt);
}
/*
* Handle switch_root API command.
* Parses data: "newroot\0newinit\0"
* Sends ACK before attempting switch_root since it doesn't return on success.
* Returns: result from switch_root() on failure, doesn't return on success.
*/
static int do_switch_root_api(int sd, struct init_request *rq)
{
char *newroot, *newinit = NULL;
char *ptr;
int result;
dbg("switch-root %s", rq->data);
strterm(rq->data, sizeof(rq->data));
newroot = rq->data;
ptr = strchr(newroot, '\0');
if (ptr && ptr < rq->data + sizeof(rq->data) - 1) {
ptr++;
if (*ptr)
newinit = ptr;
}
/*
* Send ACK first, since we won't return from
* switch_root() on success.
*/
rq->cmd = INIT_CMD_ACK;
if (write(sd, rq, sizeof(*rq)) != sizeof(*rq))
dbg("Failed sending ACK to client");
close(sd);
/* This does not return on success */
result = switch_root(newroot, newinit);
if (result)
logit(LOG_ERR, "switch_root failed: %s", strerror(errno));
return result;
}
static int do_reboot(int cmd, int timeout, char *buf, size_t len)
{
int rc = 1;
@@ -403,6 +464,15 @@ static void api_cb(uev_t *w, void *arg, int events)
rq.cmd, rq.data);
goto leave;
}
break;
case INIT_CMD_SWITCH_ROOT:
if (runlevel != INIT_LEVEL && runlevel != 1) {
warnx("switch-root only allowed in runlevel S or 1");
goto done;
}
break;
default:
break;
}
@@ -503,6 +573,10 @@ static void api_cb(uev_t *w, void *arg, int events)
result = do_reboot(rq.cmd, rq.sleeptime, rq.data, sizeof(rq.data));
break;
case INIT_CMD_SWITCH_ROOT:
do_switch_root_api(sd, &rq);
goto leave;
case INIT_CMD_ACK:
dbg("Client failed reading ACK");
goto leave;
+36 -8
View File
@@ -34,6 +34,8 @@
#endif
#include <sys/mount.h>
#include <sys/sysinfo.h> /* get_nprocs_conf() */
#include <sys/vfs.h>
#include <linux/magic.h>
#include "cgroup.h"
#include "finit.h"
@@ -188,7 +190,8 @@ int cgroup_move_pid(const char *group, const char *name, int pid, int delegate)
}
if (rc) {
warn("Failed moving pid %d to cgroup %s", pid, path);
if (errno != ESRCH)
warn("Failed moving pid %d to cgroup %s", pid, path);
return -1;
}
}
@@ -377,7 +380,8 @@ static int cgroup_leaf_init(const char *group, const char *name, int pid, const
if (!strcmp(group, "root"))
return fnwrite(str("%d", pid), FINIT_CGPATH "/cgroup.procs");
if (cgroup_move_pid(group, name, pid, delegate))
/* Error if error is not "No such process" */
if (cgroup_move_pid(group, name, pid, delegate) && errno != ESRCH)
err(1, "Failed moving pid %d to group %s/%s", pid, group, name);
/* Set up inotify watch for cgroup cleanup */
@@ -841,6 +845,7 @@ void cgroup_config(void)
void cgroup_init(uev_ctx_t *ctx)
{
int opts = MS_NODEV | MS_NOEXEC | MS_NOSUID;
int mounted = 0;
char buf[80];
FILE *fp;
int fd;
@@ -851,14 +856,36 @@ void cgroup_init(uev_ctx_t *ctx)
#endif
if (mount("none", FINIT_CGPATH, "cgroup2", opts, NULL)) {
if (errno == ENOENT)
if (errno == EBUSY) {
/*
* Already mounted - this happens after switch_root
* when cgroups were moved from the initramfs.
* Verify it's actually cgroup2 before proceeding.
*/
struct statfs sfs;
if (statfs(FINIT_CGPATH, &sfs) || sfs.f_type != CGROUP2_SUPER_MAGIC) {
logit(LOG_ERR, "Mount point %s busy but not cgroup2", FINIT_CGPATH);
avail = 0;
return;
}
dbg("cgroup2 already mounted at %s, reusing", FINIT_CGPATH);
} else if (errno == ENOENT) {
logit(LOG_INFO, "Kernel does not support cgroups v2, disabling.");
else if (errno == EPERM) /* Probably inside an unprivileged container */
avail = 0;
return;
} else if (errno == EPERM) {
/* Probably inside an unprivileged container */
logit(LOG_INFO, "Not allowed to mount cgroups v2, disabling.");
else
avail = 0;
return;
} else {
err(1, "Failed mounting cgroup v2");
avail = 0;
return;
avail = 0;
return;
}
} else {
mounted = 1;
}
avail = 1;
@@ -867,7 +894,8 @@ void cgroup_init(uev_ctx_t *ctx)
if (!fp) {
err(1, "Failed opening %s", FINIT_CGPATH "/cgroup.controllers");
abort:
umount(FINIT_CGPATH);
if (mounted)
umount(FINIT_CGPATH);
avail = 0;
return;
}
+5 -4
View File
@@ -37,6 +37,11 @@
#include "finit.h"
#include "log.h"
/* clone3() was added in Linux 5.3, syscall number 435 on all archs */
#ifndef __NR_clone3
#define __NR_clone3 435
#endif
/* Clone3 flags from linux/sched.h */
#ifndef CLONE_INTO_CGROUP
#define CLONE_INTO_CGROUP 0x200000000ULL
@@ -57,11 +62,7 @@ struct clone3_args {
uint64_t cgroup; /* Cgroup file descriptor */
} __attribute__((aligned(8)));
#ifdef __NR_clone3
static int use_clone3 = 1;
#else
static int use_clone3 = 0;
#endif
int has_clone3(void)
{
+15 -11
View File
@@ -121,9 +121,6 @@ static uev_t etcw;
static TAILQ_HEAD(, conf_change) conf_change_list = TAILQ_HEAD_INITIALIZER(conf_change_list);
static char *path;
static char *shell;
static int parse_conf(char *file, int is_rcsd);
static void drop_changes(void);
@@ -377,8 +374,6 @@ void conf_parse_cmdline(int argc, char *argv[])
fstab = strdup(ptr);
finit_conf = strdup(FINIT_CONF);
finit_rcsd = strdup(FINIT_RCSD);
path = getenv("PATH");
shell = getenv("SHELL");
for (int i = 1; i < argc; i++)
parse_arg(argv[i]);
@@ -404,13 +399,9 @@ void conf_reset_env(void)
free(node);
}
if (path)
setenv("PATH", path, 1);
else
if (!getenv("PATH"))
setenv("PATH", _PATH_STDPATH, 1);
if (shell)
setenv("SHELL", shell, 1);
else
if (!getenv("SHELL"))
setenv("SHELL", _PATH_BSHELL, 1);
setenv("LOGNAME", "root", 1);
setenv("USER", "root", 1);
@@ -745,8 +736,21 @@ void conf_parse_cond(svc_t *svc, char *cond)
return;
}
/*
* The '~' prefix means a reload of the upstream service is
* propagated to this service -- it will be reloaded (SIGHUP)
* or restarted (noreload), not just paused and resumed.
* Syntax: <!~pid/foo,~pid/bar> or <~pid/foo>
*
* Strip '~' from each condition and set the service-level
* flag. This allows '~' on any condition in the list.
*/
svc->cond[0] = 0;
for (i = 0, c = strtok(ptr, ","); c; c = strtok(NULL, ","), i++) {
if (c[0] == '~') {
c++;
svc->flux_reload = 1;
}
devmon_add_cond(c);
if (i)
strlcat(svc->cond, ",", sizeof(svc->cond));
+20 -1
View File
@@ -75,6 +75,7 @@ static void drop_node(struct dev_node *node)
void devmon_add_cond(const char *cond)
{
struct dev_node *node;
char path[PATH_MAX];
if (!cond || strncmp(cond, "dev/", 4)) {
// dbg("no match %s", cond ?: "<NIL>");
@@ -103,16 +104,34 @@ void devmon_add_cond(const char *cond)
node->refcnt = 1;
TAILQ_INSERT_TAIL(&dev_node_list, node, link);
snprintf(path, sizeof(path), "/%s", cond);
if (fexist(path))
cond_set(cond);
}
void devmon_del_cond(const char *cond)
{
if (!cond || strcmp(cond, "dev/"))
if (!cond || strncmp(cond, COND_DEV, strlen(COND_DEV)))
return;
drop_node(find_node(cond));
}
void devmon_reconf(void)
{
struct dev_node *node, *tmp;
char path[PATH_MAX];
TAILQ_FOREACH_SAFE(node, &dev_node_list, link, tmp) {
snprintf(path, sizeof(path), "/%s", node->name);
if (fexist(path))
cond_set(node->name);
else
cond_clear(node->name);
}
}
static int devmon_add_path(struct iwatch *iw, char *path)
{
char *ptr;
+1
View File
@@ -27,6 +27,7 @@
void devmon_add_cond(const char *cond);
void devmon_del_cond(const char *cond);
void devmon_reconf(void);
void devmon_init(uev_ctx_t *ctx);
#endif /* FINIT_DEVMON_H_ */
+4
View File
@@ -523,6 +523,10 @@ static void fs_init(void)
if (fismnt(fs[i].file))
continue;
/* Create mount point if it doesn't exist */
if (mkdir(fs[i].file, 0755) && errno != EEXIST)
warn("Failed creating mountpoint %s: %s", fs[i].file, strerror(errno));
fs_mount(fs[i].spec, fs[i].file, fs[i].type, 0, NULL);
}
}
+1
View File
@@ -98,6 +98,7 @@
#define INIT_CMD_HALT 21
#define INIT_CMD_POWEROFF 22
#define INIT_CMD_SUSPEND 23
#define INIT_CMD_SWITCH_ROOT 24 /* Switch to new root filesystem */
#define INIT_CMD_WDOG_HELLO 128 /* Watchdog register and hello */
#define INIT_CMD_SVC_ITER 129
#define INIT_CMD_SVC_QUERY 130
+125 -10
View File
@@ -646,6 +646,59 @@ int do_halt (char *arg) { return do_cmd(INIT_CMD_HALT); }
int do_poweroff(char *arg) { return do_cmd(INIT_CMD_POWEROFF); }
int do_suspend (char *arg) { return do_cmd(INIT_CMD_SUSPEND); }
/**
* do_switch_root - Switch to a new root filesystem (initramfs only)
* @argc: Number of arguments (1 or 2)
* @argv: [0] = newroot path, [1] = optional init path
*
* This command is only valid during runlevel S (bootstrap) or 1 when
* running from an initramfs. It stops all services, moves virtual
* filesystems, and exec's the new init.
*/
int do_switch_root(int argc, char *argv[])
{
struct init_request rq = {
.magic = INIT_MAGIC,
.cmd = INIT_CMD_SWITCH_ROOT,
};
char *newroot, *newinit = NULL;
size_t off;
if (argc < 1)
ERRX(2, "Usage: initctl switch-root NEWROOT [INIT]");
newroot = argv[0];
if (argc > 1)
newinit = argv[1];
/* Verify newroot exists */
if (access(newroot, F_OK))
ERR(1, "Cannot access %s", newroot);
/*
* Pack both strings into rq.data with null separators:
* "newroot\0newinit\0"
*/
strlcpy(rq.data, newroot, sizeof(rq.data));
off = strlen(newroot) + 1;
if (newinit && off < sizeof(rq.data) - 1)
strlcpy(rq.data + off, newinit, sizeof(rq.data) - off);
printf("Switching root to %s", newroot);
if (newinit)
printf(", init %s", newinit);
printf(" ...\n");
/*
* On success, finit exec's new init and we lose connection.
* A "failure" to read reply is actually expected on success.
*/
client_send(&rq, sizeof(rq));
return 0;
}
int utmp_show(char *file)
{
struct utmp *ut;
@@ -983,6 +1036,62 @@ static int svc_compare(svc_t *svc, char *arg)
return 0;
}
/*
* Escape a string for safe JSON output. Handles quotes, backslashes,
* and control characters. Returns pointer to static buffer.
*/
static char *json_escape(const char *str)
{
static char buf[1024];
char *ptr = buf;
size_t left = sizeof(buf) - 1;
if (!str)
return "";
while (*str && left > 1) {
char c = *str++;
switch (c) {
case '"':
case '\\':
*ptr++ = '\\';
*ptr++ = c;
left -= 2;
break;
case '\n':
*ptr++ = '\\';
*ptr++ = 'n';
left -= 2;
break;
case '\r':
*ptr++ = '\\';
*ptr++ = 'r';
left -= 2;
break;
case '\t':
*ptr++ = '\\';
*ptr++ = 't';
left -= 2;
break;
default:
if ((unsigned char)c < 0x20) {
/* Other control chars: use \uXXXX */
int n = snprintf(ptr, left, "\\u%04x", (unsigned char)c);
ptr += n;
left -= n;
} else {
*ptr++ = c;
left--;
}
break;
}
}
*ptr = '\0';
return buf;
}
static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
{
long now = jiffies();
@@ -998,14 +1107,16 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
fprintf(fp,
"%s"
"%s{\n"
"%s \"identity\": \"%s\",\n"
"%s \"description\": \"%s\",\n"
"%s \"identity\": \"%s\",\n",
prev ? ",\n" : indent, prev ? indent : "",
indent, svc_ident(svc, NULL, 0));
fprintf(fp,
"%s \"description\": \"%s\",\n",
indent, json_escape(svc->desc));
fprintf(fp,
"%s \"type\": \"%s\",\n"
"%s \"forking\": %s,\n"
"%s \"status\": \"%s\",\n",
prev ? ",\n" : indent, prev ? indent : "",
indent, svc_ident(svc, NULL, 0),
indent, svc->desc,
indent, svc_typestr(svc),
indent, svc->forking ? "true" : "false",
indent, svc_status(svc));
@@ -1027,15 +1138,17 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
}
fprintf(fp,
"%s \"origin\": \"%s\",\n"
"%s \"origin\": \"%s\",\n",
indent, svc->file[0] ? svc->file : "built-in");
svc_command(svc, buf, sizeof(buf), 0);
fprintf(fp,
"%s \"command\": \"%s\",\n",
indent, svc->file[0] ? svc->file : "built-in",
indent, svc_command(svc, buf, sizeof(buf), 0));
indent, json_escape(buf));
svc_environ(svc, buf, sizeof(buf), 0);
if (buf[0])
fprintf(fp,
"%s \"environment\": \"%s\",\n", indent, buf);
"%s \"environment\": \"%s\",\n", indent, json_escape(buf));
svc_cond(svc, buf, sizeof(buf), 0);
if (buf[0])
@@ -1474,7 +1587,8 @@ static int usage(int rc)
" reboot Reboot system\n"
" halt Halt system\n"
" poweroff Halt and power off system\n"
" suspend Suspend system\n");
" suspend Suspend system\n"
" switch-root ROOT [INIT] Switch to new root filesystem (initramfs)\n");
if (utmp)
fprintf(stderr,
@@ -1654,6 +1768,7 @@ int main(int argc, char *argv[])
{ "halt", NULL, do_halt, NULL, NULL },
{ "poweroff", NULL, do_poweroff, NULL, NULL },
{ "suspend", NULL, do_suspend, NULL, NULL },
{ "switch-root", NULL, NULL, NULL, do_switch_root },
{ "utmp", NULL, do_utmp, &utmp, NULL },
{ NULL, NULL, NULL, NULL, NULL }
+293
View File
@@ -0,0 +1,293 @@
/* Initramfs switch_root support
*
* Copyright (c) 2008-2025 Joachim Wiberg <troglobit@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "config.h" /* Generated by configure script */
#include <errno.h>
#include <fcntl.h>
#include <ftw.h>
#include <limits.h>
#include <string.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/magic.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include "finit.h"
#include "cond.h"
#include "helpers.h"
#include "log.h"
#include "plugin.h"
#include "private.h"
#include "sig.h"
#include "util.h"
/* State for delete_cb(), nftw() doesn't support passing user data */
static const char *delete_skip;
static dev_t delete_rootdev;
/*
* nftw() callback to delete initramfs contents before switch_root.
* Skips the new root mount point and stays on the same filesystem.
*/
static int delete_cb(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw)
{
(void)tflag;
/* Skip root directory itself */
if (ftw->level == 0)
return 0;
/* Skip the new root mount point */
if (delete_skip && !strncmp(fpath, delete_skip, strlen(delete_skip)))
return 0;
/* Stay on the same filesystem (initramfs) */
if (sb->st_dev != delete_rootdev)
return 0;
remove(fpath);
return 0;
}
/*
* Recursively delete initramfs contents to free memory before switch_root.
*/
static void delete_initramfs_contents(dev_t rootdev, const char *skip)
{
delete_rootdev = rootdev;
delete_skip = skip;
nftw("/", delete_cb, 20, FTW_DEPTH | FTW_PHYS | FTW_MOUNT);
}
/*
* Check if we're running on a tmpfs/ramfs (initramfs)
*/
static int is_initramfs(void)
{
struct statfs sfs;
if (statfs("/", &sfs))
return 0;
return sfs.f_type == RAMFS_MAGIC || sfs.f_type == TMPFS_MAGIC;
}
/*
* Move a mount point from oldpath to newpath under newroot
*/
static int do_move_mount(const char *oldpath, const char *newroot)
{
char newpath[PATH_MAX];
struct stat st;
if (stat(oldpath, &st))
return 0; /* Not mounted, skip */
snprintf(newpath, sizeof(newpath), "%s%s", newroot, oldpath);
/* Create target directory if needed */
makedir(newpath, 0755);
if (mount(oldpath, newpath, NULL, MS_MOVE, NULL)) {
dbg("Failed to move %s to %s: %s", oldpath, newpath, strerror(errno));
return -1;
}
dbg("Moved %s to %s", oldpath, newpath);
return 0;
}
static int kill_cb(int pid, void *data)
{
int signo = *(int *)data;
kill(pid, signo);
return 0;
}
/*
* Perform switch_root to a new root filesystem
*
* This function does not return on success - it exec's the new init.
* On failure, it returns -1 and sets errno.
*/
int switch_root(const char *newroot, const char *newinit)
{
struct stat newroot_st, oldroot_st;
char init_path[PATH_MAX];
int console_fd;
dev_t rootdev;
int signo;
if (!newroot || !newroot[0]) {
errno = EINVAL;
return -1;
}
/* Default to /sbin/init if not specified */
if (!newinit || !newinit[0])
newinit = "/sbin/init";
/* Verify we're PID 1 */
if (getpid() != 1) {
logit(LOG_ERR, "switch_root must be run as PID 1");
errno = EPERM;
return -1;
}
/* Verify newroot exists and is a directory */
if (stat(newroot, &newroot_st) || !S_ISDIR(newroot_st.st_mode)) {
logit(LOG_ERR, "switch_root: %s is not a directory", newroot);
errno = ENOTDIR;
return -1;
}
/* Verify newroot is a mount point (different device than parent) */
if (stat("/", &oldroot_st)) {
logit(LOG_ERR, "switch_root: cannot stat /");
return -1;
}
if (newroot_st.st_dev == oldroot_st.st_dev) {
logit(LOG_ERR, "switch_root: %s is not a mount point", newroot);
errno = EINVAL;
return -1;
}
/* Verify init exists in new root */
snprintf(init_path, sizeof(init_path), "%s%s", newroot, newinit);
if (access(init_path, X_OK)) {
logit(LOG_ERR, "switch_root: %s not found or not executable", init_path);
errno = ENOENT;
return -1;
}
logit(LOG_NOTICE, "Performing switch_root to %s, init %s", newroot, newinit);
/* Run switch_root hook before we start tearing things down */
plugin_run_hooks(HOOK_SWITCH_ROOT);
/* Stop all services gracefully */
dbg("Stopping all services...");
halt = SHUT_OFF; /* Prevent actual shutdown */
/* Use existing shutdown logic to stop services */
api_exit();
log_exit();
plugin_run_hooks(HOOK_SHUTDOWN);
/* Kill remaining processes (except kernel threads and ourselves) */
signo = SIGTERM;
iterate_proc(kill_cb, &signo);
do_usleep(500000); /* Give them 500ms */
signo = SIGKILL;
iterate_proc(kill_cb, &signo);
/* Reap zombies */
while (waitpid(-1, NULL, WNOHANG) > 0)
;
/* Exit plugins */
plugin_exit();
cond_exit();
/* Move virtual filesystems to new root */
dbg("Moving virtual filesystems...");
do_move_mount("/dev", newroot);
do_move_mount("/proc", newroot);
do_move_mount("/sys", newroot);
do_move_mount("/run", newroot);
/* Change to new root directory */
if (chdir(newroot)) {
err(1, "Failed to chdir to %s", newroot);
return -1;
}
/* Delete contents of old root if we're on initramfs */
rootdev = oldroot_st.st_dev;
if (is_initramfs()) {
dbg("Deleting initramfs contents...");
delete_initramfs_contents(rootdev, newroot);
}
/* Mount --move newroot to / */
if (mount(newroot, "/", NULL, MS_MOVE, NULL)) {
err(1, "Failed to move %s to /", newroot);
return -1;
}
/* chroot to new root */
if (chroot(".")) {
err(1, "Failed to chroot to new root");
return -1;
}
if (chdir("/")) {
err(1, "Failed to chdir to /");
return -1;
}
/* Reopen console */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
console_fd = open("/dev/console", O_RDWR);
if (console_fd >= 0) {
dup2(console_fd, STDIN_FILENO);
dup2(console_fd, STDOUT_FILENO);
dup2(console_fd, STDERR_FILENO);
if (console_fd > STDERR_FILENO)
close(console_fd);
}
/* Reset signals to default */
sig_unblock();
/* Exec the new init - this does not return on success */
dbg("Executing %s...", newinit);
execl(newinit, newinit, NULL);
/* If we get here, exec failed */
err(1, "Failed to exec %s", newinit);
return -1;
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+3
View File
@@ -81,6 +81,9 @@
CHOOSE(HOOK_SVC_RECONF, "nop"), \
CHOOSE(HOOK_RUNLEVEL_CHANGE, "nop"), \
\
/* Switch root hook, before transitioning */ \
CHOOSE(HOOK_SWITCH_ROOT, "hook/sys/switchroot"), \
\
/* Shutdown hooks, runlevel [06] */ \
CHOOSE(HOOK_NETWORK_DN, "hook/net/down"), \
CHOOSE(HOOK_SHUTDOWN, "hook/sys/shutdown"), \
+3
View File
@@ -59,6 +59,9 @@ void plugin_script_run(hook_point_t no);
int plugin_init (uev_ctx_t *ctx);
void plugin_exit (void);
void iterate_proc (int (*cb)(int, void *), void *data);
int switch_root (const char *newroot, const char *newinit);
#endif /* FINIT_PRIVATE_H_ */
/**
+129 -53
View File
@@ -627,8 +627,11 @@ static pid_t service_fork(svc_t *svc)
set_uid(uid, svc);
/* Set default path for regular users */
if (uid > 0)
if (uid > 0) {
setenv("PATH", _PATH_DEFPATH, 1);
setenv("USER", svc->username, 1);
setenv("LOGNAME", svc->username, 1);
}
if (home) {
setenv("HOME", home, 1);
if (chdir(home)) {
@@ -1028,7 +1031,6 @@ static int service_run_script(svc_t *svc, char *script)
{
const char *id = svc_ident(svc, NULL, 0);
pid_t pid = service_fork(svc);
int status, rc;
if (pid < 0) {
err(1, "%s: failed forking off script %s", id, script);
@@ -1044,6 +1046,8 @@ static int service_run_script(svc_t *svc, char *script)
};
char pidbuf[16];
redirect(svc);
snprintf(pidbuf, sizeof(pidbuf), "%d", svc->pid);
setenv("MAINPID", pidbuf, 1);
@@ -1053,23 +1057,7 @@ static int service_run_script(svc_t *svc, char *script)
}
dbg("%s: script '%s' started as PID %d", id, script, pid);
if (waitpid(pid, &status, 0) == -1) {
warn("%s: failed calling script %s", id, script);
return -1;
}
rc = WEXITSTATUS(status);
if (WIFEXITED(status)) {
dbg("%s: script '%s' exited without signal, status: %d", id, script, rc);
} else if (WIFSIGNALED(status)) {
dbg("%s: script '%s' terminated by signal %d", id, script, WTERMSIG(status));
if (!rc)
rc = 1;
} else {
dbg("%s: script '%s' exited with status: %d", id, script, rc);
}
return rc;
return service_script_add(svc, pid, svc->killdelay);
}
/* Ensure we don't have any notify socket lingering */
@@ -1090,15 +1078,31 @@ static void service_notify_stop(svc_t *svc)
*/
static void service_cleanup(svc_t *svc)
{
char *fn;
char cond[MAX_COND_LEN];
/* PID collected, cancel any pending SIGKILL */
service_timeout_cancel(svc);
fn = pid_file(svc);
if (fn && remove(fn) && errno != ENOENT)
logit(LOG_CRIT, "Failed removing service %s pidfile %s",
svc_ident(svc, NULL, 0), fn);
/* Only clean up process' pidfile if managed by us */
if (svc_has_pidfile(svc)) {
char *fn = pid_file(svc);
if (remove(fn) && errno != ENOENT)
logit(LOG_CRIT, "Failed removing service %s pidfile %s",
svc_ident(svc, NULL, 0), fn);
}
/*
* Invalidate the pid/ condition for this service to ensure
* dependent services are properly stopped and restarted.
* Without this, the condition is only cleared asynchronously
* via inotify on pidfile removal, which may not trigger when
* the daemon fails to clean up its own pidfile, or when the
* service dies during a reload cycle and goes directly from
* RUNNING to HALTED (skipping STOPPING where cond_clear()
* is normally called).
*/
cond_clear(mkcond(svc, cond, sizeof(cond)));
service_notify_stop(svc);
@@ -1137,7 +1141,7 @@ int service_stop(svc_t *svc)
service_timeout_cancel(svc);
if (svc->stop_script[0]) {
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], calling stop:%s ...", id, svc->pid, svc->stop_script);
logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], calling stop:%s ...", id, svc->pid, svc->stop_script);
} else if (!svc_is_sysv(svc)) {
char *nm = pid_get_name(svc->pid, NULL, 0);
const char *sig = sig_name(svc->sighalt);
@@ -1152,10 +1156,10 @@ int service_stop(svc_t *svc)
}
dbg("Sending %s to pid:%d name:%s(%s)", sig, svc->pid, id, nm);
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], stopping, sending %s ...", id, svc->pid, sig);
logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], sending %s ...", id, svc->pid, sig);
} else {
compose_cmdline(svc, cmdline, sizeof(cmdline));
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], calling '%s stop' ...", id, svc->pid, cmdline);
logit(LOG_CONSOLE | LOG_NOTICE, "Stopping %s[%d], calling '%s stop' ...", id, svc->pid, cmdline);
}
/*
@@ -1275,7 +1279,7 @@ static int service_reload(svc_t *svc)
print_desc("Restarting ", svc->desc);
if (svc->reload_script[0]) {
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], calling reload:%s ...", id, svc->pid, svc->reload_script);
logit(LOG_CONSOLE | LOG_NOTICE, "Reloading %s[%d], calling reload:%s ...", id, svc->pid, svc->reload_script);
rc = service_run_script(svc, svc->reload_script);
} else if (svc->sighup) {
if (svc->pid <= 1) {
@@ -1283,8 +1287,8 @@ static int service_reload(svc_t *svc)
svc->start_time = svc->pid = 0;
goto done;
}
dbg("%s[%d], sending SIGHUP", id, svc->pid);
logit(LOG_CONSOLE | LOG_NOTICE, "%s[%d], sending SIGHUP ...", id, svc->pid);
dbg("Reloading %s[%d], sending SIGHUP", id, svc->pid);
logit(LOG_CONSOLE | LOG_NOTICE, "Reloading %s[%d], sending SIGHUP ...", id, svc->pid);
rc = kill(svc->pid, SIGHUP);
if (rc == -1 && (errno == ESRCH || errno == ENOENT)) {
/* nobody home, reset internal state machine */
@@ -1795,7 +1799,7 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
char *dev = NULL;
int respawn = 0;
int levels = 0;
int forking = 0, manual = 0, nowarn = 0;
int forking = 0, manual = 0, remain = 0, nowarn = 0;
int restart_max = SVC_RESPAWN_MAX;
int restart_tmo = 0;
unsigned oncrash_action = SVC_ONCRASH_IGNORE;
@@ -1863,6 +1867,8 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
forking = 1;
else if (MATCH_CMD(cmd, "manual:yes", arg))
manual = 1;
else if (MATCH_CMD(cmd, "remain:yes", arg))
remain = 1;
else if (MATCH_CMD(cmd, "restart:", arg)) {
if (MATCH_CMD(arg, "always", arg))
restart_max = -1;
@@ -2168,6 +2174,18 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
memset(svc->ifstmt, 0, sizeof(svc->ifstmt));
svc->manual = manual;
svc->nowarn = nowarn;
/*
* remain:yes is not supported for bootstrap-only tasks. These
* tasks are deleted immediately after completion and their post:
* scripts never run. This is by design.
*/
if (remain && svc_is_runtask(svc) && !ISOTHER(levels, INIT_LEVEL)) {
logit(LOG_WARNING, "%s: remain:yes ignored for bootstrap-only tasks",
svc_ident(svc, NULL, 0));
remain = 0;
}
svc->remain = remain;
svc->respawn = respawn;
svc->forking = forking;
svc->restart_max = restart_max;
@@ -2191,15 +2209,20 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
/* only set forking based on pidfile if user supplied pid: option */
if (pid && svc->pidfile[0] == '!')
svc->forking = 1;
if (svc->restart_tmo == 0) {
if (svc_is_forking(svc))
svc->restart_tmo = 2000;
else
svc->restart_tmo = 1;
}
}
/* Set default restart_tmo for services and TTYs that can restart */
if (svc_is_daemon(svc) && svc->restart_tmo == 0) {
if (svc_is_forking(svc))
svc->restart_tmo = 2000;
else
svc->restart_tmo = 1;
}
/* TTYs need a longer default to throttle errors (e.g., missing device) */
if (svc_is_tty(svc) && svc->restart_tmo == 0)
svc->restart_tmo = 2000;
/* Set configured limits */
memcpy(svc->rlimit, rlimit, sizeof(svc->rlimit));
@@ -2395,7 +2418,21 @@ void service_update_rdeps(void)
if (!svc_is_noreload(svc))
continue; /* Yup, no need to stop start rdeps */
svc_mark_affected(mkcond(svc, cond, sizeof(cond)));
/*
* Clear the condition immediately, before service_step_all()
* runs. cond_clear() calls cond_update() which calls
* service_step() on all affected services right now. Those
* services see COND_OFF and get service_stop() called,
* transitioning to STOPPING_STATE before we ever send SIGTERM
* to this service. Without this, the condition is only cleared
* after the service dies, by which time reverse-dependencies
* may have already crashed due to the lost connection.
* See also: api.c do_reload() which does the same for direct
* 'initctl reload <svc>' calls.
*/
mkcond(svc, cond, sizeof(cond));
cond_clear(cond);
svc_mark_affected(cond);
}
}
@@ -2628,7 +2665,6 @@ static void service_cleanup_script(svc_t *svc)
static void service_retry(svc_t *svc)
{
char *restart_cnt = (char *)&svc->restart_cnt;
int rc = WEXITSTATUS(svc->status);
int timeout;
service_timeout_cancel(svc);
@@ -2638,17 +2674,10 @@ static void service_retry(svc_t *svc)
if (svc->respawn) {
/*
* Non-zero exit indicates an error that may not be resolved
* by immediate retry. Add delay to prevent busy-loop and to
* rate-limit retries, e.g. when TTY device doesn't exist.
* Respawn services (TTYs) that exited with non-zero status
* have already been delayed in the SVC_RUNNING_STATE handler.
* Just restart now.
*/
if (WIFEXITED(svc->status) && rc != 0) {
dbg("%s exited with error %d, delaying respawn ...",
svc_ident(svc, NULL, 0), rc);
service_timeout_after(svc, svc->restart_tmo, service_retry);
return;
}
dbg("%s crashed/exited, respawning ...", svc_ident(svc, NULL, 0));
svc_unblock(svc);
service_step(svc);
@@ -2859,8 +2888,12 @@ restart:
if (svc_is_removed(svc) && svc_has_cleanup(svc)) {
svc_set_state(svc, SVC_CLEANUP_STATE);
service_cleanup_script(svc);
} else
} else {
/* Unblock remain tasks after post script so they can restart */
if (svc_is_remain(svc))
svc_unblock(svc);
svc_set_state(svc, SVC_HALTED_STATE);
}
}
break;
@@ -2876,9 +2909,16 @@ restart:
break;
case SVC_DONE_STATE:
if (svc_is_changed(svc))
/* Remain tasks: always run post script when stopped, even if config changed */
if (svc_is_runtask(svc) && svc_is_remain(svc) && svc_is_stopped(svc)) {
if (svc_has_post(svc)) {
svc_set_state(svc, SVC_TEARDOWN_STATE);
service_post_script(svc);
} else
svc_set_state(svc, SVC_HALTED_STATE);
} else if (svc_is_changed(svc))
svc_set_state(svc, SVC_HALTED_STATE);
if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled)
else if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled)
svc_set_state(svc, SVC_WAITING_STATE);
break;
@@ -3109,6 +3149,19 @@ restart:
case COND_ON:
kill(svc->pid, SIGCONT);
svc_set_state(svc, SVC_RUNNING_STATE);
/*
* Propagate reload (~): upstream reloaded, so
* we must also reload/restart, not just resume.
*/
if (svc->flux_reload && !svc_is_changed(svc)) {
if (svc_is_noreload(svc))
service_stop(svc);
else
service_reload(svc);
break;
}
/* Reassert condition if we go from waiting and no change */
if (!svc_is_changed(svc)) {
if (svc->notify == SVC_NOTIFY_PID) {
@@ -3180,6 +3233,29 @@ void service_runtask_clean(void)
if (!svc_is_runtask(svc))
continue;
/* Remain tasks stay in DONE state if still valid in new runlevel */
if (svc_is_remain(svc) && svc->state == SVC_DONE_STATE) {
if (svc_in_runlevel(svc, runlevel) && !svc_is_changed(svc))
continue; /* Keep once flag, stay in DONE */
/* Config changed or leaving runlevel: stop, run post, restart */
svc->once = 0;
svc_stop(svc);
service_step(svc);
continue;
}
/*
* On reload (SIGHUP), only remain tasks (handled above) should
* have their once flag cleared and be restarted. Regular run/task
* that already ran in this runlevel must not run again.
*
* On runlevel change, continue below to reset once flag so tasks
* can run again in the new runlevel.
*/
if (sm_in_reload())
continue;
/* run/task declared with <!> */
if (svc->sighup)
svc->once = 1;
+12 -4
View File
@@ -77,7 +77,9 @@
#endif
#include <string.h> /* strerror() */
#include <sched.h>
#include <sys/mount.h>
#include <sys/reboot.h>
#include <sys/stat.h>
#include <sys/wait.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
@@ -206,7 +208,7 @@ static void fs_swapoff(void)
*
* https://www.freedesktop.org/wiki/Software/systemd/RootStorageDaemons/
*/
void do_iterate_proc(int (*cb)(int, void *), void *data)
void iterate_proc(int (*cb)(int, void *), void *data)
{
DIR *dirp;
@@ -288,7 +290,7 @@ static int do_wait(int secs)
;
has_proc = 0;
iterations--;
do_iterate_proc(status_cb, &has_proc);
iterate_proc(status_cb, &has_proc);
}
while (has_proc && iterations > 0);
@@ -301,6 +303,8 @@ void do_shutdown(shutop_t op)
int in_cont = in_container();
int signo = SIGTERM;
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: entered, op=%d\n", op);
if (!in_cont) {
/*
* On a PREEMPT-RT system, Finit must run as the highest prioritized
@@ -320,13 +324,15 @@ void do_shutdown(shutop_t op)
* Tell remaining non-monitored processes to exit, give them
* time to exit gracefully, 2 sec was customary, we go for 1.
*/
do_iterate_proc(kill_cb, &signo);
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: killing remaining processes ...\n");
iterate_proc(kill_cb, &signo);
if (do_wait(1)) {
signo = SIGKILL;
do_iterate_proc(kill_cb, &signo);
iterate_proc(kill_cb, &signo);
}
/* Exit plugins gracefully */
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: plugin_exit() + cond_exit() ...\n");
plugin_exit();
/* We can no longer act on conditions, activate script runner */
@@ -349,12 +355,14 @@ void do_shutdown(shutop_t op)
for (int fd = 3; fd < 128; fd++)
close(fd);
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: vfork, child will reboot ...\n");
if (vfork()) {
/*
* Put PID 1 aside and let child perform reboot/halt
* kernel may exit child and we don't want to exit PID 1
* ... causing "aiii killing init" during reboot ...
*/
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: PID 1 returning from vfork\n");
return;
}
+71 -4
View File
@@ -32,6 +32,7 @@
#include "cgroup.h"
#include "cond.h"
#include "conf.h"
#include "devmon.h"
#include "log.h"
#include "helpers.h"
#include "private.h"
@@ -62,9 +63,37 @@ static struct {
int skip_bootstrap; /* Set to skip bootstrap waiting */
} sm;
/* Shutdown trace: set SHUTDOWN_DEBUG to 1 in sm.h to enable */
#define shutdbg(fmt, args...) do { \
if (SHUTDOWN_DEBUG && (runlevel == 0 || runlevel == 6)) \
cprintf("shutdown[%c]: " fmt "\n", \
sm_rl2ch(runlevel), ##args); \
} while (0)
static uev_t console_watcher;
static int console_fd = -1;
/*
* Shutdown watchdog: if graceful service stop takes too long,
* force do_shutdown() directly from the event loop callback.
* Default timeout is 30 seconds, adjustable at build time.
*/
#ifndef SHUTDOWN_WDT_TIMEOUT
#define SHUTDOWN_WDT_TIMEOUT 30
#endif
static uev_t shutdown_wdt;
static void shutdown_wdt_cb(uev_t *w, void *arg, int events)
{
(void)w;
(void)arg;
(void)events;
logit(LOG_CONSOLE | LOG_CRIT, "Timed out (%d sec) waiting for services to stop, forcing shutdown.",
SHUTDOWN_WDT_TIMEOUT);
do_shutdown(halt);
}
/*
* Console input callback - handles Ctrl-C during bootstrap wait
*/
@@ -349,6 +378,7 @@ restart:
/* reload ? */
if (sm.reload) {
shutdbg("RUNNING got reload request!");
sm.reload = 0;
sm.state = SM_RELOAD_CHANGE_STATE;
}
@@ -361,12 +391,15 @@ restart:
/* Restore terse mode and run hooks before shutdown */
if (runlevel == 0 || runlevel == 6) {
/* Hide cursor, we're going down ... */
dprint(STDOUT_FILENO, "\033[?25l", 6);
shutdbg("entering CHANGE state, starting %d sec watchdog",
SHUTDOWN_WDT_TIMEOUT);
uev_timer_init(ctx, &shutdown_wdt, shutdown_wdt_cb,
NULL, SHUTDOWN_WDT_TIMEOUT * 1000, 0);
api_exit();
log_exit();
shutdbg("running HOOK_SHUTDOWN ...");
plugin_run_hooks(HOOK_SHUTDOWN);
shutdbg("HOOK_SHUTDOWN done");
}
dbg("Setting new runlevel --> %c <-- previous %c", sm_rl2ch(runlevel), sm_rl2ch(prevlevel));
@@ -386,12 +419,14 @@ restart:
}
/* Reset once flag of runtasks */
shutdbg("runtask_clean + step_all ...");
service_runtask_clean();
dbg("Stopping services not allowed in new runlevel ...");
sm.in_reload = 1;
service_step_all(SVC_TYPE_ANY);
shutdbg("CHANGE done, entering WAIT");
sm.state = SM_RUNLEVEL_WAIT_STATE;
break;
@@ -402,10 +437,13 @@ restart:
*/
svc = svc_stop_completed();
if (svc) {
shutdbg("WAIT, collecting %s[%d] ...", svc_ident(svc, NULL, 0), svc->pid);
dbg("Waiting to collect %s, cmd %s(%d) ...", svc_ident(svc, NULL, 0), svc->cmd, svc->pid);
break;
}
shutdbg("WAIT done, all services stopped");
/* Prev runlevel services stopped, call hooks before starting new runlevel ... */
dbg("All services have been stopped, calling runlevel change hooks ...");
plugin_run_hooks(HOOK_RUNLEVEL_CHANGE); /* Reconfigure HW/VLANs/etc here */
@@ -418,6 +456,7 @@ restart:
sm.in_reload = 0;
service_step_all(SVC_TYPE_ANY);
shutdbg("entering CLEAN");
sm.state = SM_RUNLEVEL_CLEAN_STATE;
break;
@@ -429,11 +468,15 @@ restart:
*/
svc = svc_clean_completed();
if (svc) {
shutdbg("CLEAN, waiting for %s[%d] (%s) ...",
svc_ident(svc, NULL, 0), svc->pid, svc_status(svc));
dbg("Waiting to collect post/cleanup script for %s, cmd %s(%d) ...",
svc_ident(svc, NULL, 0), svc->cmd, svc->pid);
break;
}
shutdbg("CLEAN done, all post/cleanup scripts collected");
/* Cleanup stale services */
svc_clean_dynamic(service_unregister);
@@ -449,9 +492,15 @@ restart:
* Tannhäuser Gate. All those .. moments .. will be lost in time, like
* tears ... in ... rain."
*/
if (runlevel == 0 || runlevel == 6)
if (runlevel == 0 || runlevel == 6) {
shutdbg("calling do_shutdown()");
uev_timer_stop(&shutdown_wdt);
do_shutdown(halt);
shutdbg("do_shutdown() returned to SM!");
}
shutdbg("entering RUNNING (reload=%d, newlevel=%d)",
sm.reload, sm.newlevel);
sm.state = SM_RUNNING_STATE;
break;
@@ -461,6 +510,9 @@ restart:
/* First reload all *.conf in /etc/finit.d/ */
conf_reload();
/* Handle remain tasks that need to run post script before restart */
service_runtask_clean();
/*
* Then, mark all affected service conditions as in-flux and
* let all affected services move to WAITING/HALTED
@@ -509,12 +561,27 @@ restart:
/* Cleanup stale services */
svc_clean_dynamic(service_unregister);
dbg("Step all services waiting for reasserted conditions ...");
service_step_all(SVC_TYPE_ANY);
dbg("Calling reconf hooks ...");
plugin_run_hooks(HOOK_SVC_RECONF);
dbg("Step all services waiting for reasserted conditions ...");
service_step_all(SVC_TYPE_ANY);
dbg("Update configuration generation of device conditions ...");
devmon_reconf();
dbg("Step all services waiting for reasserted conditions ...");
service_step_all(SVC_TYPE_ANY);
dbg("Update configuration generation of unmodified non-native services ...");
service_notify_reconf();
dbg("Step all services waiting for reasserted conditions ...");
service_step_all(SVC_TYPE_ANY);
dbg("Reconfiguration done");
sm.state = SM_RUNNING_STATE;
break;
+5
View File
@@ -24,6 +24,11 @@
#ifndef FINIT_SM_H_
#define FINIT_SM_H_
/* Set to 1 to trace shutdown (independent of 'initctl debug') */
#ifndef SHUTDOWN_DEBUG
#define SHUTDOWN_DEBUG 0
#endif
void sm_init (void);
void sm_step (void);
+4
View File
@@ -344,6 +344,10 @@ svc_t *svc_stop_completed(void)
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
if (svc->state == SVC_STOPPING_STATE && svc->pid > 1)
return svc;
/* Also wait for remain tasks running their post script */
if (svc_is_remain(svc) && svc->state == SVC_TEARDOWN_STATE && svc->pid > 1)
return svc;
}
return NULL;
+3
View File
@@ -139,12 +139,14 @@ typedef struct svc {
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 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 */
const int removed;
int starting; /* ... waiting for pidfile to be re-asserted */
int runlevels;
int sighup; /* This service supports SIGHUP :) */
int flux_reload; /* Propagate reload from dependency, '~' prefix */
int forking; /* This is a service/sysv daemon that forks, wait for it ... */
svc_block_t block; /* Reason that this service is currently stopped */
char cond[MAX_COND_LEN];
@@ -282,6 +284,7 @@ static inline int svc_is_tty (svc_t *svc) { return svc && SVC_TYPE_TTY
static inline int svc_is_runtask (svc_t *svc) { return svc && (SVC_TYPE_RUNTASK & svc->type);}
static inline int svc_is_forking (svc_t *svc) { return svc && svc->forking; }
static inline int svc_is_manual (svc_t *svc) { return svc && svc->manual; }
static inline int svc_is_remain (svc_t *svc) { return svc && svc->remain; }
static inline int svc_is_noreload (svc_t *svc) { return svc && (0 == svc->sighup && 0 == svc->reload_script[0]); }
static inline int svc_in_runlevel (svc_t *svc, int runlevel) { return svc && ISSET(svc->runlevels, runlevel); }
+299 -45
View File
@@ -34,6 +34,7 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
@@ -45,8 +46,29 @@
int debug;
int c_flag = 0;
int r_flag = 0;
int create_flag = 0;
int clean_flag = 0;
int remove_flag = 0;
#define MAX_EXCLUDES 32
static const char *exclude_prefixes[MAX_EXCLUDES];
static int num_excludes;
static void add_exclude_prefix(const char *prefix)
{
if (num_excludes < MAX_EXCLUDES)
exclude_prefixes[num_excludes++] = prefix;
}
static int excluded(const char *path)
{
for (int i = 0; i < num_excludes; i++) {
if (!strncmp(path, exclude_prefixes[i], strlen(exclude_prefixes[i])))
return 1;
}
return 0;
}
static int is_dir_empty(const char *path)
{
@@ -261,6 +283,122 @@ static void write_arg(FILE *fp, char *arg)
fputs(arg, fp);
}
/**
* Parse age string and return age in seconds.
* Returns 0 on invalid/empty age ("-" or NULL).
* Examples: "10d" = 864000, "1w" = 604800, "2h" = 7200
*/
static time_t parse_age(const char *age)
{
time_t val;
char *end;
if (!age || !strcmp(age, "-") || !strcmp(age, "0"))
return 0;
errno = 0;
val = strtoul(age, &end, 10);
if (errno || end == age)
return 0;
switch (*end) {
case 'w':
val *= 7;
/* fallthrough */
case 'd':
val *= 24;
/* fallthrough */
case 'h':
val *= 60;
/* fallthrough */
case 'm':
val *= 60;
/* fallthrough */
case 's':
case '\0':
break;
default:
return 0; /* unknown suffix */
}
return val;
}
/**
* Parse user string - supports both names and numeric UIDs.
* Returns UID on success, -1 on failure.
*/
static int parse_uid(const char *user)
{
long val;
if (!user || !user[0])
return 0;
/* Check if it's a numeric UID */
val = atonum(user);
if (val >= 0)
return val;
/* Not numeric, look up by name */
return getuser(user, NULL);
}
/**
* Parse group string - supports both names and numeric GIDs.
* Returns GID on success, -1 on failure.
*/
static int parse_gid(const char *group)
{
long val;
if (!group || !group[0])
return 0;
/* Check if it's a numeric GID */
val = atonum(group);
if (val >= 0)
return val;
/* Not numeric, look up by name */
return getgroup(group);
}
/* Globals for do_clean() callback - nftw doesn't support user data */
static time_t clean_age;
static time_t clean_now;
static int do_clean(const char *fpath, const struct stat *sb, int tflag, struct FTW *ftw)
{
int is_dir;
/* Skip the root directory itself */
if (ftw->level == 0)
return 0;
is_dir = (tflag == FTW_D || tflag == FTW_DP);
/*
* Conservative cleanup matching systemd-tmpfiles behavior:
* - Files: keep if ANY of atime, ctime, mtime is recent
* - Directories: keep if ANY of atime, mtime is recent (ctime
* excluded because cleanup itself updates directory ctime)
*/
if (clean_now - sb->st_mtime < clean_age)
return 0; /* mtime is too new, keep it */
if (clean_now - sb->st_atime < clean_age)
return 0; /* atime is too new, keep it */
if (!is_dir && clean_now - sb->st_ctime < clean_age)
return 0; /* ctime is too new, keep it (files only) */
if (remove(fpath) && errno != ENOENT && errno != EBUSY)
warn("Failed cleaning %s", fpath);
return 0;
}
/*
* The configuration format is one line per path, containing type, path,
* mode, ownership, age, and argument fields. The lines are separated by
@@ -295,6 +433,8 @@ static void tmpfiles(char *line)
errx(1, "Path name specifiers unsupported, skipping.");
return;
}
if (excluded(path))
return;
token = strtok(NULL, "\t ");
if (token) {
@@ -312,13 +452,12 @@ static void tmpfiles(char *line)
group = "root";
age = strtok(NULL, "\t ");
(void)age; /* unused atm. */
arg = strtok(NULL, "\n");
strc = stat(path, &st);
// file and directory removal logic
if (r_flag) {
if (remove_flag) {
switch (type[0]) {
case 'b':
case 'c':
@@ -349,7 +488,7 @@ static void tmpfiles(char *line)
break;
case 'X':
case 'x':
dbg("Unsupported x/X command, ignoring %s, no support for clean at runtime.", path);
/* handled by clean_flag (--clean) */
break;
case 'Z':
case 'z':
@@ -360,8 +499,34 @@ static void tmpfiles(char *line)
}
}
/* age-based cleanup logic */
if (clean_flag) {
time_t max_age = parse_age(age);
/* Only process if age is specified */
if (max_age > 0) {
switch (type[0]) {
case 'd':
case 'D':
case 'e':
if (!fisdir(path))
break;
clean_age = max_age;
clean_now = time(NULL);
nftw(path, do_clean, 20, FTW_DEPTH | FTW_PHYS);
break;
case 'x':
case 'X':
/* exclusion patterns - not yet implemented */
break;
default:
break;
}
}
}
// file & directory creation logic
if (c_flag) {
if (create_flag) {
switch (type[0]) {
case 'b':
rc = parse_mm(arg, &major, &minor);
@@ -409,26 +574,55 @@ static void tmpfiles(char *line)
rc = 0;
break;
case 'd':
case 'D':
case 'D': {
int uid, gid;
mode_t omask;
mkparent(path, 0755);
rc = mksubsys(path, mode ?: 0755, user, group);
omask = umask(0);
uid = parse_uid(user);
if (uid >= 0) {
gid = parse_gid(group);
if (gid < 0)
gid = 0;
rc = makedir(path, mode ?: 0755);
if (rc && errno == EEXIST)
rc = chmod(path, mode ?: 0755);
if (chown(path, uid, gid))
warn("Failed chown(%s, %d, %d)", path, uid, gid);
}
umask(omask);
break;
}
case 'e':
if (glob(path, GLOB_NOESCAPE, NULL, &gl))
break;
for (size_t i = 0; i < gl.gl_pathc; i++)
rc += mksubsys(gl.gl_pathv[i], mode ?: 0755, user, group);
for (size_t i = 0; i < gl.gl_pathc; i++) {
char *p = gl.gl_pathv[i];
int uid, gid;
/* e only adjusts existing directories */
if (!fisdir(p))
continue;
uid = parse_uid(user);
gid = parse_gid(group);
if (gid < 0)
gid = 0;
if (mode)
chmod(p, mode);
if (uid >= 0 && chown(p, uid, gid))
warn("Failed chown(%s, %d, %d)", p, uid, gid);
}
break;
case 'f':
case 'F':
mkparent(path, 0755);
if (type[1] == '+' || type[0] == 'F') {
/* f+/F will create or truncate the file */
if (!arg) {
rc = create(path, mode ?: 0644, user, group);
break;
}
fp = fopen(path, "w+");
} else {
/* f will create the file if it doesn't exist */
@@ -437,8 +631,20 @@ static void tmpfiles(char *line)
}
if (fp) {
int uid, gid;
write_arg(fp, arg);
rc = fclose(fp);
/* Apply mode and ownership */
if (mode)
chmod(path, mode);
uid = parse_uid(user);
gid = parse_gid(group);
if (gid < 0)
gid = 0;
if (uid >= 0 && chown(path, uid, gid))
warn("Failed chown(%s, %d, %d)", path, uid, gid);
}
break;
case 'l': /* Finit extension, like 'L' but only if target exists */
@@ -465,7 +671,10 @@ static void tmpfiles(char *line)
if (!strc) {
if (type[1] != '+')
break;
rmrf(path);
if (fisdir(path))
rmrf(path);
else
erase(path);
}
mkparent(path, 0755);
if (!arg) {
@@ -505,7 +714,7 @@ static void tmpfiles(char *line)
break;
case 'X':
case 'x':
dbg("Unsupported x/X command, ignoring %s, no support for clean at runtime.", path);
/* handled by clean_flag (--clean) */
break;
case 'Z':
opts = "-R";
@@ -532,45 +741,96 @@ static void tmpfiles(char *line)
warn("Failed %s operation on path %s", type, path);
}
static void process_file(const char *fn)
{
FILE *fp;
fp = fopen(fn, "r");
if (!fp) {
warn("Failed to open %s", fn);
return;
}
while (!feof(fp)) {
char *line;
line = fparseln(fp, NULL, NULL, NULL, FPARSELN_UNESCCOMM);
if (!line)
continue;
tmpfiles(line);
free(line);
}
fclose(fp);
}
static int usage(int rc)
{
fprintf(stderr,
"Usage: tmpfiles [COMMAND...]\n"
"Usage: tmpfiles [OPTIONS] [CONFIGFILE...]\n"
"\n"
"Commands:\n"
"Options:\n"
" -C, --clean Clean files and directories based on age\n"
" -c, --create Create files and directories\n"
" -d, --debug Show developer debug messages\n"
" -E Exclude /dev, /proc, /run, and /sys\n"
" --exclude-prefix=PFX Ignore rules for paths starting with PFX\n"
" -r, --remove Remove files and directories marked for removal\n"
" -h, --help This help text\n"
"\n"
"If no CONFIGFILE is specified, all *.conf files in the standard\n"
"tmpfiles.d directories are processed.\n"
"\n");
return rc;
}
enum {
OPT_EXCLUDE_PREFIX = 256,
};
int main(int argc, char *argv[])
{
struct option long_options[] = {
{ "create", 0, NULL, 'c' },
{ "debug", 0, NULL, 'd' },
{ "remove", 0, NULL, 'r' },
{ "help", 0, NULL, 'h' },
{ "clean", 0, NULL, 'C' },
{ "create", 0, NULL, 'c' },
{ "debug", 0, NULL, 'd' },
{ "exclude-prefix", 1, NULL, OPT_EXCLUDE_PREFIX },
{ "remove", 0, NULL, 'r' },
{ "help", 0, NULL, 'h' },
{ NULL, 0, NULL, 0 }
};
int c;
while ((c = getopt_long(argc, argv, "cdrh?", long_options, NULL)) != EOF) {
while ((c = getopt_long(argc, argv, "CcdrEh?", long_options, NULL)) != EOF) {
switch(c) {
case 'C':
clean_flag = 1;
break;
case 'c':
c_flag = 1;
create_flag = 1;
break;
case 'd':
debug = 1;
break;
case 'E':
add_exclude_prefix("/dev");
add_exclude_prefix("/proc");
add_exclude_prefix("/run");
add_exclude_prefix("/sys");
break;
case OPT_EXCLUDE_PREFIX:
add_exclude_prefix(optarg);
break;
case 'r':
r_flag = 1;
remove_flag = 1;
break;
case 'h':
@@ -582,12 +842,21 @@ int main(int argc, char *argv[])
}
}
if (c_flag + r_flag == 0) {
fprintf(stderr, "You need to specify at least one of --create or --remove.\n");
if (create_flag + clean_flag + remove_flag == 0) {
fprintf(stderr, "You need to specify at least one of --clean, --create, or --remove.\n");
return 1;
}
/* If config files specified on command line, process only those */
if (optind < argc) {
for (int i = optind; i < argc; i++)
process_file(argv[i]);
return 0;
}
/*
* No config files specified, process standard tmpfiles.d directories.
*
* Only the three last tmpfiles.d/ directories are defined in
* tmpfiles.d(5) as system search paths. Finit adds two more
* before that to have Finit specific ones sorted first, and
@@ -614,7 +883,6 @@ int main(int argc, char *argv[])
for (i = 0; i < gl.gl_pathc; i++) {
char *fn = gl.gl_pathv[i];
size_t j;
FILE *fp;
/* check for overrides */
for (j = i + 1; j < gl.gl_pathc; j++) {
@@ -627,26 +895,12 @@ int main(int argc, char *argv[])
if (!fn)
continue; /* skip, override exists */
fp = fopen(fn, "r");
if (!fp)
continue;
// info("Parsing %s ...", fn);
while (!feof(fp)) {
char *line;
line = fparseln(fp, NULL, NULL, NULL, FPARSELN_UNESCCOMM);
if (!line)
continue;
tmpfiles(line);
free(line);
}
fclose(fp);
process_file(fn);
}
globfree(&gl);
return 0;
}
/**
+2
View File
@@ -32,6 +32,7 @@ EXTRA_DIST += add-remove-dynamic-service-sub-config.sh
EXTRA_DIST += bootstrap-crash.sh
EXTRA_DIST += cond-start-task.sh
EXTRA_DIST += crashing.sh
EXTRA_DIST += dep-chain-reload.sh
EXTRA_DIST += depserv.sh
EXTRA_DIST += devmon.sh
EXTRA_DIST += failing-sysv.sh
@@ -74,6 +75,7 @@ TESTS += add-remove-dynamic-service-sub-config.sh
TESTS += bootstrap-crash.sh
TESTS += cond-start-task.sh
TESTS += crashing.sh
TESTS += dep-chain-reload.sh
TESTS += depserv.sh
TESTS += devmon.sh
TESTS += failing-sysv.sh
+181
View File
@@ -0,0 +1,181 @@
#!/bin/sh
# Verify transitive dependency chain during reload and crash
#
# Four services in a chain: A → B → C → D, all using notify:pid.
# B and C are placed in separate sub-config files so we can use
# 'initctl touch' on them independently. B supports SIGHUP (reload),
# while C and D use '!' (noreload) and '~' (propagate reload),
# matching a common pattern in real-world setups (e.g., FRR routing
# daemons on Infix OS).
#
# Chain: A ← B <pid/A> ← C <!~pid/B> ← D <!~pid/C>
#
# Test 1 - touch C + reload (the "Infix" scenario):
# C is in the middle of the chain. After 'initctl touch svc_c.conf'
# + 'initctl reload':
# - A and B should be unaffected (same PID)
# - C is stopped/started (config was touched, noreload)
# - D must be restarted (reload propagated from C)
#
# Test 2 - crash (kill -9):
# When B is killed with SIGKILL the crash path (RUNNING → HALTED)
# bypasses STOPPING. Without cond_clear() in service_cleanup(),
# pid/B is never invalidated and C, D are never restarted.
#
# Test 3 - touch B + reload:
# B supports SIGHUP (no '!' prefix). After 'initctl touch
# svc_b.conf' + 'initctl reload':
# - A should be unaffected (same PID)
# - B is SIGHUP'd (same PID, config was touched)
# - C and D must be restarted (reload propagated, '~' prefix)
set -eu
TEST_DIR=$(dirname "$0")
test_teardown()
{
say "Running test teardown."
run "rm -f $FINIT_RCSD/svc_b.conf $FINIT_RCSD/svc_c.conf"
}
pidof()
{
texec initctl -j status "$1" | jq .pid
}
test_setup()
{
run "cat >> $FINIT_CONF" <<EOF
service log:stdout notify:pid name:svc_a serv -np -i svc_a -- Chain root
service log:stdout notify:pid <!~pid/svc_c> name:svc_d serv -np -i svc_d -- Needs C
EOF
run "cat >> $FINIT_RCSD/svc_b.conf" <<EOF
service log:stdout notify:pid <pid/svc_a> name:svc_b serv -np -i svc_b -- Needs A
EOF
run "cat >> $FINIT_RCSD/svc_c.conf" <<EOF
service log:stdout notify:pid <!~pid/svc_b> name:svc_c serv -np -i svc_c -- Needs B
EOF
}
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
sep "Configuration"
run "cat $FINIT_CONF"
run "cat $FINIT_RCSD/svc_b.conf"
run "cat $FINIT_RCSD/svc_c.conf"
say "Reload Finit to start all services"
run "initctl reload"
say "Wait for full chain to start"
retry 'assert_status "svc_d" "running"' 10 1
run "initctl status"
run "initctl cond dump"
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Test 1: touch C (middle of chain, noreload) + reload
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
sep "Test 1: Touch C (middle) and global reload"
pid_a=$(pidof svc_a)
pid_b=$(pidof svc_b)
pid_c=$(pidof svc_c)
pid_d=$(pidof svc_d)
say "PIDs before: A=$pid_a B=$pid_b C=$pid_c D=$pid_d"
run "initctl touch svc_c.conf"
run "initctl reload"
say "Wait for chain to settle"
retry 'assert_status "svc_d" "running"' 15 1
run "initctl status"
run "initctl cond dump"
new_pid_a=$(pidof svc_a)
new_pid_b=$(pidof svc_b)
new_pid_c=$(pidof svc_c)
new_pid_d=$(pidof svc_d)
say "PIDs after: A=$new_pid_a B=$new_pid_b C=$new_pid_c D=$new_pid_d"
# shellcheck disable=SC2086
assert "A was not restarted" $new_pid_a -eq $pid_a
# shellcheck disable=SC2086
assert "B was not restarted" $new_pid_b -eq $pid_b
# shellcheck disable=SC2086
assert "C was restarted (touched)" $new_pid_c -ne $pid_c
# shellcheck disable=SC2086
assert "D was restarted (transitive dep)" $new_pid_d -ne $pid_d
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Test 2: crash (kill -9), bypasses STOPPING
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
sep "Test 2: Kill B with SIGKILL (bypasses STOPPING)"
pid_b=$(pidof svc_b)
pid_c=$(pidof svc_c)
pid_d=$(pidof svc_d)
say "PIDs before: B=$pid_b C=$pid_c D=$pid_d"
run "kill -9 $pid_b"
say "Wait for B to respawn and chain to settle"
retry 'assert_status "svc_d" "running"' 15 1
run "initctl status"
run "initctl cond dump"
new_pid_b=$(pidof svc_b)
new_pid_c=$(pidof svc_c)
new_pid_d=$(pidof svc_d)
say "PIDs after: B=$new_pid_b C=$new_pid_c D=$new_pid_d"
# shellcheck disable=SC2086
assert "B was restarted (crashed+respawn)" $new_pid_b -ne $pid_b
# shellcheck disable=SC2086
assert "C was restarted (transitive dep)" $new_pid_c -ne $pid_c
# shellcheck disable=SC2086
assert "D was restarted (transitive dep)" $new_pid_d -ne $pid_d
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
# Test 3: touch B (supports SIGHUP) + reload
# ――――――――――――――――――――――――――――――――――――――――――――――――――――――
sep "Test 3: Touch B (SIGHUP) and global reload"
pid_a=$(pidof svc_a)
pid_b=$(pidof svc_b)
pid_c=$(pidof svc_c)
pid_d=$(pidof svc_d)
say "PIDs before: A=$pid_a B=$pid_b C=$pid_c D=$pid_d"
run "initctl debug"
run "initctl touch svc_b.conf"
run "initctl reload"
sleep 2
run "initctl status"
say "Wait for chain to settle"
retry 'assert_status "svc_d" "running"' 15 1
run "initctl status"
run "initctl cond dump"
new_pid_a=$(pidof svc_a)
new_pid_b=$(pidof svc_b)
new_pid_c=$(pidof svc_c)
new_pid_d=$(pidof svc_d)
say "PIDs after: A=$new_pid_a B=$new_pid_b C=$new_pid_c D=$new_pid_d"
# shellcheck disable=SC2086
assert "A was not restarted" $new_pid_a -eq $pid_a
# shellcheck disable=SC2086
assert "B was not restarted (SIGHUP)" $new_pid_b -eq $pid_b
# shellcheck disable=SC2086
assert "C was restarted (transitive dep)" $new_pid_c -ne $pid_c
# shellcheck disable=SC2086
assert "D was restarted (transitive dep)" $new_pid_d -ne $pid_d
return 0
+12 -3
View File
@@ -5,6 +5,7 @@
# - bar must not start until foo is ready
# - bar must be stopped when foo goes down
# - bar must be restarted when foo is restarted
# - bar must not be restarted when foo is reloaded (per-service reload)
#
# Regression test bug #314, that bar is not in a restart loop when foo
# is stopped. Also, verify that changing between runlevels, where foo
@@ -49,8 +50,15 @@ test_one()
run "initctl status"
assert "bar is restarted" "$(texec initctl |grep bar | awk '{print $1;}')" != "$pid"
say "Verify bar is NOT restarted when foo is reloaded (per-service) ..."
retry 'assert_status "bar" "running"' 5 1
pid=$(texec initctl |grep bar | awk '{print $1;}')
run "initctl reload foo"
retry 'assert_status "bar" "running"' 5 1
assert "bar is NOT restarted on reload" "$(texec initctl |grep bar | awk '{print $1;}')" == "$pid"
# Wait for spice to be stolen by the Harkonnen
sleep 3
sleep 5
# bar should now have detected the loss of spice and be in restart
run "initctl status"
assert_status "bar" "restart"
@@ -61,8 +69,9 @@ test_one()
assert_status "bar" "waiting"
}
run "initctl debug"
sep
test_one "pid/foo"
sep
run "initctl debug"
test_one "service/foo/running"
#run "initctl debug"
test_one "service/foo/ready"
+5
View File
@@ -42,6 +42,11 @@ test_one()
mkdev "$node"
assert_status "serv" "running"
assert_cond "$cond"
run "initctl reload"
assert_status "serv" "running"
assert_cond "$cond"
rmdev "$node"
assert_status "serv" "waiting"
+1 -1
View File
@@ -52,4 +52,4 @@ done
echo "$pid" > /tmp/oldpid
#echo "PID $pid, kill -9 ..."
kill -9 "$pid"
kill -9 "$pid" 2>/dev/null || true
+6
View File
@@ -325,6 +325,12 @@ int main(int argc, char *argv[])
#endif
}
}
if (melange) {
mine(melange);
vanish = -2;
}
if (do_pidfile > 0)
pidfile(NULL);
reloading = 0;