fork 0.7.0

Library for creating a new process detached from the controlling terminal (daemon)
Documentation
//! Daemon-specific integration tests
//!
//! This module tests the `daemon()` function which creates a detached background process.
//! These tests verify:
//! - Process detachment and proper PID management
//! - Directory handling (chdir vs nochdir)
//! - Process group and session management
//! - File descriptor handling (noclose option)
//! - Command execution in daemon context
//! - Absence of controlling terminal
//!
//! Note: These tests fork twice (the daemon pattern) so they run in separate
//! processes to avoid terminating the test runner when `daemon()` calls `exit(0)`.

#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::panic)]
#![allow(clippy::match_wild_err_arm)]
#![allow(clippy::similar_names)]
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::indexing_slicing)]

mod common;

use std::{
    env, fs,
    process::{Command, exit},
};

use fork::{Fork, daemon, fork};

use common::{get_unique_test_dir, setup_test_dir, wait_for_file};

#[test]
fn test_daemon_creates_detached_process() {
    // Tests that daemon() successfully creates a detached background process
    // Expected behavior:
    // 1. Parent process forks
    // 2. First child creates new session and forks again
    // 3. First child exits (daemon() calls exit(0))
    // 4. Grandchild (daemon) is detached and writes its PID
    // 5. Daemon changes to root directory (nochdir=false)
    // 6. Daemon has valid PID > 0
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_creates_detached"));
    let marker_file = test_dir.join("daemon.marker");

    // Fork the test to avoid daemon() calling exit(0) on parent
    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            // Parent waits for marker file to be created
            assert!(
                wait_for_file(&marker_file, 500),
                "Daemon should have created marker file"
            );

            // Read PID from marker file
            let content = fs::read_to_string(&marker_file).expect("Failed to read marker file");
            let daemon_pid: i32 = content.trim().parse().expect("Failed to parse PID");
            assert!(daemon_pid > 0, "Daemon PID should be positive");

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            // Child calls daemon()
            if let Ok(Fork::Child) = daemon(false, true) {
                // This is the daemon process
                // Write our PID to marker file
                let pid = unsafe { libc::getpid() };
                fs::write(&marker_file, format!("{}", pid)).expect("Failed to write marker file");

                // Verify we're in root directory
                let current = env::current_dir().expect("Failed to get current dir");
                assert_eq!(current.to_str(), Some("/"));

                exit(0);
            }
            // Parent of daemon exits (daemon() calls exit(0) for us)
        }
    }
}

#[test]
fn test_daemon_with_nochdir() {
    // Tests that daemon(nochdir=true) preserves the current working directory
    // Expected behavior:
    // 1. Test changes to a specific directory before calling daemon()
    // 2. daemon(true, true) is called (nochdir=true, noclose=true)
    // 3. Daemon process should remain in the same directory (not /)
    // 4. Daemon writes current directory to file for verification
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_nochdir"));
    let marker_file = test_dir.join("nochdir.marker");

    // Change to test directory
    env::set_current_dir(&test_dir).expect("Failed to change directory");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(
                wait_for_file(&marker_file, 500),
                "Daemon should have created marker file"
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            if let Ok(Fork::Child) = daemon(true, true) {
                // Daemon with nochdir=true should preserve directory
                let current = env::current_dir().expect("Failed to get current dir");

                // Write confirmation to marker file
                fs::write(&marker_file, format!("{}", current.display()))
                    .expect("Failed to write marker file");

                // Directory should still be test_dir, not root
                assert_ne!(current.to_str(), Some("/"));

                exit(0);
            }
        }
    }
}

#[test]
fn test_daemon_process_group() {
    // Tests that daemon creates proper process group structure
    // Expected behavior:
    // 1. daemon() performs double-fork pattern
    // 2. After double-fork, daemon is NOT a session leader (PID != PGID)
    // 3. This prevents daemon from acquiring a controlling terminal
    // 4. Both PID and PGID are positive values
    // 5. Daemon writes PID,PGID to file for verification
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_process_group"));
    let marker_file = test_dir.join("pgid.marker");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(
                wait_for_file(&marker_file, 500),
                "Daemon should have created marker file"
            );

            // Read and verify process group info
            let content = fs::read_to_string(&marker_file).expect("Failed to read marker file");
            let parts: Vec<&str> = content.trim().split(',').collect();
            assert_eq!(parts.len(), 2);

            let pid: i32 = parts[0].parse().expect("Failed to parse PID");
            let pgid: i32 = parts[1].parse().expect("Failed to parse PGID");

            // Daemon (after double-fork) should NOT be session leader
            // but should be in a new process group
            assert!(pid > 0, "PID should be positive");
            assert!(pgid > 0, "PGID should be positive");
            assert_ne!(
                pid, pgid,
                "Daemon (after double-fork) should NOT be session leader"
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            if let Ok(Fork::Child) = daemon(false, true) {
                let pid = unsafe { libc::getpid() };
                let pgid = unsafe { libc::getpgrp() };

                fs::write(&marker_file, format!("{},{}", pid, pgid))
                    .expect("Failed to write marker file");

                exit(0);
            }
        }
    }
}

