use std::io::{self, Read, Write};
use std::os::windows::io::{AsHandle, BorrowedHandle, OwnedHandle};
use std::process::{ExitStatus, Output};
use std::thread;
use crate::handles::Job;
use crate::sys;
#[derive(Debug)]
pub struct ChildStdin {
handle: OwnedHandle,
}
impl Write for ChildStdin {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
sys::write_handle(self.handle.as_handle(), buffer)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl AsHandle for ChildStdin {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.handle.as_handle()
}
}
#[derive(Debug)]
pub struct ChildStdout {
handle: OwnedHandle,
}
impl Read for ChildStdout {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
sys::read_handle(self.handle.as_handle(), buffer)
}
}
impl AsHandle for ChildStdout {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.handle.as_handle()
}
}
#[derive(Debug)]
pub struct ChildStderr {
handle: OwnedHandle,
}
impl Read for ChildStderr {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
sys::read_handle(self.handle.as_handle(), buffer)
}
}
impl AsHandle for ChildStderr {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.handle.as_handle()
}
}
#[derive(Debug)]
pub struct Child {
kill_job: Option<Job>,
pub stdin: Option<ChildStdin>,
pub stdout: Option<ChildStdout>,
pub stderr: Option<ChildStderr>,
process: OwnedHandle,
pid: u32,
exit: Option<ExitStatus>,
}
impl Child {
pub(crate) fn new(
process: OwnedHandle,
pid: u32,
kill_job: Option<Job>,
stdin: Option<OwnedHandle>,
stdout: Option<OwnedHandle>,
stderr: Option<OwnedHandle>,
) -> Self {
Self {
kill_job,
stdin: stdin.map(|handle| ChildStdin { handle }),
stdout: stdout.map(|handle| ChildStdout { handle }),
stderr: stderr.map(|handle| ChildStderr { handle }),
process,
pid,
exit: None,
}
}
pub(crate) fn process_handle(&self) -> BorrowedHandle<'_> {
self.process.as_handle()
}
#[must_use]
pub const fn id(&self) -> u32 {
self.pid
}
pub fn kill(&mut self) -> io::Result<()> {
if self.exit.is_some() {
return Ok(());
}
sys::terminate_process(self.process.as_handle(), 1)
}
pub fn wait(&mut self) -> io::Result<ExitStatus> {
if let Some(status) = self.exit {
return Ok(status);
}
drop(self.stdin.take());
sys::wait_process(self.process.as_handle())?;
let status = sys::exit_status(self.process.as_handle())?;
self.exit = Some(status);
Ok(status)
}
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
if self.exit.is_some() {
return Ok(self.exit);
}
if !sys::try_wait_process(self.process.as_handle())? {
return Ok(None);
}
let status = sys::exit_status(self.process.as_handle())?;
self.exit = Some(status);
Ok(self.exit)
}
pub fn wait_with_output(mut self) -> io::Result<Output> {
drop(self.stdin.take());
let stdout_reader = self
.stdout
.take()
.map(|stream| thread::spawn(move || drain_output(stream.handle.as_handle())));
let stderr_reader = self
.stderr
.take()
.map(|stream| thread::spawn(move || drain_output(stream.handle.as_handle())));
let status = self.wait();
let termination = self
.kill_job
.as_ref()
.map_or(Ok(()), |job| job.terminate(1));
let (stdout, stderr) = join_readers(stdout_reader, stderr_reader)?;
termination?;
Ok(Output {
status: status?,
stdout,
stderr,
})
}
}
impl AsHandle for Child {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.process.as_handle()
}
}
fn drain_output(handle: BorrowedHandle<'_>) -> io::Result<Vec<u8>> {
let mut bytes = Vec::new();
let mut buffer = [0_u8; 8_192];
loop {
let Some(read) = std::num::NonZeroUsize::new(sys::read_handle(handle, &mut buffer)?) else {
return Ok(bytes);
};
bytes.extend_from_slice(&buffer[..read.get()]);
}
}
fn join_reader(reader: Option<thread::JoinHandle<io::Result<Vec<u8>>>>) -> io::Result<Vec<u8>> {
match reader {
Some(reader) => reader
.join()
.map_err(|_| io::Error::other("output reader thread panicked"))?,
None => Ok(Vec::new()),
}
}
fn join_readers(
stdout: Option<thread::JoinHandle<io::Result<Vec<u8>>>>,
stderr: Option<thread::JoinHandle<io::Result<Vec<u8>>>>,
) -> io::Result<(Vec<u8>, Vec<u8>)> {
let stdout = join_reader(stdout);
let stderr = join_reader(stderr);
Ok((stdout?, stderr?))
}
#[derive(Debug)]
#[must_use = "dropping a suspended child terminates it"]
pub struct SuspendedChild {
child: Option<Child>,
main_thread: OwnedHandle,
}
impl SuspendedChild {
pub(crate) fn new(child: Child, main_thread: OwnedHandle) -> Self {
Self {
child: Some(child),
main_thread,
}
}
#[must_use]
pub fn id(&self) -> u32 {
self.child
.as_ref()
.expect("a suspended child owns its process until resume")
.id()
}
#[must_use]
pub fn primary_thread_handle(&self) -> BorrowedHandle<'_> {
self.main_thread.as_handle()
}
pub fn resume(mut self) -> io::Result<Child> {
let previous = sys::resume_thread(self.main_thread.as_handle())?;
if previous != 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("primary thread suspend count was {previous}, expected 1"),
));
}
self.child
.take()
.ok_or_else(|| io::Error::other("suspended child lost its process"))
}
}
impl AsHandle for SuspendedChild {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.child
.as_ref()
.expect("a suspended child owns its process until resume")
.as_handle()
}
}
impl Drop for SuspendedChild {
fn drop(&mut self) {
if let Some(child) = &mut self.child {
let _ = child.kill();
}
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use super::*;
#[test]
fn absent_and_panicked_output_readers_become_results() {
assert!(join_reader(None).unwrap().is_empty());
let panicked = thread::spawn(|| -> io::Result<Vec<u8>> { panic!("reader panic") });
assert_eq!(
join_reader(Some(panicked)).unwrap_err().kind(),
io::ErrorKind::Other
);
}
#[test]
fn both_output_readers_are_joined_when_the_first_panics() {
let joined = Arc::new(AtomicBool::new(false));
let stdout = thread::spawn(|| -> io::Result<Vec<u8>> { panic!("stdout panic") });
let stderr_joined = Arc::clone(&joined);
let stderr = thread::spawn(move || {
stderr_joined.store(true, Ordering::Release);
Ok(Vec::new())
});
assert_eq!(
join_readers(Some(stdout), Some(stderr)).unwrap_err().kind(),
io::ErrorKind::Other
);
assert!(joined.load(Ordering::Acquire));
}
}