fix: 修复 Windows Codex 进程完成后不自动退出问题 (v1.7.21)
问题根因: 1. messageTimerCh 中阻塞等待 waitCh 导致无限挂起 2. Windows 上 proc.Kill() 只杀主进程不杀子进程 解决方案: 1. 移除 messageTimerCh case 中的阻塞等待 2. 新增 killProcessTree() 使用 taskkill /T /F /PID 终止进程树 3. 修改 terminateCommand/terminateProcess/forwardSignals 使用进程树终止
This commit is contained in:
1 parent
e5cfdee7a9
commit
1237d2bfe0
5 files changed
+123
-15
No files matched your search
@@ -7,6 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [1.7.21] - 2026-01-13
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
**彻底修复 Windows 系统 Codex 进程无法自动终止问题**
|
||||
|
||||
#### 问题背景
|
||||
|
||||
v1.7.18 的修复不完整,Windows 上 Codex 完成后进程仍然卡住:
|
||||
- Web UI 显示完成,但 codeagent-wrapper 进程一直不退出
|
||||
- 只有手动杀死进程才能继续
|
||||
- Mac 没问题,Windows 的 Gemini 也没问题
|
||||
|
||||
#### 根本原因分析
|
||||
|
||||
1. **进程树问题**:`proc.Kill()` 只终止主进程,Codex 启动的子进程(Node.js workers)仍然运行,持有 stdout handle
|
||||
2. **阻塞等待问题**:`messageTimerCh` case 里直接 `waitErr = <-waitCh` 阻塞等待,如果进程没被杀死就永远卡住
|
||||
|
||||
#### 核心修复
|
||||
|
||||
1. **使用 `taskkill /T` 终止整个进程树**:
|
||||
- 新增 `killProcessTree()` 函数,使用 `taskkill /T /F /PID` 递归杀死所有子进程
|
||||
- `terminateCommand()`、`terminateProcess()`、`forwardSignals()` 在 Windows 上调用进程树终止
|
||||
|
||||
2. **移除阻塞等待**:
|
||||
- `messageTimerCh` case 里不再直接阻塞等待 `waitCh`
|
||||
- 让循环继续,下一轮通过 `case waitErr = <-waitCh` 正常退出
|
||||
- 即使 `taskkill` 失败,也不会永远卡住
|
||||
|
||||
#### 技术细节
|
||||
|
||||
修改文件:`codeagent-wrapper/executor.go`
|
||||
|
||||
```go
|
||||
// 新增:Windows 进程树终止
|
||||
func killProcessTree(pid int) error {
|
||||
if !isWindows() {
|
||||
return nil
|
||||
}
|
||||
cmd := exec.Command("taskkill", "/T", "/F", "/PID", fmt.Sprintf("%d", pid))
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// 修改:terminateCommand 使用进程树终止
|
||||
if isWindows() {
|
||||
if err := killProcessTree(proc.Pid()); err != nil {
|
||||
_ = proc.Kill() // fallback
|
||||
}
|
||||
}
|
||||
|
||||
// 修改:messageTimerCh case 不再阻塞
|
||||
case <-messageTimerCh:
|
||||
// ...terminate logic...
|
||||
// Do NOT block here - let loop continue
|
||||
```
|
||||
|
||||
#### 影响范围
|
||||
|
||||
- ✅ **修复前**:Windows 用户 Codex 完成后进程卡住
|
||||
- ✅ **修复后**:所有平台进程正确终止
|
||||
- ✅ **向后兼容**:Unix 平台行为不变
|
||||
|
||||
#### codeagent-wrapper 版本
|
||||
|
||||
- 升级至 v5.6.0
|
||||
- 重新编译所有平台二进制文件
|
||||
|
||||
---
|
||||
|
||||
## [1.7.20] - 2026-01-13
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
@@ -1126,8 +1126,9 @@ waitLoop:
|
||||
terminated = true
|
||||
}
|
||||
}
|
||||
waitErr = <-waitCh
|
||||
break waitLoop
|
||||
// Do NOT block here waiting for waitCh - let the loop continue
|
||||
// This is critical for Windows where process termination may fail silently
|
||||
// The next iteration will catch waitCh if the process exits
|
||||
case <-completeSeen:
|
||||
completeSeenObserved = true
|
||||
if messageTimer != nil {
|
||||
@@ -1256,9 +1257,11 @@ func forwardSignals(ctx context.Context, cmd commandRunner, logErrorFn func(stri
|
||||
case sig := <-sigCh:
|
||||
logErrorFn(fmt.Sprintf("Received signal: %v", sig))
|
||||
if proc := cmd.Process(); proc != nil {
|
||||
// Windows does not support SIGTERM - use Kill() directly
|
||||
// Windows does not support SIGTERM - use taskkill /T to kill process tree
|
||||
if isWindows() {
|
||||
_ = proc.Kill()
|
||||
if err := killProcessTree(proc.Pid()); err != nil {
|
||||
_ = proc.Kill()
|
||||
}
|
||||
} else {
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
@@ -1322,6 +1325,26 @@ func (t *forceKillTimer) Stop() {
|
||||
t.stopped.Store(true)
|
||||
}
|
||||
|
||||
// killProcessTree terminates a process and all its child processes on Windows.
|
||||
// Uses taskkill /T /F /PID which recursively kills the entire process tree.
|
||||
// This is necessary because Codex CLI may spawn child processes (Node.js workers, etc.)
|
||||
// that inherit stdout handles and prevent the parent from exiting cleanly.
|
||||
// Returns nil on non-Windows platforms or if the process has already exited.
|
||||
func killProcessTree(pid int) error {
|
||||
if !isWindows() {
|
||||
return nil
|
||||
}
|
||||
if pid <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// taskkill /T = kill process tree, /F = force, /PID = process ID
|
||||
cmd := exec.Command("taskkill", "/T", "/F", "/PID", fmt.Sprintf("%d", pid))
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func terminateCommand(cmd commandRunner) *forceKillTimer {
|
||||
if cmd == nil {
|
||||
return nil
|
||||
@@ -1333,9 +1356,14 @@ func terminateCommand(cmd commandRunner) *forceKillTimer {
|
||||
|
||||
// Windows does not support SIGTERM - it silently fails without terminating the process.
|
||||
// This causes codeagent-wrapper to hang waiting for the process to exit.
|
||||
// On Windows, we directly use Kill() which calls TerminateProcess().
|
||||
// On Windows, we use taskkill /T to kill the entire process tree, not just the main process.
|
||||
// This is critical because Codex CLI spawns child processes that hold stdout handles open.
|
||||
if isWindows() {
|
||||
_ = proc.Kill()
|
||||
pid := proc.Pid()
|
||||
if err := killProcessTree(pid); err != nil {
|
||||
// Fallback to direct Kill() if taskkill fails
|
||||
_ = proc.Kill()
|
||||
}
|
||||
} else {
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
@@ -1343,7 +1371,11 @@ func terminateCommand(cmd commandRunner) *forceKillTimer {
|
||||
done := make(chan struct{}, 1)
|
||||
timer := time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
if isWindows() {
|
||||
_ = killProcessTree(p.Pid())
|
||||
} else {
|
||||
_ = p.Kill()
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
})
|
||||
@@ -1360,16 +1392,23 @@ func terminateProcess(cmd commandRunner) *time.Timer {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Windows does not support SIGTERM - use Kill() directly
|
||||
// Windows does not support SIGTERM - use taskkill /T to kill process tree
|
||||
if isWindows() {
|
||||
_ = proc.Kill()
|
||||
pid := proc.Pid()
|
||||
if err := killProcessTree(pid); err != nil {
|
||||
_ = proc.Kill()
|
||||
}
|
||||
} else {
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
|
||||
return time.AfterFunc(time.Duration(forceKillDelay.Load())*time.Second, func() {
|
||||
if p := cmd.Process(); p != nil {
|
||||
_ = p.Kill()
|
||||
if isWindows() {
|
||||
_ = killProcessTree(p.Pid())
|
||||
} else {
|
||||
_ = p.Kill()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
version = "5.5.0"
|
||||
version = "5.6.0"
|
||||
defaultWorkdir = "."
|
||||
defaultTimeout = 7200 // seconds (2 hours)
|
||||
defaultCoverageTarget = 90.0
|
||||
|
||||
@@ -3067,7 +3067,7 @@ func TestVersionFlag(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
want := "codeagent-wrapper version 5.4.0\n"
|
||||
want := "codeagent-wrapper version 5.6.0\n"
|
||||
|
||||
if output != want {
|
||||
t.Fatalf("output = %q, want %q", output, want)
|
||||
@@ -3083,7 +3083,7 @@ func TestVersionShortFlag(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
want := "codeagent-wrapper version 5.4.0\n"
|
||||
want := "codeagent-wrapper version 5.6.0\n"
|
||||
|
||||
if output != want {
|
||||
t.Fatalf("output = %q, want %q", output, want)
|
||||
@@ -3099,7 +3099,7 @@ func TestVersionLegacyAlias(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
want := "codex-wrapper version 5.4.0\n"
|
||||
want := "codex-wrapper version 5.6.0\n"
|
||||
|
||||
if output != want {
|
||||
t.Fatalf("output = %q, want %q", output, want)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ccg-workflow",
|
||||
"version": "1.7.20",
|
||||
"version": "1.7.21",
|
||||
"description": "Claude-Codex-Gemini 多模型协作系统 - 智能路由多模型开发工作流",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.17.1",
|
||||
|
||||
Reference in new issue
Block a user