v0.50.0 c4ec31f

proc_windows.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close