[Windows] Process tree not killed when future is dropped in run_windows_fallback
[Windows] Process tree not killed when future is dropped in run_windows_fallback
Summary
On Windows, when a user cancels a Bash tool execution (causing the future to be dropped), child processes (cmd.exe and spawned scripts like python.exe) continue running in the background.
Root Cause
run_windows_fallback() in src-rust/crates/tools/src/pty_bash.rs (lines 559-629) lacks a process cleanup guard:
- Unix/macOS: Uses
PtyKillGuard(lines 331-364) which implementsDropto automatically kill child processes when the future is dropped - Windows: Uses
run_windows_fallbackwith no guard mechanism - When the future is dropped (user cancellation), there's no cleanup logic to terminate the process tree
Current Behavior
Windows:
cmd.exe (PID 22492)
└── python.exe (PID 18412) ← Both continue running after cancellationUnix/macOS: ✅ Both processes are killed automatically via PtyKillGuard::drop()
Expected Behavior
Windows should have equivalent cleanup behavior as Unix:
- When the future is dropped, all child processes should be terminated
- Use
taskkill /PID <pid> /T /Fto recursively kill the process tree on Windows
Reproduction Steps
- Run a long-running command on Windows:
let result = run_tool("bash", json!({ "command": "python -c \"import time; time.sleep(300)\"" })).await; - Cancel/abort the future mid-execution (drop the future)
- Check Task Manager →
cmd.exeandpython.exeare still running
Observed in Real Application
This issue was discovered in a Tauri desktop application on Windows 10/11:
- User clicks "Stop" button during AI script execution
- Frontend stops, but
cmd.exeand childpython.execontinue running - Processes only terminate when the script naturally completes
Suggested Fix
Implement a WindowsProcessTreeGuard similar to PtyKillGuard:
#[cfg(windows)]
struct WindowsProcessTreeGuard {
pid: u32,
armed: bool,
}
#[cfg(windows)]
impl Drop for WindowsProcessTreeGuard {
fn drop(&mut self) {
if self.armed {
// Kill the entire process tree on Windows
let _ = Command::new("taskkill")
.args(&["/PID", &self.pid.to_string(), "/T", "/F"])
.output();
}
}
}Then modify run_windows_fallback to arm the guard:
async fn run_windows_fallback(...) -> ToolResult {
let mut child = Command::new("cmd")...spawn()?;
#[cfg(windows)]
let mut guard = WindowsProcessTreeGuard {
pid: child.id().unwrap_or(0),
armed: true,
};
let result = tokio::time::timeout(timeout_dur, async {
// ... existing logic
child.wait().await
}).await;
#[cfg(windows)]
{
guard.armed = false; // Disarm on successful completion
}
// ... rest of the function
}Impact
This is a Windows-specific bug. Unix/macOS platforms are not affected because they use the PTY path with proper cleanup guards.
Environment
- OS: Windows 10 Pro (10.0.19045)
- Platform: x86_64-pc-windows-msvc
- claurst: Current main branch (submodule in downstream project)
Related Code
src-rust/crates/tools/src/pty_bash.rs:- Lines 331-364:
PtyKillGuard(Unix only) - Lines 559-629:
run_windows_fallback(Windows, no guard)
- Lines 331-364:
Source: Kuberwastaken/claurst