You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e9562a0
Browse filesBrowse the repository at this point in the historyBrowse files
fix(windows): release the Job Object on every disconnect and Stop path (follow-up to #1234) (#1260)
* fix(windows): release the Job Object on every disconnect and Stop path
Follow-up to #1234 (Windows Job Objects for child-process cleanup). The
Job was only ever closed on connect/init failure and at mcpproxy exit;
the two normal paths never reached it:
- core Disconnect: mcp-go's Stdio.Close() kills only the immediate
cmd.exe and always returns under mcpClientCloseTimeout, so the
"graceful close failed" force-kill step (the only caller of
killProcessGroup) was unreachable on Windows. Grandchildren that
ignore stdin EOF survived every restart, and the Job handle + the
windowsJobs entry leaked per disconnect. Add a releaseProcessGroup
platform hook (Unix no-op; Windows pops the entry and closes the Job)
and call it unconditionally for non-Docker stdio disconnects. Close
any stale entry on PID-reuse overwrite instead of orphaning it.
- launcher Stop: reap() waits for the log pumps before closing the Job,
but a grandchild holding the inherited pipe handles never lets the
pumps reach EOF once cmd.exe alone is killed, so Stop() hung until
the caller's ctx expired and the Job was never closed. Route
stopLocked's terminate/kill steps through per-platform handle methods
that close the Job on Windows (Unix unchanged: same process-group
signals).
- winjob: use x/sys/windows' exported SetInformationJobObject,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation
and JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE instead of hand-rolled structs
and a LazyDLL (also fixes the latent 4-byte layout mismatch on
386/arm and the gofmt nit); guard Close/Assign with a mutex now that
Stop and reap can both close the job; drop the dead
IsProcessAlive/isProcessGroupAlive helpers.
Tests (windows-tagged, run on main's windows-latest job): cmd.exe ->
ping.exe trees where ping.exe inherits the stdout pipe, so pipe EOF is
the proof that the grandchild died. Plus a platform-neutral
releaseProcessGroup no-op test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(windows): key Job release on the owning cmd, not just the PID
Review round 1 (opencode/astra) on the follow-up: Disconnect captures
pgid, mcp-go's Close() reaps cmd.exe, and only then releaseProcessGroup
runs — a concurrently connecting server can get the same PID in that
window, so a PID-only lookup would close the NEW server's Job. Store
the *exec.Cmd with each registry entry and release only a matching
owner; the takeover already closed the stale Job. Also gate the test
process trees on stdin (set /p) so the Job is attached before the
grandchild exists, and wait for ping output before killing cmd.exe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(windows): make launcher kill() a Job-only no-op when a Job exists
Review round 1 (chunk 3): after terminate() closed the Job the child is
dead but may still be unreaped while the pumps drain; falling through
to Process.Kill() there produced a misleading 'kill failed' error.
Mirror terminate(): return the Job close result, Process.Kill only on
the no-Job fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(windows): carry the owning cmd into killProcessGroup too
Review round 2 (chunk 2): Disconnect's force-kill step runs after a
Close timeout, and the leaked Close goroutine can still reap the child
before killProcessGroup pops the PID slot — same identity gap as the
release path. Thread the exec.Cmd through killProcessGroup (Unix
ignores it), only take a Job registered for that cmd, skip the kill
entirely when the slot belongs to a newer connection, and make the
no-Job fallback handle-based (cmd.Process.Kill, ErrProcessDone benign)
instead of os.FindProcess(pid). Regression test covers both paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(windows): make the PID-reuse regression observable with a live process
Review round 3 (chunk 2): with an unstarted oldCmd and a fake PID a
forbidden os.FindProcess fallback returned nil silently, so the test
could not fail. Use a live test-owned ping.exe as the new owner (its
stdout pipe reaching EOF == it died) so an old-owner release or
force-kill that reaches it by PID is caught.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
0 commit comments