UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

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