@daugasauron/dolly 0.0.0-stage → 0.1.4-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 (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -2
  3. package/dist/dolly-build-id.mjs +1 -0
  4. package/dist/dolly-dso-0.wasm +0 -0
  5. package/dist/dolly-image-build-id.mjs +1 -0
  6. package/dist/dolly-kernel-plugin-abi.mjs +2 -0
  7. package/dist/dolly-process-0.wasm +0 -0
  8. package/dist/dolly-process-abi.mjs +1 -0
  9. package/dist/dolly-process-dso.mjs +1575 -0
  10. package/dist/dolly-process-gate-0.wasm +0 -0
  11. package/dist/dolly-process-stack.mjs +50 -0
  12. package/dist/dolly-process-worker.mjs +275 -0
  13. package/dist/dolly-threads-0.wasm +0 -0
  14. package/dist/dolly.wasm +0 -0
  15. package/host/abi.mjs +5 -0
  16. package/host/dso/abi.mjs +13 -0
  17. package/host/dso/dso.mjs +26 -0
  18. package/host/dso/module.json +15 -0
  19. package/host/http/abi.mjs +28 -0
  20. package/host/http/broker.mjs +325 -0
  21. package/host/http/http.mjs +74 -0
  22. package/host/http/local-services.mjs +37 -0
  23. package/host/http/module.json +15 -0
  24. package/host/http/policy.mjs +293 -0
  25. package/host/modules.mjs +373 -0
  26. package/host/nena/abi.mjs +19 -0
  27. package/host/nena/module.json +15 -0
  28. package/host/nena/nena.mjs +131 -0
  29. package/host/packages/module.json +13 -0
  30. package/host/packages/packages.mjs +15 -0
  31. package/host/packages/service.mjs +59 -0
  32. package/host/requirements.mjs +62 -0
  33. package/host/runtime/abi.mjs +8 -0
  34. package/host/runtime/boot-files.mjs +45 -0
  35. package/host/runtime/grower.mjs +14 -0
  36. package/host/runtime/lease-bridge.mjs +65 -0
  37. package/host/runtime/module.json +16 -0
  38. package/host/runtime/runtime.mjs +209 -0
  39. package/host/snapshot/abi.mjs +16 -0
  40. package/host/snapshot/module.json +15 -0
  41. package/host/snapshot/snapshot.mjs +138 -0
  42. package/host/snapshot/transport.mjs +132 -0
  43. package/host/snapshot/ui.mjs +207 -0
  44. package/host/stack/abi.mjs +5 -0
  45. package/host/stack/module.json +17 -0
  46. package/host/stack/stack.mjs +19 -0
  47. package/host/threads/abi.mjs +8 -0
  48. package/host/threads/module.json +13 -0
  49. package/host/threads/threads.mjs +36 -0
  50. package/package.json +12 -4
  51. package/src/alphabetical.mjs +4 -0
  52. package/src/browser.mjs +237 -0
  53. package/src/build-log.mjs +30 -0
  54. package/src/image-entry.mjs +29 -0
  55. package/src/image-inputs.mjs +20 -0
  56. package/src/image-names.mjs +11 -0
  57. package/src/images.mjs +10 -0
  58. package/src/kernel-plugin.mjs +77 -0
  59. package/src/packaged-snapshot.mjs +175 -0
  60. package/src/page-chords.mjs +43 -0
  61. package/src/page-indicators.mjs +27 -0
  62. package/src/process-abi.mjs +69 -0
  63. package/src/process-constants.mjs +242 -0
  64. package/src/process-supervisor.mjs +1002 -0
  65. package/src/runtime-worker.mjs +55 -0
  66. package/src/session-store.mjs +198 -0
  67. package/src/snapshot-records.mjs +109 -0
  68. package/src/static-asset.mjs +97 -0
  69. package/src/terminal-text.mjs +72 -0
  70. package/src/version.mjs +3 -0
  71. package/src/wasm-interface.mjs +440 -0
  72. package/src/worker-image.mjs +137 -0
@@ -0,0 +1,1002 @@
1
+ import { DOLLY_PROCESS_ABI_DIGEST } from "../dist/dolly-process-abi.mjs";
2
+ import { DOLLY_ERRNO } from "./process-constants.mjs";
3
+ import { parseWasmInterface } from "./wasm-interface.mjs";
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
+ import { executableHostRequirements, checkHostAbi } from "../host/requirements.mjs";
8
+ 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,
12
+ DOLLY_PROCESS_SPAWN_FOREGROUND, DOLLY_PROCESS_SPAWN_INHERIT_ENVIRONMENT,
13
+ DOLLY_PROCESS_SPAWN_INTERACTIVE,
14
+ } from "./process-constants.mjs";
15
+ import { hex } from "./static-asset.mjs";
16
+
17
+ const encoder = new TextEncoder();
18
+ const spawnHeaderSize = DOLLY_PROCESS_SIZEOF.dolly_process_spawn_request;
19
+ // The kernel's deferred-call result; processes never observe it.
20
+ const deferredResult = -(1n << 63n);
21
+ const interruptedSystemCall = -BigInt(DOLLY_ERRNO.EINTR);
22
+ // Trusted bounds on concurrent process Workers, independent of kernel
23
+ // records: a spawn is admitted while the processes' committed Wasm memory is
24
+ // under the budget or the spawning process has no live child, and the count,
25
+ // the kernel's table, is the backstop. It stays below what Chromium reserves:
26
+ // the memory of a 61st process of 8 GiB was refused.
27
+ const processWorkerLimit = 56;
28
+ const processMemoryBudgetBytes = 4 * 1024 * 1024 * 1024;
29
+ // Chromium releases the address space of a terminated Worker's memory some
30
+ // time after terminate(): a memory it cannot reserve is asked for again for
31
+ // a while before the process is refused. A spawn the count refuses waits as
32
+ // long, like any blocking call, asked again whenever a process ends and at
33
+ // each service tick; then it fails EAGAIN, a full process table's answer. A
34
+ // spawn the budget refuses waits until a process ends, however long: make and
35
+ // Cargo end a build at a spawn's EAGAIN. So a pipeline started over the
36
+ // budget waits forever if its first stage fills the pipe before the next
37
+ // stage is admitted; a signal (Ctrl+C) ends the wait.
38
+ const reservationRetryMilliseconds = 50;
39
+ const reservationPatienceMilliseconds = 5000;
40
+ const interruptGraceMilliseconds = 500;
41
+ const compiledModuleCacheEntries = 64;
42
+ const compiledModuleCacheBytes = 256 * 1024 * 1024;
43
+ // File versions known to hold a cached module's bytes (dolly_process_image_version).
44
+ const compiledVersionEntries = 1024;
45
+ const compilationNoticeMilliseconds = 250;
46
+ const largeInteractiveProcessBytes = 128 * 1024 * 1024;
47
+ const workerReclamationMilliseconds = 500;
48
+
49
+ // How many calls a recursion nests before this thread's stack ends, entered
50
+ // with enter. Each call counts itself in memory and holds sixteen values it
51
+ // loaded there across the next call, so its frame is as large in a baseline
52
+ // compiler as in an optimizing one: a one-local recursion read anything from
53
+ // 0.4 to 2.4 times in Firefox as its tiers changed.
54
+ // (func $dive (result i64) (local i64 x 16) count++; load them; dive() + their sum)
55
+ const diver = "AGFzbQEAAAABBQFgAAF+AwIBAAUDAQABBxECBm1lbW9yeQIABGRpdmUAAAq/AQG8AQEQfkEAQQAoAgBBAWo2AgBBCCkDACEAQRAp" +
56
+ "AwAhAUEYKQMAIQJBICkDACEDQSgpAwAhBEEwKQMAIQVBOCkDACEGQcAAKQMAIQdByAApAwAhCEHQACkDACEJQdgAKQMAIQpB4AAp" +
57
+ "AwAhC0HoACkDACEMQfAAKQMAIQ1B+AApAwAhDkGAASkDACEPEAAgAHwgAXwgAnwgA3wgBHwgBXwgBnwgB3wgCHwgCXwgCnwgC3wg" +
58
+ "DHwgDXwgDnwgD3wL";
59
+ async function stackDepth(enter) {
60
+ const { memory, dive } = new WebAssembly.Instance(new WebAssembly.Module(
61
+ Uint8Array.from(atob(diver), character => character.charCodeAt(0)))).exports;
62
+ try { await enter(dive)(); } catch { /* The stack ended: that is the measurement. */ }
63
+ return new Uint32Array(memory.buffer, 0, 1)[0];
64
+ }
65
+
66
+ export function terminalFailureReason(error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ const firstLine = message.split(/[\r\n]/, 1)[0]
69
+ .replace(/[^\x20-\x7e]/g, "?")
70
+ .slice(0, 512);
71
+ return firstLine || "unknown Worker failure";
72
+ }
73
+
74
+ function encodeStrings(strings, label) {
75
+ if (!Array.isArray(strings)) throw new TypeError(`${label} must be an array`);
76
+ const encoded = [];
77
+ let size = 0;
78
+ for (const value of strings) {
79
+ if (typeof value !== "string" || value.includes("\0")) {
80
+ throw new TypeError(`${label} contains an invalid string`);
81
+ }
82
+ const bytes = encoder.encode(value);
83
+ if (bytes.length + 1 > packetLimit - size) throw new RangeError(`${label} is too large`);
84
+ encoded.push(bytes);
85
+ size += bytes.length + 1;
86
+ }
87
+ const output = new Uint8Array(size);
88
+ let offset = 0;
89
+ for (const bytes of encoded) {
90
+ output.set(bytes, offset);
91
+ offset += bytes.length + 1;
92
+ }
93
+ return output;
94
+ }
95
+
96
+ // Root processes run argv[0], inherit the kernel environment and map
97
+ // descriptors 0-2 onto themselves.
98
+ function encodeSpawn(arguments_, flags) {
99
+ if (!Array.isArray(arguments_) || arguments_.length === 0) {
100
+ throw new TypeError("a process needs argv[0]");
101
+ }
102
+ const [path] = arguments_;
103
+ if (typeof path !== "string" || !path.startsWith("/") || path.includes("\0")) {
104
+ throw new TypeError("a process path must be absolute");
105
+ }
106
+ const pathBytes = encoder.encode(path);
107
+ const argumentBytes = encodeStrings(arguments_, "process arguments");
108
+ const size = spawnHeaderSize + pathBytes.length + argumentBytes.length + 3 * 8;
109
+ if (pathBytes.length > 4096 || size > packetLimit) {
110
+ throw new RangeError("process spawn packet is too large");
111
+ }
112
+ const packet = new Uint8Array(size);
113
+ const view = new DataView(packet.buffer);
114
+ view.setUint32(0, flags | DOLLY_PROCESS_SPAWN_INHERIT_ENVIRONMENT, true);
115
+ view.setUint32(4, arguments_.length, true);
116
+ view.setUint32(16, 3, true);
117
+ view.setUint32(28, pathBytes.length, true);
118
+ view.setBigUint64(32, BigInt(argumentBytes.length), true);
119
+ view.setBigUint64(48, 0xffffffffffffffffn, true);
120
+ packet.set(pathBytes, spawnHeaderSize);
121
+ packet.set(argumentBytes, spawnHeaderSize + pathBytes.length);
122
+ for (let descriptor = 0; descriptor < 3; ++descriptor) {
123
+ view.setUint32(size - 24 + descriptor * 8, descriptor, true);
124
+ view.setUint32(size - 20 + descriptor * 8, descriptor, true);
125
+ }
126
+ return packet;
127
+ }
128
+
129
+ function createProcessMemory({ initial, maximum }) {
130
+ return new WebAssembly.Memory({
131
+ initial,
132
+ maximum,
133
+ shared: true,
134
+ address: "i64",
135
+ });
136
+ }
137
+
138
+ export class DollyProcessSupervisor {
139
+ constructor(kernel, kernelMemory, gateModule, workerUrl, processContract, hostAbi, serviceHost, threadContract, threadHost, processModules) {
140
+ if (!(kernelMemory instanceof WebAssembly.Memory) ||
141
+ !(gateModule instanceof WebAssembly.Module) || !(workerUrl instanceof URL)) {
142
+ throw new TypeError("invalid Dolly process supervisor configuration");
143
+ }
144
+ this.kernel = kernel;
145
+ this.kernelMemory = kernelMemory;
146
+ this.gateModule = gateModule;
147
+ this.workerUrl = workerUrl;
148
+ this.processContract = processContract;
149
+ this.hostAbi = hostAbi;
150
+ this.serviceHost = serviceHost;
151
+ this.threadContract = threadContract;
152
+ this.threadHost = threadHost;
153
+ this.processModules = processModules;
154
+ this.processes = new Map();
155
+ this.deferred = new Map();
156
+ this.compiledModules = new Map();
157
+ this.compiledModuleBytes = 0;
158
+ this.compiledVersions = new Map();
159
+ this.launchChain = Promise.resolve();
160
+ this.mailboxAddress = Number(kernel.dolly_process_mailbox_address());
161
+ this.mailboxCapacity = packetLimit;
162
+ if (!Number.isSafeInteger(this.mailboxAddress) || this.mailboxAddress <= 0 ||
163
+ this.mailboxAddress > kernelMemory.buffer.byteLength - this.mailboxCapacity) {
164
+ throw new Error("Dolly kernel supplied an invalid process mailbox");
165
+ }
166
+ this.serviceTimer = setInterval(() => this.#serviceTick(), 16);
167
+ }
168
+
169
+ static async create(kernel, kernelMemory, runtimeBase, hostAbi, serviceHost, threadHost, processModules) {
170
+ const [gateBytes, contractBytes, threadBytes, workerBytes] = await Promise.all([
171
+ "dolly-process-gate-0.wasm", "dolly-process-0.wasm", "dolly-threads-0.wasm",
172
+ "dolly-process-worker.mjs",
173
+ ].map(async name => {
174
+ const response = await fetch(new URL(`dist/${name}`, runtimeBase), {
175
+ cache: "no-store", credentials: "same-origin", redirect: "error",
176
+ });
177
+ if (!response.ok) throw new Error(`Dolly ${name} returned HTTP ${response.status}`);
178
+ return response.arrayBuffer();
179
+ }));
180
+ const gateModule = await WebAssembly.compile(gateBytes);
181
+ // Fresh Workers share trusted code bytes, never process memory or JS state.
182
+ const workerUrl = new URL(URL.createObjectURL(new Blob([workerBytes], { type: "text/javascript" })));
183
+ try {
184
+ const supervisor = new DollyProcessSupervisor(
185
+ kernel, kernelMemory, gateModule, workerUrl,
186
+ parseWasmInterface(contractBytes, "dolly-process-0"),
187
+ hostAbi, serviceHost, parseWasmInterface(threadBytes, "dolly-threads-0"), threadHost, processModules,
188
+ );
189
+ supervisor.releaseWorkerSource = () => URL.revokeObjectURL(workerUrl.href);
190
+ // Entered through JSPI (WebAssembly.promising), a process runs on a
191
+ // stack of its own. Chrome's holds about 950 KB where a Worker's holds
192
+ // 500 KB, which Clang needs: an executable asks for it with a
193
+ // dolly.process.stack section, and only the compiler carries one
194
+ // (src/process/compiler-main.c). Every other program is entered
195
+ // directly: a Codex build crashed Chrome's tab twice on that stack
196
+ // (tasks/20260930-232236-llvm-in-dolly), and Firefox 155 reports every
197
+ // Worker terminated inside such a call as an uncaught exception that no
198
+ // listener receives. Firefox's holds no more, so the entry is also taken
199
+ // only where it is measured half again as deep (Chrome 151: 1.90
200
+ // times, Firefox 155: 1.01, each in 48 measurements of 48). stack@0
201
+ // sizes the room of each stack by the same measurements.
202
+ supervisor.stackDepths = { direct: await stackDepth(entry => entry),
203
+ promising: WebAssembly.promising ? await stackDepth(WebAssembly.promising) : 0 };
204
+ supervisor.promisingEntry = supervisor.stackDepths.promising > 1.5 * supervisor.stackDepths.direct;
205
+ return supervisor;
206
+ } catch (error) {
207
+ URL.revokeObjectURL(workerUrl.href);
208
+ throw error;
209
+ }
210
+ }
211
+
212
+ // threads@0 was disabled and its Workers are terminated: the programs that
213
+ // ran on them end here, each told why on its own stderr.
214
+ withdrawThreads() {
215
+ this.threadHost = undefined;
216
+ for (const process of [...this.processes.values()]) {
217
+ if (process.threaded) this.#fail(process, new Error("threads@0 was disabled"));
218
+ }
219
+ }
220
+
221
+ // What a spawn waits for, "count" or "budget", or null when it is admitted.
222
+ // The budget bounds fan-out (make -j, xargs -P), not a chain: refusing the
223
+ // one child a process waits for (zig build-exe's cc, cc's wasm-opt) would
224
+ // stop a build the machine can hold, so a parent with no live child is
225
+ // admitted under the count alone. The page's own spawns have parent 0.
226
+ #spawnWait(parent) {
227
+ if (this.processes.size >= processWorkerLimit) return "count";
228
+ let committed = 0, fanning = false;
229
+ for (const process of this.processes.values()) {
230
+ committed += process.memory?.buffer.byteLength ?? 0;
231
+ fanning ||= process.parent === parent && !process.retiring;
232
+ }
233
+ return committed < processMemoryBudgetBytes || !fanning ? null : "budget";
234
+ }
235
+
236
+ spawn(arguments_, { foreground = false } = {}) {
237
+ if (this.#spawnWait(0)) throw new Error("Dolly process limit reached");
238
+ const packet = encodeSpawn(arguments_, foreground ? DOLLY_PROCESS_SPAWN_FOREGROUND : 0);
239
+ new Uint8Array(
240
+ this.kernelMemory.buffer,
241
+ this.mailboxAddress,
242
+ packet.length,
243
+ ).set(packet);
244
+ const pid = this.kernel.dolly_process_spawn_serialized(BigInt(packet.length));
245
+ if (pid < 0) {
246
+ const errno = Object.keys(DOLLY_ERRNO).find(name => DOLLY_ERRNO[name] === -pid) ?? `errno ${-pid}`;
247
+ throw new Error(`cannot start ${arguments_[0]}: ${errno}`);
248
+ }
249
+ const completion = new Promise((resolve, reject) => {
250
+ this.#registerProcess(pid, 0, resolve, reject);
251
+ });
252
+ this.#scheduleLaunches();
253
+ return completion;
254
+ }
255
+
256
+ #registerProcess(pid, parent, resolve = null, reject = null) {
257
+ const flags = this.kernel.dolly_process_spawn_flags(pid);
258
+ if (pid <= 0 || this.processes.has(pid) || flags < 0 ||
259
+ this.kernel.dolly_process_parent(pid) !== parent) {
260
+ throw new Error(`kernel supplied invalid process ${pid}`);
261
+ }
262
+ const process = {
263
+ pid, parent, resolve, reject, worker: null, gate: null, control: null,
264
+ memory: null, messageHandler: null, errorHandler: null,
265
+ messageErrorHandler: null, started: false,
266
+ failure: null, interactive: (flags & DOLLY_PROCESS_SPAWN_INTERACTIVE) !== 0, retiring: false,
267
+ interruptTimer: null, deadlineTimer: null, retirementTimer: null, lastSignal: 0,
268
+ reclamationDeadline: 0, tid: 0, threads: new Map(), threaded: false,
269
+ execution: null, signalAtStart: 0,
270
+ };
271
+ this.processes.set(pid, process);
272
+ this.#armDeadline(process);
273
+ }
274
+
275
+ #descendantDepth(process, ancestorPid) {
276
+ let candidate = process;
277
+ for (let depth = 0; depth < this.processes.size; ++depth) {
278
+ if (candidate.parent === ancestorPid) return depth + 1;
279
+ if (candidate.parent === 0) return 0;
280
+ candidate = this.processes.get(candidate.parent);
281
+ if (!candidate) return 0;
282
+ }
283
+ return 0;
284
+ }
285
+
286
+ #serviceTick() {
287
+ this.serviceHost();
288
+ const interrupted = this.kernel.dolly_process_take_interrupt();
289
+ if (interrupted > 0) this.#interruptForeground(interrupted);
290
+ for (let pid; (pid = this.kernel.dolly_process_take_alarm()) > 0;) {
291
+ this.#deliverSignal(this.processes.get(pid), DOLLY_PROCESS_SIGALRM);
292
+ }
293
+ this.#deliverRaised();
294
+ this.serviceDeferred();
295
+ }
296
+
297
+ // What a pseudo-terminal raised for the programs on its slave
298
+ // (host/pty/dolly-pty-supervisor-0.wat): its interrupt key, resize or hang-up.
299
+ #deliverRaised() {
300
+ for (let pid; (pid = this.kernel.dolly_pty_take_signal()) > 0;) {
301
+ this.#deliverSignal(this.processes.get(pid), this.kernel.dolly_pty_taken_signal());
302
+ }
303
+ }
304
+
305
+ #interruptForeground(pid) {
306
+ const process = this.processes.get(pid);
307
+ if (!process || process.retiring) return;
308
+ const descendants = [...this.processes.values()].filter(
309
+ (candidate) => !candidate.retiring && candidate.pid !== pid &&
310
+ this.#descendantDepth(candidate, pid) !== 0,
311
+ );
312
+ // Like a job-control shell, an interactive owner's running commands take
313
+ // SIGINT in its place; without any it takes SIGINT itself.
314
+ for (const target of process.interactive && descendants.length ? descendants : [process, ...descendants]) {
315
+ this.#deliverSignal(target);
316
+ }
317
+ }
318
+
319
+ #deliverSignal(process, signalNumber = DOLLY_PROCESS_SIGINT) {
320
+ if (!process || process.retiring || this.processes.get(process.pid) !== process) return false;
321
+ const result = this.kernel.dolly_process_signal(process.pid, signalNumber);
322
+ process.lastSignal = signalNumber;
323
+ // Resize notification is not an interrupt request and must never acquire
324
+ // a forced-termination deadline, including before program entry.
325
+ if (signalNumber === DOLLY_PROCESS_SIGWINCH && (result !== 0 || !process.started)) return result === 0;
326
+ if (signalNumber === DOLLY_PROCESS_SIGKILL || result !== 0 || !process.started) {
327
+ return this.#forceExit(process.pid, 128 + signalNumber, signalNumber);
328
+ }
329
+ const receiver = process.threaded
330
+ ? [...process.threads.values()].sort((a, b) => a.tid - b.tid)[0] : process;
331
+ const deferred = this.deferred.get(receiver);
332
+ if (deferred) {
333
+ this.#clearDeferred(receiver);
334
+ this.#signal(receiver, deferred.message.sequence, interruptedSystemCall);
335
+ }
336
+ // The old program takes the signal and calls EXEC again, as after any
337
+ // interrupted call; a resize waits for the new program.
338
+ if (process.execution?.thread === receiver && signalNumber !== DOLLY_PROCESS_SIGWINCH) {
339
+ const { message } = process.execution;
340
+ process.execution = null;
341
+ this.kernel.dolly_process_exec_refused(process.pid, 0n);
342
+ this.#signal(receiver, message.sequence, interruptedSystemCall);
343
+ }
344
+ if (signalNumber !== DOLLY_PROCESS_SIGWINCH) this.#armInterrupt(process, signalNumber);
345
+ return true;
346
+ }
347
+
348
+ // A program that does not finish a delivered signal in time is ended by it.
349
+ #armInterrupt(process, signalNumber) {
350
+ if (process.interruptTimer !== null) return;
351
+ process.interruptTimer = setTimeout(() => {
352
+ process.interruptTimer = null;
353
+ this.#forceExit(process.pid, 128 + signalNumber, signalNumber);
354
+ }, interruptGraceMilliseconds);
355
+ }
356
+
357
+ #scheduleLaunches() {
358
+ this.launchChain = this.launchChain
359
+ .then(() => this.#launchPending())
360
+ .catch((error) => {
361
+ // Only a kernel that cannot name its next launch reaches this point.
362
+ for (const process of [...this.processes.values()]) this.#fail(process, error);
363
+ });
364
+ }
365
+
366
+ // The module for the executable the kernel opened for a process: the one
367
+ // compiled from the same version of its file's bytes, which takes no read,
368
+ // or one compiled from the bytes, found by their SHA-256 or compiled now.
369
+ async #compiledImage(pid) {
370
+ const version = BigInt(this.kernel.dolly_process_image_version(pid));
371
+ const key = this.compiledVersions.get(version);
372
+ if (version !== 0n && this.compiledModules.has(key)) {
373
+ if (this.kernel.dolly_process_image_consumed(pid) !== 0) {
374
+ throw new Error(`kernel did not release executable ${pid}`);
375
+ }
376
+ return this.#cachedModule(key);
377
+ }
378
+ const compiled = await this.#compileProcess(this.#takeImage(pid));
379
+ if (version !== 0n) {
380
+ this.compiledVersions.delete(version);
381
+ this.compiledVersions.set(version, compiled.key);
382
+ if (this.compiledVersions.size > compiledVersionEntries) {
383
+ this.compiledVersions.delete(this.compiledVersions.keys().next().value);
384
+ }
385
+ }
386
+ return compiled;
387
+ }
388
+
389
+ // A cached module, made the most recently used.
390
+ #cachedModule(key) {
391
+ const cached = this.compiledModules.get(key);
392
+ this.compiledModules.delete(key);
393
+ this.compiledModules.set(key, cached);
394
+ return cached;
395
+ }
396
+
397
+ async #compileProcess(bytes) {
398
+ const digest = hex(await crypto.subtle.digest("SHA-256", bytes));
399
+ const key = `${bytes.byteLength}:${digest}`;
400
+ if (this.compiledModules.has(key)) return this.#cachedModule(key);
401
+
402
+ const started = performance.now();
403
+ let noticeShown = false;
404
+ const notice = setTimeout(() => {
405
+ noticeShown = true;
406
+ const mebibytes = (bytes.byteLength / (1024 * 1024)).toFixed(1);
407
+ this.#writeTerminal(
408
+ `\r\ndolly: preparing ${mebibytes} MiB WebAssembly executable for this session...\r\n`,
409
+ );
410
+ }, compilationNoticeMilliseconds);
411
+ let module;
412
+ let memoryRequirements;
413
+ let processInterface;
414
+ let threaded;
415
+ let largeStack;
416
+ let requirements;
417
+ let prepared = false;
418
+ try {
419
+ const parsed = parseWasmInterface(bytes);
420
+ memoryRequirements = validateProcessInterface(this.processContract, parsed, DOLLY_PROCESS_ABI_DIGEST);
421
+ requirements = executableHostRequirements(parsed);
422
+ checkHostAbi(requirements, this.hostAbi);
423
+ threaded = validateThreadProfile(parsed, requirements, this.threadContract);
424
+ largeStack = parsed.customSections.includes("dolly.process.stack");
425
+ module = await WebAssembly.compile(bytes);
426
+ processInterface = { imports: parsed.imports, exports: parsed.exports,
427
+ castEmulationParameters: parsed.castEmulationParameters };
428
+ prepared = true;
429
+ } finally {
430
+ clearTimeout(notice);
431
+ if (noticeShown) {
432
+ this.#writeTerminal(
433
+ prepared
434
+ ? `dolly: executable ready in ${((performance.now() - started) / 1000).toFixed(1)}s\r\n`
435
+ : "dolly: executable preparation failed\r\n",
436
+ );
437
+ }
438
+ }
439
+ const compiled = { key, module, memoryRequirements, processInterface, threaded, largeStack, requirements, byteLength: bytes.byteLength };
440
+ if (bytes.byteLength <= compiledModuleCacheBytes) {
441
+ this.compiledModules.set(key, compiled);
442
+ this.compiledModuleBytes += bytes.byteLength;
443
+ while (this.compiledModules.size > compiledModuleCacheEntries ||
444
+ this.compiledModuleBytes > compiledModuleCacheBytes) {
445
+ const oldestKey = this.compiledModules.keys().next().value;
446
+ const oldest = this.compiledModules.get(oldestKey);
447
+ this.compiledModules.delete(oldestKey);
448
+ this.compiledModuleBytes -= oldest.byteLength;
449
+ }
450
+ }
451
+ return compiled;
452
+ }
453
+
454
+ #writeTerminal(text) {
455
+ const bytes = encoder.encode(text);
456
+ const address = this.kernel.malloc(BigInt(Math.max(1, bytes.byteLength)));
457
+ if (address === 0 || address === 0n) return;
458
+ try {
459
+ new Uint8Array(
460
+ this.kernelMemory.buffer, Number(address), bytes.byteLength,
461
+ ).set(bytes);
462
+ this.kernel.dolly_terminal_write_bytes(
463
+ BigInt(address), BigInt(bytes.byteLength),
464
+ );
465
+ } finally {
466
+ this.kernel.free(BigInt(address));
467
+ }
468
+ }
469
+
470
+ async #launchPending() {
471
+ for (let pid; (pid = this.kernel.dolly_process_next_launch()) !== 0;) {
472
+ const process = this.processes.get(pid);
473
+ try {
474
+ if (!process) throw new Error(`kernel supplied unregistered process ${pid}`);
475
+ const compiled = await this.#compiledImage(pid);
476
+ if (!this.#canLaunch(process)) continue;
477
+ const local = this.#admit(compiled);
478
+ for (let waited = 0; ; waited += reservationRetryMilliseconds) {
479
+ try {
480
+ this.#start(process, compiled, local);
481
+ break;
482
+ } catch (error) {
483
+ if (!(error instanceof RangeError) || process.memory || waited >= reservationPatienceMilliseconds) throw error;
484
+ }
485
+ await new Promise(resolve => setTimeout(resolve, reservationRetryMilliseconds));
486
+ if (!this.#canLaunch(process)) break;
487
+ }
488
+ } catch (error) {
489
+ // A failed launch ends only its own process; exiting it in the kernel
490
+ // also stops next_launch from naming it again.
491
+ if (process) this.#fail(process, error);
492
+ else this.kernel.dolly_process_worker_exited(pid, 126, 0);
493
+ }
494
+ }
495
+ }
496
+
497
+ // The bytes of the executable the kernel opened for a process, which it then closes.
498
+ #takeImage(pid) {
499
+ const read = this.kernel.dolly_process_image_read(pid);
500
+ if (read !== 0) throw new Error(`cannot read the executable of process ${pid}: ${read}`);
501
+ const address = Number(this.kernel.dolly_process_image_address(pid));
502
+ const size = Number(this.kernel.dolly_process_image_size(pid));
503
+ if (!Number.isSafeInteger(address) || !Number.isSafeInteger(size) ||
504
+ address <= 0 || size < 8 || address > this.kernelMemory.buffer.byteLength - size) {
505
+ throw new Error(`kernel supplied an invalid executable for process ${pid}`);
506
+ }
507
+ const bytes = new Uint8Array(size);
508
+ bytes.set(new Uint8Array(this.kernelMemory.buffer, address, size));
509
+ if (this.kernel.dolly_process_image_consumed(pid) !== 0) {
510
+ throw new Error(`kernel did not release executable ${pid}`);
511
+ }
512
+ return bytes;
513
+ }
514
+
515
+ // Checked at every launch, not only when compiled: nena@0 may have disabled
516
+ // a module since. The modules served in this executable's own Worker each
517
+ // hold one Worker's state, such as its function table: one goes with threads
518
+ // only where it serves every thread. Returns those modules.
519
+ #admit({ requirements, threaded }) {
520
+ checkHostAbi(requirements, this.hostAbi);
521
+ const local = [...requirements.keys()].filter(requirement => this.processModules.has(requirement));
522
+ const unthreaded = local.filter(requirement => !this.processModules.get(requirement).threads);
523
+ if (threaded && unthreaded.length) throw new TypeError(`a program using threads@0 cannot use ${unthreaded.join(", ")}`);
524
+ if (threaded && !this.threadHost) throw new Error("threads@0 is unavailable");
525
+ return local;
526
+ }
527
+
528
+ #start(process, { module, memoryRequirements, processInterface, threaded, largeStack }, local) {
529
+ const memory = createProcessMemory(memoryRequirements);
530
+ process.memory = memory;
531
+ const gate = new WebAssembly.Instance(this.gateModule, {
532
+ process: { memory },
533
+ kernel: { memory: this.kernelMemory },
534
+ });
535
+ Object.assign(process, { gate, module, processInterface, threaded, largeStack, local });
536
+ if (threaded) {
537
+ const tid = this.kernel.dolly_threads_attach(process.pid);
538
+ if (tid <= 0) throw new Error(`kernel could not reserve main thread: ${tid}`);
539
+ process.tid = tid;
540
+ process.threads.set(tid, process);
541
+ }
542
+ this.#launchWorker(process, process);
543
+ }
544
+
545
+ // EXEC: the kernel has read the new program and the old one waits in the
546
+ // call while it is compiled and admitted. A refusal returns to the old
547
+ // program with ENOEXEC, after one line on its stderr; past the commit the
548
+ // old program's Workers go and the process starts again, its pid and
549
+ // record kept.
550
+ async #exec(process, thread, message) {
551
+ const execution = { thread, message };
552
+ process.execution = execution;
553
+ let compiled, local;
554
+ try {
555
+ compiled = await this.#compiledImage(process.pid);
556
+ if (process.execution === execution) local = this.#admit(compiled);
557
+ } catch (error) {
558
+ if (process.execution !== execution) return;
559
+ process.execution = null;
560
+ const line = encoder.encode(`dolly: process ${process.pid} cannot run its new program: ` +
561
+ `${terminalFailureReason(error)}\n`);
562
+ new Uint8Array(this.kernelMemory.buffer, this.mailboxAddress, line.length).set(line);
563
+ this.kernel.dolly_process_exec_refused(process.pid, BigInt(line.length));
564
+ this.#signal(thread, message.sequence, -BigInt(DOLLY_ERRNO.ENOEXEC));
565
+ return;
566
+ }
567
+ if (process.execution !== execution) return;
568
+ process.execution = null;
569
+ const signalAtStart = this.kernel.dolly_process_exec_commit(process.pid);
570
+ if (signalAtStart < 0) {
571
+ this.#fail(process, new Error(`kernel rejected process ${process.pid} exec: ${signalAtStart}`));
572
+ return;
573
+ }
574
+ // The old program's memory goes with its Workers. Termination has no
575
+ // completion event: a large interactive one gets the time it gets before
576
+ // a parent may start its replacement (#reclamationDeadline).
577
+ const delay = this.#reclamationDeadline(process) - performance.now();
578
+ if (process.interruptTimer !== null) clearTimeout(process.interruptTimer);
579
+ process.interruptTimer = null;
580
+ this.#disposeWorker(process);
581
+ Object.assign(process, { started: false, threaded: false, tid: 0, threads: new Map(), signalAtStart });
582
+ // Its descriptors with CLOEXEC are closed: a reader of their pipes goes on now.
583
+ this.#deliverRaised();
584
+ if (this.kernel.dolly_process_take_wakeup()) this.serviceDeferred();
585
+ const start = () => {
586
+ if (!this.#canLaunch(process)) return;
587
+ try { this.#start(process, compiled, local); } catch (error) { this.#fail(process, error); }
588
+ };
589
+ if (delay > 0) setTimeout(start, delay);
590
+ else start();
591
+ }
592
+
593
+ #launchWorker(process, thread, argument = undefined) {
594
+ const { pid, module, memory, processInterface, threaded, local } = process;
595
+ const control = new SharedArrayBuffer(16);
596
+ const name = `dolly-process-${pid}-thread-${thread.tid}`;
597
+ const worker = threaded ? this.threadHost.create(pid, this.workerUrl, name)
598
+ : new Worker(this.workerUrl, { type: "module", name });
599
+ if (!worker) throw new Error("thread Worker capacity exhausted");
600
+ thread.worker = worker;
601
+ thread.control = new Int32Array(control);
602
+ thread.messageHandler = event => this.#message(process, event.data, thread);
603
+ thread.errorHandler = event => this.#fail(process, new Error(event.message || `${name} failed`));
604
+ thread.messageErrorHandler = () => this.#fail(process, new Error(`${name} message could not be decoded`));
605
+ worker.addEventListener("message", thread.messageHandler);
606
+ worker.addEventListener("error", thread.errorHandler, { once: true });
607
+ worker.addEventListener("messageerror", thread.messageErrorHandler, { once: true });
608
+ // local: the modules this executable records that are served in its own
609
+ // Worker (host/dso), each as the bundle it imports and its configuration.
610
+ worker.postMessage({ type: "configure", pid, tid: thread.tid, argument, threaded, module, memory, control,
611
+ promisingEntry: this.promisingEntry && process.largeStack, stackDepths: this.stackDepths,
612
+ clockOrigin: performance.timeOrigin, processInterface, local: local.map(requirement => this.processModules.get(requirement)) });
613
+ }
614
+
615
+ #canLaunch(process) {
616
+ if (this.processes.get(process.pid) !== process || process.retiring) return false;
617
+ // An ancestor's EXIT can reach the kernel before its Worker posts finished.
618
+ if (this.kernel.dolly_process_exited(process.pid) !== 0) {
619
+ this.#retire(process);
620
+ return false;
621
+ }
622
+ return true;
623
+ }
624
+
625
+ #signal(process, sequence, result) {
626
+ const encoded = BigInt.asUintN(64, result);
627
+ Atomics.store(process.control, 2, Number(encoded & 0xffffffffn));
628
+ Atomics.store(process.control, 3, Number((encoded >> 32n) & 0xffffffffn));
629
+ Atomics.store(process.control, 1, sequence);
630
+ Atomics.notify(process.control, 1);
631
+ }
632
+
633
+ #armDeadline(process) {
634
+ const remaining = this.kernel.dolly_process_deadline_remaining(process.pid);
635
+ if (remaining >= 0) {
636
+ process.deadlineTimer = setTimeout(() => this.#endTree(process.pid, 124), Math.ceil(remaining));
637
+ }
638
+ }
639
+
640
+ #clearTimers(process) {
641
+ if (process.interruptTimer !== null) clearTimeout(process.interruptTimer);
642
+ if (process.deadlineTimer !== null) clearTimeout(process.deadlineTimer);
643
+ if (process.retirementTimer !== null) clearTimeout(process.retirementTimer);
644
+ process.interruptTimer = null;
645
+ process.deadlineTimer = null;
646
+ process.retirementTimer = null;
647
+ }
648
+
649
+ #clearDeferred(thread) {
650
+ const pending = this.deferred.get(thread);
651
+ if (!pending) return;
652
+ clearTimeout(pending.timer);
653
+ this.deferred.delete(thread);
654
+ }
655
+
656
+ #syscall(process, message, retry = false, thread = process) {
657
+ const refusedSince = this.deferred.get(thread)?.refusedSince;
658
+ this.#clearDeferred(thread);
659
+ const values = [
660
+ message.sequence,
661
+ message.operation,
662
+ message.requestAddress,
663
+ message.requestSize,
664
+ message.responseAddress,
665
+ message.responseCapacity,
666
+ ];
667
+ if (message.pid !== process.pid || message.tid !== thread.tid ||
668
+ values.some((value) => !Number.isSafeInteger(value) || value < 0) ||
669
+ message.requestSize > packetLimit || message.responseCapacity > packetLimit ||
670
+ Atomics.load(thread.control, 0) !== message.sequence ||
671
+ Atomics.load(thread.control, 1) === message.sequence) {
672
+ this.#fail(process, new Error(`process ${process.pid} sent an invalid syscall`));
673
+ return;
674
+ }
675
+ let result;
676
+ let signalDelivery;
677
+ let threadArgument;
678
+ try {
679
+ process.gate.exports.request(
680
+ BigInt(message.requestAddress),
681
+ BigInt(this.mailboxAddress),
682
+ BigInt(message.requestSize),
683
+ );
684
+ if (message.operation === DOLLY_THREAD_SPAWN && message.requestSize === 8)
685
+ threadArgument = new DataView(this.kernelMemory.buffer, this.mailboxAddress, 8).getBigUint64(0, true);
686
+ const args = [process.pid, message.operation, BigInt(message.requestSize), BigInt(message.responseCapacity)];
687
+ if (process.threaded) args.splice(1, 0, thread.tid);
688
+ const waiting = message.operation === DOLLY_PROCESS_SPAWN ? this.#spawnWait(process.pid) : null;
689
+ const now = performance.now(), since = waiting === "count" ? refusedSince ?? now : undefined;
690
+ if (waiting === "budget" || waiting === "count" && now - since < reservationPatienceMilliseconds) {
691
+ this.deferred.set(thread, { process, thread, message, timer: null, refusedSince: since });
692
+ return;
693
+ }
694
+ const exhausted = waiting === "count" ||
695
+ process.threaded && message.operation === DOLLY_THREAD_SPAWN && !this.threadHost?.available(process.pid);
696
+ // Firefox can enter a direct Wasm call twice. Use the generic call path
697
+ // so a completed syscall is never replayed against its response packet.
698
+ result = exhausted ? -BigInt(DOLLY_ERRNO.EAGAIN)
699
+ : Reflect.apply(process.threaded ? this.kernel.dolly_threads_dispatch : this.kernel.dolly_process_dispatch,
700
+ this.kernel, args);
701
+ if (result === deferredResult) {
702
+ const deferred = { process, thread, message, timer: null };
703
+ this.deferred.set(thread, deferred);
704
+ const remaining = this.kernel.dolly_process_deferred_milliseconds();
705
+ if (remaining >= 0 && remaining <= 16) {
706
+ deferred.timer = setTimeout(() => {
707
+ if (this.deferred.get(thread) === deferred) this.#syscall(process, message, true, thread);
708
+ }, Math.ceil(remaining));
709
+ }
710
+ return;
711
+ }
712
+ if (message.operation === DOLLY_PROCESS_EXEC && message.requestSize !== 0 && result === 0n) {
713
+ this.#exec(process, thread, message).catch(error => this.#fail(process, error));
714
+ return;
715
+ }
716
+ if (message.operation === DOLLY_PROCESS_EXIT && result === 0n) {
717
+ // The kernel reclaimed every thread. Stop the others now; the caller
718
+ // unwinds and reports "finished" like a single-threaded process.
719
+ for (const other of [...process.threads.values()]) {
720
+ if (other !== thread) this.#disposeThread(process, other);
721
+ }
722
+ } else if (result >= 0n) {
723
+ if (result > BigInt(message.responseCapacity)) {
724
+ throw new Error(`kernel overfilled process ${process.pid} response`);
725
+ }
726
+ process.gate.exports.response(
727
+ BigInt(this.mailboxAddress),
728
+ BigInt(message.responseAddress),
729
+ result,
730
+ );
731
+ if (message.operation === DOLLY_THREAD_SPAWN) {
732
+ if (result !== 8n || threadArgument === undefined) throw new Error("invalid kernel thread response");
733
+ const response = new DataView(this.kernelMemory.buffer, this.mailboxAddress, 8);
734
+ const tid = response.getUint32(0, true);
735
+ if (!tid || tid > 0x7fffffff || response.getUint32(4, true) || process.threads.has(tid))
736
+ throw new Error("invalid kernel thread identity");
737
+ const child = { tid, worker: null, control: null };
738
+ process.threads.set(tid, child);
739
+ try { this.#launchWorker(process, child, threadArgument); }
740
+ catch (error) {
741
+ this.#disposeThread(process, child);
742
+ this.kernel.dolly_threads_unstarted(process.pid, tid);
743
+ result = -BigInt(DOLLY_ERRNO.EAGAIN);
744
+ }
745
+ }
746
+ if (message.operation === DOLLY_PROCESS_SPAWN) {
747
+ if (result !== 8n) throw new Error("invalid kernel spawn response");
748
+ const response = new DataView(this.kernelMemory.buffer, this.mailboxAddress, 8);
749
+ const pid = response.getUint32(0, true);
750
+ if (pid === 0 || pid > 0x7fffffff || response.getUint32(4, true) !== 0) {
751
+ throw new Error("invalid kernel spawn identity");
752
+ }
753
+ this.#registerProcess(pid, process.pid);
754
+ }
755
+ if (message.operation === DOLLY_PROCESS_SIGNAL) {
756
+ if (result !== 8n) throw new Error("invalid kernel signal response");
757
+ const response = new DataView(this.kernelMemory.buffer, this.mailboxAddress, 8);
758
+ signalDelivery = { pid: response.getUint32(0, true), signal: response.getUint32(4, true) };
759
+ if (signalDelivery.pid === 0 || signalDelivery.pid > 0x7fffffff) {
760
+ throw new Error("invalid kernel signal target");
761
+ }
762
+ }
763
+ }
764
+ } catch (error) {
765
+ this.#fail(process, error);
766
+ return;
767
+ }
768
+ this.#signal(thread, message.sequence, result);
769
+ if (message.operation === DOLLY_PROCESS_SIGNAL_ACKNOWLEDGE && result === 4n &&
770
+ new DataView(this.kernelMemory.buffer, this.mailboxAddress, 4).getInt32(0, true) === 0 &&
771
+ process.interruptTimer !== null) {
772
+ clearTimeout(process.interruptTimer);
773
+ process.interruptTimer = null;
774
+ }
775
+ if (signalDelivery?.signal) {
776
+ this.#deliverSignal(this.processes.get(signalDelivery.pid), signalDelivery.signal);
777
+ }
778
+ this.#deliverRaised();
779
+ // A spawn that waited for admission is launched like any other.
780
+ if (!retry || message.operation === DOLLY_PROCESS_SPAWN) this.#scheduleLaunches();
781
+ if (retry) return;
782
+ // A pipe write or close can complete another thread's blocked read or poll,
783
+ // such as an async runtime's waker: retry now, not at the next tick.
784
+ if (this.kernel.dolly_process_take_wakeup()) this.serviceDeferred();
785
+ }
786
+
787
+ #message(process, message, thread = process) {
788
+ if (this.processes.get(process.pid) !== process || process.retiring ||
789
+ (process.threaded && process.threads.get(thread.tid) !== thread)) return;
790
+ if (message?.pid !== process.pid || message?.tid !== thread.tid) {
791
+ this.#fail(process, new Error(`process ${process.pid} sent a mismatched identity`));
792
+ } else if (message.type === "started") {
793
+ if (thread !== process) return;
794
+ if (this.kernel.dolly_process_worker_started(process.pid) !== 0) {
795
+ this.#fail(process, new Error(`kernel rejected process ${process.pid} start`));
796
+ } else {
797
+ process.started = true;
798
+ // A signal pending across EXEC is the new program's to answer.
799
+ if (process.signalAtStart) this.#armInterrupt(process, process.signalAtStart);
800
+ process.signalAtStart = 0;
801
+ }
802
+ } else if (message.type === "syscall") {
803
+ this.#syscall(process, message, false, thread);
804
+ } else if (message.type === "thread-finished") {
805
+ if (!process.threaded || typeof message.result !== "bigint") {
806
+ this.#fail(process, new Error("invalid thread completion")); return;
807
+ }
808
+ // The trusted Worker wrapper has unwound Wasm and will never re-enter it.
809
+ // Remove its event handlers/resources before making its stack joinable.
810
+ this.#disposeThread(process, thread);
811
+ const last = this.kernel.dolly_threads_retired(process.pid, thread.tid, message.result);
812
+ if (last < 0) this.#fail(process, new Error("kernel rejected thread retirement"));
813
+ else if (last) this.#forceExit(process.pid, 0);
814
+ else this.serviceDeferred();
815
+ } else if (message.type === "finished") {
816
+ // _start returned (status zero), or EXIT has already recorded the status.
817
+ this.kernel.dolly_process_worker_exited(process.pid, 0, 0);
818
+ this.#retire(process);
819
+ } else if (message.type === "failed") {
820
+ const detail = message.stack ? `${message.message}\n${message.stack}` : message.message;
821
+ this.#fail(process, new Error(detail || `process ${process.pid} failed`));
822
+ } else {
823
+ this.#fail(process, new Error(`process ${process.pid} sent an unknown message`));
824
+ }
825
+ }
826
+
827
+ #reclamationDeadline(process) {
828
+ return Math.max(process.reclamationDeadline,
829
+ process.interactive && (process.memory?.buffer.byteLength ?? 0) >= largeInteractiveProcessBytes
830
+ ? performance.now() + workerReclamationMilliseconds : 0);
831
+ }
832
+
833
+ #retire(process) {
834
+ process.retiring = true;
835
+ // A process a signal ends passes it to its descendants, as a signal to its
836
+ // process group would: Dolly has no groups, so the command of slop -c is
837
+ // the shell's child unless it is exec'd. Each answers it as it handles it.
838
+ // A descendant that has it already, as Ctrl-C gives it to the whole
839
+ // foreground tree, is not given it twice.
840
+ const signal = this.kernel.dolly_process_exit_signal(process.pid);
841
+ if (signal !== 0) {
842
+ for (const descendant of [...this.processes.values()]) {
843
+ if (descendant.lastSignal !== signal && this.#descendantDepth(descendant, process.pid) !== 0) {
844
+ this.#deliverSignal(descendant, signal);
845
+ }
846
+ }
847
+ }
848
+ // Its children live on: the kernel has given them parent 0.
849
+ for (const child of this.processes.values()) if (child.parent === process.pid) child.parent = 0;
850
+ process.reclamationDeadline = this.#reclamationDeadline(process);
851
+ this.#stop(process);
852
+ // The kernel has closed the process's descriptors: a reader of its pipe or
853
+ // a waiter for its lock proceeds now, not at the next tick.
854
+ this.#deliverRaised();
855
+ if (this.kernel.dolly_process_take_wakeup()) this.serviceDeferred();
856
+ const retired = () => {
857
+ process.retirementTimer = null;
858
+ if (this.processes.get(process.pid) !== process) return;
859
+ this.#acknowledgeRetirement(process);
860
+ if (process.resolve) this.#finish(process);
861
+ else {
862
+ // An orphan, whose parent may have ended after this one did: nobody
863
+ // waits for it, so it is reaped, as init would.
864
+ if (this.kernel.dolly_process_parent(process.pid) === 0) this.kernel.dolly_process_collect(process.pid);
865
+ this.serviceDeferred();
866
+ }
867
+ };
868
+ // Worker.terminate() has no completion event. Drop all references, then allow
869
+ // one bounded reclamation window for large interactive memories before WAIT
870
+ // can let a surviving parent launch their replacement.
871
+ const delay = process.reclamationDeadline - performance.now();
872
+ if (delay > 0) process.retirementTimer = setTimeout(retired, delay);
873
+ else retired();
874
+ }
875
+
876
+ #acknowledgeRetirement(process) {
877
+ const result = this.kernel.dolly_process_worker_retired(process.pid);
878
+ if (result !== 0) {
879
+ throw new Error(`kernel rejected process ${process.pid} retirement: ${result}`);
880
+ }
881
+ this.processes.delete(process.pid);
882
+ }
883
+
884
+ // Resolves how a root process ended, as the kernel recorded it: whoever ended
885
+ // it (the program, the supervisor or the kernel itself), the wait record and
886
+ // this are one fact.
887
+ #finish(process) {
888
+ const ending = this.kernel.dolly_process_collect(process.pid);
889
+ const status = ending < 0 ? ending : ending & 255;
890
+ // The modules see what the process left, such as its last terminal
891
+ // output, before anyone learns that it ended.
892
+ this.serviceHost();
893
+ if (process.failure) {
894
+ process.failure.status = status;
895
+ process.reject(process.failure);
896
+ } else if (!Number.isInteger(ending) || ending < 0) {
897
+ process.reject(new Error(`kernel returned invalid status for process ${process.pid}`));
898
+ } else {
899
+ process.resolve({ status, signal: ending >> 8 });
900
+ }
901
+ }
902
+
903
+ // A refusal or failure is reported to the program that asked: one line on
904
+ // the process's own stderr, then status 126.
905
+ #fail(process, error) {
906
+ if (process.retiring || this.processes.get(process.pid) !== process) return;
907
+ const detail = error instanceof Error ? error : new Error(String(error));
908
+ const reason = terminalFailureReason(detail);
909
+ const line = encoder.encode(`dolly: process ${process.pid} ` +
910
+ `${process.started ? "failed" : "was refused"}: ${reason}\n`);
911
+ new Uint8Array(this.kernelMemory.buffer, this.mailboxAddress, line.length).set(line);
912
+ this.kernel.dolly_process_worker_failed(process.pid, BigInt(line.length));
913
+ detail.message = `Dolly process ${process.pid} failed: ${detail.message}`;
914
+ process.failure = Object.assign(detail, { reason });
915
+ if (!process.reject) console.error(detail.stack ?? detail.message);
916
+ this.#retire(process);
917
+ }
918
+
919
+ #forceExit(pid, status, signalNumber = 0) {
920
+ const process = this.processes.get(pid);
921
+ if (!process) return false;
922
+ if (process.retiring) return true;
923
+ this.kernel.dolly_process_worker_exited(pid, status, signalNumber);
924
+ this.#retire(process);
925
+ return true;
926
+ }
927
+
928
+ // A timed command's deadline ends its tree, deepest first, as timeout(1)
929
+ // signals the command's process group.
930
+ #endTree(pid, status) {
931
+ const descendants = [...this.processes.values()]
932
+ .map((process) => ({ process, depth: this.#descendantDepth(process, pid) }))
933
+ .filter(({ depth }) => depth !== 0)
934
+ .sort((left, right) => right.depth - left.depth)
935
+ .map(({ process }) => process);
936
+ for (const descendant of descendants) {
937
+ this.#forceExit(descendant.pid, 128 + DOLLY_PROCESS_SIGKILL, DOLLY_PROCESS_SIGKILL);
938
+ }
939
+ this.#forceExit(pid, status);
940
+ }
941
+
942
+ #disposeThread(process, thread) {
943
+ this.#clearDeferred(thread);
944
+ if (thread.worker) {
945
+ thread.worker.removeEventListener("message", thread.messageHandler);
946
+ thread.worker.removeEventListener("error", thread.errorHandler);
947
+ thread.worker.removeEventListener("messageerror", thread.messageErrorHandler);
948
+ // Chrome keeps a Worker terminated inside Atomics.wait, and its memory,
949
+ // for two seconds. Answered with the deferred result, which no program
950
+ // is handed, a waiting Worker leaves Wasm and closes itself.
951
+ if (thread.control) this.#signal(thread, Atomics.load(thread.control, 0), deferredResult);
952
+ // threads@0 disabled since has already terminated its Workers.
953
+ if (process.threaded && this.threadHost) this.threadHost.release(thread.worker);
954
+ else thread.worker.terminate();
955
+ }
956
+ thread.worker = null;
957
+ thread.control = null;
958
+ thread.messageHandler = thread.errorHandler = thread.messageErrorHandler = null;
959
+ process.threads.delete(thread.tid);
960
+ }
961
+
962
+ // Ends a process's Workers and timers; the kernel keeps its record until collected.
963
+ #stop(process) {
964
+ process.retiring = true;
965
+ process.execution = null;
966
+ this.#clearTimers(process);
967
+ this.#disposeWorker(process);
968
+ }
969
+
970
+ #disposeWorker(process) {
971
+ for (const thread of [...process.threads.values()]) this.#disposeThread(process, thread);
972
+ this.#disposeThread(process, process);
973
+ process.gate = process.memory = process.module = process.processInterface = null;
974
+ }
975
+
976
+ serviceDeferred() {
977
+ for (const deferred of [...this.deferred.values()]) {
978
+ const { process, thread, message } = deferred;
979
+ // An earlier retry in this pass may have ended this thread: a completed
980
+ // EXIT stops the other threads of its process.
981
+ if (this.deferred.get(thread) !== deferred) continue;
982
+ if (this.processes.get(process.pid) === process && !process.retiring) {
983
+ this.#syscall(process, message, true, thread);
984
+ } else this.#clearDeferred(thread);
985
+ }
986
+ }
987
+
988
+ dispose() {
989
+ clearInterval(this.serviceTimer);
990
+ for (const process of this.processes.values()) {
991
+ this.#stop(process);
992
+ process.reject?.(new Error("Dolly runtime closed"));
993
+ }
994
+ this.processes.clear();
995
+ this.deferred.clear();
996
+ this.compiledModules.clear();
997
+ this.compiledModuleBytes = 0;
998
+ this.compiledVersions.clear();
999
+ this.releaseWorkerSource?.();
1000
+ this.releaseWorkerSource = undefined;
1001
+ }
1002
+ }