use std::{
fmt,
path::{Path, PathBuf},
};
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Holder {
pub pid: u32,
pub name: String,
pub reason: Reason,
pub path: PathBuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reason {
Running,
WorkingDirectory,
}
impl fmt::Display for Holder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self {
pid, name, path, ..
} = self;
match self.reason {
Reason::Running => write!(f, "{name} (pid {pid}) running {}", path.display()),
Reason::WorkingDirectory => {
write!(f, "{name} (pid {pid}) working in {}", path.display())
}
}
}
}
pub fn holders_of(dir: &Path) -> Vec<Holder> {
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing()
.with_exe(UpdateKind::Always)
.with_cwd(UpdateKind::Always),
);
let mut out: Vec<Holder> = sys
.processes()
.iter()
.filter_map(|(pid, proc)| {
let name = proc.name().to_string_lossy().into_owned();
let (reason, path) = match (proc.exe(), proc.cwd()) {
(Some(exe), _) if is_under(exe, dir) => (Reason::Running, exe),
(_, Some(cwd)) if is_under(cwd, dir) => (Reason::WorkingDirectory, cwd),
_ => return None,
};
Some(Holder {
pid: pid.as_u32(),
name,
reason,
path: path.to_path_buf(),
})
})
.collect();
out.sort_by_key(|holder| holder.pid);
out
}
pub fn describe_holders(dir: &Path) -> String {
let holders = holders_of(dir);
if holders.is_empty() {
return "\nnothing was found running from it: look for an open handle \
(Sysinternals handle64.exe, or Resource Monitor's associated-handles \
search) and check this is running elevated"
.into();
}
let list = holders
.iter()
.map(|holder| format!("\n - {holder}"))
.collect::<String>();
format!("\nstill in use by:{list}")
}
fn is_under(path: &Path, dir: &Path) -> bool {
if cfg!(windows) {
let path = path.to_string_lossy().to_lowercase();
let dir = dir.to_string_lossy().to_lowercase();
Path::new(&path).starts_with(Path::new(&dir))
} else {
path.starts_with(dir)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn under_matches_the_tree_but_not_a_name_prefix() {
let dir = Path::new("/srv/pg/16");
assert!(is_under(Path::new("/srv/pg/16"), dir));
assert!(is_under(Path::new("/srv/pg/16/bin/postgres"), dir));
assert!(!is_under(Path::new("/srv/pg/166/bin/postgres"), dir));
assert!(!is_under(Path::new("/srv/pg"), dir));
}
#[cfg(windows)]
#[test]
fn under_ignores_case_on_windows() {
assert!(is_under(
Path::new(r"c:\program files\postgresql\12\bin\postgres.exe"),
Path::new(r"C:\Program Files\PostgreSQL\12")
));
}
#[test]
fn this_process_holds_its_own_executable() {
let exe = std::env::current_exe().unwrap();
let dir = exe.parent().unwrap();
let me = std::process::id();
let holders = holders_of(dir);
assert!(
holders.iter().any(|holder| holder.pid == me),
"expected pid {me} among {holders:?}"
);
}
#[test]
fn an_unused_tree_has_no_holders_and_describes_the_next_step() {
let tmp = tempfile::tempdir().unwrap();
assert!(holders_of(tmp.path()).is_empty());
assert!(describe_holders(tmp.path()).contains("handle64"));
}
}