fix(lsp): make transport recovery ownership-safe

This commit is contained in:
Tianyi Cui
2026-07-22 15:56:29 +08:00
parent 2a8e7c661d
commit 769710cfb9
10 changed files with 94 additions and 35 deletions

View File

@@ -52,7 +52,7 @@ export type ConnectionWriter = (
export interface ProcessTreeOperations {
/** Signal a POSIX process group. */
readonly signal: (target: number, signal: NodeJS.Signals) => void
/** Signal the direct child when group/tree signalling is unavailable. */
/** Signal the direct child when POSIX group signaling is unavailable. */
readonly killChild: (signal: NodeJS.Signals) => void
/** Terminate a Windows process tree by root pid. */
readonly taskkill: (pid: number) => void
@@ -78,6 +78,9 @@ export type ProcessSignalRunner = (target: number, signal: NodeJS.Signals) => bo
const processSignalRunner: ProcessSignalRunner = process.kill.bind(process)
/** taskkill status for "process not found": the requested process tree is already absent. */
const TASKKILL_TREE_NOT_FOUND_STATUS = 128
const writeConnectionMessage: ConnectionWriter = (stdin, message, done) => {
stdin.write(encodeMessage(message), done)
}
@@ -93,6 +96,7 @@ export function taskkillProcessTree(
): void {
const result = run('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' })
if (result.error !== undefined) throw result.error
if (result.status === TASKKILL_TREE_NOT_FOUND_STATUS) return
if (result.status !== 0) throw new Error(`taskkill exited with status ${String(result.status)}`)
}
@@ -130,7 +134,8 @@ export async function waitForTreeExit(
}
/**
* Signal a detached process tree with platform-correct semantics and a direct-child fallback.
* Signal a detached process tree with platform-correct semantics. POSIX falls back to the direct
* child; Windows requires taskkill to reach the full tree.
* @param platform - host platform.
* @param pid - detached root process id.
* @param signal - requested termination signal.
@@ -142,9 +147,12 @@ export function signalProcessTree(
signal: NodeJS.Signals,
operations: ProcessTreeOperations,
): void {
if (platform === 'win32') {
operations.taskkill(pid)
return
}
try {
if (platform === 'win32') operations.taskkill(pid)
else operations.signal(-pid, signal)
operations.signal(-pid, signal)
} catch {
try {
operations.killChild(signal)
@@ -219,6 +227,15 @@ export class LspConnection {
return this.closeReason !== undefined
}
/**
* Test whether a caught error is this connection's retained fatal transport cause.
* @param error - error caught by the instance or provider.
* @returns `true` only when this connection produced that exact failure.
*/
failedWith(error: unknown): boolean {
return this.closeReason === error
}
/**
* Send a request and await its result.
* @param method - the JSON-RPC method.
@@ -291,10 +308,7 @@ export class LspConnection {
return await waitForTreeExit(this.processTreeAlive.bind(this), signal)
}
/**
* Signal the whole process tree so helper processes are reached; fall back to the direct child if
* tree signaling fails. Never throws because teardown races process exit.
*/
/** Signal the whole process tree. */
private signalTree(sig: NodeJS.Signals): void {
const pid = this.child.pid
if (pid === undefined) return