use std::path::PathBuf;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Described {
pub(crate) pid: libc::pid_t,
pub(crate) name: String,
pub(crate) argv: Vec<String>,
pub(crate) cwd: Option<PathBuf>,
}
pub(crate) fn describe_group(pgid: libc::pid_t) -> Option<Described> {
if pgid <= 0 {
return None;
}
let pid = if alive(pgid) {
pgid
} else {
all_pids().into_iter().find(|pid| {
let group = unsafe { libc::getpgid(*pid) };
group == pgid && alive(*pid)
})?
};
Some(Described {
pid,
name: name(pid)?,
argv: argv(pid).unwrap_or_default(),
cwd: cwd(pid),
})
}
#[cfg(target_os = "macos")]
pub(crate) fn name(pid: libc::pid_t) -> Option<String> {
let mut buf = [0u8; 4 * libc::MAXPATHLEN as usize];
let len = unsafe { libc::proc_name(pid, buf.as_mut_ptr().cast(), buf.len() as u32) };
(len > 0).then(|| String::from_utf8_lossy(&buf[..len as usize]).into_owned())
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn name(pid: libc::pid_t) -> Option<String> {
let comm = std::fs::read_to_string(format!("/proc/{pid}/comm")).ok()?;
Some(comm.trim_end_matches('\n').to_owned())
}
#[cfg(target_os = "macos")]
fn argv(pid: libc::pid_t) -> Option<Vec<String>> {
let mut mib = [libc::CTL_KERN, libc::KERN_ARGMAX];
let mut max: libc::c_int = 0;
let mut size = std::mem::size_of_val(&max);
if unsafe {
libc::sysctl(
mib.as_mut_ptr(),
2,
(&raw mut max).cast(),
&raw mut size,
std::ptr::null_mut(),
0,
)
} != 0
{
return None;
}
let mut buf = vec![0u8; usize::try_from(max).ok()?];
let mut size = buf.len();
let mut mib = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid];
if unsafe {
libc::sysctl(
mib.as_mut_ptr(),
3,
buf.as_mut_ptr().cast(),
&raw mut size,
std::ptr::null_mut(),
0,
)
} != 0
{
return None;
}
let buf = buf.get(..size)?;
let count = i32::from_ne_bytes(buf.get(..4)?.try_into().ok()?);
let rest = buf.get(4..)?;
let path_end = rest.iter().position(|b| *b == 0)?;
let start = rest[path_end..].iter().position(|b| *b != 0)? + path_end;
Some(
rest[start..]
.split(|b| *b == 0)
.take(usize::try_from(count).ok()?)
.map(|arg| String::from_utf8_lossy(arg).into_owned())
.collect(),
)
}
#[cfg(not(target_os = "macos"))]
fn argv(pid: libc::pid_t) -> Option<Vec<String>> {
let line = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
Some(
line.split(|b| *b == 0)
.filter(|arg| !arg.is_empty())
.map(|arg| String::from_utf8_lossy(arg).into_owned())
.collect(),
)
}
#[cfg(target_os = "macos")]
pub(crate) fn cwd(pid: libc::pid_t) -> Option<PathBuf> {
use std::os::unix::ffi::OsStrExt as _;
let mut info: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_vnodepathinfo>() as libc::c_int;
let read = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDVNODEPATHINFO,
0,
(&raw mut info).cast(),
size,
)
};
if read != size {
return None;
}
let path = unsafe { std::ffi::CStr::from_ptr(info.pvi_cdir.vip_path.as_ptr().cast()) };
let bytes = path.to_bytes();
(!bytes.is_empty()).then(|| PathBuf::from(std::ffi::OsStr::from_bytes(bytes)))
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn cwd(pid: libc::pid_t) -> Option<PathBuf> {
std::fs::read_link(format!("/proc/{pid}/cwd")).ok()
}
const STEP: Duration = Duration::from_millis(250);
#[cfg(unix)]
pub(crate) fn end_session(leader: libc::pid_t, mut reap: impl FnMut() -> bool) {
for signal in [libc::SIGHUP, libc::SIGTERM, libc::SIGKILL] {
let pids = session(leader);
if pids.is_empty() {
return;
}
for pid in &pids {
unsafe { libc::kill(*pid, signal) };
}
let deadline = Instant::now() + STEP;
loop {
reap();
if session(leader).is_empty() || Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
}
reap();
}
#[cfg(unix)]
pub(crate) fn session(sid: libc::pid_t) -> Vec<libc::pid_t> {
all_pids()
.into_iter()
.filter(|pid| {
unsafe { libc::getsid(*pid) == sid }
})
.filter(|pid| alive(*pid))
.collect()
}
#[cfg(unix)]
fn alive(pid: libc::pid_t) -> bool {
if unsafe { libc::kill(pid, 0) } != 0 {
return false;
}
!zombie(pid)
}
#[cfg(target_os = "macos")]
fn zombie(pid: libc::pid_t) -> bool {
let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
let read =
unsafe { libc::proc_pidinfo(pid, libc::PROC_PIDTBSDINFO, 0, (&raw mut info).cast(), size) };
read == size && info.pbi_status == 5
}
#[cfg(all(unix, not(target_os = "macos")))]
fn zombie(pid: libc::pid_t) -> bool {
std::fs::read_to_string(format!("/proc/{pid}/stat"))
.ok()
.and_then(|stat| {
let state = stat.rsplit_once(')')?.1.trim_start().chars().next()?;
Some(state == 'Z')
})
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
fn all_pids() -> Vec<libc::pid_t> {
let count = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) };
if count <= 0 {
return Vec::new();
}
let mut pids = vec![0 as libc::pid_t; count as usize + 64];
let bytes = (pids.len() * std::mem::size_of::<libc::pid_t>()) as libc::c_int;
let found = unsafe { libc::proc_listallpids(pids.as_mut_ptr().cast(), bytes) };
pids.truncate(found.max(0) as usize);
pids.retain(|pid| *pid > 0);
pids
}
#[cfg(all(unix, not(target_os = "macos")))]
fn all_pids() -> Vec<libc::pid_t> {
std::fs::read_dir("/proc")
.map(|entries| {
entries
.filter_map(|entry| entry.ok()?.file_name().to_str()?.parse().ok())
.collect()
})
.unwrap_or_default()
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::io::BufRead as _;
use std::os::unix::process::CommandExt as _;
use std::process::{Command, Stdio};
#[test]
fn a_process_group_is_described_by_its_name_arguments_and_directory() {
let dir = std::env::temp_dir().canonicalize().unwrap();
let mut child = Command::new("/bin/sleep")
.arg("300")
.current_dir(&dir)
.process_group(0)
.spawn()
.unwrap();
let pid = child.id() as libc::pid_t;
let deadline = Instant::now() + Duration::from_secs(5);
let described = loop {
let described = describe_group(pid).expect("described");
if !described.argv.is_empty() || Instant::now() >= deadline {
break described;
}
std::thread::sleep(Duration::from_millis(10));
};
assert_eq!(described.pid, pid);
assert_eq!(described.name, "sleep");
assert_eq!(described.argv, ["/bin/sleep", "300"]);
assert_eq!(
described.cwd.map(|cwd| cwd.canonicalize().unwrap()),
Some(dir)
);
child.kill().unwrap();
child.wait().unwrap();
assert_eq!(describe_group(pid), None, "gone");
}
#[test]
fn a_group_whose_leader_exited_is_described_by_a_member() {
let mut leader = Command::new("/bin/sh")
.args(["-c", "/bin/sleep 300 & echo $!"])
.stdout(Stdio::piped())
.process_group(0)
.spawn()
.unwrap();
let pgid = leader.id() as libc::pid_t;
let mut line = String::new();
std::io::BufReader::new(leader.stdout.take().unwrap())
.read_line(&mut line)
.unwrap();
let member: libc::pid_t = line.trim().parse().unwrap();
leader.wait().unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
let described = loop {
let described = describe_group(pgid).expect("the member");
if described.name == "sleep" || Instant::now() >= deadline {
break described;
}
std::thread::sleep(Duration::from_millis(10));
};
assert_eq!(described.pid, member);
assert_eq!(described.name, "sleep");
unsafe { libc::kill(member, libc::SIGKILL) };
}
#[test]
fn ending_a_session_ends_its_jobs_in_other_process_groups() {
let mut command = Command::new("/bin/bash");
command
.args(["-c", "set -m; sleep 300 & echo $!; exec sleep 300"])
.stdout(Stdio::piped());
unsafe {
command.pre_exec(|| {
libc::setsid();
Ok(())
});
}
let mut child = command.spawn().unwrap();
let leader = child.id() as libc::pid_t;
let mut line = String::new();
std::io::BufReader::new(child.stdout.take().unwrap())
.read_line(&mut line)
.unwrap();
let job: libc::pid_t = line.trim().parse().unwrap();
let pids = session(leader);
assert!(pids.contains(&leader) && pids.contains(&job), "{pids:?}");
assert_ne!(
unsafe { libc::getpgid(job) },
leader,
"the job has its own process group"
);
end_session(leader, || matches!(child.try_wait(), Ok(Some(_))));
assert!(session(leader).is_empty(), "the whole session ended");
assert!(!alive(job));
}
}