@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
@@ -1,4 +1,4 @@
1
- import { DOLLY_SESSION_MAX_BYTES } from "../../src/session-store.mjs";
1
+ import { DOLLY_SESSION_MAX_BYTES } from "../../site/session-store.mjs";
2
2
  import { DOLLY_ERRNO } from "./abi.mjs";
3
3
  import * as A from "./abi.mjs";
4
4
 
@@ -17,15 +17,16 @@ export class SessionTransport {
17
17
  static totalSizeLow = A.DOLLY_SESSION_WORD_TOTAL_SIZE_LOW;
18
18
  static totalSizeHigh = A.DOLLY_SESSION_WORD_TOTAL_SIZE_HIGH;
19
19
  static cancelledSequence = A.DOLLY_SESSION_WORD_CANCELLED_SEQUENCE;
20
+ static changes = A.DOLLY_SESSION_WORD_CHANGES;
20
21
 
21
22
  constructor(buffer, address, transferAddress, wake) {
22
23
  const range = (start, size) => Number.isSafeInteger(start) && start > 0 && start <= buffer.byteLength - size;
23
- if (!(buffer instanceof SharedArrayBuffer) || !range(address, A.DOLLY_SESSION_HEADER_SIZE) || address % 4 ||
24
+ if (!(buffer instanceof SharedArrayBuffer) || !range(address, A.DOLLY_SESSION_MAILBOX_HEADER_SIZE) || address % 4 ||
24
25
  !range(transferAddress, A.DOLLY_SESSION_TRANSFER_CAPACITY)) {
25
26
  throw new Error("Dolly supplied an invalid session mailbox");
26
27
  }
27
28
  this.bytes = new Uint8Array(buffer);
28
- this.words = new Int32Array(buffer, address, A.DOLLY_SESSION_HEADER_SIZE / 4);
29
+ this.words = new Int32Array(buffer, address, A.DOLLY_SESSION_MAILBOX_HEADER_SIZE / 4);
29
30
  this.transferAddress = transferAddress;
30
31
  this.transferCapacity = A.DOLLY_SESSION_TRANSFER_CAPACITY;
31
32
  this.wake = wake;
@@ -110,6 +111,10 @@ export class SessionTransport {
110
111
  if (status === -DOLLY_ERRNO.EFBIG) {
111
112
  throw new Error(`Dolly session exceeds its ${DOLLY_SESSION_MAX_BYTES / 1024 / 1024} MiB limit; remove files or installed packages and save again`);
112
113
  }
114
+ // DOLLY_SESSION_MAX_RECORDS (kernel/session-records.h).
115
+ if (status === -DOLLY_ERRNO.E2BIG) {
116
+ throw new Error("Dolly session holds more than 1,000,000 files and directories; remove some and save again");
117
+ }
113
118
  if (status !== 0) throw new Error(`Dolly session capture failed with status ${status}`);
114
119
  if (offset < 16 || offset !== declaredTotal) throw new Error("Dolly session snapshot was incomplete");
115
120
  complete = true;
@@ -120,5 +125,7 @@ export class SessionTransport {
120
125
  this.cancel = null;
121
126
  }
122
127
  }
128
+ // Whether processes changed what a save holds since the last capture began.
129
+ get changed() { return Atomics.load(this.words, SessionTransport.changes) !== 0; }
123
130
  close() { this.closed = true; this.cancel?.(); }
124
131
  }
@@ -1,5 +1,5 @@
1
- import { DOLLY_BUILD_ID } from "../../dist/dolly-build-id.mjs";
2
- import { DOLLY_IMAGES } from "../../src/images.mjs";
1
+ import { DOLLY_IMAGE_BUILD_ID } from "../../page/generated.mjs";
2
+ import { DOLLY_IMAGES } from "../../page/images.mjs";
3
3
  import {
4
4
  DOLLY_SESSION_FORMAT_VERSION,
5
5
  customSessionIdentity,
@@ -9,28 +9,30 @@ import {
9
9
  sessionImageIdentity,
10
10
  sessionLoadUrl,
11
11
  validSessionName,
12
- } from "../../src/session-store.mjs";
12
+ } from "../../site/session-store.mjs";
13
+ import { keepRuntime } from "../../site/offline.mjs";
13
14
 
14
- const markup = `<style>
15
+ // An adopted style sheet, which a page's style-src without 'unsafe-inline' allows.
16
+ const style = `
15
17
  #session-open { position: absolute; right: 0.5rem; bottom: 0.35rem; z-index: 6;
16
- padding: 0.15rem 0.45rem; color: inherit; background: #262626;
17
- border: 1px solid #77736c; font: inherit; font-size: 13px; cursor: pointer; }
18
- #session-open[data-failed] { border-color: #e98773; }
18
+ padding: 0.15rem 0.45rem; color: inherit; background: var(--dolly-ui-background);
19
+ border: 1px solid var(--dolly-ui-quiet); font: inherit; font-size: 13px; cursor: pointer; }
20
+ #session-open[data-failed] { border-color: var(--dolly-ui-alert); }
19
21
  #session-dialog { width: min(420px, calc(100vw - 2rem)); padding: 1.2rem;
20
- background: #262626; color: inherit; border: 1px solid #77736c; font: inherit; }
22
+ background: var(--dolly-ui-background); color: inherit; border: 1px solid var(--dolly-ui-quiet); font: inherit; }
21
23
  #session-dialog::backdrop { background: #0008; }
