#![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;
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(config: &DaemonConfig) -> Result<DaemonContext, DaemonizeError> {
config.validate()?;
daemonize_inner(config, &mut RealForker)
}
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
fn single_threaded_violation(count: usize) -> Option<String> {
(count != 1).then(|| {
format!(
"daemonize_checked: {count} threads running (expected 1). \
Call daemonize 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 fn daemonize_checked(config: &DaemonConfig) -> Result<DaemonContext, DaemonizeError> {
let count = thread_count::count()
.expect("daemonize_checked: cannot determine thread count to verify single-threadedness");
if let Some(msg) = single_threaded_violation(count) {
panic!("{msg}");
}
#[allow(unsafe_code)]
unsafe {
daemonize(config)
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
#[deprecated(
note = "daemonize_checked cannot verify the thread count on this target. \
Call `unsafe { daemonize(&config) }` and ensure the process is \
single-threaded yourself."
)]
pub fn daemonize_checked(_config: &DaemonConfig) -> Result<DaemonContext, DaemonizeError> {
panic!(
"daemonize_checked is unsupported on this target (cannot query the thread \
count); call `unsafe {{ daemonize(&config) }}` and ensure the process is \
single-threaded yourself"
)
}
#[allow(unsafe_code)]
pub(crate) 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),
};
match (unsafe { forker.fork() })? {
ForkResult::Parent { .. } => {
drop(pipe_wr);
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 { .. }) => {
drop(pipe_wr);
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) => {
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 = match config.lockfile.as_ref() {
Some(path) => Some(steps::open_and_lock(path)?),
None => None,
};
if let Some(ref pidfile_path) = config.pidfile {
steps::write_pidfile(
pidfile_path,
config.lockfile.as_deref().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(1).is_none(),
"exactly one thread is the single-threaded case"
);
assert!(
single_threaded_violation(0).is_some(),
"an anomalous 0 must fail closed, not green-light a fork"
);
let msg = single_threaded_violation(2).expect("2 threads is a violation");
assert!(
msg.contains("2 threads running (expected 1)"),
"message should name the count and expectation, got: {msg}"
);
}
#[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 = daemonize_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(|| {
daemonize_inner(&config, &mut forker)
}));
assert!(result.is_err()); }
#[test]
fn second_fork_parent_exits() {
let config = DaemonConfig::new();
let mut forker = NullForker::second_parent();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
daemonize_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 = daemonize_inner(&config, &mut forker);
assert!(matches!(result, Err(DaemonizeError::ForkFailed(_))));
}
#[test]
fn setsid_fails_exits() {
let config = DaemonConfig::new();
let mut forker = NullForker::setsid_fails();
let result = catch_unwind(std::panic::AssertUnwindSafe(|| {
daemonize_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(|| {
daemonize_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(|| {
daemonize_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]
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 = daemonize_inner(&config, &mut forker);
let ctx = result.expect("foreground daemonize_inner should succeed");
assert!(ctx.lockfile_fd().is_none());
}
#[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 = daemonize_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 = daemonize_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 = daemonize_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");
}
}