@daugasauron/dolly 0.1.5-rc.0 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +51 -78
  2. package/coi-serviceworker.js +23 -8
  3. package/dist/dolly-build-id.mjs +1 -1
  4. package/dist/dolly-image-build-id.mjs +1 -1
  5. package/dist/dolly-process-0.wasm +0 -0
  6. package/dist/dolly-process-abi.mjs +1 -1
  7. package/dist/dolly-process-dso.mjs +19 -11
  8. package/dist/dolly-process-worker.mjs +88 -33
  9. package/dist/dolly-runtime-worker.mjs +1544 -0
  10. package/dist/dolly.wasm +0 -0
  11. package/host/dso/dso.mjs +2 -2
  12. package/host/dso/module.json +4 -1
  13. package/host/http/abi.mjs +3 -1
  14. package/host/http/broker.mjs +17 -7
  15. package/host/http/http.mjs +19 -5
  16. package/host/http/module.json +3 -1
  17. package/host/http/policy.mjs +13 -6
  18. package/host/http/refusals.mjs +77 -0
  19. package/host/input/abi.mjs +1 -1
  20. package/host/modules.mjs +64 -23
  21. package/host/nena/module.json +2 -1
  22. package/host/nena/nena.mjs +2 -0
  23. package/host/packages/abi.mjs +27 -0
  24. package/host/packages/module.json +6 -4
  25. package/host/packages/packages.mjs +23 -8
  26. package/host/packages/service.mjs +46 -20
  27. package/host/packages/transport.mjs +123 -0
  28. package/host/packages/worker.mjs +13 -9
  29. package/host/requirements.mjs +1 -1
  30. package/host/runtime/abi.mjs +32 -0
  31. package/host/runtime/lease-bridge.mjs +13 -8
  32. package/host/runtime/module.json +3 -3
  33. package/host/runtime/runtime.mjs +3 -3
  34. package/host/snapshot/abi.mjs +2 -1
  35. package/host/snapshot/module.json +1 -1
  36. package/host/snapshot/snapshot.mjs +80 -57
  37. package/host/snapshot/transport.mjs +10 -3
  38. package/host/snapshot/ui.mjs +64 -39
  39. package/host/stack/module.json +1 -0
  40. package/host/stack/stack.mjs +1 -1
  41. package/host/threads/module.json +6 -1
  42. package/host/threads/threads.mjs +32 -13
  43. package/package.json +2 -2
  44. package/{src → page}/browser.mjs +37 -23
  45. package/page/generated.mjs +8 -0
  46. package/{src → page}/image-entry.mjs +10 -6
  47. package/{src → page}/image-inputs.mjs +11 -0
  48. package/{src → page}/image-names.mjs +2 -2
  49. package/{src → page}/kernel-plugin.mjs +1 -1
  50. package/{src → page}/pack-store.mjs +1 -1
  51. package/{src → page}/packaged-snapshot.mjs +56 -46
  52. package/{src → page}/process-constants.mjs +72 -15
  53. package/{src → page}/process-supervisor.mjs +155 -227
  54. package/{src → page}/snapshot-records.mjs +63 -44
  55. package/{src → page}/static-asset.mjs +36 -20
  56. package/{src → page}/version.mjs +1 -1
  57. package/{src → page}/worker-image.mjs +20 -9
  58. package/{src → site}/build-worker.mjs +36 -25
  59. package/{src → site}/custom-image.mjs +2 -10
  60. package/{src → site}/dollyfile-graph.mjs +1 -1
  61. package/{src → site}/dollyfile-view.mjs +1 -1
  62. package/{src → site}/image-artifact.mjs +9 -8
  63. package/{src → site}/image-build.mjs +4 -4
  64. package/{src → site}/image-builder.mjs +5 -4
  65. package/site/offline.mjs +53 -0
  66. package/{src → site}/session-store.mjs +47 -38
  67. package/src/runtime-worker.mjs +0 -57
  68. /package/{src → page}/build-log.mjs +0 -0
  69. /package/{src → page}/images.mjs +0 -0
  70. /package/{src → page}/page-chords.mjs +0 -0
  71. /package/{src → page}/page-indicators.mjs +0 -0
  72. /package/{src → page}/process-abi.mjs +0 -0
  73. /package/{src → page}/sha256.mjs +0 -0
  74. /package/{src → page}/terminal-text.mjs +0 -0
  75. /package/{src → page}/wasm-interface.mjs +0 -0
  76. /package/{src → site}/alphabetical.mjs +0 -0
@@ -1,22 +1,23 @@
1
- import { DOLLY_PROCESS_ABI_DIGEST } from "../dist/dolly-process-abi.mjs";
1
+ import { DOLLY_PROCESS_ABI_DIGEST } from "./generated.mjs";
2
2
  import { DOLLY_ERRNO } from "./process-constants.mjs";
3
3
  import { parseWasmInterface } from "./wasm-interface.mjs";
4
4
  import { validateProcessInterface } from "./process-abi.mjs";
5
- import { validateThreadProfile } from "../host/threads/threads.mjs";
6
- import { DOLLY_THREAD_SPAWN } from "../host/threads/abi.mjs";
7
5
  import { executableHostRequirements, checkHostAbi } from "../host/requirements.mjs";
8
6
  import {
9
- DOLLY_PROCESS_EXEC, DOLLY_PROCESS_EXIT, DOLLY_PROCESS_PACKET_LIMIT as packetLimit, DOLLY_PROCESS_SIGALRM, DOLLY_PROCESS_SIGINT,
10
- DOLLY_PROCESS_SIGKILL, DOLLY_PROCESS_SIGNAL, DOLLY_PROCESS_SIGNAL_ACKNOWLEDGE,
11
- DOLLY_PROCESS_SIGWINCH, DOLLY_PROCESS_SIZEOF, DOLLY_PROCESS_SPAWN,
7
+ DOLLY_PROCESS_EXEC, DOLLY_PROCESS_EXIT, DOLLY_PROCESS_PACKET_LIMIT as packetLimit,
8
+ DOLLY_PROCESS_SIGWINCH, DOLLY_PROCESS_OFFSETOF, DOLLY_PROCESS_SIZEOF, DOLLY_PROCESS_SPAWN,
12
9
  DOLLY_PROCESS_SPAWN_FOREGROUND, DOLLY_PROCESS_SPAWN_INHERIT_ENVIRONMENT,
13
10
  DOLLY_PROCESS_SPAWN_INTERACTIVE,
14
11
  } from "./process-constants.mjs";
15
12
  import { hex } from "./static-asset.mjs";
