use std::{ fs::{self, OpenOptions}, io::Write, path::Path, thread, time::{Duration, Instant}, }; pub struct FileLock { path: String, } impl FileLock { pub fn acquire(path: &str) -> anyhow::Result { Self::acquire_timeout(path, Duration::from_secs(0)) } pub fn acquire_timeout(path: &str, timeout: Duration) -> anyhow::Result { let deadline = Instant::now() + timeout; loop { match Self::try_acquire_once(path) { Ok(lock) => return Ok(lock), Err(e) => { let msg = e.to_string(); if !msg.contains("lock already exists") { return Err(e); } if Instant::now() >= deadline { anyhow::bail!("lock busy timeout: {path}"); } thread::sleep(Duration::from_millis(100)); } } } } fn try_acquire_once(path: &str) -> anyhow::Result { let p = Path::new(path); if let Some(parent) = p.parent() { fs::create_dir_all(parent)?; } match OpenOptions::new().create_new(true).write(true).open(p) { Ok(mut file) => { file.write_all(std::process::id().to_string().as_bytes())?; return Ok(Self { path: path.to_string(), }); } Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { let stale = is_stale_lock(p)?; if stale { let _ = fs::remove_file(p); let mut file = OpenOptions::new().create_new(true).write(true).open(p)?; file.write_all(std::process::id().to_string().as_bytes())?; return Ok(Self { path: path.to_string(), }); } anyhow::bail!("lock already exists: {path}"); } Err(e) => return Err(e.into()), } } } impl Drop for FileLock { fn drop(&mut self) { let _ = fs::remove_file(&self.path); } } fn is_stale_lock(path: &Path) -> anyhow::Result { let pid_raw = fs::read_to_string(path)?; let pid = match pid_raw.trim().parse::() { Ok(v) => v, Err(_) => return Ok(true), }; let proc_path = format!("/proc/{pid}"); Ok(!Path::new(&proc_path).exists()) } #[cfg(test)] mod tests { use super::*; #[test] fn second_acquire_fails_for_live_lock() { let p = format!("/tmp/awg2_harness_lock_test_{}", std::process::id()); let _first = FileLock::acquire(&p).expect("first lock"); let second = FileLock::acquire(&p); assert!(second.is_err()); } #[test] fn acquire_timeout_times_out_for_live_lock() { let p = format!("/tmp/awg2_harness_lock_test_timeout_{}", std::process::id()); let _first = FileLock::acquire(&p).expect("first lock"); let second = FileLock::acquire_timeout(&p, Duration::from_millis(250)); assert!(second.is_err()); } }