1use std::fs;
19use std::io::{self, Write};
20use std::path::{Path, PathBuf};
21
22#[derive(Debug)]
23pub enum LockError {
24 HeldBy {
26 pid: u32,
27 path: PathBuf,
28 },
29 Io(io::Error),
30}
31
32impl From<io::Error> for LockError {
33 fn from(e: io::Error) -> Self {
34 Self::Io(e)
35 }
36}
37
38impl std::fmt::Display for LockError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 Self::HeldBy { pid, path } => {
42 write!(f, "pidfile {} held by live process {pid}", path.display())
43 }
44 Self::Io(e) => write!(f, "pidfile io: {e}"),
45 }
46 }
47}
48
49impl std::error::Error for LockError {}
50
51#[derive(Debug)]
57pub struct PidFile {
58 path: PathBuf,
59 pid: u32,
60 released: bool,
63}
64
65impl PidFile {
66 pub fn acquire(path: impl Into<PathBuf>) -> Result<Self, LockError> {
73 let path = path.into();
74 if let Some(parent) = path.parent() {
75 fs::create_dir_all(parent)?;
76 }
77
78 if let Some(existing) = read_pidfile(&path) {
79 if is_process_alive(existing) && existing != std::process::id() {
80 return Err(LockError::HeldBy {
81 pid: existing,
82 path,
83 });
84 }
85 }
87
88 let pid = std::process::id();
89 let temp = path.with_extension("pid.tmp");
92 {
93 let mut f = fs::File::create(&temp)?;
94 writeln!(f, "{pid}")?;
95 f.sync_all()?;
96 }
97 fs::rename(&temp, &path)?;
98
99 Ok(Self {
100 path,
101 pid,
102 released: false,
103 })
104 }
105
106 pub fn path(&self) -> &Path {
107 &self.path
108 }
109
110 pub fn pid(&self) -> u32 {
111 self.pid
112 }
113
114 pub fn release(mut self) -> io::Result<()> {
117 self.released = true;
118 remove_if_ours(&self.path, self.pid)
119 }
120}
121
122impl Drop for PidFile {
123 fn drop(&mut self) {
124 if self.released {
125 return;
126 }
127 let _ = remove_if_ours(&self.path, self.pid);
130 }
131}
132
133pub fn read_pidfile(path: &Path) -> Option<u32> {
135 let raw = fs::read_to_string(path).ok()?;
136 raw.trim().parse::<u32>().ok()
137}
138
139pub fn is_process_alive(pid: u32) -> bool {
145 if pid == 0 {
146 return false;
147 }
148 let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
153 if rc == 0 {
154 return true;
155 }
156 match io::Error::last_os_error().raw_os_error() {
157 Some(libc::EPERM) => true, _ => false,
159 }
160}
161
162fn remove_if_ours(path: &Path, our_pid: u32) -> io::Result<()> {
163 match read_pidfile(path) {
166 Some(pid) if pid == our_pid => fs::remove_file(path),
167 _ => Ok(()),
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use std::sync::atomic::{AtomicUsize, Ordering};
177 use std::time::{SystemTime, UNIX_EPOCH};
178
179 fn unique_tmp(label: &str) -> PathBuf {
180 static COUNTER: AtomicUsize = AtomicUsize::new(0);
181 let nanos = SystemTime::now()
182 .duration_since(UNIX_EPOCH)
183 .unwrap()
184 .as_nanos();
185 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
186 std::env::temp_dir().join(format!("ao-rs-lock-{label}-{nanos}-{n}.pid"))
187 }
188
189 #[test]
190 fn acquire_when_no_file_writes_our_pid() {
191 let path = unique_tmp("fresh");
192 let lock = PidFile::acquire(&path).unwrap();
193 assert!(path.exists());
194 assert_eq!(read_pidfile(&path), Some(std::process::id()));
195 assert_eq!(lock.pid(), std::process::id());
196 drop(lock);
197 assert!(!path.exists(), "drop should remove the pidfile");
198 }
199
200 #[test]
201 fn acquire_replaces_stale_pidfile() {
202 let stale_pid: u32 = 999_999;
206 assert!(!is_process_alive(stale_pid), "sanity: {stale_pid} is dead");
207
208 let path = unique_tmp("stale");
209 fs::create_dir_all(path.parent().unwrap()).unwrap();
210 fs::write(&path, format!("{stale_pid}\n")).unwrap();
211
212 let lock = PidFile::acquire(&path).unwrap();
213 assert_eq!(read_pidfile(&path), Some(std::process::id()));
214 drop(lock);
215 }
216
217 #[test]
218 fn acquire_rejects_live_other_pid() {
219 let path = unique_tmp("held");
222 fs::create_dir_all(path.parent().unwrap()).unwrap();
223 fs::write(&path, "1\n").unwrap();
224
225 match PidFile::acquire(&path) {
226 Err(LockError::HeldBy { pid, .. }) => assert_eq!(pid, 1),
227 other => panic!("expected HeldBy(1), got {other:?}"),
228 }
229 assert_eq!(read_pidfile(&path), Some(1));
231 fs::remove_file(&path).ok();
232 }
233
234 #[test]
235 fn drop_does_not_remove_file_if_stolen() {
236 let path = unique_tmp("stolen");
237 let lock = PidFile::acquire(&path).unwrap();
238
239 fs::write(&path, "1\n").unwrap();
241
242 drop(lock);
243
244 assert_eq!(read_pidfile(&path), Some(1));
246 fs::remove_file(&path).ok();
247 }
248
249 #[test]
250 fn is_process_alive_returns_true_for_self() {
251 assert!(is_process_alive(std::process::id()));
252 }
253
254 #[test]
255 fn is_process_alive_returns_false_for_zero() {
256 assert!(!is_process_alive(0));
257 }
258}