@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 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.
@@ -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 wait of waits) wait.catch(() => void 0);
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 codeOf3 = (err) => err?.code;
7207
- var unreachable = (err) => codeOf3(err) === "ENOTCONN";
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 || codeOf3(err) !== "EACCES") throw err;
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 wait = first;
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
- wait = first;
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(wait, signal);
7485
- wait = Math.min(wait * 2, most);
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 {