@volter/tabnode 0.5.3 → 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
  }
@@ -27130,6 +27142,10 @@ const stackOverrides = /* @__PURE__ */ new Map();
27130
27142
  const stackOverrideMap = {
27131
27143
  set(error, override) {
27132
27144
  stackOverrides.set(error, override);
27145
+ const nativeSetTimeout = globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout;
27146
+ nativeSetTimeout.call(globalThis, () => {
27147
+ if (stackOverrides.get(error) === override) stackOverrides.delete(error);
27148
+ }, 0);
27133
27149
  return this;
27134
27150
  },
27135
27151
  get: (error) => stackOverrides.get(error),
@@ -27567,14 +27583,16 @@ function createNodeError(code2, syscall, path2, message) {
27567
27583
  ENOTDIR: -20,
27568
27584
  EISDIR: -21,
27569
27585
  EEXIST: -17,
27570
- ENOTEMPTY: -39
27586
+ ENOTEMPTY: -39,
27587
+ EROFS: -30
27571
27588
  };
27572
27589
  const messages2 = {
27573
27590
  ENOENT: "no such file or directory",
27574
27591
  ENOTDIR: "not a directory",
27575
27592
  EISDIR: "is a directory",
27576
27593
  EEXIST: "file already exists",
27577
- ENOTEMPTY: "directory not empty"
27594
+ ENOTEMPTY: "directory not empty",
27595
+ EROFS: "read-only file system"
27578
27596
  };
27579
27597
  const err2 = new Error(
27580
27598
  `${code2}: ${messages2[code2]}, ${syscall} '${path2}'`
@@ -27633,6 +27651,7 @@ class VirtualFS {
27633
27651
  return { files };
27634
27652
  }
27635
27653
  serializeNode(path2, node, files) {
27654
+ if (node.mount) return;
27636
27655
  if (node.type === "file") {
27637
27656
  let content = "";
27638
27657
  if (node.content && node.content.length > 0) {
@@ -27688,6 +27707,7 @@ class VirtualFS {
27688
27707
  */
27689
27708
  writeFileSyncInternal(path2, data2, emitEvent) {
27690
27709
  const normalized = this.normalizePath(path2);
27710
+ this.assertWritable(normalized, "open");
27691
27711
  const parentPath = this.getParentPath(normalized);
27692
27712
  const basename = this.getBasename(normalized);
27693
27713
  if (!basename) {
@@ -27763,11 +27783,12 @@ class VirtualFS {
27763
27783
  if (depth > 40) throw createNodeError("ELOOP", "stat", path2);
27764
27784
  const segments = this.getPathSegments(path2);
27765
27785
  let current3 = this.root;
27786
+ let mounted;
27766
27787
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
27767
- if (current3.type !== "directory" || !current3.children) {
27788
+ if (current3.type !== "directory" || !mounted && !current3.children) {
27768
27789
  return void 0;
27769
27790
  }
27770
- 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]);
27771
27792
  if (!child) {
27772
27793
  return void 0;
27773
27794
  }
@@ -27777,16 +27798,53 @@ class VirtualFS {
27777
27798
  const target = this.__substrateLinkTarget(linkPath, child.target);
27778
27799
  return this.__substrateNode(rest ? target + "/" + rest : target, follow, depth + 1);
27779
27800
  }
27780
- 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
+ }
27781
27807
  }
27782
27808
  return current3;
27783
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
+ }
27784
27841
  /** A link's target as an absolute path: absolute as written, or relative to the link's directory. */
27785
27842
  __substrateLinkTarget(linkPath, target) {
27786
27843
  return this.normalizePath(target.startsWith("/") ? target : this.getParentPath(linkPath) + "/" + target);
27787
27844
  }
27788
27845
  symlinkSync(target, path2) {
27789
27846
  const normalized = this.normalizePath(path2);
27847
+ this.assertWritable(normalized, "symlink");
27790
27848
  const parentPath = this.getParentPath(normalized);
27791
27849
  const basename = this.getBasename(normalized);
27792
27850
  const parent = this.getNode(parentPath);
@@ -27847,11 +27905,13 @@ class VirtualFS {
27847
27905
  * Get stats for path
27848
27906
  */
27849
27907
  chmodSync(path2, mode) {
27908
+ this.assertWritable(path2, "chmod");
27850
27909
  const node = this.getNode(path2);
27851
27910
  if (!node) throw createNodeError("ENOENT", "chmod", path2);
27852
27911
  node.mode = mode & 4095;
27853
27912
  }
27854
27913
  utimesSync(path2, atime, mtime) {
27914
+ this.assertWritable(path2, "utime");
27855
27915
  const node = this.getNode(path2);
27856
27916
  if (!node) throw createNodeError("ENOENT", "utimes", path2);
27857
27917
  node.atime = atime instanceof Date ? atime.getTime() : atime * 1e3;
@@ -27957,7 +28017,10 @@ class VirtualFS {
27957
28017
  * Create directory, optionally with recursive parent creation
27958
28018
  */
27959
28019
  mkdirSync(path2, options) {
28020
+ var _a3;
27960
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");
27961
28024
  if (options == null ? void 0 : options.recursive) {
27962
28025
  this.ensureDirectory(normalized);
27963
28026
  return;
@@ -28002,6 +28065,7 @@ class VirtualFS {
28002
28065
  */
28003
28066
  unlinkSync(path2) {
28004
28067
  const normalized = this.normalizePath(path2);
28068
+ this.assertWritable(normalized, "unlink");
28005
28069
  const parentPath = this.getParentPath(normalized);
28006
28070
  const basename = this.getBasename(normalized);
28007
28071
  const parent = this.getNode(parentPath);
@@ -28024,6 +28088,7 @@ class VirtualFS {
28024
28088
  */
28025
28089
  rmdirSync(path2) {
28026
28090
  const normalized = this.normalizePath(path2);
28091
+ this.assertWritable(normalized, "rmdir");
28027
28092
  const parentPath = this.getParentPath(normalized);
28028
28093
  const basename = this.getBasename(normalized);
28029
28094
  if (!basename) {
@@ -28052,6 +28117,8 @@ class VirtualFS {
28052
28117
  renameSync(oldPath, newPath) {
28053
28118
  const normalizedOld = this.normalizePath(oldPath);
28054
28119
  const normalizedNew = this.normalizePath(newPath);
28120
+ this.assertWritable(normalizedOld, "rename");
28121
+ this.assertWritable(normalizedNew, "rename");
28055
28122
  const oldParentPath = this.getParentPath(normalizedOld);
28056
28123
  const oldBasename = this.getBasename(normalizedOld);
28057
28124
  const newParentPath = this.getParentPath(normalizedNew);
@@ -28150,10 +28217,12 @@ class VirtualFS {
28150
28217
  for (let hops = 0; hops <= 40; hops += 1) {
28151
28218
  const segments = resolved.split("/").filter(Boolean);
28152
28219
  let current3 = this.root;
28220
+ let mounted;
28153
28221
  let linked = false;
28154
28222
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
28155
- 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;
28156
28224
  if (!child) throw createNodeError("ENOENT", "realpath", path2);
28225
+ if (!mounted && child.mount) mounted = { tree: child.mount, at: index2 };
28157
28226
  if (child.type === "symlink") {
28158
28227
  const linkPath = "/" + segments.slice(0, index2 + 1).join("/");
28159
28228
  const rest = segments.slice(index2 + 1).join("/");
@@ -69749,7 +69818,7 @@ const _runStreams = /* @__PURE__ */ new Map();
69749
69818
  async function runHostedNode(host, launch) {
69750
69819
  var _a3;
69751
69820
  const { token, streams } = launch;
69752
- if (!runPid(token)) setRunPid(token, mintPid(), 0);
69821
+ if (!runPid(token)) setRunPid(token, mintPid(), 0, { argv: ["node", ...launch.argv], cwd: launch.cwd });
69753
69822
  let input;
69754
69823
  try {
69755
69824
  (_a3 = streams == null ? void 0 : streams.signal) == null ? void 0 : _a3.throwIfAborted();
@@ -69985,7 +70054,7 @@ function initChildProcess(vfs2) {
69985
70054
  ...runPid(runToken) ? { pid: runPid(runToken).pid, ppid: runPid(runToken).ppid } : {}
69986
70055
  });
69987
70056
  const proc = runtime2.getProcess();
69988
- 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 });
69989
70058
  const releaseRun = runToken === null ? null : __recordRun(runToken, {
69990
70059
  process: proc,
69991
70060
  stdout: appendStdout,
@@ -70711,7 +70780,12 @@ function startChildRun(request) {
70711
70780
  var _a3;
70712
70781
  const token = `child-${__nextChildRun++}`;
70713
70782
  const pid = mintPid();
70714
- 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
+ );
70715
70789
  const controller = new AbortController();
70716
70790
  const pendingStdin = [];
70717
70791
  const admittedNode = nodeProcessHostInstalled();
@@ -75839,17 +75913,27 @@ function __substrateCallSiteFileNames() {
75839
75913
  }
75840
75914
  return namer;
75841
75915
  };
75842
- const overriding = (error, sites) => {
75843
- const override = error !== null && typeof error === "object" ? stackOverrides.get(error) : void 0;
75844
- if (override) {
75845
- stackOverrides.delete(error);
75846
- return override(error, sites.map(named));
75847
- }
75848
- if (typeof guestPrepare === "function") return namerFor(guestPrepare)(error, sites);
75849
- return `${Error.prototype.toString.call(error)}${sites.map((site) => `
75916
+ const overridingFor = (guest) => {
75917
+ const known = typeof guest === "function" ? overridings.get(guest) : overridingNone;
75918
+ if (known) return known;
75919
+ const overriding = (error, sites) => {
75920
+ const override = error !== null && typeof error === "object" ? stackOverrides.get(error) : void 0;
75921
+ if (override) {
75922
+ stackOverrides.delete(error);
75923
+ return override(error, sites.map(named));
75924
+ }
75925
+ if (typeof guest === "function") return namerFor(guest)(error, sites);
75926
+ return `${Error.prototype.toString.call(error)}${sites.map((site) => `
75850
75927
  at ${String(site)}`).join("")}`;
75928
+ };
75929
+ guests.set(overriding, guest);
75930
+ if (typeof guest === "function") overridings.set(guest, overriding);
75931
+ else overridingNone = overriding;
75932
+ return overriding;
75851
75933
  };
75852
- const get2 = () => stackOverrides.size > 0 ? overriding : typeof guestPrepare === "function" ? namerFor(guestPrepare) : guestPrepare;
75934
+ const overridings = /* @__PURE__ */ new WeakMap();
75935
+ let overridingNone;
75936
+ const get2 = () => stackOverrides.size > 0 ? overridingFor(guestPrepare) : typeof guestPrepare === "function" ? namerFor(guestPrepare) : guestPrepare;
75853
75937
  get2.__substrateNamesFrames = true;
75854
75938
  defineOnHost(Error, "prepareStackTrace", {
75855
75939
  get: get2,
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
  }
@@ -27297,6 +27325,10 @@ const stackOverrides = /* @__PURE__ */ new Map();
27297
27325
  const stackOverrideMap = {
27298
27326
  set(error, override) {
27299
27327
  stackOverrides.set(error, override);
27328
+ const nativeSetTimeout = globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout;
27329
+ nativeSetTimeout.call(globalThis, () => {
27330
+ if (stackOverrides.get(error) === override) stackOverrides.delete(error);
27331
+ }, 0);
27300
27332
  return this;
27301
27333
  },
27302
27334
  get: (error) => stackOverrides.get(error),
@@ -27740,14 +27772,16 @@ function createNodeError(code2, syscall, path2, message) {
27740
27772
  ENOTDIR: -20,
27741
27773
  EISDIR: -21,
27742
27774
  EEXIST: -17,
27743
- ENOTEMPTY: -39
27775
+ ENOTEMPTY: -39,
27776
+ EROFS: -30
27744
27777
  };
27745
27778
  const messages2 = {
27746
27779
  ENOENT: "no such file or directory",
27747
27780
  ENOTDIR: "not a directory",
27748
27781
  EISDIR: "is a directory",
27749
27782
  EEXIST: "file already exists",
27750
- ENOTEMPTY: "directory not empty"
27783
+ ENOTEMPTY: "directory not empty",
27784
+ EROFS: "read-only file system"
27751
27785
  };
27752
27786
  const err2 = new Error(
27753
27787
  `${code2}: ${messages2[code2]}, ${syscall} '${path2}'`
@@ -27806,6 +27840,7 @@ class VirtualFS {
27806
27840
  return { files };
27807
27841
  }
27808
27842
  serializeNode(path2, node, files) {
27843
+ if (node.mount) return;
27809
27844
  if (node.type === "file") {
27810
27845
  let content = "";
27811
27846
  if (node.content && node.content.length > 0) {
@@ -27861,6 +27896,7 @@ class VirtualFS {
27861
27896
  */
27862
27897
  writeFileSyncInternal(path2, data2, emitEvent) {
27863
27898
  const normalized = this.normalizePath(path2);
27899
+ this.assertWritable(normalized, "open");
27864
27900
  const parentPath = this.getParentPath(normalized);
27865
27901
  const basename2 = this.getBasename(normalized);
27866
27902
  if (!basename2) {
@@ -27936,11 +27972,12 @@ class VirtualFS {
27936
27972
  if (depth > 40) throw createNodeError("ELOOP", "stat", path2);
27937
27973
  const segments = this.getPathSegments(path2);
27938
27974
  let current3 = this.root;
27975
+ let mounted;
27939
27976
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
27940
- if (current3.type !== "directory" || !current3.children) {
27977
+ if (current3.type !== "directory" || !mounted && !current3.children) {
27941
27978
  return void 0;
27942
27979
  }
27943
- 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]);
27944
27981
  if (!child) {
27945
27982
  return void 0;
27946
27983
  }
@@ -27950,16 +27987,53 @@ class VirtualFS {
27950
27987
  const target = this.__substrateLinkTarget(linkPath, child.target);
27951
27988
  return this.__substrateNode(rest ? target + "/" + rest : target, follow, depth + 1);
27952
27989
  }
27953
- 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
+ }
27954
27996
  }
27955
27997
  return current3;
27956
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
+ }
27957
28030
  /** A link's target as an absolute path: absolute as written, or relative to the link's directory. */
27958
28031
  __substrateLinkTarget(linkPath, target) {
27959
28032
  return this.normalizePath(target.startsWith("/") ? target : this.getParentPath(linkPath) + "/" + target);
27960
28033
  }
27961
28034
  symlinkSync(target, path2) {
27962
28035
  const normalized = this.normalizePath(path2);
28036
+ this.assertWritable(normalized, "symlink");
27963
28037
  const parentPath = this.getParentPath(normalized);
27964
28038
  const basename2 = this.getBasename(normalized);
27965
28039
  const parent = this.getNode(parentPath);
@@ -28020,11 +28094,13 @@ class VirtualFS {
28020
28094
  * Get stats for path
28021
28095
  */
28022
28096
  chmodSync(path2, mode) {
28097
+ this.assertWritable(path2, "chmod");
28023
28098
  const node = this.getNode(path2);
28024
28099
  if (!node) throw createNodeError("ENOENT", "chmod", path2);
28025
28100
  node.mode = mode & 4095;
28026
28101
  }
28027
28102
  utimesSync(path2, atime, mtime) {
28103
+ this.assertWritable(path2, "utime");
28028
28104
  const node = this.getNode(path2);
28029
28105
  if (!node) throw createNodeError("ENOENT", "utimes", path2);
28030
28106
  node.atime = atime instanceof Date ? atime.getTime() : atime * 1e3;
@@ -28130,7 +28206,10 @@ class VirtualFS {
28130
28206
  * Create directory, optionally with recursive parent creation
28131
28207
  */
28132
28208
  mkdirSync(path2, options) {
28209
+ var _a2;
28133
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");
28134
28213
  if (options == null ? void 0 : options.recursive) {
28135
28214
  this.ensureDirectory(normalized);
28136
28215
  return;
@@ -28175,6 +28254,7 @@ class VirtualFS {
28175
28254
  */
28176
28255
  unlinkSync(path2) {
28177
28256
  const normalized = this.normalizePath(path2);
28257
+ this.assertWritable(normalized, "unlink");
28178
28258
  const parentPath = this.getParentPath(normalized);
28179
28259
  const basename2 = this.getBasename(normalized);
28180
28260
  const parent = this.getNode(parentPath);
@@ -28197,6 +28277,7 @@ class VirtualFS {
28197
28277
  */
28198
28278
  rmdirSync(path2) {
28199
28279
  const normalized = this.normalizePath(path2);
28280
+ this.assertWritable(normalized, "rmdir");
28200
28281
  const parentPath = this.getParentPath(normalized);
28201
28282
  const basename2 = this.getBasename(normalized);
28202
28283
  if (!basename2) {
@@ -28225,6 +28306,8 @@ class VirtualFS {
28225
28306
  renameSync(oldPath, newPath) {
28226
28307
  const normalizedOld = this.normalizePath(oldPath);
28227
28308
  const normalizedNew = this.normalizePath(newPath);
28309
+ this.assertWritable(normalizedOld, "rename");
28310
+ this.assertWritable(normalizedNew, "rename");
28228
28311
  const oldParentPath = this.getParentPath(normalizedOld);
28229
28312
  const oldBasename = this.getBasename(normalizedOld);
28230
28313
  const newParentPath = this.getParentPath(normalizedNew);
@@ -28323,10 +28406,12 @@ class VirtualFS {
28323
28406
  for (let hops = 0; hops <= 40; hops += 1) {
28324
28407
  const segments = resolved.split("/").filter(Boolean);
28325
28408
  let current3 = this.root;
28409
+ let mounted;
28326
28410
  let linked = false;
28327
28411
  for (let index2 = 0; index2 < segments.length; index2 += 1) {
28328
- 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;
28329
28413
  if (!child) throw createNodeError("ENOENT", "realpath", path2);
28414
+ if (!mounted && child.mount) mounted = { tree: child.mount, at: index2 };
28330
28415
  if (child.type === "symlink") {
28331
28416
  const linkPath = "/" + segments.slice(0, index2 + 1).join("/");
28332
28417
  const rest = segments.slice(index2 + 1).join("/");
@@ -29382,7 +29467,7 @@ const _runStreams = /* @__PURE__ */ new Map();
29382
29467
  async function runHostedNode(host2, launch) {
29383
29468
  var _a2;
29384
29469
  const { token, streams } = launch;
29385
- if (!runPid(token)) setRunPid(token, mintPid(), 0);
29470
+ if (!runPid(token)) setRunPid(token, mintPid(), 0, { argv: ["node", ...launch.argv], cwd: launch.cwd });
29386
29471
  let input;
29387
29472
  try {
29388
29473
  (_a2 = streams == null ? void 0 : streams.signal) == null ? void 0 : _a2.throwIfAborted();
@@ -29641,7 +29726,7 @@ function initChildProcess(vfs2) {
29641
29726
  ...runPid(runToken) ? { pid: runPid(runToken).pid, ppid: runPid(runToken).ppid } : {}
29642
29727
  });
29643
29728
  const proc = runtime.getProcess();
29644
- 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 });
29645
29730
  const releaseRun = runToken === null ? null : __recordRun(runToken, {
29646
29731
  process: proc,
29647
29732
  stdout: appendStdout,
@@ -30387,7 +30472,12 @@ function startChildRun(request) {
30387
30472
  var _a2;
30388
30473
  const token = `child-${__nextChildRun++}`;
30389
30474
  const pid = mintPid();
30390
- 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
+ );
30391
30481
  const controller = new AbortController();
30392
30482
  const pendingStdin = [];
30393
30483
  const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === "node";
@@ -35520,17 +35610,27 @@ function __substrateCallSiteFileNames() {
35520
35610
  }
35521
35611
  return namer;
35522
35612
  };
35523
- const overriding = (error, sites) => {
35524
- const override = error !== null && typeof error === "object" ? stackOverrides.get(error) : void 0;
35525
- if (override) {
35526
- stackOverrides.delete(error);
35527
- return override(error, sites.map(named));
35528
- }
35529
- if (typeof guestPrepare === "function") return namerFor(guestPrepare)(error, sites);
35530
- return `${Error.prototype.toString.call(error)}${sites.map((site) => `
35613
+ const overridingFor = (guest) => {
35614
+ const known = typeof guest === "function" ? overridings.get(guest) : overridingNone;
35615
+ if (known) return known;
35616
+ const overriding = (error, sites) => {
35617
+ const override = error !== null && typeof error === "object" ? stackOverrides.get(error) : void 0;
35618
+ if (override) {
35619
+ stackOverrides.delete(error);
35620
+ return override(error, sites.map(named));
35621
+ }
35622
+ if (typeof guest === "function") return namerFor(guest)(error, sites);
35623
+ return `${Error.prototype.toString.call(error)}${sites.map((site) => `
35531
35624
  at ${String(site)}`).join("")}`;
35532
- };
35533
- const get2 = () => stackOverrides.size > 0 ? overriding : typeof guestPrepare === "function" ? namerFor(guestPrepare) : guestPrepare;
35625
+ };
35626
+ guests.set(overriding, guest);
35627
+ if (typeof guest === "function") overridings.set(guest, overriding);
35628
+ else overridingNone = overriding;
35629
+ return overriding;
35630
+ };
35631
+ const overridings = /* @__PURE__ */ new WeakMap();
35632
+ let overridingNone;
35633
+ const get2 = () => stackOverrides.size > 0 ? overridingFor(guestPrepare) : typeof guestPrepare === "function" ? namerFor(guestPrepare) : guestPrepare;
35534
35634
  get2.__substrateNamesFrames = true;
35535
35635
  defineOnHost(Error, "prepareStackTrace", {
35536
35636
  get: get2,
@@ -35555,7 +35655,7 @@ class WorkerRuntime {
35555
35655
  this.worker = new Worker(
35556
35656
  new URL(
35557
35657
  /* @vite-ignore */
35558
- "/assets/runtime-worker-CfmZOVKz.js",
35658
+ "/assets/runtime-worker-iInsmIAS.js",
35559
35659
  import.meta.url
35560
35660
  ),
35561
35661
  { type: "module" }
@@ -38271,6 +38371,16 @@ function createContainer(options) {
38271
38371
  },
38272
38372
  /** The ports the servers the named run opened are listening on. */
38273
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),
38274
38384
  /** End the named run: its timers stop and its servers are released. */
38275
38385
  stopProcess: (token) => {
38276
38386
  const known = __runFor(token) !== void 0 || __ownedServerPorts(token).length > 0;
@@ -38343,6 +38453,7 @@ export {
38343
38453
  nodeProcessHostInstalled,
38344
38454
  index as npm,
38345
38455
  ownerProcessRegistryScope,
38456
+ ownerProcessTable,
38346
38457
  path,
38347
38458
  perfHooksShim as perf_hooks,
38348
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;
@@ -9,7 +9,7 @@
9
9
  * registration; `Error.prepareStackTrace` is the door it has. Every process's
10
10
  * `internal/errors` writes here, and the realm's hook reads here, so the
11
11
  * override reaches V8 whichever process set it. An entry is consumed by the
12
- * stack it formats, as Node's is.
12
+ * stack it formats, as Node's is, and lapses at the end of the task that set it.
13
13
  */
14
14
  export type StackOverride = (error: unknown, sites: unknown[]) => unknown;
15
15
  export declare const stackOverrides: Map<object, StackOverride>;
@@ -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.3",
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
 
package/src/runtime.ts CHANGED
@@ -2942,13 +2942,25 @@ function __substrateCallSiteFileNames(): void {
2942
2942
  // override and every other error what it would have had: the guest's
2943
2943
  // function, else V8's own text, which is the error and one `at` line
2944
2944
  // per call site. Otherwise V8 is handed exactly what the guest set.
2945
- const overriding: Prepare = (error, sites) => {
2946
- const override = error !== null && typeof error === 'object' ? stackOverrides.get(error) : undefined;
2947
- if (override) { stackOverrides.delete(error as object); return override(error, sites.map(named)); }
2948
- if (typeof guestPrepare === 'function') return namerFor(guestPrepare as Prepare)(error, sites);
2949
- return `${Error.prototype.toString.call(error)}${sites.map(site => `\n at ${String(site)}`).join('')}`;
2945
+ const overridingFor = (guest: unknown): Prepare => {
2946
+ const known = typeof guest === 'function' ? overridings.get(guest) : overridingNone;
2947
+ if (known) return known;
2948
+ const overriding: Prepare = (error, sites) => {
2949
+ const override = error !== null && typeof error === 'object' ? stackOverrides.get(error) : undefined;
2950
+ if (override) { stackOverrides.delete(error as object); return override(error, sites.map(named)); }
2951
+ if (typeof guest === 'function') return namerFor(guest as Prepare)(error, sites);
2952
+ return `${Error.prototype.toString.call(error)}${sites.map(site => `\n at ${String(site)}`).join('')}`;
2953
+ };
2954
+ // A guest that saves the hook and restores it later restores the function
2955
+ // it read; that stands for the guest's own, as a namer does.
2956
+ guests.set(overriding, guest);
2957
+ if (typeof guest === 'function') overridings.set(guest, overriding);
2958
+ else overridingNone = overriding;
2959
+ return overriding;
2950
2960
  };
2951
- const get = () => (stackOverrides.size > 0 ? overriding
2961
+ const overridings = new WeakMap<object, Prepare>();
2962
+ let overridingNone: Prepare | undefined;
2963
+ const get = () => (stackOverrides.size > 0 ? overridingFor(guestPrepare)
2952
2964
  : typeof guestPrepare === 'function' ? namerFor(guestPrepare as Prepare) : guestPrepare);
2953
2965
  (get as { __substrateNamesFrames?: boolean }).__substrateNamesFrames = true;
2954
2966
  defineOnHost(Error, 'prepareStackTrace', {
@@ -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
@@ -9,7 +9,7 @@
9
9
  * registration; `Error.prepareStackTrace` is the door it has. Every process's
10
10
  * `internal/errors` writes here, and the realm's hook reads here, so the
11
11
  * override reaches V8 whichever process set it. An entry is consumed by the
12
- * stack it formats, as Node's is.
12
+ * stack it formats, as Node's is, and lapses at the end of the task that set it.
13
13
  */
14
14
  export type StackOverride = (error: unknown, sites: unknown[]) => unknown;
15
15
 
@@ -17,7 +17,16 @@ export const stackOverrides = new Map<object, StackOverride>();
17
17
 
18
18
  /** A WeakMap-shaped view of the table, which is what `internal/errors` holds. */
19
19
  export const stackOverrideMap = {
20
- set(this: object, error: object, override: StackOverride) { stackOverrides.set(error, override); return this; },
20
+ set(this: object, error: object, override: StackOverride) {
21
+ stackOverrides.set(error, override);
22
+ // Node's map is weak, so an override whose stack is never read costs nothing
23
+ // there. Here it would hold its error and keep every stack on the fallback
24
+ // formatter; Node's readers read in the same task, so it lapses after it.
25
+ // The realm's own timer: a guest's would count as that guest's work.
26
+ const nativeSetTimeout = (globalThis as { __browserRuntimeNativeSetTimeout?: typeof setTimeout }).__browserRuntimeNativeSetTimeout ?? setTimeout;
27
+ nativeSetTimeout.call(globalThis, () => { if (stackOverrides.get(error) === override) stackOverrides.delete(error); }, 0);
28
+ return this;
29
+ },
21
30
  get: (error: object) => stackOverrides.get(error),
22
31
  has: (error: object) => stackOverrides.has(error),
23
32
  delete: (error: object) => stackOverrides.delete(error),
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('/');