use std::path::Path;
use std::time::{Duration, SystemTime};
const LOCK_TTL: Duration = Duration::from_secs(60);
const LOCK_EXT: &str = "lock";
pub fn acquire(definition_path: &Path) -> anyhow::Result<LockGuard> {
let lock_path = definition_path.with_extension(LOCK_EXT);
if lock_path.exists() {
if let Ok(meta) = std::fs::metadata(&lock_path) {
if let Ok(modified) = meta.modified() {
if SystemTime::now().duration_since(modified).unwrap_or_default() < LOCK_TTL {
anyhow::bail!(
"Definition file is locked by another process: {}.\n\
Delete {} to force-unlock.",
definition_path.display(),
lock_path.display()
);
}
log::warn!("Removing stale lock: {}", lock_path.display());
let _ = std::fs::remove_file(&lock_path);
}
}
}
std::fs::write(&lock_path, format!("pid:{}", std::process::id()))?;
log::debug!("Lock acquired: {}", lock_path.display());
Ok(LockGuard { lock_path })
}
#[must_use]
pub struct LockGuard {
lock_path: std::path::PathBuf,
}
impl Drop for LockGuard {
fn drop(&mut self) {
if let Err(e) = std::fs::remove_file(&self.lock_path) {
log::warn!("Could not remove lock file {}: {e}", self.lock_path.display());
} else {
log::debug!("Lock released: {}", self.lock_path.display());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_acquire_and_release() {
let tmp = tempfile::tempdir().unwrap();
let def = tmp.path().join("audit.yaml");
std::fs::write(&def, "").unwrap();
{
let _guard = acquire(&def).unwrap();
let lock = def.with_extension("lock");
assert!(lock.exists(), "lock file should exist while guard is alive");
}
let lock = def.with_extension("lock");
assert!(!lock.exists(), "lock file should be removed on drop");
}
#[test]
fn double_lock_fails() {
let tmp = tempfile::tempdir().unwrap();
let def = tmp.path().join("audit.yaml");
std::fs::write(&def, "").unwrap();
let _guard1 = acquire(&def).unwrap();
let result2 = acquire(&def);
assert!(result2.is_err(), "second acquire should fail while lock is held");
}
}