main 4509ec7

proc_unix.go RawHistoryFolder

1//go:build !windows
2
3package mcp
4
5import (
6	"errors"
7	"os"
8	"os/exec"
9	"sync"
10	"syscall"
11	"time"
12)
13
14// isolateProcess puts the server in its own process group so it does not
15// receive terminal signals aimed at zut and so cleanup can stop its children.
16func isolateProcess(cmd *exec.Cmd) {
17	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
18}
19
20// processGuard owns the process group of one stdio server.
21//
22// A process-group id may be reused by an unrelated process as soon as no
23// process uses the number any more. A group stays in use while ANY member
24// exists, and that includes a zombie: the leader's pid (which is the pgid) is
25// not freed until the leader is reaped. So the guard keeps the leader
26// unreaped, as a zombie, until it has cleaned the group, wherever the platform
27// can wait for a child without reaping it (waitLeaderExited; Linux). Only then
28// is the leader reaped by cmd.Wait.
29//
30// Where that is not available (darwin, the BSDs) cleanup runs after the leader
31// was reaped; the id is then only guaranteed to be ours while other group
32// members live, which is the best that can be done and is documented as a
33// residual race. In every case the guard
34//
35//	(a) cleans the group exactly once,
36//	(b) forgets the id as soon as the group is observed empty or cleaned,
37//	(c) is cleaned as soon as the direct child exits, so no stale id stays
38//	    signalable for the life of the transport.
39type processGuard struct {
40	mu     sync.Mutex
41	pgid   int // 0 once cleaned up or observed gone
42	leader int // pid of the group leader (== original pgid)
43	proc   *os.Process
44	kill   func(pid int, sig syscall.Signal) error // syscall.Kill; replaced in tests
45	// zombie is set once the leader has exited but not been reaped. The pgid
46	// is then reserved, so cleanup always ends with a group SIGKILL; the
47	// process table is read only to end the SIGTERM grace period early.
48	zombie bool
49
50	once sync.Once
51}
52
53// newProcessGuard is called right after cmd.Start.
54func newProcessGuard(cmd *exec.Cmd) (*processGuard, error) {
55	g := &processGuard{kill: func(pid int, sig syscall.Signal) error { return syscall.Kill(pid, sig) }}
56	if cmd != nil && cmd.Process != nil {
57		g.pgid = cmd.Process.Pid
58		g.leader = cmd.Process.Pid
59		g.proc = cmd.Process
60	}
61	return g, nil
62}
63
64// awaitLeaderExit blocks until the group leader has exited, without reaping
65// it. It reports false when the platform cannot do that, in which case it
66// returns immediately and the caller must Wait first.
67func (g *processGuard) awaitLeaderExit() bool {
68	if g == nil || g.leader == 0 {
69		return false
70	}
71	if !waitLeaderExited(g.leader) {
72		return false
73	}
74	g.mu.Lock()
75	g.zombie = true
76	g.mu.Unlock()
77	return true
78}
79
80// terminate signals the whole group, including descendants that outlive the
81// direct child. It does nothing once the group is cleaned up.
82func (g *processGuard) terminate(force bool) {
83	if g == nil {
84		return
85	}
86	g.mu.Lock()
87	defer g.mu.Unlock()
88	if g.pgid == 0 {
89		return
90	}
91	sig := syscall.SIGTERM
92	if force {
93		sig = syscall.SIGKILL
94	}
95	err := g.kill(-g.pgid, sig)
96	switch {
97	case err == nil:
98	case errors.Is(err, syscall.ESRCH):
99		g.pgid = 0 // group gone: never signal this number again
100	default:
101		if g.proc != nil {
102			_ = g.proc.Signal(sig)
103		}
104	}
105}
106
107// alive reports whether the group still has members.
108func (g *processGuard) alive() bool {
109	if g == nil {
110		return false
111	}
112	g.mu.Lock()
113	defer g.mu.Unlock()
114	if g.pgid == 0 {
115		return false
116	}
117	if g.zombie {
118		// A signal-0 probe would see the zombie leader. The /proc scan is
119		// advisory only (not an atomic snapshot), so it never clears pgid.
120		live, known := liveGroupMembers(g.pgid)
121		return live || !known
122	}
123	err := g.kill(-g.pgid, 0)
124	if errors.Is(err, syscall.ESRCH) {
125		g.pgid = 0
126		return false
127	}
128	return true // nil, or EPERM: a member exists that we cannot signal
129}
130
131// cleanup stops every remaining group member (SIGTERM, then SIGKILL for
132// survivors) and releases the id. It runs its body once; later and concurrent
133// calls wait for that run and then return.
134func (g *processGuard) cleanup() {
135	if g == nil {
136		return
137	}
138	g.once.Do(func() {
139		defer func() {
140			g.mu.Lock()
141			g.pgid = 0
142			g.proc = nil
143			g.mu.Unlock()
144		}()
145		g.mu.Lock()
146		reserved := g.zombie
147		g.mu.Unlock()
148		if reserved {
149			// The unreaped leader keeps the pgid ours: give members a grace
150			// period, then kill the group unconditionally. The kernel delivers
151			// a group signal to every member, including ones forked meanwhile.
152			g.terminate(false)
153			g.waitGone(300 * time.Millisecond)
154			g.terminate(true)
155			return
156		}
157		if !g.alive() {
158			return
159		}
160		g.terminate(false)
161		if g.waitGone(300 * time.Millisecond) {
162			return
163		}
164		g.terminate(true)
165		g.waitGone(200 * time.Millisecond)
166	})
167}
168
169func (g *processGuard) waitGone(d time.Duration) bool {
170	deadline := time.Now().Add(d)
171	for {
172		if !g.alive() {
173			return true
174		}
175		if time.Now().After(deadline) {
176			return false
177		}
178		time.Sleep(10 * time.Millisecond)
179	}
180}

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 476

↑↓ moveenter openesc close