@daugasauron/dolly 0.1.5 → 0.1.6

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 (76) hide show
  1. package/README.md +51 -78
  2. package/coi-serviceworker.js +23 -8
  3. package/dist/dolly-build-id.mjs +1 -1
  4. package/dist/dolly-image-build-id.mjs +1 -1
  5. package/dist/dolly-process-0.wasm +0 -0
  6. package/dist/dolly-process-abi.mjs +1 -1
  7. package/dist/dolly-process-dso.mjs +19 -11
  8. package/dist/dolly-process-worker.mjs +88 -33
  9. package/dist/dolly-runtime-worker.mjs +1544 -0
  10. package/dist/dolly.wasm +0 -0
  11. package/host/dso/dso.mjs +2 -2
  12. package/host/dso/module.json +4 -1
  13. package/host/http/abi.mjs +3 -1
  14. package/host/http/broker.mjs +17 -7
  15. package/host/http/http.mjs +19 -5
  16. package/host/http/module.json +3 -1
  17. package/host/http/policy.mjs +13 -6
  18. package/host/http/refusals.mjs +77 -0
  19. package/host/input/abi.mjs +1 -1
  20. package/host/modules.mjs +64 -23
  21. package/host/nena/module.json +2 -1
  22. package/host/nena/nena.mjs +2 -0
  23. package/host/packages/abi.mjs +27 -0
  24. package/host/packages/module.json +6 -4
  25. package/host/packages/packages.mjs +23 -8
  26. package/host/packages/service.mjs +46 -20
  27. package/host/packages/transport.mjs +123 -0
  28. package/host/packages/worker.mjs +13 -9
  29. package/host/requirements.mjs +1 -1
  30. package/host/runtime/abi.mjs +32 -0
  31. package/host/runtime/lease-bridge.mjs +13 -8
  32. package/host/runtime/module.json +3 -3
  33. package/host/runtime/runtime.mjs +3 -3
  34. package/host/snapshot/abi.mjs +2 -1
  35. package/host/snapshot/module.json +1 -1
  36. package/host/snapshot/snapshot.mjs +80 -57
  37. package/host/snapshot/transport.mjs +10 -3
  38. package/host/snapshot/ui.mjs +64 -39
  39. package/host/stack/module.json +1 -0
  40. package/host/stack/stack.mjs +1 -1
  41. package/host/threads/module.json +6 -1
  42. package/host/threads/threads.mjs +32 -13
  43. package/package.json +2 -2
  44. package/{src → page}/browser.mjs +37 -23
  45. package/page/generated.mjs +8 -0
  46. package/{src → page}/image-entry.mjs +10 -6
  47. package/{src → page}/image-inputs.mjs +11 -0
  48. package/{src → page}/image-names.mjs +2 -2
  49. package/{src → page}/kernel-plugin.mjs +1 -1
  50. package/{src → page}/pack-store.mjs +1 -1
  51. package/{src → page}/packaged-snapshot.mjs +56 -46
  52. package/{src → page}/process-constants.mjs +72 -15
  53. package/{src → page}/process-supervisor.mjs +155 -227
  54. package/{src → page}/snapshot-records.mjs +63 -44
  55. package/{src → page}/static-asset.mjs +36 -20
  56. package/{src → page}/version.mjs +1 -1
  57. package/{src → page}/worker-image.mjs +20 -9
  58. package/{src → site}/build-worker.mjs +36 -25
  59. package/{src → site}/custom-image.mjs +2 -10
  60. package/{src → site}/dollyfile-graph.mjs +1 -1
  61. package/{src → site}/dollyfile-view.mjs +1 -1
  62. package/{src → site}/image-artifact.mjs +9 -8
  63. package/{src → site}/image-build.mjs +4 -4
  64. package/{src → site}/image-builder.mjs +5 -4
  65. package/site/offline.mjs +53 -0
  66. package/{src → site}/session-store.mjs +47 -38
  67. package/src/runtime-worker.mjs +0 -57
  68. /package/{src → page}/build-log.mjs +0 -0
  69. /package/{src → page}/images.mjs +0 -0
  70. /package/{src → page}/page-chords.mjs +0 -0
  71. /package/{src → page}/page-indicators.mjs +0 -0
  72. /package/{src → page}/process-abi.mjs +0 -0
  73. /package/{src → page}/sha256.mjs +0 -0
  74. /package/{src → page}/terminal-text.mjs +0 -0
  75. /package/{src → page}/wasm-interface.mjs +0 -0
  76. /package/{src → site}/alphabetical.mjs +0 -0
package/dist/dolly.wasm CHANGED
Binary file
package/host/dso/dso.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { parseWasmInterface } from "../../src/wasm-interface.mjs";
1
+ import { parseWasmInterface } from "../../page/wasm-interface.mjs";
2
2
  export { DOLLY_DSO_ABI_DIGEST as digest } from "./abi.mjs";
3
3
 
4
4
  // The loader and the FFI dispatcher (process.mjs) run inside the process Worker
