use std::fs::{File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use fs2::FileExt;
pub struct WatchLock {
_file: File,
path: PathBuf,
}
impl WatchLock {
fn lock_path(db_path: &Path) -> PathBuf {
let mut lock_path = db_path.as_os_str().to_owned();
lock_path.push(".watch.lock");
PathBuf::from(lock_path)
}
fn open_lock_file(db_path: &Path) -> io::Result<(File, PathBuf)> {
let path = Self::lock_path(db_path);
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)?;
Ok((file, path))
}
pub fn acquire_exclusive(db_path: &Path) -> io::Result<Self> {
let (file, path) = Self::open_lock_file(db_path)?;
file.lock_exclusive()?;
Ok(Self { _file: file, path })
}
pub fn acquire_shared(db_path: &Path) -> io::Result<Self> {
let (file, path) = Self::open_lock_file(db_path)?;
file.lock_shared()?;
Ok(Self { _file: file, path })
}
pub fn try_exclusive(db_path: &Path) -> io::Result<Option<Self>> {
let (file, path) = Self::open_lock_file(db_path)?;
match file.try_lock_exclusive() {
Ok(()) => Ok(Some(Self { _file: file, path })),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
Err(e) if e.raw_os_error().is_some() => Ok(None),
Err(e) => Err(e),
}
}
pub fn is_writer_active(db_path: &Path) -> bool {
let (file, _path) = match Self::open_lock_file(db_path) {
Ok(f) => f,
Err(_) => return false,
};
match file.try_lock_exclusive() {
Ok(()) => {
drop(file);
false
}
Err(_) => {
true
}
}
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl std::fmt::Debug for WatchLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WatchLock")
.field("path", &self.path)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn test_db_path() -> (tempfile::TempDir, PathBuf) {
let dir = tempdir().unwrap();
let db_path = dir.path().join("test.db");
(dir, db_path)
}
#[test]
fn test_exclusive_lock_acquired() {
let (_dir, db_path) = test_db_path();
let lock = WatchLock::acquire_exclusive(&db_path).unwrap();
assert!(lock.path().exists());
}
#[test]
fn test_shared_lock_acquired() {
let (_dir, db_path) = test_db_path();
let lock = WatchLock::acquire_shared(&db_path).unwrap();
assert!(lock.path().exists());
}
#[test]
fn test_multiple_shared_locks() {
let (_dir, db_path) = test_db_path();
let _lock1 = WatchLock::acquire_shared(&db_path).unwrap();
let _lock2 = WatchLock::acquire_shared(&db_path).unwrap();
}
#[test]
fn test_exclusive_blocks_second_exclusive() {
let (_dir, db_path) = test_db_path();
let _lock = WatchLock::acquire_exclusive(&db_path).unwrap();
let result = WatchLock::try_exclusive(&db_path).unwrap();
assert!(result.is_none());
}
#[test]
fn test_is_writer_active_when_locked() {
let (_dir, db_path) = test_db_path();
let _lock = WatchLock::acquire_exclusive(&db_path).unwrap();
assert!(WatchLock::is_writer_active(&db_path));
}
#[test]
fn test_is_writer_not_active_when_unlocked() {
let (_dir, db_path) = test_db_path();
{
let _lock = WatchLock::acquire_exclusive(&db_path).unwrap();
}
assert!(!WatchLock::is_writer_active(&db_path));
}
#[test]
fn test_is_writer_active_no_lock_file() {
let dir = tempdir().unwrap();
let db_path = dir.path().join("nonexistent.db");
assert!(!WatchLock::is_writer_active(&db_path));
}
#[test]
fn test_lock_released_on_drop() {
let (_dir, db_path) = test_db_path();
{
let _lock = WatchLock::acquire_exclusive(&db_path).unwrap();
}
let lock = WatchLock::try_exclusive(&db_path).unwrap();
assert!(lock.is_some());
}
}