@volter/tabnode 0.4.1 → 0.5.1

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-LWfW6VWE.js → runtime-worker-DBZ2flHK.js} +4000 -2944
  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 +5196 -3062
  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 +12 -1
  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 +11 -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 +18 -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 +50 -9
  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 +55 -6
  72. package/src/promise-ownership.ts +28 -0
  73. package/src/runtime.ts +90 -214
  74. package/src/shims/child_process.ts +310 -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;
@@ -534,7 +629,11 @@ export function initChildProcess(vfs: VirtualFS): void {
534
629
  // Set up process.argv for the script. Node fills argv[0] with the
535
630
  // executable's path, the same value `process.execPath` reports; `argv0`
536
631
  // keeps the original argv[0], the plain word.
537
- 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)];
538
637
  proc.argv0 = 'node';
539
638
  proc.execArgv = execArgv;
540
639
 
@@ -599,7 +698,8 @@ export function initChildProcess(vfs: VirtualFS): void {
599
698
  syncExecution = false;
600
699
  try { proc.exit(1); } finally { syncExecution = wasSync; }
601
700
  };
602
- const detachRejections = __listenForUnhandledRejections(onUnhandledRejection);
701
+ const detachRejections = __listenForUnhandledRejections(proc, onUnhandledRejection,
702
+ runToken !== null && nodeProcessRealmToken() === runToken);
603
703
 
604
704
  // An exception nobody caught ends the program it was thrown in, as it ends
605
705
  // one in Node: a guest listener for `uncaughtException` on its process
@@ -668,7 +768,14 @@ export function initChildProcess(vfs: VirtualFS): void {
668
768
  // this program's, not whichever run started last. The engine's storage
669
769
  // carries it into the timers, microtasks and `then` callbacks the entry
670
770
  // schedules from here, so a child spawned later still names its parent.
671
- 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!);
672
779
  entrySettling = __substratePendingOf(
673
780
  (runToken === null ? runEntry() : enterRun(runToken, runEntry)).exports,
674
781
  );
@@ -734,7 +841,7 @@ export function initChildProcess(vfs: VirtualFS): void {
734
841
  if ((stdout.length > 0 || stderr.length > 0) && !__printedThenWorking()) {
735
842
  // Settling the command is host work. Killing guest timers must not
736
843
  // cancel this continuation and leave the command's promise unresolved.
737
- await new Promise(r => (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, r, 0));
844
+ await settlePrintedEntry();
738
845
  // Work that began during that tick is the program's still: a build
739
846
  // reached through a dynamic import's chain, a timer the entry set.
740
847
  // vue-pure-admin's mock loader, `import('bundle-import').then(...)`
@@ -950,7 +1057,6 @@ export function initChildProcess(vfs: VirtualFS): void {
950
1057
  },
951
1058
  start: startChildRun,
952
1059
  });
953
- watchChildren();
954
1060
  }
955
1061
 
956
1062
  /**
@@ -961,19 +1067,10 @@ export function initChildProcess(vfs: VirtualFS): void {
961
1067
  * a realm here holds many programs and the channel belongs to one of them.
962
1068
  */
963
1069
  function attachChannel(proc: ReturnType<Runtime['getProcess']>, fd: number, serialization: string): void {
964
- const channel = new Pipe(pipeConstants.IPC);
965
- if (channel.open(fd) !== 0) { channel.close(); return; }
966
- // Node unrefs a child's channel: the channel alone does not hold a program
967
- // open, and `Control.refCounted` puts it back the moment the program
968
- // listens for a message.
969
- channel.unref();
970
- const control = setupChannel(proc, channel, serialization);
971
- proc.on('newListener', (name: unknown) => {
972
- if (name === 'message' || name === 'disconnect') control.refCounted();
973
- });
974
- proc.on('removeListener', (name: unknown) => {
975
- if (name === 'message' || name === 'disconnect') control.unrefCounted();
976
- });
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);
977
1074
  }
978
1075
 
979
1076
  /** The shell's environment as a record: just-bash hands a command a Map. */
@@ -1251,6 +1348,19 @@ interface ChildProcessHostResult {
1251
1348
  exitCode: number;
1252
1349
  }
1253
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
+
1254
1364
  /** How a command is handed to that host. */
1255
1365
  interface ChildProcessHostRequest {
1256
1366
  cwd?: string;
@@ -1258,10 +1368,13 @@ interface ChildProcessHostRequest {
1258
1368
  /** Bytes already on fd 0 when the command begins, as Node delivers them. */
1259
1369
  stdin?: string;
1260
1370
  stdinStream: AsyncIterable<Uint8Array>;
1371
+ terminal?: RunStreams['terminal'];
1261
1372
  signal: AbortSignal;
1262
1373
  hold: { value: boolean };
1263
1374
  onStdout: (data: string) => void;
1264
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>;
1265
1378
  }
1266
1379
 
1267
1380
  /** The executor a host publishes on the well-known global symbol. */
@@ -1308,10 +1421,13 @@ interface CommandRun {
1308
1421
  stdin?: string;
1309
1422
  /** Bytes a writer still holds the run's fd 0 open with, as they arrive. */
1310
1423
  stdinStream?: AsyncIterable<Uint8Array>;
1424
+ terminal?: RunStreams['terminal'];
1311
1425
  signal?: AbortSignal;
1312
1426
  onStdout?: (data: string) => void;
1313
1427
  onStderr?: (data: string) => void;
1314
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 }[];
1315
1431
  }
