Skip to main content

windows_spawn/
child.rs

1//! Owned child process and suspended type state.
2
3use std::io::{self, Read, Write};
4use std::os::windows::io::{AsHandle, BorrowedHandle, OwnedHandle};
5use std::process::{ExitStatus, Output};
6use std::thread;
7
8use crate::handles::Job;
9use crate::sys;
10
11/// The writable parent end of a child's standard-input pipe.
12#[derive(Debug)]
13pub struct ChildStdin {
14    handle: OwnedHandle,
15}
16
17impl Write for ChildStdin {
18    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
19        sys::write_handle(self.handle.as_handle(), buffer)
20    }
21
22    fn flush(&mut self) -> io::Result<()> {
23        Ok(())
24    }
25}
26
27impl AsHandle for ChildStdin {
28    fn as_handle(&self) -> BorrowedHandle<'_> {
29        self.handle.as_handle()
30    }
31}
32
33/// The readable parent end of a child's standard-output pipe.
34#[derive(Debug)]
35pub struct ChildStdout {
36    handle: OwnedHandle,
37}
38
39impl Read for ChildStdout {
40    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
41        sys::read_handle(self.handle.as_handle(), buffer)
42    }
43}
44
45impl AsHandle for ChildStdout {
46    fn as_handle(&self) -> BorrowedHandle<'_> {
47        self.handle.as_handle()
48    }
49}
50
51/// The readable parent end of a child's standard-error pipe.
52#[derive(Debug)]
53pub struct ChildStderr {
54    handle: OwnedHandle,
55}
56
57impl Read for ChildStderr {
58    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
59        sys::read_handle(self.handle.as_handle(), buffer)
60    }
61}
62
63impl AsHandle for ChildStderr {
64    fn as_handle(&self) -> BorrowedHandle<'_> {
65        self.handle.as_handle()
66    }
67}
68
69/// A running or exited process whose handle is owned exactly once.
70#[derive(Debug)]
71pub struct Child {
72    // Declared first so kill-on-close takes effect before pipe and process
73    // handles are released by Rust's field drop order.
74    kill_job: Option<Job>,
75    /// A pipe connected to the child's standard input, when requested.
76    pub stdin: Option<ChildStdin>,
77    /// A pipe connected to the child's standard output, when requested.
78    pub stdout: Option<ChildStdout>,
79    /// A pipe connected to the child's standard error, when requested.
80    pub stderr: Option<ChildStderr>,
81    process: OwnedHandle,
82    pid: u32,
83    exit: Option<ExitStatus>,
84}
85
86impl Child {
87    pub(crate) fn new(
88        process: OwnedHandle,
89        pid: u32,
90        kill_job: Option<Job>,
91        stdin: Option<OwnedHandle>,
92        stdout: Option<OwnedHandle>,
93        stderr: Option<OwnedHandle>,
94    ) -> Self {
95        Self {
96            kill_job,
97            stdin: stdin.map(|handle| ChildStdin { handle }),
98            stdout: stdout.map(|handle| ChildStdout { handle }),
99            stderr: stderr.map(|handle| ChildStderr { handle }),
100            process,
101            pid,
102            exit: None,
103        }
104    }
105
106    pub(crate) fn process_handle(&self) -> BorrowedHandle<'_> {
107        self.process.as_handle()
108    }
109
110    /// Returns the process identifier captured at creation.
111    #[must_use]
112    pub const fn id(&self) -> u32 {
113        self.pid
114    }
115
116    /// Terminates the root process.
117    ///
118    /// # Errors
119    ///
120    /// Returns the operating-system error from `TerminateProcess`.
121    pub fn kill(&mut self) -> io::Result<()> {
122        if self.exit.is_some() {
123            return Ok(());
124        }
125        sys::terminate_process(self.process.as_handle(), 1)
126    }
127
128    /// Waits for exit and caches the status.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if waiting or retrieving the exit code fails.
133    pub fn wait(&mut self) -> io::Result<ExitStatus> {
134        if let Some(status) = self.exit {
135            return Ok(status);
136        }
137        drop(self.stdin.take());
138        sys::wait_process(self.process.as_handle())?;
139        let status = sys::exit_status(self.process.as_handle())?;
140        self.exit = Some(status);
141        Ok(status)
142    }
143
144    /// Checks for exit without blocking, returning the cached status thereafter.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if querying the process or its exit code fails.
149    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
150        if self.exit.is_some() {
151            return Ok(self.exit);
152        }
153        if !sys::try_wait_process(self.process.as_handle())? {
154            return Ok(None);
155        }
156        let status = sys::exit_status(self.process.as_handle())?;
157        self.exit = Some(status);
158        Ok(self.exit)
159    }
160
161    /// Waits while draining both output pipes concurrently.
162    ///
163    /// Under [`crate::DropPolicy::KillTree`], descendants are terminated after
164    /// the root exits and before reader threads are joined. This guarantees EOF
165    /// even when a grandchild retained a pipe handle.
166    ///
167    /// # Errors
168    ///
169    /// Returns an error from process waiting, pipe reading, or Job termination.
170    pub fn wait_with_output(mut self) -> io::Result<Output> {
171        drop(self.stdin.take());
172
173        let stdout_reader = self
174            .stdout
175            .take()
176            .map(|stream| thread::spawn(move || drain_output(stream.handle.as_handle())));
177        let stderr_reader = self
178            .stderr
179            .take()
180            .map(|stream| thread::spawn(move || drain_output(stream.handle.as_handle())));
181
182        let status = self.wait();
183        let termination = self
184            .kill_job
185            .as_ref()
186            .map_or(Ok(()), |job| job.terminate(1));
187        let (stdout, stderr) = join_readers(stdout_reader, stderr_reader)?;
188        termination?;
189
190        Ok(Output {
191            status: status?,
192            stdout,
193            stderr,
194        })
195    }
196}
197
198impl AsHandle for Child {
199    fn as_handle(&self) -> BorrowedHandle<'_> {
200        self.process.as_handle()
201    }
202}
203
204fn drain_output(handle: BorrowedHandle<'_>) -> io::Result<Vec<u8>> {
205    let mut bytes = Vec::new();
206    let mut buffer = [0_u8; 8_192];
207    loop {
208        let Some(read) = std::num::NonZeroUsize::new(sys::read_handle(handle, &mut buffer)?) else {
209            return Ok(bytes);
210        };
211        bytes.extend_from_slice(&buffer[..read.get()]);
212    }
213}
214
215fn join_reader(reader: Option<thread::JoinHandle<io::Result<Vec<u8>>>>) -> io::Result<Vec<u8>> {
216    match reader {
217        Some(reader) => reader
218            .join()
219            .map_err(|_| io::Error::other("output reader thread panicked"))?,
220        None => Ok(Vec::new()),
221    }
222}
223
224fn join_readers(
225    stdout: Option<thread::JoinHandle<io::Result<Vec<u8>>>>,
226    stderr: Option<thread::JoinHandle<io::Result<Vec<u8>>>>,
227) -> io::Result<(Vec<u8>, Vec<u8>)> {
228    let stdout = join_reader(stdout);
229    let stderr = join_reader(stderr);
230    Ok((stdout?, stderr?))
231}
232
233/// A process whose primary thread has not yet been resumed.
234///
235/// Dropping this value without resuming always terminates the process.
236/// The consuming transition makes a second resume unrepresentable:
237///
238/// ```compile_fail
239/// use windows_spawn::Command;
240///
241/// let mut command = Command::new("cmd.exe");
242/// let suspended = command.spawn_suspended().unwrap();
243/// let _child = suspended.resume().unwrap();
244/// let _second = suspended.resume().unwrap();
245/// ```
246#[derive(Debug)]
247#[must_use = "dropping a suspended child terminates it"]
248pub struct SuspendedChild {
249    child: Option<Child>,
250    main_thread: OwnedHandle,
251}
252
253impl SuspendedChild {
254    pub(crate) fn new(child: Child, main_thread: OwnedHandle) -> Self {
255        Self {
256            child: Some(child),
257            main_thread,
258        }
259    }
260
261    /// Returns the process identifier captured at creation.
262    ///
263    /// # Panics
264    ///
265    /// Panics only if an internal ownership invariant was violated and the
266    /// process was removed before this suspended value was consumed.
267    #[must_use]
268    pub fn id(&self) -> u32 {
269        self.child
270            .as_ref()
271            .expect("a suspended child owns its process until resume")
272            .id()
273    }
274
275    /// Borrows the suspended process's primary thread handle.
276    ///
277    /// This handle is available for supported thread configuration and
278    /// inspection before [`Self::resume`] consumes the suspended state.
279    ///
280    #[must_use]
281    pub fn primary_thread_handle(&self) -> BorrowedHandle<'_> {
282        self.main_thread.as_handle()
283    }
284
285    /// Resumes the primary thread and transitions to an ordinary [`Child`].
286    ///
287    /// # Errors
288    ///
289    /// Returns the operating-system error when the primary thread cannot be
290    /// resumed. It also returns `InvalidData` when external suspension or
291    /// resumption changed the expected suspend count of exactly one. The
292    /// process is terminated during either rollback.
293    pub fn resume(mut self) -> io::Result<Child> {
294        let previous = sys::resume_thread(self.main_thread.as_handle())?;
295        if previous != 1 {
296            return Err(io::Error::new(
297                io::ErrorKind::InvalidData,
298                format!("primary thread suspend count was {previous}, expected 1"),
299            ));
300        }
301        self.child
302            .take()
303            .ok_or_else(|| io::Error::other("suspended child lost its process"))
304    }
305}
306
307impl AsHandle for SuspendedChild {
308    fn as_handle(&self) -> BorrowedHandle<'_> {
309        self.child
310            .as_ref()
311            .expect("a suspended child owns its process until resume")
312            .as_handle()
313    }
314}
315
316impl Drop for SuspendedChild {
317    fn drop(&mut self) {
318        if let Some(child) = &mut self.child {
319            let _ = child.kill();
320        }
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use std::sync::atomic::{AtomicBool, Ordering};
327    use std::sync::Arc;
328
329    use super::*;
330
331    #[test]
332    fn absent_and_panicked_output_readers_become_results() {
333        assert!(join_reader(None).unwrap().is_empty());
334        let panicked = thread::spawn(|| -> io::Result<Vec<u8>> { panic!("reader panic") });
335        assert_eq!(
336            join_reader(Some(panicked)).unwrap_err().kind(),
337            io::ErrorKind::Other
338        );
339    }
340
341    #[test]
342    fn both_output_readers_are_joined_when_the_first_panics() {
343        let joined = Arc::new(AtomicBool::new(false));
344        let stdout = thread::spawn(|| -> io::Result<Vec<u8>> { panic!("stdout panic") });
345        let stderr_joined = Arc::clone(&joined);
346        let stderr = thread::spawn(move || {
347            stderr_joined.store(true, Ordering::Release);
348            Ok(Vec::new())
349        });
350
351        assert_eq!(
352            join_readers(Some(stdout), Some(stderr)).unwrap_err().kind(),
353            io::ErrorKind::Other
354        );
355        assert!(joined.load(Ordering::Acquire));
356    }
357}