use std::cell::RefCell;
use std::fs::{self, File, OpenOptions};
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
thread_local! {
static HELD: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn lock_path_for(store_path: &Path) -> PathBuf {
let mut os = store_path.as_os_str().to_os_string();
os.push(".lock");
PathBuf::from(os)
}
#[derive(Debug)]
pub(crate) enum StoreLock {
Owner { file: File, key: PathBuf },
Reentrant,
}
impl StoreLock {
pub(crate) fn acquire(store_path: &Path) -> Result<Self> {
let path = lock_path_for(&super::atomic::resolve(store_path));
let key = fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
if HELD.with(|held| held.borrow().contains(&key)) {
return Ok(Self::Reentrant);
}
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let file = opts.open(&path).map_err(|e| {
Error::Io(format!(
"cannot open the store lock {}: {e}",
path.display()
))
})?;
file.lock()
.map_err(|e| Error::Io(format!("cannot lock {}: {e}", path.display())))?;
let key = fs::canonicalize(&path).unwrap_or(key);
HELD.with(|held| held.borrow_mut().push(key.clone()));
Ok(Self::Owner { file, key })
}
pub(crate) fn is_reentrant(&self) -> bool {
matches!(self, Self::Reentrant)
}
}
impl Drop for StoreLock {
fn drop(&mut self) {
if let Self::Owner { file, key } = self {
let _ = file.unlock();
HELD.with(|held| held.borrow_mut().retain(|held_key| held_key != key));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lock_path_sits_beside_the_store() {
assert_eq!(
lock_path_for(Path::new("/h/.xurl")),
PathBuf::from("/h/.xurl.lock")
);
}
#[test]
fn a_missing_parent_directory_is_an_error() {
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path().join("absent").join(".xurl");
assert!(StoreLock::acquire(&store).is_err());
}
#[test]
fn a_second_acquire_on_the_same_thread_is_reentrant_until_the_owner_drops() {
let dir = tempfile::TempDir::new().unwrap();
let store = dir.path().join(".xurl");
let owner = StoreLock::acquire(&store).unwrap();
assert!(!owner.is_reentrant());
assert!(StoreLock::acquire(&store).unwrap().is_reentrant());
drop(owner);
assert!(!StoreLock::acquire(&store).unwrap().is_reentrant());
}
}