python_proc_windows.go RawHistoryFolder
1//go:build windows
2
3package tools
4
5import (
6 "bytes"
7 "context"
8 "io"
9 "os/exec"
10 "strconv"
11 "sync"
12 "syscall"
13 "time"
14)
15
16// pythonProcessWaitDelay bounds process cleanup beyond the invocation or
17// probe deadline. Bounded return is required; absolute containment of every
18// detached descendant is not promised.
19const pythonProcessWaitDelay = 5 * time.Second
20
21// pythonTreeKillTimeout bounds the tree-termination subprocess. On
22// failure or timeout the caller falls back to a direct process kill.
23const pythonTreeKillTimeout = 2 * time.Second
24
25// maxPythonKillOutput bounds termination-command output.
26const maxPythonKillOutput = 4 * 1024
27
28// configurePythonProcess makes context cancellation terminate the Python
29// process tree on Windows and closes the output pipe exactly once. Unlike
30// Bash's unbounded taskkill path, tree termination is bounded with a
31// direct-kill fallback. When cancelErr is non-nil, the termination error
32// from the Cancel callback is recorded there (guarded by its mutex):
33// Wait folds that error into its own process error, so without this
34// capture the cleanup detail is unreachable and a partial kill cannot
35// report surviving descendants.
36func configurePythonProcess(cmd *exec.Cmd, output io.Closer, cancelErr *pythonCancelError) func() {
37 if cmd != nil {
38 cmd.SysProcAttr = &syscall.SysProcAttr{
39 CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
40 }
41 cmd.WaitDelay = pythonProcessWaitDelay
42 }
43 var closeOutput sync.Once
44 closePipe := func() {
45 closeOutput.Do(func() {
46 if output != nil {
47 _ = output.Close()
48 }
49 })
50 }
51 if cmd != nil {
52 cmd.Cancel = func() error {
53 closePipe()
54 err := killPythonProcessTree(cmd)
55 if cancelErr != nil {
56 cancelErr.set(err)
57 }
58 return err
59 }
60 }
61 return closePipe
62}
63
64// killPythonProcessTree terminates the process tree with a bounded
65// taskkill call, then falls back to a direct kill on failure or timeout.
66func killPythonProcessTree(cmd *exec.Cmd) error {
67 if cmd == nil || cmd.Process == nil {
68 return nil
69 }
70 if err := boundedTaskkill(cmd.Process.Pid); err != nil {
71 _ = cmd.Process.Kill()
72 return err
73 }
74 return nil
75}
76
77func boundedTaskkill(pid int) error {
78 if pid <= 0 {
79 return nil
80 }
81 ctx, cancel := context.WithTimeout(context.Background(), pythonTreeKillTimeout)
82 defer cancel()
83 killCmd := exec.CommandContext(ctx, "taskkill", "/PID", strconv.Itoa(pid), "/T", "/F")
84 var out bytes.Buffer
85 killCmd.Stdout = &limitedWriter{W: &out, N: maxPythonKillOutput}
86 killCmd.Stderr = &limitedWriter{W: &out, N: maxPythonKillOutput}
87 if err := killCmd.Run(); err != nil {
88 return err
89 }
90 return ctx.Err()
91}