use crate::Result;
use crate::core::matcher::journal::lock_path;
use crate::error::SubXError;
use std::path::PathBuf;
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct SubxLockGuard {
file: std::fs::File,
}
impl Drop for SubxLockGuard {
fn drop(&mut self) {
let _ = self.file.unlock();
}
}
pub async fn acquire_subx_lock() -> Result<SubxLockGuard> {
let path = lock_path()?;
tokio::task::spawn_blocking(move || acquire_lock_blocking(&path))
.await
.map_err(|e| SubXError::Io(std::io::Error::other(e.to_string())))?
}
fn acquire_lock_blocking(path: &PathBuf) -> Result<SubxLockGuard> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(path)?;
let deadline = Instant::now() + Duration::from_secs(2);
loop {
match file.try_lock() {
Ok(()) => return Ok(SubxLockGuard { file }),
Err(std::fs::TryLockError::WouldBlock) => {}
Err(e) => return Err(SubXError::Io(e.into())),
}
if Instant::now() >= deadline {
return Err(SubXError::config(format!(
"Another SubX operation is in progress. \
Please wait for it to finish or terminate the other process. \
Lock file: {}",
path.display()
)));
}
std::thread::sleep(Duration::from_millis(100));
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn acquire_lock_succeeds_when_uncontended() {
let tmp = TempDir::new().unwrap();
unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
let guard = acquire_subx_lock().await;
assert!(guard.is_ok());
drop(guard);
unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
}
#[tokio::test]
async fn lock_is_released_on_drop() {
let tmp = TempDir::new().unwrap();
unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
{
let _g1 = acquire_subx_lock().await.unwrap();
}
let g2 = acquire_subx_lock().await;
assert!(g2.is_ok());
unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
}
#[tokio::test]
async fn contention_produces_timeout_error() {
let tmp = TempDir::new().unwrap();
let lock_file = tmp.path().join("subx").join("subx.lock");
std::fs::create_dir_all(lock_file.parent().unwrap()).unwrap();
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&lock_file)
.unwrap();
file.lock().unwrap();
unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
let start = Instant::now();
let result = acquire_subx_lock().await;
let elapsed = start.elapsed();
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("Another SubX operation is in progress"));
assert!(elapsed >= Duration::from_secs(2));
file.unlock().unwrap();
unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
}
}