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