@daugasauron/dolly 0.0.0-stage → 0.1.4-rc.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -2
  3. package/dist/dolly-build-id.mjs +1 -0
  4. package/dist/dolly-dso-0.wasm +0 -0
  5. package/dist/dolly-image-build-id.mjs +1 -0
  6. package/dist/dolly-kernel-plugin-abi.mjs +2 -0
  7. package/dist/dolly-process-0.wasm +0 -0
  8. package/dist/dolly-process-abi.mjs +1 -0
  9. package/dist/dolly-process-dso.mjs +1575 -0
  10. package/dist/dolly-process-gate-0.wasm +0 -0
  11. package/dist/dolly-process-stack.mjs +50 -0
  12. package/dist/dolly-process-worker.mjs +275 -0
  13. package/dist/dolly-threads-0.wasm +0 -0
  14. package/dist/dolly.wasm +0 -0
  15. package/host/abi.mjs +5 -0
  16. package/host/dso/abi.mjs +13 -0
  17. package/host/dso/dso.mjs +26 -0
  18. package/host/dso/module.json +15 -0
  19. package/host/http/abi.mjs +28 -0
  20. package/host/http/broker.mjs +325 -0
  21. package/host/http/http.mjs +74 -0
  22. package/host/http/local-services.mjs +37 -0
  23. package/host/http/module.json +15 -0
  24. package/host/http/policy.mjs +293 -0
  25. package/host/modules.mjs +373 -0
  26. package/host/nena/abi.mjs +19 -0
  27. package/host/nena/module.json +15 -0
  28. package/host/nena/nena.mjs +131 -0
  29. package/host/packages/module.json +13 -0
  30. package/host/packages/packages.mjs +15 -0
  31. package/host/packages/service.mjs +59 -0
  32. package/host/requirements.mjs +62 -0
  33. package/host/runtime/abi.mjs +8 -0
  34. package/host/runtime/boot-files.mjs +45 -0
  35. package/host/runtime/grower.mjs +14 -0
  36. package/host/runtime/lease-bridge.mjs +65 -0
  37. package/host/runtime/module.json +16 -0
  38. package/host/runtime/runtime.mjs +209 -0
  39. package/host/snapshot/abi.mjs +16 -0
  40. package/host/snapshot/module.json +15 -0
  41. package/host/snapshot/snapshot.mjs +138 -0
  42. package/host/snapshot/transport.mjs +132 -0
  43. package/host/snapshot/ui.mjs +207 -0
  44. package/host/stack/abi.mjs +5 -0
  45. package/host/stack/module.json +17 -0
  46. package/host/stack/stack.mjs +19 -0
  47. package/host/threads/abi.mjs +8 -0
  48. package/host/threads/module.json +13 -0
  49. package/host/threads/threads.mjs +36 -0
  50. package/package.json +12 -4
  51. package/src/alphabetical.mjs +4 -0
  52. package/src/browser.mjs +237 -0
  53. package/src/build-log.mjs +30 -0
  54. package/src/image-entry.mjs +29 -0
  55. package/src/image-inputs.mjs +20 -0
  56. package/src/image-names.mjs +11 -0
  57. package/src/images.mjs +10 -0
  58. package/src/kernel-plugin.mjs +77 -0
  59. package/src/packaged-snapshot.mjs +175 -0
  60. package/src/page-chords.mjs +43 -0
  61. package/src/page-indicators.mjs +27 -0
  62. package/src/process-abi.mjs +69 -0
  63. package/src/process-constants.mjs +242 -0
  64. package/src/process-supervisor.mjs +1002 -0
  65. package/src/runtime-worker.mjs +55 -0
  66. package/src/session-store.mjs +198 -0
  67. package/src/snapshot-records.mjs +109 -0
  68. package/src/static-asset.mjs +97 -0
  69. package/src/terminal-text.mjs +72 -0
  70. package/src/version.mjs +3 -0
  71. package/src/wasm-interface.mjs +440 -0
  72. package/src/worker-image.mjs +137 -0
