@volter/tabnode 0.5.4 → 0.5.5

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.
@@ -6641,6 +6641,14 @@ var asyncHooksShim$1 = /* @__PURE__ */ Object.freeze({
6641
6641
  executionAsyncResource,
6642
6642
  triggerAsyncId
6643
6643
  });
6644
+ function describedIdentity(identity) {
6645
+ const { pid, ppid, argv, cwd, startedAt } = identity;
6646
+ const described = { pid, ppid };
6647
+ if (Array.isArray(argv) && argv.length <= 4096 && argv.every((arg) => typeof arg === "string")) described.argv = Object.freeze([...argv]);
6648
+ if (typeof cwd === "string" && cwd.startsWith("/")) described.cwd = cwd;
6649
+ if (typeof startedAt === "number" && Number.isFinite(startedAt)) described.startedAt = startedAt;
6650
+ return Object.freeze(described);
6651
+ }
6644
6652
  function createProcessRegistryOwner() {
6645
6653
  let nextPid2 = 1e3 + Math.floor(Math.random() * 3e4);
6646
6654
  const live = /* @__PURE__ */ new Map();
@@ -6697,7 +6705,7 @@ function createProcessRegistryOwner() {
6697
6705
  if (identity.ppid !== 0 && !Array.from(runs2.values()).some((parent) => parent.pid === identity.ppid)) {
6698
6706
  throw new Error("Parent process is not owned by this realm.");
6699
6707
  }
6700
- const entry = Object.freeze({ pid: identity.pid, ppid: identity.ppid });
6708
+ const entry = describedIdentity(identity);
6701
6709
  runs2.set(token, entry);
6702
6710
  live.set(entry.pid, entry);
6703
6711
  receivers.set(entry.pid, receiver);
@@ -6739,6 +6747,10 @@ function createProcessRegistryOwner() {
6739
6747
  };
6740
6748
  scopes.set(scope, { allocated, runs: runs2, active: active2 });
6741
6749
  return scope;
6750
+ },
6751
+ /** Every live process of the container, in pid order: what `/proc` lists. */
6752
+ table() {
6753
+ return [...live.values()].sort((a, b) => a.pid - b.pid);
6742
6754
  }
6743
6755
  };
6744
6756
  }
@@ -6779,8 +6791,8 @@ const pidsOfRuns = /* @__PURE__ */ new Map();
6779
6791
  function mintPid() {
6780
6792
  return processRegistry.allocate();
6781
6793
  }
