Handle CLI server stdin EPIPE on teardown#4464
Open
cklin wants to merge 1 commit into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Handles intermittent Windows EPIPE failures when restarting the persistent CodeQL CLI server.
Changes:
- Adds a per-process stdin error listener.
- Logs and suppresses asynchronous stdin errors during teardown.
Show a summary per file
| File | Description |
|---|---|
extensions/ql-vscode/src/codeql-cli/cli.ts |
Handles CLI server stdin errors. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Medium
The CLI server process's stdin can emit an asynchronous 'error' during teardown: killProcessIfRunning() writes a "shutdown" request and then immediately end()s and kill()s the process, so on Windows the buffered write can complete with EPIPE once the read end closes. With no listener this became an unhandled error that faulted whichever operation was running, causing the flaky "should restart the database and run a query" failures in the Windows cli-integration tests (the restart command also restarts the CLI server via cliServer.restartCliServer()). Attach an 'error' listener to the CLI server's stdin inside killProcessIfRunning(), scoped to teardown, before issuing the shutdown write. The process is killed and dereferenced immediately afterwards, so the listener is short-lived and does not accumulate. Because it only exists during teardown, stdin errors during an active command retain their existing behaviour and cannot be masked. Co-authored-by: Copilot <[email protected]> Copilot-Session: 90105d62-02ab-405e-9c95-10466c971814
cklin
force-pushed
the
cklin/fix-cli-server-stdin-epipe
branch
from
July 16, 2026 22:31
be2e207 to
a2c5cc8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a flaky
write EPIPEfailure that intermittently fails the CLI integration tests on Windows (e.g.queries.test.ts→ "should restart the database and run a query"). Related: #4456.Root cause
codeQL.restartQueryServeralso restarts the persistent CLI server process (codeql execute cli-server) viacliServer.restartCliServer(). During teardown,killProcessIfRunning()writes a["shutdown"]request to the child's stdin and then synchronously callsstdin.end()+kill(). On Windows the buffered write can flush asynchronously and emit an'error'(EPIPE) after the read end has already closed.With no
'error'listener on that stream, Node treats it as an unhandled stream error, which jest then attributes to whichever test happened to be running — surfacing as a barewrite EPIPE.This is Windows-specific because the pipe-close/kill timing loses the in-flight write; on Linux the write drains first.
Fix
Attach an
'error'listener to the CLI server's stdin insidekillProcessIfRunning(), scoped to teardown, before issuing the shutdown write. The process is killed and dereferenced (this.process = undefined) immediately afterwards, so the listener is short-lived and does not accumulate across restarts.Scoping the listener to teardown (rather than attaching it once-per-process in
launchProcess()) is deliberate:close/errorevents, never via stdin. Because the listener only exists during teardown, stdin errors during an active command retain their existing behavior and cannot be swallowed.killProcessIfRunning()is fully synchronous, so it can't re-enter on the same process; each restart spawns a fresh child, and the old stream plus its lone listener are GC'd.try/catch(which only guards the synchronous throw fromwrite()) and the new async'error'listener are complementary and non-overlapping.Diagnosis
Root cause was confirmed via two instrumented CI runs on a throwaway branch: instrumenting the query server showed it healthy, while instrumenting the CLI server captured the stdin
'error'immediately afterkillProcessIfRunning, and handling it turned the Windows lane green.