Skip to main content

secunit_core/evidence/
lock.rs

1//! Advisory file lock at `<root>/.secunit.lock`. Concurrent invocations
2//! against the same root serialise on this lock so `state.json` writes
3//! never interleave.
4
5use std::fs::{File, OpenOptions};
6use std::io;
7use std::path::Path;
8
9use fs2::FileExt;
10
11pub struct RootLock {
12    file: File,
13}
14
15impl RootLock {
16    /// Acquire an exclusive lock at `<root>/.secunit.lock`. Blocks until
17    /// the previous holder releases. Drop the returned guard to release.
18    pub fn acquire(root: &Path) -> io::Result<Self> {
19        let path = root.join(".secunit.lock");
20        let file = OpenOptions::new()
21            .create(true)
22            .read(true)
23            .write(true)
24            .truncate(false)
25            .open(&path)?;
26        file.lock_exclusive()?;
27        Ok(Self { file })
28    }
29
30    /// Try to acquire the lock without blocking. Returns `Ok(None)` if
31    /// another process holds it.
32    pub fn try_acquire(root: &Path) -> io::Result<Option<Self>> {
33        let path = root.join(".secunit.lock");
34        let file = OpenOptions::new()
35            .create(true)
36            .read(true)
37            .write(true)
38            .truncate(false)
39            .open(&path)?;
40        match file.try_lock_exclusive() {
41            Ok(()) => Ok(Some(Self { file })),
42            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
43            Err(e) => Err(e),
44        }
45    }
46}
47
48impl Drop for RootLock {
49    fn drop(&mut self) {
50        let _ = self.file.unlock();
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn try_acquire_returns_none_when_held() {
60        let dir = tempfile::tempdir().unwrap();
61        let _held = RootLock::acquire(dir.path()).unwrap();
62        let again = RootLock::try_acquire(dir.path()).unwrap();
63        assert!(again.is_none());
64    }
65
66    #[test]
67    fn lock_is_released_on_drop() {
68        let dir = tempfile::tempdir().unwrap();
69        {
70            let _held = RootLock::acquire(dir.path()).unwrap();
71        }
72        let again = RootLock::try_acquire(dir.path()).unwrap();
73        assert!(again.is_some());
74    }
75}