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 return scan_proc_fd_holders_until(proc_root, device, inode, self_pid, None);
212}
213
214#[cfg(unix)]
228pub fn scan_proc_fd_holders_until(proc_root: &Path, device: u64, inode: u64, self_pid: u32, deadline: Option<std::time::Instant>) -> HolderEvidence {
229 let mut evidence: HolderEvidence = HolderEvidence {
230 method: "proc-fd",
231 holders: Vec::new(),
232 partial: Vec::new(),
233 };
234 let listing: std::fs::ReadDir = match std::fs::read_dir(proc_root) {
235 Ok(found) => found,
236 Err(error) => {
237 evidence.partial.push(format!("{}: {error}", proc_root.display()));
238 return evidence;
239 }
240 };
241 let own: String = self_pid.to_string();
242 let mut pids: Vec<String> = Vec::new();
243 for item in listing.flatten() {
244 let name: String = item.file_name().to_string_lossy().into_owned();
245 if is_pid_name(name.as_str()) && name != own {
246 pids.push(name);
247 }
248 }
249 for (reached, pid) in pids.iter().enumerate() {
250 if deadline.is_some_and(|limit| { return std::time::Instant::now() >= limit; }) {
251 evidence.partial.push(format!("stopped at the time bound after {reached} of {} processes", pids.len()));
252 break;
253 }
254 let (holds, partial) = scan_process(proc_root, pid.as_str(), device, inode);
255 if holds && let Ok(number) = pid.parse::<i64>() {
256 let command: Option<String> = std::fs::read_to_string(proc_root.join(pid).join("comm"))
257 .ok()
258 .map(trimmed);
259 evidence.holders.push(Holder { pid: number, command });
260 }
261 if let Some(reason) = partial {
262 evidence.partial.push(reason);
263 }
264 }
265 super::diagnostic_log::debug(
266 "scanProcFdHolders",
267 format!(
268 "scanned {} processes: {} holders, {} unreadable",
269 pids.len(),
270 evidence.holders.len(),
271 evidence.partial.len()
272 )
273 .as_str(),
274 );
275 return evidence;
276}
277
278#[cfg(unix)]
288fn trimmed(text: String) -> String {
289 return String::from(super::js_text::trim_javascript(text.as_str()));
290}
291
292pub const SERVICE_SCAN_BUDGET: std::time::Duration = std::time::Duration::from_millis(250);
294
295pub fn scan_platform_holders_bounded(lock_path: &Path, device: u64, inode: u64) -> HolderEvidence {
299 #[cfg(target_os = "linux")]
300 {
301 let _ = lock_path;
302 return scan_proc_fd_holders_until(Path::new("/proc"), device, inode, std::process::id(), Some(std::time::Instant::now() + SERVICE_SCAN_BUDGET));
303 }
304 #[cfg(not(target_os = "linux"))]
305 {
306 return scan_platform_holders(lock_path, device, inode);
307 }
308}
309
310pub fn scan_platform_holders(lock_path: &Path, device: u64, inode: u64) -> HolderEvidence {
320 #[cfg(target_os = "linux")]
321 {
322 let _ = lock_path;
323 return scan_proc_fd_holders(Path::new("/proc"), device, inode, std::process::id());
324 }
325 #[cfg(target_os = "macos")]
326 {
327 return scan_lsof_holders(lock_path, device, inode);
328 }
329 #[cfg(windows)]
331 {
332 let _ = (device, inode);
333 return scan_restart_manager_holders(lock_path);
334 }
335 #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
336 {
337 let _ = (lock_path, device, inode);
338 return HolderEvidence {
339 method: "unsupported",
340 holders: Vec::new(),
341 partial: vec![String::from("no open-holder scan on this platform")],
342 };
343 }
344}
345
346#[cfg(target_os = "macos")]
358fn scan_lsof_holders(lock_path: &Path, device: u64, inode: u64) -> HolderEvidence {
359 let output: std::io::Result<std::process::Output> = std::process::Command::new("lsof")
360 .args(["-n", "-P", "-w", "-F", "pcDi", "--"])
361 .arg(lock_path)
362 .env("LC_ALL", "C")
363 .stdin(std::process::Stdio::null())
364 .output();
365 let stdout: String = match output {
366 Ok(finished) if finished.status.success() || finished.status.code() == Some(1) => {
367 String::from_utf8_lossy(finished.stdout.as_slice()).into_owned()
368 }
369 Ok(finished) => {
370 return HolderEvidence {
371 method: "lsof",
372 holders: Vec::new(),
373 partial: vec![format!("lsof failed: exit {:?}", finished.status.code())],
374 };
375 }
376 Err(error) => {
377 return HolderEvidence {
378 method: "lsof",
379 holders: Vec::new(),
380 partial: vec![format!("lsof failed: {error}")],
381 };
382 }
383 };
384 let root: bool = unsafe { libc::getuid() } == 0;
386 return HolderEvidence {
387 method: "lsof",
388 holders: matching_holders(parse_lsof_fields(stdout.as_str()).as_slice(), device, inode),
389 partial: if root { Vec::new() } else { vec![String::from(SILENT_DENIAL_NOTE)] },
390 };
391}
392
393#[cfg(windows)]
405fn scan_restart_manager_holders(lock_path: &Path) -> HolderEvidence {
406 let output: std::io::Result<std::process::Output> = std::process::Command::new("powershell.exe")
407 .args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", RESTART_MANAGER_SCRIPT])
408 .env("CLI_GIT_RM_TARGET", lock_path)
409 .stdin(std::process::Stdio::null())
410 .output();
411 match output {
412 Ok(finished) if finished.status.success() => {
413 return HolderEvidence {
414 method: "restart-manager",
415 holders: parse_restart_manager_output(String::from_utf8_lossy(finished.stdout.as_slice()).as_ref()),
416 partial: Vec::new(),
417 };
418 }
419 Ok(finished) => {
420 return HolderEvidence {
421 method: "restart-manager",
422 holders: Vec::new(),
423 partial: vec![format!("Restart Manager query failed: exit {:?}", finished.status.code())],
424 };
425 }
426 Err(error) => {
427 return HolderEvidence {
428 method: "restart-manager",
429 holders: Vec::new(),
430 partial: vec![format!("Restart Manager query failed: {error}")],
431 };
432 }
433 }
434}
435
436pub use super::index_lock_holder_parsers::{LsofProcess, matching_holders, parse_lsof_fields, parse_lsof_number, parse_restart_manager_output};
439
440#[cfg(test)]
442#[path = "index_lock_holders_tests.rs"]
443mod tests;