use std::fs;
use std::path::{Path, PathBuf};
use crate::error::ServerError;
#[derive(Debug)]
pub struct PidGuard {
path: PathBuf,
}
impl PidGuard {
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for PidGuard {
fn drop(&mut self) {
if let Ok(contents) = fs::read_to_string(&self.path) {
if contents.trim().parse::<i32>().ok() == Some(std::process::id() as i32) {
let _ = fs::remove_file(&self.path);
}
}
}
}
pub fn acquire_pid_lock(path: &Path) -> Result<PidGuard, ServerError> {
if let Some(existing) = read_pid(path)? {
if process_is_alive(existing) {
return Err(ServerError::AlreadyRunning(existing));
}
fs::remove_file(path).map_err(|e| ServerError::Pid(format!("removing stale pid: {e}")))?;
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)
.map_err(|e| ServerError::Pid(format!("creating pid dir: {e}")))?;
}
}
let pid = std::process::id();
fs::write(path, pid.to_string())
.map_err(|e| ServerError::Pid(format!("writing pid file: {e}")))?;
Ok(PidGuard {
path: path.to_path_buf(),
})
}
fn read_pid(path: &Path) -> Result<Option<i32>, ServerError> {
match fs::read_to_string(path) {
Ok(contents) => Ok(contents.trim().parse::<i32>().ok()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ServerError::Pid(format!("reading pid file: {e}"))),
}
}
#[cfg(unix)]
fn process_is_alive(pid: i32) -> bool {
use nix::sys::signal::kill;
use nix::unistd::Pid;
match kill(Pid::from_raw(pid), None) {
Ok(()) => true,
Err(nix::errno::Errno::EPERM) => true,
Err(_) => false,
}
}
#[cfg(windows)]
fn process_is_alive(_pid: i32) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn acquires_when_no_pidfile() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("kindling.pid");
let guard = acquire_pid_lock(&path).expect("should acquire");
let written = fs::read_to_string(&path).unwrap();
assert_eq!(written.trim(), std::process::id().to_string());
drop(guard);
assert!(!path.exists(), "guard should remove pid file on drop");
}
#[test]
fn cleans_up_stale_pidfile() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("kindling.pid");
let dead_pid = i32::MAX;
{
let mut f = fs::File::create(&path).unwrap();
write!(f, "{dead_pid}").unwrap();
}
assert!(!process_is_alive(dead_pid));
let guard = acquire_pid_lock(&path).expect("stale pid must not block acquisition");
let written = fs::read_to_string(&path).unwrap();
assert_eq!(
written.trim(),
std::process::id().to_string(),
"pid file should be rewritten with the new (live) pid"
);
drop(guard);
}
#[test]
fn live_pidfile_blocks_acquisition() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("kindling.pid");
let me = std::process::id() as i32;
fs::write(&path, me.to_string()).unwrap();
let result = acquire_pid_lock(&path);
assert!(
matches!(result, Err(ServerError::AlreadyRunning(p)) if p == me),
"a live pidfile must block acquisition"
);
assert_eq!(fs::read_to_string(&path).unwrap().trim(), me.to_string());
}
}