@daugasauron/dolly 0.1.4 → 0.1.5
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 +134 -14
- package/coi-serviceworker.js +28 -0
- package/dist/dolly-build-id.mjs +1 -1
- package/dist/dolly-dso-0.wasm +0 -0
- 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 +382 -53
- package/dist/dolly-process-gate-0.wasm +0 -0
- package/dist/dolly-process-worker.mjs +164 -35
- package/dist/dolly.wasm +0 -0
- package/host/dso/abi.mjs +3 -1
- package/host/dso/dso.mjs +1 -2
- package/host/http/abi.mjs +2 -2
- package/host/http/broker.mjs +18 -8
- package/host/http/http.mjs +0 -1
- package/host/http/policy.mjs +8 -4
- package/host/input/abi.mjs +47 -0
- package/host/modules.mjs +4 -3
- package/host/nena/abi.mjs +1 -1
- package/host/nena/nena.mjs +16 -18
- package/host/packages/service.mjs +35 -15
- package/host/packages/worker.mjs +21 -0
- package/host/runtime/abi.mjs +1 -0
- package/host/runtime/boot-files.mjs +8 -2
- package/host/runtime/grower.mjs +11 -8
- package/host/runtime/lease-bridge.mjs +8 -5
- package/host/runtime/module.json +1 -1
- package/host/runtime/runtime.mjs +37 -18
- package/host/snapshot/abi.mjs +7 -9
- package/host/snapshot/snapshot.mjs +14 -12
- package/host/snapshot/transport.mjs +4 -12
- package/host/snapshot/ui.mjs +17 -7
- package/host/stack/stack.mjs +1 -2
- package/host/threads/abi.mjs +1 -1
- package/host/threads/threads.mjs +2 -1
- package/package.json +1 -1
- package/src/browser.mjs +34 -37
- package/src/build-log.mjs +13 -2
- package/src/build-worker.mjs +182 -0
- package/src/custom-image.mjs +59 -0
- package/src/dollyfile-graph.mjs +127 -0
- package/src/dollyfile-view.mjs +335 -0
- package/src/image-artifact.mjs +248 -0
- package/src/image-build.mjs +89 -0
- package/src/image-builder.mjs +63 -0
- package/src/images.mjs +13 -2
- package/src/pack-store.mjs +141 -0
- package/src/packaged-snapshot.mjs +263 -91
- package/src/page-chords.mjs +4 -2
- package/src/process-constants.mjs +4 -2
- package/src/process-supervisor.mjs +209 -122
- package/src/runtime-worker.mjs +9 -10
- package/src/session-store.mjs +5 -7
- package/src/sha256.mjs +65 -0
- package/src/snapshot-records.mjs +135 -23
- package/src/static-asset.mjs +29 -2
- package/src/terminal-text.mjs +8 -5
- package/src/version.mjs +1 -1
- package/src/worker-image.mjs +3 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { DOLLY_IMAGES } from "../../src/images.mjs";
|
|
2
|
-
import { loadPackagedSnapshotMetadata
|
|
1
|
+
import { DOLLY_IMAGES, imageBase } from "../../src/images.mjs";
|
|
2
|
+
import { loadPackagedSnapshotMetadata } from "../../src/packaged-snapshot.mjs";
|
|
3
3
|
import { MAX_SNAPSHOT_BYTES } from "../../src/snapshot-records.mjs";
|
|
4
4
|
|
|
5
5
|
const PACKAGES_ORIGIN = "https://packages.dolly.invalid";
|
|
@@ -16,8 +16,9 @@ function reply(body, status, type = "text/plain; charset=utf-8") {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// GET /v1/packages/SHA256 is the exact snapshot of the package whose recipe
|
|
19
|
-
// has that pin, verified from the published packs
|
|
20
|
-
// time, until the guest has read it or
|
|
19
|
+
// has that pin, verified from the published packs and merged as the guest
|
|
20
|
+
// reads it. One snapshot is served at a time, until the guest has read it or
|
|
21
|
+
// the broker's timeout cancels it.
|
|
21
22
|
export class PackageService {
|
|
22
23
|
constructor() {
|
|
23
24
|
this.active = false;
|
|
@@ -35,25 +36,44 @@ export class PackageService {
|
|
|
35
36
|
if (this.served >= PACKAGE_LIMITS.maxSnapshots) return reply("Package snapshot quota exceeded\n", 429);
|
|
36
37
|
this.active = true;
|
|
37
38
|
this.served += 1;
|
|
38
|
-
|
|
39
|
+
// The Worker (worker.mjs) answers each message in turn: the first with the
|
|
40
|
+
// packs checked, every later one with the snapshot's next chunk.
|
|
41
|
+
const worker = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module", name: "dolly-packages" });
|
|
42
|
+
let released = false, refuse;
|
|
43
|
+
const release = () => {
|
|
44
|
+
if (released) return;
|
|
45
|
+
released = true;
|
|
46
|
+
worker.terminate();
|
|
47
|
+
refuse?.(new Error("the package transfer ended"));
|
|
48
|
+
this.active = false;
|
|
49
|
+
};
|
|
50
|
+
init.signal?.addEventListener("abort", release, { once: true });
|
|
51
|
+
const ask = message => new Promise((resolve, reject) => {
|
|
52
|
+
refuse = reject;
|
|
53
|
+
worker.onmessage = ({ data }) => data.error === undefined ? resolve(data) : reject(new Error(data.error));
|
|
54
|
+
worker.onerror = event => { event.preventDefault(); reject(new Error(event.message || "package Worker failed")); };
|
|
55
|
+
if (released) reject(new Error("the package transfer ended"));
|
|
56
|
+
else worker.postMessage(message);
|
|
57
|
+
});
|
|
39
58
|
try {
|
|
40
59
|
const metadata = await loadPackagedSnapshotMetadata(definition.image);
|
|
41
|
-
|
|
60
|
+
await ask({ base: imageBase.href, metadata });
|
|
42
61
|
} catch (error) {
|
|
43
|
-
|
|
62
|
+
release();
|
|
44
63
|
return reply(`${definition.image}: ${error.message}\n`, 502);
|
|
45
64
|
}
|
|
46
|
-
const release = () => { this.active = false; };
|
|
47
|
-
let offset = 0;
|
|
48
65
|
const body = new ReadableStream({
|
|
49
|
-
pull
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
async pull(controller) {
|
|
67
|
+
try {
|
|
68
|
+
const { done, value } = await ask({});
|
|
69
|
+
if (done) { release(); controller.close(); } else controller.enqueue(value);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
release();
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
54
74
|
},
|
|
55
75
|
cancel: release,
|
|
56
|
-
}, { highWaterMark:
|
|
76
|
+
}, { highWaterMark: 4 });
|
|
57
77
|
return reply(body, 200, "application/octet-stream");
|
|
58
78
|
}
|
|
59
79
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The packages@0 Worker merges one package's packs into its snapshot, a chunk
|
|
2
|
+
// per request, off the page's thread: the page must answer the guest's reads
|
|
3
|
+
// at once, since a guest that finds no record waits 10 ms.
|
|
4
|
+
import { loadImages } from "../../src/images.mjs";
|
|
5
|
+
import { packagedSystemSnapshot } from "../../src/packaged-snapshot.mjs";
|
|
6
|
+
|
|
7
|
+
let chunks;
|
|
8
|
+
onmessage = async ({ data }) => {
|
|
9
|
+
try {
|
|
10
|
+
if (data.metadata) {
|
|
11
|
+
await loadImages(data.base);
|
|
12
|
+
chunks = await packagedSystemSnapshot(data.metadata);
|
|
13
|
+
postMessage({ done: false });
|
|
14
|
+
} else {
|
|
15
|
+
const { done, value } = await chunks.next();
|
|
16
|
+
postMessage({ done, value }, done ? [] : [value.buffer]);
|
|
17
|
+
}
|
|
18
|
+
} catch (error) {
|
|
19
|
+
postMessage({ error: error.message });
|
|
20
|
+
}
|
|
21
|
+
};
|
package/host/runtime/abi.mjs
CHANGED
|
@@ -5,4 +5,5 @@ export const DOLLY_TERMINAL_WORD_FOREGROUND_PID = 2;
|
|
|
5
5
|
export const DOLLY_TERMINAL_WORD_FOREGROUND_INTERRUPTIBLE = 3;
|
|
6
6
|
export const DOLLY_TERMINAL_WORD_INTERRUPT_SEQUENCE = 4;
|
|
7
7
|
export const DOLLY_TERMINAL_WORD_INTERRUPT_TARGET_PID = 5;
|
|
8
|
+
export const DOLLY_FS_BLOCK_SIZE = 1048576;
|
|
8
9
|
export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
|
|
@@ -2,8 +2,9 @@ import { DOLLY_ERRNO } from "./abi.mjs";
|
|
|
2
2
|
|
|
3
3
|
const encoder = new TextEncoder();
|
|
4
4
|
|
|
5
|
-
// Boot files cross between the Worker and the in-Wasm filesystem through
|
|
6
|
-
// kernel exports, which take paths and bytes copied into kernel memory
|
|
5
|
+
// Boot files cross between the Worker and the in-Wasm filesystem through four
|
|
6
|
+
// kernel exports, which take paths and bytes copied into kernel memory; one
|
|
7
|
+
// makes a build's input image a file whose bytes stay in the Worker.
|
|
7
8
|
export function bootFiles(kernel, memory) {
|
|
8
9
|
const allocate = size => {
|
|
9
10
|
const address = Number(kernel.malloc(BigInt(Math.max(1, size))));
|
|
@@ -26,6 +27,11 @@ export function bootFiles(kernel, memory) {
|
|
|
26
27
|
kernel.dolly_write_file(pathAddress, address, BigInt(bytes.length))));
|
|
27
28
|
if (status !== 0) throw new Error(`Dolly could not write ${path}: status ${status}`);
|
|
28
29
|
},
|
|
30
|
+
// The build's input INPUT (the runtime's setBuildInputs), SIZE bytes, as PATH.
|
|
31
|
+
input(path, input, size) {
|
|
32
|
+
const status = withPath(path, pathAddress => kernel.dolly_build_input(pathAddress, input, BigInt(size)));
|
|
33
|
+
if (status !== 0) throw new Error(`Dolly could not place ${path}: status ${status}`);
|
|
34
|
+
},
|
|
29
35
|
// Guest-writable files are bounded before trusted code copies them.
|
|
30
36
|
read(path, limit) {
|
|
31
37
|
const address = allocate(limit);
|
package/host/runtime/grower.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
// Grows the kernel's memory for the runtime Worker, which
|
|
2
|
-
//
|
|
3
|
-
// the old size in pages
|
|
1
|
+
// Grows the kernel's memory for the runtime Worker (runtime.mjs), which hands
|
|
2
|
+
// it the memory and a control once. A growth is asked while the control's third word is 1: its
|
|
3
|
+
// first value, the pages asked, becomes the old size in pages or -1, and the
|
|
4
|
+
// word 0. Requests pass through the word, not messages: the kernel's growth
|
|
5
|
+
// waits in Atomics.wait, and Chrome keeps a message posted then to a Worker
|
|
6
|
+
// that started moments before until its sender's next task.
|
|
4
7
|
onmessage = ({ data: { memory, control } }) => {
|
|
5
8
|
const values = new BigInt64Array(control), word = new Int32Array(control);
|
|
6
9
|
postMessage("waiting");
|
|
7
10
|
for (;;) {
|
|
8
|
-
// A notify can be the
|
|
9
|
-
while (Atomics.load(word,
|
|
10
|
-
try { values[
|
|
11
|
-
Atomics.store(word,
|
|
12
|
-
Atomics.notify(word,
|
|
11
|
+
// A notify can be the asker's for a growth done already.
|
|
12
|
+
while (Atomics.load(word, 2) === 0) Atomics.wait(word, 2, 0);
|
|
13
|
+
try { values[0] = BigInt(memory.grow(values[0])); } catch { values[0] = -1n; }
|
|
14
|
+
Atomics.store(word, 2, 0);
|
|
15
|
+
Atomics.notify(word, 2);
|
|
13
16
|
}
|
|
14
17
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { DOLLY_ERRNO as E } from "./abi.mjs";
|
|
2
2
|
|
|
3
3
|
// The Worker side of a device leased per process (src/device-lease.c). Each
|
|
4
|
-
// packet a process submits
|
|
4
|
+
// packet a process submits (dolly_device_header, runtime.h, then the
|
|
5
|
+
// operation's body) is checked whole here and goes to the page as
|
|
6
|
+
// `TYPE-request` with its operation, scope, sequence and body; the page's
|
|
5
7
|
// `TYPE-complete` is published in the lease's reply slot, a 64-byte header and
|
|
6
|
-
// `replyBytes`. `open` and `close` are the device's
|
|
7
|
-
export function createLeaseBridge({ type, slots: count, packetBytes, replyBytes, open, close }, memory, mailbox, send, complete) {
|
|
8
|
+
// `replyBytes`. `version`, `open` and `close` are the device's.
|
|
9
|
+
export function createLeaseBridge({ type, version, slots: count, packetBytes, replyBytes, open, close }, memory, mailbox, send, complete) {
|
|
8
10
|
const slots = new Array(count), stride = 64 + replyBytes;
|
|
9
11
|
if (!Number.isSafeInteger(mailbox) || mailbox <= 0 || mailbox % 4 ||
|
|
10
12
|
mailbox > memory.buffer.byteLength - count * stride) throw new Error(`Invalid ${type} mailbox`);
|
|
@@ -50,7 +52,8 @@ export function createLeaseBridge({ type, slots: count, packetBytes, replyBytes,
|
|
|
50
52
|
const packet = new Uint8Array(memory.buffer, address, size).slice();
|
|
51
53
|
const view = new DataView(packet.buffer), scope = view.getUint32(8, true), sequence = view.getUint32(16, true);
|
|
52
54
|
const operation = view.getUint32(4, true);
|
|
53
|
-
if (!scope || !sequence || view.getUint32(12, true) || view.getUint32(20, true)
|
|
55
|
+
if (view.getUint32(0, true) !== version || !scope || !sequence || view.getUint32(12, true) || view.getUint32(20, true) ||
|
|
56
|
+
view.getUint32(24, true) !== size - 32 || view.getUint32(28, true)) return -E.EINVAL;
|
|
54
57
|
const slot = (scope - 1) % count, entry = slots[slot];
|
|
55
58
|
if (entry?.pending || entry?.released) return -E.EBUSY;
|
|
56
59
|
if (entry && operation === open) return -E.EBUSY;
|
|
@@ -58,7 +61,7 @@ export function createLeaseBridge({ type, slots: count, packetBytes, replyBytes,
|
|
|
58
61
|
if (!entry && operation !== open) return -E.EBADF;
|
|
59
62
|
if (entry && sequence <= entry.sequence) return -E.ESTALE;
|
|
60
63
|
slots[slot] = {scope, sequence, operation, pending: true, released: false};
|
|
61
|
-
send({type: `${type}-request`, scope, sequence, packet}, [packet.buffer]);
|
|
64
|
+
send({type: `${type}-request`, operation, scope, sequence, body: packet.subarray(32)}, [packet.buffer]);
|
|
62
65
|
return 0;
|
|
63
66
|
}
|
|
64
67
|
return {dispatch, acknowledge, dispose};
|
package/host/runtime/module.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"provides": "kernel",
|
|
7
7
|
"dependencies": [],
|
|
8
8
|
"phase": "kernel",
|
|
9
|
-
"imports": ["env.memory", "env.dolly_bootstrap_write_bytes", "env.dolly_clock_realtime", "env.dolly_clock_monotonic", "env.dolly_entropy", "env.dolly_memory_grow"],
|
|
9
|
+
"imports": ["env.memory", "env.dolly_bootstrap_write_bytes", "env.dolly_clock_realtime", "env.dolly_clock_monotonic", "env.dolly_entropy", "env.dolly_memory_grow", "env.dolly_build_input_read"],
|
|
10
10
|
"host": "runtime.mjs",
|
|
11
11
|
"contracts": ["../../abi/dolly-supervisor-0.wat", "../../abi/dolly-image-0.wat"],
|
|
12
12
|
"process": ["../../abi/dolly-process-0.wat", "../../abi/dolly-process-gate-0.wat"],
|
package/host/runtime/runtime.mjs
CHANGED
|
@@ -17,6 +17,8 @@ export function check() {
|
|
|
17
17
|
const maxBootstrapMessage = 1024 * 1024;
|
|
18
18
|
// What crypto.getRandomValues fills in one call.
|
|
19
19
|
const maxEntropy = 65536;
|
|
20
|
+
// What the kernel reads of a build's input in one call: its window.
|
|
21
|
+
const maxBuildInputRead = 8 * 1024 * 1024;
|
|
20
22
|
|
|
21
23
|
// The terminal mailbox (abi/dolly-supervisor-0.wat), the Worker's end: the
|
|
22
24
|
// foreground command, shell results and the page's interrupt request, with or
|
|
@@ -100,14 +102,35 @@ export function worker({ send, runtimeBase, abi, service }) {
|
|
|
100
102
|
// one waits. Chrome counts a shared memory as external memory of the isolate
|
|
101
103
|
// that grows it; past twice its heap limit it then collects between every
|
|
102
104
|
// message of this one, and every system call takes fifteen times as long.
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
+
// A process's memory never reaches this Worker (process-supervisor.mjs).
|
|
106
|
+
const growerUrl = new URL("./grower.mjs", import.meta.url);
|
|
107
|
+
const grower = new Worker(growerUrl, { type: "module", name: "dolly-memory" });
|
|
108
|
+
const control = new SharedArrayBuffer(16), values = new BigInt64Array(control), word = new Int32Array(control);
|
|
109
|
+
// Once the grower holds the memory and waits on the word.
|
|
105
110
|
const growing = new Promise((resolve, reject) => {
|
|
106
111
|
grower.onmessage = resolve;
|
|
107
|
-
grower.onerror = event => reject(new Error(event.message || "the
|
|
112
|
+
grower.onerror = event => reject(new Error(event.message || "the memory Worker failed"));
|
|
108
113
|
});
|
|
109
|
-
grower.postMessage({ memory, control
|
|
114
|
+
grower.postMessage({ memory, control });
|
|
115
|
+
// The old size in pages, or -1: this thread sees the new size when it returns.
|
|
116
|
+
const grow = pages => {
|
|
117
|
+
values[0] = pages;
|
|
118
|
+
Atomics.store(word, 2, 1);
|
|
119
|
+
Atomics.notify(word, 2);
|
|
120
|
+
// A notify can be the grower's for the last growth, whose wait found it
|
|
121
|
+
// done already: this one is done when the word says so.
|
|
122
|
+
while (Atomics.load(word, 2) === 1) Atomics.wait(word, 2, 1);
|
|
123
|
+
const old = values[0];
|
|
124
|
+
// This thread learns of the growth by an interrupt, which a wait that
|
|
125
|
+
// found it done already does not take: take it before the kernel touches
|
|
126
|
+
// the new pages, as a loop does at each turn.
|
|
127
|
+
if (old >= 0n) while (BigInt(memory.buffer.byteLength) < (old + pages) * 65536n);
|
|
128
|
+
return old;
|
|
129
|
+
};
|
|
110
130
|
let supervisor, threadProvider, terminal, reported = "";
|
|
131
|
+
// The Blobs of a build's input images, which the build Worker sets: the
|
|
132
|
+
// kernel reads them as files (dolly_build_input), never anything else.
|
|
133
|
+
let buildInputs = [];
|
|
111
134
|
// The foreground program, sent to the page when it changes.
|
|
112
135
|
function reportForeground() {
|
|
113
136
|
if (!terminal) return;
|
|
@@ -149,20 +172,7 @@ export function worker({ send, runtimeBase, abi, service }) {
|
|
|
149
172
|
},
|
|
150
173
|
"env.dolly_clock_realtime": () => Date.now(),
|
|
151
174
|
"env.dolly_clock_monotonic": () => performance.now(),
|
|
152
|
-
"env.dolly_memory_grow":
|
|
153
|
-
growth[1] = pages;
|
|
154
|
-
Atomics.store(growthWord, 0, 1);
|
|
155
|
-
Atomics.notify(growthWord, 0);
|
|
156
|
-
// A notify can be the grower's for the last growth, whose wait found
|
|
157
|
-
// it done already: this one is done when the word says so.
|
|
158
|
-
while (Atomics.load(growthWord, 0) === 1) Atomics.wait(growthWord, 0, 1);
|
|
159
|
-
const old = growth[1];
|
|
160
|
-
// This thread learns of the growth by an interrupt, which a wait that
|
|
161
|
-
// found it done already does not take: take it before the kernel
|
|
162
|
-
// touches the new pages, as a loop does at each turn.
|
|
163
|
-
if (old >= 0n) while (BigInt(memory.buffer.byteLength) < (old + pages) * 65536n);
|
|
164
|
-
return old;
|
|
165
|
-
},
|
|
175
|
+
"env.dolly_memory_grow": grow,
|
|
166
176
|
// Web Crypto rejects views of shared memory: fill a private buffer, then copy.
|
|
167
177
|
"env.dolly_entropy": (address, length) => {
|
|
168
178
|
const bytes = span(address, length, maxEntropy);
|
|
@@ -170,7 +180,16 @@ export function worker({ send, runtimeBase, abi, service }) {
|
|
|
170
180
|
bytes.set(crypto.getRandomValues(new Uint8Array(bytes.length)));
|
|
171
181
|
return 0;
|
|
172
182
|
},
|
|
183
|
+
// Synchronous: the kernel reads inside a system call.
|
|
184
|
+
"env.dolly_build_input_read": (input, offset, address, length) => {
|
|
185
|
+
const blob = buildInputs[input], bytes = span(address, length, maxBuildInputRead), start = Number(offset);
|
|
186
|
+
if (!blob || !bytes || start < 0 || start > blob.size - bytes.length) return -E.EINVAL;
|
|
187
|
+
try { bytes.set(new Uint8Array(new FileReaderSync().readAsArrayBuffer(blob.slice(start, start + bytes.length)))); }
|
|
188
|
+
catch { return -E.EIO; }
|
|
189
|
+
return 0;
|
|
190
|
+
},
|
|
173
191
|
},
|
|
192
|
+
setBuildInputs(blobs) { buildInputs = blobs; },
|
|
174
193
|
// Both follow their modules, which nena@0 may enable or disable while the
|
|
175
194
|
// supervisor runs: undefined takes the provider or the module away.
|
|
176
195
|
setThreadProvider(provider) {
|
package/host/snapshot/abi.mjs
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
// Generated from host/snapshot/dolly-snapshot-0.wat, include/dolly/process.h.
|
|
2
2
|
export const DOLLY_SESSION_HEADER_SIZE = 64;
|
|
3
|
-
export const DOLLY_SESSION_NAME_CAPACITY = 128;
|
|
4
3
|
export const DOLLY_SESSION_TRANSFER_CAPACITY = 1048576;
|
|
5
4
|
export const DOLLY_SESSION_WORD_REQUEST_SEQUENCE = 0;
|
|
6
5
|
export const DOLLY_SESSION_WORD_COMPLETED_SEQUENCE = 1;
|
|
7
6
|
export const DOLLY_SESSION_WORD_STATUS = 2;
|
|
8
|
-
export const
|
|
9
|
-
export const
|
|
10
|
-
export const
|
|
11
|
-
export const
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
export const DOLLY_SESSION_WORD_CANCELLED_SEQUENCE = 10;
|
|
7
|
+
export const DOLLY_SESSION_WORD_CHUNK_SEQUENCE = 3;
|
|
8
|
+
export const DOLLY_SESSION_WORD_CHUNK_CONSUMED_SEQUENCE = 4;
|
|
9
|
+
export const DOLLY_SESSION_WORD_CHUNK_LENGTH = 5;
|
|
10
|
+
export const DOLLY_SESSION_WORD_CHUNK_EOF = 6;
|
|
11
|
+
export const DOLLY_SESSION_WORD_TOTAL_SIZE_LOW = 7;
|
|
12
|
+
export const DOLLY_SESSION_WORD_TOTAL_SIZE_HIGH = 8;
|
|
13
|
+
export const DOLLY_SESSION_WORD_CANCELLED_SEQUENCE = 9;
|
|
16
14
|
export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
|
|
@@ -6,17 +6,19 @@ import { DOLLY_IMAGES } from "../../src/images.mjs";
|
|
|
6
6
|
import { DOLLY_SESSION_FORMAT_VERSION, decodeSessionSnapshot, loadStoredSession, sessionCompatible,
|
|
7
7
|
sessionLoadUrl, validSessionName, DOLLY_SESSION_MAX_BYTES } from "../../src/session-store.mjs";
|
|
8
8
|
|
|
9
|
-
// A session route (/session/?name=NAME)
|
|
10
|
-
//
|
|
11
|
-
// image instead.
|
|
9
|
+
// A session route (/session/?name=NAME), or boot()'s session option on a page
|
|
10
|
+
// of one's own, boots the saved session's image and restores its delta; with
|
|
11
|
+
// recover=1 the site's route unpacks the files into a fresh system image instead.
|
|
12
12
|
export async function boot(route) {
|
|
13
13
|
if (!route.loadSession) return undefined;
|
|
14
14
|
if (route.mode !== "snapshot") throw new Error("invalid Dolly route configuration");
|
|
15
15
|
const query = new URL(location.href).searchParams;
|
|
16
|
-
const name = query.get("name"), recovering = query.get("recover") === "1";
|
|
17
|
-
|
|
16
|
+
const name = route.session ?? query.get("name"), recovering = route.session === undefined && query.get("recover") === "1";
|
|
17
|
+
// The site lists its saves; a page of one's own has no such page.
|
|
18
|
+
const listed = route.session === undefined ? " Open /sessions to see saved sessions." : "";
|
|
19
|
+
if (!validSessionName(name)) throw new Error(`The Dolly session name is invalid.${listed}`);
|
|
18
20
|
const record = await loadStoredSession(name);
|
|
19
|
-
if (record === null) throw new Error(`Session '${name}' was not found in this browser
|
|
21
|
+
if (record === null) throw new Error(`Session '${name}' was not found in this browser.${listed}`);
|
|
20
22
|
if (record.name !== name) throw new Error("Stored session name does not match its key");
|
|
21
23
|
let image = record.image;
|
|
22
24
|
if (recovering) {
|
|
@@ -24,7 +26,7 @@ export async function boot(route) {
|
|
|
24
26
|
if (!DOLLY_IMAGES.some(definition => definition.image === "system")) throw new Error("File recovery needs the system image in this distribution");
|
|
25
27
|
image = "system";
|
|
26
28
|
} else if (!sessionCompatible(record, DOLLY_IMAGES, DOLLY_BUILD_ID, DOLLY_IMAGE_BUILD_ID)) {
|
|
27
|
-
throw new Error(
|
|
29
|
+
throw new Error(`This save belongs to an older runtime or image recipe. It has not been deleted or overwritten.${listed}`);
|
|
28
30
|
}
|
|
29
31
|
const bytes = await decodeSessionSnapshot(record);
|
|
30
32
|
record.bytes.transfer(0);
|
|
@@ -38,13 +40,13 @@ export async function boot(route) {
|
|
|
38
40
|
function sessionCapture({ send, ask }) {
|
|
39
41
|
let consume = null;
|
|
40
42
|
return {
|
|
41
|
-
async capture(
|
|
43
|
+
async capture({ signal, onChunk }) {
|
|
42
44
|
signal?.throwIfAborted();
|
|
43
45
|
if (consume) throw new Error("A Dolly session save is already active");
|
|
44
46
|
const cancel = () => send({ type: "snapshot-cancel" });
|
|
45
47
|
consume = onChunk;
|
|
46
48
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
47
|
-
try { return await ask({ type: "snapshot-capture"
|
|
49
|
+
try { return await ask({ type: "snapshot-capture" }); }
|
|
48
50
|
finally {
|
|
49
51
|
consume = null;
|
|
50
52
|
signal?.removeEventListener("abort", cancel);
|
|
@@ -118,9 +120,9 @@ export function worker({ get, ask, configuration: { bytes, recover } }) {
|
|
|
118
120
|
},
|
|
119
121
|
service() { kernel?.dolly_session_service(); },
|
|
120
122
|
messages: {
|
|
121
|
-
"snapshot-capture"(
|
|
123
|
+
"snapshot-capture"() {
|
|
122
124
|
const controller = cancelled = new AbortController();
|
|
123
|
-
return transport.capture(
|
|
125
|
+
return transport.capture({ signal: controller.signal,
|
|
124
126
|
onChunk: chunk => ask({ type: "snapshot-chunk", bytes: chunk }, [chunk.buffer]) });
|
|
125
127
|
},
|
|
126
128
|
"snapshot-cancel"() { cancelled?.abort(); },
|
|
@@ -130,7 +132,7 @@ export function worker({ get, ask, configuration: { bytes, recover } }) {
|
|
|
130
132
|
// The kernel serves the mailbox on a request, after each consumed chunk
|
|
131
133
|
// and on cancellation, in a task of its own.
|
|
132
134
|
transport = new SessionTransport(memory.buffer, Number(kernel.dolly_session_mailbox_address()),
|
|
133
|
-
Number(kernel.
|
|
135
|
+
Number(kernel.dolly_session_transfer_address()),
|
|
134
136
|
() => setTimeout(() => kernel?.dolly_session_service()));
|
|
135
137
|
},
|
|
136
138
|
dispose() { transport?.close(); },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DOLLY_SESSION_MAX_BYTES
|
|
1
|
+
import { DOLLY_SESSION_MAX_BYTES } from "../../src/session-store.mjs";
|
|
2
2
|
import { DOLLY_ERRNO } from "./abi.mjs";
|
|
3
3
|
import * as A from "./abi.mjs";
|
|
4
4
|
|
|
@@ -10,7 +10,6 @@ export class SessionTransport {
|
|
|
10
10
|
static requestSequence = A.DOLLY_SESSION_WORD_REQUEST_SEQUENCE;
|
|
11
11
|
static completedSequence = A.DOLLY_SESSION_WORD_COMPLETED_SEQUENCE;
|
|
12
12
|
static status = A.DOLLY_SESSION_WORD_STATUS;
|
|
13
|
-
static nameLength = A.DOLLY_SESSION_WORD_NAME_LENGTH;
|
|
14
13
|
static chunkSequence = A.DOLLY_SESSION_WORD_CHUNK_SEQUENCE;
|
|
15
14
|
static chunkConsumedSequence = A.DOLLY_SESSION_WORD_CHUNK_CONSUMED_SEQUENCE;
|
|
16
15
|
static chunkLength = A.DOLLY_SESSION_WORD_CHUNK_LENGTH;
|
|
@@ -19,16 +18,14 @@ export class SessionTransport {
|
|
|
19
18
|
static totalSizeHigh = A.DOLLY_SESSION_WORD_TOTAL_SIZE_HIGH;
|
|
20
19
|
static cancelledSequence = A.DOLLY_SESSION_WORD_CANCELLED_SEQUENCE;
|
|
21
20
|
|
|
22
|
-
constructor(buffer, address,
|
|
21
|
+
constructor(buffer, address, transferAddress, wake) {
|
|
23
22
|
const range = (start, size) => Number.isSafeInteger(start) && start > 0 && start <= buffer.byteLength - size;
|
|
24
23
|
if (!(buffer instanceof SharedArrayBuffer) || !range(address, A.DOLLY_SESSION_HEADER_SIZE) || address % 4 ||
|
|
25
|
-
!range(
|
|
24
|
+
!range(transferAddress, A.DOLLY_SESSION_TRANSFER_CAPACITY)) {
|
|
26
25
|
throw new Error("Dolly supplied an invalid session mailbox");
|
|
27
26
|
}
|
|
28
27
|
this.bytes = new Uint8Array(buffer);
|
|
29
28
|
this.words = new Int32Array(buffer, address, A.DOLLY_SESSION_HEADER_SIZE / 4);
|
|
30
|
-
this.nameAddress = nameAddress;
|
|
31
|
-
this.nameCapacity = A.DOLLY_SESSION_NAME_CAPACITY;
|
|
32
29
|
this.transferAddress = transferAddress;
|
|
33
30
|
this.transferCapacity = A.DOLLY_SESSION_TRANSFER_CAPACITY;
|
|
34
31
|
this.wake = wake;
|
|
@@ -36,8 +33,7 @@ export class SessionTransport {
|
|
|
36
33
|
this.cancel = null;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
async capture(
|
|
40
|
-
if (!validSessionName(name)) throw new TypeError("invalid Dolly session name");
|
|
36
|
+
async capture({ signal, timeoutMilliseconds = 30000, onChunk } = {}) {
|
|
41
37
|
if (onChunk !== undefined && typeof onChunk !== "function") throw new TypeError("invalid session consumer");
|
|
42
38
|
signal?.throwIfAborted();
|
|
43
39
|
if (this.closed) throw new Error("Snapshot provider closed");
|
|
@@ -77,10 +73,6 @@ export class SessionTransport {
|
|
|
77
73
|
signal?.addEventListener("abort", cancel, { once: true });
|
|
78
74
|
let complete = false;
|
|
79
75
|
try {
|
|
80
|
-
const nameBytes = new TextEncoder().encode(name);
|
|
81
|
-
this.bytes.fill(0, this.nameAddress, this.nameAddress + this.nameCapacity);
|
|
82
|
-
this.bytes.set(nameBytes, this.nameAddress);
|
|
83
|
-
Atomics.store(words, SessionTransport.nameLength, nameBytes.length);
|
|
84
76
|
Atomics.store(words, SessionTransport.requestSequence, requested);
|
|
85
77
|
this.wake();
|
|
86
78
|
let snapshot;
|
package/host/snapshot/ui.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../../src/session-store.mjs";
|
|
13
13
|
|
|
14
14
|
const markup = `<style>
|
|
15
|
-
#session-open { position:
|
|
15
|
+
#session-open { position: absolute; right: 0.5rem; bottom: 0.35rem; z-index: 6;
|
|
16
16
|
padding: 0.15rem 0.45rem; color: inherit; background: #262626;
|
|
17
17
|
border: 1px solid #77736c; font: inherit; font-size: 13px; cursor: pointer; }
|
|
18
18
|
#session-open[data-failed] { border-color: #e98773; }
|
|
@@ -51,10 +51,11 @@ const markup = `<style>
|
|
|
51
51
|
// The Save button, its dialog and Ctrl+Shift+S. A save captures the session
|
|
52
52
|
// delta through the snapshot mailbox and keeps it in this browser. The page
|
|
53
53
|
// describes the running image once its ENTRY starts; restored names the
|
|
54
|
-
// session this page booted from, if any.
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
// session this page booted from, if any. A save names itself in the tab's URL
|
|
55
|
+
// (the site's session route, or ?session= on a page of one's own), and on a
|
|
56
|
+
// page with a site the dialog links the site's list of saves.
|
|
57
|
+
export function mountSessionSave({ mount, keyboard, showStatus, site, indicators }, transport, restored) {
|
|
58
|
+
mount.insertAdjacentHTML("beforeend", markup);
|
|
58
59
|
const button = document.querySelector("#session-open");
|
|
59
60
|
const dialog = document.querySelector("#session-dialog");
|
|
60
61
|
const nameField = document.querySelector("#session-name");
|
|
@@ -101,6 +102,10 @@ export function mountSessionSave({ keyboard, showStatus, site, indicators }, tra
|
|
|
101
102
|
|
|
102
103
|
async function save(requestedName) {
|
|
103
104
|
if (saving) return saving;
|
|
105
|
+
// 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.
|
|
108
|
+
navigator.storage.persist().catch(() => {});
|
|
104
109
|
saving = (async () => {
|
|
105
110
|
if (!base) throw new Error("Dolly is not ready to save a session");
|
|
106
111
|
await base.verifyBase?.();
|
|
@@ -122,7 +127,7 @@ export function mountSessionSave({ keyboard, showStatus, site, indicators }, tra
|
|
|
122
127
|
button.disabled = nameField.disabled = saveButton.disabled = true;
|
|
123
128
|
controller = new AbortController();
|
|
124
129
|
const capture = async onChunk => {
|
|
125
|
-
const result = await transport().capture(
|
|
130
|
+
const result = await transport().capture({ signal: controller.signal, onChunk });
|
|
126
131
|
dataset.sessionUncompressedBytes = String(onChunk ? result : result.byteLength);
|
|
127
132
|
dataset.sessionStatus = "compressing";
|
|
128
133
|
return result;
|
|
@@ -148,7 +153,11 @@ export function mountSessionSave({ keyboard, showStatus, site, indicators }, tra
|
|
|
148
153
|
dataset.session = name;
|
|
149
154
|
dataset.sessionBytes = String(encodedSize);
|
|
150
155
|
dataset.sessionStatus = "saved";
|
|
151
|
-
|
|
156
|
+
// A reload opens the save again: the site's session route, or on a page of
|
|
157
|
+
// one's own the address's ?session=, which the page gives boot().
|
|
158
|
+
const address = new URL(location.href);
|
|
159
|
+
address.searchParams.set("session", name);
|
|
160
|
+
history.replaceState(null, "", site ? sessionLoadUrl(name, site) : address);
|
|
152
161
|
showStatus(`Saved ${name} locally${site ? " · /sessions lists your saves" : ""}`);
|
|
153
162
|
indicators.hold("session", false);
|
|
154
163
|
update();
|
|
@@ -175,6 +184,7 @@ export function mountSessionSave({ keyboard, showStatus, site, indicators }, tra
|
|
|
175
184
|
// first time through the dialog.
|
|
176
185
|
claimsKey(event) {
|
|
177
186
|
if (dialog.open || button.contains(event.target)) return true;
|
|
187
|
+
if (event.target !== document.body && !mount.contains(event.target)) return false;
|
|
178
188
|
if (!base || !event.ctrlKey || !event.shiftKey || event.altKey || event.metaKey || event.code !== "KeyS") return false;
|
|
179
189
|
event.preventDefault();
|
|
180
190
|
event.stopImmediatePropagation();
|
package/host/stack/stack.mjs
CHANGED
|
@@ -10,9 +10,8 @@ export function worker({ get, runtimeBase }) {
|
|
|
10
10
|
cache: "no-store", 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
|
-
// threads: it serves each thread's Worker of a program using threads@0.
|
|
14
13
|
get("runtime").serveInProcess("stack@0", {
|
|
15
|
-
bundle: new Blob([await response.arrayBuffer()], { type: "text/javascript" }), configuration: {}
|
|
14
|
+
bundle: new Blob([await response.arrayBuffer()], { type: "text/javascript" }), configuration: {} });
|
|
16
15
|
},
|
|
17
16
|
disabled() { get("runtime").serveInProcess("stack@0"); },
|
|
18
17
|
};
|
package/host/threads/abi.mjs
CHANGED
|
@@ -5,4 +5,4 @@ export const DOLLY_THREAD_EXIT = 146;
|
|
|
5
5
|
export const DOLLY_THREAD_WAIT = 147;
|
|
6
6
|
export const DOLLY_THREAD_WAIT_NONBLOCK = 1;
|
|
7
7
|
export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
|
|
8
|
-
export const DOLLY_THREADS_ABI_DIGEST = "
|
|
8
|
+
export const DOLLY_THREADS_ABI_DIGEST = "23550e119f668b3a52a3e688699cf34880ee3dc2050c06c92c5311d6d347fe95";
|
package/host/threads/threads.mjs
CHANGED
|
@@ -26,7 +26,8 @@ export function worker({ get }) {
|
|
|
26
26
|
workers.set(worker, pid);
|
|
27
27
|
return worker;
|
|
28
28
|
},
|
|
29
|
-
|
|
29
|
+
// A Worker the supervisor leaves to end itself is not terminated.
|
|
30
|
+
release(worker, terminate = true) { if (terminate) worker.terminate(); workers.delete(worker); },
|
|
30
31
|
dispose() { for (const worker of workers.keys()) worker.terminate(); workers.clear(); },
|
|
31
32
|
// The runtime goes on without the module: the programs on its Workers end.
|
|
32
33
|
disabled() { get("runtime").setThreadProvider(undefined); },
|
package/package.json
CHANGED