blivet
A correct, full-featured Unix daemon library and CLI for Rust.
blivet implements the full double-fork daemonization sequence with a
parent-notification pipe, so your process detaches cleanly and the calling shell
(or init system) knows exactly when the daemon is ready -- or why it failed.
Errors that happen after forking are reported back to the parent with
sysexits.h codes instead of vanishing into a void.
Why this crate?
- Correct by default. Mandatory double-fork,
setsid, signal reset (including real-time signals on Linux), signal mask clear, fd close,/dev/nullredirect -- the things most hand-rolled daemonizers forget. - Parent notification. The calling process blocks until the daemon signals readiness or reports an error. No more "did it start?" polling.
- Split-phase privilege dropping.
daemonize()returns while still privileged, giving you a window for operations like binding privileged ports before callingdrop_privileges(). - Fail-safe drop. If you forget to call
notify_parent(), the parent exits non-zero automatically. - Unsafe contained.
#![deny(unsafe_code)]at the crate root. All unsafe lives in a single module (unsafe_ops), with safe wrappers for everything else. - Library and CLI. Use it as a Rust library with a builder API, or as a
standalone
daemonizebinary (installed bycargo install blivet) that wraps any program.
Why the name blivet?
A blivet is the "impossible pitchfork" optical illusion, also known as the devil's fork, where the prongs are mysteriously detached from the base. Daemons are created by forking to detach from their parent terminal.
Install
Or add the library to your project:
CLI Quickstart
Daemonize any program:
# Basic usage
# With pidfile and log redirection (stderr mirrors stdout by default)
# Split stdout/stderr using .stdout/.stderr or .out/.err extensions (auto-derived)
# Separate lockfile (overrides the pidfile default)
# Run as a different user and group (requires root)
# Run in foreground with supervisor-passed fds kept open
# Set environment variables
The parent process blocks until the daemon successfully calls exec, then
exits 0. If anything fails (lockfile conflict, permission denied, exec error),
the parent prints the error to stderr and exits with a sysexits.h code.
When -u/-g are specified, the CLI transfers ownership of the pidfile,
lockfile, and log files to the target user/group before dropping privileges,
so the daemon can continue to write to them after the switch.
CLI flags
| Flag | Long | Description |
|---|---|---|
-p |
--pidfile PATH |
Write daemon PID to file |
-l |
--lock PATH |
Exclusive lockfile (default: pidfile path, if set) |
-c |
--chdir PATH |
Working directory (default: /) |
-m |
--umask MODE |
Process umask in octal (e.g. 022) |
-o |
--stdout PATH |
Redirect stdout to file (also sets stderr if -e is not given) |
-e |
--stderr PATH |
Redirect stderr to file (default: stdout path; .stdout→.stderr, .out→.err) |
-a |
--append |
Append to stdout/stderr files instead of truncating |
-u |
--user NAME|UID |
Switch to user after daemonizing (requires root) |
-g |
--group NAME|GID |
Switch to group after daemonizing (requires root) |
-f |
--foreground |
Stay in foreground (no fork/setsid); consider --no-close-fds |
--no-close-fds |
Keep inherited fds open (useful with -f for supervisor-passed fds) |
|
-E |
--env NAME=VAL |
Set environment variable (repeatable) |
-v |
--verbose |
Print diagnostic info before daemonizing |
Library quickstart
use ;
On Linux, daemonize_checked provides a safe wrapper that verifies the process
is single-threaded (via /proc/self/status) before forking:
use ;
let config = new;
let mut ctx = daemonize_checked?; // panics if threads > 1
ctx.notify_parent?;
Split-phase privilege dropping
When your daemon needs to perform privileged operations (like binding to
port 80, calling chroot, or setting resource limits) before dropping to
an unprivileged user:
use ;
Foreground mode
For systemd, containers, or debugging, use foreground mode to skip forking
while still applying all other daemon setup (umask, chdir, signal reset, etc.).
Stdout and stderr are left inherited (not redirected to /dev/null) unless
explicitly configured with .stdout()/.stderr():
let mut config = new;
config
.foreground
.close_fds; // keep supervisor-passed fds
Pidfile cleanup
When cleanup_on_drop is true (the default), the pidfile is removed when
DaemonContext is dropped. However, Drop does not run when the process is
killed by a signal (SIGTERM, SIGKILL, etc.) — which is how most daemons
are stopped. To clean up the pidfile on signal termination, install a signal
handler that either calls cleanup() or lets DaemonContext drop:
use ;
use Arc;
use ;
API overview
DaemonConfig
Builder for daemonization settings. All methods are infallible setters;
validation is deferred to validate().
| Method | Default | Description |
|---|---|---|
pidfile(path) |
None | Write PID to file |
lockfile(path) |
None | Exclusive flock-based lockfile |
chdir(path) |
/ |
Working directory |
umask(mode) |
0 |
Process umask |
stdout(path) |
None | Redirect stdout (stays /dev/null if unset) |
stderr(path) |
None | Redirect stderr (stays /dev/null if unset) |
append(bool) |
false |
Append vs truncate output files |
user(name) |
None | Switch user -- name or numeric UID (requires root) |
group(name) |
None | Switch group -- name or numeric GID (requires root) |
foreground(bool) |
false |
Skip fork/setsid (for systemd, containers, debugging) |
close_fds(bool) |
true |
Close inherited fds 3+ |
cleanup_on_drop(bool) |
true |
Remove pidfile when DaemonContext is dropped |
env(key, val) |
None | Set env var (accumulates, last-write-wins) |
validate() |
-- | Check paths, permissions, overlaps before forking |
daemonize(&config) -> Result<DaemonContext, DaemonizeError>
Performs the daemonization sequence: pipe, double-fork, setsid, umask, chdir,
/dev/null redirect, lockfile, pidfile, signal reset, signal mask clear, env
vars, output redirect, fd close. Returns a DaemonContext in the grandchild
(or the current process in foreground mode). The original parent blocks on the
notification pipe.
User/group switching is not performed during this call. Use
DaemonContext::drop_privileges() after doing any privileged work.
DaemonContext
Returned by a successful daemonize() call. Holds the lockfile, notification
pipe, and config state needed for privilege operations.
| Method | Description |
|---|---|
cleanup() |
Remove pidfile from disk (best-effort, idempotent) |
set_cleanup_on_drop(bool) |
Override cleanup_on_drop at runtime |
chown_paths() |
Transfer pidfile/lockfile/log ownership to target user/group |
drop_privileges() |
Switch user/group (initgroups + setgid + setuid) |
notify_parent() |
Signal readiness -- parent exits 0 |
report_error(err) |
Report error to parent and _exit |
lockfile_fd() |
Borrow the lockfile fd (if configured) |
Dropping without calling notify_parent() causes the parent to exit non-zero.
When cleanup_on_drop is true (the default), dropping also removes the pidfile.
Note that Drop does not run on signal termination — see
Pidfile cleanup above.
DaemonizeError
Fourteen variants covering validation, fork, setsid, lock, permission, chown,
and exec failures. Each maps to a sysexits.h exit code via exit_code().
| Variant | Exit code | Meaning |
|---|---|---|
ValidationError |
64 | Bad config (paths, env keys, overlaps) |
ProgramNotFound |
66 | CLI: program missing or not executable |
UserNotFound |
67 | User doesn't exist |
GroupNotFound |
67 | Group doesn't exist |
LockConflict |
69 | Lockfile held by another process |
LockfileError |
73 | Can't open lockfile |
PidfileError |
73 | Can't write pidfile |
OutputFileError |
73 | Can't open/redirect output file |
ChownError |
73 | Can't chown pidfile/lockfile/output file |
ForkFailed |
71 | fork() error |
SetsidFailed |
71 | setsid() error |
ChdirFailed |
71 | chdir() error |
PermissionDenied |
77 | Not root, or setuid/setgid failed |
ExecFailed |
71 | CLI: exec of target program failed |
Minimum supported Rust version
1.85
License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.