@volter/tabnode 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/__sw__.js +259 -5
  2. package/dist/assets/{runtime-worker-DoV8lqBn.js → runtime-worker-B9buriZs.js} +4021 -2961
  3. package/dist/fetch-transport.d.ts +20 -0
  4. package/dist/guest-timers.d.ts +15 -0
  5. package/dist/http-client-transport.d.ts +19 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.mjs +5204 -3057
  8. package/dist/native-stream-binding.d.ts +57 -0
  9. package/dist/native-stream-owner.d.ts +121 -0
  10. package/dist/node-lib/binding/duplex_stream.d.ts +26 -0
  11. package/dist/node-lib/binding/fds.d.ts +8 -0
  12. package/dist/node-lib/binding/fs.d.ts +311 -78
  13. package/dist/node-lib/binding/misc.d.ts +1 -0
  14. package/dist/node-lib/binding/pipe_wrap.d.ts +4 -4
  15. package/dist/node-lib/binding/process_wrap.d.ts +13 -2
  16. package/dist/node-lib/binding/stream_wrap.d.ts +27 -18
  17. package/dist/node-lib/binding/tcp_wrap.d.ts +0 -1
  18. package/dist/node-lib/binding/tty_wrap.d.ts +43 -2
  19. package/dist/node-lib/binding/util.d.ts +83 -0
  20. package/dist/node-lib/binding/uv.d.ts +4 -0
  21. package/dist/node-lib/fs-module.d.ts +1 -0
  22. package/dist/node-lib/internals/buffer-and-streams.d.ts +30 -17
  23. package/dist/node-lib/internals/index.d.ts +1 -1
  24. package/dist/node-lib/load.d.ts +17 -3
  25. package/dist/node-lib/module-hooks.d.ts +1 -0
  26. package/dist/node-lib/public-modules.d.ts +1 -1
  27. package/dist/node-lib/timers.d.ts +38 -0
  28. package/dist/node-process-host.d.ts +41 -0
  29. package/dist/process-registry.d.ts +39 -0
  30. package/dist/process-tokens.d.ts +16 -0
  31. package/dist/promise-ownership.d.ts +3 -0
  32. package/dist/runtime.d.ts +10 -9
  33. package/dist/shims/child_process.d.ts +2 -0
  34. package/dist/shims/crypto.d.ts +660 -119
  35. package/dist/shims/dns.d.ts +76 -2
  36. package/dist/shims/process.d.ts +1 -0
  37. package/dist/shims/string_decoder.d.ts +19 -3
  38. package/dist/shims/tls.d.ts +853 -31
  39. package/dist/source-edits.d.ts +7 -0
  40. package/package.json +1 -1
  41. package/src/code-transforms.ts +2 -3
  42. package/src/fetch-transport.ts +98 -0
  43. package/src/guest-timers.ts +89 -0
  44. package/src/http-client-transport.ts +169 -0
  45. package/src/index.ts +12 -0
  46. package/src/native-stream-binding.ts +393 -0
  47. package/src/native-stream-owner.ts +317 -0
  48. package/src/node-lib/binding/duplex_stream.ts +95 -0
  49. package/src/node-lib/binding/fds.ts +18 -1
  50. package/src/node-lib/binding/fs.ts +45 -17
  51. package/src/node-lib/binding/misc.ts +2 -0
  52. package/src/node-lib/binding/pipe_wrap.ts +35 -12
  53. package/src/node-lib/binding/process_wrap.ts +72 -18
  54. package/src/node-lib/binding/stream_wrap.ts +196 -72
  55. package/src/node-lib/binding/tcp_wrap.ts +43 -8
  56. package/src/node-lib/binding/tty_wrap.ts +135 -8
  57. package/src/node-lib/binding/util.ts +24 -15
  58. package/src/node-lib/binding/uv.ts +4 -0
  59. package/src/node-lib/fs-module.ts +2 -18
  60. package/src/node-lib/internal/navigator.js +149 -0
  61. package/src/node-lib/internals/buffer-and-streams.ts +50 -11
  62. package/src/node-lib/internals/events-util.ts +5 -0
  63. package/src/node-lib/internals/index.ts +12 -2
  64. package/src/node-lib/load.ts +170 -62
  65. package/src/node-lib/module-hooks.ts +5 -2
  66. package/src/node-lib/public-modules.ts +12 -48
  67. package/src/node-lib/sources.ts +4 -0
  68. package/src/node-lib/timers.ts +93 -0
  69. package/src/node-process-host.ts +124 -0
  70. package/src/process-registry.ts +144 -0
  71. package/src/process-tokens.ts +60 -6
  72. package/src/promise-ownership.ts +28 -0
  73. package/src/runtime.ts +90 -214
  74. package/src/shims/child_process.ts +313 -80
  75. package/src/shims/crypto.ts +115 -100
  76. package/src/shims/dns.ts +17 -1
  77. package/src/shims/process.ts +33 -11
  78. package/src/shims/string_decoder.ts +10 -6
  79. package/src/shims/tls.ts +49 -19
  80. package/src/source-edits.ts +20 -0
@@ -33,6 +33,7 @@ if (typeof globalThis.process === 'undefined') {
33
33
  }
34
34
 
35
35
  import { forGuestRealm, heldWork } from '../host-globals';
36
+ import { promiseOwner } from '../promise-ownership';
36
37
  import { Bash, defineCommand } from 'just-bash';
37
38
  import type { CommandContext, ExecResult as JustBashExecResult } from 'just-bash';
38
39
  import { EventEmitter } from '../node-lib/events-module';
