@daugasauron/dolly 0.1.4 → 0.1.5-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +134 -14
  2. package/coi-serviceworker.js +28 -0
  3. package/dist/dolly-build-id.mjs +1 -1
  4. package/dist/dolly-dso-0.wasm +0 -0
  5. package/dist/dolly-image-build-id.mjs +1 -1
  6. package/dist/dolly-process-0.wasm +0 -0
  7. package/dist/dolly-process-abi.mjs +1 -1
  8. package/dist/dolly-process-dso.mjs +382 -53
  9. package/dist/dolly-process-gate-0.wasm +0 -0
  10. package/dist/dolly-process-worker.mjs +164 -35
  11. package/dist/dolly.wasm +0 -0
  12. package/host/dso/abi.mjs +3 -1
  13. package/host/dso/dso.mjs +1 -2
  14. package/host/http/abi.mjs +2 -2
  15. package/host/http/broker.mjs +18 -8
  16. package/host/http/http.mjs +0 -1
  17. package/host/http/policy.mjs +8 -4
  18. package/host/input/abi.mjs +47 -0
  19. package/host/modules.mjs +4 -3
  20. package/host/nena/abi.mjs +1 -1
  21. package/host/nena/nena.mjs +16 -18
  22. package/host/packages/service.mjs +35 -15
  23. package/host/packages/worker.mjs +21 -0
  24. package/host/runtime/abi.mjs +1 -0
  25. package/host/runtime/boot-files.mjs +8 -2
  26. package/host/runtime/grower.mjs +11 -8
  27. package/host/runtime/lease-bridge.mjs +8 -5
  28. package/host/runtime/module.json +1 -1
  29. package/host/runtime/runtime.mjs +37 -18
  30. package/host/snapshot/abi.mjs +7 -9
  31. package/host/snapshot/snapshot.mjs +14 -12
  32. package/host/snapshot/transport.mjs +4 -12
  33. package/host/snapshot/ui.mjs +17 -7
  34. package/host/stack/stack.mjs +1 -2
  35. package/host/threads/abi.mjs +1 -1
  36. package/host/threads/threads.mjs +2 -1
  37. package/package.json +1 -1
  38. package/src/browser.mjs +34 -37
  39. package/src/build-log.mjs +13 -2
  40. package/src/build-worker.mjs +182 -0
  41. package/src/custom-image.mjs +59 -0
  42. package/src/dollyfile-graph.mjs +127 -0
  43. package/src/dollyfile-view.mjs +335 -0
  44. package/src/image-artifact.mjs +248 -0
  45. package/src/image-build.mjs +89 -0
  46. package/src/image-builder.mjs +63 -0
  47. package/src/images.mjs +13 -2
  48. package/src/pack-store.mjs +141 -0
  49. package/src/packaged-snapshot.mjs +263 -91
  50. package/src/page-chords.mjs +4 -2
  51. package/src/process-constants.mjs +4 -2
  52. package/src/process-supervisor.mjs +209 -122
  53. package/src/runtime-worker.mjs +9 -10
  54. package/src/session-store.mjs +5 -7
  55. package/src/sha256.mjs +65 -0
  56. package/src/snapshot-records.mjs +135 -23
  57. package/src/static-asset.mjs +29 -2
  58. package/src/terminal-text.mjs +8 -5
  59. package/src/version.mjs +1 -1
  60. package/src/worker-image.mjs +3 -5
@@ -1,5 +1,5 @@
1
- import { DOLLY_IMAGES } from "../../src/images.mjs";
2
- import { loadPackagedSnapshotMetadata, loadPackagedSystemSnapshot } from "../../src/packaged-snapshot.mjs";
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. One snapshot is held at a
20
- // time, until the guest has read it or the broker's deadline cancels it.
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
- let bytes;
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
- bytes = await loadPackagedSystemSnapshot(definition.image, metadata, init.signal);
60
+ await ask({ base: imageBase.href, metadata });
42
61
  } catch (error) {
43
- this.active = false;
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: controller => {
50
- const end = Math.min(offset + 1024 * 1024, bytes.byteLength);
51
- controller.enqueue(new Uint8Array(bytes, offset, end - offset));
52
- offset = end;
53
- if (offset === bytes.byteLength) { controller.close(); release(); }
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: 0 });
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
+ };
@@ -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 three
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);
@@ -1,14 +1,17 @@
1
- // Grows the kernel's memory for the runtime Worker, which waits meanwhile
2
- // (runtime.mjs): word 0 is 1 while a growth by values[1] pages is asked, and
3
- // the old size in pages, or -1, replaces that count.
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 runtime Worker's for a growth done already.
9
- while (Atomics.load(word, 0) === 0) Atomics.wait(word, 0, 0);
10
- try { values[1] = BigInt(memory.grow(values[1])); } catch { values[1] = -1n; }
11
- Atomics.store(word, 0, 0);
12
- Atomics.notify(word, 0);
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 goes to the page as `TYPE-request`; the page's
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 operation numbers.
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)) return -E.EINVAL;
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};
@@ -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"],
@@ -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
- const growth = new BigInt64Array(new SharedArrayBuffer(16)), growthWord = new Int32Array(growth.buffer);
104
- const grower = new Worker(new URL("./grower.mjs", import.meta.url), { type: "module", name: "dolly-memory" });
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 kernel memory Worker failed"));
112
+ grower.onerror = event => reject(new Error(event.message || "the memory Worker failed"));
108
113
  });
