use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct FileLock {
#[cfg(unix)]
_flock: nix::fcntl::Flock<std::fs::File>,
#[cfg(windows)]
_handle: std::fs::File,
}
impl FileLock {
#[cfg(unix)]
pub fn acquire(path: &Path) -> std::io::Result<Self> {
use std::os::unix::fs::OpenOptionsExt as _;
use nix::fcntl::{Flock, FlockArg};
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.mode(crate::atomic_file::OWNER_ONLY_FILE_MODE)
.open(lock_path(path))?;
Flock::lock(file, FlockArg::LockExclusive)
.map(|flock| Self { _flock: flock })
.map_err(|(_file, errno)| std::io::Error::from(errno))
}
#[cfg(windows)]
pub fn acquire(path: &Path) -> std::io::Result<Self> {
use std::os::windows::fs::OpenOptionsExt as _;
const ERROR_SHARING_VIOLATION: i32 = 32;
const RETRY_INTERVAL: std::time::Duration = std::time::Duration::from_millis(2);
let lock_path = lock_path(path);
loop {
match std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.share_mode(0)
.open(&lock_path)
{
Ok(handle) => return Ok(Self { _handle: handle }),
Err(error) if error.raw_os_error() == Some(ERROR_SHARING_VIOLATION) => {
std::thread::sleep(RETRY_INTERVAL);
}
Err(error) => return Err(error),
}
}
}
}
fn lock_path(path: &Path) -> PathBuf {
let mut name = path
.file_name()
.map(std::ffi::OsStr::to_os_string)
.unwrap_or_default();
name.push(".lock");
path.parent().unwrap_or_else(|| Path::new(".")).join(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_lock_file_is_a_sibling_of_the_file_it_guards() {
let path = Path::new("/var/lib/shep/kv.json");
assert_eq!(lock_path(path), Path::new("/var/lib/shep/kv.json.lock"));
}
#[test]
fn a_second_acquire_on_the_same_path_blocks_until_the_first_drops() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("dogs.toml");
let first = FileLock::acquire(&path).unwrap();
let path2 = path.clone();
let (tx, rx) = std::sync::mpsc::channel();
let t = std::thread::spawn(move || {
let _second = FileLock::acquire(&path2).unwrap();
tx.send(()).unwrap();
});
assert!(
rx.recv_timeout(std::time::Duration::from_millis(200))
.is_err(),
"must block"
);
drop(first);
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("must proceed once released");
t.join().unwrap();
}
#[test]
fn two_different_paths_do_not_exclude_each_other() {
let dir = tempfile::tempdir().unwrap();
let _kv = FileLock::acquire(&dir.path().join("kv.json")).unwrap();
let _secrets = FileLock::acquire(&dir.path().join("secrets.json")).unwrap();
}
}