fix(subprocess): keep awaited tree waits and the pending SIGKILL ref'd (Codex round 2)

An awaited waitForExit()/dispose() must hold the event loop open until the
tree really exits: with the liveness tick and the escalation timer unref'd,
a parent with no other live handles could exit claiming quiescence and
orphan the survivors it promised to reap. The escalation's pending SIGKILL
is a commitment; it self-bounds at graceMs. Module graph picks up the
subprocess-local -> timeout edge.
This commit is contained in:
Tianyi Cui
2026-07-26 20:34:57 +08:00
parent 79a28ad6d9
commit f2972e846d
2 changed files with 13 additions and 6 deletions

View File

@@ -66,9 +66,14 @@ export interface SpawnInternals {
/** Timeout code marking a dispose-ladder tier bound (vs an external abort). */
const DISPOSE_TIER_TIMEOUT = 'SUBPROCESS_DISPOSE_TIER'
/** Liveness-poll cadence for tree-exit waits; unref'd so an abandoned wait cannot hold the parent's loop open. */
/**
* Liveness-poll cadence for tree-exit waits. The timer stays ref'd: an
* awaited teardown must keep the event loop alive until the tree really
* exits, or the parent can exit while claiming quiescence and orphan the
* survivors it promised to reap.
*/
function sleepTick(): Promise<void> {
return sleepMs(15, undefined, { ref: false })
return sleepMs(15)
}
let spillCounter = 0
@@ -401,12 +406,13 @@ export function spawnSubprocess(spec: SubprocessSpawnSpec, internals: SpawnInter
if (!treeAlive()) return
signalTree(platform, pid, 'SIGTERM', child, taskkill)
// The escalation must survive direct-child settlement — the leader dying
// does not mean the tree died — so the timer is unref'd rather than
// cleared at settle, and re-checks tree liveness before force-killing.
// 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)
graceTimer.unref()
}
// The caller owns timeout classification; this layer only reacts to abort.