@ai-ecoverse/slicc-kernel 1.117.1 → 1.117.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
@@ -701,7 +701,7 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
701
701
 
702
702
  ### Exec
703
703
 
704
- 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.
704
+ 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. A rename over a held file stays atomic for everyone else, as `rename(2)` is: moving the held file aside and the new one into its place are two steps, so a lookup of that name (`open`, `stat`, a read) that comes while they run waits for both, and one that started before them and missed the name in between looks again (also when it spelled the name through a symlinked parent, which the wait doesn't match by path: a miss is retried whenever any replacement finished meanwhile, up to three tries). Every open finds the old file or the new one, never no file (#903). The OPFS root does the same for its own two steps: a rename onto an existing name replaces the file with `move()` (or, in a browser without it, a copy into the name and a removal of the source), and a lookup that loses that race to a stale handle looks again. A `tmpfs` mount replaces the name in one step; on `fsa` and `hostfs` mounts it is the real file system's `rename`. 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.
705
705
 
706
706
  - **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).
707
707
  - **WASIX:** `proc_exec` does this by itself.
@@ -998,6 +998,7 @@ var ABSENT_MS = 2;
998
998
  var ABSENT_NAMES = 4096;
999
999
  var FILE_HANDLES = 4096;
1000
1000
  var STALE = /* @__PURE__ */ new Set(["NotFoundError", "TypeMismatchError"]);
1001
+ var REPLACED_ATTEMPTS = 4;
1001
1002
  var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