@@ -10,7 +10,7 @@ export function worker({ get, abi, runtimeBase }) {
10
10
  async start() {
11
11
  const [contract, bundle] = await Promise.all(["dolly-dso-0.wasm", "dolly-process-dso.mjs"].map(async name => {
12
12
  const response = await fetch(new URL(`dist/${name}`, runtimeBase), {
13
- cache: "no-store", credentials: "same-origin", redirect: "error",
13
+ credentials: "same-origin", redirect: "error",
14
14
  });
15
15
  if (!response.ok) throw new Error(`Dolly ${name} returned HTTP ${response.status}`);
16
16
  return response.arrayBuffer();
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "dso",
3
3
  "version": 0,
4
+ "build": true,
4
5
  "gives": "dlopen and FFI in the process's own Worker",
5
6
  "dependencies": ["runtime@0"],
6
7
  "phase": "kernel",
@@ -11,5 +12,7 @@
11
12
  "process": ["dolly-dso-0.wat"],
12
13
  "headers": ["dso.h", "dso-abi.h"],
13
14
  "kernel": [],
14
- "client": ["client.c", "ffi.c"]
15
+ "client": ["client.c", "ffi.c"],
16
+ "ccExports": { "rdynamic": ["__dolly_dso_allocate"] },
17
+ "fixtures": "../../test/build-dso-fixtures.mjs"
15
18
  }
package/host/http/abi.mjs CHANGED
@@ -24,5 +24,7 @@ export const DOLLY_HTTP_START = 80;
24
24
  export const DOLLY_HTTP_POLL = 81;
25
25
  export const DOLLY_HTTP_CANCEL = 82;
26
26
  export const DOLLY_HTTP_BODY_WRITE = 83;
27
+ export const DOLLY_HTTP_FAIL_STATUS = 1;
28
+ export const DOLLY_HTTP_FOLLOW_REDIRECTS = 2;
27
29
  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 });
28
- export const DOLLY_HTTP_ABI_DIGEST = "9e3dd4f36f656a23f477c67ce6fe723130caa9b4bf69f6af8670859ea653470f";
30
+ export const DOLLY_HTTP_ABI_DIGEST = "fc788cef38bb5cd423369a5c12c274fa6203ff14eef72540347e778236986aee";
@@ -1,11 +1,12 @@
1
1
  import { HttpError, isDollyCredentialHeader, stripDollyBrowserOwnedHeaders } from "./policy.mjs";
2
2
  import { DOLLY_ERRNO as errno } from "./abi.mjs";
3
- import { decodeStaticAsset, sitePath } from "../../src/static-asset.mjs";
3
+ import { decodeStaticAsset, sitePath } from "../../page/static-asset.mjs";
4
4
  import { DOLLY_HTTP_SLOT_COUNT, DOLLY_HTTP_CHUNK_CAPACITY, DOLLY_HTTP_MAX_METHOD, DOLLY_HTTP_MAX_URL,
5
5
  DOLLY_HTTP_MAX_HEADERS, DOLLY_HTTP_MAX_BODY, DOLLY_HTTP_HEADER_SIZE, DOLLY_HTTP_WORD_SEQUENCE,
6
6
  DOLLY_HTTP_WORD_STATUS, DOLLY_HTTP_WORD_LENGTH, DOLLY_HTTP_WORD_EOF, DOLLY_HTTP_WORD_ERROR,
7
7
  DOLLY_HTTP_WORD_KIND, DOLLY_HTTP_STATE_WRITABLE, DOLLY_HTTP_STATE_READABLE, DOLLY_HTTP_STATE_FAILED,
8
- DOLLY_HTTP_KIND_URL, DOLLY_HTTP_KIND_HEADER, DOLLY_HTTP_KIND_BODY } from "./abi.mjs";
8
+ DOLLY_HTTP_KIND_URL, DOLLY_HTTP_KIND_HEADER, DOLLY_HTTP_KIND_BODY, DOLLY_HTTP_FAIL_STATUS,
9
+ DOLLY_HTTP_FOLLOW_REDIRECTS } from "./abi.mjs";
9
10
 
10
11
  const encoder = new TextEncoder();
11
12
  const now = () => performance.timeOrigin + performance.now();
@@ -54,7 +55,7 @@ export class HttpMailbox {
54
55
  message.flags === 0 };
55
56
  }
