ic-host-process 0.24.0

Native process observations, admitted execution and Git provenance
Documentation
//! Native regressions adapted from Backup's command-custody boundary.

use super::*;
use crate::test_support::Fixture;
use rustix::io::{FdFlags, fcntl_getfd};
use std::{
    os::unix::fs::{PermissionsExt, symlink},
    process::Stdio,
};

const DESCRIPTOR_ENV: &str = "HOST_TEST_CUSTODY_FD";
const ROOT_ENV: &str = "HOST_TEST_CUSTODY_ROOT";
const ROLE_ENV: &str = "HOST_TEST_CUSTODY_ROLE";
const TREE_CASE: &str = "custody::tests::owner_death_retains_custody_until_last_descendant_exits";

fn until(mut ready: impl FnMut() -> bool) {
    let deadline = Instant::now() + Duration::from_secs(15);
    while !ready() {
        assert!(Instant::now() < deadline, "custody fixture did not settle");
        thread::sleep(Duration::from_millis(5));
    }
}

fn wait_file(path: &Path) {
    until(|| path.is_file());
}

fn command_for(root: &Path, role: &str) -> Command {
    let mut command = Command::new(std::env::current_exe().unwrap());
    command
        .args(["--exact", TREE_CASE, "--nocapture"])
        .env(ROOT_ENV, root)
        .env(ROLE_ENV, role)
        .stdin(Stdio::null());
    command
}

#[test]
fn one_spawn_preserves_identity_permissions_and_owner_cloexec() {
    let fixture = Fixture::new();
    let path = fixture.root.join("sidecar");
    let mut custody = CommandCustody::acquire(&path).unwrap();
    let identity = custody.identity();
    assert_eq!(custody.path(), path);
    assert_eq!(
        fs::metadata(&path).unwrap().permissions().mode() & 0o777,
        0o600
    );
    assert!(
        fcntl_getfd(&custody.file)
            .unwrap()
            .contains(FdFlags::CLOEXEC)
    );
    let mut command = Command::new("/bin/sh");
    command.args(["-c", "test -f /dev/fd/$HOST_TEST_CUSTODY_FD"]);
    assert!(
        custody
            .spawn(command, DESCRIPTOR_ENV)
            .unwrap()
            .wait()
            .unwrap()
            .success()
    );
    assert!(
        fcntl_getfd(&custody.file)
            .unwrap()
            .contains(FdFlags::CLOEXEC)
    );
    assert!(matches!(
        custody.spawn(Command::new("unused"), DESCRIPTOR_ENV),
        Err(CustodyError::AlreadyDispatched { .. })
    ));
    let guard = custody
        .finish(Duration::from_secs(5), Duration::from_millis(5))
        .unwrap();
    assert_eq!(guard.identity(), identity);
    assert!(matches!(
        CommandCustody::acquire(&path),
        Err(CustodyError::InFlight { .. })
    ));
    drop(guard);
    assert_eq!(CommandCustody::acquire(&path).unwrap().identity(), identity);
}

#[test]
fn failed_spawn_consumes_allowance_and_closes_command_copy() {
    let fixture = Fixture::new();
    let mut custody = CommandCustody::acquire(&fixture.root.join("sidecar")).unwrap();
    assert!(
        matches!(custody.spawn(Command::new(fixture.root.join("absent")), DESCRIPTOR_ENV), Err(CustodyError::Io(e)) if e.kind() == io::ErrorKind::NotFound)
    );
    assert!(matches!(
        custody.spawn(Command::new("unused"), DESCRIPTOR_ENV),
        Err(CustodyError::AlreadyDispatched { .. })
    ));
    custody
        .finish(Duration::from_secs(5), Duration::from_millis(1))
        .unwrap();
}

