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