use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{LockError, Result};
pub const LOCK_EXTENSION: &str = "lock";
#[derive(Debug)]
pub struct StoreLock {
path: PathBuf,
released: bool,
}
impl StoreLock {
pub fn acquire(store_path: &Path) -> Result<Self> {
let lock_path = lock_path_for(store_path);
if let Some(parent) = lock_path.parent() {
fs::create_dir_all(parent).map_err(|source| LockError::Acquire {
path: lock_path.clone(),
source,
})?;
}
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
.map_err(|source| {
if source.kind() == std::io::ErrorKind::AlreadyExists {
LockError::AlreadyHeld {
path: lock_path.clone(),
}
} else {
LockError::Acquire {
path: lock_path.clone(),
source,
}
}
})?;
let metadata = LockMetadata::current();
let payload = metadata.render();
file.write_all(payload.as_bytes())
.map_err(|source| LockError::Acquire {
path: lock_path.clone(),
source,
})?;
file.flush().map_err(|source| LockError::Acquire {
path: lock_path.clone(),
source,
})?;
Ok(Self {
path: lock_path,
released: false,
})
}
pub fn release(mut self) -> Result<()> {
self.release_inner()
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
fn release_inner(&mut self) -> Result<()> {
if self.released {
return Ok(());
}
if self.path.exists() {
fs::remove_file(&self.path).map_err(|source| LockError::Release {
path: self.path.clone(),
source,
})?;
}
self.released = true;
Ok(())
}
}
impl Drop for StoreLock {
fn drop(&mut self) {
let _ = self.release_inner();
}
}
#[must_use]
pub fn lock_path_for(store_path: &Path) -> PathBuf {
let rendered = format!("{}.{}", store_path.to_string_lossy(), LOCK_EXTENSION);
PathBuf::from(rendered)
}
#[must_use]
pub fn is_locked(store_path: &Path) -> bool {
lock_path_for(store_path).exists()
}
pub fn remove_stale_lock(store_path: &Path) -> Result<()> {
let path = lock_path_for(store_path);
if !path.exists() {
return Ok(());
}
fs::remove_file(&path).map_err(|source| LockError::Release { path, source }.into())
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct LockMetadata {
pid: u32,
created_unix_seconds: u64,
}
impl LockMetadata {
fn current() -> Self {
Self {
pid: process::id(),
created_unix_seconds: unix_now(),
}
}
fn render(&self) -> String {
format!(
"pid={}\ncreated_unix_seconds={}\n",
self.pid, self.created_unix_seconds
)
}
}
fn unix_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_secs())
}