use std::fs;
use std::io;
use std::path::PathBuf;
use std::process;
pub struct PidFile {
path: PathBuf,
}
impl PidFile {
pub fn create(path: &std::path::Path) -> io::Result<Self> {
Self::new(path.to_path_buf())
}
pub fn new(path: PathBuf) -> io::Result<Self> {
if path.exists() {
let pid_str = fs::read_to_string(&path)?;
if let Ok(pid) = pid_str.trim().parse::<u32>() {
#[cfg(unix)]
{
use nix::sys::signal::kill;
use nix::unistd::Pid;
let result = kill(Pid::from_raw(pid as i32), None);
if result.is_ok() {
use nix::sys::signal::Signal;
let _ = kill(Pid::from_raw(pid as i32), Some(Signal::SIGTERM));
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
fs::remove_file(&path)?;
}
let current_pid = process::id();
fs::write(&path, current_pid.to_string())?;
Ok(PidFile { path })
}
}
impl Drop for PidFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_pid_file_creation() {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join("test.pid");
let _pid_file = PidFile::new(pid_path.clone()).expect("operation should succeed in test");
assert!(pid_path.exists(), "PID file should be created");
let contents = fs::read_to_string(&pid_path).expect("operation should succeed in test");
let expected = process::id().to_string();
assert_eq!(
contents, expected,
"PID file should contain current process ID"
);
}
#[test]
fn test_pid_file_cleanup() {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join("test.pid");
{
let _pid_file =
PidFile::new(pid_path.clone()).expect("operation should succeed in test");
assert!(pid_path.exists(), "PID file should exist while in scope");
}
assert!(!pid_path.exists(), "PID file should be removed after drop");
}
#[test]
fn test_pid_file_replaces_stale() {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join("test.pid");
fs::write(&pid_path, "999999").expect("operation should succeed in test");
let _pid_file = PidFile::new(pid_path.clone()).expect("operation should succeed in test");
let contents = fs::read_to_string(&pid_path).expect("operation should succeed in test");
let expected = process::id().to_string();
assert_eq!(
contents, expected,
"Stale PID should be replaced with current process ID"
);
}
#[test]
fn test_pid_file_kills_running_process() {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join("test.pid");
let mut child = std::process::Command::new("sleep")
.arg("10")
.spawn()
.expect("operation should succeed in test");
let child_pid = child.id();
fs::write(&pid_path, child_pid.to_string()).expect("operation should succeed in test");
let _pid_file = PidFile::new(pid_path).expect("operation should succeed in test");
thread::sleep(Duration::from_millis(100));
let _ = child.wait();
#[cfg(unix)]
{
use nix::sys::signal::kill;
use nix::unistd::Pid;
let result = kill(Pid::from_raw(child_pid as i32), None);
assert!(
result.is_err(),
"Old process should be killed before creating new PID file"
);
}
}
#[test]
fn prop_pid_file_always_valid() {
use proptest::prelude::*;
proptest!(|(seed in any::<u32>())| {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join(format!("test_{seed}.pid"));
let _pid_file = PidFile::new(pid_path.clone()).expect("operation should succeed in test");
prop_assert!(pid_path.exists());
let contents = fs::read_to_string(&pid_path).expect("operation should succeed in test");
let pid: u32 = contents.parse().expect("operation should succeed in test");
prop_assert_eq!(pid, process::id());
});
}
#[test]
fn test_pid_file_create() {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join("create_test.pid");
let _pid_file = PidFile::create(&pid_path).expect("operation should succeed in test");
assert!(pid_path.exists(), "PID file should be created via create()");
let contents = fs::read_to_string(&pid_path).expect("operation should succeed in test");
assert_eq!(contents, process::id().to_string());
}
#[test]
fn test_pid_file_with_invalid_pid_content() {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join("invalid.pid");
fs::write(&pid_path, "not_a_number").expect("operation should succeed in test");
let _pid_file = PidFile::new(pid_path.clone()).expect("operation should succeed in test");
let contents = fs::read_to_string(&pid_path).expect("operation should succeed in test");
assert_eq!(
contents,
process::id().to_string(),
"Invalid PID should be replaced"
);
}
#[test]
fn test_pid_file_multiple_creates() {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join("multi.pid");
{
let _pid1 = PidFile::new(pid_path.clone()).expect("operation should succeed in test");
assert!(pid_path.exists());
}
{
let _pid2 = PidFile::new(pid_path.clone()).expect("operation should succeed in test");
assert!(pid_path.exists());
}
assert!(!pid_path.exists(), "PID file should be cleaned up");
}
#[test]
fn test_pid_file_empty_file() {
let temp_dir = tempfile::tempdir().expect("operation should succeed in test");
let pid_path = temp_dir.path().join("empty.pid");
fs::write(&pid_path, "").expect("operation should succeed in test");
let _pid_file = PidFile::new(pid_path.clone()).expect("operation should succeed in test");
let contents = fs::read_to_string(&pid_path).expect("operation should succeed in test");
assert_eq!(
contents,
process::id().to_string(),
"Empty PID file should be replaced"
);
}
}
pub mod watcher;