@ai-ecoverse/slicc-kernel 1.39.1 → 1.39.2

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.
package/README.md CHANGED
@@ -420,7 +420,7 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
420
420
 
421
421
  ### Exec
422
422
 
423
- An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux: the kernel keeps one copy for all holders, shared by their descriptors (writes through one are seen through the others), and frees it when the last one closes. That holds for files a program opened in its own worker too: every open, read-only ones included, is known to the kernel. When another process removes such a file, or renames another file over it, the kernel does not copy it: as NFS does with its "silly rename", it moves the file into a hidden directory, `.slicc-unlinked/`, at the root of the mount it lives on (or of the root file system), and serves that holder's reads and writes of the old path from there while the path is gone. No file data is held in memory for it. The file is deleted when the last holder closes it or exits, and the directory goes as soon as it is empty. A file created at the path afterwards is the new file for everyone, also for the holder; a holder whose file another process renames (rather than removes) still uses the old path. Listings in the kernel leave `.slicc-unlinked` out, but on an `fsa` or `hostfs` mount it is visible on the real machine while a removed file is held (add `.slicc-unlinked/` to a `.gitignore` there). Each kernel names its entries after itself and holds a Web Lock for that name while it runs; when a mount is set up (and the root at start), the kernel removes the entries whose owner holds no lock, so a tab that died while holding a file leaves nothing behind for long, and entries of other live tabs on the same origin are left alone. Where Web Locks are missing (the Node entry), nothing is swept. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
423
+ An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux, whether the descriptors live in the kernel or a program opened the file in its own worker: every open, read-only ones included, is known to the kernel, and all holders share the one file (writes through one are seen through the others). The kernel does not copy it: as NFS does with its "silly rename", it moves the file into a hidden directory, `.slicc-unlinked/`, at the root of the mount it lives on (or of the root file system), and serves the holders' reads and writes from there (a worker's I/O on the old path while the path is gone). No file data is held in memory for it. The file is deleted when the last holder closes it or exits, and the directory goes as soon as it is empty. A file created at the path afterwards is the new file for everyone, also for the holder; a holder whose file another process renames (rather than removes) still uses the old path. Listings in the kernel leave `.slicc-unlinked` out, but on an `fsa` or `hostfs` mount it is visible on the real machine while a removed file is held (add `.slicc-unlinked/` to a `.gitignore` there). Each kernel names its entries after itself and holds a Web Lock for that name while it runs; when a mount is set up (and the root at start), the kernel removes the entries whose owner holds no lock, so a tab that died while holding a file leaves nothing behind for long, and entries of other live tabs on the same origin are left alone. Under the Node entry, which has no Web Locks, entries are named after the kernel's process id instead, and an entry is removed when that process is gone (`kill(pid, 0)` fails with `ESRCH`); a reused pid leaves the entry alone. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
424
424
 
425
425
  - **Emscripten:** `Module.sliccKernel.execve(file, argv, env, cwd)` runs `file` as this process's new image on its fds 0, 1 and 2, waits for it, and returns its wait status, or a negative errno when it cannot start. `env` and `cwd` may be `null` for this process's own. An exec shim detects it with `typeof Module.sliccKernel.execve === 'function'` and exits with the status it returns, as `execve` never returns on success. Older shims spawn the program and then call `Module.sliccKernel.execWait(pid)`; that works the same way: a new program asks the kernel for its pid before `main`, and the kernel answers once its parent has made its next call, so an image an older shim execs also reports the pid it replaced (the wait starts once the program is loaded; should the parent make no call within 100 ms after that, the kernel answers anyway, and the image may then report its own pid; `launcher.identities` counts the programs that asked and those answered by that timeout).
426
426
  - **WASIX:** `proc_exec` does this by itself.
