use anyhow::{Context, Result};
use fs2::FileExt;
use std::fs::{File, OpenOptions};
use std::path::Path;
pub struct FileLock {
file: File,
}
impl FileLock {
pub fn lock(path: &Path) -> Result<Self> {
let file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path)
.with_context(|| format!("Failed to open lock file {}", path.display()))?;
file.lock_exclusive()
.with_context(|| format!("Failed to acquire lock on {}", path.display()))?;
Ok(Self { file })
}
#[allow(dead_code)]
pub fn try_lock(path: &Path) -> Result<Option<Self>> {
let file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path)
.with_context(|| format!("Failed to open lock file {}", path.display()))?;
match file.try_lock_exclusive() {
Ok(()) => Ok(Some(Self { file })),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
Err(e) => Err(e).with_context(|| format!("Failed to try lock on {}", path.display())),
}
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let _ = self.file.unlock();
}
}
pub fn with_lock<F, T>(path: &Path, f: F) -> Result<T>
where
F: FnOnce() -> Result<T>,
{
let _lock = FileLock::lock(path)?;
f()
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
use tempfile::tempdir;
#[test]
fn lock_prevents_concurrent_access() {
let dir = tempdir().unwrap();
let lock_path = dir.path().join("test.lock");
let lock1 = FileLock::lock(&lock_path).unwrap();
let lock_path_clone = lock_path.clone();
let handle = thread::spawn(move || {
match FileLock::try_lock(&lock_path_clone) {
Ok(None) => None, Ok(Some(_)) => Some(FileLock::lock(&lock_path_clone).unwrap()), Err(_) => None, }
});
thread::sleep(Duration::from_millis(50));
let result = handle.join().unwrap();
assert!(result.is_none(), "Second lock should fail");
drop(lock1);
let lock2 = FileLock::try_lock(&lock_path).unwrap();
assert!(
lock2.is_some(),
"Should acquire lock after first is released"
);
}
#[test]
fn with_lock_releases_on_panic() {
let dir = tempdir().unwrap();
let lock_path = dir.path().join("panic.lock");
let result: Result<Result<(), _>, _> = std::panic::catch_unwind(|| {
with_lock(&lock_path, || {
panic!("intentional panic");
})
});
assert!(result.is_err());
let lock = FileLock::try_lock(&lock_path).unwrap();
assert!(lock.is_some());
}
#[test]
fn with_lock_works_normally() {
let dir = tempdir().unwrap();
let lock_path = dir.path().join("normal.lock");
let result = with_lock(&lock_path, || Ok(42));
assert_eq!(result.unwrap(), 42);
}
}