summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-26 22:41:18 +0000
committersoryu <soryu@soryu.co>2026-01-26 22:41:30 +0000
commitde1eb0a923f6c5b768ac49f0425b7213a89301b7 (patch)
tree410e886a638f91bc9d0e9bc7b6a6f1beead4e818
parentd1f5dadb549d499c5aeee9cacf6c9aa0a233c198 (diff)
downloadsoryu-de1eb0a923f6c5b768ac49f0425b7213a89301b7.tar.gz
soryu-de1eb0a923f6c5b768ac49f0425b7213a89301b7.zip
Terminate all processes on makima CLI SIGTERM
-rw-r--r--makima/src/daemon/process/claude.rs78
-rw-r--r--makima/src/daemon/task/manager.rs29
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");
}
}
}