@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.
- package/README.md +51 -78
- package/coi-serviceworker.js +23 -8
- package/dist/dolly-build-id.mjs +1 -1
- package/dist/dolly-image-build-id.mjs +1 -1
- package/dist/dolly-process-0.wasm +0 -0
- package/dist/dolly-process-abi.mjs +1 -1
- package/dist/dolly-process-dso.mjs +19 -11
- package/dist/dolly-process-worker.mjs +88 -33
- package/dist/dolly-runtime-worker.mjs +1544 -0
- package/dist/dolly.wasm +0 -0
- package/host/dso/dso.mjs +2 -2
- package/host/dso/module.json +4 -1
- package/host/http/abi.mjs +3 -1
- package/host/http/broker.mjs +17 -7
- package/host/http/http.mjs +19 -5
- package/host/http/module.json +3 -1
- package/host/http/policy.mjs +13 -6
- package/host/http/refusals.mjs +77 -0
- package/host/input/abi.mjs +1 -1
- package/host/modules.mjs +64 -23
- package/host/nena/module.json +2 -1
- package/host/nena/nena.mjs +2 -0
- package/host/packages/abi.mjs +27 -0
- package/host/packages/module.json +6 -4
- package/host/packages/packages.mjs +23 -8
- package/host/packages/service.mjs +46 -20
- package/host/packages/transport.mjs +123 -0
- package/host/packages/worker.mjs +13 -9
- package/host/requirements.mjs +1 -1
- package/host/runtime/abi.mjs +32 -0
- package/host/runtime/lease-bridge.mjs +13 -8
- package/host/runtime/module.json +3 -3
- package/host/runtime/runtime.mjs +3 -3
- package/host/snapshot/abi.mjs +2 -1
- package/host/snapshot/module.json +1 -1
- package/host/snapshot/snapshot.mjs +80 -57
- package/host/snapshot/transport.mjs +10 -3
- package/host/snapshot/ui.mjs +64 -39
- package/host/stack/module.json +1 -0
- package/host/stack/stack.mjs +1 -1
- package/host/threads/module.json +6 -1
- package/host/threads/threads.mjs +32 -13
- package/package.json +2 -2
- package/{src → page}/browser.mjs +37 -23
- package/page/generated.mjs +8 -0
- package/{src → page}/image-entry.mjs +10 -6
- package/{src → page}/image-inputs.mjs +11 -0
- package/{src → page}/image-names.mjs +2 -2
- package/{src → page}/kernel-plugin.mjs +1 -1
- package/{src → page}/pack-store.mjs +1 -1
- package/{src → page}/packaged-snapshot.mjs +56 -46
- package/{src → page}/process-constants.mjs +72 -15
- package/{src → page}/process-supervisor.mjs +155 -227
- package/{src → page}/snapshot-records.mjs +63 -44
- package/{src → page}/static-asset.mjs +36 -20
- package/{src → page}/version.mjs +1 -1
- package/{src → page}/worker-image.mjs +20 -9
- package/{src → site}/build-worker.mjs +36 -25
- package/{src → site}/custom-image.mjs +2 -10
- package/{src → site}/dollyfile-graph.mjs +1 -1
- package/{src → site}/dollyfile-view.mjs +1 -1
- package/{src → site}/image-artifact.mjs +9 -8
- package/{src → site}/image-build.mjs +4 -4
- package/{src → site}/image-builder.mjs +5 -4
- package/site/offline.mjs +53 -0
- package/{src → site}/session-store.mjs +47 -38
- package/src/runtime-worker.mjs +0 -57
- /package/{src → page}/build-log.mjs +0 -0
- /package/{src → page}/images.mjs +0 -0
- /package/{src → page}/page-chords.mjs +0 -0
- /package/{src → page}/page-indicators.mjs +0 -0
- /package/{src → page}/process-abi.mjs +0 -0
- /package/{src → page}/sha256.mjs +0 -0
- /package/{src → page}/terminal-text.mjs +0 -0
- /package/{src → page}/wasm-interface.mjs +0 -0
- /package/{src → site}/alphabetical.mjs +0 -0
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { DOLLY_PROCESS_ABI_DIGEST } from "
|
|
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,
|
|
10
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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(
|
|
151
|
-
view.setUint32(
|
|
152
|
-
view.setUint32(
|
|
153
|
-
view.setUint32(
|
|
154
|
-
view.setBigUint64(
|
|
155
|
-
view.setBigUint64(
|
|
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 <
|
|
159
|
-
|
|
160
|
-
view.setUint32(
|
|
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
|
|
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,
|
|
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,
|
|
205
|
-
"dolly-process-gate-0.wasm", "dolly-process-0.wasm", "dolly-
|
|
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
|
-
|
|
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,
|
|
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
|
-
// (
|
|
229
|
-
// directly: a Codex build crashed Chrome's tab twice on that stack
|
|
230
|
-
//
|
|
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
|
|
247
|
-
// ran on them end here, each told why on its
|
|
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
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
332
|
-
// (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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 (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
process.
|
|
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
|
-
|
|
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
|
-
`\
|
|
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 =
|
|
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\
|
|
474
|
-
: "dolly: executable preparation failed\
|
|
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.
|
|
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
|
|
533
|
-
|
|
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("
|
|
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.
|
|
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
|
|
614
|
-
if (
|
|
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: ${
|
|
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()
|
|
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.#
|
|
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 (
|
|
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 (
|
|
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 ===
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
:
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
this.#
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
921
|
-
|
|
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
|
|
914
|
+
this.#recordExit(pid, status, signalNumber);
|
|
990
915
|
this.#retire(process);
|
|
991
916
|
return true;
|
|
992
917
|
}
|
|
993
918
|
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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 (
|
|
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
|
-
// (
|
|
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
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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"));
|