Skip to main content

Crate blivet

Crate blivet 

Source
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:

  • daemonize is the safe default: it verifies the process is single-threaded for you, so no unsafe is needed. It is available on Linux, macOS, FreeBSD, NetBSD, and OpenBSD, each using the kernel’s own thread count (/proc/self/status on Linux, proc_pidinfo on macOS, sysctl on the BSDs). On any other target it is a #[deprecated] stub that never daemonizes — a hard compile error under -D warnings / #![deny(deprecated)]; use daemonize_unchecked there.
  • daemonize_unchecked is unsafe and 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 pipe

Both 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 open

Structs§

DaemonConfig
Configuration for the daemonization process.
DaemonContext
Context returned by a successful daemonization.

Enums§

DaemonizeError
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.