use std::io::Read as _;
use std::process::{Command, Output, Stdio};
#[cfg(any(target_os = "macos", test))]
use super::DISABLE_ENV;
#[cfg(target_os = "macos")]
use super::macos;
pub struct StdoutPipedSpawn {
pub id: u32,
pub(super) stdout: Box<dyn std::io::Read + Send>,
pub(super) handle: StdoutPipedHandle,
}
pub(super) enum StdoutPipedHandle {
Native(std::process::Child),
#[cfg(target_os = "macos")]
Disclaimed(libc::pid_t),
}
impl StdoutPipedSpawn {
pub fn wait_with_output(mut self) -> std::io::Result<Output> {
let mut stdout = Vec::new();
self.stdout.read_to_end(&mut stdout)?;
let status = match self.handle {
StdoutPipedHandle::Native(mut child) => child.wait()?,
#[cfg(target_os = "macos")]
StdoutPipedHandle::Disclaimed(pid) => macos::wait_for(pid)?,
};
Ok(Output {
status,
stdout,
stderr: Vec::new(),
})
}
}
pub fn disclaimed_stdout_piped_spawn(mut cmd: Command) -> std::io::Result<StdoutPipedSpawn> {
#[cfg(target_os = "macos")]
{
if std::env::var_os(DISABLE_ENV).is_none() {
return macos::spawn_stdout_piped_disclaimed(&cmd);
}
}
cmd.stdout(Stdio::piped()).stderr(Stdio::null());
let mut child = cmd.spawn()?;
let id = child.id();
let stdout = child
.stdout
.take()
.ok_or_else(|| std::io::Error::other("child stdout pipe was not opened"))?;
Ok(StdoutPipedSpawn {
id,
stdout: Box::new(stdout),
handle: StdoutPipedHandle::Native(child),
})
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
#[serial_test::serial]
fn disclaimed_stdout_piped_spawn_captures_and_waits() {
let mut cmd = std::process::Command::new("/bin/sh");
cmd.args([
"-c",
"echo captured-out; echo should-be-discarded >&2; exit 5",
]);
let spawned = disclaimed_stdout_piped_spawn(cmd).unwrap();
assert!(spawned.id > 0, "pid must be available before waiting");
let out = spawned.wait_with_output().unwrap();
assert_eq!(out.status.code(), Some(5));
assert_eq!(String::from_utf8_lossy(&out.stdout), "captured-out\n");
assert!(out.stderr.is_empty());
}
#[test]
#[serial_test::serial]
fn disclaimed_stdout_piped_spawn_id_allows_watchdog_kill_before_wait() {
let mut cmd = std::process::Command::new("/bin/sleep");
cmd.arg("30");
let spawned = disclaimed_stdout_piped_spawn(cmd).unwrap();
let pid = spawned.id;
assert!(pid > 0);
let killed = unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
assert_eq!(killed, 0, "SIGKILL must reach the child by its exposed pid");
let out = spawned.wait_with_output().unwrap();
assert!(!out.status.success());
}
#[test]
#[serial_test::serial]
fn disclaimed_stdout_piped_spawn_reports_spawn_error_for_missing_binary() {
let cmd =
std::process::Command::new("/nonexistent/definitely-not-a-real-binary-3267-stdout");
let err = disclaimed_stdout_piped_spawn(cmd)
.err()
.expect("spawning a missing binary must error, not hang or panic");
assert!(matches!(
err.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::Other
));
}
#[test]
#[serial_test::serial]
fn disclaimed_stdout_piped_spawn_disable_env_forces_plain_path() {
unsafe { std::env::set_var(DISABLE_ENV, "1") };
let mut cmd = std::process::Command::new("/bin/sh");
cmd.args(["-c", "echo out; exit 3"]);
let spawned = disclaimed_stdout_piped_spawn(cmd).unwrap();
let out = spawned.wait_with_output().unwrap();
unsafe { std::env::remove_var(DISABLE_ENV) };
assert_eq!(out.status.code(), Some(3));
assert_eq!(String::from_utf8_lossy(&out.stdout), "out\n");
}
}
#[cfg(test)]
mod native_tests {
use super::*;
#[test]
#[serial_test::serial]
fn disclaimed_stdout_piped_spawn_native_path_round_trips() {
if cfg!(target_os = "windows") {
return; }
unsafe { std::env::set_var(DISABLE_ENV, "1") };
let mut cmd = std::process::Command::new("/bin/sh");
cmd.args(["-c", "echo native-stdout; exit 4"]);
let spawned = disclaimed_stdout_piped_spawn(cmd).unwrap();
unsafe { std::env::remove_var(DISABLE_ENV) };
assert!(spawned.id > 0);
let out = spawned.wait_with_output().unwrap();
assert_eq!(String::from_utf8_lossy(&out.stdout), "native-stdout\n");
assert_eq!(out.status.code(), Some(4));
}
}