#391·claurst

[Windows] Process tree not killed when future is dropped in run_windows_fallback

Author: hopwesleyCreated Aug 17, 2026Updated Aug 17, 2026

[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 implements Drop to automatically kill child processes when the future is dropped
  • Windows: Uses run_windows_fallback with 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 cancellation

Unix/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 /F to recursively kill the process tree on Windows

Reproduction Steps

  1. Run a long-running command on Windows:
    rust
    let result = run_tool("bash", json!({
        "command": "python -c \"import time; time.sleep(300)\""
    })).await;
  2. Cancel/abort the future mid-execution (drop the future)
  3. Check Task Manager → cmd.exe and python.exe are 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.exe and child python.exe continue running
  • Processes only terminate when the script naturally completes

Suggested Fix

Implement a WindowsProcessTreeGuard similar to PtyKillGuard:

rust
#[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:

rust
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)