UNPKG

chrome-devtools-frontend

Version:
600 lines (526 loc) • 23.5 kB
// Copyright 2014 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. import type * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Workspace from '../workspace/workspace.js'; import {CompilerScriptMapping} from './CompilerScriptMapping.js'; import {DebuggerLanguagePluginManager} from './DebuggerLanguagePlugins.js'; import {DefaultScriptMapping} from './DefaultScriptMapping.js'; import {IgnoreListManager} from './IgnoreListManager.js'; import {LiveLocationWithPool, type LiveLocation, type LiveLocationPool} from './LiveLocation.js'; import {ResourceMapping} from './ResourceMapping.js'; import {ResourceScriptMapping, type ResourceScriptFile} from './ResourceScriptMapping.js'; let debuggerWorkspaceBindingInstance: DebuggerWorkspaceBinding|undefined; export class DebuggerWorkspaceBinding implements SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> { readonly workspace: Workspace.Workspace.WorkspaceImpl; readonly #sourceMappings: DebuggerSourceMapping[]; readonly #debuggerModelToData: Map<SDK.DebuggerModel.DebuggerModel, ModelData>; readonly #liveLocationPromises: Set<Promise<void|Location|StackTraceTopFrameLocation|null>>; pluginManager: DebuggerLanguagePluginManager|null; #targetManager: SDK.TargetManager.TargetManager; private constructor(targetManager: SDK.TargetManager.TargetManager, workspace: Workspace.Workspace.WorkspaceImpl) { this.workspace = workspace; this.#sourceMappings = []; this.#debuggerModelToData = new Map(); targetManager.addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this); targetManager.addModelListener( SDK.DebuggerModel.DebuggerModel, SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this); targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this); this.#targetManager = targetManager; this.#liveLocationPromises = new Set(); this.pluginManager = Root.Runtime.experiments.isEnabled('wasmDWARFDebugging') ? new DebuggerLanguagePluginManager(targetManager, workspace, this) : null; } initPluginManagerForTest(): DebuggerLanguagePluginManager|null { if (Root.Runtime.experiments.isEnabled('wasmDWARFDebugging')) { if (!this.pluginManager) { this.pluginManager = new DebuggerLanguagePluginManager(this.#targetManager, this.workspace, this); } } else { this.pluginManager = null; } return this.pluginManager; } static instance(opts: { forceNew: boolean|null, targetManager: SDK.TargetManager.TargetManager|null, workspace: Workspace.Workspace.WorkspaceImpl|null, } = {forceNew: null, targetManager: null, workspace: null}): DebuggerWorkspaceBinding { const {forceNew, targetManager, workspace} = opts; if (!debuggerWorkspaceBindingInstance || forceNew) { if (!targetManager || !workspace) { throw new Error(`Unable to create DebuggerWorkspaceBinding: targetManager and workspace must be provided: ${ new Error().stack}`); } debuggerWorkspaceBindingInstance = new DebuggerWorkspaceBinding(targetManager, workspace); } return debuggerWorkspaceBindingInstance; } static removeInstance(): void { debuggerWorkspaceBindingInstance = undefined; } addSourceMapping(sourceMapping: DebuggerSourceMapping): void { this.#sourceMappings.push(sourceMapping); } removeSourceMapping(sourceMapping: DebuggerSourceMapping): void { const index = this.#sourceMappings.indexOf(sourceMapping); if (index !== -1) { this.#sourceMappings.splice(index, 1); } } private async computeAutoStepRanges(mode: SDK.DebuggerModel.StepMode, callFrame: SDK.DebuggerModel.CallFrame): Promise<RawLocationRange[]> { function contained(location: SDK.DebuggerModel.Location, range: RawLocationRange): boolean { const {start, end} = range; if (start.scriptId !== location.scriptId) { return false; } if (location.lineNumber < start.lineNumber || location.lineNumber > end.lineNumber) { return false; } if (location.lineNumber === start.lineNumber && location.columnNumber < start.columnNumber) { return false; } if (location.lineNumber === end.lineNumber && location.columnNumber >= end.columnNumber) { return false; } return true; } const rawLocation = callFrame.location(); if (!rawLocation) { return []; } const pluginManager = this.pluginManager; let ranges: RawLocationRange[] = []; if (pluginManager) { if (mode === SDK.DebuggerModel.StepMode.StepOut) { // Step out of inline function. return await pluginManager.getInlinedFunctionRanges(rawLocation); } const uiLocation = await pluginManager.rawLocationToUILocation(rawLocation); if (uiLocation) { ranges = await pluginManager.uiLocationToRawLocationRanges( uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber) || []; // TODO(bmeurer): Remove the {rawLocation} from the {ranges}? ranges = ranges.filter(range => contained(rawLocation, range)); if (mode === SDK.DebuggerModel.StepMode.StepOver) { // Step over an inlined function. ranges = ranges.concat(await pluginManager.getInlinedCalleesRanges(rawLocation)); } return ranges; } } const compilerMapping = this.#debuggerModelToData.get(rawLocation.debuggerModel)?.compilerMapping; if (!compilerMapping) { return []; } if (mode === SDK.DebuggerModel.StepMode.StepOut) { // We should actually return the source range for the entire function // to skip over. Since we don't have that, we return an empty range // instead, to signal that we should perform a regular step-out. return []; } ranges = compilerMapping.getLocationRangesForSameSourceLocation(rawLocation); ranges = ranges.filter(range => contained(rawLocation, range)); return ranges; } modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void { this.#debuggerModelToData.set(debuggerModel, new ModelData(debuggerModel, this)); debuggerModel.setComputeAutoStepRangesCallback(this.computeAutoStepRanges.bind(this)); } modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void { debuggerModel.setComputeAutoStepRangesCallback(null); const modelData = this.#debuggerModelToData.get(debuggerModel); if (modelData) { modelData.dispose(); this.#debuggerModelToData.delete(debuggerModel); } } /** * The promise returned by this function is resolved once all *currently* * pending LiveLocations are processed. */ async pendingLiveLocationChangesPromise(): Promise<void|Location|StackTraceTopFrameLocation|null> { await Promise.all(this.#liveLocationPromises); } private recordLiveLocationChange(promise: Promise<void|Location|StackTraceTopFrameLocation|null>): void { void promise.then(() => { this.#liveLocationPromises.delete(promise); }); this.#liveLocationPromises.add(promise); } async updateLocations(script: SDK.Script.Script): Promise<void> { const modelData = this.#debuggerModelToData.get(script.debuggerModel); if (modelData) { const updatePromise = modelData.updateLocations(script); this.recordLiveLocationChange(updatePromise); await updatePromise; } } async createLiveLocation( rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<Location|null> { const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel); if (!modelData) { return null; } const liveLocationPromise = modelData.createLiveLocation(rawLocation, updateDelegate, locationPool); this.recordLiveLocationChange(liveLocationPromise); return liveLocationPromise; } async createStackTraceTopFrameLiveLocation( rawLocations: SDK.DebuggerModel.Location[], updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<LiveLocation> { console.assert(rawLocations.length > 0); const locationPromise = StackTraceTopFrameLocation.createStackTraceTopFrameLocation(rawLocations, this, updateDelegate, locationPool); this.recordLiveLocationChange(locationPromise); return locationPromise; } async createCallFrameLiveLocation( location: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<Location|null> { const script = location.script(); if (!script) { return null; } const debuggerModel = location.debuggerModel; const liveLocationPromise = this.createLiveLocation(location, updateDelegate, locationPool); this.recordLiveLocationChange(liveLocationPromise); const liveLocation = await liveLocationPromise; if (!liveLocation) { return null; } this.registerCallFrameLiveLocation(debuggerModel, liveLocation); return liveLocation; } async rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Promise<Workspace.UISourceCode.UILocation|null> { for (const sourceMapping of this.#sourceMappings) { const uiLocation = sourceMapping.rawLocationToUILocation(rawLocation); if (uiLocation) { return uiLocation; } } if (this.pluginManager) { const uiLocation = await this.pluginManager.rawLocationToUILocation(rawLocation); if (uiLocation) { return uiLocation; } } const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel); return modelData ? modelData.rawLocationToUILocation(rawLocation) : null; } uiSourceCodeForSourceMapSourceURL( debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString, isContentScript: boolean): Workspace.UISourceCode.UISourceCode|null { const modelData = this.#debuggerModelToData.get(debuggerModel); if (!modelData) { return null; } return modelData.compilerMapping.uiSourceCodeForURL(url, isContentScript); } async uiLocationToRawLocations( uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber?: number): Promise<SDK.DebuggerModel.Location[]> { for (const sourceMapping of this.#sourceMappings) { const locations = sourceMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); if (locations.length) { return locations; } } const locations = await this.pluginManager?.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); if (locations) { return locations; } for (const modelData of this.#debuggerModelToData.values()) { const locations = modelData.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); if (locations.length) { return locations; } } return []; } uiLocationToRawLocationsForUnformattedJavaScript( uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number): SDK.DebuggerModel.Location[] { console.assert(uiSourceCode.contentType().isScript()); const locations = []; for (const modelData of this.#debuggerModelToData.values()) { locations.push(...modelData.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber)); } return locations; } async normalizeUILocation(uiLocation: Workspace.UISourceCode.UILocation): Promise<Workspace.UISourceCode.UILocation> { const rawLocations = await this.uiLocationToRawLocations(uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber); for (const location of rawLocations) { const uiLocationCandidate = await this.rawLocationToUILocation(location); if (uiLocationCandidate) { return uiLocationCandidate; } } return uiLocation; } scriptFile(uiSourceCode: Workspace.UISourceCode.UISourceCode, debuggerModel: SDK.DebuggerModel.DebuggerModel): ResourceScriptFile|null { const modelData = this.#debuggerModelToData.get(debuggerModel); return modelData ? modelData.getResourceMapping().scriptFile(uiSourceCode) : null; } scriptsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] { const scripts = new Set<SDK.Script.Script>(); if (this.pluginManager) { this.pluginManager.scriptsForUISourceCode(uiSourceCode).forEach(script => scripts.add(script)); } for (const modelData of this.#debuggerModelToData.values()) { const resourceScriptFile = modelData.getResourceMapping().scriptFile(uiSourceCode); if (resourceScriptFile && resourceScriptFile.script) { scripts.add(resourceScriptFile.script); } modelData.compilerMapping.scriptsForUISourceCode(uiSourceCode).forEach(script => scripts.add(script)); } return [...scripts]; } scriptsForResource(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] { const scripts = new Set<SDK.Script.Script>(); for (const modelData of this.#debuggerModelToData.values()) { const resourceScriptFile = modelData.getResourceMapping().scriptFile(uiSourceCode); if (resourceScriptFile && resourceScriptFile.script) { scripts.add(resourceScriptFile.script); } } return [...scripts]; } supportsConditionalBreakpoints(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean { // DevTools traditionally supported (JavaScript) conditions // for breakpoints everywhere, so we keep that behavior... if (!this.pluginManager) { return true; } const scripts = this.pluginManager.scriptsForUISourceCode(uiSourceCode); return scripts.every(script => script.isJavaScript()); } sourceMapForScript(script: SDK.Script.Script): SDK.SourceMap.SourceMap|null { const modelData = this.#debuggerModelToData.get(script.debuggerModel); if (!modelData) { return null; } return modelData.compilerMapping.sourceMapForScript(script); } private globalObjectCleared(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void { this.reset(event.data); } private reset(debuggerModel: SDK.DebuggerModel.DebuggerModel): void { const modelData = this.#debuggerModelToData.get(debuggerModel); if (!modelData) { return; } for (const location of modelData.callFrameLocations.values()) { this.removeLiveLocation(location); } modelData.callFrameLocations.clear(); } resetForTest(target: SDK.Target.Target): void { const debuggerModel = (target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel); const modelData = this.#debuggerModelToData.get(debuggerModel); if (modelData) { modelData.getResourceMapping().resetForTest(); } } private registerCallFrameLiveLocation(debuggerModel: SDK.DebuggerModel.DebuggerModel, location: Location): void { const modelData = this.#debuggerModelToData.get(debuggerModel); if (modelData) { const locations = modelData.callFrameLocations; locations.add(location); } } removeLiveLocation(location: Location): void { const modelData = this.#debuggerModelToData.get(location.rawLocation.debuggerModel); if (modelData) { modelData.disposeLocation(location); } } private debuggerResumed(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void { this.reset(event.data); } } class ModelData { readonly #debuggerModel: SDK.DebuggerModel.DebuggerModel; readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding; callFrameLocations: Set<Location>; #defaultMapping: DefaultScriptMapping; resourceMapping: ResourceScriptMapping; readonly compilerMapping: CompilerScriptMapping; readonly #locations: Platform.MapUtilities.Multimap<string, Location>; constructor(debuggerModel: SDK.DebuggerModel.DebuggerModel, debuggerWorkspaceBinding: DebuggerWorkspaceBinding) { this.#debuggerModel = debuggerModel; this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding; this.callFrameLocations = new Set(); const workspace = debuggerWorkspaceBinding.workspace; this.#defaultMapping = new DefaultScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding); this.resourceMapping = new ResourceScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding); this.compilerMapping = new CompilerScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding); this.#locations = new Platform.MapUtilities.Multimap(); debuggerModel.setBeforePausedCallback(this.beforePaused.bind(this)); } async createLiveLocation( rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<Location> { console.assert(rawLocation.scriptId !== ''); const scriptId = rawLocation.scriptId; const location = new Location(scriptId, rawLocation, this.#debuggerWorkspaceBinding, updateDelegate, locationPool); this.#locations.set(scriptId, location); await location.update(); return location; } disposeLocation(location: Location): void { this.#locations.delete(location.scriptId, location); } async updateLocations(script: SDK.Script.Script): Promise<void> { const promises = []; for (const location of this.#locations.get(script.scriptId)) { promises.push(location.update()); } await Promise.all(promises); } rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null { let uiLocation = this.compilerMapping.rawLocationToUILocation(rawLocation); uiLocation = uiLocation || this.resourceMapping.rawLocationToUILocation(rawLocation); uiLocation = uiLocation || ResourceMapping.instance().jsLocationToUILocation(rawLocation); uiLocation = uiLocation || this.#defaultMapping.rawLocationToUILocation(rawLocation); return uiLocation; } uiLocationToRawLocations( uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number|undefined = 0): SDK.DebuggerModel.Location[] { // TODO(crbug.com/1153123): Revisit the `#columnNumber = 0` and also preserve `undefined` for source maps? let locations = this.compilerMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); locations = locations.length ? locations : this.resourceMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); locations = locations.length ? locations : ResourceMapping.instance().uiLocationToJSLocations(uiSourceCode, lineNumber, columnNumber); locations = locations.length ? locations : this.#defaultMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber); return locations; } private beforePaused(debuggerPausedDetails: SDK.DebuggerModel.DebuggerPausedDetails): boolean { return Boolean(debuggerPausedDetails.callFrames[0]); } dispose(): void { this.#debuggerModel.setBeforePausedCallback(null); this.compilerMapping.dispose(); this.resourceMapping.dispose(); this.#defaultMapping.dispose(); } getResourceMapping(): ResourceScriptMapping { return this.resourceMapping; } } export class Location extends LiveLocationWithPool { readonly scriptId: string; readonly rawLocation: SDK.DebuggerModel.Location; readonly #binding: DebuggerWorkspaceBinding; constructor( scriptId: string, rawLocation: SDK.DebuggerModel.Location, binding: DebuggerWorkspaceBinding, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool) { super(updateDelegate, locationPool); this.scriptId = scriptId; this.rawLocation = rawLocation; this.#binding = binding; } async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> { const debuggerModelLocation = this.rawLocation; return this.#binding.rawLocationToUILocation(debuggerModelLocation); } dispose(): void { super.dispose(); this.#binding.removeLiveLocation(this); } async isIgnoreListed(): Promise<boolean> { const uiLocation = await this.uiLocation(); if (!uiLocation) { return false; } return IgnoreListManager.instance().isUserOrSourceMapIgnoreListedUISourceCode(uiLocation.uiSourceCode); } } class StackTraceTopFrameLocation extends LiveLocationWithPool { #updateScheduled: boolean; #current: LiveLocation|null; #locations: LiveLocation[]|null; constructor(updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool) { super(updateDelegate, locationPool); this.#updateScheduled = true; this.#current = null; this.#locations = null; } static async createStackTraceTopFrameLocation( rawLocations: SDK.DebuggerModel.Location[], binding: DebuggerWorkspaceBinding, updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool): Promise<StackTraceTopFrameLocation> { const location = new StackTraceTopFrameLocation(updateDelegate, locationPool); const locationsPromises = rawLocations.map( rawLocation => binding.createLiveLocation(rawLocation, location.scheduleUpdate.bind(location), locationPool)); location.#locations = ((await Promise.all(locationsPromises)).filter(l => Boolean(l)) as Location[]); await location.updateLocation(); return location; } async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> { return this.#current ? this.#current.uiLocation() : null; } async isIgnoreListed(): Promise<boolean> { return this.#current ? this.#current.isIgnoreListed() : false; } dispose(): void { super.dispose(); if (this.#locations) { for (const location of this.#locations) { location.dispose(); } } this.#locations = null; this.#current = null; } private async scheduleUpdate(): Promise<void> { if (this.#updateScheduled) { return; } this.#updateScheduled = true; queueMicrotask(() => { void this.updateLocation(); }); } private async updateLocation(): Promise<void> { this.#updateScheduled = false; if (!this.#locations || this.#locations.length === 0) { return; } this.#current = this.#locations[0]; for (const location of this.#locations) { if (!(await location.isIgnoreListed())) { this.#current = location; break; } } void this.update(); } } export interface RawLocationRange { start: SDK.DebuggerModel.Location; end: SDK.DebuggerModel.Location; } export interface DebuggerSourceMapping { rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null; uiLocationToRawLocations( uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber?: number): SDK.DebuggerModel.Location[]; }