running-process-platform-internal 4.10.6

Blessed platform process operations for running-process (implementation detail)
Documentation
use super::*;
use std::io::Read as _;
use std::process::Stdio;

struct TracedChildCleanup(TracedChild);

impl std::ops::Deref for TracedChildCleanup {
    type Target = TracedChild;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl std::ops::DerefMut for TracedChildCleanup {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl Drop for TracedChildCleanup {
    fn drop(&mut self) {
        let _ = self.0.kill();
        let deadline = std::time::Instant::now() + Duration::from_secs(2);
        while std::time::Instant::now() < deadline {
            match self.0.try_wait_code() {
                Ok(Some(_)) | Err(_) => return,
                Ok(None) => std::thread::sleep(Duration::from_millis(5)),
            }
        }
    }
}

#[test]
fn proc_helpers_describe_the_current_process_and_reject_missing_pids() {
    let pid = std::process::id();
    assert!(read_executable(pid).is_some());
    assert!(read_argv(pid).is_some_and(|args| !args.is_empty()));
    assert!(process_start_key(pid).is_some());
    assert_eq!(thread_group_id(pid), Some(pid));
    let _ = read_syscall_pointers(pid);

    let missing = u32::MAX;
    assert!(read_executable(missing).is_none());
    assert!(read_argv(missing).is_none());
    assert!(process_start_key(missing).is_none());
    assert!(thread_group_id(missing).is_none());
    assert_eq!(read_syscall_pointers(missing), (None, None));
}

#[test]
fn event_and_loss_helpers_preserve_trace_identity() {
    let tracee = Tracee {
        parent_pid: Some(7),
        parent_start_key: Some(70),
        start_key: Some(420),
        process_leader: true,
        executable: Some("/bin/echo".into()),
        argv: Some(vec![OsString::from("echo"), OsString::from("hello")]),
        origin: Some(TraceOriginArtifact::default()),
    };
    let event = event_for(3, 42, &tracee, ExactTraceEventKind::Exec);
    assert_eq!(event.sequence, 3);
    assert_eq!(event.pid, 42);
    assert_eq!(event.parent_pid, Some(7));
    assert_eq!(event.executable.as_deref(), Some(std::path::Path::new("/bin/echo")));

    let emitted = Mutex::new(Vec::new());
    emit_loss(
        &|event| emitted.lock().unwrap().push(event),
        4,
        43,
        Some(&tracee),
        "coverage loss".into(),
    );
    let emitted = emitted.into_inner().unwrap();
    assert_eq!(emitted.len(), 1);
    assert!(matches!(
        &emitted[0].kind,
        ExactTraceEventKind::Loss { reason } if reason == "coverage loss"
    ));

    let without_tracee = Mutex::new(Vec::new());
    emit_loss(
        &|event| without_tracee.lock().unwrap().push(event),
        5,
        44,
        None,
        "unknown".into(),
    );
    assert_eq!(without_tracee.into_inner().unwrap()[0].parent_pid, None);
}

#[test]
fn origin_capture_handles_live_and_missing_processes() {
    let pid = std::process::id();
    let live = capture_origin(pid);
    assert_eq!(live.origin_pid, pid);
    assert_eq!(live.thread_id, pid);
    assert!(!live.architecture.is_empty());
    assert!(live.register_format.starts_with("linux-nt-prstatus-"));
    assert!(live.executable.is_some());
    assert!(!live.module_map.is_empty());

    let missing = 2_147_483_647;
    let absent = capture_origin(missing);
    assert_eq!(absent.origin_pid, missing);
    assert_eq!(absent.thread_id, missing);
    assert!(absent.executable.is_none());
    assert!(absent.registers.is_empty());
    assert!(absent.stack.is_empty());
    assert!(absent.module_map.is_empty());
    assert!(!absent.module_map_truncated);
}

#[test]
fn root_completion_and_ptrace_errors_are_observable() {
    let shared = Shared {
        state: Mutex::new(RootState::default()),
        wake: Condvar::new(),
    };
    finish_root(&shared, -9);
    let state = shared.state.lock().unwrap();
    assert_eq!(state.exit_code, Some(-9));
    assert!(state.done);
    drop(state);

    assert!(ptrace_value(PtraceRequest::MAX, u32::MAX, 0, 0).is_err());
    detach_all(std::iter::empty());
}

#[test]
fn fatal_trace_cleanup_reports_loss_reaps_root_and_wakes_waiters() {
    let mut child = std::process::Command::new("/bin/sh")
        .args(["-c", "sleep 30"])
        .spawn()
        .expect("spawn trace-cleanup fixture");
    let root_pid = child.id();
    let missing_pid = 2_147_483_647;
    let tracee = Tracee {
        parent_pid: Some(root_pid),
        parent_start_key: None,
        start_key: None,
        process_leader: true,
        executable: None,
        argv: None,
        origin: None,
    };
    let tracees = HashMap::from([(root_pid, tracee.clone()), (missing_pid, tracee.clone())]);
    let shared = Shared {
        state: Mutex::new(RootState::default()),
        wake: Condvar::new(),
    };
    let events = Mutex::new(Vec::new());
    abort_runtime_trace(
        &mut child,
        &shared,
        &|event| events.lock().unwrap().push(event),
        9,
        missing_pid,
        Some(&tracee),
        &tracees,
        "forced coverage cleanup".into(),
    );
    assert!(child.try_wait().unwrap().is_some());
    let state = shared.state.lock().unwrap();
    assert!(state.done);
    assert_eq!(state.tracer_error.as_deref(), Some("forced coverage cleanup"));
    drop(state);
    assert!(matches!(
        &events.into_inner().unwrap()[0].kind,
        ExactTraceEventKind::Loss { reason } if reason == "forced coverage cleanup"
    ));

    let handle = TracedChild {
        pid: missing_pid,
        shared: Arc::new(shared),
        stdin: None,
        stdout: None,
        stderr: None,
    };
    assert_eq!(
        handle.try_wait_code().unwrap_err().to_string(),
        "forced coverage cleanup"
    );
}

#[test]
fn exact_trace_round_trip_exposes_stdio_exit_and_idempotent_kill() {
    let events = Arc::new(Mutex::new(Vec::new()));
    let completed = Arc::new(std::sync::atomic::AtomicBool::new(false));
    let mut command = std::process::Command::new("/bin/sh");
    command
        .args([
            "-c",
            "printf trace-out; printf trace-err >&2; /bin/sh -c 'exit 0' & wait; exit 7",
        ])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    let event_sink = Arc::clone(&events);
    let complete_sink = Arc::clone(&completed);
    let mut child = start_exact_trace(
        command,
        Box::new(move |event| event_sink.lock().unwrap().push(event)),
        Box::new(move || {
            complete_sink.store(true, std::sync::atomic::Ordering::Release);
        }),
    )
    .expect("start exact trace");
    let deadline = std::time::Instant::now() + Duration::from_secs(5);
    assert_ne!(child.id(), 0);
    drop(child.take_stdin());
    let exit_code = loop {
        let code = child.try_wait_code().expect("trace state");
        if let Some(code) = code.filter(|_| {
            completed.load(std::sync::atomic::Ordering::Acquire)
        }) {
            break code;
        }
        assert!(std::time::Instant::now() < deadline, "trace did not complete");
        std::thread::sleep(Duration::from_millis(5));
    };
    assert_eq!(exit_code, 7);

    let mut stdout = child.take_stdout().expect("stdout");
    let mut stderr = child.take_stderr().expect("stderr");
    let mut stdout_text = String::new();
    let mut stderr_text = String::new();
    stdout.read_to_string(&mut stdout_text).unwrap();
    stderr.read_to_string(&mut stderr_text).unwrap();
    assert_eq!(stdout_text, "trace-out");
    assert_eq!(stderr_text, "trace-err");
    child.kill().expect("already exited is harmless");
    assert!(events
        .lock()
        .unwrap()
        .iter()
        .any(|event| matches!(event.kind, ExactTraceEventKind::Exec)));
}

#[test]
fn exact_trace_can_kill_a_running_root() {
    let completed = Arc::new(std::sync::atomic::AtomicBool::new(false));
    let completed_sink = Arc::clone(&completed);
    let mut command = std::process::Command::new("/bin/sleep");
    command.arg("30");
    let mut child = TracedChildCleanup(start_exact_trace(
        command,
        Box::new(|_| {}),
        Box::new(move || {
            completed_sink.store(true, std::sync::atomic::Ordering::Release);
        }),
    )
    .expect("start killable exact trace"));
    child.kill().expect("kill traced root");
    let deadline = std::time::Instant::now() + Duration::from_secs(5);
    loop {
        if let Some(code) = child.try_wait_code().expect("trace state") {
            assert_eq!(code, -9);
            break;
        }
        assert!(std::time::Instant::now() < deadline, "trace kill did not complete");
        std::thread::sleep(Duration::from_millis(5));
    }
    assert!(completed.load(std::sync::atomic::Ordering::Acquire));
}

#[test]
fn failed_setup_cleanup_terminates_and_reaps_the_child() {
    let mut child = std::process::Command::new("/bin/sh")
        .args(["-c", "sleep 30"])
        .spawn()
        .expect("spawn child");
    cleanup_failed_setup(&mut child);
    assert!(child.try_wait().unwrap().is_some());
}