@daugasauron/dolly 0.1.5-rc.0 → 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.
- package/README.md +51 -78
- package/coi-serviceworker.js +23 -8
- package/dist/dolly-build-id.mjs +1 -1
- package/dist/dolly-image-build-id.mjs +1 -1
- package/dist/dolly-process-0.wasm +0 -0
- package/dist/dolly-process-abi.mjs +1 -1
- package/dist/dolly-process-dso.mjs +19 -11
- package/dist/dolly-process-worker.mjs +88 -33
- package/dist/dolly-runtime-worker.mjs +1544 -0
- package/dist/dolly.wasm +0 -0
- package/host/dso/dso.mjs +2 -2
- package/host/dso/module.json +4 -1
- package/host/http/abi.mjs +3 -1
- package/host/http/broker.mjs +17 -7
- package/host/http/http.mjs +19 -5
- package/host/http/module.json +3 -1
- package/host/http/policy.mjs +13 -6
- package/host/http/refusals.mjs +77 -0
- package/host/input/abi.mjs +1 -1
- package/host/modules.mjs +64 -23
- package/host/nena/module.json +2 -1
- package/host/nena/nena.mjs +2 -0
- package/host/packages/abi.mjs +27 -0
- package/host/packages/module.json +6 -4
- package/host/packages/packages.mjs +23 -8
- package/host/packages/service.mjs +46 -20
- package/host/packages/transport.mjs +123 -0
- package/host/packages/worker.mjs +13 -9
- package/host/requirements.mjs +1 -1
- package/host/runtime/abi.mjs +32 -0
- package/host/runtime/lease-bridge.mjs +13 -8
- package/host/runtime/module.json +3 -3
- package/host/runtime/runtime.mjs +3 -3
- package/host/snapshot/abi.mjs +2 -1
- package/host/snapshot/module.json +1 -1
- package/host/snapshot/snapshot.mjs +80 -57
- package/host/snapshot/transport.mjs +10 -3
- package/host/snapshot/ui.mjs +64 -39
- package/host/stack/module.json +1 -0
- package/host/stack/stack.mjs +1 -1
- package/host/threads/module.json +6 -1
- package/host/threads/threads.mjs +32 -13
- package/package.json +2 -2
- package/{src → page}/browser.mjs +37 -23
- package/page/generated.mjs +8 -0
- package/{src → page}/image-entry.mjs +10 -6
- package/{src → page}/image-inputs.mjs +11 -0
- package/{src → page}/image-names.mjs +2 -2
- package/{src → page}/kernel-plugin.mjs +1 -1
- package/{src → page}/pack-store.mjs +1 -1
- package/{src → page}/packaged-snapshot.mjs +56 -46
- package/{src → page}/process-constants.mjs +72 -15
- package/{src → page}/process-supervisor.mjs +155 -227
- package/{src → page}/snapshot-records.mjs +63 -44
- package/{src → page}/static-asset.mjs +36 -20
- package/{src → page}/version.mjs +1 -1
- package/{src → page}/worker-image.mjs +20 -9
- package/{src → site}/build-worker.mjs +36 -25
- package/{src → site}/custom-image.mjs +2 -10
- package/{src → site}/dollyfile-graph.mjs +1 -1
- package/{src → site}/dollyfile-view.mjs +1 -1
- package/{src → site}/image-artifact.mjs +9 -8
- package/{src → site}/image-build.mjs +4 -4
- package/{src → site}/image-builder.mjs +5 -4
- package/site/offline.mjs +53 -0
- package/{src → site}/session-store.mjs +47 -38
- package/src/runtime-worker.mjs +0 -57
- /package/{src → page}/build-log.mjs +0 -0
- /package/{src → page}/images.mjs +0 -0
- /package/{src → page}/page-chords.mjs +0 -0
- /package/{src → page}/page-indicators.mjs +0 -0
- /package/{src → page}/process-abi.mjs +0 -0
- /package/{src → page}/sha256.mjs +0 -0
- /package/{src → page}/terminal-text.mjs +0 -0
- /package/{src → page}/wasm-interface.mjs +0 -0
- /package/{src → site}/alphabetical.mjs +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DOLLY_SESSION_MAX_BYTES } from "../../
|
|
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.
|
|
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.
|
|
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
|
}
|
package/host/snapshot/ui.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { DOLLY_IMAGES } from "../../
|
|
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 "../../
|
|
12
|
+
} from "../../site/session-store.mjs";
|
|
13
|
+
import { keepRuntime } from "../../site/offline.mjs";
|
|
13
14
|
|
|
14
|
-
|
|
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:
|
|
17
|
-
border: 1px solid
|
|
18
|
-
#session-open[data-failed] { border-color:
|
|
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:
|
|
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:
|
|
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:
|
|
29
|
-
#session-dialog a { color:
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
107
|
-
//
|
|
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
|
-
|
|
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.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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);
|
|
199
|
-
|
|
200
|
-
|
|
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
|
}
|
package/host/stack/module.json
CHANGED
package/host/stack/stack.mjs
CHANGED
|
@@ -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
|
-
|
|
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", {
|
package/host/threads/module.json
CHANGED
|
@@ -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": [
|
|
14
|
+
"client": [],
|
|
15
|
+
"ccExports": { "pthread": ["dolly_thread_start"] },
|
|
16
|
+
"seed": "seed.sh",
|
|
17
|
+
"package": "libdolly-threads"
|
|
13
18
|
}
|
package/host/threads/threads.mjs
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
import { sameWasmType } from "../../
|
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
2
|
+
"version": "0.1.6",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"name": "@daugasauron/dolly",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./
|
|
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": {
|
package/{src → page}/browser.mjs
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
21
|
-
|
|
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:
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 <
|
|
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) !==
|
|
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 >
|
|
13
|
-
let offset =
|
|
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 >
|
|
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
|
|
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 "
|
|
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 "
|
|
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
|
|