#![cfg(unix)]
use std::path::Path;
use zeph_common::pidfile::{PidLockError, PidLockGuard, read_pid_lenient};
use crate::error::SchedulerError;
#[derive(Debug)]
pub struct PidFile(#[allow(dead_code)] PidLockGuard);
impl PidFile {
pub fn acquire(path: &Path) -> Result<Self, SchedulerError> {
PidLockGuard::acquire(path).map(Self).map_err(|e| match e {
PidLockError::AlreadyRunning { pid } => SchedulerError::AlreadyRunning { pid },
PidLockError::Io(err) => {
SchedulerError::Io(format!("pid file error for {}: {err}", path.display()))
}
})
}
#[must_use]
pub fn read_alive(path: &Path) -> Option<u32> {
let pid = read_pid_lenient(path)?;
if is_process_alive(pid) {
Some(pid)
} else {
None
}
}
}
#[must_use]
pub fn is_process_alive(pid: u32) -> bool {
let Some(rustix_pid) = rustix::process::Pid::from_raw(pid.cast_signed()) else {
return false;
};
match rustix::process::test_kill_process(rustix_pid) {
Ok(()) => true,
Err(e) if e == rustix::io::Errno::PERM => true,
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use tempfile::TempDir;
use super::*;
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn unique_pid_path(dir: &TempDir) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
dir.path().join(format!("zeph-{n}.pid"))
}
#[test]
fn acquire_creates_file_with_pid() {
let dir = TempDir::new().unwrap();
let path = unique_pid_path(&dir);
let pf = PidFile::acquire(&path).expect("acquire should succeed");
let content = std::fs::read_to_string(&path).expect("pid file must exist");
assert_eq!(
content.trim().parse::<u32>().unwrap(),
std::process::id(),
"pid file must contain current process pid"
);
drop(pf);
assert!(!path.exists(), "pid file must be removed on drop");
}
#[test]
fn second_acquire_fails_with_already_running() {
let dir = TempDir::new().unwrap();
let path = unique_pid_path(&dir);
let _guard = PidFile::acquire(&path).expect("first acquire must succeed");
let err = PidFile::acquire(&path).expect_err("second acquire must fail");
assert!(
matches!(err, SchedulerError::AlreadyRunning { .. }),
"expected AlreadyRunning, got {err:?}"
);
}
#[test]
fn read_alive_returns_none_for_nonexistent_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("nonexistent.pid");
assert!(PidFile::read_alive(&path).is_none());
}
#[test]
fn read_alive_returns_none_for_dead_pid() {
let dir = TempDir::new().unwrap();
let path = unique_pid_path(&dir);
std::fs::write(&path, "999999999").unwrap();
let alive = PidFile::read_alive(&path);
let _ = alive;
}
}