process.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * The `process` global the worker needs before any VFS module runs. Cordis
  3. * reads `process.env` and `process.versions.node` while the Loader is
  4. * constructed, and `cordis.yml` keeps its `!!js process.*` expressions, so the
  5. * configuration bytes stay identical to the Node deployment. Third-party Node
  6. * packages use the presence of `process.title` to avoid browser-only globals.
  7. * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/node/globals/process
  8. */
  9. import { requireActiveModuleLoader } from '../../module-system/module-loader.ts'
  10. import { processAlive, signalProcess } from '../process-table.ts'
  11. /** Construction inputs for {@link installProcessGlobal}. */
  12. export interface ProcessShimOptions {
  13. /** Virtual root reported by `cwd()`. */
  14. readonly cwd: string
  15. /** Environment the tree reads; `DSH_HOME` belongs here. */
  16. readonly env: Readonly<Record<string, string>>
  17. /** Argument vector reported to the tree. */
  18. readonly argv?: readonly string[]
  19. }
  20. /** The members this shim publishes. */
  21. export interface ProcessShim {
  22. readonly env: Record<string, string>
  23. readonly argv: string[]
  24. readonly execArgv: string[]
  25. /** Virtual host identity; spawning this path reports ENOENT because Node execution is unavailable. */
  26. readonly execPath: string
  27. /** Node process identity used by dependencies for environment detection. */
  28. readonly title: string
  29. /**
  30. * Node 22 `process.getBuiltinModule`: the worker's module proxy for a
  31. * builtin id (`fs`, `node:fs`), or undefined for anything else — it never
  32. * resolves image modules.
  33. * @param id - Builtin module id, with or without the `node:` prefix.
  34. * @returns the proxied builtin, or undefined.
  35. */
  36. getBuiltinModule(id: string): unknown
  37. readonly platform: string
  38. readonly arch: string
  39. readonly pid: number
  40. readonly version: string
  41. readonly versions: Record<string, string>
  42. cwd(): string
  43. /**
  44. * Signal one command started through the `node:child_process` shim. Signal
  45. * `0` is the liveness probe the subprocess service polls a process tree
  46. * with; a negative pid addresses the group, which here holds exactly the one
  47. * command that leads it.
  48. * @param pid - the target pid, negative for its group.
  49. * @param signal - signal name, or `0` to probe without delivering one.
  50. * @returns true once the signal is recorded.
  51. * @throws Error with `code: 'ESRCH'` when no such command is running.
  52. */
  53. kill(pid: number, signal?: NodeJS.Signals | 0): boolean
  54. nextTick(callback: (...args: unknown[]) => void, ...args: unknown[]): void
  55. readonly stdout: { write(chunk: string): boolean }
  56. readonly stderr: { write(chunk: string): boolean }
  57. on(): ProcessShim
  58. off(): ProcessShim
  59. once(): ProcessShim
  60. prependListener(): ProcessShim
  61. prependOnceListener(): ProcessShim
  62. removeListener(): ProcessShim
  63. removeAllListeners(): ProcessShim
  64. listeners(): unknown[]
  65. listenerCount(): number
  66. setMaxListeners(): ProcessShim
  67. emit(): boolean
  68. readonly hrtime: { bigint(): bigint }
  69. uptime(): number
  70. exit(code?: number): void
  71. }
  72. /**
  73. * Publish `globalThis.process`.
  74. *
  75. * `versions.node` is `0.0.0` on purpose: it makes Cordis's
  76. * `ModuleLoader.fromInternal()` return undefined instead of reaching for Node
  77. * internals, which is what lets the worker install its own module seam.
  78. * @param options - Root, environment, and argument vector.
  79. * @returns The published object, for the module proxy table.
  80. */
  81. export function installProcessGlobal(options: ProcessShimOptions): ProcessShim {
  82. const start = performance.now()
  83. const write = (target: 'log' | 'error') => (chunk: string): boolean => {
  84. console[target](chunk.replace(/\n$/, ''))
  85. return true
  86. }
  87. const shim: ProcessShim = {
  88. env: { ...options.env },
  89. argv: [...(options.argv ?? ['node', 'dsh-webworker'])],
  90. execArgv: [],
  91. execPath: '/dsh/bin/node',
  92. title: 'dsh-webworker',
  93. platform: 'linux',
  94. arch: 'x64',
  95. pid: 1,
  96. version: 'v0.0.0',
  97. versions: { node: '0.0.0' },
  98. cwd: () => options.cwd,
  99. getBuiltinModule: (id: string): unknown => {
  100. let resolution
  101. try {
  102. resolution = requireActiveModuleLoader().resolve(id, '/')
  103. } catch {
  104. // No loader mounted yet, or an id that resolves nowhere: Node answers
  105. // undefined for non-builtins instead of throwing.
  106. return undefined
  107. }
  108. return resolution.kind === 'static' ? resolution.factory() : undefined
  109. },
  110. kill: (pid: number, signal: NodeJS.Signals | 0 = 'SIGTERM'): boolean => {
  111. if (signal === 0) {
  112. if (processAlive(pid)) return true
  113. const error = new Error('kill ESRCH') as NodeJS.ErrnoException
  114. error.code = 'ESRCH'
  115. error.syscall = 'kill'
  116. throw error
  117. }
  118. return signalProcess(pid, signal)
  119. },
  120. nextTick: (callback, ...args) => { queueMicrotask(() => { callback(...args) }) },
  121. stdout: { write: write('log') },
  122. stderr: { write: write('error') },
  123. on: () => shim,
  124. off: () => shim,
  125. once: () => shim,
  126. prependListener: () => shim,
  127. prependOnceListener: () => shim,
  128. removeListener: () => shim,
  129. removeAllListeners: () => shim,
  130. listeners: () => [],
  131. listenerCount: () => 0,
  132. setMaxListeners: () => shim,
  133. emit: () => false,
  134. hrtime: { bigint: () => BigInt(Math.round((performance.now() - start) * 1e6)) },
  135. uptime: () => (performance.now() - start) / 1000,
  136. exit: (code?: number) => { console.warn(`webworker process: exit(${String(code ?? 0)}) requested; the worker keeps running`) },
  137. }
  138. ;(globalThis as { process?: unknown }).process = shim
  139. return shim
  140. }