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}