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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user