as-test 1.0.0 → 1.0.3
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/CHANGELOG.md +116 -1
- package/README.md +138 -406
- package/as-test.config.schema.json +210 -17
- package/assembly/__fuzz__/array.fuzz.ts +10 -0
- package/assembly/__fuzz__/bytes.fuzz.ts +8 -0
- package/assembly/__fuzz__/math.fuzz.ts +9 -0
- package/assembly/__fuzz__/string.fuzz.ts +21 -0
- package/assembly/index.ts +141 -86
- package/assembly/src/expectation.ts +104 -19
- package/assembly/src/fuzz.ts +723 -0
- package/assembly/src/log.ts +6 -1
- package/assembly/src/suite.ts +45 -3
- package/assembly/util/json.ts +38 -4
- package/assembly/util/wipc.ts +35 -26
- package/bin/build-worker-pool.js +149 -0
- package/bin/build-worker.js +43 -0
- package/bin/commands/build-core.js +221 -29
- package/bin/commands/build.js +1 -0
- package/bin/commands/fuzz-core.js +306 -0
- package/bin/commands/fuzz.js +10 -0
- package/bin/commands/init-core.js +129 -24
- package/bin/commands/run-core.js +525 -123
- package/bin/commands/run.js +4 -1
- package/bin/commands/test.js +8 -3
- package/bin/commands/web-runner-source.js +634 -0
- package/bin/crash-store.js +64 -0
- package/bin/index.js +1484 -169
- package/bin/reporters/default.js +281 -49
- package/bin/reporters/tap.js +83 -2
- package/bin/types.js +19 -2
- package/bin/util.js +315 -33
- package/bin/wipc.js +79 -0
- package/package.json +19 -9
- package/transform/lib/coverage.js +1 -2
- package/transform/lib/index.js +3 -3
- package/transform/lib/log.js +1 -1
package/bin/commands/run.js
CHANGED
|
@@ -5,11 +5,14 @@ export async function executeRunCommand(rawArgs, flags, configPath, selectedMode
|
|
|
5
5
|
const featureToggles = deps.resolveFeatureToggles(rawArgs, "run");
|
|
6
6
|
const runFlags = {
|
|
7
7
|
snapshot: !flags.includes("--no-snapshot"),
|
|
8
|
-
|
|
8
|
+
createSnapshots: flags.includes("--create-snapshots"),
|
|
9
|
+
overwriteSnapshots: flags.includes("--overwrite-snapshots"),
|
|
9
10
|
clean: flags.includes("--clean"),
|
|
10
11
|
showCoverage: flags.includes("--show-coverage"),
|
|
11
12
|
verbose: flags.includes("--verbose"),
|
|
13
|
+
...deps.resolveParallelJobs(rawArgs, "run"),
|
|
12
14
|
coverage: featureToggles.coverage,
|
|
15
|
+
browser: deps.resolveBrowserOverride(rawArgs, "run"),
|
|
13
16
|
};
|
|
14
17
|
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
15
18
|
if (listFlags.list || listFlags.listModes) {
|
package/bin/commands/test.js
CHANGED
|
@@ -8,16 +8,21 @@ export async function executeTestCommand(rawArgs, flags, configPath, selectedMod
|
|
|
8
8
|
};
|
|
9
9
|
const runFlags = {
|
|
10
10
|
snapshot: !flags.includes("--no-snapshot"),
|
|
11
|
-
|
|
11
|
+
createSnapshots: flags.includes("--create-snapshots"),
|
|
12
|
+
overwriteSnapshots: flags.includes("--overwrite-snapshots"),
|
|
12
13
|
clean: flags.includes("--clean"),
|
|
13
14
|
showCoverage: flags.includes("--show-coverage"),
|
|
14
15
|
verbose: flags.includes("--verbose"),
|
|
16
|
+
...deps.resolveParallelJobs(rawArgs, "test"),
|
|
15
17
|
coverage: featureToggles.coverage,
|
|
18
|
+
browser: deps.resolveBrowserOverride(rawArgs, "test"),
|
|
16
19
|
};
|
|
20
|
+
const fuzzEnabled = flags.includes("--fuzz");
|
|
21
|
+
const fuzzOverrides = deps.resolveFuzzOverrides(rawArgs, "test");
|
|
17
22
|
const modeTargets = deps.resolveExecutionModes(configPath, selectedModes);
|
|
18
23
|
if (listFlags.list || listFlags.listModes) {
|
|
19
|
-
await deps.listExecutionPlan("test", configPath, commandArgs, modeTargets, listFlags);
|
|
24
|
+
await deps.listExecutionPlan("test", configPath, commandArgs, modeTargets, listFlags, fuzzEnabled);
|
|
20
25
|
return;
|
|
21
26
|
}
|
|
22
|
-
await deps.runTestModes(runFlags, configPath, commandArgs, modeTargets, buildFeatureToggles);
|
|
27
|
+
await deps.runTestModes(runFlags, configPath, commandArgs, modeTargets, buildFeatureToggles, fuzzEnabled, fuzzOverrides);
|
|
23
28
|
}
|
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
export function buildWebRunnerSource() {
|
|
2
|
+
const html = String.raw `<!doctype html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>as-test web runner</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
--bg: #0f172a;
|
|
12
|
+
--panel: #111827;
|
|
13
|
+
--muted: #94a3b8;
|
|
14
|
+
--text: #e5e7eb;
|
|
15
|
+
--accent: #38bdf8;
|
|
16
|
+
--error: #f87171;
|
|
17
|
+
}
|
|
18
|
+
body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
display: grid;
|
|
22
|
+
place-items: center;
|
|
23
|
+
background:
|
|
24
|
+
radial-gradient(circle at top, rgba(56, 189, 248, 0.18), transparent 35%),
|
|
25
|
+
linear-gradient(180deg, #020617, var(--bg));
|
|
26
|
+
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
27
|
+
color: var(--text);
|
|
28
|
+
}
|
|
29
|
+
main {
|
|
30
|
+
width: min(640px, calc(100vw - 32px));
|
|
31
|
+
background: rgba(17, 24, 39, 0.92);
|
|
32
|
+
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
33
|
+
border-radius: 18px;
|
|
34
|
+
padding: 24px;
|
|
35
|
+
box-shadow: 0 20px 60px rgba(2, 6, 23, 0.45);
|
|
36
|
+
}
|
|
37
|
+
h1 {
|
|
38
|
+
margin: 0 0 12px;
|
|
39
|
+
font-size: 20px;
|
|
40
|
+
}
|
|
41
|
+
p {
|
|
42
|
+
margin: 0 0 10px;
|
|
43
|
+
color: var(--muted);
|
|
44
|
+
}
|
|
45
|
+
code {
|
|
46
|
+
color: var(--accent);
|
|
47
|
+
}
|
|
48
|
+
pre {
|
|
49
|
+
margin: 16px 0 0;
|
|
50
|
+
padding: 12px;
|
|
51
|
+
border-radius: 12px;
|
|
52
|
+
background: rgba(2, 6, 23, 0.8);
|
|
53
|
+
overflow: auto;
|
|
54
|
+
white-space: pre-wrap;
|
|
55
|
+
word-break: break-word;
|
|
56
|
+
}
|
|
57
|
+
.error {
|
|
58
|
+
color: var(--error);
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<main>
|
|
64
|
+
<h1>as-test web runner</h1>
|
|
65
|
+
<p id="status">Connecting to the terminal runner...</p>
|
|
66
|
+
<pre id="details">Waiting for browser runtime bootstrap.</pre>
|
|
67
|
+
</main>
|
|
68
|
+
<script type="module" src="/client.js"></script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>`;
|
|
71
|
+
const client = String.raw `const status = document.getElementById("status");
|
|
72
|
+
const details = document.getElementById("details");
|
|
73
|
+
const replyBuffer = new SharedArrayBuffer(8 + 4 * 1024 * 1024);
|
|
74
|
+
const replyState = new Int32Array(replyBuffer, 0, 2);
|
|
75
|
+
const replyBytes = new Uint8Array(replyBuffer, 8);
|
|
76
|
+
const worker = new Worker("/worker.js", { type: "module" });
|
|
77
|
+
const ws = new WebSocket((location.protocol == "https:" ? "wss://" : "ws://") + location.host + "/ws");
|
|
78
|
+
ws.binaryType = "arraybuffer";
|
|
79
|
+
|
|
80
|
+
function setStatus(message, error = false) {
|
|
81
|
+
status.textContent = message;
|
|
82
|
+
status.className = error ? "error" : "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setDetails(message) {
|
|
86
|
+
details.textContent = message;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pushReply(frame) {
|
|
90
|
+
if (frame.byteLength > replyBytes.byteLength) {
|
|
91
|
+
throw new Error("WIPC reply exceeded shared browser buffer");
|
|
92
|
+
}
|
|
93
|
+
if (Atomics.load(replyState, 0) != 0) {
|
|
94
|
+
throw new Error("received concurrent WIPC replies in web runner");
|
|
95
|
+
}
|
|
96
|
+
replyBytes.set(new Uint8Array(frame), 0);
|
|
97
|
+
Atomics.store(replyState, 1, frame.byteLength);
|
|
98
|
+
Atomics.store(replyState, 0, 1);
|
|
99
|
+
Atomics.notify(replyState, 0, 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
worker.onmessage = (event) => {
|
|
103
|
+
const message = event.data ?? {};
|
|
104
|
+
if (message.kind == "wipc" && message.frame instanceof ArrayBuffer) {
|
|
105
|
+
ws.send(message.frame);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (message.kind == "ready") {
|
|
109
|
+
setStatus("Running tests in the browser...");
|
|
110
|
+
setDetails("Worker loaded the generated bindings helper and wasm artifact.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (message.kind == "done") {
|
|
114
|
+
setStatus("Finished. Closing browser runner...");
|
|
115
|
+
setDetails("Test execution completed successfully.");
|
|
116
|
+
ws.send(JSON.stringify({ kind: "done" }));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (message.kind == "error") {
|
|
120
|
+
setStatus("Browser runtime failed.", true);
|
|
121
|
+
setDetails(String(message.message ?? "unknown browser runtime error"));
|
|
122
|
+
ws.send(
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
kind: "error",
|
|
125
|
+
message: String(message.message ?? "unknown browser runtime error"),
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
ws.addEventListener("open", () => {
|
|
132
|
+
setStatus("Connected. Starting browser worker...");
|
|
133
|
+
setDetails("WebSocket tunnel established; waiting for wasm startup.");
|
|
134
|
+
ws.send(JSON.stringify({ kind: "ready" }));
|
|
135
|
+
worker.postMessage({
|
|
136
|
+
kind: "init",
|
|
137
|
+
helperUrl: "/artifact.js",
|
|
138
|
+
wasmUrl: "/artifact.wasm",
|
|
139
|
+
replyBuffer,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
ws.addEventListener("message", (event) => {
|
|
144
|
+
if (typeof event.data == "string") {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
pushReply(event.data);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = String(error instanceof Error ? error.message : error);
|
|
151
|
+
setStatus("Bridge failure.", true);
|
|
152
|
+
setDetails(message);
|
|
153
|
+
ws.send(JSON.stringify({ kind: "error", message }));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
ws.addEventListener("close", () => {
|
|
158
|
+
setStatus("Runner disconnected.", true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
ws.addEventListener("error", () => {
|
|
162
|
+
setStatus("WebSocket connection failed.", true);
|
|
163
|
+
});
|
|
164
|
+
`;
|
|
165
|
+
const worker = String.raw `let replyState = null;
|
|
166
|
+
let replyBytes = null;
|
|
167
|
+
|
|
168
|
+
self.onmessage = async (event) => {
|
|
169
|
+
const message = event.data ?? {};
|
|
170
|
+
if (message.kind != "init") {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const shared = message.replyBuffer;
|
|
175
|
+
replyState = new Int32Array(shared, 0, 2);
|
|
176
|
+
replyBytes = new Uint8Array(shared, 8);
|
|
177
|
+
|
|
178
|
+
globalThis.process = {
|
|
179
|
+
stdout: {
|
|
180
|
+
write(data) {
|
|
181
|
+
const frame = data instanceof ArrayBuffer ? data : data?.buffer;
|
|
182
|
+
self.postMessage({ kind: "wipc", frame }, [frame]);
|
|
183
|
+
return true;
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
stdin: {
|
|
187
|
+
read(size) {
|
|
188
|
+
return readReply(Number(size ?? 0));
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const helper = await import(message.helperUrl);
|
|
195
|
+
if (typeof helper.instantiate != "function") {
|
|
196
|
+
throw new Error("bindings helper missing instantiate export");
|
|
197
|
+
}
|
|
198
|
+
const response = await fetch(message.wasmUrl);
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
throw new Error("failed to fetch wasm artifact: " + response.status);
|
|
201
|
+
}
|
|
202
|
+
const binary = await response.arrayBuffer();
|
|
203
|
+
const module = new WebAssembly.Module(binary);
|
|
204
|
+
self.postMessage({ kind: "ready" });
|
|
205
|
+
await helper.instantiate(module, {});
|
|
206
|
+
self.postMessage({ kind: "done" });
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const message =
|
|
209
|
+
error && typeof error == "object" && "stack" in error
|
|
210
|
+
? String(error.stack)
|
|
211
|
+
: String(error);
|
|
212
|
+
self.postMessage({ kind: "error", message });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
function readReply(max) {
|
|
217
|
+
if (!replyState || !replyBytes || max <= 0) {
|
|
218
|
+
return new ArrayBuffer(0);
|
|
219
|
+
}
|
|
220
|
+
while (Atomics.load(replyState, 0) == 0) {
|
|
221
|
+
Atomics.wait(replyState, 0, 0);
|
|
222
|
+
}
|
|
223
|
+
const total = Atomics.load(replyState, 1);
|
|
224
|
+
const size = Math.min(max, total);
|
|
225
|
+
const out = new Uint8Array(size);
|
|
226
|
+
out.set(replyBytes.subarray(0, size));
|
|
227
|
+
if (size < total) {
|
|
228
|
+
replyBytes.copyWithin(0, size, total);
|
|
229
|
+
Atomics.store(replyState, 1, total - size);
|
|
230
|
+
} else {
|
|
231
|
+
Atomics.store(replyState, 1, 0);
|
|
232
|
+
Atomics.store(replyState, 0, 0);
|
|
233
|
+
Atomics.notify(replyState, 0, 1);
|
|
234
|
+
}
|
|
235
|
+
return out.buffer;
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
return `import { createHash } from "crypto";
|
|
239
|
+
import { existsSync, readFileSync } from "fs";
|
|
240
|
+
import http from "http";
|
|
241
|
+
import path from "path";
|
|
242
|
+
import { spawn } from "child_process";
|
|
243
|
+
|
|
244
|
+
const INDEX_HTML = ${JSON.stringify(html)};
|
|
245
|
+
const CLIENT_JS = ${JSON.stringify(client)};
|
|
246
|
+
const WORKER_JS = ${JSON.stringify(worker)};
|
|
247
|
+
const MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
248
|
+
const HEADLESS_FLAGS = [
|
|
249
|
+
"--headless=new",
|
|
250
|
+
"--disable-gpu",
|
|
251
|
+
"--no-first-run",
|
|
252
|
+
"--no-default-browser-check",
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
const rawArgs = process.argv.slice(2);
|
|
256
|
+
const headless = rawArgs.includes("--headless");
|
|
257
|
+
const wasmArg = rawArgs.find((value) => value != "--headless");
|
|
258
|
+
if (!wasmArg) {
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
"usage: node ./.as-test/runners/default.web.js [--headless] <file.wasm>\\n",
|
|
261
|
+
);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const wasmPath = path.resolve(process.cwd(), wasmArg);
|
|
266
|
+
const helperPath = wasmPath.replace(/\\.wasm$/, ".js");
|
|
267
|
+
if (!existsSync(wasmPath)) {
|
|
268
|
+
process.stderr.write("missing wasm artifact: " + wasmPath + "\\n");
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
if (!existsSync(helperPath)) {
|
|
272
|
+
process.stderr.write("missing bindings helper: " + helperPath + "\\n");
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let wsSocket = null;
|
|
277
|
+
let wsBuffer = Buffer.alloc(0);
|
|
278
|
+
let stdinBuffer = Buffer.alloc(0);
|
|
279
|
+
let browserProcess = null;
|
|
280
|
+
let closed = false;
|
|
281
|
+
let exitCode = 0;
|
|
282
|
+
let ready = false;
|
|
283
|
+
const pendingFrames = [];
|
|
284
|
+
|
|
285
|
+
const server = http.createServer((req, res) => {
|
|
286
|
+
const headers = {
|
|
287
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
288
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
289
|
+
"Cache-Control": "no-store",
|
|
290
|
+
};
|
|
291
|
+
const url = req.url ?? "/";
|
|
292
|
+
if (url == "/" || url.startsWith("/?")) {
|
|
293
|
+
res.writeHead(200, {
|
|
294
|
+
...headers,
|
|
295
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
296
|
+
});
|
|
297
|
+
res.end(INDEX_HTML);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (url == "/client.js") {
|
|
301
|
+
res.writeHead(200, {
|
|
302
|
+
...headers,
|
|
303
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
304
|
+
});
|
|
305
|
+
res.end(CLIENT_JS);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (url == "/worker.js") {
|
|
309
|
+
res.writeHead(200, {
|
|
310
|
+
...headers,
|
|
311
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
312
|
+
});
|
|
313
|
+
res.end(WORKER_JS);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (url == "/artifact.js") {
|
|
317
|
+
res.writeHead(200, {
|
|
318
|
+
...headers,
|
|
319
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
320
|
+
});
|
|
321
|
+
res.end(readFileSync(helperPath, "utf8"));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (url == "/artifact.wasm") {
|
|
325
|
+
res.writeHead(200, {
|
|
326
|
+
...headers,
|
|
327
|
+
"Content-Type": "application/wasm",
|
|
328
|
+
});
|
|
329
|
+
res.end(readFileSync(wasmPath));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
res.writeHead(404, headers);
|
|
333
|
+
res.end("not found");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
server.on("upgrade", (req, socket) => {
|
|
337
|
+
if ((req.url ?? "") != "/ws") {
|
|
338
|
+
socket.destroy();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const key = String(req.headers["sec-websocket-key"] ?? "");
|
|
342
|
+
if (!key) {
|
|
343
|
+
socket.destroy();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const accept = createHash("sha1")
|
|
347
|
+
.update(key + MAGIC)
|
|
348
|
+
.digest("base64");
|
|
349
|
+
socket.write(
|
|
350
|
+
[
|
|
351
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
352
|
+
"Upgrade: websocket",
|
|
353
|
+
"Connection: Upgrade",
|
|
354
|
+
"Sec-WebSocket-Accept: " + accept,
|
|
355
|
+
"",
|
|
356
|
+
"",
|
|
357
|
+
].join("\\r\\n"),
|
|
358
|
+
);
|
|
359
|
+
wsSocket = socket;
|
|
360
|
+
wsBuffer = Buffer.alloc(0);
|
|
361
|
+
socket.on("data", (chunk) => onWebSocketData(chunk));
|
|
362
|
+
socket.on("close", () => {
|
|
363
|
+
wsSocket = null;
|
|
364
|
+
if (!closed) {
|
|
365
|
+
finish(exitCode || 1);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
socket.on("error", (error) => {
|
|
369
|
+
process.stderr.write("web runner websocket error: " + String(error) + "\\n");
|
|
370
|
+
});
|
|
371
|
+
flushPendingFrames();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
server.listen(0, "127.0.0.1", () => {
|
|
375
|
+
const address = server.address();
|
|
376
|
+
if (!address || typeof address == "string") {
|
|
377
|
+
process.stderr.write("failed to determine local web runner address\\n");
|
|
378
|
+
finish(1);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const url = "http://127.0.0.1:" + address.port + "/";
|
|
382
|
+
try {
|
|
383
|
+
browserProcess = launchBrowser(url, headless);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
process.stderr.write(String(error) + "\\n");
|
|
386
|
+
finish(1);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
process.stdin.on("data", (chunk) => {
|
|
391
|
+
stdinBuffer = Buffer.concat([stdinBuffer, chunk]);
|
|
392
|
+
while (stdinBuffer.length >= 9) {
|
|
393
|
+
const length = stdinBuffer.readUInt32LE(5);
|
|
394
|
+
const frameSize = 9 + length;
|
|
395
|
+
if (stdinBuffer.length < frameSize) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const frame = stdinBuffer.subarray(0, frameSize);
|
|
399
|
+
stdinBuffer = stdinBuffer.subarray(frameSize);
|
|
400
|
+
if (ready && wsSocket) {
|
|
401
|
+
sendWebSocketFrame(wsSocket, 0x2, frame);
|
|
402
|
+
} else {
|
|
403
|
+
pendingFrames.push(Buffer.from(frame));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
process.stdin.on("end", () => finish(exitCode));
|
|
409
|
+
process.on("SIGINT", () => finish(130));
|
|
410
|
+
process.on("SIGTERM", () => finish(143));
|
|
411
|
+
|
|
412
|
+
function onWebSocketData(chunk) {
|
|
413
|
+
wsBuffer = Buffer.concat([wsBuffer, chunk]);
|
|
414
|
+
while (wsBuffer.length >= 2) {
|
|
415
|
+
const first = wsBuffer[0];
|
|
416
|
+
const second = wsBuffer[1];
|
|
417
|
+
const opcode = first & 0x0f;
|
|
418
|
+
const masked = (second & 0x80) !== 0;
|
|
419
|
+
let length = second & 0x7f;
|
|
420
|
+
let offset = 2;
|
|
421
|
+
if (length == 126) {
|
|
422
|
+
if (wsBuffer.length < offset + 2) return;
|
|
423
|
+
length = wsBuffer.readUInt16BE(offset);
|
|
424
|
+
offset += 2;
|
|
425
|
+
} else if (length == 127) {
|
|
426
|
+
if (wsBuffer.length < offset + 8) return;
|
|
427
|
+
length = Number(wsBuffer.readBigUInt64BE(offset));
|
|
428
|
+
offset += 8;
|
|
429
|
+
}
|
|
430
|
+
const maskLength = masked ? 4 : 0;
|
|
431
|
+
if (wsBuffer.length < offset + maskLength + length) return;
|
|
432
|
+
let payload = wsBuffer.subarray(offset + maskLength, offset + maskLength + length);
|
|
433
|
+
if (masked) {
|
|
434
|
+
const mask = wsBuffer.subarray(offset, offset + 4);
|
|
435
|
+
const unmasked = Buffer.alloc(length);
|
|
436
|
+
for (let i = 0; i < length; i++) {
|
|
437
|
+
unmasked[i] = payload[i] ^ mask[i % 4];
|
|
438
|
+
}
|
|
439
|
+
payload = unmasked;
|
|
440
|
+
} else {
|
|
441
|
+
payload = Buffer.from(payload);
|
|
442
|
+
}
|
|
443
|
+
wsBuffer = wsBuffer.subarray(offset + maskLength + length);
|
|
444
|
+
if (opcode == 0x8) {
|
|
445
|
+
finish(exitCode);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (opcode == 0x1) {
|
|
449
|
+
handleControl(payload.toString("utf8"));
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (opcode == 0x2) {
|
|
453
|
+
process.stdout.write(payload);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function handleControl(raw) {
|
|
459
|
+
let message = null;
|
|
460
|
+
try {
|
|
461
|
+
message = JSON.parse(raw);
|
|
462
|
+
} catch {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (message?.kind == "ready") {
|
|
466
|
+
ready = true;
|
|
467
|
+
flushPendingFrames();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (message?.kind == "done") {
|
|
471
|
+
finish(0);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (message?.kind == "error") {
|
|
475
|
+
process.stderr.write(String(message.message ?? "browser runtime failed") + "\\n");
|
|
476
|
+
finish(1);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function flushPendingFrames() {
|
|
481
|
+
if (!ready || !wsSocket) return;
|
|
482
|
+
while (pendingFrames.length) {
|
|
483
|
+
sendWebSocketFrame(wsSocket, 0x2, pendingFrames.shift());
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function sendWebSocketFrame(socket, opcode, payload) {
|
|
488
|
+
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload ?? []);
|
|
489
|
+
let header = null;
|
|
490
|
+
if (body.length < 126) {
|
|
491
|
+
header = Buffer.from([0x80 | opcode, body.length]);
|
|
492
|
+
} else if (body.length < 65536) {
|
|
493
|
+
header = Buffer.alloc(4);
|
|
494
|
+
header[0] = 0x80 | opcode;
|
|
495
|
+
header[1] = 126;
|
|
496
|
+
header.writeUInt16BE(body.length, 2);
|
|
497
|
+
} else {
|
|
498
|
+
header = Buffer.alloc(10);
|
|
499
|
+
header[0] = 0x80 | opcode;
|
|
500
|
+
header[1] = 127;
|
|
501
|
+
header.writeBigUInt64BE(BigInt(body.length), 2);
|
|
502
|
+
}
|
|
503
|
+
socket.write(Buffer.concat([header, body]));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function launchBrowser(url, headlessMode) {
|
|
507
|
+
if (!headlessMode) {
|
|
508
|
+
const opener = openWithSystemBrowser(url);
|
|
509
|
+
if (opener) return opener;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const direct = openWithInstalledBrowser(url, headlessMode);
|
|
513
|
+
if (direct) return direct;
|
|
514
|
+
|
|
515
|
+
throw new Error(
|
|
516
|
+
headlessMode
|
|
517
|
+
? "could not find a headless-capable browser; set BROWSER to a Chromium/Firefox executable"
|
|
518
|
+
: "could not open a browser automatically; set BROWSER to a browser executable",
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function openWithSystemBrowser(url) {
|
|
523
|
+
if (process.env.BROWSER) {
|
|
524
|
+
return spawnBrowserCommand(process.env.BROWSER, url, false);
|
|
525
|
+
}
|
|
526
|
+
if (process.platform == "darwin") {
|
|
527
|
+
if (!hasExecutable("open")) return null;
|
|
528
|
+
return spawn("open", [url], { stdio: "ignore", detached: true });
|
|
529
|
+
}
|
|
530
|
+
if (process.platform == "win32") {
|
|
531
|
+
if (!hasExecutable("cmd")) return null;
|
|
532
|
+
return spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true });
|
|
533
|
+
}
|
|
534
|
+
if (!hasExecutable("xdg-open")) return null;
|
|
535
|
+
return spawn("xdg-open", [url], { stdio: "ignore", detached: true });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function openWithInstalledBrowser(url, headlessMode) {
|
|
539
|
+
const browserEnv = process.env.BROWSER;
|
|
540
|
+
if (browserEnv) {
|
|
541
|
+
return spawnBrowserCommand(browserEnv, url, headlessMode);
|
|
542
|
+
}
|
|
543
|
+
const candidates = [
|
|
544
|
+
{ command: "chromium", headless: HEADLESS_FLAGS },
|
|
545
|
+
{ command: "chromium-browser", headless: HEADLESS_FLAGS },
|
|
546
|
+
{ command: "google-chrome", headless: HEADLESS_FLAGS },
|
|
547
|
+
{ command: "google-chrome-stable", headless: HEADLESS_FLAGS },
|
|
548
|
+
{ command: "chrome", headless: HEADLESS_FLAGS },
|
|
549
|
+
{ command: "msedge", headless: HEADLESS_FLAGS },
|
|
550
|
+
{ command: "firefox", headless: ["-headless"] },
|
|
551
|
+
];
|
|
552
|
+
for (const candidate of candidates) {
|
|
553
|
+
if (!hasExecutable(candidate.command)) continue;
|
|
554
|
+
return spawn(candidate.command, [...(headlessMode ? candidate.headless : []), url], {
|
|
555
|
+
stdio: "ignore",
|
|
556
|
+
detached: !headlessMode,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function spawnBrowserCommand(commandValue, url, headlessMode) {
|
|
563
|
+
const parts = String(commandValue)
|
|
564
|
+
.split(/\\s+/)
|
|
565
|
+
.filter((part) => part.length > 0);
|
|
566
|
+
if (!parts.length) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
const command = parts[0];
|
|
570
|
+
if (!hasExecutable(command)) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const args = parts.slice(1);
|
|
574
|
+
if (headlessMode) {
|
|
575
|
+
args.push(...resolveHeadlessFlags(commandValue, command));
|
|
576
|
+
}
|
|
577
|
+
args.push(url);
|
|
578
|
+
return spawn(command, args, {
|
|
579
|
+
stdio: "ignore",
|
|
580
|
+
detached: !headlessMode,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function resolveHeadlessFlags(commandValue, command) {
|
|
585
|
+
const lower = String(commandValue + " " + command).toLowerCase();
|
|
586
|
+
if (lower.includes("firefox")) {
|
|
587
|
+
return ["-headless"];
|
|
588
|
+
}
|
|
589
|
+
if (lower.includes("webkit") || lower.includes("minibrowser")) {
|
|
590
|
+
return ["--headless"];
|
|
591
|
+
}
|
|
592
|
+
return HEADLESS_FLAGS;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function hasExecutable(command) {
|
|
596
|
+
if (!command) return false;
|
|
597
|
+
if (command.includes("/") || command.includes("\\\\")) {
|
|
598
|
+
return existsSync(command);
|
|
599
|
+
}
|
|
600
|
+
const pathValue = process.env.PATH ?? "";
|
|
601
|
+
const suffixes = process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
602
|
+
for (const base of pathValue.split(path.delimiter)) {
|
|
603
|
+
if (!base) continue;
|
|
604
|
+
for (const suffix of suffixes) {
|
|
605
|
+
if (existsSync(path.join(base, command + suffix))) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function finish(code) {
|
|
614
|
+
if (closed) return;
|
|
615
|
+
closed = true;
|
|
616
|
+
exitCode = code;
|
|
617
|
+
try {
|
|
618
|
+
if (wsSocket) {
|
|
619
|
+
sendWebSocketFrame(wsSocket, 0x8, Buffer.alloc(0));
|
|
620
|
+
wsSocket.end();
|
|
621
|
+
}
|
|
622
|
+
} catch {}
|
|
623
|
+
try {
|
|
624
|
+
server.close();
|
|
625
|
+
} catch {}
|
|
626
|
+
if (browserProcess && !browserProcess.killed && headless) {
|
|
627
|
+
try {
|
|
628
|
+
browserProcess.kill("SIGTERM");
|
|
629
|
+
} catch {}
|
|
630
|
+
}
|
|
631
|
+
setTimeout(() => process.exit(exitCode), 25);
|
|
632
|
+
}
|
|
633
|
+
`;
|
|
634
|
+
}
|