v0.50.0 c4ec31f

proc_wait_linux_test.go RawHistoryFolder

1//go:build linux
2
3package mcp
4
5import (
6	"os"
7	"strconv"
8	"sync"
9	"syscall"
10	"testing"
11	"time"
12)
13
14func after5s() <-chan time.Time { return time.After(5 * time.Second) }
15
16func TestParseProcStat(t *testing.T) {
17	cases := []struct {
18		name, in string
19		state    byte
20		pgrp     int
21		ok       bool
22	}{
23		{"plain", "123 (bash) S 1 123 123 0 -1 4194560", 'S', 123, true},
24		{"zombie", "5 (fake-server) Z 1 77 77 0 -1 4", 'Z', 77, true},
25		{"spaces and parens", "9 (a) b (c)) R 2 42 42 0", 'R', 42, true},
26		{"truncated", "9 (x) S 1", 0, 0, false},
27		{"no comm", "garbage", 0, 0, false},
28	}
29	for _, c := range cases {
30		state, pg, ok := parseProcStat(c.in)
31		if state != c.state || pg != c.pgrp || ok != c.ok {
32			t.Errorf("%s: got (%c,%d,%v), want (%c,%d,%v)", c.name, state, pg, ok, c.state, c.pgrp, c.ok)
33		}
34	}
35}
36
37func TestLiveGroupMembersSeesSelfOnly(t *testing.T) {
38	// Our own process group has live members (us); a made-up one does not.
39	live, known := liveGroupMembers(os.Getpid())
40	if !known {
41		t.Skip("/proc not readable")
42	}
43	_ = live // our pgid is usually not our pid; only the empty case is stable
44	if live, known := liveGroupMembers(1 << 30); !known || live {
45		t.Errorf("unused group reported live=%v known=%v", live, known)
46	}
47}
48
49// ---- GO-020: group cleanup happens while the leader is still a zombie ----
50
51func TestStdioGroupCleanedBeforeLeaderIsReaped(t *testing.T) {
52	c, _ := connectFake(t, nil)
53	st := c.t.(*stdioTransport)
54	leader := st.cmd.Process.Pid
55
56	childPID, err := strconv.Atoi(callText(t, c, "spawn", `{"mode":"sleeper-stubborn"}`))
57	if err != nil {
58		t.Fatal(err)
59	}
60	t.Cleanup(func() { killPID(childPID) })
61	if !processAlive(childPID) {
62		t.Fatal("descendant not running")
63	}
64
65	type observation struct {
66		state      byte
67		childAlive bool
68		pgidKept   int
69	}
70	obs := make(chan observation, 1)
71	hook := func(pid int) {
72		if pid != leader {
73			return
74		}
75		var o observation
76		if raw, err := os.ReadFile("/proc/" + strconv.Itoa(leader) + "/stat"); err == nil {
77			o.state, _, _ = parseProcStat(string(raw))
78		}
79		o.childAlive = processAlive(childPID)
80		st.guard.mu.Lock()
81		o.pgidKept = st.guard.pgid
82		st.guard.mu.Unlock()
83		select {
84		case obs <- o:
85		default:
86		}
87	}
88	beforeReapHook.Store(&hook)
89	t.Cleanup(func() { beforeReapHook.Store(nil) })
90
91	// The server exits on its own.
92	_, _ = c.CallTool(t.Context(), "exit", []byte(`{}`))
93	var o observation
94	select {
95	case o = <-obs:
96	case <-after5s():
97		t.Fatal("cleanup hook never ran: the leader was reaped before group cleanup")
98	}
99	if o.state != 'Z' {
100		t.Errorf("leader state at cleanup = %q, want Z (exited but not reaped, so its pid/pgid cannot be reused)", o.state)
101	}
102	if o.childAlive {
103		t.Error("descendant still alive when the leader was about to be reaped")
104	}
105	if o.pgidKept != 0 {
106		t.Errorf("process-group id %d retained after cleanup", o.pgidKept)
107	}
108	// And the leader is reaped afterwards.
109	eventually(t, "leader to be reaped", func() bool {
110		_, err := os.Stat("/proc/" + strconv.Itoa(leader))
111		return os.IsNotExist(err)
112	})
113}
114
115// While the leader is an unreaped zombie the pgid is reserved, so cleanup must
116// end with a group SIGKILL even when the (non-atomic) /proc scan sees no live
117// member: a descendant may have forked between the scan and its end.
118func TestReservedGroupIsKilledDespiteEmptyScan(t *testing.T) {
119	var mu sync.Mutex
120	var sigs []syscall.Signal
121	g := &processGuard{pgid: 1 << 30, leader: 1 << 30, zombie: true,
122		kill: func(pid int, sig syscall.Signal) error {
123			mu.Lock()
124			defer mu.Unlock()
125			if pid != -(1 << 30) {
126				t.Errorf("signal to %d, want group", pid)
127			}
128			sigs = append(sigs, sig)
129			return nil
130		}}
131	g.cleanup()
132	mu.Lock()
133	defer mu.Unlock()
134	if len(sigs) == 0 || sigs[len(sigs)-1] != syscall.SIGKILL {
135		t.Fatalf("signals = %v, want a final group SIGKILL", sigs)
136	}
137	if g.pgid != 0 {
138		t.Error("pgid retained after cleanup")
139	}
140}

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