#![expect(missing_docs)]
use std::fs::File;
use std::path::PathBuf;
use rustix::fs::FlockOperation;
use tracing::instrument;
use super::FileLockError;
pub struct FileLock {
path: PathBuf,
file: File,
}
impl FileLock {
pub fn lock(path: PathBuf) -> Result<Self, FileLockError> {
Ok(Self::lock_inner(path, true)?.expect("blocking lock should return a lock"))
}
pub fn try_lock(path: PathBuf) -> Result<Option<Self>, FileLockError> {
Self::lock_inner(path, false)
}
fn lock_inner(path: PathBuf, blocking: bool) -> Result<Option<Self>, FileLockError> {
tracing::info!("Attempting to lock {path:?}");
let operation = if blocking {
FlockOperation::LockExclusive
} else {
FlockOperation::NonBlockingLockExclusive
};
loop {
let file = File::create(&path).map_err(|err| FileLockError {
message: "Failed to open lock file",
path: path.clone(),
err,
})?;
match rustix::fs::flock(&file, operation) {
Ok(()) => {}
Err(rustix::io::Errno::WOULDBLOCK) if !blocking => return Ok(None),
Err(errno) => {
return Err(FileLockError {
message: "Failed to lock lock file",
path: path.clone(),
err: errno.into(),
});
}
}
match rustix::fs::fstat(&file) {
Ok(stat) => {
if stat.st_nlink == 0 {
continue;
}
}
Err(rustix::io::Errno::STALE) => {
continue;
}
Err(errno) => {
return Err(FileLockError {
message: "failed to stat lock file",
path: path.clone(),
err: errno.into(),
});
}
}
tracing::info!("Locked {path:?}");
return Ok(Some(Self { path, file }));
}
}
}
impl Drop for FileLock {
#[instrument(skip_all)]
fn drop(&mut self) {
std::fs::remove_file(&self.path).ok();
rustix::fs::flock(&self.file, FlockOperation::Unlock).ok();
}
}