Compare commits

...
169 Commits
Author SHA1 Message Date
Joachim Wiberg e74dff96ba Update ChangeLog and bump version for v4.17 GA
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-28 10:09:16 +02:00
Joachim Wiberg 9ed4407b20 Follow-up to 96f944b: fix regression in status output at boot
status() returns a pointer to a single static buffer, so calling it twice
in the same cprintf() argument list — status(3) and status(rc) — causes
one to overwrite the other before the format string is rendered.  When
status(3) wins, the line shows [ ⋯  ] instead of [ OK ].  Fix by copying
status(rc) into a local buffer before calling status(3).

Also drop the delline() calls added to print() — that macro writes \033[2K
to buffered stdout while cprintf() writes unbuffered to stderr, so the
erase sequences can arrive out of order.  The \r\e[K already present in
the cprintf format strings makes them redundant anyway.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-28 10:02:21 +02:00
Joachim Wiberg eb913e8bf2 initramfs: fix Coverity defects
Check return value of remove() in delete_cb() and log failures via
dbg(), CID 909395

Replace stat() calls with open(O_DIRECTORY)+ fstat() for newroot and "/"
checks.  Eliminates the check-then-use race and lets O_DIRECTORY do the
isdir validation atomically, CID 909394

Drop the explicit close(0/1/2) before opening /dev/console.  dup2()
closes the old targets itself, so open() returns a fd > STDERR_FILENO
that can always be closed unconditionally, removing the conditional
guard, CID 909393

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-26 22:11:19 +02:00
Joachim Wiberg 17d04779fb Update ChangeLog and bump version for v4.17-rc1
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-26 21:54:49 +02:00
Joachim Wiberg ab661423ba Merge pull request #488 from Ollie-Gutierrez/ctty-support
service: add optional controlling TTY via tty: stanza
2026-04-23 19:41:02 +02:00
Ollie Gutierrez 8e77ea68b2 service: add optional controlling TTY via tty: stanza 2026-04-23 15:08:57 +00:00
Joachim Wiberg b0e672bc9a Merge pull request #487 from aanderse/patch-1
tmpfiles.d: update /var/tmp mode
2026-04-18 07:34:37 +02:00
Aaron Andersen ad76e15949 tmpfiles.d: update /var/tmp mode
`/var/tmp` mode should match `/tmp`
2026-04-18 09:17:10 +09:00
Joachim Wiberg b8735cc893 Merge pull request #486 from aanderse/patch-1 2026-04-14 04:57:00 +02:00
Aaron Andersen c058b3f9ce plugins/plymouth.c: fixup email address 2026-04-14 09:03:19 +09:00
Joachim Wiberg 8f6c61606a Merge pull request #485 from finit-project/console-output-fixes
Atomic print and re-print desc on status, flush before reboot
2026-04-08 17:11:57 +02:00
Joachim Wiberg 96f944b816 Atomic print and re-print desc on status, flush before reboot
Refactor print() to emit description + final status in a single call to
cprintf(), preventing kernel messages from splitting the two parts.

For two-phase print(-1,...) + print_result() sequences used by, e.g.,
run_interactive, save the last description and re-print it before the
[ OK ] / [FAIL] output so the status is never left stranded on a blank
line when command output or kernel messages have scrolled away the
original description.

Finally, add print_exit() which drains the console output buffer with
tcdrain(2) and resets ANSI SGR attributes + cursor visibility before the
kernel takes back the console on reboot/halt, preventing escape code
leakage into bootloader or early-kernel output.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-04-07 11:36:19 +02:00
Joachim Wiberg a81530754b Merge pull request #483 from aanderse/plymouth
Add Plymouth boot splash plugin
2026-03-20 14:35:53 +01:00
Aaron Andersen 1518eb9466 Add Plymouth boot splash plugin
Manage the plymouthd lifecycle across boot, switch_root, and shutdown.
Activated by the "splash" kernel command line argument.
2026-03-20 09:18:18 -04:00
Joachim Wiberg 50b9e46e19 Merge pull request #484 from finit-project/rdep-stop-cond
Clear condition before stopping rdeps on reload
2026-03-19 06:53:40 +01:00
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
Joachim Wiberg d7fd5bc902 .github: ensure regression tests do not run in parallel
At least the sysvpart.sh regression test cannot run in parallel yet with
other tests (probably runparts.sh), so we must ensure the tests never
run in parallel, in particular at release.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 16:34:16 +01:00
Joachim Wiberg 3f98220d5d test: simplify
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:54:06 +01:00
Joachim Wiberg 0ca509a42e test: debug sysroot setup
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:47:11 +01:00
Joachim Wiberg d17144d16f .github: extract test results dir at release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:46:43 +01:00
Joachim Wiberg a608c5a5c9 .github: always upload test results, regardless
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:46:10 +01:00
Joachim Wiberg 9b44b99480 .github: remember to ldconfig
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 15:40:06 +01:00
Joachim Wiberg ad225156f1 Update ChangeLog and bump version for release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 11:09:16 +01:00
Joachim Wiberg eb92a915d3 initctl: drop logically dead code, found by Coverity Scan
The defines already check for plain mode.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 10:46:48 +01:00
Joachim Wiberg 69a4f2c115 Drop logically dead code, found by Coverity Scan
Checks for uid and gid introduced in d017661

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 10:42:41 +01:00
Joachim Wiberg 9ce95fe8c0 .github: fix logic in weekly workflow
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 09:51:02 +01:00
Joachim Wiberg 06889cc014 .github: verify system can find .so libs in /usr/local
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 09:48:33 +01:00
Joachim Wiberg 329f11b4da test: add barebones /etc/{passwd,group} and an ld.so.conf
Finit now requires being able to query at least for the root user and
group before starting any services.

Also, add support for using libraries installed in /usr/local

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-01-01 09:45:06 +01:00
Joachim Wiberg ce40e2b9d2 Rename tty services early from "init" -> "getty"
Finit has support for "Please press Enter to activate this console."
which means there's no getty yet running.  However, when profiling
systems with Finit, and embedded systems in general, a common metric
is the time from power-on to getty has started.

This commit makes sure to rename the process so that BusyBox pidof is
capable of detecting that "getty" has started.  This is mostly for the
bootchart2 project's bootchartd, the native BusyBox bootchartd does not
have this issue.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-31 23:34:04 +01:00
Joachim Wiberg d0176612c9 Default to user/group root for services and check for errors
This is a refactor of getuser() and getgroup() so that they always
return a valid user, and group, for all normal use-cases.  When an
error occurs we now handle it properly in service_fork() so as to
not attempt to start services with an invalid user/group setting
as root.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-31 23:29:28 +01:00
Joachim Wiberg 21753e26dd Set critical env PATH + SHELL early
When running Finit under boothcartd (bootchart2 project) the PATH is
lost due to a bug.  This was a wakeup, so set critical variables in
main() early, before calling fs_init().

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-31 23:29:28 +01:00
Joachim Wiberg 0dc2513b32 Follow-up to 702a606, too long string to hide cursor
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-31 23:29:27 +01:00
Joachim Wiberg d16bfa789b Follow-up to 7c8ab79, missing semicolon
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 15:22:09 +01:00
Joachim Wiberg 0b27778c96 .github: install missing glightbox plugin for mkdocs
This is for automatic image zoom.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 15:21:06 +01:00
Joachim Wiberg 3e25297e3c doc: major overhaul of distro recs, simplify & clarify
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 14:12:13 +01:00
Joachim Wiberg 6560968c8b doc: update feature list
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 13:54:22 +01:00
Joachim Wiberg c9a3c5c885 doc: reorg. of menu a bit
Try out navigation.tabs, if we don't like it we can revert.

Reorg for stricter sections, what information actually belongs where?
E.g., introduction is now in a Getting Started section.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 13:52:13 +01:00
Joachim Wiberg a2d45e8f5e doc: add missing link to setrlimit(2)
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 13:06:56 +01:00
Joachim Wiberg f6c69eb108 doc: follow-up to be2a0ec, clearify capabilities with @root
Also, add links to relevant man pages.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 13:06:07 +01:00
Joachim Wiberg f0032ab6b7 Throttle failing services, e.g., tty, on error exit code
Some 'respawn' type services, like gettys, may hog the CPU in error
states if the service immediately exits.  E.g., due to missing dev.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 12:32:01 +01:00
Joachim Wiberg 53c5d0e55a Always reset ownership and permissions on TTY device nodes
During /bin/login phase the TTY device node is chowned and chmodded to
the authenticated user.  It will remain in this state until the next
call to getty.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 12:12:18 +01:00
Joachim Wiberg 7c8ab7915f Fix #458: follow-up to be2a0ec, capabilities breaks root access
A user reports inability to re-start getty on the console after logging
out from a serial console.  After some digging it was found that the tty
was owned by the last user logged in and 600.  Even thougn getty runs as
root, it did not have permission to re-open the device node.

Turns out there was a minor bug in the new capability code that cleared
all capabilities from the root user.  A surprising amount of programs
worked just fine, but restarting getty gave it away.

The fix is to only call cap_setuid() when capabilities are set for the
service, otherwise we just fall back to setuid().

Also, refactor service_register() wrt. capabilities a bit so that we can
give users an early warning if the configuration is invalid, by adding a
parse_caps() helper function that calls cap_iab_from_text() to verify.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-27 00:49:48 +01:00
Joachim Wiberg 12f7855a78 Update ChangeLog for v4.15 release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 13:41:18 +01:00
Joachim Wiberg bcc3360b53 Update site_url for generated documentation
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 13:30:30 +01:00
Joachim Wiberg d3408d14a8 Simplify and update links
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 13:28:43 +01:00
Joachim Wiberg 804060444a .github: fix deploy issue
- We already have the SHA
 - Use 'git add -A' to handle deleted files too

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 13:16:45 +01:00
Joachim Wiberg de544a5b36 .github: deploy docs to project's pages
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-26 12:47:25 +01:00
Joachim Wiberg 0d48d1df49 Merge pull request #461 from aanderse/master
Add support for supplementary groups
2025-12-26 10:28:51 +01:00
Aaron Andersen b46592e818 Add support for supplementary groups
Implement supplementary group support for services, allowing them to
access resources owned by multiple groups. Uses the @user:group,sup1,sup2
syntax to explicitly specify supplementary groups, in addition to now
reading group membership from /etc/group.
2025-12-24 09:54:37 -05:00
Joachim Wiberg 22bc218c84 Fix #462: /dev/pts mounted with wrong mode
Before this fix:

    admin@infix:~$ sudo ls -la /dev/pts/
    total 0
    drwxr-xr-x    2 root     root             0 Dec 24 08:16 .
    drwxr-xr-x   13 root     root         13340 Dec 24 08:16 ..
    cr--------    1 root     tty       136,   0 Dec 24 08:18 0
    crw-rw-rw-    1 root     root        5,   2 Dec 24 08:16 ptmx
    admin@infix:~$ mount | grep devpts
    devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=400,ptmxmode=666)

After:

    admin@infix-00-00-00:~$ sudo ls -l /dev/pts/
    total 0
    crw--w----    1 root     tty       136,   0 Dec 24 08:21 0
    crw-rw-rw-    1 root     root        5,   2 Dec 24 08:20 ptmx
    admin@infix-00-00-00:~$ mount |grep pts
    devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-24 09:23:30 +01:00
Joachim Wiberg 702a606d26 Hide cursor at boot and shutdown
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-17 08:19:39 +01:00
Joachim Wiberg a3d9b6e9b1 initctl: fix remaining lingering artifacts in 'top' output
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-17 08:19:38 +01:00
Joachim Wiberg 390a0f48c1 initctl: minor, simplify code
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-17 08:19:38 +01:00
Joachim Wiberg f4a13687e8 initctl: resolve hierarchical cgroup limits
Cgroups v2 limits are hierarchical - a process is constrained by the
most restrictive limit in its ancestor chain, not just its immediate
cgroup.  This patch updates cg_conf() to walk up the hierarchy and
report effective limits by comparing values at each level.

This fixes incorrect "max" (unlimited) reporting in 'initctl --json
status', 'initctl cgroup', and 'initctl top' when child cgroups have
no explicit limits but parents do.

For memory.max and cpu.max: take minimum (most restrictive)
For memory.min: take maximum (most protection)

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-17 08:19:37 +01:00
Joachim Wiberg 43ca51c3b1 initctl: add cpu/mem limits as well to json status output
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-17 08:19:37 +01:00
Joachim Wiberg 864b71af14 Follow-up to d87d298, prevent "cursor jumps"
Comment-out code that makes the cursor "jump" around at boot before
displaying: Please press Enter to activate this console.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-17 08:19:36 +01:00
Joachim Wiberg 44a928e5e6 Follow-up to e635ea8, adjust libite version dependency
libite v2.6.2 is not yet in Buildroot, so let's relax the dependency a
bit.  Load bearing functionality was in v2.6.0, any fixes on top is a
nice-to-have only.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-16 10:19:29 +01:00
Joachim Wiberg f513348963 doc: update ChangeLog for upcoming v4.15
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:38:41 +01:00
Joachim Wiberg 820b9a77c1 doc: update cgroup configuration section
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:37:09 +01:00
Joachim Wiberg 85baafe99c doc: update links to new project home and add release badge
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:37:08 +01:00
Joachim Wiberg e635ea882a Bump required libite and libuev versions
We now require readsnf() introduced in libite 2.6.0, with bug fixes
this effectively means v2.6.2.

The libuev bump is for 64-bit time_t, with bug fix => v2.4.1

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:37:08 +01:00
Joachim Wiberg f908c8d43a Fix possible out-of-bounds write, found by Coverity Scan
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:37:07 +01:00
Joachim Wiberg 308f75b209 Silence false positive from Coverity Scan
51static int is_dir_empty(const char *path)
    52{
	 1. var_decl: Declaring variable namelist without initializer.
    53        struct dirent **namelist;
    54        int num;
    55

   CID 898746: (#1 of 1): Uninitialized pointer read (UNINIT)
   2. uninit_use_in_call: Using uninitialized value namelist when calling scandir.
    56        num = scandir(path, &namelist, NULL, NULL);
    57        if (num < 0)
    58                return 0;

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:37:07 +01:00
Joachim Wiberg d87d298c85 getty: fix terminal scrollback issues after login on console
Fixes an issue where the mouse scroll wheel and Shift+PgUp/PgDn
sometimes would not work properly after login.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:37:06 +01:00
Joachim Wiberg ae7931f68e plugins: follow-up to 13a8f56, fix missing else branch
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:37:06 +01:00
Joachim Wiberg 8422d2715c initctl: fix flickering in 'top' and handle smaller screens
Drop clear screen to fix flickering in 'initctl top' output.  Also, make
sure to not garble the display if the the terminal is too small.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:04:34 +01:00
Joachim Wiberg 2eefaa5db6 initctl: fix file descriptor leak causing 'initctl top' to crash
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:04:33 +01:00
Joachim Wiberg fb0ad18d3a initctl: add CPU throttled information alongside memory usage
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:04:33 +01:00
Joachim Wiberg 2681444b07 system: use reasonable cgroup names from hotplug services
Instead of /system/10-hotplug/ we now place udevd, mdevd, and others, in
more aptly named groups using the new 'name:' syntax.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:04:32 +01:00
Joachim Wiberg dd36116a97 Add housekeeping functions to clean up unused cgroups
This should not be needed, but for some reason we don't get events when
early processes exit, so we end up with lingering cgroups.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:04:32 +01:00
Joachim Wiberg e756d6f8ac Allow forking sysv/services like podman to move between cgroups
The container monitor that podman forks off when starting a container
instance creates subgroups in the cgroup v2 hieararchy that we want to
reuse.  This patch adds cgroup_move_svc() which we call from the pidfile
plugins to relocate the conmon process.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:04:32 +01:00
Joachim Wiberg 567c695954 Start services with clone3() and place in cgroup directly
This commit activates the use of clone3() for service_fork(), to allow
Linux to create the new process directly in the correct cgroup instead
of later moving it there -- much cheaper and less error prone.

To facilitate this a few new helper cgroup functions have been added and
two new configuration directives introduced: delegate and name:leafname.
The delegate option is for running, e.g., container runtimes that want
to create their own cgroup v2 structur, and the name:leafname allows a
user to change the name of the subgroup under user/system/init.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 22:04:31 +01:00
Joachim Wiberg 4877aecc1e Add support for clone3() to replace fork()
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-15 09:32:03 +01:00
Joachim Wiberg 65644f5956 Follow-up to d9a0c2d, dead code
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-14 10:04:01 +01:00
Joachim Wiberg a02760f499 Minor, constify
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-14 10:00:39 +01:00
Joachim Wiberg 7aab3bb421 mkdocs: update project URLs and name
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-08 09:16:26 +01:00
Joachim Wiberg b5beb2b8d4 Minor, update URLs and clean up a bit
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-07 09:53:56 +01:00
Joachim Wiberg 09b4aee5bc Update ChangeLog and bump version for v4.15-rc1
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-07 09:38:14 +01:00
Joachim Wiberg d9a0c2dce9 Switch from PTY to pipe for service log redirection
The PTY approach caused isatty() to return true for services using
the log directive, triggering programs like fprintd (using glib) to
emit ANSI escape codes and other TTY-specific formatting in syslog.

Using a standard pipe ensures isatty() correctly returns false, so
programs produce plain text output suitable for logging.

For services that require line-buffered output, users can wrap the
command with `stdbuf -oL` as documented in doc/config/logging.md.

Fixes #455

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-07 09:38:13 +01:00
Joachim Wiberg 178301baf2 Change default reboot to SOC reset from WDT reset
Fixes #460

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-07 09:38:13 +01:00
Joachim Wiberg 4e18affa9b Allow skipping bootstrap wait with Ctrl-C
When setting up a new system with Finit it is very common to make small
logical mistakes that cause "hangs" at boot.  This is when Finit waits
for 180 sec. for run/tasks to complete before moving to the configured
runlevel.  This patch adds console input monitoring during bootstrap
wait that allows users to press Ctrl-C to skip waiting and proceed to
the configured runlevel.

When Ctrl-C is detected, all incomplete run/task/services are logged
to syslog for later troubleshooting after login.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-07 09:38:12 +01:00
Joachim Wiberg 21f40fb95e Allow more initctl commands in runlevels S/0/6
Relax the constraints introduced in a39ee0b, for issue #342, a bit on
when start/stop/restart/reload service can be called.  Also, allow
'initctl reload', but ignore it when the system is in runlevels S/0/6.

This makes it possible to start manual:yes type services at botostrap,
for example, which has been a common feature request.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-07 09:38:12 +01:00
Joachim Wiberg 24f0cee49a Ensure mount/unmount skips 'noauto' entries
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-12-07 09:38:11 +01:00
Joachim Wiberg fee38d4359 Merge pull request #458 from aanderse/master
Add support for Linux capabilities
2025-12-05 12:53:04 +01:00
Joachim Wiberg 0c85ea844a Merge pull request #459 from rical/add-mem-to-initctl-json
initctl: add memory field to JSON status output
2025-12-01 22:42:50 +01:00
Richard Alpe c3839edc40 initctl: add memory field to JSON status output
Add missing memory information to JSON output in json_status_one().
The memory field shows cgroup memory usage in bytes when cgroup
support is available and the service has a valid PID, matching
the behavior of the text status output.

Signed-off-by: Richard Alpe <richard@bit42.se>
2025-12-01 14:51:27 +01:00
Aaron Andersen be2a0ec3e7 Add support for Linux capabilities
Implement Linux capability support for services, allowing them to run
with minimal required privileges instead of running as root. This uses
the modern IAB (Inheritable, Ambient, Bounding) API from libcap.
2025-11-30 11:36:53 -05:00
Joachim Wiberg c23952cd9e Merge pull request #453 from liuming50/fix-rtc-restore-issues
plugins:rtc.c: drop some invalid condition checks

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-14 20:33:25 +02:00
Ming Liu 13a8f56b46 plugins:rtc.c: drop some invalid condition checks
There are some invalid condition checks in rtc.c, when rtc_file is
missing, it should not consider that as a error and return, otherwise,
it can lead to the case that time_set wont be called at all.

With this fix, when both RTC and file restore fail, it will fall back
to call time_set(NULL) and restore time from rtc_timestamp, this
ensures the OS has a valid time at the very first boot.

Signed-off-by: Ming Liu <liu.ming50@gmail.com>
2025-10-14 15:38:56 +02:00
Joachim Wiberg 927acdbd7e Increase MAX_ID_LEN to support longer service identifiers
Allow service IDs up to 64 characters to support SHA-256 hashes,
UUIDs, and other long unique identifiers. This increases memory
usage by ~98 bytes per service instance, which is negligible for
typical deployments.

The IDENT column width in initctl output adapts dynamically based
on actual ID lengths in use, so short IDs remain unaffected.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-10-14 11:49:57 +02:00
Joachim Wiberg 6f6267f3b3 When collecting a script or unregistering a service, cancel timeouts
Both service and sysv stanzas support a cleanup:script (since v4.10), with an
optional timeout.  These timers must be stopped when collecting a PID for an
svc_t, or when unregistering an svc_t, otherwise the timer callback will be
continuously called, reporting "spurious problem", and cause 100% CPU usage.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 16:49:34 +02:00
Joachim Wiberg dec89d4054 Minor, drop __func__ from dbg() statements
The dbg() macro already includes __func__ as the first text string in
the format string.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 16:48:38 +02:00
Joachim Wiberg 193e114596 Enable libsystemd replacement lib for tests also in distcheck stage
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 16:02:42 +02:00
Joachim Wiberg 1be67c434e Update ChangeLog and bump version for v4.14 release
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 15:07:45 +02:00
Joachim Wiberg 4f8dab75b2 Update test helper and all workflows to enble libsystemd build
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 14:43:25 +02:00
Joachim Wiberg 37d063fd3b Build --without-libsystemd by default
Many packages fail to detect that this replacement libsystemd does not
support the logging API (yet), so let's disable it by default.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 13:12:43 +02:00
Joachim Wiberg 97be3da126 Minor doc styling fixes
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 12:54:06 +02:00
Joachim Wiberg e7f97f8264 Fix state machine ordering issue
This patch fixes an ordering issue where the configuration generation is
stepped before all old/disabled services had been stopped.  Finit should
ensure disabled services are stopped before entering the next generation.

If the genation is stepped first, all conditions are put in "flux" state,
so, e.g, a podman container running as a sysv service may lock-up until
it is killed by Finit.  Found in the Infix OS project.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 12:47:43 +02:00
Joachim Wiberg 6415aeebac Ensure sysv 'stop' command has same env. as 'start'
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 10:17:19 +02:00
Mattias Barthel e69200ea43 Add exit status/signal info when a process dies
Signed-off-by: Mattias Barthel <mattiasbarthel@gmail.com>
2025-08-29 10:12:58 +02:00
Joachim Wiberg 26d5018224 Allow kill delay up to five minutes for debug
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 10:02:36 +02:00
Joachim Wiberg 1b7699ca93 Expand norespawn grasp to service_retry()
The norespawn hack pause Finit while a developer debugs the system.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 10:00:25 +02:00
Joachim Wiberg 8eab099f09 Minor, encase script + args in '' for debug help
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-08-29 09:58:10 +02:00
Joachim Wiberg c7c5426586 Fix #449: alsa plugin ftbfs
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-07-22 15:46:46 +02:00
Joachim Wiberg 85484178e0 Update ChangeLog and bump version for v4.13 GA
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-07-10 18:33:24 +02:00
Joachim Wiberg e958189e6a initctl: rename command signal -> kill
Already backwards compatible, both kill and signal map to the same command.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-07-10 18:29:39 +02:00
Joachim Wiberg eb13309a34 Restore README.md symlinks for GitHub viewing of docs
This reverts parts of commit e004762, because it obviously doesn't work.  See
https://github.com/orgs/community/discussions/72144#discussioncomment-11917736

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-07-10 18:18:09 +02:00
99 changed files with 4837 additions and 609 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
finit is a small project, as such we have no possibility to support older versions.
The only supported version is the latest released on GitHub:
<https://github.com/troglobit/finit/releases>
<https://github.com/finit-project/finit/releases>
## Reporting a Vulnerability
+15 -6
View File
@@ -34,21 +34,29 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config tree jq libuev-dev libite-dev
sudo apt-get -y install pkg-config tree jq libcap-dev
sudo apt-get -y install pkg-config
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- uses: actions/checkout@v4
- name: Static Finit
run: |
./autogen.sh
./configure --prefix= --enable-static
make -j3 V=1
make -j9 V=1
- name: Regular Finit
run: |
./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \
--enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \
--with-keventd \
--with-keventd --with-libsystemd \
CFLAGS="-fsanitize=address -ggdb"
make -j3 clean
make -j3 V=1
make -j9 clean
make -j9 V=1
- name: Install to /tmp
run: |
DESTDIR=/tmp make install-strip
@@ -70,8 +78,9 @@ jobs:
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
- name: Run Unit Tests
run: |
make check || (cat test/test-suite.log; false)
make -j1 check || (cat test/test-suite.log; false)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: finit-test-${{ matrix.compiler }}
+9 -8
View File
@@ -55,18 +55,19 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config
wget https://github.com/troglobit/libuev/releases/download/v2.4.0/libuev-2.4.0.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.5.2/libite-2.5.2.tar.gz
tar xf libuev-2.4.0.tar.xz
tar xf libite-2.5.2.tar.gz
(cd libuev-2.4.0 && ./configure --prefix=/usr && make && sudo make install-strip)
(cd libite-2.5.2 && ./configure --prefix=/usr && make && sudo make install-strip)
sudo apt-get -y install pkg-config libcap-dev
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- name: Configure
run: |
./autogen.sh
./configure --prefix= --enable-x11-common-plugin --with-watchdog --with-keventd
./configure --prefix= --enable-x11-common-plugin --with-watchdog \
--with-keventd --with-libsystemd
- name: Build
run: |
export PATH=`pwd`/coverity/bin:$PATH
+47 -15
View File
@@ -19,12 +19,6 @@ on:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
@@ -46,23 +40,61 @@ jobs:
pipx inject mkdocs mkdocs-material
pipx inject mkdocs pymdown-extensions
pipx inject mkdocs mkdocs-callouts
pipx inject mkdocs mkdocs-glightbox
- name: Build documentation
run: mkdocs build
run: mkdocs build --clean
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
- name: Upload site artifact
uses: actions/upload-artifact@v4
with:
name: site
path: site/
deploy:
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: Download site artifact
uses: actions/download-artifact@v4
with:
name: site
path: site/
- name: Checkout pages repo
uses: actions/checkout@v4
with:
repository: finit-project/finit-project.github.io
path: pages
ssh-key: ${{ secrets.DOCS_DEPLOY_KEY }}
persist-credentials: true
- name: Setup SSH for push
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan github.com >> ~/.ssh/known_hosts
- name: Sync site to pages repo
run: |
rsync -a --delete --exclude .git site/ pages/
- name: Commit and push
run: |
cd pages/
if [ -z "$(git status --porcelain)" ]; then
exit 0
fi
SRC_REPO="${GITHUB_REPOSITORY}"
SRC_REF="${GITHUB_SHA::7}"
SRC_URL="https://github.com/${SRC_REPO}/commit/${SRC_REF}"
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add -A
git commit -m "docs: update from ${SRC_REPO}@${SRC_REF}" \
-m "For details, see ${SRC_URL}"
git push
+24 -7
View File
@@ -13,14 +13,19 @@ jobs:
name: Build and upload release tarball
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
env:
MAKEFLAGS: -j3
steps:
- uses: actions/checkout@v4
- name: Installing dependencies ...
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config jq libuev-dev libite-dev
sudo apt-get -y install pkg-config jq libcap-dev
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- name: Setting release variables ...
id: build
run: |
@@ -39,18 +44,30 @@ jobs:
- name: Creating Makefiles ...
run: |
./autogen.sh
./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \
--enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \
--with-keventd
./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc \
--localstatedir=/var --enable-x11-common-plugin \
--enable-testserv-plugin --with-watchdog \
--with-keventd --with-libsystemd
- name: Enable unprivileged userns (unshare)
run: |
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
- name: Build release ...
run: |
make release || (cat test/test-suite.log; false)
# Get version for finding distcheck build directory
./configure --version | head -1
ver=$(./configure --version | head -1 | awk '{print $NF}')
dir=finit-$ver/_build/sub
make -j1 release || (cat $dir/test/test-suite.log; false)
ls -lF ../
mkdir -p artifacts/
mv ../*.tar.* artifacts/
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: finit-release-test-logs
path: finit-*/_build/sub/test/*.log
- name: Extract ChangeLog entry ...
run: |
awk '/-----*/{if (x == 1) exit; x=1;next}x' doc/ChangeLog.md \
+16 -3
View File
@@ -14,14 +14,14 @@ jobs:
schedule:
runs-on: ubuntu-latest
outputs:
run: ${{ steps.check.outputs.run }}
run: ${{ steps.schedule.outputs.run }}
steps:
- uses: actions/checkout@v4
- name: Check for any commits since last week
id: schedule
run: |
commits="$(git log --format=%H --since='1 week ago')"
if [ -n "$commits" ]; then
if [ -z "$commits" ]; then
echo "run=true" >> $GITHUB_OUTPUT
else
echo "run=false" >> $GITHUB_OUTPUT
@@ -34,7 +34,14 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get -y update
sudo apt-get -y install pkg-config jq libuev-dev libite-dev
sudo apt-get -y install pkg-config jq libcap-dev
wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz
wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz
tar xf libuev-2.4.1.tar.xz
tar xf libite-2.6.2.tar.gz
(cd libuev-2.4.1 && ./configure && make -j9 && sudo make install-strip)
(cd libite-2.6.2 && ./configure && make -j9 && sudo make install-strip)
sudo ldconfig
- uses: actions/checkout@v4
- name: Base run
run: |
@@ -52,3 +59,9 @@ jobs:
ver=$(/tmp/sbin/initctl -V | awk '/Finit.*/ {print $2;}')
dir=finit-$ver/_build/sub
make -j1 distcheck || (cat $dir/test/test-suite.log; false)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: finit-weekly-test-logs
path: finit-*/_build/sub/test/*.log
+1 -1
View File
@@ -43,4 +43,4 @@ release: distcheck
DISTCHECK_CONFIGURE_FLAGS = --prefix=/usr --sysconfdir=/etc --localstatedir=/var \
--enable-testserv-plugin --enable-x11-common-plugin \
--with-watchdog --with-keventd --with-fstab=/etc/fstab \
--with-bash-completion-dir=no
--with-libsystemd --with-bash-completion-dir=no
+22 -19
View File
@@ -1,17 +1,16 @@
[![License Badge][]][License] [![GitHub Status][]][GitHub] [![Coverity Status][]][Coverity Scan]
[![License Badge][]][License] [![Release Badge][]][Release] [![GitHub Status][]][GitHub] [![Coverity Status][]][Coverity Scan]
<img align="right" src="doc/img/finit3.png" alt="Finit: Fast Init">
Finit is an alternative to [SysV init](https://en.wikipedia.org/wiki/Init)
and [systemd](https://www.freedesktop.org/wiki/Software/systemd/), focused
on small and embedded Linux systems, although fully usable on server and
[desktop systems][finix].
Finit is a fast, simple alternative to [SysV init](https://en.wikipedia.org/wiki/Init)
and [systemd](https://systemd.io), designed for small and embedded Linux systems.
It can also run on desktop and server systems, like [finix][].
> Reverse engineered from the [EeePC fastinit][]
> "gaps filled with frog DNA …"
> — [Claudio Matsuoka][]
For detailed information, explore our extensive documentation
:books: **<https://troglobit.github.io/finit/>**
:books: **<http://finit-project.github.io>**
<div align="center">
<img src="doc/img/alpine-screenshot2.png" alt="Alpine screenshot"><br/>
@@ -20,28 +19,32 @@ For detailed information, explore our extensive documentation
For working examples, see the :rocket: [contrib/](contrib/) section or these
tutorials:
* :hammer_and_wrench: [Buildroot embedded Linux](https://troglobit.com/post/2022-12-26-buildroot-demo-of-fastinit-finit/),
* :package: [Debian GNU/Linux](contrib/debian/),
* :mountain: [Alpine Linux](contrib/alpine/), and
* :milky_way: [Void Linux](contrib/void/)
* :hammer_and_wrench: [Buildroot embedded Linux][br2demo],
* :package: [Debian GNU/Linux](contrib/debian/),
* :mountain: [Alpine Linux](contrib/alpine/), and
* :milky_way: [Void Linux](contrib/void/)
> [!NOTE]
> Support for various Linux distributions does not mean Finit installs
> easily on all architectures. Bundled install scripts are examples for
> standard installations, tested on amd64 (x86_64) systems. Custom
> setups, e.g., for embedded systems, can be found in the following
> [Buildroot][] based examples: [myLinux][], [Infix][], or the plain
> [br2-finit-demo](https://github.com/troglobit/br2-finit-demo).
> Finit can run on various Linux distributions, but the bundled install
> scripts are examples only. They have been tested on amd64 (x86_64) systems
> with standard configurations.
>
> For embedded systems, see these [Buildroot][]-based examples:
> [myLinux][], [Infix][], or [br2-finit-demo][].
[finix]: https://github.com/aanderse/finix
[finix]: https://github.com/finix-community/finix
[Buildroot]: https://buildroot.org
[Infix]: https://kernelkit.github.io
[myLinux]: https://github.com/troglobit/myLinux/
[br2demo]: https://troglobit.com/post/2022-12-26-buildroot-demo-of-fastinit-finit/
[EeePC fastinit]: https://web.archive.org/web/20071208212450/http://wiki.eeeuser.com/boot_process:the_boot_process
[Claudio Matsuoka]: https://github.com/cmatsuoka
[License]: https://en.wikipedia.org/wiki/MIT_License
[License Badge]: https://img.shields.io/badge/License-MIT-teal.svg
[GitHub]: https://github.com/troglobit/finit/actions/workflows/build.yml/
[GitHub Status]: https://github.com/troglobit/finit/actions/workflows/build.yml/badge.svg
[Release]: https://github.com/finit-project/finit/releases
[Release Badge]: https://img.shields.io/github/v/release/finit-project/finit
[GitHub]: https://github.com/finit-project/finit/actions/workflows/build.yml/
[GitHub Status]: https://github.com/finit-project/finit/actions/workflows/build.yml/badge.svg
[Coverity Scan]: https://scan.coverity.com/projects/3545
[Coverity Status]: https://scan.coverity.com/projects/3545/badge.svg
[br2-finit-demo]: https://github.com/finit-project/br2-finit-demo
+21 -5
View File
@@ -1,5 +1,5 @@
AC_INIT([Finit], [4.13-rc1], [https://github.com/troglobit/finit/issues],
[finit], [https://troglobit.com/projects/finit/])
AC_INIT([Finit], [4.17], [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])
AM_SILENT_RULES([yes])
@@ -48,8 +48,8 @@ AC_TYPE_UINT32_T
PKG_PROG_PKG_CONFIG
# Check for required libraries
PKG_CHECK_MODULES([uev], [libuev >= 2.2.0])
PKG_CHECK_MODULES([lite], [libite >= 2.2.0])
PKG_CHECK_MODULES([uev], [libuev >= 2.4.1])
PKG_CHECK_MODULES([lite], [libite >= 2.6.1])
# Check for configured Finit features
AC_ARG_ENABLE(auto_reload,
@@ -72,6 +72,10 @@ AC_ARG_ENABLE(cgroup,
AS_HELP_STRING([--disable-cgroup], [Disable cgroup v2 support, default: autodetect from /sys/fs/cgroup]),,[
enable_cgroup=yes])
AC_ARG_ENABLE(libcap,
AS_HELP_STRING([--disable-libcap], [Disable Linux capabilities support]),,[
enable_libcap=yes])
AC_ARG_ENABLE(redirect,
AS_HELP_STRING([--disable-redirect], [Disable redirection of service output to /dev/null]),,[
enable_redirect=yes])
@@ -112,6 +116,7 @@ AC_PLUGIN([hotplug], [yes], [Start udevd or mdev kernel event datamon])
AC_PLUGIN([rtc], [yes], [Save and restore RTC using hwclock])
AC_PLUGIN([tty], [yes], [Automatically activate new TTYs, e.g. USB-to-serial])
AC_PLUGIN([urandom], [yes], [Setup and save random seed at boot/shutdown])
AC_PLUGIN([plymouth], [no], [Plymouth boot splash integration])
AC_PLUGIN([testserv], [no], [Test plugin to start test serv daemon])
# Check for extra arguments or packages
@@ -171,7 +176,7 @@ AC_ARG_WITH(watchdog,
AC_ARG_WITH(libsystemd,
AS_HELP_STRING([--with-libsystemd], [Build replacement libsystemd library, default: yes]),
[with_libsystemd=$withval], [with_libsystemd=yes])
[with_libsystemd=$withval], [with_libsystemd=no])
AC_ARG_WITH(hook-scripts-path,
AS_HELP_STRING([--with-hook-scripts-path=DIR], [Base directory for hook scripts, default $libexecdir/finit/hook]),
@@ -204,6 +209,16 @@ AS_IF([test "x$enable_kernel_logging" = "xyes"], [
AS_IF([test "x$enable_cgroup" = "xyes"], [
AC_DEFINE(CGROUP2_ENABLED, 1, [Autodetect cgroup v2 support from /sys/fs/cgroup])])
AS_IF([test "x$enable_libcap" = "xyes"], [
AC_CHECK_LIB([cap], [cap_from_text], [
AC_DEFINE(HAVE_LIBCAP, 1, [Have libcap for Linux capabilities support])
LIBS="$LIBS -lcap"
], [
AC_MSG_WARN([libcap not found, capabilities support disabled])
enable_libcap=no
])
])
AS_IF([test "x$enable_fastboot" = "xyes"], [
AC_DEFINE(FAST_BOOT, 1, [Skip fsck check on filesystems listed in /etc/fstab])])
@@ -419,6 +434,7 @@ Optional features:
Built-in logrotate....: $enable_logrotate
Replacement libsystemd: $with_libsystemd
Use cgroup v2.........: $enable_cgroup
Use libcap............: $enable_libcap
Parse kernel cmdline..: $enable_kernel_cmdline
Keep kernel logging...: $enable_kernel_logging
Skip fsck check.......: $enable_fastboot
@@ -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
+176 -1
View File
@@ -3,6 +3,176 @@ Change Log
All relevant changes are documented in this file.
[4.17][] - 2026-04-28
---------------------
### Changes
- Add Plymouth boot splash plugin to manage the `plymouthd` lifecycle across boot,
`switch_root`, and shutdown. Activated by the `splash` kernel command line
argument. By Aaron Andersen
- Add `tty:` option for run/task/service stanzas to acquire a controlling
terminal. The device is opened as stdin, set as the session's controlling TTY
via `TIOCSCTTY`, and `TERM` is set automatically — `linux` for virtual terminals
and `vt102` for serial lines. By Ollie Gutierrez
- Improve console output atomicity at boot and shutdown: description and status
(`[ OK ]` / `[FAIL]`) are emitted in a single write so kernel messages cannot
split them. If output has been scrolled away, the description is re-printed
before the final status line. Console buffer is drained with `tcdrain(2)` and
ANSI attributes are reset before handing control back to the kernel at
shutdown/reboot
### Fixes
- Fix reverse-dependency condition handling on reload: when a service without
SIGHUP support is reloaded, its condition is now cleared immediately so
dependents transition to `STOPPING` before `SIGTERM` is sent to the parent,
preventing them from crashing with an incremented restart counter
- Fix `/var/tmp` mode in `tmpfiles.d/var.conf` from `0777` to `1777` (sticky bit)
to match `/tmp`, by Aaron Andersen
- Fix potential memory corruption in config parser: stale pointer captures of
`PATH` and `SHELL` at startup could dangle after environment modification;
`conf_reset_env()` now reads from the live environment instead, by Vasily Zubko
- Fix plugin name registration: replace `__FILE__` macro with explicit string
literals in all plugin structs and debug messages, preventing subtle build-
system–dependent macro expansion issues, by Vasily Zubko
[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
---------------------
The Finit project now has a new home page: <https://finit-project.github.io>
### Changes
- Change default reboot to SOC reset from WDT reset, issue #460
- Bump required versions of libite (v2.6.2) and libuev (v2.4.1)
- Allow longer service identifiers (`:id`), up to 64 characters
- Allow skipping bootstrap wait (180 sec) with Ctrl-C
- Unlock `initctl` commands start/stop/restart/reload service during bootstrap,
shutdown, and reboot. Also, allow `initctl reload` in bootstrap. This makes
it possible to start manual:yes type services at botostrap, for example, which
has been a common feature request
- Add support for `clone3()` syscall with atomic cgroup assignment, from Linux
kernel 5.3+. Eliminates race conditions where forked processes (e.g., logger
processes) ended up in wrong cgroups. Falls back to `fork()` on older kernels
- Add support for cgroup delegation, useful for, e.g., container runtimes
creating sub-groups for the container instance's processes
- Add cgroup leaf name override: `cgroup.system,name:udevd` allows using
descriptive names like `/system/udevd/` instead of filename derived names
like `/system/10-hotplug/`. Syntax supports combining with other options:
`cgroup.system,name:foo,delegate,cpu.max:10000`
- Add support for Linux capabilities, allowing services to run with minimal
required privileges instead of running as root. Uses the modern IAB API
(Inheritable, Ambient, Bounding) from libcap. By Aaron Andersen
- Add support for supplementary groups for services. Uses the syntax
`@user:group,sup1,sup2` to explicitly specify supplementary groups, in
addition to reading group membership from `/etc/group`. By Aaron Andersen
- Add CPU throttled information to `initctl` output, showing when a service
has been throttled due to reaching its CPU quota
- Add memory field to `initctl --json` status output for cgroup-enabled
services
- Default to user and group `root` (UID 0) for services
- Always check that service user and group resolve to valid UID/GID pairs,
otherwise log error and refuse to start service
- Hide cursor at boot and shutdown for cleaner visual experience
- Rename `tty` services early from `init` -> `getty` even though no getty
process has been started yet. This to aid in boot timing detection using
tools like `bootchartd` (BusyBox version or bootchart2 project) when Finit
only shows *"Please press Enter to activate this console."*
- Services with `respawn` option (including TTYs) now delay retry on non-zero
exit codes instead of immediate respawn. Prevents busy-loop when service
fails repeatedly, which on some systems may have a crippling effect
### Fixes
- Fix #453: RTC plugin fixes by Ming Liu, Atlas Copco
- Fix #455: ANSI escape sequences logged by tools using glib logging,
caused by use of PTY instead of pipe for stdout/stderr redirection
- Fix #462: `/dev/pts` mounted with wrong mode (400 instead of 620), causing
permission issues with pseudo-terminals
- getty: fix terminal scrollback issues after login on console TTY. Mouse
scroll wheel and Shift+PgUp/PgDn sometimes would not work properly
- Fix flickering and artifacts in `initctl top`
- Fix file descriptor leak in `initctl top`
- Fix incorrect "max" limit reporting in `initctl [top|cgroup|--json]` output
for child cgroups. Resolves effective limits by walking the cgroup hierarchy
- Fix issue with running Finit under `bootchartd` (bootchart2 project), always
set `PATH` and `SHELL` envs early, before attempting to mount `/proc` et al
- Always reset ownership and permissions on TTY device nodes before launching getty
- Ensure mount/unmount skips `noauto` entries
[4.14][] - 2025-08-29
---------------------
Minor bug fix release.
### Changes
- Add exit status/signal info when a process dies, by Mattias Berthel
- Extend `service kill:SEC` to support up to 300 sec.
- Extend `norespawn` debug to cover `service_retry()`
- Build *without* libsystemd replacement by default
### Fixes
- Fix #449: ALSA plugin fail-to-build-from-source
- Fix `sysv` stop command process environment, should be same as start
- Fix state machine ordering issue: enter new config generation *after*
services disabled in previous generation have been stopped
[4.13][] - 2025-07-10
---------------------
@@ -13,6 +183,7 @@ All relevant changes are documented in this file.
> `reload:script` and `stop:script`, mimicking ExecReload and ExecStop.
### Changes
- `initctl signal` command has been renamed to `initctl kill`
- Drop xz tarballs from distribution, gzip is good enough
- New `tmpfiles` stand-alone program, refactored from the existing
tmpfiles.d support in Finit, by Aaron Andersen, PR #439
@@ -1832,7 +2003,11 @@ Major bug fix release.
* Initial release
[UNRELEASED]: https://github.com/troglobit/finit/compare/4.12...HEAD
[UNRELEASED]: https://github.com/finit-project/finit/compare/4.17...HEAD
[4.17]: https://github.com/finit-project/finit/compare/4.16...4.17
[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
[4.12]: https://github.com/troglobit/finit/compare/4.11...4.12
[4.11]: https://github.com/troglobit/finit/compare/4.10...4.11
+1
View File
@@ -0,0 +1 @@
index.md
+1 -2
View File
@@ -186,7 +186,6 @@ it only for debugging start up issues when Finit crashes.
> None of these options should be enabled on production systems since
> they can potentially give a user root access.
[1]: ftp://troglobit.com/finit/finit-4.3.tar.gz
[1]: https://github.com/finit-project/finit/releases/download/4.3/finit-4.3.tar.gz
[libuEv]: https://github.com/troglobit/libuev
[libite]: https://github.com/troglobit/libite
+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
+1
View File
@@ -0,0 +1 @@
index.md
+186
View File
@@ -0,0 +1,186 @@
Linux Capabilities
==================
Finit supports Linux capabilities, allowing services to run with minimal
required privileges instead of running as root. This significantly improves
system security by following the principle of least privilege.
## Overview
Linux capabilities divide the traditional root privileges into distinct units
that can be independently granted to processes. For example, a web server only
needs the capability to bind to privileged ports (< 1024), not full root access.
Finit uses the modern IAB (Inheritable, Ambient, Bounding) API from libcap,
which is the same approach used by other modern service managers like dinit.
## Basic Usage
Capabilities are specified using the `caps:` directive in service configuration:
```conf
service [2345] name:nginx \
@www-data:www-data \
caps:^cap_net_bind_service \
/usr/sbin/nginx -g 'daemon off;' \
-- Web server
```
This example allows nginx to bind to privileged ports (like 80 and 443) while
running as the unprivileged `www-data` user.
## IAB Format
The capability string uses the IAB (Inheritable, Ambient, Bounding) format
with the following prefixes:
- `^` **Ambient** (and Inheritable) - **Recommended for most use cases**
- Capabilities survive across `exec()` calls
- Automatically raised to effective after exec
- Example: `^cap_net_bind_service`
- `%` **Inheritable** only
- Requires the executed binary to have matching file capabilities
- Less common, more complex setup
- Example: `%cap_net_admin`
- `!` **Bounding** - Block capability from bounding set
- Prevents the service from ever acquiring this capability
- Useful for security hardening
- Example: `!cap_sys_admin`
Multiple capabilities can be specified as a comma-separated list:
```conf
caps:^cap_net_raw,^cap_net_admin,^cap_net_bind_service
```
## Common Use Cases
### Web Server (Privileged Ports)
Allow a web server to bind to ports 80 and 443 without running as root:
```conf
service [2345] name:webserver \
@www-data:www-data \
caps:^cap_net_bind_service \
/usr/sbin/nginx -g 'daemon off;'
```
### Network Monitoring (Raw Sockets)
Allow packet capture without root privileges:
```conf
service [2345] name:tcpdump \
@tcpdump \
caps:^cap_net_raw,^cap_net_admin \
/usr/sbin/tcpdump -i eth0 -w /var/log/capture.pcap
```
### NTP Daemon (System Time)
Allow time synchronization without full root:
```conf
service [2345] name:ntpd \
@ntp \
caps:^cap_sys_time,^cap_sys_nice \
/usr/sbin/ntpd -n
```
## Available Capabilities
Common capabilities include (see `man 7 capabilities` for the complete list):
- `cap_chown` - Make arbitrary changes to file UIDs and GIDs
- `cap_dac_override` - Bypass file read, write, and execute permission checks
- `cap_dac_read_search` - Bypass file read permission checks
- `cap_fowner` - Bypass permission checks on operations that normally require filesystem UID
- `cap_kill` - Bypass permission checks for sending signals
- `cap_net_admin` - Perform various network-related operations
- `cap_net_bind_service` - Bind to privileged ports (< 1024)
- `cap_net_raw` - Use RAW and PACKET sockets
- `cap_setgid` - Make arbitrary manipulations of process GIDs
- `cap_setuid` - Make arbitrary manipulations of process UIDs
- `cap_sys_admin` - Perform system administration operations (very powerful!)
- `cap_sys_module` - Load and unload kernel modules
- `cap_sys_nice` - Raise process nice value and change scheduling
- `cap_sys_time` - Set system clock
## Security Best Practices
1. **Use the minimum required capabilities**
- Only grant what the service actually needs
- Don't grant `cap_sys_admin` unless absolutely necessary
2. **Specify a user (preferably non-root)**
- The `@user` directive is **required** for `caps:` to take effect
- For ambient capabilities (`^`), use a non-root user (not `@root`)
- Example: `@www-data`, `@nginx`, `@tcpdump`
3. **Use ambient capabilities (`^`)**
- The `^` prefix ensures capabilities survive exec()
- Simpler than setting file capabilities on binaries
4. **Block dangerous capabilities**
- Use `!` to explicitly block capabilities you don't want
- Example: `!cap_sys_admin,!cap_sys_module`
5. **Test with `getpcaps`**
- After starting a service, verify its capabilities:
```bash
getpcaps $(pidof nginx)
```
- Should show only the capabilities you granted
## Verification
After configuring a service with capabilities, verify it works correctly:
```bash
# Start the service
initctl start webserver
# Check the process capabilities
getpcaps $(pidof nginx)
# Should show something like:
# 12345: cap_net_bind_service=eip
# Verify the user
ps -o user,pid,cmd -p $(pidof nginx)
# Should show the service running as the specified user
```
## Requirements
- Linux kernel 4.3+ (for ambient capabilities support)
- libcap library installed
- Finit built with `--enable-libcap`
## Limitations
- The `caps:` directive requires `@user` to be specified for it to take effect
- Without `@user`, the service runs as root with full capabilities and
the `caps:` configuration is silently ignored
- You can use `@root` with `caps:`, but see below about ambient capabilities
- For ambient capabilities (`^`, recommended), the user **must be non-root**
- Using `@root` with `caps:^...` will not work effectively, as ambient
capabilities are only added to the effective set when euid ≠ 0
- Use inheritable (`%`) or bounding (`!`) capabilities with `@root` if needed
- Services without `caps:` use standard privilege dropping:
- Services with `@user` (non-root) have no special capabilities
- Services without `@user` run as root with full capabilities
- Some very old binaries may not work correctly with ambient capabilities
- File system capabilities are not managed by Finit (use `setcap` for that)
## See Also
- [capabilities(7)](https://man7.org/linux/man-pages/man7/capabilities.7.html) - Linux capabilities overview
- [cap_iab(3)](https://man7.org/linux/man-pages/man3/cap_iab.3.html) - IAB capability API documentation
- [setcap(8)](https://man7.org/linux/man-pages/man8/setcap.8.html) - Set file capabilities
- [getcap(8)](https://man7.org/linux/man-pages/man8/getcap.8.html) - Query file capabilities
- [capsh(1)](https://man7.org/linux/man-pages/man1/capsh.1.html) - Capability shell wrapper
+171 -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,32 +47,169 @@ 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 on the form:
This form also allows per-service limits. Two syntaxes are supported:
**New comma-separated syntax (recommended):**
service [...] <...> cgroup.maint,cpu.max:10000,mem.max:655360 /path/to/foo args -- description
**Old colon-separated syntax (legacy):**
service [...] <...> cgroup.maint:cpu.max:10000,mem.max:655360 /path/to/foo args -- description
Notice the comma separation and the `mem.` exception to the rule: every
cgroup setting maps directly to cgroup v2 syntax. I.e., `cpu.max` maps
to the file `/sys/fs/cgroup/maint/foo/cpu.max`. There is no filtering,
except for expanding the shorthand `mem.` to `memory.`, if the file is
not available, either the cgroup controller is not available in your
Linux kernel, or the name is misspelled.
You can also apply options to the current default cgroup (without changing it)
using the `cgroup:options` syntax:
A daemon using `SCHED_RR` currently need to run outside the default cgroups.
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.
Note the `mem.` exception to the rule: every cgroup setting maps directly to
cgroup v2 syntax. I.e., `cpu.max` maps to the file `/sys/fs/cgroup/maint/foo/cpu.max`.
There is no filtering, except for expanding the shorthand `mem.` to `memory.`.
If the file is not available, either the cgroup controller is not available
in your Linux kernel, or the name is misspelled.
### Overriding Cgroup Leaf Names
By default, the cgroup leaf directory name is derived from the service
configuration filename (without the `.conf` extension). For example, a
service defined in `system/10-hotplug.conf` would create a cgroup at
`/sys/fs/cgroup/system/10-hotplug/` by default.
To use a more descriptive name (recommended for clarity), you can specify
`name:` in the cgroup directive:
service [...] <...> cgroup.system,name:udevd /lib/systemd/systemd-udevd -- Device event daemon
This creates the cgroup at `/sys/fs/cgroup/system/udevd/` instead.
The syntax supports combining the name override with other options:
service [...] <...> cgroup.system,name:udevd,cpu.max:10000 /lib/systemd/systemd-udevd -- Device event daemon
Or with delegation:
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 needs to run outside the default cgroups.
service [...] <...> cgroup.root /path/to/daemon arg -- Real-Time process
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] @dockerd:dockerd \
cgroup.system,delegate /usr/bin/dockerd -- Docker daemon
Or with the old colon syntax:
service [2345] @dockerd:dockerd \
cgroup.system:delegate /usr/bin/dockerd -- Docker daemon
This allows the container runtime to:
- Create child cgroups for containers
- Manage controller settings for containers
- Move processes between cgroups
When delegation is enabled, Finit:
1. Creates the service cgroup as a **domain group** (not a leaf)
2. Enables all available controllers in `cgroup.subtree_control`
3. Changes ownership of delegation files to the service user
4. Moves the service process to the cgroup root
5. Lets the container runtime manage its own subdirectories
**Requirements:**
- The service should specify `@user:group` for proper ownership
- Controllers are delegated from the parent cgroup
**Example with additional config (new syntax):**
service [2345] @podman:podman \
cgroup.containers,delegate,mem.max:4G \
/usr/bin/podman system service -- Podman API
**Or with old syntax:**
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:
/sys/fs/cgroup/system/container@web/
├── cgroup.procs (service PID - owned by service user)
├── cgroup.subtree_control (+cpu +memory +io - owned by service user)
└── (container children will be created here)
Once the container runtime creates child cgroups (e.g., `libpod-*/`), cgroups v2
enforces the "no internal processes" rule. When Finit detects this (`EBUSY` error),
it automatically creates an `supervisor/` subdirectory and moves service-related
processes there:
/sys/fs/cgroup/system/container@web/
├── cgroup.procs (empty)
├── cgroup.subtree_control (+cpu +memory +io)
├── supervisor/ (service processes)
│ └── cgroup.procs (conmon PIDs, etc.)
└── libpod-$HASH/ (container processes)
└── cgroup.procs (container PIDs)
This happens automatically - no configuration needed. Without delegation, the
cgroup would be a leaf and the container runtime could not create child cgroups.
+1 -1
View File
@@ -32,7 +32,7 @@ the following layout is recommended:
`- ...
Configuration files in `/etc` are provided by the user, or projects like
[finit-skel](https://github.com/troglobit/finit-skel) and extended by
[finit-skel](https://github.com/finit-project/finit-skel) and extended by
the user.
The files in `/lib/finit/system/*.conf` are system critical services and
+22
View File
@@ -39,3 +39,25 @@ Log rotation is controlled using the global `log` setting.
**Example:**
service log:prio:user.warn,tag:ntpd /sbin/ntpd pool.ntp.org -- NTP daemon
Output Buffering
----------------
When using the `log` directive, Finit redirects the service's stdout and
stderr to a pipe connected to a logger process. Programs detect this as
non-interactive output (i.e., `isatty()` returns false) and typically
switch from line-buffered to fully-buffered mode.
Most well-behaved daemons explicitly flush their output or use syslog
directly, so this is rarely an issue. However, if a service's log
messages appear delayed or batched, you can force line-buffered output
by wrapping the command with `stdbuf`:
service log /usr/bin/stdbuf -oL /path/to/command -- My service
The `-oL` option forces line-buffered output, and `-o0` forces unbuffered
output. See `stdbuf(1)` for details.
> [!NOTE]
> Using `stdbuf` is rarely necessary. Only use it if you observe actual
> buffering issues with a specific service.
+24 -5
View File
@@ -127,11 +127,12 @@ Resource Limits
Set the hard or soft limit for a resource, or both if that argument is
omitted. `RESOURCE` is the lower-case `RLIMIT_` string constants from
`setrlimit(2)`, without prefix. E.g. to set `RLIMIT_CPU`, use `cpu`.
LIMIT is an integer that depends on the resource being modified, see
the man page, or the kernel `/proc/PID/limits` file, for details.
Finit versions before v3.1 used `infinity` for `unlimited`, which is
still supported, albeit deprecated.
[setrlimit(2)](https://man7.org/linux/man-pages/man2/setrlimit.2.html),
or the kernel `/proc/PID/limits` file, for details. Finit versions
before v3.1 used `infinity` for `unlimited`, which is still supported,
albeit deprecated.
# No process is allowed more than 8MB of address space
rlimit hard as 8388608
@@ -165,4 +166,22 @@ sync(2) has been called, twice.
> "On Linux, sync is only guaranteed to schedule the dirty blocks for
> writing; it can actually take a short time before all the blocks are
> finally written.
> finally written.
**Syntax:** `reboot-watchdog <on|off|true|false|1|0>`
Controls whether the system should reboot via the watchdog timer (WDT)
or directly via the SoC/kernel. When enabled, Finit will:
1. Send `SIGPWR` to the registered watchdog daemon before shutdown
2. Send `SIGTERM` to the watchdog daemon and wait up to 10 seconds
for the watchdog to trigger a hardware reset
When disabled (default), Finit skips the watchdog reboot logic and
calls the kernel's `reboot(2)` syscall directly for a clean SoC reboot.
*Default:* off (reboot via SoC)
> [!NOTE]
> This setting only affects reboots. The watchdog daemon will still
> run and monitor the system during normal operation.
+8 -1
View File
@@ -42,9 +42,11 @@ started at any time by running `initctl start <service>`.
Other run/task/service options are:
* `cgroups:...` -- see the [Cgroups](cgroups.md) section
* `caps:...` -- see the [Linux Capabilities](capabilities.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)
* `tty:<dev>` -- see [Controlling TTY](tty.md#controlling-tty)
* `nowarn` -- see [Conditional Loading](services.md#conditional-loading)
* `notify:...` -- see [Service Synchronization](service-sync.md)
* `if:...` -- see [Conditional Execution](services.md#conditional-execution)
@@ -68,6 +70,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
+24 -1
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
@@ -116,10 +124,25 @@ Non-privileged Services
Every `run`, `task`, or `service` can also list the privileges the
`/path/to/cmd` should be executed with. Prefix the command with
`@USR[:GRP]`, group is optional, like this:
`@USR[:GRP[,SUPP,...]]`, where group and supplementary groups are
optional, like this:
run [2345] @joe:users logger "Hello world"
Finit reads the user's supplementary group membership from `/etc/group`
automatically. Any groups the user belongs to will be inherited by
the service.
To specify additional supplementary groups beyond those in `/etc/group`,
append them after the primary group, separated by commas:
service @caddy:caddy,ssl-cert /usr/bin/caddy run
This runs the `caddy` service as user `caddy`, with primary group
`caddy`, inheriting any groups `caddy` is a member of in `/etc/group`,
plus the additional `ssl-cert` group. This is useful when a service
needs access to resources owned by groups not listed in `/etc/group`.
For multiple instances of the same command, e.g. a DHCP client or
multiple web servers, add `:ID` somewhere between the `run`, `task`,
`service` keyword and the command, like this:
+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.
+28 -1
View File
@@ -83,4 +83,31 @@ board bringup and system debugging it can come in handy.
One can also use the `service` stanza to start a stand-alone shell:
service [12345] /bin/sh -l
service [12345] /bin/sh -l
Controlling TTY for Services
----------------------------
The `tty:<dev>` option gives a `run`, `task`, or `service` a controlling
terminal on the given device. The device is opened, set as the
controlling terminal for the session (after `setsid()`), and connected to
the process's stdin, stdout, and stderr. A default `TERM` environment
variable is set based on the device type: `vt102` for serial lines and
`linux` for virtual terminals.
`<dev>` may be a device node like `/dev/ttyS0`, or the special keyword
`@console` (see above). Note that `@console` expands only to the
first console, not all.
When `tty:` is combined with `log:`, stdout and stderr are redirected
to the log sink instead of the TTY, but stdin remains connected to the
TTY device.
> The `tty:<dev>` option is for `run`, `task`, and `service` stanzas only.
> The `tty` directive itself (for getty/login) has its own syntax, see
> above.
**Example:**
service [2345] tty:/dev/ttyS0 /usr/sbin/foo -- Foo on serial console
task [S] tty:@console my-setup-script -- Board bringup on console
+105 -46
View File
@@ -1,80 +1,139 @@
Distro Recommendations
======================
By default Finit uses the following directories for configuration files:
Finit supports two directory layouts for managing service configurations:
1. **Simple layout** - all configuration files directly in `/etc/finit.d/`
2. **Distro layout** - separate `available/` and `enabled/` directories
The distro layout is recommended for distributions that need clear
separation between installed and enabled services, and want to use the
`initctl enable/disable` commands for service management.
## Simple Layout
Finit supports using a flat directory structure:
```
/etc/
|-- finit.d/ -- Regular (enabled) services
|-- finit.d/ -- All enabled services
| |-- lighttpd.conf
| `- *.conf
|-- finit.conf -- Bootstrap tasks and services
:
```
To enable a service one simply drops a small configuration file in the
`/etc/finit.d/` directory. This practice works with systems that
keep disabled services elsewhere, or generates them as needed from some
other tool.
In this layout, you enable a service by placing its `.conf` file in
`/etc/finit.d/` and disable it by removing the file. This works well for
embedded systems or custom setups where services are managed by other tools
or generated dynamically.
Distributions, however, may want a clearer separation of enabled and
available (installed but not enabled) services. They may even want to
customize the directories used, for brand labeling or uniformity.
> [!NOTE] No tooling support
> The `initctl enable/disable` commands **do not work** with this layout.
> You must manually manage files in `/etc/finit.d/`.
To that end Finit allows for a sub-directory `/etc/finit.d/available/`
where installed but disabled services can reside. Adding a symlink to a
configuration in this sub-directory enables the service, but it will not
be started.
## Distro Layout
To change the default configuration directory and configuration file
names the Finit `configure` script offers the following two options at
build time:
Distributions typically want clearer separation between available (installed)
and enabled services. Finit supports this through the `available/` and
`enabled/` subdirectories:
```
/etc/
|-- finit.d/
| |-- available/ -- Installed but disabled services
| | |-- httpd.conf
| | |-- ntpd.conf
| | `-- sshd.conf
| |-- enabled/ -- Enabled services (symlinks)
| | `- sshd.conf -- Symlink to ../available/sshd.conf
|-- finit.conf -- Bootstrap tasks and services
:
```
In this layout:
- **available/** contains all installed service configurations
- **enabled/** contains symlinks to configurations that should start at boot
- Services are enabled/disabled by creating/removing symlinks
> [!IMPORTANT] Recommended
> This is the recommended layout. In fact, the `initctl enable/disable`
> commands **require** this layout with both `available/` and `enabled/`
> directories and will not work without it.
## Managing Services with initctl
When using the distro layout, the `initctl` tool provides convenient commands
for managing service configurations:
```
list List all .conf in /etc/finit.d/
enable <CONF> Enable .conf by creating symlink in enabled/
disable <CONF> Disable .conf by removing symlink from enabled/
reload Reload *.conf in /etc/finit.d/ (activate changes)
```
Example usage:
```bash
# Enable sshd (creates enabled/sshd.conf -> ../available/sshd.conf)
initctl enable sshd
# Disable httpd (removes enabled/httpd.conf symlink)
initctl disable httpd
# Apply changes (reload configuration and start/stop services)
initctl reload
```
The `.conf` suffix is optional - `initctl` adds it automatically if missing.
> [!NOTE] Remember `initctl reload`
> Changes made with `enable`/`disable` take effect only after running
> `initctl reload`, unless Finit was built with `--enable-auto-reload`
> which automatically detects and applies configuration changes.
### Service Overrides
The `initctl` tool only operates on symlinks in the `enabled/` directory.
If you place a regular (non-symlink) `.conf` file directly in the parent
directory (e.g., `/etc/finit.d/`), `initctl` will ignore it. This allows
you to create system-level overrides that cannot be accidentally disabled
by package management tools.
## Customizing Directory Paths
Distributions can customize the directory names and locations at build time
using the Finit `configure` script:
```sh
./configure --with-rcsd=/etc/init.d --with-config=/etc/init.d/init.conf
```
> [!IMPORTANT]
> Remember `--prefix` et al as well, the default is likely *not* what
> you want. See the [build docs][1] for details.
This changes the default `/etc/finit.d/` to `/etc/init.d/` and moves the
bootstrap configuration file accordingly.
The resulting directory structure is depicted below. Please notice how
`/etc/finit.conf` now resides in the same sub-directory as a non-symlink
`/etc/init.d/init.conf`:
> [!IMPORTANT] Remember to set `--prefix`
> Remember to set `--prefix` and related options appropriately. The default
> prefix is `/usr/local`, which is likely not what you want for a system
> init. See the [build documentation][1] for details.
Example with custom paths:
```
/etc/
|-- init.d/
| |-- available/ -- Regular (disabled) services
| |-- available/ -- Installed services
| | |-- httpd.conf
| | |-- ntpd.conf
| | `-- sshd.conf
| |-- sshd.conf -- Symlink to available/sshd.conf
| |-- enabled/ -- Enabled services (symlinks)
| | `- sshd.conf -- Symlink to ../available/sshd.conf
| `- init.conf -- Bootstrap tasks and services
:
```
To facilitate the task of managing configurations, be it a service,
task, run, or other stanza, the `initctl` tool has a a few built-in
commands:
```
list List all .conf in /etc/finit.d/
enable <CONF> Enable .conf in /etc/finit.d/available/
disable <CONF> Disable .conf in /etc/finit.d/[enabled/]
reload Reload *.conf in /etc/finit.d/ (activates changes)
```
To enable a service like `sshd.conf`, above, use
initctl enable sshd
The `.conf` suffix is not needed, `initctl` adds it implicitly if it is
missing. The `disable` command works in a similar fashion.
Note, however, that `initctl` only operates on symlinks and it always
requires the `available/` sub-directory. Any non-symlink in the parent
directory, here `/etc/init.d/`, will be considered a system override by
`initctl`.
Notice how both the service directory and bootstrap configuration file now
use the custom `/etc/init.d/` path specified at build time.
[1]: build.md#configure
+1 -1
View File
@@ -134,4 +134,4 @@ and as soon as it comes back up again `ntpd` is restarted automatically.
[finit.conf(5)]: https://man.troglobit.com/man5/finit.conf.5.html
[infix]: https://kernelkit.github.io
[myLinux]: https://github.com/troglobit/myLinux/
[contrib]: https://github.com/troglobit/finit/tree/master/contrib
[contrib]: https://github.com/finit-project/finit/tree/master/contrib
+11
View File
@@ -0,0 +1,11 @@
.md-header__topic:first-child {
font-weight: 500;
font-style: italic;
}
.md-header__title {
font-size: 1.1rem;
font-style: italic;
font-weight: 500;
line-height: 2.6rem;
}
+136
View File
@@ -1,6 +1,10 @@
Finit Features
==============
This page highlights some of Finit's key features with examples and usage
scenarios. For complete documentation, configuration syntax, and advanced
options, see the [Configuration](config/index.md) section.
**Process Supervision**
Start, monitor and restart services should they fail.
@@ -130,6 +134,60 @@ unnecessary overhead, which can be removed at build-time using:
configure --enable-auto-reload
**Linux Capabilities**
Finit supports Linux capabilities, allowing services to run with minimal
required privileges instead of running as root. This improves security by
following the principle of least privilege.
```conf
service [2345] name:nginx \
www-data:www-data \
caps:^cap_net_bind_service \
/usr/sbin/nginx -g 'daemon off;'
```
In this example, nginx runs as the unprivileged `www-data` user but retains
the ability to bind to privileged ports (80, 443) through the
`cap_net_bind_service` capability.
The `caps:` directive uses the IAB (Inheritable, Ambient, Bounding) format:
- `^` = Ambient (recommended) - capabilities survive exec()
- `%` = Inheritable only - requires file capabilities
- `!` = Bounding - block from acquiring capability
Multiple capabilities can be specified as comma-separated:
```conf
caps:^cap_net_raw,^cap_net_admin,!cap_sys_admin
```
See the [Linux Capabilities](config/capabilities.md) section for detailed
information, examples, and security best practices.
**Supplementary Groups**
Finit supports supplementary groups for services, allowing them to access
resources owned by multiple groups without running as root. This complements
capabilities for fine-grained privilege control.
```conf
service @caddy:caddy,ssl-cert /usr/bin/caddy run
```
In this example, the Caddy web server runs as user `caddy` with primary group
`caddy`, but also has access to resources owned by the `ssl-cert` group (such
as TLS certificates).
Finit automatically reads the user's supplementary group membership from
`/etc/group`. Additional groups can be specified explicitly using the syntax
`@user:group,sup1,sup2,...`.
See the [Non-privileged Services](config/services.md#non-privileged-services)
section for more information.
**Cgroups**
Finit supports cgroups v2 and comes with the following default groups in
@@ -175,4 +233,82 @@ commands for details.
> disabled early at boot.
**Service Management**
Finit includes the `initctl` tool for managing services and system state at
runtime. Key capabilities include:
- **Enable/Disable services**: Manage which services start at boot by moving
configuration files between `/etc/finit.d/available` and
`/etc/finit.d/enabled`
- **Start/Stop/Restart**: Control individual services without requiring a
full system reboot
- **Status monitoring**: View service state, PID, uptime, and resource usage
- **Condition management**: Set and clear user-defined conditions to control
service dependencies
- **Cgroup monitoring**: Real-time process and resource monitoring with
`initctl top`, similar to the traditional `top` command but cgroup-aware
Example commands:
```bash
initctl enable myservice # Enable service for next boot
initctl start myservice # Start service now
initctl status # Show all services
initctl top # Interactive resource monitor
initctl cond set usr/custom # Set custom condition
```
See the [Commands & Status](initctl.md) section for complete documentation.
**Rescue Mode**
Finit provides a built-in rescue mode for system recovery and maintenance.
When booting with the `rescue` kernel parameter, the system enters a
protected maintenance shell.
If the bundled `sulogin` program is available (from Finit, util-linux, or
BusyBox), you'll be prompted for the root password before accessing the
maintenance shell. This provides secure access for system recovery.
If `sulogin` is not available, Finit falls back to reading
`/lib/finit/rescue.conf` and boots the system in a limited maintenance mode.
```
# Kernel command line
linux /vmlinuz root=/dev/sda1 rescue
```
In rescue mode, `initctl` will not work. After fixing the problem, use
`reboot -f` to force reboot.
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
+21 -15
View File
@@ -1,7 +1,7 @@
Introduction
============
![Alpine screenshot](img/alpine-screenshot2.png){ align=right }
![Alpine screenshot](img/alpine-screenshot2.png){ align=right width=40% }
> Reverse engineered from the [EeePC fastinit][]
> "gaps filled with frog DNA …"
@@ -17,26 +17,31 @@ PID file monitoring, or [conditions](conditions.md).
Features
--------
* [Runlevels][5], defined per service
* One-shot tasks, services (daemons), or [SysV init][4] start/stop scripts
* Runparts and `/etc/rc.local` support
* [Runlevels](config/runlevels.md), defined per service
* [One-shot tasks](config/task-and-run.md), [services](config/services.md) (daemons), or [SysV init][4] start/stop scripts
* [Runparts](config/runparts.md) and `/etc/rc.local` support
* Process supervision similar to [systemd][]
* Sourcing environment files
* Conditions for network/process/custom dependencies
* Readiness notification; PID files (native) for synchronizing system
* Fine-grained privilege control:
- [Linux capabilities](config/capabilities.md) for minimal required privileges
- [Supplementary groups](config/services.md#non-privileged-services) for multi-group resource access
* [Sourcing environment files](config/service-env.md)
* [Conditions](conditions.md) for network/process/custom dependencies
* [Readiness notification](config/service-sync.md); PID files (native) for synchronizing system
startup, support for systemd [sd_notify()][], or [s6 style][] too
* Limited support for [tmpfiles.d(5)][] (no aging, attributes, or subvolumes)
* Pre/Post script actions
* [Pre/Post script actions](config/service-wrappers.md)
* Rudimentary [templating support](config/templating.md)
* Tooling to enable/disable services
* Built-in getty
* [Tooling](initctl.md) to enable/disable services
* [Automatic reload](features.md#automatic-reload) of modified configuration files (optional)
* [Built-in getty](config/tty.md)
* Built-in watchdog, with support for hand-over to [watchdogd][]
* Built-in support for Debian/BusyBox [`/etc/network/interfaces`][3]
* Cgroups v2, both configuration and monitoring in `initctl top`
* Plugin support for customization
* Proper rescue mode with bundled `sulogin` for protected maintenance shell
* [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 to kernel ring buffer before `syslogd` has started, see the
* [Logging](config/logging.md) to kernel ring buffer before `syslogd` has started, see the
recommended [sysklogd][] project for complete logging integration
and how to log to the kernel ring buffer from scripts using `logger`
@@ -63,7 +68,8 @@ and proposed extensions.
[3]: https://manpages.debian.org/unstable/ifupdown2/interfaces.5.en.html
[4]: https://en.wikipedia.org/wiki/Init
[5]: https://en.wikipedia.org/wiki/Runlevel
[6]: https://github.com/troglobit/finit
[6]: https://github.com/finit-project/finit
[dinit]: https://davmac.org/projects/dinit/
[systemd]: https://www.freedesktop.org/wiki/Software/systemd/
[sd_notify()]: https://www.freedesktop.org/software/systemd/man/sd_notify.html
[s6 style]: https://skarnet.org/software/s6/notifywhenup.html
+23 -1
View File
@@ -49,7 +49,7 @@ Commands:
This allows restart of run/tasks that have already run
Note: Finit .conf file(s) are *not* reloaded!
restart <NAME>[:ID] Restart (stop/start) service by name
signal <NAME>[:ID] <S> Send signal S to service by name, with optional ID
kill <NAME>[:ID] <S> Send signal S to service by name, with optional ID
ident [NAME] Show matching identities for NAME, or all
status <NAME>[:ID] Show service status, by name
status Show status of services, default command
@@ -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
+149
View File
@@ -0,0 +1,149 @@
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
# The tty:@console stanza is required so cryptsetup can prompt for a passphrase
run [S] name:cryptsetup tty:@console /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 -7
View File
@@ -10,14 +10,21 @@ many embedded systems this is crucial to ensure all circuits on the
board are properly reset for the next boot, in effect ensuring the
system works the same after both a power-on and reboot event.
> [!NOTE]
> The watchdog reboot delegation can be enabled with the `reboot-watchdog`
> configuration option in `/etc/finit.conf`. By default this is disabled
> and the system reboots directly via the SoC using the kernel's `reboot(2)`
> syscall. See [Runlevels](config/runlevels.md) for details.
The delegation is performed at the very last steps of system shutdown,
if reboot has been selected and an elected watchdog is known, first a
`SIGPWR` is sent to advise watchdogd of the pending reboot. Then, when
the necessary steps of preparing the system for shutdown (umount etc.)
are completed, Finit sends `SIGTERM` to watchdogd and puts itself in a
10 sec timeout loop waiting for the WDT to reset the board. If a reset
is not done before the timeout, Finit falls back to`reboot(RB_AUTOBOOT)`
which tells the kernel to do the reboot.
if reboot has been selected, `reboot-watchdog` is enabled, and an elected
watchdog is known. First a `SIGPWR` is sent to advise watchdogd of the
pending reboot. Then, when the necessary steps of preparing the system
for shutdown (umount etc.) are completed, Finit sends `SIGTERM` to
watchdogd and puts itself in a 10 sec timeout loop waiting for the WDT
to reset the board. If a reset is not done before the timeout, Finit
falls back to `reboot(RB_AUTOBOOT)` which tells the kernel to do the
reboot.
An external watchdog service can also be used. The more advanced cousin
[watchdogd](https://github.com/troglobit/watchdogd/) is the recommended
+45
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
@@ -730,6 +744,37 @@ is
and the default
.Cm tag
identity is the basename of the service or run/task command.
.It Cm tty:\&<dev>
Give the
.Cm run , task ,
or
.Cm service
a controlling terminal on
.Cm <dev> .
The device is opened, set as the controlling terminal for the session
(after
.Fn setsid ) ,
and connected to stdin, stdout, and stderr.
A default
.Ev TERM
variable is set based on the device type:
.Cm vt102
for serial lines and
.Cm linux
for virtual terminals.
.Cm <dev>
may be a device node like
.Pa /dev/ttyS0 ,
or the special keyword
.Cm @console
(see TTYs and Consoles above).
When combined with
.Cm log: ,
stdout and stderr are redirected to the log sink instead, but stdin
remains connected to the TTY device.
.Bd -unfilled -offset indent
service [2345] tty:/dev/ttyS0 /usr/sbin/foo -- Foo on serial console
.Ed
.El
.Sh RESCUE MODE
Finit supports a rescue mode which is activated by the
+34 -24
View File
@@ -1,14 +1,17 @@
site_name: doc/
site_name: ""
site_description: Fast Init for Linux Systems
site_url: https://troglobit.github.io/finit/
repo_url: https://github.com/troglobit/finit
repo_name: troglobit/finit
site_url: http://finit-project.github.io
repo_url: https://github.com/finit-project/finit
repo_name: Finit Project
copyright: Copyright &copy; 2008-2025 Joachim Wiberg
docs_dir: doc/
edit_uri: edit/master/doc/
extra_css:
- extra.css
exclude_docs: |
TODO.md
ChangeLog.md
theme:
logo: img/logo.png
@@ -20,7 +23,7 @@ theme:
- navigation.instant
- navigation.instant.progress
- navigation.tracking
# - navigation.tabs
- navigation.tabs
# - navigation.sections
# - navigation.expand
# - navigation.top
@@ -47,19 +50,16 @@ theme:
name: Switch to light mode
nav:
- Introduction: index.md
- Example: example.md
- Features: features.md
- Commands & Status: initctl.md
- Rebooting & Halting: commands.md
- Runlevels: runlevels.md
- Runparts & /etc/rc.local: runparts.md
- Getting Started:
- Introduction: index.md
- Features: features.md
- Quick Example: example.md
- Building: build.md
- Configuration:
- Overview: config/index.md
- Files & Layout: config/files.md
- Conditions: conditions.md
- Cgroups: config/cgroups.md
- Environment Variables: config/env.md
- Runlevels: config/runlevels.md
- One-shot Tasks: config/task-and-run.md
- Services:
- Syntax: config/services.md
@@ -68,24 +68,30 @@ nav:
- Synchronization: config/service-sync.md
- Wrapper Scripts: config/service-wrappers.md
- Run-parts Scripts: config/runparts.md
- Runlevels: config/runlevels.md
- TTY & Console: config/tty.md
- Templating: config/templating.md
- Environment Variables: config/env.md
- Logging: config/logging.md
- Cgroups: config/cgroups.md
- Capabilities: config/capabilities.md
- Templating: config/templating.md
- SysV Compatibility: config/sysv.md
- Rescue Mode: config/rescue.md
- Limitations: config/limitations.md
- Usage:
- Building: build.md
- Command Line: cmdline.md
- Plugins: plugins.md
- Services: service.md
- Commands & Status: initctl.md
- Switch Root: switchroot.md
- Rebooting & Halting: commands.md
- Command Line Options: cmdline.md
- Managing Services: service.md
- Signals: signals.md
- Reference:
- Distributions: distro.md
- keventd: keventd.md
- Runlevels Overview: runlevels.md
- Runparts & rc.local: runparts.md
- Plugins: plugins.md
- Watchdog: watchdog.md
- keventd: keventd.md
- Service State Machine: state-machine.md
- Distributions: distro.md
- Requirements: requirements.md
markdown_extensions:
@@ -106,7 +112,11 @@ markdown_extensions:
plugins:
- search
- callouts
- glightbox
extra:
homepage: https://troglobit.com/projects/finit/
homepage: https://finit-project.github.io/
social:
- icon: fontawesome/brands/github
link: https://github.com/finit-project/finit
name: Finit on GitHub
+8
View File
@@ -29,6 +29,10 @@ if BUILD_NETLINK_PLUGIN
libplug_la_SOURCES += netlink.c
endif
if BUILD_PLYMOUTH_PLUGIN
libplug_la_SOURCES += plymouth.c
endif
if BUILD_RESOLVCONF_PLUGIN
libplug_la_SOURCES += resolvconf.c
endif
@@ -76,6 +80,10 @@ if BUILD_NETLINK_PLUGIN
pkglib_LTLIBRARIES += netlink.la
endif
if BUILD_PLYMOUTH_PLUGIN
pkglib_LTLIBRARIES += plymouth.la
endif
if BUILD_RESOLVCONF_PLUGIN
pkglib_LTLIBRARIES += resolvconf.la
endif
+4 -3
View File
@@ -27,6 +27,7 @@
# include <lite/lite.h>
#endif
#include "conf.h"
#include "finit.h"
#include "helpers.h"
#include "plugin.h"
@@ -37,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;
}
@@ -50,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;
}
@@ -61,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,
+4 -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)
@@ -143,6 +143,7 @@ static void pidfile_update_conds(char *dir, char *name, uint32_t mask)
/* Complement log in service.c for non-forking services */
logit(LOG_CONSOLE | LOG_NOTICE, "Started %s[%d]", svc_ident(svc, NULL, 0), pid);
cgroup_move_svc(svc);
}
}
@@ -275,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);
}
@@ -333,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 */
+293
View File
@@ -0,0 +1,293 @@
/* Plymouth boot splash plugin for Finit
*
* Copyright (c) 2012-2026 Aaron Andersen <aaron@fosslib.net>
*
* 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.
*/
/*
* This plugin integrates the Plymouth boot splash screen with Finit.
* It manages the plymouthd lifecycle across the full boot process:
*
* - HOOK_BANNER: Start plymouthd early, before console output.
* In initramfs, starts fresh. In stage 2, reuses
* the daemon carried over from initramfs if alive.
* - HOOK_ROOTFS_UP .. HOOK_SYSTEM_UP: Display status messages.
* - HOOK_SVC_UP: Tear down plymouth once boot is complete.
* - HOOK_SWITCH_ROOT: Notify plymouth of root filesystem change so
* the daemon survives the initramfs -> rootfs
* transition.
* - HOOK_SHUTDOWN: Restart plymouthd in shutdown mode for a
* splash during poweroff/reboot.
*
* The plugin is only activated when "splash" is present on the kernel
* command line. Plymouth requires devpts for VT takeover; fs_init()
* mounts it early enough for HOOK_BANNER.
*
* NOTE: The initramfs must include /etc/initrd-release. Plymouth
* checks for this file and, when present, prefixes its argv[0] with
* '@' so that the process is not killed during switch_root. Without
* it, plymouthd will not survive the initramfs-to-rootfs transition.
*/
#include "config.h"
#include "finit.h"
#include "helpers.h"
#include "conf.h"
#include "pid.h"
#include "plugin.h"
#include "sig.h"
#include "util.h"
#include "log.h"
#ifndef PLYMOUTH_PATH
#define PLYMOUTH_PATH "/sbin/plymouth"
#endif
#ifndef PLYMOUTHD_PATH
#define PLYMOUTHD_PATH "/sbin/plymouthd"
#endif
#define PLYMOUTH_PIDFILE "/run/plymouthd.pid"
static pid_t daemon_pid;
static int switching_root;
static int in_initramfs;
static int plymouth_cmd(const char *action)
{
char cmd[256];
snprintf(cmd, sizeof(cmd), PLYMOUTH_PATH " %s", action);
return run(cmd, NULL);
}
static void plymouth_message(const char *msg)
{
pid_t pid;
pid = fork();
if (pid == 0) {
sig_unblock();
execl(PLYMOUTH_PATH, PLYMOUTH_PATH,
"display-message", "--text", msg, NULL);
_exit(EX_OSERR);
}
}
static int plymouth_alive(void)
{
return daemon_pid > 0 && pid_alive(daemon_pid);
}
/* Start plymouthd in the given mode ("boot" or "shutdown"). */
static void plymouth_start(const char *mode)
{
char cmd[256];
int rc;
if (plymouth_alive())
return;
snprintf(cmd, sizeof(cmd),
PLYMOUTHD_PATH " --attach-to-session --mode %s --pid-file %s",
mode, PLYMOUTH_PIDFILE);
rc = run(cmd, NULL);
if (rc) {
warnx("plymouthd failed to start (exit %d)", rc);
return;
}
daemon_pid = pid_file_read(PLYMOUTH_PIDFILE);
if (daemon_pid <= 0) {
warnx("plymouthd started but no PID in %s", PLYMOUTH_PIDFILE);
return;
}
rc = plymouth_cmd("show-splash");
if (rc)
warnx("plymouth show-splash failed (exit %d)", rc);
}
static void plymouth_stop(void)
{
if (!plymouth_alive())
return;
plymouth_cmd("quit");
/*
* Don't poll -- we're in a finit hook, so finit's event loop
* is blocked and can't reap children. Trust that plymouthd
* exits after receiving the quit command.
*/
daemon_pid = 0;
unlink(PLYMOUTH_PIDFILE);
}
/*
* HOOK_BANNER - earliest possible hook, before any console output.
*
* In initramfs: start plymouthd fresh.
* In stage 2: reuse plymouthd from initramfs if still alive,
* otherwise start a new instance.
*/
static void plymouth_boot(void *arg)
{
in_initramfs = fexist("/etc/initrd-release");
if (rescue)
return;
enable_progress(0);
if (!in_initramfs) {
if (plymouth_cmd("--ping") == 0) {
daemon_pid = pid_file_read(PLYMOUTH_PIDFILE);
if (daemon_pid <= 0)
daemon_pid = 1; /* alive but unknown pid */
return;
}
}
plymouth_start("boot");
}
/*
* HOOK_SVC_UP - all services launched.
*
* In initramfs: keep splash alive for switch_root.
* In stage 2: boot is done, tear down plymouth.
*/
static void plymouth_boot_done(void *arg)
{
if (in_initramfs)
return;
plymouth_stop();
enable_progress(1);
}
/* HOOK_SWITCH_ROOT - initramfs transitioning to real root. */
static void plymouth_switchroot(void *arg)
{
switching_root = 1;
plymouth_message("Switching to root filesystem...");
if (plymouth_alive())
run(PLYMOUTH_PATH " update-root-fs --new-root-dir=/sysroot", NULL);
enable_progress(1);
}
/* HOOK_SHUTDOWN - entering runlevel 0 or 6. */
static void plymouth_shutdown(void *arg)
{
if (rescue || switching_root)
return;
enable_progress(0);
plymouth_start("shutdown");
}
static void on_rootfs_up(void *arg)
{
plymouth_message("Root filesystem mounted");
}
static void on_mount_post(void *arg)
{
plymouth_message("Mounting filesystems...");
}
static void on_basefs_up(void *arg)
{
plymouth_message("All filesystems mounted");
}
static void on_network_up(void *arg)
{
plymouth_message("Network is up");
}
static void on_system_up(void *arg)
{
plymouth_message("System ready");
}
static plugin_t plugin = {
.name = "plymouth",
.hook[HOOK_BANNER] = { .cb = plymouth_boot },
.hook[HOOK_ROOTFS_UP] = { .cb = on_rootfs_up },
.hook[HOOK_MOUNT_POST] = { .cb = on_mount_post },
.hook[HOOK_BASEFS_UP] = { .cb = on_basefs_up },
.hook[HOOK_NETWORK_UP] = { .cb = on_network_up },
.hook[HOOK_SYSTEM_UP] = { .cb = on_system_up },
.hook[HOOK_SVC_UP] = { .cb = plymouth_boot_done },
.hook[HOOK_SWITCH_ROOT] = { .cb = plymouth_switchroot },
.hook[HOOK_SHUTDOWN] = { .cb = plymouth_shutdown },
};
/*
* Check kernel command line for "splash" argument. Plymouth should
* only be activated when the user explicitly requests it.
*/
static int has_splash_arg(void)
{
char line[LINE_SIZE], *tok, *saveptr;
FILE *fp;
fp = fopen("/proc/cmdline", "r");
if (!fp)
return 0;
if (!fgets(line, sizeof(line), fp)) {
fclose(fp);
return 0;
}
fclose(fp);
for (tok = strtok_r(line, " \t\n", &saveptr); tok;
tok = strtok_r(NULL, " \t\n", &saveptr)) {
if (!strcmp(tok, "splash"))
return 1;
}
return 0;
}
PLUGIN_INIT(__init)
{
if (!has_splash_arg())
return;
plugin_register(&plugin);
}
PLUGIN_EXIT(__exit)
{
plugin_unregister(&plugin);
}
/**
* Local Variables:
* indent-tabs-mode: t
* c-file-style: "linux"
* End:
*/
+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
},
+23 -16
View File
@@ -170,35 +170,43 @@ static void file_save(void *arg)
static void file_restore(void *arg)
{
struct tm tm = { 0 };
int rc = 1;
char msg[120];
int rc = 0;
FILE *fp;
if (!rtc_file) {
logit(LOG_NOTICE, "System has no RTC (missing driver?), skipping restore.");
snprintf(msg, sizeof(msg), "Resetting system clock to kernel default, %s.", rtc_timestamp);
print_desc(NULL, msg);
rc = time_set(NULL);
print(rc, NULL);
return;
}
print_desc(NULL, "Restoring system clock from backup");
fp = fopen(rtc_file, "r");
if (fp) {
char buf[32];
if (fgets(buf, sizeof(buf), fp)) {
chomp(buf);
strptime(buf, RTC_FMT, &tm);
rc = time_set(&tm);
if (!strptime(buf, RTC_FMT, &tm))
rc = 1;
}
fclose(fp);
} else
logit(LOG_WARNING, "Missing %s", rtc_file);
rc = 1;
if (rc) {
time_set(NULL);
rc = 2;
print_desc(NULL, "Failed to restore system clock from restore file");
print(2, NULL);
snprintf(msg, sizeof(msg), "Resetting system clock to kernel default, %s.", rtc_timestamp);
print_desc(NULL, msg);
rc = time_set(NULL);
print(rc, NULL);
} else {
print_desc(NULL, "Restoring system clock from restore file");
rc = time_set(&tm);
print(rc, NULL);
}
print(rc, NULL);
}
static int rtc_open(void)
@@ -228,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;
}
@@ -258,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;
}
@@ -292,8 +300,7 @@ static void rtc_restore(void *arg)
print(2, NULL);
/* Try restoring from last save game */
if (rtc_file)
file_restore(arg);
file_restore(arg);
} else
print(0, NULL);
@@ -314,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 },
};
+2
View File
@@ -56,12 +56,14 @@ logit_LDADD = $(lite_LIBS)
finit_SOURCES = api.c cgroup.c cgroup.h \
client.c client.h \
clone3.c clone3.h \
cond.c cond-w.c cond.h \
conf.c conf.h \
devmon.c devmon.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 \
+83 -8
View File
@@ -68,7 +68,8 @@ static int stop(svc_t *svc, void *user_data)
service_timeout_cancel(svc);
svc_stop(svc);
service_step(svc);
service_step_all(SVC_TYPE_ANY);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
}
@@ -83,7 +84,8 @@ static int start(svc_t *svc, void *user_data)
service_timeout_cancel(svc);
svc_start(svc);
service_step(svc);
service_step_all(SVC_TYPE_ANY);
if (!IS_RESERVED_RUNLEVEL(runlevel))
service_step_all(SVC_TYPE_ANY);
return 0;
}
@@ -110,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)
@@ -120,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);
@@ -243,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;
@@ -391,11 +454,6 @@ static void api_cb(uev_t *w, void *arg, int events)
}
switch (rq.cmd) {
case INIT_CMD_RELOAD:
case INIT_CMD_START_SVC:
case INIT_CMD_RESTART_SVC:
case INIT_CMD_STOP_SVC:
case INIT_CMD_RELOAD_SVC:
case INIT_CMD_REBOOT:
case INIT_CMD_HALT:
case INIT_CMD_POWEROFF:
@@ -406,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;
}
@@ -453,6 +520,10 @@ static void api_cb(uev_t *w, void *arg, int events)
break;
case INIT_CMD_RELOAD: /* 'init q' and 'initctl reload' */
if (IS_RESERVED_RUNLEVEL(runlevel)) {
warnx("Ignoring reload in runlevel S and 6/0.");
goto done;
}
dbg("reload");
sm_reload();
break;
@@ -502,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;
@@ -576,7 +651,7 @@ static void api_cb(uev_t *w, void *arg, int events)
dbg("Unsupported cmd: %d", rq.cmd);
break;
}
done:
if (result)
rq.cmd = INIT_CMD_NACK;
else
+421 -25
View File
@@ -21,6 +21,7 @@
* THE SOFTWARE.
*/
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
@@ -33,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"
@@ -61,6 +64,161 @@ static uev_t cgw;
static int avail;
/*
* Set up inotify watch on cgroup for automatic cleanup when empty.
* Should be called after cgroup_prepare() but before process starts.
*/
int cgroup_watch(const char *group, const char *name)
{
char path[256];
int rc;
if (!avail)
return 0;
/* Special case: root cgroup has no separate events file to watch */
if (!strcmp(group, "root"))
return 0;
snprintf(path, sizeof(path), "/sys/fs/cgroup/%s/%s/cgroup.events", group, name);
rc = iwatch_add(&iw_cgroup, path, 0);
if (rc < 0) {
warn("Failed setting up inotify watch on %s", path);
return -1;
}
dbg("Watching %s for automatic cleanup", path);
return 0;
}
/*
* Derive cgroup name from service (mirrors group_name() in service.c)
* For use by plugins that need to determine the cgroup path.
*
* MUST return the same value that group_name() in service.c returns!
*/
char *cgroup_svc_name(svc_t *svc, char *buf, size_t len)
{
char *ptr;
/* Use explicit cgroup leaf name if specified */
if (svc->cgroup.leafname[0]) {
strlcpy(buf, svc->cgroup.leafname, len);
return buf;
}
if (!svc->file[0])
return svc_ident(svc, buf, len);
ptr = strrchr(svc->file, '/');
if (ptr)
ptr++;
else
ptr = svc->file;
strlcpy(buf, ptr, len);
/* Strip .conf extension - this gives us the cgroup name */
ptr = strstr(buf, ".conf");
if (ptr)
*ptr = 0;
return buf;
}
/*
* Helper function to move a PID to a service's cgroup.
* @group: Top-level cgroup (e.g., "system", "user", "maint")
* @name: Service-specific cgroup name (e.g., "dockerd", "nginx")
* @pid: Process ID to move
* @delegate: If 1, handle potential EBUSY by using supervisor/ subdirectory
*
* Useful for forking services where child PIDs need to be moved after they appear.
*
* For delegated cgroups, handles the cgroups v2 "no internal processes" rule:
* if the parent has child cgroups, tries to reuse process-named subdirectory or
* falls back to supervisor/ subdirectory.
*
* Returns: 0 on success, -1 on failure
*/
int cgroup_move_pid(const char *group, const char *name, int pid, int delegate)
{
int rc = 0, reuse = 0;
char path[512];
if (!avail)
return 0;
/* Sanity checks */
if (!group || !name || pid <= 0) {
errno = EINVAL;
return -1;
}
/* Disallow path traversal */
if (strstr(group, "..") || strstr(name, "..") ||
strchr(group, '/') || strchr(name, '/')) {
errno = EINVAL;
warn("Invalid cgroup path components: group=%s name=%s", group, name);
return -1;
}
snprintf(path, sizeof(path), FINIT_CGPATH "/%s/%s", group, name);
if (!fisdir(path)) {
warn("Cgroup %s does not exist", path);
return -1;
}
if ((rc = fnwrite(str("%d", pid), "%s/cgroup.procs", path))) {
/*
* EBUSY means the cgroup has children (cgroups v2 "no internal processes" rule).
* For delegated cgroups, fall back to subdirectory.
*/
if (errno == EBUSY && delegate) {
char comm[64];
/* Check if subdirectory matching process name, e.g., conmon, exists */
readsnf(comm, sizeof(comm), "/proc/%d/comm", pid);
strlcat(path, "/", sizeof(path));
strlcat(path, comm, sizeof(path));
if (fisdir(path))
reuse = 1;
else
snprintf(path, sizeof(path), FINIT_CGPATH "/%s/%s/supervisor", group, name);
rc = fnwrite(str("%d", pid), "%s/cgroup.procs", path);
}
if (rc) {
if (errno != ESRCH)
warn("Failed moving pid %d to cgroup %s", pid, path);
return -1;
}
}
if (reuse) {
snprintf(path, sizeof(path), FINIT_CGPATH "/%s/%s/supervisor", group, name);
rmdir(path);
}
return 0;
}
int cgroup_move_svc(svc_t *svc)
{
const char *group = "system";
char name[MAX_ARG_LEN];
if (!avail || !svc)
return 0;
if (svc->cgroup.name[0])
group = svc->cgroup.name;
return cgroup_move_pid(group, cgroup_svc_name(svc, name, sizeof(name)),
svc->pid, svc->cgroup.delegate);
}
static void cgset(const char *path, char *ctrl, char *prop)
{
char *val;
@@ -148,40 +306,100 @@ static void group_init(char *path, int leaf, const char *cfg)
}
}
static int cgroup_leaf_init(char *group, char *name, int pid, const char *cfg)
static int cgroup_create(const char *group, const char *name, const char *cfg,
int delegate, const char *username, const char *grpname,
char *pathbuf, size_t pathlen)
{
char path[256];
dbg("group %s, name %s, pid %d, cfg %s", group, name, pid, cfg ?: "NIL");
if (pid < 0 || pid == 1) {
errno = EINVAL;
return 1;
snprintf(path, sizeof(path), "/sys/fs/cgroup/%s/%s", group, name);
if (delegate) {
char initpath[PATH_MAX];
/* For delegation, create as domain group (not leaf) */
group_init(path, 0, cfg);
/* Enable controllers for delegation */
if (fnwrite(controllers, "%s/cgroup.subtree_control", path))
warn("Failed enabling controllers in %s for delegation", path);
/* Change ownership of delegation files */
if (username && username[0] && grpname && grpname[0]) {
uid_t uid = getuser(username, NULL);
gid_t gid = getgroup(grpname);
if (uid != (uid_t)-1 && gid != (gid_t)-1) {
char filepath[PATH_MAX];
char *files[] = {
"cgroup.procs",
"cgroup.subtree_control",
"cgroup.threads",
"cgroup.type",
NULL
};
for (int i = 0; files[i]; i++) {
snprintf(filepath, sizeof(filepath), "%s/%s", path, files[i]);
if (chown(filepath, uid, gid))
warn("Failed chown %s to %d:%d", filepath, uid, gid);
}
}
}
snprintf(initpath, sizeof(initpath), "%s/supervisor", path);
group_init(initpath, 1, NULL);
strlcpy(path, initpath, sizeof(path));
} else {
/* Normal leaf cgroup */
group_init(path, 1, cfg);
}
/* create and initialize new group */
snprintf(path, sizeof(path), "/sys/fs/cgroup/%s/%s", group, name);
group_init(path, 1, cfg);
if (!fisdir(path)) {
warn("Cgroup directory %s doesn't exist after creation", path);
return -1;
}
/* move process to new group */
if (fnwrite(str("%d", pid), "%s/cgroup.procs", path))
err(1, "Failed moving pid %d to group %s", pid, path);
/* Return the path to caller if they provided a buffer */
if (pathbuf && pathlen > 0)
strlcpy(pathbuf, path, pathlen);
strlcat(path, "/cgroup.events", sizeof(path));
return iwatch_add(&iw_cgroup, path, 0);
return cgroup_watch(group, name);
}
int cgroup_user(char *name, int pid)
static int cgroup_leaf_init(const char *group, const char *name, int pid, const char *cfg,
int delegate, const char *username, const char *grpname)
{
dbg("group %s, name %s, pid %d, cfg %s, delegate %d", group, name, pid, cfg ?: "NIL", delegate);
if (cgroup_create(group, name, cfg, delegate, username, grpname, NULL, 0))
return -1;
dbg("Assigning PID %d to cgroup %s/%s", pid, group, name);
/* Special case: "root" cgroup means the actual cgroup root */
if (!strcmp(group, "root"))
return fnwrite(str("%d", pid), FINIT_CGPATH "/cgroup.procs");
/* 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 */
return cgroup_watch(group, name);
}
int cgroup_user(const char *name, int pid)
{
if (!avail)
return 0;
return cgroup_leaf_init("user", name, pid, NULL);
return cgroup_leaf_init("user", name, pid, NULL, 0, NULL, NULL);
}
int cgroup_service(char *name, int pid, struct cgroup *cg)
int cgroup_service(const char *name, int pid, struct cgroup *cg, char *username, char *grpname)
{
char *group = "system";
int delegate = 0;
if (!avail)
return 0;
@@ -198,9 +416,64 @@ int cgroup_service(char *name, int pid, struct cgroup *cg)
snprintf(path, sizeof(path), "/sys/fs/cgroup/%s", cg->name);
if (fisdir(path))
group = cg->name;
delegate = cg->delegate;
}
return cgroup_leaf_init(group, name, pid, cg ? cg->cfg : NULL);
return cgroup_leaf_init(group, name, pid, cg ? cg->cfg : NULL, delegate, username, grpname);
}
/* Create cgroup for the requested type of service, return fd to cgroup for clone3() */
int cgroup_prepare(svc_t *svc, const char *name)
{
const char *group;
const char *cfg = NULL;
const char *username = NULL;
const char *grpname = NULL;
int delegate = 0;
char path[256];
int fd = -1;
if (!avail)
return -1;
if (!name) {
errno = EINVAL;
return -1;
}
if (!svc) {
/* Helper process (e.g., networking) */
group = "system";
} else if (svc_is_tty(svc)) {
/* TTY/getty services go in user cgroup */
group = "user";
} else if (svc->cgroup.name[0] && !strcmp(svc->cgroup.name, "root")) {
/* Special case: SCHED_RR processes go in root cgroup */
fd = open("/sys/fs/cgroup", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0)
warn("Failed opening root cgroup");
return fd;
} else {
/* Regular service */
group = svc->cgroup.name[0] ? svc->cgroup.name : "system";
cfg = svc->cgroup.cfg;
delegate = svc->cgroup.delegate;
username = svc->username;
grpname = svc->group;
}
/* Create the cgroup and get the path back */
if (cgroup_create(group, name, cfg, delegate, username, grpname, path, sizeof(path)))
return -1;
/* Open and return fd for clone3() */
fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd < 0)
warn("Failed opening cgroup %s", path);
return fd;
}
static void append_ctrl(char *ctrl)
@@ -405,6 +678,79 @@ int cgroup_add(char *name, char *cfg, int is_protected)
return 0;
}
/*
* Walk a cgroup directory and remove all empty subdirectories recursively.
*/
static void cgroup_prune_recursive(const char *path)
{
struct dirent *d;
DIR *dir;
dir = opendir(path);
if (!dir)
return;
while ((d = readdir(dir)) != NULL) {
char subpath[PATH_MAX];
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
continue;
if (d->d_type != DT_DIR)
continue;
snprintf(subpath, sizeof(subpath), "%s/%s", path, d->d_name);
cgroup_prune_recursive(subpath);
}
closedir(dir);
/*
* Try to remove this directory. If it still has children or processes,
* rmdir() will fail with ENOTEMPTY/EBUSY - that's fine.
*/
if (rmdir(path) && errno != ENOTEMPTY && errno != EBUSY)
warn("Failed to prune %s", path);
}
/*
* Housekeeping: prune all empty cgroup subdirectories.
* Called after runlevel transitions to clean up finished run/task cgroups.
*/
void cgroup_prune(void)
{
struct dirent *entry;
struct cg *cg;
char path[256];
DIR *dir;
if (!avail)
return;
dbg("Pruning empty cgroup directories...");
/* Walk through each top-level cgroup (init, system, user) */
TAILQ_FOREACH(cg, &cgroups, link) {
snprintf(path, sizeof(path), FINIT_CGPATH "/%s", cg->name);
dir = opendir(path);
if (!dir)
continue;
/* Check each subdirectory */
while ((entry = readdir(dir)) != NULL) {
char subpath[512];
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
if (entry->d_type != DT_DIR)
continue;
snprintf(subpath, sizeof(subpath), "%s/%s", path, entry->d_name);
cgroup_prune_recursive(subpath);
}
closedir(dir);
}
}
/*
* Remove inactive top-level cgroup
*/
@@ -428,7 +774,8 @@ int cgroup_del(char *dir)
}
if (rmdir(dir) && errno != ENOENT) {
dbg("Failed removing %s: %s", dir, strerror(errno));
if (errno != EBUSY)
warn("Failed rmdir(%s): %s", dir, strerror(errno));
return -1;
}
@@ -442,6 +789,31 @@ int cgroup_del(char *dir)
return 0;
}
/*
* Delete cgroup for a service (convenience wrapper)
*/
int cgroup_del_svc(svc_t *svc, const char *name)
{
const char *group;
char path[256];
if (!avail)
return 0;
/* Determine group from service */
if (svc_is_tty(svc)) {
group = "user";
} else if (svc->cgroup.name[0] && !strcmp(svc->cgroup.name, "root")) {
/* Root cgroup - nothing to delete */
return 0;
} else {
group = svc->cgroup.name[0] ? svc->cgroup.name : "system";
}
snprintf(path, sizeof(path), "/sys/fs/cgroup/%s/%s", group, name);
return cgroup_del(path);
}
/* the top-level init cgroup is a leaf, that's ensured in cgroup_init() */
void cgroup_config(void)
{
@@ -473,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;
@@ -483,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;
@@ -499,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;
}
+23 -8
View File
@@ -26,21 +26,36 @@
#include <uev/uev.h>
/* Forward declaration */
typedef struct svc svc_t;
struct cgroup {
char name[16];
char cfg[128];
char leafname[128];
char delegate;
};
void cgroup_mark_all(void);
void cgroup_cleanup (void);
void cgroup_mark_all(void);
void cgroup_cleanup (void);
int cgroup_add (char *name, char *cfg, int is_protected);
int cgroup_del (char *dir);
void cgroup_config (void);
int cgroup_add (char *name, char *cfg, int is_protected);
int cgroup_del (char *dir);
int cgroup_del_svc (svc_t *svc, const char *name);
void cgroup_config (void);
void cgroup_init (uev_ctx_t *ctx);
void cgroup_init (uev_ctx_t *ctx);
int cgroup_user (char *name, int pid);
int cgroup_service (char *name, int pid, struct cgroup *cg);
int cgroup_user (const char *name, int pid);
int cgroup_service (const char *name, int pid, struct cgroup *cg, char *username, char *group);
char *cgroup_svc_name(svc_t *svc, char *buf, size_t len);
int cgroup_prepare (svc_t *svc, const char *name);
int cgroup_watch (const char *group, const char *name);
int cgroup_move_pid(const char *group, const char *name, int pid, int delegate);
int cgroup_move_svc(svc_t *svc);
void cgroup_prune (void);
#endif /* FINIT_CGROUP_H_ */
+243 -65
View File
@@ -29,19 +29,15 @@
#include <stdlib.h>
#include <search.h>
#include <inttypes.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include <sys/sysinfo.h> /* sysinfo() */
#include "cgutil.h"
#include "initctl.h"
#include "log.h"
#define CDIM plain ? "" : "\e[2m"
#define CRST plain ? "" : "\e[0m"
#define CDIM plain ? "" : "\e[2m"
#define CRST plain ? "" : "\e[0m"
#define CLREOL plain ? "" : "\e[K" /* Clear to end of line */
#define NONE " "
#define PIPE plain ? "| " : "│ "
@@ -102,6 +98,7 @@ char *pid_cmdline(int pid)
buf[i] = ' ';
}
fclose(fp);
return buf;
}
@@ -144,7 +141,7 @@ char *pid_cgroup(int pid)
return NULL;
}
static char *cgroup_val(char *path, char *file, char *buf, size_t len)
char *cgroup_val(char *path, char *file, char *buf, size_t len)
{
char *val = NULL;
FILE *fp;
@@ -252,6 +249,42 @@ uint64_t cgroup_memory(char *group)
return cgroup_uint64(path, "memory.current");
}
int cgroup_throttle(char *group, uint64_t *throttled_usec, uint64_t *nr_throttled)
{
char path[256];
char buf[256];
FILE *fp;
int found = 0;
paste(path, sizeof(path), FINIT_CGPATH, group);
fp = fopenf("r", "%s/cpu.stat", path);
if (!fp)
return -1;
*throttled_usec = 0;
*nr_throttled = 0;
while (fgets(buf, sizeof(buf), fp)) {
chomp(buf);
if (!strncmp(buf, "throttled_usec", 14)) {
*throttled_usec = strtoull(&buf[15], NULL, 10);
found++;
} else if (!strncmp(buf, "nr_throttled", 12)) {
*nr_throttled = strtoull(&buf[13], NULL, 10);
found++;
}
if (found == 2)
break;
}
fclose(fp);
return 0;
}
static float cgroup_cpuload(struct cg *cg)
{
char fn[256];
@@ -344,18 +377,148 @@ struct cg *cg_stats(char *path)
return cg;
}
/* query config */
/*
* Compare two memory limit values, returning the more restrictive one.
* For memory.max: smaller value wins (except "max" means unlimited)
* For memory.min: larger value wins (more protection)
*/
static uint64_t cmp_mem_limit(const char *a, const char *b, int is_max)
{
uint64_t val_a, val_b;
if (!a || !a[0])
return b && b[0] ? strtoull(b, NULL, 10) : 0;
if (!b || !b[0])
return strtoull(a, NULL, 10);
/* Handle "max" (unlimited) */
if (!strcmp(a, "max"))
return !strcmp(b, "max") ? UINT64_MAX : strtoull(b, NULL, 10);
if (!strcmp(b, "max"))
return strtoull(a, NULL, 10);
val_a = strtoull(a, NULL, 10);
val_b = strtoull(b, NULL, 10);
if (is_max)
return val_a < val_b ? val_a : val_b; /* min for max limit */
else
return val_a > val_b ? val_a : val_b; /* max for min limit */
}
/*
* Compare two cpu.max values (format: "quota period")
* Returns the more restrictive quota (smallest quota/period ratio)
* Stores result in dst, up to len bytes
*/
static void cmp_cpu_max(const char *a, const char *b, char *dst, size_t len)
{
char a_copy[32], b_copy[32];
char *a_quota, *a_period, *b_quota, *b_period;
uint64_t qa, pa, qb, pb;
double ratio_a, ratio_b;
if (!a || !a[0]) {
if (b && b[0])
strlcpy(dst, b, len);
else
dst[0] = 0;
return;
}
if (!b || !b[0]) {
strlcpy(dst, a, len);
return;
}
/* Parse a */
strlcpy(a_copy, a, sizeof(a_copy));
a_quota = strtok(a_copy, " ");
a_period = strtok(NULL, " ");
/* Parse b */
strlcpy(b_copy, b, sizeof(b_copy));
b_quota = strtok(b_copy, " ");
b_period = strtok(NULL, " ");
/* Handle "max" quota (unlimited) */
if (a_quota && !strcmp(a_quota, "max")) {
strlcpy(dst, b, len);
return;
}
if (b_quota && !strcmp(b_quota, "max")) {
strlcpy(dst, a, len);
return;
}
/* Compare ratios (quota/period) - smaller ratio is more restrictive */
if (!a_quota || !a_period || !b_quota || !b_period) {
strlcpy(dst, a, len); /* fallback */
return;
}
qa = strtoull(a_quota, NULL, 10);
pa = strtoull(a_period, NULL, 10);
qb = strtoull(b_quota, NULL, 10);
pb = strtoull(b_period, NULL, 10);
if (pa == 0 || pb == 0) {
strlcpy(dst, a, len); /* fallback */
return;
}
ratio_a = (double)qa / (double)pa;
ratio_b = (double)qb / (double)pb;
strlcpy(dst, ratio_a <= ratio_b ? a : b, len);
}
/* query config with hierarchical limit resolution */
struct cg *cg_conf(char *path)
{
static struct cg cg;
char parent[512];
char tmp[32];
uint64_t mem_min_val, mem_max_val;
/* Read initial values from the leaf cgroup */
cgroup_val(path, "memory.min", cg.cg_mem.min, sizeof(cg.cg_mem.min));
cgroup_memval(path, "memory.max", cg.cg_mem.max, sizeof(cg.cg_mem.max));
cgroup_val(path, "memory.max", cg.cg_mem.max, sizeof(cg.cg_mem.max));
cgroup_val(path, "cpu.weight", cg.cg_cpu.weight, sizeof(cg.cg_cpu.weight));
cgroup_val(path, "cpu.max", cg.cg_cpu.max, sizeof(cg.cg_cpu.max));
cgroup_val(path, "cpuset.cpus.effective", cg.cg_cpu.set, sizeof(cg.cg_cpu.set));
cg.cg_vmsize = cgroup_uint64(path, "memory.current");
/* Walk up the hierarchy to find most restrictive limits */
strlcpy(parent, path, sizeof(parent));
while (strcmp(parent, FINIT_CGPATH) != 0) {
char *slash = strrchr(parent, '/');
if (!slash || slash == parent)
break;
*slash = '\0'; /* Move up one level */
/* Compare and update memory.min (take maximum) */
if (cgroup_val(parent, "memory.min", tmp, sizeof(tmp))) {
mem_min_val = cmp_mem_limit(cg.cg_mem.min, tmp, 0);
if (mem_min_val == UINT64_MAX)
strlcpy(cg.cg_mem.min, "max", sizeof(cg.cg_mem.min));
else
snprintf(cg.cg_mem.min, sizeof(cg.cg_mem.min), "%lu", mem_min_val);
}
/* Compare and update memory.max (take minimum) */
if (cgroup_val(parent, "memory.max", tmp, sizeof(tmp))) {
mem_max_val = cmp_mem_limit(cg.cg_mem.max, tmp, 1);
if (mem_max_val == UINT64_MAX)
strlcpy(cg.cg_mem.max, "max", sizeof(cg.cg_mem.max));
else
snprintf(cg.cg_mem.max, sizeof(cg.cg_mem.max), "%lu", mem_max_val);
}
/* Compare and update cpu.max (take most restrictive) */
if (cgroup_val(parent, "cpu.max", tmp, sizeof(tmp)))
cmp_cpu_max(cg.cg_cpu.max, tmp, cg.cg_cpu.max, sizeof(cg.cg_cpu.max));
}
return &cg;
}
@@ -378,32 +541,29 @@ static int cgroup_filter(const struct dirent *entry)
int cgroup_tree(char *path, char *pfx, int mode, int pos)
{
struct dirent **namelist = NULL;
char s[32], r[32], l[32];
char row[ttcols + 9]; /* + control codes */
char s[32], r[32], l[32], row[1024];
size_t rlen = sizeof(row) - 1;
size_t rplen = rlen - 9;
struct stat st;
struct cg *cg;
char buf[512];
int rc = 0;
FILE *fp;
int i, n;
int num;
if (pos >= ttrows)
return 0;
if (pos >= ttrows - 1)
return pos;
if (-1 == lstat(path, &st))
return 1;
return pos;
if ((st.st_mode & S_IFMT) != S_IFDIR) {
errno = ENOTDIR;
return -1;
return pos;
}
fp = fopenf("r", "%s/cgroup.procs", path);
if (!fp)
return -1;
return pos;
num = 0;
while (fgets(buf, sizeof(buf), fp))
num++;
@@ -413,7 +573,7 @@ int cgroup_tree(char *path, char *pfx, int mode, int pos)
switch (mode) {
case 1:
cg = cg_stats(path);
snprintf(row, rplen, "\r %6.6s %6.6s %6.6s %5.1f %5.1f %s",
snprintf(row, rlen, " %6.6s %6.6s %6.6s %5.1f %5.1f %s",
memsz(cg->cg_vmsize, s, sizeof(s)),
memsz(cg->cg_rss, r, sizeof(r)),
memsz(cg->cg_vmlib, l, sizeof(l)),
@@ -421,18 +581,20 @@ int cgroup_tree(char *path, char *pfx, int mode, int pos)
break;
case 2:
cg = cg_conf(path);
snprintf(row, rplen, "\r%6.6s [%-6.6s%6.6s] %6s [%-6.6s%6.6s] %s",
snprintf(row, rlen, "%6.6s [%-6.6s%6.6s] %6s [%-6.6s%6.6s] %s",
memsz(cg->cg_vmsize, s, sizeof(s)),
cg->cg_mem.min, cg->cg_mem.max, cg->cg_cpu.set,
cg->cg_cpu.weight, cg->cg_cpu.max, path);
break;
default:
strlcpy(row, "\r", rplen);
strlcat(row, path, rplen);
strlcpy(row, path, rlen);
break;
}
puts(row);
printf("\r%s%s\n", row, CLREOL);
pos++;
if (pos >= ttrows - 1)
goto out;
}
if (num > 0) {
@@ -450,53 +612,40 @@ int cgroup_tree(char *path, char *pfx, int mode, int pos)
/* skip kernel threads for now (no cmdline) */
cmdline = pid_cmdline(pid);
if (cmdline) {
char proc[ttcols];
char proc[512]; /* Build full command, truncate row later */
switch (mode) {
case 1:
snprintf(row, rplen, "\r%37s", " ");
snprintf(row, rlen, "%37s", " ");
break;
case 2:
snprintf(row, rplen, "\r --.-- [ ] [ ] ");
snprintf(row, rlen, " --.-- [ ] [ ] ");
break;
default:
strlcpy(row, "\r", rplen);
row[0] = '\0';
break;
}
strlcat(row, pfx, rplen);
strlcat(row, pfx, rlen);
strlcat(row, ++i == num ? END : FORK, rlen);
snprintf(proc, sizeof(proc), " %d %s", pid, cmdline);
strlcat(row, CDIM, rlen);
strlcat(row, proc, rlen);
strlcat(row, CRST, sizeof(row));
if (plain) {
strlcat(row, proc, rplen);
} else {
int len;
strlcat(row, CDIM, rlen);
strlcat(row, proc, rlen);
len = strlen(row) + strlen(CRST);
if (len > (int)rlen)
row[rlen - strlen(CRST)] = 0;
strlcat(row, CRST, sizeof(row));
}
puts(row);
printf("\r%s%s\n", row, CLREOL);
pos++;
free(cmdline);
}
if (mode == 1) {
pos += i;
if (pos >= ttrows)
if (pos >= ttrows - 1)
break;
}
}
}
out:
fclose(fp);
n = scandir(path, &namelist, cgroup_filter, alphasort);
@@ -505,12 +654,15 @@ int cgroup_tree(char *path, char *pfx, int mode, int pos)
char *nm = namelist[i]->d_name;
char prefix[80];
if (pos >= ttrows - 1)
break;
snprintf(buf, sizeof(buf), "%s/%s", path, nm);
switch (mode) {
case 1:
cg = cg_stats(buf);
snprintf(row, rplen,
"\r %6.6s %6.6s %6.6s %5.1f %5.1f ",
snprintf(row, rlen,
" %6.6s %6.6s %6.6s %5.1f %5.1f ",
memsz(cg->cg_vmsize, s, sizeof(s)),
memsz(cg->cg_rss, r, sizeof(r)),
memsz(cg->cg_vmlib, l, sizeof(l)),
@@ -518,32 +670,33 @@ int cgroup_tree(char *path, char *pfx, int mode, int pos)
break;
case 2:
cg = cg_conf(buf);
snprintf(row, rplen, "\r%6.6s [%-6.6s%6.6s] %6.6s [%-6.6s%6.6s] ",
snprintf(row, rlen, "%6.6s [%-6.6s%6.6s] %6.6s [%-6.6s%6.6s] ",
memsz(cg->cg_vmsize, s, sizeof(s)),
cg->cg_mem.min, cg->cg_mem.max,
cg->cg_cpu.set, cg->cg_cpu.weight, cg->cg_cpu.max);
break;
default:
strlcpy(row, "\r", rplen);
row[0] = '\0';
break;
}
strlcat(row, pfx, rplen);
strlcat(row, pfx, rlen);
if (i + 1 == n) {
strlcat(row, END, rplen);
strlcat(row, END, rlen);
snprintf(prefix, sizeof(prefix), "%s ", pfx);
} else {
strlcat(row, FORK, rplen);
strlcat(row, FORK, rlen);
snprintf(prefix, sizeof(prefix), "%s%s ", pfx, PIPE);
}
strlcat(row, " ", rplen);
strlcat(row, " ", rlen);
strlcat(row, nm, rplen);
strlcat(row, "/ ", rplen);
strlcat(row, nm, rlen);
strlcat(row, "/ ", rlen);
puts(row);
printf("\r%s%s\n", row, CLREOL);
pos++;
rc += cgroup_tree(buf, prefix, mode, pos + i);
pos = cgroup_tree(buf, prefix, mode, pos);
free(namelist[i]);
}
@@ -551,7 +704,7 @@ int cgroup_tree(char *path, char *pfx, int mode, int pos)
free(namelist);
}
return rc;
return pos;
}
int show_cgps(char *arg)
@@ -570,17 +723,39 @@ int show_cgps(char *arg)
static void cgtop(uev_t *w, void *arg, int events)
{
static int first = 1;
int lines;
(void)w;
(void)events;
fputs("\e[2J\e[1;1H", stdout);
if (heading)
/* Re-probe screen size in case terminal was resized */
ttinit(1);
/* Clear screen on first run to remove any artifacts */
if (first) {
fputs("\e[?7l\e[2J\e[H", stdout); /* Disable line wrap, clear, home */
first = 0;
} else {
/* Move cursor to home instead of clearing screen to avoid flicker */
fputs("\e[H", stdout);
}
lines = 0;
if (heading) {
print_header(" VmSIZE RSS VmLIB %%MEM %%CPU GROUP");
cgroup_tree(arg, NULL, 1, 0);
lines = 1;
}
cgroup_tree(arg, NULL, 1, lines);
/* Clear from cursor to end of screen to remove leftover lines */
fputs("\e[J", stdout);
fflush(stdout);
}
static void cleanup(void)
{
fputs("\e[?7h", stdout); /* Re-enable line wrap */
ttcooked();
showcursor();
puts("");
@@ -631,6 +806,9 @@ int show_cgtop(char *arg)
if (!hcreate(ttrows + 25))
ERR(70, "failed creating hash table");
/* Ensure we have correct terminal size before starting */
ttinit(1);
sysinfo(&si);
total_ram = si.totalram * si.mem_unit;
+2
View File
@@ -57,7 +57,9 @@ int cgroup_avail(void);
char *pid_cmdline (int pid);
char *pid_cgroup (int pid);
char *cgroup_val(char *path, char *file, char *buf, size_t len);
uint64_t cgroup_memory(char *group);
int cgroup_throttle(char *group, uint64_t *throttled_usec, uint64_t *nr_throttled);
struct cg *cg_stats(char *path);
struct cg *cg_conf (char *path);
+96
View File
@@ -0,0 +1,96 @@
/* Portable clone3() wrapper with fallback to fork()
*
* Copyright (c) 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"
#include <errno.h>
#include <sched.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "clone3.h"
#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
#endif
/* Linux kernel struct clone_args for clone3() syscall */
struct clone3_args {
uint64_t flags; /* Flags bit mask */
uint64_t pidfd; /* File descriptor for PID */
uint64_t child_tid; /* Child TID */
uint64_t parent_tid; /* Parent TID */
uint64_t exit_signal; /* Signal to deliver on exit */
uint64_t stack; /* Stack address */
uint64_t stack_size; /* Stack size */
uint64_t tls; /* TLS descriptor */
uint64_t set_tid; /* Set PID */
uint64_t set_tid_size; /* Number of set_tid entries */
uint64_t cgroup; /* Cgroup file descriptor */
} __attribute__((aligned(8)));
static int use_clone3 = 1;
int has_clone3(void)
{
return use_clone3 == 1;
}
pid_t call_clone3(uint64_t flags, int cgroup_fd)
{
struct clone3_args cl = {
.flags = flags,
.exit_signal = SIGCHLD,
};
pid_t pid;
if (!use_clone3)
goto fallback;
if (cgroup_fd >= 0) {
cl.flags |= CLONE_INTO_CGROUP;
cl.cgroup = cgroup_fd;
}
pid = syscall(__NR_clone3, &cl, sizeof(cl));
if (pid != -1)
return pid;
logit(LOG_WARNING, "clone3() failed, falling back to fork(): %s", strerror(errno));
use_clone3 = 0;
fallback:
return fork();
}
+33
View File
@@ -0,0 +1,33 @@
/* Portable clone3() wrapper with fallback to fork()
*
* Copyright (c) 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.
*/
#ifndef FINIT_CLONE3_H_
#define FINIT_CLONE3_H_
#include <stdint.h>
#include <sys/types.h>
pid_t call_clone3 (uint64_t flags, int cgroup_fd);
int has_clone3 (void);
#endif /* FINIT_CLONE3_H_ */
+56 -13
View File
@@ -60,6 +60,7 @@ int single = 0; /* single user mode from kernel cmdline */
int bootstrap = 1; /* set while bootstrapping (for TTYs) */
int kerndebug = 0; /* set if /proc/sys/kernel/printk > 7 */
int syncsec = 0; /* reboot delay */
int wdtreboot = 0; /* reboot via watchdog, default: SOC */
int readiness = SVC_NOTIFY_PID;
char *finit_conf= NULL;
char *finit_rcsd= NULL;
@@ -105,7 +106,9 @@ char *runparts = NULL;
int runparts_progress;
int runparts_sysv;
char cgroup_current[16]; /* cgroup.NAME sets current cgroup for a set of services */
char cgroup_current[16]; /* cgroup.NAME sets current cgroup for a set of services */
char cgroup_settings_current[128]; /* cgroup.system,cpu.weight:500 - cgroup settings */
int cgroup_delegate_current; /* cgroup.system,delegate - delegation flag */
struct conf_change {
TAILQ_ENTRY(conf_change) link;
@@ -118,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);
@@ -374,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]);
@@ -401,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);
@@ -742,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));
@@ -1034,6 +1041,11 @@ static int parse_static(char *line, int is_rcsd)
return 0;
}
if (MATCH_CMD(line, "reboot-watchdog ", x)) {
wdtreboot = get_bool(strip_line(x), 0);
return 0;
}
/*
* Periodic check and instability index leveler, seconds
*/
@@ -1099,7 +1111,38 @@ static int parse_dynamic(char *line, struct rlimit rlimit[], char *file)
/* Set current cgroup for the following services/run/tasks */
if (MATCH_CMD(line, "cgroup.", x)) {
strlcpy(cgroup_current, x, sizeof(cgroup_current));
char *group, *token, *saveptr;
char settings[128] = {0};
/* Reset cgroup state */
cgroup_settings_current[0] = '\0';
cgroup_delegate_current = 0;
/* First token is the group name (system/user/init) */
group = strtok_r(x, ",", &saveptr);
if (group)
strlcpy(cgroup_current, group, sizeof(cgroup_current));
else
strlcpy(cgroup_current, x, sizeof(cgroup_current));
/* Parse any remaining comma-separated options */
while ((token = strtok_r(NULL, ",", &saveptr)) != NULL) {
if (strncmp(token, "name:", 5) == 0) {
/* Cgroup leaf name override - only valid in per-service directives */
warnx("cgroup %s: name: option not valid in global directive!", group);
} else if (strcmp(token, "delegate") == 0) {
cgroup_delegate_current = 1;
} else {
/* Other settings (cpu.weight:500, memory.max:1G, etc.) */
if (settings[0])
strlcat(settings, ",", sizeof(settings));
strlcat(settings, token, sizeof(settings));
}
}
if (settings[0])
strlcpy(cgroup_settings_current, settings, sizeof(cgroup_settings_current));
return 0;
}
+3
View File
@@ -37,6 +37,7 @@ extern int single;
extern int bootstrap;
extern int kerndebug;
extern int syncsec;
extern int wdtreboot;
extern int readiness;
extern char *fstab;
extern char *sdown;
@@ -50,6 +51,8 @@ extern int logfile_count_max;
extern struct rlimit global_rlimit[];
extern char cgroup_current[];
extern char cgroup_settings_current[];
extern int cgroup_delegate_current;
int str2rlim(char *str);
char *rlim2str(int rlim);
+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_ */
+6
View File
@@ -286,6 +286,12 @@ static void prepare_tty(char *tty, speed_t speed, const char *procname, struct r
char name[80];
int fd, dummy;
/* Set proper protections and ownership */
if (chown(tty, 0, 0))
logit(LOG_ERR, "failed chowning 0:0 %s: %s", tty, strerror(errno));
if (chmod(tty, 0620))
logit(LOG_ERR, "failed chmoding 620 %s: %s", tty, strerror(errno));
fd = open(tty, O_RDWR);
if (fd < 0) {
logit(LOG_ERR, "Failed opening %s: %s", tty, strerror(errno));
+42 -8
View File
@@ -39,6 +39,7 @@
#include <sys/sysmacros.h>
#endif
#include <sys/prctl.h>
#include <paths.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
@@ -60,8 +61,9 @@
#include "util.h"
#include "utmp-api.h"
uev_ctx_t *ctx = NULL; /* Main loop context */
svc_t *wdog = NULL; /* No watchdog by default */
uev_ctx_t *ctx; /* Main loop context */
svc_t *wdog; /* No watchdog by default */
char *arg0; /* Saved for setprocnm() */
/*
@@ -212,6 +214,11 @@ static int fsck(int pass)
if (mnt->mnt_passno == 0 || mnt->mnt_passno != pass)
continue;
if (hasmntopt(mnt, "noauto")) {
dbg("Skipping fsck of %s, marked noauto", mnt->mnt_fsname);
continue;
}
/* Device to maybe fsck, */
dev = mnt->mnt_fsname;
@@ -363,10 +370,9 @@ static void fs_finalize(void)
fs_mount("shm", "/dev/shm", "tmpfs", flags | MS_NODEV, "mode=1777");
}
/* Modern systems use /dev/pts */
if (!fismnt("/dev/pts")) {
/* Remount devpts with proper gid/mode now that /etc/group is available */
{
char opts[32];
int mode;
int gid;
gid = getgroup("tty");
@@ -374,11 +380,11 @@ static void fs_finalize(void)
gid = 0;
/* 0600 is default on Debian, use 0620 to get mesg y by default */
mode = 0620;
snprintf(opts, sizeof(opts), "gid=%d,mode=%d,ptmxmode=0666", gid, mode);
snprintf(opts, sizeof(opts), "gid=%d,mode=0620,ptmxmode=0666", gid);
makedir("/dev/pts", 0755);
fs_mount("devpts", "/dev/pts", "devpts", flags, opts);
fs_mount("devpts", "/dev/pts", "devpts",
flags | MS_REMOUNT, opts);
}
/* Needed on systems like Alpine Linux */
@@ -425,6 +431,11 @@ static void fs_swapon(char *cmd, size_t len)
if (strcmp(mnt->mnt_type, MNTTYPE_SWAP))
continue;
if (hasmntopt(mnt, "noauto")) {
dbg("Skipping swap %s, marked noauto", mnt->mnt_fsname);
continue;
}
snprintf(cmd, len, "swapon %s", mnt->mnt_fsname);
run_interactive(cmd, "Enabling swap %s", mnt->mnt_fsname);
}
@@ -513,8 +524,19 @@ 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);
}
/* Early devpts mount for plugins that need PTY access (e.g. plymouth) */
if (!fismnt("/dev/pts")) {
makedir("/dev/pts", 0755);
fs_mount("devpts", "/dev/pts", "devpts",
MS_NOSUID | MS_NOEXEC, "ptmxmode=0666");
}
}
@@ -600,6 +622,9 @@ int main(int argc, char *argv[])
{
uev_ctx_t loop;
/* Save argv for setprocnm() in child processes */
arg0 = argv[0];
/* user calling telinit or init */
if (getpid() != 1) {
if (geteuid() == 0)
@@ -614,6 +639,15 @@ int main(int argc, char *argv[])
return EX_NOPERM;
}
/*
* Sanity check critical environment variables. We need PATH
* and SHELL for early commands like mount in fs_init().
*/
if (!checkenv("PATH"))
setenv("PATH", _PATH_STDPATH, 1);
if (!checkenv("SHELL"))
setenv("SHELL", _PATH_BSHELL, 1);
/*
* Need /dev, /proc, and /sys for console=, remount and cgroups
*/
+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
+95 -7
View File
@@ -32,6 +32,7 @@
#include <stdarg.h>
#include <sysexits.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
@@ -49,6 +50,7 @@
static pstyle_t progress_onoff = PROGRESS_DEFAULT;
static pstyle_t progress_style = PROGRESS_DEFAULT;
static char last_desc[512]; /* saved description for re-print after interruption */
#ifndef HOSTNAME_PATH
#define HOSTNAME_PATH "/etc/hostname"
@@ -89,6 +91,9 @@ void console_init(void)
/* Reset attributes, background and foreground color */
dprint(STDOUT_FILENO, "\033[49m\033[39m\e[2J", 14);
/* Hide cursor while we boot up. */
dprint(STDOUT_FILENO, "\033[?25l", 6);
log_init();
}
@@ -332,6 +337,8 @@ void printv(const char *fmt, va_list ap)
len = print_timestamp(buf, sizeof(buf));
vsnprintf(&buf[len], sizeof(buf) - len, fmt, ap);
strlcpy(last_desc, &buf[len], sizeof(last_desc)); /* save for re-print */
if (progress_style == PROGRESS_CLASSIC)
cprintf("\r%s ", pad(buf, sizeof(buf), ".", sizeof(buf)));
else
@@ -340,24 +347,70 @@ void printv(const char *fmt, va_list ap)
void print(int rc, const char *fmt, ...)
{
char buf[ttcols];
size_t len;
if (progress_style == PROGRESS_SILENT)
return;
buf[0] = 0;
len = print_timestamp(buf, sizeof(buf));
if (fmt) {
va_list ap;
va_start(ap, fmt);
printv(fmt, ap);
vsnprintf(&buf[len], sizeof(buf) - len, fmt, ap);
va_end(ap);
strlcpy(last_desc, &buf[len], sizeof(last_desc));
} else if (rc >= 0 && last_desc[0]) {
/*
* No new description, but we have one saved from an earlier
* print(-1, ...) call. Re-print it so the final status is not
* left stranded if command output or a kernel message scrolled
* away the original description line.
*/
strlcpy(&buf[len], last_desc, sizeof(buf) - len);
} else {
buf[0] = 0;
}
if (rc < 0)
if (rc < 0) {
/* Pending state: show description with spinner, no final status yet */
if (!buf[0])
return;
if (progress_style == PROGRESS_CLASSIC)
cprintf("\r%s ", pad(buf, sizeof(buf), ".", sizeof(buf)));
else
cprintf("\r\e[K%s%s", status(3), buf);
return;
}
if (progress_style == PROGRESS_CLASSIC)
cprintf("%s\n", status(rc));
else
cprintf("\r%s\n", status(rc));
/*
* Final status. Emit description + status in a single cprintf() so
* both reach the console in one write(), preventing kernel messages or
* command output from splitting the description from its [ OK ]/[FAIL].
*
* status() returns a pointer to a static buffer, so we must copy the
* result for rc before calling status(3) — the second call overwrites
* the same buffer, corrupting the first result.
*/
last_desc[0] = 0;
if (buf[0]) {
char st[64];
strlcpy(st, status(rc), sizeof(st));
if (progress_style == PROGRESS_CLASSIC)
cprintf("\r%s %s\n", pad(buf, sizeof(buf), ".", sizeof(buf)), st);
else
cprintf("\r\e[K%s%s\r%s\n", status(3), buf, st);
} else {
if (progress_style == PROGRESS_CLASSIC)
cprintf("%s\n", status(rc));
else
cprintf("\r%s\n", status(rc));
}
}
void print_desc(char *action, char *desc)
@@ -371,6 +424,18 @@ int print_result(int fail)
return fail;
}
/*
* Reset console state and drain the output buffer before kernel takeover.
* Called just before reboot()/halt() to prevent ANSI escape codes from
* leaking into bootloader or early-kernel output.
*/
void print_exit(void)
{
tcdrain(STDERR_FILENO);
dprint(STDERR_FILENO, "\e[0m\e[?25h", 10); /* reset SGR, show cursor */
tcdrain(STDERR_FILENO);
}
void set_hostname(char **hostname)
{
FILE *fp;
@@ -463,7 +528,7 @@ void networking(int updown)
_exit(EX_OSERR);
_exit(WEXITSTATUS(rc));
}
cgroup_service("network", pid, NULL);
cgroup_service("network", pid, NULL, NULL, NULL);
print(pid > 0 ? 0 : 1, "%s network interfaces ...",
updown ? "Bringing up" : "Taking down");
@@ -524,6 +589,29 @@ int in_container(void)
return 0;
}
/**
* setprocnm - Set process name in argv[0] for ps/pidof
* @name: New process name to set
*
* Changes the process name by modifying argv[0]. This is particularly
* useful for compatibility with BusyBox's pidof which reads cmdline
* from /proc/pid/cmdline rather than /proc/pid/comm.
*/
void setprocnm(const char *name)
{
extern char *arg0;
size_t len;
if (!arg0 || !name)
return;
prctl(PR_SET_NAME, name, 0, 0, 0);
len = strlen(arg0);
memset(arg0, 0, len);
strlcpy(arg0, name, len + 1);
}
/**
* Local Variables:
* indent-tabs-mode: t
+13
View File
@@ -68,10 +68,12 @@ void printv (const char *fmt, va_list ap);
void print (int action, const char *fmt, ...) __attribute__ ((format (printf, 2, 3)));
void print_desc (char *action, char *desc);
int print_result (int fail);
void print_exit (void);
void set_hostname (char **hostname);
void networking (int updown);
int in_container (void);
void setprocnm (const char *name);
int complete (char *cmd, int pid);
int run (char *cmd, char *log);
@@ -82,6 +84,17 @@ pid_t run_sh (char *tty, int noclear, int nowait, struct rlimit rlimi
pid_t run_bg (char *cmd, char *args[]);
int run_parts (char *dir, char *cmd, const char *env[], int progress, int sysv);
static inline int checkenv(const char *env)
{
const char *val = getenv(env);
if (!val || !val[0])
return 0;
return 1;
}
static inline int dprint(int fd, const char *s, size_t len)
{
size_t loop = 3;
+248 -19
View File
@@ -33,11 +33,6 @@
#include <time.h>
#include <utmp.h>
#include <arpa/inet.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include "initctl.h"
#include "client.h"
@@ -45,7 +40,6 @@
#include "serv.h"
#include "service.h"
#include "cgutil.h"
#include "util.h"
#include "utmp-api.h"
struct cmd {
@@ -124,7 +118,7 @@ void print_header(const char *fmt, ...)
fputc('=', stdout);
fputs("\n", stdout);
} else {
char buf[ttcols];
char buf[ttcols - 9];
vsnprintf(buf, sizeof(buf), fmt, ap);
printheader(stdout, buf, 0);
@@ -652,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;
@@ -989,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();
@@ -1004,17 +1107,20 @@ 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));
if (svc->state != SVC_RUNNING_STATE) {
int rc, sig;
@@ -1030,24 +1136,136 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
"%s \"exit\": { \"%s\": %d },\n",
indent, "signal", sig);
}
fprintf(fp,
"%s \"origin\": \"%s\",\n",
indent, svc->file[0] ? svc->file : "built-in");
svc_command(svc, buf, sizeof(buf), 0);
fprintf(fp,
"%s \"origin\": \"%s\",\n"
"%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])
fprintf(fp,
"%s \"condition\": %s,\n", indent, buf);
if (svc->manual)
fprintf(fp,
"%s \"starts\": %d,\n", indent, svc->once);
fprintf(fp,
"%s \"restarts\": %d,\n", indent, svc->restart_tot); /* XXX: add restart_cnt and restart_max */
/* Add memory and CPU information if cgroup support is available */
if (cgrp && svc->pid > 1) {
char *group = pid_cgroup(svc->pid);
if (group) {
uint64_t throttled_usec = 0;
uint64_t nr_throttled = 0;
char path[256];
struct cg *cg;
fprintf(fp, "%s \"cgroup\": \"%s\",\n", indent, group);
fprintf(fp, "%s \"memory\": %lu,\n", indent, cgroup_memory(group));
if (cgroup_throttle(group, &throttled_usec, &nr_throttled) == 0) {
fprintf(fp, "%s \"cpu\": {\n", indent);
fprintf(fp, "%s \"throttled_usec\": %lu,\n", indent, throttled_usec);
fprintf(fp, "%s \"nr_throttled\": %lu\n", indent, nr_throttled);
fprintf(fp, "%s },\n", indent);
}
/* Add limits structure using existing cg_conf() */
snprintf(path, sizeof(path), "%s/%s", FINIT_CGPATH, group);
cg = cg_conf(path);
if (cg && (cg->cg_mem.min[0] || cg->cg_mem.max[0] ||
cg->cg_cpu.weight[0] || cg->cg_cpu.max[0])) {
int has_memory = cg->cg_mem.min[0] || cg->cg_mem.max[0];
int has_cpu = cg->cg_cpu.weight[0] || cg->cg_cpu.max[0];
fprintf(fp, "%s \"limits\": {\n", indent);
if (has_memory) {
fprintf(fp, "%s \"memory\": {\n", indent);
if (cg->cg_mem.min[0]) {
if (!strcmp(cg->cg_mem.min, "max"))
fprintf(fp, "%s \"min\": \"%s\"", indent, cg->cg_mem.min);
else
fprintf(fp, "%s \"min\": %s", indent, cg->cg_mem.min);
if (cg->cg_mem.max[0])
fprintf(fp, ",\n");
else
fprintf(fp, "\n");
}
if (cg->cg_mem.max[0]) {
if (!strcmp(cg->cg_mem.max, "max"))
fprintf(fp, "%s \"max\": \"%s\"\n", indent, cg->cg_mem.max);
else
fprintf(fp, "%s \"max\": %s\n", indent, cg->cg_mem.max);
}
fprintf(fp, "%s }", indent);
if (has_cpu)
fprintf(fp, ",\n");
else
fprintf(fp, "\n");
}
if (has_cpu) {
char cpu_burst[32];
char *quota, *period;
char max_copy[32];
fprintf(fp, "%s \"cpu\": {\n", indent);
if (cg->cg_cpu.weight[0])
fprintf(fp, "%s \"weight\": %s,\n", indent, cg->cg_cpu.weight);
if (cg->cg_cpu.max[0]) {
/* Split cpu.max into quota and period */
strlcpy(max_copy, cg->cg_cpu.max, sizeof(max_copy));
quota = strtok(max_copy, " ");
period = strtok(NULL, " ");
fprintf(fp, "%s \"max\": {\n", indent);
if (quota) {
if (!strcmp(quota, "max"))
fprintf(fp, "%s \"quota\": \"%s\"", indent, quota);
else
fprintf(fp, "%s \"quota\": %s", indent, quota);
if (period)
fprintf(fp, ",\n");
else
fprintf(fp, "\n");
}
if (period)
fprintf(fp, "%s \"period\": %s\n", indent, period);
fprintf(fp, "%s }", indent);
/* Check for cpu.max.burst (kernel >= 5.14) */
if (cgroup_val(path, "cpu.max.burst", cpu_burst, sizeof(cpu_burst)))
fprintf(fp, ",\n%s \"max_burst\": %s\n", indent, cpu_burst);
else
fprintf(fp, "\n");
}
fprintf(fp, "%s }\n", indent);
}
fprintf(fp, "%s },\n", indent);
}
free(group);
}
}
fprintf(fp,
"%s \"pidfile\": \"%s\",\n"
"%s \"pid\": %d,\n"
@@ -1134,8 +1352,11 @@ static int show_status(char *arg)
printf(" Starts : %d\n", svc->once);
printf(" Restarts : %d (%d/%d)\n", svc->restart_tot, svc->restart_cnt, svc->restart_max);
printf(" Runlevels : %s\n", runlevel_string(runlevel, svc->runlevels));
if (cgrp && svc->pid > 1) {
const struct cg *cg;
uint64_t throttled_usec = 0;
uint64_t nr_throttled = 0;
char path[256];
char *group;
@@ -1147,6 +1368,12 @@ static int show_status(char *arg)
cg = cg_conf(path);
printf(" Memory : %s\n", memsz(cgroup_memory(group), uptm, sizeof(uptm)));
if (cgroup_throttle(group, &throttled_usec, &nr_throttled) == 0) {
printf("CPU Throttle : %lu usec (%lu times)\n",
throttled_usec, nr_throttled);
}
printf(" CGroup : %s cpu %s [%s, %s] mem [%s, %s]\n",
group, cg->cg_cpu.set, cg->cg_cpu.weight, cg->cg_cpu.max,
cg->cg_mem.min, cg->cg_mem.max);
@@ -1342,7 +1569,7 @@ static int usage(int rc)
" This allows restart of run/tasks that have already run\n"
" Note: Finit .conf file(s) are *not* reloaded!\n"
" restart <NAME>[:ID] Restart (stop/start) service by name\n"
" signal <NAME>[:ID] <S> Send signal S to service by name, with optional ID\n"
" kill <NAME>[:ID] <S> Send signal S to service by name, with optional ID\n"
" ident [NAME] Show matching identities for NAME, or all\n"
" status <NAME>[:ID] Show service status, by name\n"
" status Show status of services, default command\n");
@@ -1360,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,
@@ -1540,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 }
@@ -1610,7 +1839,7 @@ int main(int argc, char *argv[])
}
if (interactive)
ttinit();
ttinit(0);
return cmd_parse(argc - optind, &argv[optind], command);
}
+305
View File
@@ -0,0 +1,305 @@
/* 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;
if (remove(fpath))
dbg("Failed to remove %s: %s", fpath, strerror(errno));
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;
int 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 */
fd = open(newroot, O_RDONLY | O_DIRECTORY);
if (fd < 0) {
logit(LOG_ERR, "switch_root: %s is not a directory", newroot);
errno = ENOTDIR;
return -1;
}
if (fstat(fd, &newroot_st)) {
close(fd);
logit(LOG_ERR, "switch_root: cannot stat %s", newroot);
return -1;
}
close(fd);
/* Verify newroot is a mount point (different device than parent) */
fd = open("/", O_RDONLY | O_DIRECTORY);
if (fd < 0) {
logit(LOG_ERR, "switch_root: cannot open /");
return -1;
}
if (fstat(fd, &oldroot_st)) {
close(fd);
logit(LOG_ERR, "switch_root: cannot stat /");
return -1;
}
close(fd);
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 in new root. dup2() closes the old fds itself,
* so open() returns a fd > STDERR_FILENO that we can always close. */
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);
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_ */
/**
+1 -1
View File
@@ -95,7 +95,7 @@ static void run_env(const char *env[])
int run_parts(char *dir, char *cmd, const char *env[], int progress, int sysv)
{
size_t cmdlen = cmd ? strlen(cmd) : strlen("start");
struct dirent **d;
struct dirent **d = NULL;
int i, num;
int rc = 0;
+507 -126
View File
@@ -25,13 +25,16 @@
#include "config.h" /* Generated by configure script */
#include <ctype.h> /* isblank() */
#include <grp.h> /* setgroups() */
#include <sched.h> /* sched_yield() */
#include <string.h>
#include <sys/reboot.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <linux/vt.h>
#include <net/if.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
@@ -39,8 +42,12 @@
# include <lite/lite.h>
#endif
#include <wordexp.h>
#ifdef HAVE_LIBCAP
# include <sys/capability.h>
#endif
#include "cgroup.h"
#include "clone3.h"
#include "client.h"
#include "conf.h"
#include "cond.h"
@@ -88,7 +95,7 @@ static void service_timeout_cb(uev_t *w, void *arg, int events)
svc_t *svc = arg;
if (UEV_ERROR == events) {
dbg("%s(): spurious problem, svc %s.", __func__, svc_ident(svc, NULL, 0));
dbg("%s: spurious problem", svc_ident(svc, NULL, 0));
uev_timer_start(w);
return;
}
@@ -271,23 +278,18 @@ static int lredirect(svc_t *svc)
{
static int have_sysklogd = -1;
pid_t svc_pid = getpid();
int pipefd[2];
pid_t pid;
int fd;
/*
* Open PTY to connect to logger. A pty isn't buffered
* like a pipe, and it eats newlines so they aren't logged
* Use a pipe to connect to logger. This ensures isatty()
* returns false for the service, preventing programs from
* emitting ANSI escape codes and other TTY-specific output.
*/
fd = posix_openpt(O_RDWR);
if (fd == -1) {
dbg("Failed posix_openpt(), errno %d: %s", errno, strerror(errno));
svc->log.enabled = 0;
return -1;
}
if (grantpt(fd) == -1 || unlockpt(fd) == -1) {
dbg("Failed grantpt()|unlockpt(), errno %d: %s", errno, strerror(errno));
close(fd);
if (pipe(pipefd) == -1) {
dbg("Failed pipe(), errno %d: %s", errno, strerror(errno));
svc->log.enabled = 0;
return -1;
}
@@ -319,17 +321,12 @@ static int lredirect(svc_t *svc)
char *prio = "daemon.info";
char buf[MAX_IDENT_LEN];
char *tag;
int fds;
sched_yield();
fds = open(ptsname(fd), O_RDONLY);
close(fd);
if (fds == -1) {
logit(LOG_WARNING, "failed open() ptsname(%d), errno %d", fd, errno);
_exit(0);
}
dup2(fds, STDIN_FILENO);
close(pipefd[1]); /* Close write end in logger */
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
/* Reset signals */
sig_unblock();
@@ -385,10 +382,48 @@ static int lredirect(svc_t *svc)
_exit(1);
}
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(pipefd[0]); /* Close read end in service */
dup2(pipefd[1], STDOUT_FILENO);
dup2(pipefd[1], STDERR_FILENO);
return close(fd);
return close(pipefd[1]);
}
/*
* Set up a controlling TTY for run/task/service.
* Opens the TTY device, dups to 0/1/2, resets termios to
* sane defaults, sets TERM env, and updates procname.
*/
static void svc_prepare_ctty(const char *tty, const char *procname, int log)
{
int fd, dummy;
fd = open(tty, O_RDWR);
if (fd < 0) {
logit(LOG_ERR, "Failed opening %s: %s", tty, strerror(errno));
_exit(1);
}
/* Acquire as controlling terminal for the session */
ioctl(fd, TIOCSCTTY, 0);
dup2(fd, STDIN_FILENO);
if (!log) {
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
}
/* Set default TERM */
if (ioctl(fd, VT_OPENQRY, &dummy) == -1)
setenv("TERM", "vt102", 1); /* likely a serial line */
else
setenv("TERM", "linux", 1);
/* Reset to sane defaults */
stty(fd, B0);
close(fd);
prctl(PR_SET_NAME, procname, 0, 0, 0);
}
/*
@@ -483,30 +518,6 @@ static int is_norespawn(void)
fexist("/tmp/norespawn");
}
/* used for process group name, derived from originating filename,
* so to group multiple services, place them in the same .conf
*/
static char *group_name(svc_t *svc, char *buf, size_t len)
{
char *ptr;
if (!svc->file[0])
return svc_ident(svc, buf, len);
ptr = strrchr(svc->file, '/');
if (ptr)
ptr++;
else
ptr = svc->file;
strlcpy(buf, ptr, len);
ptr = strstr(buf, ".conf");
if (ptr)
*ptr = 0;
return buf;
}
static void compose_cmdline(svc_t *svc, char *buf, size_t len)
{
size_t i;
@@ -521,11 +532,61 @@ static void compose_cmdline(svc_t *svc, char *buf, size_t len)
}
}
static void set_uid(uid_t uid, svc_t *svc)
{
#ifdef HAVE_LIBCAP
/*
* Only use cap_setuid() if capabilities are configured,
* otherwise it would (by design) drop all capabilities,
* breaking root services
*/
if (svc->capabilities[0]) {
cap_iab_t cap_iab;
if (cap_setuid(uid)) {
err(1, "%s: failed cap_setuid(%d)", svc_ident(svc, NULL, 0), uid);
return;
}
/* After dropping privileges, set the specific capabilities we need */
cap_iab = cap_iab_from_text(svc->capabilities);
if (!cap_iab) {
err(1, "%s: failed parsing capabilities '%s'",
svc_ident(svc, NULL, 0), svc->capabilities);
return;
}
if (cap_iab_set_proc(cap_iab) != 0) {
cap_free(cap_iab);
err(1, "%s: failed setting capabilities",
svc_ident(svc, NULL, 0));
}
cap_free(cap_iab);
} else
#endif
if (setuid(uid))
err(1, "%s: failed setuid(%d)", svc_ident(svc, NULL, 0), uid);
}
static pid_t service_fork(svc_t *svc)
{
const char *cgnm;
char grnam[128];
int cgfd = -1;
pid_t pid;
pid = fork();
cgnm = cgroup_svc_name(svc, grnam, sizeof(grnam));
cgfd = cgroup_prepare(svc, cgnm);
pid = call_clone3(0, cgfd);
if (cgfd >= 0)
close(cgfd);
if (pid < 0) {
cgroup_del_svc(svc, cgnm);
return pid;
}
if (pid == 0) {
char *home = NULL;
#ifdef ENABLE_STATIC
@@ -534,7 +595,21 @@ static pid_t service_fork(svc_t *svc)
#else
int uid = getuser(svc->username, &home);
int gid = getgroup(svc->group);
/* Check for invalid user/group - fail rather than silent fallback */
if (uid < 0) {
err(1, "%s: user '%s' not found, cannot start service",
svc_ident(svc, NULL, 0), svc->username);
return -1;
}
if (gid < 0) {
err(1, "%s: group '%s' not found, cannot start service",
svc_ident(svc, NULL, 0), svc->group);
return -1;
}
#endif
if (svc_is_tty(svc))
setprocnm("getty");
sched_yield();
@@ -545,6 +620,42 @@ static pid_t service_fork(svc_t *svc)
svc_ident(svc, NULL, 0), rlim2str(i));
}
#ifndef ENABLE_STATIC
/* Set supplementary groups from /etc/group and config */
{
gid_t supgids[NGROUPS_MAX];
int ngroups = NGROUPS_MAX;
int i, j, n = 0;
/* Get user's supplementary groups from /etc/group */
if (getgrouplist(svc->username, gid, supgids, &ngroups) >= 0)
n = ngroups;
/* Add explicitly configured supplementary groups */
for (i = 0; i < svc->num_supgroups && n < NGROUPS_MAX; i++) {
int g = getgroup(svc->supgroups[i]);
int found = 0;
if (g < 0) {
warn("%s: unknown supplementary group '%s'",
svc_ident(svc, NULL, 0), svc->supgroups[i]);
continue;
}
/* Skip if already in list from /etc/group */
for (j = 0; j < n; j++) {
if (supgids[j] == (gid_t)g) {
found = 1;
break;
}
}
if (!found)
supgids[n++] = g;
}
if (n > 0 && setgroups(n, supgids))
err(1, "%s: failed setgroups()", svc_ident(svc, NULL, 0));
}
#endif
/* Set desired user+group */
if (gid >= 0) {
if (setgid(gid))
@@ -552,12 +663,14 @@ static pid_t service_fork(svc_t *svc)
}
if (uid >= 0) {
if (setuid(uid))
err(1, "%s: failed setuid(%d)", svc_ident(svc, NULL, 0), uid);
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)) {
@@ -571,13 +684,11 @@ static pid_t service_fork(svc_t *svc)
source_env(svc);
}
if (pid > 1) {
char grnam[80];
if (pid > 0 && !has_clone3()) {
if (svc_is_tty(svc))
cgroup_user("getty", pid);
else
cgroup_service(group_name(svc, grnam, sizeof(grnam)), pid, &svc->cgroup);
cgroup_service(cgnm, pid, &svc->cgroup, svc->username, svc->group);
}
return pid;
@@ -735,7 +846,7 @@ static int service_start(svc_t *svc)
}
} else { /* pid == 0 */
char str[strlen(NOTIFY_PATH) + 32];
char *args[MAX_NUM_SVC_ARGS + 1];
char *args[MAX_NUM_SVC_ARGS + 3];
int status;
if (!svc_is_tty(svc))
@@ -874,6 +985,14 @@ static int service_start(svc_t *svc)
sig_unblock();
/*
* If the stanza declared tty:<dev>, set up a controlling
* terminal on that device now that we're a session leader.
* Must run after setsid() to acquire the controlling TTY.
*/
if (!svc_is_tty(svc) && svc_has_ctty(svc))
svc_prepare_ctty(svc->log.ctty, svc->cmd, !!svc->log.enabled);
if (svc_is_runtask(svc))
status = exec_runtask(args[0], &args[1]);
else if (svc_is_tty(svc))
@@ -959,7 +1078,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);
@@ -975,6 +1093,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);
@@ -984,23 +1104,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 */
@@ -1021,15 +1125,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);
@@ -1068,7 +1188,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);
@@ -1083,10 +1203,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);
}
/*
@@ -1134,12 +1254,12 @@ int service_stop(svc_t *svc)
svc_set_state(svc, SVC_HALTED_STATE);
}
} else {
char *args[MAX_NUM_SVC_ARGS + 1];
char *args[MAX_NUM_SVC_ARGS + 3];
size_t i = 0, j;
args[i++] = svc->cmd;
/* this handles, e.g., bridge-stop br0 stop */
for (j = 0; j < MAX_NUM_SVC_ARGS - 2; j++) {
for (j = 0; j < MAX_NUM_SVC_ARGS; j++) {
if (!strlen(svc->args[j]))
break;
args[i++] = svc->args[j];
@@ -1147,10 +1267,11 @@ int service_stop(svc_t *svc)
args[i++] = "stop";
args[i] = NULL;
switch (fork()) {
switch (service_fork(svc)) {
case 0:
setsid();
redirect(svc);
setsid();
sig_unblock();
exec_runtask(args[0], &args[1]);
_exit(0);
break;
@@ -1205,7 +1326,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) {
@@ -1213,8 +1334,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 */
@@ -1371,37 +1492,89 @@ static void parse_env(svc_t *svc, char *env)
strlcpy(svc->env, env, sizeof(svc->env));
}
static void parse_caps(svc_t *svc, char *caps)
{
#ifdef HAVE_LIBCAP
cap_iab_t cap_iab;
cap_iab = cap_iab_from_text(caps);
if (!cap_iab) {
err(1, "%s: failed parsing capabilities '%s'", svc_ident(svc, NULL, 0), caps);
memset(svc->capabilities, 0, sizeof(svc->capabilities));
return;
}
cap_free(cap_iab);
strlcpy(svc->capabilities, caps, sizeof(svc->capabilities));
#else
(void)svc;
(void)caps;
#endif
}
/*
* the @cgroup argument can be, e.g., .system:mem.max:1234 or just the
* the @cgroup argument can be, e.g., .system,mem.max:1234 or just the
* default group with some cfg, e.g., :mem.max:1234 as a side effect,
* cgroupinit also work, selecting group init.
*/
static void parse_cgroup(svc_t *svc, char *cgroup)
{
char *ptr = NULL;
if (!cgroup)
return;
/* Per-service cgroup directive completely overrides global settings */
svc->cgroup.delegate = 0;
svc->cgroup.leafname[0] = 0;
svc->cgroup.cfg[0] = 0;
/* Detect syntax: old colon-based vs new comma-separated */
if (cgroup[0] == '.') {
ptr = strchr(cgroup, ':');
if (ptr)
*ptr++ = 0;
group:
strlcpy(svc->cgroup.name, &cgroup[1], sizeof(svc->cgroup.name));
if (!ptr)
return;
char *token, *ptr, *group;
char settings[128] = {0};
char cgcopy[256];
/* New comma-separated syntax: cgroup.system,name:udevd,delegate,cpu.max:10000 */
strlcpy(cgcopy, cgroup, sizeof(cgcopy));
group = strtok_r(cgcopy, ",", &ptr);
if (group && group[0] == '.')
strlcpy(svc->cgroup.name, &group[1], sizeof(svc->cgroup.name));
/* Parse remaining comma-separated options */
while ((token = strtok_r(NULL, ",", &ptr)) != NULL) {
if (strncmp(token, "name:", 5) == 0) {
/* Cgroup leaf name override */
strlcpy(svc->cgroup.leafname, token + 5, sizeof(svc->cgroup.leafname));
} else if (strcmp(token, "delegate") == 0) {
svc->cgroup.delegate = 1;
} else {
/* Other settings (cpu.weight:500, memory.max:1G, etc.) */
if (settings[0])
strlcat(settings, ",", sizeof(settings));
strlcat(settings, token, sizeof(settings));
}
}
if (settings[0])
strlcpy(svc->cgroup.cfg, settings, sizeof(svc->cgroup.cfg));
dbg("%s: cgroup name:%s leafname:%s cfg:%s delegate:%s", svc_ident(svc, NULL, 0),
svc->cgroup.name, svc->cgroup.leafname, svc->cgroup.cfg,
svc->cgroup.delegate ? "yes" : "no");
} else if (cgroup[0] == ':') {
char *ptr;
/* Old syntax: cgroup:settings (keeps current group) */
ptr = &cgroup[1];
} else
goto group;
if (strlen(ptr) >= sizeof(svc->cgroup)) {
errx(1, "%s: cgroup settings too long (>%zu chars)", svc_ident(svc, NULL, 0), sizeof(svc->cgroup));
return;
if (strlen(ptr) >= sizeof(svc->cgroup.cfg)) {
errx(1, "%s: cgroup settings too long (>%zu chars)", svc_ident(svc, NULL, 0),
sizeof(svc->cgroup.cfg));
return;
}
strlcpy(svc->cgroup.cfg, ptr, sizeof(svc->cgroup.cfg));
} else {
/* Just a group name without dot or colon */
strlcpy(svc->cgroup.name, cgroup, sizeof(svc->cgroup.name));
}
strlcpy(svc->cgroup.cfg, ptr, sizeof(svc->cgroup.cfg));
}
static void parse_sighalt(svc_t *svc, char *arg)
@@ -1420,9 +1593,9 @@ static void parse_killdelay(svc_t *svc, char *delay)
const char *errstr;
long long sec;
sec = strtonum(delay, 1, 60, &errstr);
sec = strtonum(delay, 1, 300, &errstr);
if (errstr) {
errx(1, "%s: killdelay %s is %s (1-60)", svc_ident(svc, NULL, 0), delay, errstr);
errx(1, "%s: killdelay %s is %s (1-300)", svc_ident(svc, NULL, 0), delay, errstr);
return;
}
@@ -1464,7 +1637,7 @@ static void parse_script(svc_t *svc, char *type, char *script, int *tmo, char *b
found = which(path);
if (!found) {
logit(LOG_WARNING, "%s:%s is missing or not executable, skipping.", type, path);
logit(LOG_WARNING, "%s:'%s' is missing or not executable, skipping.", type, path);
goto err;
}
free(found);
@@ -1665,14 +1838,16 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
char *ready_script = NULL, *conflict = NULL;
char *reload_script = NULL, *stop_script = NULL;
char *cleanup_script = NULL;
char *caps = NULL;
char ident[MAX_IDENT_LEN];
char *ifstmt = NULL;
char *notify = NULL;
char *ctty = NULL;
struct tty tty = { 0 };
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;
@@ -1740,6 +1915,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;
@@ -1780,6 +1957,10 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
stop_script = arg;
else if (MATCH_CMD(cmd, "env:", arg))
env = arg;
else if (MATCH_CMD(cmd, "caps:", arg))
caps = arg;
else if (MATCH_CMD(cmd, "tty:", arg))
ctty = arg;
/* catch both cgroup: and cgroup. handled in parse_cgroup() */
else if (MATCH_CMD(cmd, "cgroup", arg))
cgroup = arg;
@@ -1909,7 +2090,29 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
char *ptr = strchr(username, ':');
if (ptr) {
char *sup;
*ptr++ = 0;
/* Check for supplementary groups: group,sup1,sup2,... */
sup = strchr(ptr, ',');
if (sup) {
*sup++ = 0;
svc->num_supgroups = 0;
while (sup) {
char *next = strchr(sup, ',');
if (next)
*next++ = 0;
if (svc->num_supgroups >= MAX_NUM_SUPGROUPS) {
warn("%s: too many supplementary groups, max %d",
svc->cmd, MAX_NUM_SUPGROUPS);
break;
}
strlcpy(svc->supgroups[svc->num_supgroups], sup,
sizeof(svc->supgroups[0]));
svc->num_supgroups++;
sup = next;
}
}
strlcpy(svc->group, ptr, sizeof(svc->group));
}
strlcpy(svc->username, username, sizeof(svc->username));
@@ -2003,6 +2206,26 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
parse_env(svc, env);
else
memset(svc->env, 0, sizeof(svc->env));
if (caps)
parse_caps(svc, caps);
else
memset(svc->capabilities, 0, sizeof(svc->capabilities));
if (!svc_is_tty(svc) && ctty) {
char *dev = ctty;
/* NOTE: @console expands only to first console, not all */
if (tty_isatcon(ctty))
dev = tty_atcon();
dev = tty_canonicalize(dev);
if (dev)
strlcpy(svc->log.ctty, dev, sizeof(svc->log.ctty));
else
logit(LOG_WARNING, "%s: tty: %s not found, skipping ctty", svc_ident(svc, NULL, 0), ctty);
} else if (!svc_is_tty(svc)) {
memset(svc->log.ctty, 0, sizeof(svc->log.ctty));
}
if (file)
strlcpy(svc->file, file, sizeof(svc->file));
else
@@ -2017,6 +2240,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;
@@ -2040,20 +2275,33 @@ 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));
/* Seed with currently active group, may be empty */
strlcpy(svc->cgroup.name, cgroup_current, sizeof(svc->cgroup.name));
/* Apply cgroup settings if specified */
if (cgroup_settings_current[0])
strlcpy(svc->cgroup.cfg, cgroup_settings_current, sizeof(svc->cgroup.cfg));
/* Apply delegation flag */
svc->cgroup.delegate = cgroup_delegate_current;
if (cgroup)
parse_cgroup(svc, cgroup);
@@ -2097,6 +2345,7 @@ void service_unregister(svc_t *svc)
return;
service_stop(svc);
service_timeout_cancel(svc);
for (c = strtok(svc->cond, ","); c; c = strtok(NULL, ","))
devmon_del_cond(c);
@@ -2136,6 +2385,10 @@ void service_monitor(pid_t lost, int status)
: (svc->state == SVC_CLEANUP_STATE
? svc->cleanup_script
: svc->pre_script)), lost, ok, sig, rc);
/* Prevent: spurious problem from timeout callback */
service_timeout_cancel(svc);
/* Kill all children in the same proess group, e.g. logit */
dbg("Killing lingering children in same process group ...");
kill(-svc->pid, SIGKILL);
@@ -2231,7 +2484,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);
}
}
@@ -2467,7 +2734,16 @@ static void service_retry(svc_t *svc)
int timeout;
service_timeout_cancel(svc);
if (is_norespawn())
return;
if (svc->respawn) {
/*
* Respawn services (TTYs) that exited with non-zero status
* have already been delayed in the SVC_RUNNING_STATE handler.
* Just restart now.
*/
dbg("%s crashed/exited, respawning ...", svc_ident(svc, NULL, 0));
svc_unblock(svc);
service_step(svc);
@@ -2518,8 +2794,13 @@ static void service_retry(svc_t *svc)
timeout = ((*restart_cnt) <= (svc->restart_max / 2)) ? 2000 : 5000;
/* If a longer timeout was specified in the conf, use that instead. */
svc->restart_tmo = max(svc->restart_tmo, timeout);
logit(LOG_CONSOLE|LOG_WARNING, "Service %s[%d] died, restarting (retry in %d msec) (attempt: %d/%d)",
svc_ident(svc, NULL, 0), svc->oldpid, svc->restart_tmo, *restart_cnt, svc->restart_max);
logit(LOG_CONSOLE|LOG_WARNING, "Service %s[%d] died (%s%d), restarting (retry in %d msec) (attempt: %d/%d)",
svc_ident(svc, NULL, 0), svc->oldpid,
WIFEXITED(svc->status) ? "with exit status: " : "by signal: ",
WIFEXITED(svc->status) ? WEXITSTATUS(svc->status) : WTERMSIG(svc->status),
svc->restart_tmo,
*restart_cnt,
svc->restart_max);
svc_unblock(svc);
service_step(svc);
@@ -2673,8 +2954,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;
@@ -2690,9 +2975,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;
@@ -2771,6 +3063,9 @@ restart:
if (sm_in_reload())
break;
if (is_norespawn())
break;
/* Don't start if it conflicts with something else already started */
if (svc_conflicts(svc)) {
logit(svc->nowarn ? LOG_DEBUG : LOG_INFO,
@@ -2822,6 +3117,8 @@ restart:
if (!svc->pid) {
if (svc_is_daemon(svc) || svc_is_tty(svc)) {
int rc = WEXITSTATUS(svc->status);
svc_restarting(svc); /* BLOCK_RESTARTING */
svc_set_state(svc, SVC_HALTED_STATE);
@@ -2836,6 +3133,18 @@ restart:
goto done;
}
/*
* Non-zero exit for respawn services (TTYs): delay retry
* to prevent busy-loop and rate-limit retries
*/
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);
goto done;
}
dbg("respawning %s", svc_ident(svc, NULL, 0));
svc_unblock(svc);
break;
@@ -2906,6 +3215,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) {
@@ -2977,6 +3299,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;
@@ -3035,6 +3380,42 @@ int service_completed(svc_t **svcp)
return 1;
}
/*
* service_log_incomplete - Log all incomplete run/task in current runlevel
*
* Iterates through all run/task services and logs those that have not
* yet completed. Useful for diagnosing bootstrap delays or reporting
* what was skipped when user interrupts bootstrap wait.
*/
void service_log_incomplete(void)
{
svc_t *svc, *iter = NULL;
int count = 0;
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
if (!svc_is_runtask(svc))
continue;
if (!svc_enabled(svc))
continue;
if (svc_conflicts(svc))
continue;
if (strstr(svc->cond, plugin_hook_str(HOOK_SVC_UP)) ||
strstr(svc->cond, plugin_hook_str(HOOK_SYSTEM_UP)))
continue;
if (!svc->once) {
logit(LOG_WARNING, "Incomplete: %s", svc_ident(svc, NULL, 0));
count++;
}
}
if (count == 0)
logit(LOG_WARNING, "No incomplete run/task services found");
}
/*
* Called in SM_RELOAD_WAIT_STATE to update unmodified non-native
* services' READY condition to the current generation. Similar
@@ -3141,7 +3522,7 @@ static void service_interval_cb(uev_t *w, void *arg, int events)
(void)arg;
if (UEV_ERROR == events) {
dbg("%s(): spurious problem, restarting.", __func__);
dbg("spurious problem, restarting.");
uev_timer_start(w);
return;
}
+1
View File
@@ -48,6 +48,7 @@ void service_step_all (int types);
void service_worker (void *unused);
int service_completed (svc_t **svc);
void service_log_incomplete (void);
void service_notify_reconf (void);
void service_init (uev_ctx_t *ctx);
+24 -8
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>
@@ -187,6 +189,11 @@ static void fs_swapoff(void)
if (strcmp(mnt->mnt_type, MNTTYPE_SWAP))
continue;
if (hasmntopt(mnt, "noauto")) {
dbg("Skipping swap %s, marked noauto", mnt->mnt_fsname);
continue;
}
snprintf(cmd, sizeof(cmd), "swapoff %s", mnt->mnt_fsname);
run_interactive(cmd, "Disabling swap %s", mnt->mnt_fsname);
}
@@ -201,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;
@@ -283,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);
@@ -296,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
@@ -315,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 */
@@ -344,16 +355,18 @@ 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;
}
if (wdog) {
if (wdtreboot && wdog) {
print(kill(wdog->pid, SIGPWR) == 1, "Advising watchdog, system going down");
do_sleep(2);
}
@@ -400,25 +413,28 @@ void do_shutdown(shutop_t op)
/* Reboot via watchdog or kernel, or shutdown? */
if (op == SHUT_REBOOT) {
print(0, "Rebooting ...");
if (wdog && wdog->pid > 1) {
if (wdtreboot && wdog && wdog->pid > 1) {
int timeout = 10;
/* Wait here until the WDT reboots, or timeout with fallback */
print(0, "Rebooting using WDT, please wait ...");
print(kill(wdog->pid, SIGTERM) == 1, "Pending watchdog reboot");
while (timeout--)
do_sleep(1);
}
dbg("Rebooting ...");
print(0, "Rebooting ...");
print_exit();
reboot(RB_AUTOBOOT);
} else if (op == SHUT_OFF) {
print(0, "Powering down ...");
print_exit();
reboot(RB_POWER_OFF);
}
/* Also fallback if any of the other two fails */
print(0, "Halting ...");
print_exit();
reboot(RB_HALT_SYSTEM);
}
+172 -5
View File
@@ -23,12 +23,16 @@
#include "config.h" /* Generated by configure script */
#include <fcntl.h>
#include <paths.h>
#include <sys/types.h>
#include <termios.h>
#include "finit.h"
#include "cgroup.h"
#include "cond.h"
#include "conf.h"
#include "devmon.h"
#include "log.h"
#include "helpers.h"
#include "private.h"
@@ -56,8 +60,108 @@ static struct {
int newlevel; /* Set to new runlevel on change, -1 otherwise */
int reload; /* Set on reload event, else 0 */
int in_reload; /* Set when waiting for all processes to be halted */
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
*/
static void console_input_cb(uev_t *w, void *arg, int events)
{
ssize_t len;
char ch;
(void)arg;
if (UEV_ERROR == events) {
warnx("Error in console watcher");
return;
}
/* Read and check for Ctrl-C (ASCII 3) */
len = read(w->fd, &ch, 1);
if (len == 1 && ch == 3) { /* Ctrl-C */
sm.skip_bootstrap = 1;
print(2, "Bootstrap wait interrupted by Ctrl-C, resuming ...");
}
}
/*
* Stop and cleanup the console watcher
*/
static void console_watcher_stop(void)
{
if (console_fd >= 0) {
uev_io_stop(&console_watcher);
close(console_fd);
console_fd = -1;
}
}
/*
* Setup console watcher for Ctrl-C during bootstrap
*/
static void console_watcher_start(uev_ctx_t *ctx)
{
struct termios tio;
/* Open console in non-blocking mode */
console_fd = open(console(), O_RDONLY | O_NONBLOCK);
if (console_fd < 0) {
dbg("Failed opening console for input monitoring: %s", strerror(errno));
return;
}
/* Get current terminal settings */
if (tcgetattr(console_fd, &tio) == 0) {
/* Put terminal in raw mode to catch Ctrl-C */
tio.c_lflag &= ~(ICANON | ECHO | ISIG);
tio.c_cc[VMIN] = 0;
tio.c_cc[VTIME] = 0;
tcsetattr(console_fd, TCSANOW, &tio);
}
/* Register I/O watcher */
if (uev_io_init(ctx, &console_watcher, console_input_cb, NULL, console_fd, UEV_READ)) {
dbg("Failed setting up console watcher");
close(console_fd);
console_fd = -1;
} else {
dbg("Console watcher active, press Ctrl-C to skip bootstrap wait");
}
}
/*
* Wait for system bootstrap to complete, all SVC_TYPE_RUNTASK must be
@@ -76,13 +180,19 @@ static void sm_check_bootstrap(void *work)
service_step_all(SVC_TYPE_ANY);
bootstrap_done = service_completed(&svc);
if (timeout-- > 0 && !bootstrap_done) {
if (timeout-- > 0 && !bootstrap_done && !sm.skip_bootstrap) {
dbg("Not all bootstrap run/tasks have completed yet ... %d", timeout);
schedule_work(work);
return;
}
if (timeout > 0) {
/* Stop console watcher before proceeding */
console_watcher_stop();
if (sm.skip_bootstrap) {
logit(LOG_WARNING, "Ctrl-C received, skipped waiting for the following:");
service_log_incomplete();
} else if (timeout > 0) {
dbg("All run/task have completed, resuming bootstrap.");
} else {
dbg("Timeout, resuming bootstrap.");
@@ -111,6 +221,10 @@ static void sm_check_bootstrap(void *work)
dbg("Clean up all bootstrap-only tasks/services ...");
svc_prune_bootstrap();
/* Clean up empty cgroups from bootstrap tasks (e.g., udevadm, coldplug) */
dbg("Pruning empty cgroups from bootstrap ...");
cgroup_prune();
/* All services/tasks/etc. in configure runlevel have started */
dbg("Running svc up hooks ...");
plugin_run_hooks(HOOK_SVC_UP);
@@ -184,6 +298,7 @@ void sm_init(void)
sm.newlevel = -1;
sm.reload = 0;
sm.in_reload = 0;
sm.skip_bootstrap = 0;
dbg("Starting bootstrap finalize timer ...");
schedule_work(&work);
@@ -206,6 +321,7 @@ restart:
service_step_all(SVC_TYPE_RUNTASK | SVC_TYPE_SERVICE | SVC_TYPE_SYSV);
sm.state = SM_BOOTSTRAP_WAIT_STATE;
console_watcher_start(ctx);
break;
/*
@@ -229,6 +345,9 @@ restart:
if (sm.newlevel == -1)
break;
/* Ensure console watcher is stopped before proceeding */
console_watcher_stop();
/* Hooks that should run at the very end */
dbg("Calling all system up hooks ...");
plugin_run_hooks(HOOK_SYSTEM_UP);
@@ -259,6 +378,7 @@ restart:
/* reload ? */
if (sm.reload) {
shutdbg("RUNNING got reload request!");
sm.reload = 0;
sm.state = SM_RELOAD_CHANGE_STATE;
}
@@ -271,9 +391,15 @@ restart:
/* Restore terse mode and run hooks before shutdown */
if (runlevel == 0 || runlevel == 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));
@@ -293,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;
@@ -309,18 +437,26 @@ 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 */
/* Clean up empty cgroups from previous runlevel */
dbg("Pruning empty cgroups from previous runlevel ...");
cgroup_prune();
dbg("Starting services new to this runlevel ...");
sm.in_reload = 0;
service_step_all(SVC_TYPE_ANY);
shutdbg("entering CLEAN");
sm.state = SM_RUNLEVEL_CLEAN_STATE;
break;
@@ -332,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);
@@ -352,25 +492,37 @@ 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;
case SM_RELOAD_CHANGE_STATE:
sm.in_reload = 1;
/* 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
*/
dbg("Stopping services not allowed after reconf ...");
sm.in_reload = 1;
cond_reload();
service_step_all(SVC_TYPE_ANY);
/* Step the generation ... */
cond_reload();
sm.state = SM_RELOAD_WAIT_STATE;
break;
@@ -409,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);
+12 -5
View File
@@ -128,11 +128,18 @@ void stty(int fd, speed_t speed)
term.c_cc[VERASE] = CERASE;
tcsetattr(fd, TCSAFLUSH, &term);
/* Show cursor again, if it was hidden previously */
dprint(fd, "\033[?25h", 6);
/* Enable line wrap, if disabled previously */
dprint(fd, "\033[?7h", 5);
/* Reset terminal to sane state - disable modes that break scrollback */
dprint(fd, "\033[m", 3); /* Reset SGR attributes */
dprint(fd, "\033[?1000l", 8); /* Disable X11 mouse tracking */
dprint(fd, "\033[?1002l", 8); /* Disable button event tracking */
dprint(fd, "\033[?1003l", 8); /* Disable all mouse tracking */
dprint(fd, "\033[?1006l", 8); /* Disable SGR mouse mode */
dprint(fd, "\033[?1015l", 8); /* Disable urxvt mouse mode */
// dprint(fd, "\033[?1049l", 8); /* Exit alternate screen buffer */
dprint(fd, "\033[?1l", 6); /* Exit application cursor keys mode */
dprint(fd, "\033[?2004l", 8); /* Disable bracketed paste mode */
dprint(fd, "\033[?25h", 6); /* Show cursor */
dprint(fd, "\033[?7h", 5); /* Enable line wrap */
}
/**
+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;
+10 -1
View File
@@ -94,13 +94,14 @@ typedef enum {
SVC_NOTIFY_S6,
} svc_notify_t;
#define MAX_ID_LEN 16
#define MAX_ID_LEN 65
#define MAX_ARG_LEN 64
#define MAX_CMD_LEN 256
#define MAX_IDENT_LEN (MAX_ARG_LEN + MAX_ID_LEN + 1)
#define MAX_STR_LEN 64
#define MAX_COND_LEN (MAX_ARG_LEN * 3)
#define MAX_USER_LEN 16
#define MAX_NUM_SUPGROUPS 4
#define MAX_NUM_FDS 64 /* Max number of I/O plugins */
#define MAX_NUM_SVC_ARGS 64
@@ -138,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];
@@ -173,6 +176,7 @@ typedef struct svc {
char file[64];
char prio[20];
char ident[20];
char ctty[32];
} log;
/* Only for TTY type services */
@@ -191,6 +195,9 @@ typedef struct svc {
/* Identity */
char username[MAX_USER_LEN];
char group[MAX_USER_LEN];
char supgroups[MAX_NUM_SUPGROUPS][MAX_USER_LEN];
int num_supgroups;
char capabilities[MAX_CMD_LEN];
/* Command, arguments and service description */
char cmd[MAX_CMD_LEN];
@@ -278,12 +285,14 @@ 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); }
static inline int svc_has_pidfile (svc_t *svc) { return svc_is_daemon(svc) && svc->pidfile[0] != 0 && svc->pidfile[0] != '!'; }
static inline int svc_has_pre (svc_t *svc) { return svc->pre_script[0]; }
static inline int svc_has_post (svc_t *svc) { return svc->post_script[0]; }
static inline int svc_has_ctty (svc_t *svc) { return svc && svc->log.ctty[0]; }
static inline int svc_has_ready (svc_t *svc) { return svc->ready_script[0];}
static inline int svc_has_cleanup (svc_t *svc) { return svc->cleanup_script[0]; }
+300 -46
View File
@@ -34,6 +34,7 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
@@ -45,12 +46,33 @@
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)
{
struct dirent **namelist;
struct dirent **namelist = NULL;
int num;
num = scandir(path, &namelist, NULL, NULL);
@@ -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;
}
/**
+44 -7
View File
@@ -180,16 +180,34 @@ char *str(char *fmt, ...)
return buf;
}
int getuser(char *username, char **home)
/*
* Return UID for the given user, with fallback to 'root' if user is unset.
* Fails with -1 only if user (incl. fallback) does not exist.
*/
int getuser(const char *username, char **home)
{
#ifdef ENABLE_STATIC
if (!username || !username[0])
username = "root";
if (home)
*home = "/"; /* XXX: Fixme */
return fgetint("/etc/passwd", "x:\n", username);
#else
struct passwd *usr;
const struct passwd *usr;
if (!username || (usr = getpwnam(username)) == NULL)
if (!username || !username[0]) {
usr = getpwuid(0);
if (!usr)
return -1;
if (home)
*home = usr->pw_dir;
return usr->pw_uid;
}
usr = getpwnam(username);
if (!usr)
return -1;
if (home)
@@ -198,14 +216,30 @@ int getuser(char *username, char **home)
#endif
}
int getgroup(char *group)
/*
* Return GID for the given group, with fallback to 'root' if group is unset.
* Fails with -1 only if user (incl. fallback) does not exist.
*/
int getgroup(const char *group)
{
#ifdef ENABLE_STATIC
if (!group || !group[0])
group = "root";
return fgetint("/etc/group", "x:\n", group);
#else
struct group *grp;
const struct group *grp;
if ((grp = getgrnam(group)) == NULL)
if (!group || !group[0]) {
grp = getgrgid(0);
if (!grp)
return -1;
return grp->gr_gid;
}
grp = getgrnam(group);
if (!grp)
return -1;
return grp->gr_gid;
@@ -687,7 +721,7 @@ int fistmpfs(char *dir)
* Called by initctl, and by finit at boot and shutdown, to
* (re)initialize the screen size for print() et al.
*/
int ttinit(void)
int ttinit(int probe_size)
{
struct pollfd fd = { STDIN_FILENO, POLLIN, 0 };
struct winsize ws = { 0 };
@@ -707,6 +741,9 @@ int ttinit(void)
tcsetattr(STDERR_FILENO, TCSANOW, &tc);
}
if (probe_size)
goto fallback;
/* 1. Try TIOCWINSZ to query window size from kernel */
if (!ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws)) {
ttrows = ws.ws_row;
+4 -4
View File
@@ -62,8 +62,8 @@ const char *basenm (const char *path);
char *str (char *fmt, ...) __attribute__ ((format (printf, 1, 2)));
int getuser (char *username, char **home);
int getgroup (char *group);
int getuser (const char *username, char **home);
int getgroup (const char *group);
int getcuser (char *buf, size_t len);
int getcgroup (char *buf, size_t len);
@@ -95,11 +95,11 @@ int fismnt (char *dir);
int fistmpfs (char *dir);
#ifdef HAVE_TERMIOS_H
int ttinit (void);
int ttinit (int probe_size);
int ttraw (void);
int ttcooked (void);
#else
#define ttinit() ttcols
#define ttinit(v) ttcols
#define ttraw() 0
#define ttcooked() 0
#endif
+10 -10
View File
@@ -18,33 +18,33 @@
# change the contents any way you like, it can even be empty.
# Check for systemd-udevd and eudev, if we find both, we opt for the latter.
service nowarn cgroup.system name:udevd notify:none env:-/etc/default/udevd pid:udevd log \
service nowarn cgroup.system,name:udevd name:udevd notify:none env:-/etc/default/udevd pid:udevd log \
[S12345789] /lib/systemd/systemd-udevd $UDEVD_ARGS -- Device event daemon (udev)
service nowarn cgroup.system name:udevd notify:none env:-/etc/default/udevd pid:udevd log \
service nowarn cgroup.system,name:udevd name:udevd notify:none env:-/etc/default/udevd pid:udevd log \
[S12345789] udevd $UDEVD_ARGS -- Device event daemon (udev)
# Wait for udevd to start, then trigger coldplug events and module loading.
# The last 'settle' call waits for it to finalize processing all uevents.
run nowarn if:udevd cgroup.init :1 [S] <service/udevd/ready> log udevadm settle -t 0 --
run nowarn if:udevd cgroup.init :2 [S] <service/udevd/ready> log udevadm control --reload --
run nowarn if:udevd cgroup.init :3 [S] <service/udevd/ready> log udevadm trigger -c add -t devices -- Requesting device events
run nowarn if:udevd cgroup.init :4 [S] <service/udevd/ready> log udevadm trigger -c add -t subsystems -- Requesting subsystem events
run nowarn if:udevd cgroup.init :5 [S] <service/udevd/ready> log udevadm settle -t 30 -- Waiting for udev to finish
run nowarn if:udevd cgroup.init,name:udevadm :1 [S] <service/udevd/ready> log udevadm settle -t 0 --
run nowarn if:udevd cgroup.init,name:udevadm :2 [S] <service/udevd/ready> log udevadm control --reload --
run nowarn if:udevd cgroup.init,name:udevadm :3 [S] <service/udevd/ready> log udevadm trigger -c add -t devices -- Requesting device events
run nowarn if:udevd cgroup.init,name:udevadm :4 [S] <service/udevd/ready> log udevadm trigger -c add -t subsystems -- Requesting subsystem events
run nowarn if:udevd cgroup.init,name:udevadm :5 [S] <service/udevd/ready> log udevadm settle -t 30 -- Waiting for udev to finish
# If udevd is not available, check for mdevd. Unlike plain 'mdev -df', the
# mdevd-coldplug process exits when mdevd has completed. So the condition
# <run/coldplug/success> (same as for plain mdev below) can be relied on as a
# barrier for services that, e.g., need to probe for switchcore ports or PHYs
# that require firmware loading before they show up.
service nowarn conflict:udevd,mdev cgroup.init notify:none if:!udevd \
service nowarn conflict:udevd,mdev cgroup.init,name:mdevd notify:none if:!udevd \
[S12345789] notify:s6 mdevd -O 4 -D %n -- Device event daemon (mdevd)
run nowarn conflict:udevd,mdev cgroup.init name:coldplug <service/mdevd/ready> \
run nowarn conflict:udevd,mdev cgroup.init,name:coldplug <service/mdevd/ready> \
[S] mdevd-coldplug -- Cold plugging system
# If neither udevd or mdevd are available, fall back to mdev. The new daemon
# mode calls `mdev -s` internally but does not create a PID file when that is
# done, so synchronization with coldplug is fragile.
service nowarn conflict:udevd,mdevd cgroup.init notify:none if:!udevd,!mdevd \
service nowarn conflict:udevd,mdevd cgroup.init,name:mdev notify:none if:!udevd,!mdevd \
[S12345789] mdev -df -S -- Device event daemon (mdev)
# Trigger a replay of all uevents from boot, before userspace was up and the
+3
View File
@@ -10,6 +10,7 @@ EXTRA_DIST = skel/bin/busybox-x86_64.sha256 skel/sbin/service.sh skel/etc/env
skel/etc/rcS.d/S01abc.sh skel/etc/rcS.d/S02def.sh \
skel/cdrom/.empty skel/dev/shm/.empty skel/dev/pts/.empty \
skel/etc/inittab skel/etc/hostname skel/etc/fstab \
skel/etc/passwd skel/etc/group skel/etc/ld.so.conf \
skel/etc/init.d/rcS skel/etc/init.d/rcK skel/tmp/.empty \
skel/etc/finit.d/.empty skel/etc/finit.d/available/.empty \
skel/etc/finit.d/enabled/.empty \
@@ -31,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
@@ -73,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
+2 -1
View File
@@ -25,7 +25,8 @@ if [ "$run_make" -eq 1 ]; then
fi
./configure --prefix=/usr --exec-prefix= --sysconfdir=/etc --localstatedir=/var \
--enable-x11-common-plugin --with-watchdog --with-keventd \
--enable-x11-common-plugin --enable-testserv-plugin --with-watchdog \
--with-keventd --with-libsystemd \
CFLAGS='-fsanitize=address -ggdb'
if [ "$run_make" -eq 1 ]; then
+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"
+18
View File
@@ -2,6 +2,14 @@
set -eu
echo "=== Finit Test Sysroot Setup ==="
echo "Date: $(date)"
echo "SYSROOT: $SYSROOT"
echo "top_builddir: $top_builddir"
echo "srcdir: $srcdir"
echo "================================"
echo
# shellcheck disable=SC2154
make -C "$top_builddir" DESTDIR="$SYSROOT" install
@@ -21,3 +29,13 @@ done
for conf in 10-hotplug.conf; do
find "$SYSROOT" -name $conf -delete
done
# Update dynamic linker cache for /usr/local/lib libraries
echo "Running ldconfig in sysroot: $SYSROOT"
echo "Contents of $SYSROOT/etc/ld.so.conf:"
cat "$SYSROOT/etc/ld.so.conf" || echo "Warning: ld.so.conf not found"
echo "Libraries in $SYSROOT/usr/local/lib:"
ls -la "$SYSROOT/usr/local/lib/" 2>/dev/null || echo "Warning: /usr/local/lib not found in sysroot"
ldconfig -v -r "$SYSROOT" || echo "Warning: ldconfig failed with exit code $?"
echo "Verifying ldconfig cache was created:"
ls -la "$SYSROOT/etc/ld.so.cache" || echo "Warning: ld.so.cache not created"
+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
+9
View File
@@ -0,0 +1,9 @@
root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:
tty:x:5:
disk:x:6:
dialout:x:20:
nogroup:x:65534:
+3
View File
@@ -0,0 +1,3 @@
/usr/local/lib
/usr/lib
/lib
+5
View File
@@ -0,0 +1,5 @@
root:x:0:0:root:/root:/bin/sh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
+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;
+1 -1
View File
@@ -8,7 +8,7 @@ d /var/lib/alarm 0755 - - -
d /var/lib/misc 0755 - - -
d /var/lib/urandom 0755 - - -
d /var/spool 0755 - - -
d /var/tmp 0777 - - -
d /var/tmp 1777 - - -
# Used by pam_console(8)
d /run/console 1777 - - -