use std::path::Path;
#[cfg(unix)]
use std::path::PathBuf;
pub const RESTART_MANAGER_SCRIPT: &str = include_str!("index_lock_restart_manager.ps1");
pub const SILENT_DENIAL_NOTE: &str = "lsof omits other users' processes on macOS without reporting it";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Holder {
pub pid: i64,
pub command: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HolderEvidence {
pub method: &'static str,
pub holders: Vec<Holder>,
pub partial: Vec<String>,
}
#[cfg(unix)]
fn is_pid_name(name: &str) -> bool {
return !name.is_empty() && name.bytes().all(is_digit);
}
#[cfg(unix)]
fn is_digit(byte: u8) -> bool {
return byte.is_ascii_digit();
}
#[cfg(unix)]
fn exited(error: &std::io::Error) -> bool {
if error.kind() == std::io::ErrorKind::NotFound {
return true;
}
#[cfg(unix)]
{
return error.raw_os_error() == Some(libc::ESRCH);
}
#[cfg(not(unix))]
{
return false;
}
}
#[cfg(unix)]
fn followed_identity(path: &Path) -> std::io::Result<(u64, u64)> {
use std::os::unix::fs::MetadataExt;
let metadata: std::fs::Metadata = std::fs::metadata(path)?;
return Ok((metadata.dev(), metadata.ino()));
}
#[cfg(unix)]
fn scan_process(proc_root: &Path, pid: &str, device: u64, inode: u64) -> (bool, Option<String>) {
let fd_directory: PathBuf = proc_root.join(pid).join("fd");
let listing: std::fs::ReadDir = match std::fs::read_dir(&fd_directory) {
Ok(found) => found,
Err(error) if exited(&error) => return (false, None),
Err(error) => return (false, Some(format!("PID {pid}: {error}"))),
};
let mut holds: bool = false;
let mut partial: Option<String> = None;
for item in listing {
let Ok(entry) = item else {
continue;
};
match followed_identity(entry.path().as_path()) {
Ok((found_device, found_inode)) => {
if found_device == device && found_inode == inode {
holds = true;
}
}
Err(error) if exited(&error) => {}
Err(error) => {
if partial.is_none() {
partial = Some(format!(
"PID {pid} fd {}: {error}",
entry.file_name().to_string_lossy()
));
}
}
}
}
return (holds, partial);
}
#[cfg(unix)]
pub fn scan_proc_fd_holders(proc_root: &Path, device: u64, inode: u64, self_pid: u32) -> HolderEvidence {
return scan_proc_fd_holders_until(proc_root, device, inode, self_pid, None);
}
#[cfg(unix)]
pub fn scan_proc_fd_holders_until(proc_root: &Path, device: u64, inode: u64, self_pid: u32, deadline: Option<std::time::Instant>) -> HolderEvidence {
let mut evidence: HolderEvidence = HolderEvidence {
method: "proc-fd",
holders: Vec::new(),
partial: Vec::new(),
};
let listing: std::fs::ReadDir = match std::fs::read_dir(proc_root) {
Ok(found) => found,
Err(error) => {
evidence.partial.push(format!("{}: {error}", proc_root.display()));
return evidence;
}
};
let own: String = self_pid.to_string();
let mut pids: Vec<String> = Vec::new();
for item in listing.flatten() {
let name: String = item.file_name().to_string_lossy().into_owned();
if is_pid_name(name.as_str()) && name != own {
pids.push(name);
}
}
for (reached, pid) in pids.iter().enumerate() {
if deadline.is_some_and(|limit| { return std::time::Instant::now() >= limit; }) {
evidence.partial.push(format!("stopped at the time bound after {reached} of {} processes", pids.len()));
break;
}
let (holds, partial) = scan_process(proc_root, pid.as_str(), device, inode);
if holds && let Ok(number) = pid.parse::<i64>() {
let command: Option<String> = std::fs::read_to_string(proc_root.join(pid).join("comm"))
.ok()
.map(trimmed);
evidence.holders.push(Holder { pid: number, command });
}
if let Some(reason) = partial {
evidence.partial.push(reason);
}
}
crate::diagnostic_log::debug(
"scanProcFdHolders",
format!(
"scanned {} processes: {} holders, {} unreadable",
pids.len(),
evidence.holders.len(),
evidence.partial.len()
)
.as_str(),
);
return evidence;
}
#[cfg(unix)]
fn trimmed(text: String) -> String {
return String::from(crate::js_text::trim_javascript(text.as_str()));
}
pub const SERVICE_SCAN_BUDGET: std::time::Duration = std::time::Duration::from_millis(250);
pub fn scan_platform_holders_bounded(lock_path: &Path, device: u64, inode: u64) -> HolderEvidence {
#[cfg(target_os = "linux")]
{
let _ = lock_path;
return scan_proc_fd_holders_until(Path::new("/proc"), device, inode, std::process::id(), Some(std::time::Instant::now() + SERVICE_SCAN_BUDGET));
}
#[cfg(not(target_os = "linux"))]
{
return scan_platform_holders(lock_path, device, inode);
}
}
pub fn scan_platform_holders(lock_path: &Path, device: u64, inode: u64) -> HolderEvidence {
#[cfg(target_os = "linux")]
{
let _ = lock_path;
return scan_proc_fd_holders(Path::new("/proc"), device, inode, std::process::id());
}
#[cfg(target_os = "macos")]
{
return scan_lsof_holders(lock_path, device, inode);
}
#[cfg(windows)]
{
let _ = (device, inode);
return scan_restart_manager_holders(lock_path);
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
{
let _ = (lock_path, device, inode);
return HolderEvidence {
method: "unsupported",
holders: Vec::new(),
partial: vec![String::from("no open-holder scan on this platform")],
};
}
}
#[cfg(target_os = "macos")]
fn scan_lsof_holders(lock_path: &Path, device: u64, inode: u64) -> HolderEvidence {
let output: std::io::Result<std::process::Output> = std::process::Command::new("lsof")
.args(["-n", "-P", "-w", "-F", "pcDi", "--"])
.arg(lock_path)
.env("LC_ALL", "C")
.stdin(std::process::Stdio::null())
.output();
let stdout: String = match output {
Ok(finished) if finished.status.success() || finished.status.code() == Some(1) => {
String::from_utf8_lossy(finished.stdout.as_slice()).into_owned()
}
Ok(finished) => {
return HolderEvidence {
method: "lsof",
holders: Vec::new(),
partial: vec![format!("lsof failed: exit {:?}", finished.status.code())],
};
}
Err(error) => {
return HolderEvidence {
method: "lsof",
holders: Vec::new(),
partial: vec![format!("lsof failed: {error}")],
};
}
};
let root: bool = unsafe { libc::getuid() } == 0;
return HolderEvidence {
method: "lsof",
holders: matching_holders(parse_lsof_fields(stdout.as_str()).as_slice(), device, inode),
partial: if root { Vec::new() } else { vec![String::from(SILENT_DENIAL_NOTE)] },
};
}
#[cfg(windows)]
fn scan_restart_manager_holders(lock_path: &Path) -> HolderEvidence {
let output: std::io::Result<std::process::Output> = std::process::Command::new("powershell.exe")
.args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", RESTART_MANAGER_SCRIPT])
.env("CLI_GIT_RM_TARGET", lock_path)
.stdin(std::process::Stdio::null())
.output();
match output {
Ok(finished) if finished.status.success() => {
return HolderEvidence {
method: "restart-manager",
holders: parse_restart_manager_output(String::from_utf8_lossy(finished.stdout.as_slice()).as_ref()),
partial: Vec::new(),
};
}
Ok(finished) => {
return HolderEvidence {
method: "restart-manager",
holders: Vec::new(),
partial: vec![format!("Restart Manager query failed: exit {:?}", finished.status.code())],
};
}
Err(error) => {
return HolderEvidence {
method: "restart-manager",
holders: Vec::new(),
partial: vec![format!("Restart Manager query failed: {error}")],
};
}
}
}
pub use crate::index_lock_holder_parsers::{LsofProcess, matching_holders, parse_lsof_fields, parse_lsof_number, parse_restart_manager_output};
#[cfg(test)]
#[path = "index_lock_holders_tests.rs"]
mod tests;