1use std::fs;
14use std::path::{Path, PathBuf};
15
16use crate::error::ServerError;
17
18#[derive(Debug)]
21pub struct PidGuard {
22 path: PathBuf,
23}
24
25impl PidGuard {
26 pub fn path(&self) -> &Path {
28 &self.path
29 }
30}
31
32impl Drop for PidGuard {
33 fn drop(&mut self) {
34 if let Ok(contents) = fs::read_to_string(&self.path) {
37 if contents.trim().parse::<i32>().ok() == Some(std::process::id() as i32) {
38 let _ = fs::remove_file(&self.path);
39 }
40 }
41 }
42}
43
44pub fn acquire_pid_lock(path: &Path) -> Result<PidGuard, ServerError> {
49 if let Some(existing) = read_pid(path)? {
50 if process_is_alive(existing) {
51 return Err(ServerError::AlreadyRunning(existing));
52 }
53 fs::remove_file(path).map_err(|e| ServerError::Pid(format!("removing stale pid: {e}")))?;
55 }
56
57 if let Some(parent) = path.parent() {
58 if !parent.as_os_str().is_empty() {
59 fs::create_dir_all(parent)
60 .map_err(|e| ServerError::Pid(format!("creating pid dir: {e}")))?;
61 }
62 }
63
64 let pid = std::process::id();
65 fs::write(path, pid.to_string())
66 .map_err(|e| ServerError::Pid(format!("writing pid file: {e}")))?;
67
68 Ok(PidGuard {
69 path: path.to_path_buf(),
70 })
71}
72
73fn read_pid(path: &Path) -> Result<Option<i32>, ServerError> {
79 match fs::read_to_string(path) {
80 Ok(contents) => Ok(contents.trim().parse::<i32>().ok()),
81 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
82 Err(e) => Err(ServerError::Pid(format!("reading pid file: {e}"))),
83 }
84}
85
86#[cfg(unix)]
88fn process_is_alive(pid: i32) -> bool {
89 use nix::sys::signal::kill;
90 use nix::unistd::Pid;
91 match kill(Pid::from_raw(pid), None) {
95 Ok(()) => true,
96 Err(nix::errno::Errno::EPERM) => true,
97 Err(_) => false,
98 }
99}
100
101#[cfg(windows)]
102fn process_is_alive(_pid: i32) -> bool {
103 false
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use std::io::Write;
112
113 #[test]
114 fn acquires_when_no_pidfile() {
115 let dir = tempfile::tempdir().unwrap();
116 let path = dir.path().join("kindling.pid");
117 let guard = acquire_pid_lock(&path).expect("should acquire");
118 let written = fs::read_to_string(&path).unwrap();
119 assert_eq!(written.trim(), std::process::id().to_string());
120 drop(guard);
121 assert!(!path.exists(), "guard should remove pid file on drop");
122 }
123
124 #[test]
125 fn cleans_up_stale_pidfile() {
126 let dir = tempfile::tempdir().unwrap();
127 let path = dir.path().join("kindling.pid");
128
129 let dead_pid = i32::MAX;
132 {
133 let mut f = fs::File::create(&path).unwrap();
134 write!(f, "{dead_pid}").unwrap();
135 }
136 assert!(!process_is_alive(dead_pid));
137
138 let guard = acquire_pid_lock(&path).expect("stale pid must not block acquisition");
139 let written = fs::read_to_string(&path).unwrap();
140 assert_eq!(
141 written.trim(),
142 std::process::id().to_string(),
143 "pid file should be rewritten with the new (live) pid"
144 );
145 drop(guard);
146 }
147
148 #[test]
149 fn live_pidfile_blocks_acquisition() {
150 let dir = tempfile::tempdir().unwrap();
151 let path = dir.path().join("kindling.pid");
152
153 let me = std::process::id() as i32;
155 fs::write(&path, me.to_string()).unwrap();
156
157 let result = acquire_pid_lock(&path);
158 assert!(
159 matches!(result, Err(ServerError::AlreadyRunning(p)) if p == me),
160 "a live pidfile must block acquisition"
161 );
162 assert_eq!(fs::read_to_string(&path).unwrap().trim(), me.to_string());
164 }
165}