Compare commits

...
36 Commits
Author SHA1 Message Date
Anees Rehman 70a44d3860 mount: report unmount failures
Return a failure status from unmount_regular() when filesystems remain
mounted, and use it to report the final shutdown unmount result.

Signed-off-by: Anees Rehman <anees.rehman@atlascopco.com>
2026-10-12 09:39:03 +02:00
Joachim Wiberg f29a3db86c doc: ChangeLog entry for the lost reload fix
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-09-27 18:56:00 +02:00
Joachim Wiberg f6b394e0a2 service: keep a pending reload across a second conf reload
A service whose condition goes into flux during a reload is paused
with its reload still pending.  If another reload was requested in
the meantime, re-parsing its unchanged .conf file cleared the pending
mark, so the service was resumed without ever being reloaded.  Seen
with sshd <pid/syslogd> on Infix, where a configuration change that
touched both landed as two reloads in a row and sshd kept its old
listen addresses.

The mark is only ever cleared once the change has been applied, so a
mark that is still set when the file is parsed again means exactly
that: not applied yet.  Leave it alone.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-09-27 18:40:44 +02:00
Joachim Wiberg ccaa4caf48 Bump version and update ChangeLog for 4.18
Covers the longer user and group names, the tmpfiles mode and owner
work, the shutdown remount order, and the script timeout crash.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-09-27 18:03:24 +02:00
Anees Rehman 77d199f1a4 sig: avoid reading fstab during shutdown
Only fall back to the regular mount invocation when both
fstab-independent root remount attempts fail.
This avoids reading an encrypted fstab during a normal shutdown.

Signed-off-by: Anees Rehman <anees.rehman@atlascopco.com>
(cherry picked from commit 263ea929fd)
2026-09-27 17:59:15 +02:00
Anees Rehman 0448151c0a svc: allow longer user and group names
Service user and group names are stored in 16-byte buffers, limiting
them to 15 characters. This silently truncates longer names before
getpwnam() and getgrnam(), preventing services configured with valid
longer account names from starting.

Increase the buffers to match the existing maximum configuration
argument length.

Signed-off-by: Anees Rehman <anees.rehman@atlascopco.com>
2026-09-27 17:43:11 +02:00
Joachim Wiberg 75739fbeef tmpfiles: apply mode and owner of e and f/F entries through an fd
Same treatment as d/D got: e went through fisdir() and a path based
chmod/chown, f/F did the chmod/chown by path after closing the file.
Both now work on the open fd, so the mode and owner end up on the
directory or file that was just checked or written.  f/F use open(2)
directly, which lets a plain f rely on O_EXCL for create-if-missing
instead of the earlier stat().

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-09-27 17:29:51 +02:00
Sam Brkopac 3a72d1263c tmpfiles: set mode and owner of d/D directories through an fd
makedir() swallows EEXIST, so the chmod branch for directories that
already existed never ran, while chown() ran every time.  On finix
/var/empty is immutable, which gave a warning on every boot even
though the owner was already correct.

mksubsys() is the original of this code and has the same shape, the
dbus plugin uses it for /tmp/dbus.  Both now go through dirperm(),
which opens the directory and uses fstat/fchmod/fchown on the fd,
changing only what differs.  A failed chown in mksubsys() is a warning
now rather than err(1), PID 1 should not exit over a directory it
cannot adjust.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-09-27 17:25:50 +02:00
Joachim Wiberg 7e2b154f9b .github: update actions to Node.js 24
All workflow runs now warn:

    Node.js 20 is deprecated. The following actions target Node.js 20
    but are being forced to run on Node.js 24.