1002
1003
  var ERRNO_BY_DOM_ERROR = {
1003
1004
  NotFoundError: "ENOENT",
@@ -1132,6 +1133,8 @@ var OpfsFs = class {
1132
1133
  files = new PathMap();
1133
1134
  absent = new PathMap();
1134
1135
  changes = 0;
1136
+ replacing = Promise.resolve();
1137
+ replacements = 0;
1135
1138
  snapshots = /* @__PURE__ */ new WeakMap();
1136
1139
  found = /* @__PURE__ */ new WeakMap();
1137
1140
  checked = /* @__PURE__ */ new WeakMap();
@@ -1466,7 +1469,22 @@ var OpfsFs = class {
1466
1469
  }
1467
1470
  return void 0;
1468
1471
  }
1469
- async locate(path, follow2) {
1472
+ locate(path, follow2) {
1473
+ return this.unreplaced(() => this.located(path, follow2));
1474
+ }
1475
+ async unreplaced(op) {
1476
+ for (let attempt = 1; ; attempt++) {
1477
+ const seen = this.replacements;
1478
+ try {
1479
+ return await op();
1480
+ } catch (err) {
1481
+ if (attempt >= REPLACED_ATTEMPTS || !missing2(err)) throw err;
1482
+ await this.replacing;
1483
+ if (this.replacements === seen) throw err;
1484
+ }
1485
+ }
1486
+ }
1487
+ async located(path, follow2) {
1470
1488
  let current = path;
1471
1489
  for (let hops = 0; hops <= MAX_LINKS2; hops++) {
1472
1490
  try {
@@ -1664,11 +1682,11 @@ var OpfsFs = class {
1664
1682
  const stat = await this.statOf(found.path, found.handle, meta, touched);
1665
1683
  return { ...stat, ...ownOf(meta) ?? inherit(entries) };
1666
1684
  }
1667
- async stat(path) {
1668
- return this.describe(await this.locate(path, true));
1685
+ stat(path) {
1686
+ return this.unreplaced(async () => this.describe(await this.located(path, true)));
1669
1687
  }
1670
- async lstat(path) {
1671
- return this.describe(await this.locate(path, false));
1688
+ lstat(path) {
1689
+ return this.unreplaced(async () => this.describe(await this.located(path, false)));
1672
1690
  }
1673
1691
  async exists(path) {
1674
1692
  return this.locate(path, true).then(
@@ -1692,6 +1710,12 @@ var OpfsFs = class {
1692
1710
  }
1693
1711
  }
1694
1712
  }
1713
+ reading(path, op) {
1714
+ return this.unreplaced(async () => {
1715
+ const handle = await this.existing(path);
1716
+ return this.retrying(path, () => op(handle), false);
1717
+ });
1718
+ }
1695
1719
  async existing(path, writing = false) {
1696
1720
  const found = await this.locate(path, true);
1697
1721
  if (writing) this.refuse(found.path);
@@ -1705,52 +1729,35 @@ var OpfsFs = class {
1705
1729
  if (entry?.fifo || entry?.socket) throw fsError("ENXIO", path);
1706
1730
  }
1707
1731
  async readFileBuffer(path) {
1708
- const handle = await this.existing(path);
1709
- const bytes = await this.retrying(
1732
+ const bytes = await this.reading(
1710
1733
  path,
1711
- async () => new Uint8Array(await (await handle.getFile()).arrayBuffer()),
1712
- false
1734
+ async (handle) => new Uint8Array(await (await handle.getFile()).arrayBuffer())
1713
1735
  );
1714
1736
  await this.refuseNode(path, bytes.length === 0);
1715
1737
  return bytes;
1716
1738
  }
1717
1739
  async readVersioned(path, cached = false) {
1718
- const handle = await this.existing(path);
1719
- return this.retrying(
1720
- path,
1721
- async () => {
1722
- const stamp = await this.stamp(path, cached);
1723
- const file = await handle.getFile();
1724
- const bytes = new Uint8Array(await file.arrayBuffer());
1725
- await this.refuseNode(path, bytes.length === 0);
1726
- return { bytes, version: `${file.lastModified}:${file.size}:${stamp}` };
1727
- },
1728
- false
1729
- );
1740
+ return this.reading(path, async (handle) => {
1741
+ const stamp = await this.stamp(path, cached);
1742
+ const file = await handle.getFile();
1743
+ const bytes = new Uint8Array(await file.arrayBuffer());
1744
+ await this.refuseNode(path, bytes.length === 0);
1745
+ return { bytes, version: `${file.lastModified}:${file.size}:${stamp}` };
1746
+ });
1730
1747
  }
1731
1748
  async version(path, cached = false) {
1732
- const handle = await this.existing(path);
1733
- return this.retrying(
1734
- path,
1735
- async () => {
1736
- const stamp = await this.stamp(path, cached);
1737
- const file = await handle.getFile();
1738
- return `${file.lastModified}:${file.size}:${stamp}`;
1739
- },
1740
- false
1741
- );
1749
+ return this.reading(path, async (handle) => {
1750
+ const stamp = await this.stamp(path, cached);
1751
+ const file = await handle.getFile();
1752
+ return `${file.lastModified}:${file.size}:${stamp}`;
1753
+ });
1742
1754
  }
1743
1755
  async pread(path, offset, length2) {
1744
- const handle = await this.existing(path);
1745
- return this.retrying(
1746
- path,
1747
- async () => {
1748
- const file = await handle.getFile();
1749
- await this.refuseNode(path, file.size === 0);
1750
- return new Uint8Array(await file.slice(offset, offset + length2).arrayBuffer());
1751
- },
1752
- false
1753
- );
1756
+ return this.reading(path, async (handle) => {
1757
+ const file = await handle.getFile();
1758
+ await this.refuseNode(path, file.size === 0);
1759
+ return new Uint8Array(await file.slice(offset, offset + length2).arrayBuffer());
1760
+ });
1754
1761
  }
1755
1762
  async pwrite(path, offset, bytes) {
1756
1763
  const [handle, , target] = await this.fileFor(path);
@@ -2063,7 +2070,10 @@ var OpfsFs = class {
2063
2070
  const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
2064
2071
  this.forget(destination);
2065
2072
  const timed2 = await this.explicitTimes(source.path);
2066
- const moved2 = await this.place(source.handle, dir, staging);
2073
+ const moved2 = await this.replacingTarget(
2074
+ targetStat !== null,
2075
+ () => this.place(source.handle, dir, staging)
2076
+ );
2067
2077
  if (swap) await this.rm(destination, { recursive: true });
2068
2078
  await this.meta.move(source.path, destination, carry);
2069
2079
  this.movePending(source.path, destination);
@@ -2079,6 +2089,21 @@ var OpfsFs = class {
2079
2089
  this.changed(source.path, destination);
2080
2090
  this.touch(Date.now(), parentOf3(source.path), parent.path);
2081
2091
  }
2092
+ async replacingTarget(replaces, op) {
2093
+ if (!replaces) return op();
2094
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
2095
+ const before = this.replacing;
2096
+ this.replacing = Promise.all([before, promise]).then(() => void 0);
2097
+ this.replacements++;
2098
+ this.changes++;
2099
+ try {
2100
+ return await op();
2101
+ } finally {
2102
+ this.replacements++;
2103
+ this.changes++;
2104
+ resolve2();
2105
+ }
2106
+ }
2082
2107
  async modified(path) {
2083
2108
  const handle = await this.handle(path);
2084
2109
  return handle.kind === "file" ? (await handle.getFile()).lastModified : 0;
@@ -6260,7 +6285,7 @@ function satisfies(version, range2) {
6260
6285
  }
6261
6286
 
6262
6287
  // src/version.ts
6263
- var KERNEL_VERSION = true ? "1.117.1" : "0.0.0-dev";
6288
+ var KERNEL_VERSION = true ? "1.117.2" : "0.0.0-dev";
6264
6289
  var OS_RELEASE = "6.0.0-slicc";
6265
6290
  function procVersion() {
6266
6291
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -7089,6 +7114,16 @@ var UNLINKED_DIR = ".slicc-unlinked";
7089
7114
  function webLocks() {
7090
7115
  return globalThis.navigator?.locks;
7091
7116
  }
7117
+ var LOOKUP_OPS = [
7118
+ "exists",
7119
+ "readFile",
7120
+ "readFileBuffer",
7121
+ "readVersioned",
7122
+ "version",
7123
+ "pread",
7124
+ "readlink",
7125
+ "realpath"
7126
+ ];
7092
7127
  var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
7093
7128
  var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
7094
7129
  var lockName = (instance) => `slicc-unlinked:${instance}`;
@@ -7127,6 +7162,8 @@ var UnlinkedKeeper = class {
7127
7162
  gate = new PathGate();
7128
7163
  queue = Promise.resolve();
7129
7164
  count = 0;
7165
+ replacing = /* @__PURE__ */ new Map();
7166
+ replaced = 0;
7130
7167
  links = /* @__PURE__ */ new Map();
7131
7168
  linked = /* @__PURE__ */ new Map();
7132
7169
  dirOwner;
@@ -7149,6 +7186,44 @@ var UnlinkedKeeper = class {
7149
7186
  }
7150
7187
  wrap(fs = this.fs) {
7151
7188
  const keeper = this;
7189
+ const { replacing } = this;
7190
+ const replace = async (target, op) => {
7191
+ const before = replacing.get(target);
7192
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
7193
+ const mine = {
7194
+ done: Promise.all([before?.done, promise]).then(() => void 0)
7195
+ };
7196
+ replacing.set(target, mine);
7197
+ try {
7198
+ await op();
7199
+ } finally {
7200
+ keeper.replaced += 1;
7201
+ resolve2();
7202
+ if (replacing.get(target) === mine) replacing.delete(target);
7203
+ }
7204
+ };
7205
+ const unreplaced = async (path, op) => {
7206
+ const at = fs.resolvePath("/", path);
7207
+ for (let attempt = 1; ; attempt++) {
7208
+ await replacing.get(at)?.done;
7209
+ const seen = keeper.replaced;
7210
+ try {
7211
+ return await op();
7212
+ } catch (err) {
7213
+ const code = err?.code;
7214
+ if (attempt >= 3 || code !== "ENOENT") throw err;
7215
+ await replacing.get(at)?.done;
7216
+ if (keeper.replaced === seen) throw err;
7217
+ }
7218
+ }
7219
+ };
7220
+ const looking = (name2) => {
7221
+ const call = fs[name2];
7222
+ if (!call) return {};
7223
+ return {
7224
+ [name2]: (path, ...rest2) => unreplaced(path, () => call.call(fs, path, ...rest2))
7225
+ };
7226
+ };
7152
7227
  const visible = (names) => names.filter((name2) => name2 !== UNLINKED_DIR);
7153
7228
  const counted2 = (st) => {
7154
7229
  const hidden = keeper.links.get(`${st.dev ?? 0}:${st.ino}`) ?? 0;
@@ -7156,9 +7231,10 @@ var UnlinkedKeeper = class {
7156
7231
  };
7157
7232
  return {
7158
7233
  ...fs,
7234
+ ...LOOKUP_OPS.reduce((all, name2) => Object.assign(all, looking(name2)), {}),
7159
7235
  links: (path) => mayLink(fs, path),
7160
- stat: async (path) => counted2(await fs.stat(path)),
7161
- lstat: async (path) => counted2(await fs.lstat(path)),
7236
+ stat: (path) => unreplaced(path, async () => counted2(await fs.stat(path))),
7237
+ lstat: (path) => unreplaced(path, async () => counted2(await fs.lstat(path))),
7162
7238
  async rm(path, options) {
7163
7239
  const target = fs.resolvePath("/", path);
7164
7240
  const pinned = await keeper.pin(target);
@@ -7168,8 +7244,10 @@ var UnlinkedKeeper = class {
7168
7244
  async rename(from, to) {
7169
7245
  const target = fs.resolvePath("/", to);
7170
7246
  if (target === fs.resolvePath("/", from)) return fs.rename(from, to);
7171
- const pinned = await keeper.pin(target);
7172
- await keeper.settle(pinned, () => fs.rename(from, to));
7247
+ await replace(target, async () => {
7248
+ const pinned = await keeper.pin(target);
7249
+ await keeper.settle(pinned, () => fs.rename(from, to));
7250
+ });
7173
7251
  },
7174
7252
  readdir: async (path) => visible(await fs.readdir(path)),
7175
7253
  readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR).map(([name2, st]) => [name2, st && counted2(st)])