proc_daemon/lock.rs
1//! File-based locking mechanism to prevent multiple daemon instances.
2//!
3//! This module provides cross-platform file locking capabilities to ensure
4//! only a single instance of a daemon runs at any given time.
5
6use crate::error::{Error, Result};
7use fs2::FileExt;
8use std::{fs::File, path::Path};
9
10/// File lock manager for ensuring single-instance daemon execution.
11#[derive(Debug)]
12pub struct InstanceLock {
13 /// The lock file handle
14 file: Option<File>,
15 /// Path to the lock file
16 path: String,
17}
18
19impl InstanceLock {
20 /// Creates a new instance lock manager.
21 ///
22 /// # Arguments
23 ///
24 /// * `path` - Path where the lock file will be created
25 pub fn new<P: AsRef<Path>>(path: P) -> Self {
26 let path_str = path.as_ref().to_string_lossy().to_string();
27 Self {
28 file: None,
29 path: path_str,
30 }
31 }
32
33 /// Attempts to acquire the lock.
34 ///
35 /// # Returns
36 ///
37 /// * `Ok(())` if the lock was successfully acquired
38 /// * `Err` if the lock could not be acquired (possibly because another instance is running)
39 /// Acquires a lock on the file to ensure single-instance execution
40 ///
41 /// # Errors
42 ///
43 /// Returns an error if the file cannot be created, opened, or locked
44 pub fn lock(&mut self) -> Result<()> {
45 // Create or open the lock file
46 let file = File::options()
47 .read(true)
48 .write(true)
49 .create(true)
50 .truncate(true)
51 .open(&self.path)
52 .map_err(|e| {
53 Error::io_with_source(
54 format!("Failed to open or create lock file at {}", self.path),
55 e,
56 )
57 })?;
58
59 // Try to acquire an exclusive lock
60 file.try_lock_exclusive().map_err(|e| {
61 Error::runtime_with_source(
62 format!(
63 "Failed to acquire exclusive lock on {}, another instance may be running",
64 self.path
65 ),
66 e,
67 )
68 })?;
69
70 // Store the locked file
71 self.file = Some(file);
72 Ok(())
73 }
74
75 /// Releases the lock if it was acquired.
76 /// Releases the lock on the file
77 ///
78 /// # Errors
79 ///
80 /// Returns an error if the file cannot be unlocked
81 pub fn unlock(&mut self) -> Result<()> {
82 if let Some(file) = self.file.take() {
83 // Release the lock by unlocking the file
84 fs2::FileExt::unlock(&file).map_err(|e| {
85 Error::io_with_source(format!("Failed to release lock on file {}", self.path), e)
86 })?;
87 }
88 Ok(())
89 }
90
91 /// Checks if the lock is currently held by this instance.
92 /// Checks if a lock is currently held
93 #[must_use]
94 pub const fn is_locked(&self) -> bool {
95 self.file.is_some()
96 }
97}
98
99impl Drop for InstanceLock {
100 fn drop(&mut self) {
101 // Ensure the lock is released when the InstanceLock is dropped
102 if self.is_locked() {
103 let _ = self.unlock();
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 // std::fs removed as it's not used
112 use tempfile::tempdir;
113
114 #[cfg_attr(miri, ignore)]
115 #[test]
116 fn test_lock_acquisition() {
117 // Create a temporary directory for the lock file
118 let dir = tempdir().expect("Failed to create temporary directory");
119 let lock_path = dir.path().join("test.lock");
120
121 // Create an instance lock
122 let mut lock = InstanceLock::new(&lock_path);
123
124 // Should be able to acquire the lock
125 assert!(lock.lock().is_ok());
126 assert!(lock.is_locked());
127
128 // Create a second lock on the same file
129 let mut lock2 = InstanceLock::new(&lock_path);
130
131 // Should not be able to acquire the lock
132 assert!(lock2.lock().is_err());
133 assert!(!lock2.is_locked());
134
135 // Release the first lock
136 assert!(lock.unlock().is_ok());
137 assert!(!lock.is_locked());
138
139 // Now the second lock should be able to acquire it
140 assert!(lock2.lock().is_ok());
141 assert!(lock2.is_locked());
142 }
143
144 #[cfg_attr(miri, ignore)]
145 #[test]
146 fn test_lock_drop() {
147 // Create a temporary directory for the lock file
148 let dir = tempdir().expect("Failed to create temporary directory");
149 let lock_path = dir.path().join("drop_test.lock");
150
151 {
152 // Create and acquire lock in an inner scope
153 let mut lock = InstanceLock::new(&lock_path);
154 assert!(lock.lock().is_ok());
155 // Lock goes out of scope here and should be automatically released
156 }
157
158 // Should be able to create and acquire a new lock on the same file
159 let mut lock2 = InstanceLock::new(&lock_path);
160 assert!(lock2.lock().is_ok());
161 }
162}