UNPKG

chrome-devtools-frontend

Version:
1,317 lines (1,164 loc) • 57.3 kB
// Copyright 2021 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* * Copyright (C) 2010 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the #name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as Root from '../root/root.js'; import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; import * as Protocol from '../../generated/protocol.js'; import {ScopeRef, type GetPropertiesResult, type RemoteObject} from './RemoteObject.js'; import {Events as ResourceTreeModelEvents, ResourceTreeModel} from './ResourceTreeModel.js'; import {RuntimeModel, type EvaluationOptions, type EvaluationResult, type ExecutionContext} from './RuntimeModel.js'; import {Script} from './Script.js'; import {Capability, Type, type Target} from './Target.js'; import {SDKModel} from './SDKModel.js'; import {SourceMapManager} from './SourceMapManager.js'; const UIStrings = { /** *@description Title of a section in the debugger showing local JavaScript variables. */ local: 'Local', /** *@description Text that refers to closure as a programming term */ closure: 'Closure', /** *@description Noun that represents a section or block of code in the Debugger Model. Shown in the Sources tab, while paused on a breakpoint. */ block: 'Block', /** *@description Label for a group of JavaScript files */ script: 'Script', /** *@description Title of a section in the debugger showing JavaScript variables from the a 'with' *block. Block here means section of code, 'with' refers to a JavaScript programming concept and *is a fixed term. */ withBlock: '`With` block', /** *@description Title of a section in the debugger showing JavaScript variables from the a 'catch' *block. Block here means section of code, 'catch' refers to a JavaScript programming concept and *is a fixed term. */ catchBlock: '`Catch` block', /** *@description Title of a section in the debugger showing JavaScript variables from the global scope. */ global: 'Global', /** *@description Text for a JavaScript module, the programming concept */ module: 'Module', /** *@description Text describing the expression scope in WebAssembly */ expression: 'Expression', }; const str_ = i18n.i18n.registerUIStrings('core/sdk/DebuggerModel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export function sortAndMergeRanges(locationRanges: Protocol.Debugger.LocationRange[]): Protocol.Debugger.LocationRange[] { function compare(p1: Protocol.Debugger.ScriptPosition, p2: Protocol.Debugger.ScriptPosition): number { return (p1.lineNumber - p2.lineNumber) || (p1.columnNumber - p2.columnNumber); } function overlap(r1: Protocol.Debugger.LocationRange, r2: Protocol.Debugger.LocationRange): boolean { if (r1.scriptId !== r2.scriptId) { return false; } const n = compare(r1.start, r2.start); if (n < 0) { return compare(r1.end, r2.start) >= 0; } if (n > 0) { return compare(r1.start, r2.end) <= 0; } return true; } if (locationRanges.length === 0) { return []; } locationRanges.sort((r1, r2): number => { if (r1.scriptId < r2.scriptId) { return -1; } if (r1.scriptId > r2.scriptId) { return 1; } return compare(r1.start, r2.start) || compare(r1.end, r2.end); }); let prev = locationRanges[0]; const merged = []; for (let i = 1; i < locationRanges.length; ++i) { const curr = locationRanges[i]; if (overlap(prev, curr)) { if (compare(prev.end, curr.end) <= 0) { prev = {...prev, end: curr.end}; } } else { merged.push(prev); prev = curr; } } merged.push(prev); return merged; } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum StepMode { StepInto = 'StepInto', StepOut = 'StepOut', StepOver = 'StepOver', } export class DebuggerModel extends SDKModel<EventTypes> { readonly agent: ProtocolProxyApi.DebuggerApi; runtimeModelInternal: RuntimeModel; readonly #sourceMapManagerInternal: SourceMapManager<Script>; #debuggerPausedDetailsInternal: DebuggerPausedDetails|null; readonly #scriptsInternal: Map<string, Script>; readonly #scriptsBySourceURL: Map<string, Script[]>; #discardableScripts: Script[]; continueToLocationCallback: ((arg0: DebuggerPausedDetails) => boolean)|null; #selectedCallFrameInternal: CallFrame|null; #debuggerEnabledInternal: boolean; #debuggerId: string|null; #skipAllPausesTimeout: number; #beforePausedCallback: ((arg0: DebuggerPausedDetails, stepOver: Location|null) => Promise<boolean>)|null; #computeAutoStepRangesCallback: ((arg0: StepMode, arg1: CallFrame) => Promise<Array<{ start: Location, end: Location, }>>)|null; #expandCallFramesCallback: ((arg0: Array<CallFrame>) => Promise<Array<CallFrame>>)|null; evaluateOnCallFrameCallback: ((arg0: CallFrame, arg1: EvaluationOptions) => Promise<EvaluationResult|null>)|null; #synchronizeBreakpointsCallback: ((script: Script) => Promise<void>)|null; // We need to be able to register listeners for individual breakpoints. As such, we dispatch // on breakpoint ids, which are not statically known. The event #payload will always be a `Location`. readonly #breakpointResolvedEventTarget = new Common.ObjectWrapper.ObjectWrapper<{[breakpointId: string]: Location}>(); // When stepping over with autostepping enabled, the context denotes the function to which autostepping is restricted // to by way of its functionLocation (as per Debugger.CallFrame). #autoSteppingContext: Location|null; #isPausingInternal: boolean; constructor(target: Target) { super(target); target.registerDebuggerDispatcher(new DebuggerDispatcher(this)); this.agent = target.debuggerAgent(); this.runtimeModelInternal = (target.model(RuntimeModel) as RuntimeModel); this.#sourceMapManagerInternal = new SourceMapManager(target); this.#debuggerPausedDetailsInternal = null; this.#scriptsInternal = new Map(); this.#scriptsBySourceURL = new Map(); this.#discardableScripts = []; this.continueToLocationCallback = null; this.#selectedCallFrameInternal = null; this.#debuggerEnabledInternal = false; this.#debuggerId = null; this.#skipAllPausesTimeout = 0; this.#beforePausedCallback = null; this.#computeAutoStepRangesCallback = null; this.#expandCallFramesCallback = null; this.evaluateOnCallFrameCallback = null; this.#synchronizeBreakpointsCallback = null; this.#autoSteppingContext = null; this.#isPausingInternal = false; Common.Settings.Settings.instance() .moduleSetting('pauseOnExceptionEnabled') .addChangeListener(this.pauseOnExceptionStateChanged, this); Common.Settings.Settings.instance() .moduleSetting('pauseOnCaughtException') .addChangeListener(this.pauseOnExceptionStateChanged, this); Common.Settings.Settings.instance() .moduleSetting('pauseOnUncaughtException') .addChangeListener(this.pauseOnExceptionStateChanged, this); Common.Settings.Settings.instance() .moduleSetting('disableAsyncStackTraces') .addChangeListener(this.asyncStackTracesStateChanged, this); Common.Settings.Settings.instance() .moduleSetting('breakpointsActive') .addChangeListener(this.breakpointsActiveChanged, this); if (!target.suspended()) { void this.enableDebugger(); } this.#sourceMapManagerInternal.setEnabled( Common.Settings.Settings.instance().moduleSetting('jsSourceMapsEnabled').get()); Common.Settings.Settings.instance() .moduleSetting('jsSourceMapsEnabled') .addChangeListener(event => this.#sourceMapManagerInternal.setEnabled((event.data as boolean))); const resourceTreeModel = (target.model(ResourceTreeModel) as ResourceTreeModel); if (resourceTreeModel) { resourceTreeModel.addEventListener(ResourceTreeModelEvents.FrameNavigated, this.onFrameNavigated, this); } } sourceMapManager(): SourceMapManager<Script> { return this.#sourceMapManagerInternal; } runtimeModel(): RuntimeModel { return this.runtimeModelInternal; } debuggerEnabled(): boolean { return Boolean(this.#debuggerEnabledInternal); } debuggerId(): string|null { return this.#debuggerId; } private async enableDebugger(): Promise<void> { if (this.#debuggerEnabledInternal) { return; } this.#debuggerEnabledInternal = true; // Set a limit for the total size of collected script sources retained by debugger. // 10MB for remote frontends, 100MB for others. const isRemoteFrontend = Root.Runtime.Runtime.queryParam('remoteFrontend') || Root.Runtime.Runtime.queryParam('ws'); const maxScriptsCacheSize = isRemoteFrontend ? 10e6 : 100e6; const enablePromise = this.agent.invoke_enable({maxScriptsCacheSize}); let instrumentationPromise: Promise<Protocol.Debugger.SetInstrumentationBreakpointResponse>|undefined; if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.INSTRUMENTATION_BREAKPOINTS)) { instrumentationPromise = this.agent.invoke_setInstrumentationBreakpoint({ instrumentation: Protocol.Debugger.SetInstrumentationBreakpointRequestInstrumentation.BeforeScriptExecution, }); } this.pauseOnExceptionStateChanged(); void this.asyncStackTracesStateChanged(); if (!Common.Settings.Settings.instance().moduleSetting('breakpointsActive').get()) { this.breakpointsActiveChanged(); } this.dispatchEventToListeners(Events.DebuggerWasEnabled, this); const [enableResult] = await Promise.all([enablePromise, instrumentationPromise]); this.registerDebugger(enableResult); } async syncDebuggerId(): Promise<Protocol.Debugger.EnableResponse> { const isRemoteFrontend = Root.Runtime.Runtime.queryParam('remoteFrontend') || Root.Runtime.Runtime.queryParam('ws'); const maxScriptsCacheSize = isRemoteFrontend ? 10e6 : 100e6; const enablePromise = this.agent.invoke_enable({maxScriptsCacheSize}); void enablePromise.then(this.registerDebugger.bind(this)); return enablePromise; } private onFrameNavigated(): void { if (DebuggerModel.shouldResyncDebuggerId) { return; } DebuggerModel.shouldResyncDebuggerId = true; } private registerDebugger(response: Protocol.Debugger.EnableResponse): void { if (response.getError()) { return; } const {debuggerId} = response; _debuggerIdToModel.set(debuggerId, this); this.#debuggerId = debuggerId; this.dispatchEventToListeners(Events.DebuggerIsReadyToPause, this); } isReadyToPause(): boolean { return Boolean(this.#debuggerId); } static async modelForDebuggerId(debuggerId: string): Promise<DebuggerModel|null> { if (DebuggerModel.shouldResyncDebuggerId) { await DebuggerModel.resyncDebuggerIdForModels(); DebuggerModel.shouldResyncDebuggerId = false; } return _debuggerIdToModel.get(debuggerId) || null; } static async resyncDebuggerIdForModels(): Promise<void> { const dbgModels = _debuggerIdToModel.values(); for (const dbgModel of dbgModels) { if (dbgModel.debuggerEnabled()) { await dbgModel.syncDebuggerId(); } } } private async disableDebugger(): Promise<void> { if (!this.#debuggerEnabledInternal) { return; } this.#debuggerEnabledInternal = false; await this.asyncStackTracesStateChanged(); await this.agent.invoke_disable(); this.#isPausingInternal = false; this.globalObjectCleared(); this.dispatchEventToListeners(Events.DebuggerWasDisabled, this); if (typeof this.#debuggerId === 'string') { _debuggerIdToModel.delete(this.#debuggerId); } this.#debuggerId = null; } private skipAllPauses(skip: boolean): void { if (this.#skipAllPausesTimeout) { clearTimeout(this.#skipAllPausesTimeout); this.#skipAllPausesTimeout = 0; } void this.agent.invoke_setSkipAllPauses({skip}); } skipAllPausesUntilReloadOrTimeout(timeout: number): void { if (this.#skipAllPausesTimeout) { clearTimeout(this.#skipAllPausesTimeout); } void this.agent.invoke_setSkipAllPauses({skip: true}); // If reload happens before the timeout, the flag will be already unset and the timeout callback won't change anything. this.#skipAllPausesTimeout = window.setTimeout(this.skipAllPauses.bind(this, false), timeout); } private pauseOnExceptionStateChanged(): void { const pauseOnCaughtEnabled = Common.Settings.Settings.instance().moduleSetting('pauseOnCaughtException').get(); let state: Protocol.Debugger.SetPauseOnExceptionsRequestState; const pauseOnUncaughtEnabled = Common.Settings.Settings.instance().moduleSetting('pauseOnUncaughtException').get(); if (pauseOnCaughtEnabled && pauseOnUncaughtEnabled) { state = Protocol.Debugger.SetPauseOnExceptionsRequestState.All; } else if (pauseOnCaughtEnabled) { state = Protocol.Debugger.SetPauseOnExceptionsRequestState.Caught; } else if (pauseOnUncaughtEnabled) { state = Protocol.Debugger.SetPauseOnExceptionsRequestState.Uncaught; } else { state = Protocol.Debugger.SetPauseOnExceptionsRequestState.None; } void this.agent.invoke_setPauseOnExceptions({state}); } private asyncStackTracesStateChanged(): Promise<Protocol.ProtocolResponseWithError> { const maxAsyncStackChainDepth = 32; const enabled = !Common.Settings.Settings.instance().moduleSetting('disableAsyncStackTraces').get() && this.#debuggerEnabledInternal; const maxDepth = enabled ? maxAsyncStackChainDepth : 0; return this.agent.invoke_setAsyncCallStackDepth({maxDepth}); } private breakpointsActiveChanged(): void { void this.agent.invoke_setBreakpointsActive( {active: Common.Settings.Settings.instance().moduleSetting('breakpointsActive').get()}); } setComputeAutoStepRangesCallback(callback: ((arg0: StepMode, arg1: CallFrame) => Promise<LocationRange[]>)| null): void { this.#computeAutoStepRangesCallback = callback; } private async computeAutoStepSkipList(mode: StepMode): Promise<Protocol.Debugger.LocationRange[]> { let ranges: LocationRange[] = []; if (this.#computeAutoStepRangesCallback && this.#debuggerPausedDetailsInternal && this.#debuggerPausedDetailsInternal.callFrames.length > 0) { const [callFrame] = this.#debuggerPausedDetailsInternal.callFrames; ranges = await this.#computeAutoStepRangesCallback.call(null, mode, callFrame); } const skipList = ranges.map(({start, end}) => ({ scriptId: start.scriptId, start: {lineNumber: start.lineNumber, columnNumber: start.columnNumber}, end: {lineNumber: end.lineNumber, columnNumber: end.columnNumber}, })); return sortAndMergeRanges(skipList); } async stepInto(): Promise<void> { const skipList = await this.computeAutoStepSkipList(StepMode.StepInto); void this.agent.invoke_stepInto({breakOnAsyncCall: false, skipList}); } async stepOver(): Promise<void> { this.#autoSteppingContext = this.#debuggerPausedDetailsInternal?.callFrames[0]?.functionLocation() ?? null; const skipList = await this.computeAutoStepSkipList(StepMode.StepOver); void this.agent.invoke_stepOver({skipList}); } async stepOut(): Promise<void> { const skipList = await this.computeAutoStepSkipList(StepMode.StepOut); if (skipList.length !== 0) { void this.agent.invoke_stepOver({skipList}); } else { void this.agent.invoke_stepOut(); } } scheduleStepIntoAsync(): void { void this.computeAutoStepSkipList(StepMode.StepInto).then(skipList => { void this.agent.invoke_stepInto({breakOnAsyncCall: true, skipList}); }); } resume(): void { void this.agent.invoke_resume({terminateOnResume: false}); this.#isPausingInternal = false; } pause(): void { this.#isPausingInternal = true; this.skipAllPauses(false); void this.agent.invoke_pause(); } async setBreakpointByURL( url: Platform.DevToolsPath.UrlString, lineNumber: number, columnNumber?: number, condition?: BackendCondition): Promise<SetBreakpointResult> { // Convert file url to node-js path. let urlRegex; if (this.target().type() === Type.Node && url.startsWith('file://')) { const platformPath = Common.ParsedURL.ParsedURL.urlToRawPathString(url, Host.Platform.isWin()); urlRegex = `${Platform.StringUtilities.escapeForRegExp(platformPath)}|${Platform.StringUtilities.escapeForRegExp(url)}`; if (Host.Platform.isWin() && platformPath.match(/^.:\\/)) { // Match upper or lower case drive letter urlRegex = `[${platformPath[0].toUpperCase()}${platformPath[0].toLowerCase()}]` + urlRegex.substr(1); } } // Adjust column if needed. let minColumnNumber = 0; const scripts = this.#scriptsBySourceURL.get(url) || []; for (let i = 0, l = scripts.length; i < l; ++i) { const script = scripts[i]; if (lineNumber === script.lineOffset) { minColumnNumber = minColumnNumber ? Math.min(minColumnNumber, script.columnOffset) : script.columnOffset; } } columnNumber = Math.max(columnNumber || 0, minColumnNumber); const response = await this.agent.invoke_setBreakpointByUrl({ lineNumber: lineNumber, url: urlRegex ? undefined : url, urlRegex: urlRegex, columnNumber: columnNumber, condition: condition, }); if (response.getError()) { return {locations: [], breakpointId: null}; } let locations: Location[] = []; if (response.locations) { locations = response.locations.map(payload => Location.fromPayload(this, payload)); } return {locations, breakpointId: response.breakpointId}; } async setBreakpointInAnonymousScript( scriptHash: string, lineNumber: number, columnNumber?: number, condition?: BackendCondition): Promise<SetBreakpointResult> { const response = await this.agent.invoke_setBreakpointByUrl( {lineNumber: lineNumber, scriptHash: scriptHash, columnNumber: columnNumber, condition: condition}); if (response.getError()) { return {locations: [], breakpointId: null}; } let locations: Location[] = []; if (response.locations) { locations = response.locations.map(payload => Location.fromPayload(this, payload)); } return {locations, breakpointId: response.breakpointId}; } async removeBreakpoint(breakpointId: Protocol.Debugger.BreakpointId): Promise<void> { await this.agent.invoke_removeBreakpoint({breakpointId}); } async getPossibleBreakpoints(startLocation: Location, endLocation: Location|null, restrictToFunction: boolean): Promise<BreakLocation[]> { const response = await this.agent.invoke_getPossibleBreakpoints({ start: startLocation.payload(), end: endLocation ? endLocation.payload() : undefined, restrictToFunction: restrictToFunction, }); if (response.getError() || !response.locations) { return []; } return response.locations.map(location => BreakLocation.fromPayload(this, location)); } async fetchAsyncStackTrace(stackId: Protocol.Runtime.StackTraceId): Promise<Protocol.Runtime.StackTrace|null> { const response = await this.agent.invoke_getStackTrace({stackTraceId: stackId}); return response.getError() ? null : response.stackTrace; } breakpointResolved(breakpointId: string, location: Protocol.Debugger.Location): void { this.#breakpointResolvedEventTarget.dispatchEventToListeners(breakpointId, Location.fromPayload(this, location)); } globalObjectCleared(): void { this.resetDebuggerPausedDetails(); this.reset(); // TODO(dgozman): move clients to ExecutionContextDestroyed/ScriptCollected events. this.dispatchEventToListeners(Events.GlobalObjectCleared, this); } private reset(): void { for (const script of this.#scriptsInternal.values()) { this.#sourceMapManagerInternal.detachSourceMap(script); } this.#scriptsInternal.clear(); this.#scriptsBySourceURL.clear(); this.#discardableScripts = []; this.#autoSteppingContext = null; } scripts(): Script[] { return Array.from(this.#scriptsInternal.values()); } scriptForId(scriptId: string): Script|null { return this.#scriptsInternal.get(scriptId) || null; } /** * Returns all `Script` objects with the same provided `sourceURL`. The * resulting array is sorted by time with the newest `Script` in the front. */ scriptsForSourceURL(sourceURL: string): Script[] { return this.#scriptsBySourceURL.get(sourceURL) || []; } scriptsForExecutionContext(executionContext: ExecutionContext): Script[] { const result = []; for (const script of this.#scriptsInternal.values()) { if (script.executionContextId === executionContext.id) { result.push(script); } } return result; } get callFrames(): CallFrame[]|null { return this.#debuggerPausedDetailsInternal ? this.#debuggerPausedDetailsInternal.callFrames : null; } debuggerPausedDetails(): DebuggerPausedDetails|null { return this.#debuggerPausedDetailsInternal; } private async setDebuggerPausedDetails(debuggerPausedDetails: DebuggerPausedDetails): Promise<boolean> { this.#isPausingInternal = false; this.#debuggerPausedDetailsInternal = debuggerPausedDetails; if (this.#beforePausedCallback) { if (!await this.#beforePausedCallback.call(null, debuggerPausedDetails, this.#autoSteppingContext)) { return false; } } // If we resolved a location in auto-stepping callback, reset the // auto-step-over context. this.#autoSteppingContext = null; this.dispatchEventToListeners(Events.DebuggerPaused, this); this.setSelectedCallFrame(debuggerPausedDetails.callFrames[0]); return true; } private resetDebuggerPausedDetails(): void { this.#isPausingInternal = false; this.#debuggerPausedDetailsInternal = null; this.setSelectedCallFrame(null); } setBeforePausedCallback(callback: ((arg0: DebuggerPausedDetails, autoSteppingContext: Location|null) => Promise<boolean>)| null): void { this.#beforePausedCallback = callback; } setExpandCallFramesCallback(callback: ((arg0: Array<CallFrame>) => Promise<Array<CallFrame>>)|null): void { this.#expandCallFramesCallback = callback; } setEvaluateOnCallFrameCallback(callback: ((arg0: CallFrame, arg1: EvaluationOptions) => Promise<EvaluationResult|null>)| null): void { this.evaluateOnCallFrameCallback = callback; } setSynchronizeBreakpointsCallback(callback: ((script: Script) => Promise<void>)|null): void { this.#synchronizeBreakpointsCallback = callback; } async pausedScript( callFrames: Protocol.Debugger.CallFrame[], reason: Protocol.Debugger.PausedEventReason, auxData: Object|undefined, breakpointIds: string[], asyncStackTrace?: Protocol.Runtime.StackTrace, asyncStackTraceId?: Protocol.Runtime.StackTraceId): Promise<void> { if (reason === Protocol.Debugger.PausedEventReason.Instrumentation) { const script = this.scriptForId((auxData as PausedOnInstrumentationData).scriptId); if (this.#synchronizeBreakpointsCallback && script) { await this.#synchronizeBreakpointsCallback(script); } this.resume(); return; } const pausedDetails = new DebuggerPausedDetails(this, callFrames, reason, auxData, breakpointIds, asyncStackTrace, asyncStackTraceId); if (this.#expandCallFramesCallback) { pausedDetails.callFrames = await this.#expandCallFramesCallback.call(null, pausedDetails.callFrames); } if (this.continueToLocationCallback) { const callback = this.continueToLocationCallback; this.continueToLocationCallback = null; if (callback(pausedDetails)) { return; } } if (!await this.setDebuggerPausedDetails(pausedDetails)) { if (this.#autoSteppingContext) { void this.stepOver(); } else { void this.stepInto(); } } else { Common.EventTarget.fireEvent('DevTools.DebuggerPaused'); } } resumedScript(): void { this.resetDebuggerPausedDetails(); this.dispatchEventToListeners(Events.DebuggerResumed, this); } parsedScriptSource( scriptId: Protocol.Runtime.ScriptId, sourceURL: Platform.DevToolsPath.UrlString, startLine: number, startColumn: number, endLine: number, endColumn: number, // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any executionContextId: number, hash: string, executionContextAuxData: any, isLiveEdit: boolean, sourceMapURL: string|undefined, hasSourceURLComment: boolean, hasSyntaxError: boolean, length: number, isModule: boolean|null, originStackTrace: Protocol.Runtime.StackTrace|null, codeOffset: number|null, scriptLanguage: string|null, debugSymbols: Protocol.Debugger.DebugSymbols|null, embedderName: Platform.DevToolsPath.UrlString|null): Script { const knownScript = this.#scriptsInternal.get(scriptId); if (knownScript) { return knownScript; } let isContentScript = false; if (executionContextAuxData && ('isDefault' in executionContextAuxData)) { isContentScript = !executionContextAuxData['isDefault']; } const script = new Script( this, scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, isContentScript, isLiveEdit, sourceMapURL, hasSourceURLComment, length, isModule, originStackTrace, codeOffset, scriptLanguage, debugSymbols, embedderName); this.registerScript(script); this.dispatchEventToListeners(Events.ParsedScriptSource, script); if (script.isInlineScript() && !script.hasSourceURL) { if (script.isModule) { Host.userMetrics.inlineScriptParsed(Host.UserMetrics.VMInlineScriptType.MODULE_SCRIPT); } else { Host.userMetrics.inlineScriptParsed(Host.UserMetrics.VMInlineScriptType.CLASSIC_SCRIPT); } } if (script.sourceMapURL && !hasSyntaxError) { this.#sourceMapManagerInternal.attachSourceMap(script, script.sourceURL, script.sourceMapURL); } const isDiscardable = hasSyntaxError && script.isAnonymousScript(); if (isDiscardable) { this.#discardableScripts.push(script); this.collectDiscardedScripts(); } return script; } setSourceMapURL(script: Script, newSourceMapURL: Platform.DevToolsPath.UrlString): void { // Detach any previous source map from the `script` first. this.#sourceMapManagerInternal.detachSourceMap(script); script.sourceMapURL = newSourceMapURL; this.#sourceMapManagerInternal.attachSourceMap(script, script.sourceURL, script.sourceMapURL); } async setDebugInfoURL(script: Script, _externalURL: Platform.DevToolsPath.UrlString): Promise<void> { if (this.#expandCallFramesCallback && this.#debuggerPausedDetailsInternal) { this.#debuggerPausedDetailsInternal.callFrames = await this.#expandCallFramesCallback.call(null, this.#debuggerPausedDetailsInternal.callFrames); } this.dispatchEventToListeners(Events.DebugInfoAttached, script); } executionContextDestroyed(executionContext: ExecutionContext): void { for (const script of this.#scriptsInternal.values()) { if (script.executionContextId === executionContext.id) { this.#sourceMapManagerInternal.detachSourceMap(script); } } } private registerScript(script: Script): void { this.#scriptsInternal.set(script.scriptId, script); if (script.isAnonymousScript()) { return; } let scripts = this.#scriptsBySourceURL.get(script.sourceURL); if (!scripts) { scripts = []; this.#scriptsBySourceURL.set(script.sourceURL, scripts); } // Newer scripts with the same URL should be preferred so we put them in // the front. Consuming code usually will iterate over the array and pick // the first script that works. scripts.unshift(script); } private unregisterScript(script: Script): void { console.assert(script.isAnonymousScript()); this.#scriptsInternal.delete(script.scriptId); } private collectDiscardedScripts(): void { if (this.#discardableScripts.length < 1000) { return; } const scriptsToDiscard = this.#discardableScripts.splice(0, 100); for (const script of scriptsToDiscard) { this.unregisterScript(script); this.dispatchEventToListeners(Events.DiscardedAnonymousScriptSource, script); } } createRawLocation(script: Script, lineNumber: number, columnNumber: number, inlineFrameIndex?: number): Location { return this.createRawLocationByScriptId(script.scriptId, lineNumber, columnNumber, inlineFrameIndex); } createRawLocationByURL(sourceURL: string, lineNumber: number, columnNumber?: number, inlineFrameIndex?: number): Location|null { for (const script of this.#scriptsBySourceURL.get(sourceURL) || []) { if (script.lineOffset > lineNumber || (script.lineOffset === lineNumber && columnNumber !== undefined && script.columnOffset > columnNumber)) { continue; } if (script.endLine < lineNumber || (script.endLine === lineNumber && columnNumber !== undefined && script.endColumn <= columnNumber)) { continue; } return new Location(this, script.scriptId, lineNumber, columnNumber, inlineFrameIndex); } return null; } createRawLocationByScriptId( scriptId: Protocol.Runtime.ScriptId, lineNumber: number, columnNumber?: number, inlineFrameIndex?: number): Location { return new Location(this, scriptId, lineNumber, columnNumber, inlineFrameIndex); } createRawLocationsByStackTrace(stackTrace: Protocol.Runtime.StackTrace): Location[] { const rawLocations: Location[] = []; for (let current: Protocol.Runtime.StackTrace|undefined = stackTrace; current; current = current.parent) { for (const {scriptId, lineNumber, columnNumber} of current.callFrames) { rawLocations.push(this.createRawLocationByScriptId(scriptId, lineNumber, columnNumber)); } } return rawLocations; } isPaused(): boolean { return Boolean(this.debuggerPausedDetails()); } isPausing(): boolean { return this.#isPausingInternal; } setSelectedCallFrame(callFrame: CallFrame|null): void { if (this.#selectedCallFrameInternal === callFrame) { return; } this.#selectedCallFrameInternal = callFrame; this.dispatchEventToListeners(Events.CallFrameSelected, this); } selectedCallFrame(): CallFrame|null { return this.#selectedCallFrameInternal; } async evaluateOnSelectedCallFrame(options: EvaluationOptions): Promise<EvaluationResult> { const callFrame = this.selectedCallFrame(); if (!callFrame) { throw new Error('No call frame selected'); } return callFrame.evaluate(options); } functionDetailsPromise(remoteObject: RemoteObject): Promise<FunctionDetails|null> { return remoteObject.getAllProperties(false /* accessorPropertiesOnly */, false /* generatePreview */) .then(buildDetails.bind(this)); function buildDetails(this: DebuggerModel, response: GetPropertiesResult): FunctionDetails|null { if (!response) { return null; } let location: (RemoteObject|null|undefined)|null = null; if (response.internalProperties) { for (const prop of response.internalProperties) { if (prop.name === '[[FunctionLocation]]') { location = prop.value; } } } let functionName: RemoteObject|null = null; if (response.properties) { for (const prop of response.properties) { if (prop.name === 'name' && prop.value && prop.value.type === 'string') { functionName = prop.value; } } } let debuggerLocation: Location|null = null; if (location) { debuggerLocation = this.createRawLocationByScriptId( location.value.scriptId, location.value.lineNumber, location.value.columnNumber); } return {location: debuggerLocation, functionName: functionName ? functionName.value as string : ''}; } } async setVariableValue( scopeNumber: number, variableName: string, newValue: Protocol.Runtime.CallArgument, callFrameId: Protocol.Debugger.CallFrameId): Promise<string|undefined> { const response = await this.agent.invoke_setVariableValue({scopeNumber, variableName, newValue, callFrameId}); const error = response.getError(); return error; } addBreakpointListener( breakpointId: string, listener: (arg0: Common.EventTarget.EventTargetEvent<Location>) => void, thisObject?: Object): void { this.#breakpointResolvedEventTarget.addEventListener(breakpointId, listener, thisObject); } removeBreakpointListener( breakpointId: string, listener: (arg0: Common.EventTarget.EventTargetEvent<Location>) => void, thisObject?: Object): void { this.#breakpointResolvedEventTarget.removeEventListener(breakpointId, listener, thisObject); } async setBlackboxPatterns(patterns: string[]): Promise<boolean> { const response = await this.agent.invoke_setBlackboxPatterns({patterns}); const error = response.getError(); return !error; } override dispose(): void { this.#sourceMapManagerInternal.dispose(); if (this.#debuggerId) { _debuggerIdToModel.delete(this.#debuggerId); } Common.Settings.Settings.instance() .moduleSetting('pauseOnExceptionEnabled') .removeChangeListener(this.pauseOnExceptionStateChanged, this); Common.Settings.Settings.instance() .moduleSetting('pauseOnCaughtException') .removeChangeListener(this.pauseOnExceptionStateChanged, this); Common.Settings.Settings.instance() .moduleSetting('disableAsyncStackTraces') .removeChangeListener(this.asyncStackTracesStateChanged, this); } override async suspendModel(): Promise<void> { await this.disableDebugger(); } override async resumeModel(): Promise<void> { await this.enableDebugger(); } private static shouldResyncDebuggerId = false; getContinueToLocationCallback(): ((arg0: DebuggerPausedDetails) => boolean)|null { return this.continueToLocationCallback; } getEvaluateOnCallFrameCallback(): ((arg0: CallFrame, arg1: EvaluationOptions) => Promise<EvaluationResult|null>)|null { return this.evaluateOnCallFrameCallback; } } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/naming-convention export const _debuggerIdToModel = new Map<string, DebuggerModel>(); /** * Keep these in sync with WebCore::V8Debugger */ // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum PauseOnExceptionsState { DontPauseOnExceptions = 'none', PauseOnAllExceptions = 'all', PauseOnCaughtExceptions = 'caught', PauseOnUncaughtExceptions = 'uncaught', } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { DebuggerWasEnabled = 'DebuggerWasEnabled', DebuggerWasDisabled = 'DebuggerWasDisabled', DebuggerPaused = 'DebuggerPaused', DebuggerResumed = 'DebuggerResumed', DebugInfoAttached = 'DebugInfoAttached', ParsedScriptSource = 'ParsedScriptSource', DiscardedAnonymousScriptSource = 'DiscardedAnonymousScriptSource', GlobalObjectCleared = 'GlobalObjectCleared', CallFrameSelected = 'CallFrameSelected', DebuggerIsReadyToPause = 'DebuggerIsReadyToPause', ScriptSourceWasEdited = 'ScriptSourceWasEdited', } export type EventTypes = { [Events.DebuggerWasEnabled]: DebuggerModel, [Events.DebuggerWasDisabled]: DebuggerModel, [Events.DebuggerPaused]: DebuggerModel, [Events.DebuggerResumed]: DebuggerModel, [Events.ParsedScriptSource]: Script, [Events.DiscardedAnonymousScriptSource]: Script, [Events.GlobalObjectCleared]: DebuggerModel, [Events.CallFrameSelected]: DebuggerModel, [Events.DebuggerIsReadyToPause]: DebuggerModel, [Events.DebugInfoAttached]: Script, [Events.ScriptSourceWasEdited]: { script: Script, status: Protocol.Debugger.SetScriptSourceResponseStatus, }, }; class DebuggerDispatcher implements ProtocolProxyApi.DebuggerDispatcher { #debuggerModel: DebuggerModel; constructor(debuggerModel: DebuggerModel) { this.#debuggerModel = debuggerModel; } paused({callFrames, reason, data, hitBreakpoints, asyncStackTrace, asyncStackTraceId}: Protocol.Debugger.PausedEvent): void { if (!this.#debuggerModel.debuggerEnabled()) { return; } void this.#debuggerModel.pausedScript( callFrames, reason, data, hitBreakpoints || [], asyncStackTrace, asyncStackTraceId); } resumed(): void { if (!this.#debuggerModel.debuggerEnabled()) { return; } this.#debuggerModel.resumedScript(); } scriptParsed({ scriptId, url, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, isLiveEdit, sourceMapURL, hasSourceURL, length, isModule, stackTrace, codeOffset, scriptLanguage, debugSymbols, embedderName, }: Protocol.Debugger.ScriptParsedEvent): void { if (!this.#debuggerModel.debuggerEnabled()) { return; } this.#debuggerModel.parsedScriptSource( scriptId, url as Platform.DevToolsPath.UrlString, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, Boolean(isLiveEdit), sourceMapURL, Boolean(hasSourceURL), false, length || 0, isModule || null, stackTrace || null, codeOffset || null, scriptLanguage || null, debugSymbols || null, embedderName as Platform.DevToolsPath.UrlString || null); } scriptFailedToParse({ scriptId, url, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, sourceMapURL, hasSourceURL, length, isModule, stackTrace, codeOffset, scriptLanguage, embedderName, }: Protocol.Debugger.ScriptFailedToParseEvent): void { if (!this.#debuggerModel.debuggerEnabled()) { return; } this.#debuggerModel.parsedScriptSource( scriptId, url as Platform.DevToolsPath.UrlString, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, false, sourceMapURL, Boolean(hasSourceURL), true, length || 0, isModule || null, stackTrace || null, codeOffset || null, scriptLanguage || null, null, embedderName as Platform.DevToolsPath.UrlString || null); } breakpointResolved({breakpointId, location}: Protocol.Debugger.BreakpointResolvedEvent): void { if (!this.#debuggerModel.debuggerEnabled()) { return; } this.#debuggerModel.breakpointResolved(breakpointId, location); } } export class Location { debuggerModel: DebuggerModel; scriptId: Protocol.Runtime.ScriptId; lineNumber: number; columnNumber: number; inlineFrameIndex: number; constructor( debuggerModel: DebuggerModel, scriptId: Protocol.Runtime.ScriptId, lineNumber: number, columnNumber?: number, inlineFrameIndex?: number) { this.debuggerModel = debuggerModel; this.scriptId = scriptId; this.lineNumber = lineNumber; this.columnNumber = columnNumber || 0; this.inlineFrameIndex = inlineFrameIndex || 0; } static fromPayload(debuggerModel: DebuggerModel, payload: Protocol.Debugger.Location, inlineFrameIndex?: number): Location { return new Location(debuggerModel, payload.scriptId, payload.lineNumber, payload.columnNumber, inlineFrameIndex); } payload(): Protocol.Debugger.Location { return {scriptId: this.scriptId, lineNumber: this.lineNumber, columnNumber: this.columnNumber}; } script(): Script|null { return this.debuggerModel.scriptForId(this.scriptId); } continueToLocation(pausedCallback?: (() => void)): void { if (pausedCallback) { this.debuggerModel.continueToLocationCallback = this.paused.bind(this, pausedCallback); } void this.debuggerModel.agent.invoke_continueToLocation({ location: this.payload(), targetCallFrames: Protocol.Debugger.ContinueToLocationRequestTargetCallFrames.Current, }); } private paused(pausedCallback: () => void|undefined, debuggerPausedDetails: DebuggerPausedDetails): boolean { const location = debuggerPausedDetails.callFrames[0].location(); if (location.scriptId === this.scriptId && location.lineNumber === this.lineNumber && location.columnNumber === this.columnNumber) { pausedCallback(); return true; } return false; } id(): string { return this.debuggerModel.target().id() + ':' + this.scriptId + ':' + this.lineNumber + ':' + this.columnNumber; } } export interface LocationRange { start: Location; end: Location; } export class BreakLocation extends Location { type: Protocol.Debugger.BreakLocationType|undefined; constructor( debuggerModel: DebuggerModel, scriptId: Protocol.Runtime.ScriptId, lineNumber: number, columnNumber?: number, type?: Protocol.Debugger.BreakLocationType) { super(debuggerModel, scriptId, lineNumber, columnNumber); if (type) { this.type = type; } } static override fromPayload(debuggerModel: DebuggerModel, payload: Protocol.Debugger.BreakLocation): BreakLocation { return new BreakLocation(debuggerModel, payload.scriptId, payload.lineNumber, payload.columnNumber, payload.type); } } export interface MissingDebugInfoDetails { details: string; resources: string[]; } export class CallFrame { debuggerModel: DebuggerModel; readonly #scriptInternal: Script; payload: Protocol.Debugger.CallFrame; readonly #locationInternal: Location; readonly #scopeChainInternal: Scope[]; readonly #localScopeInternal: Scope|null; readonly #inlineFrameIndexInternal: number; readonly #functionNameInternal: string; readonly #functionLocationInternal: Location|undefined; #returnValueInternal: RemoteObject|null; #missingDebugInfoDetails: MissingDebugInfoDetails|null = null; readonly canBeRestarted: boolean; constructor( debuggerModel: DebuggerModel, script: Script, payload: Protocol.Debugger.CallFrame, inlineFrameIndex?: number, functionName?: string) { this.debuggerModel = debuggerModel; this.#scriptInternal = script; this.payload = payload; this.#locationInternal = Location.fromPayload(debuggerModel, payload.location, inlineFrameIndex); this.#scopeChainInternal = []; this.#localScopeInternal = null; this.#inlineFrameIndexInternal = inlineFrameIndex || 0; this.#functionNameInternal = functionName || payload.functionName; this.canBeRestarted = Boolean(payload.canBeRestarted); for (let i = 0; i < payload.scopeChain.length; ++i) { const scope = new Scope(this, i); this.#scopeChainInternal.push(scope); if (scope.type() === Protocol.Debugger.ScopeType.Local) { this.#localScopeInternal = scope; } } if (payload.functionLocation) { this.#functionLocationInternal = Location.fromPayload(debuggerModel, payload.functionLocation); } this.#returnValueInternal = payload.returnValue ? this.debuggerModel.runtimeModel().createRemoteObject(payload.returnValue) : null; } static fromPayloadArray(debuggerModel: DebuggerModel, callFrames: Protocol.Debugger.CallFrame[]): CallFrame[] { const result = []; for (let i = 0; i < callFrames.length; ++i) { const callFrame = callFrames[i]; const script = debuggerModel.scriptForId(callFrame.location.scriptId); if (script) { result.push(new CallFrame(debuggerModel, script, callFrame)); } } return result; } createVirtualCallFrame(inlineFrameIndex: number, name: string): CallFrame { return new CallFrame(this.debuggerModel, this.#scriptInternal, this.payload, inlineFrameIndex, name); } setMissingDebugInfoDetails(details: MissingDebugInfoDetails): void { this.#missingDebugInfoDetails = details; } get missingDebugInfoDetails(): MissingDebugInfoDetails|null { return this.#missingDebugInfoDetails; } get script(): Script { return this.#scriptInternal; } get id(): Protocol.Debugger.CallFrameId { return this.payload.callFrameId; } get inlineFrameIndex(): number { return this.#inlineFrameIndexInternal; } scopeChain(): Scope[] { return this.#scopeChainInternal; } localScope(): Scope|null { return this.#localScopeInternal; } thisObject(): RemoteObject|null { return this.payload.this ? this.debuggerModel.runtimeModel().createRemoteObject(this.payload.this) : null; } returnValue(): RemoteObject|null { return this.#returnValueInternal; } async setReturnValue(expression: string): Promise<RemoteObject|null> { if (!this.#returnValueInternal) { return null; } const evaluateResponse = await this.debuggerModel.agent.invoke_evaluateOnCallFrame( {callFrameId: this.id, expression: expression, silent: true, objectGroup: 'backtrace'}); if (evaluateResponse.getError() || evaluateResponse.exceptionDetails) { return null; } const response = await this.debuggerModel.agent.invoke_setReturnValue({newValue: evaluateResponse.result}); if (response.getError()) { return null; } this.#returnValueInternal = this.debuggerModel.runtimeModel().createRemoteObject(evaluateResponse.result); return this.#returnValueInternal; } get functionName(): string { return this.#functionNameInternal; } location(): Location { return this.#locationInternal; } functionLocation(): Location|null { return this.#functionLocationInternal || null; } async evaluate(options: EvaluationOptions): Promise<EvaluationResult> { const debuggerModel = this.debuggerModel; const runtimeModel = debuggerModel.runtimeModel(); // Assume backends either support both throwOnSideEffect and timeout options or neither. const needsTerminationOptions = Boolean(options.throwOnSideEffect) || options.timeout !== undefined; if (needsTerminationOptions && (runtimeModel.hasSideEffectSupport() === false || (runtimeModel.hasSideEffectSupport() === null && !await runtimeModel.checkSideEffectSupport()))) { return {error: 'Side-effect checks not supported by backend.'}; } const evaluateOnCallFrameCallback = debuggerModel.getEvaluateOnCallFrameCallback(); if (evaluateOnCallFrameCallback) { const result = await evaluateOnCallFrameCallback(this, options); if (result) { return result; } } const response = await this.debuggerModel.agent.invoke_evaluateOnCallFrame({ callFrameId: this.id, expression: options.expression, objectGroup: options.objectGroup, includeCommandLineAPI: options.includeCommandLineAPI, silent: options.silent, returnByValue: options.returnByValue, generatePreview: options.generatePreview, throwOnSideEffect: options.throwOnSideEffect, timeout: options.timeout, }); const error = response.getError(); if (error) { return {error: error}; } return {object: runtimeModel.createRemoteObject(response.result), exceptionDetails: response.exceptionDetails}; } async restart(): Promise<void> { console.assert(this.canBeRestarted, 'This frame can not be restarted.'); // Note that even if