use std::path::{Path, PathBuf};
pub const PROC_ROOT: &str = "/proc";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum UseTarget<'a> {
Inside(&'a Path),
File {
device: u64,
inode: u64,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProcessUser {
pub pid: i64,
pub command: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct UseScan {
pub users: Vec<ProcessUser>,
pub unreadable: Vec<String>,
pub unavailable: Option<String>,
}
fn exited(error: &std::io::Error) -> bool {
return error.kind() == std::io::ErrorKind::NotFound || error.raw_os_error() == Some(libc::ESRCH);
}
#[cfg(unix)]
fn effective_user() -> u32 {
return unsafe { libc::geteuid() };
}
#[cfg(unix)]
fn belongs_to_this_account(path: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
return std::fs::metadata(path).is_ok_and(|metadata| { return metadata.uid() == effective_user(); });
}
#[cfg(not(unix))]
fn belongs_to_this_account(_path: &Path) -> bool {
return false;
}
#[cfg(unix)]
fn same_file(link: &Path, device: u64, inode: u64) -> std::io::Result<bool> {
use std::os::unix::fs::MetadataExt;
let metadata: std::fs::Metadata = std::fs::metadata(link)?;
return Ok(metadata.dev() == device && metadata.ino() == inode);
}
#[cfg(not(unix))]
fn same_file(_link: &Path, _device: u64, _inode: u64) -> std::io::Result<bool> {
return Ok(false);
}
fn matches_target(link: &Path, target: &UseTarget<'_>) -> std::io::Result<bool> {
match target {
UseTarget::Inside(directory) => {
let resolved: PathBuf = std::fs::read_link(link)?;
return Ok(resolved.starts_with(directory));
}
UseTarget::File { device, inode } => return same_file(link, *device, *inode),
}
}
fn scan_process(proc_root: &Path, pid: &str, target: &UseTarget<'_>) -> (bool, Option<String>) {
let process: PathBuf = proc_root.join(pid);
let mut uses: bool = false;
let mut unreadable: Option<String> = None;
if matches!(target, UseTarget::Inside(_)) {
match matches_target(process.join("cwd").as_path(), target) {
Ok(found) => uses = found,
Err(error) if exited(&error) => {}
Err(error) => unreadable = Some(format!("PID {pid} cwd: {error}")),
}
}
let listing: std::fs::ReadDir = match std::fs::read_dir(process.join("fd")) {
Ok(found) => found,
Err(error) if exited(&error) => return (uses, unreadable),
Err(error) => return (uses, unreadable.or(Some(format!("PID {pid} fd: {error}")))),
};
for item in listing.flatten() {
match matches_target(item.path().as_path(), target) {
Ok(true) => uses = true,
Ok(false) => {}
Err(error) if exited(&error) => {}
Err(error) => {
if unreadable.is_none() {
unreadable = Some(format!("PID {pid} fd {}: {error}", item.file_name().to_string_lossy()));
}
}
}
}
return (uses, unreadable);
}
pub fn processes_using_through(proc_root: &Path, target: &UseTarget<'_>, self_pid: u32) -> UseScan {
let mut scan: UseScan = UseScan::default();
let listing: std::fs::ReadDir = match std::fs::read_dir(proc_root) {
Ok(found) => found,
Err(error) => {
scan.unavailable = Some(format!("{}: {error}", proc_root.display()));
return scan;
}
};
let own: String = self_pid.to_string();
for item in listing.flatten() {
let name: String = item.file_name().to_string_lossy().into_owned();
let Ok(number) = name.parse::<i64>() else {
continue;
};
if name == own {
continue;
}
let (uses, unreadable) = scan_process(proc_root, name.as_str(), target);
if uses {
let command: Option<String> = std::fs::read_to_string(item.path().join("comm"))
.ok()
.map(|text| { return String::from(text.trim()); });
scan.users.push(ProcessUser { pid: number, command });
}
if let Some(reason) = unreadable
&& belongs_to_this_account(item.path().as_path())
{
scan.unreadable.push(reason);
}
}
return scan;
}
pub fn processes_using(target: &UseTarget<'_>) -> UseScan {
return processes_using_through(Path::new(PROC_ROOT), target, std::process::id());
}
#[cfg(test)]
#[path = "process_use_tests.rs"]
mod tests;