detached a9d674e

proc_unix_guard_test.go RawHistoryFolder

1//go:build !windows
2
3package mcp
4
5import (
6	"strconv"
7	"sync"
8	"syscall"
9	"testing"
10)
11
12// fakeKill records signals and answers from a script.
13type fakeKill struct {
14	mu    sync.Mutex
15	calls []int // pids passed, in order (negative = group)
16	err   error
17}
18
19func (f *fakeKill) kill(pid int, _ syscall.Signal) error {
20	f.mu.Lock()
21	defer f.mu.Unlock()
22	f.calls = append(f.calls, pid)
23	return f.err
24}
25
26func (f *fakeKill) count() int {
27	f.mu.Lock()
28	defer f.mu.Unlock()
29	return len(f.calls)
30}
31
32func newGuardForTest(pgid int, k *fakeKill) *processGuard {
33	return &processGuard{pgid: pgid, kill: k.kill}
34}
35
36func TestGuardForgetsGroupOnceGone(t *testing.T) {
37	k := &fakeKill{err: syscall.ESRCH}
38	g := newGuardForTest(4242, k)
39	if g.alive() {
40		t.Fatal("ESRCH means the group is gone")
41	}
42	before := k.count()
43	// A gone group is never probed or signalled again: its number may belong
44	// to somebody else by now.
45	g.terminate(false)
46	g.terminate(true)
47	if g.alive() {
48		t.Error("alive after gone")
49	}
50	g.cleanup()
51	if k.count() != before {
52		t.Errorf("signals sent to a group already observed gone: %v", k.calls)
53	}
54}
55
56func TestGuardCleanupIsOneShot(t *testing.T) {
57	k := &fakeKill{} // every probe/signal "succeeds": the group never empties
58	g := newGuardForTest(4243, k)
59	g.cleanup() // escalates SIGTERM then SIGKILL, then gives up
60	after := k.count()
61	if after == 0 {
62		t.Fatal("cleanup sent nothing to a live group")
63	}
64	for i := 0; i < 3; i++ {
65		g.cleanup()
66		g.terminate(true)
67		g.alive()
68	}
69	if k.count() != after {
70		t.Errorf("signals after cleanup: %d more", k.count()-after)
71	}
72	if g.pgid != 0 {
73		t.Error("process group id retained after cleanup")
74	}
75}
76
77func TestGuardNoSignalWhenAlreadyEmpty(t *testing.T) {
78	k := &fakeKill{err: syscall.ESRCH}
79	g := newGuardForTest(4244, k)
80	g.cleanup()
81	for _, pid := range k.calls {
82		if pid != -4244 {
83			t.Errorf("unexpected target %d", pid)
84		}
85	}
86	// Only the liveness probe ran, and nothing after it.
87	if k.count() != 1 {
88		t.Errorf("calls = %v, want a single probe", k.calls)
89	}
90}
91
92func TestStdioRepeatedCloseDoesNotSignalAgain(t *testing.T) {
93	cfg, _ := fakeStdioConfig(t, nil)
94	st, err := startStdio("x", cfg, "")
95	if err != nil {
96		t.Fatal(err)
97	}
98	if err := st.close(); err != nil {
99		t.Fatal(err)
100	}
101	k := &fakeKill{}
102	st.guard.mu.Lock()
103	st.guard.kill = k.kill
104	st.guard.mu.Unlock()
105	for i := 0; i < 3; i++ {
106		if err := st.close(); err != nil {
107			t.Fatal(err)
108		}
109	}
110	if k.count() != 0 {
111		t.Errorf("late Close signalled a saved process-group id: %v", k.calls)
112	}
113}
114
115func TestStdioNaturalExitCleansDescendants(t *testing.T) {
116	c, _ := connectFake(t, nil)
117	pid, err := strconv.Atoi(callText(t, c, "spawn", `{"mode":"sleeper-stubborn"}`))
118	if err != nil {
119		t.Fatal(err)
120	}
121	t.Cleanup(func() { killPID(pid) })
122	if !processAlive(pid) {
123		t.Fatal("descendant not running")
124	}
125	// The server dies on its own (no Close): the descendant must not be left
126	// running, and the transport must have released the group by itself.
127	_, _ = c.CallTool(t.Context(), "exit", []byte(`{}`))
128	eventually(t, "descendant to be cleaned after the leader exited", func() bool { return !processAlive(pid) })
129	st := c.t.(*stdioTransport)
130	eventually(t, "group id to be released", func() bool {
131		st.guard.mu.Lock()
132		defer st.guard.mu.Unlock()
133		return st.guard.pgid == 0
134	})
135}

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