running_process_platform_internal/
sync_spawn_group.rs1use 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 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 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 if let Some(timeout) = stdio.drain_timeout {
226 let child_clone = Arc::clone(&child);
227 thread::spawn(move || {
228 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 thread::sleep(std::time::Duration::from_millis(50));
247 }
248 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 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 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 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}