| // Copyright 2010 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; |
| import * as Protocol from '../../generated/protocol.js'; |
| import * as Common from '../common/common.js'; |
| import * as i18n from '../i18n/i18n.js'; |
| import type * as Platform from '../platform/platform.js'; |
| import * as Root from '../root/root.js'; |
| |
| import type {PageResourceLoadInitiator} from './PageResourceLoader.js'; |
| import {type GetPropertiesResult, type RemoteObject, RemoteObjectProperty, ScopeRef} from './RemoteObject.js'; |
| import {Events as ResourceTreeModelEvents, ResourceTreeModel} from './ResourceTreeModel.js'; |
| import {type EvaluationOptions, type EvaluationResult, type ExecutionContext, RuntimeModel} from './RuntimeModel.js'; |
| import {Script} from './Script.js'; |
| import {SDKModel} from './SDKModel.js'; |
| import {SourceMap} from './SourceMap.js'; |
| import {SourceMapManager} from './SourceMapManager.js'; |
| import {Capability, type Target} from './Target.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', |
| /** |
| * @description Text in Scope Chain Sidebar Pane of the Sources panel |
| */ |
| exception: 'Exception', |
| /** |
| * @description Text in Scope Chain Sidebar Pane of the Sources panel |
| */ |
| returnValue: 'Return value', |
| } as const; |
| 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) => { |
| 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; |
| } |
| |
| export const enum StepMode { |
| STEP_INTO = 'StepInto', |
| STEP_OUT = 'StepOut', |
| STEP_OVER = 'StepOver', |
| } |
| |
| export const WASM_SYMBOLS_PRIORITY = [ |
| Protocol.Debugger.DebugSymbolsType.ExternalDWARF, |
| Protocol.Debugger.DebugSymbolsType.EmbeddedDWARF, |
| Protocol.Debugger.DebugSymbolsType.SourceMap, |
| ]; |
| |
| export class DebuggerModel extends SDKModel<EventTypes> { |
| readonly agent: ProtocolProxyApi.DebuggerApi; |
| #runtimeModel: RuntimeModel; |
| readonly #sourceMapManager: SourceMapManager<Script>; |
| #debuggerPausedDetails: DebuggerPausedDetails|null = null; |
| readonly #scripts = new Map<string, Script>(); |
| readonly #scriptsBySourceURL = new Map<string, Script[]>(); |
| #discardableScripts: Script[] = []; |
| continueToLocationCallback: ((arg0: DebuggerPausedDetails) => boolean)|null = null; |
| #selectedCallFrame: CallFrame|null = null; |
| #debuggerEnabled = false; |
| #debuggerId: string|null = null; |
| #skipAllPausesTimeout = 0; |
| #beforePausedCallback: ((arg0: DebuggerPausedDetails, stepOver: Location|null) => Promise<boolean>)|null = null; |
| #computeAutoStepRangesCallback: ((arg0: StepMode, arg1: CallFrame) => Promise<Array<{ |
| start: Location, |
| end: Location, |
| }>>)|null = null; |
| #expandCallFramesCallback: ((arg0: CallFrame[]) => Promise<CallFrame[]>)|null = null; |
| evaluateOnCallFrameCallback: |
| ((arg0: CallFrame, arg1: EvaluationOptions) => Promise<EvaluationResult|null>)|null = null; |
| #synchronizeBreakpointsCallback: ((script: Script) => Promise<void>)|null = 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<Record<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 = null; |
| #isPausing = false; |
| |
| constructor(target: Target) { |
| super(target); |
| |
| target.registerDebuggerDispatcher(new DebuggerDispatcher(this)); |
| this.agent = target.debuggerAgent(); |
| this.#runtimeModel = (target.model(RuntimeModel) as RuntimeModel); |
| |
| this.#sourceMapManager = new SourceMapManager( |
| target, |
| (compiledURL, sourceMappingURL, payload, script) => |
| new SourceMap(compiledURL, sourceMappingURL, payload, script)); |
| |
| Common.Settings.Settings.instance() |
| .moduleSetting('pause-on-exception-enabled') |
| .addChangeListener(this.pauseOnExceptionStateChanged, this); |
| Common.Settings.Settings.instance() |
| .moduleSetting('pause-on-caught-exception') |
| .addChangeListener(this.pauseOnExceptionStateChanged, this); |
| Common.Settings.Settings.instance() |
| .moduleSetting('pause-on-uncaught-exception') |
| .addChangeListener(this.pauseOnExceptionStateChanged, this); |
| Common.Settings.Settings.instance() |
| .moduleSetting('disable-async-stack-traces') |
| .addChangeListener(this.asyncStackTracesStateChanged, this); |
| Common.Settings.Settings.instance() |
| .moduleSetting('breakpoints-active') |
| .addChangeListener(this.breakpointsActiveChanged, this); |
| |
| if (!target.suspended()) { |
| void this.enableDebugger(); |
| } |
| |
| this.#sourceMapManager.setEnabled( |
| Common.Settings.Settings.instance().moduleSetting('js-source-maps-enabled').get()); |
| Common.Settings.Settings.instance() |
| .moduleSetting('js-source-maps-enabled') |
| .addChangeListener(event => this.#sourceMapManager.setEnabled((event.data as boolean))); |
| |
| const resourceTreeModel = (target.model(ResourceTreeModel) as ResourceTreeModel); |
| if (resourceTreeModel) { |
| resourceTreeModel.addEventListener(ResourceTreeModelEvents.FrameNavigated, this.onFrameNavigated, this); |
| } |
| } |
| |
| static selectSymbolSource(debugSymbols: Protocol.Debugger.DebugSymbols[]|null): Protocol.Debugger.DebugSymbols|null { |
| if (!debugSymbols || debugSymbols.length === 0) { |
| return null; |
| } |
| |
| // Provides backwards compatibility to previous CDP version on Protocol.Debugger.DebugSymbols. |
| // TODO(crbug.com/369515221): Remove extra code as soon as old v8 versions used in Node are no longer supported. |
| if ('type' in debugSymbols) { |
| if (debugSymbols.type === 'None') { |
| return null; |
| } |
| return debugSymbols as Protocol.Debugger.DebugSymbols; |
| } |
| |
| let debugSymbolsSource = null; |
| const symbolTypes = new Map(debugSymbols.map(symbol => [symbol.type, symbol])); |
| for (const symbol of WASM_SYMBOLS_PRIORITY) { |
| if (symbolTypes.has(symbol)) { |
| debugSymbolsSource = symbolTypes.get(symbol) || null; |
| break; |
| } |
| } |
| |
| console.assert( |
| debugSymbolsSource !== null, |
| 'Unknown symbol types. Front-end and back-end should be kept in sync regarding Protocol.Debugger.DebugSymbolTypes'); |
| if (debugSymbolsSource && debugSymbols.length > 1) { |
| Common.Console.Console.instance().warn( |
| `Multiple debug symbols for script were found. Using ${debugSymbolsSource.type}`); |
| } |
| return debugSymbolsSource; |
| } |
| |
| sourceMapManager(): SourceMapManager<Script> { |
| return this.#sourceMapManager; |
| } |
| |
| runtimeModel(): RuntimeModel { |
| return this.#runtimeModel; |
| } |
| |
| debuggerEnabled(): boolean { |
| return Boolean(this.#debuggerEnabled); |
| } |
| |
| debuggerId(): string|null { |
| return this.#debuggerId; |
| } |
| |
| private async enableDebugger(): Promise<void> { |
| if (this.#debuggerEnabled) { |
| return; |
| } |
| this.#debuggerEnabled = 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('breakpoints-active').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 await enablePromise; |
| } |
| |
| private onFrameNavigated(): void { |
| if (DebuggerModel.shouldResyncDebuggerId) { |
| return; |
| } |
| |
| DebuggerModel.shouldResyncDebuggerId = true; |
| } |
| |
| private registerDebugger(response: Protocol.Debugger.EnableResponse): void { |
| if (response.getError()) { |
| this.#debuggerEnabled = false; |
| 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.#debuggerEnabled) { |
| return; |
| } |
| this.#debuggerEnabled = false; |
| |
| await this.asyncStackTracesStateChanged(); |
| await this.agent.invoke_disable(); |
| this.#isPausing = 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('pause-on-caught-exception').get(); |
| let state: Protocol.Debugger.SetPauseOnExceptionsRequestState; |
| |
| const pauseOnUncaughtEnabled = |
| Common.Settings.Settings.instance().moduleSetting('pause-on-uncaught-exception').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('disable-async-stack-traces').get() && this.#debuggerEnabled; |
| 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('breakpoints-active').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.#debuggerPausedDetails && |
| this.#debuggerPausedDetails.callFrames.length > 0) { |
| const [callFrame] = this.#debuggerPausedDetails.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.STEP_INTO); |
| void this.agent.invoke_stepInto({breakOnAsyncCall: false, skipList}); |
| } |
| |
| async stepOver(): Promise<void> { |
| this.#autoSteppingContext = this.#debuggerPausedDetails?.callFrames[0]?.functionLocation() ?? null; |
| const skipList = await this.computeAutoStepSkipList(StepMode.STEP_OVER); |
| void this.agent.invoke_stepOver({skipList}); |
| } |
| |
| async stepOut(): Promise<void> { |
| const skipList = await this.computeAutoStepSkipList(StepMode.STEP_OUT); |
| if (skipList.length !== 0) { |
| void this.agent.invoke_stepOver({skipList}); |
| } else { |
| void this.agent.invoke_stepOut(); |
| } |
| } |
| |
| scheduleStepIntoAsync(): void { |
| void this.computeAutoStepSkipList(StepMode.STEP_INTO).then(skipList => { |
| void this.agent.invoke_stepInto({breakOnAsyncCall: true, skipList}); |
| }); |
| } |
| |
| resume(): void { |
| void this.agent.invoke_resume({terminateOnResume: false}); |
| this.#isPausing = false; |
| } |
| |
| pause(): void { |
| this.#isPausing = true; |
| this.skipAllPauses(false); |
| void this.agent.invoke_pause(); |
| } |
| |
| async setBreakpointByURL( |
| url: Platform.DevToolsPath.UrlString, lineNumber: number, columnNumber?: number, |
| condition?: BackendCondition): Promise<SetBreakpointResult> { |
| // 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, |
| url, |
| columnNumber, |
| 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, scriptHash, columnNumber, 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, |
| }); |
| 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.#scripts.values()) { |
| this.#sourceMapManager.detachSourceMap(script); |
| } |
| this.#scripts.clear(); |
| this.#scriptsBySourceURL.clear(); |
| this.#discardableScripts = []; |
| this.#autoSteppingContext = null; |
| } |
| |
| scripts(): Script[] { |
| return Array.from(this.#scripts.values()); |
| } |
| |
| scriptForId(scriptId: string): Script|null { |
| return this.#scripts.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.#scripts.values()) { |
| if (script.executionContextId === executionContext.id) { |
| result.push(script); |
| } |
| } |
| return result; |
| } |
| |
| get callFrames(): CallFrame[]|null { |
| return this.#debuggerPausedDetails ? this.#debuggerPausedDetails.callFrames : null; |
| } |
| |
| debuggerPausedDetails(): DebuggerPausedDetails|null { |
| return this.#debuggerPausedDetails; |
| } |
| |
| private async setDebuggerPausedDetails(debuggerPausedDetails: DebuggerPausedDetails): Promise<boolean> { |
| this.#isPausing = false; |
| this.#debuggerPausedDetails = 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.#isPausing = false; |
| this.#debuggerPausedDetails = null; |
| this.setSelectedCallFrame(null); |
| } |
| |
| setBeforePausedCallback( |
| callback: ((arg0: DebuggerPausedDetails, autoSteppingContext: Location|null) => Promise<boolean>)|null): void { |
| this.#beforePausedCallback = callback; |
| } |
| |
| setExpandCallFramesCallback(callback: ((arg0: CallFrame[]) => Promise<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); |
| await this.#expandCallFrames(pausedDetails); |
| |
| 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'); |
| } |
| } |
| |
| /** Delegates to the DebuggerLanguagePlugin and potential attached source maps to expand inlined call frames */ |
| async #expandCallFrames(pausedDetails: DebuggerPausedDetails): Promise<void> { |
| if (this.#expandCallFramesCallback) { |
| pausedDetails.callFrames = await this.#expandCallFramesCallback.call(null, pausedDetails.callFrames); |
| } |
| |
| if (!Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.USE_SOURCE_MAP_SCOPES)) { |
| return; |
| } |
| |
| // TODO(crbug.com/40277685): Support attaching/detaching source maps after pausing. |
| // Expanding call frames via source maps here is only suitable for the experiment prototype because |
| // we block until all relevant source maps are loaded. |
| // We should change this so the "Debugger Plugin" and "Source Map" have a bottle neck where they expand |
| // call frames and that bottleneck should support attaching/detaching source maps while paused. |
| const finalFrames: CallFrame[] = []; |
| for (const frame of pausedDetails.callFrames) { |
| const sourceMap = await this.sourceMapManager().sourceMapForClientPromise(frame.script); |
| if (sourceMap?.hasScopeInfo()) { |
| finalFrames.push(...sourceMap.expandCallFrame(frame)); |
| } else { |
| finalFrames.push(frame); |
| } |
| } |
| pausedDetails.callFrames = finalFrames; |
| } |
| |
| 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, buildId: string|null): Script { |
| const knownScript = this.#scripts.get(scriptId); |
| if (knownScript) { |
| return knownScript; |
| } |
| let isContentScript = false; |
| if (executionContextAuxData && ('isDefault' in executionContextAuxData)) { |
| isContentScript = !executionContextAuxData['isDefault']; |
| } |
| |
| const selectedDebugSymbol = DebuggerModel.selectSymbolSource(debugSymbols); |
| const script = new Script( |
| this, scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, |
| isContentScript, isLiveEdit, sourceMapURL, hasSourceURLComment, length, isModule, originStackTrace, codeOffset, |
| scriptLanguage, selectedDebugSymbol, embedderName, buildId); |
| this.registerScript(script); |
| this.dispatchEventToListeners(Events.ParsedScriptSource, script); |
| |
| if (script.sourceMapURL && !hasSyntaxError) { |
| this.#sourceMapManager.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.#sourceMapManager.detachSourceMap(script); |
| script.sourceMapURL = newSourceMapURL; |
| this.#sourceMapManager.attachSourceMap(script, script.sourceURL, script.sourceMapURL); |
| } |
| |
| async setDebugInfoURL(script: Script, _externalURL: Platform.DevToolsPath.UrlString): Promise<void> { |
| if (this.#expandCallFramesCallback && this.#debuggerPausedDetails) { |
| this.#debuggerPausedDetails.callFrames = |
| await this.#expandCallFramesCallback.call(null, this.#debuggerPausedDetails.callFrames); |
| } |
| this.dispatchEventToListeners(Events.DebugInfoAttached, script); |
| } |
| |
| executionContextDestroyed(executionContext: ExecutionContext): void { |
| for (const script of this.#scripts.values()) { |
| if (script.executionContextId === executionContext.id) { |
| this.#sourceMapManager.detachSourceMap(script); |
| } |
| } |
| } |
| |
| private registerScript(script: Script): void { |
| this.#scripts.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.#scripts.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.#isPausing; |
| } |
| |
| setSelectedCallFrame(callFrame: CallFrame|null): void { |
| if (this.#selectedCallFrame === callFrame) { |
| return; |
| } |
| this.#selectedCallFrame = callFrame; |
| this.dispatchEventToListeners(Events.CallFrameSelected, this); |
| } |
| |
| selectedCallFrame(): CallFrame|null { |
| return this.#selectedCallFrame; |
| } |
| |
| async evaluateOnSelectedCallFrame(options: EvaluationOptions): Promise<EvaluationResult> { |
| const callFrame = this.selectedCallFrame(); |
| if (!callFrame) { |
| throw new Error('No call frame selected'); |
| } |
| return await 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?.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[], skipAnonymous: boolean): Promise<boolean> { |
| const response = await this.agent.invoke_setBlackboxPatterns({patterns, skipAnonymous}); |
| const error = response.getError(); |
| return !error; |
| } |
| |
| async setBlackboxExecutionContexts(uniqueIds: string[]): Promise<boolean> { |
| const response = await this.agent.invoke_setBlackboxExecutionContexts({uniqueIds}); |
| const error = response.getError(); |
| return !error; |
| } |
| |
| override dispose(): void { |
| if (this.#debuggerId) { |
| debuggerIdToModel.delete(this.#debuggerId); |
| } |
| Common.Settings.Settings.instance() |
| .moduleSetting('pause-on-exception-enabled') |
| .removeChangeListener(this.pauseOnExceptionStateChanged, this); |
| Common.Settings.Settings.instance() |
| .moduleSetting('pause-on-caught-exception') |
| .removeChangeListener(this.pauseOnExceptionStateChanged, this); |
| Common.Settings.Settings.instance() |
| .moduleSetting('disable-async-stack-traces') |
| .removeChangeListener(this.asyncStackTracesStateChanged, this); |
| } |
| |
| override async suspendModel(): Promise<void> { |
| await this.disableDebugger(); |
| } |
| |
| override async resumeModel(): Promise<void> { |
| await this.enableDebugger(); |
| } |
| |
| private static shouldResyncDebuggerId = false; |
| |
| getEvaluateOnCallFrameCallback(): |
| ((arg0: CallFrame, arg1: EvaluationOptions) => Promise<EvaluationResult|null>)|null { |
| return this.evaluateOnCallFrameCallback; |
| } |
| |
| /** |
| * Iterates the async stack trace parents. |
| * |
| * Retrieving cross-target async stack fragments requires CDP interaction, so this is an async generator. |
| * |
| * Important: This iterator will not yield the "synchronous" part of the stack trace, only the async parent chain. |
| */ |
| async * |
| iterateAsyncParents( |
| stackTraceOrPausedDetails: Protocol.Runtime.StackTrace| |
| Pick<DebuggerPausedDetails, 'asyncStackTrace'|'asyncStackTraceId'>): |
| AsyncGenerator<{stackTrace: Protocol.Runtime.StackTrace, target: Target}> { |
| // We make `DebuggerPausedDetails` look like a stack trace. We are only interested in `parent` and `parentId` in any case. |
| const isPausedDetails = (details: typeof stackTraceOrPausedDetails): |
| details is Pick<DebuggerPausedDetails, 'asyncStackTrace'|'asyncStackTraceId'> => |
| !('parent' in details) && !('parentId' in details); |
| let stackTrace: Protocol.Runtime.StackTrace = isPausedDetails(stackTraceOrPausedDetails) ? |
| { |
| callFrames: [], |
| parent: stackTraceOrPausedDetails.asyncStackTrace, |
| parentId: stackTraceOrPausedDetails.asyncStackTraceId |
| } : |
| stackTraceOrPausedDetails; |
| let target = this.target(); |
| |
| while (true) { |
| if (stackTrace.parent) { |
| stackTrace = stackTrace.parent; |
| } else if (stackTrace.parentId) { |
| const model: DebuggerModel|null = stackTrace.parentId.debuggerId ? |
| await DebuggerModel.modelForDebuggerId(stackTrace.parentId.debuggerId) : |
| this; |
| if (!model) { |
| return; |
| } |
| const maybeStackTrace = await model.fetchAsyncStackTrace(stackTrace.parentId); |
| if (!maybeStackTrace) { |
| return; |
| } |
| stackTrace = maybeStackTrace; |
| target = model.target(); |
| } else { |
| return; |
| } |
| |
| yield {stackTrace, target}; |
| } |
| } |
| } |
| |
| const debuggerIdToModel = new Map<string, DebuggerModel>(); |
| |
| /** |
| * Keep these in sync with WebCore::V8Debugger |
| */ |
| export enum PauseOnExceptionsState { |
| /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */ |
| DontPauseOnExceptions = 'none', |
| PauseOnAllExceptions = 'all', |
| PauseOnCaughtExceptions = 'caught', |
| PauseOnUncaughtExceptions = 'uncaught', |
| /* eslint-enable @typescript-eslint/naming-convention */ |
| } |
| |
| export enum Events { |
| /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */ |
| DebuggerWasEnabled = 'DebuggerWasEnabled', |
| DebuggerWasDisabled = 'DebuggerWasDisabled', |
| DebuggerPaused = 'DebuggerPaused', |
| DebuggerResumed = 'DebuggerResumed', |
| DebugInfoAttached = 'DebugInfoAttached', |
| ParsedScriptSource = 'ParsedScriptSource', |
| DiscardedAnonymousScriptSource = 'DiscardedAnonymousScriptSource', |
| GlobalObjectCleared = 'GlobalObjectCleared', |
| CallFrameSelected = 'CallFrameSelected', |
| DebuggerIsReadyToPause = 'DebuggerIsReadyToPause', |
| ScriptSourceWasEdited = 'ScriptSourceWasEdited', |
| /* eslint-enable @typescript-eslint/naming-convention */ |
| } |
| |
| export interface 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, |
| buildId, |
| }: 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, buildId || null); |
| } |
| |
| scriptFailedToParse({ |
| scriptId, |
| url, |
| startLine, |
| startColumn, |
| endLine, |
| endColumn, |
| executionContextId, |
| hash, |
| executionContextAuxData, |
| sourceMapURL, |
| hasSourceURL, |
| length, |
| isModule, |
| stackTrace, |
| codeOffset, |
| scriptLanguage, |
| embedderName, |
| buildId, |
| }: 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, buildId || 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 MissingDebugFiles { |
| resourceUrl: Platform.DevToolsPath.UrlString; |
| initiator: PageResourceLoadInitiator; |
| } |
| |
| export interface MissingDebugInfoDetails { |
| details: string; |
| resources: MissingDebugFiles[]; |
| } |
| |
| export class CallFrame { |
| debuggerModel: DebuggerModel; |
| readonly script: Script; |
| payload: Protocol.Debugger.CallFrame; |
| readonly #location: Location; |
| readonly #scopeChain: Scope[]; |
| readonly #localScope: Scope|null; |
| readonly inlineFrameIndex: number; |
| readonly functionName: string; |
| readonly #functionLocation: Location|undefined; |
| #returnValue: RemoteObject|null; |
| missingDebugInfoDetails: MissingDebugInfoDetails|null; |
| readonly exception: RemoteObject|null; |
| |
| readonly canBeRestarted: boolean; |
| |
| constructor( |
| debuggerModel: DebuggerModel, script: Script, payload: Protocol.Debugger.CallFrame, inlineFrameIndex?: number, |
| functionName?: string, exception: RemoteObject|null = null) { |
| this.debuggerModel = debuggerModel; |
| this.script = script; |
| this.payload = payload; |
| this.#location = Location.fromPayload(debuggerModel, payload.location, inlineFrameIndex); |
| this.#scopeChain = []; |
| this.#localScope = null; |
| this.inlineFrameIndex = inlineFrameIndex || 0; |
| this.functionName = functionName ?? payload.functionName; |
| this.missingDebugInfoDetails = null; |
| this.canBeRestarted = Boolean(payload.canBeRestarted); |
| this.exception = exception; |
| for (let i = 0; i < payload.scopeChain.length; ++i) { |
| const scope = new Scope(this, i); |
| this.#scopeChain.push(scope); |
| if (scope.type() === Protocol.Debugger.ScopeType.Local) { |
| this.#localScope = scope; |
| } |
| } |
| if (payload.functionLocation) { |
| this.#functionLocation = Location.fromPayload(debuggerModel, payload.functionLocation); |
| } |
| this.#returnValue = |
| payload.returnValue ? this.debuggerModel.runtimeModel().createRemoteObject(payload.returnValue) : null; |
| } |
| |
| static fromPayloadArray( |
| debuggerModel: DebuggerModel, callFrames: Protocol.Debugger.CallFrame[], |
| exception: RemoteObject|null): CallFrame[] { |
| const result = []; |
| for (let i = 0; i < callFrames.length; ++i) { |
| const callFrame = callFrames[i]; |
| const script = debuggerModel.scriptForId(callFrame.location.scriptId); |
| if (script) { |
| const ex = i === 0 ? exception : null; |
| result.push(new CallFrame(debuggerModel, script, callFrame, undefined, undefined, ex)); |
| } |
| } |
| return result; |
| } |
| |
| createVirtualCallFrame(inlineFrameIndex: number, name: string): CallFrame { |
| return new CallFrame(this.debuggerModel, this.script, this.payload, inlineFrameIndex, name, this.exception); |
| } |
| |
| get id(): Protocol.Debugger.CallFrameId { |
| return this.payload.callFrameId; |
| } |
| |
| scopeChain(): Scope[] { |
| return this.#scopeChain; |
| } |
| |
| localScope(): Scope|null { |
| return this.#localScope; |
| } |
| |
| thisObject(): RemoteObject|null { |
| return this.payload.this ? this.debuggerModel.runtimeModel().createRemoteObject(this.payload.this) : null; |
| } |
| |
| returnValue(): RemoteObject|null { |
| return this.#returnValue; |
| } |
| |
| async setReturnValue(expression: string): Promise<RemoteObject|null> { |
| if (!this.#returnValue) { |
| return null; |
| } |
| |
| const evaluateResponse = await this.debuggerModel.agent.invoke_evaluateOnCallFrame( |
| {callFrameId: this.id, 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.#returnValue = this.debuggerModel.runtimeModel().createRemoteObject(evaluateResponse.result); |
| return this.#returnValue; |
| } |
| |
| location(): Location { |
| return this.#location; |
| } |
| |
| functionLocation(): Location|null { |
| return this.#functionLocation || null; |
| } |
| |
| async evaluate(options: EvaluationOptions): Promise<EvaluationResult> { |
| const debuggerModel = this.debuggerModel; |
| const runtimeModel = debuggerModel.runtimeModel(); |
| |
| 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}; |
| } |
| 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 `canBeRestarted` is true, the restart frame call can still fail. |
| // The user can evaluate arbitrary code between pausing and restarting the frame that |
| // could mess with the call stack. |
| await this.debuggerModel.agent.invoke_restartFrame( |
| {callFrameId: this.id, mode: Protocol.Debugger.RestartFrameRequestMode.StepInto}); |
| } |
| |
| getPayload(): Protocol.Debugger.CallFrame { |
| return this.payload; |
| } |
| } |
| |
| export interface ScopeChainEntry { |
| callFrame(): CallFrame; |
| |
| type(): string; |
| |
| typeName(): string; |
| |
| name(): string|undefined; |
| |
| range(): LocationRange|null; |
| |
| object(): RemoteObject; |
| |
| description(): string; |
| |
| icon(): string|undefined; |
| |
| /** |
| * Extra and/or synthetic properties that should be added to the `RemoteObject` |
| * returned by {@link ScopeChainEntry.object}. |
| */ |
| extraProperties(): RemoteObjectProperty[]; |
| } |
| |
| export class Scope implements ScopeChainEntry { |
| #callFrame: CallFrame; |
| #payload: Protocol.Debugger.Scope; |
| readonly #type: Protocol.Debugger.ScopeType; |
| readonly #name: string|undefined; |
| #ordinal: number; |
| readonly #locationRange: LocationRange|null; |
| #object: RemoteObject|null = null; |
| constructor(callFrame: CallFrame, ordinal: number) { |
| this.#callFrame = callFrame; |
| this.#payload = callFrame.getPayload().scopeChain[ordinal]; |
| this.#type = this.#payload.type; |
| this.#name = this.#payload.name; |
| this.#ordinal = ordinal; |
| |
| const start = |
| this.#payload.startLocation ? Location.fromPayload(callFrame.debuggerModel, this.#payload.startLocation) : null; |
| const end = |
| this.#payload.endLocation ? Location.fromPayload(callFrame.debuggerModel, this.#payload.endLocation) : null; |
| if (start && end && start.scriptId === end.scriptId) { |
| this.#locationRange = {start, end}; |
| } else { |
| this.#locationRange = null; |
| } |
| } |
| |
| callFrame(): CallFrame { |
| return this.#callFrame; |
| } |
| |
| type(): string { |
| return this.#type; |
| } |
| |
| typeName(): string { |
| switch (this.#type) { |
| case Protocol.Debugger.ScopeType.Local: |
| return i18nString(UIStrings.local); |
| case Protocol.Debugger.ScopeType.Closure: |
| return i18nString(UIStrings.closure); |
| case Protocol.Debugger.ScopeType.Catch: |
| return i18nString(UIStrings.catchBlock); |
| case Protocol.Debugger.ScopeType.Eval: |
| return i18n.i18n.lockedString('Eval'); |
| case Protocol.Debugger.ScopeType.Block: |
| return i18nString(UIStrings.block); |
| case Protocol.Debugger.ScopeType.Script: |
| return i18nString(UIStrings.script); |
| case Protocol.Debugger.ScopeType.With: |
| return i18nString(UIStrings.withBlock); |
| case Protocol.Debugger.ScopeType.Global: |
| return i18nString(UIStrings.global); |
| case Protocol.Debugger.ScopeType.Module: |
| return i18nString(UIStrings.module); |
| case Protocol.Debugger.ScopeType.WasmExpressionStack: |
| return i18nString(UIStrings.expression); |
| } |
| return ''; |
| } |
| |
| name(): string|undefined { |
| return this.#name; |
| } |
| |
| range(): LocationRange|null { |
| return this.#locationRange; |
| } |
| |
| object(): RemoteObject { |
| if (this.#object) { |
| return this.#object; |
| } |
| const runtimeModel = this.#callFrame.debuggerModel.runtimeModel(); |
| |
| const declarativeScope = |
| this.#type !== Protocol.Debugger.ScopeType.With && this.#type !== Protocol.Debugger.ScopeType.Global; |
| if (declarativeScope) { |
| this.#object = |
| runtimeModel.createScopeRemoteObject(this.#payload.object, new ScopeRef(this.#ordinal, this.#callFrame.id)); |
| } else { |
| this.#object = runtimeModel.createRemoteObject(this.#payload.object); |
| } |
| |
| return this.#object; |
| } |
| |
| description(): string { |
| const declarativeScope = |
| this.#type !== Protocol.Debugger.ScopeType.With && this.#type !== Protocol.Debugger.ScopeType.Global; |
| return declarativeScope ? '' : (this.#payload.object.description || ''); |
| } |
| |
| icon(): undefined { |
| return undefined; |
| } |
| |
| extraProperties(): RemoteObjectProperty[] { |
| if (this.#ordinal !== 0 || this.#type !== Protocol.Debugger.ScopeType.Local || this.#callFrame.script.isWasm()) { |
| return []; |
| } |
| |
| const extraProperties = []; |
| const exception = this.#callFrame.exception; |
| if (exception) { |
| extraProperties.push(new RemoteObjectProperty( |
| i18nString(UIStrings.exception), exception, undefined, undefined, undefined, undefined, undefined, |
| /* synthetic */ true)); |
| } |
| const returnValue = this.#callFrame.returnValue(); |
| if (returnValue) { |
| extraProperties.push(new RemoteObjectProperty( |
| i18nString(UIStrings.returnValue), returnValue, undefined, undefined, undefined, undefined, undefined, |
| /* synthetic */ true, this.#callFrame.setReturnValue.bind(this.#callFrame))); |
| } |
| return extraProperties; |
| } |
| } |
| |
| export class DebuggerPausedDetails { |
| debuggerModel: DebuggerModel; |
| callFrames: CallFrame[]; |
| reason: Protocol.Debugger.PausedEventReason; |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| auxData: Record<string, any>|undefined; |
| breakpointIds: string[]; |
| asyncStackTrace?: Protocol.Runtime.StackTrace; |
| asyncStackTraceId?: Protocol.Runtime.StackTraceId; |
| constructor( |
| debuggerModel: DebuggerModel, |
| callFrames: Protocol.Debugger.CallFrame[], |
| reason: Protocol.Debugger.PausedEventReason, |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| auxData: Record<string, any>|undefined, |
| breakpointIds: string[], |
| asyncStackTrace?: Protocol.Runtime.StackTrace, |
| asyncStackTraceId?: Protocol.Runtime.StackTraceId, |
| ) { |
| this.debuggerModel = debuggerModel; |
| this.reason = reason; |
| this.auxData = auxData; |
| this.breakpointIds = breakpointIds; |
| if (asyncStackTrace) { |
| this.asyncStackTrace = this.cleanRedundantFrames(asyncStackTrace); |
| } |
| this.asyncStackTraceId = asyncStackTraceId; |
| this.callFrames = CallFrame.fromPayloadArray(debuggerModel, callFrames, this.exception()); |
| } |
| |
| private exception(): RemoteObject|null { |
| if (this.reason !== Protocol.Debugger.PausedEventReason.Exception && |
| this.reason !== Protocol.Debugger.PausedEventReason.PromiseRejection) { |
| return null; |
| } |
| return this.debuggerModel.runtimeModel().createRemoteObject((this.auxData as Protocol.Runtime.RemoteObject)); |
| } |
| |
| private cleanRedundantFrames(asyncStackTrace: Protocol.Runtime.StackTrace): Protocol.Runtime.StackTrace { |
| let stack: (Protocol.Runtime.StackTrace|undefined)|Protocol.Runtime.StackTrace = asyncStackTrace; |
| let previous: Protocol.Runtime.StackTrace|null = null; |
| while (stack) { |
| if (previous && !stack.callFrames.length) { |
| previous.parent = stack.parent; |
| } else { |
| previous = stack; |
| } |
| stack = stack.parent; |
| } |
| return asyncStackTrace; |
| } |
| } |
| |
| SDKModel.register(DebuggerModel, {capabilities: Capability.JS, autostart: true}); |
| |
| export interface FunctionDetails { |
| location: Location|null; |
| functionName: string; |
| } |
| export interface SetBreakpointResult { |
| breakpointId: Protocol.Debugger.BreakpointId|null; |
| locations: Location[]; |
| } |
| |
| interface PausedOnInstrumentationData { |
| scriptId: Protocol.Runtime.ScriptId; |
| } |
| |
| export interface EventListenerPausedDetailsAuxData { |
| eventName: string; |
| targetName?: string; |
| webglErrorName?: string; |
| directiveText?: string; |
| } |
| |
| export const enum BreakpointType { |
| LOGPOINT = 'LOGPOINT', |
| CONDITIONAL_BREAKPOINT = 'CONDITIONAL_BREAKPOINT', |
| REGULAR_BREAKPOINT = 'REGULAR_BREAKPOINT', |
| } |
| |
| /** |
| * A breakpoint condition as sent to V8. This helps distinguish |
| * the breakpoint condition as it is entered by the user. |
| */ |
| export type BackendCondition = Platform.Brand.Brand<string, 'BackendCondition'>; |
| |
| export const LOGPOINT_SOURCE_URL = 'debugger://logpoint'; |
| export const COND_BREAKPOINT_SOURCE_URL = 'debugger://breakpoint'; |