1use std::io;
29use std::path::{Path, PathBuf};
30
31#[derive(Debug)]
36pub struct PidFile {
37 path: PathBuf,
38}
39
40fn pid_file_in_use(path: &Path) -> Result<bool, io::Error> {
45 match std::fs::read_to_string(path) {
46 Ok(info) => {
47 let pid: libc::pid_t = info.trim().parse().map_err(|error| {
48 tracing::debug!(path=%path.display(), "Unable to parse PID file {path}: {error}", path = path.display());
49 io::Error::new(io::ErrorKind::InvalidData, "expected a PID")
50 })?;
51
52 #[allow(unsafe_code)]
54 let errno = unsafe { libc::kill(pid, 0) };
55
56 if errno == 0 {
57 tracing::debug!(%pid, "PID {pid} is still running", pid = pid);
58 return Ok(true);
60 }
61
62 if errno == -1 {
63 tracing::debug!(%pid, "Unkonwn error checking PID file: {errno}");
64 return Ok(false);
65 };
66
67 let error = io::Error::from_raw_os_error(errno);
68 match error.kind() {
69 io::ErrorKind::NotFound => Ok(false),
70 _ => Err(error),
71 }
72 }
73 Err(error) => match error.kind() {
74 io::ErrorKind::NotFound => Ok(false),
75 _ => Err(error),
76 },
77 }
78}
79
80impl PidFile {
81 pub fn new(path: impl Into<PathBuf>) -> Result<Self, io::Error> {
88 let path = path.into();
89 if path.exists() {
90 match pid_file_in_use(&path) {
91 Ok(true) => {
92 tracing::error!(path=%path.display(), "PID File {path} is already in use", path = path.display());
93 return Err(io::Error::new(
94 io::ErrorKind::AddrInUse,
95 format!("PID File {path} is already in use", path = path.display()),
96 ));
97 }
98 Ok(false) => {
99 tracing::debug!(path=%path.display(), "Removing stale PID file at {path}", path = path.display());
100 let _ = std::fs::remove_file(&path);
101 }
102 Err(error) if error.kind() == io::ErrorKind::InvalidData => {
103 tracing::warn!(path=%path.display(), "Removing invalid PID file at {path}", path = path.display());
104 let _ = std::fs::remove_file(&path);
105 }
106 Err(error) => {
107 tracing::error!(path=%path.display(), "Unable to check PID file {path}: {error}", path = path.display());
108 return Err(error);
109 }
110 }
111 }
112
113 #[allow(unsafe_code)]
115 let pid = unsafe { libc::getpid() };
116
117 if pid <= 0 {
118 tracing::error!("libc::getpid() returned a negative PID: {pid}");
119 return Err(io::Error::new(io::ErrorKind::Other, "negative PID"));
120 }
121
122 std::fs::write(&path, format!("{}", pid))?;
123 tracing::trace!(%pid, path=%path.display(), "Locked PID file at {path}", path = path.display());
124
125 Ok(Self { path })
126 }
127
128 pub fn is_locked(path: &Path) -> Result<bool, io::Error> {
133 match pid_file_in_use(path) {
134 Ok(true) => Ok(true),
135 Ok(false) => Ok(false),
136 Err(error) if error.kind() == io::ErrorKind::InvalidData => {
137 tracing::warn!(path=%path.display(), "Invalid PID file at {path}", path = path.display());
138 Ok(false)
139 }
140 Err(error) => {
141 tracing::error!(path=%path.display(), "Unable to check PID file {path}: {error}", path=path.display());
142 Err(error)
143 }
144 }
145 }
146}
147
148impl Drop for PidFile {
149 fn drop(&mut self) {
150 match std::fs::remove_file(&self.path) {
151 Ok(_) => {}
152 Err(error) => eprintln!(
153 "Encountered an error removing the PID file at {}: {}",
154 self.path.display(),
155 error
156 ),
157 }
158 }
159}
160
161#[cfg(test)]
162mod test {
163 use super::*;
164
165 #[test]
166 fn test_pid_file() {
167 let tmp = tempfile::tempdir().unwrap();
168 let path = tmp.path().join("pidfile-test.pid");
169 let pid_file = PidFile::new(path.clone()).unwrap();
170 assert!(PidFile::is_locked(&path).unwrap());
171 drop(pid_file);
172 assert!(!PidFile::is_locked(&path).unwrap());
173 }
174
175 #[test]
176 fn test_invalid_file() {
177 let path = Path::new("/tmp/pidfile-test.pid");
178 std::fs::write(path, "not a pid").unwrap();
179 tracing::subscriber::with_default(tracing::subscriber::NoSubscriber::new(), || {
180 assert!(
181 !PidFile::is_locked(path).unwrap(),
182 "Invalid file should not be locked."
183 )
184 });
185 assert!(
186 path.exists(),
187 "Invalid file should exist after checking for locks."
188 );
189
190 let pid_file =
191 tracing::subscriber::with_default(tracing::subscriber::NoSubscriber::new(), || {
192 PidFile::new(path).unwrap()
193 });
194 assert!(
195 PidFile::is_locked(path).unwrap(),
196 "PID file should be locked after creation."
197 );
198 drop(pid_file);
199 assert!(
200 !PidFile::is_locked(path).unwrap(),
201 "PID file should not be locked after drop."
202 );
203 }
204}