use std::{
fs::{self, File},
io::{self, Read, Seek, SeekFrom, Write},
num::ParseIntError,
path::{Path, PathBuf},
process,
};
use fs2::FileExt;
use thiserror::Error;
use tracing::warn;
#[derive(Debug)]
pub(crate) struct PidFile {
_pidfile: File,
path: PathBuf,
previous: Option<u32>,
}
#[derive(Debug, Error)]
pub(crate) enum PidFileError {
#[error("could not open pidfile: {0}")]
CouldNotOpen(#[source] io::Error),
#[error("could not lock pidfile: {0}")]
LockFailed(#[source] io::Error),
#[error("reading existing pidfile failed: {0}")]
ReadFailed(#[source] io::Error),
#[error("updating pidfile failed: {0}")]
WriteFailed(#[source] io::Error),
#[error("corrupt pidfile")]
Corrupted(ParseIntError),
}
#[must_use]
#[derive(Debug)]
pub(crate) enum PidFileOutcome {
AnotherNodeRunning(PidFileError),
Crashed(PidFile),
Clean(PidFile),
PidFileError(PidFileError),
}
impl PidFile {
pub(crate) fn acquire<P: AsRef<Path>>(path: P) -> PidFileOutcome {
match PidFile::new(path) {
Ok(pidfile) => {
if pidfile.unclean_shutdown() {
PidFileOutcome::Crashed(pidfile)
} else {
PidFileOutcome::Clean(pidfile)
}
}
Err(err @ PidFileError::LockFailed(_)) => PidFileOutcome::AnotherNodeRunning(err),
Err(err) => PidFileOutcome::PidFileError(err),
}
}
fn new<P: AsRef<Path>>(path: P) -> Result<PidFile, PidFileError> {
let mut pidfile = fs::OpenOptions::new()
.truncate(false)
.create(true)
.read(true)
.write(true)
.open(path.as_ref())
.map_err(PidFileError::CouldNotOpen)?;
pidfile
.try_lock_exclusive()
.map_err(PidFileError::LockFailed)?;
let mut raw_contents = String::new();
pidfile
.read_to_string(&mut raw_contents)
.map_err(PidFileError::ReadFailed)?;
let previous = if raw_contents.is_empty() {
None
} else {
Some(raw_contents.parse().map_err(PidFileError::Corrupted)?)
};
let pid = process::id();
pidfile.set_len(0).map_err(PidFileError::WriteFailed)?;
pidfile
.seek(SeekFrom::Start(0))
.map_err(PidFileError::WriteFailed)?;
pidfile
.write_all(pid.to_string().as_bytes())
.map_err(PidFileError::WriteFailed)?;
pidfile.flush().map_err(PidFileError::WriteFailed)?;
Ok(PidFile {
_pidfile: pidfile,
path: path.as_ref().to_owned(),
previous,
})
}
fn unclean_shutdown(&self) -> bool {
self.previous.is_some()
}
}
impl Drop for PidFile {
fn drop(&mut self) {
if let Err(err) = fs::remove_file(&self.path) {
warn!(path=%self.path.display(), %err, "could not delete pidfile");
}
}
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::TempDir;
use super::{PidFile, PidFileOutcome};
#[test]
fn pidfile_creates_file_and_cleans_it_up() {
let tmp_dir = TempDir::new().expect("could not create tmp_dir");
let pidfile_path = tmp_dir.path().join("create_and_cleanup.pid");
let outcome = PidFile::acquire(&pidfile_path);
match outcome {
PidFileOutcome::Clean(pidfile) => {
assert!(pidfile_path.exists());
drop(pidfile);
assert!(!pidfile_path.exists());
}
other => panic!("pidfile outcome not clean, but {:?}", other),
}
}
#[test]
fn detects_unclean_shutdown() {
let tmp_dir = TempDir::new().expect("could not create tmp_dir");
let pidfile_path = tmp_dir.path().join("create_and_cleanup.pid");
fs::write(&pidfile_path, b"12345").expect("could not write garbage pid file");
let outcome = PidFile::acquire(&pidfile_path);
match outcome {
PidFileOutcome::Crashed(pidfile) => {
assert_eq!(pidfile.previous, Some(12345));
assert!(pidfile_path.exists());
drop(pidfile);
assert!(!pidfile_path.exists());
}
other => panic!("pidfile outcome did not detect crash, is {:?}", other),
}
}
#[test]
fn blocks_second_instance() {
let tmp_dir = TempDir::new().expect("could not create tmp_dir");
let pidfile_path = tmp_dir.path().join("create_and_cleanup.pid");
let outcome = PidFile::acquire(&pidfile_path);
match outcome {
PidFileOutcome::Clean(_pidfile) => {
match PidFile::acquire(&pidfile_path) {
PidFileOutcome::AnotherNodeRunning(_) => {
}
other => panic!(
"expected detection of duplicate pidfile access, instead got: {:?}",
other
),
}
}
other => panic!("pidfile outcome not clean, but {:?}", other),
}
}
}