1//go:build windows
2
3package mcp
4
5import (
6 "errors"
7 "fmt"
8 "os/exec"
9 "sync"
10 "syscall"
11 "unsafe"
12
13 "golang.org/x/sys/windows"
14)
15
16// isolateProcess starts the server suspended. The guard assigns it to a Job
17// Object before its first instruction runs and then resumes it, so no
18// descendant can be created outside the job.
19func isolateProcess(cmd *exec.Cmd) {
20 if cmd.SysProcAttr == nil {
21 cmd.SysProcAttr = &syscall.SysProcAttr{}
22 }
23 cmd.SysProcAttr.CreationFlags |= windows.CREATE_SUSPENDED
24}
25
26// processGuard owns a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE that
27// contains the server and all its descendants. The mutex is held across every
28// use of the handle so a concurrent release cannot close it mid-call.
29type processGuard struct {
30 mu sync.Mutex
31 job windows.Handle // 0 once released
32 once sync.Once
33}
34
35// newProcessGuard is called right after cmd.Start with the process still
36// suspended. It fails closed: if the job cannot be set up, the child is killed
37// and an error is returned.
38func newProcessGuard(cmd *exec.Cmd) (*processGuard, error) {
39 if cmd == nil || cmd.Process == nil {
40 return nil, errors.New("process not started")
41 }
42 fail := func(what string, err error) (*processGuard, error) {
43 _ = cmd.Process.Kill()
44 return nil, fmt.Errorf("%s: %w", what, err)
45 }
46 job, err := windows.CreateJobObject(nil, nil)
47 if err != nil {
48 return fail("create job object", err)
49 }
50 var info windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
51 info.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
52 if _, err := windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation,
53 uintptr(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info))); err != nil {
54 _ = windows.CloseHandle(job)
55 return fail("configure job object", err)
56 }
57 proc, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(cmd.Process.Pid))
58 if err != nil {
59 _ = windows.CloseHandle(job)
60 return fail("open server process", err)
61 }
62 err = windows.AssignProcessToJobObject(job, proc)
63 _ = windows.CloseHandle(proc)
64 if err != nil {
65 _ = windows.CloseHandle(job)
66 return fail("assign server to job object", err)
67 }
68 if err := resumeProcess(uint32(cmd.Process.Pid)); err != nil {
69 _ = windows.CloseHandle(job) // kills the job's only member
70 return fail("resume server", err)
71 }
72 return &processGuard{job: job}, nil
73}
74
75// resumeProcess resumes every suspended thread of pid (a CREATE_SUSPENDED
76// process has exactly its main thread).
77func resumeProcess(pid uint32) error {
78 snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPTHREAD, 0)
79 if err != nil {
80 return err
81 }
82 defer windows.CloseHandle(snap)
83 entry := windows.ThreadEntry32{Size: uint32(unsafe.Sizeof(windows.ThreadEntry32{}))}
84 if err := windows.Thread32First(snap, &entry); err != nil {
85 return err
86 }
87 resumed := 0
88 for {
89 if entry.OwnerProcessID == pid {
90 th, err := windows.OpenThread(windows.THREAD_SUSPEND_RESUME, false, entry.ThreadID)
91 if err != nil {
92 return err
93 }
94 _, err = windows.ResumeThread(th)
95 _ = windows.CloseHandle(th)
96 if err != nil {
97 return err
98 }
99 resumed++
100 }
101 entry.Size = uint32(unsafe.Sizeof(entry))
102 if err := windows.Thread32Next(snap, &entry); err != nil {
103 if errors.Is(err, windows.ERROR_NO_MORE_FILES) {
104 break
105 }
106 return err
107 }
108 }
109 if resumed == 0 {
110 return errors.New("no thread to resume")
111 }
112 return nil
113}
114
115// awaitLeaderExit is not needed on Windows: the Job Object, not a reusable
116// numeric id, identifies the group. It reports false so the caller waits for
117// the process first.
118func (g *processGuard) awaitLeaderExit() bool { return false }
119
120// terminate kills every process in the job. Windows has no graceful group
121// signal, so force is ignored.
122func (g *processGuard) terminate(_ bool) {
123 if g == nil {
124 return
125 }
126 g.mu.Lock()
127 defer g.mu.Unlock()
128 if g.job != 0 {
129 _ = windows.TerminateJobObject(g.job, 1)
130 }
131}
132
133// cleanup kills the job and closes its handle, once.
134func (g *processGuard) cleanup() {
135 if g == nil {
136 return
137 }
138 g.once.Do(func() {
139 g.mu.Lock()
140 defer g.mu.Unlock()
141 if g.job != 0 {
142 _ = windows.TerminateJobObject(g.job, 1)
143 _ = windows.CloseHandle(g.job)
144 g.job = 0
145 }
146 })
147}