use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum PidFileError {
#[error("pid file I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("pid file contents are not a valid PID: {raw:?}")]
Parse {
raw: String,
},
#[error("no home directory could be resolved for the pid file path")]
NoHomeDir,
}
pub fn pid_file_path() -> Result<PathBuf, PidFileError> {
let home = dirs::home_dir().ok_or(PidFileError::NoHomeDir)?;
Ok(home.join(".aasm").join("gateway.pid"))
}
pub fn write_pid(path: &Path, pid: u32) -> Result<(), PidFileError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, format!("{pid}\n"))?;
Ok(())
}
pub fn read_pid(path: &Path) -> Result<Option<u32>, PidFileError> {
let raw = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e.into()),
};
let trimmed = raw.trim();
trimmed.parse::<u32>().map(Some).map_err(|_| PidFileError::Parse {
raw: trimmed.to_string(),
})
}
pub fn is_pid_alive(pid: u32) -> bool {
let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
rc == 0
}
pub fn remove_pid(path: &Path) -> Result<(), PidFileError> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pid_file_path_lives_under_aasm_directory() {
let path = pid_file_path().expect("home dir resolves in tests");
assert!(
path.ends_with(".aasm/gateway.pid"),
"expected suffix `.aasm/gateway.pid`, got {}",
path.display()
);
}
#[test]
fn write_pid_creates_missing_parent_directory() {
let tmp = tempfile::TempDir::new().unwrap();
let nested = tmp.path().join("layer-a").join("layer-b").join("gateway.pid");
assert!(!nested.parent().unwrap().exists());
write_pid(&nested, 42).expect("write_pid should mkdir -p the parent");
assert!(nested.exists(), "pid file should exist after write_pid");
}
#[test]
fn read_pid_returns_none_for_missing_file() {
let tmp = tempfile::TempDir::new().unwrap();
let absent = tmp.path().join("gateway.pid");
assert!(!absent.exists());
assert_eq!(read_pid(&absent).unwrap(), None);
}
#[test]
fn write_then_read_round_trip_preserves_pid() {
let tmp = tempfile::TempDir::new().unwrap();
let pid_file = tmp.path().join("gateway.pid");
write_pid(&pid_file, 13_579).unwrap();
assert_eq!(read_pid(&pid_file).unwrap(), Some(13_579));
}
#[test]
fn read_pid_returns_parse_error_on_garbage_contents() {
let tmp = tempfile::TempDir::new().unwrap();
let pid_file = tmp.path().join("gateway.pid");
std::fs::write(&pid_file, "not-a-pid\n").unwrap();
let err = read_pid(&pid_file).unwrap_err();
match err {
PidFileError::Parse { raw } => assert_eq!(raw, "not-a-pid"),
other => panic!("expected Parse error, got {other:?}"),
}
}
#[test]
fn remove_pid_is_idempotent_when_file_is_absent() {
let tmp = tempfile::TempDir::new().unwrap();
let pid_file = tmp.path().join("gateway.pid");
remove_pid(&pid_file).expect("first call should be a no-op");
remove_pid(&pid_file).expect("second call should also be a no-op");
}
#[test]
fn is_pid_alive_recognises_current_process_and_rejects_obvious_dead_pid() {
let self_pid = std::process::id();
assert!(is_pid_alive(self_pid), "self PID must be reported alive");
let unreachable = (libc::pid_t::MAX as u32).saturating_sub(1);
assert!(!is_pid_alive(unreachable), "PID {unreachable} should not be alive");
}
}