process_wrap/std/
process_group.rs

1use std::{
2	io::{Error, Result},
3	ops::ControlFlow,
4	os::unix::process::{CommandExt, ExitStatusExt},
5	process::{Child, Command, ExitStatus},
6};
7
8use nix::{
9	errno::Errno,
10	libc,
11	sys::{
12		signal::{killpg, Signal},
13		wait::WaitPidFlag,
14	},
15	unistd::Pid,
16};
17#[cfg(feature = "tracing")]
18use tracing::instrument;
19
20use crate::ChildExitStatus;
21
22use super::{StdChildWrapper, StdCommandWrap, StdCommandWrapper};
23
24/// Wrapper which sets the process group of a `Command`.
25///
26/// This wrapper is only available on Unix.
27///
28/// It sets the process group of a [`Command`], either to itself as the leader of a new group, or to
29/// an existing one by its PGID. See [setpgid(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/setpgid.html).
30///
31/// Process groups direct signals to all members of the group, and also serve to control job
32/// placement in foreground or background, among other actions.
33///
34/// This wrapper provides a child wrapper: [`ProcessGroupChild`].
35#[derive(Clone, Copy, Debug)]
36pub struct ProcessGroup {
37	leader: Pid,
38}
39
40impl ProcessGroup {
41	/// Create a process group wrapper setting up a new process group with the command as the leader.
42	pub fn leader() -> Self {
43		Self {
44			leader: Pid::from_raw(0),
45		}
46	}
47
48	/// Create a process group wrapper attaching the command to an existing process group ID.
49	pub fn attach_to(leader: u32) -> Self {
50		Self {
51			leader: Pid::from_raw(leader as _),
52		}
53	}
54}
55
56/// Wrapper for `Child` which ensures that all processes in the group are reaped.
57#[derive(Debug)]
58pub struct ProcessGroupChild {
59	inner: Box<dyn StdChildWrapper>,
60	exit_status: ChildExitStatus,
61	pgid: Pid,
62}
63
64impl ProcessGroupChild {
65	#[cfg_attr(feature = "tracing", instrument(level = "debug"))]
66	pub(crate) fn new(inner: Box<dyn StdChildWrapper>, pgid: Pid) -> Self {
67		Self {
68			inner,
69			exit_status: ChildExitStatus::Running,
70			pgid,
71		}
72	}
73
74	/// Get the process group ID of this child process.
75	///
76	/// See: [`man 'setpgid(2)'`](https://www.man7.org/linux/man-pages/man2/setpgid.2.html)
77	pub fn pgid(&self) -> u32 {
78		self.pgid.as_raw() as _
79	}
80}
81
82impl StdCommandWrapper for ProcessGroup {
83	#[cfg_attr(feature = "tracing", instrument(level = "debug", skip(self)))]
84	fn pre_spawn(&mut self, command: &mut Command, _core: &StdCommandWrap) -> Result<()> {
85		command.process_group(self.leader.as_raw());
86		Ok(())
87	}
88
89	#[cfg_attr(feature = "tracing", instrument(level = "debug", skip(self)))]
90	fn wrap_child(
91		&mut self,
92		inner: Box<dyn StdChildWrapper>,
93		_core: &StdCommandWrap,
94	) -> Result<Box<dyn StdChildWrapper>> {
95		let pgid = Pid::from_raw(i32::try_from(inner.id()).expect("Command PID > i32::MAX"));
96
97		Ok(Box::new(ProcessGroupChild::new(inner, pgid)))
98	}
99}
100
101impl ProcessGroupChild {
102	#[cfg_attr(feature = "tracing", instrument(level = "debug", skip(self)))]
103	fn signal_imp(&self, sig: Signal) -> Result<()> {
104		killpg(self.pgid, sig).map_err(Error::from)
105	}
106
107	#[cfg_attr(feature = "tracing", instrument(level = "debug"))]
108	fn wait_imp(pgid: Pid, flag: WaitPidFlag) -> Result<ControlFlow<Option<ExitStatus>>> {
109		// wait for processes in a loop until every process in this group has
110		// exited (this ensures that we reap any zombies that may have been
111		// created if the parent exited after spawning children, but didn't wait
112		// for those children to exit)
113		let mut parent_exit_status: Option<ExitStatus> = None;
114		loop {
115			// we can't use the safe wrapper directly because it doesn't return
116			// the raw status, and we need it to convert to the std's ExitStatus
117			let mut status: i32 = 0;
118			match unsafe {
119				libc::waitpid(-pgid.as_raw(), &mut status as *mut libc::c_int, flag.bits())
120			} {
121				0 => {
122					// zero should only happen if WNOHANG was passed in,
123					// and means that no processes have yet to exit
124					return Ok(ControlFlow::Continue(()));
125				}
126				-1 => {
127					match Errno::last() {
128						Errno::ECHILD => {
129							// no more children to reap; this is a graceful exit
130							return Ok(ControlFlow::Break(parent_exit_status));
131						}
132						errno => {
133							return Err(Error::from(errno));
134						}
135					}
136				}
137				pid => {
138					// a process exited. was it the parent process that we
139					// started? if so, collect the exit signal, otherwise we
140					// reaped a zombie process and should continue looping
141					if pgid == Pid::from_raw(pid) {
142						parent_exit_status = Some(ExitStatus::from_raw(status));
143					} else {
144						// reaped a zombie child; keep looping
145					}
146				}
147			};
148		}
149	}
150}
151
152impl StdChildWrapper for ProcessGroupChild {
153	fn inner(&self) -> &Child {
154		self.inner.inner()
155	}
156	fn inner_mut(&mut self) -> &mut Child {
157		self.inner.inner_mut()
158	}
159	fn into_inner(self: Box<Self>) -> Child {
160		self.inner.into_inner()
161	}
162
163	#[cfg_attr(feature = "tracing", instrument(level = "debug", skip(self)))]
164	fn start_kill(&mut self) -> Result<()> {
165		self.signal_imp(Signal::SIGKILL)
166	}
167
168	#[cfg_attr(feature = "tracing", instrument(level = "debug", skip(self)))]
169	fn wait(&mut self) -> Result<ExitStatus> {
170		if let ChildExitStatus::Exited(status) = &self.exit_status {
171			return Ok(*status);
172		}
173
174		// always wait for parent to exit first, as by the time it does,
175		// it's likely that all its children have already been reaped.
176		let status = self.inner.wait()?;
177		self.exit_status = ChildExitStatus::Exited(status);
178
179		// nevertheless, now wait and make sure we reap all children.
180		Self::wait_imp(self.pgid, WaitPidFlag::empty())?;
181		Ok(status)
182	}
183
184	#[cfg_attr(feature = "tracing", instrument(level = "debug", skip(self)))]
185	fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
186		if let ChildExitStatus::Exited(status) = &self.exit_status {
187			return Ok(Some(*status));
188		}
189
190		match Self::wait_imp(self.pgid, WaitPidFlag::WNOHANG)? {
191			ControlFlow::Break(res) => {
192				if let Some(status) = res {
193					self.exit_status = ChildExitStatus::Exited(status);
194				}
195				Ok(res)
196			}
197			ControlFlow::Continue(()) => {
198				let exited = self.inner.try_wait()?;
199				if let Some(exited) = exited {
200					self.exit_status = ChildExitStatus::Exited(exited);
201				}
202				Ok(exited)
203			}
204		}
205	}
206
207	fn signal(&self, sig: i32) -> Result<()> {
208		self.signal_imp(Signal::try_from(sig)?)
209	}
210}