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 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 if let Some(timeout) = stdio.drain_timeout {
253 let child_clone = Arc::clone(&child);
254 thread::spawn(move || {
255 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 thread::sleep(std::time::Duration::from_millis(50));
274 }
275 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 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 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 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}