mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
fix(inspector): bound retained metadata and preserve cancellation
This commit is contained in:
@@ -119,12 +119,18 @@ targets belong to the same Electron application.
|
||||
|
||||
## Capture and determinism
|
||||
|
||||
Every observation records its time window and returns all valid script and
|
||||
execution-context events received during it. Per-location protocol validation
|
||||
remains in force. Scripts are validated after capture, deduplicated by stable
|
||||
metadata, and canonically sorted. Wall-clock timestamps and protocol script IDs
|
||||
are excluded from the durable result, so identical inputs and captured metadata
|
||||
produce the same Evidence ID.
|
||||
Capture retains valid script and execution-context metadata within an 8 MiB
|
||||
aggregate budget, including conservative estimates for UTF-16 strings, record
|
||||
and collection overhead, and projected URL fields. Each CDP message also has a
|
||||
16 MiB transport limit. A message over that limit fails with `payload_limit`;
|
||||
when the aggregate budget is reached, later metadata events are dropped and
|
||||
the partial result reports `events_dropped`, `truncated: true`, and
|
||||
`retained_metadata_budget_exceeded`. Repeated context transitions update the
|
||||
current context record instead of accumulating duplicate objects. Scripts are
|
||||
validated after capture, deduplicated by stable metadata, and canonically
|
||||
sorted. Wall-clock timestamps and protocol script IDs are excluded from the
|
||||
durable result, so identical inputs and captured metadata produce the same
|
||||
Evidence ID.
|
||||
|
||||
Inspector attach is inherently incomplete. Enabling `Debugger` reports known
|
||||
and uncollected scripts, but scripts collected before attachment may be
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { JavaScriptRuntimeObservationPort } from "./JavaScriptRuntimeObservationPort.js";
|
||||
import { createJavaScriptRuntimeObservationEvidence } from "./JavaScriptRuntimeObservationEvidence.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
ListJavaScriptRuntimeTargetsInput,
|
||||
@@ -40,6 +43,8 @@ export const observeJavaScriptRuntime = async (
|
||||
const ready = requireProvider(provider, "observe_javascript_runtime");
|
||||
if (!ready.ok) return ready;
|
||||
const result = await ready.value.observe(input, options);
|
||||
if (options.signal?.aborted === true)
|
||||
return err(new AnalysisCancelledError("observe_javascript_runtime"));
|
||||
return result.ok
|
||||
? ok(
|
||||
createJavaScriptRuntimeObservationEvidence(
|
||||
|
||||
@@ -25,7 +25,9 @@ interface PendingCommand {
|
||||
export class CdpConnection {
|
||||
readonly #pending = new Map<number, PendingCommand>();
|
||||
readonly #listeners = new Set<(event: CdpEvent) => void>();
|
||||
readonly #disconnectListeners = new Set<() => void>();
|
||||
readonly #disconnectListeners = new Set<
|
||||
(error: BrowserObservationError) => void
|
||||
>();
|
||||
readonly #protocolFailureListeners = new Set<
|
||||
(error: BrowserObservationError) => void
|
||||
>();
|
||||
@@ -74,9 +76,9 @@ export class CdpConnection {
|
||||
}
|
||||
|
||||
/** Subscribe to an unexpected transport loss while an operation is active. */
|
||||
onDisconnect(listener: () => void): () => void {
|
||||
onDisconnect(listener: (error: BrowserObservationError) => void): () => void {
|
||||
if (this.#closed) {
|
||||
listener();
|
||||
listener(this.#transportError(this.#transportFailure));
|
||||
return () => undefined;
|
||||
}
|
||||
this.#disconnectListeners.add(listener);
|
||||
@@ -281,7 +283,8 @@ export class CdpConnection {
|
||||
if (!wasClosed) this.#transportFailure = reason;
|
||||
this.#failPending(reason);
|
||||
if (wasClosed) return;
|
||||
for (const listener of this.#disconnectListeners) listener();
|
||||
const error = this.#transportError(reason);
|
||||
for (const listener of this.#disconnectListeners) listener(error);
|
||||
this.#disconnectListeners.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
JavaScriptRuntimeLocation,
|
||||
JavaScriptRuntimeTargetLocation,
|
||||
} from "../domain/javascript/javascriptRuntimeObservation.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { authorizedElectronFile } from "../browser/ElectronFileScope.js";
|
||||
|
||||
export type RuntimeLocationDecision =
|
||||
@@ -18,6 +19,14 @@ export type RuntimeLocationDecision =
|
||||
readonly reason: BrowserExclusionReason;
|
||||
};
|
||||
|
||||
/** Throw the operation's typed cancellation error whenever its signal is aborted. */
|
||||
export const throwIfRuntimeObservationCancelled = (
|
||||
signal?: AbortSignal,
|
||||
): void => {
|
||||
if (signal?.aborted === true)
|
||||
throw new AnalysisCancelledError("observe_javascript_runtime");
|
||||
};
|
||||
|
||||
/**
|
||||
* Wire bucket for inspector script exclusions. The V8 observation schemas
|
||||
* fix the `unsupported_location` key, so every canonical denial reason maps
|
||||
@@ -35,7 +44,9 @@ export const inspectorExclusionKey = (
|
||||
/** Resolve a protocol location exposed by the explicitly selected Inspector endpoint. */
|
||||
export const authorizeRuntimeLocation = async (
|
||||
value: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RuntimeLocationDecision> => {
|
||||
throwIfRuntimeObservationCancelled(signal);
|
||||
if (value.startsWith("node:") && value.length > 0)
|
||||
return {
|
||||
allowed: true,
|
||||
@@ -43,6 +54,7 @@ export const authorizeRuntimeLocation = async (
|
||||
};
|
||||
if (value.startsWith("file:")) {
|
||||
const filePath = await authorizedElectronFile(value);
|
||||
throwIfRuntimeObservationCancelled(signal);
|
||||
return filePath === undefined
|
||||
? { allowed: false, reason: "not_approved" }
|
||||
: { allowed: true, location: { kind: "file", file_path: filePath } };
|
||||
|
||||
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
||||
|
||||
import { afterAll, beforeAll, expect, it } from "vitest";
|
||||
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { finalizeInspectorCapture } from "./V8InspectorCaptureProjection.js";
|
||||
import type { CaptureState } from "./V8InspectorProvider.js";
|
||||
|
||||
@@ -214,3 +215,48 @@ it("waits for active authorization workers to settle before returning a failure"
|
||||
expect(new Set(startedUrls)).toEqual(initialStarted);
|
||||
expect(active).toBe(0);
|
||||
});
|
||||
|
||||
it("stops finalization when cancellation arrives during location authorization", async () => {
|
||||
const controller = new AbortController();
|
||||
let authorizationStarted = false;
|
||||
const state = stateFor([
|
||||
{
|
||||
rawUrl: "https://example.test/script.js",
|
||||
executionContextKey: null,
|
||||
cdpHash: null,
|
||||
length: 0,
|
||||
isModule: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const finalization = finalizeInspectorCapture({
|
||||
input: {
|
||||
inspector_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: target.id,
|
||||
observation_ms: 100,
|
||||
},
|
||||
runtime: {
|
||||
product: "Node.js/v24.18.0",
|
||||
protocol_version: "1.3",
|
||||
v8_version: null,
|
||||
},
|
||||
target,
|
||||
state,
|
||||
signal: controller.signal,
|
||||
authorizeLocation: async () => {
|
||||
authorizationStarted = true;
|
||||
controller.abort();
|
||||
return {
|
||||
allowed: true,
|
||||
location: {
|
||||
kind: "url",
|
||||
origin: "https://example.test",
|
||||
sanitized_url: "https://example.test/script.js",
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
await expect(finalization).rejects.toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(authorizationStarted).toBe(true);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
import {
|
||||
authorizeRuntimeLocation,
|
||||
inspectorExclusionKey,
|
||||
throwIfRuntimeObservationCancelled,
|
||||
} from "./JavaScriptRuntimeScope.js";
|
||||
import type { CaptureState, ScriptDraft } from "./V8InspectorProvider.js";
|
||||
import type { AuthorizedV8InspectorTarget } from "./V8InspectorEndpoint.js";
|
||||
@@ -38,7 +39,11 @@ interface FinalizeCaptureInput {
|
||||
readonly runtime: JavaScriptRuntimeTargetList["runtime"];
|
||||
readonly target: AuthorizedV8InspectorTarget;
|
||||
readonly state: CaptureState;
|
||||
readonly authorizeLocation?: typeof authorizeRuntimeLocation;
|
||||
readonly authorizeLocation?: (
|
||||
rawUrl: string,
|
||||
signal?: AbortSignal,
|
||||
) => ReturnType<typeof authorizeRuntimeLocation>;
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
|
||||
const LOCATION_AUTHORIZATION_WORKERS = 8;
|
||||
@@ -55,7 +60,9 @@ export const finalizeInspectorCapture = async ({
|
||||
target,
|
||||
state,
|
||||
authorizeLocation = authorizeRuntimeLocation,
|
||||
signal,
|
||||
}: FinalizeCaptureInput): Promise<JavaScriptRuntimeObservation> => {
|
||||
throwIfRuntimeObservationCancelled(signal);
|
||||
const exclusions = createInspectorExclusionCounts();
|
||||
const scripts = new Map<
|
||||
string,
|
||||
@@ -74,6 +81,7 @@ export const finalizeInspectorCapture = async ({
|
||||
const worker = async (): Promise<void> => {
|
||||
try {
|
||||
for (;;) {
|
||||
throwIfRuntimeObservationCancelled(signal);
|
||||
if (stopScheduling) return;
|
||||
const index = nextGroup;
|
||||
nextGroup += 1;
|
||||
@@ -82,8 +90,9 @@ export const finalizeInspectorCapture = async ({
|
||||
const [rawUrl, drafts] = group;
|
||||
authorizedGroups[index] = {
|
||||
drafts,
|
||||
decision: await authorizeLocation(rawUrl),
|
||||
decision: await authorizeLocation(rawUrl, signal),
|
||||
};
|
||||
throwIfRuntimeObservationCancelled(signal);
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
stopScheduling = true;
|
||||
@@ -99,6 +108,7 @@ export const finalizeInspectorCapture = async ({
|
||||
const failedWorker = workerResults.find(
|
||||
(result) => result.status === "rejected",
|
||||
);
|
||||
throwIfRuntimeObservationCancelled(signal);
|
||||
if (failedWorker?.status === "rejected") throw failedWorker.reason;
|
||||
for (const { drafts, decision } of authorizedGroups) {
|
||||
for (const draft of drafts) {
|
||||
@@ -131,6 +141,7 @@ export const finalizeInspectorCapture = async ({
|
||||
? 1
|
||||
: 0,
|
||||
);
|
||||
throwIfRuntimeObservationCancelled(signal);
|
||||
return {
|
||||
runtime,
|
||||
target: {
|
||||
|
||||
@@ -42,6 +42,12 @@ import {
|
||||
export { V8_INSPECTOR_PROVIDER_IDENTITY } from "./providerIdentity.js";
|
||||
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "./providerIdentity.js";
|
||||
|
||||
/** Maximum decoded CDP message accepted while observing one Inspector target. */
|
||||
export const INSPECTOR_MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
|
||||
/** Aggregate retained metadata budget, including an estimate of JS object overhead. */
|
||||
export const INSPECTOR_MAX_RETAINED_METADATA_BYTES = 8 * 1024 * 1024;
|
||||
const RETAINED_OBJECT_OVERHEAD_BYTES = 512;
|
||||
|
||||
export interface ScriptDraft {
|
||||
readonly rawUrl: string;
|
||||
readonly executionContextKey: string | null;
|
||||
@@ -147,6 +153,7 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort {
|
||||
target.webSocketUrl,
|
||||
"observe_javascript_runtime",
|
||||
options.signal,
|
||||
{ maxPayloadBytes: INSPECTOR_MAX_PAYLOAD_BYTES },
|
||||
);
|
||||
const state = emptyCaptureState();
|
||||
const removeListener = connection.onEvent((event) =>
|
||||
@@ -164,6 +171,7 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort {
|
||||
runtime: discovery.runtime,
|
||||
target,
|
||||
state,
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
});
|
||||
outcome = ok(javascriptRuntimeObservationSchema.parse(result));
|
||||
} catch (cause: unknown) {
|
||||
@@ -344,8 +352,8 @@ const ingestScript = (
|
||||
length: nonnegativeInteger(value?.length),
|
||||
isModule: value?.isModule === true,
|
||||
};
|
||||
const bytes = metadataBytes(draft);
|
||||
retainEvent(state, bytes);
|
||||
const bytes = retainedMetadataBytes(draft);
|
||||
if (!retainEvent(state, bytes)) return;
|
||||
state.scripts.push(draft);
|
||||
};
|
||||
|
||||
@@ -355,8 +363,16 @@ const ingestContext = (
|
||||
state: CaptureState,
|
||||
): void => {
|
||||
if (event.method === "Runtime.executionContextsCleared") {
|
||||
for (const context of state.contexts.values()) context.state = "cleared";
|
||||
retainEvent(state, 0);
|
||||
let byteDelta = 0;
|
||||
for (const context of state.contexts.values()) {
|
||||
byteDelta +=
|
||||
retainedMetadataBytes({ ...context, state: "cleared" }) -
|
||||
retainedMetadataBytes(context);
|
||||
}
|
||||
if (replaceRetainedMetadata(state, byteDelta)) {
|
||||
for (const context of state.contexts.values()) context.state = "cleared";
|
||||
retainEvent(state, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const parameters = recordValue(event.params);
|
||||
@@ -385,14 +401,32 @@ const ingestContext = (
|
||||
: "destroyed",
|
||||
origin: origin === "" ? null : origin,
|
||||
};
|
||||
const bytes = metadataBytes(draft);
|
||||
retainEvent(state, bytes);
|
||||
const previous = state.contexts.get(key);
|
||||
const bytes =
|
||||
retainedMetadataBytes(draft) -
|
||||
(previous === undefined ? 0 : retainedMetadataBytes(previous));
|
||||
if (!retainEvent(state, bytes)) return;
|
||||
state.contexts.set(key, draft);
|
||||
};
|
||||
|
||||
const retainEvent = (state: CaptureState, bytes: number): void => {
|
||||
state.metadataBytes += bytes;
|
||||
const retainEvent = (state: CaptureState, bytes: number): boolean => {
|
||||
if (!replaceRetainedMetadata(state, bytes)) return false;
|
||||
state.eventsRetained += 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
const replaceRetainedMetadata = (
|
||||
state: CaptureState,
|
||||
byteDelta: number,
|
||||
): boolean => {
|
||||
if (state.metadataBytes + byteDelta > INSPECTOR_MAX_RETAINED_METADATA_BYTES) {
|
||||
state.eventsDropped += 1;
|
||||
state.truncated = true;
|
||||
state.truncationReasons.add("retained_metadata_budget_exceeded");
|
||||
return false;
|
||||
}
|
||||
state.metadataBytes += byteDelta;
|
||||
return true;
|
||||
};
|
||||
|
||||
const waitForCapture = async (
|
||||
@@ -402,14 +436,7 @@ const waitForCapture = async (
|
||||
): Promise<void> => {
|
||||
let removeDisconnect = (): void => undefined;
|
||||
const disconnected = new Promise<never>((_resolve, reject) => {
|
||||
removeDisconnect = connection.onDisconnect(() =>
|
||||
reject(
|
||||
new BrowserObservationError(
|
||||
"observe_javascript_runtime",
|
||||
"disconnected",
|
||||
),
|
||||
),
|
||||
);
|
||||
removeDisconnect = connection.onDisconnect((error) => reject(error));
|
||||
});
|
||||
try {
|
||||
await Promise.race([
|
||||
@@ -425,8 +452,16 @@ const waitForCapture = async (
|
||||
}
|
||||
};
|
||||
|
||||
const metadataBytes = (value: object): number =>
|
||||
Buffer.byteLength(JSON.stringify(value));
|
||||
const retainedMetadataBytes = (value: ScriptDraft | ContextDraft): number => {
|
||||
const strings =
|
||||
"rawUrl" in value
|
||||
? [value.rawUrl, value.rawUrl, value.executionContextKey, value.cdpHash]
|
||||
: [value.contextKey, value.origin];
|
||||
return (
|
||||
RETAINED_OBJECT_OVERHEAD_BYTES +
|
||||
strings.reduce((total, text) => total + (text?.length ?? 0) * 2, 0)
|
||||
);
|
||||
};
|
||||
|
||||
const nonnegativeInteger = (value: unknown): number => {
|
||||
const parsed = numberValue(value);
|
||||
|
||||
@@ -175,9 +175,10 @@ describe("complete Inspector script hashes", () => {
|
||||
scriptHashes,
|
||||
});
|
||||
try {
|
||||
const result = await new V8InspectorProvider().observe(
|
||||
observeInput(fake.endpoint, fake.targetId, "node"),
|
||||
);
|
||||
const result = await new V8InspectorProvider().observe({
|
||||
...observeInput(fake.endpoint, fake.targetId, "node"),
|
||||
observation_ms: 1_000,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.scripts.items).toHaveLength(scriptHashes.length);
|
||||
expect(
|
||||
@@ -212,7 +213,7 @@ describe("undeclared V8 runtime role", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("passive V8 Inspector evidence", () => {
|
||||
describe("V8 Inspector script retention", () => {
|
||||
test("retains authorized script locations longer than the former byte ceiling", async () => {
|
||||
const longUrl = `https://example.test/${"a".repeat(20_000)}`;
|
||||
const fake = await startFakeV8Inspector({
|
||||
@@ -261,7 +262,69 @@ describe("passive V8 Inspector evidence", () => {
|
||||
await fake.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("bounded V8 Inspector capture", () => {
|
||||
test("bounds aggregate script and context metadata while retaining a useful partial capture", async () => {
|
||||
const fixture = await runtimeFixture();
|
||||
const url = pathToFileURL(fixture.entry).href;
|
||||
const scriptHashes = Array.from(
|
||||
{ length: 2_300 },
|
||||
(_, index) => `${"h".repeat(4_096)}${String(index)}`,
|
||||
);
|
||||
const fake = await startFakeV8Inspector({
|
||||
targetUrl: url,
|
||||
scriptUrls: scriptHashes.map(() => url),
|
||||
scriptHashes,
|
||||
contextTransitionCount: 2_000,
|
||||
});
|
||||
try {
|
||||
const result = await new V8InspectorProvider().observe({
|
||||
...observeInput(fake.endpoint, fake.targetId, "node"),
|
||||
observation_ms: 1_000,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.capture.truncated).toBe(true);
|
||||
expect(result.value.capture.truncation_reasons).toEqual([
|
||||
"retained_metadata_budget_exceeded",
|
||||
]);
|
||||
expect(result.value.capture.events_dropped).toBeGreaterThan(0);
|
||||
expect(result.value.capture.metadata_bytes_retained).toBeLessThanOrEqual(
|
||||
8 * 1024 * 1024,
|
||||
);
|
||||
expect(result.value.capture.events_observed).toBeGreaterThan(
|
||||
result.value.capture.events_retained,
|
||||
);
|
||||
expect(result.value.execution_contexts).toHaveLength(1);
|
||||
} finally {
|
||||
await fake.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects a CDP event larger than the configured transport payload budget", async () => {
|
||||
const fixture = await runtimeFixture();
|
||||
const fake = await startFakeV8Inspector({
|
||||
targetUrl: pathToFileURL(fixture.entry).href,
|
||||
oversizedEventBytes: 16 * 1024 * 1024,
|
||||
});
|
||||
try {
|
||||
const result = await new V8InspectorProvider().observe(
|
||||
observeInput(fake.endpoint, fake.targetId, "node"),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BrowserObservationError",
|
||||
reason: "payload_limit",
|
||||
});
|
||||
expect(result.error.userMessage).toContain("protocol budget");
|
||||
} finally {
|
||||
await fake.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("passive V8 Inspector evidence", () => {
|
||||
test("produces deterministic Evidence through the service without grants", async () => {
|
||||
const fixture = await runtimeFixture();
|
||||
const fake = await startFakeV8Inspector({
|
||||
|
||||
+80
-41
@@ -14,6 +14,8 @@ interface FakeV8InspectorOptions {
|
||||
readonly runtimeProduct?: string;
|
||||
readonly scriptUrls?: readonly string[];
|
||||
readonly scriptHashes?: readonly string[];
|
||||
readonly oversizedEventBytes?: number;
|
||||
readonly contextTransitionCount?: number;
|
||||
readonly additionalTargetUrl?: string;
|
||||
readonly additionalTargetCount?: number;
|
||||
readonly closeOnMethod?: string;
|
||||
@@ -94,47 +96,7 @@ export const startFakeV8Inspector = async (
|
||||
);
|
||||
});
|
||||
webSockets.on("connection", (socket: WebSocket) => {
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
socket.on("message", (raw) => {
|
||||
const command = parseCommand(raw.toString());
|
||||
commands.push(command);
|
||||
if (options.closeOnMethod === command.method) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
socket.send(JSON.stringify({ id: command.id, result: {} }));
|
||||
if (command.method === "Runtime.enable")
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
method: "Runtime.executionContextCreated",
|
||||
params: {
|
||||
context: {
|
||||
id: 1,
|
||||
origin: "",
|
||||
name: "node[fixture]",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
if (command.method === "Debugger.enable")
|
||||
for (const [index, url] of (
|
||||
options.scriptUrls ?? [options.targetUrl]
|
||||
).entries())
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
method: "Debugger.scriptParsed",
|
||||
params: {
|
||||
scriptId: String(index + 1),
|
||||
url,
|
||||
executionContextId: 1,
|
||||
hash: options.scriptHashes?.[index] ?? `hash-${String(index)}`,
|
||||
length: 100 + index,
|
||||
isModule: index % 2 === 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
registerInspectorSocket(socket, options, commands, sockets);
|
||||
});
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
http.once("error", reject);
|
||||
@@ -160,6 +122,83 @@ export const startFakeV8Inspector = async (
|
||||
};
|
||||
};
|
||||
|
||||
const registerInspectorSocket = (
|
||||
socket: WebSocket,
|
||||
options: FakeV8InspectorOptions,
|
||||
commands: InspectorCommand[],
|
||||
sockets: Set<WebSocket>,
|
||||
): void => {
|
||||
sockets.add(socket);
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
socket.on("message", (raw) => {
|
||||
const command = parseCommand(raw.toString());
|
||||
commands.push(command);
|
||||
if (options.closeOnMethod === command.method) {
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
socket.send(JSON.stringify({ id: command.id, result: {} }));
|
||||
respondToInspectorCommand(socket, command, options);
|
||||
});
|
||||
};
|
||||
|
||||
const respondToInspectorCommand = (
|
||||
socket: WebSocket,
|
||||
command: InspectorCommand,
|
||||
options: FakeV8InspectorOptions,
|
||||
): void => {
|
||||
if (command.method === "Runtime.enable") sendRuntimeEvents(socket, options);
|
||||
if (command.method === "Debugger.enable") sendScriptEvents(socket, options);
|
||||
};
|
||||
|
||||
const sendRuntimeEvents = (
|
||||
socket: WebSocket,
|
||||
options: FakeV8InspectorOptions,
|
||||
): void => {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
method: "Runtime.executionContextCreated",
|
||||
params: { context: { id: 1, origin: "", name: "node[fixture]" } },
|
||||
}),
|
||||
);
|
||||
for (let index = 0; index < (options.contextTransitionCount ?? 0); index += 1)
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
method: "Runtime.executionContextDestroyed",
|
||||
params: { executionContextId: 1 },
|
||||
}),
|
||||
);
|
||||
if (options.oversizedEventBytes !== undefined)
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
method: "Runtime.noisyFixtureEvent",
|
||||
params: { padding: "x".repeat(options.oversizedEventBytes) },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const sendScriptEvents = (
|
||||
socket: WebSocket,
|
||||
options: FakeV8InspectorOptions,
|
||||
): void => {
|
||||
for (const [index, url] of (
|
||||
options.scriptUrls ?? [options.targetUrl]
|
||||
).entries())
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
method: "Debugger.scriptParsed",
|
||||
params: {
|
||||
scriptId: String(index + 1),
|
||||
url,
|
||||
executionContextId: 1,
|
||||
hash: options.scriptHashes?.[index] ?? `hash-${String(index)}`,
|
||||
length: 100 + index,
|
||||
isModule: index % 2 === 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const target = (
|
||||
id: string,
|
||||
url: string,
|
||||
|
||||
Reference in New Issue
Block a user