@@ -43,12 +44,18 @@ import { VirtualFSAdapter } from './vfs-adapter';
43
44
  import { __releaseOwnedServers, __ownedServerPorts } from '../node-lib/net-module';
44
45
  import { __ownedHandleCount, __releaseOwnedHandles } from '../node-lib/net-module';
45
46
  import { setProcessRunner, type RunRequest, type StartedRun } from '../node-lib/binding/process_wrap';
47
+ import { registerRunFd, releaseRunFds, inheritedRunFds } from '../node-lib/binding/fds';
48
+ import { nodeProcessHostFor, nodeProcessHostInstalled, nodeProcessRealmToken, nodeProcessInput, type NodeProcessHost, type NodeProcessLaunch } from '../node-process-host';
49
+ import { nativeStreamDescriptor } from '../native-stream-binding';
46
50
  import { Pipe, constants as pipeConstants } from '../node-lib/binding/pipe_wrap';
47
- import { registerRunFd, releaseRunFds } from '../node-lib/binding/fds';
51
+ import {
52
+ WriteWrap, ShutdownWrap, streamBaseState, kReadBytesOrError, kArrayBufferOffset,
53
+ type LibuvStreamWrap,
54
+ } from '../node-lib/binding/stream_wrap';
48
55
  import { UV_ESRCH } from '../node-lib/binding/uv';
49
- import { setupChannel } from '../node-lib/child-process-module';
56
+ import { loadNodeLibFor } from '../node-lib/load';
57
+ import type { ChildProcessModule } from '../node-lib/child-process-module';
50
58
  import { getCommandNames } from 'just-bash';
51
- import { diagnosticsChannelModule } from '../node-lib/small-modules';
52
59
  import { __substrateExecPath, __substrateProgramName, __substrateLineFor, __substrateShellLine, setProgramResolver } from './command-line';
53
60
 
54
61
  import { __substrateChildren, __onUncaughtException, __reportUncaughtException } from './process';
@@ -65,6 +72,34 @@ import { resolve as __resolvePath } from './path';
65
72
  import { setSyncChildVfs, warmSyncChild } from './sync-child';
66
73
  import type { PackageJson } from '../types/package-json';
67
74
 
75
+ // Capture host capabilities before the guest's authority is restricted. These
76
+ // endpoints are private, carry no guest bytes, and close at each checkpoint.
77
+ const HostBroadcastChannel = globalThis.BroadcastChannel;
78
+ const hostChannelName = globalThis.crypto?.randomUUID.bind(globalThis.crypto);
79
+ const isWorkerRealm = typeof (globalThis as Record<string, unknown>).WorkerGlobalScope !== 'undefined';
80
+
81
+ async function settlePrintedEntry(): Promise<void> {
82
+ await new Promise<void>(resolve => (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, resolve, 0));
83
+ if (!isWorkerRealm || nodeProcessRealmToken() === null) return;
84
+ if (!HostBroadcastChannel || !hostChannelName) throw new Error('Native process rejection checkpoint is unavailable.');
85
+ // HTML queues rejection reporting after the microtask checkpoint on the
86
+ // DOM manipulation task source. A timer alone can overtake that report.
87
+ // BroadcastChannel uses the same source, ordering this acknowledgment
88
+ // behind reports already queued before the timer turn above.
89
+ const name = hostChannelName();
90
+ const receiver = new HostBroadcastChannel(name);
91
+ try {
92
+ const sender = new HostBroadcastChannel(name);
93
+ try {
94
+ await new Promise<void>((resolve, reject) => {
95
+ receiver.onmessage = () => resolve();
96
+ receiver.onmessageerror = () => reject(new Error('Native process rejection checkpoint failed.'));
97
+ sender.postMessage(null);
98
+ });
99
+ } finally { sender.close(); }
100
+ } finally { receiver.close(); }
101
+ }
102
+
68
103
  // Singleton bash instance - uses VFS adapter for two-way file sync
69
104
  let bashInstance: Bash | null = null;
70
105
  let vfsAdapter: VirtualFSAdapter | null = null;
@@ -114,7 +149,12 @@ Object.defineProperty = function(target: object, key: PropertyKey, descriptor: P
114
149
  } as typeof Object.defineProperty;
115
150
 
116
151
  /** The stdin of a held run's guest, as the run's own. */
117
- type RunStdin = { emit: (event: string, ...args: unknown[]) => void; push: (chunk: string | Uint8Array | null) => boolean };
152
+ type RunStdin = {
153
+ emit: (event: string, ...args: unknown[]) => void;
154
+ push: (chunk: string | Uint8Array | null) => boolean;
155
+ /** A hosted sink acknowledges consumption before another pipe read. */
156
+ writeAsync?: (chunk: string | Uint8Array | null) => Promise<void>;
157
+ };
118
158
 
119
159
  /**
120
160
  * What a host gave one run: where its output goes, the handle that aborts it,
@@ -156,6 +196,29 @@ export interface RunStreams {
156
196
  */
157
197
  const _runStreams = new Map<ProcessToken, RunStreams>();
158
198
 
