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    apply_environment(command, environment);
184    command
185        .stdin(Stdio::null())
186        .stdout(daemon_slot_to_stdio(&stdio.stdout)?)
187        .stderr(daemon_slot_to_stdio(&stdio.stderr)?);
188
189    crate::platform::process::configure_sync_daemon_command(command)?;
190
191    let child = command.spawn()?;
192    let pid = child.id();
193    Ok(crate::platform::process::DaemonChild {
194        pid,
195        inner: Box::new(child),
196    })
197}
198
199pub fn spawn_sync(
200    command: &mut Command,
201    stdio: crate::platform::process::SpawnStdio<'_>,
202    environment: crate::platform::process::SyncEnvironment,
203) -> io::Result<crate::platform::process::SpawnedChild> {
204    apply_environment(command, environment);
205    command.stdin(slot_to_stdio(&stdio.stdin)?);
206    command.stdout(slot_to_stdio(&stdio.stdout)?);
207    command.stderr(slot_to_stdio(&stdio.stderr)?);
208
209    crate::platform::process::configure_sync_contained_command(command)?;
210
211    let mut child = command.spawn()?;
212    let pid = child.id();
213    let pgid = pid as i32;
214
215    let stdin = child.stdin.take();
216    let stdout = child.stdout.take();
217    let stderr = child.stderr.take();
218
219    let child: Arc<Mutex<Option<Box<dyn UnixChild>>>> = Arc::new(Mutex::new(Some(Box::new(child))));
220
221    // Drain watcher: wait for exit, then sleep `drain_timeout`. We
222    // don't proactively close anything on Unix — Rust's ChildStdin/etc.
223    // own their fds; once the child exits and the kernel ref-counts
224    // its copies to zero, parent reads will EOF naturally.
225    if let Some(timeout) = stdio.drain_timeout {
226        let child_clone = Arc::clone(&child);
227        thread::spawn(move || {
228            // Borrow child for try_wait.  We do a polling loop so
229            // shutdown() taking the inner Child during Drop doesn't
230            // wedge us.
231            loop {
232                {
233                    let mut guard = child_clone.lock().expect("child mutex poisoned");
234                    match guard.as_mut() {
235                        Some(c) => match c.try_wait() {
236                            Ok(Some(_)) => break,
237                            Ok(None) => {}
238                            Err(_) => break,
239                        },
240                        None => return,
241                    }
242                }
243                // #199: intentional — try_wait poll on the contained
244                // child, 50ms cadence inside a bounded outer drain
245                // loop. waitpid(WNOHANG)-equivalent semantics.
246                thread::sleep(std::time::Duration::from_millis(50));
247            }
248            // #199: intentional — post-mortem pipe drain. Children's
249            // write-ends of the captured stdio pipes are still being
250            // closed by the kernel after exit; this gives readers a
251            // chance to see the final bytes before the watcher
252            // releases its keep-alive.
253            thread::sleep(timeout);
254        });
255    }
256
257    Ok(crate::platform::process::SpawnedChild {
258        stdin,
259        stdout,
260        stderr,
261        pid,
262        inner: Box::new(SpawnedInner { child, pgid }),
263    })
264}
265
266fn apply_environment(
267    command: &mut Command,
268    environment: crate::platform::process::SyncEnvironment,
269) {
270    let crate::platform::process::SyncEnvironment::Explicit(base) = environment else {
271        return;
272    };
273
274    // `env_clear` also clears Command's mutation map. Preserve additions,
275    // overrides, and removals so they are replayed after the selected base.
276    let explicit: Vec<_> = command
277        .get_envs()
278        .map(|(key, value)| (key.to_os_string(), value.map(std::ffi::OsStr::to_os_string)))
279        .collect();
280    command.env_clear();
281    command.envs(base);
282    for (key, value) in explicit {
283        match value {
284            Some(value) => {
285                command.env(key, value);
286            }
287            None => {
288                command.env_remove(key);
289            }
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use std::sync::atomic::{AtomicUsize, Ordering};
298    use std::sync::{mpsc, Condvar};
299
300    struct FakeChild {
301        wait_gate: Arc<(Mutex<bool>, Condvar)>,
302        waits: Arc<AtomicUsize>,
303        kills: Arc<AtomicUsize>,
304    }
305
306    impl UnixChild for FakeChild {
307        fn kill(&mut self) -> io::Result<()> {
308            self.kills.fetch_add(1, Ordering::SeqCst);
309            Ok(())
310        }
311
312        fn wait(&mut self) -> io::Result<i32> {
313            self.waits.fetch_add(1, Ordering::SeqCst);
314            let (lock, condvar) = &*self.wait_gate;
315            let mut released = lock.lock().expect("wait gate mutex poisoned");
316            while !*released {
317                released = condvar.wait(released).expect("wait gate mutex poisoned");
318            }
319            Ok(0)
320        }
321
322        fn try_wait(&mut self) -> io::Result<Option<i32>> {
323            self.waits.fetch_add(1, Ordering::SeqCst);
324            let released = *self.wait_gate.0.lock().expect("wait gate mutex poisoned");
325            Ok(released.then_some(0))
326        }
327    }
328
329    struct BlockedFixture {
330        inner: SpawnedInner,
331        child: Arc<Mutex<Option<Box<dyn UnixChild>>>>,
332        wait_gate: Arc<(Mutex<bool>, Condvar)>,
333        waits: Arc<AtomicUsize>,
334        kills: Arc<AtomicUsize>,
335    }
336
337    fn blocked_inner() -> BlockedFixture {
338        let wait_gate = Arc::new((Mutex::new(false), Condvar::new()));
339        let waits = Arc::new(AtomicUsize::new(0));
340        let kills = Arc::new(AtomicUsize::new(0));
341        let child: Arc<Mutex<Option<Box<dyn UnixChild>>>> =
342            Arc::new(Mutex::new(Some(Box::new(FakeChild {
343                wait_gate: Arc::clone(&wait_gate),
344                waits: Arc::clone(&waits),
345                kills: Arc::clone(&kills),
346            }))));
347        BlockedFixture {
348            inner: SpawnedInner {
349                child: Arc::clone(&child),
350                pgid: i32::MAX,
351            },
352            child,
353            wait_gate,
354            waits,
355            kills,
356        }
357    }
358
359    fn release_wait(wait_gate: &Arc<(Mutex<bool>, Condvar)>) {
360        let (lock, condvar) = &**wait_gate;
361        *lock.lock().expect("wait gate mutex poisoned") = true;
362        condvar.notify_all();
363    }
364
365    struct ShutdownOnDrop {
366        inner: Option<SpawnedInner>,
367        deadline: Instant,
368    }
369
370    impl Drop for ShutdownOnDrop {
371        fn drop(&mut self) {
372            self.inner
373                .as_mut()
374                .expect("test wrapper missing inner")
375                .shutdown_with_deadline(self.deadline);
376        }
377    }
378
379    #[test]
380    fn drop_is_bounded_when_child_wait_does_not_complete() {
381        // Regression for #619: SpawnedChild::drop delegates directly to
382        // SpawnedInner::shutdown, modeled by this wrapper around the fake child.
383        let BlockedFixture {
384            inner, wait_gate, ..
385        } = blocked_inner();
386        let (tx, rx) = mpsc::channel();
387        let started = Instant::now();
388        let worker = thread::spawn(move || {
389            drop(ShutdownOnDrop {
390                inner: Some(inner),
391                deadline: Instant::now() + Duration::from_millis(50),
392            });
393            let _ = tx.send(started.elapsed());
394        });
395
396        // The property is causal, not a stopwatch reading: Drop must return
397        // WITHOUT waiting for the child, so it must report back before the
398        // gate is released. A blocked Drop cannot, whatever the machine load.
399        //
400        // Asserting a wall-clock bound instead conflated that with "finished
401        // inside 100ms", which a loaded runner broke by 0.2ms. The window
402        // below is generous because it only bounds how long a *failure* takes
403        // to detect: a correct Drop returns at its 50ms deadline and never
404        // approaches it.
405        let timely = rx.recv_timeout(Duration::from_secs(5));
406        release_wait(&wait_gate);
407        let returned_before_release = timely.is_ok();
408        let elapsed = timely
409            .or_else(|_| rx.recv_timeout(Duration::from_secs(5)))
410            .expect("shutdown did not unblock even after releasing fake child");
411        worker.join().expect("shutdown worker panicked");
412        assert!(
413            returned_before_release,
414            "Drop blocked in child.wait() until the fake child was released              (took {elapsed:?}); its deadline should have bounded it"
415        );
416    }
417
418    #[test]
419    fn shutdown_does_not_hold_child_mutex_while_reaping() {
420        let BlockedFixture {
421            mut inner,
422            child,
423            wait_gate,
424            waits,
425            ..
426        } = blocked_inner();
427        let worker = thread::spawn(move || {
428            inner.shutdown_with_deadline(Instant::now() + Duration::from_millis(50));
429        });
430        let deadline = Instant::now() + Duration::from_secs(1);
431        while waits.load(Ordering::SeqCst) == 0 && Instant::now() < deadline {
432            thread::yield_now();
433        }
434        assert_eq!(waits.load(Ordering::SeqCst), 1, "fake wait never started");
435
436        let child_mutex_available = child.try_lock().is_ok();
437        release_wait(&wait_gate);
438        worker.join().expect("shutdown worker panicked");
439        assert!(
440            child_mutex_available,
441            "shutdown held the child mutex across reaping"
442        );
443    }
444
445    struct ReadyChild {
446        polls: Arc<AtomicUsize>,
447        waits: Arc<AtomicUsize>,
448    }
449
450    impl UnixChild for ReadyChild {
451        fn kill(&mut self) -> io::Result<()> {
452            Ok(())
453        }
454
455        fn wait(&mut self) -> io::Result<i32> {
456            self.waits.fetch_add(1, Ordering::SeqCst);
457            Ok(0)
458        }
459
460        fn try_wait(&mut self) -> io::Result<Option<i32>> {
461            self.polls.fetch_add(1, Ordering::SeqCst);
462            Ok(Some(0))
463        }
464    }
465
466    #[test]
467    fn shutdown_reaps_ready_child_exactly_once() {
468        let polls = Arc::new(AtomicUsize::new(0));
469        let waits = Arc::new(AtomicUsize::new(0));
470        let child: Arc<Mutex<Option<Box<dyn UnixChild>>>> =
471            Arc::new(Mutex::new(Some(Box::new(ReadyChild {
472                polls: Arc::clone(&polls),
473                waits: Arc::clone(&waits),
474            }))));
475        let mut inner = SpawnedInner {
476            child,
477            pgid: i32::MAX,
478        };
479
480        inner.shutdown_with_deadline(Instant::now() + Duration::from_secs(1));
481
482        assert_eq!(polls.load(Ordering::SeqCst), 1);
483        assert_eq!(waits.load(Ordering::SeqCst), 0);
484    }
485
486    #[test]
487    fn shutdown_falls_back_to_direct_kill_when_group_signal_fails() {
488        let BlockedFixture {
489            mut inner,
490            wait_gate,
491            kills,
492            ..
493        } = blocked_inner();
494        release_wait(&wait_gate);
495
496        inner.shutdown_with_deadline(Instant::now() + Duration::from_secs(1));
497
498        assert_eq!(kills.load(Ordering::SeqCst), 1);
499    }
500}