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}