22
24
  #session-dialog h2 { margin: 0 0 1rem; font-size: 20px; }
23
25
  #session-dialog p { font-size: 14px; line-height: 1.5; }
24
26
  #session-dialog label { display: block; margin: 1rem 0 0.4rem; }
25
27
  #session-dialog input { width: 100%; padding: 0.4rem; font: inherit;
26
- color: inherit; background: #191919; border: 1px solid #77736c; }
28
+ color: inherit; background: var(--dolly-ui-sunken); border: 1px solid var(--dolly-ui-quiet); }
27
29
  #session-dialog button { padding: 0.35rem 0.65rem; font: inherit;
28
- color: inherit; background: #333; border: 1px solid #77736c; cursor: pointer; }
29
- #session-dialog a { color: #f2d45c; }
30
+ color: inherit; background: var(--dolly-ui-raised); border: 1px solid var(--dolly-ui-quiet); cursor: pointer; }
31
+ #session-dialog a { color: var(--dolly-ui-accent); }
30
32
  #session-dialog nav { display: flex; gap: 0.6rem; align-items: center; margin-top: 1rem; }
31
33
  #session-dialog nav a { margin-right: auto; font-size: 14px; }
32
- </style>
33
- <button id="session-open" class="page-indicator" type="button" aria-label="Save session" title="Save session (Ctrl+Shift+S)" hidden>Save</button>
34
+ `;
35
+ const markup = `<button id="session-open" class="page-indicator" type="button" aria-label="Save session" title="Save session (Ctrl+Shift+S)" hidden>Save</button>
34
36
  <dialog id="session-dialog" aria-labelledby="session-title">
35
37
  <form id="session-form">
36
38
  <h2 id="session-title">Save session</h2>
@@ -55,6 +57,9 @@ const markup = `<style>
55
57
  // (the site's session route, or ?session= on a page of one's own), and on a
56
58
  // page with a site the dialog links the site's list of saves.
57
59
  export function mountSessionSave({ mount, keyboard, showStatus, site, indicators }, transport, restored) {
60
+ const look = new CSSStyleSheet();
61
+ look.replaceSync(style);
62
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, look];
58
63
  mount.insertAdjacentHTML("beforeend", markup);
59
64
  const button = document.querySelector("#session-open");
60
65
  const dialog = document.querySelector("#session-dialog");
@@ -64,6 +69,15 @@ export function mountSessionSave({ mount, keyboard, showStatus, site, indicators
64
69
  const dataset = document.documentElement.dataset;
65
70
  let base, sessionName = null, lastSave = null, saveError = "";
66
71
  let saving = null, controller = null;
72
+ // When the save this tab restored or last made was stored: a later one is another tab's.
73
+ let knownSave = null;
74
+ // Whether processes changed files since the last capture began (the kernel's
75
+ // count, snapshot.mjs), and whether any change was reported since the last save
76
+ // that was stored: a failed save leaves it set.
77
+ let changed = false, unsaved = false;
78
+ // A tab of the site whose session was saved or restored asks, with the
79
+ // browser's own prompt, before it is left with changes no save holds.
80
+ if (site) addEventListener("beforeunload", event => { if (sessionName !== null && (changed || unsaved)) event.preventDefault(); });
67
81
 
68
82
  function update(message) {
69
83
  const failed = dataset.sessionStatus === "failed";
@@ -103,12 +117,11 @@ export function mountSessionSave({ mount, keyboard, showStatus, site, indicators
103
117
  async function save(requestedName) {
104
118
  if (saving) return saving;
105
119
  // A save asks the browser to keep the origin's storage: the saves and the
106
- // packs their images start from (src/pack-store.mjs). Chrome answers from
107
- // the site's engagement, Firefox asks the user; the save does not wait.
120
+ // packs images start from (page/pack-store.mjs). Chrome answers from the
121
+ // site's engagement, Firefox asks the user; the save does not wait.
108
122
  navigator.storage.persist().catch(() => {});
109
123
  saving = (async () => {
110
124
  if (!base) throw new Error("Dolly is not ready to save a session");
111
- await base.verifyBase?.();
112
125
  let name = requestedName ?? sessionName;
113
126
  if (name === null) {
114
127
  name = window.prompt("Save Dolly session as:", "");
@@ -118,10 +131,16 @@ export function mountSessionSave({ mount, keyboard, showStatus, site, indicators
118
131
  if (!validSessionName(name)) {
119
132
  throw new Error("Session names use 1-64 letters, numbers, '.', '_' or '-'");
120
133
  }
121
- if (name !== sessionName && await loadStoredSession(name) !== null &&
122
- !window.confirm(`Replace the saved session '${name}'?`)) return null;
123
- showStatus(`Saving ${name}…`, true);
134
+ const before = dataset.sessionStatus;
124
135
  dataset.sessionStatus = "capturing";
136
+ const stored = await loadStoredSession(name);
137
+ if (stored !== null && (name !== sessionName ? !window.confirm(`Replace the saved session '${name}'?`)
138
+ : stored.updatedAt !== knownSave && !window.confirm(`'${name}' was saved from another tab at ` +
139
+ `${new Date(stored.updatedAt).toLocaleTimeString()}. Replace that save with this tab's files?`))) {
140
+ if (before === undefined) delete dataset.sessionStatus; else dataset.sessionStatus = before;
141
+ return null;
142
+ }
143
+ showStatus(`Saving ${name}…`, true);
125
144
  indicators.hold("session", true);
