use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct PidFile {
path: PathBuf,
}
fn pid_file_in_use(path: &Path) -> Result<bool, io::Error> {
match std::fs::read_to_string(path) {
Ok(info) => {
let pid: libc::pid_t = info.trim().parse().map_err(|error| {
tracing::debug!(path=%path.display(), "Unable to parse PID file {path}: {error}", path = path.display());
io::Error::new(io::ErrorKind::InvalidData, "expected a PID")
})?;
#[allow(unsafe_code)]
let errno = unsafe { libc::kill(pid, 0) };
if errno == 0 {
tracing::debug!(%pid, "PID {pid} is still running", pid = pid);
return Ok(true);
}
if errno == -1 {
tracing::debug!(%pid, "Unkonwn error checking PID file: {errno}");
return Ok(false);
};
let error = io::Error::from_raw_os_error(errno);
match error.kind() {
io::ErrorKind::NotFound => Ok(false),
_ => Err(error),
}
}
Err(error) => match error.kind() {
io::ErrorKind::NotFound => Ok(false),
_ => Err(error),
},
}
}
impl PidFile {
pub fn new(path: impl Into<PathBuf>) -> Result<Self, io::Error> {
let path = path.into();
if path.exists() {
match pid_file_in_use(&path) {
Ok(true) => {
tracing::error!(path=%path.display(), "PID File {path} is already in use", path = path.display());
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!("PID File {path} is already in use", path = path.display()),
));
}
Ok(false) => {
tracing::debug!(path=%path.display(), "Removing stale PID file at {path}", path = path.display());
let _ = std::fs::remove_file(&path);
}
Err(error) if error.kind() == io::ErrorKind::InvalidData => {
tracing::warn!(path=%path.display(), "Removing invalid PID file at {path}", path = path.display());
let _ = std::fs::remove_file(&path);
}
Err(error) => {
tracing::error!(path=%path.display(), "Unable to check PID file {path}: {error}", path = path.display());
return Err(error);
}
}
}
#[allow(unsafe_code)]
let pid = unsafe { libc::getpid() };
if pid <= 0 {
tracing::error!("libc::getpid() returned a negative PID: {pid}");
return Err(io::Error::new(io::ErrorKind::Other, "negative PID"));
}
std::fs::write(&path, format!("{}", pid))?;
tracing::trace!(%pid, path=%path.display(), "Locked PID file at {path}", path = path.display());
Ok(Self { path })
}
pub fn is_locked(path: &Path) -> Result<bool, io::Error> {
match pid_file_in_use(path) {
Ok(true) => Ok(true),
Ok(false) => Ok(false),
Err(error) if error.kind() == io::ErrorKind::InvalidData => {
tracing::warn!(path=%path.display(), "Invalid PID file at {path}", path = path.display());
Ok(false)
}
Err(error) => {
tracing::error!(path=%path.display(), "Unable to check PID file {path}: {error}", path=path.display());
Err(error)
}
}
}
}
impl Drop for PidFile {
fn drop(&mut self) {
match std::fs::remove_file(&self.path) {
Ok(_) => {}
Err(error) => eprintln!(
"Encountered an error removing the PID file at {}: {}",
self.path.display(),
error
),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_pid_file() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pidfile-test.pid");
let pid_file = PidFile::new(path.clone()).unwrap();
assert!(PidFile::is_locked(&path).unwrap());
drop(pid_file);
assert!(!PidFile::is_locked(&path).unwrap());
}
#[test]
fn test_invalid_file() {
let path = Path::new("/tmp/pidfile-test.pid");
std::fs::write(path, "not a pid").unwrap();
tracing::subscriber::with_default(tracing::subscriber::NoSubscriber::new(), || {
assert!(
!PidFile::is_locked(path).unwrap(),
"Invalid file should not be locked."
)
});
assert!(
path.exists(),
"Invalid file should exist after checking for locks."
);
let pid_file =
tracing::subscriber::with_default(tracing::subscriber::NoSubscriber::new(), || {
PidFile::new(path).unwrap()
});
assert!(
PidFile::is_locked(path).unwrap(),
"PID file should be locked after creation."
);
drop(pid_file);
assert!(
!PidFile::is_locked(path).unwrap(),
"PID file should not be locked after drop."
);
}
}