mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-08 14:56:19 +02:00
On cancellation/timeout the opencode backend closed the stdout read end immediately, leaving the child writing into a closed pipe. Every write then returns EPIPE and, per anomalyco/opencode#33653, can spin an orphaned process at 100% CPU — surfacing as high idle CPU after a cancelled task or daemon restart (MUL-3655). Cleanup now runs opencode in its own process group and, on cancel, drives a graceful group-wide SIGTERM → grace → SIGKILL, closing the stdout pipe only as a last-resort unblock once the tree has been signalled (SIGKILL is uncatchable, so no member can write again — no EPIPE window). The group signal also reaps tool subprocesses opencode spawned instead of orphaning them. WaitDelay remains the hard backstop. Adds unix tests covering the graceful path and the SIGTERM-ignored → SIGKILL escalation, asserting the whole process group is reaped and the run never deadlocks on the scanner. Windows behaviour is unchanged (no process groups). Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
50 lines
1.9 KiB
Go
50 lines
1.9 KiB
Go
//go:build windows
|
|
|
|
package agent
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"syscall"
|
|
)
|
|
|
|
// createNewConsole allocates a fresh console for the child process. Combined
|
|
// with HideWindow=true (STARTF_USESHOWWINDOW + SW_HIDE) the console window
|
|
// stays off-screen, and — critically — any grandchildren the agent spawns
|
|
// (tool subprocesses like bash, cmd, netstat, findstr) inherit this hidden
|
|
// console instead of each allocating their own visible one.
|
|
//
|
|
// Using CREATE_NO_WINDOW here instead would strip the console entirely,
|
|
// which forces Windows to allocate a new visible console per grandchild
|
|
// when the grandchild is a console-subsystem program that doesn't itself
|
|
// pass CREATE_NO_WINDOW — the exact popup storm reported in #1521.
|
|
const createNewConsole = 0x00000010
|
|
|
|
// hideAgentWindow configures cmd to suppress the console window on Windows
|
|
// while still giving descendant processes a hidden console to inherit.
|
|
// Stdio pipes set via cmd.StdoutPipe/StdinPipe keep working because
|
|
// STARTF_USESTDHANDLES takes precedence over the new console's stdio.
|
|
func hideAgentWindow(cmd *exec.Cmd) {
|
|
if cmd.SysProcAttr == nil {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
cmd.SysProcAttr.HideWindow = true
|
|
cmd.SysProcAttr.CreationFlags |= createNewConsole
|
|
}
|
|
|
|
// configureProcessGroup is a no-op on Windows: there is no Setpgid/process-group
|
|
// signalling. Descendant cleanup relies on the hidden console group set up by
|
|
// hideAgentWindow plus exec.CommandContext / WaitDelay terminating the child.
|
|
func configureProcessGroup(cmd *exec.Cmd) {}
|
|
|
|
// signalProcessGroup terminates the process on Windows. Windows has no
|
|
// SIGTERM/SIGKILL distinction or process-group signalling, so the signal is
|
|
// ignored and the process is killed directly (TerminateProcess via Kill). The
|
|
// caller's grace window still applies before this is invoked with SIGKILL.
|
|
func signalProcessGroup(p *os.Process, _ syscall.Signal) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
_ = p.Kill()
|
|
}
|