@@ -5315,16 +5315,20 @@ var UnlinkedKeeper = class {
5315
5315
  locks;
5316
5316
  busy;
5317
5317
  waiting = /* @__PURE__ */ new Set();
5318
- instance = crypto.randomUUID();
5318
+ owner;
5319
+ instance;
5319
5320
  gate = new PathGate();
5320
5321
  queue = Promise.resolve();
5321
5322
  count = 0;
5322
- constructor(fs, holders, rootOf, locks, busy = () => false) {
5323
+ constructor(fs, holders, rootOf, options = {}) {
5324
+ const { locks, owner } = options;
5323
5325
  this.fs = fs;
5324
5326
  this.holders = holders;
5325
5327
  this.rootOf = rootOf;
5326
5328
  this.locks = locks;
5327
- this.busy = busy;
5329
+ this.busy = options.busy ?? (() => false);
5330
+ this.owner = owner;
5331
+ this.instance = owner ? `${owner.id}~${crypto.randomUUID()}` : crypto.randomUUID();
5328
5332
  void locks?.request(lockName(this.instance), () => new Promise(() => void 0));
5329
5333
  }
5330
5334
  serial(op) {
@@ -5332,8 +5336,7 @@ var UnlinkedKeeper = class {
5332
5336
  this.queue = next.catch(() => void 0);
5333
5337
  return next;
5334
5338
  }
5335
- wrap() {
5336
- const fs = this.fs;
5339
+ wrap(fs = this.fs) {
5337
5340
  const keeper = this;
5338
5341
  const visible = (names) => names.filter((name2) => name2 !== UNLINKED_DIR);
5339
5342
  return {
@@ -5345,7 +5348,9 @@ var UnlinkedKeeper = class {
5345
5348
  else await keeper.settle(pinned, () => fs.rm(path, options));
5346
5349
  },
5347
5350
  async rename(from, to) {
5348
- const pinned = await keeper.pin(fs.resolvePath("/", to));
5351
+ const target = fs.resolvePath("/", to);
5352
+ if (target === fs.resolvePath("/", from)) return fs.rename(from, to);
5353
+ const pinned = await keeper.pin(target);
5349
5354
  await keeper.settle(pinned, () => fs.rename(from, to));
5350
5355
  },
5351
5356
  readdir: async (path) => visible(await fs.readdir(path)),
@@ -5365,8 +5370,8 @@ var UnlinkedKeeper = class {
5365
5370
  const held = /* @__PURE__ */ new Map();
5366
5371
  for (const holder of this.holders) {
5367
5372
  if (holder.owns(target)) continue;
5368
- for (const path of holder.opens.keys()) {
5369
- if (!within3(path, target) || holder.unlinked.has(path)) continue;
5373
+ for (const path of holder.held()) {
5374
+ if (!within3(path, target) || holder.isKept(path)) continue;
5370
5375
  held.set(path, [...held.get(path) ?? [], holder]);
5371
5376
  }
5372
5377
  }
@@ -5381,21 +5386,25 @@ var UnlinkedKeeper = class {
5381
5386
  const done4 = () => {
5382
5387
  for (const release of releases) release();
5383
5388
  };
5389
+ const files = /* @__PURE__ */ new Map();
5390
+ for (const [path, at] of hidden) {
5391
+ const holders = held.get(path);
5392
+ const file = this.keptFile(at, { refs: holders.length + 1 });
5393
+ files.set(path, file);
5394
+ for (const holder of holders) holder.keep(path, file);
5395
+ }
5384
5396
  return {
5385
5397
  moved: new Set(hidden.keys()),
5386
5398
  commit: () => {
5387
- for (const [path, at] of hidden) {
5388
- const holders = held.get(path);
5389
- const file = this.keptFile(at, { refs: holders.length });
5390
- for (const holder of holders) holder.unlinked.set(path, file);
5391
- }
5399
+ for (const file of files.values()) file.release();
5392
5400
  done4();
5393
5401
  },
5394
5402
  rollback: async () => {
5395
- for (const [path, at] of hidden) {
5403
+ for (const [path, file] of files) {
5404
+ for (const holder of held.get(path)) holder.unkeep(path, file);
5396
5405
  await this.serial(async () => {
5397
- await Promise.allSettled([this.fs.rename(at, path)]);
5398
- await this.tidy(parentOf3(at));
5406
+ await Promise.allSettled([this.fs.rename(file.hidden, path)]);
5407
+ await this.tidy(parentOf3(file.hidden));
5399
5408
  });
5400
5409
  }
5401
5410
  done4();
@@ -5441,16 +5450,18 @@ var UnlinkedKeeper = class {
5441
5450
  }
5442
5451
  sweep(root) {
5443
5452
  const query = this.locks?.query?.bind(this.locks);
5444
- if (!query) return Promise.resolve();
5453
+ const owner = this.owner;
5454
+ if (!query && !owner) return Promise.resolve();
5445
5455
  return this.serial(async () => {
5446
5456
  const dir = dirOf(root);
5447
5457
  const names = await this.fs.readdir(dir).catch(() => void 0);
5448
5458
  if (!names) return;
5449
- const held = new Set(((await query()).held ?? []).map((lock) => lock.name));
5459
+ const held = query && new Set(((await query()).held ?? []).map((lock) => lock.name));
5450
5460
  for (const name2 of names) {
5451
- const owner = name2.slice(0, name2.lastIndexOf("."));
5452
- if (owner === this.instance || held.has(lockName(owner))) continue;
5453
- await this.fs.rm(`${dir}/${name2}`, { recursive: true }).catch(() => void 0);
5461
+ const id = name2.slice(0, name2.lastIndexOf("."));
5462
+ const at = id.indexOf("~");
5463
+ const dead = id !== this.instance && (at < 0 ? held !== void 0 && !held.has(lockName(id)) : owner?.alive(id.slice(0, at)) === false);
5464
+ if (dead) await this.fs.rm(`${dir}/${name2}`, { recursive: true }).catch(() => void 0);
5454
5465
  }
5455
5466
  await this.tidy(dir);
5456
5467
  });
@@ -5884,6 +5895,18 @@ var HeldPaths = class extends Map {
5884
5895
  holding(path) {
5885
5896
  return this.opens.has(this.keyOf(path));
5886
5897
  }
5898
+ held() {
5899
+ return this.opens.keys();
5900
+ }
5901
+ isKept(path) {
5902
+ return this.unlinked.has(path);
5903
+ }
5904
+ keep(path, file) {
5905
+ this.unlinked.set(path, file);
5906
+ }
5907
+ unkeep(path) {
5908
+ this.unlinked.delete(path);
5909
+ }
5887
5910
  hold(path, on, open = false, real) {
5888
5911
  if (open) {
5889
5912
  const key = real ?? this.keyOf(path);
@@ -6751,6 +6774,9 @@ async function run(entry, path, req) {
6751
6774
  case "symlink":
6752
6775
  await fs.symlink(req.arg2 ?? "", path);
6753
6776
  return done;
6777
+ case "create":
6778
+ await createExclusive(fs, path);
6779
+ return done;
6754
6780
  case "readlink":
6755
6781
  return json2(await fs.readlink(path));
6756
6782
  case "chmod":
@@ -6770,6 +6796,15 @@ async function run(entry, path, req) {
6770
6796
  return { ok: false, errno: "EINVAL", message: `sync-fs: unknown op '${req.op}'` };
6771
6797
  }
6772
6798
  }
6799
+ async function createExclusive(fs, path) {
6800
+ if (fs.createExclusive) return fs.createExclusive(path);
6801
+ if (await fs.lstat(path).then(
6802
+ () => true,
6803
+ () => false
6804
+ ))
6805
+ throw syncError("EEXIST", path);
6806
+ await fs.writeFile(path, new Uint8Array(0));
6807
+ }
6773
6808
  async function ranged(fs, path, req) {
6774
6809
  const ops = rangedOps(fs);
6775
6810
  if (!ops) throw syncError("ENOSYS", path);
@@ -8384,6 +8419,8 @@ var VfsNode = class {
8384
8419
  };
8385
8420
  var VfsNodes = class {
8386
8421
  byPath = /* @__PURE__ */ new Map();
8422
+ kept = /* @__PURE__ */ new Map();
8423
+ keepsOnDisk = false;
8387
8424
  fs;
8388
8425
  entries = /* @__PURE__ */ new Map();
8389
8426
  onClosed;
@@ -8466,15 +8503,47 @@ var VfsNodes = class {
8466
8503
  return;
8467
8504
  }
8468
8505
  if (this.byPath.get(node.path) === node) this.byPath.delete(node.path);
8506
+ this.kept.get(node)?.release();
8507
+ this.kept.delete(node);
8469
8508
  this.onClosed?.(node.path);
8470
8509
  }
8510
+ held() {
8511
+ return this.byPath.keys();
8512
+ }
8513
+ owns() {
8514
+ return false;
8515
+ }
8516
+ isKept() {
8517
+ return false;
8518
+ }
8519
+ keep(path, file) {
8520
+ const node = this.byPath.get(path);
8521
+ if (!node) {
8522
+ file.release();
8523
+ return;
8524
+ }
8525
+ this.byPath.delete(path);
8526
+ node.renamedTo(file.hidden);
8527
+ this.byPath.set(file.hidden, node);
8528
+ this.kept.set(node, file);
8529
+ }
8530
+ unkeep(path, file) {
8531
+ const node = this.byPath.get(file.hidden);
8532
+ if (!node) return;
8533
+ this.byPath.delete(file.hidden);
8534
+ node.renamedTo(path);
8535
+ this.byPath.set(path, node);
8536
+ this.kept.delete(node);
8537
+ }
8471
8538
  async flush(path) {
8472
8539
  for (const [p, node] of this.byPath) if (within5(p, path)) await node.serial(() => node.flush());
8473
8540
  }
8474
8541
  async unlinking(path) {
8475
8542
  for (const [held, node] of [...this.byPath]) {
8476
8543
  if (!within5(held, path)) continue;
8477
- await node.serial(async () => void await Promise.all([node.keep(), node.limit()]));
8544
+ await node.serial(async () => {
8545
+ await Promise.all([this.keepsOnDisk || node.keep(), node.limit()]);
8546
+ });
8478
8547
  }
8479
8548
  }
8480
8549
  revoke(prefix) {
@@ -8604,6 +8673,7 @@ function keepingOpen(fs, nodes) {
8604
8673
  });
8605
8674
  },
8606
8675
  writeFile: (path, content) => nodes.onEntry(fs.resolvePath("/", path), () => fs.writeFile(path, content)),
8676
+ createExclusive: async (path) => nodes.createExclusive(await nodes.entryKey(fs.resolvePath("/", path))),
8607
8677
  symlink: (target, path) => nodes.onEntry(fs.resolvePath("/", path), () => fs.symlink(target, path)),
8608
8678
  mkdir: (path, options) => nodes.onEntry(fs.resolvePath("/", path), () => fs.mkdir(path, options))
8609
8679
  };
@@ -11262,15 +11332,20 @@ var Launcher = class {
11262
11332
  enableCdp(this.net, this.cdp);
11263
11333
  const fs = withCommandDirs(this.base, async () => new Set((await this.commands()).keys()));
11264
11334
  this.nodes = new VfsNodes(fs, (path) => this.released(path));
11335
+ this.nodes.keepsOnDisk = true;
11265
11336
  this.openFiles.add(this.nodes);
11337
+ const holders = [this.held, [this.nodes]];
11266
11338
  this.unlinked = new UnlinkedKeeper(
11267
11339
  fs,
11268
- this.held,
11340
+ { [Symbol.iterator]: () => holders.flatMap((set2) => [...set2]).values() },
11269
11341
  (path) => this.mountRoot(path),
11270
- webLocks(),
11271
- (path) => [...this.openFiles].some((nodes) => nodes.holds(path))
11342
+ {
11343
+ locks: webLocks(),
11344
+ busy: (path) => [...this.openFiles].some((nodes) => nodes.holds(path)),
11345
+ owner: options.unlinkOwner
11346
+ }
11272
11347
  );
11273
- this.fs = keepingOpen(this.unlinked.wrap(), this.nodes);
11348
+ this.fs = this.unlinked.wrap(keepingOpen(fs, this.nodes));
11274
11349
  void this.unlinked.sweep("/");
11275
11350
  this.watchers.watch([this.modulesDir, this.pnpmHome], { recursive: true }, () => {
11276
11351
  this.catalog = void 0;