use std::path::{Path, PathBuf};
pub fn create_config_file(parent: &Path) -> std::io::Result<tempfile::NamedTempFile> {
crate::atomic_file::create_staging_file(parent, "shep", ".toml.tmp")
}
#[derive(Debug)]
pub struct ConfigLock {
#[cfg(unix)]
_flock: nix::fcntl::Flock<std::fs::File>,
#[cfg(windows)]
_handle: std::fs::File,
}
impl ConfigLock {
#[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),
}
}
}
#[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))
}
}
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 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 = ConfigLock::acquire(&path).unwrap();
let path2 = path.clone();
let (tx, rx) = std::sync::mpsc::channel();
let t = std::thread::spawn(move || {
let _second = ConfigLock::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 a_staged_config_file_is_owner_only_named_for_the_pair_and_lands_where_asked() {
let dir = tempfile::tempdir().unwrap();
let tmp = create_config_file(dir.path()).unwrap();
assert_eq!(tmp.path().parent(), Some(dir.path()));
let name = tmp.path().file_name().unwrap().to_str().unwrap();
assert!(name.starts_with("shep"), "{name}");
assert!(name.ends_with(".toml.tmp"), "{name}");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = tmp.as_file().metadata().unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
}
}
}