Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/sandbox/tests/reap_grace.rs

//! **A PID 1's reaper leaves an owner's child to its owner** (lane lsm-shim, 2026-11-03).
//!
//! MEASURED on the LSM appliance kernel: Heimdall's runner spawns its shim and polls
//! `try_wait`; between two polls the PID-1 reaper's `waitpid(-1)` took the exit, and every
//! job read `setup: waiting for the shim: No child process (os error 10)`. The claim
//! (`appliance_pid1::claim`) covers only the engines that claim; Heimdall's runner, a
//! library, cannot. The reaper therefore leaves every exited child a grace before it takes
//! the status: an owner that waits gets its child's exit, an orphan is still reaped.
//!
//! One test function: the reaper looks at every child of this process, so the steps run
//! in one binary, one after another.

use std::process::Command;
use std::time::{Duration, Instant};

use holger_sandbox::reap::Reaper;

fn exited_child() -> std::process::Child {
    let child = Command::new("true").spawn().expect("spawn true");
    // Let it exit and stay a zombie.
    let pid = child.id() as i32;
    let t0 = Instant::now();
    while !holger_sandbox::reap::zombie_children()
        .expect("read /proc")
        .contains(&pid)
    {
        assert!(
            t0.elapsed() < Duration::from_secs(10),
            "child {pid} never exited"
        );
        std::thread::sleep(Duration::from_millis(5));
    }
    child
}

#[test]
fn the_reaper_leaves_an_owners_child_its_grace_and_reaps_the_abandoned() {
    // 1. An owner polling `try_wait` (Heimdall's run_shim) gets the exit, not ECHILD.
    let mut r = Reaper::new(Duration::from_secs(5));
    let mut owned = exited_child();
    let pass = r.pass(Instant::now(), &|_| false);
    assert!(
        pass.reaped.is_empty(),
        "the reaper took an owner's child: {:?}",
        pass.reaped
    );
    assert_eq!(pass.waiting, 1, "the exited child is waiting in its grace");
    let status = owned.try_wait().expect("the owner's try_wait: not ECHILD");
    assert!(
        status.is_some_and(|s| s.success()),
        "the owner reads its child's exit"
    );

    // 2. A child nobody waits for (an orphan, a dropped Child) is reaped once its grace is over.
    let mut r = Reaper::new(Duration::from_millis(50));
    let abandoned = exited_child();
    let pid = abandoned.id() as i32;
    std::mem::forget(abandoned);
    let first = r.pass(Instant::now(), &|_| false);
    assert!(first.reaped.is_empty(), "reaped inside its grace");
    std::thread::sleep(Duration::from_millis(80));
    let later = r.pass(Instant::now(), &|_| false);
    assert_eq!(
        later.reaped,
        vec![pid],
        "the abandoned child is reaped after its grace"
    );
    assert!(
        !holger_sandbox::reap::zombie_children()
            .unwrap()
            .contains(&pid),
        "no zombie left"
    );

    // 3. A claimed child is never taken, even past its grace.
    let mut r = Reaper::new(Duration::from_millis(1));
    let mut claimed = exited_child();
    let cpid = claimed.id() as i32;
    std::thread::sleep(Duration::from_millis(20));
    let pass = r.pass(Instant::now(), &|p| p == cpid);
    let pass2 = r.pass(Instant::now() + Duration::from_secs(1), &|p| p == cpid);
    assert!(
        pass.reaped.is_empty() && pass2.reaped.is_empty(),
        "a claimed child was taken"
    );
    assert!(claimed.try_wait().expect("claimed: not ECHILD").is_some());
}