use std::io;
use std::path::PathBuf;
pub fn pid_path() -> PathBuf {
if let Ok(dir) = std::env::var("AA_DATA_DIR") {
if !dir.is_empty() {
return PathBuf::from(dir).join("gateway.pid");
}
}
dirs::data_local_dir()
.expect("cannot determine local data directory")
.join("aasm")
.join("gateway.pid")
}
pub fn write_pid(pid: u32, listen: &str, started_at: &str) -> io::Result<()> {
let path = pid_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = format!("{pid}\n{listen}\n{started_at}\n");
std::fs::write(&path, content)
}
pub fn read_pid() -> Option<(u32, String, String)> {
let content = std::fs::read_to_string(pid_path()).ok()?;
let mut lines = content.lines();
let pid: u32 = lines.next()?.parse().ok()?;
let listen = lines.next()?.to_string();
let started_at = lines.next().unwrap_or("").to_string();
Some((pid, listen, started_at))
}
pub fn remove_pid() -> io::Result<()> {
let path = pid_path();
if path.exists() {
std::fs::remove_file(path)?;
}
Ok(())
}
pub fn is_process_alive(pid: u32) -> bool {
#[cfg(unix)]
{
let ret = unsafe { libc::kill(pid as libc::pid_t, 0) };
ret == 0
}
#[cfg(not(unix))]
{
let _ = pid;
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::MutexGuard;
struct EnvGuard {
_lock: MutexGuard<'static, ()>,
prior: Option<String>,
}
impl EnvGuard {
fn set(value: &str) -> Self {
let lock = crate::test_support::env_guard();
let prior = std::env::var("AA_DATA_DIR").ok();
std::env::set_var("AA_DATA_DIR", value);
Self { _lock: lock, prior }
}
fn unset() -> Self {
let lock = crate::test_support::env_guard();
let prior = std::env::var("AA_DATA_DIR").ok();
std::env::remove_var("AA_DATA_DIR");
Self { _lock: lock, prior }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match self.prior.take() {
Some(v) => std::env::set_var("AA_DATA_DIR", v),
None => std::env::remove_var("AA_DATA_DIR"),
}
}
}
#[test]
fn pid_path_honors_aa_data_dir_when_set() {
let _guard = EnvGuard::set("/tmp/aasm-gateway-pid-test");
assert_eq!(pid_path(), PathBuf::from("/tmp/aasm-gateway-pid-test/gateway.pid"));
}
#[test]
fn pid_path_falls_back_to_data_local_dir_when_unset() {
let _guard = EnvGuard::unset();
let path = pid_path();
assert!(
path.ends_with("aasm/gateway.pid"),
"default path should end with aasm/gateway.pid; got {path:?}"
);
}
#[test]
fn pid_path_falls_back_when_aa_data_dir_is_empty() {
let _guard = EnvGuard::set("");
let path = pid_path();
assert!(
path.ends_with("aasm/gateway.pid"),
"empty AA_DATA_DIR should fall through to data_local_dir; got {path:?}"
);
}
#[test]
fn write_and_read_pid_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
let _guard = EnvGuard::set(tmp.path().to_str().unwrap());
write_pid(99999, "127.0.0.1:50051", "2026-05-18T00:00:00Z").unwrap();
let result = read_pid();
assert!(result.is_some());
let (pid, listen, started_at) = result.unwrap();
assert_eq!(pid, 99999);
assert_eq!(listen, "127.0.0.1:50051");
assert_eq!(started_at, "2026-05-18T00:00:00Z");
}
#[test]
fn read_pid_returns_none_when_missing() {
let tmp = tempfile::tempdir().unwrap();
let _guard = EnvGuard::set(tmp.path().to_str().unwrap());
assert!(read_pid().is_none());
}
#[test]
fn remove_pid_is_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let _guard = EnvGuard::set(tmp.path().to_str().unwrap());
assert!(remove_pid().is_ok());
write_pid(1, "127.0.0.1:50051", "2026-05-18T00:00:00Z").unwrap();
assert!(remove_pid().is_ok());
assert!(read_pid().is_none());
}
#[test]
fn is_process_alive_returns_true_for_current_process() {
let pid = std::process::id();
assert!(is_process_alive(pid));
}
#[test]
fn is_process_alive_returns_false_for_dead_process() {
let mut child = std::process::Command::new("true")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("failed to spawn 'true'");
let pid = child.id();
child.wait().expect("wait failed");
assert!(!is_process_alive(pid));
}
}