process.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. /** Node child execution over an inherited control channel; no Harness services run here. */
  2. import type { Duplex } from 'node:stream'
  3. import { JsonChannel } from './channel.ts'
  4. import { runProgram } from './bootstrap.ts'
  5. import { STARTUP_ENVIRONMENT_NAMES } from './environment.ts'
  6. import type { PatchableStream } from './bootstrap.ts'
  7. import type { ReplyMessage, ProgramBootData, ProgramToHost } from './protocol.ts'
  8. /** Process-owned environment and output streams consumed by the child bootstrap. */
  9. export interface ProgramProcess {
  10. env: NodeJS.ProcessEnv
  11. stdout: PatchableStream
  12. stderr: PatchableStream
  13. exitCode: string | number | null | undefined
  14. }
  15. /**
  16. * Run one host-supplied program after the control handshake.
  17. * @param stream - Inherited, already-adopted control endpoint.
  18. * @param maxMessageBytes - Host-validated maximum frame and queued-write bytes.
  19. * @param processState - Environment, output streams and exit status of this Node child.
  20. * @returns After control output flushes and host shutdown is observed, or transport failure closes the channel.
  21. */
  22. export async function runNodeMain(stream: Duplex, maxMessageBytes: number, processState: ProgramProcess): Promise<void> {
  23. if (!Number.isSafeInteger(maxMessageBytes) || maxMessageBytes <= 0 || maxMessageBytes > 0xffff_ffff) throw new Error('invalid control message limit')
  24. for (const key of Object.keys(processState.env)) {
  25. if (!STARTUP_ENVIRONMENT_NAMES.has(key.toUpperCase())) Reflect.deleteProperty(processState.env, key)
  26. }
  27. // Windows native process creation still needs SystemRoot in the OS environment.
  28. processState.env = Object.create(null) as NodeJS.ProcessEnv
  29. const boot = Promise.withResolvers<ProgramBootData>()
  30. const listeners: Array<(message: ReplyMessage) => void> = []
  31. let started = false
  32. let failed = false
  33. let terminalSent = false
  34. const hostClosed = Promise.withResolvers<void>()
  35. const onClose = (): void => { hostClosed.resolve() }
  36. stream.once('close', onClose)
  37. const channel = new JsonChannel(stream, maxMessageBytes, (raw) => {
  38. if (!started) {
  39. started = true
  40. if (typeof raw !== 'object' || raw === null || (raw as { type?: unknown }).type !== 'boot') {
  41. boot.reject(new Error('expected program boot frame'))
  42. return
  43. }
  44. boot.resolve((raw as { data: ProgramBootData }).data)
  45. return
  46. }
  47. if (terminalSent) return
  48. for (const listener of listeners) listener(raw as ReplyMessage)
  49. }, (error, kind) => {
  50. if (!terminalSent || kind === 'protocol') {
  51. failed = true
  52. boot.reject(error)
  53. channel.close()
  54. processState.exitCode = 1
  55. }
  56. hostClosed.resolve()
  57. })
  58. const pending = new Set<Promise<void>>()
  59. const send = (message: ProgramToHost): void => {
  60. if (terminalSent) return
  61. if (message.type === 'done') terminalSent = true
  62. const task = channel.send(message).catch(() => {
  63. failed = true
  64. channel.close()
  65. processState.exitCode = 1
  66. hostClosed.resolve()
  67. }).finally(() => { pending.delete(task) })
  68. pending.add(task)
  69. }
  70. try {
  71. await channel.send({ type: 'ready' })
  72. const data = await boot.promise
  73. await runProgram({
  74. postMessage: send,
  75. on: (_event, listener) => { listeners.push(listener) },
  76. }, data, { stdout: processState.stdout, stderr: processState.stderr })
  77. while (pending.size > 0) await Promise.all(pending)
  78. await channel.drain()
  79. // Host binding replies can race the terminal frame; the host owns channel shutdown.
  80. await hostClosed.promise
  81. } finally {
  82. stream.off('close', onClose)
  83. channel.close()
  84. // Transport callbacks can set failed while the awaited program executes.
  85. // oxlint-disable-next-line typescript/no-unnecessary-condition
  86. if (failed) processState.exitCode = 1
  87. }
  88. }