use std::fs::{File, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum LockError {
#[error("lock path is a symlink (refused): {0}")]
Symlink(PathBuf),
#[error("lock path is not a regular file: {0}")]
NotARegularFile(PathBuf),
#[error("another inferd-daemon already holds the lock at {0}")]
AlreadyHeld(PathBuf),
#[error("io: {0}")]
Io(#[from] io::Error),
}
#[derive(Debug)]
pub struct Lock {
_file: File,
path: PathBuf,
}
impl Lock {
pub fn acquire(path: impl AsRef<Path>) -> Result<Self, LockError> {
let path = path.as_ref();
if let Ok(meta) = std::fs::symlink_metadata(path) {
if meta.file_type().is_symlink() {
return Err(LockError::Symlink(path.to_path_buf()));
}
if !meta.file_type().is_file() {
return Err(LockError::NotARegularFile(path.to_path_buf()));
}
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
match file.try_lock() {
Ok(()) => Ok(Lock {
_file: file,
path: path.to_path_buf(),
}),
Err(std::fs::TryLockError::WouldBlock) => {
Err(LockError::AlreadyHeld(path.to_path_buf()))
}
Err(std::fs::TryLockError::Error(e)) => Err(LockError::Io(e)),
}
}
pub fn path(&self) -> &Path {
&self.path
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn acquire_then_release_succeeds() {
let dir = tempdir().unwrap();
let path = dir.path().join("inferd.lock");
let lock = Lock::acquire(&path).unwrap();
assert_eq!(lock.path(), &path);
drop(lock);
let _again = Lock::acquire(&path).unwrap();
}
#[test]
fn second_acquire_while_held_fails() {
let dir = tempdir().unwrap();
let path = dir.path().join("inferd.lock");
let _first = Lock::acquire(&path).unwrap();
let err = Lock::acquire(&path).unwrap_err();
assert!(matches!(err, LockError::AlreadyHeld(_)));
}
#[cfg(unix)]
#[test]
fn pre_existing_symlink_is_refused() {
let dir = tempdir().unwrap();
let target = dir.path().join("target.bin");
std::fs::write(&target, b"x").unwrap();
let symlink = dir.path().join("inferd.lock");
std::os::unix::fs::symlink(&target, &symlink).unwrap();
let err = Lock::acquire(&symlink).unwrap_err();
assert!(matches!(err, LockError::Symlink(_)));
}
#[test]
fn directory_at_lock_path_refused() {
let dir = tempdir().unwrap();
let bad = dir.path().join("inferd.lock");
std::fs::create_dir(&bad).unwrap();
let err = Lock::acquire(&bad).unwrap_err();
assert!(matches!(err, LockError::NotARegularFile(_)));
}
}