use super::probe::{Probe, WriterProbe};
use std::path::{Path, PathBuf};
pub(super) struct ProcFsProbe {
proc_root: PathBuf,
}
impl Default for ProcFsProbe {
fn default() -> Self {
Self {
proc_root: PathBuf::from("/proc"),
}
}
}
impl ProcFsProbe {
#[cfg(test)]
fn with_root(proc_root: PathBuf) -> Self {
Self { proc_root }
}
fn scan(&self, path: &Path) -> bool {
let Ok(target) = std::fs::canonicalize(path) else {
return false;
};
let Ok(entries) = std::fs::read_dir(&self.proc_root) else {
return false;
};
for entry in entries.flatten() {
if entry
.file_name()
.to_str()
.and_then(|s| s.parse::<u32>().ok())
.is_none()
{
continue;
}
if pid_holds_writable(&entry.path(), &target) {
return true;
}
}
false
}
}
impl WriterProbe for ProcFsProbe {
fn writer_state(&self, path: &Path) -> Probe {
if self.scan(path) {
Probe::Held
} else {
Probe::Free
}
}
}
fn pid_holds_writable(proc_pid: &Path, target: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(proc_pid.join("fd")) else {
return false;
};
for entry in entries.flatten() {
if !std::fs::read_link(entry.path())
.and_then(std::fs::canonicalize)
.is_ok_and(|link| link == target)
{
continue;
}
let Some(fd) = entry
.file_name()
.to_str()
.and_then(|s| s.parse::<u32>().ok())
else {
continue;
};
if fdinfo_is_writable(proc_pid, fd) {
return true;
}
}
false
}
fn fdinfo_is_writable(proc_pid: &Path, fd: u32) -> bool {
let Ok(contents) = std::fs::read_to_string(proc_pid.join("fdinfo").join(fd.to_string())) else {
return false;
};
for line in contents.lines() {
if let Some(rest) = line.strip_prefix("flags:") {
return rest.trim().chars().last().is_some_and(|c| c != '0');
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn fake_proc(root: &Path, pid: &str, fd: &str, target: &Path, flags: &str) {
let fd_dir = root.join(pid).join("fd");
fs::create_dir_all(&fd_dir).unwrap();
std::os::unix::fs::symlink(target, fd_dir.join(fd)).unwrap();
let fdinfo = root.join(pid).join("fdinfo");
fs::create_dir_all(&fdinfo).unwrap();
fs::write(fdinfo.join(fd), format!("pos:\t0\nflags:\t{flags}\n")).unwrap();
}
fn target_file(dir: &Path) -> PathBuf {
let p = dir.join("response.json");
fs::write(&p, b"x").unwrap();
p
}
#[test]
fn detects_a_writer_holding_the_path() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
let proc = dir.path().join("proc");
fake_proc(&proc, "42", "3", &target, "0100001");
assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&target),
Probe::Held
);
}
#[test]
fn ignores_a_reader_only_handle() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
let proc = dir.path().join("proc");
fake_proc(&proc, "42", "3", &target, "0100000");
assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&target),
Probe::Free
);
}
#[test]
fn no_writer_when_no_fd_matches() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
let other = dir.path().join("other.json");
fs::write(&other, b"y").unwrap();
let proc = dir.path().join("proc");
fake_proc(&proc, "42", "3", &other, "0100001");
assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&target),
Probe::Free
);
}
#[test]
fn missing_target_is_no_writer() {
let dir = tempdir().unwrap();
let proc = dir.path().join("proc");
fs::create_dir_all(&proc).unwrap();
assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&dir.path().join("gone.json")),
Probe::Free
);
}
#[test]
fn missing_proc_root_is_no_writer() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
assert_eq!(
ProcFsProbe::with_root(dir.path().join("no-proc")).writer_state(&target),
Probe::Free
);
}
#[test]
fn numeric_pid_without_an_fd_dir_is_skipped() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
let proc = dir.path().join("proc");
fs::create_dir_all(proc.join("55")).unwrap(); assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&target),
Probe::Free
);
}
#[test]
fn non_numeric_proc_entries_and_missing_fdinfo_are_skipped() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
let proc = dir.path().join("proc");
fs::create_dir_all(proc.join("acpi")).unwrap();
let fd_dir = proc.join("7").join("fd");
fs::create_dir_all(&fd_dir).unwrap();
std::os::unix::fs::symlink(&target, fd_dir.join("4")).unwrap();
assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&target),
Probe::Free
);
}
#[test]
fn fdinfo_without_flags_line_is_not_writable() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
let proc = dir.path().join("proc");
let fd_dir = proc.join("9").join("fd");
fs::create_dir_all(&fd_dir).unwrap();
std::os::unix::fs::symlink(&target, fd_dir.join("5")).unwrap();
let fdinfo = proc.join("9").join("fdinfo");
fs::create_dir_all(&fdinfo).unwrap();
fs::write(fdinfo.join("5"), "pos:\t0\n").unwrap(); assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&target),
Probe::Free
);
}
#[test]
fn non_numeric_fd_entry_is_skipped() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
let proc = dir.path().join("proc");
let fd_dir = proc.join("11").join("fd");
fs::create_dir_all(&fd_dir).unwrap();
std::os::unix::fs::symlink(&target, fd_dir.join("notanum")).unwrap();
assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&target),
Probe::Free
);
}
#[test]
fn non_symlink_fd_entry_is_skipped() {
let dir = tempdir().unwrap();
let target = target_file(dir.path());
let proc = dir.path().join("proc");
let fd_dir = proc.join("13").join("fd");
fs::create_dir_all(&fd_dir).unwrap();
fs::write(fd_dir.join("3"), b"not a symlink").unwrap();
assert_eq!(
ProcFsProbe::with_root(proc).writer_state(&target),
Probe::Free
);
}
}