#![deny(unsafe_code)]
mod config;
mod context;
mod error;
pub(crate) mod forker;
mod identity;
pub(crate) mod unsafe_ops;
mod notify;
mod steps;
mod thread_count;
pub(crate) mod util;
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod doc_sync;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctests {}
pub use config::DaemonConfig;
pub use context::DaemonContext;
pub use error::DaemonizeError;
use std::io::Read;
use std::os::fd::{AsRawFd, OwnedFd};
use nix::unistd::ForkResult;
use forker::{Forker, RealForker};
use notify::NotifyPipe;
#[allow(unsafe_code)]
pub unsafe fn daemonize_unchecked(config: &DaemonConfig) -> Result<DaemonContext, DaemonizeError> {
config.validate()?;
unsafe { daemonize_inner(config, &mut RealForker) }
}
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn single_threaded_violation(caller: &str, count: usize) -> Option<String> {
(count != 1).then(|| {
format!(
"{caller}: {count} threads running (expected 1). \
Call {caller} before spawning threads, async runtimes, \
or libraries with background threads."
)
})
}
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn assert_single_threaded(caller: &str) {
let count = thread_count::count().unwrap_or_else(|_| {
panic!("{caller}: cannot determine thread count to verify single-threadedness")
});
if let Some(msg) = single_threaded_violation(caller, count) {
panic!("{msg}");
}
}
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub fn daemonize(config: &DaemonConfig) -> Result<DaemonContext, DaemonizeError> {
assert_single_threaded("daemonize");
#[allow(unsafe_code)]
unsafe {
daemonize_unchecked(config)
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
#[deprecated(note = "daemonize cannot verify the thread count on this target. \
Call `unsafe { daemonize_unchecked(&config) }` and ensure the process \
is single-threaded yourself.")]
pub fn daemonize(_config: &DaemonConfig) -> Result<DaemonContext, DaemonizeError> {
panic!(
"daemonize is unsupported on this target (cannot query the thread \
count); call `unsafe {{ daemonize_unchecked(&config) }}` and ensure the \
process is single-threaded yourself"
)
}
#[allow(unsafe_code)]
pub(crate) unsafe fn daemonize_inner(
config: &DaemonConfig,
forker: &mut impl Forker,
) -> Result<DaemonContext, DaemonizeError> {
let foreground = config.foreground;
let mut pipe_wr = if foreground {
None
} else {
let pipe = forker.create_notification_pipe();
let (pipe_rd, mut pipe_wr) = match pipe {
Some((rd, wr)) => (Some(rd), Some(NotifyPipe::new(wr))),
None => (None, None),
};
let first_fork = unsafe { forker.fork() };
let first_fork = match first_fork {
Ok(result) => result,
Err(e) => {
if let Some(pipe) = pipe_wr {
pipe.close();
}
return Err(e);
}
};
match first_fork {
ForkResult::Parent { .. } => {
if let Some(pipe) = pipe_wr {
pipe.close();
}
if let Some(rd) = pipe_rd {
parent_pipe_reader(rd, forker);
}
forker.exit(0);
}
ForkResult::Child => {
drop(pipe_rd);
}
}
if let Err(e) = forker.setsid() {
signal_error_to_parent(&mut pipe_wr, &e);
forker.exit(e.exit_code() as i32);
}
match unsafe { forker.fork() } {
Ok(ForkResult::Parent { .. }) => {
if let Some(pipe) = pipe_wr {
pipe.close();
}
forker.exit(0);
}
Ok(ForkResult::Child) => {
}
Err(e) => {
signal_error_to_parent(&mut pipe_wr, &e);
forker.exit(e.exit_code() as i32);
}
}
pipe_wr
};
match run_post_fork(config, &mut pipe_wr) {
Ok(ctx) => Ok(ctx),
Err(e) if foreground => Err(e),
Err(e) => {
signal_error_to_parent(&mut pipe_wr, &e);
forker.exit(e.exit_code() as i32);
}
}
}
fn run_post_fork(
config: &DaemonConfig,
pipe_wr: &mut Option<NotifyPipe>,
) -> Result<DaemonContext, DaemonizeError> {
steps::set_umask(config.umask);
steps::change_dir(&config.chdir)?;
steps::redirect_to_devnull(!config.foreground)?;
let lockfile_path = config.effective_lockfile().map(std::path::PathBuf::as_path);
let lockfile = match lockfile_path {
Some(path) => Some(steps::open_and_lock(path)?),
None => None,
};
if let Some(ref pidfile_path) = config.pidfile {
steps::write_pidfile(pidfile_path, lockfile_path.zip(lockfile.as_ref()))?;
}
unsafe_ops::reset_signal_dispositions()?;
steps::clear_signal_mask()?;
steps::set_env_vars(&config.env);
if config.stdout.is_some() || config.stderr.is_some() {
steps::redirect_output(
config.stdout.as_deref(),
config.stderr.as_deref(),
config.append,
)?;
}
if config.close_fds {
let mut skip_fds: Vec<i32> = Vec::new();
if let Some(ref flock) = lockfile {
skip_fds.push(flock.as_raw_fd());
}
if let Some(ref wr) = pipe_wr {
skip_fds.push(wr.as_fd().as_raw_fd());
}
steps::close_inherited_fds(&skip_fds)?;
}
Ok(DaemonContext::new(config, lockfile, pipe_wr.take()))
}
fn parent_pipe_reader(rd: OwnedFd, forker: &impl Forker) -> ! {
let mut file = std::fs::File::from(rd);
let mut buf = Vec::new();
let _ = file.read_to_end(&mut buf);
match notify::decode(&buf) {
notify::Outcome::Success => forker.exit(0),
notify::Outcome::Failure { code, message } => {
eprintln!("{message}");
forker.exit(code);
}
}
}
fn signal_error_to_parent(pipe_wr: &mut Option<NotifyPipe>, err: &DaemonizeError) {
if let Some(pipe) = pipe_wr.take() {
pipe.signal_error(err);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forker::null_forker::NullForker;
use crate::test_support::{is_subprocess, run_in_subprocess};
use std::panic::catch_unwind;
#[test]
fn both_forks_child_succeeds() {
run_in_subprocess("tests::both_forks_child_succeeds_subprocess");
}
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
#[test]
fn single_threaded_violation_accepts_only_exactly_one() {
assert!(
single_threaded_violation("daemonize", 1).is_none(),
"exactly one thread is the single-threaded case"
);
assert!(
single_threaded_violation("daemonize", 0).is_some(),
"an anomalous 0 must fail closed, not green-light a fork"
);
let msg =
single_threaded_violation("drop_privileges", 2).expect("2 threads is a violation");
assert!(
msg.contains("drop_privileges: 2 threads running (expected 1)"),
"message should name the caller, count, and expectation, got: {msg}"
);
}
#[allow(unsafe_code)]
fn run_inner(
config: &DaemonConfig,
forker: &mut NullForker,
) -> Result<DaemonContext, DaemonizeError> {
unsafe { daemonize_inner(config, forker) }
}
#[test]
#[ignore]
fn both_forks_child_succeeds_subprocess() {
if !is_subprocess() {
return;
}
let mut config = DaemonConfig::new();
config.close_fds(false); let mut forker = NullForker::both_child();
let result = run_inner(&config, &mut forker);
assert!(result.is_ok());
}
#[test]
fn first_fork_parent_exits() {
let config = DaemonConfig::new();
let mut forker = NullForker::first_parent();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
run_inner(&config, &mut forker)
}));
assert!(result.is_err()); }
#[test]
fn first_fork_parent_closes_pipe_silently() {
let config = DaemonConfig::new();
let mut forker = NullForker::first_parent().with_pipe();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
run_inner(&config, &mut forker)
}));
let panic_msg = result
.expect_err("parent exits after reading the pipe")
.downcast_ref::<String>()
.cloned()
.unwrap();
assert!(
panic_msg.contains("NullForker::exit(0)"),
"parent must read EOF = success from its own silently-closed write \
end, got: {panic_msg}"
);
}
#[test]
fn second_fork_intermediate_closes_pipe_silently() {
let config = DaemonConfig::new();
let mut forker = NullForker::second_parent().with_pipe();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
run_inner(&config, &mut forker)
}));
assert!(result.is_err(), "intermediate child exits");
let rd = forker
.take_pipe_reader()
.expect("with_pipe stores a reader for the test");
let mut buf = Vec::new();
std::fs::File::from(rd).read_to_end(&mut buf).unwrap();
assert_eq!(
buf,
Vec::<u8>::new(),
"intermediate child must close the pipe without writing; the \
daemon is the sole writer"
);
}
#[test]
fn second_fork_parent_exits() {
let config = DaemonConfig::new();
let mut forker = NullForker::second_parent();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
run_inner(&config, &mut forker)
}));
assert!(result.is_err());
}
#[test]
fn first_fork_fails_returns_error() {
let config = DaemonConfig::new();
let mut forker = NullForker::first_fork_fails();
let result = run_inner(&config, &mut forker);
assert!(matches!(result, Err(DaemonizeError::ForkFailed(_))));
}
#[test]
fn first_fork_failure_closes_pipe_silently() {
let config = DaemonConfig::new();
let mut forker = NullForker::first_fork_fails().with_pipe();
let result = run_inner(&config, &mut forker);
assert!(matches!(result, Err(DaemonizeError::ForkFailed(_))));
let rd = forker
.take_pipe_reader()
.expect("with_pipe stores a reader");
let mut buf = Vec::new();
std::fs::File::from(rd).read_to_end(&mut buf).unwrap();
assert_eq!(
buf,
Vec::<u8>::new(),
"a pre-fork failure must leave the pipe unwritten"
);
}
#[test]
fn setsid_fails_exits() {
let config = DaemonConfig::new();
let mut forker = NullForker::setsid_fails();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
run_inner(&config, &mut forker)
}));
assert!(result.is_err());
}
#[test]
fn second_fork_fails_exits() {
let config = DaemonConfig::new();
let mut forker = NullForker::second_fork_fails();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
run_inner(&config, &mut forker)
}));
assert!(result.is_err());
}
#[test]
fn exit_panic_contains_code() {
let config = DaemonConfig::new();
let mut forker = NullForker::first_parent();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
run_inner(&config, &mut forker)
}));
let panic_msg = result
.unwrap_err()
.downcast_ref::<String>()
.cloned()
.unwrap();
assert!(
panic_msg.contains("NullForker::exit(0)"),
"panic message should contain exit code, got: {panic_msg}"
);
}
#[test]
#[serial_test::serial]
fn daemon_post_fork_failure_exits_not_returns() {
let _umask = test_support::UmaskGuard::set(nix::sys::stat::Mode::empty());
let mut config = DaemonConfig::new();
config.chdir("/nonexistent_daemonize_postfork_failure");
let mut forker = NullForker::both_child();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
run_inner(&config, &mut forker)
}));
let panic_msg = result
.expect_err("daemon-mode post-fork failure must exit (panic), not return")
.downcast_ref::<String>()
.cloned()
.unwrap();
assert!(
panic_msg.contains("NullForker::exit(71)"),
"must exit with ChdirFailed's EX_OSERR code 71, got: {panic_msg}"
);
}
#[test]
fn signal_error_to_parent_noop_with_none() {
signal_error_to_parent(&mut None, &DaemonizeError::ForkFailed("test".into()));
}
#[test]
fn signal_error_to_parent_writes_protocol() {
let (rd, wr) = nix::unistd::pipe().unwrap();
let mut pipe_wr = Some(NotifyPipe::new(wr));
let err = DaemonizeError::ForkFailed("test error".into());
signal_error_to_parent(&mut pipe_wr, &err);
assert!(pipe_wr.is_none(), "write end consumed after signalling");
let mut file = std::fs::File::from(rd);
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
assert_eq!(buf[0], 71); assert_eq!(
std::str::from_utf8(&buf[1..]).unwrap(),
"fork failed: test error"
);
}
#[test]
fn foreground_mode_skips_fork() {
run_in_subprocess("tests::foreground_mode_skips_fork_subprocess");
}
#[test]
#[ignore]
fn foreground_mode_skips_fork_subprocess() {
if !is_subprocess() {
return;
}
let mut config = DaemonConfig::new();
config.foreground(true).close_fds(false);
let mut forker = NullForker::new(vec![], Ok(()));
let result = run_inner(&config, &mut forker);
let ctx = result.expect("foreground daemonize_inner should succeed");
assert!(ctx.lockfile_fd().is_none());
}
#[test]
fn devnull_failure_propagates() {
run_in_subprocess("tests::devnull_failure_propagates_subprocess");
}
#[test]
#[ignore]
fn devnull_failure_propagates_subprocess() {
if !is_subprocess() {
return;
}
steps::failpoints::DEVNULL_OPEN_FAILS.store(true, std::sync::atomic::Ordering::Relaxed);
let mut config = DaemonConfig::new();
config.foreground(true).close_fds(false);
let mut forker = NullForker::new(vec![], Ok(()));
match run_inner(&config, &mut forker) {
Err(DaemonizeError::SystemError(msg)) => {
assert!(
msg.contains("/dev/null"),
"message names the syscall: {msg}"
);
}
other => panic!("expected SystemError to propagate, got {other:?}"),
}
}
#[test]
fn sigaction_failure_propagates() {
run_in_subprocess("tests::sigaction_failure_propagates_subprocess");
}
#[test]
#[ignore]
fn sigaction_failure_propagates_subprocess() {
if !is_subprocess() {
return;
}
steps::failpoints::SIGACTION_FAILS.store(true, std::sync::atomic::Ordering::Relaxed);
let mut config = DaemonConfig::new();
config.foreground(true).close_fds(false);
let mut forker = NullForker::new(vec![], Ok(()));
match run_inner(&config, &mut forker) {
Err(DaemonizeError::SystemError(msg)) => {
assert!(
msg.contains("sigaction"),
"message names the syscall: {msg}"
);
}
other => panic!("expected SystemError to propagate, got {other:?}"),
}
}
#[test]
fn sigprocmask_failure_propagates() {
run_in_subprocess("tests::sigprocmask_failure_propagates_subprocess");
}
#[test]
#[ignore]
fn sigprocmask_failure_propagates_subprocess() {
if !is_subprocess() {
return;
}
steps::failpoints::SIGPROCMASK_FAILS.store(true, std::sync::atomic::Ordering::Relaxed);
let mut config = DaemonConfig::new();
config.foreground(true).close_fds(false);
let mut forker = NullForker::new(vec![], Ok(()));
match run_inner(&config, &mut forker) {
Err(DaemonizeError::SystemError(msg)) => {
assert!(
msg.contains("sigprocmask"),
"message names the syscall: {msg}"
);
}
other => panic!("expected SystemError to propagate, got {other:?}"),
}
}
#[test]
fn getrlimit_failure_propagates() {
run_in_subprocess("tests::getrlimit_failure_propagates_subprocess");
}
#[test]
#[ignore]
fn getrlimit_failure_propagates_subprocess() {
if !is_subprocess() {
return;
}
steps::failpoints::FD_LISTING_UNAVAILABLE.store(true, std::sync::atomic::Ordering::Relaxed);
steps::failpoints::GETRLIMIT_FAILS.store(true, std::sync::atomic::Ordering::Relaxed);
let mut config = DaemonConfig::new();
config.foreground(true).close_fds(true);
let mut forker = NullForker::new(vec![], Ok(()));
match run_inner(&config, &mut forker) {
Err(DaemonizeError::SystemError(msg)) => {
assert!(
msg.contains("getrlimit"),
"message names the syscall: {msg}"
);
}
other => panic!("expected SystemError to propagate, got {other:?}"),
}
}
#[test]
fn pidfile_only_holds_derived_lock() {
run_in_subprocess("tests::pidfile_only_holds_derived_lock_subprocess");
}
#[test]
#[ignore]
fn pidfile_only_holds_derived_lock_subprocess() {
if !is_subprocess() {
return;
}
let dir = tempfile::tempdir().unwrap();
let pidfile = dir.path().join("app.pid");
let mut config = DaemonConfig::new();
config.foreground(true).close_fds(false).pidfile(&pidfile);
let mut forker = NullForker::new(vec![], Ok(()));
let ctx = run_inner(&config, &mut forker).expect("daemonize should succeed");
let lock_fd = ctx
.lockfile_fd()
.expect("a lone pidfile should be flock'd by default");
let lock_stat = nix::sys::stat::fstat(lock_fd).unwrap();
let pidfile_stat = nix::sys::stat::stat(&pidfile).unwrap();
assert_eq!(
(lock_stat.st_dev, lock_stat.st_ino),
(pidfile_stat.st_dev, pidfile_stat.st_ino),
"derived lock fd should refer to the pidfile"
);
let second = steps::open_and_lock(&pidfile);
assert!(matches!(second, Err(DaemonizeError::LockConflict { .. })));
}
#[test]
fn foreground_lock_conflict_returns_err() {
run_in_subprocess("tests::foreground_lock_conflict_returns_err_subprocess");
}
#[test]
#[ignore]
fn foreground_lock_conflict_returns_err_subprocess() {
if !is_subprocess() {
return;
}
let dir = tempfile::tempdir().unwrap();
let pidfile = dir.path().join("app.pid");
let _held = steps::open_and_lock(&pidfile).expect("first lock should succeed");
let mut config = DaemonConfig::new();
config.foreground(true).close_fds(false).pidfile(&pidfile);
let mut forker = NullForker::new(vec![], Ok(()));
let result = run_inner(&config, &mut forker);
assert!(
matches!(result, Err(DaemonizeError::LockConflict { .. })),
"foreground mode should surface setup errors as Err, not exit"
);
}
#[test]
fn foreground_mode_notify_parent_noop() {
run_in_subprocess("tests::foreground_mode_notify_parent_noop_subprocess");
}
#[test]
#[ignore]
fn foreground_mode_notify_parent_noop_subprocess() {
if !is_subprocess() {
return;
}
let mut config = DaemonConfig::new();
config.foreground(true).close_fds(false);
let mut forker = NullForker::new(vec![], Ok(()));
let mut ctx = run_inner(&config, &mut forker).unwrap();
assert!(ctx.notify_parent().is_ok());
}
#[test]
fn close_fds_false_preserves_fds() {
run_in_subprocess("tests::close_fds_false_preserves_fds_subprocess");
}
#[test]
#[ignore]
fn close_fds_false_preserves_fds_subprocess() {
if !is_subprocess() {
return;
}
let (rd, wr) = nix::unistd::pipe().unwrap();
let mut config = DaemonConfig::new();
config.close_fds(false);
let mut forker = NullForker::both_child();
let _ctx = run_inner(&config, &mut forker).unwrap();
assert!(
nix::unistd::write(&wr, b"alive").is_ok(),
"write fd should still be open with close_fds=false"
);
let mut buf = [0u8; 5];
assert!(
nix::unistd::read(&rd, &mut buf).is_ok(),
"read fd should still be open with close_fds=false"
);
}
#[test]
fn context_carries_config_fields() {
run_in_subprocess("tests::context_carries_config_fields_subprocess");
}
#[test]
#[ignore]
fn context_carries_config_fields_subprocess() {
if !is_subprocess() {
return;
}
let dir = tempfile::tempdir().unwrap();
let pidfile = dir.path().join("test.pid");
let stdout = dir.path().join("out.log");
let mut config = DaemonConfig::new();
config
.pidfile(&pidfile)
.stdout(&stdout)
.user("nobody")
.group("nogroup")
.foreground(true)
.close_fds(false);
let mut forker = NullForker::new(vec![], Ok(()));
let ctx = run_inner(&config, &mut forker).unwrap();
let debug = format!("{:?}", ctx);
assert!(
debug.contains("test.pid"),
"context should contain pidfile path"
);
assert!(
debug.contains("out.log"),
"context should contain stdout path"
);
assert!(debug.contains("nobody"), "context should contain user");
assert!(debug.contains("nogroup"), "context should contain group");
}
}