126
145
  update(`Saving ${name}…`);
127
146
  button.disabled = nameField.disabled = saveButton.disabled = true;
@@ -134,20 +153,20 @@ export function mountSessionSave({ mount, keyboard, showStatus, site, indicators
134
153
  };
135
154
  const encoded = await encodeSessionStream(capture);
136
155
  dataset.sessionStatus = "storing";
137
- const encodedSize = encoded.bytes.byteLength;
138
- try {
139
- await saveStoredSession({
140
- name,
141
- formatVersion: DOLLY_SESSION_FORMAT_VERSION,
142
- buildId: DOLLY_BUILD_ID,
143
- image: base.image,
144
- imageIdentity: base.identity,
145
- ...(base.custom ? { customImage: base.custom } : {}),
146
- updatedAt: Date.now(),
147
- encoding: encoded.encoding,
148
- bytes: encoded.bytes,
149
- });
150
- } finally { encoded.bytes.transfer(0); }
156
+ const encodedSize = encoded.bytes.size, updatedAt = Date.now();
157
+ await saveStoredSession({
158
+ name,
159
+ formatVersion: DOLLY_SESSION_FORMAT_VERSION,
160
+ buildId: DOLLY_IMAGE_BUILD_ID,
161
+ image: base.image,
162
+ imageIdentity: base.identity,
163
+ ...(base.custom ? { customImage: base.custom } : {}),
164
+ updatedAt,
165
+ encoding: encoded.encoding,
166
+ bytes: encoded.bytes,
167
+ });
168
+ knownSave = updatedAt;
169
+ unsaved = false;
151
170
  sessionName = name;
152
171
  lastSave = new Date();
153
172
  dataset.session = name;
@@ -161,10 +180,16 @@ export function mountSessionSave({ mount, keyboard, showStatus, site, indicators
161
180
  showStatus(`Saved ${name} locally${site ? " · /sessions lists your saves" : ""}`);
162
181
  indicators.hold("session", false);
163
182
  update();
183
+ // The site's runtime is kept beside the save, which then opens without a
184
+ // network (site/offline.mjs); the save does not wait for it.
185
+ if (site) void keepRuntime(site).then(() => "kept", () => "failed").then(state => { dataset.sessionOffline = state; });
164
186
  return name;
165
187
  })().catch((error) => {
166
188
  dataset.sessionStatus = "failed";
167
- saveError = error instanceof Error ? error.message : String(error);
189
+ // Chrome's error for a full store has no text.
190
+ saveError = error?.name === "QuotaExceededError"
191
+ ? "this browser has no room left for the site. Delete saves or clear downloads on the sessions page; the previous save is unchanged"
192
+ : error instanceof Error ? error.message : String(error);
168
193
  showStatus(`Save failed: ${saveError}`, true);
169
194
  indicators.hold("session", true);
170
195
  update(`Save failed: ${saveError}`);
@@ -180,6 +205,7 @@ export function mountSessionSave({ mount, keyboard, showStatus, site, indicators
180
205
  return {
181
206
  get name() { return sessionName; },
182
207
  save,
208
+ changed(value) { changed = value; unsaved ||= value; },
183
209
  // The dialog and the Save button keep their keys; Ctrl+Shift+S saves, the
184
210
  // first time through the dialog.
185
211
  claimsKey(event) {
@@ -195,17 +221,16 @@ export function mountSessionSave({ mount, keyboard, showStatus, site, indicators
195
221
  return true;
196
222
  },
197
223
  // custom is the running custom image record (Dollyfile, artifact and
198
- // what the tab inherited); verifyBase, given, checks before a save that
199
- // the base is still the one the session restores onto (the site's routes,
200
- // src/site.mjs). The identity names that base.
201
- entryStarted({ image, custom, verifyBase }) {
202
- base = { image, custom, verifyBase,
224
+ // what the tab inherited); the identity names the image the save was made on.
225
+ entryStarted({ image, custom }) {
226
+ base = { image, custom,
203
227
  identity: custom ? customSessionIdentity(custom) : sessionImageIdentity(DOLLY_IMAGES, image) };
204
228
  if (restored?.recovering) {
205
229
  dataset.sessionStatus = "recovered";
206
230
  showStatus(`Recovered files in /workspace/recovered-${restored.name}. Ctrl+Shift+S saves this as a new session.`, true);
207
231
  } else if (restored) {
208
232
  sessionName = restored.name;
233
+ knownSave = restored.updatedAt;
209
234
  dataset.session = restored.name;
210
235
  dataset.sessionStatus = "restored";
211
236
  }
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "stack",
3
3
  "version": 0,
4
+ "build": true,
4
5
  "gives": "recursion deeper than one browser stack",
5
6
  "dependencies": ["runtime@0"],
6
7
  "phase": "kernel",
@@ -7,7 +7,7 @@ export function worker({ get, runtimeBase }) {
7
7
  return {
8
8
  async start() {
9
9
  const response = await fetch(new URL("dist/dolly-process-stack.mjs", runtimeBase), {
10
- cache: "no-store", credentials: "same-origin", redirect: "error",
10
+ credentials: "same-origin", redirect: "error",
11
11
  });
12
12
  if (!response.ok) throw new Error(`Dolly dolly-process-stack.mjs returned HTTP ${response.status}`);
13
13
  get("runtime").serveInProcess("stack@0", {
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "threads",
3
3
  "version": 0,
4
+ "build": true,
4
5
  "gives": "threads, a Worker each: 32 a process, 64 in all",
5
6
  "dependencies": ["runtime@0"],
7
+ "phase": "kernel",
6
8
  "imports": [],
7
9
  "host": "threads.mjs",
8
10
  "contracts": ["dolly-threads-supervisor-0.wat"],
9
11
  "process": ["dolly-threads-0.wat"],
10
12
  "headers": ["threads.h", "threads-abi.h"],
11
13
  "kernel": ["kernel.c"],
12
- "client": ["client.c"]
14
+ "client": [],
15
+ "ccExports": { "pthread": ["dolly_thread_start"] },
16
+ "seed": "seed.sh",
17
+ "package": "libdolly-threads"
13
18
  }
@@ -1,22 +1,41 @@
1
- import { sameWasmType } from "../../src/wasm-interface.mjs";
1
+ import { parseWasmInterface, sameWasmType } from "../../page/wasm-interface.mjs";
2
+ import { DOLLY_THREAD_EXIT, DOLLY_THREAD_SPAWN } from "./abi.mjs";
2
3
  export { DOLLY_THREADS_ABI_DIGEST as digest } from "./abi.mjs";
3
4
 
4
- // An executable that links the threads client (every -pthread program) is
5
- // threaded; its requirement's digest is checked with the others.
6
- export function validateThreadProfile(parsed, requirements, schema) {
7
- if (!requirements.has("threads@0")) return false;
8
- const expected = schema.exports.find(entry => entry.name === "dolly_thread_start");
9
- const actual = parsed.exports.find(entry => entry.name === expected.name);
10
- if (!actual || !sameWasmType(actual.type, expected.type))
11
- throw new TypeError("a program using threads@0 must export dolly_thread_start(i32, i64) -> i64; build C and C++ with -pthread");
12
- return true;
13
- }
14
-
15
- export function worker({ get }) {
5
+ // The supervisor runs a process on several Workers through this provider
6
+ // (runtime.mjs setThreadProvider): which executables are threaded, the
7
+ // operations that start and end a thread, the kernel's thread bookkeeping
8
+ // (dolly-threads-supervisor-0.wat) and the Workers themselves.
9
+ export function worker({ get, runtimeBase }) {
10
+ let kernel, schema;
16
11
  // Trusted resource limits, independent of kernel bookkeeping or guest claims.
17
12
  // Retired threads retain their join result in Wasm, not a Worker or host slot.
18
13
  const workers = new Map();
19
14
  const provider = {
15
+ async start(context) {
16
+ ({ kernel } = context);
17
+ const response = await fetch(new URL("dist/dolly-threads-0.wasm", runtimeBase),
18
+ { cache: "no-store", credentials: "same-origin", redirect: "error" });
19
+ if (!response.ok) throw new Error(`Dolly dolly-threads-0.wasm returned HTTP ${response.status}`);
20
+ schema = parseWasmInterface(await response.arrayBuffer(), "dolly-threads-0");
21
+ },
22
+ // An executable that links the threads client (every -pthread program) is
23
+ // threaded; its requirement's digest is checked with the others.
24
+ threaded(parsed, requirements) {
25
+ if (!requirements.has("threads@0")) return false;
26
+ const expected = schema.exports.find(entry => entry.name === "dolly_thread_start");
27
+ const actual = parsed.exports.find(entry => entry.name === expected.name);
28
+ if (!actual || !sameWasmType(actual.type, expected.type))
29
+ throw new TypeError("a program using threads@0 must export dolly_thread_start(i32, i64) -> i64; build C and C++ with -pthread");
30
+ return true;
31
+ },
32
+ operations: { spawn: DOLLY_THREAD_SPAWN, exit: DOLLY_THREAD_EXIT },
33
+ attach: pid => kernel.dolly_threads_attach(pid),
34
+ // Through the generic call path, as the supervisor calls dispatch: Firefox
35
+ // can enter a direct Wasm call twice.
36
+ dispatch: request => Reflect.apply(kernel.dolly_threads_dispatch, kernel, request),
37
+ unstarted: (pid, tid) => kernel.dolly_threads_unstarted(pid, tid),
38
+ retired: (pid, tid, result) => kernel.dolly_threads_retired(pid, tid, result),
20
39
  available(pid) {
21
40
  return workers.size < 64 && [...workers.values()].filter(owner => owner === pid).length < 32;
22
41
  },
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
- "version": "0.1.5",
2
+ "version": "0.1.6",
3
3
  "type": "module",
4
4
  "name": "@daugasauron/dolly",
5
5
  "license": "MIT",
6
6
  "exports": {
7
- ".": "./src/browser.mjs"
7
+ ".": "./page/browser.mjs"
8
8
  },
9
9
  "description": "Dolly's runtime: a POSIX-like userspace for wasm64 in a browser tab. Run images with boot().",
10
10
  "repository": {
@@ -10,22 +10,33 @@ import * as processConstants from "./process-constants.mjs";
10
10
  // the status line, which lie in boot()'s element and style nothing else of the
11
11
  // page. A host module brings the look of what it adds; the page, the boot log's
12
12
  // font (the site's is Iosevka, dist/IosevkaTerm-SemiBold.woff2).
13
- const style = `
13
+ const pageStyle = new CSSStyleSheet();
14
+ pageStyle.replaceSync(`
14
15
  .dolly-terminal { position: relative; overflow: hidden; outline: none; caret-color: transparent;
15
- display: flex; flex-direction: column; }
16
- .dolly-terminal #display { display: block; width: 100%; flex: 1 1 0; min-height: 0; cursor: text; touch-action: none; }
16
+ display: flex; flex-direction: column;
17
+ /* Dolly's own UI in the embedding's colours (docs/embedding.md), unset Dolly's: the three, the
18
+ terminal's bright black for what is quieter, and mixes of them for raised and sunken parts. */
19
+ --dolly-ui-background: var(--dolly-background, #262626); --dolly-ui-color: var(--dolly-color, #e8e3d7);
20
+ --dolly-ui-accent: var(--dolly-accent, #f2d45c); --dolly-ui-quiet: var(--dolly-display-palette-8, #77736c);
21
+ --dolly-ui-raised: color-mix(in srgb, var(--dolly-ui-color) 7%, var(--dolly-ui-background));
22
+ --dolly-ui-sunken: color-mix(in srgb, var(--dolly-ui-background) 66%, black);
23
+ --dolly-ui-alert: #e98773; }
24
+ .dolly-terminal #display { display: block; width: 100%; flex: 1 1 0; min-height: 0; cursor: text; touch-action: none;
25
+ object-fit: contain; }
17
26
  .dolly-terminal #display[hidden] { display: none; }
18
27
  .dolly-terminal #keyboard { position: fixed; left: -10000px; top: 0; width: 1px; height: 1px; opacity: 0; caret-color: transparent; }
19
28
  .dolly-terminal #bootstrap-log { position: absolute; inset: 0; z-index: 1; margin: 0; padding: 1rem 1.15rem; overflow: auto;
20
- white-space: pre-wrap; color: #e8e3d7; background: #262626; font: 600 20px/1.35 "Dolly IosevkaTerm SemiBold", monospace;
21
- scrollbar-width: thin; scrollbar-color: #f2d45c #262626; }
29
+ white-space: pre-wrap; color: var(--dolly-ui-color); background: var(--dolly-ui-background);
30
+ font: 600 20px/1.35 "Dolly IosevkaTerm SemiBold", monospace;
31
+ scrollbar-width: thin; scrollbar-color: var(--dolly-ui-accent) var(--dolly-ui-background); }
22
32
  .dolly-terminal #bootstrap-log[hidden] { display: none; }
23
33
  .dolly-terminal #session-status { position: absolute; right: 1rem; top: 1rem; z-index: 5; max-width: calc(100% - 2rem);
24
- padding: 0.5rem 0.75rem; color: #e8e3d7; background: #262626; border: 1px solid #77736c; font-size: 14px; }
34
+ padding: 0.5rem 0.75rem; color: var(--dolly-ui-color); background: var(--dolly-ui-background);
35
+ border: 1px solid var(--dolly-ui-quiet); font-size: 14px; }
25
36
  .dolly-terminal #session-status[hidden] { display: none; }
26
37
  html[data-indicators="hidden"] .page-indicator { display: none; }
27
38
  @media (max-width: 640px) { .dolly-terminal #bootstrap-log { padding: 0.65rem 0.75rem; } }
28
- `;
39
+ `);
29
40
 
30
41
  function element(tag, properties, attributes = {}) {
31
42
  const node = Object.assign(document.createElement(tag), properties);
@@ -63,7 +74,7 @@ export function boot(mount, { images, image, modules, site, http = {}, files = {
63
74
  // enables (by default the required ones), which a build alone may set;
64
75
  // - configuration, inherited: the modules' configuration, and what http@0
65
76
  // inherits from the tab that opened this one;
66
- // - worker: the runtime Worker's URL, by default runtime-worker.mjs;
77
+ // - worker: the runtime Worker's URL, by default dist/dolly-runtime-worker.mjs (runtime-worker.mjs bundled);
67
78
  // - start(host, report): before the Worker starts, the fields it adds to the
68
79
  // configure message ({ message, transfers }), or null once an image without
69
80
  // ENTRY is built;
@@ -71,9 +82,8 @@ export function boot(mount, { images, image, modules, site, http = {}, files = {
71
82
  // - running(host): the context host modules' entryStarted is given beside
72
83
  // `image`, and members of the page's handle.
73
84
  export async function run(mount, { images, modules, site, http }, route) {
74
- if (!document.querySelector("style[data-dolly]")) {
75
- document.head.append(element("style", { textContent: style }, { "data-dolly": "" }));
76
- }
85
+ // Adopted once, which a page's style-src without 'unsafe-inline' allows.
86
+ if (!document.adoptedStyleSheets.includes(pageStyle)) document.adoptedStyleSheets = [...document.adoptedStyleSheets, pageStyle];
77
87
  const canvas = element("canvas", { id: "display", hidden: true }, { "aria-label": "Dolly sandbox framebuffer" });
78
88
  const keyboard = element("textarea", { id: "keyboard" }, { "aria-label": "Dolly keyboard input",
79
89
  autocomplete: "off", autocapitalize: "off", spellcheck: "false" });
@@ -118,9 +128,9 @@ export async function run(mount, { images, modules, site, http }, route) {
118
128
  : `exited with status ${code}`;
119
129
  const notice = mount.appendChild(element("div", { id: "image-ended" }, { role: "alert" }));
120
130
  notice.style.cssText = "position:absolute;left:0;right:0;bottom:0;z-index:9;padding:0.6rem 1rem;" +
121
- "color:#e8e3d7;background:#262626;border-top:1px solid #f2d45c;font-size:16px";
131
+ "color:var(--dolly-ui-color);background:var(--dolly-ui-background);border-top:1px solid var(--dolly-ui-accent);font-size:16px";
122
132
  const links = [{ text: "Reload to start again", href: location.href }, ...offers].map(({ text, href }) =>
123
- Object.assign(document.createElement("a"), { textContent: text, href, style: "color:#f2d45c;margin-left:1.5ch" }));
133
+ Object.assign(document.createElement("a"), { textContent: text, href, style: "color:var(--dolly-ui-accent);margin-left:1.5ch" }));
124
134
  notice.append(`This image has ended: its program ${how}.`, ...links);
125
135
  // The notice takes the element's bottom strip; the last frame and the log keep the rest.
126
136
  Object.assign(canvas.style, { flex: "none", height: `calc(100% - ${notice.offsetHeight}px)`, objectFit: "contain",
@@ -147,6 +157,19 @@ export async function run(mount, { images, modules, site, http }, route) {
147
157
  const registry = await hostRegistry(modules);
148
158
  const selected = await route(registry);
149
159
  const { image } = selected;
160
+ // The Worker loads its code while the page starts the host modules; it waits for configure.
161
+ runtimeWorker = new Worker(selected.worker ?? new URL("../dist/dolly-runtime-worker.mjs", import.meta.url), {
162
+ type: "module",
163
+ name: "dolly-runtime",
164
+ });
165
+ // An uncaught failure in the Worker, a kernel trap outside a system call
166
+ // among them, is the runtime's end: the page says so.
167
+ runtimeWorker.addEventListener("error", event => {
168
+ const message = event.message || "the runtime Worker failed";
169
+ if (rejectReady) rejectReady(new Error(message)); else fatal(message);
170
+ });
171
+ runtimeWorker.postMessage({ type: "prepare", images: imageBase.href, modules: registry.urls,
172
+ hostModules: selected.enabled ?? selected.required });
150
173
  appendBootstrap(`DOLLY / ${image.toUpperCase()} / ${selected.label}\n\n`);
151
174
  host = await registry.createHost("browser", selected.enabled ?? selected.required, {
152
175
  send: (message, transfers = []) => runtimeWorker.postMessage(message, transfers),
@@ -160,19 +183,10 @@ export async function run(mount, { images, modules, site, http }, route) {
160
183
  if (started === null) {
161
184
  document.documentElement.dataset.dollyStatus = "built";
162
185
  host.dispose();
186
+ runtimeWorker.terminate();
163
187
  return null;
164
188
  }
165
189
 
166
- runtimeWorker = new Worker(selected.worker ?? new URL("./runtime-worker.mjs", import.meta.url), {
167
- type: "module",
168
- name: "dolly-runtime",
169
- });
170
- // An uncaught failure in the Worker, a kernel trap outside a system call
171
- // among them, is the runtime's end: the page says so.
172
- runtimeWorker.addEventListener("error", event => {
173
- const message = event.message || "the runtime Worker failed";
174
- if (rejectReady) rejectReady(new Error(message)); else fatal(message);
175
- });
176
190
  runtimeWorker.addEventListener("message", (event) => {
177
191
  const message = event.data;
178
192
  void host.handle(message).catch(error => fatal(error.message));
@@ -0,0 +1,8 @@
1
+ // The modules the runtime's build writes beside it (scripts/build.sh): its identities and
2
+ // the digests and imports of the contracts it was built against. The page, its Workers
3
+ // and the host modules import them from here alone; an image directory's catalog is
4
+ // data, which images.mjs loads.
5
+ export { DOLLY_BUILD_ID } from "../dist/dolly-build-id.mjs";
6
+ export { DOLLY_IMAGE_BUILD_ID } from "../dist/dolly-image-build-id.mjs";
7
+ export { DOLLY_PROCESS_ABI_DIGEST } from "../dist/dolly-process-abi.mjs";
8
+ export { DOLLY_KERNEL_PLUGIN_ABI_DIGEST, DOLLY_KERNEL_PLUGIN_IMPORTS } from "../dist/dolly-kernel-plugin-abi.mjs";
@@ -1,22 +1,26 @@
1
- // Shared by packaging and browser admission; empty arguments are valid.
1
+ import { DOLLY_ENTRY_VERSION, DOLLY_ENTRY_HEADER_BYTES, DOLLY_ENTRY_MAX_ARGUMENTS, DOLLY_ENTRY_ARGUMENT_MAX_BYTES,
2
+ DOLLY_ENTRY_MAX_BYTES } from "../host/runtime/abi.mjs";
3
+
4
+ // /etc/dolly/entry, as abi/dolly-image-0.wat lays it out. Shared by packaging and
5
+ // browser admission; empty arguments are valid.
2
6
  export function decodeImageEntry(bytes) {
3
- if (!(bytes instanceof Uint8Array) || bytes.byteLength < 16 || bytes.byteLength > 64 * 1024) {
7
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength < DOLLY_ENTRY_HEADER_BYTES || bytes.byteLength > DOLLY_ENTRY_MAX_BYTES) {
4
8
  throw new TypeError("invalid image ENTRY record");
5
9
  }
6
10
  const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
7
11
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
8
- if (decoder.decode(bytes.subarray(0, 8)) !== "DOLLYENT" || view.getUint32(8, true) !== 1) {
12
+ if (decoder.decode(bytes.subarray(0, 8)) !== "DOLLYENT" || view.getUint32(8, true) !== DOLLY_ENTRY_VERSION) {
9
13
  throw new TypeError("invalid image ENTRY version");
10
14
  }
11
15
  const count = view.getUint32(12, true);
12
- if (count === 0 || count > 256) throw new TypeError("invalid image ENTRY count");
13
- let offset = 16;
16
+ if (count === 0 || count > DOLLY_ENTRY_MAX_ARGUMENTS) throw new TypeError("invalid image ENTRY count");
17
+ let offset = DOLLY_ENTRY_HEADER_BYTES;
14
18
  const arguments_ = [];
15
19
  for (let index = 0; index < count; ++index) {
16
20
  if (offset > bytes.byteLength - 4) throw new TypeError("truncated image ENTRY");
17
21
  const size = view.getUint32(offset, true);
18
22
  offset += 4;
19
- if (size > 4096 || offset > bytes.byteLength - size) throw new TypeError("invalid image ENTRY argument");
23
+ if (size > DOLLY_ENTRY_ARGUMENT_MAX_BYTES || offset > bytes.byteLength - size) throw new TypeError("invalid image ENTRY argument");
20
24
  const value = decoder.decode(bytes.subarray(offset, offset + size));
21
25
  if (value.includes("\0")) throw new TypeError("image ENTRY has a NUL argument");
22
26
  arguments_.push(value);
@@ -1,3 +1,5 @@
1
+ import { Sha256 } from "./sha256.mjs";
2
+
1
3
  // Recipe pins identify instructions; artifact digests identify their results.
2
4
  export function imageInputs(artifacts = []) {
3
5
  if (!Array.isArray(artifacts) || artifacts.length > 256) throw new Error("invalid image inputs");
@@ -18,3 +20,12 @@ export function imageInputs(artifacts = []) {
18
20
  export function imageInputsMatch(actual, expected) {
19
21
  return JSON.stringify(imageInputs(actual)) === JSON.stringify(imageInputs(expected));
20
22
  }
23
+
24
+ // The catalog's digest of an image's recipe records (its metadata's recipes),
25
+ // one line each, "SHA256 BYTES PATH\n", in their order: a start checks the
26
+ // records against the catalog without the catalog repeating them.
27
+ export function recipesDigest(recipes) {
28
+ const hash = new Sha256();
29
+ hash.update(new TextEncoder().encode(recipes.map(({ sha256, byteLength, sourcePath }) => `${sha256} ${byteLength} ${sourcePath}\n`).join("")));
30
+ return hash.hex();
31
+ }
@@ -5,7 +5,7 @@
5
5
  // Dollyfile-example.txt is never a recipe.
6
6
  export const validName = name => /^[a-z][a-z0-9]*(?:-[a-z0-9]+|\.[0-9]+)*$/.test(name) && name.length <= 32;
7
7
 
8
- // No image retains scratch space.
8
+ // No image retains scratch space, /tmp.
9
9
  export function unretainedPath(value) {
10
- return ["/tmp", "/workspace"].some((prefix) => value === prefix || value.startsWith(`${prefix}/`));
10
+ return value === "/tmp" || value.startsWith("/tmp/");
11
11
  }
@@ -1,4 +1,4 @@
1
- import { DOLLY_KERNEL_PLUGIN_ABI_DIGEST, DOLLY_KERNEL_PLUGIN_IMPORTS } from "../dist/dolly-kernel-plugin-abi.mjs";
1
+ import { DOLLY_KERNEL_PLUGIN_ABI_DIGEST, DOLLY_KERNEL_PLUGIN_IMPORTS } from "./generated.mjs";
2
2
  import { hex } from "./static-asset.mjs";
3
3
  import { Reader } from "./wasm-interface.mjs";
4
4
 
@@ -7,7 +7,7 @@
7
7
  // being read on the origin, what the records of this runtime's images do not
8
8
  // name is removed. Only the page and its Workers write here, from an image's
9
9
  // trusted metadata; nothing of the guest reaches it.
10
- import { DOLLY_IMAGE_BUILD_ID } from "../dist/dolly-image-build-id.mjs";
10
+ import { DOLLY_IMAGE_BUILD_ID } from "./generated.mjs";
11
11
  import { imageBase, imageDefinition } from "./images.mjs";
12
12
  import { sha256 } from "./static-asset.mjs";
13
13