1use crate::Result;
11use crate::core::matcher::journal::lock_path;
12use crate::error::SubXError;
13use std::path::PathBuf;
14use std::time::{Duration, Instant};
15
16#[derive(Debug)]
20pub struct SubxLockGuard {
21 file: std::fs::File,
22}
23
24impl Drop for SubxLockGuard {
25 fn drop(&mut self) {
26 let _ = self.file.unlock();
27 }
28}
29
30pub async fn acquire_subx_lock() -> Result<SubxLockGuard> {
37 let path = lock_path()?;
38 tokio::task::spawn_blocking(move || acquire_lock_blocking(&path))
39 .await
40 .map_err(|e| SubXError::Io(std::io::Error::other(e.to_string())))?
41}
42
43fn acquire_lock_blocking(path: &PathBuf) -> Result<SubxLockGuard> {
44 if let Some(parent) = path.parent() {
45 std::fs::create_dir_all(parent)?;
46 }
47
48 let file = std::fs::OpenOptions::new()
49 .create(true)
50 .read(true)
51 .write(true)
52 .truncate(false)
53 .open(path)?;
54
55 let deadline = Instant::now() + Duration::from_secs(2);
56 loop {
57 match file.try_lock() {
58 Ok(()) => return Ok(SubxLockGuard { file }),
59 Err(std::fs::TryLockError::WouldBlock) => {}
60 Err(e) => return Err(SubXError::Io(e.into())),
61 }
62 if Instant::now() >= deadline {
63 return Err(SubXError::config(format!(
64 "Another SubX operation is in progress. \
65 Please wait for it to finish or terminate the other process. \
66 Lock file: {}",
67 path.display()
68 )));
69 }
70 std::thread::sleep(Duration::from_millis(100));
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use tempfile::TempDir;
78
79 #[tokio::test]
80 async fn acquire_lock_succeeds_when_uncontended() {
81 let tmp = TempDir::new().unwrap();
82 unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
83 let guard = acquire_subx_lock().await;
84 assert!(guard.is_ok());
85 drop(guard);
86 unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
87 }
88
89 #[tokio::test]
90 async fn lock_is_released_on_drop() {
91 let tmp = TempDir::new().unwrap();
92 unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
93 {
94 let _g1 = acquire_subx_lock().await.unwrap();
95 }
96 let g2 = acquire_subx_lock().await;
98 assert!(g2.is_ok());
99 unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
100 }
101
102 #[tokio::test]
103 async fn contention_produces_timeout_error() {
104 let tmp = TempDir::new().unwrap();
105 let lock_file = tmp.path().join("subx").join("subx.lock");
106 std::fs::create_dir_all(lock_file.parent().unwrap()).unwrap();
107
108 let file = std::fs::OpenOptions::new()
110 .create(true)
111 .truncate(false)
112 .read(true)
113 .write(true)
114 .open(&lock_file)
115 .unwrap();
116 file.lock().unwrap();
117
118 unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
119 let start = Instant::now();
120 let result = acquire_subx_lock().await;
121 let elapsed = start.elapsed();
122
123 assert!(result.is_err());
124 let err_msg = format!("{}", result.unwrap_err());
125 assert!(err_msg.contains("Another SubX operation is in progress"));
126 assert!(elapsed >= Duration::from_secs(2));
127
128 file.unlock().unwrap();
129 unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
130 }
131}