1use std::path::Path;
8use std::time::{Duration, SystemTime};
9
10const LOCK_TTL: Duration = Duration::from_secs(60);
11const LOCK_EXT: &str = "lock";
12
13pub fn acquire(definition_path: &Path) -> anyhow::Result<LockGuard> {
18 let lock_path = definition_path.with_extension(LOCK_EXT);
19
20 if lock_path.exists() {
21 if let Ok(meta) = std::fs::metadata(&lock_path) {
23 if let Ok(modified) = meta.modified() {
24 if SystemTime::now().duration_since(modified).unwrap_or_default() < LOCK_TTL {
25 anyhow::bail!(
26 "Definition file is locked by another process: {}.\n\
27 Delete {} to force-unlock.",
28 definition_path.display(),
29 lock_path.display()
30 );
31 }
32 log::warn!("Removing stale lock: {}", lock_path.display());
34 let _ = std::fs::remove_file(&lock_path);
35 }
36 }
37 }
38
39 std::fs::write(&lock_path, format!("pid:{}", std::process::id()))?;
40 log::debug!("Lock acquired: {}", lock_path.display());
41 Ok(LockGuard { lock_path })
42}
43
44#[must_use]
46pub struct LockGuard {
47 lock_path: std::path::PathBuf,
48}
49
50impl Drop for LockGuard {
51 fn drop(&mut self) {
52 if let Err(e) = std::fs::remove_file(&self.lock_path) {
53 log::warn!("Could not remove lock file {}: {e}", self.lock_path.display());
54 } else {
55 log::debug!("Lock released: {}", self.lock_path.display());
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn lock_acquire_and_release() {
66 let tmp = tempfile::tempdir().unwrap();
67 let def = tmp.path().join("audit.yaml");
68 std::fs::write(&def, "").unwrap();
69
70 {
71 let _guard = acquire(&def).unwrap();
72 let lock = def.with_extension("lock");
73 assert!(lock.exists(), "lock file should exist while guard is alive");
74 }
75
76 let lock = def.with_extension("lock");
77 assert!(!lock.exists(), "lock file should be removed on drop");
78 }
79
80 #[test]
81 fn double_lock_fails() {
82 let tmp = tempfile::tempdir().unwrap();
83 let def = tmp.path().join("audit.yaml");
84 std::fs::write(&def, "").unwrap();
85
86 let _guard1 = acquire(&def).unwrap();
87 let result2 = acquire(&def);
88 assert!(result2.is_err(), "second acquire should fail while lock is held");
89 }
90}