use std::collections::BTreeSet;
use std::fs::{File, OpenOptions};
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt as _;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};
pub const STORE_LOCK_FILE: &str = "writer.lock";
type StoreKey = (u64, u64);
static HELD_STORES: Mutex<BTreeSet<StoreKey>> = Mutex::new(BTreeSet::new());
#[derive(Debug)]
pub struct StoreLock {
file: Option<File>,
registry_key: StoreKey,
}
impl StoreLock {
pub fn acquire(directory: &Path) -> Result<Self, StoreLockError> {
let path = directory.join(STORE_LOCK_FILE);
let registry_key = store_identity(directory).map_err(|source| StoreLockError::Io {
path: directory.to_path_buf(),
source,
})?;
if !held_stores().insert(registry_key) {
return Err(StoreLockError::Io {
path,
source: std::io::Error::from(std::io::ErrorKind::WouldBlock),
});
}
let file = open_lock_file(&path).map_err(|source| {
held_stores().remove(®istry_key);
StoreLockError::Io {
path: path.clone(),
source,
}
})?;
if let Err(source) = lock_exclusive(&file) {
drop(file);
held_stores().remove(®istry_key);
return Err(StoreLockError::Io { path, source });
}
Ok(Self {
file: Some(file),
registry_key,
})
}
}
fn held_stores() -> MutexGuard<'static, BTreeSet<StoreKey>> {
match HELD_STORES.lock() {
Ok(held) => held,
Err(poisoned) => poisoned.into_inner(),
}
}
#[cfg(unix)]
fn store_identity(directory: &Path) -> std::io::Result<StoreKey> {
let metadata = std::fs::metadata(directory)?;
Ok((metadata.dev(), metadata.ino()))
}
#[cfg(windows)]
fn store_identity(directory: &Path) -> std::io::Result<StoreKey> {
crate::sys::windows::directory_identity(directory)
}
#[cfg(not(any(unix, windows)))]
fn store_identity(_directory: &Path) -> std::io::Result<StoreKey> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"store identity requires a supported platform",
))
}
#[cfg(not(windows))]
fn open_lock_file(path: &Path) -> std::io::Result<File> {
OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(path)
}
#[cfg(windows)]
fn open_lock_file(path: &Path) -> std::io::Result<File> {
use std::os::windows::fs::OpenOptionsExt as _;
OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.share_mode(crate::sys::windows::FILE_SHARE_READ | crate::sys::windows::FILE_SHARE_WRITE)
.open(path)
}
#[cfg(unix)]
fn lock_exclusive(file: &File) -> std::io::Result<()> {
let mut lock = unsafe {
std::mem::zeroed::<libc::flock>()
};
lock.l_type = libc::F_WRLCK as libc::c_short;
lock.l_whence = libc::SEEK_SET as libc::c_short;
lock.l_start = 0;
lock.l_len = 0;
let result = unsafe {
libc::fcntl(file.as_raw_fd(), libc::F_SETLK, &lock)
};
if result == -1 {
let error = std::io::Error::last_os_error();
if error
.raw_os_error()
.is_some_and(|errno| errno == libc::EAGAIN || errno == libc::EACCES)
{
Err(std::io::Error::from(std::io::ErrorKind::WouldBlock))
} else {
Err(error)
}
} else {
Ok(())
}
}
#[cfg(windows)]
fn lock_exclusive(file: &File) -> std::io::Result<()> {
crate::sys::windows::lock_file_exclusive(file)
}
#[cfg(not(any(unix, windows)))]
fn lock_exclusive(_: &File) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"store locks require a supported file-locking platform",
))
}
impl Drop for StoreLock {
fn drop(&mut self) {
drop(self.file.take());
held_stores().remove(&self.registry_key);
}
}
#[derive(Debug)]
pub enum StoreLockError {
Io {
path: PathBuf,
source: std::io::Error,
},
}
impl std::fmt::Display for StoreLockError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, source } => {
write!(formatter, "store lock {}: {source}", path.display())
}
}
}
}
impl std::error::Error for StoreLockError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
}
}
}