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 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(&mut 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 self.acquired = false;
116 tracing::info!(
117 daemon_pid,
118 lock_file = %self.lock_file.display(),
119 "Wrote daemon PID to lock file"
120 );
121 Ok(())
122 }
123
124 pub fn read_daemon_pid(&self) -> Result<Option<u32>> {
126 if !self.lock_file.exists() {
127 return Ok(None);
128 }
129 let content = fs::read_to_string(&self.lock_file).context(format!(
130 "Failed to read PID from lock file at {}",
131 self.lock_file.display()
132 ))?;
133 let pid = content.trim().parse::<u32>().context(format!(
134 "Failed to parse PID '{}' from lock file at {}",
135 content.trim(),
136 self.lock_file.display()
137 ))?;
138 Ok(Some(pid))
139 }
140
141 pub fn release(&mut self) -> Result<()> {
143 if self.lock_file.exists() {
144 fs::remove_file(&self.lock_file).context("Failed to remove lock file")?;
145 tracing::info!("Released process lock");
146 }
147 self.acquired = false;
148 Ok(())
149 }
150
151 #[cfg(unix)]
153 fn is_process_running(&self, pid: u32) -> bool {
154 use nix::sys::signal::kill;
156 use nix::unistd::Pid;
157
158 kill(Pid::from_raw(pid as i32), None).is_ok()
159 }
160
161 #[cfg(windows)]
162 fn is_process_running(&self, pid: u32) -> bool {
163 use windows_sys::Win32::Foundation::CloseHandle;
165 use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION};
166
167 unsafe {
168 let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
169 if handle == std::ptr::null_mut() {
170 return false;
171 }
172 CloseHandle(handle);
173 true
174 }
175 }
176}
177
178impl Drop for ProcessLock {
179 fn drop(&mut self) {
180 if self.acquired {
184 let _ = self.release();
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use tempfile::TempDir;
193
194 #[test]
195 fn test_process_lock_acquire_release() {
196 let temp_dir = TempDir::new().unwrap();
197 let mut lock = ProcessLock::new(temp_dir.path());
198
199 assert!(lock.try_acquire().unwrap());
201 assert!(lock.acquired);
202
203 lock.release().unwrap();
205 assert!(!lock.acquired);
206
207 assert!(lock.try_acquire().unwrap());
209 }
210
211 #[test]
212 fn test_process_lock_double_acquire() {
213 let temp_dir = TempDir::new().unwrap();
214 let mut lock1 = ProcessLock::new(temp_dir.path());
215 let mut lock2 = ProcessLock::new(temp_dir.path());
216
217 assert!(lock1.try_acquire().unwrap());
219
220 assert!(!lock2.try_acquire().unwrap());
222
223 lock1.release().unwrap();
225
226 assert!(lock2.try_acquire().unwrap());
228 }
229
230 #[test]
231 fn test_process_lock_cleanup_on_drop() {
232 let temp_dir = TempDir::new().unwrap();
233 let lock_file = temp_dir.path().join("lore").join("pid");
234
235 {
236 let mut lock = ProcessLock::new(temp_dir.path());
237 lock.try_acquire().unwrap();
238 assert!(lock_file.exists());
239 }
240
241 assert!(!lock_file.exists());
243 }
244
245 #[test]
246 fn test_daemon_pid_write_and_read() {
247 let temp_dir = TempDir::new().unwrap();
248 let lock_file = temp_dir.path().join("lore").join("pid");
249
250 std::fs::create_dir_all(temp_dir.path().join("lore")).unwrap();
252
253 let mut lock = ProcessLock::new(temp_dir.path());
254 lock.write_daemon_pid(12345).unwrap();
255
256 assert!(lock_file.exists());
257 let content = std::fs::read_to_string(&lock_file).unwrap();
258 assert_eq!(content, "12345");
259
260 let read_pid = lock.read_daemon_pid().unwrap();
262 assert_eq!(read_pid, Some(12345));
263 }
264
265 #[test]
266 fn daemon_pid_survives_startup_guard_drop() {
267 let temp_dir = TempDir::new().unwrap();
268 let lock_file = temp_dir.path().join("lore").join("pid");
269
270 {
271 let mut lock = ProcessLock::new(temp_dir.path());
272 lock.try_acquire().unwrap();
273 lock.write_daemon_pid(12345).unwrap();
274 }
275
276 assert_eq!(std::fs::read_to_string(lock_file).unwrap(), "12345");
277 }
278
279 #[test]
280 fn test_read_daemon_pid_no_file() {
281 let temp_dir = TempDir::new().unwrap();
282 let lock = ProcessLock::new(temp_dir.path());
283 let pid = lock.read_daemon_pid().unwrap();
284 assert_eq!(pid, None);
285 }
286
287 #[test]
288 fn test_stale_lock_removal() {
289 let temp_dir = TempDir::new().unwrap();
290 let lock_file = temp_dir.path().join("lore").join("pid");
291
292 std::fs::create_dir_all(lock_file.parent().unwrap()).unwrap();
295 std::fs::write(&lock_file, "9999999").unwrap();
296
297 let mut lock = ProcessLock::new(temp_dir.path());
298 assert!(lock.try_acquire().unwrap());
300 let content = std::fs::read_to_string(&lock_file).unwrap();
302 assert_eq!(content.trim().parse::<u32>().unwrap(), std::process::id());
303 }
304}