use std::{
error::Error,
fs,
io::{self, Read},
os::fd::AsRawFd,
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
thread,
time::{Duration, Instant},
};
use fork::{
CheckedDaemon, DaemonCleanup, DaemonError, DaemonOptions, DaemonProcess, DaemonStage, Signal,
checked_daemon, pipe_cloexec, signal_process_group,
};
const STARTUP_TIMEOUT: Duration = Duration::from_secs(3);
const POLL_INTERVAL: Duration = Duration::from_millis(5);
static NEXT_PATH: AtomicU64 = AtomicU64::new(0);
static INTERRUPTS: AtomicU64 = AtomicU64::new(0);
struct SignalActionGuard {
signal: libc::c_int,
previous: libc::sigaction,
}
impl Drop for SignalActionGuard {
fn drop(&mut self) {
unsafe {
libc::sigaction(self.signal, &raw const self.previous, std::ptr::null_mut());
}
}
}
struct TestDirectory(PathBuf);
impl TestDirectory {
fn new() -> io::Result<Self> {
let sequence = NEXT_PATH.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"fork-checked-daemon-{}-{sequence}",
std::process::id()
));
fs::create_dir(&path)?;
Ok(Self(path))
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn parent_returns_only_after_detached_daemon_is_ready() -> Result<(), Box<dyn Error>> {
let directory = TestDirectory::new()?;
let marker = directory.path().join("ready");
let mut options = DaemonOptions::new();
options
.current_directory(directory.path())?
.redirect_standard_io_to_null()?;
match checked_daemon(options, STARTUP_TIMEOUT)? {
CheckedDaemon::Parent(process) => {
assert_ne!(process.process().get(), std::process::id().cast_signed());
assert!(marker.is_file());
wait_for_group_exit(process)?;
}
CheckedDaemon::Daemon(notifier) => {
if !standard_io_is_dev_null()
|| fs::write("ready", b"initialized").is_err()
|| notifier.notify_ready().is_err()
{
daemon_exit(2);
}
daemon_exit(0);
}
}
Ok(())
}
#[test]
fn failure_after_detachment_preserves_stage_and_errno() -> Result<(), Box<dyn Error>> {
let missing =
std::env::temp_dir().join(format!("fork-missing-daemon-dir-{}", std::process::id()));
let mut options = DaemonOptions::new();
options.current_directory(&missing)?;
match checked_daemon(options, STARTUP_TIMEOUT) {
Err(DaemonError::OperatingSystem {
stage,
error,
cleanup_pending,
}) => {
assert_eq!(stage, DaemonStage::CurrentDirectory);
assert_eq!(error.raw_os_error(), Some(libc::ENOENT));
assert!(cleanup_pending.is_empty());
}
other => return Err(io::Error::other(format!("unexpected result: {other:?}")).into()),
}
Ok(())
}
#[test]
fn descriptor_plan_failure_is_reported_before_fork() -> Result<(), Box<dyn Error>> {
let pipe = pipe_cloexec()?;
let (_, writer) = pipe.into_parts();
let mut options = DaemonOptions::new();
options.map_descriptor(writer, libc::c_int::MAX)?;
match checked_daemon(options, STARTUP_TIMEOUT) {
Err(DaemonError::OperatingSystem {
stage,
error,
cleanup_pending,
}) => {
assert_eq!(stage, DaemonStage::Descriptors);
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(cleanup_pending.is_empty());
}
other => return Err(io::Error::other(format!("unexpected result: {other:?}")).into()),
}
Ok(())
}
#[test]
fn daemon_can_report_initialization_failure() -> Result<(), Box<dyn Error>> {
match checked_daemon(DaemonOptions::new(), STARTUP_TIMEOUT) {
Ok(CheckedDaemon::Daemon(notifier)) => {
notifier.fail_and_exit(&io::Error::from_raw_os_error(libc::EACCES));
}
Err(DaemonError::OperatingSystem {
stage,
error,
cleanup_pending,
}) => {
assert_eq!(stage, DaemonStage::Initialization);
assert_eq!(error.raw_os_error(), Some(libc::EACCES));
assert!(cleanup_pending.is_empty());
}
other => return Err(io::Error::other(format!("unexpected result: {other:?}")).into()),
}
Ok(())
}
#[test]
fn dropped_notifier_is_reported_as_abandoned_startup() -> Result<(), Box<dyn Error>> {
match checked_daemon(DaemonOptions::new(), STARTUP_TIMEOUT) {
Ok(CheckedDaemon::Daemon(notifier)) => {
drop(notifier);
daemon_exit(3);
}
Err(DaemonError::Abandoned { cleanup_pending }) => {
assert!(cleanup_pending.is_empty());
}
other => return Err(io::Error::other(format!("unexpected result: {other:?}")).into()),
}
Ok(())
}
#[test]
fn startup_timeout_kills_the_owned_daemon_group() -> Result<(), Box<dyn Error>> {
let timeout = Duration::from_millis(25);
match checked_daemon(DaemonOptions::new(), timeout) {
Ok(CheckedDaemon::Daemon(_notifier)) => loop {
unsafe {
libc::pause();
}
},
Err(DaemonError::TimedOut {
cleanup_pending, ..
}) => {
finish_pending_cleanup(cleanup_pending)?;
}
other => return Err(io::Error::other(format!("unexpected result: {other:?}")).into()),
}
Ok(())
}
#[test]
fn preserved_descriptor_survives_detachment() -> Result<(), Box<dyn Error>> {
let pipe = pipe_cloexec()?;
let (reader, writer) = pipe.into_parts();
let descriptor = writer.as_raw_fd();
let mut options = DaemonOptions::new();
options.preserve_descriptor(writer)?;
match checked_daemon(options, STARTUP_TIMEOUT)? {
CheckedDaemon::Parent(process) => {
let mut output = String::new();
std::fs::File::from(reader).read_to_string(&mut output)?;
assert_eq!(output, "preserved");
wait_for_group_exit(process)?;
}
CheckedDaemon::Daemon(notifier) => {
let written = unsafe { libc::write(descriptor, c"preserved".as_ptr().cast(), 9) };
if written != 9 || notifier.notify_ready().is_err() {
daemon_exit(4);
}
daemon_exit(0);
}
}
Ok(())
}
#[test]
fn startup_handshake_retries_repeated_eintr() -> Result<(), Box<dyn Error>> {
INTERRUPTS.store(0, Ordering::SeqCst);
let _signal_guard = install_signal_handler(libc::SIGUSR1)?;
let original = unsafe { libc::getpid() };
match checked_daemon(DaemonOptions::new(), STARTUP_TIMEOUT)? {
CheckedDaemon::Parent(process) => {
assert!(INTERRUPTS.load(Ordering::SeqCst) > 0);
wait_for_group_exit(process)?;
}
CheckedDaemon::Daemon(notifier) => {
for _ in 0..8 {
unsafe {
libc::kill(original, libc::SIGUSR1);
libc::usleep(1_000);
}
}
if notifier.notify_ready().is_err() {
daemon_exit(5);
}
daemon_exit(0);
}
}
Ok(())
}
fn finish_pending_cleanup(cleanup: DaemonCleanup) -> Result<(), Box<dyn Error>> {
if cleanup.intermediate().is_some() {
return Err(io::Error::other("intermediate child remained after timeout cleanup").into());
}
if let Some(group) = cleanup.process_group() {
let _ = signal_process_group(group, Signal::KILL);
let deadline = Instant::now() + STARTUP_TIMEOUT;
while group_exists(group.get()) {
if Instant::now() >= deadline {
return Err(io::Error::other("daemon group remained after timeout cleanup").into());
}
thread::sleep(POLL_INTERVAL);
}
}
Ok(())
}
fn wait_for_group_exit(process: DaemonProcess) -> Result<(), Box<dyn Error>> {
let deadline = Instant::now() + STARTUP_TIMEOUT;
while group_exists(process.process_group().get()) {
if Instant::now() >= deadline {
let _ = signal_process_group(process.process_group(), Signal::KILL);
return Err(io::Error::other("ready daemon did not exit").into());
}
thread::sleep(POLL_INTERVAL);
}
Ok(())
}
fn group_exists(group: libc::pid_t) -> bool {
loop {
let result = unsafe { libc::kill(-group, 0) };
if result == 0 {
return true;
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return error.raw_os_error() != Some(libc::ESRCH);
}
}
fn daemon_exit(code: libc::c_int) -> ! {
unsafe { libc::_exit(code) }
}
fn standard_io_is_dev_null() -> bool {
unsafe {
let mut expected: libc::stat = std::mem::zeroed();
if libc::stat(c"/dev/null".as_ptr(), &raw mut expected) == -1 {
return false;
}
for descriptor in libc::STDIN_FILENO..=libc::STDERR_FILENO {
let mut actual: libc::stat = std::mem::zeroed();
if libc::fstat(descriptor, &raw mut actual) == -1 || actual.st_rdev != expected.st_rdev
{
return false;
}
}
}
true
}
extern "C" fn record_interrupt(_signal: libc::c_int) {
INTERRUPTS.fetch_add(1, Ordering::SeqCst);
}
fn install_signal_handler(signal: libc::c_int) -> io::Result<SignalActionGuard> {
let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
let mut previous: libc::sigaction = unsafe { std::mem::zeroed() };
action.sa_sigaction = record_interrupt as *const () as usize;
action.sa_flags = 0;
unsafe {
if libc::sigemptyset(&raw mut action.sa_mask) == -1 {
return Err(io::Error::last_os_error());
}
if libc::sigaction(signal, &raw const action, &raw mut previous) == -1 {
return Err(io::Error::last_os_error());
}
}
Ok(SignalActionGuard { signal, previous })
}