use std::{
fs::{File, OpenOptions},
io,
path::PathBuf,
};
use fs4::{FileExt, TryLockError};
use thiserror::Error;
use tracing::debug;
use crate::paths::{self, PathsError};
#[allow(
dead_code,
reason = "the File is held only so the OS keeps the lock — not read again"
)]
pub struct InstanceGuard {
_handle: File,
}
#[derive(Debug, Error)]
pub enum InstanceError {
#[error("could not resolve lock path")]
Path(#[from] PathsError),
#[error("could not open lock file at {path}")]
Open {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("another instance already holds the lock at {path}")]
AlreadyRunning { path: PathBuf },
#[error("lock attempt at {path} failed")]
LockFailed {
path: PathBuf,
#[source]
source: io::Error,
},
}
pub fn acquire(lock_name: &str) -> Result<InstanceGuard, InstanceError> {
let path = paths::config_dir()?.join(lock_name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|source| InstanceError::Open {
path: path.clone(),
source,
})?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(|source| InstanceError::Open {
path: path.clone(),
source,
})?;
match FileExt::try_lock(&file) {
Ok(()) => {
debug!(path = %path.display(), "single-instance lock acquired");
Ok(InstanceGuard { _handle: file })
}
Err(TryLockError::WouldBlock) => Err(InstanceError::AlreadyRunning { path }),
Err(TryLockError::Error(source)) => Err(InstanceError::LockFailed { path, source }),
}
}