56
57
  try {
57
- if (!(memory instanceof SharedArrayBuffer) || (message.flags & ~3) !== 0)
58
+ if (!(memory instanceof SharedArrayBuffer) || (message.flags & ~(DOLLY_HTTP_FAIL_STATUS | DOLLY_HTTP_FOLLOW_REDIRECTS)) !== 0)
58
59
  throw new HttpError(errno.EINVAL, "invalid HTTP admission");
59
60
  const spans = {};
60
61
  for (const [name, maximum] of Object.entries(DOLLY_HTTP_LIMITS)) {
@@ -127,13 +128,15 @@ export class HttpMailbox {
127
128
  // The page's broker: policy, Fetch and the slots' bookkeeping. It never reads
128
129
  // kernel memory: `mailbox` (HttpMailbox, or messages to it) writes the slots.
129
130
  export class NetworkTransport {
130
- // `site` is the URL of the root of the site serving this release, if any.
131
- constructor(mailbox, policy, { fetchRequest = globalThis.fetch.bind(globalThis), site } = {}) {
131
+ // `site` is the URL of the root of the site serving this release, if any;
132
+ // `refused({ method, target, reason })` hears of each request the policy refuses.
133
+ constructor(mailbox, policy, { fetchRequest = globalThis.fetch.bind(globalThis), site, refused } = {}) {
132
134
  this.mailbox = mailbox;
133
135
  this.capacity = DOLLY_HTTP_CHUNK_CAPACITY;
134
136
  this.policy = policy;
135
137
  this.fetchRequest = fetchRequest;
136
138
  this.site = site;
139
+ this.refused = refused;
137
140
  // Host bookkeeping, never derived from a guest's claimed active count.
138
141
  this.slots = Array(DOLLY_HTTP_SLOT_COUNT).fill(null);
139
142
  this.closed = false;
@@ -278,7 +281,14 @@ class HttpTransfer {
278
281
  }
279
282
  stripDollyBrowserOwnedHeaders(headers);
280
283
  const upperMethod = method.toUpperCase();
281
- const rule = this.broker.policy.authorize(target, upperMethod, headers, body?.byteLength ?? 0);
284
+ let rule;
285
+ try { rule = this.broker.policy.authorize(target, upperMethod, headers, body?.byteLength ?? 0); }
286
+ catch (error) {
287
+ if (error instanceof HttpError && [errno.EACCES, errno.EDQUOT].includes(error.errno)) {
288
+ this.broker.refused?.({ method: upperMethod, target, reason: error.message });
289
+ }
290
+ throw error;
291
+ }
282
292
  this.timeout = rule.timeoutMilliseconds;
283
293
  this.moved();
284
294
  failure = errno.EIO;
@@ -286,7 +296,7 @@ class HttpTransfer {
286
296
  method: upperMethod, headers,
287
297
  body: body === null || upperMethod === "GET" || upperMethod === "HEAD" ? undefined : body,
288
298
  credentials: "omit",
289
- redirect: (flags & 2) && rule.followRedirects === true ? "follow" : "error",
299
+ redirect: (flags & DOLLY_HTTP_FOLLOW_REDIRECTS) && rule.followRedirects === true ? "follow" : "error",
290
300
  referrerPolicy: "no-referrer",
291
301
  signal: this.controller.signal,
292
302
  };
@@ -1,15 +1,17 @@
1
1
  import { createHttpAdmission, HttpMailbox, NetworkTransport } from "./broker.mjs";
2
2
  import { DollyHttpPolicy, httpPolicyConfigurations, restrictDollyHttpPolicy } from "./policy.mjs";
3
3
  import { localServicesTransport } from "./local-services.mjs";
4
+ import { refusalNotices } from "./refusals.mjs";
4
5
  export { DOLLY_HTTP_ABI_DIGEST as digest } from "./abi.mjs";
5
6
 
6
7
  // The page takes the embedding's policy once, with the image directory's
7
8
  // trusted sources and, in a custom tab, the policies it inherited; a builder or
8
9
  // headless host is given its network instead. Modules depending on http@0 add
9
- // their local services to `services`.
10
- export function browser({ applicationBase, configuration, send, ask }) {
10
+ // their local services to `services`. Each request the policy refuses is a line in the
11
+ // console and a notice on the page (refusals.mjs).
12
+ export function browser({ applicationBase, configuration, mount, keyboard, send, ask }) {
11
13
  let transport, admission, network = configuration.network;
12
- const services = {};
14
+ const services = {}, refusals = refusalNotices(mount, keyboard);
13
15
  const instance = {
14
16
  services,
15
17
  page: {
@@ -25,7 +27,8 @@ export function browser({ applicationBase, configuration, send, ask }) {
25
27
  // The Worker writes the records into the guest's slots.
26
28
  const mailbox = { publish: record => ask({ type: "http-publish", ...record }, [record.bytes.buffer]),
27
29
  fail: ending => send({ type: "http-fail", ...ending }) };
28
- transport = new NetworkTransport(mailbox, network.policy, { fetchRequest: network.fetchRequest, site: network.site });
30
+ transport = new NetworkTransport(mailbox, network.policy, { fetchRequest: network.fetchRequest, site: network.site,
31
+ refused: refusals.refused });
29
32
  },
30
33
  messages: {
31
34
  async "http-request"(message) {
@@ -36,7 +39,18 @@ export function browser({ applicationBase, configuration, send, ask }) {
36
39
  Atomics.notify(admission, 0);
37
40
  },
38
41
  },
39
- dispose() { transport?.close(); },
42
+ // A GET a host module makes for the guest, judged by the page's policy as
43
+ // the guest's own requests are, and shown as theirs when refused.
44
+ admit(url) {
45
+ try {
46
+ network.policy.authorize(url, "GET", new Headers(), 0);
47
+ } catch (error) {
48
+ refusals.refused({ method: "GET", target: url, reason: error.message });
49
+ throw error;
50
+ }
51
+ },
52
+ claimsKey: refusals.claimsKey,
53
+ dispose() { transport?.close(); refusals.dispose(); },
40
54
  };
41
55
  if (!network) {
42
56
  // A custom tab intersects the policy of the page that built it with its
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "http",
3
3
  "version": 0,
4
+ "build": true,
4
5
  "gives": "HTTP(S) requests under the page's policy",
5
6
  "fixed": "the page's HTTP policy is consumed once, at load",
6
7
  "dependencies": ["runtime@0"],
@@ -11,5 +12,6 @@
11
12
  "process": [],
12
13
  "headers": ["http.h", "http-abi.h"],
13
14
  "kernel": ["kernel.c"],
14
- "client": ["client.c"]
15
+ "client": ["client.c"],
16
+ "tools": ["libcurl.c"]
15
17
  }
@@ -27,6 +27,13 @@ const browserOwnedTransportHeaderNames = new Set([
27
27
  "user-agent",
28
28
  ]);
29
29
 
30
+ // What an ordinary request carries: under a hardened rule these and the
31
+ // headers the rule names leave the sandbox, and nothing else does, a token in
32
+ // a header of any other name included.
33
+ const ordinaryHeaderNames = new Set(["accept", "accept-language", "content-encoding", "content-language", "content-type",
34
+ "range", "if-match", "if-none-match", "if-modified-since", "if-unmodified-since", "if-range",
35
+ "cache-control", "pragma"]);
36
+
30
37
  export function isDollyCredentialHeader(name) {
31
38
  return credentialHeaderNames.has(String(name).toLowerCase());
32
39
  }
@@ -86,8 +93,8 @@ function normalizeRule(rule) {
86
93
  }
87
94
  const credentialHeaders = new Set((rule.credentialHeaders ?? []).map((value) => {
88
95
  const name = String(value).toLowerCase();
89
- if (!credentialHeaderNames.has(name)) {
90
- throw new TypeError(`invalid Dolly HTTP credential header: ${name}`);
96
+ if (!/^[!#$%&'*+.^_`|~0-9a-z-]+$/.test(name)) {
97
+ throw new TypeError(`invalid Dolly HTTP header name: ${name}`);
91
98
  }
92
99
  return name;
93
100
  }));
@@ -243,11 +250,11 @@ export class DollyHttpPolicy {
243
250
  }
244
251
 
245
252
  // Credentials are ordinary sandbox state. Development mode preserves
246
- // them. A hardened destination rule must explicitly name which common
247
- // credential headers may leave for that exact destination.
253
+ // them. A hardened destination rule names every header beyond an
254
+ // ordinary request's that may leave for that exact destination.
248
255
  if (rule.credentialHeaders !== null) {
249
- for (const name of credentialHeaderNames) {
250
- if (!rule.credentialHeaders.has(name)) headers.delete(name);
256
+ for (const name of [...headers.keys()]) {
257
+ if (!ordinaryHeaderNames.has(name) && !rule.credentialHeaders.has(name)) headers.delete(name);
251
258
  }
252
259
  }
253
260
  const relay = rule.bootstrap ? undefined : relayUrl(this.relays, target, headers);
@@ -0,0 +1,77 @@
1
+ // What the HTTP policy refuses, for whoever runs the page: each refusal is a line in the
2
+ // browser's console, and a notice in boot()'s element that stays until the user closes it.
3
+ // Refusals of one method and URL are one notice with a count; the newest few show, with a
4
+ // line for the rest and Close all. A URL is shown without its query, which may hold a key.
5
+ // The list lies over the display: its words let the pointer through to the program, and only
6
+ // its buttons take a click.
7
+ const shownAtOnce = 5;
8
+
9
+ const style = `
10
+ #http-refusals { position: absolute; left: 0.6rem; top: 0.6rem; z-index: 7; width: min(34rem, calc(100% - 1.2rem));
11
+ margin: 0; padding: 0.35rem 0.35rem 0; list-style: none; color: var(--dolly-ui-color); background: var(--dolly-ui-background); font-size: 14px; line-height: 1.4;
12
+ pointer-events: none; }
13
+ #http-refusals[hidden] { display: none; }
14
+ #http-refusals li { display: flex; gap: 0.5rem; align-items: baseline; margin-bottom: 0.35rem; padding: 0.4rem 0.5rem;
15
+ background: var(--dolly-ui-background); border: 1px solid var(--dolly-ui-alert); overflow-wrap: anywhere; }
16
+ #http-refusals li span { flex: 1; }
17
+ #http-refusals li.rest { border-color: var(--dolly-ui-quiet); }
18
+ #http-refusals button { color: inherit; background: var(--dolly-ui-raised); border: 1px solid var(--dolly-ui-quiet); font: inherit; cursor: pointer;
19
+ pointer-events: auto; }
20
+ `;
21
+
22
+ export function refusalNotices(mount, keyboard) {
23
+ const notices = new Map();
24
+ let list, look;
25
+ if (mount) {
26
+ look = new CSSStyleSheet();
27
+ look.replaceSync(style);
28
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, look];
29
+ list = mount.appendChild(Object.assign(document.createElement("ul"), { id: "http-refusals", hidden: true }));
30
+ list.setAttribute("role", "status");
31
+ list.setAttribute("aria-label", "Requests the HTTP policy refused");
32
+ }
33
+ const button = (label, name, press) => {
34
+ const element = Object.assign(document.createElement("button"), { type: "button", textContent: label });
35
+ element.setAttribute("aria-label", name);
36
+ element.addEventListener("click", () => {
37
+ press();
38
+ render();
39
+ keyboard?.focus({ preventScroll: true });
40
+ });
41
+ return element;
42
+ };
43
+ function render() {
44
+ const newest = [...notices].reverse(), shown = newest.slice(0, shownAtOnce);
45
+ list.replaceChildren(...shown.map(([key, { text, count }]) => {
46
+ const item = document.createElement("li"), words = document.createElement("span");
47
+ words.textContent = `The HTTP policy refused ${text}${count > 1 ? ` (${count} times)` : ""}`;
48
+ item.append(words, button("×", `Close: ${text}`, () => notices.delete(key)));
49
+ return item;
50
+ }));
51
+ if (notices.size > 1) {
52
+ const rest = document.createElement("li"), words = document.createElement("span");
53
+ rest.className = "rest";
54
+ const hidden = notices.size - shown.length;
55
+ words.textContent = hidden ? `${hidden} more refused (the console lists every one)` : "";
56
+ rest.append(words, button("Close all", "Close all refusals", () => notices.clear()));
57
+ list.append(rest);
58
+ }
59
+ list.hidden = notices.size === 0;
60
+ }
61
+ return {
62
+ refused({ method, target, reason }) {
63
+ const text = `${method} ${target.origin}${target.pathname}`;
64
+ console.warn(`${reason}: ${text}`);
65
+ if (!list) return;
66
+ const notice = notices.get(text);
67
+ notices.delete(text);
68
+ notices.set(text, { text, count: (notice?.count ?? 0) + 1 });
69
+ render();
70
+ },
71
+ claimsKey: event => list?.contains(event.target) ?? false,
72
+ dispose() {
73
+ list?.remove();
74
+ if (look) document.adoptedStyleSheets = document.adoptedStyleSheets.filter(sheet => sheet !== look);
75
+ },
76
+ };
77
+ }
@@ -44,4 +44,4 @@ export const DOLLY_INPUT_NEXT_EVENT = 89;
44
44
  export const DOLLY_INPUT_SET_POINTER = 90;
45
45
  export const DOLLY_INPUT_RELEASE = 91;
46
46
  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 });
47
- export const DOLLY_INPUT_ABI_DIGEST = "862edb42d7dbb04c1c49411d740a97503fe387678a1b660f19b3b2dc4f18b9c6";
47
+ export const DOLLY_INPUT_ABI_DIGEST = "6bdd3f083670e13711d45c6bb787d6a6545ccee2c60d6de2ba9111780fc6c174";
package/host/modules.mjs CHANGED
@@ -1,11 +1,5 @@
1
1
  import { hostRequirement, hostRequirements } from "./requirements.mjs";
2
- import { DOLLY_ERRNO as E } from "../src/process-constants.mjs";
3
-
4
- // What a build enables beside the runtime its image declares: the broker the
5
- // engine fetches sources through, the threads its toolchain uses, the
6
- // modules its compilers and interpreters load and the fresh stacks of the
7
- // tools stack-wrap rewrote.
8
- export const buildHost = Object.freeze(["http@0", "threads@0", "dso@0", "stack@0"]);
2
+ import { DOLLY_ERRNO as E } from "../page/process-constants.mjs";
9
3
 
10
4
  // A page's host modules, from the URLs of their manifests. The embedding, trusted
11
5
  // code, names them; images select no JS or Worker URLs, and each module's provider
@@ -15,7 +9,10 @@ export async function hostRegistry(urls) {
15
9
  const definitions = await Promise.all(urls.map(async url => {
16
10
  const { default: manifest } = await import(url, { with: { type: "json" } });
17
11
  const contract = Object.freeze({ ...manifest, url });
18
- return { ...await import(new URL(contract.host, url).href), contract };
12
+ // A module's provider is imported when the page or a Worker first needs it:
13
+ // its module attached or admitted, or a session route's boot selected.
14
+ let provider;
15
+ return { contract, provider: () => provider ??= import(new URL(contract.host, url).href) };
19
16
  }));
20
17
  const byName = new Map(), owners = new Map();
21
18
  for (const module of definitions) {
@@ -27,25 +24,30 @@ export async function hostRegistry(urls) {
27
24
  }
28
25
  }
29
26
  const contracts = Object.freeze(definitions.map(module => module.contract));
30
- const runtimes = Object.freeze(contracts.filter(contract => contract.provides === "kernel")
31
- .map(({ name, version }) => `${name}@${version}`));
27
+ const requirementsOf = keep => Object.freeze(contracts.filter(keep).map(({ name, version }) => `${name}@${version}`));
28
+ const runtimes = requirementsOf(contract => contract.provides === "kernel");
29
+ // What a build enables beside the runtime its image declares: the modules
30
+ // whose manifests say `build` (README.md).
31
+ const buildHost = requirementsOf(contract => contract.build);
32
32
  const registry = { byName, owners, contracts, runtimes };
33
33
  return Object.freeze({
34
34
  urls: Object.freeze([...urls]),
35
35
  buildHostFor: declared => [...declared.filter(value => runtimes.includes(value)), ...buildHost],
36
+ load: values => loadProviders(byName, values),
36
37
  selectBoot: route => selectBoot(definitions, route),
37
38
  createHost: (...values) => createHost(registry, ...values),
38
39
  });
39
40
  }
40
41
 
41
- // A module may select what a route boots, as a saved session names its image:
42
- // at most one does, and the page then requires it. The selection carries the
43
- // image, a bootstrap label, the custom image record it restores and the
42
+ // A module may select what a session route boots, as a saved session names its
43
+ // image: at most one does, and the page then requires it. The selection carries
44
+ // the image, a bootstrap label, the custom image record it restores and the
44
45
  // module's configuration, keyed for createHost.
45
46
  async function selectBoot(definitions, route) {
47
+ if (!route.loadSession) return null;
46
48
  let selection = null;
47
49
  for (const module of definitions) {
48
- const selected = await module.boot?.(route);
50
+ const selected = await (await module.provider()).boot?.(route);
49
51
  if (!selected) continue;
50
52
  if (selection) throw new Error("two host modules selected the boot");
51
53
  const { name, version } = module.contract;
@@ -54,6 +56,18 @@ async function selectBoot(definitions, route) {
54
56
  return selection;
55
57
  }
56
58
 
59
+ // The providers of these modules and their dependencies start loading together; createHost
60
+ // attaches them in order.
61
+ function loadProviders(byName, values, seen = new Set()) {
62
+ for (const value of values) {
63
+ const { name, version } = hostRequirement(value), module = byName.get(name);
64
+ if (seen.has(name) || module?.contract.version !== version) continue;
65
+ seen.add(name);
66
+ module.provider().catch(() => {});
67
+ loadProviders(byName, module.contract.dependencies, seen);
68
+ }
69
+ }
70
+
57
71
  async function createHost({ byName, owners, contracts, runtimes }, side, enabled,
58
72
  { send, resources = {}, configuration = {} } = {}) {
59
73
  if (!["browser", "worker"].includes(side)) throw new TypeError("invalid host side");
@@ -96,7 +110,7 @@ async function createHost({ byName, owners, contracts, runtimes }, side, enabled
96
110
  const get = name => instances.get(name);
97
111
  // Page UI across modules: the first module that claims a key event takes it
98
112
  // from the guest's input and may act on it; a module drawing over the
99
- // display canvas reports the pixel size pointer input maps to.
113
+ // display canvas reports the pixel size of its frame.
100
114
  function claimsKey(event) {
101
115
  for (const instance of instances.values()) {
102
116
  const claim = instance.claimsKey?.(event);
@@ -105,6 +119,15 @@ async function createHost({ byName, owners, contracts, runtimes }, side, enabled
105
119
  return false;
106
120
  }
107
121
  const surfaceSize = () => [...instances.values()].find(instance => instance.surfaceSize)?.surfaceSize;
122
+ // A pointer's position in pixels of the frame shown, which the canvas fits
123
+ // within its box at the frame's own shape, centred (object-fit: contain).
124
+ function framePoint({ clientX, clientY }) {
125
+ const bounds = resources.canvas.getBoundingClientRect(), { width, height } = surfaceSize() ?? resources.canvas;
126
+ const scale = Math.min(bounds.width / width, bounds.height / height);
127
+ if (!(scale > 0)) return { x: 0, y: 0 };
128
+ return { x: (clientX - bounds.left - (bounds.width - width * scale) / 2) / scale,
129
+ y: (clientY - bounds.top - (bounds.height - height * scale) / 2) / scale };
130
+ }
108
131
  async function attach(value, visiting = new Set(), checked = false) {
109
132
  const { name, version } = hostRequirement(value), module = byName.get(name);
110
133
  if (instances.has(name) || reasons.has(name)) return;
@@ -117,8 +140,9 @@ async function createHost({ byName, owners, contracts, runtimes }, side, enabled
117
140
  visiting.delete(name);
118
141
  try { requireModules(module.contract.dependencies); }
119
142
  catch (error) { reasons.set(name, error.message); return; }
143
+ const provider = await module.provider();
120
144
  if (side === "browser" && !checked) {
121
- const reason = await module.check?.(resources);
145
+ const reason = await provider.check?.(resources);
122
146
  if (reason) { reasons.set(name, reason); return; }
123
147
  }
124
148
  const dependency = name => {
@@ -127,9 +151,10 @@ async function createHost({ byName, owners, contracts, runtimes }, side, enabled
127
151
  }
128
152
  return get(name);
129
153
  };
130
- const instance = module[side]?.({ ...resources, send, ask, get: dependency,
154
+ // `switched`: the page enabled the module after boot (nena@0), not the image.
155
+ const instance = provider[side]?.({ ...resources, switched: checked, send, ask, get: dependency,
131
156
  service: () => { for (const instance of instances.values()) instance.service?.(); },
132
- claimsKey, surfaceSize,
157
+ claimsKey, framePoint,
133
158
  abi: admittedAbi, modules: module.contract.switches && side === "browser" ? { list, enable, disable } : undefined,
134
159
  configuration: configuration[name] ?? {} }) ?? {};
135
160
  instances.set(name, instance);
@@ -210,7 +235,7 @@ async function createHost({ byName, owners, contracts, runtimes }, side, enabled
210
235
  // later switch: after five seconds the switch is refused and the check
211
236
  // told so, that an answer coming later shows nothing on the page.
212
237
  const abandoned = new AbortController(), timer = setTimeout(() => abandoned.abort(), 5000);
213
- const unavailable = await Promise.race([module.check?.({ ...resources, signal: abandoned.signal }), new Promise(resolve =>
238
+ const unavailable = await Promise.race([(await module.provider()).check?.({ ...resources, signal: abandoned.signal }), new Promise(resolve =>
214
239
  abandoned.signal.addEventListener("abort", () => resolve("the browser did not answer in time")))]);
215
240
  clearTimeout(timer);
216
241
  if (unavailable) return unavailable;
@@ -244,6 +269,7 @@ async function createHost({ byName, owners, contracts, runtimes }, side, enabled
244
269
  for (const instance of [...instances.values()].reverse()) instance.dispose?.();
245
270
  }
246
271
  try {
272
+ loadProviders(byName, selected);
247
273
  for (const value of selected) await attach(value);
248
274
  if (kernels.length !== 1) {
249
275
  // A requested module this page lacks may be the runtime the image declares.
@@ -261,12 +287,27 @@ async function createHost({ byName, owners, contracts, runtimes }, side, enabled
261
287
  require: requireModules, dispose,
262
288
  // Executables may use only the modules the image declares. A declared module
263
289
  // this host does not enable (a build host) answers its calls with ENOSYS.
264
- admit(requirements) {
265
- admittedAbi.clear();
290
+ async admit(requirements) {
291
+ const admitted = new Map();
266
292
  for (const value of hostRequirements(requirements)) {
267
293
  const { name, version } = hostRequirement(value), module = byName.get(name);
268
- if (module?.digest && module.contract.version === version) admittedAbi.set(value, module.digest);
294
+ const digest = module?.contract.version === version ? (await module.provider()).digest : undefined;
295
+ if (digest) admitted.set(value, digest);
269
296
  }
297
+ admittedAbi.clear();
298
+ for (const [value, digest] of admitted) admittedAbi.set(value, digest);
299
+ },
300
+ // The Worker asks this before it restores the image: a module that restores
301
+ // the whole filesystem itself (a saved session, host/snapshot) says so, and
302
+ // no image is loaded.
303
+ async restoreFilesystem(context) {
304
+ for (const instance of instances.values()) if (await instance.restoreFilesystem?.(context)) return true;
305
+ return false;
306
+ },
307
+ // Whether restoreFilesystem will restore one, known from the configuration,
308
+ // before a kernel exists.
309
+ restoresFilesystem() {
310
+ return [...instances.values()].some(instance => instance.restoresFilesystem?.());
270
311
  },
271
312
  // The Worker calls this after restoring the system image and before starting
272
313
  // image-phase modules, in registry order.
@@ -332,7 +373,7 @@ async function createHost({ byName, owners, contracts, runtimes }, side, enabled
332
373
  const instance = instances.get(name);
333
374
  if (!instance) throw new Error(reasons.get(name));
334
375
  if (instance.start) await startProvider(name, instance, startContext);
335
- const { digest } = byName.get(name);
376
+ const { digest } = await byName.get(name).provider();
336
377
  if (digest) admittedAbi.set(message.module, digest);
337
378
  } else if (instances.has(name)) detach(name);
338
379
  } catch (error) {
@@ -11,5 +11,6 @@
11
11
  "process": [],
12
12
  "headers": ["nena.h", "nena-abi.h"],
13
13
  "kernel": ["kernel.c"],
14
- "client": ["client.c"]
14
+ "client": ["client.c"],
15
+ "package": "libdolly-nena"
15
16
  }
@@ -66,6 +66,8 @@ export function browser({ send, modules, showStatus }) {
66
66
  // travels through the provider that the switch takes away.
67
67
  if (!enabling && name === "nena") return { bytes: moduleRecord({ ...found(), enabled: false }), after: () => modules.disable(value) };
68
68
  const refusal = await (enabling ? modules.enable(value) : modules.disable(value));
69
+ // The page says every switch: what a session gives itself is seen.
70
+ if (!refusal) showStatus(`This session ${enabling ? "enabled" : "disabled"} ${value}.`);
69
71
  return { bytes: moduleRecord(found(), refusal ?? undefined), enabled: enabledSince() };
70
72
  }
71
73
  return {
@@ -0,0 +1,27 @@
1
+ // Generated from host/packages/dolly-packages-0.wat, host/packages/packages.h, include/dolly/process.h.
2
+ export const DOLLY_PACKAGES_HEADER_SIZE = 64;
3
+ export const DOLLY_PACKAGES_WORD_REQUEST = 0;
4
+ export const DOLLY_PACKAGES_WORD_CANCELLED = 1;
5
+ export const DOLLY_PACKAGES_WORD_COMPLETED = 2;
6
+ export const DOLLY_PACKAGES_WORD_CHUNK = 3;
7
+ export const DOLLY_PACKAGES_WORD_CONSUMED = 4;
8
+ export const DOLLY_PACKAGES_WORD_LENGTH = 5;
9
+ export const DOLLY_PACKAGES_WORD_ERROR = 6;
10
+ export const DOLLY_PACKAGES_WORD_EOF = 7;
11
+ export const DOLLY_PACKAGES_WORD_ENABLED = 8;
12
+ export const DOLLY_PACKAGES_WORD_SIZE = 9;
13
+ export const DOLLY_PACKAGES_WORD_KIND = 10;
14
+ export const DOLLY_PACKAGES_WORD_DIRECTORY_LENGTH = 11;
15
+ export const DOLLY_PACKAGES_DIRECTORY_OFFSET = 64;
16
+ export const DOLLY_PACKAGES_DIRECTORY_CAPACITY = 2048;
17
+ export const DOLLY_PACKAGES_PIN_OFFSET = 2112;
18
+ export const DOLLY_PACKAGES_PIN_BYTES = 64;
19
+ export const DOLLY_PACKAGES_CHUNK_OFFSET = 2176;
20
+ export const DOLLY_PACKAGES_CHUNK_CAPACITY = 1048576;
21
+ export const DOLLY_PACKAGES_MAX_SIZE = 2147483647;
22
+ export const DOLLY_PACKAGES_KIND_DIRECTORY = 1;
23
+ export const DOLLY_PACKAGES_KIND_INDEX = 2;
24
+ export const DOLLY_PACKAGES_KIND_SNAPSHOT = 3;
25
+ export const DOLLY_PACKAGES_FETCH = 134;
26
+ 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 });
27
+ export const DOLLY_PACKAGES_ABI_DIGEST = "2e7905a73d0f041833b239074042ed9ca77e692c3caa0d9d147be98f94ea95b8";
@@ -3,11 +3,13 @@
3
3
  "version": 0,
4
4
  "gives": "the release's published packages, for amy",
5
5
  "dependencies": ["http@0"],
6
+ "phase": "image",
6
7
  "imports": [],
7
8
  "host": "packages.mjs",
8
- "contracts": [],
9
+ "contracts": ["dolly-packages-0.wat"],
9
10
  "process": [],
10
- "headers": [],
11
- "kernel": [],
12
- "client": []
11
+ "headers": ["packages.h", "packages-abi.h"],
12
+ "kernel": ["kernel.c"],
13
+ "client": ["client.c"],
14
+ "package": "libdolly-packages"
13
15
  }
@@ -1,15 +1,30 @@
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.
1
+ // Published package snapshots for a running image. The guest names a package
2
+ // by the recipe pin an image directory publishes, by default the page's own;
3
+ // the page materializes and verifies its snapshot exactly as it does a build
4
+ // input. From the page's own directory the index, amy-index.txt, and the
5
+ // snapshots come over this module's own mailbox (dolly-packages-0.wat,
6
+ // transport.mjs); another directory is read through http@0's broker, as
7
+ // https://packages.dolly.invalid/v1/… (host/http/local-services.mjs), only
8
+ // where the page's HTTP policy admits it, as a request of the guest's would be.
8
9
  import { PackageService } from "./service.mjs";
10
+ import { PackagesTransport } from "./transport.mjs";
11
+ export { DOLLY_PACKAGES_ABI_DIGEST as digest } from "./abi.mjs";
9
12
 
10
13
  export function browser({ get }) {
11
14
  return {
12
- entryStarted() { get("http").services.packages = new PackageService(); },
15
+ entryStarted() { const http = get("http"); http.services.packages = new PackageService(http.admit); },
13
16
  disabled() { delete get("http").services.packages; },
14
17
  };
15
18
  }
19
+
20
+ // The kernel's mailbox reports each request at the service tick.
21
+ export function worker({ images }) {
22
+ let transport;
23
+ return {
24
+ start({ kernel, memory }) {
25
+ transport = new PackagesTransport(memory.buffer, Number(kernel.dolly_packages_mailbox_address()), images);
26
+ },
27
+ service() { transport?.service(); },
28
+ dispose() { transport?.close(); },
29
+ };
30
+ }