199
+ /** Submit before returning to a spawning parent; completion remains async. */
200
+ async function runHostedNode(host: NodeProcessHost, launch: Omit<NodeProcessLaunch, 'identity' | 'inherited' | 'stdinStream'>): Promise<CommandOutcome> {
201
+ const { token, streams } = launch;
202
+ if (!runPid(token)) setRunPid(token, mintPid(), 0);
203
+ let input: ReturnType<typeof nodeProcessInput> | undefined;
204
+ try {
205
+ streams?.signal?.throwIfAborted();
206
+ const inherited = inheritedRunFds(token).map(({ fd, handle }) => {
207
+ const descriptor = nativeStreamDescriptor(handle as LibuvStreamWrap);
208
+ if (!descriptor) throw new Error(`Node descriptor ${fd} has no native owner for process isolation.`);
209
+ return { fd, handle: descriptor };
210
+ });
211
+ input = nodeProcessInput(streams);
212
+ return await host.run({ ...launch, identity: { ...runPid(token)! }, inherited,
213
+ ...(input.stream ? { stdinStream: input.stream } : {}) });
214
+ } finally {
215
+ if (streams) streams.stdin = null;
216
+ input?.dispose();
217
+ // Adoption moves owner registration; only the source-local lookup remains.
218
+ forgetRunPid(token);
219
+ }
220
+ }
221
+
159
222
  /**
160
223
  * One line on the host realm's console when a run the tab owns dies and
161
224
  * nobody is reading its stderr.
@@ -274,7 +337,7 @@ forGuestRealm(() => {
274
337
  if (typeof globalThis.addEventListener !== 'function') return;
275
338
  __backstopInstalled = true;
276
339
  globalThis.addEventListener('error', (event: Event) => {
277
- const token = __currentProcessToken() ?? __lastLaunchedToken;
340
+ const token = nodeProcessRealmToken() ?? __currentProcessToken() ?? __lastLaunchedToken;
278
341
  if (token === null) return;
279
342
  const raised = event as ErrorEvent;
280
343
  const error = raised.error ?? new Error(raised.message || 'uncaught exception');
@@ -333,16 +396,25 @@ export function initChildProcess(vfs: VirtualFS): void {
333
396
  * of the server's became `TypeError: Cannot read properties of undefined
334
397
  * (reading 'catch')` inside a timer, 59 of them in one boot.
335
398
  */