109
- grower.postMessage({ memory, control: growth.buffer });
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": pages => {
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) {
@@ -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 DOLLY_SESSION_WORD_NAME_LENGTH = 3;
9
- export const DOLLY_SESSION_WORD_CHUNK_SEQUENCE = 4;
10
- export const DOLLY_SESSION_WORD_CHUNK_CONSUMED_SEQUENCE = 5;
11
- export const DOLLY_SESSION_WORD_CHUNK_LENGTH = 6;
12
- export const DOLLY_SESSION_WORD_CHUNK_EOF = 7;
13
- export const DOLLY_SESSION_WORD_TOTAL_SIZE_LOW = 8;
14
- export const DOLLY_SESSION_WORD_TOTAL_SIZE_HIGH = 9;
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) boots the saved session's image and
10
- // restores its delta; with recover=1 it unpacks the files into a fresh system
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
- if (!validSessionName(name)) throw new Error("The Dolly session URL has an invalid name. Open /sessions to see saved sessions.");
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. Open /sessions to see saved sessions.`);
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("This save belongs to an older runtime or image recipe. It has not been deleted or overwritten. Open /sessions to see saved sessions.");
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(name, { signal, onChunk }) {
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", name }); }
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"({ name }) {
123
+ "snapshot-capture"() {
122
124
  const controller = cancelled = new AbortController();
123
- return transport.capture(name, { signal: controller.signal,
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.dolly_session_name_address()), Number(kernel.dolly_session_transfer_address()),
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, validSessionName } from "../../src/session-store.mjs";
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, nameAddress, transferAddress, wake) {
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(nameAddress, A.DOLLY_SESSION_NAME_CAPACITY) || !range(transferAddress, A.DOLLY_SESSION_TRANSFER_CAPACITY)) {
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(name, { signal, timeoutMilliseconds = 30000, onChunk } = {}) {
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;
@@ -12,7 +12,7 @@ import {
12
12
  } from "../../src/session-store.mjs";
13
13
 
14
14
  const markup = `<style>
15
- #session-open { position: fixed; right: 0.5rem; bottom: 0.35rem; z-index: 6;
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. On a page with a site, a save names
55
- // itself in the tab's URL and the dialog links the site's list of saves.
56
- export function mountSessionSave({ keyboard, showStatus, site, indicators }, transport, restored) {
57
- document.body.insertAdjacentHTML("beforeend", markup);
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(name, { signal: controller.signal, onChunk });
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
- if (site) history.replaceState(null, "", sessionLoadUrl(name, site));
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();
@@ -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: {}, threads: true });
14
+ bundle: new Blob([await response.arrayBuffer()], { type: "text/javascript" }), configuration: {} });
16
15
  },
17
16
  disabled() { get("runtime").serveInProcess("stack@0"); },
18
17
  };
@@ -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 = "fcbd4fc02150731c9a351445157d854916d5c3fd84faf9933bbcdcc16bf146af";
8
+ export const DOLLY_THREADS_ABI_DIGEST = "23550e119f668b3a52a3e688699cf34880ee3dc2050c06c92c5311d6d347fe95";
@@ -26,7 +26,8 @@ export function worker({ get }) {
26
26
  workers.set(worker, pid);
27
27
  return worker;
28
28
  },
29
- release(worker) { worker.terminate(); workers.delete(worker); },
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.4",
2
+ "version": "0.1.5-rc.0",
3
3
  "type": "module",
4
4
  "name": "@daugasauron/dolly",
5
5
  "license": "MIT",