test(windows): cover teardown decisions deterministically

This commit is contained in:
Tianyi Cui
2026-07-22 14:34:26 +08:00
parent 5da343126c
commit 7b6b544243
5 changed files with 54 additions and 11 deletions

View File

@@ -110,6 +110,25 @@ export function signalProcessGroup(
run(target, signal)
}
/**
* Wait until a process-tree liveness probe reports exit.
* @param isAlive - process-tree liveness probe.
* @param signal - optional bound for the wait.
* @param yieldNow - event-loop yield primitive.
* @returns `true` when the tree exited, or `false` when the signal aborted first.
*/
export async function waitForTreeExit(
isAlive: () => boolean,
signal?: AbortSignal,
yieldNow: () => Promise<unknown> = yieldToEventLoop,
): Promise<boolean> {
while (isAlive()) {
if (signal?.aborted) return false
await yieldNow()
}
return true
}
/**
* Signal a detached process tree with platform-correct semantics and a direct-child fallback.
* @param platform - host platform.
@@ -269,11 +288,7 @@ export class LspConnection {
* @returns `true` when the tree exited, or `false` when the signal aborted first.
*/
async waitForProcessTreeExit(signal?: AbortSignal): Promise<boolean> {
while (this.processTreeAlive()) {
if (signal?.aborted) return false
await yieldToEventLoop()
}
return true
return await waitForTreeExit(this.processTreeAlive.bind(this), signal)
}
/**

View File

@@ -221,10 +221,6 @@ class LocalLspProvider implements LspProvider {
// synchronous get-or-create so every spawned process remains owned by teardown.
this.assertActive(signal)
let instance = this.instanceFor(workspace)
if (instance.dead) {
this.evictIfCurrent(workspace, instance)
instance = this.instanceFor(workspace)
}
try {
return await instance.query(request, source, signal)
} catch (error) {

View File

@@ -39,6 +39,15 @@ export interface InstanceSpec extends ConnectionSpec {
readonly killGraceMs: number
}
/**
* Force-kill a process tree only when graceful termination did not make it exit.
* @param treeExited - whether the tree exited within its grace period.
* @param forceKill - forceful process-tree termination primitive.
*/
export function escalateProcessTree(treeExited: boolean, forceKill: () => void): void {
if (!treeExited) forceKill()
}
/**
* A single initialized server process. Not exported as a provider — the provider single-flights and
* pools these. `query()` serializes; `dispose()` rejects queued work and tears the process down.
@@ -297,7 +306,7 @@ export class LspInstance {
} finally {
graceDeadline[Symbol.dispose]()
}
if (!treeExited) this.connection.kill()
escalateProcessTree(treeExited, this.connection.kill.bind(this.connection))
await Promise.all([
this.connection.closed,
this.connection.waitForProcessTreeExit(),