use std::io::{self, Read};
use std::time::Duration;
use crate::platform::process::{CaptureCancellation, CaptureStream, UnixSignalKind};
use crate::{
capture_reader_done, exit_code, prepare_capture_reader, unix_signal_process_group, TracedChild,
WindowsJobHandle,
};
const TRACED_WAIT_POLL: Duration = Duration::from_millis(10);
enum StdChildKind {
Standard(std::process::Child),
ExactTrace(TracedChild),
}
pub struct PlatformStdChild {
kind: StdChildKind,
job: Option<WindowsJobHandle>,
own_process_group: bool,
}
pub struct PlatformCaptureReaders {
pub stdout: Box<dyn Read + Send>,
pub stderr: Box<dyn Read + Send>,
}
impl PlatformStdChild {
pub fn from_std(child: std::process::Child, own_process_group: bool) -> Self {
Self::new(StdChildKind::Standard(child), own_process_group)
}
pub fn from_exact_trace(child: TracedChild, own_process_group: bool) -> Self {
Self::new(StdChildKind::ExactTrace(child), own_process_group)
}
fn new(kind: StdChildKind, own_process_group: bool) -> Self {
Self {
kind,
job: None,
own_process_group,
}
}
pub fn id(&self) -> u32 {
match &self.kind {
StdChildKind::Standard(child) => child.id(),
StdChildKind::ExactTrace(child) => child.id(),
}
}
pub fn is_exact_trace(&self) -> bool {
matches!(self.kind, StdChildKind::ExactTrace(_))
}
pub fn std_child(&self) -> Option<&std::process::Child> {
match &self.kind {
StdChildKind::Standard(child) => Some(child),
StdChildKind::ExactTrace(_) => None,
}
}
pub fn attach_job(&mut self, job: WindowsJobHandle) {
self.job = Some(job);
}
pub fn try_wait_code(&mut self) -> io::Result<Option<i32>> {
match &mut self.kind {
StdChildKind::Standard(child) => child.try_wait().map(|status| status.map(exit_code)),
StdChildKind::ExactTrace(child) => child.try_wait_code(),
}
}
pub fn wait_code(&mut self) -> io::Result<i32> {
match &mut self.kind {
StdChildKind::Standard(child) => child.wait().map(exit_code),
StdChildKind::ExactTrace(child) => loop {
if let Some(code) = child.try_wait_code()? {
return Ok(code);
}
std::thread::sleep(TRACED_WAIT_POLL);
},
}
}
pub fn kill(&mut self) -> io::Result<()> {
match &mut self.kind {
StdChildKind::Standard(child) => child.kill(),
StdChildKind::ExactTrace(child) => child.kill(),
}
}
pub fn kill_group_or_child(&mut self) -> io::Result<()> {
if self.own_process_group
&& unix_signal_process_group(self.id() as i32, UnixSignalKind::Kill).is_ok()
{
return Ok(());
}
self.kill()
}
pub fn take_stdin(&mut self) -> Option<std::process::ChildStdin> {
match &mut self.kind {
StdChildKind::Standard(child) => child.stdin.take(),
StdChildKind::ExactTrace(child) => child.take_stdin(),
}
}
pub fn take_stdout(&mut self) -> Option<std::process::ChildStdout> {
match &mut self.kind {
StdChildKind::Standard(child) => child.stdout.take(),
StdChildKind::ExactTrace(child) => child.take_stdout(),
}
}
pub fn take_stderr(&mut self) -> Option<std::process::ChildStderr> {
match &mut self.kind {
StdChildKind::Standard(child) => child.stderr.take(),
StdChildKind::ExactTrace(child) => child.take_stderr(),
}
}
pub fn prepare_capture(
&mut self,
cancellation: &CaptureCancellation,
) -> io::Result<PlatformCaptureReaders> {
let (Some(stdout), Some(stderr)) = (self.take_stdout(), self.take_stderr()) else {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"child stdout and stderr must both be piped for capture",
));
};
let stdout = prepare_capture_reader(stdout, cancellation, CaptureStream::Stdout)?;
let stderr = match prepare_capture_reader(stderr, cancellation, CaptureStream::Stderr) {
Ok(stderr) => stderr,
Err(error) => {
capture_reader_done(cancellation, CaptureStream::Stdout);
return Err(error);
}
};
Ok(PlatformCaptureReaders { stdout, stderr })
}
pub fn discard_after_start_error(self) {
let Self { kind, job, .. } = self;
match kind {
StdChildKind::Standard(mut child) => {
let _ = child.kill();
std::thread::spawn(move || {
let _job = job;
let _ = child.wait();
});
}
StdChildKind::ExactTrace(mut child) => {
let _ = child.kill();
}
}
}
}
#[cfg(test)]
mod tests {
use super::{PlatformStdChild, TRACED_WAIT_POLL};
use crate::platform::process::CaptureCancellation;
use std::io::Read;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const FIXTURE_ENV: &str = "RUNNING_PROCESS_STD_CHILD_FIXTURE";
#[test]
fn std_child_fixture() {
let mode = std::env::var_os(FIXTURE_ENV);
match mode.as_ref().and_then(|mode| mode.to_str()) {
Some("exit7") => std::process::exit(7),
Some("sleep") => std::thread::sleep(Duration::from_secs(60)),
Some("print") => {
println!("std-child-stdout");
eprintln!("std-child-stderr");
}
_ => {}
}
}
fn fixture(mode: &str, piped: bool) -> PlatformStdChild {
let mut command = Command::new(std::env::current_exe().expect("test executable"));
command
.args([
"--exact",
"std_child::tests::std_child_fixture",
"--nocapture",
])
.env(FIXTURE_ENV, mode)
.stdin(Stdio::null());
if piped {
command.stdout(Stdio::piped()).stderr(Stdio::piped());
} else {
command.stdout(Stdio::null()).stderr(Stdio::null());
}
PlatformStdChild::from_std(command.spawn().expect("spawn fixture"), false)
}
fn poll_exit(child: &mut PlatformStdChild, limit: Duration) -> Option<i32> {
let deadline = Instant::now() + limit;
while Instant::now() < deadline {
if let Some(code) = child.try_wait_code().expect("try_wait") {
return Some(code);
}
std::thread::sleep(TRACED_WAIT_POLL);
}
None
}
#[test]
fn exit_is_observed_by_polling() {
let mut child = fixture("exit7", false);
assert!(child.id() > 0);
assert!(!child.is_exact_trace());
assert!(child.std_child().is_some());
assert_eq!(poll_exit(&mut child, Duration::from_secs(30)), Some(7));
assert_eq!(child.try_wait_code().expect("try_wait"), Some(7));
assert_eq!(child.wait_code().expect("wait"), 7);
}
#[test]
fn kill_ends_a_running_child() {
let mut child = fixture("sleep", false);
let pid = child.id();
assert_eq!(child.try_wait_code().expect("try_wait"), None);
child.kill().expect("kill");
let code = child.wait_code().expect("reap");
assert_ne!(code, 0);
assert_eq!(child.id(), pid, "pid is stable across the reap");
}
#[test]
fn group_kill_without_own_group_falls_back_to_the_child() {
let mut child = fixture("sleep", false);
child.kill_group_or_child().expect("kill");
assert!(poll_exit(&mut child, Duration::from_secs(30)).is_some());
}
#[test]
fn capture_readers_drain_and_register_for_cancellation() {
let mut child = fixture("print", true);
let cancellation = CaptureCancellation::default();
let readers = child
.prepare_capture(&cancellation)
.expect("prepare capture");
let collect = |mut reader: Box<dyn Read + Send>| {
std::thread::spawn(move || {
let mut text = String::new();
reader.read_to_string(&mut text).expect("read capture");
text
})
};
let stdout = collect(readers.stdout);
let stderr = collect(readers.stderr);
assert_eq!(child.wait_code().expect("wait"), 0);
assert!(stdout.join().expect("stdout").contains("std-child-stdout"));
assert!(stderr.join().expect("stderr").contains("std-child-stderr"));
crate::cancel_capture_reader(&cancellation);
}
#[test]
fn capture_requires_both_pipes() {
let mut child = fixture("sleep", false);
let error = child
.prepare_capture(&CaptureCancellation::default())
.err()
.expect("unpiped capture is rejected");
assert_eq!(error.kind(), std::io::ErrorKind::BrokenPipe);
child.discard_after_start_error();
}
}