2026-07-26 06:59:01 +08:00
|
|
|
/**
|
2026-07-26 14:07:42 +08:00
|
|
|
* 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, …).
|
2026-07-26 12:43:14 +08:00
|
|
|
* @module @deepseek-ai/dsh-subprocess-local
|
2026-07-26 06:59:01 +08:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { Context } from 'cordis'
|
2026-07-26 12:43:14 +08:00
|
|
|
import { SubprocessService } from '@deepseek-ai/dsh-subprocess'
|
|
|
|
|
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
2026-07-26 14:07:42 +08:00
|
|
|
import { spawnSubprocess } from './spawn.ts'
|
2026-07-26 06:59:01 +08:00
|
|
|
import type { SpawnInternals } from './spawn.ts'
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-26 14:07:42 +08:00
|
|
|
* Local subprocess service: detached process trees, Node-shaped stdio
|
|
|
|
|
* dispositions (raw pipes, inherit, bounded tail-keep collection with spill
|
2026-07-27 03:52:41 +08:00
|
|
|
* files), credential-scrubbed environment, and tree-scoped signalling with
|
|
|
|
|
* SIGTERM→grace→SIGKILL escalation.
|
2026-07-26 06:59:01 +08:00
|
|
|
*/
|
2026-07-26 12:43:14 +08:00
|
|
|
export class LocalSubprocessService extends SubprocessService {
|
2026-07-26 14:07:42 +08:00
|
|
|
/** Live handles retained only so disposal can terminate and join them. */
|
2026-07-26 12:43:14 +08:00
|
|
|
private live = new Set<SubprocessHandle>()
|
2026-07-26 14:07:42 +08:00
|
|
|
/** Test seam: spill and platform knobs forwarded to spawnSubprocess. */
|
2026-07-26 06:59:01 +08:00
|
|
|
internals: SpawnInternals = {}
|
|
|
|
|
|
|
|
|
|
constructor(ctx: Context) {
|
|
|
|
|
super(ctx)
|
|
|
|
|
ctx.effect(() => async () => {
|
2026-07-26 16:50:36 +08:00
|
|
|
// Terminate (escalating), then await WHOLE-TREE exit — not just the
|
|
|
|
|
// direct child's settlement — so even a TERM-trapping descendant cannot
|
|
|
|
|
// outlive the fiber.
|
2026-07-26 06:59:01 +08:00
|
|
|
const pending: Promise<unknown>[] = []
|
|
|
|
|
for (const handle of this.live) {
|
2026-07-26 14:07:42 +08:00
|
|
|
handle.terminate()
|
2026-07-26 06:59:01 +08:00
|
|
|
// Spawn-failure rejections already settled and left the live set.
|
2026-07-26 16:50:36 +08:00
|
|
|
pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
|
2026-07-26 06:59:01 +08:00
|
|
|
}
|
|
|
|
|
this.live.clear()
|
|
|
|
|
await Promise.all(pending)
|
2026-07-26 12:43:14 +08:00
|
|
|
}, 'local subprocess teardown')
|
2026-07-26 06:59:01 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-26 12:43:14 +08:00
|
|
|
spawn(spec: SubprocessSpawnSpec): SubprocessHandle {
|
2026-07-26 14:07:42 +08:00
|
|
|
const handle = spawnSubprocess(spec, this.internals)
|
2026-07-26 06:59:01 +08:00
|
|
|
this.live.add(handle)
|
2026-07-26 16:50:36 +08:00
|
|
|
// 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)
|
2026-07-26 06:59:01 +08:00
|
|
|
return handle
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 12:43:14 +08:00
|
|
|
export default LocalSubprocessService
|