(A1) terminate()/dispose()/service teardown keyed on direct-child settlement could leak a TERM-trapping descendant that outlived the leader (Codex reproduced it with a disowned trap-SIGTERM helper). kill()/terminate() now gate on tree liveness instead of outcome settlement; the SIGKILL escalation timer survives settle (unref'd, re-probing the tree); dispose's tier quiescence is whole-tree exit via a bounded waitForExit; the service's live set releases handles only when their tree is gone, and its teardown awaits tree exit. Three new suites pin the survivor scenarios end to end (terminate, dispose, service teardown). (A2) the escalation branch is now real tested behavior — its ignore is gone; the one remaining signalTree guard ignore states why it is unreachable through the handle verbs. (A3) docs contradictions fixed: the impl README's stale POSIX-only bullet now states the contained best-effort Windows tree story; the lsp-local README no longer claims taskkill failures stay visible (containment + the tree-liveness wait is the actual contract); the architecture tables (en+zh) list all three consumer families. (B1) OutputCollector keeps a byte-exact tail across uneven chunk boundaries (trim the head chunk instead of dropping it whole) — the LSP diagnostic-tail contract; pinned by a cross-chunk test. (B2) the subagent-acp coverage ignore is narrowed to exactly the never-settling success arm.
60 lines
2.5 KiB
TypeScript
60 lines
2.5 KiB
TypeScript
/**
|
|
* Local implementation of the subprocess seam. Each spawn is a detached
|
|
* process tree with the spec's per-stream stdio dispositions; disposal
|
|
* terminates and joins live trees. It has no config: every disposition and
|
|
* limit arrives on the spec, so the deployment-varying choices stay with the
|
|
* calling seam's config (the bash executor's, the LSP host's, …).
|
|
* @module @deepseek-ai/dsh-subprocess-local
|
|
*/
|
|
|
|
import { Context } from 'cordis'
|
|
import { SubprocessService } from '@deepseek-ai/dsh-subprocess'
|
|
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
|
import { spawnSubprocess } from './spawn.ts'
|
|
import type { SpawnInternals } from './spawn.ts'
|
|
|
|
/**
|
|
* Local subprocess service: detached process trees, Node-shaped stdio
|
|
* dispositions (raw pipes, inherit, bounded tail-keep collection with spill
|
|
* files), credential-scrubbed environment, tree-scoped signalling with
|
|
* SIGTERM→grace→SIGKILL escalation, and the cooperative dispose ladder.
|
|
*/
|
|
export class LocalSubprocessService extends SubprocessService {
|
|
/** Live handles retained only so disposal can terminate and join them. */
|
|
private live = new Set<SubprocessHandle>()
|
|
/** Test seam: spill and platform knobs forwarded to spawnSubprocess. */
|
|
internals: SpawnInternals = {}
|
|
|
|
constructor(ctx: Context) {
|
|
super(ctx)
|
|
ctx.effect(() => async () => {
|
|
// Terminate (escalating), then await WHOLE-TREE exit — not just the
|
|
// direct child's settlement — so even a TERM-trapping descendant cannot
|
|
// outlive the fiber.
|
|
const pending: Promise<unknown>[] = []
|
|
for (const handle of this.live) {
|
|
handle.terminate()
|
|
// Spawn-failure rejections already settled and left the live set.
|
|
pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
|
|
}
|
|
this.live.clear()
|
|
await Promise.all(pending)
|
|
}, 'local subprocess teardown')
|
|
}
|
|
|
|
spawn(spec: SubprocessSpawnSpec): SubprocessHandle {
|
|
const handle = spawnSubprocess(spec, this.internals)
|
|
this.live.add(handle)
|
|
// Release ownership only once the whole TREE is gone, not at direct-child
|
|
// settlement — a TERM-trapping helper that outlives the leader must stay
|
|
// owned so teardown can still escalate it. For the common no-survivor
|
|
// case waitForExit resolves immediately after settlement.
|
|
const release = (): Promise<void> =>
|
|
handle.waitForExit().then(() => { this.live.delete(handle) })
|
|
handle.done.then(release, release)
|
|
return handle
|
|
}
|
|
}
|
|
|
|
export default LocalSubprocessService
|