use std::path::PathBuf;
pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
impl_::read(pid, limit)
}
#[cfg(target_os = "linux")]
mod impl_ {
use super::*;
pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
crate::proc_::read_writing_files(pid, limit)
}
}
#[cfg(target_os = "macos")]
mod impl_ {
use super::*;
use std::ffi::c_void;
use std::os::raw::{c_char, c_int, c_uint};
const PROC_PIDLISTFDS: c_int = 1;
const PROC_PIDFDVNODEPATHINFO: c_int = 2;
const PROX_FDTYPE_VNODE: u32 = 1;
extern "C" {
fn proc_pidinfo(
pid: c_int, flavor: c_int, arg: u64,
buffer: *mut c_void, buffersize: c_int,
) -> c_int;
fn proc_pidfdinfo(
pid: c_int, fd: c_int, flavor: c_int,
buffer: *mut c_void, buffersize: c_int,
) -> c_int;
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct ProcFdInfo {
proc_fd: c_int,
proc_fdtype: c_uint,
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct ProcFileInfo {
fi_openflags: u32,
fi_status: u32,
fi_offset: i64,
fi_type: i32,
fi_guardflags: u32,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct VnodeInfo {
_opaque: [u8; 152],
}
impl Default for VnodeInfo {
fn default() -> Self { Self { _opaque: [0u8; 152] } }
}
#[repr(C)]
#[derive(Clone, Copy)]
struct VnodeInfoPath {
vip_vi: VnodeInfo,
vip_path: [c_char; 1024], }
impl Default for VnodeInfoPath {
fn default() -> Self { Self { vip_vi: VnodeInfo::default(), vip_path: [0; 1024] } }
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct VnodeFdInfoWithPath {
pfi: ProcFileInfo,
pvip: VnodeInfoPath,
}
pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
let pid = pid as c_int;
let probe = unsafe {
proc_pidinfo(pid, PROC_PIDLISTFDS, 0, std::ptr::null_mut(), 0)
};
if probe <= 0 { return Vec::new(); }
let needed = (probe as usize).min(4096 * std::mem::size_of::<ProcFdInfo>());
let entry_count = needed / std::mem::size_of::<ProcFdInfo>();
let mut buf: Vec<ProcFdInfo> = vec![ProcFdInfo::default(); entry_count];
let written = unsafe {
proc_pidinfo(
pid, PROC_PIDLISTFDS, 0,
buf.as_mut_ptr() as *mut c_void,
(buf.len() * std::mem::size_of::<ProcFdInfo>()) as c_int,
)
};
if written <= 0 { return Vec::new(); }
let got = (written as usize) / std::mem::size_of::<ProcFdInfo>();
let mut out: Vec<PathBuf> = Vec::with_capacity(limit.min(got));
for fd in buf.iter().take(got) {
if out.len() >= limit { break; }
if fd.proc_fdtype != PROX_FDTYPE_VNODE { continue; }
let mut info = VnodeFdInfoWithPath::default();
let n = unsafe {
proc_pidfdinfo(
pid, fd.proc_fd, PROC_PIDFDVNODEPATHINFO,
&mut info as *mut _ as *mut c_void,
std::mem::size_of::<VnodeFdInfoWithPath>() as c_int,
)
};
if n <= 0 { continue; }
let flags: u32 = info.pfi.fi_openflags;
if flags & (libc::O_WRONLY as u32 | libc::O_RDWR as u32) == 0 { continue; }
let path = &info.pvip.vip_path;
let nul = path.iter().position(|&b| b == 0).unwrap_or(path.len());
let bytes: Vec<u8> = path[..nul].iter().map(|&c| c as u8).collect();
let s = match std::str::from_utf8(&bytes) {
Ok(s) => s,
Err(_) => continue,
};
if s.is_empty() || s.starts_with("/dev/") { continue; }
out.push(PathBuf::from(s));
}
out
}
}
#[cfg(windows)]
mod impl_ {
use super::*;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Foundation::{
CloseHandle, DuplicateHandle, DUPLICATE_SAME_ACCESS, HANDLE, INVALID_HANDLE_VALUE,
STATUS_INFO_LENGTH_MISMATCH, NTSTATUS,
};
use windows_sys::Win32::Storage::FileSystem::{
GetFinalPathNameByHandleW, FILE_GENERIC_WRITE, FILE_NAME_NORMALIZED,
};
use windows_sys::Win32::System::Threading::{
GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE,
};
use windows_sys::Wdk::System::SystemInformation::{
NtQuerySystemInformation,
};
const SYSTEM_EXTENDED_HANDLE_INFORMATION: i32 = 0x40;
#[repr(C)]
struct SystemHandleInformationEx {
number_of_handles: usize,
reserved: usize,
handles: [SystemHandleTableEntryInfoEx; 1],
}
#[repr(C)]
#[derive(Clone, Copy)]
struct SystemHandleTableEntryInfoEx {
object: *mut std::ffi::c_void,
unique_process_id: usize,
handle_value: usize,
granted_access: u32,
creator_back_trace_index: u16,
object_type_index: u16,
handle_attributes: u32,
reserved: u32,
}
pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
let target_pid = pid as usize;
let mut buf: Vec<u8> = vec![0u8; 256 * 1024];
let mut retries = 0u32;
let table: *const SystemHandleInformationEx = loop {
retries += 1;
if retries > 8 { return Vec::new(); }
let mut needed: u32 = 0;
let status: NTSTATUS = unsafe {
NtQuerySystemInformation(
SYSTEM_EXTENDED_HANDLE_INFORMATION,
buf.as_mut_ptr() as *mut _,
buf.len() as u32,
&mut needed,
)
};
if status == 0 { break buf.as_ptr() as *const _; }
if status == STATUS_INFO_LENGTH_MISMATCH {
let grow = ((needed as usize).max(buf.len() * 2)).min(64 * 1024 * 1024);
if grow == buf.len() { return Vec::new(); }
buf.resize(grow, 0);
continue;
}
return Vec::new();
};
let proc_handle: HANDLE = unsafe {
OpenProcess(PROCESS_DUP_HANDLE, 0, pid)
};
if proc_handle.is_null() || proc_handle == INVALID_HANDLE_VALUE {
return Vec::new();
}
let header = unsafe { &*table };
let header_size = std::mem::size_of::<usize>() * 2;
let entry_size = std::mem::size_of::<SystemHandleTableEntryInfoEx>();
let max_entries = buf.len().saturating_sub(header_size) / entry_size;
let count = header.number_of_handles.min(max_entries);
let entries: *const SystemHandleTableEntryInfoEx = unsafe {
(table as *const u8)
.add(header_size)
as *const SystemHandleTableEntryInfoEx
};
let mut out: Vec<PathBuf> = Vec::with_capacity(limit);
let me = unsafe { GetCurrentProcess() };
for i in 0..count {
if out.len() >= limit { break; }
let entry = unsafe { *entries.add(i) };
if entry.unique_process_id != target_pid { continue; }
if entry.granted_access & FILE_GENERIC_WRITE == 0 { continue; }
let mut dup: HANDLE = std::ptr::null_mut();
let ok = unsafe {
DuplicateHandle(
proc_handle,
entry.handle_value as HANDLE,
me,
&mut dup,
0,
0,
DUPLICATE_SAME_ACCESS,
)
};
if ok == 0 || dup.is_null() { continue; }
let mut wbuf = [0u16; 32_768];
let n = unsafe {
GetFinalPathNameByHandleW(dup, wbuf.as_mut_ptr(), wbuf.len() as u32, FILE_NAME_NORMALIZED)
};
unsafe { CloseHandle(dup); }
if n == 0 || (n as usize) > wbuf.len() { continue; }
let s = OsString::from_wide(&wbuf[..n as usize]);
let s = s.to_string_lossy();
let trimmed = s.strip_prefix(r"\\?\").unwrap_or(&s);
if trimmed.starts_with(r"\Device\") { continue; }
out.push(PathBuf::from(trimmed));
}
unsafe { CloseHandle(proc_handle); }
out
}
}
#[cfg(any(target_os = "freebsd", target_os = "dragonfly"))]
mod impl_ {
use super::*;
use std::ffi::{c_char, c_int, c_uint, c_void, CStr};
const PS_FST_TYPE_VNODE: c_int = 1;
const PS_FST_FFLAG_WRITE: c_int = 0x0002;
const KERN_PROC_PID: c_int = 1;
#[repr(C)]
struct FileStat {
fs_type: c_int,
fs_flags: c_int,
fs_fflags: c_int,
fs_uflags: c_int,
fs_fd: c_int,
fs_ref_count: c_int,
fs_offset: i64,
fs_typedep: *mut c_void,
fs_path: *mut c_char,
next_stqe_next: *mut FileStat,
}
#[repr(C)]
struct FileStatList {
stqh_first: *mut FileStat,
stqh_last: *mut *mut FileStat,
}
#[link(name = "procstat")]
extern "C" {
fn procstat_open_sysctl() -> *mut c_void;
fn procstat_close(ps: *mut c_void);
fn procstat_getprocs(
ps: *mut c_void, what: c_int, arg: c_int, count: *mut c_uint,
) -> *mut c_void; fn procstat_freeprocs(ps: *mut c_void, p: *mut c_void);
fn procstat_getfiles(
ps: *mut c_void, p: *mut c_void, mmapped: c_int,
) -> *mut FileStatList;
fn procstat_freefiles(ps: *mut c_void, head: *mut FileStatList);
}
pub fn read(pid: u32, limit: usize) -> Vec<PathBuf> {
unsafe {
let ps = procstat_open_sysctl();
if ps.is_null() { return Vec::new(); }
let mut count: c_uint = 0;
let kproc = procstat_getprocs(ps, KERN_PROC_PID, pid as c_int, &mut count);
if kproc.is_null() || count == 0 {
procstat_close(ps);
return Vec::new();
}
let head = procstat_getfiles(ps, kproc, 0);
if head.is_null() {
procstat_freeprocs(ps, kproc);
procstat_close(ps);
return Vec::new();
}
let mut out: Vec<PathBuf> = Vec::with_capacity(limit);
let mut node = (*head).stqh_first;
let mut walked = 0u32;
while !node.is_null() && walked < 4096 {
walked += 1;
if out.len() >= limit { break; }
let f = &*node;
let writable = (f.fs_flags & PS_FST_FFLAG_WRITE) != 0;
let is_vnode = f.fs_type == PS_FST_TYPE_VNODE;
if writable && is_vnode && !f.fs_path.is_null() {
let cstr = CStr::from_ptr(f.fs_path);
if let Ok(s) = cstr.to_str() {
if !s.is_empty() && !s.starts_with("/dev/") {
out.push(PathBuf::from(s));
}
}
}
node = f.next_stqe_next;
}
procstat_freefiles(ps, head);
procstat_freeprocs(ps, kproc);
procstat_close(ps);
out
}
}
}
#[cfg(any(target_os = "openbsd", target_os = "netbsd"))]
mod impl_ {
use super::*;
pub fn read(_pid: u32, _limit: usize) -> Vec<PathBuf> { Vec::new() }
}
#[cfg(not(any(
target_os = "linux", target_os = "macos", windows,
target_os = "freebsd", target_os = "dragonfly",
target_os = "openbsd", target_os = "netbsd",
)))]
mod impl_ {
use super::*;
pub fn read(_pid: u32, _limit: usize) -> Vec<PathBuf> { Vec::new() }
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::fs::OpenOptions;
#[test]
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
fn enumerates_self_open_writable_file() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
let mut f = OpenOptions::new().write(true).open(&path).unwrap();
writeln!(f, "agtop-test").unwrap();
f.sync_all().unwrap();
let pid = std::process::id();
let files = read(pid, 4096);
let canon_target = std::fs::canonicalize(&path).unwrap_or(path.clone());
let found = files.iter().any(|p| {
p == &path || p == &canon_target
|| p.to_string_lossy().contains(path.file_name().unwrap().to_str().unwrap())
});
drop(f);
drop(tmp);
assert!(found,
"writing_files::read({}) did not include the test tempfile.\n\
Opened: {:?}\n\
Returned ({} entries):\n{}",
pid, path, files.len(),
files.iter().take(20).map(|p| format!(" {}", p.display())).collect::<Vec<_>>().join("\n"));
}
}