1use anyhow::{Context, Result};
9use std::fs;
10use std::path::Path;
11use std::process;
12
13pub struct ProcessLock {
15 lock_file: std::path::PathBuf,
16 acquired: bool,
17}
18
19impl ProcessLock {
20 pub fn new(nap_home: &Path) -> Self {
22 let lock_file = nap_home.join("lore").join("pid");
23 Self {
24 lock_file,
25 acquired: false,
26 }
27 }
28
29 pub fn try_acquire(&mut self) -> Result<bool> {
37 if let Some(parent) = self.lock_file.parent() {
39 fs::create_dir_all(parent)
40 .context("Failed to create lock directory for process lock")?;
41 }
42
43 if self.lock_file.exists() {
45 let existing_pid = fs::read_to_string(&self.lock_file).with_context(|| {
47 format!(
48 "Failed to read PID lock file at {}",
49 self.lock_file.display()
50 )
51 })?;
52
53 let existing_pid: u32 = existing_pid.trim().parse().with_context(|| {
54 format!(
55 "Failed to parse PID from lock file at {}",
56 self.lock_file.display()
57 )
58 })?;
59
60 if self.is_process_running(existing_pid) {
62 tracing::warn!(
63 pid = existing_pid,
64 lock_file = %self.lock_file.display(),
65 "Lore server is already running (lock held by daemon PID {})",
66 existing_pid
67 );
68 return Ok(false);
69 } else {
70 tracing::info!(
72 pid = existing_pid,
73 lock_file = %self.lock_file.display(),
74 "Removing stale lock file (PID {} no longer running)",
75 existing_pid
76 );
77 fs::remove_file(&self.lock_file).with_context(|| {
78 format!(
79 "Failed to remove stale lock file at {}",
80 self.lock_file.display()
81 )
82 })?;
83 }
84 }
85
86 let current_pid = process::id();
90 fs::write(&self.lock_file, current_pid.to_string()).with_context(|| {
91 format!("Failed to write lock file at {}", self.lock_file.display())
92 })?;
93
94 self.acquired = true;
95 tracing::info!(
96 pid = current_pid,
97 "Acquired process lock (placeholder PID written)"
98 );
99 Ok(true)
100 }
101
102 pub fn write_daemon_pid(&self, daemon_pid: u32) -> Result<()> {
107 fs::write(&self.lock_file, daemon_pid.to_string()).context(format!(
108 "Failed to write daemon PID {} to lock file at {}",
109 daemon_pid,
110 self.lock_file.display()
111 ))?;
112 tracing::info!(
113 daemon_pid,
114 lock_file = %self.lock_file.display(),
115 "Wrote daemon PID to lock file"
116 );
117 Ok(())
118 }
119
120 pub fn read_daemon_pid(&self) -> Result<Option<u32>> {
122 if !self.lock_file.exists() {
123 return Ok(None);
124 }
125 let content = fs::read_to_string(&self.lock_file).context(format!(
126 "Failed to read PID from lock file at {}",
127 self.lock_file.display()
128 ))?;
129 let pid = content.trim().parse::<u32>().context(format!(
130 "Failed to parse PID '{}' from lock file at {}",
131 content.trim(),
132 self.lock_file.display()
133 ))?;
134 Ok(Some(pid))
135 }
136
137 pub fn release(&mut self) -> Result<()> {
139 if self.acquired && self.lock_file.exists() {
140 fs::remove_file(&self.lock_file).context("Failed to remove lock file")?;
141 self.acquired = false;
142 tracing::info!("Released process lock");
143 }
144 Ok(())
145 }
146
147 #[cfg(unix)]
149 fn is_process_running(&self, pid: u32) -> bool {
150 use nix::sys::signal::kill;
152 use nix::unistd::Pid;
153
154 kill(Pid::from_raw(pid as i32), None).is_ok()
155 }
156
157 #[cfg(windows)]
158 fn is_process_running(&self, pid: u32) -> bool {
159 use windows_sys::Win32::Foundation::CloseHandle;
161 use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION};
162
163 unsafe {
164 let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
165 if handle == std::ptr::null_mut() {
166 return false;
167 }
168 CloseHandle(handle);
169 true
170 }
171 }
172}
173
174impl Drop for ProcessLock {
175 fn drop(&mut self) {
176 let _ = self.release();
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use tempfile::TempDir;
185
186 #[test]
187 fn test_process_lock_acquire_release() {
188 let temp_dir = TempDir::new().unwrap();
189 let mut lock = ProcessLock::new(temp_dir.path());
190
191 assert!(lock.try_acquire().unwrap());
193 assert!(lock.acquired);
194
195 lock.release().unwrap();
197 assert!(!lock.acquired);
198
199 assert!(lock.try_acquire().unwrap());
201 }
202
203 #[test]
204 fn test_process_lock_double_acquire() {
205 let temp_dir = TempDir::new().unwrap();
206 let mut lock1 = ProcessLock::new(temp_dir.path());
207 let mut lock2 = ProcessLock::new(temp_dir.path());
208
209 assert!(lock1.try_acquire().unwrap());
211
212 assert!(!lock2.try_acquire().unwrap());
214
215 lock1.release().unwrap();
217
218 assert!(lock2.try_acquire().unwrap());
220 }
221
222 #[test]
223 fn test_process_lock_cleanup_on_drop() {
224 let temp_dir = TempDir::new().unwrap();
225 let lock_file = temp_dir.path().join("lore").join("pid");
226
227 {
228 let mut lock = ProcessLock::new(temp_dir.path());
229 lock.try_acquire().unwrap();
230 assert!(lock_file.exists());
231 }
232
233 assert!(!lock_file.exists());
235 }
236
237 #[test]
238 fn test_daemon_pid_write_and_read() {
239 let temp_dir = TempDir::new().unwrap();
240 let lock_file = temp_dir.path().join("lore").join("pid");
241
242 std::fs::create_dir_all(temp_dir.path().join("lore")).unwrap();
244
245 let lock = ProcessLock::new(temp_dir.path());
246 lock.write_daemon_pid(12345).unwrap();
247
248 assert!(lock_file.exists());
249 let content = std::fs::read_to_string(&lock_file).unwrap();
250 assert_eq!(content, "12345");
251
252 let read_pid = lock.read_daemon_pid().unwrap();
254 assert_eq!(read_pid, Some(12345));
255 }
256
257 #[test]
258 fn test_read_daemon_pid_no_file() {
259 let temp_dir = TempDir::new().unwrap();
260 let lock = ProcessLock::new(temp_dir.path());
261 let pid = lock.read_daemon_pid().unwrap();
262 assert_eq!(pid, None);
263 }
264
265 #[test]
266 fn test_stale_lock_removal() {
267 let temp_dir = TempDir::new().unwrap();
268 let lock_file = temp_dir.path().join("lore").join("pid");
269
270 std::fs::create_dir_all(lock_file.parent().unwrap()).unwrap();
273 std::fs::write(&lock_file, "9999999").unwrap();
274
275 let mut lock = ProcessLock::new(temp_dir.path());
276 assert!(lock.try_acquire().unwrap());
278 let content = std::fs::read_to_string(&lock_file).unwrap();
280 assert_eq!(content.trim().parse::<u32>().unwrap(), std::process::id());
281 }
282}