@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 +1 -1
- package/dist/kernel-worker.js +125 -47
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +125 -47
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +1 -1
- package/dist/types/fs/opfs.d.ts +6 -0
- package/dist/types/fs/unlinked.d.ts +2 -0
- package/package.json +1 -1
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.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1668
|
-
return this.describe(await this.
|
|
1685
|
+
stat(path) {
|
|
1686
|
+
return this.unreplaced(async () => this.describe(await this.located(path, true)));
|
|
1669
1687
|
}
|
|
1670
|
-
|
|
1671
|
-
return this.describe(await this.
|
|
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
|
|
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
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
path,
|
|
1747
|
-
|
|
1748
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
7161
|
-
lstat: async (
|
|
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
|
-
|
|
7172
|
-
|
|
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)])
|