Skip to main content

running_process_platform_internal/
std_child.rs

1//! The non-Tokio child backend (#850).
2//!
3//! [`PlatformStdChild`] owns a child spawned by a synchronous launch path: a
4//! plain [`std::process::Child`], or a [`TracedChild`] whose exit is reaped by
5//! its dedicated exact-trace tracer (Tokio cannot adopt either). Exit is
6//! observed by polling [`PlatformStdChild::try_wait_code`], which is what lets
7//! a timer task on the shared actor runtime own the child without Tokio's
8//! `process` feature and without an I/O driver: a process-only build compiles
9//! this module with no Tokio at all.
10//!
11//! It also carries the per-spawn containment the synchronous engine attaches
12//! after launch -- the Windows Job Object (descendant observer and memory
13//! limit) -- and the capture-cancellation wiring for its output pipes, so the
14//! owner of the child owns everything whose lifetime is tied to it.
15
16use std::io::{self, Read};
17use std::time::Duration;
18
19use crate::platform::process::{CaptureCancellation, CaptureStream, UnixSignalKind};
20use crate::{
21    capture_reader_done, exit_code, prepare_capture_reader, unix_signal_process_group, TracedChild,
22    WindowsJobHandle,
23};
24
25/// Interval of the blocking exact-trace wait fallback.
26const TRACED_WAIT_POLL: Duration = Duration::from_millis(10);
27
28enum StdChildKind {
29    Standard(std::process::Child),
30    ExactTrace(TracedChild),
31}
32
33/// A synchronously launched child whose exit is observed by polling.
34pub struct PlatformStdChild {
35    // Field order is drop order: the child handle is released before the Job
36    // Object whose kill-on-close contains it, as the engine always did.
37    kind: StdChildKind,
38    job: Option<WindowsJobHandle>,
39    own_process_group: bool,
40}
41
42/// The two prepared, cancellable capture readers of a child.
43pub struct PlatformCaptureReaders {
44    pub stdout: Box<dyn Read + Send>,
45    pub stderr: Box<dyn Read + Send>,
46}
47
48impl PlatformStdChild {
49    /// Adopt a standard child. `own_process_group` records whether it was
50    /// launched as the leader of its own process group.
51    pub fn from_std(child: std::process::Child, own_process_group: bool) -> Self {
52        Self::new(StdChildKind::Standard(child), own_process_group)
53    }
54
55    /// Adopt an exact-trace child; its tracer remains the sole reaper.
56    pub fn from_exact_trace(child: TracedChild, own_process_group: bool) -> Self {
57        Self::new(StdChildKind::ExactTrace(child), own_process_group)
58    }
59
60    fn new(kind: StdChildKind, own_process_group: bool) -> Self {
61        Self {
62            kind,
63            job: None,
64            own_process_group,
65        }
66    }
67
68    /// Operating-system process identifier of the direct child.
69    pub fn id(&self) -> u32 {
70        match &self.kind {
71            StdChildKind::Standard(child) => child.id(),
72            StdChildKind::ExactTrace(child) => child.id(),
73        }
74    }
75
76    /// Whether the child is reaped by an exact-trace tracer.
77    pub fn is_exact_trace(&self) -> bool {
78        matches!(self.kind, StdChildKind::ExactTrace(_))
79    }
80
81    /// The standard child handle, for post-spawn containment such as Job
82    /// Object assignment. `None` for exact-trace children.
83    pub fn std_child(&self) -> Option<&std::process::Child> {
84        match &self.kind {
85            StdChildKind::Standard(child) => Some(child),
86            StdChildKind::ExactTrace(_) => None,
87        }
88    }
89
90    /// Keep the per-spawn Job Object alive exactly as long as this child.
91    pub fn attach_job(&mut self, job: WindowsJobHandle) {
92        self.job = Some(job);
93    }
94
95    /// Non-blocking exit check. `Ok(None)` while the child is running.
96    pub fn try_wait_code(&mut self) -> io::Result<Option<i32>> {
97        match &mut self.kind {
98            StdChildKind::Standard(child) => child.try_wait().map(|status| status.map(exit_code)),
99            StdChildKind::ExactTrace(child) => child.try_wait_code(),
100        }
101    }
102
103    /// Blocking wait for the exit code.
104    ///
105    /// An exact-trace child is waited by polling its tracer's published
106    /// state. This parks the calling thread, so it must never run on an
107    /// actor-runtime worker; the runtime observes exit with
108    /// [`Self::try_wait_code`] instead.
109    pub fn wait_code(&mut self) -> io::Result<i32> {
110        match &mut self.kind {
111            StdChildKind::Standard(child) => child.wait().map(exit_code),
112            StdChildKind::ExactTrace(child) => loop {
113                if let Some(code) = child.try_wait_code()? {
114                    return Ok(code);
115                }
116                std::thread::sleep(TRACED_WAIT_POLL);
117            },
118        }
119    }
120
121    /// Hard-kill the direct child without reaping it.
122    pub fn kill(&mut self) -> io::Result<()> {
123        match &mut self.kind {
124            StdChildKind::Standard(child) => child.kill(),
125            StdChildKind::ExactTrace(child) => child.kill(),
126        }
127    }
128
129    /// Hard-kill the child's own process group when it leads one and the
130    /// host can signal groups, otherwise the direct child. Never reaps.
131    pub fn kill_group_or_child(&mut self) -> io::Result<()> {
132        if self.own_process_group
133            && unix_signal_process_group(self.id() as i32, UnixSignalKind::Kill).is_ok()
134        {
135            return Ok(());
136        }
137        self.kill()
138    }
139
140    pub fn take_stdin(&mut self) -> Option<std::process::ChildStdin> {
141        match &mut self.kind {
142            StdChildKind::Standard(child) => child.stdin.take(),
143            StdChildKind::ExactTrace(child) => child.take_stdin(),
144        }
145    }
146
147    pub fn take_stdout(&mut self) -> Option<std::process::ChildStdout> {
148        match &mut self.kind {
149            StdChildKind::Standard(child) => child.stdout.take(),
150            StdChildKind::ExactTrace(child) => child.take_stdout(),
151        }
152    }
153
154    pub fn take_stderr(&mut self) -> Option<std::process::ChildStderr> {
155        match &mut self.kind {
156            StdChildKind::Standard(child) => child.stderr.take(),
157            StdChildKind::ExactTrace(child) => child.take_stderr(),
158        }
159    }
160
161    /// Take both piped output streams and make them cancellable through
162    /// `cancellation` (see `cancel_capture_reader`). The caller keeps sole
163    /// ownership of the cancellation state, so dropping its last reference
164    /// is the reader-teardown signal it always was. A failure leaves no
165    /// half-registered reader.
166    pub fn prepare_capture(
167        &mut self,
168        cancellation: &CaptureCancellation,
169    ) -> io::Result<PlatformCaptureReaders> {
170        let (Some(stdout), Some(stderr)) = (self.take_stdout(), self.take_stderr()) else {
171            return Err(io::Error::new(
172                io::ErrorKind::BrokenPipe,
173                "child stdout and stderr must both be piped for capture",
174            ));
175        };
176        let stdout = prepare_capture_reader(stdout, cancellation, CaptureStream::Stdout)?;
177        let stderr = match prepare_capture_reader(stderr, cancellation, CaptureStream::Stderr) {
178            Ok(stderr) => stderr,
179            Err(error) => {
180                capture_reader_done(cancellation, CaptureStream::Stdout);
181                return Err(error);
182            }
183        };
184        Ok(PlatformCaptureReaders { stdout, stderr })
185    }
186
187    /// Abandon a child whose launch failed after spawn: kill it and reap it
188    /// off the calling thread, so the caller's error path stays bounded.
189    pub fn discard_after_start_error(self) {
190        let Self { kind, job, .. } = self;
191        match kind {
192            StdChildKind::Standard(mut child) => {
193                let _ = child.kill();
194                // Keep ownership until the child is eventually reaped, even if
195                // kill delivery takes time; the job outlives the reap.
196                std::thread::spawn(move || {
197                    let _job = job;
198                    let _ = child.wait();
199                });
200            }
201            StdChildKind::ExactTrace(mut child) => {
202                // The dedicated tracer remains the sole waiter and will reap it.
203                let _ = child.kill();
204            }
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::{PlatformStdChild, TRACED_WAIT_POLL};
212    use crate::platform::process::CaptureCancellation;
213    use std::io::Read;
214    use std::process::{Command, Stdio};
215    use std::time::{Duration, Instant};
216
217    const FIXTURE_ENV: &str = "RUNNING_PROCESS_STD_CHILD_FIXTURE";
218
219    /// Child half of these tests, selected by the fixture variable.
220    #[test]
221    fn std_child_fixture() {
222        let mode = std::env::var_os(FIXTURE_ENV);
223        match mode.as_ref().and_then(|mode| mode.to_str()) {
224            Some("exit7") => std::process::exit(7),
225            Some("sleep") => std::thread::sleep(Duration::from_secs(60)),
226            Some("print") => {
227                println!("std-child-stdout");
228                eprintln!("std-child-stderr");
229            }
230            _ => {}
231        }
232    }
233
234    fn fixture(mode: &str, piped: bool) -> PlatformStdChild {
235        let mut command = Command::new(std::env::current_exe().expect("test executable"));
236        command
237            .args([
238                "--exact",
239                "std_child::tests::std_child_fixture",
240                "--nocapture",
241            ])
242            .env(FIXTURE_ENV, mode)
243            .stdin(Stdio::null());
244        if piped {
245            command.stdout(Stdio::piped()).stderr(Stdio::piped());
246        } else {
247            command.stdout(Stdio::null()).stderr(Stdio::null());
248        }
249        PlatformStdChild::from_std(command.spawn().expect("spawn fixture"), false)
250    }
251
252    fn poll_exit(child: &mut PlatformStdChild, limit: Duration) -> Option<i32> {
253        let deadline = Instant::now() + limit;
254        while Instant::now() < deadline {
255            if let Some(code) = child.try_wait_code().expect("try_wait") {
256                return Some(code);
257            }
258            std::thread::sleep(TRACED_WAIT_POLL);
259        }
260        None
261    }
262
263    #[test]
264    fn exit_is_observed_by_polling() {
265        let mut child = fixture("exit7", false);
266        assert!(child.id() > 0);
267        assert!(!child.is_exact_trace());
268        assert!(child.std_child().is_some());
269        assert_eq!(poll_exit(&mut child, Duration::from_secs(30)), Some(7));
270        // Once observed, the status stays observable.
271        assert_eq!(child.try_wait_code().expect("try_wait"), Some(7));
272        assert_eq!(child.wait_code().expect("wait"), 7);
273    }
274
275    #[test]
276    fn kill_ends_a_running_child() {
277        let mut child = fixture("sleep", false);
278        let pid = child.id();
279        assert_eq!(child.try_wait_code().expect("try_wait"), None);
280        child.kill().expect("kill");
281        let code = child.wait_code().expect("reap");
282        assert_ne!(code, 0);
283        assert_eq!(child.id(), pid, "pid is stable across the reap");
284    }
285
286    #[test]
287    fn group_kill_without_own_group_falls_back_to_the_child() {
288        let mut child = fixture("sleep", false);
289        child.kill_group_or_child().expect("kill");
290        assert!(poll_exit(&mut child, Duration::from_secs(30)).is_some());
291    }
292
293    #[test]
294    fn capture_readers_drain_and_register_for_cancellation() {
295        let mut child = fixture("print", true);
296        let cancellation = CaptureCancellation::default();
297        let readers = child
298            .prepare_capture(&cancellation)
299            .expect("prepare capture");
300        let collect = |mut reader: Box<dyn Read + Send>| {
301            std::thread::spawn(move || {
302                let mut text = String::new();
303                reader.read_to_string(&mut text).expect("read capture");
304                text
305            })
306        };
307        let stdout = collect(readers.stdout);
308        let stderr = collect(readers.stderr);
309        assert_eq!(child.wait_code().expect("wait"), 0);
310        assert!(stdout.join().expect("stdout").contains("std-child-stdout"));
311        assert!(stderr.join().expect("stderr").contains("std-child-stderr"));
312        // Cancelling after EOF is harmless.
313        crate::cancel_capture_reader(&cancellation);
314    }
315
316    #[test]
317    fn capture_requires_both_pipes() {
318        let mut child = fixture("sleep", false);
319        let error = child
320            .prepare_capture(&CaptureCancellation::default())
321            .err()
322            .expect("unpiped capture is rejected");
323        assert_eq!(error.kind(), std::io::ErrorKind::BrokenPipe);
324        child.discard_after_start_error();
325    }
326}