use std::io;
#[cfg(all(unix, feature = "process"))]
pub fn kill_process_group(pgid: i32) -> io::Result<()> {
if pgid <= 0 {
let refusal = Err(invalid_pgid());
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "kill_process_group: returning an error to the caller");
return refusal;
}
let pid = pid_from_raw(pgid)?;
rustix::process::kill_process_group(pid, rustix::process::Signal::KILL).map_err(errno_to_io)
}
#[cfg(all(not(unix), feature = "process"))]
pub fn kill_process_group(pgid: i32) -> io::Result<()> {
let _ = pgid;
Err(unsupported("process kill_process_group is Unix-only"))
}
#[cfg(all(unix, feature = "process"))]
pub fn process_group_exists(pgid: i32) -> io::Result<bool> {
if pgid <= 0 {
let refusal = Err(invalid_pgid());
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "process_group_exists: returning an error to the caller");
return refusal;
}
let pid = pid_from_raw(pgid)?;
match rustix::process::test_kill_process_group(pid) {
Ok(()) | Err(rustix::io::Errno::PERM) => Ok(true),
Err(rustix::io::Errno::SRCH) => Ok(false),
Err(errno) => Err(errno_to_io(errno)),
}
}
#[cfg(all(not(unix), feature = "process"))]
pub fn process_group_exists(pgid: i32) -> io::Result<bool> {
let _ = pgid;
Err(unsupported("process process_group_exists is Unix-only"))
}
#[cfg(all(unix, feature = "process"))]
pub fn child_has_exited_without_reaping(pid: i32) -> io::Result<bool> {
if pid <= 0 {
let refusal = Err(invalid_pid());
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "child_has_exited_without_reaping: returning an error to the caller");
return refusal;
}
let Some(target) = rustix::process::Pid::from_raw(pid) else {
let refusal = Err(invalid_pid());
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "child_has_exited_without_reaping: returning an error to the caller");
return refusal;
};
let observed = rustix::process::waitid(
rustix::process::WaitId::Pid(target),
rustix::process::WaitIdOptions::EXITED
| rustix::process::WaitIdOptions::NOHANG
| rustix::process::WaitIdOptions::NOWAIT,
)
.map_err(errno_to_io)?;
Ok(observed.is_some())
}
#[cfg(all(not(unix), feature = "process"))]
pub fn child_has_exited_without_reaping(pid: i32) -> io::Result<bool> {
let _ = pid;
Err(unsupported(
"process child_has_exited_without_reaping is Unix-only",
))
}
#[cfg(all(unix, feature = "process"))]
fn errno_to_io(errno: rustix::io::Errno) -> io::Error {
io::Error::from_raw_os_error(errno.raw_os_error())
}
#[cfg(all(unix, feature = "process"))]
fn pid_from_raw(pgid: i32) -> io::Result<rustix::process::Pid> {
rustix::process::Pid::from_raw(pgid).ok_or_else(invalid_pgid)
}
#[cfg(all(unix, feature = "process"))]
fn invalid_pgid() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
"process group id must be a positive pid",
)
}
#[cfg(all(unix, feature = "process"))]
fn invalid_pid() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
"process id must be a positive pid",
)
}
#[cfg(all(not(unix), feature = "process"))]
fn unsupported(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::Unsupported, message)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_pgid_is_rejected() {
assert!(kill_process_group(0).is_err());
assert!(kill_process_group(-1).is_err());
}
#[test]
#[cfg(unix)]
fn nonexistent_group_surfs_os_error() {
assert!(kill_process_group(i32::MAX).is_err());
}
#[test]
#[cfg(unix)]
fn a_live_group_is_present_and_a_reaped_one_is_absent() -> Result<(), Box<dyn std::error::Error>>
{
use std::os::unix::process::CommandExt;
let mut child = std::process::Command::new("sleep")
.arg("30")
.process_group(0)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
let group = i32::try_from(child.id())?;
assert!(
process_group_exists(group)?,
"the live child owns its group"
);
child.kill()?;
child.wait()?;
assert!(!process_group_exists(group)?, "the reaped group is absent");
Ok(())
}
#[test]
#[cfg(unix)]
fn the_callers_group_and_nonpositive_ids_are_not_probed() {
for refused in [0, -1] {
assert_eq!(
process_group_exists(refused).map_err(|error| error.kind()),
Err(std::io::ErrorKind::InvalidInput),
"group id {refused} names the caller or a process, not a group"
);
}
}
#[test]
fn invalid_pid_is_rejected_for_the_exit_observation() {
assert!(child_has_exited_without_reaping(0).is_err());
assert!(child_has_exited_without_reaping(-1).is_err());
}
#[test]
#[cfg(unix)]
fn a_non_child_pid_is_an_error_not_an_observation() {
assert!(child_has_exited_without_reaping(i32::MAX).is_err());
}
#[test]
#[cfg(unix)]
fn a_live_child_is_reported_not_yet_exited() -> Result<(), Box<dyn std::error::Error>> {
let mut child = std::process::Command::new("sleep")
.arg("30")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
let pid = i32::try_from(child.id())?;
assert!(
!child_has_exited_without_reaping(pid)?,
"live child reported exited"
);
child.kill()?;
let status = child.wait()?;
assert!(
!status.success(),
"the killed probe child must report its signal, not success"
);
Ok(())
}
#[test]
#[cfg(unix)]
fn an_exited_child_is_reported_exited_and_stays_reapable()
-> Result<(), Box<dyn std::error::Error>> {
let mut child = std::process::Command::new("true")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
let pid = i32::try_from(child.id())?;
let mut observed_exit = false;
for _ in 0..100_000 {
if child_has_exited_without_reaping(pid)? {
observed_exit = true;
break;
}
std::thread::yield_now();
}
assert!(
observed_exit,
"an exited child must be observed within the bound"
);
let status = child.wait()?;
assert!(status.success(), "`true` must report success after reaping");
Ok(())
}
}