Replies: 2 comments
|
Verified against v0.1.1-rc.2 source — the asymmetry is exact, and your one-line fix is the right minimal change. I traced the surrounding lifecycle to confirm nothing else needs to move with it. Source confirmation (packages/lsp/lsp-stdio/src/connection.ts)
One-line fix: mirror the stdin line — attach the same error-to-fail handler on stdout next to the data handler. Same semantics as the documented stdin handler; the fail path is idempotent against the later close path (the closeReason latch at lines 113-116 means a second fail after close is a no-op, so no double-eviction concern). Regression test shape: attach a fake subprocess handle whose stdout is a PassThrough; emit an error on it; assert the connection failed flag flips and pending requests reject with that error before any close event. Also assert a subsequent close does not double-evict. Family: this is the second member of the connection-lifecycle family I documented with #4059 (the upgrade-socket error listener removed inside the close callback, packages/host/webserver/src/index.ts:202-204 — a late ECONNRESET with zero listeners terminates the whole dsh web process). Same root shape: a stream or socket whose error path is unlistened, converting a contained transport failure into a whole-process crash. The #4059 fix there is also about two lines (keep the listener plus a destroyed guard); together they suggest a sweep — any other stdout/stderr pipe consumers that attach only a data handler without a sibling error handler are candidates for the same crash class. Nice catch on the stdin/stdout asymmetry — the existing comment at lines 127-129 states the design intent, so the fix is strictly finishing the intended behavior. |
|
I implemented and verified a minimal reference fix for this report:
The production change attaches the missing stdout The deterministic regression first failed on the unhandled emitted stdout error, then passed with the fix. It also verifies that pending, future, and post-close requests retain the original error. Validation passed: 22/22 focused connection tests; 151 passed / 2 skipped related tests; 100% statements, branches, functions, and lines for |
Uh oh!
There was an error while loading. Please reload this page.
Bug: LSP server stdout has no
errorlistener — one transport failure crashes the whole host processPackage:
@deepseek-ai/dsh-lsp-stdioSeverity: medium-high (one language server's pipe failure takes down every session in the process)
Confidence: the stdin/stdout asymmetry is certain; the triggering OS error is platform/timing-dependent (EPIPE/EBADF-class reads)
Problem
packages/lsp/lsp-stdio/src/connection.tsdeliberately attaches anerrorhandler to the child's stdin and documents why:The stdout stream gets only a
datahandler — noerrorhandler. In Node, an'error'event on a stream with no listener is raised as an uncaught exception.I verified the exposure chain end to end:
child.stdoutfor pipe mode with nothing attached:packages/subprocess/subprocess-local/src/spawn.ts(stdout: outMode === 'pipe' ? child.stdout ?? undefined : undefined;collectStreamonly wraps collect-mode streams).app-bootregisters onlyunhandledRejection(packages/boot/app-boot/src/index.ts), notuncaughtException. A stream'error'becomes exactly the unhandled kind.fail()→failAll()→ pool eviction — but that containment never runs, because the crash happens at the raw stream layer below it.Failure scenario
A language server closes its stdout fd while staying alive briefly (or a tree-kill escalation races the reader; most plausible on Windows or under load). Node emits
'error'(EPIPE/EBADF) onchild.stdout, no listener exists → uncaught exception → the entire harness process dies, taking every other live session, agent, and language-server connection with it. The exact outcomeLspConnection.fail()/ instance teardown exists to contain.Suggested fix
Mirror the stdin treatment:
so a read-side failure degrades to failing one connection (rejecting its pending requests and evicting it from the pool) instead of killing the host. One line, same semantics as the existing stdin handler.
All reactions