use anyhow::{bail, Result};
use colored::Colorize;
use std::time::{Duration, Instant};
const TERM_GRACE: Duration = Duration::from_secs(5);
pub async fn handle_stop() -> Result<()> {
stop_daemons_in(&list_processes(), std::process::id(), TERM_GRACE)?;
cleanup_addr_file();
Ok(())
}
pub(crate) fn stop_daemons_in(procs: &[ProcInfo], me: u32, grace: Duration) -> Result<()> {
let targets = daemon_pids_in(procs, me);
if targets.is_empty() {
bail!("No daemon running");
}
println!(
"{} Stopping trusty-memory daemon ({} process(es): {:?})…",
"⟳".cyan(),
targets.len(),
targets
);
for pid in &targets {
let _ = send_signal(*pid, "TERM");
}
let deadline = Instant::now() + grace;
loop {
std::thread::sleep(Duration::from_millis(100));
if !targets.iter().any(|p| pid_alive(*p)) {
println!("{} Daemon stopped", "✓".green());
return Ok(());
}
if Instant::now() >= deadline {
break;
}
}
let stragglers: Vec<u32> = targets.iter().copied().filter(|p| pid_alive(*p)).collect();
if !stragglers.is_empty() {
println!(
"{} {} process(es) ignored SIGTERM — sending SIGKILL: {:?}",
"⚠".yellow(),
stragglers.len(),
stragglers
);
for pid in &stragglers {
let _ = send_signal(*pid, "KILL");
}
std::thread::sleep(Duration::from_millis(500));
}
let alive: Vec<u32> = targets.iter().copied().filter(|p| pid_alive(*p)).collect();
if !alive.is_empty() {
bail!(
"daemon still running after SIGKILL ({} process(es): {alive:?})",
alive.len()
);
}
println!("{} Daemon stopped", "✓".green());
Ok(())
}
fn cleanup_addr_file() {
if let Ok(dir) = trusty_common::resolve_data_dir("trusty-memory") {
let _ = std::fs::remove_file(dir.join("http_addr"));
}
}
#[derive(Debug, Clone)]
pub(crate) struct ProcInfo {
pub pid: u32,
pub name: String,
pub argv: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProcessRole {
Daemon,
StdioBridge,
Other,
}
pub(crate) fn classify_argv(argv: &[String]) -> ProcessRole {
let args = argv.get(1..).unwrap_or_default();
let Some(sub) = args.iter().position(|a| !a.starts_with('-')) else {
return ProcessRole::Other;
};
if args[sub] != "serve" {
return ProcessRole::Other;
}
let flags = &args[sub + 1..];
let has = |f: &str| flags.iter().any(|a| a == f);
if has("--stdio") {
ProcessRole::StdioBridge
} else if has("--foreground")
|| flags
.iter()
.any(|a| a == "--http" || a.starts_with("--http="))
{
ProcessRole::Daemon
} else {
ProcessRole::StdioBridge
}
}
pub(crate) fn daemon_pids_in(procs: &[ProcInfo], me: u32) -> Vec<u32> {
let mut out: Vec<u32> = procs
.iter()
.filter(|p| p.pid != me && p.name == "trusty-memory")
.filter(|p| classify_argv(&p.argv) == ProcessRole::Daemon)
.map(|p| p.pid)
.collect();
out.sort_unstable();
out
}
pub(crate) fn list_processes() -> Vec<ProcInfo> {
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
let mut sys = System::new();
sys.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing().with_cmd(UpdateKind::Always),
);
sys.processes()
.iter()
.map(|(pid, p)| ProcInfo {
pid: pid.as_u32(),
name: p.name().to_string_lossy().into_owned(),
argv: p
.cmd()
.iter()
.map(|a| a.to_string_lossy().into_owned())
.collect(),
})
.collect()
}
pub(crate) fn find_daemon_pids() -> Vec<u32> {
daemon_pids_in(&list_processes(), std::process::id())
}
#[cfg(unix)]
fn send_signal(pid: u32, sig: &str) -> std::io::Result<()> {
let status = std::process::Command::new("kill")
.arg(format!("-{sig}"))
.arg(pid.to_string())
.status()?;
if !status.success() {
return Err(std::io::Error::other(format!(
"kill -{sig} {pid} exited {status}"
)));
}
Ok(())
}
#[cfg(not(unix))]
fn send_signal(_pid: u32, _sig: &str) -> std::io::Result<()> {
Err(std::io::Error::other(
"signals unsupported on this platform",
))
}
#[cfg(unix)]
fn pid_alive(pid: u32) -> bool {
std::process::Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(not(unix))]
fn pid_alive(_pid: u32) -> bool {
true
}
#[cfg(test)]
#[path = "stop_tests.rs"]
mod tests;