@ai-ecoverse/slicc-kernel 1.20.1 → 1.21.0
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 +14 -0
- package/dist/kernel-worker.js +176 -95
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node.js +176 -95
- package/dist/node.js.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,6 +217,20 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
|
|
|
217
217
|
- `options.ro` makes any mount read-only: writes fail with `EROFS`;
|
|
218
218
|
- a driver that crashes or doesn't answer within 30 s makes its mount's calls fail with `EIO`, and the mount is listed as `failed`.
|
|
219
219
|
|
|
220
|
+
### `/etc/fstab`
|
|
221
|
+
|
|
222
|
+
At boot the kernel mounts what `/etc/fstab` lists, in fstab(5) format: `source target type options` (the dump and pass fields are ignored), with `#` comments and `\040` for a space in a field. Options are those of `mount -o`, and lines with `noauto` are left out.
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
none /mnt/scratch tmpfs maxfile=1G 0 0
|
|
226
|
+
fsa:<id> /mnt/folder fsa rw 0 0
|
|
227
|
+
project /mnt/project hostfs ro 0 0
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **Boot never waits for it:** the lines are mounted in the background once the kernel has started, all at once, so a slow source holds up nothing.
|
|
231
|
+
- **Retries:** a line that fails is tried again after 1, 4 and 16 seconds (a driver package installed meanwhile, a mount point created later), except when it cannot succeed (`EINVAL`, `EBUSY`, `EPERM`, `EACCES`). `terminate()` stops the retries.
|
|
232
|
+
- **No user involvement:** an `fsa` line names a drive by its id: it comes up with the folder it had while its permission holds, and otherwise with no medium, asking the page for the folder through `onMountPending`, as any `fsa` mount does. With `media: false` the drive finds its folder only within the same kernel. With the default `media`, an `fsa` line reads its folder handle back from IndexedDB at boot, which crashes Chrome in an Incognito or other off-the-record profile ([#95](https://github.com/ai-ecoverse/slicc-kernel/issues/95)); a page cannot reliably tell it runs in one, so an embedder that cannot rule it out passes `media: false`, which keeps `fsa` lines in `/etc/fstab` safe. A `hostfs` line waits only for its first connection, and comes up with no medium if the proxy is not there yet.
|
|
233
|
+
|
|
220
234
|
### Removable media: `fsa`
|
|
221
235
|
|
|
222
236
|
An `fsa` mount is a drive for a local folder, from the File System Access API, and the folder is its medium.
|
package/dist/kernel-worker.js
CHANGED
|
@@ -748,7 +748,7 @@ var KernelSocket = class _KernelSocket {
|
|
|
748
748
|
const own = this.changes.wait(signal);
|
|
749
749
|
if (this.state !== "connected" || !this.link) return own;
|
|
750
750
|
const waits = [own, this.link.rx.changed(signal), this.link.tx.changed(signal)];
|
|
751
|
-
for (const
|
|
751
|
+
for (const wait2 of waits) wait2.catch(() => void 0);
|
|
752
752
|
return pipeCall(() => Promise.race(waits));
|
|
753
753
|
}
|
|
754
754
|
bind(addr) {
|
|
@@ -7201,10 +7201,164 @@ function removableMedium(onLost, open = (handle) => new OpfsFs(handle)) {
|
|
|
7201
7201
|
};
|
|
7202
7202
|
}
|
|
7203
7203
|
|
|
7204
|
+
// src/mount/syscall.ts
|
|
7205
|
+
var MS_RDONLY = 1;
|
|
7206
|
+
var MS_MGC_MSK = 4294901760;
|
|
7207
|
+
var MS_MGC_VAL = 3236757504;
|
|
7208
|
+
var IGNORED_FLAGS = 2 | 4 | 8 | 16 | 128 | 1024 | 2048 | 32768 | 1 << 21 | 1 << 24 | 1 << 25;
|
|
7209
|
+
var MNT_FORCE = 1;
|
|
7210
|
+
var MNT_DETACH = 2;
|
|
7211
|
+
var UMOUNT_NOFOLLOW = 8;
|
|
7212
|
+
var GENERIC = /* @__PURE__ */ new Set([
|
|
7213
|
+
"defaults",
|
|
7214
|
+
"async",
|
|
7215
|
+
"sync",
|
|
7216
|
+
"dirsync",
|
|
7217
|
+
"atime",
|
|
7218
|
+
"noatime",
|
|
7219
|
+
"diratime",
|
|
7220
|
+
"nodiratime",
|
|
7221
|
+
"relatime",
|
|
7222
|
+
"norelatime",
|
|
7223
|
+
"strictatime",
|
|
7224
|
+
"nostrictatime",
|
|
7225
|
+
"lazytime",
|
|
7226
|
+
"nolazytime",
|
|
7227
|
+
"suid",
|
|
7228
|
+
"nosuid",
|
|
7229
|
+
"dev",
|
|
7230
|
+
"nodev",
|
|
7231
|
+
"exec",
|
|
7232
|
+
"noexec",
|
|
7233
|
+
"auto",
|
|
7234
|
+
"noauto",
|
|
7235
|
+
"user",
|
|
7236
|
+
"nouser",
|
|
7237
|
+
"users",
|
|
7238
|
+
"nofail",
|
|
7239
|
+
"_netdev",
|
|
7240
|
+
"silent",
|
|
7241
|
+
"loud"
|
|
7242
|
+
]);
|
|
7243
|
+
function parseMountOptions(data) {
|
|
7244
|
+
const options = {};
|
|
7245
|
+
for (const raw of data.split(",")) {
|
|
7246
|
+
const item = raw.trim();
|
|
7247
|
+
if (!item || GENERIC.has(item)) continue;
|
|
7248
|
+
if (item === "rw") {
|
|
7249
|
+
delete options.ro;
|
|
7250
|
+
continue;
|
|
7251
|
+
}
|
|
7252
|
+
const eq = item.indexOf("=");
|
|
7253
|
+
if (eq === 0) throw errnoError("EINVAL", `bad mount option ${item}`);
|
|
7254
|
+
if (eq < 0) options[item] = "";
|
|
7255
|
+
else options[item.slice(0, eq)] = item.slice(eq + 1);
|
|
7256
|
+
}
|
|
7257
|
+
if (options.maxfile !== void 0) parseSize(options.maxfile);
|
|
7258
|
+
return options;
|
|
7259
|
+
}
|
|
7260
|
+
function refusedTarget(target) {
|
|
7261
|
+
const at = normalizePath(target);
|
|
7262
|
+
const kernel = at === "/" || [`/proc`, `/dev`].some((k) => at === k || at.startsWith(`${k}/`));
|
|
7263
|
+
if (kernel) throw errnoError("EBUSY", `${at} belongs to the kernel`);
|
|
7264
|
+
}
|
|
7265
|
+
function absolute(target) {
|
|
7266
|
+
if (!target) throw errnoError("ENOENT", "no mount point");
|
|
7267
|
+
if (!target.startsWith("/")) throw errnoError("EINVAL", `not an absolute path: ${target}`);
|
|
7268
|
+
return normalizePath(target);
|
|
7269
|
+
}
|
|
7270
|
+
function mountCall(call) {
|
|
7271
|
+
let flags = call.flags >>> 0;
|
|
7272
|
+
if ((flags & MS_MGC_MSK) >>> 0 === MS_MGC_VAL) flags &= ~MS_MGC_MSK;
|
|
7273
|
+
if (flags & ~(MS_RDONLY | IGNORED_FLAGS)) {
|
|
7274
|
+
throw errnoError("EINVAL", `unsupported mount flags ${flags.toString(16)}`);
|
|
7275
|
+
}
|
|
7276
|
+
if (!call.type) throw errnoError("EINVAL", "no file system type");
|
|
7277
|
+
const target = absolute(call.target);
|
|
7278
|
+
refusedTarget(target);
|
|
7279
|
+
const options = parseMountOptions(call.data);
|
|
7280
|
+
if (flags & MS_RDONLY) options.ro = "";
|
|
7281
|
+
return { type: call.type, source: call.source || "none", target, options };
|
|
7282
|
+
}
|
|
7283
|
+
function umountCall(target, flags) {
|
|
7284
|
+
const known = MNT_FORCE | MNT_DETACH | UMOUNT_NOFOLLOW;
|
|
7285
|
+
if (flags >>> 0 & ~known) throw errnoError("EINVAL", `unsupported umount flags ${flags}`);
|
|
7286
|
+
const at = absolute(target);
|
|
7287
|
+
refusedTarget(at);
|
|
7288
|
+
return { target: at, detach: (flags & (MNT_FORCE | MNT_DETACH)) !== 0 };
|
|
7289
|
+
}
|
|
7290
|
+
|
|
7291
|
+
// src/mount/fstab.ts
|
|
7292
|
+
var FSTAB_PATH = "/etc/fstab";
|
|
7293
|
+
var FSTAB_RETRIES = [1e3, 4e3, 16e3];
|
|
7294
|
+
var FINAL = /* @__PURE__ */ new Set(["EINVAL", "EBUSY", "EPERM", "EACCES"]);
|
|
7295
|
+
function unescape(field) {
|
|
7296
|
+
return field.replace(
|
|
7297
|
+
/\\([0-7]{3})/g,
|
|
7298
|
+
(_, octal) => String.fromCharCode(Number.parseInt(octal, 8))
|
|
7299
|
+
);
|
|
7300
|
+
}
|
|
7301
|
+
function parseFstab(text) {
|
|
7302
|
+
const lines = [];
|
|
7303
|
+
text.split("\n").forEach((raw, index) => {
|
|
7304
|
+
const content = raw.replace(/#.*$/, "").trim();
|
|
7305
|
+
if (!content) return;
|
|
7306
|
+
const [source, target, type, options = "defaults"] = content.split(/\s+/).map(unescape);
|
|
7307
|
+
if (source === void 0 || target === void 0 || type === void 0) return;
|
|
7308
|
+
if (options.split(",").includes("noauto")) return;
|
|
7309
|
+
const spec = { source, target, type, options: {} };
|
|
7310
|
+
try {
|
|
7311
|
+
lines.push({ line: index + 1, spec: { ...spec, options: parseMountOptions(options) } });
|
|
7312
|
+
} catch (err) {
|
|
7313
|
+
lines.push({ line: index + 1, spec, invalid: err });
|
|
7314
|
+
}
|
|
7315
|
+
});
|
|
7316
|
+
return lines;
|
|
7317
|
+
}
|
|
7318
|
+
function codeOf3(err) {
|
|
7319
|
+
const code = err?.code;
|
|
7320
|
+
return typeof code === "string" ? code : "EIO";
|
|
7321
|
+
}
|
|
7322
|
+
function wait(ms, signal) {
|
|
7323
|
+
if (signal.aborted) return Promise.resolve();
|
|
7324
|
+
return new Promise((resolve) => {
|
|
7325
|
+
const done4 = () => {
|
|
7326
|
+
clearTimeout(timer);
|
|
7327
|
+
signal.removeEventListener("abort", done4);
|
|
7328
|
+
resolve();
|
|
7329
|
+
};
|
|
7330
|
+
const timer = setTimeout(done4, ms);
|
|
7331
|
+
signal.addEventListener("abort", done4, { once: true });
|
|
7332
|
+
});
|
|
7333
|
+
}
|
|
7334
|
+
async function mountFstab(lines, mount, signal, retries = FSTAB_RETRIES, discard = () => {
|
|
7335
|
+
}) {
|
|
7336
|
+
return Promise.all(
|
|
7337
|
+
lines.map(async ({ line, spec, invalid }) => {
|
|
7338
|
+
let failed = invalid;
|
|
7339
|
+
for (let attempt = 0; !invalid && attempt <= retries.length && !signal.aborted; attempt++) {
|
|
7340
|
+
try {
|
|
7341
|
+
const entry = await mount(spec);
|
|
7342
|
+
if (!signal.aborted) return { line, target: spec.target, entry };
|
|
7343
|
+
discard(entry);
|
|
7344
|
+
failed = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
|
|
7345
|
+
break;
|
|
7346
|
+
} catch (err) {
|
|
7347
|
+
failed = err;
|
|
7348
|
+
if (FINAL.has(codeOf3(err)) || attempt === retries.length) break;
|
|
7349
|
+
await wait(retries[attempt], signal);
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
7352
|
+
const code = codeOf3(failed);
|
|
7353
|
+
return { line, target: spec.target, code, error: String(failed?.message ?? code) };
|
|
7354
|
+
})
|
|
7355
|
+
);
|
|
7356
|
+
}
|
|
7357
|
+
|
|
7204
7358
|
// src/mount/hostfs.ts
|
|
7205
7359
|
var HOSTFS_IO = 16 * 1024 * 1024;
|
|
7206
|
-
var
|
|
7207
|
-
var unreachable = (err) =>
|
|
7360
|
+
var codeOf4 = (err) => err?.code;
|
|
7361
|
+
var unreachable = (err) => codeOf4(err) === "ENOTCONN";
|
|
7208
7362
|
function rel(path) {
|
|
7209
7363
|
return path.replace(/^\/+/, "");
|
|
7210
7364
|
}
|
|
@@ -7339,7 +7493,7 @@ function hostfsHandlers(current) {
|
|
|
7339
7493
|
try {
|
|
7340
7494
|
return { opened: await opening(), restore: void 0 };
|
|
7341
7495
|
} catch (err) {
|
|
7342
|
-
if (!flags.write ||
|
|
7496
|
+
if (!flags.write || codeOf4(err) !== "EACCES") throw err;
|
|
7343
7497
|
const mode = (await client.call("stat", { path: rel(path) })).mode ?? 0;
|
|
7344
7498
|
if (mode & 128) throw err;
|
|
7345
7499
|
const restore = { path: rel(path), mode };
|
|
@@ -7428,7 +7582,7 @@ function keepWatching(loop, signal) {
|
|
|
7428
7582
|
const uncap = () => clearTimeout(capped);
|
|
7429
7583
|
void ready2.promise.then(uncap);
|
|
7430
7584
|
signal.addEventListener("abort", uncap, { once: true });
|
|
7431
|
-
let
|
|
7585
|
+
let wait2 = first;
|
|
7432
7586
|
let up = false;
|
|
7433
7587
|
let current;
|
|
7434
7588
|
async function session() {
|
|
@@ -7473,7 +7627,7 @@ function keepWatching(loop, signal) {
|
|
|
7473
7627
|
const opened2 = await session();
|
|
7474
7628
|
if (signal.aborted) return;
|
|
7475
7629
|
if (opened2 && Date.now() - began >= first) {
|
|
7476
|
-
|
|
7630
|
+
wait2 = first;
|
|
7477
7631
|
continue;
|
|
7478
7632
|
}
|
|
7479
7633
|
if (!opened2 && up) {
|
|
@@ -7481,8 +7635,8 @@ function keepWatching(loop, signal) {
|
|
|
7481
7635
|
loop.down();
|
|
7482
7636
|
}
|
|
7483
7637
|
ready2.resolve();
|
|
7484
|
-
await sleep(
|
|
7485
|
-
|
|
7638
|
+
await sleep(wait2, signal);
|
|
7639
|
+
wait2 = Math.min(wait2 * 2, most);
|
|
7486
7640
|
}
|
|
7487
7641
|
})();
|
|
7488
7642
|
return { ready: ready2.promise, kick: () => current?.abort() };
|
|
@@ -7537,93 +7691,6 @@ async function openHostfs(spec, hook, fetch2, timing = {}) {
|
|
|
7537
7691
|
};
|
|
7538
7692
|
}
|
|
7539
7693
|
|
|
7540
|
-
// src/mount/syscall.ts
|
|
7541
|
-
var MS_RDONLY = 1;
|
|
7542
|
-
var MS_MGC_MSK = 4294901760;
|
|
7543
|
-
var MS_MGC_VAL = 3236757504;
|
|
7544
|
-
var IGNORED_FLAGS = 2 | 4 | 8 | 16 | 128 | 1024 | 2048 | 32768 | 1 << 21 | 1 << 24 | 1 << 25;
|
|
7545
|
-
var MNT_FORCE = 1;
|
|
7546
|
-
var MNT_DETACH = 2;
|
|
7547
|
-
var UMOUNT_NOFOLLOW = 8;
|
|
7548
|
-
var GENERIC = /* @__PURE__ */ new Set([
|
|
7549
|
-
"defaults",
|
|
7550
|
-
"async",
|
|
7551
|
-
"sync",
|
|
7552
|
-
"dirsync",
|
|
7553
|
-
"atime",
|
|
7554
|
-
"noatime",
|
|
7555
|
-
"diratime",
|
|
7556
|
-
"nodiratime",
|
|
7557
|
-
"relatime",
|
|
7558
|
-
"norelatime",
|
|
7559
|
-
"strictatime",
|
|
7560
|
-
"nostrictatime",
|
|
7561
|
-
"lazytime",
|
|
7562
|
-
"nolazytime",
|
|
7563
|
-
"suid",
|
|
7564
|
-
"nosuid",
|
|
7565
|
-
"dev",
|
|
7566
|
-
"nodev",
|
|
7567
|
-
"exec",
|
|
7568
|
-
"noexec",
|
|
7569
|
-
"auto",
|
|
7570
|
-
"noauto",
|
|
7571
|
-
"user",
|
|
7572
|
-
"nouser",
|
|
7573
|
-
"users",
|
|
7574
|
-
"nofail",
|
|
7575
|
-
"_netdev",
|
|
7576
|
-
"silent",
|
|
7577
|
-
"loud"
|
|
7578
|
-
]);
|
|
7579
|
-
function parseMountOptions(data) {
|
|
7580
|
-
const options = {};
|
|
7581
|
-
for (const raw of data.split(",")) {
|
|
7582
|
-
const item = raw.trim();
|
|
7583
|
-
if (!item || GENERIC.has(item)) continue;
|
|
7584
|
-
if (item === "rw") {
|
|
7585
|
-
delete options.ro;
|
|
7586
|
-
continue;
|
|
7587
|
-
}
|
|
7588
|
-
const eq = item.indexOf("=");
|
|
7589
|
-
if (eq === 0) throw errnoError("EINVAL", `bad mount option ${item}`);
|
|
7590
|
-
if (eq < 0) options[item] = "";
|
|
7591
|
-
else options[item.slice(0, eq)] = item.slice(eq + 1);
|
|
7592
|
-
}
|
|
7593
|
-
if (options.maxfile !== void 0) parseSize(options.maxfile);
|
|
7594
|
-
return options;
|
|
7595
|
-
}
|
|
7596
|
-
function refusedTarget(target) {
|
|
7597
|
-
const at = normalizePath(target);
|
|
7598
|
-
const kernel = at === "/" || [`/proc`, `/dev`].some((k) => at === k || at.startsWith(`${k}/`));
|
|
7599
|
-
if (kernel) throw errnoError("EBUSY", `${at} belongs to the kernel`);
|
|
7600
|
-
}
|
|
7601
|
-
function absolute(target) {
|
|
7602
|
-
if (!target) throw errnoError("ENOENT", "no mount point");
|
|
7603
|
-
if (!target.startsWith("/")) throw errnoError("EINVAL", `not an absolute path: ${target}`);
|
|
7604
|
-
return normalizePath(target);
|
|
7605
|
-
}
|
|
7606
|
-
function mountCall(call) {
|
|
7607
|
-
let flags = call.flags >>> 0;
|
|
7608
|
-
if ((flags & MS_MGC_MSK) >>> 0 === MS_MGC_VAL) flags &= ~MS_MGC_MSK;
|
|
7609
|
-
if (flags & ~(MS_RDONLY | IGNORED_FLAGS)) {
|
|
7610
|
-
throw errnoError("EINVAL", `unsupported mount flags ${flags.toString(16)}`);
|
|
7611
|
-
}
|
|
7612
|
-
if (!call.type) throw errnoError("EINVAL", "no file system type");
|
|
7613
|
-
const target = absolute(call.target);
|
|
7614
|
-
refusedTarget(target);
|
|
7615
|
-
const options = parseMountOptions(call.data);
|
|
7616
|
-
if (flags & MS_RDONLY) options.ro = "";
|
|
7617
|
-
return { type: call.type, source: call.source || "none", target, options };
|
|
7618
|
-
}
|
|
7619
|
-
function umountCall(target, flags) {
|
|
7620
|
-
const known = MNT_FORCE | MNT_DETACH | UMOUNT_NOFOLLOW;
|
|
7621
|
-
if (flags >>> 0 & ~known) throw errnoError("EINVAL", `unsupported umount flags ${flags}`);
|
|
7622
|
-
const at = absolute(target);
|
|
7623
|
-
refusedTarget(at);
|
|
7624
|
-
return { target: at, detach: (flags & (MNT_FORCE | MNT_DETACH)) !== 0 };
|
|
7625
|
-
}
|
|
7626
|
-
|
|
7627
7694
|
// src/mount/tmpfs.ts
|
|
7628
7695
|
function tmpfs() {
|
|
7629
7696
|
let inodes = 0;
|
|
@@ -8042,6 +8109,8 @@ var Launcher = class {
|
|
|
8042
8109
|
openFiles = /* @__PURE__ */ new Set();
|
|
8043
8110
|
held = /* @__PURE__ */ new Set();
|
|
8044
8111
|
processMounts;
|
|
8112
|
+
booting = new AbortController();
|
|
8113
|
+
fstab = Promise.resolve([]);
|
|
8045
8114
|
nextPid = 1e3;
|
|
8046
8115
|
boot = Date.now();
|
|
8047
8116
|
terminals = 0;
|
|
@@ -8449,6 +8518,7 @@ var Launcher = class {
|
|
|
8449
8518
|
for (const holds of this.held) holds.revoke(at);
|
|
8450
8519
|
}
|
|
8451
8520
|
unmountAll() {
|
|
8521
|
+
this.booting.abort();
|
|
8452
8522
|
const targets = this.mounts.list().map((m) => m.target);
|
|
8453
8523
|
for (const target of targets.sort((a, b) => b.length - a.length)) this.umount(target, true);
|
|
8454
8524
|
}
|
|
@@ -8480,6 +8550,17 @@ var Launcher = class {
|
|
|
8480
8550
|
async prepare() {
|
|
8481
8551
|
for (const dir of SHARED_DIRS) await this.base.mkdir(dir, { recursive: true });
|
|
8482
8552
|
await writeCaFile(this.base, this.ca).catch(() => void 0);
|
|
8553
|
+
this.fstab = this.mountFstab();
|
|
8554
|
+
}
|
|
8555
|
+
async mountFstab() {
|
|
8556
|
+
const text = await this.base.readFile(FSTAB_PATH).catch(() => "");
|
|
8557
|
+
return mountFstab(
|
|
8558
|
+
parseFstab(text),
|
|
8559
|
+
(spec) => this.mount(spec),
|
|
8560
|
+
this.booting.signal,
|
|
8561
|
+
FSTAB_RETRIES,
|
|
8562
|
+
(entry) => this.umount(entry.target, true)
|
|
8563
|
+
);
|
|
8483
8564
|
}
|
|
8484
8565
|
environment(cwd, extra) {
|
|
8485
8566
|
return {
|