use std::path::Path;
pub(crate) struct FileLock {
#[cfg(unix)]
_file: std::fs::File,
}
impl FileLock {
#[cfg(unix)]
pub(crate) fn acquire(target: &Path) -> std::io::Result<Self> {
use std::os::unix::io::AsRawFd;
let mut lock_path = target.as_os_str().to_os_string();
lock_path.push(".lock");
let lock_path = std::path::PathBuf::from(lock_path);
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)?;
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Self { _file: file })
}
#[cfg(not(unix))]
pub(crate) fn acquire(_target: &Path) -> std::io::Result<Self> {
Ok(Self {})
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::sync::mpsc;
use std::time::Duration;
#[test]
fn acquire_creates_sibling_lock_file_and_releases_on_drop() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("networks.json");
let lock_path = tmp.path().join("networks.json.lock");
let guard = FileLock::acquire(&target).unwrap();
assert!(lock_path.exists());
drop(guard);
let _guard = FileLock::acquire(&target).unwrap();
}
#[test]
fn exclusive_lock_blocks_other_file_descriptors_until_released() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("index.json");
let guard = FileLock::acquire(&target).unwrap();
let thread_target = target.clone();
let (tx, rx) = mpsc::channel();
let waiter = std::thread::spawn(move || {
let _guard = FileLock::acquire(&thread_target).unwrap();
tx.send(()).unwrap();
});
assert!(
rx.recv_timeout(Duration::from_millis(100)).is_err(),
"second lock acquisition should block while the first guard is alive"
);
drop(guard);
rx.recv_timeout(Duration::from_secs(2))
.expect("second lock acquisition should proceed after drop");
waiter.join().unwrap();
}
}