refactor(subprocess): keep terminate() as the seam's only termination verb
Delete kill(signal?) from SubprocessHandle: consumers stop a process only through terminate()'s tree-scoped SIGTERM→graceMs→SIGKILL escalation (idempotent, also driven by the spec's abort signal, a no-op once the tree is gone). The single-signal verb had exactly one consumer family — lsp-local — and what it bought there was a private re-implementation of the same escalation. The internal kill closure stays in spawn.ts as the dispose ladder's tier primitive; terminate() now routes through it too. lsp-local collapses onto the seam's escalation: - LspConnection replaces its terminate()/kill() pair with one terminate() that delegates to handle.terminate(). Behavior change: the framing-failure path terminates instead of instant SIGKILL, so a misbehaving server now gets SIGTERM plus the killGraceMs window to flush before SIGKILL. - ConnectionSpec.pipeDrainGraceMs becomes killGraceMs: one grace, the spawn spec's graceMs, drives both the escalation window and post-exit pipe draining (the provider already passed killGraceMs for it). - LspInstance.forceTerminate() drops its hand-rolled bounded first wait (LSP_KILL_GRACE) and escalateProcessTree (deleted with its export and unit test): the seam's escalation already commits to SIGKILL after killGraceMs, so only the unbounded quiescence awaits stay load-bearing. Tests: kill()-shaped spawn specs become terminate()-shaped or fold into the terminate() suites (group-wide delivery; the settled no-op case was already pinned by 'terminate() after the tree died'); tree-survivor coverage is intact. A stderr-'inherit' disposition test completes the stdout/stderr symmetry so the scoped subprocess+lsp coverage gate stands alone instead of leaning on subagent-acp's cross-package runs. Docs: SubprocessHandle type-equiv block, seam/impl/group READMEs, and the consumer-migration Agent Note lose the kill(signal?) vocabulary (zh pairs re-recorded); cordis api/services catalogs regenerated.
This commit is contained in:
@@ -30,11 +30,11 @@ export interface ConnectionSpec {
|
||||
/** Largest stderr tail retained for diagnostics. */
|
||||
readonly maxStderrBytes: number
|
||||
/**
|
||||
* Bound (ms) for draining pipes a surviving helper still holds after the
|
||||
* server exits; the instance passes its kill grace so exit observation is
|
||||
* never slower than the escalation it feeds.
|
||||
* The subprocess spec's `graceMs`: the SIGTERM→SIGKILL window of
|
||||
* {@link LspConnection.terminate}'s escalation, and the bound for draining
|
||||
* pipes a surviving helper still holds after the server exits.
|
||||
*/
|
||||
readonly pipeDrainGraceMs: number
|
||||
readonly killGraceMs: number
|
||||
/** Static answer to every `workspace/configuration` item. */
|
||||
readonly configuration: unknown
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export class LspConnection {
|
||||
stdout: 'pipe',
|
||||
stderr: { maxBytes: spec.maxStderrBytes },
|
||||
},
|
||||
graceMs: spec.pipeDrainGraceMs,
|
||||
graceMs: spec.killGraceMs,
|
||||
// spec.env mixes the scrubbed base with explicit config entries; a
|
||||
// configured DSH_* fact takes the managed channel the seam reserves.
|
||||
...splitEnvChannels(spec.env),
|
||||
@@ -210,14 +210,9 @@ export class LspConnection {
|
||||
return this.nextId
|
||||
}
|
||||
|
||||
/** Request termination of the server's process tree (SIGTERM, no escalation). */
|
||||
/** Terminate the server's process tree (the seam's SIGTERM→grace→SIGKILL escalation; idempotent). */
|
||||
terminate(): void {
|
||||
this.handle.kill('SIGTERM')
|
||||
}
|
||||
|
||||
/** Force termination of the server's process tree. */
|
||||
kill(): void {
|
||||
this.handle.kill('SIGKILL')
|
||||
this.handle.terminate()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,9 +230,10 @@ export class LspConnection {
|
||||
messages = this.decoder.push(chunk)
|
||||
} catch (error) {
|
||||
// A framing/JSON failure corrupts the stream position irrecoverably: fail the instance and
|
||||
// SIGKILL the whole group so helper processes don't outlive the leader.
|
||||
// terminate the whole group so helper processes don't outlive the leader (SIGTERM first, then
|
||||
// the kill grace's SIGKILL — a misbehaving server still gets its bounded flush window).
|
||||
this.fail(asError(error))
|
||||
this.handle.kill('SIGKILL')
|
||||
this.handle.terminate()
|
||||
return
|
||||
}
|
||||
for (const message of messages) this.dispatch(message)
|
||||
|
||||
Reference in New Issue
Block a user