@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/worker-program.js
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
+
import { normalizeProcessFilesystemPolicy, intersectProcessFilesystemPolicies, requireProcessFilesystemPolicySupport } from "@volter/browser-runtime/browser-process-filesystem-policy.js";
|
|
2
|
+
import { localBrowserFileSystemStore, readBrowserFileSystemSnapshot, readBrowserFileSystemEntry } from "@volter/browser-runtime/browser-filesystem.js";
|
|
3
|
+
import { BROWSER_PACK_RETENTION_ROOT, NODE_ENGINE_ROOT, PROJECT_ROOTS } from "@volter/browser-runtime/browser-pack-store.js";
|
|
4
|
+
import { browserProcessLineage } from "@volter/browser-runtime/browser-process.js";
|
|
1
5
|
import { SharedWaliStdinWriter } from "./stdin.js";
|
|
2
|
-
import {
|
|
6
|
+
import { createWaliSignalState, defaultTerminates, WaliSignals } from "./signals.js";
|
|
7
|
+
import { waliSharedPipeDescribesFd } from "./shared-pipes.js";
|
|
8
|
+
import { applyProcessFilesystemPatch, closeSharedPipe, createProcessFilesystemPatch, encodeProcessPatch, readSharedPipe, writeSharedPipe, } from "./process-bridge.js";
|
|
3
9
|
import { createBrowserFileLockChannel, sharedBrowserFileLockCoordinator, } from "@volter/browser-runtime/browser-file-locks.js";
|
|
4
10
|
import { createBrowserFileCoherenceChannel, sharedBrowserFileCoherenceCoordinator, } from "@volter/browser-runtime/browser-file-coherence.js";
|
|
11
|
+
import { currentEntry, patchedSnapshot } from "@volter/browser-runtime/browser-process-filesystem.js";
|
|
12
|
+
import { pageWorker } from "@volter/browser-runtime/page-workers.js";
|
|
5
13
|
/** Runs one unchanged WALI executable off the UI thread while retaining its compiled module. */
|
|
14
|
+
/**
|
|
15
|
+
* Each program's module, compiled by the first worker that ran it, for every
|
|
16
|
+
* worker after: a module is compiled once per page, not once per worker. Only
|
|
17
|
+
* a pinned artifact is kept; a `blob:` module is a URL of its own each run.
|
|
18
|
+
*/
|
|
19
|
+
const compiledModules = new Map();
|
|
20
|
+
const compiledKey = (url, integrity) => integrity ? `${url}\n${integrity}` : undefined;
|
|
6
21
|
export class BrowserWaliWorkerProgram {
|
|
7
22
|
filesystem;
|
|
8
23
|
program;
|
|
24
|
+
capabilities = Object.freeze({ filesystemPolicy: true });
|
|
25
|
+
/** Started by the first run, so a program the page offers and never runs costs no worker. */
|
|
9
26
|
worker;
|
|
10
27
|
pending;
|
|
11
28
|
stdin;
|
|
12
29
|
terminalSize = { columns: 80, rows: 24 };
|
|
13
30
|
nextId = 1;
|
|
31
|
+
forkWorkers = new Map();
|
|
14
32
|
childControllers = new Map();
|
|
15
33
|
openUrlHandler;
|
|
16
34
|
urlScanTail = "";
|
|
@@ -18,22 +36,55 @@ export class BrowserWaliWorkerProgram {
|
|
|
18
36
|
processPorts = [];
|
|
19
37
|
threadOutputPorts = [];
|
|
20
38
|
filesystemUpdatePort;
|
|
39
|
+
traceId = crypto.randomUUID();
|
|
21
40
|
preparing = false;
|
|
41
|
+
applyingFilesystem = false;
|
|
22
42
|
constructor(filesystem, program) {
|
|
23
43
|
this.filesystem = filesystem;
|
|
24
44
|
this.program = program;
|
|
25
|
-
this.worker = this.createWorker();
|
|
26
45
|
}
|
|
27
46
|
async run(argv, options = {}) {
|
|
47
|
+
if (options.filesystemPolicy !== undefined)
|
|
48
|
+
options = { ...options, filesystemPolicy: normalizeProcessFilesystemPolicy(options.filesystemPolicy) };
|
|
28
49
|
if (this.pending || this.preparing)
|
|
29
50
|
throw new Error("The browser WALI program is already running.");
|
|
30
51
|
if (options.signal?.aborted)
|
|
31
52
|
throw options.signal.reason;
|
|
53
|
+
// Started here so its boot overlaps the run's preparation.
|
|
54
|
+
this.worker ??= this.createWorker();
|
|
32
55
|
this.preparing = true;
|
|
56
|
+
console.log("[wali-boot]", JSON.stringify({ stage: "prepare", run: this.traceId, argv, at: Date.now() }));
|
|
33
57
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
58
|
+
// A page over a worker's store sends the worker to the store, not the
|
|
59
|
+
// tree: what the page has written must be in the store first, and the
|
|
60
|
+
// snapshot is the index, which the worker reads the bytes behind.
|
|
61
|
+
const store = localBrowserFileSystemStore(this.filesystem);
|
|
62
|
+
if (store)
|
|
63
|
+
await this.filesystem.flush?.();
|
|
64
|
+
// The worker reads every store root's bytes from the store itself, so
|
|
65
|
+
// those roots travel as metadata. The trees the program says it never
|
|
66
|
+
// reads travel the same way: their directories are there (a process may
|
|
67
|
+
// be started with one as its cwd: VS Code's extension host runs from the
|
|
68
|
+
// server's install tree and spawns git from it), their files are not.
|
|
69
|
+
console.log("[wali-boot]", JSON.stringify({ stage: "flushed", run: this.traceId, at: Date.now() }));
|
|
70
|
+
// A live pack's retention links are the shared store's references, not
|
|
71
|
+
// files a program reads; carried as content, one installed model is
|
|
72
|
+
// gigabytes in every run's image. The Node engine's prepared module
|
|
73
|
+
// bodies are its own: carried as content, an editor's 296 MB of them went
|
|
74
|
+
// into every `git` run's image, and two runs at once ended the page on
|
|
75
|
+
// its heap (measured 2026-09-24 in examples/volter-editor).
|
|
76
|
+
const unread = [BROWSER_PACK_RETENTION_ROOT, NODE_ENGINE_ROOT, ...this.program.excludedImagePaths?.() ?? []];
|
|
77
|
+
const metadataRoots = [...(store ? PROJECT_ROOTS : []), ...unread];
|
|
78
|
+
const image = await readBrowserFileSystemSnapshot(this.filesystem, "/", metadataRoots.length > 0 ? { metadataRoots } : {});
|
|
79
|
+
// The base every patch is diffed against is the image as the worker
|
|
80
|
+
// holds it: the unread trees' files are not in it (their directories
|
|
81
|
+
// are). Diffed against the page's own listing, the worker's final tree
|
|
82
|
+
// reported every one of those files removed, and the page deleted them
|
|
83
|
+
// (measured 2026-09-21: one git run's exit took 3,318 files of the
|
|
84
|
+
// GitLens and server trees).
|
|
85
|
+
const unreadFile = (path) => unread.some((root) => path === root || path.startsWith(`${root}/`));
|
|
86
|
+
const base = { files: image.files.filter((entry) => entry.type === "directory" || !unreadFile(entry.path)) };
|
|
87
|
+
const filesystem = this.program.prepareFilesystem ? await this.program.prepareFilesystem(image) : image;
|
|
37
88
|
if (options.signal?.aborted)
|
|
38
89
|
throw options.signal.reason;
|
|
39
90
|
const id = this.nextId++;
|
|
@@ -41,25 +92,70 @@ export class BrowserWaliWorkerProgram {
|
|
|
41
92
|
const lockChannel = createBrowserFileLockChannel();
|
|
42
93
|
const releaseFileLocks = sharedBrowserFileLockCoordinator.attach(lockChannel.server, lockOwner);
|
|
43
94
|
const coherenceChannel = createBrowserFileCoherenceChannel();
|
|
44
|
-
const
|
|
95
|
+
const coherenceFilesystem = this.program.coherenceFilesystem ?? this.filesystem;
|
|
96
|
+
const releaseFileCoherence = sharedBrowserFileCoherenceCoordinator.attach(coherenceChannel.server, coherenceFilesystem, () => this.program.onFilesystemCommit?.(this.filesystem),
|
|
97
|
+
// What coherence wrote is what the tree holds; the base records it so a
|
|
98
|
+
// later live patch on the same path is judged against the truth.
|
|
99
|
+
(path) => {
|
|
100
|
+
const pending = this.pending;
|
|
101
|
+
if (!pending || pending.id !== id)
|
|
102
|
+
return;
|
|
103
|
+
const entry = currentEntry(coherenceFilesystem, path);
|
|
104
|
+
pending.filesystemBase = patchedSnapshot(pending.filesystemBase, entry ? { files: [entry], removed: [] } : { files: [], removed: [path] });
|
|
105
|
+
});
|
|
45
106
|
this.urlScanTail = "";
|
|
46
107
|
this.openedUrls.clear();
|
|
47
108
|
const stdin = new SharedWaliStdinWriter();
|
|
48
|
-
|
|
109
|
+
const size = options.terminal ?? this.terminalSize;
|
|
110
|
+
stdin.resize(size.columns, size.rows);
|
|
49
111
|
this.stdin = stdin;
|
|
112
|
+
const signalState = createWaliSignalState();
|
|
50
113
|
return await new Promise((resolve, reject) => {
|
|
51
|
-
|
|
114
|
+
// What `kill` sent (the abort reason's `signal`; an abort that names
|
|
115
|
+
// none ends the process, as SIGKILL). A handler the program installed
|
|
116
|
+
// runs, and the program ends as it chooses: git removes its lockfiles
|
|
117
|
+
// and raises the signal again, which then ends it. An AbortSignal fires
|
|
118
|
+
// once, so a signal the program survived leaves `redeliver` on the
|
|
119
|
+
// reason: the next `kill` (a SIGKILL after an ignored SIGTERM) still
|
|
120
|
+
// reaches it, and `handled` says whether the program ended by itself.
|
|
121
|
+
const reason = () => options.signal?.reason ?? new DOMException("The WALI process was interrupted.", "AbortError");
|
|
122
|
+
const end = () => {
|
|
123
|
+
const current = options.signal?.reason;
|
|
124
|
+
if (current && typeof current === "object")
|
|
125
|
+
current.handled = false;
|
|
52
126
|
this.resetWorker();
|
|
53
127
|
this.finishPending();
|
|
54
|
-
reject(
|
|
128
|
+
reject(reason());
|
|
129
|
+
};
|
|
130
|
+
const survives = (signal) => {
|
|
131
|
+
if (signal === 9)
|
|
132
|
+
return false;
|
|
133
|
+
const disposition = new WaliSignals(signalState).raise(signal);
|
|
134
|
+
return disposition !== "default" || !defaultTerminates(signal);
|
|
135
|
+
};
|
|
136
|
+
const abort = () => {
|
|
137
|
+
const current = options.signal?.reason;
|
|
138
|
+
if (!survives(abortSignalNumber(current))) {
|
|
139
|
+
end();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (current && typeof current === "object") {
|
|
143
|
+
current.handled = true;
|
|
144
|
+
current.redeliver = (signal) => { if (this.pending && !survives(signal))
|
|
145
|
+
end(); };
|
|
146
|
+
}
|
|
55
147
|
};
|
|
56
148
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
57
149
|
let releaseLoopback = () => { };
|
|
58
150
|
const loopbackChannel = this.program.loopback ? new MessageChannel() : undefined;
|
|
59
151
|
if (loopbackChannel)
|
|
60
152
|
releaseLoopback = this.program.loopback.attach(loopbackChannel.port1);
|
|
153
|
+
let resolveFilesystemFinalization;
|
|
154
|
+
const filesystemFinalization = new Promise((resolve) => { resolveFilesystemFinalization = resolve; });
|
|
61
155
|
this.pending = {
|
|
62
156
|
id,
|
|
157
|
+
filesystemBase: base,
|
|
158
|
+
filesystemQueue: Promise.resolve(),
|
|
63
159
|
resolve,
|
|
64
160
|
reject,
|
|
65
161
|
options,
|
|
@@ -69,17 +165,41 @@ export class BrowserWaliWorkerProgram {
|
|
|
69
165
|
releaseLoopback,
|
|
70
166
|
releaseFileLocks,
|
|
71
167
|
releaseFileCoherence,
|
|
168
|
+
releaseFilesystemChanges: () => { },
|
|
169
|
+
changedPaths: new Set(),
|
|
170
|
+
importingFilesystem: false,
|
|
171
|
+
filesystemFinalized: false,
|
|
172
|
+
filesystemFinalization,
|
|
173
|
+
resolveFilesystemFinalization,
|
|
72
174
|
};
|
|
73
175
|
const request = {
|
|
74
176
|
type: "run",
|
|
177
|
+
// A program the lane exec'd into an existing process runs AS that
|
|
178
|
+
// process: it takes its pid, the parent it kept, and the counter its
|
|
179
|
+
// own children are numbered from. A run that is not an exec carries
|
|
180
|
+
// no lineage and is the runtime's first process, as before.
|
|
181
|
+
...(options[browserProcessLineage] ? {
|
|
182
|
+
pid: options[browserProcessLineage].pid,
|
|
183
|
+
parentPid: options[browserProcessLineage].parentPid,
|
|
184
|
+
childPidAllocator: options[browserProcessLineage].childPidAllocator,
|
|
185
|
+
} : {}),
|
|
75
186
|
id,
|
|
187
|
+
...(options.descriptorTable ? { descriptorTable: options.descriptorTable } : {}),
|
|
76
188
|
url: this.program.url,
|
|
77
189
|
...(this.program.integrity ? { integrity: this.program.integrity } : {}),
|
|
190
|
+
...(() => {
|
|
191
|
+
const key = compiledKey(this.program.url, this.program.integrity);
|
|
192
|
+
const compiled = key ? compiledModules.get(key) : undefined;
|
|
193
|
+
return compiled ? { compiled } : {};
|
|
194
|
+
})(),
|
|
78
195
|
...(options.executablePath ? { executablePath: options.executablePath } : {}),
|
|
79
196
|
argv: [...argv],
|
|
80
197
|
cwd: options.cwd ?? this.program.cwd ?? "/",
|
|
81
|
-
env: { ...this.program.env },
|
|
198
|
+
env: { ...options.env, ...this.program.env },
|
|
82
199
|
threadPoolSize: this.program.threadPoolSize ?? 18,
|
|
200
|
+
...(this.program.codec ? { codec: this.program.codec } : {}),
|
|
201
|
+
...(options.filesystemPolicy ? { filesystemPolicy: options.filesystemPolicy } : {}),
|
|
202
|
+
...(this.program.spawn?.capabilities?.filesystemPolicy === true ? { childFilesystemPolicyVersion: 1 } : {}),
|
|
83
203
|
...(this.program.network.allowedHosts.length > 0 || loopbackChannel ? { network: {
|
|
84
204
|
...(this.program.network.relayUrl ? { relayUrl: this.program.network.relayUrl } : {}),
|
|
85
205
|
allowedHosts: [...this.program.network.allowedHosts],
|
|
@@ -88,9 +208,12 @@ export class BrowserWaliWorkerProgram {
|
|
|
88
208
|
} } : {}),
|
|
89
209
|
...(loopbackChannel ? { loopbackPort: loopbackChannel.port2 } : {}),
|
|
90
210
|
stdin: stdin.buffer,
|
|
211
|
+
terminal: options.terminal !== undefined,
|
|
91
212
|
filesystem,
|
|
213
|
+
...(store ? { store: { projectId: store } } : {}),
|
|
92
214
|
fileLocks: { channel: lockChannel.client, owner: lockOwner },
|
|
93
215
|
fileCoherence: coherenceChannel.client,
|
|
216
|
+
signalState,
|
|
94
217
|
};
|
|
95
218
|
const channels = Array.from({ length: request.threadPoolSize }, () => new MessageChannel());
|
|
96
219
|
this.processPorts = channels.map((channel) => channel.port1);
|
|
@@ -121,14 +244,17 @@ export class BrowserWaliWorkerProgram {
|
|
|
121
244
|
this.emitStderr(active, `\nWALI pthread ${event.data.tid ?? "unknown"} error: ${event.data.message}\n`);
|
|
122
245
|
return;
|
|
123
246
|
}
|
|
124
|
-
|
|
247
|
+
if (event.data.type === "diagnostic") {
|
|
248
|
+
active.options.onDiagnostic?.(event.data.message);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
125
251
|
if (event.data.type === "stdout")
|
|
126
|
-
this.emitStdout(active,
|
|
252
|
+
this.emitStdout(active, event.data.bytes);
|
|
127
253
|
else
|
|
128
|
-
this.emitStderr(active,
|
|
254
|
+
this.emitStderr(active, new TextDecoder().decode(event.data.bytes));
|
|
129
255
|
}
|
|
130
256
|
finally {
|
|
131
|
-
if (event.data.type !== "error" && event.data.ack) {
|
|
257
|
+
if (event.data.type !== "error" && event.data.type !== "diagnostic" && event.data.ack) {
|
|
132
258
|
const ack = new Int32Array(event.data.ack);
|
|
133
259
|
Atomics.store(ack, 0, 1);
|
|
134
260
|
Atomics.notify(ack, 0);
|
|
@@ -141,11 +267,28 @@ export class BrowserWaliWorkerProgram {
|
|
|
141
267
|
const filesystemUpdates = new MessageChannel();
|
|
142
268
|
this.filesystemUpdatePort = filesystemUpdates.port1;
|
|
143
269
|
this.filesystemUpdatePort.onmessage = (event) => {
|
|
144
|
-
|
|
270
|
+
const active = this.pending;
|
|
271
|
+
if (active?.id === id)
|
|
272
|
+
active.filesystemQueue = active.filesystemQueue.then(() => this.applyLiveFilesystemPatch(id, event.data));
|
|
145
273
|
};
|
|
146
274
|
this.filesystemUpdatePort.start();
|
|
275
|
+
const changed = (path) => {
|
|
276
|
+
const active = this.pending;
|
|
277
|
+
if (!active || active.id !== id || this.applyingFilesystem)
|
|
278
|
+
return;
|
|
279
|
+
active.changedPaths.add(path);
|
|
280
|
+
this.scheduleFilesystemImport(active);
|
|
281
|
+
};
|
|
282
|
+
this.filesystem.on("change", changed);
|
|
283
|
+
this.filesystem.on("delete", changed);
|
|
284
|
+
this.pending.releaseFilesystemChanges = () => {
|
|
285
|
+
this.filesystem.off("change", changed);
|
|
286
|
+
this.filesystem.off("delete", changed);
|
|
287
|
+
};
|
|
147
288
|
request.filesystemUpdatePort = filesystemUpdates.port2;
|
|
289
|
+
console.log("[wali-boot]", JSON.stringify({ stage: "dispatch", run: this.traceId, at: Date.now() }));
|
|
148
290
|
this.worker.postMessage(request, [
|
|
291
|
+
...(request.descriptorTable ? [request.descriptorTable.port] : []),
|
|
149
292
|
...request.browserProcessPorts,
|
|
150
293
|
...request.threadOutputPorts,
|
|
151
294
|
request.filesystemUpdatePort,
|
|
@@ -153,14 +296,21 @@ export class BrowserWaliWorkerProgram {
|
|
|
153
296
|
request.fileCoherence.port,
|
|
154
297
|
...(request.loopbackPort ? [request.loopbackPort] : []),
|
|
155
298
|
]);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
299
|
+
// THE INHERITED TABLE OWNS fd 0. When a run inherits a descriptor table
|
|
300
|
+
// whose fd 0 is an open pipe description, that pipe IS the run's stdin
|
|
301
|
+
// and the guest reads it itself. Pumping the host's own reader on the
|
|
302
|
+
// same description into this run's private stdin buffer puts two readers
|
|
303
|
+
// on one pipe, and the pump -- polling from launch, while the guest is
|
|
304
|
+
// still instantiating -- takes the bytes and drops them where nothing
|
|
305
|
+
// reads. Measured: git's remote helper never saw its `capabilities` line.
|
|
306
|
+
const inheritedPipes = inheritedSharedPipes(options.descriptorTable);
|
|
307
|
+
const inheritedStdin = inheritedPipes !== undefined && waliSharedPipeDescribesFd(inheritedPipes, 0);
|
|
308
|
+
if (inheritedStdin) {
|
|
309
|
+
// Nothing to pump: fd 0 is the description, not this run's stdin.
|
|
310
|
+
}
|
|
311
|
+
else if (options.stdin !== undefined) {
|
|
312
|
+
const value = options.stdin;
|
|
313
|
+
void this.forwardStdin(id, stdin, (async function* () { yield value; })());
|
|
164
314
|
}
|
|
165
315
|
else if (options.stdinStream) {
|
|
166
316
|
void this.forwardStdin(id, stdin, options.stdinStream);
|
|
@@ -172,10 +322,21 @@ export class BrowserWaliWorkerProgram {
|
|
|
172
322
|
}
|
|
173
323
|
}
|
|
174
324
|
async forwardStdin(id, stdin, stream) {
|
|
325
|
+
const encoder = new TextEncoder();
|
|
175
326
|
try {
|
|
176
327
|
for await (const chunk of stream) {
|
|
177
|
-
|
|
178
|
-
|
|
328
|
+
const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
|
|
329
|
+
let offset = 0;
|
|
330
|
+
while (this.pending?.id === id && offset < bytes.byteLength) {
|
|
331
|
+
const free = stdin.free();
|
|
332
|
+
if (free > 0) {
|
|
333
|
+
const length = Math.min(free, bytes.byteLength - offset);
|
|
334
|
+
stdin.write(bytes.subarray(offset, offset + length));
|
|
335
|
+
offset += length;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
339
|
+
}
|
|
179
340
|
}
|
|
180
341
|
if (this.pending?.id !== id)
|
|
181
342
|
return;
|
|
@@ -207,14 +368,22 @@ export class BrowserWaliWorkerProgram {
|
|
|
207
368
|
terminate() {
|
|
208
369
|
this.pending?.reject(new DOMException("The WALI program was terminated.", "AbortError"));
|
|
209
370
|
this.finishPending();
|
|
210
|
-
this.worker
|
|
371
|
+
this.worker?.terminate();
|
|
211
372
|
}
|
|
212
373
|
createWorker() {
|
|
213
|
-
const worker = new Worker(new URL("./worker-program-worker.js", import.meta.url), {
|
|
374
|
+
const worker = pageWorker(new Worker(new URL("./worker-program-worker.js", import.meta.url), {
|
|
214
375
|
type: "module",
|
|
215
376
|
name: "browser-wali-program",
|
|
377
|
+
}));
|
|
378
|
+
worker.addEventListener("message", (event) => {
|
|
379
|
+
void this.handleMessage(event.data).catch((cause) => {
|
|
380
|
+
if (this.pending?.id !== event.data.id)
|
|
381
|
+
return;
|
|
382
|
+
this.pending.reject(cause);
|
|
383
|
+
this.finishPending();
|
|
384
|
+
this.resetWorker();
|
|
385
|
+
});
|
|
216
386
|
});
|
|
217
|
-
worker.addEventListener("message", (event) => void this.handleMessage(event.data));
|
|
218
387
|
worker.addEventListener("error", (event) => {
|
|
219
388
|
this.pending?.reject(event.error ?? new Error(event.message));
|
|
220
389
|
this.finishPending();
|
|
@@ -223,6 +392,12 @@ export class BrowserWaliWorkerProgram {
|
|
|
223
392
|
return worker;
|
|
224
393
|
}
|
|
225
394
|
async handleMessage(message) {
|
|
395
|
+
if (message.type === "compiled") {
|
|
396
|
+
const key = compiledKey(this.program.url, this.program.integrity);
|
|
397
|
+
if (key)
|
|
398
|
+
compiledModules.set(key, message.compiled);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
226
401
|
const pending = this.pending;
|
|
227
402
|
if (!pending || message.id !== pending.id)
|
|
228
403
|
return;
|
|
@@ -234,18 +409,46 @@ export class BrowserWaliWorkerProgram {
|
|
|
234
409
|
this.emitStderr(pending, message.chunk);
|
|
235
410
|
return;
|
|
236
411
|
}
|
|
412
|
+
if (message.type === "diagnostic") {
|
|
413
|
+
pending.options.onDiagnostic?.(message.message);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (message.type === "fork") {
|
|
417
|
+
this.forkChild(message);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
237
420
|
if (message.type === "spawn") {
|
|
238
421
|
void this.spawnChild(message, pending);
|
|
239
422
|
return;
|
|
240
423
|
}
|
|
241
424
|
if (message.type === "kill") {
|
|
242
|
-
this.childControllers.get(message.pid)
|
|
425
|
+
const controller = this.childControllers.get(message.pid);
|
|
426
|
+
if (!controller)
|
|
427
|
+
return;
|
|
428
|
+
// a child that survived an earlier signal takes the next through its reason
|
|
429
|
+
if (controller.signal.aborted)
|
|
430
|
+
controller.signal.reason?.redeliver?.(message.signal);
|
|
431
|
+
else
|
|
432
|
+
controller.abort(signalAbortReason(message.signal, `Signal ${message.signal}`));
|
|
243
433
|
return;
|
|
244
434
|
}
|
|
435
|
+
console.log("[wali-boot]", JSON.stringify({ stage: "result-received", run: this.traceId, at: Date.now() }));
|
|
245
436
|
const safeFilesystem = this.program.sanitizeFilesystem
|
|
246
437
|
? await this.program.sanitizeFilesystem(message.filesystem)
|
|
247
438
|
: message.filesystem;
|
|
248
|
-
|
|
439
|
+
if (message.filesystemFinalized)
|
|
440
|
+
await pending.filesystemFinalization;
|
|
441
|
+
await pending.filesystemQueue;
|
|
442
|
+
console.log("[wali-boot]", JSON.stringify({ stage: "filesystem-settled", run: this.traceId, at: Date.now() }));
|
|
443
|
+
if (this.pending !== pending)
|
|
444
|
+
return;
|
|
445
|
+
// A threaded process commits its final delta on the ordered update port.
|
|
446
|
+
// Its full snapshot may predate a peer import acknowledged on that port;
|
|
447
|
+
// diffing it against the advanced base would manufacture a deletion.
|
|
448
|
+
const patch = message.filesystemFinalized ? { files: [], removed: [] }
|
|
449
|
+
: createProcessFilesystemPatch(pending.filesystemBase, safeFilesystem);
|
|
450
|
+
if (!message.filesystemFinalized)
|
|
451
|
+
applyProcessFilesystemPatch(this.filesystem, patch, pending.filesystemBase);
|
|
249
452
|
try {
|
|
250
453
|
await this.program.onFilesystemCommit?.(this.filesystem, patch);
|
|
251
454
|
}
|
|
@@ -255,6 +458,7 @@ export class BrowserWaliWorkerProgram {
|
|
|
255
458
|
return;
|
|
256
459
|
}
|
|
257
460
|
this.finishPending();
|
|
461
|
+
console.log("[wali-boot]", JSON.stringify({ stage: "done", run: this.traceId, at: Date.now() }));
|
|
258
462
|
if (message.result)
|
|
259
463
|
pending.resolve({ ...message.result, stdout: pending.stdout, stderr: pending.stderr });
|
|
260
464
|
else {
|
|
@@ -263,15 +467,76 @@ export class BrowserWaliWorkerProgram {
|
|
|
263
467
|
pending.reject(new Error(streamedStderr ? `${streamedStderr}\n${detail}` : detail));
|
|
264
468
|
}
|
|
265
469
|
}
|
|
470
|
+
forkChild(message) {
|
|
471
|
+
const worker = pageWorker(new Worker(new URL("./fork-worker.js", import.meta.url), { type: "module", name: `wali-fork-${message.pid}` }));
|
|
472
|
+
this.forkWorkers.set(message.pid, worker);
|
|
473
|
+
const finish = () => { worker.terminate(); this.forkWorkers.delete(message.pid); };
|
|
474
|
+
worker.addEventListener("message", (event) => {
|
|
475
|
+
if (event.data.type === "fork-done") {
|
|
476
|
+
finish();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
void this.handleMessage(event.data).catch(error => {
|
|
480
|
+
this.pending?.reject(error);
|
|
481
|
+
this.finishPending();
|
|
482
|
+
this.resetWorker();
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
worker.addEventListener("error", event => {
|
|
486
|
+
this.pending?.reject(event.error ?? new Error(event.message));
|
|
487
|
+
this.finishPending();
|
|
488
|
+
this.resetWorker();
|
|
489
|
+
});
|
|
490
|
+
worker.postMessage(message, [message.descriptorTable.port,
|
|
491
|
+
...(message.descriptorTable.releasePort ? [message.descriptorTable.releasePort] : []), message.forkState.memory]);
|
|
492
|
+
}
|
|
266
493
|
async spawnChild(message, pending) {
|
|
267
494
|
const backend = this.program.spawn;
|
|
268
495
|
const controller = new AbortController();
|
|
269
496
|
this.childControllers.set(message.pid, controller);
|
|
497
|
+
const trace = (stage) => console.log("[wali-boot]", JSON.stringify({ stage, run: this.traceId, child: message.pid, at: Date.now() }));
|
|
498
|
+
trace("spawn-received");
|
|
499
|
+
const releasePort = message.descriptorTable?.releasePort;
|
|
500
|
+
const descriptorTable = message.descriptorTable && {
|
|
501
|
+
port: message.descriptorTable.port, control: message.descriptorTable.control,
|
|
502
|
+
payload: message.descriptorTable.payload,
|
|
503
|
+
...(message.descriptorTable.sharedPipes ? { sharedPipes: message.descriptorTable.sharedPipes } : {}),
|
|
504
|
+
};
|
|
505
|
+
let released;
|
|
506
|
+
const releaseDescriptors = () => released ??= releasePort ? new Promise((resolve) => {
|
|
507
|
+
releasePort.onmessage = () => { releasePort.close(); resolve(); };
|
|
508
|
+
releasePort.postMessage(true);
|
|
509
|
+
}) : Promise.resolve();
|
|
270
510
|
let stdoutWrites = Promise.resolve();
|
|
271
511
|
let stderrWrites = Promise.resolve();
|
|
272
512
|
let stdoutStreamed = false;
|
|
273
513
|
let stderrStreamed = false;
|
|
274
|
-
|
|
514
|
+
// The child's patch is a diff of the tree before and after it ran; the
|
|
515
|
+
// diff needs sizes and times, and only the entries it names need bytes.
|
|
516
|
+
const metadataSnapshot = () => readBrowserFileSystemSnapshot(this.filesystem, "/", { metadataRoots: ["/"] });
|
|
517
|
+
const patchAgainst = async (before) => {
|
|
518
|
+
const patch = createProcessFilesystemPatch(before, await metadataSnapshot());
|
|
519
|
+
const previous = new Map(before.files.map((entry) => [entry.path, entry]));
|
|
520
|
+
const files = [];
|
|
521
|
+
const removed = new Set(patch.removed);
|
|
522
|
+
for (const entry of patch.files) {
|
|
523
|
+
const current = previous.get(entry.path);
|
|
524
|
+
const bytesMoved = entry.type === "file" && (!current || current.type !== "file"
|
|
525
|
+
|| current.size !== entry.size || current.mtimeMs !== entry.mtimeMs);
|
|
526
|
+
if (!bytesMoved) {
|
|
527
|
+
files.push(entry);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const captured = await readBrowserFileSystemEntry(this.filesystem, entry.path);
|
|
531
|
+
if (captured)
|
|
532
|
+
files.push(captured);
|
|
533
|
+
else if (current)
|
|
534
|
+
removed.add(entry.path);
|
|
535
|
+
}
|
|
536
|
+
return { files, removed: [...removed] };
|
|
537
|
+
};
|
|
538
|
+
let childFilesystem = await metadataSnapshot();
|
|
539
|
+
trace("spawn-snapshot");
|
|
275
540
|
const stdout = (chunk) => {
|
|
276
541
|
stdoutStreamed = true;
|
|
277
542
|
if (message.stdout)
|
|
@@ -279,6 +544,10 @@ export class BrowserWaliWorkerProgram {
|
|
|
279
544
|
else
|
|
280
545
|
this.emitStdout(pending, chunk);
|
|
281
546
|
};
|
|
547
|
+
const stdoutBytes = (bytes) => {
|
|
548
|
+
stdoutStreamed = true;
|
|
549
|
+
stdoutWrites = stdoutWrites.then(() => writeSharedPipe(message.stdout, bytes));
|
|
550
|
+
};
|
|
282
551
|
const stderr = (chunk) => {
|
|
283
552
|
stderrStreamed = true;
|
|
284
553
|
if (message.stderr)
|
|
@@ -289,35 +558,50 @@ export class BrowserWaliWorkerProgram {
|
|
|
289
558
|
try {
|
|
290
559
|
if (!backend)
|
|
291
560
|
throw new Error("No browser child-process backend is configured.");
|
|
561
|
+
const filesystemPolicy = pending.options.filesystemPolicy
|
|
562
|
+
? intersectProcessFilesystemPolicies(pending.options.filesystemPolicy, message.filesystemPolicy)
|
|
563
|
+
: message.filesystemPolicy === undefined ? undefined : normalizeProcessFilesystemPolicy(message.filesystemPolicy);
|
|
564
|
+
requireProcessFilesystemPolicySupport(filesystemPolicy, backend.capabilities);
|
|
292
565
|
const safePatch = this.program.sanitizeFilesystemPatch
|
|
293
566
|
? await this.program.sanitizeFilesystemPatch(message.filesystemPatch)
|
|
294
567
|
: message.filesystemPatch;
|
|
295
|
-
applyProcessFilesystemPatch(this.filesystem, safePatch);
|
|
296
|
-
|
|
568
|
+
applyProcessFilesystemPatch(this.filesystem, safePatch, pending.filesystemBase);
|
|
569
|
+
pending.filesystemBase = patchedSnapshot(pending.filesystemBase, safePatch);
|
|
570
|
+
childFilesystem = await metadataSnapshot();
|
|
571
|
+
trace("spawn-patched");
|
|
297
572
|
const stdinStream = message.stdin ? streamBrowserPipe(message.stdin, controller.signal) : undefined;
|
|
298
|
-
const
|
|
573
|
+
const spawned = browserCommand(message);
|
|
574
|
+
const result = await backend.run(spawned.command, {
|
|
575
|
+
[browserProcessLineage]: { pid: message.pid, parentPid: message.parentPid, childPidAllocator: message.childPidAllocator },
|
|
576
|
+
...(filesystemPolicy ? { filesystemPolicy } : {}),
|
|
577
|
+
...(descriptorTable ? { descriptorTable } : {}),
|
|
299
578
|
cwd: message.cwd,
|
|
300
|
-
executablePath:
|
|
301
|
-
env: message.env,
|
|
579
|
+
...(spawned.executablePath ? { executablePath: spawned.executablePath } : {}),
|
|
580
|
+
env: message.inheritEnvironment ? { ...pending.options.env, ...this.program.env } : message.env,
|
|
302
581
|
...(stdinStream ? { stdinStream } : {}),
|
|
303
582
|
signal: controller.signal,
|
|
583
|
+
...(message.stdout ? { onStdoutBytes: stdoutBytes } : {}),
|
|
304
584
|
onStdout: stdout,
|
|
305
585
|
onStderr: stderr,
|
|
306
586
|
});
|
|
307
|
-
//
|
|
587
|
+
// Tabnode streams long-running commands, but may only return buffered
|
|
308
588
|
// output for short commands. Deliver exactly one form to the child pipe.
|
|
309
589
|
if (result.stdout && !stdoutStreamed)
|
|
310
590
|
stdout(result.stdout);
|
|
311
591
|
if (result.stderr && !stderrStreamed)
|
|
312
592
|
stderr(result.stderr);
|
|
593
|
+
trace("spawn-ran");
|
|
313
594
|
await Promise.all([stdoutWrites, stderrWrites]);
|
|
314
|
-
|
|
595
|
+
await releaseDescriptors();
|
|
596
|
+
encodeProcessPatch(message, await patchAgainst(childFilesystem), result.exitCode);
|
|
597
|
+
trace("spawn-returned");
|
|
315
598
|
}
|
|
316
599
|
catch (error) {
|
|
317
600
|
const text = `${error instanceof Error ? error.message : String(error)}\n`;
|
|
318
601
|
stderr(text);
|
|
319
602
|
await stderrWrites;
|
|
320
|
-
|
|
603
|
+
await releaseDescriptors();
|
|
604
|
+
encodeProcessPatch(message, await patchAgainst(childFilesystem), controller.signal.aborted ? 130 : 127);
|
|
321
605
|
}
|
|
322
606
|
finally {
|
|
323
607
|
controller.abort(new DOMException("Browser child process completed.", "AbortError"));
|
|
@@ -329,14 +613,77 @@ export class BrowserWaliWorkerProgram {
|
|
|
329
613
|
}
|
|
330
614
|
async applyLiveFilesystemPatch(id, message) {
|
|
331
615
|
const pending = this.pending;
|
|
332
|
-
if (!pending || pending.id !== id
|
|
616
|
+
if (!pending || pending.id !== id)
|
|
617
|
+
return;
|
|
618
|
+
if (message.type === "finalized") {
|
|
619
|
+
pending.filesystemFinalized = true;
|
|
620
|
+
pending.changedPaths.clear();
|
|
621
|
+
pending.releaseFilesystemChanges();
|
|
622
|
+
pending.resolveFilesystemFinalization();
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (message.type === "checkpoint") {
|
|
626
|
+
// This method runs on the ordered filesystem queue, after prior writes
|
|
627
|
+
// and their durability callbacks have completed.
|
|
628
|
+
const control = new Int32Array(message.control);
|
|
629
|
+
Atomics.store(control, 0, 1);
|
|
630
|
+
Atomics.notify(control, 0);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (message.type === "imported") {
|
|
634
|
+
pending.filesystemBase = patchedSnapshot(pending.filesystemBase, message.patch);
|
|
635
|
+
pending.importingFilesystem = false;
|
|
636
|
+
this.scheduleFilesystemImport(pending);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
if (message.type === "import-error")
|
|
641
|
+
throw new Error(message.message);
|
|
642
|
+
const safePatch = this.program.sanitizeFilesystemPatch
|
|
643
|
+
? await this.program.sanitizeFilesystemPatch(message.patch)
|
|
644
|
+
: message.patch;
|
|
645
|
+
this.applyingFilesystem = true;
|
|
646
|
+
try {
|
|
647
|
+
applyProcessFilesystemPatch(this.filesystem, safePatch, pending.filesystemBase);
|
|
648
|
+
}
|
|
649
|
+
finally {
|
|
650
|
+
this.applyingFilesystem = false;
|
|
651
|
+
}
|
|
652
|
+
pending.filesystemBase = patchedSnapshot(pending.filesystemBase, safePatch);
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
// THIS IS WHERE A LIVE PROGRAM BECOMES A DEAD ONE, and it is not an
|
|
656
|
+
// exit. `resetWorker()` terminates the worker from the HOST, so no
|
|
657
|
+
// guest exit path runs: instruments on `SYS_exit`, `SYS_exit_group` and
|
|
658
|
+
// musl's last-thread path are all silent through this, and the caller
|
|
659
|
+
// is told only that the program is gone.
|
|
660
|
+
//
|
|
661
|
+
// MEASURED 2026-09-19, the editor's Blender session on the WALI skew,
|
|
662
|
+
// the "one-session death" that had been attributed to a guest exit(0):
|
|
663
|
+
// it dies HERE, on `assertPatchBase`, at "write conflict at
|
|
664
|
+
// /work/.vgai-session/out/<n>.done". The directory channel's poll loop
|
|
665
|
+
// unlinks four files on the page after every answer, so a guest patch
|
|
666
|
+
// naming one of those paths crosses the page's own edit and the base
|
|
667
|
+
// the guest computed against no longer matches. Reproduced at session
|
|
668
|
+
// call 37 of a render loop (`base`: a file, `current`: null) and again
|
|
669
|
+
// at call 14 (`current`: the same file at a different mtime) -- the
|
|
670
|
+
// disagreement has more than one shape, which is why a targeted "drop
|
|
671
|
+
// the crossed paths" filter narrowed it without closing it (tried at
|
|
672
|
+
// both ends, reverted; it is not a one-line fix).
|
|
673
|
+
//
|
|
674
|
+
// The mechanism is a protocol one: TWO WRITERS SHARE ONE DIRECTORY with
|
|
675
|
+
// no ownership rule. The channel would not have this class of failure
|
|
676
|
+
// if the session owned its own files and the page only read them.
|
|
677
|
+
if (this.pending?.id !== id)
|
|
678
|
+
return;
|
|
679
|
+
this.finishPending();
|
|
680
|
+
this.resetWorker();
|
|
681
|
+
pending.reject(new Error(`WALI filesystem update could not be applied: ${error instanceof Error ? error.message : String(error)}`));
|
|
333
682
|
return;
|
|
334
|
-
|
|
335
|
-
? await this.program.sanitizeFilesystemPatch(message.patch)
|
|
336
|
-
: message.patch;
|
|
337
|
-
applyProcessFilesystemPatch(this.filesystem, safePatch);
|
|
683
|
+
}
|
|
338
684
|
try {
|
|
339
|
-
|
|
685
|
+
if (message.type === "filesystem")
|
|
686
|
+
await this.program.onFilesystemCommit?.(this.filesystem, message.patch);
|
|
340
687
|
}
|
|
341
688
|
catch (error) {
|
|
342
689
|
if (this.pending?.id !== id)
|
|
@@ -346,11 +693,52 @@ export class BrowserWaliWorkerProgram {
|
|
|
346
693
|
pending.reject(new Error(`WALI filesystem durability barrier failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
347
694
|
}
|
|
348
695
|
}
|
|
696
|
+
scheduleFilesystemImport(pending) {
|
|
697
|
+
if (pending.filesystemFinalized || pending.importingFilesystem || pending.changedPaths.size === 0 || this.pending !== pending)
|
|
698
|
+
return;
|
|
699
|
+
pending.importingFilesystem = true;
|
|
700
|
+
pending.filesystemQueue = pending.filesystemQueue.then(async () => {
|
|
701
|
+
if (this.pending !== pending)
|
|
702
|
+
return;
|
|
703
|
+
const paths = new Set(pending.changedPaths);
|
|
704
|
+
pending.changedPaths.clear();
|
|
705
|
+
const before = { files: pending.filesystemBase.files.filter((entry) => paths.has(entry.path)) };
|
|
706
|
+
const after = { files: [] };
|
|
707
|
+
// THE CHANGED PATHS, ONE AT A TIME, never the whole tree. This read a
|
|
708
|
+
// full `toSnapshot()` to pick two entries out of it, and a page-side
|
|
709
|
+
// snapshot carries every file's bytes as base64: measured 2026-09-19
|
|
710
|
+
// with Blender's 78 MB runtime tree in the page's filesystem, that is
|
|
711
|
+
// ~100 MB of string built and thrown away PER PAGE WRITE, and a
|
|
712
|
+
// directory-channel session writes twice per request.
|
|
713
|
+
for (const path of paths) {
|
|
714
|
+
const entry = await readBrowserFileSystemEntry(this.filesystem, path);
|
|
715
|
+
if (entry)
|
|
716
|
+
after.files.push(entry);
|
|
717
|
+
}
|
|
718
|
+
if (this.pending !== pending)
|
|
719
|
+
return;
|
|
720
|
+
const patch = createProcessFilesystemPatch(before, after);
|
|
721
|
+
if (patch.files.length === 0 && patch.removed.length === 0) {
|
|
722
|
+
pending.importingFilesystem = false;
|
|
723
|
+
this.scheduleFilesystemImport(pending);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.filesystemUpdatePort?.postMessage({ type: "import", patch, base: before });
|
|
727
|
+
}).catch((error) => {
|
|
728
|
+
if (this.pending !== pending)
|
|
729
|
+
return;
|
|
730
|
+
this.finishPending();
|
|
731
|
+
this.resetWorker();
|
|
732
|
+
pending.reject(error);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
349
735
|
finishPending() {
|
|
736
|
+
this.pending?.resolveFilesystemFinalization();
|
|
350
737
|
this.pending?.removeAbortListener();
|
|
351
738
|
this.pending?.releaseLoopback();
|
|
352
739
|
this.pending?.releaseFileLocks();
|
|
353
740
|
this.pending?.releaseFileCoherence();
|
|
741
|
+
this.pending?.releaseFilesystemChanges();
|
|
354
742
|
this.pending = undefined;
|
|
355
743
|
this.stdin?.close();
|
|
356
744
|
this.stdin = undefined;
|
|
@@ -364,9 +752,13 @@ export class BrowserWaliWorkerProgram {
|
|
|
364
752
|
this.filesystemUpdatePort = undefined;
|
|
365
753
|
}
|
|
366
754
|
emitStdout(pending, chunk) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
755
|
+
const text = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
|
|
756
|
+
pending.stdout += text;
|
|
757
|
+
this.scanOpenUrls(text);
|
|
758
|
+
if (typeof chunk !== "string" && pending.options.onStdoutBytes)
|
|
759
|
+
pending.options.onStdoutBytes(chunk);
|
|
760
|
+
else
|
|
761
|
+
pending.options.onStdout?.(text);
|
|
370
762
|
}
|
|
371
763
|
emitStderr(pending, chunk) {
|
|
372
764
|
pending.stderr += chunk;
|
|
@@ -387,13 +779,33 @@ export class BrowserWaliWorkerProgram {
|
|
|
387
779
|
this.urlScanTail = text.slice(-2048);
|
|
388
780
|
}
|
|
389
781
|
resetWorker() {
|
|
782
|
+
for (const worker of this.forkWorkers.values())
|
|
783
|
+
worker.terminate();
|
|
784
|
+
this.forkWorkers.clear();
|
|
390
785
|
for (const controller of this.childControllers.values())
|
|
391
786
|
controller.abort();
|
|
392
787
|
this.childControllers.clear();
|
|
393
|
-
this.worker
|
|
788
|
+
this.worker?.terminate();
|
|
394
789
|
this.worker = this.createWorker();
|
|
395
790
|
}
|
|
396
791
|
}
|
|
792
|
+
/**
|
|
793
|
+
* An abort that is a signal: the reason carries its number as `signal`, which
|
|
794
|
+
* the shell's `kill` and a process's own kill of a child set.
|
|
795
|
+
*/
|
|
796
|
+
export function signalAbortReason(signal, message) {
|
|
797
|
+
return Object.assign(new DOMException(message, "AbortError"), { signal });
|
|
798
|
+
}
|
|
799
|
+
function abortSignalNumber(reason) {
|
|
800
|
+
const signal = reason?.signal;
|
|
801
|
+
return typeof signal === "number" && Number.isInteger(signal) && signal > 0 && signal <= 64 ? signal : 9;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* The process host's command for a spawn. A shell is handed over whole, as
|
|
805
|
+
* its argv, so the host reads its line and the `$0` and positional arguments
|
|
806
|
+
* after it (`sh -c '"$@"' editor file`); the path it was exec'd at named the
|
|
807
|
+
* shell, not what the line runs, so it does not go with it.
|
|
808
|
+
*/
|
|
397
809
|
function browserCommand(message) {
|
|
398
810
|
const [program = "", ...args] = message.argv;
|
|
399
811
|
const executable = message.executablePath || program;
|
|
@@ -404,22 +816,24 @@ function browserCommand(message) {
|
|
|
404
816
|
if (script?.includes("__grok_user_cmd")) {
|
|
405
817
|
const separator = args.indexOf("--", commandIndex + 2);
|
|
406
818
|
if (separator >= 0 && args[separator + 1])
|
|
407
|
-
return args[separator + 1];
|
|
819
|
+
return { command: args[separator + 1] };
|
|
408
820
|
}
|
|
409
|
-
if (script !== undefined)
|
|
410
|
-
return script;
|
|
411
821
|
// The browser shell is a registered process-host executable, not a file
|
|
412
|
-
// at the native harness's host-specific absolute path
|
|
413
|
-
//
|
|
414
|
-
return [basename, ...args].map(shellQuote).join(" ");
|
|
822
|
+
// at the native harness's host-specific absolute path, so the shell goes
|
|
823
|
+
// by its name: `/bin/bash --version` is `bash --version`.
|
|
824
|
+
return { command: [basename, ...args].map(shellQuote).join(" ") };
|
|
415
825
|
}
|
|
416
|
-
return message.argv.map(shellQuote).join(" ");
|
|
826
|
+
return { command: message.argv.map(shellQuote).join(" "), ...(message.executablePath ? { executablePath: message.executablePath } : {}) };
|
|
417
827
|
}
|
|
418
828
|
function shellQuote(value) {
|
|
419
829
|
if (/^[A-Za-z0-9_./:+,@%=-]+$/u.test(value))
|
|
420
830
|
return value;
|
|
421
831
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
422
832
|
}
|
|
833
|
+
/** The WALI lane's own field on a run option's descriptor channel. */
|
|
834
|
+
function inheritedSharedPipes(channel) {
|
|
835
|
+
return channel?.sharedPipes;
|
|
836
|
+
}
|
|
423
837
|
function closeExternalPipe(handle) {
|
|
424
838
|
if (handle)
|
|
425
839
|
closeSharedPipe(handle);
|
|
@@ -440,9 +854,4 @@ async function* streamBrowserPipe(handle, signal) {
|
|
|
440
854
|
if (signal.aborted)
|
|
441
855
|
throw signal.reason;
|
|
442
856
|
}
|
|
443
|
-
function synchronizeFilesystem(target, snapshot) {
|
|
444
|
-
const patch = createProcessFilesystemPatch(target.toSnapshot(), snapshot);
|
|
445
|
-
applyProcessFilesystemPatch(target, patch);
|
|
446
|
-
return patch;
|
|
447
|
-
}
|
|
448
857
|
//# sourceMappingURL=worker-program.js.map
|