use crate::config::Config;
use anyhow::{Context, Result};
use fs2::FileExt;
use std::fs::OpenOptions;
use std::io;
use std::thread;
use std::time::{Duration, Instant};
const LOCK_FILE_NAME: &str = ".govctl.lock";
const POLL_INTERVAL_MS: u64 = 100;
pub struct GovLockGuard {
_file: std::fs::File,
}
pub fn acquire_gov_lock(config: &Config) -> Result<GovLockGuard> {
let gov_root = config.gov_root.as_path();
let lock_path = gov_root.join(LOCK_FILE_NAME);
let timeout_secs = config.concurrency.lock_timeout_secs;
if !gov_root.exists() {
anyhow::bail!(
"Gov root does not exist: {}. Run 'govctl init' first.",
gov_root.display()
);
}
let file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&lock_path)
.with_context(|| format!("Failed to open lock file: {}", lock_path.display()))?;
let deadline = Instant::now() + Duration::from_secs(timeout_secs);
let poll = Duration::from_millis(POLL_INTERVAL_MS);
loop {
match file.try_lock_exclusive() {
Ok(()) => {
return Ok(GovLockGuard { _file: file });
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
if Instant::now() >= deadline {
anyhow::bail!(
"Another govctl write command is in progress. \
Wait for it to finish or retry later. \
(Timed out after {} seconds waiting for exclusive access.)",
timeout_secs
);
}
thread::sleep(poll);
}
Err(e) => {
return Err(e)
.with_context(|| format!("Failed to acquire lock: {}", lock_path.display()));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_file_name_is_under_gov_root() {
assert_eq!(LOCK_FILE_NAME, ".govctl.lock");
}
}