mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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:
@@ -392,11 +392,12 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
|
||||
}
|
||||
}
|
||||
|
||||
const kill = (sig: NodeJS.Signals = 'SIGTERM'): void => {
|
||||
// Guard on TREE liveness, not outcome settlement: a TERM-trapping helper
|
||||
// can outlive the settled direct child and must stay signalable, while a
|
||||
// fully-dead tree (possible pid reuse) must not be re-signalled from a
|
||||
// caller's finally block.
|
||||
// The dispose ladder's tier primitive (not on the handle — terminate() is
|
||||
// the only consumer-facing termination verb). Guards on TREE liveness, not
|
||||
// outcome settlement: a TERM-trapping helper can outlive the settled direct
|
||||
// child and must stay signalable, while a fully-dead tree (possible pid
|
||||
// reuse) must not be re-signalled by a later tier.
|
||||
const kill = (sig: NodeJS.Signals): void => {
|
||||
if (!treeAlive()) return
|
||||
signalTree(platform, pid, sig, child, taskkill)
|
||||
}
|
||||
@@ -404,15 +405,13 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
|
||||
const terminate = (): void => {
|
||||
if (graceTimer !== undefined) return // escalation already in flight
|
||||
if (!treeAlive()) return
|
||||
signalTree(platform, pid, 'SIGTERM', child, taskkill)
|
||||
kill('SIGTERM')
|
||||
// The escalation must survive direct-child settlement — the leader dying
|
||||
// does not mean the tree died — so settle does not clear this timer, and
|
||||
// it re-probes tree liveness before force-killing. It stays ref'd: the
|
||||
// pending SIGKILL is a commitment, and a parent exiting before it fires
|
||||
// would orphan a trapped survivor. Self-bounds at graceMs.
|
||||
graceTimer = setTimeout(() => {
|
||||
if (treeAlive()) signalTree(platform, pid, 'SIGKILL', child, taskkill)
|
||||
}, spec.graceMs)
|
||||
// kill() re-probes tree liveness before force-killing. It stays ref'd:
|
||||
// the pending SIGKILL is a commitment, and a parent exiting before it
|
||||
// fires would orphan a trapped survivor. Self-bounds at graceMs.
|
||||
graceTimer = setTimeout(() => { kill('SIGKILL') }, spec.graceMs)
|
||||
}
|
||||
|
||||
// The caller owns timeout classification; this layer only reacts to abort.
|
||||
@@ -514,7 +513,6 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
|
||||
...stderrCollector !== undefined ? { stderr: stderrCollector } : {},
|
||||
},
|
||||
done,
|
||||
kill,
|
||||
terminate,
|
||||
waitForExit,
|
||||
dispose,
|
||||
|
||||
Reference in New Issue
Block a user