@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.
- 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,6 +1,11 @@
|
|
|
1
1
|
import { unretainedPath } from "./image-names.mjs";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { DOLLY_SNAPSHOT_VERSION, DOLLY_SNAPSHOT_HEADER_BYTES as HEADER_BYTES, DOLLY_SNAPSHOT_RECORD_HEADER_BYTES as RECORD_HEADER_BYTES,
|
|
3
|
+
DOLLY_SNAPSHOT_RECORD_DIRECTORY as DIRECTORY, DOLLY_SNAPSHOT_RECORD_FILE as FILE, DOLLY_SNAPSHOT_RECORD_SYMLINK as SYMLINK,
|
|
4
|
+
DOLLY_SNAPSHOT_MAX_PATHS, DOLLY_SNAPSHOT_MAX_MEBIBYTES, DOLLY_SNAPSHOT_PATH_MIN_BYTES, DOLLY_SNAPSHOT_PATH_MAX_BYTES,
|
|
5
|
+
DOLLY_SNAPSHOT_SYMLINK_MIN_BYTES, DOLLY_SNAPSHOT_SYMLINK_MAX_BYTES } from "../host/runtime/abi.mjs";
|
|
6
|
+
// A snapshot part's layout and limits are abi/dolly-image-0.wat's.
|
|
7
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }), encoder = new TextEncoder();
|
|
8
|
+
export const MAX_SNAPSHOT_BYTES = DOLLY_SNAPSHOT_MAX_MEBIBYTES * 1024 * 1024;
|
|
4
9
|
// Packs average 2 MiB or more (scripts/share-pages-snapshots.mjs): an image of
|
|
5
10
|
// MAX_SNAPSHOT_BYTES needs about 1,024, which this bound leaves room for.
|
|
6
11
|
const MAX_SNAPSHOT_PACKS = MAX_SNAPSHOT_BYTES / (1024 * 1024);
|
|
@@ -12,27 +17,46 @@ function compareBytes(left, right) {
|
|
|
12
17
|
return left.length - right.length;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
//
|
|
16
|
-
function
|
|
20
|
+
// A snapshot part's header: its record count.
|
|
21
|
+
export function decodeSnapshotHeader(bytes) {
|
|
17
22
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
18
|
-
if (bytes.length <
|
|
19
|
-
|
|
20
|
-
}
|
|
23
|
+
if (bytes.length < HEADER_BYTES || decoder.decode(bytes.subarray(0, 8)) !== "DOLLYSNP" ||
|
|
24
|
+
view.getUint32(8, true) !== DOLLY_SNAPSHOT_VERSION) throw new Error("invalid Dolly snapshot header");
|
|
21
25
|
const count = view.getUint32(12, true);
|
|
22
|
-
if (count === 0 || count >
|
|
26
|
+
if (count === 0 || count > DOLLY_SNAPSHOT_MAX_PATHS) throw new Error("invalid Dolly snapshot record count");
|
|
23
27
|
return count;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
// A record's
|
|
27
|
-
function decodeRecordHeader(bytes) {
|
|
30
|
+
// A record's header: its kind, path length and data size.
|
|
31
|
+
export function decodeRecordHeader(bytes) {
|
|
28
32
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
29
33
|
const kind = view.getUint32(0, true), pathLength = view.getUint32(4, true), size = view.getBigUint64(8, true);
|
|
30
|
-
if (kind <
|
|
31
|
-
size > BigInt(MAX_SNAPSHOT_BYTES) || (kind ===
|
|
32
|
-
(kind ===
|
|
34
|
+
if (kind < DIRECTORY || kind > SYMLINK || pathLength < DOLLY_SNAPSHOT_PATH_MIN_BYTES || pathLength > DOLLY_SNAPSHOT_PATH_MAX_BYTES ||
|
|
35
|
+
size > BigInt(MAX_SNAPSHOT_BYTES) || (kind === DIRECTORY && size !== 0n) ||
|
|
36
|
+
(kind === SYMLINK && (size < BigInt(DOLLY_SNAPSHOT_SYMLINK_MIN_BYTES) || size > BigInt(DOLLY_SNAPSHOT_SYMLINK_MAX_BYTES)))) {
|
|
37
|
+
throw new Error("invalid snapshot record size");
|
|
38
|
+
}
|
|
33
39
|
return { kind, pathLength, size: Number(size) };
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
export function snapshotHeader(count) {
|
|
43
|
+
const bytes = new Uint8Array(HEADER_BYTES), view = new DataView(bytes.buffer);
|
|
44
|
+
bytes.set(encoder.encode("DOLLYSNP"));
|
|
45
|
+
view.setUint32(8, DOLLY_SNAPSHOT_VERSION, true);
|
|
46
|
+
view.setUint32(12, count, true);
|
|
47
|
+
return bytes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A record's header and its encoded path PATH.
|
|
51
|
+
export function recordHeader(kind, path, size) {
|
|
52
|
+
const bytes = new Uint8Array(RECORD_HEADER_BYTES + path.length), view = new DataView(bytes.buffer);
|
|
53
|
+
view.setUint32(0, kind, true);
|
|
54
|
+
view.setUint32(4, path.length, true);
|
|
55
|
+
view.setBigUint64(8, BigInt(size), true);
|
|
56
|
+
bytes.set(path, RECORD_HEADER_BYTES);
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
// The path of PATHBYTES, valid and after PREVIOUS in byte order.
|
|
37
61
|
function decodePath(pathBytes, previous) {
|
|
38
62
|
const path = decoder.decode(pathBytes);
|
|
@@ -47,7 +71,7 @@ function checkParents(records) {
|
|
|
47
71
|
for (const path of records.keys()) {
|
|
48
72
|
for (let slash = path.indexOf("/", 1); slash !== -1; slash = path.indexOf("/", slash + 1)) {
|
|
49
73
|
const parent = records.get(path.slice(0, slash));
|
|
50
|
-
if (parent && parent.kind !==
|
|
74
|
+
if (parent && parent.kind !== DIRECTORY) throw new Error(`snapshot parent is not a directory: ${path}`);
|
|
51
75
|
}
|
|
52
76
|
}
|
|
53
77
|
}
|
|
@@ -55,20 +79,20 @@ function checkParents(records) {
|
|
|
55
79
|
export function decodeSnapshotRecords(input) {
|
|
56
80
|
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
57
81
|
if (bytes.length > MAX_SNAPSHOT_BYTES) throw new Error("invalid Dolly snapshot header");
|
|
58
|
-
const count =
|
|
82
|
+
const count = decodeSnapshotHeader(bytes.subarray(0, HEADER_BYTES));
|
|
59
83
|
const records = new Map();
|
|
60
|
-
let offset =
|
|
84
|
+
let offset = HEADER_BYTES, previous;
|
|
61
85
|
for (let index = 0; index < count; index += 1) {
|
|
62
|
-
if (offset > bytes.length -
|
|
63
|
-
const { kind, pathLength, size } = decodeRecordHeader(bytes.subarray(offset, offset +
|
|
64
|
-
offset +=
|
|
86
|
+
if (offset > bytes.length - RECORD_HEADER_BYTES) throw new Error("truncated snapshot record");
|
|
87
|
+
const { kind, pathLength, size } = decodeRecordHeader(bytes.subarray(offset, offset + RECORD_HEADER_BYTES));
|
|
88
|
+
offset += RECORD_HEADER_BYTES;
|
|
65
89
|
if (offset > bytes.length - pathLength - size) throw new Error("truncated snapshot record");
|
|
66
90
|
const pathBytes = bytes.subarray(offset, offset + pathLength);
|
|
67
91
|
const path = decodePath(pathBytes, previous);
|
|
68
92
|
previous = pathBytes;
|
|
69
93
|
offset += pathLength;
|
|
70
94
|
const data = bytes.subarray(offset, offset + size);
|
|
71
|
-
if (kind ===
|
|
95
|
+
if (kind === SYMLINK && data.includes(0)) throw new Error("invalid snapshot symlink");
|
|
72
96
|
records.set(path, { kind, data });
|
|
73
97
|
offset += size;
|
|
74
98
|
}
|
|
@@ -143,19 +167,19 @@ class PieceReader {
|
|
|
143
167
|
// snapshot's size.
|
|
144
168
|
export async function scanSnapshotRecords(pieces, keep) {
|
|
145
169
|
const reader = new PieceReader(pieces);
|
|
146
|
-
const count =
|
|
170
|
+
const count = decodeSnapshotHeader(await reader.take(HEADER_BYTES));
|
|
147
171
|
const records = new Map();
|
|
148
172
|
let previous;
|
|
149
173
|
for (let index = 0; index < count; index += 1) {
|
|
150
|
-
const { kind, pathLength, size } = decodeRecordHeader(await reader.take(
|
|
174
|
+
const { kind, pathLength, size } = decodeRecordHeader(await reader.take(RECORD_HEADER_BYTES));
|
|
151
175
|
const pathBytes = await reader.take(pathLength);
|
|
152
176
|
const path = decodePath(pathBytes, previous);
|
|
153
177
|
previous = pathBytes;
|
|
154
178
|
const offset = reader.position;
|
|
155
179
|
let data;
|
|
156
|
-
if (kind ===
|
|
180
|
+
if (kind === SYMLINK || (kind === FILE && keep(path))) {
|
|
157
181
|
data = await reader.take(size);
|
|
158
|
-
if (kind ===
|
|
182
|
+
if (kind === SYMLINK && data.includes(0)) throw new Error("invalid snapshot symlink");
|
|
159
183
|
} else await reader.skip(size);
|
|
160
184
|
records.set(path, { kind, data, offset, size });
|
|
161
185
|
}
|
|
@@ -165,35 +189,30 @@ export async function scanSnapshotRecords(pieces, keep) {
|
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
export function encodeSnapshotRecords(records) {
|
|
168
|
-
const encoder = new TextEncoder();
|
|
169
192
|
const entries = [...records].map(([path, record]) => ({ path: encoder.encode(path), ...record }))
|
|
170
193
|
.sort((left, right) => compareBytes(left.path, right.path));
|
|
171
|
-
const size = entries.reduce((sum, record) => sum +
|
|
172
|
-
if (size > MAX_SNAPSHOT_BYTES || entries.length === 0 || entries.length >
|
|
173
|
-
const bytes = new Uint8Array(size)
|
|
174
|
-
bytes.set(
|
|
175
|
-
|
|
176
|
-
view.setUint32(12, entries.length, true);
|
|
177
|
-
let offset = 16;
|
|
194
|
+
const size = entries.reduce((sum, record) => sum + RECORD_HEADER_BYTES + record.path.length + record.data.length, HEADER_BYTES);
|
|
195
|
+
if (size > MAX_SNAPSHOT_BYTES || entries.length === 0 || entries.length > DOLLY_SNAPSHOT_MAX_PATHS) throw new Error("snapshot exceeds record limits");
|
|
196
|
+
const bytes = new Uint8Array(size);
|
|
197
|
+
bytes.set(snapshotHeader(entries.length));
|
|
198
|
+
let offset = HEADER_BYTES;
|
|
178
199
|
for (const record of entries) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
bytes.set(record.data, offset + 16 + record.path.length);
|
|
184
|
-
offset += 16 + record.path.length + record.data.length;
|
|
200
|
+
const header = recordHeader(record.kind, record.path, record.data.length);
|
|
201
|
+
bytes.set(header, offset);
|
|
202
|
+
bytes.set(record.data, offset + header.length);
|
|
203
|
+
offset += header.length + record.data.length;
|
|
185
204
|
}
|
|
186
205
|
return bytes;
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
export function mergeSnapshotRecords(parts) {
|
|
190
209
|
const records = new Map();
|
|
191
|
-
let size =
|
|
210
|
+
let size = HEADER_BYTES;
|
|
192
211
|
for (const part of parts) {
|
|
193
212
|
for (const [path, record] of decodeSnapshotRecords(part)) {
|
|
194
213
|
if (records.has(path)) throw new Error(`duplicate packed snapshot path: ${path}`);
|
|
195
|
-
size +=
|
|
196
|
-
if (size > MAX_SNAPSHOT_BYTES || records.size >=
|
|
214
|
+
size += RECORD_HEADER_BYTES + encoder.encode(path).length + record.data.length;
|
|
215
|
+
if (size > MAX_SNAPSHOT_BYTES || records.size >= DOLLY_SNAPSHOT_MAX_PATHS) throw new Error("packed snapshot exceeds limits");
|
|
197
216
|
records.set(path, record);
|
|
198
217
|
}
|
|
199
218
|
}
|
|
@@ -205,15 +224,15 @@ export function mergeSnapshotRecords(parts) {
|
|
|
205
224
|
export function validateSnapshotPacks(metadata) {
|
|
206
225
|
if (!Array.isArray(metadata.packs) || metadata.packs.length === 0 || metadata.packs.length > MAX_SNAPSHOT_PACKS) throw new Error("invalid snapshot packs");
|
|
207
226
|
const hashes = new Set();
|
|
208
|
-
let size =
|
|
227
|
+
let size = HEADER_BYTES;
|
|
209
228
|
for (const pack of metadata.packs) {
|
|
210
229
|
if (!/^[0-9a-f]{64}$/.test(pack.sha256) || hashes.has(pack.sha256) ||
|
|
211
|
-
!Number.isSafeInteger(pack.byteLength) || pack.byteLength <
|
|
230
|
+
!Number.isSafeInteger(pack.byteLength) || pack.byteLength < HEADER_BYTES || pack.byteLength > MAX_SNAPSHOT_BYTES ||
|
|
212
231
|
!Number.isSafeInteger(pack.encodedByteLength) || pack.encodedByteLength <= 0 || pack.encodedByteLength > MAX_SNAPSHOT_BYTES) {
|
|
213
232
|
throw new Error("invalid snapshot pack descriptor");
|
|
214
233
|
}
|
|
215
234
|
hashes.add(pack.sha256);
|
|
216
|
-
size += pack.byteLength -
|
|
235
|
+
size += pack.byteLength - HEADER_BYTES;
|
|
217
236
|
if (size > MAX_SNAPSHOT_BYTES) throw new Error("packed snapshot exceeds size limit");
|
|
218
237
|
}
|
|
219
238
|
if (size !== metadata.byteLength) throw new Error("snapshot pack sizes do not match image");
|
|
@@ -61,31 +61,47 @@ export async function boundedAssetBody(response, limit, signal) {
|
|
|
61
61
|
// Pages before v0.1.5 read the X-Dolly-Parts header instead.
|
|
62
62
|
export const PARTS_MANIFEST_START = '{"byteLength":';
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
size += taken.length;
|
|
77
|
-
}
|
|
78
|
-
} finally {
|
|
79
|
-
// Not awaited: cancelling a copy settles only once the response is read too.
|
|
80
|
-
reader.cancel().catch(() => {});
|
|
64
|
+
// The first `length` bytes of a response's body, and a response whose body is
|
|
65
|
+
// all of it again, those bytes first. It reads the body itself: reading a
|
|
66
|
+
// copy of it and cancelling the copy cost the original its first bytes, in
|
|
67
|
+
// Firefox up to 151 and in Node, when other bodies were read meanwhile.
|
|
68
|
+
async function peeked(response, length) {
|
|
69
|
+
const reader = response.body.getReader(), chunks = [];
|
|
70
|
+
let size = 0, ended = false;
|
|
71
|
+
while (size < length) {
|
|
72
|
+
const next = await reader.read();
|
|
73
|
+
if (next.done) { ended = true; break; }
|
|
74
|
+
chunks.push(next.value);
|
|
75
|
+
size += next.value.length;
|
|
81
76
|
}
|
|
82
|
-
|
|
77
|
+
const head = new Uint8Array(Math.min(size, length));
|
|
78
|
+
for (let offset = 0, index = 0; offset < head.length; ++index) {
|
|
79
|
+
const taken = chunks[index].subarray(0, head.length - offset);
|
|
80
|
+
head.set(taken, offset);
|
|
81
|
+
offset += taken.length;
|
|
82
|
+
}
|
|
83
|
+
const body = new ReadableStream({
|
|
84
|
+
start(controller) {
|
|
85
|
+
for (const chunk of chunks) controller.enqueue(chunk);
|
|
86
|
+
if (ended) controller.close();
|
|
87
|
+
},
|
|
88
|
+
async pull(controller) {
|
|
89
|
+
const next = await reader.read();
|
|
90
|
+
if (next.done) controller.close(); else controller.enqueue(next.value);
|
|
91
|
+
},
|
|
92
|
+
cancel: reason => reader.cancel(reason),
|
|
93
|
+
}, { highWaterMark: 0 });
|
|
94
|
+
const whole = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
95
|
+
Object.defineProperty(whole, "url", { value: response.url });
|
|
96
|
+
return { head, whole };
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
// Streams verified parts, holding at most one 20 MiB part per request. Each
|
|
86
100
|
// part matches the manifest hash before any of its bytes are delivered.
|
|
87
|
-
export async function decodeStaticAsset(
|
|
88
|
-
if (!
|
|
101
|
+
export async function decodeStaticAsset(given, target, init, maximumBytes, fetchRequest = globalThis.fetch.bind(globalThis)) {
|
|
102
|
+
if (given.status !== 200 || !given.body) return given;
|
|
103
|
+
const start = new TextEncoder().encode(PARTS_MANIFEST_START), { head, whole: response } = await peeked(given, start.length);
|
|
104
|
+
if (head.length !== start.length || head.some((byte, index) => byte !== start[index])) return response;
|
|
89
105
|
const url = new URL(target);
|
|
90
106
|
if ((init.method ?? "GET") !== "GET" || url.search || url.hash ||
|
|
91
107
|
url.username || url.password || !/^https?:$/.test(url.protocol)) throw new Error("invalid multipart asset request");
|
package/{src → page}/version.mjs
RENAMED
|
@@ -12,19 +12,29 @@ import { bootFiles } from "../host/runtime/boot-files.mjs";
|
|
|
12
12
|
const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
13
13
|
|
|
14
14
|
// Listen before the host registry loads its manifests: the page's configure
|
|
15
|
-
// message may arrive while that import is still pending.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
15
|
+
// message may arrive while that import is still pending. The page starts this
|
|
16
|
+
// Worker before its own host modules and configures it once they are up, which
|
|
17
|
+
// a route that builds first takes minutes to be.
|
|
18
|
+
const configured = new Promise(resolve => {
|
|
20
19
|
self.addEventListener("message", function configure(event) {
|
|
21
20
|
if (event.data?.type !== "configure") return;
|
|
22
21
|
self.removeEventListener("message", configure);
|
|
23
|
-
clearTimeout(timeout);
|
|
24
22
|
resolve(event.data);
|
|
25
23
|
});
|
|
26
24
|
});
|
|
27
|
-
|
|
25
|
+
// The page names its image directory and modules as it starts this Worker: their catalog,
|
|
26
|
+
// manifests and providers load while the page starts its own, and configure finds them loaded.
|
|
27
|
+
const registry = import("../host/modules.mjs");
|
|
28
|
+
self.addEventListener("message", function prepare(event) {
|
|
29
|
+
if (event.data?.type !== "prepare") return;
|
|
30
|
+
self.removeEventListener("message", prepare);
|
|
31
|
+
const { images, modules, hostModules } = event.data;
|
|
32
|
+
void (async () => {
|
|
33
|
+
await loadImages(images);
|
|
34
|
+
(await (await registry).hostRegistry(modules)).load(hostModules);
|
|
35
|
+
})().catch(() => {});
|
|
36
|
+
});
|
|
37
|
+
const { hostRegistry } = await registry;
|
|
28
38
|
|
|
29
39
|
// The runtime's own files: the kernel and the contracts beside this module.
|
|
30
40
|
const runtimeBase = new URL("../", import.meta.url);
|
|
@@ -78,7 +88,8 @@ export async function runImage(image) {
|
|
|
78
88
|
host = await (await hostRegistry(config.modules)).createHost("worker", config.hostModules, {
|
|
79
89
|
send: (message, transfers = []) => self.postMessage(message, transfers),
|
|
80
90
|
configuration: config.hostConfiguration,
|
|
81
|
-
|
|
91
|
+
// The image directory reaches the modules here: one the bundle holds is not theirs.
|
|
92
|
+
resources: { runtimeBase, images: new URL(config.images) },
|
|
82
93
|
});
|
|
83
94
|
const steps = await image(config, host);
|
|
84
95
|
stage("creating wasm64 userspace kernel...");
|
|
@@ -101,7 +112,7 @@ export async function runImage(image) {
|
|
|
101
112
|
const supervisor = restored.supervisor ?? await host.kernel.supervisor(kernel);
|
|
102
113
|
await steps.finish?.({ kernel, stage });
|
|
103
114
|
|
|
104
|
-
await host.imageRestored({ kernel, supervisor, stage, files, image: steps.name });
|
|
115
|
+
await host.imageRestored({ kernel, memory, supervisor, stage, files, image: steps.name });
|
|
105
116
|
if (kernel.dolly_bootstrap_environment() !== 0) throw new Error("Dolly image environment is invalid");
|
|
106
117
|
await host.start("image", { kernel, memory });
|
|
107
118
|
const runtimeImage = decoder.decode(files.read("/etc/dolly/image", 256));
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// The site's runtime Worker (worker-image.mjs): it builds an image from its
|
|
2
|
-
// recipe, from the
|
|
3
|
-
// it and runs it, telling the page its identity, or,
|
|
4
|
-
// hands the page the cached artifact (buildOnly); or it
|
|
5
|
-
// image the site built before. Images cross to and from Wasm
|
|
6
|
-
// at a time: the page holds them as Blobs, never in a JS heap.
|
|
7
|
-
import { DOLLY_IMAGE_BUILD_ID } from "../
|
|
8
|
-
import { imageBase, imageDefinition } from "
|
|
9
|
-
import { scanSnapshotRecords, MAX_SNAPSHOT_BYTES as snapshotSizeLimit } from "
|
|
2
|
+
// recipe, from the base image or the engine package the page hands it or from
|
|
3
|
+
// the compiler seed, caches it and runs it, telling the page its identity, or,
|
|
4
|
+
// for the image builder, hands the page the cached artifact (buildOnly); or it
|
|
5
|
+
// restores a custom image the site built before. Images cross to and from Wasm
|
|
6
|
+
// memory a piece at a time: the page holds them as Blobs, never in a JS heap.
|
|
7
|
+
import { DOLLY_IMAGE_BUILD_ID } from "../page/generated.mjs";
|
|
8
|
+
import { imageBase, imageDefinition } from "../page/images.mjs";
|
|
9
|
+
import { scanSnapshotRecords, MAX_SNAPSHOT_BYTES as snapshotSizeLimit } from "../page/snapshot-records.mjs";
|
|
10
10
|
import { describeImageArtifact, saveImageArtifact, sha256 } from "./image-artifact.mjs";
|
|
11
|
-
import { imageInputs } from "
|
|
12
|
-
import { inspectDollyfile, MAX_DOLLYFILE_BYTES, recipeRows } from "./dollyfile-view.mjs";
|
|
11
|
+
import { imageInputs } from "../page/image-inputs.mjs";
|
|
12
|
+
import { imageFileName, inspectDollyfile, MAX_DOLLYFILE_BYTES, recipeFileName, recipeRows } from "./dollyfile-view.mjs";
|
|
13
13
|
import { checkedCustomArtifact } from "./custom-image.mjs";
|
|
14
|
-
import { siteReference, decodeStaticAsset } from "
|
|
15
|
-
import { checkedMemoryRange, kernelArtifact, runImage } from "
|
|
14
|
+
import { siteReference, decodeStaticAsset } from "../page/static-asset.mjs";
|
|
15
|
+
import { checkedMemoryRange, kernelArtifact, runImage } from "../page/worker-image.mjs";
|
|
16
16
|
|
|
17
17
|
const encoder = new TextEncoder();
|
|
18
18
|
const PIECE = 8 * 1024 * 1024;
|
|
@@ -69,17 +69,21 @@ await runImage(async (config, host) => {
|
|
|
69
69
|
return range.size;
|
|
70
70
|
};
|
|
71
71
|
if (mode === "snapshot") {
|
|
72
|
-
|
|
72
|
+
// A saved session is the whole filesystem: it needs the recipe, not the image.
|
|
73
|
+
const artifact = config.customArtifact === undefined ? null : await checkedCustomArtifact(customSource, config.customArtifact);
|
|
73
74
|
config.customArtifact = undefined;
|
|
74
|
-
|
|
75
|
-
host.
|
|
75
|
+
const hosts = artifact?.hostRequirements ?? recipe.hostRequirements;
|
|
76
|
+
host.require(hosts);
|
|
77
|
+
await host.admit(hosts);
|
|
76
78
|
return {
|
|
77
79
|
name: image,
|
|
78
80
|
prepare({ files }) {
|
|
79
81
|
prepareRecipe(files);
|
|
80
|
-
files.write("/etc/dolly/image.manifest", `${artifact.manifest.join("\n")}\n`);
|
|
82
|
+
if (artifact) files.write("/etc/dolly/image.manifest", `${artifact.manifest.join("\n")}\n`);
|
|
81
83
|
},
|
|
82
84
|
async restore({ kernel, memory, stage }) {
|
|
85
|
+
if (await host.restoreFilesystem({ kernel, memory, stage })) return { status: 0 };
|
|
86
|
+
if (!artifact) throw new Error("invalid Dolly image selection");
|
|
83
87
|
stage("loading precompiled userspace snapshot...");
|
|
84
88
|
const size = await stageSnapshot(kernel, memory, artifact.bytes);
|
|
85
89
|
artifact.bytes = undefined;
|
|
@@ -93,6 +97,10 @@ await runImage(async (config, host) => {
|
|
|
93
97
|
// /bin/dollyfile executes the rows this page's parser compiles.
|
|
94
98
|
const rows = recipeRows(custom ? recipe : await catalogRecipe(definition), recipeSha256);
|
|
95
99
|
const baseReference = custom ? recipe.from : definition.artifacts.find(reference => reference.operation === "from");
|
|
100
|
+
// A root build that installs the recipe engine's package starts from that package's
|
|
101
|
+
// /bin/dollyfile, as a FROM build starts from its base's; any other from the compiler seed.
|
|
102
|
+
const engineReference = baseReference ? null : (custom ? recipe : definition).artifacts.find(reference =>
|
|
103
|
+
reference.operation === "install" && imageFileName(recipeFileName(reference.location)) === "dollyfile");
|
|
96
104
|
const artifacts = new Map();
|
|
97
105
|
if (config.artifacts !== undefined && (!Array.isArray(config.artifacts) || config.artifacts.length > 256)) {
|
|
98
106
|
throw new Error("invalid build artifacts");
|
|
@@ -108,16 +116,19 @@ await runImage(async (config, host) => {
|
|
|
108
116
|
// A build runs the engine and its tools with the build host's modules, and
|
|
109
117
|
// the programs of the images it reads with their modules: one the build host
|
|
110
118
|
// does not enable answers ENOSYS.
|
|
111
|
-
host.admit([...declared, ...host.enabled, ...[...artifacts.values()].flatMap(artifact => artifact.hostRequirements)]);
|
|
119
|
+
await host.admit([...declared, ...host.enabled, ...[...artifacts.values()].flatMap(artifact => artifact.hostRequirements)]);
|
|
112
120
|
const inputs = imageInputs([...artifacts.values()]);
|
|
113
121
|
const base = baseReference ? artifacts.get(baseReference.sha256) : null;
|
|
114
122
|
if (baseReference && !base) throw new Error("base image artifact was not provided");
|
|
123
|
+
const engine = engineReference ? artifacts.get(engineReference.sha256) : null;
|
|
124
|
+
if (engineReference && !engine) throw new Error("dollyfile package artifact was not provided");
|
|
125
|
+
const builder = base ?? engine, builderReference = baseReference ?? engineReference;
|
|
115
126
|
let seed = null, built = false;
|
|
116
127
|
return {
|
|
117
128
|
name: image,
|
|
118
129
|
async prepare({ files, stage }) {
|
|
119
|
-
// A root build starts from the compiler seed, a snapshot like any image.
|
|
120
|
-
if (!
|
|
130
|
+
// A root build without the engine starts from the compiler seed, a snapshot like any image.
|
|
131
|
+
if (!builder) {
|
|
121
132
|
stage("loading root compiler seed...");
|
|
122
133
|
// Static hosts may serve the seed as verified parts.
|
|
123
134
|
const url = kernelArtifact("dolly.data");
|
|
@@ -129,15 +140,15 @@ await runImage(async (config, host) => {
|
|
|
129
140
|
[...artifacts.values()].forEach((artifact, input) =>
|
|
130
141
|
files.input(`/etc/dolly/artifacts/${artifact.recipeSha256}.snapshot`, input, artifact.bytes.size));
|
|
131
142
|
// The kernel restores a snapshot against the list of its paths.
|
|
132
|
-
const manifest =
|
|
143
|
+
const manifest = builder?.manifest ?? [...(await scanSnapshotRecords(seed.stream(), () => false)).records.keys()];
|
|
133
144
|
files.write("/etc/dolly/image.manifest", `${manifest.join("\n")}\n`);
|
|
134
145
|
},
|
|
135
146
|
async restore({ kernel, memory, files, stage }) {
|
|
136
147
|
stage("building userspace from the Dollyfile...");
|
|
137
148
|
let status;
|
|
138
|
-
if (
|
|
139
|
-
stage(`loading builder from ${
|
|
140
|
-
status = kernel.dolly_process_bootstrap_resume_prepare(BigInt(await stageSnapshot(kernel, memory,
|
|
149
|
+
if (builder) {
|
|
150
|
+
stage(`loading builder from ${builderReference.location}...`);
|
|
151
|
+
status = kernel.dolly_process_bootstrap_resume_prepare(BigInt(await stageSnapshot(kernel, memory, builder.bytes)), 1);
|
|
141
152
|
} else {
|
|
142
153
|
status = kernel.dolly_process_bootstrap_prepare(BigInt(await stageSnapshot(kernel, memory, seed)));
|
|
143
154
|
seed = null;
|
|
@@ -145,7 +156,7 @@ await runImage(async (config, host) => {
|
|
|
145
156
|
let supervisor;
|
|
146
157
|
if (status === 0) {
|
|
147
158
|
supervisor = await host.kernel.supervisor(kernel);
|
|
148
|
-
status = (await supervisor.spawn(
|
|
159
|
+
status = (await supervisor.spawn(builder ? ["/bin/dollyfile", locator] : ["/usr/libexec/dolly/process-bin/bootstrap"])).status;
|
|
149
160
|
}
|
|
150
161
|
for (const artifact of artifacts.values()) files.remove(`/etc/dolly/artifacts/${artifact.recipeSha256}.snapshot`);
|
|
151
162
|
artifacts.clear();
|
|
@@ -166,7 +177,7 @@ await runImage(async (config, host) => {
|
|
|
166
177
|
// The image runs here with its own modules. The page keeps its identity,
|
|
167
178
|
// which a session saved on it names, not its bytes.
|
|
168
179
|
host.require(artifact.hostRequirements);
|
|
169
|
-
host.admit(artifact.hostRequirements);
|
|
180
|
+
await host.admit(artifact.hostRequirements);
|
|
170
181
|
const { buildId, sha256: digest, byteLength } = artifact;
|
|
171
182
|
self.postMessage({ type: "image-built", artifact: { buildId, recipeSha256, sha256: digest, byteLength, inputs: artifact.inputs } });
|
|
172
183
|
built = true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DOLLY_IMAGE_BUILD_ID } from "../
|
|
2
|
-
import { describeImageArtifact, loadImageArtifact,
|
|
1
|
+
import { DOLLY_IMAGE_BUILD_ID } from "../page/generated.mjs";
|
|
2
|
+
import { describeImageArtifact, loadImageArtifact, sha256 } from "./image-artifact.mjs";
|
|
3
3
|
import { inspectDollyfile } from "./dollyfile-view.mjs";
|
|
4
4
|
|
|
5
5
|
export async function loadCustomImage(source, descriptor) {
|
|
@@ -13,14 +13,6 @@ export async function loadCustomImage(source, descriptor) {
|
|
|
13
13
|
return artifact;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// Whether this browser's image cache still records the completed custom image
|
|
17
|
-
// ARTIFACT (its descriptor): a session saved on it can then be restored, which
|
|
18
|
-
// checks the image's bytes.
|
|
19
|
-
export async function customImageCached(artifact) {
|
|
20
|
-
const cached = await loadImageArtifactDescriptor(artifact.recipeSha256, artifact.inputs);
|
|
21
|
-
return cached?.sha256 === artifact.sha256 && cached.byteLength === artifact.byteLength;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
16
|
export async function checkedCustomArtifact(source, candidate) {
|
|
25
17
|
inspectDollyfile(source);
|
|
26
18
|
if (candidate?.buildId !== DOLLY_IMAGE_BUILD_ID || !(candidate.bytes instanceof Blob) ||
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inspectDollyfile, recipeFileName, imageFileName } from "./dollyfile-view.mjs";
|
|
2
|
-
import { sha256 } from "
|
|
2
|
+
import { sha256 } from "../page/static-asset.mjs";
|
|
3
3
|
|
|
4
4
|
const key = object => `${object.type}:${object.name}`;
|
|
5
5
|
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
// starting, whatever the machine), a file is not. A file is deleted when its
|
|
10
10
|
// entry is replaced or evicted; a page that still holds the file then reads
|
|
11
11
|
// nothing, so an entry's bytes are read only for a build the entry outlives.
|
|
12
|
-
import { DOLLY_IMAGE_BUILD_ID } from "../
|
|
13
|
-
import { imageInputs, imageInputsMatch } from "
|
|
12
|
+
import { DOLLY_IMAGE_BUILD_ID } from "../page/generated.mjs";
|
|
13
|
+
import { imageInputs, imageInputsMatch } from "../page/image-inputs.mjs";
|
|
14
14
|
import { loadRecipeGraph } from "./dollyfile-graph.mjs";
|
|
15
15
|
import { recipeFileName } from "./dollyfile-view.mjs";
|
|
16
|
-
import { sha256 } from "
|
|
17
|
-
import { Sha256 } from "
|
|
18
|
-
import { scanSnapshotRecords, MAX_SNAPSHOT_BYTES as snapshotSizeLimit } from "
|
|
16
|
+
import { sha256 } from "../page/static-asset.mjs";
|
|
17
|
+
import { Sha256 } from "../page/sha256.mjs";
|
|
18
|
+
import { scanSnapshotRecords, MAX_SNAPSHOT_BYTES as snapshotSizeLimit } from "../page/snapshot-records.mjs";
|
|
19
|
+
import { DOLLY_SNAPSHOT_RECORD_FILE } from "../host/runtime/abi.mjs";
|
|
19
20
|
export { sha256 };
|
|
20
21
|
|
|
21
22
|
const STORE_DIRECTORY = "dolly-images";
|
|
@@ -75,14 +76,14 @@ export async function describeImageArtifact(source, recipeSha256, inputs = []) {
|
|
|
75
76
|
try {
|
|
76
77
|
const { records, byteLength } = await scanSnapshotRecords(pieces, path => path.startsWith("/etc/dolly/"));
|
|
77
78
|
const recipe = records.get("/etc/dolly/Dollyfile");
|
|
78
|
-
if (recipe?.kind !==
|
|
79
|
-
records.get("/etc/dolly/artifact")?.kind !==
|
|
79
|
+
if (recipe?.kind !== DOLLY_SNAPSHOT_RECORD_FILE || await sha256(recipe.data) !== recipeSha256 ||
|
|
80
|
+
records.get("/etc/dolly/artifact")?.kind !== DOLLY_SNAPSHOT_RECORD_FILE) throw new Error("artifact recipe identity mismatch");
|
|
80
81
|
// The artifact retains every recipe it was built from, by file name.
|
|
81
82
|
const graph = await loadRecipeGraph(reference => {
|
|
82
83
|
if (reference === "Dollyfile") return recipe.data;
|
|
83
84
|
const path = `/etc/dolly/recipes/${recipeFileName(reference)}`;
|
|
84
85
|
const record = records.get(path);
|
|
85
|
-
if (record?.kind !==
|
|
86
|
+
if (record?.kind !== DOLLY_SNAPSHOT_RECORD_FILE) throw new Error(`artifact does not retain recipe ${reference}`);
|
|
86
87
|
return record.data;
|
|
87
88
|
}, "Dollyfile");
|
|
88
89
|
return { buildId: DOLLY_IMAGE_BUILD_ID, recipeSha256, sha256: hash.hex(),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DOLLY_IMAGES, imageBase, imageDefinition } from "
|
|
1
|
+
import { DOLLY_IMAGES, imageBase, imageDefinition } from "../page/images.mjs";
|
|
2
2
|
import { loadRecipeGraph } from "./dollyfile-graph.mjs";
|
|
3
|
-
import { imageInputs, imageInputsMatch } from "
|
|
3
|
+
import { imageInputs, imageInputsMatch } from "../page/image-inputs.mjs";
|
|
4
4
|
import { describeImageArtifact, loadImageArtifactDescriptor, loadImageArtifact, saveImageArtifact } from "./image-artifact.mjs";
|
|
5
|
-
import { loadPackagedSnapshotMetadata, packagedSystemSnapshot } from "
|
|
6
|
-
import { siteReference, sitePath } from "
|
|
5
|
+
import { loadPackagedSnapshotMetadata, packagedSystemSnapshot } from "../page/packaged-snapshot.mjs";
|
|
6
|
+
import { siteReference, sitePath } from "../page/static-asset.mjs";
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
// A custom recipe builds from this release's published recipes and sources.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hostRegistry } from "../host/modules.mjs";
|
|
2
2
|
import { hostRequirement } from "../host/requirements.mjs";
|
|
3
|
-
import { imageBase, imageDefinition } from "
|
|
3
|
+
import { imageBase, imageDefinition } from "../page/images.mjs";
|
|
4
4
|
import { inspectDollyfile } from "./dollyfile-view.mjs";
|
|
5
5
|
|
|
6
6
|
// Disposable Wasm userspace on the runtime the image declares, with the build
|
|
@@ -19,9 +19,10 @@ export async function buildImage(image, artifacts, configuration, report, { modu
|
|
|
19
19
|
// inputs declare: it enables only the runtime and the build host, but a step
|
|
20
20
|
// may run a program linked against a declared module's client (transfer's
|
|
21
21
|
// download --help), which the Worker admits by that module's ABI.
|
|
22
|
-
const needed = new Set([...buildHost, ...declared, ...artifacts.flatMap(artifact => artifact.hostRequirements)]
|
|
23
|
-
.map(value => hostRequirement(value).name));
|
|
24
22
|
const manifests = await Promise.all(modules.map(async url => ({ url, ...(await import(url, { with: { type: "json" } })).default })));
|
|
23
|
+
const needed = new Set([...declared, ...artifacts.flatMap(artifact => artifact.hostRequirements)]
|
|
24
|
+
.map(value => hostRequirement(value).name));
|
|
25
|
+
for (const { name, build } of manifests) if (build) needed.add(name);
|
|
25
26
|
const registry = await hostRegistry(manifests.filter(({ name }) => needed.has(name)).map(({ url }) => url));
|
|
26
27
|
const host = await registry.createHost("browser", registry.buildHostFor(declared),
|
|
27
28
|
{ send: message => worker.postMessage(message), configuration });
|
package/site/offline.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// The site's runtime kept in this browser, so that a saved session opens when
|
|
2
|
+
// the network fails: the files dist/dolly-offline.json lists (the pages of a
|
|
3
|
+
// session, the page's and the Workers' code, the host modules and the kernel),
|
|
4
|
+
// in a cache named by the site and that list's digest. The service worker
|
|
5
|
+
// answers from it only when a request fails (coi-serviceworker.js). The page
|
|
6
|
+
// alone writes it, each file checked against the list's SHA-256 for it; nothing
|
|
7
|
+
// of the guest reaches it. A cache is complete once it holds the list itself.
|
|
8
|
+
import { sha256 } from "../page/static-asset.mjs";
|
|
9
|
+
|
|
10
|
+
const prefix = site => `dolly-offline ${new URL(site).href} `;
|
|
11
|
+
const listUrl = site => new URL("dist/dolly-offline.json", site).href;
|
|
12
|
+
|
|
13
|
+
async function keptCaches(site) {
|
|
14
|
+
return (await caches.keys()).filter(name => name.startsWith(prefix(site)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Whether SITE's runtime is kept whole.
|
|
18
|
+
export async function runtimeKept(site) {
|
|
19
|
+
for (const name of await keptCaches(site)) if (await (await caches.open(name)).match(listUrl(site))) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Keeps SITE's runtime as its server gives it now, and removes what was kept of
|
|
24
|
+
// another. Rejects where the browser refuses the page a service worker or a
|
|
25
|
+
// cache, or a file is not the one the list names.
|
|
26
|
+
export async function keepRuntime(site) {
|
|
27
|
+
await navigator.serviceWorker.register(new URL("coi-serviceworker.js", site), { scope: new URL(site).pathname });
|
|
28
|
+
const listed = await fetch(listUrl(site), { cache: "no-cache" });
|
|
29
|
+
if (!listed.ok) throw new Error("this site lists no runtime to keep");
|
|
30
|
+
const list = await listed.clone().text(), name = prefix(site) + await sha256(new TextEncoder().encode(list));
|
|
31
|
+
const cache = await caches.open(name);
|
|
32
|
+
if (!await cache.match(listUrl(site))) {
|
|
33
|
+
try {
|
|
34
|
+
await Promise.all(JSON.parse(list).map(async ([path, digest]) => {
|
|
35
|
+
const url = new URL(path, site).href;
|
|
36
|
+
const response = await fetch(url, { cache: "no-cache" }), copy = response.clone();
|
|
37
|
+
if (!response.ok || await sha256(await response.arrayBuffer()) !== digest) throw new Error(`${path} is not the file this site lists`);
|
|
38
|
+
await cache.put(url, copy);
|
|
39
|
+
}));
|
|
40
|
+
await cache.put(listUrl(site), listed);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// A part of a runtime opens nothing and takes room the saves may need.
|
|
43
|
+
await caches.delete(name);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const other of await keptCaches(site)) if (other !== name) await caches.delete(other);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Removes what is kept of SITE's runtime.
|
|
51
|
+
export async function clearRuntime(site) {
|
|
52
|
+
for (const name of await keptCaches(site)) await caches.delete(name);
|
|
53
|
+
}
|