1use std::io;
4use std::process::{Child, Command, ExitStatus};
5
6#[cfg(unix)]
7use std::os::unix::process::CommandExt as _;
8#[cfg(windows)]
9use std::os::windows::process::CommandExt as _;
10#[cfg(any(
11 windows,
12 all(
13 unix,
14 not(any(
15 target_os = "cygwin",
16 target_os = "horizon",
17 target_os = "openbsd",
18 target_os = "redox",
19 target_os = "wasi"
20 ))
21 )
22))]
23use std::sync::Arc;
24
25#[cfg(unix)]
26use rustix::process::Pid;
27#[cfg(all(
28 unix,
29 not(any(
30 target_os = "cygwin",
31 target_os = "horizon",
32 target_os = "openbsd",
33 target_os = "redox",
34 target_os = "wasi"
35 ))
36))]
37use rustix::process::{waitid, WaitId, WaitIdOptions};
38#[cfg(windows)]
39use windows_sys::Win32::System::Threading::{CREATE_NO_WINDOW, CREATE_SUSPENDED};
40
41#[cfg(windows)]
42use crate::process::ProcessJob;
43
44#[cfg(unix)]
45mod terminal;
46#[cfg(unix)]
47pub use terminal::ForegroundTerminalGuard;
48mod termination;
49#[cfg(all(
50 unix,
51 not(any(
52 target_os = "cygwin",
53 target_os = "horizon",
54 target_os = "openbsd",
55 target_os = "redox",
56 target_os = "wasi"
57 ))
58))]
59mod unix_controller;
60#[cfg(all(
61 unix,
62 not(any(
63 target_os = "cygwin",
64 target_os = "horizon",
65 target_os = "openbsd",
66 target_os = "redox",
67 target_os = "wasi"
68 ))
69))]
70use unix_controller::UnixProcessGroup;
71#[cfg(all(
72 unix,
73 not(any(
74 target_os = "cygwin",
75 target_os = "horizon",
76 target_os = "openbsd",
77 target_os = "redox",
78 target_os = "wasi"
79 ))
80))]
81mod unix_group_liveness;
82#[cfg(windows)]
83mod windows;
84#[cfg(windows)]
85use windows::resume_suspended_process;
86
87pub struct ProcessTreeChild {
95 child: Child,
96 armed: bool,
97 #[cfg(unix)]
98 process_group: i32,
99 #[cfg(all(
100 unix,
101 not(any(
102 target_os = "cygwin",
103 target_os = "horizon",
104 target_os = "openbsd",
105 target_os = "redox",
106 target_os = "wasi"
107 ))
108 ))]
109 process_group_control: Arc<UnixProcessGroup>,
110 #[cfg(windows)]
111 job: Arc<ProcessJob>,
112}
113
114#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
119pub enum ConsoleWindowBehavior {
120 #[default]
122 Inherit,
123 Suppress,
125}
126
127#[derive(Clone, Copy)]
128enum JobBreakawayBehavior {
129 Disallow,
130 AllowExplicit,
131}
132
133#[derive(Clone)]
135pub struct ProcessTreeController {
136 #[cfg(all(
137 unix,
138 not(any(
139 target_os = "cygwin",
140 target_os = "horizon",
141 target_os = "openbsd",
142 target_os = "redox",
143 target_os = "wasi"
144 ))
145 ))]
146 process_group: Arc<UnixProcessGroup>,
147 #[cfg(windows)]
148 job: Arc<ProcessJob>,
149}
150
151impl ProcessTreeController {
152 pub fn terminate(&self) -> io::Result<()> {
154 #[cfg(all(
155 unix,
156 not(any(
157 target_os = "cygwin",
158 target_os = "horizon",
159 target_os = "openbsd",
160 target_os = "redox",
161 target_os = "wasi"
162 ))
163 ))]
164 {
165 self.process_group.terminate()
166 }
167
168 #[cfg(all(
169 unix,
170 any(
171 target_os = "cygwin",
172 target_os = "horizon",
173 target_os = "openbsd",
174 target_os = "redox",
175 target_os = "wasi"
176 )
177 ))]
178 {
179 Ok(())
180 }
181
182 #[cfg(windows)]
183 {
184 self.job.terminate(1)
185 }
186
187 #[cfg(not(any(unix, windows)))]
188 {
189 Ok(())
190 }
191 }
192}
193
194impl ProcessTreeChild {
195 pub fn spawn(command: &mut Command) -> io::Result<Self> {
198 Self::spawn_with_console_window(command, ConsoleWindowBehavior::Inherit)
199 }
200
201 pub fn spawn_allowing_explicit_job_breakaway(command: &mut Command) -> io::Result<Self> {
207 Self::spawn_with_options(
208 command,
209 ConsoleWindowBehavior::Inherit,
210 JobBreakawayBehavior::AllowExplicit,
211 )
212 }
213
214 pub fn spawn_with_console_window(
217 command: &mut Command,
218 console_window: ConsoleWindowBehavior,
219 ) -> io::Result<Self> {
220 Self::spawn_with_options(command, console_window, JobBreakawayBehavior::Disallow)
221 }
222
223 fn spawn_with_options(
224 command: &mut Command,
225 console_window: ConsoleWindowBehavior,
226 job_breakaway: JobBreakawayBehavior,
227 ) -> io::Result<Self> {
228 #[cfg(not(windows))]
229 let _ = (console_window, job_breakaway);
230
231 #[cfg(all(
232 unix,
233 not(any(
234 target_os = "cygwin",
235 target_os = "horizon",
236 target_os = "openbsd",
237 target_os = "redox",
238 target_os = "wasi"
239 ))
240 ))]
241 {
242 command.process_group(0);
243 let child = command.spawn()?;
244 let process_group = i32::try_from(child.id())
245 .map_err(|_| io::Error::other("child process id does not fit in i32"))?;
246 Ok(Self {
247 child,
248 armed: true,
249 process_group,
250 process_group_control: Arc::new(UnixProcessGroup::new(process_group)),
251 })
252 }
253
254 #[cfg(windows)]
255 {
256 command.creation_flags(windows_creation_flags(console_window));
257 let mut child = command.spawn()?;
258 let job = match job_breakaway {
259 JobBreakawayBehavior::Disallow => ProcessJob::for_child(&child),
260 JobBreakawayBehavior::AllowExplicit => {
261 ProcessJob::for_child_allowing_breakaway(&child)
262 }
263 };
264 let job = match job {
265 Ok(job) => Arc::new(job),
266 Err(error) => {
267 let _ = child.kill();
268 let _ = child.wait();
269 return Err(error);
270 }
271 };
272 if let Err(error) = resume_suspended_process(child.id()) {
273 let _ = job.terminate(1);
274 let _ = child.kill();
275 let _ = child.wait();
276 return Err(error);
277 }
278 Ok(Self {
279 child,
280 armed: true,
281 job,
282 })
283 }
284
285 #[cfg(all(
286 unix,
287 any(
288 target_os = "cygwin",
289 target_os = "horizon",
290 target_os = "openbsd",
291 target_os = "redox",
292 target_os = "wasi"
293 )
294 ))]
295 {
296 let child = command.spawn()?;
297 let process_group = i32::try_from(child.id())
298 .map_err(|_| io::Error::other("child process id does not fit in i32"))?;
299 Ok(Self {
300 child,
301 armed: true,
302 process_group,
303 })
304 }
305
306 #[cfg(not(any(unix, windows)))]
307 {
308 Ok(Self {
309 child: command.spawn()?,
310 armed: true,
311 })
312 }
313 }
314
315 #[must_use]
318 pub fn controller(&self) -> ProcessTreeController {
319 ProcessTreeController {
320 #[cfg(all(
321 unix,
322 not(any(
323 target_os = "cygwin",
324 target_os = "horizon",
325 target_os = "openbsd",
326 target_os = "redox",
327 target_os = "wasi"
328 ))
329 ))]
330 process_group: Arc::clone(&self.process_group_control),
331 #[cfg(windows)]
332 job: Arc::clone(&self.job),
333 }
334 }
335
336 pub fn child_mut(&mut self) -> &mut Child {
339 &mut self.child
340 }
341
342 pub fn has_exited(&mut self) -> io::Result<bool> {
345 if !self.armed {
346 return Ok(true);
347 }
348
349 #[cfg(all(
350 unix,
351 not(any(
352 target_os = "cygwin",
353 target_os = "horizon",
354 target_os = "openbsd",
355 target_os = "redox",
356 target_os = "wasi"
357 ))
358 ))]
359 {
360 let pid = Pid::from_raw(self.process_group)
361 .ok_or_else(|| io::Error::other("child process id is zero"))?;
362 Ok(waitid(
363 WaitId::Pid(pid),
364 WaitIdOptions::EXITED | WaitIdOptions::NOHANG | WaitIdOptions::NOWAIT,
365 )?
366 .is_some_and(|status| status.exited() || status.killed() || status.dumped()))
367 }
368
369 #[cfg(any(
370 not(unix),
371 all(
372 unix,
373 any(
374 target_os = "cygwin",
375 target_os = "horizon",
376 target_os = "openbsd",
377 target_os = "redox",
378 target_os = "wasi"
379 )
380 )
381 ))]
382 {
383 self.child.try_wait().map(|status| status.is_some())
384 }
385 }
386
387 #[cfg(unix)]
390 pub fn has_stopped(&mut self) -> io::Result<bool> {
391 if !self.armed {
392 return Ok(false);
393 }
394
395 #[cfg(not(any(
396 target_os = "cygwin",
397 target_os = "horizon",
398 target_os = "openbsd",
399 target_os = "redox",
400 target_os = "wasi"
401 )))]
402 {
403 let pid = Pid::from_raw(self.process_group)
404 .ok_or_else(|| io::Error::other("child process id is zero"))?;
405 Ok(waitid(
406 WaitId::Pid(pid),
407 WaitIdOptions::STOPPED | WaitIdOptions::NOHANG | WaitIdOptions::NOWAIT,
408 )?
409 .is_some_and(|status| status.stopped()))
410 }
411
412 #[cfg(any(
413 target_os = "cygwin",
414 target_os = "horizon",
415 target_os = "openbsd",
416 target_os = "redox",
417 target_os = "wasi"
418 ))]
419 {
420 Ok(false)
421 }
422 }
423
424 #[cfg(all(
426 unix,
427 not(any(
428 target_os = "cygwin",
429 target_os = "horizon",
430 target_os = "openbsd",
431 target_os = "redox",
432 target_os = "wasi"
433 ))
434 ))]
435 pub fn forward_signal(&mut self, signal: i32) -> io::Result<()> {
436 if !self.armed {
437 return Ok(());
438 }
439 let result = unsafe {
440 libc::kill(-self.process_group, signal)
444 };
445 if result == 0 {
446 return Ok(());
447 }
448 let error = io::Error::last_os_error();
449 if error.raw_os_error() == Some(libc::ESRCH)
450 || (error.raw_os_error() == Some(libc::EPERM) && self.has_exited()?)
451 {
452 Ok(())
453 } else {
454 Err(error)
455 }
456 }
457
458 #[cfg(all(
461 unix,
462 any(
463 target_os = "cygwin",
464 target_os = "horizon",
465 target_os = "openbsd",
466 target_os = "redox",
467 target_os = "wasi"
468 )
469 ))]
470 pub fn forward_signal(&mut self, signal: i32) -> io::Result<()> {
471 if !self.armed {
472 return Ok(());
473 }
474 let result = unsafe {
475 libc::kill(self.process_group, signal)
479 };
480 if result == 0 {
481 return Ok(());
482 }
483 let error = io::Error::last_os_error();
484 if error.raw_os_error() == Some(libc::ESRCH) {
485 Ok(())
486 } else {
487 Err(error)
488 }
489 }
490
491 pub fn terminate(&mut self) -> io::Result<()> {
494 if !self.armed {
495 return Ok(());
496 }
497
498 #[cfg(all(
499 unix,
500 not(any(
501 target_os = "cygwin",
502 target_os = "horizon",
503 target_os = "openbsd",
504 target_os = "redox",
505 target_os = "wasi"
506 ))
507 ))]
508 {
509 self.forward_signal(libc::SIGKILL)
510 }
511
512 #[cfg(all(
513 unix,
514 any(
515 target_os = "cygwin",
516 target_os = "horizon",
517 target_os = "openbsd",
518 target_os = "redox",
519 target_os = "wasi"
520 )
521 ))]
522 {
523 self.child.kill()
524 }
525
526 #[cfg(windows)]
527 {
528 self.job.terminate(1)
529 }
530
531 #[cfg(not(any(unix, windows)))]
532 {
533 self.child.kill()
534 }
535 }
536
537 pub fn wait(&mut self) -> io::Result<ExitStatus> {
540 #[cfg(all(
541 unix,
542 not(any(
543 target_os = "cygwin",
544 target_os = "horizon",
545 target_os = "openbsd",
546 target_os = "redox",
547 target_os = "wasi"
548 ))
549 ))]
550 let status = if let Some(status) = self.process_group_control.try_reap(&mut self.child)? {
551 status
552 } else {
553 let pid = Pid::from_raw(self.process_group)
554 .ok_or_else(|| io::Error::other("child process id is zero"))?;
555 let _ = waitid(
556 WaitId::Pid(pid),
557 WaitIdOptions::EXITED | WaitIdOptions::NOWAIT,
558 )?;
559 self.process_group_control.reap_exited(&mut self.child)?
560 };
561
562 #[cfg(not(all(
563 unix,
564 not(any(
565 target_os = "cygwin",
566 target_os = "horizon",
567 target_os = "openbsd",
568 target_os = "redox",
569 target_os = "wasi"
570 ))
571 )))]
572 let status = self.child.wait()?;
573 #[cfg(windows)]
574 self.job.disarm_kill_on_close()?;
575 self.armed = false;
576 Ok(status)
577 }
578}
579
580#[cfg(windows)]
581const fn windows_creation_flags(console_window: ConsoleWindowBehavior) -> u32 {
582 match console_window {
583 ConsoleWindowBehavior::Inherit => CREATE_SUSPENDED,
584 ConsoleWindowBehavior::Suppress => CREATE_SUSPENDED | CREATE_NO_WINDOW,
585 }
586}
587
588#[cfg(all(test, windows))]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn process_tree_creation_flags_preserve_console_window_policy() {
594 let inherited = windows_creation_flags(ConsoleWindowBehavior::Inherit);
595 assert_eq!(inherited & CREATE_SUSPENDED, CREATE_SUSPENDED);
596 assert_eq!(inherited & CREATE_NO_WINDOW, 0);
597
598 let suppressed = windows_creation_flags(ConsoleWindowBehavior::Suppress);
599 assert_eq!(suppressed & CREATE_SUSPENDED, CREATE_SUSPENDED);
600 assert_eq!(suppressed & CREATE_NO_WINDOW, CREATE_NO_WINDOW);
601 }
602}
603
604impl Drop for ProcessTreeChild {
605 fn drop(&mut self) {
606 if !self.armed {
607 return;
608 }
609 let _ = self.terminate();
610 let _ = self.child.kill();
611
612 #[cfg(all(
613 unix,
614 not(any(
615 target_os = "cygwin",
616 target_os = "horizon",
617 target_os = "openbsd",
618 target_os = "redox",
619 target_os = "wasi"
620 ))
621 ))]
622 if self.wait().is_err() {
623 self.process_group_control.disarm();
624 }
625
626 #[cfg(not(all(
627 unix,
628 not(any(
629 target_os = "cygwin",
630 target_os = "horizon",
631 target_os = "openbsd",
632 target_os = "redox",
633 target_os = "wasi"
634 ))
635 )))]
636 let _ = self.child.wait();
637 self.armed = false;
638 }
639}