use std::path::PathBuf;
#[cfg(target_os = "linux")]
pub fn open_files(pid: u32) -> Option<Vec<PathBuf>> {
let rd = std::fs::read_dir(format!("/proc/{pid}/fd")).ok()?;
Some(rd.flatten().filter_map(|e| std::fs::read_link(e.path()).ok()).filter(|p| p.is_absolute()).collect())
}
#[cfg(target_os = "macos")]
pub fn open_files(pid: u32) -> Option<Vec<PathBuf>> {
use libproc::libproc::bsd_info::BSDInfo;
use libproc::libproc::file_info::{ListFDs, PIDFDInfo, PIDFDInfoFlavor, ProcFDType, pidfdinfo};
use libproc::libproc::proc_pid::{listpidinfo, pidinfo};
#[repr(C)]
struct VnodePathInfo {
_head: [u8; 176],
path: [u8; 1024],
}
impl Default for VnodePathInfo {
fn default() -> Self {
VnodePathInfo { _head: [0; 176], path: [0; 1024] }
}
}
impl PIDFDInfo for VnodePathInfo {
fn flavor() -> PIDFDInfoFlavor {
PIDFDInfoFlavor::VNodePathInfo
}
}
let pid = pid as i32;
let info = pidinfo::<BSDInfo>(pid, 0).ok()?;
let fds = listpidinfo::<ListFDs>(pid, info.pbi_nfiles as usize).ok()?;
let paths = fds
.iter()
.filter(|fd| fd.proc_fdtype == ProcFDType::VNode as u32)
.filter_map(|fd| pidfdinfo::<VnodePathInfo>(pid, fd.proc_fd).ok())
.filter_map(|v| {
let end = v.path.iter().position(|&b| b == 0).unwrap_or(v.path.len());
let s = std::str::from_utf8(&v.path[..end]).ok()?;
(!s.is_empty()).then(|| PathBuf::from(s))
})
.collect();
Some(paths)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub fn open_files(_pid: u32) -> Option<Vec<PathBuf>> {
None
}
#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
mod tests {
use super::*;
#[test]
fn lists_a_file_this_process_holds_open() {
let path = std::env::temp_dir().join(format!("agent-top-openfiles-{}.jsonl", std::process::id()));
let file = std::fs::File::create(&path).unwrap();
let canonical = std::fs::canonicalize(&path).unwrap();
let open = open_files(std::process::id()).expect("own process is readable");
assert!(open.contains(&canonical), "{canonical:?} not in {open:?}");
drop(file);
std::fs::remove_file(&path).unwrap();
let open = open_files(std::process::id()).unwrap();
assert!(!open.contains(&canonical), "closed files are not listed");
}
#[test]
fn a_missing_process_cannot_be_answered() {
assert_eq!(open_files(u32::MAX - 1), None);
}
}