process_windows.go RawHistoryFolder
1//go:build windows
2
3package subagents
4
5import (
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "os/exec"
11 "strconv"
12 "strings"
13 "sync"
14 "syscall"
15 "time"
16)
17
18// configureWorkerProcess makes CommandContext cancellation terminate the
19// worker and its descendants on Windows. Windows does not provide a Unix-like
20// process-group kill operation, so killProcessGroup uses taskkill's tree mode.
21func configureWorkerProcess(cmd *exec.Cmd, pipes ...io.Closer) {
22 setProcessGroup(cmd)
23 cmd.WaitDelay = workerProcessWaitDelay
24 var closePipes sync.Once
25 cmd.Cancel = func() error {
26 err := killProcessGroup(cmd)
27 closePipes.Do(func() { closeWorkerPipes(pipes) })
28 return err
29 }
30}
31
32func closeWorkerPipes(pipes []io.Closer) {
33 for _, pipe := range pipes {
34 if pipe != nil {
35 _ = pipe.Close()
36 }
37 }
38}
39
40const workerProcessWaitDelay = 5 * time.Second
41
42// setProcessGroup gives the worker its own Windows process group. The group
43// flag prevents it from being attached to the supervisor's console group;
44// taskkill /T below is still needed because Windows group creation alone does
45// not terminate descendants.
46func setProcessGroup(cmd *exec.Cmd) {
47 cmd.SysProcAttr = &syscall.SysProcAttr{
48 CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
49 }
50}
51
52// killProcessGroup terminates the worker process tree. taskkill is part of
53// Windows and /T follows the parent-child relationships to include workers
54// that spawned their own subprocesses.
55func killProcessGroup(cmd *exec.Cmd) error {
56 if cmd == nil || cmd.Process == nil {
57 return nil
58 }
59 err := forceKillProcess(cmd.Process.Pid)
60 var taskkillErr *taskkillError
61 if errors.As(err, &taskkillErr) && taskkillErr.processDone() {
62 return os.ErrProcessDone
63 }
64 return err
65}
66
67func forceKillProcess(pid int) error {
68 if pid <= 0 {
69 return nil
70 }
71 output, err := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/T", "/F").CombinedOutput()
72 if err == nil {
73 return nil
74 }
75 return &taskkillError{pid: pid, err: err, diagnostics: string(output)}
76}
77
78type taskkillError struct {
79 pid int
80 err error
81 diagnostics string
82}
83
84func (e *taskkillError) Error() string {
85 if diagnostics := strings.TrimSpace(e.diagnostics); diagnostics != "" {
86 return fmt.Sprintf("taskkill process tree for PID %d: %v: %s", e.pid, e.err, diagnostics)
87 }
88 return fmt.Sprintf("taskkill process tree for PID %d: %v", e.pid, e.err)
89}
90
91func (e *taskkillError) Unwrap() error { return e.err }
92
93// taskkill returns exit code 128 when the requested process no longer exists.
94// Other taskkill failures remain errors so callers can distinguish a
95// cancellation race from a real process-control failure.
96func (e *taskkillError) processDone() bool {
97 var exitErr *exec.ExitError
98 return errors.As(e.err, &exitErr) && exitErr.ExitCode() == 128
99}