Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cdp-proxy/CDPMessageHandlers/CDPAPINames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export const CDP_API_NAMES = {
CLOSE: "close",
DEBUGGER_SCRIPT_PARSED: "Debugger.scriptParsed",
DEBUGGER_SET_BREAKPOINT_BY_URL: "Debugger.setBreakpointByUrl",
PAGE_RELOAD: "Page.reload",
};
66 changes: 66 additions & 0 deletions src/cdp-proxy/CDPMessageHandlers/hermesCDPMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,34 @@ export class HermesCDPMessageHandler extends BaseCDPMessageHandler {
*/
private readonly HERMES_NATIVE_FUNCTION_SCRIPT_ID: string = "4294967295";

/**
* @description Quiet period after the last breakpoint request before the application
* is reloaded, so that a burst of breakpoint requests causes a single reload
* @type {number}
*/
private readonly RELOAD_DEBOUNCE_MS: number = 300;

/**
* @description Id of the out-of-band Page.reload request. The debugger never sent it,
* so it discards the reply as an unknown response id
* @type {number}
*/
private readonly RELOAD_REQUEST_ID: number = 2147483646;

private isFirstRun: boolean = true;
private reloadTimer: NodeJS.Timeout | null = null;

public processDebuggerCDPMessage(event: any): ProcessedCDPMessage {
let sendBack = false;
if (event.method === CDP_API_NAMES.CLOSE) {
this.cancelFirstRunReload();
} else if (
event.method === CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT ||
event.method === CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT_BY_URL
) {
this.scheduleFirstRunReload();
}

if (event.method === CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT) {
event = this.handleBreakpointSetting(event);
} else if (event.method === CDP_API_NAMES.RUNTIME_CALL_FUNCTION_ON) {
Expand Down Expand Up @@ -100,6 +126,46 @@ export class HermesCDPMessageHandler extends BaseCDPMessageHandler {
return event;
}

/**
* The application finishes executing its bundle before the debugger manages to attach,
* so breakpoints registered for the first run resolve but are never reached. Reload the
* application once the debugger has finished sending its initial breakpoints, so that the
* bundle is evaluated again while they are in place. Reloading is driven by the device
* (the "nativePageReloads" capability), so the debugger stays attached across it.
*/
private scheduleFirstRunReload(): void {
if (!this.isFirstRun) {
return;
}

if (this.reloadTimer) {
clearTimeout(this.reloadTimer);
}

this.reloadTimer = setTimeout(() => {
this.reloadTimer = null;

if (!this.isFirstRun) {
return;
}
this.isFirstRun = false;

this.applicationTarget?.send({
id: this.RELOAD_REQUEST_ID,
method: CDP_API_NAMES.PAGE_RELOAD,
params: {},
});
}, this.RELOAD_DEBOUNCE_MS);
}

private cancelFirstRunReload(): void {
if (this.reloadTimer) {
clearTimeout(this.reloadTimer);
this.reloadTimer = null;
}
this.isFirstRun = true;
}

private handleBreakpointSetting(event: any): any {
if (event.params) {
delete event.params.location.columnNumber;
Expand Down
101 changes: 101 additions & 0 deletions test/cdp-proxy/hermesCDPMessageHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

import { Connection } from "vscode-cdp-proxy";
import { HermesCDPMessageHandler } from "../../src/cdp-proxy/CDPMessageHandlers/hermesCDPMessageHandler";
import { CDP_API_NAMES } from "../../src/cdp-proxy/CDPMessageHandlers/CDPAPINames";
import { PromiseUtil } from "../../src/common/node/promise";
import assert = require("assert");

suite("hermesCDPMessageHandler", function () {
// Must outlast RELOAD_DEBOUNCE_MS (300) in the handler
const afterDebounce = 500;

function createHandler(): {
handler: HermesCDPMessageHandler;
sent: Array<Record<string, any>>;
} {
const handler = new HermesCDPMessageHandler();
const sent: Array<Record<string, any>> = [];
handler.setApplicationTarget({
send: (message: Record<string, any>) => sent.push(message),
} as unknown as Connection);
return { handler, sent };
}

suite("first run reload", function () {
test("reloads the application once the debugger has sent its breakpoints", async () => {
const { handler, sent } = createHandler();

handler.processDebuggerCDPMessage({
method: CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT_BY_URL,
params: {},
});
await PromiseUtil.delay(afterDebounce);

assert.strictEqual(sent.length, 1);
assert.strictEqual(sent[0].method, CDP_API_NAMES.PAGE_RELOAD);
});

test("coalesces a burst of breakpoints into a single reload", async () => {
const { handler, sent } = createHandler();

for (let i = 0; i < 5; i++) {
handler.processDebuggerCDPMessage({
method: CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT_BY_URL,
params: {},
});
}
await PromiseUtil.delay(afterDebounce);

assert.strictEqual(sent.length, 1);
});

test("does not reload again for later breakpoints in the same session", async () => {
const { handler, sent } = createHandler();

handler.processDebuggerCDPMessage({
method: CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT_BY_URL,
params: {},
});
await PromiseUtil.delay(afterDebounce);

handler.processDebuggerCDPMessage({
method: CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT_BY_URL,
params: {},
});
await PromiseUtil.delay(afterDebounce);

assert.strictEqual(sent.length, 1);
});

test("does not reload after the debugger disconnects", async () => {
const { handler, sent } = createHandler();

handler.processDebuggerCDPMessage({
method: CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT_BY_URL,
params: {},
});
handler.processDebuggerCDPMessage({ method: CDP_API_NAMES.CLOSE });
await PromiseUtil.delay(afterDebounce);

assert.strictEqual(sent.length, 0);
});

test("leaves breakpoint messages on their way to the application", function () {
const { handler } = createHandler();

const processed = handler.processDebuggerCDPMessage({
method: CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT,
params: { location: { lineNumber: 1, columnNumber: 2 } },
});

const event = processed.event as Record<string, any>;

assert.strictEqual(processed.sendBack, false);
assert.strictEqual(event.method, CDP_API_NAMES.DEBUGGER_SET_BREAKPOINT);
// Hermes rejects breakpoints carrying a column number
assert.strictEqual(event.params.location.columnNumber, undefined);
});
});
});