Repository navigation
client: reject start() if server process exits before connecting on pipe/socket transport - #1862
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The failure path leaves the underlying pipe/socket listener active and its resources occupied.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Prevents pipe/socket client startup from hanging when the server exits before connecting.
Changes:
- Races transport connection against child-process exit.
- Adds pipe and socket integration tests using an immediate-exit server.
| File | Description |
|---|---|
client/src/node/main.ts |
Adds connection/exit coordination. |
client-node-tests/src/servers/exitBeforeConnectServer.ts |
Adds failing test server. |
client-node-tests/src/integration.test.ts |
Tests socket and pipe rejection behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Pujitha Paladugu (@pujitha24) see review comments. |
…ipe/socket transport
Motivation:
When using TransportKind.pipe or TransportKind.socket, the client creates
a pipe/socket server and waits for the spawned server process to connect
to it via transport.onConnected(). That promise (created in
createClientPipeTransport / createClientSocketTransport in
jsonrpc/src/node/main.ts) is only ever resolved from inside the
connection callback - it has no reject path. If the server process exits
before connecting (for example because it fails to start, or exits with
a nonzero code), the promise never settles and LanguageClient#start()
hangs forever instead of failing.
Approach:
Add a small waitForConnection() helper in client/src/node/main.ts that
races transport.onConnected() against the spawned child process's 'exit'
event, rejecting with a descriptive error if the process exits first.
Use it at all six pipe/socket call sites in createMessageTransports
(module+runtime, forked module, and Executable/command branches), in
place of the bare transport.onConnected().then(...) calls.
Validation:
- npm run compile (tsc -b) is clean in jsonrpc, protocol, types,
textDocument, server, client and client-node-tests after `npm run
symlink`.
- npm run lint is clean in client and client-node-tests (only a
pre-existing, unrelated eslint-plugin deprecation warning).
- Added two integration tests in client-node-tests/src/integration.test.ts
('Socket transport' and new 'Pipe transport' suites) using a new
minimal server script (servers/exitBeforeConnectServer.ts) that exits
immediately without ever connecting, mirroring the reporter's repro.
Ran via `npm test` in client-node-tests (real VS Code Insiders +
Electron extension host, @vscode/test-electron): before the fix the
new socket test times out after 5s (client.start() never settles,
reproducing the reported hang exactly); after the fix both new tests
pass, and the full existing suite (207 tests) passes with no
regressions.
- Additionally reproduced the root cause directly with a standalone
script using the repo's own built jsonrpc createClientSocketTransport
plus a real spawned process that exits non-zero without connecting,
confirming transport.onConnected() never settles without the fix and
rejects quickly with the fix in place.
This only changes what happens when the server process exits before the
pipe/socket connection is established - normal startup and shutdown
behavior (including the process 'exit' logging in the finally() block)
is unaffected, since the added listener is a no-op once the connection
promise has already settled.
Report: microsoft#1586
Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
Assisted-by: claude-sonnet-5 (via Claude Code)
Address review feedback: add dispose() to the jsonrpc client PipeTransport/SocketTransport, call it from waitForConnection when the child exits before connecting and on the spawn-failure early returns, and add jsonrpc tests that the port/pipe path is released. Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
cdcff7e to
101ba96
Compare
| * other side never connects (e.g. the server process exited early). | ||
| */ | ||
| close(): void; | ||
| dispose(): void; |
| * other side never connects (e.g. the server process exited early). | ||
| */ | ||
| close(): void; | ||
| dispose(): void; |
Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
506c0c9 to
bdfb867
Compare
Renaming PipeTransport/SocketTransport close() to dispose() broke the API that main already exports (and that is re-exported through the protocol and client node entry points). Revert jsonrpc to main's close() and its tests, and call close() from waitForConnection instead. waitForConnection now also removes its 'exit' and 'error' listeners from the child process as soon as the connection is established or fails, so a server exiting normally later no longer runs them. Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-opus-5-5 (via Claude Code)
|
I've addressed the Copilot review comments:
Checked locally: |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The socket regression test does not exercise the executable-command path reported in issue #1586.
Review effort: Balanced
Findings: 3
Open (3)
Resolved since last review (1)
|
Pujitha Paladugu (@pujitha24) another review from Copilot on the lastest changes you made. |
…before connecting Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
|
The last Copilot comment (test only hit the forked NodeModule branch) is covered by 31ad450: there's now a socket and a pipe test that use |
|
/azp run |
|
No pipelines are associated with this pull request. |


Motivation:
When using TransportKind.pipe or TransportKind.socket, the client creates
a pipe/socket server and waits for the spawned server process to connect
to it via transport.onConnected(). That promise (created in
createClientPipeTransport / createClientSocketTransport in
jsonrpc/src/node/main.ts) is only ever resolved from inside the
connection callback - it has no reject path. If the server process exits
before connecting (for example because it fails to start, or exits with
a nonzero code), the promise never settles and LanguageClient#start()
hangs forever instead of failing.
Approach:
Add a small waitForConnection() helper in client/src/node/main.ts that
races transport.onConnected() against the spawned child process's 'exit'
event, rejecting with a descriptive error if the process exits first.
Use it at all six pipe/socket call sites in createMessageTransports
(module+runtime, forked module, and Executable/command branches), in
place of the bare transport.onConnected().then(...) calls.
Validation:
textDocument, server, client and client-node-tests after
npm run symlink.pre-existing, unrelated eslint-plugin deprecation warning).
('Socket transport' and new 'Pipe transport' suites) using a new
minimal server script (servers/exitBeforeConnectServer.ts) that exits
immediately without ever connecting, mirroring the reporter's repro.
Ran via
npm testin client-node-tests (real VS Code Insiders +Electron extension host, @vscode/test-electron): before the fix the
new socket test times out after 5s (client.start() never settles,
reproducing the reported hang exactly); after the fix both new tests
pass, and the full existing suite (207 tests) passes with no
regressions.
script using the repo's own built jsonrpc createClientSocketTransport
plus a real spawned process that exits non-zero without connecting,
confirming transport.onConnected() never settles without the fix and
rejects quickly with the fix in place.
This only changes what happens when the server process exits before the
pipe/socket connection is established - normal startup and shutdown
behavior (including the process 'exit' logging in the finally() block)
is unaffected, since the added listener is a no-op once the connection
promise has already settled.
Report: #1586
Signed-off-by: Pujitha Paladugu 10557236+pujitha24@users.noreply.github.com
Assisted-by: claude-sonnet-5 (via Claude Code)
Fixes #1586