From 1237d2bfe0c324fe2ceeba3bbbc5dce210c955c9 Mon Sep 17 00:00:00 2001 From: li Date: Tue, 13 Jan 2026 16:32:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Windows=20Codex=20?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E5=AE=8C=E6=88=90=E5=90=8E=E4=B8=8D=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=80=80=E5=87=BA=E9=97=AE=E9=A2=98=20(v1.7.21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题根因: 1. messageTimerCh 中阻塞等待 waitCh 导致无限挂起 2. Windows 上 proc.Kill() 只杀主进程不杀子进程 解决方案: 1. 移除 messageTimerCh case 中的阻塞等待 2. 新增 killProcessTree() 使用 taskkill /T /F /PID 终止进程树 3. 修改 terminateCommand/terminateProcess/forwardSignals 使用进程树终止 --- CHANGELOG.md | 69 ++++++++++++++++++++++++++++++++++ codeagent-wrapper/executor.go | 59 ++++++++++++++++++++++++----- codeagent-wrapper/main.go | 2 +- codeagent-wrapper/main_test.go | 6 +-- package.json | 2 +- 5 files changed, 123 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a4b8df..0254f5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 修复 diff --git a/codeagent-wrapper/executor.go b/codeagent-wrapper/executor.go index 45c1dcc..4a78005 100644 --- a/codeagent-wrapper/executor.go +++ b/codeagent-wrapper/executor.go @@ -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() + } } }) } diff --git a/codeagent-wrapper/main.go b/codeagent-wrapper/main.go index d4d8302..40d4fec 100644 --- a/codeagent-wrapper/main.go +++ b/codeagent-wrapper/main.go @@ -14,7 +14,7 @@ import ( ) const ( - version = "5.5.0" + version = "5.6.0" defaultWorkdir = "." defaultTimeout = 7200 // seconds (2 hours) defaultCoverageTarget = 90.0 diff --git a/codeagent-wrapper/main_test.go b/codeagent-wrapper/main_test.go index b4ac34a..bbc45c8 100644 --- a/codeagent-wrapper/main_test.go +++ b/codeagent-wrapper/main_test.go @@ -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) diff --git a/package.json b/package.json index 6e75de8..4e99e97 100644 --- a/package.json +++ b/package.json @@ -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",