Expand description
A correct, full-featured Unix daemon library and CLI for Rust.
A blivet is the “impossible fork” optical illusion, also known as the devil’s fork. Daemons are created by forking — and this crate performs the impossible double-fork to do it correctly.
This crate provides a library and CLI tool for daemonizing processes on Unix
systems. It performs a double-fork, resets signal dispositions and
mask, and uses a notification pipe so the parent can wait for daemon
readiness. Privilege dropping is split-phase: daemonize() returns a
context while still privileged, and the caller explicitly calls
drop_privileges() when ready.
§Example
use blivet::{DaemonConfig, daemonize};
let mut config = DaemonConfig::new();
config.pidfile("/var/run/foo.pid").chdir("/tmp");
let mut ctx = daemonize(&config)?;
// ... application initialization ...
ctx.notify_parent()?;
// daemon process continues here§Choosing an entry point
There are two entry points:
daemonizeis the safe default: it verifies the process is single-threaded for you, so nounsafeis needed. It is available on Linux, macOS, FreeBSD, NetBSD, and OpenBSD, each using the kernel’s own thread count (/proc/self/statuson Linux,proc_pidinfoon macOS,sysctlon the BSDs). On any other target it is a#[deprecated]stub that never daemonizes — a hard compile error under-D warnings/#![deny(deprecated)]; usedaemonize_uncheckedthere.daemonize_uncheckedisunsafeand available on all Unix platforms: you must guarantee the process is single-threaded at the call site (see Threads and async runtimes).
Most callers want daemonize:
let mut ctx = blivet::daemonize(&config)?;To also compile on an exotic target without thread-count support, gate the call so the deprecated stub is never built:
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd",
target_os = "netbsd", target_os = "openbsd"))]
let mut ctx = blivet::daemonize(&config)?;
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd",
target_os = "netbsd", target_os = "openbsd")))]
// SAFETY: no threads spawned before this point.
let mut ctx = unsafe { blivet::daemonize_unchecked(&config)? };§Threads and async runtimes
Daemonizing forks, and forking a multithreaded process is unsound:
mutexes held by other threads stay locked forever in the child. A second
thread-unsafe step follows:
drop_privileges calls setenv
(USER/HOME/LOGNAME) when switching users. The single-threaded window
therefore runs from the fork through the last setenv — i.e. through
drop_privileges():
[single-threaded required]
daemonize() / daemonize_unchecked() <- forks here
drop_privileges() <- last unsafe step: setenv (USER/HOME/LOGNAME)
[now safe to spawn threads / start tokio / accept connections]
notify_parent() <- thread-safe; writes one byte to the pipeBoth guards check for you and panic if violated: daemonize at the fork,
drop_privileges at its setenv (when a
user is configured). daemonize_unchecked and
drop_privileges_unchecked are
the unsafe opt-outs.
Spawn threads, start an async runtime, or begin a thread-per-connection
accept loop after drop_privileges() returns — or after daemonize
returns if you don’t switch users.
notify_parent itself is thread-safe.
§Output and the working directory
Two daemonize(1)-standard defaults bite the unwary: stdout/stderr go to
/dev/null (a println! vanishes), and the working directory becomes /
(relative paths resolve against / and usually fail). Use absolute paths;
see stdout / stderr /
chdir to change them.
§Signals
Daemonization resets every signal disposition to its default and clears
the signal mask — with one exception: SIGPIPE is preserved. The Rust
runtime ignores SIGPIPE so writes to a closed pipe or socket return
ErrorKind::BrokenPipe instead of
killing the process, and that guarantee survives daemonize. (The
daemonize CLI restores the default disposition just before exec, so
spawned programs still start with conventional signal state.)
§Pidfile cleanup on signals
Drop does not run when a signal kills the process (how daemons are
normally stopped), so the auto-cleanup from
cleanup_on_drop leaves a stale pidfile.
Call cleanup_on_term_signals
once, or run your own shutdown loop and let the context drop — see
examples/echo_server.rs.
§Exit codes
DaemonizeError::exit_code maps each error to a sysexits.h code — see
its docs for the full table — but fn main() -> Result<(), E> ignores it
and exits 1. To surface the codes, call exit_code() yourself. To
report a failure from your own init code (e.g. a socket bind) with a chosen
code, use report_error_msg or the
DaemonizeError::Application variant.
§Split-phase design
Many daemons need root during startup — bind a privileged port, write a
pidfile to /var/run, open root-owned logs — but should run unprivileged
afterward. Rather than fold privilege dropping into the daemonize call,
daemonize() returns a DaemonContext still running as root; you do the
privileged work, then call
drop_privileges() (which first chowns
the pidfile/lockfile/logs to the target user — opt out with
chown_paths), then
notify_parent(). Full ordering control:
use blivet::{DaemonConfig, daemonize};
let mut config = DaemonConfig::new();
config.pidfile("/var/run/foo.pid").user("nobody").group("nogroup");
let mut ctx = daemonize(&config)?;
// 1. Privileged work while still root:
// bind sockets, chroot, set resource limits, etc.
let _listener = std::net::TcpListener::bind("0.0.0.0:80")?;
// 2. Drop to unprivileged user (chowns pidfile/logs first, while still root)
ctx.drop_privileges()?;
// 3. Tell the parent we're ready
ctx.notify_parent()?;
// Daemon continues as "nobody" with the socket still openStructs§
- Daemon
Config - Configuration for the daemonization process.
- Daemon
Context - Context returned by a successful daemonization.
Enums§
- Daemonize
Error - Errors produced during configuration validation or the daemonization sequence.
Functions§
- daemonize
- Daemonize the current process, verifying it is single-threaded first.
- daemonize_
unchecked ⚠ - Daemonize the current process without verifying the thread count.