diff options
| author | soryu <soryu@soryu.co> | 2026-01-26 22:41:18 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-26 22:41:30 +0000 |
| commit | de1eb0a923f6c5b768ac49f0425b7213a89301b7 (patch) | |
| tree | 410e886a638f91bc9d0e9bc7b6a6f1beead4e818 | |
| parent | d1f5dadb549d499c5aeee9cacf6c9aa0a233c198 (diff) | |
| download | soryu-de1eb0a923f6c5b768ac49f0425b7213a89301b7.tar.gz soryu-de1eb0a923f6c5b768ac49f0425b7213a89301b7.zip | |
Terminate all processes on makima CLI SIGTERM
| -rw-r--r-- | makima/src/daemon/process/claude.rs | 78 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 29 |
2 files changed, 62 insertions, 45 deletions
diff --git a/makima/src/daemon/process/claude.rs b/makima/src/daemon/process/claude.rs index f3aa421..c8add1c 100644 --- a/makima/src/daemon/process/claude.rs +++ b/makima/src/daemon/process/claude.rs @@ -117,19 +117,21 @@ impl ClaudeProcess { self.child.id() } - /// Send SIGTERM to gracefully terminate the process (Unix only). + /// Send SIGTERM to gracefully terminate the process and all its children (Unix only). /// Returns Ok(true) if signal was sent, Ok(false) if process already exited. + /// Sends signal to the entire process group (negative PID) to kill all children. #[cfg(unix)] pub fn terminate(&self) -> Result<bool, ClaudeProcessError> { - use nix::sys::signal::{kill, Signal}; + use nix::sys::signal::{killpg, Signal}; use nix::unistd::Pid; if let Some(pid) = self.child.id() { - match kill(Pid::from_raw(pid as i32), Signal::SIGTERM) { + // Kill the entire process group (the process is its own group leader) + match killpg(Pid::from_raw(pid as i32), Signal::SIGTERM) { Ok(()) => Ok(true), - Err(nix::errno::Errno::ESRCH) => Ok(false), // Process doesn't exist + Err(nix::errno::Errno::ESRCH) => Ok(false), // Process group doesn't exist Err(e) => Err(ClaudeProcessError::OutputRead(format!( - "Failed to send SIGTERM: {}", + "Failed to send SIGTERM to process group: {}", e ))), } @@ -552,27 +554,34 @@ impl ProcessManager { (self.claude_command.clone(), claude_args) }; - // Spawn the process - let mut child = Command::new(&command) - .args(&args) + // Spawn the process in its own process group so we can kill all children + let mut cmd = Command::new(&command); + cmd.args(&args) .current_dir(working_dir) .envs(env) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .kill_on_drop(true) - .spawn() - .map_err(|e| { - if e.kind() == std::io::ErrorKind::NotFound { - if use_bubblewrap { - ClaudeProcessError::BubblewrapNotFound - } else { - ClaudeProcessError::CommandNotFound(self.claude_command.clone()) - } + .kill_on_drop(true); + + // On Unix, create a new process group so we can kill all child processes + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + + let mut child = cmd.spawn().map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + if use_bubblewrap { + ClaudeProcessError::BubblewrapNotFound } else { - ClaudeProcessError::SpawnFailed(e) + ClaudeProcessError::CommandNotFound(self.claude_command.clone()) } - })?; + } else { + ClaudeProcessError::SpawnFailed(e) + } + })?; // Create output channel let (tx, rx) = mpsc::channel(1000); @@ -730,23 +739,30 @@ impl ProcessManager { tracing::debug!(args = ?args, "Claude continue command arguments"); - // Spawn the process - let mut child = Command::new(&self.claude_command) - .args(&args) + // Spawn the process in its own process group so we can kill all children + let mut cmd = Command::new(&self.claude_command); + cmd.args(&args) .current_dir(working_dir) .envs(env) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .kill_on_drop(true) - .spawn() - .map_err(|e| { - if e.kind() == std::io::ErrorKind::NotFound { - ClaudeProcessError::CommandNotFound(self.claude_command.clone()) - } else { - ClaudeProcessError::SpawnFailed(e) - } - })?; + .kill_on_drop(true); + + // On Unix, create a new process group so we can kill all child processes + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + + let mut child = cmd.spawn().map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + ClaudeProcessError::CommandNotFound(self.claude_command.clone()) + } else { + ClaudeProcessError::SpawnFailed(e) + } + })?; // Create output channel let (tx, rx) = mpsc::channel(1000); diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index bbcf661..b162f33 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -1412,13 +1412,14 @@ impl TaskManager { Ok(concurrency_key) } - /// Gracefully shutdown all running Claude processes. + /// Gracefully shutdown all running Claude processes and their children. /// - /// This sends SIGTERM to all active processes, waits for them to exit gracefully, + /// This sends SIGTERM to all active process groups, waits for them to exit gracefully, /// and then sends SIGKILL to any that don't exit within the timeout. + /// Uses process groups to ensure all child processes (bash commands, etc.) are also killed. #[cfg(unix)] pub async fn shutdown_all_processes(&self, timeout: std::time::Duration) { - use nix::sys::signal::{kill, Signal}; + use nix::sys::signal::{killpg, Signal}; use nix::unistd::Pid; let pids: Vec<(Uuid, u32)> = { @@ -1431,19 +1432,19 @@ impl TaskManager { return; } - tracing::info!(count = pids.len(), "Sending SIGTERM to all Claude processes"); + tracing::info!(count = pids.len(), "Sending SIGTERM to all Claude process groups"); - // Send SIGTERM to all processes + // Send SIGTERM to all process groups (each Claude process is its own group leader) for (task_id, pid) in &pids { - match kill(Pid::from_raw(*pid as i32), Signal::SIGTERM) { + match killpg(Pid::from_raw(*pid as i32), Signal::SIGTERM) { Ok(()) => { - tracing::debug!(task_id = %task_id, pid = pid, "Sent SIGTERM to process"); + tracing::debug!(task_id = %task_id, pid = pid, "Sent SIGTERM to process group"); } Err(nix::errno::Errno::ESRCH) => { - tracing::debug!(task_id = %task_id, pid = pid, "Process already exited"); + tracing::debug!(task_id = %task_id, pid = pid, "Process group already exited"); } Err(e) => { - tracing::warn!(task_id = %task_id, pid = pid, error = %e, "Failed to send SIGTERM"); + tracing::warn!(task_id = %task_id, pid = pid, error = %e, "Failed to send SIGTERM to process group"); } } } @@ -1466,7 +1467,7 @@ impl TaskManager { tokio::time::sleep(check_interval).await; } - // Send SIGKILL to any remaining processes + // Send SIGKILL to any remaining process groups let remaining: Vec<(Uuid, u32)> = { let guard = self.active_pids.read().await; guard.iter().map(|(k, v)| (*k, *v)).collect() @@ -1475,15 +1476,15 @@ impl TaskManager { if !remaining.is_empty() { tracing::warn!( count = remaining.len(), - "Some processes did not exit gracefully, sending SIGKILL" + "Some process groups did not exit gracefully, sending SIGKILL" ); for (task_id, pid) in &remaining { - match kill(Pid::from_raw(*pid as i32), Signal::SIGKILL) { + match killpg(Pid::from_raw(*pid as i32), Signal::SIGKILL) { Ok(()) => { - tracing::debug!(task_id = %task_id, pid = pid, "Sent SIGKILL to process"); + tracing::debug!(task_id = %task_id, pid = pid, "Sent SIGKILL to process group"); } Err(e) => { - tracing::warn!(task_id = %task_id, pid = pid, error = %e, "Failed to send SIGKILL"); + tracing::warn!(task_id = %task_id, pid = pid, error = %e, "Failed to send SIGKILL to process group"); } } } |