16
- import { DOLLY_FS_BLOCK_SIZE } from "../host/runtime/abi.mjs";
13
+ import { DOLLY_FS_BLOCK_SIZE, DOLLY_PROCESS_EVENT_KIND_SHIFT, DOLLY_PROCESS_EVENT_VALUE_SHIFT, DOLLY_PROCESS_EVENT_LAUNCH, DOLLY_PROCESS_EVENT_INTERRUPT,
14
+ DOLLY_PROCESS_EVENT_TERMINATE, DOLLY_PROCESS_EVENT_WAKE } from "../host/runtime/abi.mjs";
17
15
 
18
16
  const encoder = new TextEncoder();
19
17
  const spawnHeaderSize = DOLLY_PROCESS_SIZEOF.dolly_process_spawn_request;
18
+ const spawnField = DOLLY_PROCESS_OFFSETOF.dolly_process_spawn_request;
19
+ const mappingField = DOLLY_PROCESS_OFFSETOF.dolly_process_fd_mapping;
20
+ const mappingSize = DOLLY_PROCESS_SIZEOF.dolly_process_fd_mapping;
20
21
  // The kernel's deferred-call result; processes never observe it.
21
22
  const deferredResult = -(1n << 63n);
22
23
  const interruptedSystemCall = -BigInt(DOLLY_ERRNO.EINTR);
@@ -28,7 +29,7 @@ const interruptedSystemCall = -BigInt(DOLLY_ERRNO.EINTR);
28
29
  const processWorkerLimit = 56;
29
30
  const processMemoryBudgetBytes = 4 * 1024 * 1024 * 1024;
30
31
  // A spawn the count refuses waits as long as a Worker asks again for a memory
31
- // Chromium cannot reserve yet (src/process-worker.mjs), like any blocking
32
+ // Chromium cannot reserve yet (page/process-worker.mjs), like any blocking
32
33
  // call, asked again whenever a process ends and at each service tick; then it
33
34
  // fails EAGAIN, a full process table's answer. A spawn the budget refuses
34
35
  // waits until a process ends, however long: make and Cargo end a build at a
@@ -39,7 +40,6 @@ const reservationPatienceMilliseconds = 5000;
39
40
  // A Worker ended mid-copy into its mailbox finishes the copy within
40
41
  // microseconds; one that holds the mailbox this long keeps it.
41
42
  const mailboxSealMilliseconds = 1000;
42
- const interruptGraceMilliseconds = 500;
43
43
  const compiledModuleCacheEntries = 64;
44
44
  const compiledModuleCacheBytes = 256 * 1024 * 1024;
45
45
  // File versions known to hold a cached module's bytes (dolly_process_image_version).
