index.ts 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. /**
  2. * Local implementation of the subprocess seam. Each spawn is a detached
  3. * process tree with the spec's per-stream stdio dispositions; disposal
  4. * terminates and joins live trees. It has no config: every disposition and
  5. * limit arrives on the spec, so the deployment-varying choices stay with the
  6. * calling seam's config (the bash executor's, the LSP host's, …).
  7. * @module @deepseek-ai/dsh-subprocess-local
  8. */
  9. import { Context } from 'cordis'
  10. import { SubprocessService } from '@deepseek-ai/dsh-subprocess'
  11. import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
  12. import { spawnSubprocess } from './spawn.ts'
  13. import type { SpawnInternals } from './spawn.ts'
  14. /**
  15. * Local subprocess service: detached process trees, Node-shaped stdio
  16. * dispositions (raw pipes, inherit, bounded tail-keep collection with spill
  17. * files), credential-scrubbed environment, and tree-scoped signalling with
  18. * SIGTERM→grace→SIGKILL escalation.
  19. */
  20. export class LocalSubprocessService extends SubprocessService {
  21. /** Live handles retained only so disposal can terminate and join them. */
  22. private live = new Set<SubprocessHandle>()
  23. /** Test seam: spill and platform knobs forwarded to spawnSubprocess. */
  24. internals: SpawnInternals = {}
  25. constructor(ctx: Context) {
  26. super(ctx)
  27. ctx.effect(() => async () => {
  28. // Terminate (escalating), then await WHOLE-TREE exit — not just the
  29. // direct child's settlement — so even a TERM-trapping descendant cannot
  30. // outlive the fiber.
  31. const pending: Promise<unknown>[] = []
  32. for (const handle of this.live) {
  33. handle.terminate()
  34. // Spawn-failure rejections already settled and left the live set.
  35. pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
  36. }
  37. this.live.clear()
  38. await Promise.all(pending)
  39. }, 'local subprocess teardown')
  40. }
  41. spawn(spec: SubprocessSpawnSpec): SubprocessHandle {
  42. const handle = spawnSubprocess(spec, this.internals)
  43. this.live.add(handle)
  44. // Release ownership only once the whole TREE is gone, not at direct-child
  45. // settlement — a TERM-trapping helper that outlives the leader must stay
  46. // owned so teardown can still escalate it. For the common no-survivor
  47. // case waitForExit resolves immediately after settlement.
  48. const release = (): Promise<void> =>
  49. handle.waitForExit().then(() => { this.live.delete(handle) })
  50. handle.done.then(release, release)
  51. return handle
  52. }
  53. }
  54. export default LocalSubprocessService