use std::collections::HashSet;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write as _};
use std::path::{Path, PathBuf};
use std::process;
use std::str;
use std::sync::{Mutex, OnceLock};
#[cfg(unix)]
use std::os::unix::fs::FileExt;
pub(crate) const LOCK_FILE: &str = "LOCK";
fn held() -> &'static Mutex<HashSet<PathBuf>> {
static HELD: OnceLock<Mutex<HashSet<PathBuf>>> = OnceLock::new();
HELD.get_or_init(|| Mutex::new(HashSet::new()))
}
pub(crate) enum LockFailure {
Contended {
path: PathBuf,
holder: Option<u32>,
},
Io { path: PathBuf, source: io::Error },
}
#[derive(Debug)]
pub(crate) struct DirLock {
_file: File,
key: PathBuf,
}
impl Drop for DirLock {
fn drop(&mut self) {
match held().lock() {
Ok(mut held) => {
held.remove(&self.key);
}
Err(poisoned) => {
poisoned.into_inner().remove(&self.key);
}
}
}
}
pub(crate) struct Acquired {
pub(crate) lock: DirLock,
pub(crate) unsupported: Option<io::Error>,
}
pub(crate) fn acquire(dir: &Path) -> Result<Acquired, LockFailure> {
let path = dir.join(LOCK_FILE);
let key = fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(|source| LockFailure::Io {
path: path.clone(),
source,
})?;
if !register(&key) {
return Err(LockFailure::Contended {
path,
holder: Some(process::id()),
});
}
let lock = DirLock {
_file: file,
key: key.clone(),
};
match take_record_lock(&lock._file) {
Ok(()) => {
record_holder(&lock._file);
Ok(Acquired {
lock,
unsupported: None,
})
}
Err(RecordLock::Contended) => {
let holder = read_holder(&lock._file);
drop(lock);
Err(LockFailure::Contended { path, holder })
}
Err(RecordLock::Unsupported(source)) => Ok(Acquired {
lock,
unsupported: Some(source),
}),
Err(RecordLock::Io(source)) => {
drop(lock);
Err(LockFailure::Io { path, source })
}
}
}
fn register(key: &Path) -> bool {
let mut held = match held().lock() {
Ok(held) => held,
Err(poisoned) => poisoned.into_inner(),
};
held.insert(key.to_path_buf())
}
enum RecordLock {
Contended,
Unsupported(io::Error),
Io(io::Error),
}
#[cfg(unix)]
fn take_record_lock(file: &File) -> Result<(), RecordLock> {
use nix::errno::Errno;
use nix::fcntl::{FcntlArg, fcntl};
use nix::libc;
let lock = libc::flock {
l_type: libc::F_WRLCK as libc::c_short,
l_whence: libc::SEEK_SET as libc::c_short,
l_start: 0,
l_len: 0,
l_pid: 0,
};
match fcntl(file, FcntlArg::F_SETLK(&lock)) {
Ok(_) => Ok(()),
Err(Errno::EACCES | Errno::EAGAIN) => Err(RecordLock::Contended),
Err(err @ (Errno::EOPNOTSUPP | Errno::ENOSYS)) => {
Err(RecordLock::Unsupported(io::Error::from(err)))
}
Err(err) => Err(RecordLock::Io(io::Error::from(err))),
}
}
#[cfg(not(unix))]
fn take_record_lock(file: &File) -> Result<(), RecordLock> {
use std::fs::TryLockError;
match file.try_lock() {
Ok(()) => Ok(()),
Err(TryLockError::WouldBlock) => Err(RecordLock::Contended),
Err(TryLockError::Error(source)) => Err(RecordLock::Unsupported(source)),
}
}
#[cfg(unix)]
fn record_holder(file: &File) {
let _ = file.set_len(0);
let _ = (&*file).write_all(format!("{}\n", process::id()).as_bytes());
}
#[cfg(not(unix))]
fn record_holder(_file: &File) {}
#[cfg(unix)]
fn read_holder(file: &File) -> Option<u32> {
let mut buf = [0u8; 32];
let read = file.read_at(&mut buf, 0).ok()?;
str::from_utf8(&buf[..read]).ok()?.trim().parse().ok()
}
#[cfg(not(unix))]
fn read_holder(_file: &File) -> Option<u32> {
None
}