@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.
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/dolly-build-id.mjs +1 -0
- package/dist/dolly-dso-0.wasm +0 -0
- package/dist/dolly-image-build-id.mjs +1 -0
- package/dist/dolly-kernel-plugin-abi.mjs +2 -0
- package/dist/dolly-process-0.wasm +0 -0
- package/dist/dolly-process-abi.mjs +1 -0
- package/dist/dolly-process-dso.mjs +1575 -0
- package/dist/dolly-process-gate-0.wasm +0 -0
- package/dist/dolly-process-stack.mjs +50 -0
- package/dist/dolly-process-worker.mjs +275 -0
- package/dist/dolly-threads-0.wasm +0 -0
- package/dist/dolly.wasm +0 -0
- package/host/abi.mjs +5 -0
- package/host/dso/abi.mjs +13 -0
- package/host/dso/dso.mjs +26 -0
- package/host/dso/module.json +15 -0
- package/host/http/abi.mjs +28 -0
- package/host/http/broker.mjs +325 -0
- package/host/http/http.mjs +74 -0
- package/host/http/local-services.mjs +37 -0
- package/host/http/module.json +15 -0
- package/host/http/policy.mjs +293 -0
- package/host/modules.mjs +373 -0
- package/host/nena/abi.mjs +19 -0
- package/host/nena/module.json +15 -0
- package/host/nena/nena.mjs +131 -0
- package/host/packages/module.json +13 -0
- package/host/packages/packages.mjs +15 -0
- package/host/packages/service.mjs +59 -0
- package/host/requirements.mjs +62 -0
- package/host/runtime/abi.mjs +8 -0
- package/host/runtime/boot-files.mjs +45 -0
- package/host/runtime/grower.mjs +14 -0
- package/host/runtime/lease-bridge.mjs +65 -0
- package/host/runtime/module.json +16 -0
- package/host/runtime/runtime.mjs +209 -0
- package/host/snapshot/abi.mjs +16 -0
- package/host/snapshot/module.json +15 -0
- package/host/snapshot/snapshot.mjs +138 -0
- package/host/snapshot/transport.mjs +132 -0
- package/host/snapshot/ui.mjs +207 -0
- package/host/stack/abi.mjs +5 -0
- package/host/stack/module.json +17 -0
- package/host/stack/stack.mjs +19 -0
- package/host/threads/abi.mjs +8 -0
- package/host/threads/module.json +13 -0
- package/host/threads/threads.mjs +36 -0
- package/package.json +12 -4
- package/src/alphabetical.mjs +4 -0
- package/src/browser.mjs +237 -0
- package/src/build-log.mjs +30 -0
- package/src/image-entry.mjs +29 -0
- package/src/image-inputs.mjs +20 -0
- package/src/image-names.mjs +11 -0
- package/src/images.mjs +10 -0
- package/src/kernel-plugin.mjs +77 -0
- package/src/packaged-snapshot.mjs +175 -0
- package/src/page-chords.mjs +43 -0
- package/src/page-indicators.mjs +27 -0
- package/src/process-abi.mjs +69 -0
- package/src/process-constants.mjs +242 -0
- package/src/process-supervisor.mjs +1002 -0
- package/src/runtime-worker.mjs +55 -0
- package/src/session-store.mjs +198 -0
- package/src/snapshot-records.mjs +109 -0
- package/src/static-asset.mjs +97 -0
- package/src/terminal-text.mjs +72 -0
- package/src/version.mjs +3 -0
- package/src/wasm-interface.mjs +440 -0
- 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
|
+
}
|