[Bug] Windows: ExecControl kill/interrupt does not terminate non-TTY processes started through an App Execution Alias
- Dominant language
- Rust
- Stars
- 2.2k
- Forks
- 229
- Avg merge
- 2h 46m
- Merged PRs (30d)
- 577
Description
> **EN TL;DR** — On Windows, `ExecControl` (`kill` and `interrupt`) reports success but the process survives, when the command was launched through a Windows App Execution Alias (`%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe`, a 0-byte reparse point). The same test using the interpreter's real path terminates correctly, and TTY sessions are unaffected. The extra process level introduced by the alias appears to break `taskkill /T /F` tree termination.
---
## 环境
- OpenBitFun **1.0.0** 桌面版
- Windows 10 22H2(19045)
- Windows PowerShell 5.1
- Python **3.14.6**
关键前提:当前会话里 `python` 解析到的**不是真解释器**,而是一个应用执行别名:
```
> (Get-Command python).Source
C:\Users\\AppData\Local\Microsoft\WindowsApps\python.exe
> (Get-Item 上述路径).Length / .Attributes
0 字节 / Archive, ReparsePoint
```
(0 字节 + `ReparsePoint` = 微软商店应用的执行别名转发器,不是可执行体)
---
## 复现步骤(失败路径)
1. 默认(非 TTY)模式启动一条长跑命令:
```
ExecCommand(cmd = 'python -c "import time; time.sleep(60); print(\'NEVER\')"', yield_time_ms = 1000)
# → Process is still running. session_id: 68995
```
2. 发送 `kill`:
```
ExecControl(action = "kill", session_id = 68995, yield_time_ms = 3000)
# → Sent kill to process.
# Process is still running. session_id: 68995 ← 进程没死
```
3. 用与 OpenBitFun 无关的手段独立核对:
```
Get-CimInstance Win32_Process -Filter "Name='python.exe'" | Select ProcessId, CommandLine
# → 两个进程都还活着:
# PID 10616 "...\Microsoft\WindowsApps\python.exe" -c "import time; time.sleep(60); print('NEVER')"
# PID 12712 "...\AppData\Local\Python\pythoncore-3.14-64\python.exe" -c "import time; time.sleep(60); print('NEVER')"
```
`interrupt` 表现完全一致:`Sent interrupt to process.` 之后仍是 `Process is still running.`
注意:**连最上层的别名进程(10616)也没有死**——所以这不只是"孙子进程逃出了进程树"。外部 `Stop-Process -Id 10616,12712 -Force` 可以立刻清掉两者。
---
## 预期行为
按 `src/crates/assembly/core/src/agentic/tools/implementations/exec_command/control.rs` 的注释:
> `tty=false, Windows: interrupt and kill both terminate the process tree via taskkill /T /F, with child.kill() as fallback. This is intentionally forceful because Windows pipe-mode Ctrl+C delivery is not reliable.`
两种动作都应终止整棵进程树。
---
## 对照实验 A:用真解释器完整路径启动 → 正常终止
流程完全相同,只是用**完整路径**启动解释器(绕过别名):
```
ExecCommand(cmd = '& "...\pythoncore-3.14-64\python.exe" -c "import time; time.sleep(60); print(\'NEVER\')"', yield_time_ms = 1200)
# → Process is still running. session_id: 7887
ExecControl(action = "kill", session_id = 7887, yield_time_ms = 3000)
# → Sent kill to process.
# Process exited with code 0. ← 成功
```
`Get-CimInstance` 复核:无任何 `python.exe` 进程残留。
## 对照实验 B:TTY 会话 → 正常
```
ExecCommand(cmd = 'python -u -c "import time; print(\'START\'); time.sleep(45); print(\'END\')"', tty = true, yield_time_ms = 2000)
# → session_id: 76593
ExecControl(action = "interrupt", session_id = 76593, yield_time_ms = 3000)
# → Sent interrupt to process.
# Process exited with code 1.
# Traceback (most recent call last):
# File "", line 1, in
# import time; print('START'); time.sleep(45); print('END')
# ~~~~~~~~~~^^^^
# KeyboardInterrupt
```
符合文档描述的正常 Ctrl+C 行为。
---
## 结果汇总
| 启动方式 | tty | 动作 | 结果 |
|---|---|---|---|
| `python`(PATH → 别名) | `false` | `interrupt` | ❌ 进程存活 |
| `python`(PATH → 别名) | `false` | `kill` | ❌ 进程存活 |
| 真解释器完整路径 | `false` | `kill` | ✅ 正常终止 |
| `python`(PATH → 别名) | `true` | `interrupt` | ✅ KeyboardInterrupt |
---
## 根因推测
当 `python` 经 `%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe`(应用执行别名)解析时,进程树多出一层:
```
shell → python.exe(别名,PID 10616)→ python.exe(真解释器,PID 12712)
```
`taskkill /T /F` 与 `child.kill()` 兜底在这个形状的进程树上似乎都失效了。由于**最上层的别名进程同样存活**,"遍历进程树时杀掉了子进程但漏了孙子"并不能完全解释——更可能是 `taskkill` 整体失败(例如别名对其子进程的转交方式破坏了树解析),而兜底只作用于它仍能看到的直接子进程。
---
## 为什么现有测试可能覆盖不到
`src/crates/services/terminal/src/exec.rs` 中有 `control_kill_terminates_python_child_started_by_default_windows_shell` 与 `control_interrupt_terminates_python_child_started_by_default_windows_shell`,用的正是 `python -c "import time; ..."`、由默认 Windows shell 启动。
但在典型 CI 机器上,`python` 是直接装在 `PATH` 上的,**不经过应用执行别名**——那个多余的进程层级在 CI 里根本不会出现。
---
## 临时规避
外部终止可靠:
```
Stop-Process -Id , -Force
```
---
## 补充说明
- 已检索仓库内 issue / PR / discussion(关键词:`ExecControl`、`interrupt`、`kill`、`tty`、`non-TTY`、`process-tree`、`taskkill`、`orphan process` 等):**没有匹配的报告**。PR #1175(*harden non-tty process-tree control on Windows and Unix*,2026-06-12 合并)改动的正是这块区域,因此这更像是**未被覆盖的缺口**,而不是回归。
Contributor guide
Assessment
This issue has not been assessed yet.