use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::os::windows::fs::OpenOptionsExt as _;
use std::path::Path;
use std::path::PathBuf;
use std::thread;
use tracing::instrument;
use super::FileLockError;
use super::backoff::BackoffIterator;
const FILE_SHARE_READ: u32 = 1;
const FILE_SHARE_WRITE: u32 = 2;
const ERROR_SHARING_VIOLATION: u32 = 32;
pub struct FileLock {
path: PathBuf,
file: Option<File>,
}
impl FileLock {
pub fn lock(path: PathBuf) -> Result<Self, FileLockError> {
tracing::info!("Attempting to lock {path:?}");
let file = try_create_file(&path).map_err(|err| FileLockError {
message: "Failed to open lock file",
path: path.clone(),
err,
})?;
file.lock().map_err(|err| FileLockError {
message: "Failed to lock lock file",
path: path.clone(),
err,
})?;
tracing::info!("Locked {path:?}");
Ok(Self {
path,
file: Some(file),
})
}
}
impl Drop for FileLock {
#[instrument(skip_all)]
fn drop(&mut self) {
if let Some(file) = self.file.take() {
file.unlock()
.inspect_err(|err| tracing::warn!(?err, ?self.path, "Failed to unlock lock file"))
.ok();
}
std::fs::remove_file(&self.path).ok();
}
}
fn try_create_file(path: &Path) -> io::Result<File> {
let mut backoff_iterator = BackoffIterator::new();
let mut options = OpenOptions::new();
options
.create(true)
.write(true)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE);
loop {
match options.open(path) {
Ok(file) => return Ok(file),
Err(err)
if err.kind() == io::ErrorKind::PermissionDenied
|| err.raw_os_error() == Some(ERROR_SHARING_VIOLATION as _) =>
{
let Some(duration) = backoff_iterator.next() else {
return Err(err);
};
tempfile::tempfile_in(path.parent().expect("file path should have parent"))?;
thread::sleep(duration);
}
Err(err) => return Err(err),
}
}
}