1316
1432
 
1317
1433
  /**
@@ -1349,16 +1465,29 @@ async function routeCommand(run: CommandRun): Promise<CommandOutcome> {
1349
1465
  let streamedErr = '';
1350
1466
  const onStdout = (data: string): void => { streamedOut += String(data); run.onStdout?.(String(data)); };
1351
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) }));
1352
1477
  try {
1353
1478
  const result = await bridge.run(run.command, {
1354
1479
  cwd: run.cwd,
1355
- env: run.env,
1480
+ env: hostEnv,
1356
1481
  ...(typeof run.stdin === 'string' ? { stdin: run.stdin } : {}),
1357
1482
  stdinStream: run.stdinStream ?? emptyStdinStream(),
1483
+ ...(run.terminal ? { terminal: run.terminal } : {}),
1358
1484
  signal: controller.signal,
1359
1485
  hold: { value: true },
1360
1486
  onStdout,
1361
1487
  onStderr,
1488
+ ...(opened.length > 0
1489
+ ? { descriptors: Object.fromEntries(opened.map(({ fd, descriptor }) => [fd, descriptor])) }
1490
+ : {}),
1362
1491
  });
1363
1492
  const stdout = result.stdout || streamedOut;
1364
1493
  const stderr = result.stderr || streamedErr;
@@ -1367,6 +1496,7 @@ async function routeCommand(run: CommandRun): Promise<CommandOutcome> {
1367
1496
  return { stdout, stderr, exitCode: result.exitCode };
1368
1497
  } finally {
1369
1498
  if (bridge.parentSignal) bridge.parentSignal.removeEventListener('abort', abortWithParent);
1499
+ for (const { release } of opened) release();
1370
1500
  }
1371
1501
  }
1372
1502
 
@@ -1379,6 +1509,63 @@ async function routeCommand(run: CommandRun): Promise<CommandOutcome> {
1379
1509
  return { stdout: result.stdout || '', stderr: result.stderr || '', exitCode: result.exitCode };
1380
1510
  }
1381
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
+
1382
1569
  /** A child whose stdin nobody writes to still needs a stream the host can read. */
1383
1570
  function emptyStdinStream(): AsyncIterable<Uint8Array> {
1384
1571
  return { async *[Symbol.asyncIterator]() { /* nothing is ever written */ } };
@@ -1491,9 +1678,28 @@ function startChildRun(request: RunRequest): StartedRun {
1491
1678
  // number of the run that spawned it. `src/process-tokens.ts` says what
1492
1679
  // reads them.
1493
1680
  const pid = mintPid();
1494
- 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);
1495
1684
  const controller = new AbortController();
1496
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 });
1497
1703
  let stdinSink: RunStdin | null = null;
1498
1704
  let finished = false;
1499
1705
  let started = false;
@@ -1501,9 +1707,22 @@ function startChildRun(request: RunRequest): StartedRun {
1501
1707
  /** What the parent wrote to the child's fd 0 before the command began. */
1502
1708
  const initialStdin: Uint8Array[] = [];
1503
1709
 
1504
- const flushStdin = (): void => {
1505
- if (!stdinSink) return;
1506
- 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
+ }
1507
1726
  };
1508
1727
 
1509
1728
  let streamedOut = 0;
@@ -1515,23 +1734,39 @@ function startChildRun(request: RunRequest): StartedRun {
1515
1734
  held: false,
1516
1735
  stdinOpen: request.stdinIsPipe,
1517
1736
  stderrIsPipe: request.stderrIsPipe,
1737
+ terminal: request.terminal,
1518
1738
  get stdin(): RunStdin | null | undefined { return stdinSink; },
1519
- 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
+ },
1520
1750
  };
1521
1751
  registerRunStreams(token, streams);
1522
1752
  // The child is started with the descriptors its parent wired: an IPC channel
1523
1753
  // is at the number its `NODE_CHANNEL_FD` will name, under this run's name,
1524
1754
  // because every child is told the same number.
1525
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);
1526
1758
 
1527
1759
  const end = (code: number, signal: string | null): void => {
1528
1760
  if (finished) return;
1529
1761
  finished = true;
1762
+ wakeInput?.();
1763
+ wakeInput = undefined;
1530
1764
  _activeForkedChildren -= 1;
1531
1765
  releaseRunStreams(token);
1532
1766
  releaseRunFds(token);
1533
1767
  // A run that has ended is no longer a process: its number answers ESRCH.
1534
1768
  forgetRunPid(token);
1769
+ __substrateChildren.delete(pid);
1535
1770
  _onForkedChildExit?.();
1536
1771
  request.exit(code, signal);
1537
1772
  };
