thoughts_tool/utils/
locks.rs1use anyhow::Context;
9use anyhow::Result;
10use std::fs::File;
11use std::fs::OpenOptions;
12use std::fs::TryLockError;
13use std::path::Path;
14use std::path::PathBuf;
15
16pub struct FileLock {
21 _file: File,
22 pub path: PathBuf,
23}
24
25impl FileLock {
26 pub fn lock_exclusive(path: impl AsRef<Path>) -> Result<Self> {
30 let path = path.as_ref().to_path_buf();
31 if let Some(parent) = path.parent() {
32 std::fs::create_dir_all(parent)
33 .with_context(|| format!("Failed to create lock dir: {}", parent.display()))?;
34 }
35 let file = OpenOptions::new()
36 .read(true)
37 .write(true)
38 .create(true)
39 .truncate(false)
40 .open(&path)
41 .with_context(|| format!("Failed to open lock file: {}", path.display()))?;
42 file.lock()
43 .with_context(|| format!("Failed to acquire exclusive lock: {}", path.display()))?;
44 Ok(Self { _file: file, path })
45 }
46
47 pub fn try_lock_exclusive(path: impl AsRef<Path>) -> Result<Option<Self>> {
52 let path = path.as_ref().to_path_buf();
53 if let Some(parent) = path.parent() {
54 std::fs::create_dir_all(parent)
55 .with_context(|| format!("Failed to create lock dir: {}", parent.display()))?;
56 }
57 let file = OpenOptions::new()
58 .read(true)
59 .write(true)
60 .create(true)
61 .truncate(false)
62 .open(&path)
63 .with_context(|| format!("Failed to open lock file: {}", path.display()))?;
64 match file.try_lock() {
65 Ok(()) => Ok(Some(Self { _file: file, path })),
66 Err(TryLockError::WouldBlock) => Ok(None), Err(TryLockError::Error(e)) => Err(anyhow::Error::from(e).context(format!(
68 "Failed to acquire exclusive lock: {}",
69 path.display()
70 ))),
71 }
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use tempfile::TempDir;
79
80 #[test]
81 fn test_lock_exclusive_basic() {
82 let dir = TempDir::new().unwrap();
83 let lock_path = dir.path().join("test.lock");
84
85 let lock = FileLock::lock_exclusive(&lock_path).unwrap();
86 assert!(lock_path.exists());
87 drop(lock);
88 }
89
90 #[test]
91 fn test_lock_creates_parent_dirs() {
92 let dir = TempDir::new().unwrap();
93 let lock_path = dir.path().join("nested").join("dirs").join("test.lock");
94
95 let lock = FileLock::lock_exclusive(&lock_path).unwrap();
96 assert!(lock_path.exists());
97 drop(lock);
98 }
99
100 #[test]
101 fn test_try_lock_exclusive_succeeds_when_available() {
102 let dir = TempDir::new().unwrap();
103 let lock_path = dir.path().join("test.lock");
104
105 let lock = FileLock::try_lock_exclusive(&lock_path).unwrap();
106 assert!(lock.is_some());
107 }
108
109 #[test]
110 fn test_try_lock_exclusive_fails_when_held() {
111 let dir = TempDir::new().unwrap();
112 let lock_path = dir.path().join("test.lock");
113
114 let _lock1 = FileLock::lock_exclusive(&lock_path).unwrap();
115 let lock2 = FileLock::try_lock_exclusive(&lock_path).unwrap();
116
117 assert!(
118 lock2.is_none(),
119 "Second lock should fail when first is held"
120 );
121 }
122}