1use std::path::Path;
25#[cfg(unix)]
28use std::path::PathBuf;
29
30pub const RESTART_MANAGER_SCRIPT: &str = include_str!("index_lock_restart_manager.ps1");
33
34pub const SILENT_DENIAL_NOTE: &str = "lsof omits other users' processes on macOS without reporting it";
36
37#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct Holder {
48 pub pid: i64,
50 pub command: Option<String>,
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct HolderEvidence {
67 pub method: &'static str,
69 pub holders: Vec<Holder>,
71 pub partial: Vec<String>,
73}
74
75#[cfg(unix)]
86fn is_pid_name(name: &str) -> bool {
87 return !name.is_empty() && name.bytes().all(is_digit);
88}
89
90#[cfg(unix)]
100fn is_digit(byte: u8) -> bool {
101 return byte.is_ascii_digit();
102}
103
104#[cfg(unix)]
115fn exited(error: &std::io::Error) -> bool {
116 if error.kind() == std::io::ErrorKind::NotFound {
117 return true;
118 }
119 #[cfg(unix)]
120 {
121 return error.raw_os_error() == Some(libc::ESRCH);
122 }
123 #[cfg(not(unix))]
125 {
126 return false;
127 }
128}
129
130#[cfg(unix)]
141fn followed_identity(path: &Path) -> std::io::Result<(u64, u64)> {
142 use std::os::unix::fs::MetadataExt;
145 let metadata: std::fs::Metadata = std::fs::metadata(path)?;
146 return Ok((metadata.dev(), metadata.ino()));
147}
148
149#[cfg(unix)]
161fn scan_process(proc_root: &Path, pid: &str, device: u64, inode: u64) -> (bool, Option<String>) {
162 let fd_directory: PathBuf = proc_root.join(pid).join("fd");
163 let listing: std::fs::ReadDir = match std::fs::read_dir(&fd_directory) {
164 Ok(found) => found,
165 Err(error) if exited(&error) => return (false, None),
166 Err(error) => return (false, Some(format!("PID {pid}: {error}"))),
167 };
168 let mut holds: bool = false;
169 let mut partial: Option<String> = None;
170 for item in listing {
171 let Ok(entry) = item else {
172 continue;
173 };
174 match followed_identity(entry.path().as_path()) {
175 Ok((found_device, found_inode)) => {
176 if found_device == device && found_inode == inode {
177 holds = true;
178 }
179 }
180 Err(error) if exited(&error) => {}
181 Err(error) => {
182 if partial.is_none() {
183 partial = Some(format!(
184 "PID {pid} fd {}: {error}",
185 entry.file_name().to_string_lossy()
186 ));
187 }
188 }
189 }
190 }
191 return (holds, partial);
192}
193
194#[cfg(unix)]
210pub fn scan_proc_fd_holders(proc_root: &Path, device: u64, inode: u64, self_pid: u32) -> HolderEvidence {
211 let mut evidence: HolderEvidence = HolderEvidence {
212 method: "proc-fd",
213 holders: Vec::new(),
214 partial: Vec::new(),
215 };
216 let listing: std::fs::ReadDir = match std::fs::read_dir(proc_root) {
217 Ok(found) => found,
218 Err(error) => {
219 evidence.partial.push(format!("{}: {error}", proc_root.display()));
220 return evidence;
221 }
222 };
223 let own: String = self_pid.to_string();
224 let mut pids: Vec<String> = Vec::new();
225 for item in listing.flatten() {
226 let name: String = item.file_name().to_string_lossy().into_owned();
227 if is_pid_name(name.as_str()) && name != own {
228 pids.push(name);
229 }
230 }
231 for pid in &pids {
232 let (holds, partial) = scan_process(proc_root, pid.as_str(), device, inode);
233 if holds && let Ok(number) = pid.parse::<i64>() {
234 let command: Option<String> = std::fs::read_to_string(proc_root.join(pid).join("comm"))
235 .ok()
236 .map(trimmed);
237 evidence.holders.push(Holder { pid: number, command });
238 }
239 if let Some(reason) = partial {
240 evidence.partial.push(reason);
241 }
242 }
243 super::diagnostic_log::debug(
244 "scanProcFdHolders",
245 format!(
246 "scanned {} processes: {} holders, {} unreadable",
247 pids.len(),
248 evidence.holders.len(),
249 evidence.partial.len()
250 )
251 .as_str(),
252 );
253 return evidence;
254}
255
256#[cfg(unix)]
266fn trimmed(text: String) -> String {
267 return String::from(super::js_text::trim_javascript(text.as_str()));
268}
269
270pub fn scan_platform_holders(lock_path: &Path, device: u64, inode: u64) -> HolderEvidence {
280 #[cfg(target_os = "linux")]
281 {
282 let _ = lock_path;
283 return scan_proc_fd_holders(Path::new("/proc"), device, inode, std::process::id());
284 }
285 #[cfg(target_os = "macos")]
286 {
287 return scan_lsof_holders(lock_path, device, inode);
288 }
289 #[cfg(windows)]
291 {
292 let _ = (device, inode);
293 return scan_restart_manager_holders(lock_path);
294 }
295 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
296 {
297 let _ = (lock_path, device, inode);
298 return HolderEvidence {
299 method: "unsupported",
300 holders: Vec::new(),
301 partial: vec![String::from("no open-holder scan on this platform")],
302 };
303 }
304}
305
306#[cfg(target_os = "macos")]
318fn scan_lsof_holders(lock_path: &Path, device: u64, inode: u64) -> HolderEvidence {
319 let output: std::io::Result<std::process::Output> = std::process::Command::new("lsof")
320 .args(["-n", "-P", "-w", "-F", "pcDi", "--"])
321 .arg(lock_path)
322 .env("LC_ALL", "C")
323 .stdin(std::process::Stdio::null())
324 .output();
325 let stdout: String = match output {
326 Ok(finished) if finished.status.success() || finished.status.code() == Some(1) => {
327 String::from_utf8_lossy(finished.stdout.as_slice()).into_owned()
328 }
329 Ok(finished) => {
330 return HolderEvidence {
331 method: "lsof",
332 holders: Vec::new(),
333 partial: vec![format!("lsof failed: exit {:?}", finished.status.code())],
334 };
335 }
336 Err(error) => {
337 return HolderEvidence {
338 method: "lsof",
339 holders: Vec::new(),
340 partial: vec![format!("lsof failed: {error}")],
341 };
342 }
343 };
344 let root: bool = unsafe { libc::getuid() } == 0;
346 return HolderEvidence {
347 method: "lsof",
348 holders: matching_holders(parse_lsof_fields(stdout.as_str()).as_slice(), device, inode),
349 partial: if root { Vec::new() } else { vec![String::from(SILENT_DENIAL_NOTE)] },
350 };
351}
352
353#[cfg(windows)]
365fn scan_restart_manager_holders(lock_path: &Path) -> HolderEvidence {
366 let output: std::io::Result<std::process::Output> = std::process::Command::new("powershell.exe")
367 .args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", RESTART_MANAGER_SCRIPT])
368 .env("CLI_GIT_RM_TARGET", lock_path)
369 .stdin(std::process::Stdio::null())
370 .output();
371 match output {
372 Ok(finished) if finished.status.success() => {
373 return HolderEvidence {
374 method: "restart-manager",
375 holders: parse_restart_manager_output(String::from_utf8_lossy(finished.stdout.as_slice()).as_ref()),
376 partial: Vec::new(),
377 };
378 }
379 Ok(finished) => {
380 return HolderEvidence {
381 method: "restart-manager",
382 holders: Vec::new(),
383 partial: vec![format!("Restart Manager query failed: exit {:?}", finished.status.code())],
384 };
385 }
386 Err(error) => {
387 return HolderEvidence {
388 method: "restart-manager",
389 holders: Vec::new(),
390 partial: vec![format!("Restart Manager query failed: {error}")],
391 };
392 }
393 }
394}
395
396pub use super::index_lock_holder_parsers::{LsofProcess, matching_holders, parse_lsof_fields, parse_lsof_number, parse_restart_manager_output};
399
400#[cfg(test)]
402#[path = "index_lock_holders_tests.rs"]
403mod tests;