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}