#[test]
fn quiescence_drop_unlocks_even_with_a_transient_descriptor_copy() {
    let fixture = Fixture::new();
    let path = fixture.root.join("sidecar");
    let custody = CommandCustody::acquire(&path).unwrap();
    let identity = custody.identity();
    let guard = custody
        .finish(Duration::from_secs(5), Duration::from_millis(1))
        .unwrap();
    let retained = guard.file.try_clone().unwrap();
    assert!(matches!(
        QuiescenceGuard::acquire(&path, identity),
        Err(CustodyError::InFlight { .. })
    ));
    drop(guard);
    let fresh = QuiescenceGuard::acquire(&path, identity).unwrap();
    assert_eq!(FileIdentity::from_file(&retained).unwrap(), identity);
    drop(retained);
    assert!(matches!(
        QuiescenceGuard::acquire(&path, identity),
        Err(CustodyError::InFlight { .. })
    ));
    drop(fresh);
}

#[test]
fn replaced_missing_and_unsafe_sidecars_never_prove_quiescence() {
    let fixture = Fixture::new();
    let path = fixture.root.join("sidecar");
    let retained = fixture.root.join("original");
    let mut custody = CommandCustody::acquire(&path).unwrap();
    let identity = custody.identity();
    fs::rename(&path, &retained).unwrap();
    fs::write(&path, b"replacement evidence").unwrap();
    let replacement = CommandCustody::acquire(&path).unwrap();
    assert!(matches!(
        custody.spawn(Command::new("unused"), DESCRIPTOR_ENV),
        Err(CustodyError::IdentityChanged { .. })
    ));
    assert!(matches!(
        QuiescenceGuard::acquire(&path, identity),
        Err(CustodyError::IdentityChanged { .. })
    ));
    assert!(matches!(
        custody.finish(Duration::ZERO, Duration::from_millis(1)),
        Err(CustodyError::IdentityChanged { .. })
    ));
    assert_eq!(fs::read(&path).unwrap(), b"replacement evidence");
    drop(replacement);
    fs::remove_file(&path).unwrap();
    assert!(
        matches!(QuiescenceGuard::acquire(&path, identity), Err(CustodyError::Io(e)) if e.kind() == io::ErrorKind::NotFound)
    );
    assert!(!path.exists());
    symlink(&retained, &path).unwrap();
    assert!(matches!(
        QuiescenceGuard::acquire(&path, identity),
        Err(CustodyError::Lock(RegularFileLockError::NotRegular))
    ));
    fs::remove_file(&path).unwrap();
    fs::create_dir(&path).unwrap();
    assert!(matches!(
        CommandCustody::acquire(&path),
        Err(CustodyError::Lock(RegularFileLockError::NotRegular))
    ));
}

#[test]
fn existing_bytes_and_permissions_survive_and_missing_parents_are_not_created() {
    let fixture = Fixture::new();
    let path = fixture.root.join("sidecar");
    fs::write(&path, b"retained bytes").unwrap();
    fs::set_permissions(&path, fs::Permissions::from_mode(0o640)).unwrap();
    let original = FileIdentity::from_file(&File::open(&path).unwrap()).unwrap();
    let custody = CommandCustody::acquire(&path).unwrap();
    assert_eq!(custody.identity(), original);
    assert_eq!(fs::read(&path).unwrap(), b"retained bytes");
    assert_eq!(
        fs::metadata(&path).unwrap().permissions().mode() & 0o777,
        0o640
    );
    assert!(CommandCustody::acquire(&fixture.root.join("missing/sidecar")).is_err());
    assert!(!fixture.root.join("missing").exists());
    assert!(
        matches!(custody.finish(Duration::ZERO, Duration::ZERO), Err(CustodyError::Io(e)) if e.kind() == io::ErrorKind::InvalidInput)
    );
}

