fix(daemon): handle sigterm shutdown signal

Wait for either SIGINT or SIGTERM on Unix so daemon mode behaves correctly under container and process-manager termination flows.

Record signal-specific shutdown reasons and add unit tests for shutdown signal labeling.

Refs #2529
This commit is contained in:
argenis de la rosa 2026-03-02 13:32:32 -05:00
parent 02cf1a558a
commit 7bdf8eb609

View File

@ -8,6 +8,40 @@ use tokio::time::Duration;
const STATUS_FLUSH_SECONDS: u64 = 5;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShutdownSignal {
CtrlC,
SigTerm,
}
fn shutdown_reason(signal: ShutdownSignal) -> &'static str {
match signal {
ShutdownSignal::CtrlC => "shutdown requested (SIGINT)",
ShutdownSignal::SigTerm => "shutdown requested (SIGTERM)",
}
}
async fn wait_for_shutdown_signal() -> Result<ShutdownSignal> {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate())?;
tokio::select! {
ctrl_c = tokio::signal::ctrl_c() => {
ctrl_c?;
Ok(ShutdownSignal::CtrlC)
}
_ = sigterm.recv() => Ok(ShutdownSignal::SigTerm),
}
}
#[cfg(not(unix))]
{
tokio::signal::ctrl_c().await?;
Ok(ShutdownSignal::CtrlC)
}
}
pub async fn run(config: Config, host: String, port: u16) -> Result<()> {
// Pre-flight: check if port is already in use by another zeroclaw daemon
if let Err(_e) = check_port_available(&host, port).await {
@ -106,10 +140,10 @@ pub async fn run(config: Config, host: String, port: u16) -> Result<()> {
println!("🧠 ZeroClaw daemon started");
println!(" Gateway: http://{host}:{port}");
println!(" Components: gateway, channels, heartbeat, scheduler");
println!(" Ctrl+C to stop");
println!(" Ctrl+C or SIGTERM to stop");
tokio::signal::ctrl_c().await?;
crate::health::mark_component_error("daemon", "shutdown requested");
let signal = wait_for_shutdown_signal().await?;
crate::health::mark_component_error("daemon", shutdown_reason(signal));
for handle in &handles {
handle.abort();
@ -444,6 +478,22 @@ mod tests {
assert_eq!(path, tmp.path().join("daemon_state.json"));
}
#[test]
fn shutdown_reason_for_ctrl_c_mentions_sigint() {
assert_eq!(
shutdown_reason(ShutdownSignal::CtrlC),
"shutdown requested (SIGINT)"
);
}
#[test]
fn shutdown_reason_for_sigterm_mentions_sigterm() {
assert_eq!(
shutdown_reason(ShutdownSignal::SigTerm),
"shutdown requested (SIGTERM)"
);
}
#[tokio::test]
async fn supervisor_marks_error_and_restart_on_failure() {
let handle = spawn_component_supervisor("daemon-test-fail", 1, 1, || async {