6782
- function setRunPid(token, pid, ppid) {
6783
- processRegistry.publish(token, { pid, ppid });
6794
+ function setRunPid(token, pid, ppid, started) {
6795
+ processRegistry.publish(token, { pid, ppid, ...started, startedAt: Date.now() });
6784
6796
  pidsOfRuns.set(token, { pid, ppid });
6785
6797
  }
6786
6798
  function runPid(token) {
@@ -9657,7 +9669,7 @@ function createUtilBinding(require2) {
9657
9669
  const bufferModule = lazyModule("buffer");
9658
9670
  const Buffer$9 = lazyExport("buffer", "Buffer");
9659
9671
  function createNodeError$1(code2, syscall, path2) {
9660
- const known = ["EEXIST", "EINVAL", "EISDIR", "ENOENT", "ENOTDIR", "ENOTEMPTY", "ELOOP"];
9672
+ const known = ["EEXIST", "EINVAL", "EISDIR", "ENOENT", "ENOTDIR", "ENOTEMPTY", "ELOOP", "EROFS"];
9661
9673
  if (known.includes(code2)) return createNodeError(code2, syscall, path2);
9662
9674
  return Object.assign(new Error(`${code2}: ${syscall} '${path2}'`), { code: code2, syscall, path: path2, errno: -9 });
9663
9675
  }
@@ -27571,14 +27583,16 @@ function createNodeError(code2, syscall, path2, message) {
27571
27583
  ENOTDIR: -20,
27572
27584
  EISDIR: -21,
27573
27585
  EEXIST: -17,
27574
- ENOTEMPTY: -39
27586
+ ENOTEMPTY: -39,
27587
+ EROFS: -30
27575
27588
  };
27576
27589
  const messages2 = {
27577
27590
  ENOENT: "no such file or directory",
27578
27591
  ENOTDIR: "not a directory",
27579
27592
  EISDIR: "is a directory",
27580
27593
  EEXIST: "file already exists",
27581
- ENOTEMPTY: "directory not empty"
27594
+ ENOTEMPTY: "directory not empty",
27595
+ EROFS: "read-only file system"
27582
27596
  };
27583
27597
  const err2 = new Error(
27584
27598
  `${code2}: ${messages2[code2]}, ${syscall} '${path2}'`
@@ -27637,6 +27651,7 @@ class VirtualFS {
27637
27651
  return { files };
27638
27652
  }
27639
27653
  serializeNode(path2, node, files) {
27654
+ if (node.mount) return;
27640
27655
  if (node.type === "file") {
27641
27656
  let content = "";
27642
27657
  if (node.content && node.content.length > 0) {
@@ -27692,6 +27707,7 @@ class VirtualFS {
27692
27707
  */
27693
27708
  writeFileSyncInternal(path2, data2, emitEvent) {
27694
27709
  const normalized = this.normalizePath(path2);
27710
+ this.assertWritable(normalized, "open");
27695
27711
  const parentPath = this.getParentPath(normalized);
27696
27712
  const basename = this.getBasename(normalized);
27697
27713
  if (!basename) {
@@ -27767,11 +27783,12 @@ class VirtualFS {
27767
27783
  if (depth > 40) throw createNodeError("ELOOP", "stat", path2);
27768
27784
  const segments = this.getPathSegments(path2);
27769
27785
  let current3 = this.root;
27786
+ let mounted;
27770
27787
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
27771
- if (current3.type !== "directory" || !current3.children) {
27788
+ if (current3.type !== "directory" || !mounted && !current3.children) {
27772
27789
  return void 0;
27773
27790
  }
27774
- const child = current3.children.get(segments[index2]);
27791
+ const child = mounted ? mounted.tree.node("/" + segments.slice(mounted.at + 1, index2 + 1).join("/")) : current3.children.get(segments[index2]);
27775
27792
  if (!child) {
27776
27793
  return void 0;
27777
27794
  }
@@ -27781,16 +27798,53 @@ class VirtualFS {
27781
27798
  const target = this.__substrateLinkTarget(linkPath, child.target);
27782
27799
  return this.__substrateNode(rest ? target + "/" + rest : target, follow, depth + 1);
27783
27800
  }
27784
- current3 = child;
27801
+ if (!mounted && child.mount) {
27802
+ mounted = { tree: child.mount, at: index2 };
27803
+ current3 = child.mount.node("/") ?? child;
27804
+ } else {
27805
+ current3 = child;
27806
+ }
27785
27807
  }
27786
27808
  return current3;
27787
27809
  }
27810
+ /**
27811
+ * Mount a read-only tree at a directory, as Linux mounts `/proc`: reads
27812
+ * below it are the tree's answers, writes fail with `EROFS`, and a snapshot
27813
+ * leaves it out. Answers the unmount.
27814
+ */
27815
+ mount(path2, tree2) {
27816
+ var _a3;
27817
+ const normalized = this.normalizePath(path2);
27818
+ const basename = this.getBasename(normalized);
27819
+ if (!basename) throw createNodeError("EINVAL", "mount", path2);
27820
+ const parent = this.ensureDirectory(this.getParentPath(normalized));
27821
+ const existing = parent.children.get(basename);
27822
+ if (existing && (existing.type !== "directory" || existing.mount || ((_a3 = existing.children) == null ? void 0 : _a3.size))) {
27823
+ throw createNodeError("EEXIST", "mount", path2);
27824
+ }
27825
+ const node = { type: "directory", children: /* @__PURE__ */ new Map(), mtime: Date.now(), mode: 365, mount: tree2 };
27826
+ parent.children.set(basename, node);
27827
+ return () => {
27828
+ if (parent.children.get(basename) === node) parent.children.delete(basename);
27829
+ };
27830
+ }
27831
+ /** A write at or below a mount point is refused, as a read-only filesystem refuses it. */
27832
+ assertWritable(path2, syscall) {
27833
+ var _a3;
27834
+ let current3 = this.root;
27835
+ for (const segment of this.getPathSegments(path2)) {
27836
+ current3 = (_a3 = current3 == null ? void 0 : current3.children) == null ? void 0 : _a3.get(segment);
27837
+ if (!current3) return;
27838
+ if (current3.mount) throw createNodeError("EROFS", syscall, path2);
27839
+ }
27840
+ }
27788
27841
  /** A link's target as an absolute path: absolute as written, or relative to the link's directory. */
27789
27842
  __substrateLinkTarget(linkPath, target) {
27790
27843
  return this.normalizePath(target.startsWith("/") ? target : this.getParentPath(linkPath) + "/" + target);
27791
27844
  }
27792
27845
  symlinkSync(target, path2) {
27793
27846
  const normalized = this.normalizePath(path2);
27847
+ this.assertWritable(normalized, "symlink");
27794
27848
  const parentPath = this.getParentPath(normalized);
27795
27849
  const basename = this.getBasename(normalized);
27796
27850
  const parent = this.getNode(parentPath);
@@ -27851,11 +27905,13 @@ class VirtualFS {
27851
27905
  * Get stats for path
27852
27906
  */
27853
27907
  chmodSync(path2, mode) {
27908
+ this.assertWritable(path2, "chmod");
27854
27909
  const node = this.getNode(path2);
27855
27910
  if (!node) throw createNodeError("ENOENT", "chmod", path2);
27856
27911
  node.mode = mode & 4095;
27857
27912
  }
27858
27913
  utimesSync(path2, atime, mtime) {
27914
+ this.assertWritable(path2, "utime");
27859
27915
  const node = this.getNode(path2);
27860
27916
  if (!node) throw createNodeError("ENOENT", "utimes", path2);
27861
27917
  node.atime = atime instanceof Date ? atime.getTime() : atime * 1e3;
@@ -27961,7 +28017,10 @@ class VirtualFS {
27961
28017
  * Create directory, optionally with recursive parent creation
27962
28018
  */
27963
28019
  mkdirSync(path2, options) {
28020
+ var _a3;
27964
28021
  const normalized = this.normalizePath(path2);
28022
+ if ((options == null ? void 0 : options.recursive) && ((_a3 = this.getNode(normalized)) == null ? void 0 : _a3.type) === "directory") return;
28023
+ this.assertWritable(normalized, "mkdir");
27965
28024
  if (options == null ? void 0 : options.recursive) {
27966
28025
  this.ensureDirectory(normalized);
27967
28026
  return;
@@ -28006,6 +28065,7 @@ class VirtualFS {
28006
28065
  */
28007
28066
  unlinkSync(path2) {
28008
28067
  const normalized = this.normalizePath(path2);
28068
+ this.assertWritable(normalized, "unlink");
28009
28069
  const parentPath = this.getParentPath(normalized);
28010
28070
  const basename = this.getBasename(normalized);
28011
28071
  const parent = this.getNode(parentPath);
@@ -28028,6 +28088,7 @@ class VirtualFS {
28028
28088
  */
28029
28089
  rmdirSync(path2) {
28030
28090
  const normalized = this.normalizePath(path2);
28091
+ this.assertWritable(normalized, "rmdir");
28031
28092
  const parentPath = this.getParentPath(normalized);
28032
28093
  const basename = this.getBasename(normalized);
28033
28094
  if (!basename) {
@@ -28056,6 +28117,8 @@ class VirtualFS {
28056
28117
  renameSync(oldPath, newPath) {
28057
28118
  const normalizedOld = this.normalizePath(oldPath);
28058
28119
  const normalizedNew = this.normalizePath(newPath);
28120
+ this.assertWritable(normalizedOld, "rename");
28121
+ this.assertWritable(normalizedNew, "rename");
28059
28122
  const oldParentPath = this.getParentPath(normalizedOld);
28060
28123
  const oldBasename = this.getBasename(normalizedOld);
28061
28124
  const newParentPath = this.getParentPath(normalizedNew);
@@ -28154,10 +28217,12 @@ class VirtualFS {
28154
28217
  for (let hops = 0; hops <= 40; hops += 1) {
28155
28218
  const segments = resolved.split("/").filter(Boolean);
28156
28219
  let current3 = this.root;
28220
+ let mounted;
28157
28221
  let linked = false;
28158
28222
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
28159
- const child = current3 && current3.children ? current3.children.get(segments[index2]) : void 0;
28223
+ const child = mounted ? mounted.tree.node("/" + segments.slice(mounted.at + 1, index2 + 1).join("/")) : current3 && current3.children ? current3.children.get(segments[index2]) : void 0;
28160
28224
  if (!child) throw createNodeError("ENOENT", "realpath", path2);
28225
+ if (!mounted && child.mount) mounted = { tree: child.mount, at: index2 };
28161
28226
  if (child.type === "symlink") {
28162
28227
  const linkPath = "/" + segments.slice(0, index2 + 1).join("/");
28163
28228
  const rest = segments.slice(index2 + 1).join("/");
@@ -69753,7 +69818,7 @@ const _runStreams = /* @__PURE__ */ new Map();
69753
69818
  async function runHostedNode(host, launch) {
69754
69819
  var _a3;
69755
69820
  const { token, streams } = launch;
69756
- if (!runPid(token)) setRunPid(token, mintPid(), 0);
69821
+ if (!runPid(token)) setRunPid(token, mintPid(), 0, { argv: ["node", ...launch.argv], cwd: launch.cwd });
69757
69822
  let input;
69758
69823
  try {
69759
69824
  (_a3 = streams == null ? void 0 : streams.signal) == null ? void 0 : _a3.throwIfAborted();
@@ -69989,7 +70054,7 @@ function initChildProcess(vfs2) {
69989
70054
  ...runPid(runToken) ? { pid: runPid(runToken).pid, ppid: runPid(runToken).ppid } : {}
69990
70055
  });
69991
70056
  const proc = runtime2.getProcess();
69992
- if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid);
70057
+ if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid, { argv: ["node", ...args], cwd: ctx.cwd });
69993
70058
  const releaseRun = runToken === null ? null : __recordRun(runToken, {
69994
70059
  process: proc,
69995
70060
  stdout: appendStdout,
@@ -70715,7 +70780,12 @@ function startChildRun(request) {
70715
70780
  var _a3;
70716
70781
  const token = `child-${__nextChildRun++}`;
70717
70782
  const pid = mintPid();
70718
- setRunPid(token, pid, ((_a3 = runPid(__currentProcessToken() ?? __lastLaunchedToken)) == null ? void 0 : _a3.pid) ?? 0);
70783
+ setRunPid(
70784
+ token,
70785
+ pid,
70786
+ ((_a3 = runPid(__currentProcessToken() ?? __lastLaunchedToken)) == null ? void 0 : _a3.pid) ?? 0,
70787
+ { argv: request.args.length ? request.args : [request.file], ...request.cwd ? { cwd: request.cwd } : {} }
70788
+ );
70719
70789
  const controller = new AbortController();
70720
70790
  const pendingStdin = [];
70721
70791
  const admittedNode = nodeProcessHostInstalled();
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  export { restoreHostGlobals, guestRealmInstalled } from './host-globals';
11
11
  export { VirtualFS } from './virtual-fs';
12
- export type { FSNode, Stats, FSWatcher, WatchListener, WatchEventType } from './virtual-fs';
12
+ export type { FSNode, MountedTree, Stats, FSWatcher, WatchListener, WatchEventType } from './virtual-fs';
13
13
  export { Runtime, execute } from './runtime';
14
14
  export type { Module, RuntimeOptions, RequireFunction } from './runtime';
15
15
  export { createRuntime, WorkerRuntime } from './create-runtime';
@@ -46,7 +46,7 @@ import { ServerBridge } from './server-bridge';
46
46
  import { type Socket as NetSocket } from './node-lib/net-module';
47
47
  import { runPid, processByPid } from './process-tokens';
48
48
  export { runPid, processByPid };
49
- export { createProcessRegistryScope, installProcessRegistry, ownerProcessRegistryScope } from './process-tokens';
49
+ export { createProcessRegistryScope, installProcessRegistry, ownerProcessRegistryScope, ownerProcessTable } from './process-tokens';
50
50
  export { installNodeProcessHost, nodeProcessHostInstalled } from './node-process-host';
51
51
  export type { NodeProcessLaunch, NodeProcessHost } from './node-process-host';
52
52
  export type { ProcessIdentity, ProcessRegistry, ProcessRegistryScope, InitialProcessRegistration } from './process-registry';
@@ -112,6 +112,10 @@ export declare function createContainer(options?: ContainerOptions): {
112
112
  run: (command: string, options?: RunOptions) => Promise<RunResult>;
113
113
  pendingTimers: (token: string) => number;
114
114
  processPorts: (token: string) => number[];
115
+ /** The pid of the process listening on a port of this engine, where a guest process is: `/proc`'s socket owner. */
116
+ portPid: (port: number) => number | undefined;
117
+ /** Deliver a signal to the named run as another process's `kill(pid)` does; false when the run is gone. */
118
+ signalProcess: (token: string, signal: string) => boolean;
115
119
  stopProcess: (token: string) => boolean;
116
120
  /**
117
121
  * Input for one run's guest, by its process token; without a token, the most
package/dist/index.mjs CHANGED
@@ -6350,6 +6350,14 @@ const asyncHooksShim$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.de
6350
6350
  executionAsyncResource,
6351
6351
  triggerAsyncId
6352
6352
  }, Symbol.toStringTag, { value: "Module" }));
6353
+ function describedIdentity(identity) {
6354
+ const { pid, ppid, argv, cwd, startedAt } = identity;
6355
+ const described = { pid, ppid };
6356
+ if (Array.isArray(argv) && argv.length <= 4096 && argv.every((arg) => typeof arg === "string")) described.argv = Object.freeze([...argv]);
6357
+ if (typeof cwd === "string" && cwd.startsWith("/")) described.cwd = cwd;
6358
+ if (typeof startedAt === "number" && Number.isFinite(startedAt)) described.startedAt = startedAt;
6359
+ return Object.freeze(described);
6360
+ }
6353
6361
  function createProcessRegistryOwner() {
6354
6362
  let nextPid2 = 1e3 + Math.floor(Math.random() * 3e4);
6355
6363
  const live = /* @__PURE__ */ new Map();
@@ -6406,7 +6414,7 @@ function createProcessRegistryOwner() {
6406
6414
  if (identity.ppid !== 0 && !Array.from(runs2.values()).some((parent) => parent.pid === identity.ppid)) {
6407
6415
  throw new Error("Parent process is not owned by this realm.");
6408
6416
  }
6409
- const entry = Object.freeze({ pid: identity.pid, ppid: identity.ppid });
6417
+ const entry = describedIdentity(identity);
6410
6418
  runs2.set(token, entry);
6411
6419
  live.set(entry.pid, entry);
6412
6420
  receivers.set(entry.pid, receiver);
@@ -6448,6 +6456,10 @@ function createProcessRegistryOwner() {
6448
6456
  };
6449
6457
  scopes2.set(scope, { allocated: allocated2, runs: runs2, active: active2 });
6450
6458
  return scope;
6459
+ },
6460
+ /** Every live process of the container, in pid order: what `/proc` lists. */
6461
+ table() {
6462
+ return [...live.values()].sort((a, b) => a.pid - b.pid);
6451
6463
  }
6452
6464
  };
6453
6465
  }
@@ -6485,6 +6497,18 @@ function __stopOwnedProcess(token) {
6485
6497
  var _a2;
6486
6498
  (_a2 = runs.get(token)) == null ? void 0 : _a2.stopTimers();
6487
6499
  }
6500
+ function __signalOwnedProcess(token, signal) {
6501
+ const run = runs.get(token);
6502
+ if (!run) return false;
6503
+ const guest = run.process;
6504
+ try {
6505
+ enterRun(token, () => guest.kill(guest.pid, signal));
6506
+ } catch (error) {
6507
+ if (error instanceof Error && error.message.startsWith("Process exited with code")) return true;
6508
+ return false;
6509
+ }
6510
+ return true;
6511
+ }
6488
6512
  const processRegistryOwner = createProcessRegistryOwner();
6489
6513
  let processRegistry = processRegistryOwner.createScope();
6490
6514
  let registryInstalled = false;
@@ -6494,6 +6518,10 @@ function createProcessRegistryScope() {
6494
6518
  if (registryInstalled) throw new Error("Only the container owner can create process registry scopes.");
6495
6519
  return processRegistryOwner.createScope();
6496
6520
  }
6521
+ function ownerProcessTable() {
6522
+ if (registryInstalled) throw new Error("This realm is a process registry client, not the container owner.");
6523
+ return processRegistryOwner.table();
6524
+ }
6497
6525
  function ownerProcessRegistryScope() {
6498
6526
  if (registryInstalled) throw new Error("This realm is a process registry client, not the container owner.");
6499
6527
  return processRegistry;
@@ -6513,9 +6541,9 @@ function mintPid() {
6513
6541
  registryUsed = true;
6514
6542
  return processRegistry.allocate();
6515
6543
  }
6516
- function setRunPid(token, pid, ppid) {
6544
+ function setRunPid(token, pid, ppid, started2) {
6517
6545
  registryUsed = true;
6518
- processRegistry.publish(token, { pid, ppid });
6546
+ processRegistry.publish(token, { pid, ppid, ...started2, startedAt: Date.now() });
6519
6547
  pidsOfRuns.set(token, { pid, ppid });
6520
6548
  }
6521
6549
  function runPid(token) {
@@ -9804,7 +9832,7 @@ function createUtilBinding(require2) {
9804
9832
  const bufferModule = lazyModule("buffer");
9805
9833
  const Buffer$9 = lazyExport("buffer", "Buffer");
9806
9834
  function createNodeError$1(code2, syscall, path2) {
9807
- const known = ["EEXIST", "EINVAL", "EISDIR", "ENOENT", "ENOTDIR", "ENOTEMPTY", "ELOOP"];
9835
+ const known = ["EEXIST", "EINVAL", "EISDIR", "ENOENT", "ENOTDIR", "ENOTEMPTY", "ELOOP", "EROFS"];
9808
9836
  if (known.includes(code2)) return createNodeError(code2, syscall, path2);
9809
9837
  return Object.assign(new Error(`${code2}: ${syscall} '${path2}'`), { code: code2, syscall, path: path2, errno: -9 });
9810
9838
  }
@@ -17762,10 +17790,17 @@ var implementation$1 = function bind2(that) {
17762
17790
  var implementation = implementation$1;
17763
17791
  var functionBind = Function.prototype.bind || implementation;
17764
17792
  var functionCall = Function.prototype.call;
17765
- var functionApply = Function.prototype.apply;
17793
+ var functionApply;
17794
+ var hasRequiredFunctionApply;
17795
+ function requireFunctionApply() {
17796
+ if (hasRequiredFunctionApply) return functionApply;
17797
+ hasRequiredFunctionApply = 1;
17798
+ functionApply = Function.prototype.apply;
17799
+ return functionApply;
17800
+ }
17766
17801
  var reflectApply = typeof Reflect !== "undefined" && Reflect && Reflect.apply;
17767
17802
  var bind$2 = functionBind;
17768
- var $apply$1 = functionApply;
17803
+ var $apply$1 = requireFunctionApply();
17769
17804
  var $call$2 = functionCall;
17770
17805
  var $reflectApply = reflectApply;
17771
17806
  var actualApply = $reflectApply || bind$2.call($call$2, $apply$1);
@@ -17885,7 +17920,7 @@ var hasSymbols = requireHasSymbols()();
17885
17920
  var getProto = requireGetProto();
17886
17921
  var $ObjectGPO = requireObject_getPrototypeOf();
17887
17922
  var $ReflectGPO = requireReflect_getPrototypeOf();
17888
- var $apply = functionApply;
17923
+ var $apply = requireFunctionApply();
17889
17924
  var $call = functionCall;
17890
17925
  var needsEval = {};
17891
17926
  var TypedArray = typeof Uint8Array === "undefined" || !getProto ? undefined$1 : getProto(Uint8Array);
@@ -18526,7 +18561,7 @@ function requireApplyBind() {
18526
18561
  if (hasRequiredApplyBind) return applyBind;
18527
18562
  hasRequiredApplyBind = 1;
18528
18563
  var bind3 = functionBind;
18529
- var $apply2 = functionApply;
18564
+ var $apply2 = requireFunctionApply();
18530
18565
  var actualApply$1 = actualApply;
18531
18566
  applyBind = function applyBind2() {
18532
18567
  return actualApply$1(bind3, $apply2, arguments);
@@ -27737,14 +27772,16 @@ function createNodeError(code2, syscall, path2, message) {
27737
27772
  ENOTDIR: -20,
27738
27773
  EISDIR: -21,
27739
27774
  EEXIST: -17,
27740
- ENOTEMPTY: -39
27775
+ ENOTEMPTY: -39,
27776
+ EROFS: -30
27741
27777
  };
27742
27778
  const messages2 = {
27743
27779
  ENOENT: "no such file or directory",
27744
27780
  ENOTDIR: "not a directory",
27745
27781
  EISDIR: "is a directory",
27746
27782
  EEXIST: "file already exists",
27747
- ENOTEMPTY: "directory not empty"
27783
+ ENOTEMPTY: "directory not empty",
27784
+ EROFS: "read-only file system"
27748
27785
  };
27749
27786
  const err2 = new Error(
27750
27787
  `${code2}: ${messages2[code2]}, ${syscall} '${path2}'`
@@ -27803,6 +27840,7 @@ class VirtualFS {
27803
27840
  return { files };
27804
27841
  }
27805
27842
  serializeNode(path2, node, files) {
27843
+ if (node.mount) return;
27806
27844
  if (node.type === "file") {
27807
27845
  let content = "";
27808
27846
  if (node.content && node.content.length > 0) {
@@ -27858,6 +27896,7 @@ class VirtualFS {
27858
27896
  */
27859
27897
  writeFileSyncInternal(path2, data2, emitEvent) {
27860
27898
  const normalized = this.normalizePath(path2);
27899
+ this.assertWritable(normalized, "open");
27861
27900
  const parentPath = this.getParentPath(normalized);
27862
27901
  const basename2 = this.getBasename(normalized);
27863
27902
  if (!basename2) {
@@ -27933,11 +27972,12 @@ class VirtualFS {
27933
27972
  if (depth > 40) throw createNodeError("ELOOP", "stat", path2);
27934
27973
  const segments = this.getPathSegments(path2);
27935
27974
  let current3 = this.root;
27975
+ let mounted;
27936
27976
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
27937
- if (current3.type !== "directory" || !current3.children) {
27977
+ if (current3.type !== "directory" || !mounted && !current3.children) {
27938
27978
  return void 0;
27939
27979
  }
27940
- const child = current3.children.get(segments[index2]);
27980
+ const child = mounted ? mounted.tree.node("/" + segments.slice(mounted.at + 1, index2 + 1).join("/")) : current3.children.get(segments[index2]);
27941
27981
  if (!child) {
27942
27982
  return void 0;
27943
27983
  }
@@ -27947,16 +27987,53 @@ class VirtualFS {
27947
27987
  const target = this.__substrateLinkTarget(linkPath, child.target);
27948
27988
  return this.__substrateNode(rest ? target + "/" + rest : target, follow, depth + 1);
27949
27989
  }
27950
- current3 = child;
27990
+ if (!mounted && child.mount) {
27991
+ mounted = { tree: child.mount, at: index2 };
27992
+ current3 = child.mount.node("/") ?? child;
27993
+ } else {
27994
+ current3 = child;
27995
+ }
27951
27996
  }
27952
27997
  return current3;
27953
27998
  }
27999
+ /**
28000
+ * Mount a read-only tree at a directory, as Linux mounts `/proc`: reads
28001
+ * below it are the tree's answers, writes fail with `EROFS`, and a snapshot
28002
+ * leaves it out. Answers the unmount.
28003
+ */
28004
+ mount(path2, tree2) {
28005
+ var _a2;
28006
+ const normalized = this.normalizePath(path2);
28007
+ const basename2 = this.getBasename(normalized);
28008
+ if (!basename2) throw createNodeError("EINVAL", "mount", path2);
28009
+ const parent = this.ensureDirectory(this.getParentPath(normalized));
28010
+ const existing = parent.children.get(basename2);
28011
+ if (existing && (existing.type !== "directory" || existing.mount || ((_a2 = existing.children) == null ? void 0 : _a2.size))) {
28012
+ throw createNodeError("EEXIST", "mount", path2);
28013
+ }
28014
+ const node = { type: "directory", children: /* @__PURE__ */ new Map(), mtime: Date.now(), mode: 365, mount: tree2 };
28015
+ parent.children.set(basename2, node);
28016
+ return () => {
28017
+ if (parent.children.get(basename2) === node) parent.children.delete(basename2);
28018
+ };
28019
+ }
28020
+ /** A write at or below a mount point is refused, as a read-only filesystem refuses it. */
28021
+ assertWritable(path2, syscall) {
28022
+ var _a2;
28023
+ let current3 = this.root;
28024
+ for (const segment of this.getPathSegments(path2)) {
28025
+ current3 = (_a2 = current3 == null ? void 0 : current3.children) == null ? void 0 : _a2.get(segment);
28026
+ if (!current3) return;
28027
+ if (current3.mount) throw createNodeError("EROFS", syscall, path2);
28028
+ }
28029
+ }
27954
28030
  /** A link's target as an absolute path: absolute as written, or relative to the link's directory. */
27955
28031
  __substrateLinkTarget(linkPath, target) {
27956
28032
  return this.normalizePath(target.startsWith("/") ? target : this.getParentPath(linkPath) + "/" + target);
27957
28033
  }
27958
28034
  symlinkSync(target, path2) {
27959
28035
  const normalized = this.normalizePath(path2);
28036
+ this.assertWritable(normalized, "symlink");
27960
28037
  const parentPath = this.getParentPath(normalized);
27961
28038
  const basename2 = this.getBasename(normalized);
27962
28039
  const parent = this.getNode(parentPath);
@@ -28017,11 +28094,13 @@ class VirtualFS {
28017
28094
  * Get stats for path
28018
28095
  */
28019
28096
  chmodSync(path2, mode) {
28097
+ this.assertWritable(path2, "chmod");
28020
28098
  const node = this.getNode(path2);
28021
28099
  if (!node) throw createNodeError("ENOENT", "chmod", path2);
28022
28100
  node.mode = mode & 4095;
28023
28101
  }
28024
28102
  utimesSync(path2, atime, mtime) {
28103
+ this.assertWritable(path2, "utime");
28025
28104
  const node = this.getNode(path2);
28026
28105
  if (!node) throw createNodeError("ENOENT", "utimes", path2);
28027
28106
  node.atime = atime instanceof Date ? atime.getTime() : atime * 1e3;
@@ -28127,7 +28206,10 @@ class VirtualFS {
28127
28206
  * Create directory, optionally with recursive parent creation
28128
28207
  */
28129
28208
  mkdirSync(path2, options) {
28209
+ var _a2;
28130
28210
  const normalized = this.normalizePath(path2);
28211
+ if ((options == null ? void 0 : options.recursive) && ((_a2 = this.getNode(normalized)) == null ? void 0 : _a2.type) === "directory") return;
28212
+ this.assertWritable(normalized, "mkdir");
28131
28213
  if (options == null ? void 0 : options.recursive) {
28132
28214
  this.ensureDirectory(normalized);
28133
28215
  return;
@@ -28172,6 +28254,7 @@ class VirtualFS {
28172
28254
  */
28173
28255
  unlinkSync(path2) {
28174
28256
  const normalized = this.normalizePath(path2);
28257
+ this.assertWritable(normalized, "unlink");
28175
28258
  const parentPath = this.getParentPath(normalized);
28176
28259
  const basename2 = this.getBasename(normalized);
28177
28260
  const parent = this.getNode(parentPath);
@@ -28194,6 +28277,7 @@ class VirtualFS {
28194
28277
  */
28195
28278
  rmdirSync(path2) {
28196
28279
  const normalized = this.normalizePath(path2);
28280
+ this.assertWritable(normalized, "rmdir");
28197
28281
  const parentPath = this.getParentPath(normalized);
28198
28282
  const basename2 = this.getBasename(normalized);
28199
28283
  if (!basename2) {
@@ -28222,6 +28306,8 @@ class VirtualFS {
28222
28306
  renameSync(oldPath, newPath) {
28223
28307
  const normalizedOld = this.normalizePath(oldPath);
28224
28308
  const normalizedNew = this.normalizePath(newPath);
28309
+ this.assertWritable(normalizedOld, "rename");
28310
+ this.assertWritable(normalizedNew, "rename");
28225
28311
  const oldParentPath = this.getParentPath(normalizedOld);
28226
28312
  const oldBasename = this.getBasename(normalizedOld);
28227
28313
  const newParentPath = this.getParentPath(normalizedNew);
@@ -28320,10 +28406,12 @@ class VirtualFS {
28320
28406
  for (let hops = 0; hops <= 40; hops += 1) {
28321
28407
  const segments = resolved.split("/").filter(Boolean);
28322
28408
  let current3 = this.root;
28409
+ let mounted;
28323
28410
  let linked = false;
28324
28411
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
28325
- const child = current3 && current3.children ? current3.children.get(segments[index2]) : void 0;
28412
+ const child = mounted ? mounted.tree.node("/" + segments.slice(mounted.at + 1, index2 + 1).join("/")) : current3 && current3.children ? current3.children.get(segments[index2]) : void 0;
28326
28413
  if (!child) throw createNodeError("ENOENT", "realpath", path2);
28414
+ if (!mounted && child.mount) mounted = { tree: child.mount, at: index2 };
28327
28415
  if (child.type === "symlink") {
28328
28416
  const linkPath = "/" + segments.slice(0, index2 + 1).join("/");
28329
28417
  const rest = segments.slice(index2 + 1).join("/");
@@ -29379,7 +29467,7 @@ const _runStreams = /* @__PURE__ */ new Map();
29379
29467
  async function runHostedNode(host2, launch) {
29380
29468
  var _a2;
29381
29469
  const { token, streams } = launch;
29382
- if (!runPid(token)) setRunPid(token, mintPid(), 0);
29470
+ if (!runPid(token)) setRunPid(token, mintPid(), 0, { argv: ["node", ...launch.argv], cwd: launch.cwd });
29383
29471
  let input;
29384
29472
  try {
29385
29473
  (_a2 = streams == null ? void 0 : streams.signal) == null ? void 0 : _a2.throwIfAborted();
@@ -29638,7 +29726,7 @@ function initChildProcess(vfs2) {
29638
29726
  ...runPid(runToken) ? { pid: runPid(runToken).pid, ppid: runPid(runToken).ppid } : {}
29639
29727
  });
29640
29728
  const proc = runtime.getProcess();
29641
- if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid);
29729
+ if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid, { argv: ["node", ...args], cwd: ctx.cwd });
29642
29730
  const releaseRun = runToken === null ? null : __recordRun(runToken, {
29643
29731
  process: proc,
29644
29732
  stdout: appendStdout,
@@ -30384,7 +30472,12 @@ function startChildRun(request) {
30384
30472
  var _a2;
30385
30473
  const token = `child-${__nextChildRun++}`;
30386
30474
  const pid = mintPid();
30387
- setRunPid(token, pid, ((_a2 = runPid(nodeProcessRealmToken() ?? __currentProcessToken() ?? __lastLaunchedToken)) == null ? void 0 : _a2.pid) ?? 0);
30475
+ setRunPid(
30476
+ token,
30477
+ pid,
30478
+ ((_a2 = runPid(nodeProcessRealmToken() ?? __currentProcessToken() ?? __lastLaunchedToken)) == null ? void 0 : _a2.pid) ?? 0,
30479
+ { argv: request.args.length ? request.args : [request.file], ...request.cwd ? { cwd: request.cwd } : {} }
30480
+ );
30388
30481
  const controller = new AbortController();
30389
30482
  const pendingStdin = [];
30390
30483
  const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === "node";
@@ -35562,7 +35655,7 @@ class WorkerRuntime {
35562
35655
  this.worker = new Worker(
35563
35656
  new URL(
35564
35657
  /* @vite-ignore */
35565
- "/assets/runtime-worker-DBtzjAkn.js",
35658
+ "/assets/runtime-worker-iInsmIAS.js",
35566
35659
  import.meta.url
35567
35660
  ),
35568
35661
  { type: "module" }
@@ -38278,6 +38371,16 @@ function createContainer(options) {
38278
38371
  },
38279
38372
  /** The ports the servers the named run opened are listening on. */
38280
38373
  processPorts: (token) => __ownedServerPorts(token),
38374
+ portPid: (port) => {
38375
+ var _a2;
38376
+ const listener = listenerOnPort(port);
38377
+ return listener ? (_a2 = runPid(ownerOf(listener))) == null ? void 0 : _a2.pid : void 0;
38378
+ },
38379
+ /**
38380
+ * Deliver a signal to the named run as another process's `kill(pid)` does:
38381
+ * the guest's listeners for it run, else its default action ends the run.
38382
+ */
38383
+ signalProcess: (token, signal) => __signalOwnedProcess(token, signal),
38281
38384
  /** End the named run: its timers stop and its servers are released. */
38282
38385
  stopProcess: (token) => {
38283
38386
  const known = __runFor(token) !== void 0 || __ownedServerPorts(token).length > 0;
@@ -38350,6 +38453,7 @@ export {
38350
38453
  nodeProcessHostInstalled,
38351
38454
  index as npm,
38352
38455
  ownerProcessRegistryScope,
38456
+ ownerProcessTable,
38353
38457
  path,
38354
38458
  perfHooksShim as perf_hooks,
38355
38459
  processByPid,
@@ -2,6 +2,12 @@
2
2
  export interface ProcessIdentity {
3
3
  readonly pid: number;
4
4
  readonly ppid: number;
5
+ /** What the process was started as, where its starter said: `/proc/<pid>/cmdline`. */
6
+ readonly argv?: readonly string[];
7
+ /** The directory it was started in: `/proc/<pid>/cwd`. */
8
+ readonly cwd?: string;
9
+ /** When it started, in milliseconds since the epoch. */
10
+ readonly startedAt?: number;
5
11
  }
6
12
  /** A run already admitted by the container owner into this realm's scope. */
7
13
  export interface InitialProcessRegistration {
@@ -36,4 +42,5 @@ export interface ProcessRegistryScope extends ProcessRegistry {
36
42
  */
37
43
  export declare function createProcessRegistryOwner(): {
38
44
  createScope(): ProcessRegistryScope;
45
+ table(): ProcessIdentity[];
39
46
  };
@@ -15,7 +15,7 @@
15
15
  * command gives its process has it removed.
16
16
  */
17
17
  import type { Process } from './shims/process';
18
- import { type ProcessRegistry, type ProcessRegistryScope, type InitialProcessRegistration } from './process-registry';
18
+ import { type ProcessIdentity, type ProcessRegistry, type ProcessRegistryScope, type InitialProcessRegistration } from './process-registry';
19
19
  /** Whatever the embedding runtime uses to name one guest process. */
20
20
  export type ProcessToken = string;
21
21
  /** The environment variable a run's name travels in, from `exec` to `node`. */
@@ -64,8 +64,18 @@ export declare function __currentProcessToken(): ProcessToken | null;
64
64
  * half, `__releaseOwnedServers`, which calls this for the timers.
65
65
  */
66
66
  export declare function __stopOwnedProcess(token: ProcessToken): void;
67
+ /**
68
+ * Another process's `kill(pid, signal)`, delivered to a named run's guest.
69
+ * On a machine the target's handlers run, or its default action ends it; the
70
+ * guest's own `process.kill(process.pid, signal)` is exactly that, so the
71
+ * signal is raised through it, as the run. False when the run is gone or the
72
+ * signal is not one Node names.
73
+ */
74
+ export declare function __signalOwnedProcess(token: ProcessToken, signal: string): boolean;
67
75
  /** Each child realm gets its own registration authority in this container. */
68
76
  export declare function createProcessRegistryScope(): ProcessRegistryScope;
77
+ /** Trusted container owner only: every live process of the container, from every realm. */
78
+ export declare function ownerProcessTable(): ProcessIdentity[];
69
79
  /** Trusted container owner only; worker clients cannot expose their authority. */
70
80
  export declare function ownerProcessRegistryScope(): ProcessRegistryScope;
71
81
  /** Startup-only host seam; never swap identity authorities under live runs. */
@@ -73,7 +83,10 @@ export declare function installProcessRegistry(registry: ProcessRegistry, initia
73
83
  /** The next process number this engine hands out. */
74
84
  export declare function mintPid(): number;
75
85
  /** Record the numbers a named run was started with, for the run to read back. */
76
- export declare function setRunPid(token: ProcessToken, pid: number, ppid: number): void;
86
+ export declare function setRunPid(token: ProcessToken, pid: number, ppid: number, started?: {
87
+ argv?: readonly string[];
88
+ cwd?: string;
89
+ }): void;
77
90
  /** The numbers a named run was started with, where one was recorded. */
78
91
  export declare function runPid(token: ProcessToken | null | undefined): {
79
92
  pid: number;
@@ -37,6 +37,22 @@ export interface FSNode {
37
37
  * apart). Absent until something sets it, which is the write time.
38
38
  */
39
39
  atime?: number;
40
+ /**
41
+ * A tree mounted here: the paths below this node are the tree's, answered
42
+ * when they are read, and never this filesystem's to write. Linux's
43
+ * `/proc` is one.
44
+ */
45
+ mount?: MountedTree;
46
+ }
47
+ /**
48
+ * A read-only tree another party answers, mounted at a directory: `node`
49
+ * returns what is at a path relative to the mount point (`/` is the mount
50
+ * point itself), computed when it is asked, or undefined where nothing is.
51
+ * A directory's `children` names what a listing shows; a link's `target` is
52
+ * followed as any link is.
53
+ */
54
+ export interface MountedTree {
55
+ node(path: string): FSNode | undefined;
40
56
  }
41
57
  type VFSChangeListener = (path: string, content: string) => void;
42
58
  type VFSDeleteListener = (path: string) => void;
@@ -83,7 +99,7 @@ export interface NodeError extends Error {
83
99
  syscall: string;
84
100
  path?: string;
85
101
  }
86
- export declare function createNodeError(code: 'ENOENT' | 'ENOTDIR' | 'EISDIR' | 'EEXIST' | 'ENOTEMPTY' | 'ELOOP' | 'EINVAL', syscall: string, path: string, message?: string): NodeError;
102
+ export declare function createNodeError(code: 'ENOENT' | 'ENOTDIR' | 'EISDIR' | 'EEXIST' | 'ENOTEMPTY' | 'ELOOP' | 'EINVAL' | 'EROFS', syscall: string, path: string, message?: string): NodeError;
87
103
  export declare class VirtualFS {
88
104
  private root;
89
105
  private encoder;
@@ -140,6 +156,14 @@ export declare class VirtualFS {
140
156
  getNode(path: string): FSNode | undefined;
141
157
  /** The node at a path, following every link on the way and, when `follow`, the last one too. */
142
158
  __substrateNode(path: string, follow: boolean, depth: number): FSNode | undefined;
159
+ /**
160
+ * Mount a read-only tree at a directory, as Linux mounts `/proc`: reads
161
+ * below it are the tree's answers, writes fail with `EROFS`, and a snapshot
162
+ * leaves it out. Answers the unmount.
163
+ */
164
+ mount(path: string, tree: MountedTree): () => void;
165
+ /** A write at or below a mount point is refused, as a read-only filesystem refuses it. */
166
+ private assertWritable;
143
167
  /** A link's target as an absolute path: absolute as written, or relative to the link's directory. */
144
168
  __substrateLinkTarget(linkPath: string, target: string): string;
145
169
  symlinkSync(target: string, path: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/tabnode",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Node's own library, in a browser tab.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  // hands the host's own globals back. See `host-globals.ts`.
14
14
  export { restoreHostGlobals, guestRealmInstalled } from './host-globals';
15
15
  export { VirtualFS } from './virtual-fs';
16
- export type { FSNode, Stats, FSWatcher, WatchListener, WatchEventType } from './virtual-fs';
16
+ export type { FSNode, MountedTree, Stats, FSWatcher, WatchListener, WatchEventType } from './virtual-fs';
17
17
  export { Runtime, execute } from './runtime';
18
18
  export type { Module, RuntimeOptions, RequireFunction } from './runtime';
19
19
  export { createRuntime, WorkerRuntime } from './create-runtime';
@@ -54,11 +54,12 @@ import { PackageManager } from './npm';
54
54
  import { ServerBridge, getServerBridge } from './server-bridge';
55
55
  import { runCommand, registerRunStreams, releaseRunStreams, sendStdin } from './shims/child_process';
56
56
  import { Server as NetServer, __releaseOwnedHandles, type Socket as NetSocket } from './node-lib/net-module';
57
- import { __adoptHandle, type OwnedHandle } from './node-lib/binding/handles';
57
+ import { __adoptHandle, ownerOf, type OwnedHandle } from './node-lib/binding/handles';
58
+ import { listenerOnPort } from './node-lib/binding/tcp_wrap';
58
59
 
59
- import { __currentProcessToken, __runFor, __stopOwnedProcess, runPid, processByPid } from './process-tokens';
60
+ import { __currentProcessToken, __runFor, __signalOwnedProcess, __stopOwnedProcess, runPid, processByPid } from './process-tokens';
60
61
  export { runPid, processByPid };
61
- export { createProcessRegistryScope, installProcessRegistry, ownerProcessRegistryScope } from './process-tokens';
62
+ export { createProcessRegistryScope, installProcessRegistry, ownerProcessRegistryScope, ownerProcessTable } from './process-tokens';
62
63
  export { installNodeProcessHost, nodeProcessHostInstalled } from './node-process-host';
63
64
  export type { NodeProcessLaunch, NodeProcessHost } from './node-process-host';
64
65
  export type { ProcessIdentity, ProcessRegistry, ProcessRegistryScope, InitialProcessRegistration } from './process-registry';
@@ -123,6 +124,10 @@ export function createContainer(options?: ContainerOptions): {
123
124
  run: (command: string, options?: RunOptions) => Promise<RunResult>;
124
125
  pendingTimers: (token: string) => number;
125
126
  processPorts: (token: string) => number[];
127
+ /** The pid of the process listening on a port of this engine, where a guest process is: `/proc`'s socket owner. */
128
+ portPid: (port: number) => number | undefined;
129
+ /** Deliver a signal to the named run as another process's `kill(pid)` does; false when the run is gone. */
130
+ signalProcess: (token: string, signal: string) => boolean;
126
131
  stopProcess: (token: string) => boolean;
127
132
  /**
128
133
  * Input for one run's guest, by its process token; without a token, the most
@@ -208,6 +213,15 @@ export function createContainer(options?: ContainerOptions): {
208
213
  pendingTimers: (token: string): number => __runFor(token)?.pendingTimers() ?? 0,
209
214
  /** The ports the servers the named run opened are listening on. */
210
215
  processPorts: (token: string): number[] => __ownedServerPorts(token),
216
+ portPid: (port: number): number | undefined => {
217
+ const listener = listenerOnPort(port);
218
+ return listener ? runPid(ownerOf(listener as unknown as OwnedHandle))?.pid : undefined;
219
+ },
220
+ /**
221
+ * Deliver a signal to the named run as another process's `kill(pid)` does:
222
+ * the guest's listeners for it run, else its default action ends the run.
223
+ */
224
+ signalProcess: (token: string, signal: string): boolean => __signalOwnedProcess(token, signal),
211
225
  /** End the named run: its timers stop and its servers are released. */
212
226
  stopProcess: (token: string): boolean => {
213
227
  const known = __runFor(token) !== undefined || __ownedServerPorts(token).length > 0;
@@ -40,7 +40,7 @@ import { errname } from './uv';
40
40
  * the same shape here rather than widened there.
41
41
  */
42
42
  function createNodeError(code: string, syscall: string, path: string): Error {
43
- const known = ['EEXIST', 'EINVAL', 'EISDIR', 'ENOENT', 'ENOTDIR', 'ENOTEMPTY', 'ELOOP'];
43
+ const known = ['EEXIST', 'EINVAL', 'EISDIR', 'ENOENT', 'ENOTDIR', 'ENOTEMPTY', 'ELOOP', 'EROFS'];
44
44
  if (known.includes(code)) return vfsError(code as 'ENOENT', syscall, path);
45
45
  return Object.assign(new Error(`${code}: ${syscall} '${path}'`), { code, syscall, path, errno: -9 });
46
46
  }
@@ -2,6 +2,22 @@
2
2
  export interface ProcessIdentity {
3
3
  readonly pid: number;
4
4
  readonly ppid: number;
5
+ /** What the process was started as, where its starter said: `/proc/<pid>/cmdline`. */
6
+ readonly argv?: readonly string[];
7
+ /** The directory it was started in: `/proc/<pid>/cwd`. */
8
+ readonly cwd?: string;
9
+ /** When it started, in milliseconds since the epoch. */
10
+ readonly startedAt?: number;
11
+ }
12
+
13
+ /** The parts of an identity a starter may add, checked before they are kept. */
14
+ function describedIdentity(identity: ProcessIdentity): ProcessIdentity {
15
+ const { pid, ppid, argv, cwd, startedAt } = identity;
16
+ const described: { pid: number; ppid: number; argv?: readonly string[]; cwd?: string; startedAt?: number } = { pid, ppid };
17
+ if (Array.isArray(argv) && argv.length <= 4096 && argv.every(arg => typeof arg === 'string')) described.argv = Object.freeze([...argv]);
18
+ if (typeof cwd === 'string' && cwd.startsWith('/')) described.cwd = cwd;
19
+ if (typeof startedAt === 'number' && Number.isFinite(startedAt)) described.startedAt = startedAt;
20
+ return Object.freeze(described);
5
21
  }
6
22
 
7
23
  /** A run already admitted by the container owner into this realm's scope. */
@@ -38,7 +54,7 @@ export interface ProcessRegistryScope extends ProcessRegistry {
38
54
  * so identical local run tokens cannot overwrite or release another realm's
39
55
  * process. Allocation alone does not announce a live process.
40
56
  */
41
- export function createProcessRegistryOwner(): { createScope(): ProcessRegistryScope } {
57
+ export function createProcessRegistryOwner(): { createScope(): ProcessRegistryScope; table(): ProcessIdentity[] } {
42
58
  let nextPid = 1000 + Math.floor(Math.random() * 30000);
43
59
  const live = new Map<number, ProcessIdentity>();
44
60
  // Which realm's scope answers for each live pid, including after handoff.
@@ -104,7 +120,7 @@ export function createProcessRegistryOwner(): { createScope(): ProcessRegistrySc
104
120
  if (identity.ppid !== 0 && !Array.from(runs.values()).some(parent => parent.pid === identity.ppid)) {
105
121
  throw new Error('Parent process is not owned by this realm.');
106
122
  }
107
- const entry = Object.freeze({ pid: identity.pid, ppid: identity.ppid });
123
+ const entry = describedIdentity(identity);
108
124
  runs.set(token, entry);
109
125
  live.set(entry.pid, entry);
110
126
  receivers.set(entry.pid, receiver);
@@ -140,5 +156,9 @@ export function createProcessRegistryOwner(): { createScope(): ProcessRegistrySc
140
156
  scopes.set(scope, { allocated, runs, active });
141
157
  return scope;
142
158
  },
159
+ /** Every live process of the container, in pid order: what `/proc` lists. */
160
+ table() {
161
+ return [...live.values()].sort((a, b) => a.pid - b.pid);
162
+ },
143
163
  };
144
164
  }
@@ -17,7 +17,7 @@
17
17
 
18
18
  import type { Process } from './shims/process';
19
19
  import { AsyncLocalStorage } from './shims/async_hooks';
20
- import { createProcessRegistryOwner, type ProcessRegistry, type ProcessRegistryScope, type InitialProcessRegistration } from './process-registry';
20
+ import { createProcessRegistryOwner, type ProcessIdentity, type ProcessRegistry, type ProcessRegistryScope, type InitialProcessRegistration } from './process-registry';
21
21
 
22
22
  /** Whatever the embedding runtime uses to name one guest process. */
23
23
  export type ProcessToken = string;
@@ -113,6 +113,27 @@ export function __stopOwnedProcess(token: ProcessToken): void {
113
113
  runs.get(token)?.stopTimers();
114
114
  }
115
115
 
116
+ /**
117
+ * Another process's `kill(pid, signal)`, delivered to a named run's guest.
118
+ * On a machine the target's handlers run, or its default action ends it; the
119
+ * guest's own `process.kill(process.pid, signal)` is exactly that, so the
120
+ * signal is raised through it, as the run. False when the run is gone or the
121
+ * signal is not one Node names.
122
+ */
123
+ export function __signalOwnedProcess(token: ProcessToken, signal: string): boolean {
124
+ const run = runs.get(token);
125
+ if (!run) return false;
126
+ const guest = run.process as unknown as { pid: number; kill(pid: number, signal: string): unknown };
127
+ try { enterRun(token, () => guest.kill(guest.pid, signal)); }
128
+ catch (error) {
129
+ // A default action ends the process by `exit`, which a synchronous guest
130
+ // frame reports by throwing; that is the signal delivered.
131
+ if (error instanceof Error && error.message.startsWith("Process exited with code")) return true;
132
+ return false;
133
+ }
134
+ return true;
135
+ }
136
+
116
137
  /**
117
138
  * A process's own number.
118
139
  *
@@ -147,6 +168,12 @@ export function createProcessRegistryScope(): ProcessRegistryScope {
147
168
  return processRegistryOwner.createScope();
148
169
  }
149
170
 
171
+ /** Trusted container owner only: every live process of the container, from every realm. */
172
+ export function ownerProcessTable(): ProcessIdentity[] {
173
+ if (registryInstalled) throw new Error('This realm is a process registry client, not the container owner.');
174
+ return processRegistryOwner.table();
175
+ }
176
+
150
177
  /** Trusted container owner only; worker clients cannot expose their authority. */
151
178
  export function ownerProcessRegistryScope(): ProcessRegistryScope {
152
179
  if (registryInstalled) throw new Error('This realm is a process registry client, not the container owner.');
@@ -177,9 +204,9 @@ export function mintPid(): number {
177
204
  }
178
205
 
179
206
  /** Record the numbers a named run was started with, for the run to read back. */
180
- export function setRunPid(token: ProcessToken, pid: number, ppid: number): void {
207
+ export function setRunPid(token: ProcessToken, pid: number, ppid: number, started?: { argv?: readonly string[]; cwd?: string }): void {
181
208
  registryUsed = true;
182
- processRegistry.publish(token, { pid, ppid });
209
+ processRegistry.publish(token, { pid, ppid, ...started, startedAt: Date.now() });
183
210
  pidsOfRuns.set(token, { pid, ppid });
184
211
  }
185
212
 
@@ -199,7 +199,7 @@ const _runStreams = new Map<ProcessToken, RunStreams>();
199
199
  /** Submit before returning to a spawning parent; completion remains async. */
200
200
  async function runHostedNode(host: NodeProcessHost, launch: Omit<NodeProcessLaunch, 'identity' | 'inherited' | 'stdinStream'>): Promise<CommandOutcome> {
201
201
  const { token, streams } = launch;
202
- if (!runPid(token)) setRunPid(token, mintPid(), 0);
202
+ if (!runPid(token)) setRunPid(token, mintPid(), 0, { argv: ['node', ...launch.argv], cwd: launch.cwd });
203
203
  let input: ReturnType<typeof nodeProcessInput> | undefined;
204
204
  try {
205
205
  streams?.signal?.throwIfAborted();
@@ -578,7 +578,7 @@ export function initChildProcess(vfs: VirtualFS): void {
578
578
  // This run's numbers, recorded under its name: a child it spawns reads
579
579
  // them for its own `ppid`, and `process.kill(pid, 0)` asks this registry
580
580
  // whether a pid is a live process.
581
- if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid);
581
+ if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid, { argv: ['node', ...args], cwd: ctx.cwd });
582
582
  const releaseRun = runToken === null ? null : __recordRun(runToken, {
583
583
  process: proc,
584
584
  stdout: appendStdout,
@@ -1680,7 +1680,8 @@ function startChildRun(request: RunRequest): StartedRun {
1680
1680
  const pid = mintPid();
1681
1681
  // In a process realm the admitted guest is always the spawning parent.
1682
1682
  // Async bookkeeping for a routed child must not make that child its parent.
1683
- setRunPid(token, pid, runPid(nodeProcessRealmToken() ?? __currentProcessToken() ?? __lastLaunchedToken)?.pid ?? 0);
1683
+ setRunPid(token, pid, runPid(nodeProcessRealmToken() ?? __currentProcessToken() ?? __lastLaunchedToken)?.pid ?? 0,
1684
+ { argv: request.args.length ? request.args : [request.file], ...(request.cwd ? { cwd: request.cwd } : {}) });
1684
1685
  const controller = new AbortController();
1685
1686
  const pendingStdin: Array<Uint8Array | null> = [];
1686
1687
  // A host terminal consumes input incrementally. The old string-only
package/src/virtual-fs.ts CHANGED
@@ -44,6 +44,23 @@ export interface FSNode {
44
44
  * apart). Absent until something sets it, which is the write time.
45
45
  */
46
46
  atime?: number;
47
+ /**
48
+ * A tree mounted here: the paths below this node are the tree's, answered
49
+ * when they are read, and never this filesystem's to write. Linux's
50
+ * `/proc` is one.
51
+ */
52
+ mount?: MountedTree;
53
+ }
54
+
55
+ /**
56
+ * A read-only tree another party answers, mounted at a directory: `node`
57
+ * returns what is at a path relative to the mount point (`/` is the mount
58
+ * point itself), computed when it is asked, or undefined where nothing is.
59
+ * A directory's `children` names what a listing shows; a link's `target` is
60
+ * followed as any link is.
61
+ */
62
+ export interface MountedTree {
63
+ node(path: string): FSNode | undefined;
47
64
  }
48
65
 
49
66
  // Identity belongs to the node, so writes and renames preserve it while an
@@ -116,7 +133,7 @@ export interface NodeError extends Error {
116
133
  }
117
134
 
118
135
  export function createNodeError(
119
- code: 'ENOENT' | 'ENOTDIR' | 'EISDIR' | 'EEXIST' | 'ENOTEMPTY' | 'ELOOP' | 'EINVAL',
136
+ code: 'ENOENT' | 'ENOTDIR' | 'EISDIR' | 'EEXIST' | 'ENOTEMPTY' | 'ELOOP' | 'EINVAL' | 'EROFS',
120
137
  syscall: string,
121
138
  path: string,
122
139
  message?: string
@@ -127,6 +144,7 @@ export function createNodeError(
127
144
  EISDIR: -21,
128
145
  EEXIST: -17,
129
146
  ENOTEMPTY: -39,
147
+ EROFS: -30,
130
148
  };
131
149
 
132
150
  const messages: Record<string, string> = {
@@ -135,6 +153,7 @@ export function createNodeError(
135
153
  EISDIR: 'is a directory',
136
154
  EEXIST: 'file already exists',
137
155
  ENOTEMPTY: 'directory not empty',
156
+ EROFS: 'read-only file system',
138
157
  };
139
158
 
140
159
  const err = new Error(
@@ -216,6 +235,7 @@ export class VirtualFS {
216
235
  }
217
236
 
218
237
  private serializeNode(path: string, node: FSNode, files: VFSFileEntry[]): void {
238
+ if (node.mount) return; // a mounted tree is its answerer's, never the filesystem's contents
219
239
  if (node.type === 'file') {
220
240
  // Encode binary content as base64
221
241
  let content = '';
@@ -284,6 +304,7 @@ export class VirtualFS {
284
304
  */
285
305
  private writeFileSyncInternal(path: string, data: string | Uint8Array, emitEvent: boolean): void {
286
306
  const normalized = this.normalizePath(path);
307
+ this.assertWritable(normalized, 'open');
287
308
  const parentPath = this.getParentPath(normalized);
288
309
  const basename = this.getBasename(normalized);
289
310
 
@@ -377,11 +398,14 @@ export class VirtualFS {
377
398
  if (depth > 40) throw createNodeError('ELOOP', 'stat', path);
378
399
  const segments = this.getPathSegments(path);
379
400
  let current = this.root;
401
+ let mounted: { tree: MountedTree; at: number } | undefined;
380
402
  for (let index = 0; index < segments.length; index += 1) {
381
- if (current.type !== 'directory' || !current.children) {
403
+ if (current.type !== 'directory' || (!mounted && !current.children)) {
382
404
  return undefined;
383
405
  }
384
- const child = current.children.get(segments[index]);
406
+ const child = mounted
407
+ ? mounted.tree.node('/' + segments.slice(mounted.at + 1, index + 1).join('/'))
408
+ : current.children!.get(segments[index]);
385
409
  if (!child) {
386
410
  return undefined;
387
411
  }
@@ -391,16 +415,51 @@ export class VirtualFS {
391
415
  const target = this.__substrateLinkTarget(linkPath, child.target!);
392
416
  return this.__substrateNode(rest ? target + '/' + rest : target, follow, depth + 1);
393
417
  }
394
- current = child;
418
+ if (!mounted && child.mount) {
419
+ mounted = { tree: child.mount, at: index };
420
+ current = child.mount.node('/') ?? child;
421
+ } else {
422
+ current = child;
423
+ }
395
424
  }
396
425
  return current;
397
426
  }
427
+
428
+ /**
429
+ * Mount a read-only tree at a directory, as Linux mounts `/proc`: reads
430
+ * below it are the tree's answers, writes fail with `EROFS`, and a snapshot
431
+ * leaves it out. Answers the unmount.
432
+ */
433
+ mount(path: string, tree: MountedTree): () => void {
434
+ const normalized = this.normalizePath(path);
435
+ const basename = this.getBasename(normalized);
436
+ if (!basename) throw createNodeError('EINVAL', 'mount', path);
437
+ const parent = this.ensureDirectory(this.getParentPath(normalized));
438
+ const existing = parent.children!.get(basename);
439
+ if (existing && (existing.type !== 'directory' || existing.mount || existing.children?.size)) {
440
+ throw createNodeError('EEXIST', 'mount', path);
441
+ }
442
+ const node: FSNode = { type: 'directory', children: new Map(), mtime: Date.now(), mode: 0o555, mount: tree };
443
+ parent.children!.set(basename, node);
444
+ return () => { if (parent.children!.get(basename) === node) parent.children!.delete(basename); };
445
+ }
446
+
447
+ /** A write at or below a mount point is refused, as a read-only filesystem refuses it. */
448
+ private assertWritable(path: string, syscall: string): void {
449
+ let current: FSNode | undefined = this.root;
450
+ for (const segment of this.getPathSegments(path)) {
451
+ current = current?.children?.get(segment);
452
+ if (!current) return;
453
+ if (current.mount) throw createNodeError('EROFS', syscall, path);
454
+ }
455
+ }
398
456
  /** A link's target as an absolute path: absolute as written, or relative to the link's directory. */
399
457
  __substrateLinkTarget(linkPath: string, target: string): string {
400
458
  return this.normalizePath(target.startsWith('/') ? target : this.getParentPath(linkPath) + '/' + target);
401
459
  }
402
460
  symlinkSync(target: string, path: string): void {
403
461
  const normalized = this.normalizePath(path);
462
+ this.assertWritable(normalized, 'symlink');
404
463
  const parentPath = this.getParentPath(normalized);
405
464
  const basename = this.getBasename(normalized);
406
465
  const parent = this.getNode(parentPath);
@@ -472,12 +531,14 @@ export class VirtualFS {
472
531
  * Get stats for path
473
532
  */
474
533
  chmodSync(path: string, mode: number): void {
534
+ this.assertWritable(path, 'chmod');
475
535
  const node = this.getNode(path);
476
536
  if (!node) throw createNodeError('ENOENT', 'chmod', path);
477
537
  node.mode = mode & 0o7777;
478
538
  }
479
539
 
480
540
  utimesSync(path: string, atime: number | Date, mtime: number | Date): void {
541
+ this.assertWritable(path, 'utime');
481
542
  const node = this.getNode(path);
482
543
  if (!node) throw createNodeError('ENOENT', 'utimes', path);
483
544
  node.atime = atime instanceof Date ? atime.getTime() : atime * 1000;
@@ -600,6 +661,9 @@ export class VirtualFS {
600
661
  */
601
662
  mkdirSync(path: string, options?: { recursive?: boolean }): void {
602
663
  const normalized = this.normalizePath(path);
664
+ // `mkdir -p` of a directory that is there, a mount point among them, is done
665
+ if (options?.recursive && this.getNode(normalized)?.type === 'directory') return;
666
+ this.assertWritable(normalized, 'mkdir');
603
667
 
604
668
  if (options?.recursive) {
605
669
  this.ensureDirectory(normalized);
@@ -657,6 +721,7 @@ export class VirtualFS {
657
721
  */
658
722
  unlinkSync(path: string): void {
659
723
  const normalized = this.normalizePath(path);
724
+ this.assertWritable(normalized, 'unlink');
660
725
  const parentPath = this.getParentPath(normalized);
661
726
  const basename = this.getBasename(normalized);
662
727
 
@@ -689,6 +754,7 @@ export class VirtualFS {
689
754
  */
690
755
  rmdirSync(path: string): void {
691
756
  const normalized = this.normalizePath(path);
757
+ this.assertWritable(normalized, 'rmdir');
692
758
  const parentPath = this.getParentPath(normalized);
693
759
  const basename = this.getBasename(normalized);
694
760
 
@@ -727,6 +793,8 @@ export class VirtualFS {
727
793
  renameSync(oldPath: string, newPath: string): void {
728
794
  const normalizedOld = this.normalizePath(oldPath);
729
795
  const normalizedNew = this.normalizePath(newPath);
796
+ this.assertWritable(normalizedOld, 'rename');
797
+ this.assertWritable(normalizedNew, 'rename');
730
798
 
731
799
  const oldParentPath = this.getParentPath(normalizedOld);
732
800
  const oldBasename = this.getBasename(normalizedOld);
@@ -846,10 +914,14 @@ export class VirtualFS {
846
914
  for (let hops = 0; hops <= 40; hops += 1) {
847
915
  const segments = resolved.split('/').filter(Boolean);
848
916
  let current: FSNode | undefined = this.root;
917
+ let mounted: { tree: MountedTree; at: number } | undefined;
849
918
  let linked = false;
850
919
  for (let index = 0; index < segments.length; index += 1) {
851
- const child: FSNode | undefined = current && current.children ? current.children.get(segments[index]) : void 0;
920
+ const child: FSNode | undefined = mounted
921
+ ? mounted.tree.node('/' + segments.slice(mounted.at + 1, index + 1).join('/'))
922
+ : current && current.children ? current.children.get(segments[index]) : void 0;
852
923
  if (!child) throw createNodeError('ENOENT', 'realpath', path);
924
+ if (!mounted && child.mount) mounted = { tree: child.mount, at: index };
853
925
  if (child.type === 'symlink') {
854
926
  const linkPath = '/' + segments.slice(0, index + 1).join('/');
855
927
  const rest = segments.slice(index + 1).join('/');