use std::path::Path;
use std::sync::OnceLock;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct LockOptions {
pub timeout: Duration,
pub retry_interval: Duration,
pub stale: bool,
}
impl Default for LockOptions {
fn default() -> Self {
Self {
timeout: Duration::from_secs(10),
retry_interval: Duration::from_millis(100),
stale: false,
}
}
}
pub struct LockGuard {
path: std::path::PathBuf,
}
impl Drop for LockGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
struct LockfileState {
_initialized: bool,
}
static LOCKFILE_STATE: OnceLock<LockfileState> = OnceLock::new();
fn get_lockfile_state() -> &'static LockfileState {
LOCKFILE_STATE.get_or_init(|| LockfileState { _initialized: true })
}
pub async fn lock(path: &Path, options: Option<LockOptions>) -> std::io::Result<LockGuard> {
let _ = get_lockfile_state();
let options = options.unwrap_or_default();
let lock_path = path.with_extension("lock");
let deadline = std::time::Instant::now() + options.timeout;
while std::time::Instant::now() < deadline {
match std::fs::File::create_new(&lock_path) {
Ok(_) => {
return Ok(LockGuard {
path: lock_path,
});
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if options.stale {
if let Ok(metadata) = std::fs::metadata(&lock_path) {
if let Ok(modified) = metadata.modified() {
if modified.elapsed().unwrap_or(Duration::ZERO) > options.timeout {
let _ = std::fs::remove_file(&lock_path);
continue;
}
}
}
}
tokio::time::sleep(options.retry_interval).await;
}
Err(e) => return Err(e),
}
}
Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("Could not acquire lock on {:?}", path),
))
}
pub fn lock_sync(path: &Path, options: Option<LockOptions>) -> std::io::Result<LockGuard> {
let _ = get_lockfile_state();
let options = options.unwrap_or_default();
let lock_path = path.with_extension("lock");
let deadline = std::time::Instant::now() + options.timeout;
while std::time::Instant::now() < deadline {
match std::fs::File::create_new(&lock_path) {
Ok(_) => {
return Ok(LockGuard {
path: lock_path,
});
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if options.stale {
if let Ok(metadata) = std::fs::metadata(&lock_path) {
if let Ok(modified) = metadata.modified() {
if modified.elapsed().unwrap_or(Duration::ZERO) > options.timeout {
let _ = std::fs::remove_file(&lock_path);
continue;
}
}
}
}
std::thread::sleep(options.retry_interval);
}
Err(e) => return Err(e),
}
}
Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("Could not acquire lock on {:?}", path),
))
}
pub fn unlock(path: &Path) -> std::io::Result<()> {
let lock_path = path.with_extension("lock");
std::fs::remove_file(lock_path)
}
pub fn check(path: &Path) -> bool {
let lock_path = path.with_extension("lock");
lock_path.exists()
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[tokio::test]
async fn test_lock_and_unlock() {
let dir = std::env::temp_dir();
let test_file = dir.join("test_lockfile.txt");
let mut f = std::fs::File::create(&test_file).unwrap();
writeln!(f, "test").unwrap();
let guard = lock(&test_file, None).await.unwrap();
assert!(check(&test_file));
drop(guard);
assert!(!check(&test_file));
let _ = std::fs::remove_file(&test_file);
}
}