use std::io::Read;
use std::process::{Command, ExitStatus, Stdio};
#[cfg(any(target_os = "macos", test))]
use super::DISABLE_ENV;
#[cfg(target_os = "macos")]
use super::macos;
pub struct StderrPipedSpawn {
pub stderr: Box<dyn Read + Send>,
pub(super) handle: StderrPipedHandle,
}
pub(super) enum StderrPipedHandle {
Native(std::process::Child),
#[cfg(target_os = "macos")]
Disclaimed(libc::pid_t),
}
impl StderrPipedSpawn {
pub fn wait(&mut self) -> std::io::Result<ExitStatus> {
match &mut self.handle {
StderrPipedHandle::Native(child) => child.wait(),
#[cfg(target_os = "macos")]
StderrPipedHandle::Disclaimed(pid) => macos::wait_for(*pid),
}
}
}
pub fn disclaimed_stderr_piped_spawn(mut cmd: Command) -> std::io::Result<StderrPipedSpawn> {
#[cfg(target_os = "macos")]
{
if std::env::var_os(DISABLE_ENV).is_none() {
return macos::spawn_stderr_piped_disclaimed(&cmd);
}
}
cmd.stdout(Stdio::null()).stderr(Stdio::piped());
let mut child = cmd.spawn()?;
let stderr = child
.stderr
.take()
.ok_or_else(|| std::io::Error::other("child stderr pipe was not opened"))?;
Ok(StderrPipedSpawn {
stderr: Box::new(stderr),
handle: StderrPipedHandle::Native(child),
})
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
#[test]
#[serial_test::serial]
fn disclaimed_stderr_piped_spawn_streams_and_waits() {
let mut cmd = std::process::Command::new("/bin/sh");
cmd.args([
"-c",
"echo should-be-discarded; echo captured-line >&2; exit 5",
]);
let mut spawned = disclaimed_stderr_piped_spawn(cmd).unwrap();
let mut stderr_buf = Vec::new();
std::io::Read::read_to_end(&mut spawned.stderr, &mut stderr_buf).unwrap();
assert_eq!(String::from_utf8_lossy(&stderr_buf), "captured-line\n");
let status = spawned.wait().unwrap();
assert_eq!(status.code(), Some(5));
}
#[test]
#[serial_test::serial]
fn disclaimed_stderr_piped_spawn_applies_cwd_and_env_override() {
let tmp = tempfile::TempDir::new().unwrap();
let tmp_path = std::fs::canonicalize(tmp.path()).unwrap();
let mut cmd = std::process::Command::new("/bin/sh");
cmd.current_dir(&tmp_path);
cmd.env("SPAWN_DISCLAIM_STDERR_TEST_VAR_3267", "hello-3267");
cmd.args([
"-c",
"printf '%s' \"$SPAWN_DISCLAIM_STDERR_TEST_VAR_3267\" >&2 && pwd > cwd.txt",
]);
let mut spawned = disclaimed_stderr_piped_spawn(cmd).unwrap();
let mut stderr_buf = Vec::new();
std::io::Read::read_to_end(&mut spawned.stderr, &mut stderr_buf).unwrap();
let status = spawned.wait().unwrap();
assert!(status.success());
assert_eq!(String::from_utf8_lossy(&stderr_buf), "hello-3267");
let cwd_contents = std::fs::read_to_string(tmp_path.join("cwd.txt")).unwrap();
assert_eq!(
std::fs::canonicalize(cwd_contents.trim()).unwrap(),
tmp_path,
"expected the Command's current_dir to reach the child via chdir_np"
);
}
#[test]
#[serial_test::serial]
fn disclaimed_stderr_piped_spawn_reports_spawn_error_for_missing_binary() {
let cmd =
std::process::Command::new("/nonexistent/definitely-not-a-real-binary-3267-stderr");
let err = disclaimed_stderr_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_stderr_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; echo err >&2; exit 2"]);
let mut spawned = disclaimed_stderr_piped_spawn(cmd).unwrap();
let mut stderr_buf = Vec::new();
std::io::Read::read_to_end(&mut spawned.stderr, &mut stderr_buf).unwrap();
let status = spawned.wait().unwrap();
unsafe { std::env::remove_var(DISABLE_ENV) };
assert_eq!(String::from_utf8_lossy(&stderr_buf), "err\n");
assert_eq!(status.code(), Some(2));
}
}
#[cfg(test)]
mod native_tests {
use super::*;
#[test]
#[serial_test::serial]
fn disclaimed_stderr_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 out; echo native-stderr >&2; exit 4"]);
let mut spawned = disclaimed_stderr_piped_spawn(cmd).unwrap();
unsafe { std::env::remove_var(DISABLE_ENV) };
let mut stderr_buf = Vec::new();
std::io::Read::read_to_end(&mut spawned.stderr, &mut stderr_buf).unwrap();
let status = spawned.wait().unwrap();
assert_eq!(String::from_utf8_lossy(&stderr_buf), "native-stderr\n");
assert_eq!(status.code(), Some(4));
}
}