@@ -0,0 +1,19 @@
1
+ // Generated from host/nena/dolly-nena-0.wat, host/nena/nena.h, include/dolly/process.h.
2
+ export const DOLLY_NENA_VERSION = 0;
3
+ export const DOLLY_NENA_PROCESS_OP = 133;
4
+ export const DOLLY_NENA_SLOTS = 2;
5
+ export const DOLLY_NENA_REPLY_BYTES = 256;
6
+ export const DOLLY_NENA_PACKET_BYTES = 72;
7
+ export const DOLLY_NENA_NAME_BYTES = 32;
8
+ export const DOLLY_NENA_GIVES_BYTES = 120;
9
+ export const DOLLY_NENA_REASON_BYTES = 96;
10
+ export const DOLLY_NENA_OPEN = 1;
11
+ export const DOLLY_NENA_MODULE = 2;
12
+ export const DOLLY_NENA_ENABLE = 3;
13
+ export const DOLLY_NENA_DISABLE = 4;
14
+ export const DOLLY_NENA_CLOSE = 5;
15
+ export const DOLLY_NENA_ENABLED = 1;
16
+ export const DOLLY_NENA_DECLARED = 2;
17
+ export const DOLLY_NENA_FIXED = 4;
18
+ export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
19
+ export const DOLLY_NENA_ABI_DIGEST = "3b6540395986cc839e4ef67cdfd0a0510780e26cdf7071522b60730c7b05c58e";
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "nena",
3
+ "version": 0,
4
+ "gives": "enable and disable host modules with nena; disabling it fixes them",
5
+ "dependencies": ["runtime@0"],
6
+ "phase": "image",
7
+ "switches": true,
8
+ "imports": ["env.dolly_nena_dispatch"],
9
+ "host": "nena.mjs",
10
+ "contracts": ["dolly-nena-0.wat"],
11
+ "process": [],
12
+ "headers": ["nena.h", "nena-abi.h"],
13
+ "kernel": ["kernel.c"],
14
+ "client": ["client.c"]
15
+ }
@@ -0,0 +1,131 @@
1
+ import * as N from "./abi.mjs";
2
+ import { bootFiles } from "../runtime/boot-files.mjs";
3
+ import { hostRequirements } from "../requirements.mjs";
4
+ import { DOLLY_ERRNO as E } from "./abi.mjs";
5
+ export { DOLLY_NENA_ABI_DIGEST as digest } from "./abi.mjs";
6
+
7
+ // The device as the lease bridge sees it.
8
+ export const lease = { type: "nena", slots: N.DOLLY_NENA_SLOTS, packetBytes: N.DOLLY_NENA_PACKET_BYTES,
9
+ replyBytes: N.DOLLY_NENA_REPLY_BYTES, open: N.DOLLY_NENA_OPEN, close: N.DOLLY_NENA_CLOSE };
10
+ // The session's own record of the modules it enabled, one name@version a line.
11
+ // The Worker writes it after every switch and the page enables what it lists
12
+ // when the session is loaded again; the recipe engine reads it for amy.
13
+ const record = "/etc/dolly/enabled";
14
+ // Boot waits ten seconds for a provider's page side (host/modules.mjs).
15
+ const restoreMilliseconds = 3000;
16
+
17
+ const encoder = new TextEncoder(), decoder = new TextDecoder("utf-8", { fatal: true });
18
+ const fail = errno => { throw Object.assign(new Error("nena request failed"), { errno }); };
19
+ // A NUL-terminated text field, cut at a whole character.
20
+ const text = (bytes, offset, size, value) => void encoder.encodeInto(value, bytes.subarray(offset, offset + size - 1));
21
+
22
+ // The page side: `modules` is the registry's list and its two switches
23
+ // (host/modules.mjs), which only this provider is given. Every answer is the
24
+ // page's; a packet names a module and never a file, URL or provider.
25
+ export function browser({ send, modules, showStatus }) {
26
+ const requirement = ({ name, version }) => `${name}@${version}`;
27
+ function moduleRecord(module, reason) {
28
+ const bytes = new Uint8Array(N.DOLLY_NENA_REPLY_BYTES), view = new DataView(bytes.buffer);
29
+ text(bytes, 0, N.DOLLY_NENA_NAME_BYTES, module.name);
30
+ view.setUint32(32, module.version, true);
31
+ view.setUint32(36, (module.enabled ? N.DOLLY_NENA_ENABLED : 0) | (module.declared ? N.DOLLY_NENA_DECLARED : 0) |
32
+ (module.fixed ? N.DOLLY_NENA_FIXED : 0), true);
33
+ text(bytes, 40, N.DOLLY_NENA_GIVES_BYTES, module.gives);
34
+ text(bytes, 40 + N.DOLLY_NENA_GIVES_BYTES, N.DOLLY_NENA_REASON_BYTES, reason ?? module.fixed);
35
+ return bytes;
36
+ }
37
+ // What the session enabled beyond what the page started with: its record.
38
+ const enabledSince = () => modules.list().filter(module => module.enabled && !module.declared).map(requirement);
39
+ async function dispatch(packet) {
40
+ if (!(packet instanceof Uint8Array) || packet.byteLength < 32 || packet.byteLength > N.DOLLY_NENA_PACKET_BYTES) fail(E.EINVAL);
41
+ const view = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
42
+ const operation = view.getUint32(4, true), scope = view.getUint32(8, true);
43
+ if (view.getUint32(0, true) !== N.DOLLY_NENA_VERSION || view.getUint32(12, true) || view.getUint32(20, true) ||
44
+ !scope || view.getUint32(24, true) !== packet.length - 32 || view.getUint32(28, true)) fail(E.EINVAL);
45
+ const sized = bytes => { if (packet.length !== bytes) fail(E.EINVAL); };
46
+ if (operation === N.DOLLY_NENA_OPEN) {
47
+ sized(32);
48
+ const reply = new Uint8Array(8);
49
+ new DataView(reply.buffer).setBigUint64(0, BigInt(scope), true);
50
+ return { bytes: reply };
51
+ }
52
+ if (operation === N.DOLLY_NENA_CLOSE) { sized(32); return { bytes: new Uint8Array() }; }
53
+ if (operation === N.DOLLY_NENA_MODULE) {
54
+ sized(40);
55
+ if (view.getUint32(36, true)) fail(E.EINVAL);
56
+ return { bytes: moduleRecord(modules.list()[view.getUint32(32, true)] ?? fail(E.ENOENT)) };
57
+ }
58
+ if (operation !== N.DOLLY_NENA_ENABLE && operation !== N.DOLLY_NENA_DISABLE) fail(E.EINVAL);
59
+ sized(72);
60
+ const field = packet.subarray(32, 32 + N.DOLLY_NENA_NAME_BYTES), end = field.indexOf(0);
61
+ if (end < 1 || field.subarray(end).some(byte => byte) || view.getUint32(68, true)) fail(E.EINVAL);
62
+ let name;
63
+ try { name = decoder.decode(field.subarray(0, end)); } catch { fail(E.EINVAL); }
64
+ const version = view.getUint32(64, true), enabling = operation === N.DOLLY_NENA_ENABLE;
65
+ const found = () => modules.list().find(module => module.name === name && module.version === version) ?? fail(E.ENOENT);
66
+ const value = requirement(found());
67
+ // The session gives the privilege up: answered first, since the answer
68
+ // travels through the provider that the switch takes away.
69
+ if (!enabling && name === "nena") return { bytes: moduleRecord({ ...found(), enabled: false }), after: () => modules.disable(value) };
70
+ const refusal = await (enabling ? modules.enable(value) : modules.disable(value));
71
+ return { bytes: moduleRecord(found(), refusal ?? undefined), enabled: enabledSince() };
72
+ }
73
+ return {
74
+ messages: {
75
+ async "nena-request"(message) {
76
+ let result;
77
+ try { result = await dispatch(message.packet); }
78
+ catch (error) { result = { error: error.errno ?? E.EIO, bytes: new Uint8Array() }; }
79
+ if (result.enabled) send({ type: "nena-record", enabled: result.enabled });
80
+ send({ type: "nena-complete", scope: message.scope, sequence: message.sequence, error: result.error ?? 0,
81
+ bytes: result.bytes }, [result.bytes.buffer]);
82
+ await result.after?.();
83
+ },
84
+ "nena-revoke"(message) { send({ type: "nena-complete", scope: message.scope, revoked: true }); },
85
+ },
86
+ // A loaded session has the modules its record lists again, and the page
87
+ // says in one line which, and which it could not enable, with the command
88
+ // that tries again. Boot waits for them, so that the ENTRY finds them,
89
+ // but only so long: a module slower than that comes while the session
90
+ // runs, and never stops it from loading.
91
+ async start({ enabled }) {
92
+ const restored = (async () => {
93
+ const refused = [];
94
+ for (const value of enabled) {
95
+ const refusal = await modules.enable(value);
96
+ if (refusal) refused.push(`${value} is not enabled: ${refusal}. nena enable ${value.split("@")[0]} tries again.`);
97
+ }
98
+ const again = enabledSince();
99
+ send({ type: "nena-record", enabled: again });
100
+ const said = [...again.length ? [`This session enabled ${again.join(", ")} again.`] : [], ...refused];
101
+ if (said.length) showStatus(said.join(" "), refused.length > 0);
102
+ })();
103
+ await Promise.race([restored, new Promise(resolve => setTimeout(resolve, restoreMilliseconds))]);
104
+ },
105
+ };
106
+ }
107
+
108
+ export function worker({ send, get }) {
109
+ let bridge, files;
110
+ return {
111
+ bindings: { "env.dolly_nena_dispatch": (address, bytes) =>
112
+ bridge ? bridge.dispatch({ address, bytes }) : -E.ENOSYS },
113
+ // After the image and a saved session are restored: the record names what
114
+ // the page enables again. The file is the session's own, so it is bounded
115
+ // and parsed as requirements; a malformed one enables nothing.
116
+ start({ kernel, memory }) {
117
+ bridge = get("runtime").leaseBridge(lease, Number(kernel.dolly_nena_mailbox_address()), send);
118
+ files = bootFiles(kernel, memory);
119
+ let enabled = [];
120
+ try { enabled = hostRequirements(decoder.decode(files.read(record, 4096)).split("\n").filter(Boolean)); } catch {}
121
+ return { enabled };
122
+ },
123
+ messages: {
124
+ "nena-complete": message => bridge?.acknowledge(message),
125
+ "nena-record"({ enabled }) {
126
+ if (enabled.length) files.write(record, `${enabled.join("\n")}\n`); else files.remove(record);
127
+ },
128
+ },
129
+ dispose() { bridge?.dispose(); bridge = undefined; },
130
+ };
131
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "packages",
3
+ "version": 0,
4
+ "gives": "the release's published packages, for amy",
5
+ "dependencies": ["http@0"],
6
+ "imports": [],
7
+ "host": "packages.mjs",
8
+ "contracts": [],
9
+ "process": [],
10
+ "headers": [],
11
+ "kernel": [],
12
+ "client": []
13
+ }
@@ -0,0 +1,15 @@
1
+ // Published package snapshots for a running image. No Wasm imports: enabling
2
+ // packages@0 only lets the page admit GET https://packages.dolly.invalid/v1/…
3
+ // (host/http/local-services.mjs) once the image ENTRY starts. The guest names
4
+ // a package by the recipe pin this release publishes; the page materializes
5
+ // and verifies its snapshot exactly as it does a build input, then serves the
6
+ // bytes. A request grants no network or other authority. The index of names
7
+ // is a public file of the site, amy-index.txt, read like any other URL.
8
+ import { PackageService } from "./service.mjs";
9
+
10
+ export function browser({ get }) {
11
+ return {
12
+ entryStarted() { get("http").services.packages = new PackageService(); },
13
+ disabled() { delete get("http").services.packages; },
14
+ };
15
+ }
@@ -0,0 +1,59 @@
1
+ import { DOLLY_IMAGES } from "../../src/images.mjs";
2
+ import { loadPackagedSnapshotMetadata, loadPackagedSystemSnapshot } from "../../src/packaged-snapshot.mjs";
3
+ import { MAX_SNAPSHOT_BYTES } from "../../src/snapshot-records.mjs";
4
+
5
+ const PACKAGES_ORIGIN = "https://packages.dolly.invalid";
6
+ const PACKAGE_LIMITS = Object.freeze({
7
+ maxRequestBytes: 0, maxResponseBytes: MAX_SNAPSHOT_BYTES,
8
+ timeoutMilliseconds: 30 * 60_000, credentialHeaders: new Set(),
9
+ // Snapshots served per page: enough for every package, not for a loop.
10
+ maxSnapshots: 64,
11
+ });
12
+ const packagePath = /^\/v1\/packages\/([0-9a-f]{64})$/;
13
+
14
+ function reply(body, status, type = "text/plain; charset=utf-8") {
15
+ return new Response(body, { status, headers: { "content-type": type, "cache-control": "no-store" } });
16
+ }
17
+
18
+ // GET /v1/packages/SHA256 is the exact snapshot of the package whose recipe
19
+ // has that pin, verified from the published packs. One snapshot is held at a
20
+ // time, until the guest has read it or the broker's deadline cancels it.
21
+ export class PackageService {
22
+ constructor() {
23
+ this.active = false;
24
+ this.served = 0;
25
+ }
26
+ get origin() { return PACKAGES_ORIGIN; }
27
+ authorize(url, method, bytes) {
28
+ return method === "GET" && bytes === 0 && packagePath.test(url.pathname) ? PACKAGE_LIMITS : undefined;
29
+ }
30
+ async fetch(url, init) {
31
+ const sha256 = packagePath.exec(url.pathname)[1];
32
+ const definition = DOLLY_IMAGES.find(candidate => candidate.role === "package" && candidate.sha256 === sha256);
33
+ if (!definition) return reply("This release publishes no package with that recipe pin\n", 404);
34
+ if (this.active) return reply("Another package snapshot is being served\n", 409);
35
+ if (this.served >= PACKAGE_LIMITS.maxSnapshots) return reply("Package snapshot quota exceeded\n", 429);
36
+ this.active = true;
37
+ this.served += 1;
38
+ let bytes;
39
+ try {
40
+ const metadata = await loadPackagedSnapshotMetadata(definition.image);
41
+ bytes = await loadPackagedSystemSnapshot(definition.image, metadata, init.signal);
42
+ } catch (error) {
43
+ this.active = false;
44
+ return reply(`${definition.image}: ${error.message}\n`, 502);
45
+ }
46
+ const release = () => { this.active = false; };
47
+ let offset = 0;
48
+ const body = new ReadableStream({
49
+ pull: controller => {
50
+ const end = Math.min(offset + 1024 * 1024, bytes.byteLength);
51
+ controller.enqueue(new Uint8Array(bytes, offset, end - offset));
52
+ offset = end;
53
+ if (offset === bytes.byteLength) { controller.close(); release(); }
54
+ },
55
+ cancel: release,
56
+ }, { highWaterMark: 0 });
57
+ return reply(body, 200, "application/octet-stream");
58
+ }
59
+ }
@@ -0,0 +1,62 @@
1
+ import { DOLLY_HOST_RECORD_BYTES as recordBytes, DOLLY_HOST_NAME_BYTES as nameBytes,
2
+ DOLLY_HOST_DIGEST_BYTES as digestBytes, DOLLY_HOST_MAX_RECORDS as maxRecords } from "./abi.mjs";
3
+ import { hex } from "../src/static-asset.mjs";
4
+
5
+ const pattern = /^([a-z][a-z0-9-]{0,30})@(0|[1-9][0-9]{0,4})$/;
6
+
7
+ export function hostRequirement(value) {
8
+ const match = typeof value === "string" && pattern.exec(value);
9
+ if (!match || Number(match[2]) > 65535) throw new TypeError(`invalid host requirement: ${value}`);
10
+ return { name: match[1], version: Number(match[2]) };
11
+ }
12
+
13
+ export function hostRequirements(values = []) {
14
+ if (!Array.isArray(values)) throw new TypeError("invalid host requirements");
15
+ const versions = new Map();
16
+ for (const value of values) {
17
+ const { name, version } = hostRequirement(value);
18
+ if (versions.has(name) && versions.get(name) !== version) throw new Error(`conflicting host ABI requirements for ${name}`);
19
+ versions.set(name, version);
20
+ }
21
+ if (versions.size > maxRecords) throw new TypeError("too many host requirements");
22
+ return [...versions].sort(([a], [b]) => a.localeCompare(b)).map(([name, version]) => `${name}@${version}`);
23
+ }
24
+
25
+ // The modules an executable's dolly.host records require: name@version -> ABI digest.
26
+ export function executableHostRequirements(module) {
27
+ const required = new Map(), decoder = new TextDecoder("utf-8", { fatal: true });
28
+ let records = 0;
29
+ for (const { name, data } of module.customSectionData) {
30
+ if (name !== "dolly.host") continue;
31
+ records += data.length / recordBytes;
32
+ if (data.length === 0 || data.length % recordBytes || records > maxRecords) {
33
+ throw new Error("invalid dolly.host record count");
34
+ }
35
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
36
+ for (let offset = 0; offset < data.length; offset += recordBytes) {
37
+ const bytes = data.subarray(offset, offset + nameBytes), end = bytes.indexOf(0);
38
+ if (end < 1 || bytes.subarray(end).some(byte => byte !== 0) || view.getUint32(offset + nameBytes + 4, true) !== 0) {
39
+ throw new Error("invalid dolly.host record");
40
+ }
41
+ const requirement = `${decoder.decode(bytes.subarray(0, end))}@${view.getUint32(offset + nameBytes, true)}`;
42
+ const digest = hex(data.subarray(offset + recordBytes - digestBytes, offset + recordBytes));
43
+ if (required.has(requirement) && required.get(requirement) !== digest) {
44
+ throw new Error(`conflicting host ABI digests for ${requirement}`);
45
+ }
46
+ required.set(requirement, digest);
47
+ }
48
+ }
49
+ hostRequirements([...required.keys()]);
50
+ return required;
51
+ }
52
+
53
+ // available: name@version -> the ABI digest its provider implements. A session
54
+ // with nena@0 can enable a module; any other has its image's list.
55
+ export function checkHostAbi(required, available) {
56
+ for (const [requirement, digest] of required) {
57
+ if (!available.has(requirement)) throw new Error(available.has("nena@0")
58
+ ? `host module ${requirement} is not enabled: nena enable ${hostRequirement(requirement).name}`
59
+ : `host module ${requirement} is not declared by this image (REQUIRES HOST)`);
60
+ if (available.get(requirement) !== digest) throw new Error(`host module ${requirement} has a different layout than this program was built for`);
61
+ }
62
+ }
@@ -0,0 +1,8 @@
1
+ // Generated from abi/dolly-supervisor-0.wat, abi/dolly-image-0.wat, abi/dolly-process-0.wat, abi/dolly-process-gate-0.wat, include/dolly/process.h.
2
+ export const DOLLY_TERMINAL_WORD_RESULT_SEQUENCE = 0;
3
+ export const DOLLY_TERMINAL_WORD_RESULT_STATUS = 1;
4
+ export const DOLLY_TERMINAL_WORD_FOREGROUND_PID = 2;
5
+ export const DOLLY_TERMINAL_WORD_FOREGROUND_INTERRUPTIBLE = 3;
6
+ export const DOLLY_TERMINAL_WORD_INTERRUPT_SEQUENCE = 4;
7
+ export const DOLLY_TERMINAL_WORD_INTERRUPT_TARGET_PID = 5;
8
+ export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
@@ -0,0 +1,45 @@
1
+ import { DOLLY_ERRNO } from "./abi.mjs";
2
+
3
+ const encoder = new TextEncoder();
4
+
5
+ // Boot files cross between the Worker and the in-Wasm filesystem through three
6
+ // kernel exports, which take paths and bytes copied into kernel memory.
7
+ export function bootFiles(kernel, memory) {
8
+ const allocate = size => {
9
+ const address = Number(kernel.malloc(BigInt(Math.max(1, size))));
10
+ if (address === 0) throw new Error("Dolly boot allocation failed");
11
+ return address;
12
+ };
13
+ const withBytes = (bytes, use) => {
14
+ const address = allocate(bytes.length);
15
+ try {
16
+ new Uint8Array(memory.buffer, address, bytes.length).set(bytes);
17
+ return use(BigInt(address));
18
+ } finally { kernel.free(BigInt(address)); }
19
+ };
20
+ const withPath = (path, use) => withBytes(encoder.encode(`${path}\0`), use);
21
+ return {
22
+ write(path, value) {
23
+ const bytes = typeof value === "string" ? encoder.encode(value) : value;
24
+ if (!(bytes instanceof Uint8Array)) throw new TypeError("invalid Dolly boot file");
25
+ const status = withPath(path, pathAddress => withBytes(bytes, address =>
26
+ kernel.dolly_write_file(pathAddress, address, BigInt(bytes.length))));
27
+ if (status !== 0) throw new Error(`Dolly could not write ${path}: status ${status}`);
28
+ },
29
+ // Guest-writable files are bounded before trusted code copies them.
30
+ read(path, limit) {
31
+ const address = allocate(limit);
32
+ try {
33
+ const size = withPath(path, pathAddress =>
34
+ kernel.dolly_read_file(pathAddress, BigInt(address), BigInt(limit)));
35
+ if (size === -DOLLY_ERRNO.EFBIG || size > limit) throw new Error(`${path} is larger than ${limit} bytes`);
36
+ if (size < 0) throw new Error(`Dolly could not read ${path}: status ${size}`);
37
+ return new Uint8Array(memory.buffer, address, size).slice();
38
+ } finally { kernel.free(BigInt(address)); }
39
+ },
40
+ remove(path) {
41
+ const status = withPath(path, pathAddress => kernel.dolly_remove_file(pathAddress));
42
+ if (status !== 0 && status !== -DOLLY_ERRNO.ENOENT) throw new Error(`Dolly could not remove ${path}: status ${status}`);
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,14 @@
1
+ // Grows the kernel's memory for the runtime Worker, which waits meanwhile
2
+ // (runtime.mjs): word 0 is 1 while a growth by values[1] pages is asked, and
3
+ // the old size in pages, or -1, replaces that count.
4
+ onmessage = ({ data: { memory, control } }) => {
5
+ const values = new BigInt64Array(control), word = new Int32Array(control);
6
+ postMessage("waiting");
7
+ for (;;) {
8
+ // A notify can be the runtime Worker's for a growth done already.
9
+ while (Atomics.load(word, 0) === 0) Atomics.wait(word, 0, 0);
10
+ try { values[1] = BigInt(memory.grow(values[1])); } catch { values[1] = -1n; }
11
+ Atomics.store(word, 0, 0);
12
+ Atomics.notify(word, 0);
13
+ }
14
+ };
@@ -0,0 +1,65 @@
1
+ import { DOLLY_ERRNO as E } from "./abi.mjs";
2
+
3
+ // The Worker side of a device leased per process (src/device-lease.c). Each
4
+ // packet a process submits goes to the page as `TYPE-request`; the page's
5
+ // `TYPE-complete` is published in the lease's reply slot, a 64-byte header and
6
+ // `replyBytes`. `open` and `close` are the device's operation numbers.
7
+ export function createLeaseBridge({ type, slots: count, packetBytes, replyBytes, open, close }, memory, mailbox, send, complete) {
8
+ const slots = new Array(count), stride = 64 + replyBytes;
9
+ if (!Number.isSafeInteger(mailbox) || mailbox <= 0 || mailbox % 4 ||
10
+ mailbox > memory.buffer.byteLength - count * stride) throw new Error(`Invalid ${type} mailbox`);
11
+ // The reply to the call a slot's lease waits for; its state is published last.
12
+ function publish(slot, entry, error, bytes) {
13
+ const words = new Int32Array(memory.buffer, mailbox + slot * stride, 16);
14
+ new Uint8Array(memory.buffer, mailbox + slot * stride + 64, bytes.length).set(bytes);
15
+ Atomics.store(words, 1, entry.scope); Atomics.store(words, 2, entry.sequence);
16
+ Atomics.store(words, 3, error); Atomics.store(words, 4, bytes.length); Atomics.store(words, 0, 1);
17
+ entry.pending = false;
18
+ }
19
+ function acknowledge(message) {
20
+ const slot = (message.scope - 1) % count, entry = slots[slot];
21
+ if (!entry || entry.scope !== message.scope) return;
22
+ if (message.revoked) slots[slot] = undefined;
23
+ else if (entry.sequence === message.sequence) {
24
+ const valid = message.bytes instanceof Uint8Array && message.bytes.byteLength <= replyBytes &&
25
+ Number.isInteger(message.error) && message.error >= 0 && message.error <= 4095;
26
+ const error = valid ? message.error : E.EIO;
27
+ publish(slot, entry, error, valid && !error ? message.bytes : new Uint8Array());
28
+ if (!entry.released && (entry.operation === close || (entry.operation === open && error))) slots[slot] = undefined;
29
+ }
30
+ complete();
31
+ }
32
+ // The device is going (its module was disabled, or the page ends): the page's
33
+ // answer to a call still waiting will find no one, so the call is answered
34
+ // here as every later one is, ENOSYS. The supervisor's next tick delivers it.
35
+ function dispose() {
36
+ slots.forEach((entry, slot) => { if (entry?.pending) publish(slot, entry, E.ENOSYS, new Uint8Array()); });
37
+ }
38
+ function dispatch({address: addressValue, bytes: sizeValue}) {
39
+ const address = Number(addressValue), size = Number(sizeValue);
40
+ if (!Number.isSafeInteger(address) || !Number.isSafeInteger(size) || address < 0 || size <= 0) return -E.EINVAL;
41
+ if (!address) {
42
+ if (size > 0xffffffff) return -E.EINVAL;
43
+ const entry = slots[(size - 1) % count];
44
+ if (entry?.scope === size && !entry.released) {
45
+ entry.released = true; send({type: `${type}-revoke`, scope: size});
46
+ }
47
+ return 0;
48
+ }
49
+ if (size < 32 || size > packetBytes || address > memory.buffer.byteLength - size) return -E.EINVAL;
50
+ const packet = new Uint8Array(memory.buffer, address, size).slice();
51
+ const view = new DataView(packet.buffer), scope = view.getUint32(8, true), sequence = view.getUint32(16, true);
52
+ const operation = view.getUint32(4, true);
53
+ if (!scope || !sequence || view.getUint32(12, true) || view.getUint32(20, true)) return -E.EINVAL;
54
+ const slot = (scope - 1) % count, entry = slots[slot];
55
+ if (entry?.pending || entry?.released) return -E.EBUSY;
56
+ if (entry && operation === open) return -E.EBUSY;
57
+ if (entry && entry.scope !== scope) return -E.EBUSY;
58
+ if (!entry && operation !== open) return -E.EBADF;
59
+ if (entry && sequence <= entry.sequence) return -E.ESTALE;
60
+ slots[slot] = {scope, sequence, operation, pending: true, released: false};
61
+ send({type: `${type}-request`, scope, sequence, packet}, [packet.buffer]);
62
+ return 0;
63
+ }
64
+ return {dispatch, acknowledge, dispose};
65
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "runtime",
3
+ "version": 0,
4
+ "gives": "the kernel's memory, clocks, entropy and process Workers",
5
+ "fixed": "it is the kernel",
6
+ "provides": "kernel",
7
+ "dependencies": [],
8
+ "phase": "kernel",
9
+ "imports": ["env.memory", "env.dolly_bootstrap_write_bytes", "env.dolly_clock_realtime", "env.dolly_clock_monotonic", "env.dolly_entropy", "env.dolly_memory_grow"],
10
+ "host": "runtime.mjs",
11
+ "contracts": ["../../abi/dolly-supervisor-0.wat", "../../abi/dolly-image-0.wat"],
12
+ "process": ["../../abi/dolly-process-0.wat", "../../abi/dolly-process-gate-0.wat"],
13
+ "headers": ["../../include/dolly/runtime.h", "../../include/dolly/process.h", "../../include/dolly/host.h", "../../include/dolly/host-abi.h", "../../include/dolly/toolchain.h"],
14
+ "kernel": ["../../src/dolly.c", "../../src/process-kernel.c", "../../src/system-snapshot.c", "../../src/device-lease.c", "../../src/file-blocks.cpp", "../../src/libc-host.c"],
15
+ "client": ["../../src/process/runtime-adapter.c", "../../src/process/socket-refusals.c", "../../src/process/libc-stubs.c"]
16
+ }