@@ -131,7 +131,9 @@ function encodeStrings(strings, label) {
131
131
 
132
132
  // Root processes run argv[0], inherit the kernel environment and map
133
133
  // descriptors 0-2 onto themselves.
134
- function encodeSpawn(arguments_, flags) {
134
+ // With `standard`, the child's descriptors 0, 1 and 2 are the kernel's own:
135
+ // the display's terminal, or the bootstrap log without one.
136
+ function encodeSpawn(arguments_, flags, standard = true) {
135
137
  if (!Array.isArray(arguments_) || arguments_.length === 0) {
136
138
  throw new TypeError("a process needs argv[0]");
137
139
  }
@@ -141,23 +143,25 @@ function encodeSpawn(arguments_, flags) {
141
143
  }
142
144
  const pathBytes = encoder.encode(path);
143
145
  const argumentBytes = encodeStrings(arguments_, "process arguments");
144
- const size = spawnHeaderSize + pathBytes.length + argumentBytes.length + 3 * 8;
146
+ const mappings = standard ? 3 : 0;
147
+ const size = spawnHeaderSize + pathBytes.length + argumentBytes.length + mappings * mappingSize;
145
148
  if (pathBytes.length > 4096 || size > packetLimit) {
146
149
  throw new RangeError("process spawn packet is too large");
147
150
  }
148
151
  const packet = new Uint8Array(size);
149
152
  const view = new DataView(packet.buffer);
150
- view.setUint32(0, flags | DOLLY_PROCESS_SPAWN_INHERIT_ENVIRONMENT, true);
151
- view.setUint32(4, arguments_.length, true);
152
- view.setUint32(16, 3, true);
153
- view.setUint32(28, pathBytes.length, true);
154
- view.setBigUint64(32, BigInt(argumentBytes.length), true);
155
- view.setBigUint64(48, 0xffffffffffffffffn, true);
153
+ view.setUint32(spawnField.flags, flags | DOLLY_PROCESS_SPAWN_INHERIT_ENVIRONMENT, true);
154
+ view.setUint32(spawnField.argument_count, arguments_.length, true);
155
+ view.setUint32(spawnField.mapping_count, mappings, true);
156
+ view.setUint32(spawnField.path_size, pathBytes.length, true);
157
+ view.setBigUint64(spawnField.argument_bytes, BigInt(argumentBytes.length), true);
158
+ view.setBigUint64(spawnField.deadline_nanoseconds, 0xffffffffffffffffn, true);
156
159
  packet.set(pathBytes, spawnHeaderSize);
157
160
  packet.set(argumentBytes, spawnHeaderSize + pathBytes.length);
158
- for (let descriptor = 0; descriptor < 3; ++descriptor) {
159
- view.setUint32(size - 24 + descriptor * 8, descriptor, true);
160
- view.setUint32(size - 20 + descriptor * 8, descriptor, true);
161
+ for (let descriptor = 0; descriptor < mappings; ++descriptor) {
162
+ const mapping = size - mappings * mappingSize + descriptor * mappingSize;
163
+ view.setUint32(mapping + mappingField.source_descriptor, descriptor, true);
164
+ view.setUint32(mapping + mappingField.target_descriptor, descriptor, true);
161
165
  }
162
166
  return packet;
163
167
  }
@@ -165,12 +169,12 @@ function encodeSpawn(arguments_, flags) {
165
169
  // A process's memory lives in its own Workers alone, never in this one: a
166
170
  // shared memory's pages go when the last agent holding it drops it, and a
167
171
  // living Worker drops one only when it collects garbage, which a small heap
168
- // rarely needs (tasks/20261101-102515-retained-process-memory). So each
172
+ // rarely needs. So each
169
173
  // Worker makes or receives the memory, holds the gate and copies its requests
170
174
  // into a mailbox of its own in kernel memory, and the memory goes with the
171
175
  // Worker. This Worker only names that mailbox to the kernel.
172
176
  export class DollyProcessSupervisor {
173
- constructor(kernel, kernelMemory, gateModule, workerUrl, processContract, hostAbi, serviceHost, threadContract, threadHost, processModules) {
177
+ constructor(kernel, kernelMemory, gateModule, workerUrl, processContract, hostAbi, serviceHost, threadHost, processModules) {
174
178
  if (!(kernelMemory instanceof WebAssembly.Memory) ||
175
179
  !(gateModule instanceof WebAssembly.Module) || !(workerUrl instanceof URL)) {
176
180
  throw new TypeError("invalid Dolly process supervisor configuration");
@@ -182,7 +186,6 @@ export class DollyProcessSupervisor {
182
186
  this.processContract = processContract;
183
187
  this.hostAbi = hostAbi;
184
188
  this.serviceHost = serviceHost;
185
- this.threadContract = threadContract;
186
189
  this.threadHost = threadHost;
187
190
  this.processModules = processModules;
188
191
  this.processes = new Map();
@@ -191,6 +194,8 @@ export class DollyProcessSupervisor {
191
194
  this.compiledModuleBytes = 0;
192
195
  this.compiledVersions = new Map();
193
196
  this.launchChain = Promise.resolve();
197
+ // The processes the kernel announced for launch, in order.
198
+ this.launches = [];
194
199
  this.mailboxAddress = Number(kernel.dolly_process_mailbox_address());
195
200
  this.mailboxCapacity = packetLimit;
196
201
  if (!Number.isSafeInteger(this.mailboxAddress) || this.mailboxAddress <= 0 ||
@@ -201,12 +206,11 @@ export class DollyProcessSupervisor {
201
206
  }
202
207
 
203
208
  static async create(kernel, kernelMemory, runtimeBase, hostAbi, serviceHost, threadHost, processModules) {
204
- const [gateBytes, contractBytes, threadBytes, workerBytes] = await Promise.all([
205
- "dolly-process-gate-0.wasm", "dolly-process-0.wasm", "dolly-threads-0.wasm",
206
- "dolly-process-worker.mjs",
209
+ const [gateBytes, contractBytes, workerBytes] = await Promise.all([
210
+ "dolly-process-gate-0.wasm", "dolly-process-0.wasm", "dolly-process-worker.mjs",
207
211
  ].map(async name => {
208
212
  const response = await fetch(new URL(`dist/${name}`, runtimeBase), {
209
- cache: "no-store", credentials: "same-origin", redirect: "error",
213
+ credentials: "same-origin", redirect: "error",
210
214
  });
211
215
  if (!response.ok) throw new Error(`Dolly ${name} returned HTTP ${response.status}`);
212
216
  return response.arrayBuffer();
@@ -218,16 +222,16 @@ export class DollyProcessSupervisor {
218
222
  const supervisor = new DollyProcessSupervisor(
219
223
  kernel, kernelMemory, gateModule, workerUrl,
220
224
  parseWasmInterface(contractBytes, "dolly-process-0"),
221
- hostAbi, serviceHost, parseWasmInterface(threadBytes, "dolly-threads-0"), threadHost, processModules,
225
+ hostAbi, serviceHost, threadHost, processModules,
222
226
  );
223
227
  supervisor.releaseWorkerSource = () => URL.revokeObjectURL(workerUrl.href);
224
228
  // Entered through JSPI (WebAssembly.promising), a process runs on a
225
229
  // stack of its own. Chrome's holds about 950 KB where a Worker's holds
226
230
  // 500 KB, which Clang needs: an executable asks for it with a
227
231
  // dolly.process.stack section, and only the compiler carries one
228
- // (src/process/compiler-main.c). Every other program is entered
229
- // directly: a Codex build crashed Chrome's tab twice on that stack
230
- // (tasks/20260930-232236-llvm-in-dolly), and Firefox 155 reports every
232
+ // (programs/compiler-main.c). Every other program is entered
233
+ // directly: a Codex build crashed Chrome's tab twice on that stack,
234
+ // and Firefox 155 reports every
231
235
  // Worker terminated inside such a call as an uncaught exception that no
232
236
  // listener receives. Firefox's holds no more, so the entry is also taken
233
237
  // only where it is measured half again as deep (Chrome 151: 1.90
@@ -243,12 +247,13 @@ export class DollyProcessSupervisor {
243
247
  }
244
248
  }
245
249
 
246
- // threads@0 was disabled and its Workers are terminated: the programs that
247
- // ran on them end here, each told why on its own stderr.
250
+ // The module that provides threads was disabled and its Workers are
251
+ // terminated: the programs that ran on them end here, each told why on its
252
+ // own stderr.
248
253
  withdrawThreads() {
249
254
  this.threadHost = undefined;
250
255
  for (const process of [...this.processes.values()]) {
251
- if (process.threaded) this.#fail(process, new Error("threads@0 was disabled"));
256
+ if (process.threaded) this.#fail(process, new Error("its threads' module was disabled"));
252
257
  }
253
258
  }
254
259
 
@@ -267,15 +272,18 @@ export class DollyProcessSupervisor {
267
272
  return committed < processMemoryBudgetBytes || !fanning ? null : "budget";
268
273
  }
269
274
 
270
- spawn(arguments_, { foreground = false } = {}) {
275
+ // A root process on the standard streams of the boot, or, with `start`, on
276
+ // those a module gives it: start(size) spawns the packet of that size in the
277
+ // supervisor's mailbox, which maps no stream, and returns the PID or -errno.
278
+ spawn(arguments_, { foreground = false, start } = {}) {
271
279
  if (this.#spawnWait(0)) throw new Error("Dolly process limit reached");
272
- const packet = encodeSpawn(arguments_, foreground ? DOLLY_PROCESS_SPAWN_FOREGROUND : 0);
280
+ const packet = encodeSpawn(arguments_, foreground ? DOLLY_PROCESS_SPAWN_FOREGROUND : 0, start === undefined);
273
281
  new Uint8Array(
274
282
  this.kernelMemory.buffer,
275
283
  this.mailboxAddress,
276
284
  packet.length,
277
285
  ).set(packet);
278
- const pid = this.kernel.dolly_process_spawn_serialized(BigInt(packet.length));
286
+ const pid = (start ?? (size => this.kernel.dolly_process_spawn_serialized(size)))(BigInt(packet.length));
279
287
  if (pid < 0) {
280
288
  const errno = Object.keys(DOLLY_ERRNO).find(name => DOLLY_ERRNO[name] === -pid) ?? `errno ${-pid}`;
281
289
  throw new Error(`cannot start ${arguments_[0]}: ${errno}`);
@@ -283,7 +291,7 @@ export class DollyProcessSupervisor {
283
291
  const completion = new Promise((resolve, reject) => {
284
292
  this.#registerProcess(pid, 0, resolve, reject);
285
293
  });
286
- this.#scheduleLaunches();
294
+ this.#serviceEvents();
287
295
  return completion;
288
296
  }
289
297
 
@@ -298,71 +306,58 @@ export class DollyProcessSupervisor {
298
306
  memoryBytes: 0, messageHandler: null, errorHandler: null,
299
307
  messageErrorHandler: null, started: false,
300
308
  failure: null, interactive: (flags & DOLLY_PROCESS_SPAWN_INTERACTIVE) !== 0, retiring: false,
301
- interruptTimer: null, deadlineTimer: null, retirementTimer: null, lastSignal: 0,
302
- reclamationDeadline: 0, tid: 0, threads: new Map(), threaded: false,
303
- execution: null, signalAtStart: 0,
309
+ retirementTimer: null, reclamationDeadline: 0, tid: 0, threads: new Map(), threaded: false,
310
+ execution: null,
304
311
  };
305
312
  this.processes.set(pid, process);
306
- this.#armDeadline(process);
307
- }
308
-
309
- #descendantDepth(process, ancestorPid) {
310
- let candidate = process;
311
- for (let depth = 0; depth < this.processes.size; ++depth) {
312
- if (candidate.parent === ancestorPid) return depth + 1;
313
- if (candidate.parent === 0) return 0;
314
- candidate = this.processes.get(candidate.parent);
315
- if (!candidate) return 0;
316
- }
317
- return 0;
318
313
  }
319
314
 
320
315
  #serviceTick() {
321
316
  this.serviceHost();
322
- const interrupted = this.kernel.dolly_process_take_interrupt();
323
- if (interrupted > 0) this.#interruptForeground(interrupted);
324
- for (let pid; (pid = this.kernel.dolly_process_take_alarm()) > 0;) {
325
- this.#deliverSignal(this.processes.get(pid), DOLLY_PROCESS_SIGALRM);
326
- }
327
- this.#deliverRaised();
317
+ this.#serviceEvents();
328
318
  this.serviceDeferred();
329
319
  }
330
320
 
331
- // What a pseudo-terminal raised for the programs on its slave
332
- // (host/pty/dolly-pty-supervisor-0.wat): its interrupt key, resize or hang-up.
333
- #deliverRaised() {
334
- for (let pid; (pid = this.kernel.dolly_pty_take_signal()) > 0;) {
335
- this.#deliverSignal(this.processes.get(pid), this.kernel.dolly_pty_taken_signal());
336
- }
321
+ // The kernel decides what happens to processes and the supervisor carries
322
+ // it out (dolly_process_next_event), asked after each dispatch and each
323
+ // ending, at each tick and when what the kernel has due comes due.
324
+ #serviceEvents() {
325
+ for (let event; (event = this.kernel.dolly_process_next_event()) !== 0n;) {
326
+ const kind = Number(event >> BigInt(DOLLY_PROCESS_EVENT_KIND_SHIFT)),
327
+ value = Number((event >> BigInt(DOLLY_PROCESS_EVENT_VALUE_SHIFT)) & 0xffffn), pid = Number(event & 0xffffffffn);
328
+ if (kind === DOLLY_PROCESS_EVENT_LAUNCH) this.#announced(pid);
329
+ else if (kind === DOLLY_PROCESS_EVENT_INTERRUPT) this.#interrupt(this.processes.get(pid), value);
330
+ else if (kind === DOLLY_PROCESS_EVENT_TERMINATE) this.#terminated(pid);
331
+ else if (kind === DOLLY_PROCESS_EVENT_WAKE) this.serviceDeferred();
332
+ else throw new Error(`kernel supplied unknown event ${kind}`);
333
+ }
334
+ const due = this.kernel.dolly_process_service_milliseconds();
335
+ clearTimeout(this.serviceDue);
336
+ this.serviceDue = due >= 0 ? setTimeout(() => this.#serviceEvents(), Math.ceil(due)) : undefined;
337
+ }
338
+
339
+ // A spawned process to prepare: one the page spawned is known already.
340
+ #announced(pid) {
341
+ if (!this.processes.has(pid)) this.#registerProcess(pid, this.kernel.dolly_process_parent(pid));
342
+ this.launches.push(pid);
343
+ this.#scheduleLaunches();
337
344
  }
338
345
 
339
- #interruptForeground(pid) {
346
+ // The kernel ended the process: its Workers stop, and it retires. One never
347
+ // announced has none.
348
+ #terminated(pid) {
340
349
  const process = this.processes.get(pid);
341
- if (!process || process.retiring) return;
342
- const descendants = [...this.processes.values()].filter(
343
- (candidate) => !candidate.retiring && candidate.pid !== pid &&
344
- this.#descendantDepth(candidate, pid) !== 0,
345
- );
346
- // Like a job-control shell, an interactive owner's running commands take
347
- // SIGINT in its place; without any it takes SIGINT itself.
348
- for (const target of process.interactive && descendants.length ? descendants : [process, ...descendants]) {
349
- this.#deliverSignal(target);
350
+ if (process) {
351
+ if (!process.retiring) this.#retire(process);
352
+ } else if (this.kernel.dolly_process_worker_retired(pid) !== 0) {
353
+ throw new Error(`kernel rejected the retirement of process ${pid}`);
350
354
  }
351
355
  }
352
356
 
353
- #deliverSignal(process, signalNumber = DOLLY_PROCESS_SIGINT) {
354
- if (!process || process.retiring || this.processes.get(process.pid) !== process) return;
355
- const result = this.kernel.dolly_process_signal(process.pid, signalNumber);
356
- process.lastSignal = signalNumber;
357
- // The kernel decides what the signal does: it may have ended the process.
358
- if (this.kernel.dolly_process_exited(process.pid) !== 0) {
359
- this.#retire(process);
360
- return;
361
- }
362
- // A program not yet entered has no call to interrupt; what the kernel
363
- // keeps for it, it takes when it starts. One that ignores the signal
364
- // (result 1) has nothing to answer.
365
- if (result !== 0 || !process.started) return;
357
+ // A signal reached a running process: its blocked call returns EINTR, and
358
+ // an EXEC in progress is refused unless the signal is a resize.
359
+ #interrupt(process, signalNumber) {
360
+ if (!process || process.retiring || !process.started) return;
366
361
  const receiver = process.threaded
367
362
  ? [...process.threads.values()].sort((a, b) => a.tid - b.tid)[0] : process;
368
363
  const deferred = this.deferred.get(receiver);
@@ -378,16 +373,6 @@ export class DollyProcessSupervisor {
378
373
  this.kernel.dolly_process_exec_refused(process.pid, 0n);
379
374
  this.#signal(receiver, message.sequence, interruptedSystemCall);
380
375
  }
381
- if (signalNumber !== DOLLY_PROCESS_SIGWINCH) this.#armInterrupt(process, signalNumber);
382
- }
383
-
384
- // A program that does not finish a delivered signal in time is ended by it.
385
- #armInterrupt(process, signalNumber) {
386
- if (process.interruptTimer !== null) return;
387
- process.interruptTimer = setTimeout(() => {
388
- process.interruptTimer = null;
389
- this.#forceExit(process.pid, 128 + signalNumber, signalNumber);
390
- }, interruptGraceMilliseconds);
391
376
  }
392
377
 
393
378
  #scheduleLaunches() {
@@ -411,7 +396,7 @@ export class DollyProcessSupervisor {
411
396
  }
412
397
  return this.#cachedModule(key);
413
398
  }
414
- const compiled = await this.#compileProcess(this.#takeImage(pid));
399
+ const compiled = await this.#compileProcess(this.#takeImage(pid), pid);
415
400
  if (version !== 0n) {
416
401
  this.compiledVersions.delete(version);
417
402
  this.compiledVersions.set(version, compiled.key);
@@ -430,7 +415,8 @@ export class DollyProcessSupervisor {
430
415
  return cached;
431
416
  }
432
417
 
433
- async #compileProcess(bytes) {
418
+ // The notices of a slow compile go to process `pid`, whose launch started it.
419
+ async #compileProcess(bytes, pid) {
434
420
  const byteLength = bytes.byteLength;
435
421
  const digest = hex(await crypto.subtle.digest("SHA-256", bytes));
436
422
  const key = `${byteLength}:${digest}`;
@@ -443,8 +429,8 @@ export class DollyProcessSupervisor {
443
429
  const notice = setTimeout(() => {
444
430
  noticeShown = true;
445
431
  const mebibytes = (byteLength / (1024 * 1024)).toFixed(1);
446
- this.#writeTerminal(
447
- `\r\ndolly: preparing ${mebibytes} MiB WebAssembly executable for this session...\r\n`,
432
+ this.#writeTerminal(pid,
433
+ `\ndolly: preparing ${mebibytes} MiB WebAssembly executable for this session...\n`,
448
434
  );
449
435
  }, compilationNoticeMilliseconds);
450
436
  let module;
@@ -459,7 +445,7 @@ export class DollyProcessSupervisor {
459
445
  memoryRequirements = validateProcessInterface(this.processContract, parsed, DOLLY_PROCESS_ABI_DIGEST);
460
446
  requirements = executableHostRequirements(parsed);
461
447
  checkHostAbi(requirements, this.hostAbi);
462
- threaded = validateThreadProfile(parsed, requirements, this.threadContract);
448
+ threaded = this.threadHost?.threaded(parsed, requirements) ?? false;
463
449
  largeStack = parsed.customSections.includes("dolly.process.stack");
464
450
  module = byteLength >= heldModuleBytes ? await compileHeld(bytes) : await WebAssembly.compile(bytes);
465
451
  processInterface = { imports: parsed.imports, exports: parsed.exports,
@@ -468,10 +454,10 @@ export class DollyProcessSupervisor {
468
454
  } finally {
469
455
  clearTimeout(notice);
470
456
  if (noticeShown) {
471
- this.#writeTerminal(
457
+ this.#writeTerminal(pid,
472
458
  prepared
473
- ? `dolly: executable ready in ${((performance.now() - started) / 1000).toFixed(1)}s\r\n`
474
- : "dolly: executable preparation failed\r\n",
459
+ ? `dolly: executable ready in ${((performance.now() - started) / 1000).toFixed(1)}s\n`
460
+ : "dolly: executable preparation failed\n",
475
461
  );
476
462
  }
477
463
  }
@@ -504,7 +490,7 @@ export class DollyProcessSupervisor {
504
490
  compiled.module.terminate();
505
491
  }
506
492
 
507
- #writeTerminal(text) {
493
+ #writeTerminal(pid, text) {
508
494
  const bytes = encoder.encode(text);
509
495
  const address = this.kernel.malloc(BigInt(Math.max(1, bytes.byteLength)));
510
496
  if (address === 0 || address === 0n) return;
@@ -513,7 +499,7 @@ export class DollyProcessSupervisor {
513
499
  this.kernelMemory.buffer, Number(address), bytes.byteLength,
514
500
  ).set(bytes);
515
501
  this.kernel.dolly_terminal_write_bytes(
516
- BigInt(address), BigInt(bytes.byteLength),
502
+ pid, BigInt(address), BigInt(bytes.byteLength),
517
503
  );
518
504
  } finally {
519
505
  this.kernel.free(BigInt(address));
@@ -521,18 +507,17 @@ export class DollyProcessSupervisor {
521
507
  }
522
508
 
523
509
  async #launchPending() {
524
- for (let pid; (pid = this.kernel.dolly_process_next_launch()) !== 0;) {
510
+ for (let pid; (pid = this.launches.shift()) !== undefined;) {
525
511
  const process = this.processes.get(pid);
512
+ // Ended before its turn: retired already.
513
+ if (!process || process.retiring) continue;
526
514
  let compiled;
527
515
  try {
528
- if (!process) throw new Error(`kernel supplied unregistered process ${pid}`);
529
516
  compiled = await this.#compiledImage(pid);
530
517
  if (this.#canLaunch(process)) this.#start(process, compiled, this.#admit(compiled));
531
518
  } catch (error) {
532
- // A failed launch ends only its own process; exiting it in the kernel
533
- // also stops next_launch from naming it again.
534
- if (process) this.#fail(process, error);
535
- else this.kernel.dolly_process_worker_exited(pid, 126, 0);
519
+ // A failed launch ends only its own process.
520
+ this.#fail(process, error);
536
521
  } finally {
537
522
  if (compiled) this.#release(compiled);
538
523
  }
@@ -567,7 +552,7 @@ export class DollyProcessSupervisor {
567
552
  #admit({ requirements, threaded }) {
568
553
  checkHostAbi(requirements, this.hostAbi);
569
554
  const local = [...requirements.keys()].filter(requirement => this.processModules.has(requirement));
570
- if (threaded && !this.threadHost) throw new Error("threads@0 is unavailable");
555
+ if (threaded && !this.threadHost) throw new Error("no module provides its threads");
571
556
  return local;
572
557
  }
573
558
 
@@ -576,7 +561,7 @@ export class DollyProcessSupervisor {
576
561
  Object.assign(process, { memoryRequirements, memoryBytes: Number(memoryRequirements.initial) * 65536,
577
562
  maximumBytes: Number(memoryRequirements.maximum) * 65536, compiled, processInterface, threaded, largeStack, local });
578
563
  if (threaded) {
579
- const tid = this.kernel.dolly_threads_attach(process.pid);
564
+ const tid = this.threadHost.attach(process.pid);
580
565
  if (tid <= 0) throw new Error(`kernel could not reserve main thread: ${tid}`);
581
566
  process.tid = tid;
582
567
  process.threads.set(tid, process);
@@ -610,23 +595,20 @@ export class DollyProcessSupervisor {
610
595
  }
611
596
  if (process.execution !== execution) return unused();
612
597
  process.execution = null;
613
- const signalAtStart = this.kernel.dolly_process_exec_commit(process.pid);
614
- if (signalAtStart < 0) {
598
+ const committed = this.kernel.dolly_process_exec_commit(process.pid);
599
+ if (committed < 0) {
615
600
  unused();
616
- this.#fail(process, new Error(`kernel rejected process ${process.pid} exec: ${signalAtStart}`));
601
+ this.#fail(process, new Error(`kernel rejected process ${process.pid} exec: ${committed}`));
617
602
  return;
618
603
  }
619
604
  // The old program's memory goes with its Workers. Termination has no
620
605
  // completion event: a large interactive one gets the time it gets before
621
606
  // a parent may start its replacement (#reclamationDeadline).
622
607
  const delay = this.#reclamationDeadline(process) - performance.now();
623
- if (process.interruptTimer !== null) clearTimeout(process.interruptTimer);
624
- process.interruptTimer = null;
625
608
  this.#disposeWorker(process, compiled);
626
- Object.assign(process, { started: false, threaded: false, tid: 0, threads: new Map(), signalAtStart });
609
+ Object.assign(process, { started: false, threaded: false, tid: 0, threads: new Map() });
627
610
  // Its descriptors with CLOEXEC are closed: a reader of their pipes goes on now.
628
- this.#deliverRaised();
629
- if (this.kernel.dolly_process_take_wakeup()) this.serviceDeferred();
611
+ this.#serviceEvents();
630
612
  const start = () => {
631
613
  try {
632
614
  if (this.#canLaunch(process)) this.#start(process, compiled, local);
@@ -644,7 +626,7 @@ export class DollyProcessSupervisor {
644
626
  : new Worker(this.workerUrl, { type: "module", name });
645
627
  if (!worker) throw new Error("thread Worker capacity exhausted");
646
628
  // Words: the call sequence, the answered sequence, the result's two
647
- // halves, the copy word (src/process-worker.mjs copyRequest) and a
629
+ // halves, the copy word (page/process-worker.mjs copyRequest) and a
648
630
  // thread's start word (#disposeThread).
649
631
  const control = new SharedArrayBuffer(32);
650
632
  thread.mailbox = Number(this.kernel.malloc(BigInt(packetLimit)));
@@ -659,7 +641,7 @@ export class DollyProcessSupervisor {
659
641
  thread.control = new Int32Array(control);
660
642
  thread.messageHandler = event => this.#message(process, event.data, thread);
661
643
  // Cancelled, or the browser reports the Worker's uncaught error again in
662
- // this Worker, whose end it would be (src/browser.mjs).
644
+ // this Worker, whose end it would be (page/browser.mjs).
663
645
  thread.errorHandler = event => { event.preventDefault(); this.#fail(process, new Error(event.message || `${name} failed`)); };
664
646
  thread.messageErrorHandler = () => this.#fail(process, new Error(`${name} message could not be decoded`));
665
647
  worker.addEventListener("message", thread.messageHandler);
@@ -671,6 +653,7 @@ export class DollyProcessSupervisor {
671
653
  const channel = module instanceof Worker ? new MessageChannel() : null;
672
654
  channel && module.postMessage({ port: channel.port1 }, [channel.port1]);
673
655
  worker.postMessage({ type: "configure", pid, tid: thread.tid, argument, threaded, control,
656
+ threadOperations: threaded ? this.threadHost.operations : undefined,
674
657
  module: channel ? channel.port2 : module,
675
658
  memory: memoryRequirements, port, mailbox: thread.mailbox, kernelMemory: this.kernelMemory, gateModule: this.gateModule,
676
659
  promisingEntry: this.promisingEntry && process.largeStack, stackDepths: this.stackDepths,
@@ -696,19 +679,8 @@ export class DollyProcessSupervisor {
696
679
  Atomics.notify(process.control, 1);
697
680
  }
698
681
 
699
- #armDeadline(process) {
700
- const remaining = this.kernel.dolly_process_deadline_remaining(process.pid);
701
- if (remaining >= 0) {
702
- process.deadlineTimer = setTimeout(() => this.#endTree(process.pid, 124), Math.ceil(remaining));
703
- }
704
- }
705
-
706
682
  #clearTimers(process) {
707
- if (process.interruptTimer !== null) clearTimeout(process.interruptTimer);
708
- if (process.deadlineTimer !== null) clearTimeout(process.deadlineTimer);
709
683
  if (process.retirementTimer !== null) clearTimeout(process.retirementTimer);
710
- process.interruptTimer = null;
711
- process.deadlineTimer = null;
712
684
  process.retirementTimer = null;
713
685
  }
714
686
 
@@ -734,7 +706,7 @@ export class DollyProcessSupervisor {
734
706
  message.responseCapacity,
735
707
  message.memoryBytes,
736
708
  ];
737
- const spawnsThread = process.threaded && message.operation === DOLLY_THREAD_SPAWN;
709
+ const spawnsThread = process.threaded && message.operation === this.threadHost?.operations.spawn;
738
710
  if (message.pid !== process.pid || message.tid !== thread.tid ||
739
711
  values.some((value) => !Number.isSafeInteger(value) || value < 0) ||
740
712
  message.requestSize > packetLimit || message.responseCapacity > packetLimit ||
@@ -745,17 +717,16 @@ export class DollyProcessSupervisor {
745
717
  return;
746
718
  }
747
719
  // The size the budget counts: a threaded process keeps the one it reported
748
- // before its memory took its maximum (src/process-worker.mjs).
720
+ // before its memory took its maximum (page/process-worker.mjs).
749
721
  if (!(process.threaded && message.memoryBytes >= process.maximumBytes)) process.memoryBytes = message.memoryBytes;
750
722
  let result;
751
- let signalDelivery;
752
723
  let threadArgument;
753
724
  let portTaken = false;
754
725
  try {
755
726
  // A deferred call is retried against the request still in the mailbox:
756
- // a handler that defers has written nothing (src/process-kernel.h).
727
+ // a handler that defers has written nothing (kernel/process-kernel.h).
757
728
  const mailbox = () => new DataView(this.kernelMemory.buffer, thread.mailbox, 8);
758
- if (message.operation === DOLLY_THREAD_SPAWN && message.requestSize === 8)
729
+ if (spawnsThread && message.requestSize === 8)
759
730
  threadArgument = mailbox().getBigUint64(0, true);
760
731
  const args = [process.pid, message.operation, BigInt(message.requestSize), BigInt(message.responseCapacity)];
761
732
  if (process.threaded) args.splice(1, 0, thread.tid);
@@ -769,8 +740,7 @@ export class DollyProcessSupervisor {
769
740
  // Firefox can enter a direct Wasm call twice. Use the generic call path
770
741
  // so a completed syscall is never replayed against its response packet.
771
742
  result = exhausted ? -BigInt(DOLLY_ERRNO.EAGAIN)
772
- : Reflect.apply(process.threaded ? this.kernel.dolly_threads_dispatch : this.kernel.dolly_process_dispatch,
773
- this.kernel, args);
743
+ : process.threaded ? this.threadHost.dispatch(args) : Reflect.apply(this.kernel.dolly_process_dispatch, this.kernel, args);
774
744
  if (result === deferredResult) {
775
745
  const deferred = { process, thread, message, timer: null };
776
746
  this.deferred.set(thread, deferred);
@@ -796,7 +766,7 @@ export class DollyProcessSupervisor {
796
766
  if (result > BigInt(message.responseCapacity)) {
797
767
  throw new Error(`kernel overfilled process ${process.pid} response`);
798
768
  }
799
- if (message.operation === DOLLY_THREAD_SPAWN) {
769
+ if (spawnsThread) {
800
770
  if (result !== 8n || threadArgument === undefined) throw new Error("invalid kernel thread response");
801
771
  const tid = mailbox().getUint32(0, true);
802
772
  if (!tid || tid > 0x7fffffff || mailbox().getUint32(4, true) || process.threads.has(tid))
@@ -806,30 +776,10 @@ export class DollyProcessSupervisor {
806
776
  try { this.#launchWorker(process, child, threadArgument, message.port); portTaken = true; }
807
777
  catch (error) {
808
778
  this.#disposeThread(process, child);
809
- this.kernel.dolly_threads_unstarted(process.pid, tid);
779
+ this.threadHost.unstarted(process.pid, tid);
810
780
  result = -BigInt(DOLLY_ERRNO.EAGAIN);
811
781
  }
812
782
  }
813
- if (message.operation === DOLLY_PROCESS_SPAWN) {
814
- if (result !== 8n) throw new Error("invalid kernel spawn response");
815
- const pid = mailbox().getUint32(0, true);
816
- if (pid === 0 || pid > 0x7fffffff || mailbox().getUint32(4, true) !== 0) {
817
- throw new Error("invalid kernel spawn identity");
818
- }
819
- this.#registerProcess(pid, process.pid);
820
- }
821
- if (message.operation === DOLLY_PROCESS_SIGNAL) {
822
- if (result !== 8n) throw new Error("invalid kernel signal response");
823
- signalDelivery = { pid: mailbox().getUint32(0, true), signal: mailbox().getUint32(4, true) };
824
- if (signalDelivery.pid === 0 || signalDelivery.pid > 0x7fffffff) {
825
- throw new Error("invalid kernel signal target");
826
- }
827
- }
828
- if (message.operation === DOLLY_PROCESS_SIGNAL_ACKNOWLEDGE && result === 4n &&
829
- mailbox().getInt32(0, true) === 0 && process.interruptTimer !== null) {
830
- clearTimeout(process.interruptTimer);
831
- process.interruptTimer = null;
832
- }
833
783
  }
834
784
  } catch (error) {
835
785
  message.port?.close();
@@ -838,16 +788,10 @@ export class DollyProcessSupervisor {
838
788
  }
839
789
  if (!portTaken) message.port?.close();
840
790
  this.#signal(thread, message.sequence, result);
841
- if (signalDelivery?.signal) {
842
- this.#deliverSignal(this.processes.get(signalDelivery.pid), signalDelivery.signal);
843
- }
844
- this.#deliverRaised();
845
- // A spawn that waited for admission is launched like any other.
846
- if (!retry || message.operation === DOLLY_PROCESS_SPAWN) this.#scheduleLaunches();
847
- if (retry) return;
848
- // A pipe write or close can complete another thread's blocked read or poll,
849
- // such as an async runtime's waker: retry now, not at the next tick.
850
- if (this.kernel.dolly_process_take_wakeup()) this.serviceDeferred();
791
+ // What the call set in motion: a spawn's launch, a signal's delivery, and
792
+ // a pipe write or close that completes another thread's blocked read or
793
+ // poll, such as an async runtime's waker, retried now, not at the next tick.
794
+ this.#serviceEvents();
851
795
  }
852
796
 
853
797
  #message(process, message, thread = process) {
@@ -861,9 +805,6 @@ export class DollyProcessSupervisor {
861
805
  this.#fail(process, new Error(`kernel rejected process ${process.pid} start`));
862
806
  } else {
863
807
  process.started = true;
864
- // A signal pending across EXEC is the new program's to answer.
865
- if (process.signalAtStart) this.#armInterrupt(process, process.signalAtStart);
866
- process.signalAtStart = 0;
867
808
  }
868
809
  } else if (message.type === "syscall") {
869
810
  this.#syscall(process, message, false, thread);
@@ -874,13 +815,14 @@ export class DollyProcessSupervisor {
874
815
  // The trusted Worker wrapper has unwound Wasm and will never re-enter it.
875
816
  // Remove its event handlers/resources before making its stack joinable.
876
817
  this.#disposeThread(process, thread);
877
- const last = this.kernel.dolly_threads_retired(process.pid, thread.tid, message.result);
818
+ // Withdrawn since, the module has ended the process already.
819
+ const last = this.threadHost?.retired(process.pid, thread.tid, message.result) ?? -1;
878
820
  if (last < 0) this.#fail(process, new Error("kernel rejected thread retirement"));
879
821
  else if (last) this.#forceExit(process.pid, 0);
880
822
  else this.serviceDeferred();
881
823
  } else if (message.type === "finished") {
882
824
  // _start returned (status zero), or EXIT has already recorded the status.
883
- this.kernel.dolly_process_worker_exited(process.pid, 0, 0);
825
+ this.#recordExit(process.pid, 0, 0);
884
826
  this.#retire(process);
885
827
  } else if (message.type === "failed") {
886
828
  const detail = message.stack ? `${message.message}\n${message.stack}` : message.message;
@@ -898,38 +840,20 @@ export class DollyProcessSupervisor {
898
840
 
899
841
  #retire(process) {
900
842
  process.retiring = true;
901
- // A process a signal ends passes it to its descendants, as a signal to its
902
- // process group would: Dolly has no groups, so the command of slop -c is
903
- // the shell's child unless it is exec'd. Each answers it as it handles it.
904
- // A descendant that has it already, as Ctrl-C gives it to the whole
905
- // foreground tree, is not given it twice.
906
- const signal = this.kernel.dolly_process_exit_signal(process.pid);
907
- if (signal !== 0) {
908
- for (const descendant of [...this.processes.values()]) {
909
- if (descendant.lastSignal !== signal && this.#descendantDepth(descendant, process.pid) !== 0) {
910
- this.#deliverSignal(descendant, signal);
911
- }
912
- }
913
- }
914
843
  // Its children live on: the kernel has given them parent 0.
915
844
  for (const child of this.processes.values()) if (child.parent === process.pid) child.parent = 0;
916
845
  process.reclamationDeadline = this.#reclamationDeadline(process);
917
846
  this.#stop(process);
918
847
  // The kernel has closed the process's descriptors: a reader of its pipe or
919
- // a waiter for its lock proceeds now, not at the next tick.
920
- this.#deliverRaised();
921
- if (this.kernel.dolly_process_take_wakeup()) this.serviceDeferred();
848
+ // a waiter for its lock proceeds now, not at the next tick; a signal that
849
+ // ended it has reached its descendants.
850
+ this.#serviceEvents();
922
851
  const retired = () => {
923
852
  process.retirementTimer = null;
924
853
  if (this.processes.get(process.pid) !== process) return;
925
854
  this.#acknowledgeRetirement(process);
926
855
  if (process.resolve) this.#finish(process);
927
- else {
928
- // An orphan, whose parent may have ended after this one did: nobody
929
- // waits for it, so it is reaped, as init would.
930
- if (this.kernel.dolly_process_parent(process.pid) === 0) this.kernel.dolly_process_collect(process.pid);
931
- this.serviceDeferred();
932
- }
856
+ else this.serviceDeferred();
933
857
  };
934
858
  // Worker.terminate() has no completion event. Drop all references, then allow
935
859
  // one bounded reclamation window for large interactive memories before WAIT
@@ -975,7 +899,8 @@ export class DollyProcessSupervisor {
975
899
  const line = encoder.encode(`dolly: process ${process.pid} ` +
976
900
  `${process.started ? "failed" : "was refused"}: ${reason}\n`);
977
901
  new Uint8Array(this.kernelMemory.buffer, this.mailboxAddress, line.length).set(line);
978
- this.kernel.dolly_process_worker_failed(process.pid, BigInt(line.length));
902
+ const recorded = this.kernel.dolly_process_worker_failed(process.pid, BigInt(line.length));
903
+ if (recorded !== 0) throw new Error(`kernel refused the failure of process ${process.pid} (${recorded})`);
979
904
  detail.message = `Dolly process ${process.pid} failed: ${detail.message}`;
980
905
  process.failure = Object.assign(detail, { reason });
981
906
  if (!process.reject) console.error(detail.stack ?? detail.message);
@@ -986,23 +911,15 @@ export class DollyProcessSupervisor {
986
911
  const process = this.processes.get(pid);
987
912
  if (!process) return false;
988
913
  if (process.retiring) return true;
989
- this.kernel.dolly_process_worker_exited(pid, status, signalNumber);
914
+ this.#recordExit(pid, status, signalNumber);
990
915
  this.#retire(process);
991
916
  return true;
992
917
  }
993
918
 
994
- // A timed command's deadline ends its tree, deepest first, as timeout(1)
995
- // signals the command's process group.
996
- #endTree(pid, status) {
997
- const descendants = [...this.processes.values()]
998
- .map((process) => ({ process, depth: this.#descendantDepth(process, pid) }))
999
- .filter(({ depth }) => depth !== 0)
1000
- .sort((left, right) => right.depth - left.depth)
1001
- .map(({ process }) => process);
1002
- for (const descendant of descendants) {
1003
- this.#forceExit(descendant.pid, 128 + DOLLY_PROCESS_SIGKILL, DOLLY_PROCESS_SIGKILL);
1004
- }
1005
- this.#forceExit(pid, status);
919
+ // The kernel refuses only a process it never had or an impossible status.
920
+ #recordExit(pid, status, signalNumber) {
921
+ const result = this.kernel.dolly_process_worker_exited(pid, status, signalNumber);
922
+ if (result !== 0) throw new Error(`kernel refused the exit of process ${pid} (${result})`);
1006
923
  }
1007
924
 
1008
925
  #disposeThread(process, thread) {
@@ -1018,7 +935,7 @@ export class DollyProcessSupervisor {
1018
935
  // Chrome keeps what was posted to a Worker terminated before it runs, the
1019
936
  // memory a thread's Worker is sent among it, until this Worker collects
1020
937
  // garbage. One that has not started (control word 5 still 0) is told so
1021
- // and ends itself (src/process-worker.mjs processMemory).
938
+ // and ends itself (page/process-worker.mjs processMemory).
1022
939
  const unstarted = thread !== process && Atomics.compareExchange(thread.control, 5, 0, 2) === 0;
1023
940
  if (unstarted) Atomics.notify(thread.control, 5);
1024
941
  // threads@0 disabled since has already terminated its Workers.
@@ -1026,7 +943,7 @@ export class DollyProcessSupervisor {
1026
943
  else thread.worker.terminate();
1027
944
  }
1028
945
  // The Worker copies a request into its mailbox under control word 4
1029
- // (src/process-worker.mjs copyRequest); sealed, it copies no more, and the
946
+ // (page/process-worker.mjs copyRequest); sealed, it copies no more, and the
1030
947
  // mailbox goes back to the kernel. Termination has no completion event: a
1031
948
  // Worker ended inside a copy finishes it first.
1032
949
  if (thread.mailbox) {
@@ -1060,19 +977,30 @@ export class DollyProcessSupervisor {
1060
977
  }
1061
978
 
1062
979
  serviceDeferred() {
1063
- for (const deferred of [...this.deferred.values()]) {
1064
- const { process, thread, message } = deferred;
1065
- // An earlier retry in this pass may have ended this thread: a completed
1066
- // EXIT stops the other threads of its process.
1067
- if (this.deferred.get(thread) !== deferred) continue;
1068
- if (this.processes.get(process.pid) === process && !process.retiring) {
1069
- this.#syscall(process, message, true, thread);
1070
- } else this.#clearDeferred(thread);
980
+ // A retry that wakes others asks for one more pass, after this one.
981
+ if (this.servicingDeferred) { this.deferredAgain = true; return; }
982
+ this.servicingDeferred = true;
983
+ try {
984
+ do {
985
+ this.deferredAgain = false;
986
+ for (const deferred of [...this.deferred.values()]) {
987
+ const { process, thread, message } = deferred;
988
+ // An earlier retry in this pass may have ended this thread: a completed
989
+ // EXIT stops the other threads of its process.
990
+ if (this.deferred.get(thread) !== deferred) continue;
991
+ if (this.processes.get(process.pid) === process && !process.retiring) {
992
+ this.#syscall(process, message, true, thread);
993
+ } else this.#clearDeferred(thread);
994
+ }
995
+ } while (this.deferredAgain);
996
+ } finally {
997
+ this.servicingDeferred = false;
1071
998
  }
1072
999
  }
1073
1000
 
1074
1001
  dispose() {
1075
1002
  clearInterval(this.serviceTimer);
1003
+ clearTimeout(this.serviceDue);
1076
1004
  for (const process of this.processes.values()) {
1077
1005
  this.#stop(process);
1078
1006
  process.reject?.(new Error("Dolly runtime closed"));