1use flopen::OpenAndLock;
4use libc::{getpid, pid_t};
5use log::warn;
6use std::fs::{read_to_string, remove_file, File, Metadata, OpenOptions, Permissions};
7use std::io;
8use std::io::Write;
9use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
10use std::path::{Path, PathBuf};
11use thiserror::Error;
12
13#[derive(Debug)]
73pub struct Pidfile {
74 file: File,
75 path: PathBuf,
76 metadata: Metadata,
77 autoremove: bool,
78}
79
80#[derive(Error, Debug)]
81pub enum PidfileError {
82 #[error("daemon already running with {}", match .pid {
86 Some(pid) => format!("PID {pid}"),
87 None => "unknown PID".into()
88 })]
89 AlreadyRunning { pid: Option<pid_t> },
90 #[error(transparent)]
92 Io(#[from] io::Error),
93}
94
95impl Pidfile {
96 pub fn new(path: &Path, permissions: Permissions) -> Result<Pidfile, PidfileError> {
102 let file = OpenOptions::new()
103 .write(true)
104 .create(true)
105 .mode(permissions.mode())
106 .try_open_and_lock(path);
107 match file {
108 Ok(file) => {
109 file.set_len(0)?;
110 let metadata = file.metadata()?;
111 Ok(Pidfile {
112 file,
113 path: path.into(),
114 metadata,
115 autoremove: true,
116 })
117 }
118 Err(err) => {
119 if err.kind() == io::ErrorKind::WouldBlock {
120 Err(PidfileError::AlreadyRunning {
121 pid: Pidfile::read(path),
122 })
123 } else {
124 Err(PidfileError::Io(err))
125 }
126 }
127 }
128 }
129
130 fn read(path: &Path) -> Option<pid_t> {
131 read_to_string(path).ok()?.parse::<pid_t>().ok()
132 }
133
134 fn verify(&self) -> Result<(), PidfileError> {
135 let current_metadata = self.file.metadata()?;
136 if current_metadata.ino() == self.metadata.ino()
137 && current_metadata.dev() == self.metadata.dev()
138 {
139 Ok(())
140 } else {
141 Err(PidfileError::AlreadyRunning {
142 pid: Pidfile::read(&self.path),
143 })
144 }
145 }
146
147 pub fn write(&mut self) -> Result<(), PidfileError> {
151 self.file.set_len(0)?;
152 let pid = unsafe { getpid() };
153 write!(self.file, "{pid}")?;
154 self.file.sync_data()?;
155 Ok(())
156 }
157
158 pub fn close(mut self) {
164 if let Err(err) = self.verify() {
165 warn!("Failed to verify the PID file before closing: {err}");
166 }
167 self.autoremove = false
168 }
169}
170
171impl Drop for Pidfile {
172 fn drop(&mut self) {
174 if let Err(err) = self.verify() {
175 warn!("Failed to verify the PID file before closing: {err}");
176 } else if self.autoremove {
177 if let Err(err) = remove_file(&self.path) {
178 warn!("Failed to remove the PID file: {err}");
179 }
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use crate::*;
187 use std::fs::{read_to_string, Permissions};
188 use std::os::unix::fs::PermissionsExt;
189 use std::process;
190 use tempfile::tempdir;
191
192 #[test]
193 fn create_file() {
194 let dir = tempdir().unwrap();
195 let mut pidfile_path = dir.path().to_owned();
196 pidfile_path.push("file.pid");
197 let my_pid = process::id().to_string();
198 {
199 let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
200 .expect("Failed to create PID file");
201 println!("pidfile_path = {pidfile_path:?}");
202 assert!(pidfile_path.is_file());
203 pidfile.write().expect("Failed to write PID file");
204
205 let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
206 assert_eq!(my_pid, contents);
207 }
208
209 assert!(!pidfile_path.is_file(), "PID file should have disappeared");
210 }
211
212 #[test]
213 fn close_file() {
214 let dir = tempdir().unwrap();
215 let mut pidfile_path = dir.path().to_owned();
216 pidfile_path.push("file.pid");
217 let my_pid = process::id().to_string();
218 {
219 let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
220 .expect("Failed to create PID file");
221 println!("pidfile_path = {pidfile_path:?}");
222 assert!(pidfile_path.is_file());
223 pidfile.write().expect("Failed to write PID file");
224
225 let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
226 assert_eq!(my_pid, contents);
227
228 pidfile.close();
229 }
230
231 assert!(
232 pidfile_path.is_file(),
233 "PID file should have not disappeared"
234 );
235 }
236
237 #[test]
238 fn concurrent() {
239 let dir = tempdir().unwrap();
240 let mut pidfile_path = dir.path().to_owned();
241 pidfile_path.push("file.pid");
242 let my_pid = process::id().to_string();
243 let mut pidfile = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
244 .expect("Failed to create PID file");
245 println!("pidfile_path = {pidfile_path:?}");
246 assert!(pidfile_path.is_file(), "PID file not created?");
247 pidfile.write().expect("Failed to write PID file");
248
249 let contents = read_to_string(pidfile_path.as_path()).expect("Can’t read PID file");
250 assert_eq!(my_pid, contents);
251
252 let error = Pidfile::new(&pidfile_path, Permissions::from_mode(0o600))
253 .expect_err("Expected error, but got");
254 assert_eq!(
255 error.to_string(),
256 format!("daemon already running with PID {my_pid}")
257 );
258 if let PidfileError::AlreadyRunning { pid } = error {
259 assert_eq!(
260 my_pid,
261 pid.expect("No PID written?").to_string(),
262 "PID different?!"
263 );
264 } else {
265 panic!("unexpected error: {error:?}")
266 }
267 }
268}