#[derive(Debug, clap::Args)]
pub struct StopArgs {
#[arg(long, default_value_t = 30)]
pub timeout: u64,
}
pub fn send_signal(pid: u32, sig: libc::c_int) -> bool {
let rc = unsafe { libc::kill(pid as libc::pid_t, sig) };
rc == 0
}
pub fn wait_for_exit(pid: u32, deadline: std::time::Duration) -> bool {
let start = std::time::Instant::now();
let poll = std::time::Duration::from_millis(100);
loop {
if !super::pidfile::is_pid_alive(pid) {
return true;
}
if start.elapsed() >= deadline {
return false;
}
std::thread::sleep(poll);
}
}
pub fn run(args: StopArgs) -> std::process::ExitCode {
use std::process::ExitCode;
let pid_file = match super::pidfile::pid_file_path() {
Ok(p) => p,
Err(e) => {
eprintln!("aasm stop: {e}");
return ExitCode::FAILURE;
}
};
run_with_pid_file(args, &pid_file)
}
pub fn run_with_pid_file(args: StopArgs, pid_file: &std::path::Path) -> std::process::ExitCode {
use std::process::ExitCode;
let pid = match super::pidfile::read_pid(pid_file) {
Ok(Some(p)) => p,
Ok(None) => {
println!("No gateway running.");
return ExitCode::SUCCESS;
}
Err(e) => {
eprintln!("aasm stop: {e}");
return ExitCode::FAILURE;
}
};
if !super::pidfile::is_pid_alive(pid) {
let _ = super::pidfile::remove_pid(pid_file);
println!("No gateway running (stale pid file removed).");
return ExitCode::SUCCESS;
}
let timeout = std::time::Duration::from_secs(args.timeout);
if args.timeout > 0 {
let _ = send_signal(pid, libc::SIGTERM);
if !wait_for_exit(pid, timeout) {
eprintln!(
"aasm stop: gateway PID {pid} did not exit within {}s; escalating to SIGKILL",
args.timeout,
);
let _ = send_signal(pid, libc::SIGKILL);
let _ = wait_for_exit(pid, std::time::Duration::from_secs(2));
}
} else {
let _ = send_signal(pid, libc::SIGKILL);
let _ = wait_for_exit(pid, std::time::Duration::from_secs(2));
}
let _ = super::pidfile::remove_pid(pid_file);
println!("Gateway stopped (PID {pid}).");
ExitCode::SUCCESS
}
#[cfg(test)]
mod tests {
use super::*;
fn fmt(code: std::process::ExitCode) -> String {
format!("{code:?}")
}
#[test]
fn run_with_missing_pid_file_returns_success() {
let tmp = tempfile::TempDir::new().unwrap();
let pid_file = tmp.path().join("gateway.pid");
let exit = run_with_pid_file(StopArgs { timeout: 5 }, &pid_file);
assert_eq!(fmt(exit), fmt(std::process::ExitCode::SUCCESS));
}
#[test]
fn run_with_stale_pid_removes_file_and_returns_success() {
let tmp = tempfile::TempDir::new().unwrap();
let pid_file = tmp.path().join("gateway.pid");
let dead_pid = (libc::pid_t::MAX as u32).saturating_sub(1);
super::super::pidfile::write_pid(&pid_file, dead_pid).unwrap();
assert!(pid_file.exists());
let exit = run_with_pid_file(StopArgs { timeout: 5 }, &pid_file);
assert_eq!(fmt(exit), fmt(std::process::ExitCode::SUCCESS));
assert!(!pid_file.exists(), "stale pid file should be cleaned up");
}
#[test]
fn send_signal_to_self_with_signal_zero_returns_true() {
let self_pid = std::process::id();
assert!(send_signal(self_pid, 0));
}
#[test]
fn run_kills_real_child_and_removes_pid_file() {
let tmp = tempfile::TempDir::new().unwrap();
let pid_file = tmp.path().join("gateway.pid");
let mut child = std::process::Command::new("sleep")
.arg("60")
.spawn()
.expect("system `sleep` should be available");
let pid = child.id();
super::super::pidfile::write_pid(&pid_file, pid).unwrap();
let exit = run_with_pid_file(StopArgs { timeout: 1 }, &pid_file);
assert_eq!(fmt(exit), fmt(std::process::ExitCode::SUCCESS));
assert!(!pid_file.exists(), "pid file should be removed");
let _ = child.wait();
}
#[test]
fn run_with_timeout_zero_escalates_directly_to_sigkill() {
use std::os::unix::process::ExitStatusExt;
let tmp = tempfile::TempDir::new().unwrap();
let pid_file = tmp.path().join("gateway.pid");
let mut child = std::process::Command::new("sleep")
.arg("60")
.spawn()
.expect("system `sleep` should be available");
let pid = child.id();
super::super::pidfile::write_pid(&pid_file, pid).unwrap();
let exit = run_with_pid_file(StopArgs { timeout: 0 }, &pid_file);
assert_eq!(fmt(exit), fmt(std::process::ExitCode::SUCCESS));
assert!(!pid_file.exists(), "pid file should be removed");
let status = child.wait().unwrap();
assert_eq!(
status.signal(),
Some(libc::SIGKILL),
"child should have been killed by SIGKILL on --timeout 0",
);
}
}