use fd_lock::RwLock;
use std::fs::{File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
pub struct ManifestLock {
inner: RwLock<File>,
}
impl ManifestLock {
pub fn open(manifest_path: &Path, lock_path: &Path) -> io::Result<Self> {
let _ = manifest_path;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(lock_path)?;
Ok(Self { inner: RwLock::new(file) })
}
pub fn read<R>(&mut self, f: impl FnOnce() -> R) -> io::Result<R> {
let _guard = self.inner.read()?;
Ok(f())
}
pub fn write<R>(&mut self, f: impl FnOnce() -> R) -> io::Result<R> {
let _guard = self.inner.write()?;
Ok(f())
}
}
pub struct ScopedLock {
inner: RwLock<File>,
path: PathBuf,
}
impl ScopedLock {
pub fn open(lock_path: &Path) -> io::Result<Self> {
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(lock_path)?;
Ok(Self { inner: RwLock::new(file), path: lock_path.to_path_buf() })
}
pub fn acquire(&mut self) -> io::Result<fd_lock::RwLockWriteGuard<'_, File>> {
self.inner.write()
}
pub fn try_acquire(&mut self) -> io::Result<Option<fd_lock::RwLockWriteGuard<'_, File>>> {
match self.inner.try_write() {
Ok(guard) => Ok(Some(guard)),
Err(e) => {
if e.kind() == io::ErrorKind::WouldBlock {
Ok(None)
} else {
Err(e)
}
}
}
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
}
impl std::fmt::Debug for ScopedLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScopedLock").field("path", &self.path).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn open_creates_lock_file() {
let dir = tempdir().unwrap();
let m = dir.path().join("grex.jsonl");
let p = dir.path().join(".grex.lock");
let _l = ManifestLock::open(&m, &p).unwrap();
assert!(p.exists());
}
#[test]
fn read_runs_closure() {
let dir = tempdir().unwrap();
let m = dir.path().join("grex.jsonl");
let p = dir.path().join(".grex.lock");
let mut l = ManifestLock::open(&m, &p).unwrap();
let v = l.read(|| 42u32).unwrap();
assert_eq!(v, 42);
}
#[test]
fn write_runs_closure() {
let dir = tempdir().unwrap();
let m = dir.path().join("grex.jsonl");
let p = dir.path().join(".grex.lock");
let mut l = ManifestLock::open(&m, &p).unwrap();
let v = l.write(|| "ok").unwrap();
assert_eq!(v, "ok");
}
#[test]
fn scoped_lock_creates_parent() {
let dir = tempdir().unwrap();
let p = dir.path().join("nested").join(".grex.sync.lock");
let _l = ScopedLock::open(&p).unwrap();
assert!(p.exists());
}
#[test]
fn scoped_lock_try_acquire_succeeds_once() {
let dir = tempdir().unwrap();
let p = dir.path().join(".grex.sync.lock");
let mut l = ScopedLock::open(&p).unwrap();
let g = l.try_acquire().unwrap();
assert!(g.is_some(), "first acquire must succeed");
}
#[test]
fn scoped_lock_second_acquire_reports_busy() {
let dir = tempdir().unwrap();
let p = dir.path().join(".grex.sync.lock");
let mut l1 = ScopedLock::open(&p).unwrap();
let mut l2 = ScopedLock::open(&p).unwrap();
let _g1 = l1.try_acquire().unwrap().expect("first acquires");
let g2 = l2.try_acquire().unwrap();
assert!(g2.is_none(), "second acquire must report busy while first held");
}
#[test]
fn scoped_lock_reacquire_after_drop() {
let dir = tempdir().unwrap();
let p = dir.path().join(".grex.sync.lock");
let mut l1 = ScopedLock::open(&p).unwrap();
{
let _g = l1.try_acquire().unwrap().expect("held");
}
let mut l2 = ScopedLock::open(&p).unwrap();
let g2 = l2.try_acquire().unwrap();
assert!(g2.is_some(), "lock reacquires after first guard drops");
}
}