use anyhow::Result;
use std::path::Path;
use super::HolderInfo;
use super::holder::holder_info_for_pid;
use super::mountinfo::{needle_from_path, needle_from_path_with_mountinfo};
pub(super) fn read_holders_for_needle(needle: &str) -> Result<Vec<HolderInfo>> {
use anyhow::Context;
use std::fs;
let contents = fs::read_to_string("/proc/locks")
.with_context(|| "read /proc/locks for lockfile holder lookup")?;
Ok(read_holders_from_contents(&contents, needle))
}
pub(super) fn read_holders_from_contents(contents: &str, needle: &str) -> Vec<HolderInfo> {
let pids = parse_flock_pids_for_needle(contents, needle);
pids.into_iter().map(holder_info_for_pid).collect()
}
pub(crate) fn parse_flock_pids_for_needle(contents: &str, needle: &str) -> Vec<u32> {
let mut pids: Vec<u32> = Vec::new();
for line in contents.lines() {
let mut fields = line.split_whitespace();
let _id = fields.next();
let lock_type = fields.next();
if lock_type != Some("FLOCK") {
continue;
}
let _adv = fields.next();
let _mode = fields.next();
let pid = match fields.next().and_then(|s| s.parse::<u32>().ok()) {
Some(p) => p,
None => continue,
};
let dev_inode = match fields.next() {
Some(s) => s,
None => continue,
};
if dev_inode == needle && !pids.contains(&pid) {
pids.push(pid);
}
}
pids
}
pub(crate) fn read_holders(path: &Path) -> Result<Vec<HolderInfo>> {
let needle = needle_from_path(path)?;
read_holders_for_needle(&needle)
}
pub(crate) fn read_holders_with_mountinfo(path: &Path, mountinfo: &str) -> Result<Vec<HolderInfo>> {
let needle = needle_from_path_with_mountinfo(path, mountinfo)?;
read_holders_for_needle(&needle)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flock::FlockMode;
use crate::flock::mountinfo::read_mountinfo;
use crate::flock::primitives::try_flock;
#[test]
fn parse_flock_pids_for_needle_skips_posix_and_ofdlck() {
let needle = "08:02:1234";
let contents = "\
1: POSIX ADVISORY WRITE 11111 08:02:1234 0 EOF
2: OFDLCK ADVISORY READ 22222 08:02:1234 0 EOF
3: FLOCK ADVISORY WRITE 33333 08:02:1234 0 EOF
4: FLOCK ADVISORY READ 44444 08:02:5678 0 EOF
";
let pids = parse_flock_pids_for_needle(contents, needle);
assert_eq!(
pids,
vec![33333],
"only the FLOCK line at the matching triple must contribute a PID; \
POSIX/OFDLCK must be filtered",
);
}
#[test]
fn parse_flock_pids_for_needle_deduplicates_pids() {
let needle = "08:02:1234";
let contents = "\
1: FLOCK ADVISORY WRITE 55555 08:02:1234 0 EOF
2: FLOCK ADVISORY READ 55555 08:02:1234 0 EOF
3: FLOCK ADVISORY WRITE 66666 08:02:1234 0 EOF
";
let pids = parse_flock_pids_for_needle(contents, needle);
assert_eq!(pids, vec![55555, 66666], "PIDs must dedupe");
}
#[test]
fn parse_flock_pids_for_needle_empty_contents_returns_empty() {
let pids = parse_flock_pids_for_needle("", "08:02:1234");
assert!(pids.is_empty());
}
#[test]
fn parse_flock_pids_for_needle_skips_malformed_lines() {
let needle = "08:02:1234";
let contents = "\
1: FLOCK
2: FLOCK ADVISORY WRITE notanumber 08:02:1234 0 EOF
3: FLOCK ADVISORY WRITE 77777 08:02:1234 0 EOF
";
let pids = parse_flock_pids_for_needle(contents, needle);
assert_eq!(
pids,
vec![77777],
"only the well-formed matching line contributes",
);
}
#[test]
fn read_holders_from_contents_returns_holder_info_per_matching_pid() {
let our_pid = std::process::id();
let needle = "08:02:1234";
let contents = format!(
"1: FLOCK ADVISORY WRITE {our_pid} 08:02:1234 0 EOF\n\
2: POSIX ADVISORY WRITE 11111 08:02:1234 0 EOF\n",
);
let holders = read_holders_from_contents(&contents, needle);
assert_eq!(
holders.len(),
1,
"only the FLOCK line at the matching triple produces a holder; \
POSIX must be filtered: {holders:?}",
);
assert_eq!(holders[0].pid, our_pid);
assert_ne!(holders[0].cmdline, "<cmdline unavailable>");
}
#[test]
fn read_holders_from_contents_empty_returns_empty() {
let holders = read_holders_from_contents("", "08:02:1234");
assert!(holders.is_empty());
}
#[test]
fn read_holders_from_contents_deterministic_for_same_input() {
let contents = format!(
"1: FLOCK ADVISORY WRITE {pid} 08:02:1234 0 EOF\n",
pid = std::process::id(),
);
let a = read_holders_from_contents(&contents, "08:02:1234");
let b = read_holders_from_contents(&contents, "08:02:1234");
assert_eq!(a.len(), b.len());
assert_eq!(a.len(), 1);
assert_eq!(a[0].pid, b[0].pid);
assert_eq!(a[0].cmdline, b[0].cmdline);
}
#[test]
fn read_holders_for_needle_no_match_returns_empty() {
let needle = "ff:ff:18446744073709551615";
let holders = read_holders_for_needle(needle)
.expect("/proc/locks read must succeed on any Linux host");
assert!(
holders.is_empty(),
"impossible needle must not match any holder: {holders:?}"
);
}
#[test]
fn read_holders_cached_mountinfo_equals_uncached() {
use tempfile::TempDir;
let tmp = TempDir::new().expect("tempdir");
let path = tmp.path().join("cache-holder-equivalence.lock");
let fd = try_flock(&path, FlockMode::Exclusive)
.expect("try_flock must succeed on fresh tempfile")
.expect("EX must acquire on clean pool");
let uncached = read_holders(&path).expect("uncached holders");
let mountinfo = read_mountinfo().expect("read mountinfo");
let cached = read_holders_with_mountinfo(&path, &mountinfo).expect("cached holders");
let our_pid = std::process::id();
assert!(
uncached.iter().any(|h| h.pid == our_pid),
"our pid {our_pid} must appear in uncached holders {uncached:?}",
);
assert!(
cached.iter().any(|h| h.pid == our_pid),
"our pid {our_pid} must appear in cached holders {cached:?}",
);
drop(fd);
}
}