@ai-ecoverse/slicc-kernel 1.22.0 → 1.22.1

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
@@ -118,7 +118,7 @@ const { status, stdout } = await kernel.run(['bash', '-c', 'echo hi'], { cwd: '/
118
118
  kernel.terminate();
119
119
  ```
120
120
 
121
- `createNodeKernel({ root, modules, env, network, worker, processMounts })` takes the options of `createKernel` except `metadata`. `root` is an in-memory directory by default (`memoryRoot()` makes another), and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal` and `terminate` as above, plus `root`, `writeFile(path, data)` (creating the parent directories) and `readFile(path)` to put files in place and read results. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect()` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
121
+ `createNodeKernel({ root, modules, env, network, worker, processMounts, fstabRetries })` takes the options of `createKernel` except `metadata`, and `fstabRetries`, the delays in ms between tries of a failing `/etc/fstab` line (default `[1000, 4000, 16000]`). `root` is an in-memory directory by default (`memoryRoot()` makes another), and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal` and `terminate` as above, plus `root`, `writeFile(path, data)` (creating the parent directories) and `readFile(path)` to put files in place and read results. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect()` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
122
122
 
123
123
  ## Commands
124
124
 
@@ -230,6 +230,7 @@ project /mnt/project hostfs ro 0 0
230
230
 
231
231
  - **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.
232
232
  - **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.
233
+ - **Failures are listed:** until it mounts, a line is in `kernel.mounts()` with state `pending`, and once it gives up with state `failed`, each with the last `error`. `/proc/mounts` and `mount` show the state in its options, as `none /mnt/bad nosuchfs rw,failed 0 0`. Unmounting a failed line's target takes it off the list. A line whose target another line mounted is not listed.
233
234
  - **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.
234
235
 
235
236
  ### Removable media: `fsa`
@@ -4128,6 +4128,7 @@ var MountTable = class {
4128
4128
  mounts = /* @__PURE__ */ new Map();
4129
4129
  devices = 0;
4130
4130
  pending = /* @__PURE__ */ new Set();
4131
+ notes = /* @__PURE__ */ new Map();
4131
4132
  deps;
4132
4133
  constructor(deps) {
4133
4134
  this.deps = deps;
@@ -4136,7 +4137,7 @@ var MountTable = class {
4136
4137
  return this.mounts.get(target);
4137
4138
  }
4138
4139
  list() {
4139
- return [...this.mounts.values()].map(
4140
+ const mounted = [...this.mounts.values()].map(
4140
4141
  ({ type, source, target, options, state, error, present }) => ({
4141
4142
  type,
4142
4143
  source,
@@ -4146,6 +4147,19 @@ var MountTable = class {
4146
4147
  ...error ? { error } : {}
4147
4148
  })
4148
4149
  );
4150
+ const noted = [...this.notes.values()].filter((note) => !this.mounts.has(note.target));
4151
+ return [...mounted, ...noted.map((note) => ({ ...note, options: { ...note.options } }))];
4152
+ }
4153
+ note(spec, state, error) {
4154
+ const target = normalizePath(spec.target);
4155
+ this.notes.set(target, {
4156
+ type: spec.type,
4157
+ source: spec.source,
4158
+ target,
4159
+ options: { ...spec.options },
4160
+ state,
4161
+ ...error ? { error } : {}
4162
+ });
4149
4163
  }
4150
4164
  locate(path) {
4151
4165
  let best;
@@ -4222,13 +4236,19 @@ var MountTable = class {
4222
4236
  };
4223
4237
  mounted = mount;
4224
4238
  this.mounts.set(target, mount);
4239
+ this.notes.delete(target);
4225
4240
  return this.list().find((m) => m.target === target);
4226
4241
  }
4242
+ unnote(target) {
4243
+ this.notes.delete(normalizePath(target));
4244
+ }
4227
4245
  umount(target, detach = false) {
4228
4246
  const at = normalizePath(target);
4229
4247
  const mount = this.mounts.get(at);
4248
+ if (!mount && this.notes.delete(at)) return;
4230
4249
  if (!mount) throw errnoError("EINVAL", `${at} is not mounted`);
4231
4250
  if (!detach && this.deps.busy(at)) throw errnoError("EBUSY", `${at} has open files`);
4251
+ this.notes.delete(at);
4232
4252
  this.mounts.delete(at);
4233
4253
  mount.conn.close();
4234
4254
  mount.dispose();
@@ -7717,28 +7737,34 @@ function wait(ms, signal) {
7717
7737
  });
7718
7738
  }
7719
7739
  async function mountFstab(lines, mount, signal, retries = FSTAB_RETRIES, discard = () => {
7740
+ }, settled = () => {
7720
7741
  }) {
7721
7742
  return Promise.all(
7722
- lines.map(async ({ line, spec, invalid }) => {
7723
- let failed = invalid;
7724
- for (let attempt = 0; !invalid && attempt <= retries.length && !signal.aborted; attempt++) {
7725
- try {
7726
- const entry = await mount(spec);
7727
- if (!signal.aborted) return { line, target: spec.target, entry };
7728
- discard(entry);
7729
- failed = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
7730
- break;
7731
- } catch (err) {
7732
- failed = err;
7733
- if (FINAL.has(codeOf3(err)) || attempt === retries.length) break;
7734
- await wait(retries[attempt], signal);
7735
- }
7736
- }
7737
- const code = codeOf3(failed);
7738
- return { line, target: spec.target, code, error: String(failed?.message ?? code) };
7743
+ lines.map(async (each) => {
7744
+ const result = await mountLine(each, mount, signal, retries, discard);
7745
+ settled(each, result);
7746
+ return result;
7739
7747
  })
7740
7748
  );
7741
7749
  }
7750
+ async function mountLine({ line, spec, invalid }, mount, signal, retries, discard) {
7751
+ let failed = invalid;
7752
+ for (let attempt = 0; !invalid && attempt <= retries.length && !signal.aborted; attempt++) {
7753
+ try {
7754
+ const entry = await mount(spec);
7755
+ if (!signal.aborted) return { line, target: spec.target, entry };
7756
+ discard(entry);
7757
+ failed = Object.assign(new Error("the kernel stopped"), { code: "ECANCELED" });
7758
+ break;
7759
+ } catch (err) {
7760
+ failed = err;
7761
+ if (FINAL.has(codeOf3(err)) || attempt === retries.length) break;
7762
+ await wait(retries[attempt], signal);
7763
+ }
7764
+ }
7765
+ const code = codeOf3(failed);
7766
+ return { line, target: spec.target, code, error: String(failed?.message ?? code) };
7767
+ }
7742
7768
 
7743
7769
  // src/mount/hostfs.ts
7744
7770
  var HOSTFS_IO = 16 * 1024 * 1024;
@@ -8541,6 +8567,7 @@ var Launcher = class {
8541
8567
  onMountPending;
8542
8568
  removable = /* @__PURE__ */ new Map();
8543
8569
  hostfs;
8570
+ fstabRetries;
8544
8571
  inserted = /* @__PURE__ */ new Map();
8545
8572
  openFiles = /* @__PURE__ */ new Set();
8546
8573
  held = /* @__PURE__ */ new Set();
@@ -8562,6 +8589,7 @@ var Launcher = class {
8562
8589
  this.media = options.media ?? memoryMedia();
8563
8590
  this.onMountPending = options.onMountPending;
8564
8591
  this.hostfs = options;
8592
+ this.fstabRetries = options.fstabRetries ?? FSTAB_RETRIES;
8565
8593
  this.processMounts = options.processMounts ?? true;
8566
8594
  this.pnpmHome = options.env?.PNPM_HOME ?? PNPM_HOME;
8567
8595
  this.modulesDir = options.modules ?? "/node_modules";
@@ -8995,12 +9023,22 @@ var Launcher = class {
8995
9023
  }
8996
9024
  async mountFstab() {
8997
9025
  const text = await this.base.readFile(FSTAB_PATH).catch(() => "");
9026
+ const lines = parseFstab(text);
9027
+ for (const { spec } of lines) this.mounts.note(spec, "pending");
8998
9028
  return mountFstab(
8999
- parseFstab(text),
9000
- (spec) => this.mount(spec),
9029
+ lines,
9030
+ (spec) => this.mount(spec).catch((err) => {
9031
+ this.mounts.note(spec, "pending", err?.message ?? String(err));
9032
+ throw err;
9033
+ }),
9001
9034
  this.booting.signal,
9002
- FSTAB_RETRIES,
9003
- (entry) => this.umount(entry.target, true)
9035
+ this.fstabRetries,
9036
+ (entry) => this.umount(entry.target, true),
9037
+ ({ spec }, result) => {
9038
+ if (!result.code || result.code === "ECANCELED" || this.booting.signal.aborted) {
9039
+ this.mounts.unnote(spec.target);
9040
+ } else this.mounts.note(spec, "failed", result.error);
9041
+ }
9004
9042
  );
9005
9043
  }
9006
9044
  environment(cwd, extra) {