#[test]
fn test_daemon_with_command_execution() {
    // Tests that daemon can execute commands successfully
    // Expected behavior:
    // 1. Daemon process is created
    // 2. Daemon executes a shell command
    // 3. Command output is written to a file
    // 4. Parent can verify command executed correctly
    // 5. Tests real-world daemon usage pattern
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_command_exec"));
    let output_file = test_dir.join("command.output");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(
                wait_for_file(&output_file, 500),
                "Command output file should exist"
            );

            let content = fs::read_to_string(&output_file).expect("Failed to read output file");
            assert!(
                content.contains("hello from daemon"),
                "Output should contain expected text"
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            if let Ok(Fork::Child) = daemon(false, true) {
                // Execute a command in the daemon
                Command::new("sh")
                    .arg("-c")
                    .arg(format!(
                        "echo 'hello from daemon' > {}",
                        output_file.display()
                    ))
                    .output()
                    .expect("Failed to execute command");

                exit(0);
            }
        }
    }
}

#[test]
fn test_daemon_no_controlling_terminal() {
    // Tests that daemon has no controlling terminal
    // Expected behavior:
    // 1. Daemon process is created via double-fork + setsid()
    // 2. Daemon tries to open /dev/tty (the POSIX controlling terminal device)
    // 3. open() should fail because the daemon has no controlling terminal
    // 4. This confirms daemon is properly detached
    // 5. Critical for background service behavior
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_no_tty"));
    let tty_file = test_dir.join("tty.info");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(wait_for_file(&tty_file, 500), "TTY info file should exist");

            let content = fs::read_to_string(&tty_file).expect("Failed to read tty file");
            assert_eq!(
                content.trim(),
                "no_ctty",
                "Daemon should have no controlling terminal, got: {}",
                content
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            if let Ok(Fork::Child) = daemon(false, true) {
                // The POSIX way to check for a controlling terminal:
                // opening /dev/tty fails when the process has none.
                let fd =
                    unsafe { libc::open(c"/dev/tty".as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
                let result = if fd == -1 {
                    "no_ctty"
                } else {
                    unsafe { libc::close(fd) };
                    "has_ctty"
                };

                fs::write(&tty_file, result).expect("Failed to write tty file");
                exit(0);
            }
        }
    }
}

#[test]
fn test_daemon_never_returns_parent() {
    // Tests that daemon() never returns Ok(Fork::Parent(_))
    // Expected behavior:
    // 1. daemon() performs double-fork internally
    // 2. Both parent processes call _exit(0) and never return
    // 3. Only Ok(Fork::Child) is ever returned to the caller
    // 4. The daemon writes "child" to a marker file to confirm
    // 5. If Fork::Parent were ever returned, "parent" would be written instead
    let test_dir = setup_test_dir(get_unique_test_dir("daemon_never_returns_parent"));
    let marker_file = test_dir.join("result.marker");

    match fork().expect("Failed to fork") {
        Fork::Parent(_) => {
            assert!(
                wait_for_file(&marker_file, 500),
                "Result marker file should exist"
            );

            let content = fs::read_to_string(&marker_file).expect("Failed to read marker file");
            assert_eq!(
                content.trim(),
                "child",
                "daemon() should only return Fork::Child, never Fork::Parent"
            );

            // Cleanup
            let _ = fs::remove_dir_all(&test_dir);
        }
        Fork::Child => {
            match daemon(false, true) {
                Ok(Fork::Child) => {
                    fs::write(&marker_file, "child").expect("Failed to write marker");
                    exit(0);
                }
                Ok(Fork::Parent(_)) => {
                    // This arm should be unreachable
                    fs::write(&marker_file, "parent").expect("Failed to write marker");
                    exit(1);
                }
                Err(_) => {
                    fs::write(&marker_file, "error").expect("Failed to write marker");
                    exit(2);
                }
            }
        }
    }
}