use std::fs::{File, OpenOptions};
use std::path::Path;
use anyhow::Context as _;
/// the lock belongs to the open file. never unlink the lock file.
pub fn lock(root: &Path) -> anyhow::Result<File> {
lock_named(root, ".admin.lock")
}
pub fn lock_named(root: &Path, name: &str) -> anyhow::Result<File> {
let file = lock_file(root, name)?;
file.lock().context("lock output root")?;
Ok(file)
}
pub fn lock_file(root: &Path, name: &str) -> anyhow::Result<File> {
std::fs::create_dir_all(root).context("create output root")?;
OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(root.join(name))
.context("open output lock")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_is_exclusive_and_released_on_drop() {
let root = tempfile::tempdir().unwrap();
let first = lock(root.path()).unwrap();
let second = OpenOptions::new()
.read(true)
.write(true)
.open(root.path().join(".admin.lock"))
.unwrap();
assert!(second.try_lock().is_err());
drop(first);
// macos releases a flock a moment after close. retry instead of checking once.
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
match second.try_lock() {
Ok(()) => break,
Err(_) if std::time::Instant::now() < deadline => {
std::thread::sleep(std::time::Duration::from_millis(5))
}
Err(error) => panic!("lock was not released on drop: {error}"),
}
}
}
}