UNPKG

chrome-devtools-frontend

Version:
1,601 lines (1,453 loc) • 59.3 kB
/* * 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 Platform from '../platform/platform.js'; import * as ProtocolClient from '../protocol_client/protocol_client.js'; // eslint-disable-line no-unused-vars import * as Root from '../root/root.js'; import {GetPropertiesResult, RemoteObject, ScopeRef} from './RemoteObject.js'; // eslint-disable-line no-unused-vars import {Events as ResourceTreeModelEvents, ResourceTreeModel} from './ResourceTreeModel.js'; // eslint-disable-line no-unused-vars import {EvaluationOptions, EvaluationResult, ExecutionContext, RuntimeModel} from './RuntimeModel.js'; // eslint-disable-line no-unused-vars import {Script} from './Script.js'; import {Capability, SDKModel, Target, Type} from './SDKModel.js'; // eslint-disable-line no-unused-vars import {SourceMapManager} from './SourceMapManager.js'; /** * @param {!Array<!LocationRange>} locationRanges * @return {!Array<!LocationRange>} */ export function sortAndMergeRanges(locationRanges) { if (locationRanges.length === 0) { return []; } locationRanges.sort(LocationRange.comparator); let prev = locationRanges[0]; const merged = []; for (let i = 1; i < locationRanges.length; ++i) { const current = locationRanges[i]; if (prev.overlap(current)) { const largerEnd = prev.end.compareTo(current.end) > 0 ? prev.end : current.end; prev = new LocationRange(prev.scriptId, prev.start, largerEnd); } else { merged.push(prev); prev = current; } } merged.push(prev); return merged; } /** @enum {symbol} */ export const StepMode = { StepInto: Symbol('StepInto'), StepOut: Symbol('StepOut'), StepOver: Symbol('StepOver') }; export class DebuggerModel extends SDKModel { /** * @param {!Target} target */ constructor(target) { super(target); target.registerDebuggerDispatcher(new DebuggerDispatcher(this)); this._agent = target.debuggerAgent(); this._runtimeModel = /** @type {!RuntimeModel} */ (target.model(RuntimeModel)); /** @type {!SourceMapManager<!Script>} */ this._sourceMapManager = new SourceMapManager(target); /** @type {!Map<string, !Script>} */ this._sourceMapIdToScript = new Map(); /** @type {?DebuggerPausedDetails} */ this._debuggerPausedDetails = null; /** @type {!Map<string, !Script>} */ this._scripts = new Map(); /** @type {!Map.<string, !Array.<!Script>>} */ this._scriptsBySourceURL = new Map(); /** @type {!Array.<!Script>} */ this._discardableScripts = []; /** @type {(function(!DebuggerPausedDetails):boolean)|null} */ this._continueToLocationCallback = null; /** @type {?CallFrame} */ this._selectedCallFrame = null; /** @type {boolean} */ this._debuggerEnabled = false; /** @type {?string} */ this._debuggerId = null; /** @type {number} */ this._skipAllPausesTimeout = 0; /** @type {(function(!DebuggerPausedDetails):boolean)|null} */ this._beforePausedCallback = null; /** @type {?function(!StepMode, !CallFrame):!Promise<!Array<!{start: !Location, end: !Location}>>} */ this._computeAutoStepRangesCallback = null; /** @type {?function(!Array<!CallFrame>):!Promise<!Array<!CallFrame>>} */ this._expandCallFramesCallback = null; /** @type {?function(!CallFrame, !EvaluationOptions):!Promise<?EvaluationResult>} */ this._evaluateOnCallFrameCallback = null; /** @type {boolean} */ this._ignoreDebuggerPausedEvents = false; /** @type {!Common.ObjectWrapper.ObjectWrapper} */ this._breakpointResolvedEventTarget = new Common.ObjectWrapper.ObjectWrapper(); /** @type {boolean} */ this._autoStepOver = false; this._isPausing = 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('disableAsyncStackTraces') .addChangeListener(this._asyncStackTracesStateChanged, this); Common.Settings.Settings.instance() .moduleSetting('breakpointsActive') .addChangeListener(this._breakpointsActiveChanged, this); if (!target.suspended()) { this._enableDebugger(); } this._sourceMapManager.setEnabled(Common.Settings.Settings.instance().moduleSetting('jsSourceMapsEnabled').get()); Common.Settings.Settings.instance() .moduleSetting('jsSourceMapsEnabled') .addChangeListener(event => this._sourceMapManager.setEnabled(/** @type {boolean} */ (event.data))); const resourceTreeModel = /** @type {!ResourceTreeModel} */ (target.model(ResourceTreeModel)); if (resourceTreeModel) { resourceTreeModel.addEventListener(ResourceTreeModelEvents.FrameNavigated, this._onFrameNavigated, this); } } /** * @param {!Protocol.Runtime.ExecutionContextId} executionContextId * @param {string} sourceURL * @param {string|undefined} sourceMapURL * @return {?string} */ static _sourceMapId(executionContextId, sourceURL, sourceMapURL) { if (!sourceMapURL) { return null; } return executionContextId + ':' + sourceURL + ':' + sourceMapURL; } /** * @return {!SourceMapManager<!Script>} */ sourceMapManager() { return this._sourceMapManager; } /** * @return {!RuntimeModel} */ runtimeModel() { return this._runtimeModel; } /** * @return {boolean} */ debuggerEnabled() { return Boolean(this._debuggerEnabled); } /** * @param {boolean} ignore */ ignoreDebuggerPausedEvents(ignore) { this._ignoreDebuggerPausedEvents = ignore; } /** * @return {!Promise<void>} */ async _enableDebugger() { 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}); enablePromise.then(this._registerDebugger.bind(this)); this._pauseOnExceptionStateChanged(); this._asyncStackTracesStateChanged(); if (!Common.Settings.Settings.instance().moduleSetting('breakpointsActive').get()) { this._breakpointsActiveChanged(); } if (_scheduledPauseOnAsyncCall) { this._pauseOnAsyncCall(_scheduledPauseOnAsyncCall); } this.dispatchEventToListeners(Events.DebuggerWasEnabled, this); await enablePromise; } async syncDebuggerId() { const isRemoteFrontend = Root.Runtime.Runtime.queryParam('remoteFrontend') || Root.Runtime.Runtime.queryParam('ws'); const maxScriptsCacheSize = isRemoteFrontend ? 10e6 : 100e6; const enablePromise = this._agent.invoke_enable({maxScriptsCacheSize}); enablePromise.then(this._registerDebugger.bind(this)); return enablePromise; } _onFrameNavigated() { if (DebuggerModel._shouldResyncDebuggerId) { return; } DebuggerModel._shouldResyncDebuggerId = true; } /** * @param {!Protocol.Debugger.EnableResponse} response */ _registerDebugger(response) { if (response.getError()) { return; } const {debuggerId} = response; _debuggerIdToModel.set(debuggerId, this); this._debuggerId = debuggerId; this.dispatchEventToListeners(Events.DebuggerIsReadyToPause, this); } /** * @return {boolean} */ isReadyToPause() { return Boolean(this._debuggerId); } /** * @param {string} debuggerId * @return {Promise<?DebuggerModel>} */ static async modelForDebuggerId(debuggerId) { if (DebuggerModel._shouldResyncDebuggerId) { await DebuggerModel.resyncDebuggerIdForModels(); DebuggerModel._shouldResyncDebuggerId = false; } return _debuggerIdToModel.get(debuggerId) || null; } static async resyncDebuggerIdForModels() { const dbgModels = _debuggerIdToModel.values(); for (const dbgModel of dbgModels) { if (dbgModel.debuggerEnabled()) { await dbgModel.syncDebuggerId(); } } } /** * @return {!Promise<void>} */ async _disableDebugger() { if (!this._debuggerEnabled) { return; } this._debuggerEnabled = false; await this._asyncStackTracesStateChanged(); await this._agent.invoke_disable(); this._isPausing = false; this.globalObjectCleared(); this.dispatchEventToListeners(Events.DebuggerWasDisabled); if (typeof this._debuggerId === 'string') { _debuggerIdToModel.delete(this._debuggerId); } } /** * @param {boolean} skip */ _skipAllPauses(skip) { if (this._skipAllPausesTimeout) { clearTimeout(this._skipAllPausesTimeout); this._skipAllPausesTimeout = 0; } this._agent.invoke_setSkipAllPauses({skip}); } /** * @param {number} timeout */ skipAllPausesUntilReloadOrTimeout(timeout) { if (this._skipAllPausesTimeout) { clearTimeout(this._skipAllPausesTimeout); } 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); } _pauseOnExceptionStateChanged() { /** @type {!Protocol.Debugger.SetPauseOnExceptionsRequestState} */ let state; if (!Common.Settings.Settings.instance().moduleSetting('pauseOnExceptionEnabled').get()) { state = Protocol.Debugger.SetPauseOnExceptionsRequestState.None; } else if (Common.Settings.Settings.instance().moduleSetting('pauseOnCaughtException').get()) { state = Protocol.Debugger.SetPauseOnExceptionsRequestState.All; } else { state = Protocol.Debugger.SetPauseOnExceptionsRequestState.Uncaught; } this._agent.invoke_setPauseOnExceptions({state}); } _asyncStackTracesStateChanged() { const maxAsyncStackChainDepth = 32; const enabled = !Common.Settings.Settings.instance().moduleSetting('disableAsyncStackTraces').get() && this._debuggerEnabled; const maxDepth = enabled ? maxAsyncStackChainDepth : 0; return this._agent.invoke_setAsyncCallStackDepth({maxDepth}); } _breakpointsActiveChanged() { this._agent.invoke_setBreakpointsActive( {active: Common.Settings.Settings.instance().moduleSetting('breakpointsActive').get()}); } /** * @param {?function(!StepMode, !CallFrame):!Promise<!Array<!{start: !Location, end: !Location}>>} callback */ setComputeAutoStepRangesCallback(callback) { this._computeAutoStepRangesCallback = callback; } /** * @param {!StepMode} mode * @return {!Promise<!Array<!Protocol.Debugger.LocationRange>>} */ async _computeAutoStepSkipList(mode) { /** @type {!Array<!{start: !Location, end: !Location}>} */ let ranges = []; if (this._computeAutoStepRangesCallback && this._debuggerPausedDetails) { const [callFrame] = this._debuggerPausedDetails.callFrames; ranges = await this._computeAutoStepRangesCallback.call(null, mode, callFrame); } const skipList = ranges.map( location => new LocationRange( location.start.scriptId, new ScriptPosition(location.start.lineNumber, location.start.columnNumber), new ScriptPosition(location.end.lineNumber, location.end.columnNumber))); return sortAndMergeRanges(skipList).map(x => x.payload()); } async stepInto() { const skipList = await this._computeAutoStepSkipList(StepMode.StepInto); this._agent.invoke_stepInto({breakOnAsyncCall: false, skipList}); } async stepOver() { // Mark that in case of auto-stepping, we should be doing // step-over instead of step-in. this._autoStepOver = true; const skipList = await this._computeAutoStepSkipList(StepMode.StepOver); this._agent.invoke_stepOver({skipList}); } async stepOut() { const skipList = await this._computeAutoStepSkipList(StepMode.StepOut); if (skipList.length !== 0) { this._agent.invoke_stepOver({skipList}); } else { this._agent.invoke_stepOut(); } } scheduleStepIntoAsync() { this._computeAutoStepSkipList(StepMode.StepInto).then(skipList => { this._agent.invoke_stepInto({breakOnAsyncCall: true, skipList}); }); } resume() { this._agent.invoke_resume({terminateOnResume: false}); this._isPausing = false; } pause() { this._isPausing = true; this._skipAllPauses(false); this._agent.invoke_pause(); } /** * @param {!Protocol.Runtime.StackTraceId} parentStackTraceId * @return {!Promise<!Object>} */ _pauseOnAsyncCall(parentStackTraceId) { return this._agent.invoke_pauseOnAsyncCall({parentStackTraceId: parentStackTraceId}); } /** * @param {string} url * @param {number} lineNumber * @param {number=} columnNumber * @param {string=} condition * @return {!Promise<!SetBreakpointResult>} */ async setBreakpointByURL(url, lineNumber, columnNumber, condition) { // Convert file url to node-js path. let urlRegex; if (this.target().type() === Type.Node) { const platformPath = Common.ParsedURL.ParsedURL.urlToPlatformPath(url, Host.Platform.isWin()); urlRegex = `${Platform.StringUtilities.escapeForRegExp(platformPath)}|${Platform.StringUtilities.escapeForRegExp(url)}`; } // 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}; } /** @type {!Array<!Location>} */ let locations = []; if (response.locations) { locations = response.locations.map(payload => Location.fromPayload(this, payload)); } return {locations, breakpointId: response.breakpointId}; } /** * @param {string} scriptId * @param {string} scriptHash * @param {number} lineNumber * @param {number=} columnNumber * @param {string=} condition * @return {!Promise<!SetBreakpointResult>} */ async setBreakpointInAnonymousScript(scriptId, scriptHash, lineNumber, columnNumber, condition) { const response = await this._agent.invoke_setBreakpointByUrl( {lineNumber: lineNumber, scriptHash: scriptHash, columnNumber: columnNumber, condition: condition}); const error = response.getError(); if (error) { // Old V8 backend doesn't support scriptHash argument. if (error !== 'Either url or urlRegex must be specified.') { return {locations: [], breakpointId: null}; } return this._setBreakpointBySourceId(scriptId, lineNumber, columnNumber, condition); } /** @type {!Array<!Location>} */ let locations = []; if (response.locations) { locations = response.locations.map(payload => Location.fromPayload(this, payload)); } return {locations, breakpointId: response.breakpointId}; } /** * @param {string} scriptId * @param {number} lineNumber * @param {number=} columnNumber * @param {string=} condition * @return {!Promise<!SetBreakpointResult>} */ async _setBreakpointBySourceId(scriptId, lineNumber, columnNumber, condition) { // This method is required for backward compatibility with V8 before 6.3.275. const response = await this._agent.invoke_setBreakpoint( {location: {scriptId: scriptId, lineNumber: lineNumber, columnNumber: columnNumber}, condition: condition}); if (response.getError()) { return {breakpointId: null, locations: []}; } /** @type {!Array<!Location>} */ let actualLocation = []; if (response.actualLocation) { actualLocation = [Location.fromPayload(this, response.actualLocation)]; } return {locations: actualLocation, breakpointId: response.breakpointId}; } /** * @param {!Protocol.Debugger.BreakpointId} breakpointId * @return {!Promise<void>} */ async removeBreakpoint(breakpointId) { const response = await this._agent.invoke_removeBreakpoint({breakpointId}); if (response.getError()) { console.error('Failed to remove breakpoint: ' + response.getError()); } } /** * @param {!Location} startLocation * @param {?Location} endLocation * @param {boolean} restrictToFunction * @return {!Promise<!Array<!BreakLocation>>} */ async getPossibleBreakpoints(startLocation, endLocation, restrictToFunction) { 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)); } /** * @param {!Protocol.Runtime.StackTraceId} stackId * @return {!Promise<?Protocol.Runtime.StackTrace>} */ async fetchAsyncStackTrace(stackId) { const response = await this._agent.invoke_getStackTrace({stackTraceId: stackId}); return response.getError() ? null : response.stackTrace; } /** * @param {!Protocol.Debugger.BreakpointId} breakpointId * @param {!Protocol.Debugger.Location} location */ _breakpointResolved(breakpointId, location) { this._breakpointResolvedEventTarget.dispatchEventToListeners(breakpointId, Location.fromPayload(this, location)); } globalObjectCleared() { this._setDebuggerPausedDetails(null); this._reset(); // TODO(dgozman): move clients to ExecutionContextDestroyed/ScriptCollected events. this.dispatchEventToListeners(Events.GlobalObjectCleared, this); } _reset() { for (const scriptWithSourceMap of this._sourceMapIdToScript.values()) { this._sourceMapManager.detachSourceMap(scriptWithSourceMap); } this._sourceMapIdToScript.clear(); this._scripts.clear(); this._scriptsBySourceURL.clear(); this._discardableScripts = []; this._autoStepOver = false; } /** * @return {!Array<!Script>} */ scripts() { return Array.from(this._scripts.values()); } /** * @param {!Protocol.Runtime.ScriptId} scriptId * @return {?Script} */ scriptForId(scriptId) { return this._scripts.get(scriptId) || null; } /** * @param {?string} sourceURL * @return {!Array.<!Script>} */ scriptsForSourceURL(sourceURL) { if (!sourceURL) { return []; } return this._scriptsBySourceURL.get(sourceURL) || []; } /** * @param {!ExecutionContext} executionContext * @return {!Array<!Script>} */ scriptsForExecutionContext(executionContext) { const result = []; for (const script of this._scripts.values()) { if (script.executionContextId === executionContext.id) { result.push(script); } } return result; } /** * @param {!Protocol.Runtime.ScriptId} scriptId * @param {string} newSource * @param {function(?ProtocolClient.InspectorBackend.ProtocolError, !Protocol.Runtime.ExceptionDetails=):void} callback */ setScriptSource(scriptId, newSource, callback) { const script = this._scripts.get(scriptId); if (script) { script.editSource(newSource, this._didEditScriptSource.bind(this, scriptId, newSource, callback)); } } /** * @param {!Protocol.Runtime.ScriptId} scriptId * @param {string} newSource * @param {function(?ProtocolClient.InspectorBackend.ProtocolError, !Protocol.Runtime.ExceptionDetails=):void} callback * @param {?ProtocolClient.InspectorBackend.ProtocolError} error * @param {!Protocol.Runtime.ExceptionDetails=} exceptionDetails * @param {!Array.<!Protocol.Debugger.CallFrame>=} callFrames * @param {!Protocol.Runtime.StackTrace=} asyncStackTrace * @param {!Protocol.Runtime.StackTraceId=} asyncStackTraceId * @param {boolean=} needsStepIn */ _didEditScriptSource( scriptId, newSource, callback, error, exceptionDetails, callFrames, asyncStackTrace, asyncStackTraceId, needsStepIn) { callback(error, exceptionDetails); if (needsStepIn) { this.stepInto(); return; } if (!error && callFrames && callFrames.length && this._debuggerPausedDetails) { this._pausedScript( callFrames, this._debuggerPausedDetails.reason, this._debuggerPausedDetails.auxData, this._debuggerPausedDetails.breakpointIds, asyncStackTrace, asyncStackTraceId); } } /** * @return {?Array.<!CallFrame>} */ get callFrames() { return this._debuggerPausedDetails ? this._debuggerPausedDetails.callFrames : null; } /** * @return {?DebuggerPausedDetails} */ debuggerPausedDetails() { return this._debuggerPausedDetails; } /** * @param {?DebuggerPausedDetails} debuggerPausedDetails * @return {boolean} */ _setDebuggerPausedDetails(debuggerPausedDetails) { if (debuggerPausedDetails) { this._isPausing = false; this._debuggerPausedDetails = debuggerPausedDetails; if (this._beforePausedCallback) { if (!this._beforePausedCallback.call(null, debuggerPausedDetails)) { return false; } } // If we resolved a location in auto-stepping callback, reset the // step-over marker. this._autoStepOver = false; this.dispatchEventToListeners(Events.DebuggerPaused, this); this.setSelectedCallFrame(debuggerPausedDetails.callFrames[0]); } else { this._isPausing = false; this._debuggerPausedDetails = null; this.setSelectedCallFrame(null); } return true; } /** * @param {?function(!DebuggerPausedDetails):boolean} callback */ setBeforePausedCallback(callback) { this._beforePausedCallback = callback; } /** * @param {?function(!Array<!CallFrame>):!Promise<!Array<!CallFrame>>} callback */ setExpandCallFramesCallback(callback) { this._expandCallFramesCallback = callback; } /** * @param {?function(!CallFrame, !EvaluationOptions):!Promise<?EvaluationResult>} callback */ setEvaluateOnCallFrameCallback(callback) { this._evaluateOnCallFrameCallback = callback; } /** * @param {!Array<!Protocol.Debugger.CallFrame>} callFrames * @param {!Protocol.Debugger.PausedEventReason} reason * @param {!Object|undefined} auxData * @param {!Array.<string>} breakpointIds * @param {!Protocol.Runtime.StackTrace=} asyncStackTrace * @param {!Protocol.Runtime.StackTraceId=} asyncStackTraceId * @param {!Protocol.Runtime.StackTraceId=} asyncCallStackTraceId */ async _pausedScript( callFrames, reason, auxData, breakpointIds, asyncStackTrace, asyncStackTraceId, asyncCallStackTraceId) { if (this._ignoreDebuggerPausedEvents) { return; } if (asyncCallStackTraceId) { // Note: this is only to support old backends. Newer ones do not send asyncCallStackTraceId. _scheduledPauseOnAsyncCall = asyncCallStackTraceId; const promises = []; for (const model of _debuggerIdToModel.values()) { promises.push(model._pauseOnAsyncCall(asyncCallStackTraceId)); } await Promise.all(promises); 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 (!this._setDebuggerPausedDetails(pausedDetails)) { if (this._autoStepOver) { this.stepOver(); } else { this.stepInto(); } } else { Common.EventTarget.fireEvent('DevTools.DebuggerPaused'); } _scheduledPauseOnAsyncCall = null; } _resumedScript() { this._setDebuggerPausedDetails(null); this.dispatchEventToListeners(Events.DebuggerResumed, this); } /** * @param {!Protocol.Runtime.ScriptId} scriptId * @param {string} sourceURL * @param {number} startLine * @param {number} startColumn * @param {number} endLine * @param {number} endColumn * @param {!Protocol.Runtime.ExecutionContextId} executionContextId * @param {string} hash * @param {*|undefined} executionContextAuxData * @param {boolean} isLiveEdit * @param {string|undefined} sourceMapURL * @param {boolean} hasSourceURLComment * @param {boolean} hasSyntaxError * @param {number} length * @param {?Protocol.Runtime.StackTrace} originStackTrace * @param {?number} codeOffset * @param {?string} scriptLanguage * @param {?Protocol.Debugger.DebugSymbols} debugSymbols * @param {?string} embedderName * @return {!Script} */ _parsedScriptSource( scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, isLiveEdit, sourceMapURL, hasSourceURLComment, hasSyntaxError, length, originStackTrace, codeOffset, scriptLanguage, debugSymbols, embedderName) { const knownScript = this._scripts.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, originStackTrace, codeOffset, scriptLanguage, debugSymbols, embedderName); this._registerScript(script); this.dispatchEventToListeners(Events.ParsedScriptSource, script); const sourceMapId = DebuggerModel._sourceMapId(script.executionContextId, script.sourceURL, script.sourceMapURL); if (sourceMapId && !hasSyntaxError) { // Consecutive script evaluations in the same execution context with the same sourceURL // and sourceMappingURL should result in source map reloading. const previousScript = this._sourceMapIdToScript.get(sourceMapId); if (previousScript) { this._sourceMapManager.detachSourceMap(previousScript); } this._sourceMapIdToScript.set(sourceMapId, script); this._sourceMapManager.attachSourceMap(script, script.sourceURL, script.sourceMapURL); } const isDiscardable = hasSyntaxError && script.isAnonymousScript(); if (isDiscardable) { this._discardableScripts.push(script); this._collectDiscardedScripts(); } return script; } /** * @param {!Script} script * @param {string} newSourceMapURL */ setSourceMapURL(script, newSourceMapURL) { let sourceMapId = DebuggerModel._sourceMapId(script.executionContextId, script.sourceURL, script.sourceMapURL); if (sourceMapId && this._sourceMapIdToScript.get(sourceMapId) === script) { this._sourceMapIdToScript.delete(sourceMapId); } this._sourceMapManager.detachSourceMap(script); script.sourceMapURL = newSourceMapURL; sourceMapId = DebuggerModel._sourceMapId(script.executionContextId, script.sourceURL, script.sourceMapURL); if (!sourceMapId) { return; } this._sourceMapIdToScript.set(sourceMapId, script); this._sourceMapManager.attachSourceMap(script, script.sourceURL, script.sourceMapURL); } /** * @param {!ExecutionContext} executionContext */ executionContextDestroyed(executionContext) { const sourceMapIds = Array.from(this._sourceMapIdToScript.keys()); for (const sourceMapId of sourceMapIds) { const script = this._sourceMapIdToScript.get(sourceMapId); if (script && script.executionContextId === executionContext.id) { this._sourceMapIdToScript.delete(sourceMapId); this._sourceMapManager.detachSourceMap(script); } } } /** * @param {!Script} script */ _registerScript(script) { 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); } scripts.push(script); } /** * @param {!Script} script */ _unregisterScript(script) { console.assert(script.isAnonymousScript()); this._scripts.delete(script.scriptId); } _collectDiscardedScripts() { 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); } } /** * @param {!Script} script * @param {number} lineNumber * @param {number} columnNumber * @return {!Location} */ createRawLocation(script, lineNumber, columnNumber) { return this.createRawLocationByScriptId(script.scriptId, lineNumber, columnNumber); } /** * @param {string} sourceURL * @param {number} lineNumber * @param {number} columnNumber * @return {?Location} */ createRawLocationByURL(sourceURL, lineNumber, columnNumber) { let closestScript = null; const scripts = this._scriptsBySourceURL.get(sourceURL) || []; for (let i = 0, l = scripts.length; i < l; ++i) { const script = scripts[i]; if (!closestScript) { closestScript = script; } if (script.lineOffset > lineNumber || (script.lineOffset === lineNumber && script.columnOffset > columnNumber)) { continue; } if (script.endLine < lineNumber || (script.endLine === lineNumber && script.endColumn <= columnNumber)) { continue; } closestScript = script; break; } return closestScript ? new Location(this, closestScript.scriptId, lineNumber, columnNumber) : null; } /** * @param {!Protocol.Runtime.ScriptId} scriptId * @param {number} lineNumber * @param {number} columnNumber * @return {!Location} */ createRawLocationByScriptId(scriptId, lineNumber, columnNumber) { return new Location(this, scriptId, lineNumber, columnNumber); } /** * @param {!Protocol.Runtime.StackTrace} stackTrace * @return {!Array<!Location>} */ createRawLocationsByStackTrace(stackTrace) { const frames = []; /** @type {!Protocol.Runtime.StackTrace|undefined} */ let current = stackTrace; while (current) { for (const frame of current.callFrames) { frames.push(frame); } current = current.parent; } const rawLocations = []; for (const frame of frames) { const rawLocation = this.createRawLocationByScriptId(frame.scriptId, frame.lineNumber, frame.columnNumber); if (rawLocation) { rawLocations.push(rawLocation); } } return rawLocations; } /** * @return {boolean} */ isPaused() { return Boolean(this.debuggerPausedDetails()); } /** * @return {boolean} */ isPausing() { return this._isPausing; } /** * @param {?CallFrame} callFrame */ setSelectedCallFrame(callFrame) { if (this._selectedCallFrame === callFrame) { return; } this._selectedCallFrame = callFrame; this.dispatchEventToListeners(Events.CallFrameSelected, this); } /** * @return {?CallFrame} */ selectedCallFrame() { return this._selectedCallFrame; } /** * @param {!EvaluationOptions} options * @return {!Promise<!EvaluationResult>} */ async evaluateOnSelectedCallFrame(options) { const callFrame = this.selectedCallFrame(); if (!callFrame) { throw new Error('No call frame selected'); } return callFrame.evaluate(options); } /** * @param {!RemoteObject} remoteObject * @return {!Promise<?FunctionDetails>} */ functionDetailsPromise(remoteObject) { return remoteObject.getAllProperties(false /* accessorPropertiesOnly */, false /* generatePreview */) .then(buildDetails.bind(this)); /** * @param {!GetPropertiesResult} response * @return {?FunctionDetails} * @this {!DebuggerModel} */ function buildDetails(response) { if (!response) { return null; } let location = null; if (response.internalProperties) { for (const prop of response.internalProperties) { if (prop.name === '[[FunctionLocation]]') { location = prop.value; } } } let functionName = null; if (response.properties) { for (const prop of response.properties) { if (prop.name === 'name' && prop.value && prop.value.type === 'string') { functionName = prop.value; } if (prop.name === 'displayName' && prop.value && prop.value.type === 'string') { functionName = prop.value; break; } } } let debuggerLocation = null; if (location) { debuggerLocation = this.createRawLocationByScriptId( location.value.scriptId, location.value.lineNumber, location.value.columnNumber); } return {location: debuggerLocation, functionName: functionName ? /** @type {string} */ (functionName.value) : ''}; } } /** * @param {number} scopeNumber * @param {string} variableName * @param {!Protocol.Runtime.CallArgument} newValue * @param {string} callFrameId * @return {!Promise<string|undefined>} */ async setVariableValue(scopeNumber, variableName, newValue, callFrameId) { const response = await this._agent.invoke_setVariableValue({scopeNumber, variableName, newValue, callFrameId}); const error = response.getError(); if (error) { console.error(error); } return error; } /** * @param {!Protocol.Debugger.BreakpointId} breakpointId * @param {function(!Common.EventTarget.EventTargetEvent):void} listener * @param {!Object=} thisObject */ addBreakpointListener(breakpointId, listener, thisObject) { this._breakpointResolvedEventTarget.addEventListener(breakpointId, listener, thisObject); } /** * @param {!Protocol.Debugger.BreakpointId} breakpointId * @param {function(!Common.EventTarget.EventTargetEvent):void} listener * @param {!Object=} thisObject */ removeBreakpointListener(breakpointId, listener, thisObject) { this._breakpointResolvedEventTarget.removeEventListener(breakpointId, listener, thisObject); } /** * @param {!Array<string>} patterns * @return {!Promise<boolean>} */ async setBlackboxPatterns(patterns) { const response = await this._agent.invoke_setBlackboxPatterns({patterns}); const error = response.getError(); if (error) { console.error(error); } return !error; } /** * @override */ dispose() { this._sourceMapManager.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 * @return {!Promise<void>} */ async suspendModel() { await this._disableDebugger(); } /** * @override * @return {!Promise<void>} */ async resumeModel() { await this._enableDebugger(); } } DebuggerModel._shouldResyncDebuggerId = false; /** @type {!Map<string, !DebuggerModel>} */ export const _debuggerIdToModel = new Map(); /** @type {?Protocol.Runtime.StackTraceId} */ export let _scheduledPauseOnAsyncCall = null; /** * Keep these in sync with WebCore::V8Debugger * * @enum {string} */ export const PauseOnExceptionsState = { DontPauseOnExceptions: 'none', PauseOnAllExceptions: 'all', PauseOnUncaughtExceptions: 'uncaught' }; /** @enum {symbol} */ export const Events = { DebuggerWasEnabled: Symbol('DebuggerWasEnabled'), DebuggerWasDisabled: Symbol('DebuggerWasDisabled'), DebuggerPaused: Symbol('DebuggerPaused'), DebuggerResumed: Symbol('DebuggerResumed'), ParsedScriptSource: Symbol('ParsedScriptSource'), FailedToParseScriptSource: Symbol('FailedToParseScriptSource'), DiscardedAnonymousScriptSource: Symbol('DiscardedAnonymousScriptSource'), GlobalObjectCleared: Symbol('GlobalObjectCleared'), CallFrameSelected: Symbol('CallFrameSelected'), ConsoleCommandEvaluatedInSelectedCallFrame: Symbol('ConsoleCommandEvaluatedInSelectedCallFrame'), DebuggerIsReadyToPause: Symbol('DebuggerIsReadyToPause'), }; /** * @implements {ProtocolProxyApi.DebuggerDispatcher} */ class DebuggerDispatcher { /** * @param {!DebuggerModel} debuggerModel */ constructor(debuggerModel) { this._debuggerModel = debuggerModel; } /** * @override * @param {!Protocol.Debugger.PausedEvent} event */ paused({callFrames, reason, data, hitBreakpoints, asyncStackTrace, asyncStackTraceId, asyncCallStackTraceId}) { if (!this._debuggerModel.debuggerEnabled()) { return; } this._debuggerModel._pausedScript( callFrames, reason, data, hitBreakpoints || [], asyncStackTrace, asyncStackTraceId, asyncCallStackTraceId); } /** * @override */ resumed() { if (!this._debuggerModel.debuggerEnabled()) { return; } this._debuggerModel._resumedScript(); } /** * @override * @param {!Protocol.Debugger.ScriptParsedEvent} event */ scriptParsed({ scriptId, url, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, isLiveEdit, sourceMapURL, hasSourceURL, isModule, length, stackTrace, codeOffset, scriptLanguage, debugSymbols, embedderName }) { if (!this._debuggerModel.debuggerEnabled()) { return; } this._debuggerModel._parsedScriptSource( scriptId, url, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, Boolean(isLiveEdit), sourceMapURL, Boolean(hasSourceURL), false, length || 0, stackTrace || null, codeOffset || null, scriptLanguage || null, debugSymbols || null, embedderName || null); } /** * @override * @param {!Protocol.Debugger.ScriptFailedToParseEvent} event */ scriptFailedToParse({ scriptId, url, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, sourceMapURL, hasSourceURL, isModule, length, stackTrace, codeOffset, scriptLanguage, embedderName }) { if (!this._debuggerModel.debuggerEnabled()) { return; } this._debuggerModel._parsedScriptSource( scriptId, url, startLine, startColumn, endLine, endColumn, executionContextId, hash, executionContextAuxData, false, sourceMapURL, Boolean(hasSourceURL), true, length || 0, stackTrace || null, codeOffset || null, scriptLanguage || null, null, embedderName || null); } /** * @override * @param {!Protocol.Debugger.BreakpointResolvedEvent} event */ breakpointResolved({breakpointId, location}) { if (!this._debuggerModel.debuggerEnabled()) { return; } this._debuggerModel._breakpointResolved(breakpointId, location); } } export class Location { /** * @param {!DebuggerModel} debuggerModel * @param {string} scriptId * @param {number} lineNumber * @param {number=} columnNumber * @param {number=} inlineFrameIndex */ constructor(debuggerModel, scriptId, lineNumber, columnNumber, inlineFrameIndex) { this.debuggerModel = debuggerModel; this.scriptId = scriptId; this.lineNumber = lineNumber; this.columnNumber = columnNumber || 0; this.inlineFrameIndex = inlineFrameIndex || 0; } /** * @param {!DebuggerModel} debuggerModel * @param {!Protocol.Debugger.Location} payload * @param {number=} inlineFrameIndex * @return {!Location} */ static fromPayload(debuggerModel, payload, inlineFrameIndex) { return new Location(debuggerModel, payload.scriptId, payload.lineNumber, payload.columnNumber, inlineFrameIndex); } /** * @return {!Protocol.Debugger.Location} */ payload() { return {scriptId: this.scriptId, lineNumber: this.lineNumber, columnNumber: this.columnNumber}; } /** * @return {?Script} */ script() { return this.debuggerModel.scriptForId(this.scriptId); } /** * @param {function():void=} pausedCallback */ continueToLocation(pausedCallback) { if (pausedCallback) { this.debuggerModel._continueToLocationCallback = this._paused.bind(this, pausedCallback); } this.debuggerModel._agent.invoke_continueToLocation({ location: this.payload(), targetCallFrames: Protocol.Debugger.ContinueToLocationRequestTargetCallFrames.Current }); } /** * @param {function():void|undefined} pausedCallback * @param {!DebuggerPausedDetails} debuggerPausedDetails * @return {boolean} */ _paused(pausedCallback, debuggerPausedDetails) { const location = debuggerPausedDetails.callFrames[0].location(); if (location.scriptId === this.scriptId && location.lineNumber === this.lineNumber && location.columnNumber === this.columnNumber) { pausedCallback(); return true; } return false; } /** * @return {string} */ id() { return this.debuggerModel.target().id() + ':' + this.scriptId + ':' + this.lineNumber + ':' + this.columnNumber; } } export class ScriptPosition { /** * @param {number} lineNumber * @param {number} columnNumber */ constructor(lineNumber, columnNumber) { this.lineNumber = lineNumber; this.columnNumber = columnNumber; } /** * @return {!Protocol.Debugger.ScriptPosition} */ payload() { return {lineNumber: this.lineNumber, columnNumber: this.columnNumber}; } /** * @param {!ScriptPosition} other * @return {number} */ compareTo(other) { if (this.lineNumber !== other.lineNumber) { return this.lineNumber - other.lineNumber; } return this.columnNumber - other.columnNumber; } } export class LocationRange { /** * @param {string} scriptId * @param {!ScriptPosition} start * @param {!ScriptPosition} end */ constructor(scriptId, start, end) { this.scriptId = scriptId; this.start = start; this.end = end; } /** * @return {!Protocol.Debugger.LocationRange} */ payload() { return {scriptId: this.scriptId, start: this.start.payload(), end: this.end.payload()}; } /** * @param {!LocationRange} location1 * @param {!LocationRange} location2 * @return {number} */ static comparator(location1, location2) { return location1.compareTo(location2); } /** * @param {!LocationRange} other * @return {number} */ compareTo(other) { if (this.scriptId !== other.scriptId) { return this.scriptId > other.scriptId ? 1 : -1; } const startCmp = this.start.compareTo(other.start); if (startCmp) { return startCmp; } return this.end.compareTo(other.end); } /** * @param {!LocationRange} other * @return boolean */ overlap(other) { if (this.scriptId !== other.scriptId) { return false; } const startCmp = this.start.compareTo(other.start); if (startCmp < 0) { return this.end.compareTo(other.start) >= 0; } if (startCmp > 0) { return this.start.compareTo(other.end) <= 0; } return true; } } export class BreakLocation extends Location { /** * @param {!DebuggerModel} debuggerModel * @param {string} scriptId * @param {number} lineNumber * @param {number=} columnNumber * @param {!Protocol.Debugger.BreakLocationType=} type */ constructor(debuggerModel, scriptId, lineNumber, columnNumber, type) { super(debuggerModel, scriptId, lineNumber, columnNumber); if (type) { this.type = type; } } /** * @override * @param {!DebuggerModel} debuggerModel * @param {!Protocol.Debugger.BreakLocation} payload * @return {!BreakLocation} */ static fromPayload(debuggerModel, payload) { return new BreakLocation(debuggerModel, payload.scriptId, payload.lineNumber, payload.columnNumber, payload.type); } } export class CallFrame { /** * @param {!DebuggerModel} debuggerModel * @param {!Script} script * @param {!Protocol.Debugger.CallFrame} payload * @param {number=} inlineFrameIndex * @param {string=} functionName */ constructor(debuggerModel, script, payload, inlineFrameIndex, functionName) { this.debuggerModel = debuggerModel; this._script = script; this._payload = payload; this._location = Location.fromPayload(debuggerModel, payload.location, inlineFrameIndex); /** @type {!Array<!Scope>} */ this._scopeChain = []; this._localScope = null; this._inlineFrameIndex = inlineFrameIndex || 0; this._functionName = functionName || payload.functionName; 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; } /** * @param {!DebuggerModel} debuggerModel * @param {!Array.<!Protocol.Debugger.CallFrame>} callFrames * @return {!Array.<!CallFrame>} */ static fromPayloadArray(debuggerModel, callFrames) { 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; } /** * @param {number=} inlineFrameIndex * @param {string=} functionName */ createVirtualCallFrame(inlineFrameIndex, functionName) { return new CallFrame(this.debuggerModel, this._script, this._payload, inlineFrameIndex, functionName); } /** * @return {!Script} */ get script() { return this._script; } /** * @return {string} */ get id() { return this._payload.callFrameId; } /** * @return {number} */ get inlineFrameIndex() { return this._inlineFrameIndex; } /** * @return {!Array.<!Scope>} */ scopeChain() { return this._scopeChain; } /** * @return {?Scope} */ localScope() { return this._localScope; } /** * @return {?RemoteObject} */ thisObject() { return this._payload.this ? this.debuggerModel._runtimeModel.createRemoteObject(this._payload.this) : null; } /** * @return {?RemoteObject} */ returnValue() { return this._returnValu