use super::probe::{LockProbe, Probe};
use std::path::{Path, PathBuf};
pub(super) struct ProcFsLockProbe {
proc_root: PathBuf,
}
impl Default for ProcFsLockProbe {
fn default() -> Self {
Self {
proc_root: PathBuf::from("/proc"),
}
}
}
impl ProcFsLockProbe {
#[cfg(test)]
fn with_root(proc_root: PathBuf) -> Self {
Self { proc_root }
}
fn scan(&self, inbox_dir: &Path) -> bool {
let Ok(target) = std::fs::canonicalize(inbox_dir) 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(&entry.path(), &target) {
return true;
}
}
false
}
}
impl LockProbe for ProcFsLockProbe {
fn lock_state(&self, inbox_dir: &Path) -> Probe {
if self.scan(inbox_dir) {
Probe::Held
} else {
Probe::Free
}
}
}
fn pid_holds(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)
{
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn fake_fd(root: &Path, pid: &str, fd: &str, target: &Path) {
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();
}
fn inbox(dir: &Path) -> PathBuf {
let p = dir.join("inbox").join("agent-1");
fs::create_dir_all(&p).unwrap();
p
}
#[test]
fn detects_a_holder_of_the_inbox_dir() {
let dir = tempdir().unwrap();
let target = inbox(dir.path());
let proc = dir.path().join("proc");
fake_fd(&proc, "42", "7", &target);
assert_eq!(
ProcFsLockProbe::with_root(proc).lock_state(&target),
Probe::Held
);
}
#[test]
fn no_holder_when_no_fd_matches() {
let dir = tempdir().unwrap();
let target = inbox(dir.path());
let other = dir.path().join("elsewhere");
fs::create_dir_all(&other).unwrap();
let proc = dir.path().join("proc");
fake_fd(&proc, "42", "7", &other);
assert_eq!(
ProcFsLockProbe::with_root(proc).lock_state(&target),
Probe::Free
);
}
#[test]
fn missing_inbox_dir_is_no_holder() {
let dir = tempdir().unwrap();
let proc = dir.path().join("proc");
fs::create_dir_all(&proc).unwrap();
assert_eq!(
ProcFsLockProbe::with_root(proc).lock_state(&dir.path().join("inbox/gone")),
Probe::Free
);
}
#[test]
fn missing_proc_root_is_no_holder() {
let dir = tempdir().unwrap();
let target = inbox(dir.path());
assert_eq!(
ProcFsLockProbe::with_root(dir.path().join("no-proc")).lock_state(&target),
Probe::Free
);
}
#[test]
fn non_numeric_proc_entries_and_unreadable_fd_dirs_are_skipped() {
let dir = tempdir().unwrap();
let target = inbox(dir.path());
let proc = dir.path().join("proc");
fs::create_dir_all(proc.join("acpi")).unwrap();
fs::create_dir_all(proc.join("9")).unwrap();
assert_eq!(
ProcFsLockProbe::with_root(proc).lock_state(&target),
Probe::Free
);
}
#[test]
fn non_symlink_fd_entry_is_skipped() {
let dir = tempdir().unwrap();
let target = inbox(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!(
ProcFsLockProbe::with_root(proc).lock_state(&target),
Probe::Free
);
}
}