@@ -1554,16 +1789,24 @@ function startChildRun(request: RunRequest): StartedRun {
1554
1789
  void (async () => {
1555
1790
  let outcome: CommandOutcome;
1556
1791
  try {
1557
- 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({
1558
1799
  command: __substrateLineFor(request.file, request.args, request.cwd),
1559
- engineFirst: programExists(request.file, request.cwd, request.env),
1800
+ engineFirst: !hostTerminal && programExists(request.file, request.cwd, request.env),
1560
1801
  cwd: request.cwd,
1561
1802
  env: { ...request.env, [PROCESS_TOKEN_ENV]: token },
1562
1803
  stdin,
1804
+ ...(hostTerminal ? { stdinStream: hostInput, terminal: request.terminal } : {}),
1563
1805
  signal: controller.signal,
1564
1806
  onStdout: streams.onStdout,
1565
1807
  onStderr: streams.onStderr,
1566
- });
1808
+ descriptors: request.descriptors,
1809
+ }));
1567
1810
  } catch (error) {
1568
1811
  outcome = { stdout: '', stderr: `${error instanceof Error ? error.message : String(error)}\n`, exitCode: 1 };
1569
1812
  }
@@ -1577,10 +1820,10 @@ function startChildRun(request: RunRequest): StartedRun {
1577
1820
  };
1578
1821
 
1579
1822
  _activeForkedChildren += 1;
1580
- if (request.stdinIsPipe) (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, begin, 0);
1823
+ if (request.stdinIsPipe && !admittedNode) (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, begin, 0);
1581
1824
  else begin();
1582
1825
 
1583
- return {
1826
+ const control: StartedRun = {
1584
1827
  token,
1585
1828
  pid,
1586
1829
  kill(signal: string): number {
@@ -1593,12 +1836,14 @@ function startChildRun(request: RunRequest): StartedRun {
1593
1836
  end(0, killedBy);
1594
1837
  return 0;
1595
1838
  },
1596
- writeStdin(bytes: Uint8Array): void {
1839
+ writeStdin(bytes: Uint8Array): void | Promise<void> {
1840
+ if (hostTerminal) { pendingStdin.push(bytes); wakeInput?.(); wakeInput = undefined; return; }
1597
1841
  if (!started) { initialStdin.push(bytes); return; }
1598
1842
  pendingStdin.push(bytes);
1599
- flushStdin();
1843
+ return flushStdin();
1600
1844
  },
1601
1845
  endStdin(): void {
1846
+ if (hostTerminal) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; return; }
1602
1847
  if (!started) {
1603
1848
  // The writer closed before the command began: the run's fd 0 is the
1604
1849
  // string already collected and nothing more, so it is not held open.
@@ -1607,38 +1852,23 @@ function startChildRun(request: RunRequest): StartedRun {
1607
1852
  return;
1608
1853
  }
1609
1854
  pendingStdin.push(null);
1610
- 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
+ });
1611
1860
  },
1612
1861
  };
1613
- }
1614
-
1615
- /**
1616
- * The guest's children are processes it can see, so `process.kill(pid, 0)`
1617
- * answers about a live child rather than ESRCH for every pid but its own.
1618
- * Node publishes each `ChildProcess` on the `child_process` diagnostics
1619
- * channel as it is constructed, which is the door this reads: the pid is
1620
- * filled by `spawn` a moment later, so the child is recorded on the next tick.
1621
- */
1622
- function watchChildren(): void {
1623
- // Node's own `diagnostics_channel`, the same module the vendored
1624
- // `child_process.js` publishes on. A channel is a name in one registry, and
1625
- // the engine's own imitation of the module was a second registry: a
1626
- // subscriber there heard nothing a vendored file published.
1627
- const diagnosticsChannel = diagnosticsChannelModule as unknown as {
1628
- channel(name: string): { subscribe(listener: (message: unknown) => void): void };
1629
- };
1630
- diagnosticsChannel.channel('child_process').subscribe((message: unknown) => {
1631
- const child = (message as { process?: { pid?: number; exitCode: number | null; signalCode: string | null; kill(signal?: string): boolean; once(event: string, listener: () => void): unknown } }).process;
1632
- if (!child) return;
1633
- queueMicrotask(() => {
1634
- const pid = child.pid;
1635
- if (typeof pid !== 'number' || pid === 0) return;
1636
- __substrateChildren.set(pid, child);
1637
- child.once('exit', () => { __substrateChildren.delete(pid); });
1638
- });
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,
1639
1867
  });
1868
+ return control;
1640
1869
  }
1641
1870
 
1871
+
1642
1872
  export default {
1643
1873
  initChildProcess,
1644
1874
  registerRunStreams,