Update the actions/* dependencies to their current major versions,
all of which target Node.js 24: checkout v7, upload-artifact v7,
download-artifact v8, setup-python v7, and cache v6.  The release
action tracks the floating v1 tag and updates on its own.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 15:09:36 +02:00
Joachim Wiberg 547e59b2ad .github: allow building and releasing from maintenance branches
With 5.0 on master, maintenance releases move to a 4.x branch: run
push builds, docs deploy, and weekly distcheck for any N.x branch,
and mark only the highest stable tag as the latest release.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 14:45:11 +02:00
Joachim Wiberg 7f37d0c4e9 test: refuse to run against a stale sysroot binary
'make check' refreshes the sysroot through the setup-chroot rule, but
running a test script by hand does not, so the test exercises whichever
finit was installed last and reports on code that is no longer there.
Both a passing and a failing run are then meaningless, and nothing says
so.

Compare the built binary against the installed one at startup and fail
with the command that fixes it.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 14:17:04 +02:00
Joachim Wiberg dd77f63ad3 service: do not let a script timeout take PID 1 with it
A stop: or reload: script written with a timeout killed Finit at
config load:

    service stop:5,/bin/true service.sh -- Boom

parse_script() takes the timeout as a pointer and the caller decides
whether it wants one.  However, both stop: and reload: scripts so far
have no timeout, i.e., NULL.  Guard the branch that reads a leading
number.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 14:16:50 +02:00
Joachim Wiberg e0f9df1f27 test: reap the watchdog's sleep in wdkill()
Every test left a stray `sleep 300` behind, reparented to PID 1, where
it lingered for up to five minutes after the test had finished.

wdstart() runs the watchdog in a subshell, so $! is the pid of the
subshell, not of the sleep it forks.  wdkill() killed the subshell and
orphaned the sleep.

Kill the child first, killing the subshell puts the sleep beyond the
reach of pkill -P.  Neither kill is sure to match, and wdkill() runs
from the EXIT trap under set -e, so both must tolerate failure.  Also
return early when wdpid is unset, for failures before wdstart() runs.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 14:15:22 +02:00
Joachim Wiberg b56ffc155e doc: version the published User Guide by major release
Finit 5.0 changes the .conf syntax, which has been essentially
unchanged since 1.x.  The published docs track master, so when 5.x
lands, 4.x users lose their reference.

Publish the site under a per-major directory, /4.x/ for now, with
the Material version selector to switch between them.  The selector
only needs mike's file layout -- a versions.json at the site root --
which the deploy job now generates from the version directories in
the pages repo, so mike itself is not needed.

The major comes from AC_INIT and the future 4.x maintenance branch
is already in the workflow triggers, so once 5.0 is on master, doc
fixes on the 4.x branch keep /4.x/ updated.  A root index.html
redirects to the newest version, and a 404.html rewrites
pre-versioned deep links so old bookmarks and search hits land in
the right place.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 12:27:27 +02:00
Joachim Wiberg 4596e27eb0 doc: simplify and add some polish to the User Guide
Both the Finit project and Infix use the same MkDocs Material setup, and
in the latter the User Guide has picked up a lot of polish that never
made it back here: a single sidebar with section indexes instead of
tabs, footnote tooltips, more pymdownx markup, image zoom tuning, and no
generator advert in the footer.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-07-30 12:22:17 +02:00
Joachim Wiberg a3eac3a85e Merge pull request #489 from finit-project/stale-pidfile
Handle stale pid file for crashing daemons
2026-05-12 10:40:01 +02:00
Joachim Wiberg 7d09e34b80 doc: document stale pidfile cleanup and new restart log
* src/pid.c: note the stale-pidfile-cleanup exception to the
  documented "Finit does not touch pid:! pidfiles" rule.
* doc/config/services.md: add a user-facing paragraph on the same.
* doc/ChangeLog.md: add Unreleased section covering this PR --
  stale pidfile cleanup, restart log with signal name and core
  dump flag, and the SIGUNKOWN typo fix.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-05-12 10:11:50 +02:00
Joachim Wiberg 3febc7d513 test: regression for stale pidfile cleanup after unclean exit
Cover the scenario fixed in "service: clean stale pidfile after
unclean daemon exit": a daemon with a pid:!/path config dies via
SIGKILL, leaving its pidfile behind, and the next instance must
still come up.

Add a 'serv -x' flag (refuse to start when the pidfile already
exists, dbus-style) so the test actually exercises the cleanup --
without it, plain 'serv' would happily overwrite the file and the
test would pass with or without the fix.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-05-12 10:11:49 +02:00
Joachim Wiberg 309ad1bea7 sig: spell SIGUNKNOWN correctly
The fallback for unknown signal numbers in sig_name() returned the
misspelled "SIGUNKOWN".  Now that this string surfaces in user-
facing logs ("killed by …"), fix the typo.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-05-11 21:09:02 +02:00
Joachim Wiberg 30f2ca3b2e service: log signal name and core dumps in death message
Replace the bare signal number ("by signal: 9") with the symbolic
name ("killed by SIGKILL") and annotate when the kernel wrote a
core:("killed by SIGSEGV, core dumped").  Makes the restart line
self-explanatory and gives operators a strong breadcrumb when a
daemon dies unexpectedly.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-05-11 21:09:01 +02:00
Joachim Wiberg 4a53f610cd service: clean stale pidfile after unclean daemon exit
With `pid:!/path` Finit does not manage the file -- the daemon
creates it on start and removes it on graceful exit.  If the daemon
dies before cleanup (SIGKILL, OOM, segfault, exit during startup)
the file lingers and can block the next instance from starting,
e.g. dbus-daemon refuses with EEXIST and the restart loop fails.

Remove the file when it still names the just-reaped PID and that
PID is no longer alive (the liveness check guards against reuse).
Called from service_cleanup(), and from service_monitor()'s
forking+starting branch where cleanup was previously skipped.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-05-11 21:08:51 +02:00
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
34 changed files with 1169 additions and 173 deletions
+7 -8
View File
@@ -1,12 +1,13 @@
name: Bob the Builder
# Run on all branches, including all pull requests, except the 'dev'
# branch since that's where we run Coverity Scan (limited tokens/day)
# Run on push to release branches (master and maintenance N.x), and
# on all pull requests. The 'dev' branch is where we run Coverity
# Scan (limited tokens/day)
on:
push:
branches:
- '**'
- '!dev'
- master
- '[0-9]+.x'
pull_request:
types: [opened, synchronize, reopened, labeled]
@@ -22,8 +23,6 @@ jobs:
# Verify we can build on latest Ubuntu with both gcc and clang
name: ${{ matrix.compiler }}
runs-on: ubuntu-latest
# Skip redundant builds for PRs - prefer PR builds over push builds
if: github.event_name != 'push' || github.ref == 'refs/heads/master'
strategy:
matrix:
compiler: [gcc, clang]
@@ -43,7 +42,7 @@ jobs:
(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
- uses: actions/checkout@v7
- name: Static Finit
run: |
./autogen.sh
@@ -81,7 +80,7 @@ jobs:
make -j1 check || (cat test/test-suite.log; false)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: finit-test-${{ matrix.compiler }}
path: test/*.log
+3 -3
View File
@@ -19,7 +19,7 @@ jobs:
coverity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Fetch latest Coverity Scan MD5
id: var
env:
@@ -29,7 +29,7 @@ jobs:
--post-data "token=$TOKEN&project=${COVERITY_PROJ}&md5=1" \
-O coverity-latest.tar.gz.md5
echo "md5=$(cat coverity-latest.tar.gz.md5)" | tee -a $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v6
id: cache
with:
path: coverity-latest.tar.gz
@@ -86,7 +86,7 @@ jobs:
--form description="${PROJECT_NAME} $(git rev-parse HEAD)" \
https://scan.coverity.com/builds?project=${COVERITY_PROJ}
- name: Upload build.log
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: coverity-build.log
path: cov-int/build-log.txt
+70 -7
View File
@@ -4,10 +4,12 @@ on:
push:
branches:
- master
- '[0-9]+.x'
paths:
- 'doc/**'
- 'README.md'
- 'mkdocs.yml'
- 'configure.ac'
- '.github/workflows/docs.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
@@ -15,6 +17,7 @@ on:
- 'doc/**'
- 'README.md'
- 'mkdocs.yml'
- 'configure.ac'
- '.github/workflows/docs.yml'
permissions:
@@ -23,14 +26,23 @@ permissions:
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0 # Needed for git-revision-date-localized plugin
- name: Determine docs version
id: version
run: |
version=$(sed -n 's/^AC_INIT(\[Finit\], *\[\([0-9]*\)\..*/\1.x/p' configure.ac)
test -n "$version"
echo "version=$version" >> $GITHUB_OUTPUT
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v7
with:
python-version: '3.x'
@@ -46,25 +58,27 @@ jobs:
run: mkdocs build --clean
- name: Upload site artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: site
path: site/
deploy:
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
if: github.event_name == 'push'
needs: build
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.build.outputs.version }}
steps:
- name: Download site artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: site
path: site/
- name: Checkout pages repo
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
repository: finit-project/finit-project.github.io
path: pages
@@ -79,7 +93,56 @@ jobs:
- name: Sync site to pages repo
run: |
rsync -a --delete --exclude .git site/ pages/
rsync -a --delete site/ "pages/$VERSION/"
- name: Update version index and redirects
run: |
cd pages/
# The root holds only version directories and the files
# generated below, drop anything else (pre-versioned site)
find . -maxdepth 1 ! -name . ! -name .git ! -name CNAME \
! -name '[0-9]*.x' -exec rm -rf {} +
latest=$(ls -d [0-9]*.x | sort -rV | head -1)
sep=
printf '[' > versions.json
for v in $(ls -d [0-9]*.x | sort -rV); do
printf '%s{"version": "%s", "title": "%s", "aliases": []}' \
"$sep" "$v" "$v" >> versions.json
sep=', '
done
printf ']\n' >> versions.json
cat > index.html <<EOF
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=$latest/">
<link rel="canonical" href="https://finit-project.github.io/$latest/">
</head>
</html>
EOF
cat > 404.html <<EOF
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Finit &mdash; page not found</title>
<script>
var path = window.location.pathname;
if (!/^\/[0-9]+\.x(\/|$)/.test(path))
window.location.replace("/$latest" + path);
</script>
</head>
<body>
<p>Page not found. Try the <a href="/$latest/">latest User Guide</a>.</p>
</body>
</html>
EOF
- name: Commit and push
run: |
+9 -4
View File
@@ -14,7 +14,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Installing dependencies ...
run: |
sudo apt-get -y update
@@ -31,12 +31,17 @@ jobs:
run: |
ver=${GITHUB_REF#refs/tags/}
echo "ver=${ver}" >> $GITHUB_OUTPUT
if echo $ver | grep -qE '^[0-9]+\.[0-9]+(\.[0-9]+)?(-alpha|-beta|-rc)[0-9]*$'; then
stable='[0-9]+\.[0-9]+(\.[0-9]+)?'
if echo $ver | grep -qE "^${stable}(-alpha|-beta|-rc)[0-9]*$"; then
echo "pre=true" >> $GITHUB_OUTPUT
else
echo "pre=false" >> $GITHUB_OUTPUT
fi
if echo $ver | grep -qE '^[0-9.]+\.[0-9.]+(\.[0-9]+)?$'; then
# Mark as latest release only if this is the highest stable
# tag, so a 4.x maintenance release doesn't outrank 5.x
newest=$(git ls-remote --tags --refs origin | awk -F/ '{print $NF}' \
| grep -E "^${stable}$" | sort -V | tail -1)
if [ "$ver" = "$newest" ]; then
echo "latest=true" >> $GITHUB_OUTPUT
else
echo "latest=false" >> $GITHUB_OUTPUT
@@ -64,7 +69,7 @@ jobs:
mv ../*.tar.* artifacts/
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: finit-release-test-logs
path: finit-*/_build/sub/test/*.log
+24 -13
View File
@@ -14,22 +14,31 @@ jobs:
schedule:
runs-on: ubuntu-latest
outputs:
run: ${{ steps.schedule.outputs.run }}
branches: ${{ steps.schedule.outputs.branches }}
steps:
- uses: actions/checkout@v4
- name: Check for any commits since last week
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Check for branches with no commits since last week
id: schedule
run: |
commits="$(git log --format=%H --since='1 week ago')"
if [ -z "$commits" ]; then
echo "run=true" >> $GITHUB_OUTPUT
else
echo "run=false" >> $GITHUB_OUTPUT
fi
branches=
maint=$(git branch -r --format='%(refname:lstrip=3)' | grep -E '^[0-9]+\.x$')
for branch in master $maint; do
commits="$(git log --format=%H --since='1 week ago' origin/$branch)"
if [ -z "$commits" ]; then
branches="$branches\"$branch\","
fi
done
echo "branches=[${branches%,}]" >> $GITHUB_OUTPUT
distcheck:
needs: schedule
if: ${{ needs.schedule.outputs.run == 'true' }}
if: ${{ needs.schedule.outputs.branches != '[]' }}
runs-on: ubuntu-latest
strategy:
matrix:
branch: ${{ fromJSON(needs.schedule.outputs.branches) }}
fail-fast: false
steps:
- name: Install dependencies
run: |
@@ -42,7 +51,9 @@ jobs:
(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
- uses: actions/checkout@v7
with:
ref: ${{ matrix.branch }}
- name: Base run
run: |
./autogen.sh
@@ -61,7 +72,7 @@ jobs:
make -j1 distcheck || (cat $dir/test/test-suite.log; false)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: finit-weekly-test-logs
name: finit-weekly-test-logs-${{ matrix.branch }}
path: finit-*/_build/sub/test/*.log
+2 -1
View File
@@ -1,4 +1,4 @@
AC_INIT([Finit], [4.16], [https://github.com/finit-project/finit/issues],
AC_INIT([Finit], [4.18-pre], [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])
@@ -116,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
+83 -1
View File
@@ -3,6 +3,87 @@ Change Log
All relevant changes are documented in this file.
[Unreleased]
------------
### Changes
- Restart log now spells out the signal name and flags core dumps,
e.g. `killed by SIGKILL` or `killed by SIGSEGV, core dumped`, in
place of the bare numeric `by signal: N`. Gives operators a much
stronger breadcrumb when a daemon dies unexpectedly
- Service `user:` and `group:` names, including supplementary groups,
can now be up to 63 characters, up from 15. Longer names were
silently truncated before the lookup, so the service never started.
By Anees Rehman
- Shutdown now stops after the first successful remount of `/` read
only. The plain `mount -n -o remount,ro /`, which reads
`/etc/fstab`, is only used when both device-agnostic forms fail,
the same order as Debian's `umountroot`. By Anees Rehman
### Fixes
- Remove stale daemon-owned (`pid:!`) pidfiles after unclean exits.
When a daemon dies via SIGKILL/OOM/segfault, or exits early during
startup, its pidfile lingers and can prevent the next instance from
starting -- `dbus-daemon` for example refuses to start when its
pidfile already exists, so Finit's restart loop gives up. Finit now
drops the file when it still names the just-reaped PID and that PID
is no longer alive (the liveness check guards against PID reuse).
This is a controlled exception to the long-standing rule that Finit
does not touch service-owned pidfiles
- Fix misspelled `SIGUNKOWN` returned by `sig_name()` for unknown
signal numbers, now spelled correctly as `SIGUNKNOWN`. Surfaced by
the new restart log above
- Fix mode of existing `d`/`D` directories in tmpfiles, the chmod branch
never ran because the directory creation swallowed `EEXIST`. Mode and
owner of `d`/`D`, `e`, and `f`/`F` entries, and of the directories
`mksubsys()` creates for the dbus plugin, are now set through an open fd
and only when they differ, so an immutable directory like `/var/empty`
no longer warns on every boot. By Sam Brkopac
- Fix crash at config load for a `stop:` or `reload:` script written
with a timeout, e.g. `stop:5,/bin/true`, which took PID 1 down
- Fix a lost reload for a service paused mid-reload by a condition in
flux. A second reload arriving meanwhile re-parsed the unchanged .conf
and cleared the pending mark, so the service resumed without ever being
reloaded. Seen on Infix, where sshd kept its old listen addresses after
a change that landed as two reloads in a row
[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
---------------------
@@ -1968,7 +2049,8 @@ Major bug fix release.
* Initial release
[UNRELEASED]: https://github.com/finit-project/finit/compare/4.15...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
+1
View File
@@ -46,6 +46,7 @@ Other run/task/service options are:
* `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)
+8
View File
@@ -31,6 +31,14 @@ basename of the binary to guess the PID file to watch for the PID:
`/var/run/ntpd.pid`. If Finit guesses wrong, you have to submit the
full `pid:!/path/to/file.pid`.
With `pid:!/path`, the file belongs to the service: Finit reads it
but does not create or remove it. The one exception is *stale*
cleanup — if the service dies without removing its own pidfile
(SIGKILL, OOM, segfault), and the file still names the just-reaped
PID, Finit removes it before the next retry. This prevents daemons
that refuse to start on an existing pidfile (e.g. `dbus-daemon`)
from getting stuck in a crash-restart loop.
**Example:**
In the case of `ospfd` (below), we omit the `-d` flag (daemonize) to
+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
+2 -1
View File
@@ -61,7 +61,8 @@ For more complex setups (LUKS, LVM, etc.):
```
# Unlock LUKS volume
run [S] name:cryptsetup /sbin/cryptsetup open /dev/sda2 cryptroot -- Unlocking encrypted root
# 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
+31
View File
@@ -744,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
+71 -40
View File
@@ -1,9 +1,9 @@
site_name: ""
site_name: User's Guide
site_description: Fast Init for Linux Systems
site_url: http://finit-project.github.io
site_url: https://finit-project.github.io
repo_url: https://github.com/finit-project/finit
repo_name: Finit Project
copyright: Copyright &copy; 2008-2025 Joachim Wiberg
copyright: Copyright &copy; 2008-2026 Joachim Wiberg
docs_dir: doc/
edit_uri: edit/master/doc/
extra_css:
@@ -13,42 +13,6 @@ exclude_docs: |
TODO.md
ChangeLog.md
theme:
logo: img/logo.png
name: material
features:
- toc.follow
- toc.integrate
- navigation.path
- navigation.instant
- navigation.instant.progress
- navigation.tracking
- navigation.tabs
# - navigation.sections
# - navigation.expand
# - navigation.top
- navigation.footer
- search.highlight
- search.share
# - content.action.edit
- content.code.copy
- content.code.annotate
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: orange
accent: orange
toggle:
icon: material/weather-night
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
accent: black
toggle:
icon: material/weather-sunny
name: Switch to light mode
nav:
- Getting Started:
- Introduction: index.md
@@ -94,14 +58,62 @@ nav:
- Distributions: distro.md
- Requirements: requirements.md
theme:
logo: img/logo.png
name: material
features:
- toc.follow
# - toc.integrate
- navigation.path
- navigation.instant
- navigation.instant.progress
- navigation.tracking
- navigation.indexes
# - navigation.tabs
# - navigation.sections
# - navigation.expand
# - navigation.top
# - navigation.footer
- search.highlight
- search.share
# - content.action.edit
- content.code.copy
- content.code.annotate
- content.footnote.tooltips
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: orange
accent: orange
toggle:
icon: material/weather-night
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
accent: black
toggle:
icon: material/weather-sunny
name: Switch to light mode
markdown_extensions:
- admonition
- attr_list
- footnotes
- pymdownx.blocks.caption
- pymdownx.critic
- pymdownx.caret
- pymdownx.keys
- pymdownx.mark
- pymdownx.tilde
- pymdownx.details
- pymdownx.superfences
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.snippets
- pymdownx.tabbed:
alternate_style: true
@@ -112,10 +124,29 @@ markdown_extensions:
plugins:
- search
- callouts
- glightbox
- glightbox:
touchNavigation: true
loop: false
effect: zoom
slide_effect: slide
width: 100%
height: auto
zoomable: true
draggable: true
skip_classes:
- custom-skip-class-name
auto_themed: true
auto_caption: false
caption_position: bottom
background: black
shadow: false
manual: false
extra:
generator: false
homepage: https://finit-project.github.io/
version:
provider: mike
social:
- icon: fontawesome/brands/github
link: https://github.com/finit-project/finit
+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
+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:
*/
+11 -3
View File
@@ -370,8 +370,8 @@ 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 gid;
@@ -383,7 +383,8 @@ static void fs_finalize(void)
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 */
@@ -529,6 +530,13 @@ static void fs_init(void)
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");
}
}
+67 -6
View File
@@ -50,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"
@@ -336,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
@@ -344,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)
@@ -375,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;
+1
View File
@@ -68,6 +68,7 @@ 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);
+22 -10
View File
@@ -73,7 +73,8 @@ static int delete_cb(const char *fpath, const struct stat *sb, int tflag, struct
if (sb->st_dev != delete_rootdev)
return 0;
remove(fpath);
if (remove(fpath))
dbg("Failed to remove %s: %s", fpath, strerror(errno));
return 0;
}
@@ -146,6 +147,7 @@ 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;
@@ -166,17 +168,31 @@ int switch_root(const char *newroot, const char *newinit)
}
/* Verify newroot exists and is a directory */
if (stat(newroot, &newroot_st) || !S_ISDIR(newroot_st.st_mode)) {
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) */
if (stat("/", &oldroot_st)) {
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);
@@ -259,18 +275,14 @@ int switch_root(const char *newroot, const char *newinit)
return -1;
}
/* Reopen console */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
/* 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);
if (console_fd > STDERR_FILENO)
close(console_fd);
close(console_fd);
}
/* Reset signals to default */
+6 -2
View File
@@ -121,15 +121,19 @@ void unmount_tmpfs(void)
}
}
void unmount_regular(void)
int unmount_regular(void)
{
const struct mntent *mnt;
FILE *fp = NULL;
int rc = 0;
while ((mnt = iterator("/proc/mounts", &fp))) {
if (!unmount(mnt->mnt_dir))
rc = unmount(mnt->mnt_dir);
if (!rc)
iterator_end(&fp); /* Restart iteration */
}
return !!rc;
}
/**
+4 -1
View File
@@ -201,7 +201,10 @@ int pid_file_set(svc_t *svc, char *file, int not)
* pid:!foo --> !/run/foo.pid
* pid:!/run/foo.pid --> !/run/foo.pid
*
* Note, nothing is created or removed by Finit in this latter form.
* Nothing is created or removed by Finit in this latter form, with one
* exception: a verifiably stale pidfile (still names the just-reaped
* PID, and that PID is no longer alive) is removed so the next instance
* can start. See service_clean_pidfile() in src/service.c.
*/
int pid_file_parse(svc_t *svc, char *arg)
{
+121 -12
View File
@@ -29,10 +29,12 @@
#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>
@@ -387,6 +389,43 @@ static int lredirect(svc_t *svc)
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);
}
/*
* Handle redirection of process output, if enabled
*/
@@ -946,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))
@@ -1073,6 +1120,35 @@ static void service_notify_stop(svc_t *svc)
}
}
/*
* Drop a daemon-owned (pid:!) pidfile if it still names the just-reaped
* PID and that PID is gone. The liveness check guards against reuse.
*/
static void service_clean_pidfile(svc_t *svc, pid_t reaped)
{
pid_t pid;
char *fn;
if (reaped <= 1)
return;
fn = pid_file(svc);
if (!fn)
return;
pid = pid_file_read(fn);
if (pid != reaped || pid_alive(pid))
return;
if (remove(fn) && errno != ENOENT) {
logit(LOG_CRIT, "Failed removing stale service %s pidfile %s",
svc_ident(svc, NULL, 0), fn);
return;
}
dbg("Removed stale service %s pidfile %s", svc_ident(svc, NULL, 0), fn);
}
/*
* Clean up any lingering state from dead/killed services
*/
@@ -1090,6 +1166,8 @@ static void service_cleanup(svc_t *svc)
if (remove(fn) && errno != ENOENT)
logit(LOG_CRIT, "Failed removing service %s pidfile %s",
svc_ident(svc, NULL, 0), fn);
} else if (svc->pidfile[0] == '!') {
service_clean_pidfile(svc, svc->pid);
}
/*
@@ -1576,7 +1654,8 @@ static void parse_script(svc_t *svc, char *type, char *script, int *tmo, char *b
script, errstr);
goto err;
}
*tmo = (int)(sec * 1000);
if (tmo)
*tmo = (int)(sec * 1000);
} else {
path = script;
if (tmo)
@@ -1795,6 +1874,7 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
char ident[MAX_IDENT_LEN];
char *ifstmt = NULL;
char *notify = NULL;
char *ctty = NULL;
struct tty tty = { 0 };
char *dev = NULL;
int respawn = 0;
@@ -1911,6 +1991,8 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
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;
@@ -2160,6 +2242,22 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
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
@@ -2239,11 +2337,14 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
if (cgroup)
parse_cgroup(svc, cgroup);
/* New, recently modified or unchanged ... used on reload. */
/*
* New or modified since the last reload. The mark is cleared when
* the change has been applied, on start or reload, so one that is
* still set here is a change that has not been applied yet, e.g.
* the service is paused waiting for a condition. Leave it.
*/
if ((file && conf_changed(file)) || conf_changed(svc_getenv(svc)) || svc->args_dirty)
svc_mark_dirty(svc);
else
svc_mark_clean(svc);
svc_enable(svc);
@@ -2339,7 +2440,10 @@ void service_monitor(pid_t lost, int status)
if (svc_is_forking(svc)) {
/* Likely start script exiting */
if (svc_is_starting(svc)) {
svc->pid = 0; /* Expect no more activity from this one */
/* Daemon died before clearing 'starting'; drop any stale pidfile. */
service_clean_pidfile(svc, lost);
svc->oldpid = lost; /* So service_retry() logs the real PID */
svc->pid = 0; /* Expect no more activity from this one */
goto cont;
}
@@ -2728,13 +2832,18 @@ 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 (%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);
if (WIFEXITED(svc->status))
logit(LOG_CONSOLE|LOG_WARNING,
"Service %s[%d] died (exit status: %d), restarting (retry in %d msec) (attempt: %d/%d)",
svc_ident(svc, NULL, 0), svc->oldpid, WEXITSTATUS(svc->status),
svc->restart_tmo, *restart_cnt, svc->restart_max);
else
logit(LOG_CONSOLE|LOG_WARNING,
"Service %s[%d] died (killed by %s%s), restarting (retry in %d msec) (attempt: %d/%d)",
svc_ident(svc, NULL, 0), svc->oldpid,
sig_name(WTERMSIG(svc->status)),
WCOREDUMP(svc->status) ? ", core dumped" : "",
svc->restart_tmo, *restart_cnt, svc->restart_max);
svc_unblock(svc);
service_step(svc);
+12 -7
View File
@@ -170,7 +170,7 @@ static struct sigmap {
void mdadm_wait(void);
void unmount_tmpfs(void);
void unmount_regular(void);
int unmount_regular(void);
static void fs_swapoff(void)
{
@@ -302,6 +302,7 @@ void do_shutdown(shutop_t op)
struct sched_param sched_param = { .sched_priority = 99 };
int in_cont = in_container();
int signo = SIGTERM;
int rc;
if (SHUTDOWN_DEBUG) cprintf("do_shutdown: entered, op=%d\n", op);
@@ -372,12 +373,13 @@ void do_shutdown(shutop_t op)
}
/* Unmount any tmpfs before unmounting swap ... */
print(0, "Unmounting filesystems ...");
print(-1, "Unmounting filesystems ...");
unmount_tmpfs();
fs_swapoff();
/* ... unmount remaining regular file systems. */
unmount_regular();
rc = unmount_regular();
print(rc, NULL);
/*
* We sit on / so we must remount it ro, try all the things!
@@ -386,9 +388,9 @@ void do_shutdown(shutop_t op)
* - https://bugs.launchpad.net/ubuntu/+source/util-linux/+bug/29187
*/
sync();
run("mount -n -o remount,ro -t dummytype dummydev /", NULL);
run("mount -n -o remount,ro dummydev /", NULL);
run("mount -n -o remount,ro /", "mount");
if (run("mount -n -o remount,ro -t dummytype dummydev /", NULL) &&
run("mount -n -o remount,ro dummydev /", NULL))
run("mount -n -o remount,ro /", "mount");
/* Call mdadm to mark any RAID array(s) as clean before halting. */
mdadm_wait();
@@ -424,14 +426,17 @@ void do_shutdown(shutop_t op)
}
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);
}
@@ -627,7 +632,7 @@ const char *sig_name(int signo)
return signames[i].name;
}
return "SIGUNKOWN";
return "SIGUNKNOWN";
}
/*
+3 -1
View File
@@ -100,7 +100,7 @@ typedef enum {
#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_USER_LEN MAX_ARG_LEN
#define MAX_NUM_SUPGROUPS 4
#define MAX_NUM_FDS 64 /* Max number of I/O plugins */
#define MAX_NUM_SVC_ARGS 64
@@ -176,6 +176,7 @@ typedef struct svc {
char file[64];
char prio[20];
char ident[20];
char ctty[32];
} log;
/* Only for TTY type services */
@@ -291,6 +292,7 @@ static inline int svc_in_runlevel (svc_t *svc, int runlevel) { return svc && IS
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]; }
+50 -42
View File
@@ -24,6 +24,7 @@
#include "config.h" /* Generated by configure script */
#include <dirent.h>
#include <fcntl.h>
#include <ftw.h>
#include <getopt.h>
#include <glob.h>
@@ -586,67 +587,74 @@ static void tmpfiles(char *line)
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);
if (!mode)
mode = 0755;
rc = makedir(path, mode);
if (!rc && dirperm(path, mode, uid, gid))
warn("Failed setting mode/owner on %s", path);
}
umask(omask);
break;
}
case 'e':
case 'e': {
int uid, gid;
if (glob(path, GLOB_NOESCAPE, NULL, &gl))
break;
uid = parse_uid(user);
gid = parse_gid(group);
if (gid < 0)
gid = 0;
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);
/* skip non-directories */
if (dirperm(p, mode, uid, gid) && errno != ENOTDIR)
warn("Failed setting mode/owner on %s", p);
}
break;
}
case 'f':
case 'F':
mkparent(path, 0755);
case 'F': {
int flags = O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC;
int fd, uid, gid;
if (type[1] == '+' || type[0] == 'F') {
/* f+/F will create or truncate the file */
fp = fopen(path, "w+");
flags |= O_TRUNC; /* create or truncate */
} else {
/* f will create the file if it doesn't exist */
if (strc)
fp = fopen(path, "w");
if (!strc)
break; /* create only if missing */
flags |= O_EXCL;
}
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);
mkparent(path, 0755);
fd = open(path, flags, 0666);
if (fd < 0) {
if (errno != EEXIST)
warn("Failed creating %s", path);
break;
}
fp = fdopen(fd, "w");
if (!fp) {
close(fd);
break;
}
write_arg(fp, arg);
uid = parse_uid(user);
gid = parse_gid(group);
if (gid < 0)
gid = 0;
if (fdperm(fd, mode, uid, gid))
warn("Failed setting mode/owner on %s", path);
rc = fclose(fp);
break;
}
case 'l': /* Finit extension, like 'L' but only if target exists */
if (!arg) {
paste(buf, sizeof(buf), "/usr/share/factory", path);
+40 -4
View File
@@ -25,6 +25,7 @@
#include <ctype.h> /* isprint() */
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#ifdef HAVE_MNTENT_H
@@ -280,6 +281,43 @@ int getcgroup(char *buf, size_t len)
return 0;
}
/*
* Set mode and owner on an open fd, only what differs is touched, the
* file may be immutable. mode 0 skips the chmod, uid -1 the chown.
*/
int fdperm(int fd, mode_t mode, uid_t uid, gid_t gid)
{
struct stat st;
int rc;
rc = fstat(fd, &st);
if (!rc && mode && (st.st_mode & 07777) != mode)
rc = fchmod(fd, mode);
if (!rc && uid != (uid_t)-1 && (st.st_uid != uid || st.st_gid != gid))
rc = fchown(fd, uid, gid);
return rc;
}
/*
* Same for a directory that may already exist. Opened with
* O_NOFOLLOW | O_DIRECTORY, so the change lands on the directory
* itself and not on whatever a link at @path points to.
*/
int dirperm(const char *path, mode_t mode, uid_t uid, gid_t gid)
{
int fd, rc;
fd = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0)
return -1;
rc = fdperm(fd, mode, uid, gid);
close(fd);
return rc;
}
int mksubsys(const char *dir, mode_t mode, char *user, char *group)
{
mode_t omask;
@@ -295,10 +333,8 @@ int mksubsys(const char *dir, mode_t mode, char *user, char *group)
gid = 0;
rc = makedir(dir, mode);
if (rc && errno == EEXIST)
rc = chmod(dir, mode);
if (chown(dir, uid, gid))
err(1, "Failed chown(%s, %d, %d)", dir, uid, gid);
if (!rc && dirperm(dir, mode, uid, gid))
warn("Failed setting mode/owner on %s", dir);
}
umask(omask);
+2
View File
@@ -68,6 +68,8 @@ int getgroup (const char *group);
int getcuser (char *buf, size_t len);
int getcgroup (char *buf, size_t len);
int fdperm (int fd, mode_t mode, uid_t uid, gid_t gid);
int dirperm (const char *path, mode_t mode, uid_t uid, gid_t gid);
int mksubsys (const char *dir, mode_t mode, char *user, char *group);
int fnread (char *buf, size_t len, char *fmt, ...) __attribute__ ((format (printf, 3, 4)));
+6 -1
View File
@@ -30,9 +30,10 @@ EXTRA_DIST += setup-sysroot.sh
EXTRA_DIST += add-remove-dynamic-service.sh
EXTRA_DIST += add-remove-dynamic-service-sub-config.sh
EXTRA_DIST += bootstrap-crash.sh
EXTRA_DIST += cond-start-task.sh
EXTRA_DIST += script-timeout.sh
EXTRA_DIST += crashing.sh
EXTRA_DIST += dep-chain-reload.sh
EXTRA_DIST += reload-while-paused.sh
EXTRA_DIST += depserv.sh
EXTRA_DIST += devmon.sh
EXTRA_DIST += failing-sysv.sh
@@ -41,6 +42,7 @@ EXTRA_DIST += global-envs.sh
EXTRA_DIST += initctl-status-subset.sh
EXTRA_DIST += notify.sh
EXTRA_DIST += pidfile.sh
EXTRA_DIST += stale-pidfile.sh
EXTRA_DIST += pre-post-serv.sh
EXTRA_DIST += pre-fail.sh
EXTRA_DIST += process-depends.sh
@@ -74,8 +76,10 @@ TESTS += add-remove-dynamic-service.sh
TESTS += add-remove-dynamic-service-sub-config.sh
TESTS += bootstrap-crash.sh
TESTS += cond-start-task.sh
TESTS += script-timeout.sh
TESTS += crashing.sh
TESTS += dep-chain-reload.sh
TESTS += reload-while-paused.sh
TESTS += depserv.sh
TESTS += devmon.sh
TESTS += failing-sysv.sh
@@ -84,6 +88,7 @@ TESTS += global-envs.sh
TESTS += initctl-status-subset.sh
TESTS += notify.sh
TESTS += pidfile.sh
TESTS += stale-pidfile.sh
TESTS += pre-post-serv.sh
TESTS += pre-fail.sh
TESTS += process-depends.sh
+17 -1
View File
@@ -255,8 +255,12 @@ wdstart()
wdkill()
{
[ -n "${wdpid:-}" ] || return 0
say "Stopping test watchdog, pid $wdpid"
kill -KILL $wdpid
# Reap the sleep first, killing its subshell orphans it to PID 1.
pkill -KILL -P "$wdpid" 2>/dev/null || true
kill -KILL "$wdpid" 2>/dev/null || true
}
teardown()
@@ -305,6 +309,18 @@ trap teardown EXIT
SYSROOT="${SYSROOT:-$(pwd)/${TEST_DIR}/sysroot}"
export SYSROOT
# 'make check' refreshes the sysroot through the setup-chroot rule, but
# running a test script by hand does not, and then the test silently
# exercises whatever finit was installed last. Compare and refuse.
top_builddir="${top_builddir:-$TEST_DIR/..}"
sysroot_finit="$SYSROOT/sbin/finit"
built_finit="$top_builddir/src/finit"
[ -x "$top_builddir/src/.libs/finit" ] && built_finit="$top_builddir/src/.libs/finit"
if [ -x "$built_finit" ] && [ -e "$sysroot_finit" ] && ! cmp -s "$built_finit" "$sysroot_finit"; then
fail "Stale $sysroot_finit, run 'make -C test setup-chroot' or use 'make check'"
fi
TEST_TIMEOUT=300
# shellcheck source=/dev/null
+76
View File
@@ -0,0 +1,76 @@
#!/bin/sh
# A reload must not be lost on a service paused by a condition
#
# svc_s writes its PID file one second after a SIGHUP, so a service
# with <pid/svc_s> is paused for that second whenever svc_s reloads.
# svc_x counts the SIGHUPs it gets. Both .conf files are touched and
# two reloads are requested back to back, so the second one arrives
# while svc_x is paused with its own reload still pending. svc_x must
# get exactly one SIGHUP and keep its PID.
set -eu
TEST_DIR=$(dirname "$0")
test_teardown()
{
say "Running test teardown."
run "rm -f $FINIT_RCSD/svc_s.conf $FINIT_RCSD/svc_x.conf /tmp/hup.sh /tmp/hup.log"
}
pidof()
{
texec initctl -j status "$1" | jq .pid
}
test_setup()
{
run "cat > /tmp/hup.sh" <<EOF
#!/bin/sh
trap 'echo HUP >> /tmp/hup.log' HUP
trap 'exit 0' TERM
while true; do
sleep 1
done
EOF
run "cat > $FINIT_RCSD/svc_s.conf" <<EOF
service log:stdout notify:pid pid:!/run/service.pid name:svc_s service.sh -- Slow to reassert
EOF
run "cat > $FINIT_RCSD/svc_x.conf" <<EOF
service log:stdout <pid/svc_s> name:svc_x /bin/sh /tmp/hup.sh -- Paused while svc_s reloads
EOF
}
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
sep "Configuration"
run "cat $FINIT_RCSD/svc_s.conf"
run "cat $FINIT_RCSD/svc_x.conf"
say "Reload Finit to start both services"
run "initctl reload"
retry 'assert_status "svc_x" "running"' 10 1
pid_x=$(pidof svc_x)
say "svc_x PID before: $pid_x"
sep "Touch both, reload twice"
run "rm -f /tmp/hup.log"
run "initctl touch svc_s.conf"
run "initctl touch svc_x.conf"
run "initctl reload"
run "initctl reload"
say "Wait for services to settle"
retry 'assert_status "svc_x" "running"' 15 1
run "initctl status"
# shellcheck disable=SC2016
retry 'assert "svc_x got its SIGHUP" "$(texec sh -c "cat /tmp/hup.log 2>/dev/null | wc -l")" -eq 1' 10 1
new_pid_x=$(pidof svc_x)
# shellcheck disable=SC2086
assert "svc_x was not restarted" $new_pid_x -eq $pid_x
return 0
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
# A timeout on a stop: or reload: script must not take PID 1 with it.
# Those two hooks passed a NULL timeout pointer to parse_script(),
# which wrote through it whenever the script was prefixed with a
# valid number. Only a valid number reached the store, so
# 'stop:abc,/bin/true' was harmless while 'stop:5,/bin/true' was not.
set -eu
TEST_DIR=$(dirname "$0")
test_teardown()
{
say "Running test teardown."
run "rm -f $FINIT_CONF"
}
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
# shellcheck disable=SC2154
assert_alive()
{
assert "Finit survived $1" "$(kill -0 "$finit_pid" 2>/dev/null && echo yes)" = "yes"
}
for hook in stop reload post pre; do
say "Timeout on a $hook: script"
run "echo 'service $hook:5,/bin/true service.sh -- Timeout test' > $FINIT_CONF"
run "initctl reload" || true
assert_alive "$hook:5,/bin/true"
done
say 'The service still runs afterwards'
run "echo 'service stop:5,/bin/true service.sh -- Timeout test' > $FINIT_CONF"
run "initctl reload"
retry 'assert_num_children 1 service.sh'
+16 -3
View File
@@ -34,6 +34,7 @@ volatile sig_atomic_t reloading = 1;
volatile sig_atomic_t running = 1;
static char *ident = PROGNM;
static char fn[80];
static int exclusive = 0; /* -x: refuse to start if pidfile exists (dbus-style) */
static void verify_env(char *arg)
{
@@ -122,7 +123,7 @@ static void writefn(char *fn, int val)
static int checkfn(char *fn)
{
return !access(fn, R_OK);
return !access(fn, F_OK);
}
static void mine(char *fn)
@@ -133,12 +134,18 @@ static void mine(char *fn)
static void pidfile(char *pidfn)
{
static int once = 0;
if (!pidfn) {
if (fn[0] == 0)
snprintf(fn, sizeof(fn), "%s%s.pid", _PATH_VARRUN, ident);
pidfn = fn;
}
if (exclusive && !once && checkfn(pidfn))
errx(EX_SOFTWARE, "PID file %s exists, refusing to start", pidfn);
once = 1;
if (!checkfn(pidfn)) {
pid_t pid;
@@ -148,7 +155,7 @@ static void pidfile(char *pidfn)
atexit(cleanup);
} else {
inf("Touching PID file %s", pidfn);
utimensat(0, fn, NULL, 0);
utimensat(AT_FDCWD, pidfn, NULL, 0);
}
}
@@ -182,6 +189,7 @@ static int usage(int rc)
" -p Create PID file despite running in foreground\n"
" -P FILE Create PID file using FILE\n"
" -r SVC Call initctl to restart service SVC (self)\n"
" -x Refuse to start if PID file already exists (dbus-style)\n"
"\n"
"By default this program daemonizes itself to the background, and,\n"
"when it's done setting up its signal handler(s), creates a PID file\n"
@@ -212,7 +220,7 @@ int main(int argc, char *argv[])
char cmd[80];
int c;
while ((c = getopt(argc, argv, "cCe:E:f:F:hi:nN:pP:r:")) != EOF) {
while ((c = getopt(argc, argv, "cCe:E:f:F:hi:nN:pP:r:x")) != EOF) {
switch (c) {
case 'c':
do_crash = 1;
@@ -249,12 +257,17 @@ int main(int argc, char *argv[])
break;
case 'P':
pidfn = optarg;
if ((size_t)snprintf(fn, sizeof(fn), "%s", optarg) >= sizeof(fn))
errx(EX_USAGE, "-P path too long (max %zu)", sizeof(fn) - 1);
do_pidfile++;
break;
case 'r':
snprintf(cmd, sizeof(cmd), "initctl restart %s", optarg);
do_restart = 1;
break;
case 'x':
exclusive = 1;
break;
default:
return usage(1);
}
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# Regression: Finit must remove a daemon-owned (pid:!) stale pidfile
# left behind by an unclean exit (SIGKILL), so the next instance can
# start. Simulates the dbus-daemon pattern: 'serv -x' refuses to
# start if its pidfile already exists.
set -eu
TEST_DIR=$(dirname "$0")
PIDFN="/run/serv.pid"
test_teardown()
{
say "Running test teardown."
run "rm -f $FINIT_CONF $PIDFN"
}
# shellcheck source=/dev/null
. "$TEST_DIR/lib/setup.sh"
say "Add service stanza '$FINIT_CONF' with pid:!$PIDFN"
run "echo 'service pid:!$PIDFN serv -np -P $PIDFN -x' > $FINIT_CONF"
run "initctl reload"
retry "assert_num_children 1 serv"
assert_is_pidfile "serv" "$PIDFN"
PID=$(texec cat "$PIDFN")
say "SIGKILL serv ($PID) -- leaves stale pidfile"
run "kill -9 $PID"
# Without the fix, 'serv -x' refuses to start on each retry until
# Finit hits restart_max and marks the service crashed. With the fix
# Finit removes the stale pidfile and the next instance comes up.
retry "assert_pidiff serv $PID"
retry "assert_num_children 1 serv"
sep
+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 - - -