#![deny(unsafe_code)]
mod config;
mod context;
mod error;
pub(crate) mod forker;
pub(crate) mod unsafe_ops;
mod steps;
pub(crate) mod util;
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};
#[allow(unsafe_code)]
pub unsafe fn daemonize(config: &DaemonConfig) -> Result<DaemonContext, DaemonizeError> {
config.validate()?;
daemonize_inner(config, &mut RealForker)
}
#[cfg(target_os = "linux")]
pub fn daemonize_checked(config: &DaemonConfig) -> Result<DaemonContext, DaemonizeError> {
let status = std::fs::read_to_string("/proc/self/status")
.expect("failed to read /proc/self/status: cannot verify thread count");
let threads = status
.lines()
.find(|line| line.starts_with("Threads:"))
.expect("failed to find Threads: line in /proc/self/status");
let count: usize = threads
.split_whitespace()
.nth(1)
.expect("malformed Threads: line in /proc/self/status")
.parse()
.expect("failed to parse thread count from /proc/self/status");
if count > 1 {
panic!(
"daemonize_checked: {} threads running (expected 1). \
Call daemonize before spawning threads, async runtimes, \
or libraries with background threads.",
count
);
}
#[allow(unsafe_code)]
unsafe {
daemonize(config)
}
}
#[allow(unsafe_code)]
pub(crate) fn daemonize_inner(
config: &DaemonConfig,
forker: &mut impl Forker,
) -> Result<DaemonContext, DaemonizeError> {
let foreground = config.foreground;
let pipe_wr = if foreground {
None
} else {
let pipe = forker.create_notification_pipe();
let (pipe_rd, pipe_wr) = match pipe {
Some((rd, wr)) => (Some(rd), Some(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);
}
}
let pipe_wr_ref = &pipe_wr;
if let Err(e) = forker.setsid() {
write_error_to_pipe(pipe_wr_ref, &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) => {
write_error_to_pipe(pipe_wr_ref, &e);
forker.exit(e.exit_code() as i32);
}
}
pipe_wr
};
macro_rules! post_fork_try {
($result:expr) => {
match $result {
Ok(val) => val,
Err(e) => {
write_error_to_pipe(&pipe_wr, &e);
forker.exit(e.exit_code() as i32);
}
}
};
}
steps::set_umask(config.umask);
post_fork_try!(steps::change_dir(&config.chdir));
steps::redirect_to_devnull(!foreground);
#[allow(clippy::manual_map)]
let lockfile = match config.lockfile.as_ref() {
Some(path) => Some(post_fork_try!(steps::open_and_lock(path))),
None => None,
};
if let Some(ref pidfile_path) = config.pidfile {
post_fork_try!(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() {
post_fork_try!(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_raw_fd());
}
steps::close_inherited_fds(&skip_fds);
}
Ok(DaemonContext::new(
lockfile,
pipe_wr,
config.pidfile.clone(),
config.lockfile.clone(),
config.stdout.clone(),
config.stderr.clone(),
config.user.clone(),
config.group.clone(),
config.cleanup_on_drop,
))
}
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);
if buf.is_empty() {
forker.exit(0);
}
let code = buf[0];
if code == 0x00 {
forker.exit(0);
}
let msg = String::from_utf8_lossy(&buf[1..]);
eprintln!("{msg}");
forker.exit(code as i32);
}
fn write_error_to_pipe(pipe_wr: &Option<OwnedFd>, err: &DaemonizeError) {
if let Some(ref fd) = pipe_wr {
let msg = err.to_string();
let code = err.exit_code();
let mut buf = Vec::with_capacity(1 + msg.len());
buf.push(code);
buf.extend_from_slice(msg.as_bytes());
let _ = nix::unistd::write(fd, &buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forker::null_forker::NullForker;
use std::panic::catch_unwind;
fn run_subprocess(test_name: &str) {
let exe = std::env::current_exe().unwrap();
let status = std::process::Command::new(exe)
.arg("--exact")
.arg(test_name)
.arg("--nocapture")
.env("__DAEMONIZE_SUBPROCESS_TEST", "1")
.status()
.unwrap();
assert!(status.success(), "subprocess test failed: {status}");
}
fn is_subprocess() -> bool {
std::env::var("__DAEMONIZE_SUBPROCESS_TEST").is_ok()
}
#[test]
fn both_forks_child_succeeds() {
run_subprocess("tests::both_forks_child_succeeds_subprocess");
}
#[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 write_error_to_pipe_noop_with_none() {
write_error_to_pipe(&None, &DaemonizeError::ForkFailed("test".into()));
}
#[test]
fn write_error_to_pipe_writes_protocol() {
let (rd, wr) = nix::unistd::pipe().unwrap();
let pipe_wr = Some(wr);
let err = DaemonizeError::ForkFailed("test error".into());
write_error_to_pipe(&pipe_wr, &err);
drop(pipe_wr);
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_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_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_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_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");
}
}