@volter/browser-wali 0.1.4 → 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 +73 -0
- package/dist/browser-wali-command.d.ts +3 -0
- package/dist/browser-wali-command.d.ts.map +1 -1
- package/dist/browser-wali-command.js +2 -1
- package/dist/browser-wali-command.js.map +1 -1
- package/dist/capabilities.d.ts +73 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +86 -2
- package/dist/capabilities.js.map +1 -1
- package/dist/child-processes.d.ts +24 -0
- package/dist/child-processes.d.ts.map +1 -0
- package/dist/child-processes.js +134 -0
- package/dist/child-processes.js.map +1 -0
- package/dist/codec-protocol.d.ts +66 -0
- package/dist/codec-protocol.d.ts.map +1 -0
- package/dist/codec-protocol.js +59 -0
- package/dist/codec-protocol.js.map +1 -0
- package/dist/codec-remote.d.ts +33 -0
- package/dist/codec-remote.d.ts.map +1 -0
- package/dist/codec-remote.js +190 -0
- package/dist/codec-remote.js.map +1 -0
- package/dist/codec-worker.d.ts +2 -0
- package/dist/codec-worker.d.ts.map +1 -0
- package/dist/codec-worker.js +438 -0
- package/dist/codec-worker.js.map +1 -0
- package/dist/coherent-file-locks.d.ts.map +1 -1
- package/dist/coherent-file-locks.js +17 -5
- package/dist/coherent-file-locks.js.map +1 -1
- package/dist/descriptors.d.ts +49 -0
- package/dist/descriptors.d.ts.map +1 -0
- package/dist/descriptors.js +115 -0
- package/dist/descriptors.js.map +1 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +2 -2
- package/dist/events.js.map +1 -1
- package/dist/filesystem-protocol.d.ts +28 -1
- package/dist/filesystem-protocol.d.ts.map +1 -1
- package/dist/filesystem-protocol.js +18 -0
- package/dist/filesystem-protocol.js.map +1 -1
- package/dist/filesystem-remote.d.ts +26 -2
- package/dist/filesystem-remote.d.ts.map +1 -1
- package/dist/filesystem-remote.js +132 -23
- package/dist/filesystem-remote.js.map +1 -1
- package/dist/filesystem-worker.js +274 -34
- package/dist/filesystem-worker.js.map +1 -1
- package/dist/filesystem.d.ts +30 -1
- package/dist/filesystem.d.ts.map +1 -1
- package/dist/filesystem.js +180 -50
- package/dist/filesystem.js.map +1 -1
- package/dist/fork-worker.d.ts +2 -0
- package/dist/fork-worker.d.ts.map +1 -0
- package/dist/fork-worker.js +52 -0
- package/dist/fork-worker.js.map +1 -0
- package/dist/host.d.ts +36 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +153 -0
- package/dist/host.js.map +1 -0
- package/dist/loopback-host.d.ts +25 -1
- package/dist/loopback-host.d.ts.map +1 -1
- package/dist/loopback-host.js +447 -13
- package/dist/loopback-host.js.map +1 -1
- package/dist/memory.d.ts +65 -2
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +306 -49
- package/dist/memory.js.map +1 -1
- package/dist/module-layout.d.ts +2 -0
- package/dist/module-layout.d.ts.map +1 -1
- package/dist/module-layout.js +4 -2
- package/dist/module-layout.js.map +1 -1
- package/dist/monotonic-clock.d.ts +12 -0
- package/dist/monotonic-clock.d.ts.map +1 -0
- package/dist/monotonic-clock.js +14 -0
- package/dist/monotonic-clock.js.map +1 -0
- package/dist/network-protocol.d.ts +12 -1
- package/dist/network-protocol.d.ts.map +1 -1
- package/dist/network-protocol.js +6 -0
- package/dist/network-protocol.js.map +1 -1
- package/dist/network-worker.js +149 -20
- package/dist/network-worker.js.map +1 -1
- package/dist/network.d.ts +15 -3
- package/dist/network.d.ts.map +1 -1
- package/dist/network.js +74 -39
- package/dist/network.js.map +1 -1
- package/dist/process-adapter.d.ts +4 -1
- package/dist/process-adapter.d.ts.map +1 -1
- package/dist/process-adapter.js +3 -3
- package/dist/process-adapter.js.map +1 -1
- package/dist/process-bridge.d.ts +43 -0
- package/dist/process-bridge.d.ts.map +1 -1
- package/dist/process-bridge.js +30 -1
- package/dist/process-bridge.js.map +1 -1
- package/dist/program.d.ts +60 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +92 -31
- package/dist/program.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +491 -134
- package/dist/runtime.js.map +1 -1
- package/dist/select.d.ts +16 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +100 -0
- package/dist/select.js.map +1 -0
- package/dist/shared-pipes.d.ts +17 -6
- package/dist/shared-pipes.d.ts.map +1 -1
- package/dist/shared-pipes.js +94 -70
- package/dist/shared-pipes.js.map +1 -1
- package/dist/signals.d.ts +48 -0
- package/dist/signals.d.ts.map +1 -0
- package/dist/signals.js +232 -0
- package/dist/signals.js.map +1 -0
- package/dist/snapshot-filesystem.d.ts +128 -2
- package/dist/snapshot-filesystem.d.ts.map +1 -1
- package/dist/snapshot-filesystem.js +481 -53
- package/dist/snapshot-filesystem.js.map +1 -1
- package/dist/spawn-policy.d.ts +5 -0
- package/dist/spawn-policy.d.ts.map +1 -0
- package/dist/spawn-policy.js +29 -0
- package/dist/spawn-policy.js.map +1 -0
- package/dist/stdin.d.ts +2 -0
- package/dist/stdin.d.ts.map +1 -1
- package/dist/stdin.js +4 -0
- package/dist/stdin.js.map +1 -1
- package/dist/symlink.d.ts +8 -0
- package/dist/symlink.d.ts.map +1 -1
- package/dist/symlink.js +13 -0
- package/dist/symlink.js.map +1 -1
- package/dist/system.d.ts +152 -1
- package/dist/system.d.ts.map +1 -1
- package/dist/system.js +226 -7
- package/dist/system.js.map +1 -1
- package/dist/thread-coordinator-worker.d.ts +1 -0
- package/dist/thread-coordinator-worker.d.ts.map +1 -1
- package/dist/thread-coordinator-worker.js +1 -0
- package/dist/thread-coordinator-worker.js.map +1 -1
- package/dist/thread-worker.js +241 -122
- package/dist/thread-worker.js.map +1 -1
- package/dist/threads.d.ts +12 -2
- package/dist/threads.d.ts.map +1 -1
- package/dist/threads.js +30 -10
- package/dist/threads.js.map +1 -1
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/websocket-peer.d.ts.map +1 -1
- package/dist/websocket-peer.js +4 -14
- package/dist/websocket-peer.js.map +1 -1
- package/dist/worker-program-protocol.d.ts +70 -3
- package/dist/worker-program-protocol.d.ts.map +1 -1
- package/dist/worker-program-worker.js +115 -23
- package/dist/worker-program-worker.js.map +1 -1
- package/dist/worker-program.d.ts +25 -1
- package/dist/worker-program.d.ts.map +1 -1
- package/dist/worker-program.js +470 -61
- package/dist/worker-program.js.map +1 -1
- package/dist/worker-standby.d.ts +3 -0
- package/dist/worker-standby.d.ts.map +1 -0
- package/dist/worker-standby.js +17 -0
- package/dist/worker-standby.js.map +1 -0
- package/package.json +21 -7
package/dist/runtime.js
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
+
import { createWaliSignalState, defaultTerminates, WaliSignals, WaliSignalHost } from "./signals.js";
|
|
2
|
+
import { createConfinedWaliSpawnImport } from "./spawn-policy.js";
|
|
3
|
+
import { normalizeProcessFilesystemPolicy, intersectProcessFilesystemPolicies } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
|
|
1
4
|
import { assertWaliModule, compileWaliModule, inspectWaliModule } from "./inspect.js";
|
|
5
|
+
import { WALI_ENOSYS, waliUnimplementedTraps } from "./capabilities.js";
|
|
6
|
+
import { WaliDescriptionRegistry, WaliProcessDescriptorTable } from "./descriptors.js";
|
|
2
7
|
import { createStdioAliasRegistry, WaliFileDescriptorTable } from "./filesystem.js";
|
|
3
8
|
import { createWaliAllocatorControl, createWaliSharedMmapRegistry, WaliMemoryManager } from "./memory.js";
|
|
4
9
|
import { WaliPthreadProcessExit, WaliSystemHost } from "./system.js";
|
|
5
10
|
import { WaliEventHost } from "./events.js";
|
|
6
|
-
import { createImportedMemory } from "./module-layout.js";
|
|
11
|
+
import { createImportedMemory, findImportedMemory } from "./module-layout.js";
|
|
7
12
|
import { WaliThreadPool } from "./threads.js";
|
|
8
13
|
import { WaliLocalSocketHost } from "./local-sockets.js";
|
|
9
14
|
import { WaliNetworkCoordinator, WaliNetworkHost } from "./network.js";
|
|
10
15
|
import { BufferedWaliStdin, SharedWaliStdinReader } from "./stdin.js";
|
|
11
|
-
import { WaliFilesystemCoordinator, WaliSharedFileDescriptorHost } from "./filesystem-remote.js";
|
|
16
|
+
import { WaliFilesystemClient, WaliFilesystemCoordinator, WaliSharedFileDescriptorHost } from "./filesystem-remote.js";
|
|
12
17
|
import { createWaliSharedPipeState, WaliSharedPipeHost } from "./shared-pipes.js";
|
|
18
|
+
import { createWaliCodecImports, WaliCodecClient, WaliCodecCoordinator } from "./codec-remote.js";
|
|
19
|
+
import { createSelectImport, SELECT_SCRATCH_BYTES } from "./select.js";
|
|
13
20
|
import { applyProcessFilesystemPatch, BROWSER_PROCESS_RESPONSE_BYTES, closeSharedPipe, decodeProcessSnapshot, retainSharedPipe, } from "./process-bridge.js";
|
|
14
21
|
import { BrowserFileLockClient } from "@volter/browser-runtime/browser-file-locks.js";
|
|
15
22
|
import { BrowserFileCoherenceClient } from "@volter/browser-runtime/browser-file-coherence.js";
|
|
16
23
|
import { WaliCoherentFileLockClient } from "./coherent-file-locks.js";
|
|
24
|
+
import { monotonicMilliseconds } from "./monotonic-clock.js";
|
|
17
25
|
const EBADF = 9n;
|
|
18
26
|
const EINVAL = 22n;
|
|
19
27
|
const encoder = new TextEncoder();
|
|
20
28
|
const decoder = new TextDecoder();
|
|
21
|
-
const WNOHANG = 1;
|
|
22
29
|
export class UnsupportedWaliImportError extends Error {
|
|
23
30
|
imports;
|
|
24
31
|
constructor(imports) {
|
|
@@ -27,6 +34,45 @@ export class UnsupportedWaliImportError extends Error {
|
|
|
27
34
|
this.name = "UnsupportedWaliImportError";
|
|
28
35
|
}
|
|
29
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The handler for an import this runtime does not implement.
|
|
39
|
+
*
|
|
40
|
+
* `capabilities.ts` holds the rule and the evidence: a `SYS_*` name answers
|
|
41
|
+
* `-ENOSYS` the way a kernel does, and the four names in
|
|
42
|
+
* `WALI_TRAPPING_IMPORTS` still take the process down because an errno there
|
|
43
|
+
* cannot reach a caller that can act on it. `thread-worker.ts` does the
|
|
44
|
+
* identical thing for every pthread.
|
|
45
|
+
*
|
|
46
|
+
* WHERE THE ONE-SHOT NOTICE LANDS. This runtime runs inside
|
|
47
|
+
* `worker-program-worker.ts`, a nested worker, so `console.error` alone reaches
|
|
48
|
+
* that worker's devtools console and NOTHING ELSE -- measured 2026-09-19
|
|
49
|
+
* through the vgai editor, where an `os.fork()` answered -ENOSYS left
|
|
50
|
+
* `vgai console`, the product's own unresolved-console door, silent, because
|
|
51
|
+
* the editor's collector reports only what THE PAGE sees. A trap reached that
|
|
52
|
+
* door through the worker's `{type:"result", error}` message; an answered
|
|
53
|
+
* syscall had nothing to travel on. `onDiagnostic` is that something: the
|
|
54
|
+
* runtime's own word to its host, carried by `{type:"diagnostic"}` on
|
|
55
|
+
* `WaliWorkerResponse` and by the pthread output ports, landing on
|
|
56
|
+
* `BrowserProcessRunOptions.onDiagnostic`. It is deliberately NOT stdout: a
|
|
57
|
+
* kernel does not write to your streams, and a host that parses the program's
|
|
58
|
+
* output would have to un-parse this line again.
|
|
59
|
+
*/
|
|
60
|
+
function unimplementedImport(name, report) {
|
|
61
|
+
if (waliUnimplementedTraps(name))
|
|
62
|
+
return () => { throw new UnsupportedWaliImportError([name]); };
|
|
63
|
+
let reported = false;
|
|
64
|
+
return () => {
|
|
65
|
+
if (!reported) {
|
|
66
|
+
reported = true;
|
|
67
|
+
const notice = `[WALI] ${name} is not implemented; answering -ENOSYS (38) as a kernel would.`;
|
|
68
|
+
console.error(notice);
|
|
69
|
+
report?.(notice);
|
|
70
|
+
}
|
|
71
|
+
return WALI_ENOSYS;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
class WaliProcessReplaced extends Error {
|
|
75
|
+
}
|
|
30
76
|
class WaliProcessExit extends Error {
|
|
31
77
|
code;
|
|
32
78
|
constructor(code) {
|
|
@@ -36,6 +82,9 @@ class WaliProcessExit extends Error {
|
|
|
36
82
|
}
|
|
37
83
|
export class BrowserWaliRuntime {
|
|
38
84
|
async run(source, options = {}) {
|
|
85
|
+
const inheritedFilesystemPolicy = options.filesystemPolicy === undefined ? undefined
|
|
86
|
+
: normalizeProcessFilesystemPolicy(options.filesystemPolicy);
|
|
87
|
+
const filesystemPolicy = inheritedFilesystemPolicy;
|
|
39
88
|
const module = await compileWaliModule(source);
|
|
40
89
|
const descriptors = inspectWaliModule(module);
|
|
41
90
|
assertWaliModule(descriptors);
|
|
@@ -48,10 +97,17 @@ export class BrowserWaliRuntime {
|
|
|
48
97
|
const argv = options.argv?.length ? [...options.argv] : ["wali-program"];
|
|
49
98
|
const argvBytes = argv.map((argument) => encoder.encode(`${argument}\0`));
|
|
50
99
|
const pid = normalizePid(options.pid);
|
|
51
|
-
|
|
100
|
+
// Where a run's time goes: setup, then the guest, and within it each
|
|
101
|
+
// syscall by name. A run longer than PROFILE_AFTER_MS prints it once.
|
|
102
|
+
const runStartedAt = performance.now();
|
|
103
|
+
let startEnteredAt = runStartedAt;
|
|
104
|
+
const syscallTime = new Map();
|
|
105
|
+
const signalState = options.signalState ?? createWaliSignalState();
|
|
52
106
|
let instance;
|
|
53
107
|
let threadPool;
|
|
54
108
|
let networkCoordinator;
|
|
109
|
+
let codecCoordinator;
|
|
110
|
+
let codecClient;
|
|
55
111
|
let filesystemCoordinator;
|
|
56
112
|
const importedMemory = createImportedMemory(source, options.memory);
|
|
57
113
|
const stdioAliasRegistry = createStdioAliasRegistry();
|
|
@@ -61,7 +117,8 @@ export class BrowserWaliRuntime {
|
|
|
61
117
|
// Word 1: first failing worker slot. Word 2: process exit code.
|
|
62
118
|
const pthreadFatal = new Int32Array(new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT));
|
|
63
119
|
Atomics.store(pthreadFatal, 1, -1);
|
|
64
|
-
const
|
|
120
|
+
const localDescriptors = new WaliProcessDescriptorTable(new WaliDescriptionRegistry());
|
|
121
|
+
const sharedPipeState = options.descriptorTable?.sharedPipes ?? createWaliSharedPipeState(localDescriptors);
|
|
65
122
|
const memory = () => {
|
|
66
123
|
if (importedMemory)
|
|
67
124
|
return importedMemory;
|
|
@@ -94,18 +151,70 @@ export class BrowserWaliRuntime {
|
|
|
94
151
|
}
|
|
95
152
|
return BigInt(value.byteLength);
|
|
96
153
|
};
|
|
97
|
-
|
|
154
|
+
/**
|
|
155
|
+
* THE MAIN THREAD ENDING THE PROCESS SAYS SO, the same way a pthread does.
|
|
156
|
+
*
|
|
157
|
+
* `thread-worker.ts`'s `SYS_exit_group` already names the WORKER that ends
|
|
158
|
+
* a process, because a bare "exited 0" reads as an orderly shutdown and is
|
|
159
|
+
* the most misleading answer this runtime can give. This is the other half:
|
|
160
|
+
* without it, the ABSENCE of the worker's notice is the only evidence that
|
|
161
|
+
* the main thread was the one that exited, which is reasoning from silence.
|
|
162
|
+
*
|
|
163
|
+
* MEASURED 2026-09-19 (WS-W): headless Blender ended mid-call on the WALI
|
|
164
|
+
* skew and the worker notice did NOT fire, so the exit came through here --
|
|
165
|
+
* a fact that took a second run to establish and that this line states
|
|
166
|
+
* directly. Which syscall and which status are both part of the answer:
|
|
167
|
+
* `SYS_exit` from the main thread and `SYS_exit_group` mean different
|
|
168
|
+
* things about who decided.
|
|
169
|
+
*/
|
|
170
|
+
const exit = (status, name = "SYS_exit_group") => {
|
|
171
|
+
const notice = `[WALI] ${name}(${status & 0xff}) on the MAIN thread -- the process is ending here,`
|
|
172
|
+
+ ` not in a pthread. If the program did not ask to exit, this is the caller to find.`;
|
|
173
|
+
console.error(notice);
|
|
174
|
+
options.onDiagnostic?.(notice);
|
|
175
|
+
throw new WaliProcessExit(status & 0xff);
|
|
176
|
+
};
|
|
177
|
+
const signalSelf = (signal) => {
|
|
178
|
+
if (signal < 0 || signal > 64)
|
|
179
|
+
return -22n;
|
|
180
|
+
if (signal === 0)
|
|
181
|
+
return 0n;
|
|
182
|
+
// blocked, it stays pending until the mask admits it (raise queues only for a handler)
|
|
183
|
+
if (signal !== 9 && (signalHost.mask & (1n << BigInt(signal - 1))) !== 0n) {
|
|
184
|
+
new WaliSignals(signalState).raise(signal);
|
|
185
|
+
return 0n;
|
|
186
|
+
}
|
|
187
|
+
const disposition = signal === 9 || signal === 19 ? "default" : new WaliSignals(signalState).raise(signal);
|
|
188
|
+
if (disposition === "default" && defaultTerminates(signal))
|
|
189
|
+
exit(128 + signal, `signal ${signal}`);
|
|
190
|
+
return 0n;
|
|
191
|
+
};
|
|
98
192
|
const executablePath = options.executablePath
|
|
99
193
|
?? (argv[0].startsWith("/") ? argv[0] : `/usr/bin/${argv[0]}`);
|
|
100
194
|
const threaded = waliDescriptors.some((descriptor) => descriptor.name === "__wasm_thread_spawn");
|
|
101
|
-
|
|
195
|
+
// A module that imports none of the calls that make a child never has
|
|
196
|
+
// one: it needs neither the filesystem worker that shares descriptors
|
|
197
|
+
// with children nor the network worker that carries their pipes, and
|
|
198
|
+
// starting those was most of a small program's setup (ADR-0042).
|
|
199
|
+
const makesChildren = waliDescriptors.some((descriptor) => CHILD_PROCESS_SYSCALLS.has(descriptor.name));
|
|
200
|
+
const childDoor = makesChildren ? options.browserProcess : undefined;
|
|
201
|
+
const mmapCursor = options.forkState?.allocator ?? createWaliAllocatorControl();
|
|
102
202
|
const sharedMmapRegistry = createWaliSharedMmapRegistry();
|
|
103
|
-
|
|
203
|
+
const inheritedDescriptors = options.descriptorTable ? new WaliFilesystemClient(options.descriptorTable) : undefined;
|
|
204
|
+
if (inheritedDescriptors && importedMemory)
|
|
205
|
+
inheritedDescriptors.attachMemory(importedMemory);
|
|
206
|
+
if (!inheritedDescriptors && (threaded || childDoor) && options.filesystem?.toSnapshot && options.filesystem.replaceSnapshot) {
|
|
104
207
|
if (!importedMemory) {
|
|
105
208
|
throw new Error("A threaded WALI module must import its shared process memory.");
|
|
106
209
|
}
|
|
107
|
-
|
|
210
|
+
// A filesystem worker over the same store is handed only what differs
|
|
211
|
+
// from it, not the store's whole index again (ADR-0042).
|
|
212
|
+
const overlay = options.store ? options.filesystem.toStoreOverlaySnapshot?.() : undefined;
|
|
213
|
+
filesystemCoordinator = await WaliFilesystemCoordinator.create(overlay ?? options.filesystem.toSnapshot(), {
|
|
214
|
+
...(options.store ? { store: options.store } : {}),
|
|
108
215
|
memory: importedMemory,
|
|
216
|
+
sharedPipes: sharedPipeState,
|
|
217
|
+
...(filesystemPolicy ? { filesystemPolicy } : {}),
|
|
109
218
|
cwd: options.cwd ?? "/",
|
|
110
219
|
executablePath,
|
|
111
220
|
pid,
|
|
@@ -114,13 +223,18 @@ export class BrowserWaliRuntime {
|
|
|
114
223
|
...(options.fileCoherence ? { fileCoherence: options.fileCoherence } : {}),
|
|
115
224
|
});
|
|
116
225
|
}
|
|
117
|
-
|
|
226
|
+
if (filesystemPolicy && threaded && !filesystemCoordinator && !inheritedDescriptors)
|
|
227
|
+
throw new Error("Confined WALI threads require the shared filesystem owner.");
|
|
228
|
+
const runtimeFilesystem = inheritedDescriptors ?? filesystemCoordinator?.filesystem() ?? options.filesystem;
|
|
118
229
|
const syncFilesystem = () => {
|
|
119
230
|
if (filesystemCoordinator && options.filesystem?.replaceSnapshot) {
|
|
120
231
|
options.filesystem.replaceSnapshot(filesystemCoordinator.snapshot());
|
|
121
232
|
}
|
|
122
233
|
};
|
|
123
234
|
const fileDescriptors = new WaliFileDescriptorTable({
|
|
235
|
+
...((inheritedDescriptors || filesystemCoordinator) ? { descriptorState: sharedPipeState }
|
|
236
|
+
: { descriptors: localDescriptors }),
|
|
237
|
+
...(filesystemPolicy ? { filesystemPolicy } : {}),
|
|
124
238
|
...(runtimeFilesystem ? { filesystem: runtimeFilesystem } : {}),
|
|
125
239
|
cwd: options.cwd ?? "/",
|
|
126
240
|
stdin,
|
|
@@ -138,17 +252,25 @@ export class BrowserWaliRuntime {
|
|
|
138
252
|
...(filesystemCoordinator ? { specialDescriptorMinimum: 10_000, descriptorMaximum: 19_999 } : {}),
|
|
139
253
|
});
|
|
140
254
|
const environmentFile = fileDescriptors.writeEnvironmentFile(options.env ?? {}, pid);
|
|
141
|
-
const
|
|
142
|
-
|
|
255
|
+
const sharedFileClient = inheritedDescriptors ?? filesystemCoordinator?.filesystem();
|
|
256
|
+
const sharedFileHost = sharedFileClient
|
|
257
|
+
? new WaliSharedFileDescriptorHost(sharedFileClient, stdioAliasRegistry, sharedPipeState) : undefined;
|
|
143
258
|
const memoryManager = new WaliMemoryManager(memory, (fd, offset, length) => sharedFileHost?.readForMapping(fd, offset, length)
|
|
144
259
|
?? fileDescriptors.readForMapping(fd, offset, length), mmapCursor, (fd) => sharedFileHost?.mappingPath(fd) ?? fileDescriptors.mappingPath(fd), (path, offset, value) => sharedFileHost?.writeForMapping(path, offset, value)
|
|
145
|
-
?? fileDescriptors.writeForMapping(path, offset, value), sharedMmapRegistry);
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
260
|
+
?? fileDescriptors.writeForMapping(path, offset, value), sharedMmapRegistry, options.onDiagnostic);
|
|
261
|
+
const signalHost = new WaliSignalHost(new WaliSignals(signalState), memory, () => instance?.exports.__indirect_function_table, () => Number(memoryManager.imports().SYS_mmap(0, 4096, 3, 0x22, -1, 0)));
|
|
262
|
+
// The same number the thread pool is built with below, so the CPU count
|
|
263
|
+
// the guest is told about can never exceed the workers that exist.
|
|
264
|
+
const threadPoolSize = options.threadPoolSize ?? 18;
|
|
265
|
+
const systemHost = new WaliSystemHost(memory, pid, stdin, pthreadFatal, options.terminal, threadPoolSize);
|
|
266
|
+
// One shot: a program that exhausts the pool usually keeps asking, and the
|
|
267
|
+
// hundredth copy of the notice buries the first.
|
|
268
|
+
let threadPoolExhaustionReported = false;
|
|
269
|
+
if (options.network || childDoor) {
|
|
270
|
+
networkCoordinator = await WaliNetworkCoordinator.create(options.network, sharedPipeState, stdin instanceof SharedWaliStdinReader ? stdin.buffer : undefined, stdioAliasRegistry.buffer, signalState);
|
|
149
271
|
}
|
|
150
|
-
const networkHost = networkCoordinator ? new WaliNetworkHost(memory, networkCoordinator.createChannel()) : undefined;
|
|
151
|
-
const sharedPipeHost = new WaliSharedPipeHost(memory, sharedPipeState);
|
|
272
|
+
const networkHost = networkCoordinator ? new WaliNetworkHost(memory, networkCoordinator.createChannel(), signalHost) : undefined;
|
|
273
|
+
const sharedPipeHost = new WaliSharedPipeHost(memory, sharedPipeState, sharedFileClient ? undefined : localDescriptors, sharedFileClient);
|
|
152
274
|
const eventHost = new WaliEventHost(memory, fileDescriptors, (fd, events) => sharedPipeHost.pollDescriptor(fd, events) ?? (networkHost?.isNetworkFd(fd)
|
|
153
275
|
? networkHost.pollFd(fd, events) : undefined));
|
|
154
276
|
const localSocketHost = new WaliLocalSocketHost(memory, fileDescriptors);
|
|
@@ -157,9 +279,13 @@ export class BrowserWaliRuntime {
|
|
|
157
279
|
const pipeImports = sharedPipeHost.imports({ ...localSocketHost.imports(), ...fileImports });
|
|
158
280
|
const eventImports = eventHost.imports();
|
|
159
281
|
const systemImports = systemHost.imports();
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
282
|
+
// A forked runtime learns its parent from the fork state; one the spawn
|
|
283
|
+
// door exec'd into an existing process is told; a first process's parent
|
|
284
|
+
// is init.
|
|
285
|
+
const parentProcessId = options.forkState?.parentPid ?? options.parentPid ?? 1;
|
|
286
|
+
const nextChildPid = new Int32Array(options.childPidAllocator ?? new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
287
|
+
if (!options.childPidAllocator)
|
|
288
|
+
Atomics.store(nextChildPid, 0, Math.max(pid + 1, 10_000));
|
|
163
289
|
const readCString = (pointer) => {
|
|
164
290
|
if (!Number.isSafeInteger(pointer) || pointer <= 0 || pointer >= bytes().byteLength) {
|
|
165
291
|
throw new WebAssembly.RuntimeError("WALI process string pointer is out of bounds.");
|
|
@@ -188,51 +314,164 @@ export class BrowserWaliRuntime {
|
|
|
188
314
|
return;
|
|
189
315
|
return retainSharedPipe(handle);
|
|
190
316
|
};
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
return;
|
|
194
|
-
|
|
317
|
+
const waitForChild = (requestedPid, statusPointer, optionsValue) => {
|
|
318
|
+
if (!networkHost)
|
|
319
|
+
return -10n;
|
|
320
|
+
if (statusPointer !== 0 && (statusPointer < 0 || statusPointer + 4 > bytes().byteLength))
|
|
321
|
+
return -14n;
|
|
322
|
+
const { result, process } = networkHost.waitChild(requestedPid, optionsValue, signalHost.mask);
|
|
323
|
+
if (result <= 0)
|
|
324
|
+
return BigInt(result);
|
|
195
325
|
const patch = decodeProcessSnapshot(process);
|
|
196
326
|
if (patch) {
|
|
197
327
|
if (filesystemCoordinator)
|
|
198
|
-
|
|
328
|
+
filesystemCoordinator.filesystem().importPeerPatch(patch);
|
|
199
329
|
if (options.filesystem)
|
|
200
330
|
applyProcessFilesystemPatch(options.filesystem, patch);
|
|
201
331
|
const snapshot = options.filesystem?.toSnapshot?.();
|
|
202
332
|
if (snapshot)
|
|
203
333
|
options.browserProcess?.restore?.(snapshot);
|
|
204
334
|
}
|
|
335
|
+
if (statusPointer !== 0)
|
|
336
|
+
view().setInt32(statusPointer, (Atomics.load(new Int32Array(process.control), 1) & 0xff) << 8, true);
|
|
337
|
+
return BigInt(result);
|
|
205
338
|
};
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
339
|
+
const spawnBrowserProcess = (args, filesystemPolicy) => {
|
|
340
|
+
const [pathname, argvPointer, envPointer, cwdPointer, stdinFd, stdoutFd, stderrFd] = args;
|
|
341
|
+
if (!options.browserProcess)
|
|
342
|
+
return -38;
|
|
343
|
+
if (filesystemPolicy)
|
|
344
|
+
filesystemPolicy = normalizeProcessFilesystemPolicy(filesystemPolicy);
|
|
345
|
+
const inheritedPolicy = inheritedFilesystemPolicy;
|
|
346
|
+
if (inheritedPolicy) {
|
|
347
|
+
if (options.browserProcess.filesystemPolicyVersion !== 1)
|
|
348
|
+
return -95;
|
|
349
|
+
filesystemPolicy = intersectProcessFilesystemPolicies(inheritedPolicy, filesystemPolicy);
|
|
214
350
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
351
|
+
const executablePath = readCString(Number(pathname));
|
|
352
|
+
const argvValue = readCStringArray(Number(argvPointer));
|
|
353
|
+
if (argvValue.length === 0)
|
|
354
|
+
argvValue.push(executablePath);
|
|
355
|
+
const envValue = {};
|
|
356
|
+
for (const entry of readCStringArray(Number(envPointer))) {
|
|
357
|
+
const separator = entry.indexOf("=");
|
|
358
|
+
if (separator > 0)
|
|
359
|
+
envValue[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
360
|
+
}
|
|
361
|
+
const childPid = Atomics.add(nextChildPid, 0, 1);
|
|
362
|
+
const process = {
|
|
363
|
+
control: new SharedArrayBuffer(8 * Int32Array.BYTES_PER_ELEMENT),
|
|
364
|
+
response: new SharedArrayBuffer(BROWSER_PROCESS_RESPONSE_BYTES),
|
|
365
|
+
snapshotApplied: false,
|
|
366
|
+
};
|
|
367
|
+
const stdin = exportPipe(Number(stdinFd), "reader");
|
|
368
|
+
const stdout = exportPipe(Number(stdoutFd), "writer");
|
|
369
|
+
const stderr = exportPipe(Number(stderrFd), "writer");
|
|
370
|
+
let childTable;
|
|
371
|
+
try {
|
|
372
|
+
const registered = networkHost.registerChild(childPid, process);
|
|
373
|
+
if (registered < 0)
|
|
374
|
+
throw new Error(`Child registration failed: ${registered}`);
|
|
375
|
+
sharedFileClient?.prepareSpawn();
|
|
376
|
+
syncFilesystem();
|
|
377
|
+
childTable = sharedFileClient?.copyForExecChannel([Number(stdinFd), Number(stdoutFd), Number(stderrFd)]);
|
|
378
|
+
options.browserProcess.spawn({
|
|
379
|
+
// A NEW process here, not an exec in place: its parent is this one,
|
|
380
|
+
// and it and its own children come from the container's counter.
|
|
381
|
+
pid: childPid,
|
|
382
|
+
parentPid: pid,
|
|
383
|
+
childPidAllocator: nextChildPid.buffer,
|
|
384
|
+
...(childTable ? { descriptorTable: childTable } : {}),
|
|
385
|
+
...(filesystemPolicy ? { filesystemPolicy } : {}),
|
|
386
|
+
executablePath,
|
|
387
|
+
argv: argvValue,
|
|
388
|
+
env: envValue,
|
|
389
|
+
...(Number(envPointer) === 0 ? { inheritEnvironment: true } : {}),
|
|
390
|
+
cwd: Number(cwdPointer) === 0 ? fileDescriptors.currentWorkingDirectory() : readCString(Number(cwdPointer)),
|
|
391
|
+
control: process.control,
|
|
392
|
+
response: process.response,
|
|
393
|
+
...(stdin ? { stdin } : {}),
|
|
394
|
+
...(stdout ? { stdout } : {}),
|
|
395
|
+
...(stderr ? { stderr } : {}),
|
|
396
|
+
});
|
|
397
|
+
return childPid;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
childTable?.releasePort?.postMessage(true);
|
|
401
|
+
networkHost.forgetChild(childPid);
|
|
402
|
+
if (stdin)
|
|
403
|
+
closeSharedPipe(stdin);
|
|
404
|
+
if (stdout)
|
|
405
|
+
closeSharedPipe(stdout);
|
|
406
|
+
if (stderr)
|
|
407
|
+
closeSharedPipe(stderr);
|
|
408
|
+
return -5;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
let asyncifyData = options.forkState?.asyncifyData ?? 0;
|
|
412
|
+
let unwinding = false;
|
|
413
|
+
let rewinding = Boolean(options.forkState);
|
|
414
|
+
let forkResult = 0;
|
|
415
|
+
let forkNotice = false;
|
|
416
|
+
const asyncify = (name, ...args) => {
|
|
417
|
+
const fn = instance?.exports[name];
|
|
418
|
+
if (typeof fn !== "function")
|
|
419
|
+
throw new Error(`Missing ${name}; build the module with the Binaryen Asyncify stage.`);
|
|
420
|
+
fn(...args);
|
|
421
|
+
};
|
|
422
|
+
const currentCwd = () => sharedFileClient?.currentWorkingDirectory() ?? fileDescriptors.currentWorkingDirectory();
|
|
423
|
+
const fork = () => {
|
|
424
|
+
if (typeof instance?.exports.asyncify_start_unwind !== "function") {
|
|
425
|
+
if (!forkNotice)
|
|
426
|
+
options.onDiagnostic?.("[WALI] SYS_fork answers -ENOSYS: module lacks the Binaryen Asyncify build stage (--asyncify --pass-arg=asyncify-imports@wali.SYS_fork).");
|
|
427
|
+
forkNotice = true;
|
|
428
|
+
return -38n;
|
|
221
429
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
while (Atomics.load(control, 0) === 0)
|
|
227
|
-
Atomics.wait(control, 0, 0, 1000);
|
|
430
|
+
if (rewinding) {
|
|
431
|
+
asyncify("asyncify_stop_rewind");
|
|
432
|
+
rewinding = false;
|
|
433
|
+
return BigInt(forkResult);
|
|
228
434
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
435
|
+
if (!options.browserProcess?.fork || !sharedFileClient || !networkHost)
|
|
436
|
+
return -38n;
|
|
437
|
+
unwinding = true;
|
|
438
|
+
view().setUint32(asyncifyData, asyncifyData + 8, true);
|
|
439
|
+
asyncify("asyncify_start_unwind", asyncifyData);
|
|
440
|
+
return 0n;
|
|
441
|
+
};
|
|
442
|
+
const execve = (pathname, argvPointer, envPointer) => {
|
|
443
|
+
if (!options.forkState || !options.browserProcess || !sharedFileClient)
|
|
444
|
+
return -38n;
|
|
445
|
+
const path = readCString(pathname);
|
|
446
|
+
const executable = path.startsWith("/") ? path : `${currentCwd()}/${path}`;
|
|
447
|
+
if (!runtimeFilesystem?.existsSync(executable))
|
|
448
|
+
return -2n;
|
|
449
|
+
if (!runtimeFilesystem.statSync(executable).isFile())
|
|
450
|
+
return -13n;
|
|
451
|
+
// A NULL envp is the process's own environment (see inheritEnvironment).
|
|
452
|
+
const env = envPointer === 0 ? { ...options.env } : {};
|
|
453
|
+
for (const entry of readCStringArray(envPointer)) {
|
|
454
|
+
const separator = entry.indexOf("=");
|
|
455
|
+
if (separator > 0)
|
|
456
|
+
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
233
457
|
}
|
|
234
|
-
|
|
235
|
-
|
|
458
|
+
sharedFileClient.prepareSpawn();
|
|
459
|
+
const table = sharedFileClient.copyForExecChannel([0, 1, 2]);
|
|
460
|
+
const stdin = exportPipe(0, "reader");
|
|
461
|
+
const stdout = exportPipe(1, "writer");
|
|
462
|
+
const stderr = exportPipe(2, "writer");
|
|
463
|
+
options.browserProcess.spawn({
|
|
464
|
+
// The kernel's exec: the image changes, the process does not. The pid
|
|
465
|
+
// and the parent go across, and the child numbers ITS children from
|
|
466
|
+
// the container's one allocator -- two runtimes each starting their
|
|
467
|
+
// own at 10000 hand out the same pid twice.
|
|
468
|
+
pid, parentPid: parentProcessId, childPidAllocator: nextChildPid.buffer,
|
|
469
|
+
executablePath: executable, argv: readCStringArray(argvPointer), env, cwd: currentCwd(),
|
|
470
|
+
descriptorTable: table, control: options.forkState.control, response: options.forkState.response,
|
|
471
|
+
...(filesystemPolicy ? { filesystemPolicy } : {}),
|
|
472
|
+
...(stdin ? { stdin } : {}), ...(stdout ? { stdout } : {}), ...(stderr ? { stderr } : {}),
|
|
473
|
+
});
|
|
474
|
+
throw new WaliProcessReplaced();
|
|
236
475
|
};
|
|
237
476
|
const builtins = {
|
|
238
477
|
__init: () => 0,
|
|
@@ -264,10 +503,10 @@ export class BrowserWaliRuntime {
|
|
|
264
503
|
}
|
|
265
504
|
return written;
|
|
266
505
|
},
|
|
267
|
-
SYS_exit: (status) => exit(Number(status)),
|
|
268
|
-
SYS_exit_group: (status) => exit(Number(status)),
|
|
506
|
+
SYS_exit: (status) => exit(Number(status), "SYS_exit"),
|
|
507
|
+
SYS_exit_group: (status) => exit(Number(status), "SYS_exit_group"),
|
|
269
508
|
SYS_getpid: () => BigInt(pid),
|
|
270
|
-
SYS_getppid: () =>
|
|
509
|
+
SYS_getppid: () => BigInt(parentProcessId),
|
|
271
510
|
SYS_gettid: () => BigInt(pid),
|
|
272
511
|
SYS_getuid: () => 1000n,
|
|
273
512
|
SYS_geteuid: () => 1000n,
|
|
@@ -280,83 +519,71 @@ export class BrowserWaliRuntime {
|
|
|
280
519
|
// advertises it; a program calling it on its main thread was the one
|
|
281
520
|
// path that still failed closed.
|
|
282
521
|
SYS_prctl: () => 0n,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
snapshotApplied: false,
|
|
302
|
-
};
|
|
303
|
-
const stdin = exportPipe(Number(stdinFd), "reader");
|
|
304
|
-
const stdout = exportPipe(Number(stdoutFd), "writer");
|
|
305
|
-
const stderr = exportPipe(Number(stderrFd), "writer");
|
|
306
|
-
childProcesses.set(childPid, process);
|
|
307
|
-
try {
|
|
308
|
-
syncFilesystem();
|
|
309
|
-
options.browserProcess.spawn({
|
|
310
|
-
pid: childPid,
|
|
311
|
-
executablePath,
|
|
312
|
-
argv: argvValue,
|
|
313
|
-
env: envValue,
|
|
314
|
-
cwd: Number(cwdPointer) === 0 ? fileDescriptors.currentWorkingDirectory() : readCString(Number(cwdPointer)),
|
|
315
|
-
control: process.control,
|
|
316
|
-
response: process.response,
|
|
317
|
-
...(stdin ? { stdin } : {}),
|
|
318
|
-
...(stdout ? { stdout } : {}),
|
|
319
|
-
...(stderr ? { stderr } : {}),
|
|
320
|
-
});
|
|
321
|
-
return childPid;
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
childProcesses.delete(childPid);
|
|
325
|
-
if (stdin)
|
|
326
|
-
closeSharedPipe(stdin);
|
|
327
|
-
if (stdout)
|
|
328
|
-
closeSharedPipe(stdout);
|
|
329
|
-
if (stderr)
|
|
330
|
-
closeSharedPipe(stderr);
|
|
331
|
-
return -5;
|
|
522
|
+
// A signal the process sends itself (musl's raise is tkill on its own
|
|
523
|
+
// tid): a handler runs when the call returns, SIG_DFL ends the process
|
|
524
|
+
// for the signals whose default is to end it, as with 128 + the signal.
|
|
525
|
+
SYS_tkill: (_tid, signal) => signalSelf(Number(signal)),
|
|
526
|
+
// The codec bridge is negotiated by the manifest's `media.webcodecs`;
|
|
527
|
+
// without it every one of the six answers -95 before an argument is read.
|
|
528
|
+
...createWaliCodecImports(memory, () => options.codec?.version === 1, () => codecClient),
|
|
529
|
+
SYS_browser_spawn: (...args) => spawnBrowserProcess(args),
|
|
530
|
+
SYS_browser_spawn_confined_v1: createConfinedWaliSpawnImport(memory, () => options.browserProcess?.filesystemPolicyVersion === 1, spawnBrowserProcess),
|
|
531
|
+
SYS_fork: fork,
|
|
532
|
+
SYS_execve: (path, argv, env) => execve(Number(path), Number(argv), Number(env)),
|
|
533
|
+
// interrupted by a signal: its handler runs here, and a handler that
|
|
534
|
+
// asked for SA_RESTART (musl's signal() does) has the wait made again
|
|
535
|
+
SYS_wait4: (childPid, status, optionsValue) => {
|
|
536
|
+
for (;;) {
|
|
537
|
+
const result = waitForChild(Number(childPid), Number(status), Number(optionsValue));
|
|
538
|
+
if (result !== -4n || !signalHost.deliver())
|
|
539
|
+
return result;
|
|
332
540
|
}
|
|
333
541
|
},
|
|
334
|
-
|
|
542
|
+
SYS_pidfd_open: () => -38n,
|
|
335
543
|
SYS_kill: (childPid, signal) => {
|
|
544
|
+
if (Number(childPid) === pid || Number(childPid) === 0)
|
|
545
|
+
return signalSelf(Number(signal));
|
|
336
546
|
const normalized = Math.abs(Number(childPid));
|
|
337
|
-
|
|
338
|
-
if (!process)
|
|
547
|
+
if (!networkHost?.hasChild(normalized))
|
|
339
548
|
return -3n;
|
|
340
|
-
Atomics.store(new Int32Array(process.control), 3, Number(signal));
|
|
341
549
|
options.browserProcess?.kill?.(normalized, Number(signal));
|
|
342
550
|
return 0n;
|
|
343
551
|
},
|
|
344
552
|
SYS_setpgid: () => 0n,
|
|
345
553
|
SYS_getpgid: (childPid) => BigInt(Number(childPid) === 0 ? pid : Math.abs(Number(childPid))),
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
554
|
+
/**
|
|
555
|
+
* POOL EXHAUSTION SAYS SO, ONCE, because -EAGAIN is invisible from here up.
|
|
556
|
+
*
|
|
557
|
+
* The pool is instantiated before `_start` and never grows, so it is the
|
|
558
|
+
* process's whole THREAD budget; `WaliThreadPool.spawn` answers -EAGAIN
|
|
559
|
+
* (-11) when no slot is free, exactly as a kernel out of task structs
|
|
560
|
+
* would. What the guest does with that is the problem: libc++ turns it
|
|
561
|
+
* into `std::system_error: thread constructor failed: Resource
|
|
562
|
+
* temporarily unavailable`, and an uncaught one leaves the module as the
|
|
563
|
+
* host string `[object WebAssembly.Exception]` with nothing naming a
|
|
564
|
+
* thread, a pool or a number.
|
|
565
|
+
*
|
|
566
|
+
* MEASURED 2026-09-19 (WS-Y): that string was the entire failure report
|
|
567
|
+
* of the OpenImageIO deps gate for two days, and the cause was arithmetic
|
|
568
|
+
* -- an unconfigured OpenImageIO opens its own pool of `cpus - 1` AND
|
|
569
|
+
* OpenEXR's global pool of `cpus`, so it wants `2 * cpus - 1` threads out
|
|
570
|
+
* of a budget of `cpus`. One line here names that class of death at the
|
|
571
|
+
* moment it happens. Same door as the exit notices above: console.error
|
|
572
|
+
* plus onDiagnostic, never stdout.
|
|
573
|
+
*/
|
|
574
|
+
__wasm_thread_spawn: (functionPointer, argumentPointer) => {
|
|
575
|
+
const tid = threadPool?.spawn(Number(functionPointer), Number(argumentPointer), signalHost.mask) ?? -11;
|
|
576
|
+
if (tid === -11 && !threadPoolExhaustionReported) {
|
|
577
|
+
threadPoolExhaustionReported = true;
|
|
578
|
+
const notice = `[WALI] pthread_create answered -EAGAIN: all ${threadPoolSize} worker slots are in use.`
|
|
579
|
+
+ ` The pool is the process's whole thread budget and does not grow, so this is a QUOTA, not a cpu count --`
|
|
580
|
+
+ ` sysconf(_SC_NPROCESSORS_ONLN) answers ${systemHost.reportedCpuCount()} here and a`
|
|
581
|
+
+ ` library that opens more than one pool from that number wants a multiple of it. Raise the program's`
|
|
582
|
+
+ ` requirements.workers, or state the library's concurrency.`;
|
|
583
|
+
console.error(notice);
|
|
584
|
+
options.onDiagnostic?.(notice);
|
|
585
|
+
}
|
|
586
|
+
return tid;
|
|
360
587
|
},
|
|
361
588
|
SYS_clock_gettime: (clockId, pointer) => {
|
|
362
589
|
const nowNanoseconds = clockNanoseconds(Number(clockId));
|
|
@@ -382,6 +609,7 @@ export class BrowserWaliRuntime {
|
|
|
382
609
|
},
|
|
383
610
|
...memoryManager.imports(),
|
|
384
611
|
...systemImports,
|
|
612
|
+
...signalHost.imports(),
|
|
385
613
|
...pipeImports,
|
|
386
614
|
// The relay event worker is also the process-wide descriptor table for
|
|
387
615
|
// epoll, eventfd, and timerfd. Root and pthread instances must share it:
|
|
@@ -392,15 +620,28 @@ export class BrowserWaliRuntime {
|
|
|
392
620
|
?? systemImports.SYS_ioctl(fd, request, argument),
|
|
393
621
|
};
|
|
394
622
|
const handlers = { ...builtins, ...options.imports };
|
|
623
|
+
// select(2) is the process's poll over a scratch mapping, whichever host
|
|
624
|
+
// composed that poll above.
|
|
625
|
+
let selectScratch = 0;
|
|
626
|
+
handlers.SYS_select ??= createSelectImport(memory, (pointer, count, timeout) => handlers.SYS_poll(pointer, count, timeout), () => (selectScratch ||= Number(memoryManager.imports().SYS_mmap(0, SELECT_SCRATCH_BYTES, 3, 0x22, -1, 0))));
|
|
395
627
|
const wali = {};
|
|
396
628
|
for (const descriptor of waliDescriptors) {
|
|
397
|
-
const handler = handlers[descriptor.name]
|
|
398
|
-
|
|
629
|
+
const handler = handlers[descriptor.name] ?? unimplementedImport(descriptor.name, options.onDiagnostic);
|
|
630
|
+
const spent = { calls: 0, ms: 0 };
|
|
631
|
+
syscallTime.set(descriptor.name, spent);
|
|
399
632
|
wali[descriptor.name] = (...args) => {
|
|
400
633
|
options.onSyscall?.(descriptor.name, args);
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
634
|
+
const began = performance.now();
|
|
635
|
+
try {
|
|
636
|
+
const result = handler(...args);
|
|
637
|
+
options.onSyscallResult?.(descriptor.name, result);
|
|
638
|
+
signalHost.deliver();
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
spent.calls += 1;
|
|
643
|
+
spent.ms += performance.now() - began;
|
|
644
|
+
}
|
|
404
645
|
};
|
|
405
646
|
}
|
|
406
647
|
const env = {
|
|
@@ -413,6 +654,44 @@ export class BrowserWaliRuntime {
|
|
|
413
654
|
wali,
|
|
414
655
|
env,
|
|
415
656
|
});
|
|
657
|
+
if (options.forkState) {
|
|
658
|
+
bytes().set(new Uint8Array(options.forkState.memory));
|
|
659
|
+
for (const [name, value] of Object.entries(options.forkState.globals)) {
|
|
660
|
+
const global = instance.exports[name];
|
|
661
|
+
if (!(global instanceof WebAssembly.Global))
|
|
662
|
+
throw new Error(`Fork snapshot global ${name} is not exported.`);
|
|
663
|
+
global.value = value;
|
|
664
|
+
}
|
|
665
|
+
memoryManager.restoreFork(options.forkState.mappings);
|
|
666
|
+
systemImports.SYS_umask(options.forkState.umask);
|
|
667
|
+
}
|
|
668
|
+
else if (typeof instance.exports.asyncify_start_unwind === "function") {
|
|
669
|
+
const stack = instance.exports.__stack_pointer;
|
|
670
|
+
if (!(stack instanceof WebAssembly.Global))
|
|
671
|
+
throw new Error("Asyncify fork requires --export=__stack_pointer at link.");
|
|
672
|
+
// Reserve the module's declared stack capacity outside its initial image,
|
|
673
|
+
// before brk is initialized. The guest heap begins after this reservation.
|
|
674
|
+
const capacity = Number(stack.value);
|
|
675
|
+
if (capacity <= 0)
|
|
676
|
+
throw new Error("Asyncify fork requires a nonempty module stack.");
|
|
677
|
+
asyncifyData = memory().buffer.byteLength;
|
|
678
|
+
memory().grow(Math.ceil((capacity + 8) / 65_536));
|
|
679
|
+
view().setUint32(asyncifyData + 4, asyncifyData + 8 + capacity, true);
|
|
680
|
+
}
|
|
681
|
+
if (options.codec?.version === 1) {
|
|
682
|
+
try {
|
|
683
|
+
codecCoordinator = await WaliCodecCoordinator.create(memory());
|
|
684
|
+
codecClient = new WaliCodecClient(codecCoordinator.createChannel());
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
// A host without WebCodecs (or without workers) answers the six
|
|
688
|
+
// imports -95, the ABI's answer for a codec the browser cannot do.
|
|
689
|
+
// It never takes the program down.
|
|
690
|
+
console.warn("[WALI] the codec bridge is unavailable:", error);
|
|
691
|
+
codecCoordinator = undefined;
|
|
692
|
+
codecClient = undefined;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
416
695
|
if (threaded) {
|
|
417
696
|
if (!importedMemory) {
|
|
418
697
|
throw new Error("A threaded WALI module must be linked with --import-memory so browser workers can share its process memory.");
|
|
@@ -424,34 +703,86 @@ export class BrowserWaliRuntime {
|
|
|
424
703
|
sharedMmapRegistry,
|
|
425
704
|
fatalState: pthreadFatal.buffer,
|
|
426
705
|
imports: descriptors,
|
|
427
|
-
size:
|
|
706
|
+
size: threadPoolSize,
|
|
428
707
|
pid,
|
|
429
708
|
executablePath,
|
|
430
709
|
argv,
|
|
431
710
|
cwd: fileDescriptors.currentWorkingDirectory(),
|
|
432
711
|
stdioAliasRegistry: stdioAliasRegistry.buffer,
|
|
433
712
|
sharedPipes: sharedPipeState,
|
|
713
|
+
signals: signalState,
|
|
434
714
|
childPidAllocator: nextChildPid.buffer,
|
|
715
|
+
...(inheritedFilesystemPolicy ? { inheritedFilesystemPolicy } : {}),
|
|
435
716
|
...(options.browserProcessPorts ? { browserProcessPorts: options.browserProcessPorts } : {}),
|
|
717
|
+
...(options.browserProcess?.filesystemPolicyVersion === 1 ? { childFilesystemPolicyVersion: 1 } : {}),
|
|
436
718
|
...(options.threadOutputPorts ? { outputPorts: options.threadOutputPorts } : {}),
|
|
437
719
|
...(stdin instanceof SharedWaliStdinReader ? { stdin: stdin.buffer } : {}),
|
|
720
|
+
terminal: options.terminal === true,
|
|
438
721
|
onStdout: (chunk) => { emit(1, chunk); },
|
|
439
722
|
onStderr: (chunk) => { emit(2, chunk); },
|
|
440
723
|
...(networkCoordinator ? { network: networkCoordinator } : {}),
|
|
441
|
-
...(
|
|
724
|
+
...(sharedFileClient ? { filesystem: sharedFileClient } : {}),
|
|
725
|
+
...(codecCoordinator ? { codec: codecCoordinator } : {}),
|
|
442
726
|
});
|
|
443
727
|
await threadPool.initialize();
|
|
444
728
|
}
|
|
445
729
|
const start = instance.exports._start;
|
|
446
730
|
if (typeof start !== "function")
|
|
447
731
|
throw new Error("WALI module does not export _start.");
|
|
732
|
+
startEnteredAt = performance.now();
|
|
448
733
|
let exitCode = 0;
|
|
734
|
+
let replaced = false;
|
|
449
735
|
try {
|
|
450
736
|
try {
|
|
451
|
-
|
|
737
|
+
if (rewinding)
|
|
738
|
+
asyncify("asyncify_start_rewind", asyncifyData);
|
|
739
|
+
for (;;) {
|
|
740
|
+
start();
|
|
741
|
+
if (!unwinding)
|
|
742
|
+
break;
|
|
743
|
+
asyncify("asyncify_stop_unwind");
|
|
744
|
+
unwinding = false;
|
|
745
|
+
const layout = options.memoryLayout ?? (!(source instanceof WebAssembly.Module)
|
|
746
|
+
? findImportedMemory(source instanceof Uint8Array ? source : new Uint8Array(source)) : undefined);
|
|
747
|
+
if (!layout)
|
|
748
|
+
throw new Error("Fork requires the module's imported memory layout.");
|
|
749
|
+
const globals = {};
|
|
750
|
+
for (const [name, value] of Object.entries(instance.exports)) {
|
|
751
|
+
if (!(value instanceof WebAssembly.Global))
|
|
752
|
+
continue;
|
|
753
|
+
try {
|
|
754
|
+
value.value = value.value;
|
|
755
|
+
globals[name] = value.value;
|
|
756
|
+
}
|
|
757
|
+
catch { /* Immutable globals are reconstructed by instantiation. */ }
|
|
758
|
+
}
|
|
759
|
+
const control = new SharedArrayBuffer(8 * Int32Array.BYTES_PER_ELEMENT);
|
|
760
|
+
const response = new SharedArrayBuffer(BROWSER_PROCESS_RESPONSE_BYTES);
|
|
761
|
+
forkResult = Atomics.add(nextChildPid, 0, 1);
|
|
762
|
+
const registered = networkHost.registerChild(forkResult, { control, response });
|
|
763
|
+
if (registered < 0)
|
|
764
|
+
throw new Error(`Fork child registration failed: ${registered}`);
|
|
765
|
+
const mask = Number(systemImports.SYS_umask(0));
|
|
766
|
+
systemImports.SYS_umask(mask);
|
|
767
|
+
options.onDiagnostic?.(`[WALI] fork snapshot: ${memory().buffer.byteLength} bytes, Asyncify stack ${view().getUint32(asyncifyData, true) - asyncifyData - 8} bytes.`);
|
|
768
|
+
options.browserProcess.fork({
|
|
769
|
+
module, memoryLayout: layout, pid: forkResult, control, response,
|
|
770
|
+
descriptorTable: sharedFileClient.copyForForkChannel(),
|
|
771
|
+
childPidAllocator: nextChildPid.buffer,
|
|
772
|
+
argv, env: { ...options.env }, cwd: currentCwd(), executablePath,
|
|
773
|
+
...(filesystemPolicy ? { filesystemPolicy } : {}),
|
|
774
|
+
forkState: { control, response, memory: bytes().slice().buffer, globals,
|
|
775
|
+
allocator: memoryManager.allocatorControl().slice(0), mappings: memoryManager.forkSnapshot(),
|
|
776
|
+
asyncifyData, umask: mask, parentPid: pid },
|
|
777
|
+
});
|
|
778
|
+
rewinding = true;
|
|
779
|
+
asyncify("asyncify_start_rewind", asyncifyData);
|
|
780
|
+
}
|
|
452
781
|
}
|
|
453
782
|
catch (error) {
|
|
454
|
-
if (error instanceof
|
|
783
|
+
if (error instanceof WaliProcessReplaced)
|
|
784
|
+
replaced = true;
|
|
785
|
+
else if (error instanceof WaliProcessExit || error instanceof WaliPthreadProcessExit)
|
|
455
786
|
exitCode = error.code;
|
|
456
787
|
else if (error instanceof UnsupportedWaliImportError)
|
|
457
788
|
throw error;
|
|
@@ -467,17 +798,44 @@ export class BrowserWaliRuntime {
|
|
|
467
798
|
finally {
|
|
468
799
|
threadPool?.terminate();
|
|
469
800
|
networkCoordinator?.terminate();
|
|
470
|
-
|
|
801
|
+
codecCoordinator?.terminate();
|
|
802
|
+
if (environmentFile && runtimeFilesystem?.existsSync(environmentFile))
|
|
803
|
+
runtimeFilesystem.unlinkSync(environmentFile);
|
|
804
|
+
if (filesystemCoordinator && options.filesystemUpdatePort && options.filesystem?.replaceSnapshot) {
|
|
805
|
+
options.filesystem.replaceSnapshot(filesystemCoordinator.finalSnapshot());
|
|
806
|
+
options.onFilesystemFinalized?.();
|
|
807
|
+
}
|
|
808
|
+
else
|
|
809
|
+
syncFilesystem();
|
|
810
|
+
sharedFileClient?.closeProcess();
|
|
811
|
+
if (!sharedFileClient)
|
|
812
|
+
fileDescriptors.descriptors.exit();
|
|
471
813
|
filesystemCoordinator?.terminate();
|
|
472
814
|
}
|
|
815
|
+
const wallMs = performance.now() - runStartedAt;
|
|
816
|
+
if (wallMs > PROFILE_AFTER_MS) {
|
|
817
|
+
const calls = [...syscallTime].filter(([, spent]) => spent.calls > 0);
|
|
818
|
+
const inSyscalls = calls.reduce((sum, [, spent]) => sum + spent.ms, 0);
|
|
819
|
+
const top = calls.sort((a, b) => b[1].ms - a[1].ms).slice(0, 8)
|
|
820
|
+
.map(([name, spent]) => `${name.replace(/^SYS_/u, "")} ${spent.calls}x ${Math.round(spent.ms)}ms`);
|
|
821
|
+
console.log("[wali-profile]", JSON.stringify({
|
|
822
|
+
argv: argv.slice(0, 3), wallMs: Math.round(wallMs), setupMs: Math.round(startEnteredAt - runStartedAt),
|
|
823
|
+
guestMs: Math.round(performance.now() - startEnteredAt), syscallMs: Math.round(inSyscalls), top,
|
|
824
|
+
}));
|
|
825
|
+
}
|
|
473
826
|
return {
|
|
474
827
|
exitCode,
|
|
828
|
+
...(replaced ? { replaced } : {}),
|
|
475
829
|
stdout: decoder.decode(concatenate(stdout)),
|
|
476
830
|
stderr: decoder.decode(concatenate(stderr)),
|
|
477
831
|
imports: descriptors,
|
|
478
832
|
};
|
|
479
833
|
}
|
|
480
834
|
}
|
|
835
|
+
/** The calls through which a process makes a child: fork, exec, the browser's spawn door, and the wait for one. */
|
|
836
|
+
const CHILD_PROCESS_SYSCALLS = new Set(["SYS_fork", "SYS_execve", "SYS_browser_spawn", "SYS_browser_spawn_confined_v1", "SYS_wait4"]);
|
|
837
|
+
/** A run longer than this prints where its time went (`[wali-profile]`). */
|
|
838
|
+
const PROFILE_AFTER_MS = 2000;
|
|
481
839
|
function normalizePid(pid) {
|
|
482
840
|
if (pid === undefined)
|
|
483
841
|
return 2;
|
|
@@ -507,8 +865,7 @@ function clockNanoseconds(clockId) {
|
|
|
507
865
|
return BigInt(Math.floor(milliseconds * 1_000_000));
|
|
508
866
|
}
|
|
509
867
|
if (clockId === 1 || clockId === 4) {
|
|
510
|
-
|
|
511
|
-
return BigInt(Math.floor(milliseconds * 1_000_000));
|
|
868
|
+
return BigInt(Math.floor(monotonicMilliseconds() * 1_000_000));
|
|
512
869
|
}
|
|
513
870
|
return undefined;
|
|
514
871
|
}
|