336
- const __listenForUnhandledRejections = (report: (reason: unknown, promise?: Promise<unknown>) => void): (() => void) => {
399
+ const __listenForUnhandledRejections = (owner: object, report: (reason: unknown, promise?: Promise<unknown>) => void, ownsRealm: boolean): (() => void) => {
337
400
  if (typeof globalThis.addEventListener === 'function') {
338
401
  const target = globalThis as unknown as EventTarget;
339
- const listener = (event: Event) => { event.preventDefault(); report((event as PromiseRejectionEvent).reason, (event as PromiseRejectionEvent).promise); };
402
+ const listener = (event: Event) => {
403
+ const rejection = event as PromiseRejectionEvent;
404
+ // An admitted process worker has exactly one Node owner. Shared
405
+ // embeddings still require provenance and never broadcast failures.
406
+ if (!ownsRealm && promiseOwner(rejection.promise) !== owner) return;
407
+ event.preventDefault();
408
+ report(rejection.reason, rejection.promise);
409
+ };
340
410
  target.addEventListener('unhandledrejection', listener);
341
411
  return () => target.removeEventListener('unhandledrejection', listener);
342
412
  }
343
413
  const host = __hostProcess;
344
414
  if (host) {
345
- const listener = (reason: unknown, promise?: Promise<unknown>) => report(reason, promise);
415
+ const listener = (reason: unknown, promise?: Promise<unknown>) => {
416
+ if (ownsRealm || promiseOwner(promise) === owner) report(reason, promise);
417
+ };
346
418
  host.on('unhandledRejection', listener);
347
419
  return () => host.off('unhandledRejection', listener);
348
420
  }
@@ -353,6 +425,18 @@ export function initChildProcess(vfs: VirtualFS): void {
353
425
  return { stdout: '', stderr: 'VFS not initialized\n', exitCode: 1 };
354
426
  }
355
427
 
428
+ // Capture this run's name, streams and cancellation before another Node
429
+ // entry can start. Forks and shell entries reach this same dispatch seam.
430
+ const runToken = runTokenOf(ctx);
431
+ const streams = runToken === null ? undefined : runStreamsFor(runToken);
432
+ const processHost = nodeProcessHostFor(runToken);
433
+ if (processHost && runToken !== null) {
434
+ return runHostedNode(processHost, {
435
+ token: runToken, argv: [...args], cwd: ctx.cwd, filesystem: tree, env: guestEnvironmentOf(ctx),
436
+ ...(typeof ctx.stdin === 'string' ? { stdin: ctx.stdin } : {}), ...(streams ? { streams } : {}),
437
+ });
438
+ }
439
+
356
440
  // Node reads its own options before the script: `node --turbo-fast-api-calls
357
441
  // file.js a b` runs file.js with `a b`, and the options it were given are
358
442
  // its `execArgv`. Node's own suite spawns children that way, and the
@@ -378,8 +462,28 @@ export function initChildProcess(vfs: VirtualFS): void {
378
462
  // its first fork.
379
463
  const execArgv: string[] = [];
380
464
  let first = 0;
465
+ // `-e`/`--eval` and `-p`/`--print` carry the program itself as their
466
+ // value: there is no script, and every word after the source is the
467
+ // program's argument. Taken for an option, the source was read as the
468
+ // script's path ("Cannot find module '/workspace/console.log(1+1)'").
469
+ let evaluated: { source: string; print: boolean } | null = null;
381
470
  while (first < args.length && args[first]!.startsWith('-') && args[first] !== '-' && args[first] !== '--') {
382
- execArgv.push(args[first]!);
471
+ const option = args[first]!;
472
+ const inline = /^(--eval|--print)=([\s\S]*)$/.exec(option);
473
+ if (inline) {
474
+ evaluated = { source: inline[2]!, print: inline[1] === '--print' };
475
+ execArgv.push(option);
476
+ first += 1;
477
+ break;
478
+ }
479
+ if (option === '-e' || option === '--eval' || option === '-p' || option === '--print' || option === '-pe') {
480
+ if (first + 1 >= args.length) return { stdout: '', stderr: `node: ${option} requires an argument\n`, exitCode: 9 };
481
+ evaluated = { source: args[first + 1]!, print: option.includes('p') };
482
+ execArgv.push(option, args[first + 1]!);
483
+ first += 2;
484
+ break;
485
+ }
486
+ execArgv.push(option);
383
487
  first += 1;
384
488
  }
385
489
  if (args[first] === '--') first += 1;
@@ -392,30 +496,21 @@ export function initChildProcess(vfs: VirtualFS): void {
392
496
  };
393
497
  let script = first;
394
498
  let resolvedPath: string | null = null;
395
- for (let index = first; index < args.length; index += 1) {
499
+ for (let index = first; evaluated === null && index < args.length; index += 1) {
396
500
  const found = fileNamed(args[index]!);
397
501
  if (found !== null) { script = index; resolvedPath = found; break; }
398
502
  }
399
- for (let index = first; index < script; index += 1) execArgv.push(args[index]!);
400
- if (!args[script]) {
503
+ if (evaluated === null) for (let index = first; index < script; index += 1) execArgv.push(args[index]!);
504
+ if (evaluated === null && !args[script]) {
401
505
  return { stdout: '', stderr: 'Usage: node <script.js> [args...]\n', exitCode: 1 };
402
506
  }
403
- if (resolvedPath === null) {
507
+ if (evaluated === null && resolvedPath === null) {
404
508
  return { stdout: '', stderr: `Error: Cannot find module '${__resolvePath(ctx.cwd, args[script]!)}'\n`, exitCode: 1 };
405
509
  }
406
510
 
407
511
  let stdout = '';
408
512
  let stderr = '';
409
513
 
410
- // A run the host named is this guest process, for as long as the run
411
- // lasts: `pendingTimers`, `processPorts` and `stopProcess` are answered
412
- // from here, and a server the guest opens registers under this name.
413
- const runToken = runTokenOf(ctx);
414
- // What the host gave THIS run — its streams, its abort handle, whether it
415
- // holds the run open. Read once here and nowhere from a module global, so
416
- // a child spawned by this guest (a second run) cannot take them.
417
- const streams = runToken === null ? undefined : runStreamsFor(runToken);
418
-
419
514
  // Track whether process.exit() was called
420
515
  let exitCalled = false;
421
516
  let exitCode = 0;
@@ -485,6 +580,9 @@ export function initChildProcess(vfs: VirtualFS): void {
485
580
  // whether a pid is a live process.
486
581
  if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid);
487
582
  const releaseRun = runToken === null ? null : __recordRun(runToken, {
583
+ process: proc,
584
+ stdout: appendStdout,
585
+ stderr: appendStderr,
488
586
  pendingTimers: () => pendingGuestTimers(proc),
489
587
  stopTimers: () => stopGuestTimers(proc),
490
588
  reportUncaught: (error: unknown) => __reportUncaughtException(proc, error),
@@ -531,7 +629,11 @@ export function initChildProcess(vfs: VirtualFS): void {
531
629
  // Set up process.argv for the script. Node fills argv[0] with the
532
630
  // executable's path, the same value `process.execPath` reports; `argv0`
533
631
  // keeps the original argv[0], the plain word.
534
- proc.argv = [__substrateExecPath, resolvedPath, ...args.slice(script + 1)];
632
+ // An evaluated program has no script: its argv is the executable and the
633
+ // words after the source, as Node's is.
634
+ proc.argv = evaluated !== null
635
+ ? [__substrateExecPath, ...args.slice(first)]
636
+ : [__substrateExecPath, resolvedPath!, ...args.slice(script + 1)];
535
637
  proc.argv0 = 'node';
536
638
  proc.execArgv = execArgv;
537
639
 
@@ -596,7 +698,8 @@ export function initChildProcess(vfs: VirtualFS): void {
596
698
  syncExecution = false;
597
699
  try { proc.exit(1); } finally { syncExecution = wasSync; }
598
700
  };
599
- const detachRejections = __listenForUnhandledRejections(onUnhandledRejection);
701
+ const detachRejections = __listenForUnhandledRejections(proc, onUnhandledRejection,
702
+ runToken !== null && nodeProcessRealmToken() === runToken);
600
703
 
601
704
  // An exception nobody caught ends the program it was thrown in, as it ends
602
705
  // one in Node: a guest listener for `uncaughtException` on its process
@@ -665,7 +768,14 @@ export function initChildProcess(vfs: VirtualFS): void {
665
768
  // this program's, not whichever run started last. The engine's storage
666
769
  // carries it into the timers, microtasks and `then` callbacks the entry
667
770
  // schedules from here, so a child spawned later still names its parent.
668
- const runEntry = () => runtime.runFile(resolvedPath);
771
+ // `-p` prints the completion value of the source, which a direct `eval`
772
+ // in the module body yields with the body's own `require` in scope.
773
+ const runEntry = () => evaluated !== null
774
+ ? runtime.evaluate(
775
+ evaluated.print ? `console.log(eval(${JSON.stringify(evaluated.source)}));` : evaluated.source,
776
+ __resolvePath(ctx.cwd, '[eval]'),
777
+ )
778
+ : runtime.runFile(resolvedPath!);
669
779
  entrySettling = __substratePendingOf(
670
780
  (runToken === null ? runEntry() : enterRun(runToken, runEntry)).exports,
671
781
  );
@@ -731,7 +841,7 @@ export function initChildProcess(vfs: VirtualFS): void {
731
841
  if ((stdout.length > 0 || stderr.length > 0) && !__printedThenWorking()) {
732
842
  // Settling the command is host work. Killing guest timers must not
733
843
  // cancel this continuation and leave the command's promise unresolved.
734
- await new Promise(r => (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, r, 0));
844
+ await settlePrintedEntry();
735
845
  // Work that began during that tick is the program's still: a build
736
846
  // reached through a dynamic import's chain, a timer the entry set.
737
847
  // vue-pure-admin's mock loader, `import('bundle-import').then(...)`
@@ -947,7 +1057,6 @@ export function initChildProcess(vfs: VirtualFS): void {
947
1057
  },
948
1058
  start: startChildRun,
949
1059
  });
950
- watchChildren();
951
1060
  }
952
1061
 
953
1062
  /**
@@ -958,19 +1067,10 @@ export function initChildProcess(vfs: VirtualFS): void {
958
1067
  * a realm here holds many programs and the channel belongs to one of them.
959
1068
  */
960
1069
  function attachChannel(proc: ReturnType<Runtime['getProcess']>, fd: number, serialization: string): void {
961
- const channel = new Pipe(pipeConstants.IPC);
962
- if (channel.open(fd) !== 0) { channel.close(); return; }
963
- // Node unrefs a child's channel: the channel alone does not hold a program
964
- // open, and `Control.refCounted` puts it back the moment the program
965
- // listens for a message.
966
- channel.unref();
967
- const control = setupChannel(proc, channel, serialization);
968
- proc.on('newListener', (name: unknown) => {
969
- if (name === 'message' || name === 'disconnect') control.refCounted();
970
- });
971
- proc.on('removeListener', (name: unknown) => {
972
- if (name === 'message' || name === 'disconnect') control.unrefCounted();
973
- });
1070
+ // With a process-bound loader, Node's own bootstrap now owns this too.
1071
+ // The host setupChannel built received sockets from the host net graph;
1072
+ // process.send and every received socket must use the child's graph.
1073
+ (loadNodeLibFor(proc, 'child_process') as ChildProcessModule)._forkChild(fd, serialization);
974
1074
  }
975
1075
 
976
1076
  /** The shell's environment as a record: just-bash hands a command a Map. */
@@ -1248,6 +1348,19 @@ interface ChildProcessHostResult {
1248
1348
  exitCode: number;
1249
1349
  }
1250
1350
 
1351
+ /**
1352
+ * One of a child's pipes past fd 2, as the host's program holds it: the
1353
+ * parent's handle for that fd is the other end.
1354
+ */
1355
+ interface ChildProcessHostDescriptor {
1356
+ /** Bytes the parent writes to this fd, in order; ends when the parent closes it. */
1357
+ input: AsyncIterable<Uint8Array>;
1358
+ /** Bytes the child writes on this fd; they are reads on the parent's handle. */
1359
+ write(bytes: Uint8Array): void;
1360
+ /** The child closed this fd: EOF on the parent's handle. */
1361
+ end(): void;
1362
+ }
1363
+
1251
1364
  /** How a command is handed to that host. */
1252
1365
  interface ChildProcessHostRequest {
1253
1366
  cwd?: string;
@@ -1255,10 +1368,13 @@ interface ChildProcessHostRequest {
1255
1368
  /** Bytes already on fd 0 when the command begins, as Node delivers them. */
1256
1369
  stdin?: string;
1257
1370
  stdinStream: AsyncIterable<Uint8Array>;
1371
+ terminal?: RunStreams['terminal'];
1258
1372
  signal: AbortSignal;
1259
1373
  hold: { value: boolean };
1260
1374
  onStdout: (data: string) => void;
1261
1375
  onStderr: (data: string) => void;
1376
+ /** The child's pipes past fd 2, by number; only where it was started with any. */
1377
+ descriptors?: Record<number, ChildProcessHostDescriptor>;
1262
1378
  }
1263
1379
 
1264
1380
  /** The executor a host publishes on the well-known global symbol. */
@@ -1305,10 +1421,13 @@ interface CommandRun {
1305
1421
  stdin?: string;
1306
1422
  /** Bytes a writer still holds the run's fd 0 open with, as they arrive. */
1307
1423
  stdinStream?: AsyncIterable<Uint8Array>;
1424
+ terminal?: RunStreams['terminal'];
1308
1425
  signal?: AbortSignal;
1309
1426
  onStdout?: (data: string) => void;
1310
1427
  onStderr?: (data: string) => void;
1311
1428
  vfs?: VirtualFS;
1429
+ /** The child's ends of its pipes past fd 2, for a host that runs it. */
1430
+ descriptors?: { fd: number; pipe: Pipe }[];
1312
1431
  }
1313
1432
 
1314
1433
  /**
@@ -1346,16 +1465,29 @@ async function routeCommand(run: CommandRun): Promise<CommandOutcome> {
1346
1465
  let streamedErr = '';
1347
1466
  const onStdout = (data: string): void => { streamedOut += String(data); run.onStdout?.(String(data)); };
1348
1467
  const onStderr = (data: string): void => { streamedErr += String(data); run.onStderr?.(String(data)); };
1468
+ // This token routes the engine's own shell, not a program's environment.
1469
+ // If a host shell inherits it, a later Node launch can override its new
1470
+ // stream identity with the ancestor's and deliver output to both runs.
1471
+ // Host ancestry is already carried by the current process context.
1472
+ const hostEnv = run.env === undefined ? undefined : { ...run.env };
1473
+ if (hostEnv) delete hostEnv[PROCESS_TOKEN_ENV];
1474
+ // The host's program is the one holding the child's pipes past fd 2; the
1475
+ // run's end is its end of them, whatever it did not close itself.
1476
+ const opened = (run.descriptors ?? []).map(({ fd, pipe }) => ({ fd, ...descriptorOver(pipe) }));
1349
1477
  try {
1350
1478
  const result = await bridge.run(run.command, {
1351
1479
  cwd: run.cwd,
1352
- env: run.env,
1480
+ env: hostEnv,
1353
1481
  ...(typeof run.stdin === 'string' ? { stdin: run.stdin } : {}),
1354
1482
  stdinStream: run.stdinStream ?? emptyStdinStream(),
1483
+ ...(run.terminal ? { terminal: run.terminal } : {}),
1355
1484
  signal: controller.signal,
1356
1485
  hold: { value: true },
1357
1486
  onStdout,
1358
1487
  onStderr,
1488
+ ...(opened.length > 0
1489
+ ? { descriptors: Object.fromEntries(opened.map(({ fd, descriptor }) => [fd, descriptor])) }
1490
+ : {}),
1359
1491
  });
1360
1492
  const stdout = result.stdout || streamedOut;
1361
1493
  const stderr = result.stderr || streamedErr;
@@ -1364,6 +1496,7 @@ async function routeCommand(run: CommandRun): Promise<CommandOutcome> {
1364
1496
  return { stdout, stderr, exitCode: result.exitCode };
1365
1497
  } finally {
1366
1498
  if (bridge.parentSignal) bridge.parentSignal.removeEventListener('abort', abortWithParent);
1499
+ for (const { release } of opened) release();
1367
1500
  }
1368
1501
  }
1369
1502
 
@@ -1376,6 +1509,63 @@ async function routeCommand(run: CommandRun): Promise<CommandOutcome> {
1376
1509
  return { stdout: result.stdout || '', stderr: result.stderr || '', exitCode: result.exitCode };
1377
1510
  }
1378
1511
 
1512
+ /**
1513
+ * A child's pipe past fd 2, held by a program the host runs: the parent's
1514
+ * writes on its handle are the program's input, read the way the handle's fd 0
1515
+ * is read, and the program's writes are reads on the parent's handle, as bytes.
1516
+ * `release` is the run's end: reading stops and the input ends.
1517
+ */
1518
+ function descriptorOver(pipe: Pipe): { descriptor: ChildProcessHostDescriptor; release(): void } {
1519
+ const queued: Uint8Array[] = [];
1520
+ let waiting: ((result: IteratorResult<Uint8Array>) => void) | null = null;
1521
+ let ended = false;
1522
+ const finish = (): void => {
1523
+ if (ended) return;
1524
+ ended = true;
1525
+ pipe.readStop();
1526
+ pipe.onread = null;
1527
+ if (waiting) { const wake = waiting; waiting = null; wake({ value: undefined, done: true }); }
1528
+ };
1529
+ pipe.onread = (arrayBuffer: ArrayBuffer | null): void => {
1530
+ const length = streamBaseState[kReadBytesOrError];
1531
+ if (length <= 0 || arrayBuffer === null) { finish(); return; }
1532
+ const bytes = new Uint8Array(arrayBuffer, streamBaseState[kArrayBufferOffset], length).slice();
1533
+ if (waiting) { const wake = waiting; waiting = null; wake({ value: bytes, done: false }); }
1534
+ else queued.push(bytes);
1535
+ };
1536
+ pipe.readStart();
1537
+ const input: AsyncIterable<Uint8Array> = {
1538
+ [Symbol.asyncIterator](): AsyncIterator<Uint8Array> {
1539
+ return {
1540
+ next(): Promise<IteratorResult<Uint8Array>> {
1541
+ const next = queued.shift();
1542
+ if (next) return Promise.resolve({ value: next, done: false });
1543
+ if (ended) return Promise.resolve({ value: undefined, done: true });
1544
+ return new Promise((resolve) => { waiting = resolve; });
1545
+ },
1546
+ return(): Promise<IteratorResult<Uint8Array>> {
1547
+ finish();
1548
+ return Promise.resolve({ value: undefined, done: true });
1549
+ },
1550
+ };
1551
+ },
1552
+ };
1553
+ return {
1554
+ descriptor: {
1555
+ input,
1556
+ write(bytes: Uint8Array): void {
1557
+ if (pipe.closed) return;
1558
+ pipe.writeBuffer(new WriteWrap(), bytes);
1559
+ },
1560
+ end(): void {
1561
+ finish();
1562
+ if (!pipe.closed) pipe.shutdown(new ShutdownWrap());
1563
+ },
1564
+ },
1565
+ release: finish,
1566
+ };
1567
+ }
1568
+
1379
1569
  /** A child whose stdin nobody writes to still needs a stream the host can read. */
1380
1570
  function emptyStdinStream(): AsyncIterable<Uint8Array> {
1381
1571
  return { async *[Symbol.asyncIterator]() { /* nothing is ever written */ } };
@@ -1488,9 +1678,28 @@ function startChildRun(request: RunRequest): StartedRun {
1488
1678
  // number of the run that spawned it. `src/process-tokens.ts` says what
1489
1679
  // reads them.
1490
1680
  const pid = mintPid();
1491
- setRunPid(token, pid, runPid(__currentProcessToken() ?? __lastLaunchedToken)?.pid ?? 0);
1681
+ // In a process realm the admitted guest is always the spawning parent.
1682
+ // Async bookkeeping for a routed child must not make that child its parent.
1683
+ setRunPid(token, pid, runPid(nodeProcessRealmToken() ?? __currentProcessToken() ?? __lastLaunchedToken)?.pid ?? 0);
1492
1684
  const controller = new AbortController();
1493
1685
  const pendingStdin: Array<Uint8Array | null> = [];
1686
+ // A host terminal consumes input incrementally. The old string-only
1687
+ // route dropped every keystroke that arrived after a child was launched.
1688
+ const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === 'node';
1689
+ const hostTerminal = request.terminal !== undefined && hostExecutor() !== null && !admittedNode;
1690
+ let wakeInput: (() => void) | undefined;
1691
+ const hostInput: AsyncIterable<Uint8Array> = {
1692
+ async *[Symbol.asyncIterator]() {
1693
+ while (!finished && !controller.signal.aborted) {
1694
+ if (pendingStdin.length) {
1695
+ const bytes = pendingStdin.shift();
1696
+ if (bytes === null) return;
1697
+ if (bytes) yield bytes;
1698
+ } else await new Promise<void>((resolve) => { wakeInput = resolve; });
1699
+ }
1700
+ },
1701
+ };
1702
+ controller.signal.addEventListener('abort', () => { wakeInput?.(); }, { once: true });
1494
1703
  let stdinSink: RunStdin | null = null;
1495
1704
  let finished = false;
1496
1705
  let started = false;
@@ -1498,9 +1707,22 @@ function startChildRun(request: RunRequest): StartedRun {
1498
1707
  /** What the parent wrote to the child's fd 0 before the command began. */
1499
1708
  const initialStdin: Uint8Array[] = [];
1500
1709
 
1501
- const flushStdin = (): void => {
1502
- if (!stdinSink) return;
1503
- while (pendingStdin.length > 0) stdinSink.push(pendingStdin.shift() as Uint8Array | null);
1710
+ let flushingInput: Promise<void> | undefined;
1711
+ const flushStdin = (): void | Promise<void> => {
1712
+ if (flushingInput) return flushingInput;
1713
+ if (!stdinSink || finished) return;
1714
+ while (pendingStdin.length > 0) {
1715
+ const chunk = pendingStdin.shift() as Uint8Array | null;
1716
+ if (stdinSink.writeAsync) {
1717
+ const waiting = stdinSink.writeAsync(chunk);
1718
+ flushingInput = waiting.then(() => {
1719
+ flushingInput = undefined;
1720
+ return flushStdin();
1721
+ }, cause => { flushingInput = undefined; throw cause; });
1722
+ return flushingInput;
1723
+ }
1724
+ stdinSink.push(chunk);
1725
+ }
1504
1726
  };
1505
1727
 
1506
1728
  let streamedOut = 0;
@@ -1512,23 +1734,39 @@ function startChildRun(request: RunRequest): StartedRun {
1512
1734
  held: false,
1513
1735
  stdinOpen: request.stdinIsPipe,
1514
1736
  stderrIsPipe: request.stderrIsPipe,
1737
+ terminal: request.terminal,
1515
1738
  get stdin(): RunStdin | null | undefined { return stdinSink; },
1516
- set stdin(sink: RunStdin | null | undefined) { stdinSink = sink ?? null; if (stdinSink) flushStdin(); },
1739
+ set stdin(sink: RunStdin | null | undefined) {
1740
+ stdinSink = sink ?? null;
1741
+ if (stdinSink) {
1742
+ const pending = flushStdin();
1743
+ if (pending) void pending.catch(cause => {
1744
+ // Normal child exit detaches the sink before cancelling its writer.
1745
+ // That cancellation must not turn a successful exit into failure.
1746
+ if (!finished && stdinSink === sink) { controller.abort(cause); end(1, null); }
1747
+ });
1748
+ }
1749
+ },
1517
1750
  };
1518
1751
  registerRunStreams(token, streams);
1519
1752
  // The child is started with the descriptors its parent wired: an IPC channel
1520
1753
  // is at the number its `NODE_CHANNEL_FD` will name, under this run's name,
1521
1754
  // because every child is told the same number.
1522
1755
  if (request.channel) registerRunFd(token, request.channel.fd, 'PIPE', request.channel.pipe);
1756
+ // Its pipes past fd 2 are there the same way, at the numbers it was given.
1757
+ for (const { fd, pipe } of request.descriptors) registerRunFd(token, fd, 'PIPE', pipe);
1523
1758
 
1524
1759
  const end = (code: number, signal: string | null): void => {
1525
1760
  if (finished) return;
1526
1761
  finished = true;
1762
+ wakeInput?.();
1763
+ wakeInput = undefined;
1527
1764
  _activeForkedChildren -= 1;
1528
1765
  releaseRunStreams(token);
1529
1766
  releaseRunFds(token);
1530
1767
  // A run that has ended is no longer a process: its number answers ESRCH.
1531
1768
  forgetRunPid(token);
1769
+ __substrateChildren.delete(pid);
1532
1770
  _onForkedChildExit?.();
1533
1771
  request.exit(code, signal);
1534
1772
  };
@@ -1551,16 +1789,24 @@ function startChildRun(request: RunRequest): StartedRun {
1551
1789
  void (async () => {
1552
1790
  let outcome: CommandOutcome;
1553
1791
  try {
1554
- outcome = await routeCommand({
1792
+ const processHost = admittedNode ? nodeProcessHostFor(token) : undefined;
1793
+ const env = { ...request.env };
1794
+ delete env[PROCESS_TOKEN_ENV];
1795
+ outcome = processHost ? await runHostedNode(processHost, {
1796
+ token, argv: request.args.slice(1), cwd: request.cwd ?? '/',
1797
+ filesystem: currentVfs!, env, streams, ...(stdin !== undefined ? { stdin } : {}),
1798
+ }) : await enterRun(token, () => routeCommand({
1555
1799
  command: __substrateLineFor(request.file, request.args, request.cwd),
1556
- engineFirst: programExists(request.file, request.cwd, request.env),
1800
+ engineFirst: !hostTerminal && programExists(request.file, request.cwd, request.env),
1557
1801
  cwd: request.cwd,
1558
1802
  env: { ...request.env, [PROCESS_TOKEN_ENV]: token },
1559
1803
  stdin,
1804
+ ...(hostTerminal ? { stdinStream: hostInput, terminal: request.terminal } : {}),
1560
1805
  signal: controller.signal,
1561
1806
  onStdout: streams.onStdout,
1562
1807
  onStderr: streams.onStderr,
1563
- });
1808
+ descriptors: request.descriptors,
1809
+ }));
1564
1810
  } catch (error) {
1565
1811
  outcome = { stdout: '', stderr: `${error instanceof Error ? error.message : String(error)}\n`, exitCode: 1 };
1566
1812
  }
@@ -1574,10 +1820,10 @@ function startChildRun(request: RunRequest): StartedRun {
1574
1820
  };
1575
1821
 
1576
1822
  _activeForkedChildren += 1;
1577
- if (request.stdinIsPipe) (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, begin, 0);
1823
+ if (request.stdinIsPipe && !admittedNode) (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, begin, 0);
1578
1824
  else begin();
1579
1825
 
1580
- return {
1826
+ const control: StartedRun = {
1581
1827
  token,
1582
1828
  pid,
1583
1829
  kill(signal: string): number {
@@ -1590,12 +1836,14 @@ function startChildRun(request: RunRequest): StartedRun {
1590
1836
  end(0, killedBy);
1591
1837
  return 0;
1592
1838
  },
1593
- writeStdin(bytes: Uint8Array): void {
1839
+ writeStdin(bytes: Uint8Array): void | Promise<void> {
1840
+ if (hostTerminal) { pendingStdin.push(bytes); wakeInput?.(); wakeInput = undefined; return; }
1594
1841
  if (!started) { initialStdin.push(bytes); return; }
1595
1842
  pendingStdin.push(bytes);
1596
- flushStdin();
1843
+ return flushStdin();
1597
1844
  },
1598
1845
  endStdin(): void {
1846
+ if (hostTerminal) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; return; }
1599
1847
  if (!started) {
1600
1848
  // The writer closed before the command began: the run's fd 0 is the
1601
1849
  // string already collected and nothing more, so it is not held open.
@@ -1604,38 +1852,23 @@ function startChildRun(request: RunRequest): StartedRun {
1604
1852
  return;
1605
1853
  }
1606
1854
  pendingStdin.push(null);
1607
- flushStdin();
1855
+ const sink = stdinSink;
1856
+ const pending = flushStdin();
1857
+ if (pending) void pending.catch(cause => {
1858
+ if (!finished && stdinSink === sink && sink) { controller.abort(cause); end(1, null); }
1859
+ });
1608
1860
  },
1609
1861
  };
1610
- }
1611
-
1612
- /**
1613
- * The guest's children are processes it can see, so `process.kill(pid, 0)`
1614
- * answers about a live child rather than ESRCH for every pid but its own.
1615
- * Node publishes each `ChildProcess` on the `child_process` diagnostics
1616
- * channel as it is constructed, which is the door this reads: the pid is
1617
- * filled by `spawn` a moment later, so the child is recorded on the next tick.
1618
- */
1619
- function watchChildren(): void {
1620
- // Node's own `diagnostics_channel`, the same module the vendored
1621
- // `child_process.js` publishes on. A channel is a name in one registry, and
1622
- // the engine's own imitation of the module was a second registry: a
1623
- // subscriber there heard nothing a vendored file published.
1624
- const diagnosticsChannel = diagnosticsChannelModule as unknown as {
1625
- channel(name: string): { subscribe(listener: (message: unknown) => void): void };
1626
- };
1627
- diagnosticsChannel.channel('child_process').subscribe((message: unknown) => {
1628
- const child = (message as { process?: { pid?: number; exitCode: number | null; signalCode: string | null; kill(signal?: string): boolean; once(event: string, listener: () => void): unknown } }).process;
1629
- if (!child) return;
1630
- queueMicrotask(() => {
1631
- const pid = child.pid;
1632
- if (typeof pid !== 'number' || pid === 0) return;
1633
- __substrateChildren.set(pid, child);
1634
- child.once('exit', () => { __substrateChildren.delete(pid); });
1635
- });
1862
+ // A pid is signalable before spawn returns, including cancellation in
1863
+ // the same turn. A diagnostics-channel microtask registered it too late.
1864
+ __substrateChildren.set(pid, {
1865
+ exitCode: null, signalCode: null,
1866
+ kill: (signal = 'SIGTERM') => control.kill(signal) === 0,
1636
1867
  });
1868
+ return control;
1637
1869
  }
1638
1870
 
1871
+
1639
1872
  export default {
1640
1873
  initChildProcess,
1641
1874
  registerRunStreams,