Skip to main content

kunobi_daemon/
ownership.rs

1use std::fs::{File, OpenOptions, TryLockError};
2use std::io;
3use std::path::Path;
4
5/// An exclusive OS file lock, released when this value is dropped.
6///
7/// Every participant must use the same persistent path in an application-owned
8/// directory. Neither this type nor a caller may unlink or replace the lock
9/// file: another process could still hold a lock on the original inode.
10/// Keep this guard alive through endpoint cleanup and the work it protects.
11/// Use separate paths for daemon ownership and upgrade serialization.
12#[derive(Debug)]
13pub struct ProcessLock(File);
14
15impl ProcessLock {
16    /// Acquire without waiting, creating the file if necessary.
17    ///
18    /// Returns `Ok(None)` only for contention; filesystem and locking failures
19    /// remain errors. The parent directory must already exist.
20    pub fn try_acquire(path: impl AsRef<Path>) -> io::Result<Option<Self>> {
21        let mut options = OpenOptions::new();
22        options.read(true).write(true).create(true).truncate(false);
23        #[cfg(unix)]
24        {
25            use std::os::unix::fs::OpenOptionsExt;
26            options.mode(0o600);
27        }
28        Self::try_from_file(options.open(path)?)
29    }
30
31    /// Lock an already-open file. This preserves a consumer's secure open policy.
32    pub fn try_from_file(file: File) -> io::Result<Option<Self>> {
33        match file.try_lock() {
34            Ok(()) => Ok(Some(Self(file))),
35            Err(TryLockError::WouldBlock) => Ok(None),
36            Err(TryLockError::Error(error)) => Err(error),
37        }
38    }
39
40    /// Observe an existing lock without creating a file. Only contention is true.
41    pub fn is_held(path: impl AsRef<Path>) -> io::Result<bool> {
42        let file = match OpenOptions::new().read(true).write(true).open(path) {
43            Ok(file) => file,
44            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
45            Err(error) => return Err(error),
46        };
47        Ok(Self::try_from_file(file)?.is_none())
48    }
49}
50
51impl Drop for ProcessLock {
52    fn drop(&mut self) {
53        // Explicit unlock also releases ownership if an OS-level duplicate of
54        // the descriptor survives elsewhere in the process.
55        let _ = self.0.unlock();
56    }
57}