1use libc::{c_char, c_int, mode_t, pid_t};
4use log::warn;
5use std::ffi::{CString, NulError};
6use std::fs::Permissions;
7use std::io;
8use std::os::unix::{ffi::OsStrExt, fs::PermissionsExt};
9use std::path::Path;
10use thiserror::Error;
11
12#[cfg(any(target_os = "dragonfly", target_os = "freebsd"))]
13use libc::{pidfh, pidfile_close, pidfile_open, pidfile_remove, pidfile_write};
14
15#[cfg(not(any(target_os = "dragonfly", target_os = "freebsd")))]
16extern "C" {
17 fn pidfile_open(path: *const c_char, mode: mode_t, pidptr: *mut pid_t) -> *mut pidfh;
18 fn pidfile_write(pfh: *mut pidfh) -> c_int;
19 fn pidfile_close(pfh: *mut pidfh) -> c_int;
20 fn pidfile_remove(pfh: *mut pidfh) -> c_int;
21}
22
23#[cfg(not(any(target_os = "dragonfly", target_os = "freebsd")))]
24#[allow(non_camel_case_types)]
25enum pidfh {}
26
27#[derive(Debug)]
87pub struct Pidfile {
88 pidfh: *mut pidfh,
89}
90
91#[derive(Error, Debug)]
92pub enum PidfileError {
93 #[error("daemon already running with {}", match .pid {
97 Some(pid) => format!("PID {pid}"),
98 None => "unknown PID".into()
99 })]
100 AlreadyRunning { pid: Option<pid_t> },
101 #[error(transparent)]
103 Io(#[from] io::Error),
104 #[error(transparent)]
106 NulError(#[from] NulError),
107}
108
109impl Pidfile {
110 pub fn new(path: &Path, permissions: Permissions) -> Result<Pidfile, PidfileError> {
116 let c_path = CString::new(path.as_os_str().as_bytes()).map_err(PidfileError::NulError)?;
117 let mut old_pid: pid_t = -1;
118 let pidfh =
119 unsafe { pidfile_open(c_path.as_ptr(), permissions.mode() as mode_t, &mut old_pid) };
120 if !pidfh.is_null() {
121 Ok(Pidfile { pidfh })
122 } else {
123 let err = io::Error::last_os_error();
124 if err.kind() == io::ErrorKind::AlreadyExists {
125 Err(PidfileError::AlreadyRunning {
126 pid: if old_pid != -1 { Some(old_pid) } else { None },
127 })
128 } else {
129 Err(PidfileError::Io(err))
130 }
131 }
132 }
133
134 pub fn write(&mut self) -> Result<(), PidfileError> {
138 if unsafe { pidfile_write(self.pidfh) == 0 } {
139 Ok(())
140 } else {
141 Err(PidfileError::Io(io::Error::last_os_error()))
142 }
143 }
144
145 pub fn close(mut self) {
151 if unsafe { pidfile_close(self.pidfh) != 0 } {
152 let err = io::Error::last_os_error();
153 warn!("Failed to close the PID file: {err}");
154 }
155 self.pidfh = std::ptr::null_mut();
156 }
157}
158
159impl Drop for Pidfile {
160 fn drop(&mut self) {
162 if unsafe { pidfile_remove(self.pidfh) != 0 } {
163 let err = io::Error::last_os_error();
164 warn!("Failed to remove the PID file: {err}");
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use crate::*;
172 use std::fs::{read_to_string, Permissions};
173 use std::io;
174 use std::os::unix::fs::PermissionsExt;
175 use std::process;
176 use tempfile::tempdir;
177
178 #[test]
179 fn create_file() {
180 let dir = tempdir().unwrap();
181 let mut pidfile_path = dir.path().to_owned();
182 pidfile_path.push("file.pid");
183 let my_pid = process::id().to_string();
184 {
185 let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
186 .expect("Failed to create PID file");
187 println!("pidfile_path = {pidfile_path:?}");
188 assert!(pidfile_path.is_file());
189 pidfile.write().expect("Failed to write PID file");
190
191 let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
192 assert_eq!(my_pid, contents);
193 }
194
195 assert!(!pidfile_path.is_file(), "PID file should have disappeared");
196 }
197
198 #[test]
199 fn close_file() {
200 let dir = tempdir().unwrap();
201 let mut pidfile_path = dir.path().to_owned();
202 pidfile_path.push("file.pid");
203 let my_pid = process::id().to_string();
204 {
205 let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
206 .expect("Failed to create PID file");
207 println!("pidfile_path = {pidfile_path:?}");
208 assert!(pidfile_path.is_file());
209 pidfile.write().expect("Failed to write PID file");
210
211 let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
212 assert_eq!(my_pid, contents);
213
214 pidfile.close();
215 }
216
217 assert!(
218 pidfile_path.is_file(),
219 "PID file should have not disappeared"
220 );
221 }
222
223 #[test]
224 fn invalid_path() {
225 let dir = tempdir().unwrap();
226 let mut pidfile_path = dir.path().to_owned();
227 pidfile_path.push("<<non-existing>>");
228 pidfile_path.push("file.pid");
229 let error = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
230 .expect_err("PID file shouldn’t exist, but it does");
231 println!("pidfile_path = {pidfile_path:?}");
232 assert!(!pidfile_path.is_file());
233 if let PidfileError::Io(error) = error {
234 assert_eq!(error.kind(), io::ErrorKind::NotFound);
235 } else {
236 panic!("unexpected error: {:?}", error)
237 }
238
239 pidfile_path.push("\0");
240 let error = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
241 .expect_err("NULs should not have been accepted, but they were");
242 if let PidfileError::NulError(error) = error {
243 println!("expected error: {error}");
244 } else {
245 panic!("unexpected error: {:?}", error)
246 }
247 }
248
249 #[test]
250 fn concurrent() {
251 let dir = tempdir().unwrap();
252 let mut pidfile_path = dir.path().to_owned();
253 pidfile_path.push("file.pid");
254 let my_pid = process::id().to_string();
255 let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
256 .expect("Failed to create PID file");
257 println!("pidfile_path = {pidfile_path:?}");
258 assert!(pidfile_path.is_file(), "PID file not created?");
259 pidfile.write().expect("Failed to write PID file");
260
261 let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
262 assert_eq!(my_pid, contents);
263
264 let error = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
265 .expect_err("Expected error, but got");
266 assert_eq!(
267 error.to_string(),
268 format!("daemon already running with PID {my_pid}")
269 );
270 if let PidfileError::AlreadyRunning { pid } = error {
271 assert_eq!(
272 my_pid,
273 pid.expect("No PID written?").to_string(),
274 "PID different?!"
275 );
276 } else {
277 panic!("unexpected error: {:?}", error)
278 }
279 }
280}