diff --git a/src/cdp-proxy/CDPMessageHandlers/CDPAPINames.ts b/src/cdp-proxy/CDPMessageHandlers/CDPAPINames.ts index e005635f9..710553244 100644 --- a/src/cdp-proxy/CDPMessageHandlers/CDPAPINames.ts +++ b/src/cdp-proxy/CDPMessageHandlers/CDPAPINames.ts @@ -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", }; diff --git a/src/cdp-proxy/CDPMessageHandlers/hermesCDPMessageHandler.ts b/src/cdp-proxy/CDPMessageHandlers/hermesCDPMessageHandler.ts index 82314e4ec..02b5f748d 100644 --- a/src/cdp-proxy/CDPMessageHandlers/hermesCDPMessageHandler.ts +++ b/src/cdp-proxy/CDPMessageHandlers/hermesCDPMessageHandler.ts @@ -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) { @@ -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; diff --git a/test/cdp-proxy/hermesCDPMessageHandler.test.ts b/test/cdp-proxy/hermesCDPMessageHandler.test.ts new file mode 100644 index 000000000..91a630af6 --- /dev/null +++ b/test/cdp-proxy/hermesCDPMessageHandler.test.ts @@ -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>; + } { + const handler = new HermesCDPMessageHandler(); + const sent: Array> = []; + handler.setApplicationTarget({ + send: (message: Record) => 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; + + 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); + }); + }); +});