#[test]
fn owner_death_retains_custody_until_last_descendant_exits() {
    if let Some(root) = std::env::var_os(ROOT_ENV) {
        let root = PathBuf::from(root);
        match std::env::var(ROLE_ENV).unwrap().as_str() {
            "owner" => {
                let mut custody = CommandCustody::acquire(&root.join("sidecar")).unwrap();
                let mut child = custody
                    .spawn(command_for(&root, "direct"), DESCRIPTOR_ENV)
                    .unwrap();
                wait_file(&root.join("direct-ready"));
                fs::write(root.join("owner-ready"), b"ready").unwrap();
                wait_file(&root.join("never-release-owner"));
                child.wait().unwrap();
            }
            "direct" => {
                let descriptor = std::env::var(DESCRIPTOR_ENV).unwrap();
                assert!(
                    fs::metadata(format!("/dev/fd/{descriptor}"))
                        .unwrap()
                        .is_file()
                );
                let _descendant = command_for(&root, "descendant").spawn().unwrap();
                wait_file(&root.join("descendant-ready"));
                fs::write(root.join("direct-ready"), b"ready").unwrap();
                wait_file(&root.join("allow-direct-exit"));
                std::process::exit(0);
            }
            "descendant" => {
                let direct = rustix::process::getppid();
                fs::write(root.join("descendant-ready"), b"ready").unwrap();
                until(|| rustix::process::getppid() != direct);
                fs::write(root.join("direct-exited"), b"reparented").unwrap();
                wait_file(&root.join("allow-descendant-exit"));
            }
            "holder" => {
                fs::write(root.join("holder-ready"), b"ready").unwrap();
                wait_file(&root.join("allow-holder-exit"));
            }
            role => panic!("unknown custody role: {role}"),
        }
        return;
    }
    let fixture = Fixture::new();
    let root = &fixture.root;
    let path = root.join("sidecar");
    let mut owner = command_for(root, "owner").spawn().unwrap();
    wait_file(&root.join("owner-ready"));
    let identity = FileIdentity::from_file(&File::open(&path).unwrap()).unwrap();
    assert!(matches!(
        QuiescenceGuard::acquire(&path, identity),
        Err(CustodyError::InFlight { .. })
    ));
    owner.kill().unwrap();
    owner.wait().unwrap();
    assert!(matches!(
        QuiescenceGuard::acquire(&path, identity),
        Err(CustodyError::InFlight { .. })
    ));
    fs::write(root.join("allow-direct-exit"), b"exit").unwrap();
    wait_file(&root.join("direct-exited"));
    assert!(matches!(
        QuiescenceGuard::acquire(&path, identity),
        Err(CustodyError::InFlight { .. })
    ));
    fs::write(root.join("allow-descendant-exit"), b"exit").unwrap();
    let mut quiescent = None;
    until(|| match QuiescenceGuard::acquire(&path, identity) {
        Ok(guard) => {
            quiescent = Some(guard);
            true
        }
        Err(CustodyError::InFlight { .. }) => false,
        other => panic!("unexpected final custody: {other:?}"),
    });
    assert_eq!(quiescent.unwrap().identity(), identity);
}

#[test]
fn finish_stops_after_grace_while_an_inherited_holder_remains() {
    let fixture = Fixture::new();
    let path = fixture.root.join("sidecar");
    let mut custody = CommandCustody::acquire(&path).unwrap();
    let identity = custody.identity();
    let mut child = custody
        .spawn(command_for(&fixture.root, "holder"), DESCRIPTOR_ENV)
        .unwrap();
    wait_file(&fixture.root.join("holder-ready"));
    let started = Instant::now();
    assert!(matches!(
        custody.finish(Duration::from_millis(25), Duration::from_millis(5)),
        Err(CustodyError::InFlight { .. })
    ));
    assert!(started.elapsed() < Duration::from_secs(5));
    assert!(matches!(
        QuiescenceGuard::acquire(&path, identity),
        Err(CustodyError::InFlight { .. })
    ));
    fs::write(fixture.root.join("allow-holder-exit"), b"exit").unwrap();
    assert!(child.wait().unwrap().success());
    until(|| match QuiescenceGuard::acquire(&path, identity) {
        Ok(_) => true,
        Err(CustodyError::InFlight { .. }) => false,
        other => panic!("unexpected custody after exit: {other:?}"),
    });
}