diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5c552f5b..eac70c22 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -35,6 +35,7 @@ directories = "6.0.0" open = "5.3.2" ignore = "0.4" command-group = { version = "5.0", features = ["with-tokio"] } +nix = { version = "0.29", features = ["signal", "process"] } openssl-sys = { workspace = true } rmcp = { version = "0.1.5", features = ["server", "transport-io"] } schemars = "0.8" diff --git a/backend/src/app_state.rs b/backend/src/app_state.rs index e123e905..f5067b0e 100644 --- a/backend/src/app_state.rs +++ b/backend/src/app_state.rs @@ -1,5 +1,10 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, sync::Arc, time::Duration}; +#[cfg(unix)] +use nix::{ + sys::signal::{kill, Signal}, + unistd::Pid, +}; use tokio::sync::Mutex; use uuid::Uuid; @@ -96,7 +101,62 @@ impl AppState { let mut executions = self.running_executions.lock().await; if let Some(mut execution) = executions.remove(&execution_id) { - // Also kill the direct child process as fallback + // Graceful shutdown sequence: SIGTERM -> SIGKILL -> kill() + let process_id = execution.child.id(); + + #[cfg(unix)] + { + if let Some(pid) = process_id { + let pid = Pid::from_raw(pid as i32); + + // Step 1: Send SIGTERM for graceful shutdown + tracing::info!("Sending SIGTERM to execution process {}", execution_id); + if let Err(e) = kill(pid, Signal::SIGTERM) { + tracing::warn!("Failed to send SIGTERM to process {}: {}", execution_id, e); + } else { + // Wait 2 seconds for graceful shutdown + tokio::time::sleep(Duration::from_secs(2)).await; + + // Check if process is still running + if execution + .child + .try_wait() + .is_ok_and(|status| status.is_some()) + { + tracing::info!( + "Process {} exited gracefully after SIGTERM", + execution_id + ); + return Ok(true); + } + } + + // Step 2: Send SIGKILL for forceful termination + tracing::info!("Sending SIGKILL to execution process {}", execution_id); + if let Err(e) = kill(pid, Signal::SIGKILL) { + tracing::warn!("Failed to send SIGKILL to process {}: {}", execution_id, e); + } else { + // Wait 1 second for SIGKILL to take effect + tokio::time::sleep(Duration::from_secs(1)).await; + + // Check if process is still running + if execution + .child + .try_wait() + .is_ok_and(|status| status.is_some()) + { + tracing::info!("Process {} terminated after SIGKILL", execution_id); + return Ok(true); + } + } + } + } + + // Step 3: Fallback to kill() method + tracing::info!( + "Using fallback kill() for execution process {}", + execution_id + ); match execution.child.kill().await { Ok(_) => { tracing::info!(