use std::fmt;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pid(i32);
impl Pid {
fn new(raw: u32) -> Option<Self> {
i32::try_from(raw).ok().filter(|pid| *pid > 1).map(Pid)
}
}
impl fmt::Display for Pid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "pid {}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Signal {
Terminate,
Kill,
}
#[derive(Debug, Clone)]
pub struct Holder {
pid: Pid,
cwd: PathBuf,
}
impl Holder {
pub fn pid(&self) -> Pid {
self.pid
}
pub fn cwd(&self) -> &Path {
&self.cwd
}
}
const GRACE: Duration = Duration::from_secs(2);
const POLL: Duration = Duration::from_millis(20);
pub fn holding(run_root: &Path) -> Vec<Holder> {
let ours = ancestry();
let mut found: Vec<Holder> = live_pids()
.into_iter()
.filter(|pid| !ours.contains(pid))
.filter_map(Pid::new)
.filter_map(|pid| working_dir(pid).map(|cwd| Holder { pid, cwd }))
.filter(|holder| holder.cwd.starts_with(run_root))
.collect();
found.sort_by_key(|holder| holder.pid.0);
found
}
pub enum Outcome {
Released(Pid),
Holding(Holder),
}
pub fn stop(holders: &[Holder], run_root: &Path) -> Vec<Outcome> {
let mut left: Vec<Holder> = holders.to_vec();
let mut signalled: Vec<Pid> = Vec::new();
for signal in [Signal::Terminate, Signal::Kill] {
left.retain(|holder| still_holding(holder, run_root));
for holder in &left {
signal_to(holder.pid, signal);
if !signalled.contains(&holder.pid) {
signalled.push(holder.pid);
}
}
let deadline = Instant::now() + GRACE;
while !left.is_empty() && Instant::now() < deadline {
std::thread::sleep(POLL);
left.retain(|holder| still_holding(holder, run_root));
}
if left.is_empty() {
break;
}
}
let mut outcomes: Vec<Outcome> = signalled
.into_iter()
.filter(|pid| !left.iter().any(|holder| holder.pid == *pid))
.map(Outcome::Released)
.collect();
outcomes.extend(left.into_iter().map(Outcome::Holding));
outcomes
}
fn still_holding(holder: &Holder, run_root: &Path) -> bool {
working_dir(holder.pid).is_some_and(|cwd| cwd.starts_with(run_root))
}
fn ancestry() -> Vec<u32> {
let mut chain = vec![std::process::id()];
while chain.len() < 64 {
let Some(parent) = chain.last().copied().and_then(parent_of) else {
break;
};
if parent == 0 || chain.contains(&parent) {
break;
}
chain.push(parent);
}
chain
}
#[cfg(target_os = "linux")]
fn live_pids() -> Vec<u32> {
let Ok(entries) = std::fs::read_dir("/proc") else {
return Vec::new();
};
entries
.flatten()
.filter_map(|entry| entry.file_name().to_string_lossy().parse::<u32>().ok())
.collect()
}
#[cfg(target_os = "linux")]
fn working_dir(pid: Pid) -> Option<PathBuf> {
std::fs::read_link(format!("/proc/{}/cwd", pid.0)).ok()
}
#[cfg(target_os = "linux")]
fn parent_of(pid: u32) -> Option<u32> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
stat.rsplit_once(')')?
.1
.split_whitespace()
.nth(1)?
.parse()
.ok()
}
#[cfg(target_os = "macos")]
fn live_pids() -> Vec<u32> {
use std::ffi::c_int;
let needed = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) };
let Ok(needed) = usize::try_from(needed) else {
return Vec::new();
};
let room = needed + 64;
let mut buffer = vec![0 as c_int; room];
let Ok(size) = c_int::try_from(room * std::mem::size_of::<c_int>()) else {
return Vec::new();
};
let filled = unsafe { libc::proc_listallpids(buffer.as_mut_ptr().cast(), size) };
if filled <= 0 {
return Vec::new();
}
buffer
.into_iter()
.filter_map(|pid| u32::try_from(pid).ok())
.collect()
}
#[cfg(target_os = "macos")]
fn working_dir(pid: Pid) -> Option<PathBuf> {
use std::ffi::c_int;
use std::os::unix::ffi::OsStringExt;
let pid: c_int = pid.0;
let size = c_int::try_from(std::mem::size_of::<libc::proc_vnodepathinfo>()).ok()?;
let mut info = std::mem::MaybeUninit::<libc::proc_vnodepathinfo>::zeroed();
let read = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDVNODEPATHINFO,
0,
info.as_mut_ptr().cast(),
size,
)
};
if read != size {
return None;
}
let info = unsafe { info.assume_init() };
let path: Vec<u8> = info
.pvi_cdir
.vip_path
.iter()
.flatten()
.map(|byte| *byte as u8)
.take_while(|byte| *byte != 0)
.collect();
match path.is_empty() {
true => None,
false => Some(PathBuf::from(std::ffi::OsString::from_vec(path))),
}
}
#[cfg(target_os = "macos")]
fn parent_of(pid: u32) -> Option<u32> {
use std::ffi::c_int;
let pid = c_int::try_from(pid).ok().filter(|pid| *pid > 0)?;
let size = c_int::try_from(std::mem::size_of::<libc::proc_bsdinfo>()).ok()?;
let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
let read = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDTBSDINFO,
0,
info.as_mut_ptr().cast(),
size,
)
};
if read != size {
return None;
}
Some(unsafe { info.assume_init() }.pbi_ppid)
}
#[cfg(unix)]
fn signal_to(pid: Pid, signal: Signal) {
let signal = match signal {
Signal::Terminate => libc::SIGTERM,
Signal::Kill => libc::SIGKILL,
};
unsafe {
libc::kill(pid.0, signal);
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn live_pids() -> Vec<u32> {
Vec::new()
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn working_dir(_pid: Pid) -> Option<PathBuf> {
None
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn parent_of(_pid: u32) -> Option<u32> {
None
}
#[cfg(not(unix))]
fn signal_to(_pid: Pid, _signal: Signal) {}