Compare commits

...
23 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
24 changed files with 605 additions and 155 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
+1 -1
View File
@@ -1,4 +1,4 @@
AC_INIT([Finit], [4.17-rc1], [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])
+47 -1
View File
@@ -3,7 +3,53 @@ Change Log
All relevant changes are documented in this file.
[4.17][] - 2026-04-xx
[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
+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
+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
+9 -4
View File
@@ -379,7 +379,6 @@ void print(int rc, const char *fmt, ...)
/* Pending state: show description with spinner, no final status yet */
if (!buf[0])
return;
delline();
if (progress_style == PROGRESS_CLASSIC)
cprintf("\r%s ", pad(buf, sizeof(buf), ".", sizeof(buf)));
else
@@ -391,15 +390,21 @@ void print(int rc, const char *fmt, ...)
* 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]) {
delline();
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)), status(rc));
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, status(rc));
cprintf("\r\e[K%s%s\r%s\n", status(3), buf, st);
} else {
if (progress_style == PROGRESS_CLASSIC)
cprintf("%s\n", status(rc));
+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)
{
+55 -12
View File
@@ -1120,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
*/
@@ -1137,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);
}
/*
@@ -1623,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)
@@ -2305,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);
@@ -2405,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;
}
@@ -2794,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);
+9 -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();
@@ -630,7 +632,7 @@ const char *sig_name(int signo)
return signames[i].name;
}
return "SIGUNKOWN";
return "SIGUNKNOWN";
}
/*
+1 -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
+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