Skip to main content

edda_ledger/
lock.rs

1use crate::paths::EddaPaths;
2use fs2::FileExt;
3use std::fs::{File, OpenOptions};
4
5/// Exclusive workspace lock backed by `.edda/LOCK`.
6/// Automatically released when dropped.
7pub struct WorkspaceLock {
8    _file: File,
9}
10
11impl WorkspaceLock {
12    /// Try to acquire the workspace lock (non-blocking).
13    /// Returns an error if already locked by another process.
14    pub fn acquire(paths: &EddaPaths) -> anyhow::Result<Self> {
15        let file = OpenOptions::new()
16            .create(true)
17            .truncate(false)
18            .read(true)
19            .write(true)
20            .open(&paths.lock_file)
21            .map_err(|e| {
22                anyhow::anyhow!("cannot open lock file {}: {}", paths.lock_file.display(), e)
23            })?;
24
25        file.try_lock_exclusive().map_err(|_| {
26            anyhow::anyhow!(
27                "workspace is locked by another process ({})",
28                paths.lock_file.display()
29            )
30        })?;
31
32        Ok(Self { _file: file })
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn acquire_and_drop() {
42        let tmp = std::env::temp_dir().join(format!("edda_lock_test_{}", std::process::id()));
43        let _ = std::fs::remove_dir_all(&tmp);
44        let p = EddaPaths::discover(&tmp);
45        p.ensure_layout().unwrap();
46
47        let lock = WorkspaceLock::acquire(&p).unwrap();
48        // Second acquire should fail while first is held
49        assert!(WorkspaceLock::acquire(&p).is_err());
50        drop(lock);
51        // After drop, should succeed again
52        let _lock2 = WorkspaceLock::acquire(&p).unwrap();
53
54        let _ = std::fs::remove_dir_all(&tmp);
55    }
56}