use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use anyhow::Result;
use fs4::fs_std::FileExt;
use crate::error::AppError;
#[derive(Debug)]
pub struct LockGuard {
_file: File,
path: PathBuf,
}
impl Drop for LockGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
pub fn acquire_exclusive(dir: &Path, what: &str) -> Result<LockGuard> {
let lock_path = dir.join(".skillctl.lock");
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.map_err(|e| {
AppError::Config(format!(
"could not open lock file at {}: {e}",
lock_path.display()
))
})?;
file.try_lock_exclusive().map_err(|e| {
AppError::Conflict(format!(
"another `skillctl` process is operating on the {what} ({}); try again in a moment (error: {e})",
lock_path.display()
))
})?;
Ok(LockGuard {
_file: file,
path: lock_path,
})
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn acquire_creates_lock_file_and_drop_cleans_up() {
let dir = TempDir::new().unwrap();
let lock_path = dir.path().join(".skillctl.lock");
assert!(!lock_path.exists());
{
let _g = acquire_exclusive(dir.path(), "test").unwrap();
assert!(lock_path.exists(), "lock file should exist while held");
}
assert!(!lock_path.exists(), "lock file should be cleaned up");
}
#[test]
fn reacquire_after_drop_succeeds() {
let dir = TempDir::new().unwrap();
{
let _g1 = acquire_exclusive(dir.path(), "test").unwrap();
}
let _g2 = acquire_exclusive(dir.path(), "test").unwrap();
}
}