@ai-ecoverse/slicc-kernel 1.53.4 → 1.54.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 +7 -1
- package/dist/index.js +76 -2
- package/dist/index.js.map +4 -4
- package/dist/kernel-worker.js +125 -5
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node.js +191 -6
- package/dist/node.js.map +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ const { status, stdout, stderr } = await kernel.run(['bash', '-c', 'echo hi > he
|
|
|
34
34
|
| `worker` | `new URL('./kernel-worker.js', import.meta.url)` | the kernel worker script |
|
|
35
35
|
| `network` | none | `{ transport, uplink }`: how programs reach the outside world (see [Network](#network) and [Uplink](#uplink)) |
|
|
36
36
|
| `requestDirectory` | none | `() => Promise<FileSystemDirectoryHandle>`, typically `showDirectoryPicker`: how an `fsa` mount gets its folder (see [Removable media](#removable-media-fsa)) |
|
|
37
|
-
| `hostfs` | none | `(source, { readonly }) => Promise<{ url, token, capabilities? }>`: a grant for a `hostfs` mount from the local proxy (see [Host folders](#host-folders-hostfs)) |
|
|
37
|
+
| `hostfs` | none | `(source, { readonly }) => Promise<{ url, token, capabilities? } \| { port, token?, capabilities? }>`: a grant for a `hostfs` mount from the local proxy, or a `MessagePort` the page serves the folder on (see [Host folders](#host-folders-hostfs)) |
|
|
38
38
|
| `onMountPending` | none | `({ target, source, insert }) => void`: an `fsa` mount needs a folder; call `insert()` from a user gesture |
|
|
39
39
|
| `processMounts` | `true` | whether programs may call `mount(2)` and `umount2(2)`: `false`, or `(req) => boolean \| Promise<boolean>` to decide each call (see [Mounting from a program](#mounting-from-a-program)) |
|
|
40
40
|
| `hostname` | `'slicc'` | the kernel's node name (see [Node name](#node-name)) |
|
|
@@ -428,6 +428,12 @@ A `hostfs` mount is a folder exported by the local proxy ([slicc-node](https://g
|
|
|
428
428
|
- **Changes on the host** arrive as invalidations on a watch stream (`POST /api/hostfs/watch`), so an edit made outside shows up on the next access without a remount.
|
|
429
429
|
- **Proxy loss:** when the watch stream ends, or is silent for 45 s, the kernel reconnects once at once. If that fails, the mount is `nomedium`, as with [removable media](#removable-media-fsa): operations fail with `ENOMEDIUM` (`ENODEV` for WASI), and the kernel keeps reconnecting with backoff (1 s to 30 s), so the folder comes back by itself when the proxy does. A mount made while the proxy is down starts as `nomedium`, and so does one whose grant or first watch takes more than 15 s; it comes up by itself when they arrive.
|
|
430
430
|
- **Consistency:** a file that changes on the host while a program reads it fails with `ESTALE`, and the kernel restarts the whole read (up to 3 times) instead of stitching two versions together. On a ranged mount an open file keeps the version it was opened at (its ETag) until it writes, also when it is handed to a child process: a page fetched after the host changed the file fails the program's read with `ESTALE` ("Stale file handle") instead of mixing versions. A file deleted on the host while a program holds it open fails the next read that goes to the host with `ENOENT`, not with NFS's `ESTALE`.
|
|
431
|
+
- **Over a port:** the hook may answer `{ port, token?, capabilities? }` instead, for a folder that doesn't come from slicc-node over HTTP: for example, a linked `slicc` CLI's export, which the page reaches over its own link. The page implements a `HostfsTransport` for the folder (`capabilities`, `token`, `maxIo`, `call(op, body)`, `read(fh, offset, size, ifMatch?)`, `write(fh, offset, bytes)`, `watch(signal, onEvent, onOpen)`, with slicc-node's ops and answers) and serves it with `serveHostfsPort(port, transport)`, exported from both entries, which returns a function that stops serving and closes the port.
|
|
432
|
+
- The kernel moves the port into its worker with the grant. Calls, reads and writes are request and reply messages there, and the watch is a stream of messages that the kernel ends by aborting.
|
|
433
|
+
- Errors keep their `code`. A transport that throws `ENOTCONN` (its link is down) makes the mount `nomedium` and reconnecting, as a lost proxy does: the kernel asks the hook again, and the page answers with a new port.
|
|
434
|
+
- A port that dies doesn't hang anyone: pending requests fail with `ENOTCONN` when the port closes (its `close` event), when its watch ends or goes silent, and, on engines that don't report a closed peer, when a request gets no answer within 60 s (plus 1 s per MiB of a read or write), which fails it with `ETIMEDOUT`. After any of these the port counts as gone and is closed, files opened through it are `ESTALE`, and the next operation reconnects. `serveHostfsPort` stops serving when its port closes.
|
|
435
|
+
- A read reply sends a copy of the transport's bytes, so a transport may return a view into storage it keeps (a cache, a pooled or shared buffer).
|
|
436
|
+
- `token` names the connection (a link id, say). When the next port's token differs, files that were open become `ESTALE`. Without a token, every port is a new connection.
|
|
431
437
|
- **Node:** a page's requests carry its `Origin`, and slicc-node grants a token to that origin only. Node's `fetch` sends none, so a Node embedder passes `createNodeKernel` the origin its `hostfs` hook asked for the grant with, as `hostfsOrigin`, and the kernel adds it to every hostfs request. Without it the mount stays `nomedium`. `hostfsFetch(url, init)` replaces the `fetch` those requests use (for an agent or a test proxy), and gets the `Origin` too when both are given.
|
|
432
438
|
|
|
433
439
|
### Mounting from a program
|
package/dist/index.js
CHANGED
|
@@ -1632,6 +1632,79 @@ function localProxyTransport(options) {
|
|
|
1632
1632
|
};
|
|
1633
1633
|
}
|
|
1634
1634
|
|
|
1635
|
+
// src/mount/driver.ts
|
|
1636
|
+
function fsError(code, message = code) {
|
|
1637
|
+
return Object.assign(new Error(message), { code });
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/mount/hostfs.ts
|
|
1641
|
+
var HOSTFS_IO = 16 * 1024 * 1024;
|
|
1642
|
+
var codeOf = (err) => err?.code;
|
|
1643
|
+
var errorOf = (err) => ({
|
|
1644
|
+
code: typeof codeOf(err) === "string" ? codeOf(err) : "EIO",
|
|
1645
|
+
message: err?.message ?? String(err)
|
|
1646
|
+
});
|
|
1647
|
+
function serveHostfsPort(port, transport) {
|
|
1648
|
+
const watches = /* @__PURE__ */ new Map();
|
|
1649
|
+
const send2 = (reply, transfer = []) => port.postMessage(reply, transfer);
|
|
1650
|
+
const listener = async ({
|
|
1651
|
+
data
|
|
1652
|
+
}) => {
|
|
1653
|
+
const { id, op } = data;
|
|
1654
|
+
if (op === "unwatch") return watches.get(id)?.abort();
|
|
1655
|
+
if (op === "watch") {
|
|
1656
|
+
const stop2 = new AbortController();
|
|
1657
|
+
watches.set(id, stop2);
|
|
1658
|
+
try {
|
|
1659
|
+
await transport.watch(
|
|
1660
|
+
stop2.signal,
|
|
1661
|
+
(event) => send2({ id, event }),
|
|
1662
|
+
() => send2({ id, open: true })
|
|
1663
|
+
);
|
|
1664
|
+
send2({ id, end: true });
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
send2({ id, error: errorOf(err) });
|
|
1667
|
+
} finally {
|
|
1668
|
+
watches.delete(id);
|
|
1669
|
+
}
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
try {
|
|
1673
|
+
if (op === "read") {
|
|
1674
|
+
const read = await transport.read(
|
|
1675
|
+
data.fh,
|
|
1676
|
+
data.offset,
|
|
1677
|
+
data.size,
|
|
1678
|
+
data.ifMatch
|
|
1679
|
+
);
|
|
1680
|
+
const bytes = read.slice();
|
|
1681
|
+
return send2({ id, bytes }, [bytes.buffer]);
|
|
1682
|
+
}
|
|
1683
|
+
if (op === "write") {
|
|
1684
|
+
await transport.write(data.fh, data.offset, data.bytes);
|
|
1685
|
+
return send2({ id });
|
|
1686
|
+
}
|
|
1687
|
+
if (op !== "call") throw fsError("EINVAL", `unknown hostfs port op ${op}`);
|
|
1688
|
+
send2({
|
|
1689
|
+
id,
|
|
1690
|
+
result: await transport.call(data.name, data.body)
|
|
1691
|
+
});
|
|
1692
|
+
} catch (err) {
|
|
1693
|
+
send2({ id, error: errorOf(err) });
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
const stop = () => {
|
|
1697
|
+
port.removeEventListener("message", listener);
|
|
1698
|
+
port.removeEventListener("close", stop);
|
|
1699
|
+
for (const watch of watches.values()) watch.abort();
|
|
1700
|
+
port.close();
|
|
1701
|
+
};
|
|
1702
|
+
port.addEventListener("message", listener);
|
|
1703
|
+
port.addEventListener("close", stop);
|
|
1704
|
+
port.start();
|
|
1705
|
+
return stop;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1635
1708
|
// src/index.ts
|
|
1636
1709
|
var ISOLATION = "slicc-kernel needs a cross-origin isolated page (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp or credentialless)";
|
|
1637
1710
|
function streamer(callback) {
|
|
@@ -1746,7 +1819,7 @@ async function createKernel(options = {}) {
|
|
|
1746
1819
|
try {
|
|
1747
1820
|
if (!options.hostfs) throw new Error("this page gives no host folders");
|
|
1748
1821
|
const grant = await options.hostfs(source, { readonly });
|
|
1749
|
-
worker.postMessage({ hostfsGrant: { id, grant } });
|
|
1822
|
+
worker.postMessage({ hostfsGrant: { id, grant } }, "port" in grant ? [grant.port] : []);
|
|
1750
1823
|
} catch (err) {
|
|
1751
1824
|
worker.postMessage({ hostfsGrant: { id, error: err.message ?? String(err) } });
|
|
1752
1825
|
}
|
|
@@ -1875,6 +1948,7 @@ export {
|
|
|
1875
1948
|
createKernel,
|
|
1876
1949
|
fetchTransport,
|
|
1877
1950
|
localProxyTransport,
|
|
1878
|
-
probeLocalProxy
|
|
1951
|
+
probeLocalProxy,
|
|
1952
|
+
serveHostfsPort
|
|
1879
1953
|
};
|
|
1880
1954
|
//# sourceMappingURL=index.js.map
|