From a516d80c719ca2ceab8ccea2921425e0f16a49be Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Wed, 29 Apr 2026 16:18:27 -0400 Subject: [PATCH] fix(cli): harden no-daemon teardown --- cli.ts | 8 ++++---- src/client.ts | 21 ++++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/cli.ts b/cli.ts index 76a611b..003f07b 100755 --- a/cli.ts +++ b/cli.ts @@ -79,10 +79,10 @@ async function runInProcess( const result = await runCommand(cmdArg, client, uri, params); process.stdout.write(JSON.stringify(result, null, 2) + "\n"); } finally { - // Fire-And-Forget Shutdown - With `gopls -remote=auto` (and similar) - // the spawned process is a thin client to a background daemon; a - // graceful shutdown can hang the parent. Kick it off but don't wait. - void client.dispose(); + // Hard Teardown - The no-daemon path is short-lived/debug-only. Avoid + // graceful JSON-RPC shutdown because the server stdio stream may already + // be closed, which can surface ERR_STREAM_DESTROYED during process exit. + void client.dispose({ graceful: false }); } } diff --git a/src/client.ts b/src/client.ts index bbf5527..6e7fb28 100644 --- a/src/client.ts +++ b/src/client.ts @@ -252,15 +252,22 @@ export class LspClient { } // Dispose - Best-effort shutdown; kills the process if it doesn't exit. - async dispose(): Promise { + async dispose(options: { graceful?: boolean } = {}): Promise { + const graceful = options.graceful ?? true; if (this.conn) { - try { - await this.conn.sendRequest("shutdown", undefined); - this.conn.sendNotification("exit"); - } catch { - // Ignore - we're tearing down anyway. + if (graceful) { + try { + await this.conn.sendRequest("shutdown", undefined); + this.conn.sendNotification("exit"); + } catch { + // Ignore - we're tearing down anyway. + } + } + try { + this.conn.dispose(); + } catch { + // Ignore - connection may already be closed. } - this.conn.dispose(); } if (this.proc && !this.proc.killed) { this.proc.kill();