use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::process;
pub struct ProcessLock {
lock_file: std::path::PathBuf,
acquired: bool,
}
impl ProcessLock {
pub fn new(nap_home: &Path) -> Self {
let lock_file = nap_home.join("lore").join("pid");
Self {
lock_file,
acquired: false,
}
}
pub fn try_acquire(&mut self) -> Result<bool> {
if let Some(parent) = self.lock_file.parent() {
fs::create_dir_all(parent)
.context("Failed to create lock directory for process lock")?;
}
if self.lock_file.exists() {
let existing_pid = fs::read_to_string(&self.lock_file).with_context(|| {
format!(
"Failed to read PID lock file at {}",
self.lock_file.display()
)
})?;
let existing_pid: u32 = existing_pid.trim().parse().with_context(|| {
format!(
"Failed to parse PID from lock file at {}",
self.lock_file.display()
)
})?;
if self.is_process_running(existing_pid) {
tracing::warn!(
pid = existing_pid,
lock_file = %self.lock_file.display(),
"Lore server is already running (lock held by PID {})",
existing_pid
);
return Ok(false);
} else {
tracing::info!(
pid = existing_pid,
lock_file = %self.lock_file.display(),
"Removing stale lock file (PID {} no longer running)",
existing_pid
);
fs::remove_file(&self.lock_file).with_context(|| {
format!(
"Failed to remove stale lock file at {}",
self.lock_file.display()
)
})?;
}
}
let current_pid = process::id();
fs::write(&self.lock_file, current_pid.to_string()).with_context(|| {
format!("Failed to write lock file at {}", self.lock_file.display())
})?;
self.acquired = true;
tracing::info!(
pid = current_pid,
"Acquired process lock (placeholder PID written)"
);
Ok(true)
}
pub fn write_daemon_pid(&mut self, daemon_pid: u32) -> Result<()> {
fs::write(&self.lock_file, daemon_pid.to_string()).context(format!(
"Failed to write daemon PID {} to lock file at {}",
daemon_pid,
self.lock_file.display()
))?;
self.acquired = false;
tracing::info!(
daemon_pid,
lock_file = %self.lock_file.display(),
"Wrote daemon PID to lock file"
);
Ok(())
}
pub fn read_daemon_pid(&self) -> Result<Option<u32>> {
if !self.lock_file.exists() {
return Ok(None);
}
let content = fs::read_to_string(&self.lock_file).context(format!(
"Failed to read PID from lock file at {}",
self.lock_file.display()
))?;
let pid = content.trim().parse::<u32>().context(format!(
"Failed to parse PID '{}' from lock file at {}",
content.trim(),
self.lock_file.display()
))?;
Ok(Some(pid))
}
pub fn release(&mut self) -> Result<()> {
if self.lock_file.exists() {
fs::remove_file(&self.lock_file).context("Failed to remove lock file")?;
tracing::info!("Released process lock");
}
self.acquired = false;
Ok(())
}
#[cfg(unix)]
fn is_process_running(&self, pid: u32) -> bool {
use nix::sys::signal::kill;
use nix::unistd::Pid;
kill(Pid::from_raw(pid as i32), None).is_ok()
}
#[cfg(windows)]
fn is_process_running(&self, pid: u32) -> bool {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION};
unsafe {
let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
if handle == std::ptr::null_mut() {
return false;
}
CloseHandle(handle);
true
}
}
}
impl Drop for ProcessLock {
fn drop(&mut self) {
if self.acquired {
let _ = self.release();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_process_lock_acquire_release() {
let temp_dir = TempDir::new().unwrap();
let mut lock = ProcessLock::new(temp_dir.path());
assert!(lock.try_acquire().unwrap());
assert!(lock.acquired);
lock.release().unwrap();
assert!(!lock.acquired);
assert!(lock.try_acquire().unwrap());
}
#[test]
fn test_process_lock_double_acquire() {
let temp_dir = TempDir::new().unwrap();
let mut lock1 = ProcessLock::new(temp_dir.path());
let mut lock2 = ProcessLock::new(temp_dir.path());
assert!(lock1.try_acquire().unwrap());
assert!(!lock2.try_acquire().unwrap());
lock1.release().unwrap();
assert!(lock2.try_acquire().unwrap());
}
#[test]
fn test_process_lock_cleanup_on_drop() {
let temp_dir = TempDir::new().unwrap();
let lock_file = temp_dir.path().join("lore").join("pid");
{
let mut lock = ProcessLock::new(temp_dir.path());
lock.try_acquire().unwrap();
assert!(lock_file.exists());
}
assert!(!lock_file.exists());
}
#[test]
fn test_daemon_pid_write_and_read() {
let temp_dir = TempDir::new().unwrap();
let lock_file = temp_dir.path().join("lore").join("pid");
std::fs::create_dir_all(temp_dir.path().join("lore")).unwrap();
let mut lock = ProcessLock::new(temp_dir.path());
lock.write_daemon_pid(12345).unwrap();
assert!(lock_file.exists());
let content = std::fs::read_to_string(&lock_file).unwrap();
assert_eq!(content, "12345");
let read_pid = lock.read_daemon_pid().unwrap();
assert_eq!(read_pid, Some(12345));
}
#[test]
fn daemon_pid_survives_startup_guard_drop() {
let temp_dir = TempDir::new().unwrap();
let lock_file = temp_dir.path().join("lore").join("pid");
{
let mut lock = ProcessLock::new(temp_dir.path());
lock.try_acquire().unwrap();
lock.write_daemon_pid(12345).unwrap();
}
assert_eq!(std::fs::read_to_string(lock_file).unwrap(), "12345");
}
#[test]
fn test_read_daemon_pid_no_file() {
let temp_dir = TempDir::new().unwrap();
let lock = ProcessLock::new(temp_dir.path());
let pid = lock.read_daemon_pid().unwrap();
assert_eq!(pid, None);
}
#[test]
fn test_stale_lock_removal() {
let temp_dir = TempDir::new().unwrap();
let lock_file = temp_dir.path().join("lore").join("pid");
std::fs::create_dir_all(lock_file.parent().unwrap()).unwrap();
std::fs::write(&lock_file, "9999999").unwrap();
let mut lock = ProcessLock::new(temp_dir.path());
assert!(lock.try_acquire().unwrap());
let content = std::fs::read_to_string(&lock_file).unwrap();
assert_eq!(content.trim().parse::<u32>().unwrap(), std::process::id());
}
}