Skip to main content

running_process_platform_internal/
sync_spawn_group.rs

1use std::io;
2use std::process::{Command, Stdio};
3use std::sync::{Arc, Mutex};
4use std::thread;
5use std::time::{Duration, Instant};
6
7const DEFAULT_KILL_DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
8const KILL_DRAIN_TIMEOUT_ENV: &str = "RUNNING_PROCESS_KILL_DRAIN_TIMEOUT_MS";
9
10fn kill_drain_deadline() -> Instant {
11    let timeout = std::env::var(KILL_DRAIN_TIMEOUT_ENV)
12        .ok()
13        .and_then(|raw| raw.trim().parse::<u64>().ok())
14        .map(Duration::from_millis)
15        .unwrap_or(DEFAULT_KILL_DRAIN_TIMEOUT);
16    Instant::now() + timeout
17}
18
19fn poll_until<T>(
20    deadline: Instant,
21    interval: Duration,
22    mut poll: impl FnMut() -> io::Result<Option<T>>,
23) -> io::Result<Option<T>> {
24    loop {
25        if let Some(value) = poll()? {
26            return Ok(Some(value));
27        }
28        let now = Instant::now();
29        if now >= deadline {
30            return Ok(None);
31        }
32        thread::sleep(interval.min(deadline.saturating_duration_since(now)));
33    }
34}
35
36trait UnixChild: Send {
37    fn kill(&mut self) -> io::Result<()>;
38    fn wait(&mut self) -> io::Result<i32>;
39    fn try_wait(&mut self) -> io::Result<Option<i32>>;
40}
41
42impl UnixChild for std::process::Child {
43    fn kill(&mut self) -> io::Result<()> {
44        std::process::Child::kill(self)
45    }
46
47    fn wait(&mut self) -> io::Result<i32> {
48        std::process::Child::wait(self).map(crate::platform::process::exit_code)
49    }
50
51    fn try_wait(&mut self) -> io::Result<Option<i32>> {
52        Ok(std::process::Child::try_wait(self)?.map(crate::platform::process::exit_code))
53    }
54}
55
56impl crate::platform::process::DaemonChildControl for std::process::Child {
57    fn kill(&mut self) -> io::Result<()> {
58        std::process::Child::kill(self)
59    }
60
61    fn wait(&mut self) -> io::Result<i32> {
62        std::process::Child::wait(self).map(crate::platform::process::exit_code)
63    }
64
65    fn try_wait(&mut self) -> io::Result<Option<i32>> {
66        Ok(std::process::Child::try_wait(self)?.map(crate::platform::process::exit_code))
67    }
68}
69
70pub struct SpawnedInner {
71    child: Arc<Mutex<Option<Box<dyn UnixChild>>>>,
72    pgid: i32,
73}
74
75impl SpawnedInner {
76    pub fn kill(&self) -> io::Result<()> {
77        // Try the child first, then the process group, to make sure
78        // any siblings spawned inside go down too.
79        let mut guard = self.child.lock().expect("child mutex poisoned");
80        if let Some(child) = guard.as_mut() {
81            let _ = child.kill();
82        }
83        drop(guard);
84        let _ = crate::platform::process::unix_signal_process_group(
85            self.pgid,
86            crate::platform::process::UnixSignalKind::Kill,
87        );
88        Ok(())
89    }
90
91    pub fn wait(&self) -> io::Result<i32> {
92        let mut guard = self.child.lock().expect("child mutex poisoned");
93        let Some(child) = guard.as_mut() else {
94            return Err(io::Error::other("child handle absent"));
95        };
96        child.wait()
97    }
98
99    pub fn try_wait(&self) -> io::Result<Option<i32>> {
100        let mut guard = self.child.lock().expect("child mutex poisoned");
101        let Some(child) = guard.as_mut() else {
102            return Ok(None);
103        };
104        child.try_wait()
105    }
106
107    pub fn shutdown(&mut self) {
108        self.shutdown_with_deadline(kill_drain_deadline());
109    }
110
111    fn shutdown_with_deadline(&mut self, deadline: Instant) {
112        let group_signaled = crate::platform::process::unix_signal_process_group(
113            self.pgid,
114            crate::platform::process::UnixSignalKind::Kill,
115        )
116        .is_ok();
117        let Some(mut child) = self.child.lock().expect("child mutex poisoned").take() else {
118            return;
119        };
120        if !group_signaled {
121            let _ = child.kill();
122        }
123        match poll_until(deadline, Duration::from_millis(10), || child.try_wait()) {
124            Ok(Some(_)) => {}
125            Ok(None) | Err(_) => spawn_background_reaper(child),
126        }
127    }
128}
129
130impl crate::platform::process::SpawnedChildControl for SpawnedInner {
131    fn kill(&mut self) -> io::Result<()> {
132        SpawnedInner::kill(self)
133    }
134
135    fn wait(&mut self) -> io::Result<i32> {
136        SpawnedInner::wait(self)
137    }
138
139    fn try_wait(&mut self) -> io::Result<Option<i32>> {
140        SpawnedInner::try_wait(self)
141    }
142
143    fn shutdown(&mut self) {
144        SpawnedInner::shutdown(self);
145    }
146}
147
148fn spawn_background_reaper(mut child: Box<dyn UnixChild>) {
149    thread::spawn(move || {
150        // Once ownership is off the caller's teardown path, a blocking wait is
151        // the most reliable terminal policy: it reaps exactly once without a
152        // retry loop, spinning, or retaining the shared child mutex.
153        let _ = child.wait();
154    });
155}
156
157fn slot_to_stdio(slot: &crate::platform::process::StdioSource<'_>) -> io::Result<Stdio> {
158    match slot {
159        crate::platform::process::StdioSource::Null => Ok(Stdio::null()),
160        crate::platform::process::StdioSource::Parent => Ok(Stdio::inherit()),
161        crate::platform::process::StdioSource::File(file) => Ok(Stdio::from(file.try_clone()?)),
162        crate::platform::process::StdioSource::Pipe => Ok(Stdio::piped()),
163    }
164}
165
166fn daemon_slot_to_stdio(
167    slot: &crate::platform::process::DaemonStdioSource<'_>,
168) -> io::Result<Stdio> {
169    match slot {
170        crate::platform::process::DaemonStdioSource::Null => Ok(Stdio::null()),
171        crate::platform::process::DaemonStdioSource::File(file) => {
172            Ok(Stdio::from(file.try_clone()?))
173        }
174    }
175}
176
177pub fn spawn_sync_daemon(
178    command: &mut Command,
179    stdio: crate::platform::process::DaemonStdio<'_>,
180    environment: crate::platform::process::SyncEnvironment,
181    _breakaway: bool,
182) -> io::Result<crate::platform::process::DaemonChild> {
183    spawn_sync_daemon_inner(command, stdio, environment, None)
184}
185
186pub fn spawn_sync_daemon_with_inheritance(
187    command: &mut Command,
188    stdio: crate::platform::process::DaemonStdio<'_>,
189    environment: crate::platform::process::SyncEnvironment,
190    _breakaway: bool,
191    inheritance: crate::platform::process::DaemonExecInheritance,
192) -> io::Result<crate::platform::process::DaemonChild> {
193    spawn_sync_daemon_inner(command, stdio, environment, Some(inheritance))
194}
195
196fn spawn_sync_daemon_inner(
197    command: &mut Command,
198    stdio: crate::platform::process::DaemonStdio<'_>,
199    environment: crate::platform::process::SyncEnvironment,
200    inheritance: Option<crate::platform::process::DaemonExecInheritance>,
201) -> io::Result<crate::platform::process::DaemonChild> {
202    apply_environment(command, environment);
203    command
204        .stdin(Stdio::null())
205        .stdout(daemon_slot_to_stdio(&stdio.stdout)?)
206        .stderr(daemon_slot_to_stdio(&stdio.stderr)?);
207
208    match inheritance {
209        Some(inheritance) => {
210            crate::platform::process::configure_sync_daemon_command_with_inheritance(
211                command,
212                inheritance,
213            )?;
214        }
215        None => crate::platform::process::configure_sync_daemon_command(command)?,
216    }
217
218    let child = command.spawn()?;
219    let pid = child.id();
220    Ok(crate::platform::process::DaemonChild {
221        pid,
222        inner: Box::new(child),
223    })
224}
225
226pub fn spawn_sync(
227    command: &mut Command,
228    stdio: crate::platform::process::SpawnStdio<'_>,
229    environment: crate::platform::process::SyncEnvironment,
230) -> io::Result<crate::platform::process::SpawnedChild> {
231    apply_environment(command, environment);
232    command.stdin(slot_to_stdio(&stdio.stdin)?);
233    command.stdout(slot_to_stdio(&stdio.stdout)?);
234    command.stderr(slot_to_stdio(&stdio.stderr)?);
235
236    crate::platform::process::configure_sync_contained_command(command)?;
237
238    let mut child = command.spawn()?;
239    let pid = child.id();
240    let pgid = pid as i32;
241
242    let stdin = child.stdin.take();
243    let stdout = child.stdout.take();
244    let stderr = child.stderr.take();
245
246    let child: Arc<Mutex<Option<Box<dyn UnixChild>>>> = Arc::new(Mutex::new(Some(Box::new(child))));
247
248    // Drain watcher: wait for exit, then sleep `drain_timeout`. We
249    // don't proactively close anything on Unix — Rust's ChildStdin/etc.
250    // own their fds; once the child exits and the kernel ref-counts
251    // its copies to zero, parent reads will EOF naturally.
252    if let Some(timeout) = stdio.drain_timeout {
253        let child_clone = Arc::clone(&child);
254        thread::spawn(move || {
255            // Borrow child for try_wait.  We do a polling loop so
256            // shutdown() taking the inner Child during Drop doesn't
257            // wedge us.
258            loop {
259                {
260                    let mut guard = child_clone.lock().expect("child mutex poisoned");
261                    match guard.as_mut() {
262                        Some(c) => match c.try_wait() {
263                            Ok(Some(_)) => break,
264                            Ok(None) => {}
265                            Err(_) => break,
266                        },
267                        None => return,
268                    }
269                }
270                // #199: intentional — try_wait poll on the contained
271                // child, 50ms cadence inside a bounded outer drain
272                // loop. waitpid(WNOHANG)-equivalent semantics.
273                thread::sleep(std::time::Duration::from_millis(50));
274            }
275            // #199: intentional — post-mortem pipe drain. Children's
276            // write-ends of the captured stdio pipes are still being
277            // closed by the kernel after exit; this gives readers a
278            // chance to see the final bytes before the watcher
279            // releases its keep-alive.
280            thread::sleep(timeout);
281        });
282    }
283
284    Ok(crate::platform::process::SpawnedChild {
285        stdin,
286        stdout,
287        stderr,
288        pid,
289        inner: Box::new(SpawnedInner { child, pgid }),
290    })
291}
292
293fn apply_environment(
294    command: &mut Command,
295    environment: crate::platform::process::SyncEnvironment,
296) {
297    let crate::platform::process::SyncEnvironment::Explicit(base) = environment else {
298        return;
299    };
300
301    // `env_clear` also clears Command's mutation map. Preserve additions,
302    // overrides, and removals so they are replayed after the selected base.
303    let explicit: Vec<_> = command
304        .get_envs()
305        .map(|(key, value)| (key.to_os_string(), value.map(std::ffi::OsStr::to_os_string)))
306        .collect();
307    command.env_clear();
308    command.envs(base);
309    for (key, value) in explicit {
310        match value {
311            Some(value) => {
312                command.env(key, value);
313            }
314            None => {
315                command.env_remove(key);
316            }
317        }
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use std::sync::atomic::{AtomicUsize, Ordering};
325    use std::sync::{mpsc, Condvar};
326
327    struct FakeChild {
328        wait_gate: Arc<(Mutex<bool>, Condvar)>,
329        waits: Arc<AtomicUsize>,
330        kills: Arc<AtomicUsize>,
331    }
332
333    impl UnixChild for FakeChild {
334        fn kill(&mut self) -> io::Result<()> {
335            self.kills.fetch_add(1, Ordering::SeqCst);
336            Ok(())
337        }
338
339        fn wait(&mut self) -> io::Result<i32> {
340            self.waits.fetch_add(1, Ordering::SeqCst);
341            let (lock, condvar) = &*self.wait_gate;
342            let mut released = lock.lock().expect("wait gate mutex poisoned");
343            while !*released {
344                released = condvar.wait(released).expect("wait gate mutex poisoned");
345            }
346            Ok(0)
347        }
348
349        fn try_wait(&mut self) -> io::Result<Option<i32>> {
350            self.waits.fetch_add(1, Ordering::SeqCst);
351            let released = *self.wait_gate.0.lock().expect("wait gate mutex poisoned");
352            Ok(released.then_some(0))
353        }
354    }
355
356    struct BlockedFixture {
357        inner: SpawnedInner,
358        child: Arc<Mutex<Option<Box<dyn UnixChild>>>>,
359        wait_gate: Arc<(Mutex<bool>, Condvar)>,
360        waits: Arc<AtomicUsize>,
361        kills: Arc<AtomicUsize>,
362    }
363
364    fn blocked_inner() -> BlockedFixture {
365        let wait_gate = Arc::new((Mutex::new(false), Condvar::new()));
366        let waits = Arc::new(AtomicUsize::new(0));
367        let kills = Arc::new(AtomicUsize::new(0));
368        let child: Arc<Mutex<Option<Box<dyn UnixChild>>>> =
369            Arc::new(Mutex::new(Some(Box::new(FakeChild {
370                wait_gate: Arc::clone(&wait_gate),
371                waits: Arc::clone(&waits),
372                kills: Arc::clone(&kills),
373            }))));
374        BlockedFixture {
375            inner: SpawnedInner {
376                child: Arc::clone(&child),
377                pgid: i32::MAX,
378            },
379            child,
380            wait_gate,
381            waits,
382            kills,
383        }
384    }
385
386    fn release_wait(wait_gate: &Arc<(Mutex<bool>, Condvar)>) {
387        let (lock, condvar) = &**wait_gate;
388        *lock.lock().expect("wait gate mutex poisoned") = true;
389        condvar.notify_all();
390    }
391
392    struct ShutdownOnDrop {
393        inner: Option<SpawnedInner>,
394        deadline: Instant,
395    }
396
397    impl Drop for ShutdownOnDrop {
398        fn drop(&mut self) {
399            self.inner
400                .as_mut()
401                .expect("test wrapper missing inner")
402                .shutdown_with_deadline(self.deadline);
403        }
404    }
405
406    #[test]
407    fn drop_is_bounded_when_child_wait_does_not_complete() {
408        // Regression for #619: SpawnedChild::drop delegates directly to
409        // SpawnedInner::shutdown, modeled by this wrapper around the fake child.
410        let BlockedFixture {
411            inner, wait_gate, ..
412        } = blocked_inner();
413        let (tx, rx) = mpsc::channel();
414        let started = Instant::now();
415        let worker = thread::spawn(move || {
416            drop(ShutdownOnDrop {
417                inner: Some(inner),
418                deadline: Instant::now() + Duration::from_millis(50),
419            });
420            let _ = tx.send(started.elapsed());
421        });
422
423        // The property is causal, not a stopwatch reading: Drop must return
424        // WITHOUT waiting for the child, so it must report back before the
425        // gate is released. A blocked Drop cannot, whatever the machine load.
426        //
427        // Asserting a wall-clock bound instead conflated that with "finished
428        // inside 100ms", which a loaded runner broke by 0.2ms. The window
429        // below is generous because it only bounds how long a *failure* takes
430        // to detect: a correct Drop returns at its 50ms deadline and never
431        // approaches it.
432        let timely = rx.recv_timeout(Duration::from_secs(5));
433        release_wait(&wait_gate);
434        let returned_before_release = timely.is_ok();
435        let elapsed = timely
436            .or_else(|_| rx.recv_timeout(Duration::from_secs(5)))
437            .expect("shutdown did not unblock even after releasing fake child");
438        worker.join().expect("shutdown worker panicked");
439        assert!(
440            returned_before_release,
441            "Drop blocked in child.wait() until the fake child was released              (took {elapsed:?}); its deadline should have bounded it"
442        );
443    }
444
445    #[test]
446    fn shutdown_does_not_hold_child_mutex_while_reaping() {
447        let BlockedFixture {
448            mut inner,
449            child,
450            wait_gate,
451            waits,
452            ..
453        } = blocked_inner();
454        let worker = thread::spawn(move || {
455            inner.shutdown_with_deadline(Instant::now() + Duration::from_millis(50));
456        });
457        let deadline = Instant::now() + Duration::from_secs(1);
458        while waits.load(Ordering::SeqCst) == 0 && Instant::now() < deadline {
459            thread::yield_now();
460        }
461        assert_eq!(waits.load(Ordering::SeqCst), 1, "fake wait never started");
462
463        let child_mutex_available = child.try_lock().is_ok();
464        release_wait(&wait_gate);
465        worker.join().expect("shutdown worker panicked");
466        assert!(
467            child_mutex_available,
468            "shutdown held the child mutex across reaping"
469        );
470    }
471
472    struct ReadyChild {
473        polls: Arc<AtomicUsize>,
474        waits: Arc<AtomicUsize>,
475    }
476
477    impl UnixChild for ReadyChild {
478        fn kill(&mut self) -> io::Result<()> {
479            Ok(())
480        }
481
482        fn wait(&mut self) -> io::Result<i32> {
483            self.waits.fetch_add(1, Ordering::SeqCst);
484            Ok(0)
485        }
486
487        fn try_wait(&mut self) -> io::Result<Option<i32>> {
488            self.polls.fetch_add(1, Ordering::SeqCst);
489            Ok(Some(0))
490        }
491    }
492
493    #[test]
494    fn shutdown_reaps_ready_child_exactly_once() {
495        let polls = Arc::new(AtomicUsize::new(0));
496        let waits = Arc::new(AtomicUsize::new(0));
497        let child: Arc<Mutex<Option<Box<dyn UnixChild>>>> =
498            Arc::new(Mutex::new(Some(Box::new(ReadyChild {
499                polls: Arc::clone(&polls),
500                waits: Arc::clone(&waits),
501            }))));
502        let mut inner = SpawnedInner {
503            child,
504            pgid: i32::MAX,
505        };
506
507        inner.shutdown_with_deadline(Instant::now() + Duration::from_secs(1));
508
509        assert_eq!(polls.load(Ordering::SeqCst), 1);
510        assert_eq!(waits.load(Ordering::SeqCst), 0);
511    }
512
513    #[test]
514    fn shutdown_falls_back_to_direct_kill_when_group_signal_fails() {
515        let BlockedFixture {
516            mut inner,
517            wait_gate,
518            kills,
519            ..
520        } = blocked_inner();
521        release_wait(&wait_gate);
522
523        inner.shutdown_with_deadline(Instant::now() + Duration::from_secs(1));
524
525        assert_eq!(kills.load(Ordering::SeqCst), 1);
526    }
527}