源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
import test from "node:test";
|
||||
import type { Server } from "node:http";
|
||||
import type { Duplex } from "node:stream";
|
||||
|
||||
import { callFunction, connect, evaluate } from "../src/cdp.ts";
|
||||
import { evaluateOnSurface } from "../src/surface.ts";
|
||||
import type { Surface } from "../src/surface.ts";
|
||||
|
||||
const TARGET_ID = "page-1";
|
||||
const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function portFromServer(server: Server): number {
|
||||
const address = server.address();
|
||||
if (typeof address === "object" && address !== null) return address.port;
|
||||
throw new Error("Test server did not expose a TCP port.");
|
||||
}
|
||||
|
||||
function websocketFrame(payload: string): Buffer {
|
||||
const body = Buffer.from(payload);
|
||||
if (body.length < 126) return Buffer.concat([Buffer.from([0x81, body.length]), body]);
|
||||
const header = Buffer.alloc(4);
|
||||
header[0] = 0x81;
|
||||
header[1] = 126;
|
||||
header.writeUInt16BE(body.length, 2);
|
||||
return Buffer.concat([header, body]);
|
||||
}
|
||||
|
||||
function handleClientFrames(socket: Duplex): void {
|
||||
let buffered = Buffer.alloc(0);
|
||||
socket.on("data", (chunk: Buffer) => {
|
||||
buffered = Buffer.concat([buffered, chunk]);
|
||||
while (buffered.length >= 2) {
|
||||
const lengthCode = buffered[1] & 0x7f;
|
||||
const lengthBytes = lengthCode === 126 ? 2 : 0;
|
||||
if (lengthCode === 127) throw new Error("Test websocket frame is unexpectedly large.");
|
||||
const headerLength = 2 + lengthBytes + 4;
|
||||
if (buffered.length < headerLength) return;
|
||||
const payloadLength = lengthCode === 126 ? buffered.readUInt16BE(2) : lengthCode;
|
||||
if (buffered.length < headerLength + payloadLength) return;
|
||||
const maskOffset = 2 + lengthBytes;
|
||||
const payload = Buffer.alloc(payloadLength);
|
||||
for (let index = 0; index < payloadLength; index += 1) {
|
||||
payload[index] = buffered[headerLength + index] ^ buffered[maskOffset + (index % 4)];
|
||||
}
|
||||
buffered = buffered.subarray(headerLength + payloadLength);
|
||||
const message: unknown = JSON.parse(payload.toString());
|
||||
if (!isRecord(message) || typeof message.id !== "number") continue;
|
||||
let result = {};
|
||||
if (message.method === "Runtime.evaluate") {
|
||||
const params = isRecord(message.params) ? message.params : {};
|
||||
result = params.expression === "globalThis"
|
||||
? { result: { objectId: "global-object" } }
|
||||
: { result: { value: "recovered" } };
|
||||
} else if (message.method === "Runtime.callFunctionOn") {
|
||||
const params = isRecord(message.params) ? message.params : {};
|
||||
const args = Array.isArray(params.arguments) ? params.arguments : [];
|
||||
const first = args[0];
|
||||
result = { result: { value: isRecord(first) ? first.value : undefined } };
|
||||
}
|
||||
socket.write(websocketFrame(JSON.stringify({ id: message.id, result })));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function startCdpServer(stalledConnections: number): Promise<{
|
||||
baseUrl: string;
|
||||
websocketUrl: string;
|
||||
connections(): number;
|
||||
close(): Promise<void>;
|
||||
}> {
|
||||
const sockets = new Set<Duplex>();
|
||||
let connectionCount = 0;
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === "/json/version") {
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ Browser: "test" }));
|
||||
return;
|
||||
}
|
||||
if (request.url === "/json/list") {
|
||||
const port = portFromServer(server);
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify([{
|
||||
id: TARGET_ID,
|
||||
type: "page",
|
||||
title: "OpenWork",
|
||||
url: "http://127.0.0.1/app",
|
||||
webSocketDebuggerUrl: `ws://127.0.0.1:${port}/devtools/page/${TARGET_ID}`,
|
||||
}]));
|
||||
return;
|
||||
}
|
||||
response.writeHead(404);
|
||||
response.end("not found");
|
||||
});
|
||||
server.on("upgrade", (request, socket) => {
|
||||
const key = request.headers["sec-websocket-key"];
|
||||
if (typeof key !== "string") {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
sockets.add(socket);
|
||||
socket.once("close", () => sockets.delete(socket));
|
||||
const accept = createHash("sha1").update(`${key}${WEBSOCKET_GUID}`).digest("base64");
|
||||
socket.write([
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Accept: ${accept}`,
|
||||
"",
|
||||
"",
|
||||
].join("\r\n"));
|
||||
connectionCount += 1;
|
||||
if (connectionCount > stalledConnections) handleClientFrames(socket);
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const port = portFromServer(server);
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
websocketUrl: `ws://127.0.0.1:${port}/devtools/page/${TARGET_ID}`,
|
||||
connections: () => connectionCount,
|
||||
close: async () => {
|
||||
for (const socket of sockets) socket.destroy();
|
||||
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("a stalled websocket can be condemned without awaiting close", async () => {
|
||||
const server = await startCdpServer(1);
|
||||
try {
|
||||
const client = await connect(server.websocketUrl, { connectTimeoutMs: 500, sendTimeoutMs: 500 });
|
||||
await assert.rejects(evaluate(client, "1", { timeoutMs: 30 }), /CDP call Runtime\.evaluate timed out/);
|
||||
|
||||
const pending = evaluate(client, "2", { timeoutMs: 500 });
|
||||
if (!client.abort) throw new Error("Connected CDP client did not expose abort().");
|
||||
const startedAt = Date.now();
|
||||
client.abort(new Error("probe timed out"));
|
||||
assert.ok(Date.now() - startedAt < 100, "abort() waited for the peer to complete websocket close");
|
||||
await assert.rejects(pending, /CDP transport stalled: probe timed out/);
|
||||
await assert.rejects(evaluate(client, "3", { timeoutMs: 500 }), /CDP socket is not open/);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("function calls pass dynamic values as structured CDP arguments", async () => {
|
||||
const server = await startCdpServer(0);
|
||||
const client = await connect(server.websocketUrl, { connectTimeoutMs: 500, sendTimeoutMs: 500 });
|
||||
try {
|
||||
const dynamicValue = `"); throw new Error("must stay data"); //`;
|
||||
const value = await callFunction(client, "function (value) { return value; }", [dynamicValue]);
|
||||
assert.equal(value, dynamicValue);
|
||||
} finally {
|
||||
client.close();
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("surface evaluation heals after the first websocket consumes its budget", async () => {
|
||||
const server = await startCdpServer(1);
|
||||
try {
|
||||
const firstClient = await connect(server.websocketUrl, { connectTimeoutMs: 500, sendTimeoutMs: 500 });
|
||||
const surface: Surface = {
|
||||
handle: { name: "test", kind: "electron", hostKind: "test", cdpUrl: server.baseUrl },
|
||||
client: firstClient,
|
||||
};
|
||||
|
||||
const value = await evaluateOnSurface(surface, "42", { timeoutMs: 30 });
|
||||
|
||||
assert.equal(value, "recovered");
|
||||
assert.notEqual(surface.client, firstClient);
|
||||
assert.equal(server.connections(), 2);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user