#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ProcessGroup(i32);
impl ProcessGroup {
#[must_use]
pub fn of_leader(pid: u32) -> Option<Self> {
i32::try_from(pid).ok().map(Self)
}
#[must_use]
pub const fn as_raw(self) -> i32 {
self.0
}
pub fn terminate(self) {
self.signal(Signal::Term);
}
pub fn kill(self) {
self.signal(Signal::Kill);
}
#[must_use]
pub fn is_gone(self) -> bool {
#[cfg(unix)]
{
match nix::sys::signal::killpg(nix::unistd::Pid::from_raw(self.0), None) {
Ok(()) => false,
Err(nix::errno::Errno::ESRCH | nix::errno::Errno::EPERM) => true,
Err(source) => {
tracing::warn!(
process_group = self.0,
%source,
"acp adapter: could not probe the agent's process group; reporting it as \
still present rather than claiming it is gone"
);
false
}
}
}
#[cfg(not(unix))]
{
false
}
}
fn signal(self, signal: Signal) {
#[cfg(unix)]
{
let result = nix::sys::signal::killpg(nix::unistd::Pid::from_raw(self.0), signal.nix());
match result {
Ok(()) | Err(nix::errno::Errno::ESRCH | nix::errno::Errno::EPERM) => {}
Err(source) => tracing::warn!(
process_group = self.0,
signal = signal.label(),
%source,
"acp adapter: could not signal the agent's process group; a descendant of the \
agent may still be running"
),
}
}
#[cfg(not(unix))]
{
tracing::warn!(
signal = signal.label(),
"acp adapter: this platform has no process groups, so the agent's descendants \
cannot be stopped as a tree"
);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Signal {
Term,
Kill,
}
impl Signal {
const fn label(self) -> &'static str {
match self {
Self::Term => "SIGTERM",
Self::Kill => "SIGKILL",
}
}
#[cfg(unix)]
const fn nix(self) -> nix::sys::signal::Signal {
match self {
Self::Term => nix::sys::signal::Signal::SIGTERM,
Self::Kill => nix::sys::signal::Signal::SIGKILL,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_group_id_is_the_leaders_pid_and_survives_the_round_trip() {
assert_eq!(
ProcessGroup::of_leader(4321).map(ProcessGroup::as_raw),
Some(4321),
"a pid inside the pid_t range names the group its leader leads"
);
}
#[test]
fn a_pid_outside_the_platform_range_names_no_group() {
assert!(
ProcessGroup::of_leader(u32::MAX).is_none(),
"a pid that does not fit pid_t cannot be signalled, and pretending it can would aim a \
SIGKILL at a number nobody leads"
);
}
#[test]
fn a_group_that_never_existed_reads_as_gone() {
let absent = ProcessGroup::of_leader(0x7FFF_0000);
#[cfg(unix)]
assert!(
absent.is_some_and(ProcessGroup::is_gone),
"a group nothing leads must read as gone, or the shutdown ladder would wait out its \
whole window on every session"
);
#[cfg(not(unix))]
let _ = absent;
}
}