GCWing / GCWing/OpenBitFun

[Bug] Windows: ExecControl kill/interrupt does not terminate non-TTY processes started through an App Execution Alias

Abierto
#3,084 0 comentarios 0 reacciones 1 asignado Reclamado por @wsp1911 Ver en GitHub
Lenguaje dominante
Rust
Estrellas
2.2k
Forks
229
Merge medio
2 h 46 min
PR fusionados (30 d)
577

Descripción

> **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 合并)改动的正是这块区域,因此这更像是**未被覆盖的缺口**,而不是回归。

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.