#![allow(unstable_name_collisions)]
use crate::fs::{self, FsError};
use std::fmt::Debug;
use std::fs::File;
use std::path::{Path, PathBuf};
use tracing::{instrument, trace};
pub const LOCK_FILE: &str = ".lock";
pub struct FileLock {
lock: PathBuf,
file: File,
unlocked: bool,
}
impl FileLock {
pub fn new(path: PathBuf) -> Result<Self, FsError> {
use std::io::prelude::*;
let mut file: File;
#[cfg(unix)]
{
file = fs::create_file_if_missing(&path)?;
}
#[cfg(windows)]
{
use std::thread::sleep;
use std::time::Duration;
let mut elapsed = 0;
loop {
match fs::create_file_if_missing(&path) {
Ok(inner) => {
file = inner;
break;
}
Err(error) => {
if let FsError::Create {
error: io_error, ..
} = &error
{
if io_error.raw_os_error().is_some_and(|code| code == 5) {
sleep(Duration::from_millis(100));
elapsed += 100;
if elapsed <= 60000 {
continue;
}
}
}
return Err(error);
}
}
}
}
trace!(
lock = ?path,
"Waiting to acquire lock",
);
file.lock().map_err(|error| FsError::Lock {
path: path.clone(),
error: Box::new(error),
})?;
let pid = std::process::id();
trace!(
lock = ?path,
pid,
"Acquired lock, writing PID",
);
file.write(format!("{pid}").as_ref())
.map_err(|error| FsError::Write {
path: path.clone(),
error: Box::new(error),
})?;
Ok(Self {
lock: path,
file,
unlocked: false,
})
}
pub fn unlock(&mut self) -> Result<(), FsError> {
if self.unlocked {
return Ok(());
}
trace!(path = ?self.lock, "Unlocking path");
let handle_error = |error: std::io::Error| FsError::Unlock {
path: self.lock.to_path_buf(),
error: Box::new(error),
};
#[cfg(windows)]
if let Err(error) = self.file.unlock() {
if error.raw_os_error().is_some_and(|os| os == 158) {
} else {
return Err(handle_error(error));
}
}
#[cfg(unix)]
self.file.unlock().map_err(handle_error)?;
self.unlocked = true;
fs::remove_file(&self.lock)
}
}
impl Drop for FileLock {
fn drop(&mut self) {
if let Err(error) = self.unlock() {
if matches!(error, FsError::Unlock { .. }) {
panic!("Failed to remove lock {}: {}", self.lock.display(), error);
}
}
}
}
pub type DirLock = FileLock;
pub fn is_dir_locked<T: AsRef<Path>>(path: T) -> bool {
path.as_ref().join(LOCK_FILE).exists()
}
pub fn is_file_locked<T: AsRef<Path>>(path: T) -> bool {
let Ok(file) = File::open(path) else {
return false;
};
match file.try_lock() {
Ok(_) => {
file.unlock().unwrap();
false
}
Err(_) => true,
}
}
#[inline]
#[instrument]
pub fn lock_directory<T: AsRef<Path> + Debug>(path: T) -> Result<DirLock, FsError> {
let path = path.as_ref();
fs::create_dir_all(path)?;
if !path.is_dir() {
return Err(FsError::RequireDir {
path: path.to_path_buf(),
});
}
trace!(dir = ?path, "Locking directory");
DirLock::new(path.join(LOCK_FILE))
}
#[inline]
#[instrument]
pub fn lock_file<T: AsRef<Path> + Debug>(path: T) -> Result<FileLock, FsError> {
let path = path.as_ref();
if path.is_dir() {
return Err(FsError::RequireFile {
path: path.to_path_buf(),
});
}
trace!(file = ?path, "Locking file");
FileLock::new(path.to_path_buf())
}
#[inline]
#[instrument(skip(file, op))]
pub fn lock_file_exclusive<T, F, V>(path: T, mut file: File, op: F) -> Result<V, FsError>
where
T: AsRef<Path> + Debug,
F: FnOnce(&mut File) -> Result<V, FsError>,
{
let path = path.as_ref();
trace!(file = ?path, "Locking file exclusively");
file.lock().map_err(|error| FsError::Lock {
path: path.to_path_buf(),
error: Box::new(error),
})?;
let result = op(&mut file)?;
file.unlock().map_err(|error| FsError::Unlock {
path: path.to_path_buf(),
error: Box::new(error),
})?;
trace!(file = ?path, "Unlocking file exclusively");
Ok(result)
}
#[inline]
#[instrument(skip(file, op))]
pub fn lock_file_shared<T, F, V>(path: T, mut file: File, op: F) -> Result<V, FsError>
where
T: AsRef<Path> + Debug,
F: FnOnce(&mut File) -> Result<V, FsError>,
{
let path = path.as_ref();
trace!(file = ?path, "Locking file");
file.lock_shared().map_err(|error| FsError::Lock {
path: path.to_path_buf(),
error: Box::new(error),
})?;
let result = op(&mut file)?;
file.unlock().map_err(|error| FsError::Unlock {
path: path.to_path_buf(),
error: Box::new(error),
})?;
trace!(file = ?path, "Unlocking file");
Ok(result)
}
#[inline]
pub fn read_file_with_lock<T: AsRef<Path>>(path: T) -> Result<String, FsError> {
use std::io::prelude::*;
let path = path.as_ref();
lock_file_shared(path, fs::open_file(path)?, |file| {
let mut buffer = String::new();
file.read_to_string(&mut buffer)
.map_err(|error| FsError::Read {
path: path.to_path_buf(),
error: Box::new(error),
})?;
Ok(buffer)
})
}
#[inline]
pub fn write_file_with_lock<T: AsRef<Path>, D: AsRef<[u8]>>(
path: T,
data: D,
) -> Result<(), FsError> {
use std::io::{SeekFrom, prelude::*};
let path = path.as_ref();
let handle_error = |error: std::io::Error| FsError::Write {
path: path.to_path_buf(),
error: Box::new(error),
};
lock_file_exclusive(path, fs::create_file_if_missing(path)?, |file| {
trace!(file = ?path, "Writing file");
file.set_len(0).map_err(handle_error)?;
file.seek(SeekFrom::Start(0)).map_err(handle_error)?;
file.write(data.as_ref()).map_err(handle_error)?;
Ok(())
})?;
Ok(())
}