chrome-devtools-frontend
Version:
Chrome DevTools UI
1,095 lines (981 loc) • 43.4 kB
text/typescript
// Copyright 2020 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 Chrome} from '../../../extension-api/ExtensionAPI.js'; // eslint-disable-line rulesdir/es_modules_import
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';
import {ContentProviderBasedProject} from './ContentProviderBasedProject.js';
import {assertNotNullOrUndefined} from '../../core/platform/platform.js';
import {type DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js';
import {NetworkProject} from './NetworkProject.js';
const UIStrings = {
/**
*@description Error message that is displayed in the Console when language #plugins report errors
*@example {File not found} PH1
*/
errorInDebuggerLanguagePlugin: 'Error in debugger language plugin: {PH1}',
/**
*@description Status message that is shown in the Console when debugging information is being
*loaded. The 2nd and 3rd placeholders are URLs.
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*@example {http://web.dev/file.wasm.debug.wasm} PH3
*/
loadingDebugSymbolsForVia: '[{PH1}] Loading debug symbols for {PH2} (via {PH3})...',
/**
*@description Status message that is shown in the Console when debugging information is being loaded
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*/
loadingDebugSymbolsFor: '[{PH1}] Loading debug symbols for {PH2}...',
/**
*@description Warning message that is displayed in the Console when debugging information was loaded, but no source files were found
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*/
loadedDebugSymbolsForButDidnt: '[{PH1}] Loaded debug symbols for {PH2}, but didn\'t find any source files',
/**
*@description Status message that is shown in the Console when debugging information is successfully loaded
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*@example {42} PH3
*/
loadedDebugSymbolsForFound: '[{PH1}] Loaded debug symbols for {PH2}, found {PH3} source file(s)',
/**
*@description Error message that is displayed in the Console when debugging information cannot be loaded
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*@example {File not found} PH3
*/
failedToLoadDebugSymbolsFor: '[{PH1}] Failed to load debug symbols for {PH2} ({PH3})',
/**
*@description Error message that is displayed in UI debugging information cannot be found for a call frame
*@example {main} PH1
*/
failedToLoadDebugSymbolsForFunction: 'No debug information for function "{PH1}"',
/**
*@description Error message that is displayed in UI when a file needed for debugging information for a call frame is missing
*@example {mainp.debug.wasm.dwp} PH1
*/
debugSymbolsIncomplete: 'The debug information for function {PH1} is incomplete',
};
const str_ = i18n.i18n.registerUIStrings('models/bindings/DebuggerLanguagePlugins.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
/**
* Generates the raw module ID for a script, which is used
* to uniquely identify the debugging data for a script on
* the responsible language #plugin.
*
* @param script the unique raw module ID for the script.
*/
function rawModuleIdForScript(script: SDK.Script.Script): string {
return `${script.sourceURL}@${script.hash}`;
}
function getRawLocation(callFrame: SDK.DebuggerModel.CallFrame): Chrome.DevTools.RawLocation {
const {script} = callFrame;
return {
rawModuleId: rawModuleIdForScript(script),
codeOffset: callFrame.location().columnNumber - (script.codeOffset() || 0),
inlineFrameIndex: callFrame.inlineFrameIndex,
};
}
class FormattingError extends Error {
exception: SDK.RemoteObject.RemoteObject;
exceptionDetails: Protocol.Runtime.ExceptionDetails;
constructor(exception: SDK.RemoteObject.RemoteObject, exceptionDetails: Protocol.Runtime.ExceptionDetails) {
const {description} = exceptionDetails.exception || {};
super(description || exceptionDetails.text);
this.exception = exception;
this.exceptionDetails = exceptionDetails;
}
static makeLocal(callFrame: SDK.DebuggerModel.CallFrame, message: string): FormattingError {
const exception: Protocol.Runtime.RemoteObject = {
type: Protocol.Runtime.RemoteObjectType.Object,
subtype: Protocol.Runtime.RemoteObjectSubtype.Error,
description: message,
};
const exceptionDetails: Protocol.Runtime
.ExceptionDetails = {text: 'Uncaught', exceptionId: -1, columnNumber: 0, lineNumber: 0, exception};
const errorObject = callFrame.debuggerModel.runtimeModel().createRemoteObject(exception);
return new FormattingError(errorObject, exceptionDetails);
}
}
class NamespaceObject extends SDK.RemoteObject.LocalJSONObject {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(value: any) {
super(value);
}
override get description(): string {
return this.type;
}
override get type(): string {
return 'namespace';
}
}
class SourceScopeRemoteObject extends SDK.RemoteObject.RemoteObjectImpl {
variables: Chrome.DevTools.Variable[];
#callFrame: SDK.DebuggerModel.CallFrame;
#plugin: DebuggerLanguagePlugin;
stopId: StopId;
constructor(callFrame: SDK.DebuggerModel.CallFrame, stopId: StopId, plugin: DebuggerLanguagePlugin) {
super(callFrame.debuggerModel.runtimeModel(), undefined, 'object', undefined, null);
this.variables = [];
this.#callFrame = callFrame;
this.#plugin = plugin;
this.stopId = stopId;
}
override async doGetProperties(ownProperties: boolean, accessorPropertiesOnly: boolean, _generatePreview: boolean):
Promise<SDK.RemoteObject.GetPropertiesResult> {
if (accessorPropertiesOnly) {
return {properties: [], internalProperties: []} as SDK.RemoteObject.GetPropertiesResult;
}
const properties = [];
const namespaces: {
[x: string]: SDK.RemoteObject.RemoteObject,
} = {};
function makeProperty(name: string, obj: SDK.RemoteObject.RemoteObject): SDK.RemoteObject.RemoteObjectProperty {
return new SDK.RemoteObject.RemoteObjectProperty(
name, obj,
/* enumerable=*/ false, /* writable=*/ false, /* isOwn=*/ true, /* wasThrown=*/ false);
}
for (const variable of this.variables) {
let sourceVar: SDK.RemoteObject.RemoteObject|undefined;
try {
const evalResult = await this.#plugin.evaluate(variable.name, getRawLocation(this.#callFrame), this.stopId);
sourceVar = evalResult ? new ExtensionRemoteObject(this.#callFrame, evalResult, this.#plugin) :
new SDK.RemoteObject.LocalJSONObject(undefined);
} catch (e) {
console.warn(e);
sourceVar = new SDK.RemoteObject.LocalJSONObject(undefined);
}
if (variable.nestedName && variable.nestedName.length > 1) {
let parent: {
[x: string]: SDK.RemoteObject.RemoteObject,
} = namespaces;
for (let index = 0; index < variable.nestedName.length - 1; index++) {
const nestedName = variable.nestedName[index];
let child: NamespaceObject|SDK.RemoteObject.RemoteObject = parent[nestedName];
if (!child) {
child = new NamespaceObject({});
parent[nestedName] = child;
}
parent = child.value;
}
const name = variable.nestedName[variable.nestedName.length - 1];
parent[name] = sourceVar;
} else {
properties.push(makeProperty(variable.name, sourceVar));
}
}
for (const namespace in namespaces) {
properties.push(makeProperty(namespace, (namespaces[namespace] as SDK.RemoteObject.RemoteObject)));
}
return {properties: properties, internalProperties: []};
}
}
export class SourceScope implements SDK.DebuggerModel.ScopeChainEntry {
readonly #callFrameInternal: SDK.DebuggerModel.CallFrame;
readonly #typeInternal: string;
readonly #typeNameInternal: string;
readonly #iconInternal: string|undefined;
readonly #objectInternal: SourceScopeRemoteObject;
readonly #startLocationInternal: SDK.DebuggerModel.Location|null;
readonly #endLocationInternal: SDK.DebuggerModel.Location|null;
constructor(
callFrame: SDK.DebuggerModel.CallFrame, stopId: StopId, type: string, typeName: string, icon: string|undefined,
plugin: DebuggerLanguagePlugin) {
if (icon && new URL(icon).protocol !== 'data:') {
throw new Error('The icon must be a data:-URL');
}
this.#callFrameInternal = callFrame;
this.#typeInternal = type;
this.#typeNameInternal = typeName;
this.#iconInternal = icon;
this.#objectInternal = new SourceScopeRemoteObject(callFrame, stopId, plugin);
this.#startLocationInternal = null;
this.#endLocationInternal = null;
}
async getVariableValue(name: string): Promise<SDK.RemoteObject.RemoteObject|null> {
for (let v = 0; v < this.#objectInternal.variables.length; ++v) {
if (this.#objectInternal.variables[v].name !== name) {
continue;
}
const properties = await this.#objectInternal.getAllProperties(false, false);
if (!properties.properties) {
continue;
}
const {value} = properties.properties[v];
if (value) {
return value;
}
}
return null;
}
callFrame(): SDK.DebuggerModel.CallFrame {
return this.#callFrameInternal;
}
type(): string {
return this.#typeInternal;
}
typeName(): string {
return this.#typeNameInternal;
}
name(): string|undefined {
return undefined;
}
startLocation(): SDK.DebuggerModel.Location|null {
return this.#startLocationInternal;
}
endLocation(): SDK.DebuggerModel.Location|null {
return this.#endLocationInternal;
}
object(): SourceScopeRemoteObject {
return this.#objectInternal;
}
description(): string {
return '';
}
icon(): string|undefined {
return this.#iconInternal;
}
}
export class ExtensionRemoteObject extends SDK.RemoteObject.RemoteObject {
private readonly extensionObject: Chrome.DevTools.RemoteObject;
private readonly plugin: DebuggerLanguagePlugin;
readonly callFrame: SDK.DebuggerModel.CallFrame;
constructor(
callFrame: SDK.DebuggerModel.CallFrame, extensionObject: Chrome.DevTools.RemoteObject,
plugin: DebuggerLanguagePlugin) {
super();
this.extensionObject = extensionObject;
this.plugin = plugin;
this.callFrame = callFrame;
}
get linearMemoryAddress(): number|undefined {
return this.extensionObject.linearMemoryAddress;
}
get linearMemorySize(): number|undefined {
return this.extensionObject.linearMemorySize;
}
override get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
return this.extensionObject.objectId as Protocol.Runtime.RemoteObjectId;
}
override get type(): string {
if (this.extensionObject.type === 'array' || this.extensionObject.type === 'null') {
return 'object';
}
return this.extensionObject.type;
}
override get subtype(): string|undefined {
if (this.extensionObject.type === 'array' || this.extensionObject.type === 'null') {
return this.extensionObject.type;
}
return undefined;
}
override get value(): unknown {
return this.extensionObject.value;
}
override unserializableValue(): string|undefined {
return undefined;
}
override get description(): string|undefined {
return this.extensionObject.description;
}
override set description(description: string|undefined) {
}
override get hasChildren(): boolean {
return this.extensionObject.hasChildren;
}
override get preview(): Protocol.Runtime.ObjectPreview|undefined {
return undefined;
}
override get className(): string|null {
return this.extensionObject.className ?? null;
}
override arrayLength(): number {
return 0;
}
override arrayBufferByteLength(): number {
return 0;
}
override getOwnProperties(_generatePreview: boolean, _nonIndexedPropertiesOnly?: boolean):
Promise<SDK.RemoteObject.GetPropertiesResult> {
return this.getAllProperties(false, _generatePreview, _nonIndexedPropertiesOnly);
}
override async getAllProperties(
_accessorPropertiesOnly: boolean, _generatePreview: boolean,
_nonIndexedPropertiesOnly?: boolean): Promise<SDK.RemoteObject.GetPropertiesResult> {
const {objectId} = this.extensionObject;
if (objectId) {
assertNotNullOrUndefined(this.plugin.getProperties);
const extensionObjectProperties = await this.plugin.getProperties(objectId);
const properties = extensionObjectProperties.map(
p => new SDK.RemoteObject.RemoteObjectProperty(
p.name, new ExtensionRemoteObject(this.callFrame, p.value, this.plugin)));
return {properties, internalProperties: null};
}
return {properties: null, internalProperties: null};
}
override release(): void {
const {objectId} = this.extensionObject;
if (objectId) {
assertNotNullOrUndefined(this.plugin.releaseObject);
void this.plugin.releaseObject(objectId);
}
}
override debuggerModel(): SDK.DebuggerModel.DebuggerModel {
return this.callFrame.debuggerModel;
}
override runtimeModel(): SDK.RuntimeModel.RuntimeModel {
return this.callFrame.debuggerModel.runtimeModel();
}
}
export type StopId = bigint;
export class DebuggerLanguagePluginManager implements
SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> {
readonly #workspace: Workspace.Workspace.WorkspaceImpl;
readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding;
#plugins: DebuggerLanguagePlugin[];
readonly #debuggerModelToData: Map<SDK.DebuggerModel.DebuggerModel, ModelData>;
readonly #rawModuleHandles: Map<string, {
rawModuleId: string,
plugin: DebuggerLanguagePlugin,
scripts: Array<SDK.Script.Script>,
addRawModulePromise: Promise<Array<Platform.DevToolsPath.UrlString>|{missingSymbolFiles: string[]}>,
}>;
private readonly callFrameByStopId: Map<StopId, SDK.DebuggerModel.CallFrame> = new Map();
private readonly stopIdByCallFrame: Map<SDK.DebuggerModel.CallFrame, StopId> = new Map();
private nextStopId: StopId = 0n;
constructor(
targetManager: SDK.TargetManager.TargetManager, workspace: Workspace.Workspace.WorkspaceImpl,
debuggerWorkspaceBinding: DebuggerWorkspaceBinding) {
this.#workspace = workspace;
this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
this.#plugins = [];
this.#debuggerModelToData = new Map();
targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this);
this.#rawModuleHandles = new Map();
}
private async evaluateOnCallFrame(
callFrame: SDK.DebuggerModel.CallFrame, options: SDK.RuntimeModel.EvaluationOptions): Promise<{
object: SDK.RemoteObject.RemoteObject,
exceptionDetails: Protocol.Runtime.ExceptionDetails|undefined,
}|{
error: string,
}|null> {
const {script} = callFrame;
const {expression, returnByValue, throwOnSideEffect} = options;
const {plugin} = await this.rawModuleIdAndPluginForScript(script);
if (!plugin) {
return null;
}
const location = getRawLocation(callFrame);
const sourceLocations = await plugin.rawLocationToSourceLocation(location);
if (sourceLocations.length === 0) {
return null;
}
if (returnByValue) {
return {error: 'Cannot return by value'};
}
if (throwOnSideEffect) {
return {error: 'Cannot guarantee side-effect freedom'};
}
try {
const object = await plugin.evaluate(expression, location, this.stopIdForCallFrame(callFrame));
if (object) {
return {object: new ExtensionRemoteObject(callFrame, object, plugin), exceptionDetails: undefined};
}
return {object: new SDK.RemoteObject.LocalJSONObject(undefined), exceptionDetails: undefined};
} catch (error) {
if (error instanceof FormattingError) {
const {exception: object, exceptionDetails} = error;
return {object, exceptionDetails};
}
const {exception: object, exceptionDetails} = FormattingError.makeLocal(callFrame, error.message);
return {object, exceptionDetails};
}
}
stopIdForCallFrame(callFrame: SDK.DebuggerModel.CallFrame): StopId {
let stopId = this.stopIdByCallFrame.get(callFrame);
if (stopId !== undefined) {
return stopId;
}
stopId = this.nextStopId++;
this.stopIdByCallFrame.set(callFrame, stopId);
this.callFrameByStopId.set(stopId, callFrame);
return stopId;
}
callFrameForStopId(stopId: StopId): SDK.DebuggerModel.CallFrame|undefined {
return this.callFrameByStopId.get(stopId);
}
private expandCallFrames(callFrames: SDK.DebuggerModel.CallFrame[]): Promise<SDK.DebuggerModel.CallFrame[]> {
return Promise
.all(callFrames.map(async callFrame => {
const functionInfo = await this.getFunctionInfo(callFrame.script, callFrame.location());
if (functionInfo) {
if ('frames' in functionInfo && functionInfo.frames.length) {
return functionInfo.frames.map(({name}, index) => callFrame.createVirtualCallFrame(index, name));
}
if ('missingSymbolFiles' in functionInfo && functionInfo.missingSymbolFiles.length) {
const resources = functionInfo.missingSymbolFiles;
const details = i18nString(UIStrings.debugSymbolsIncomplete, {PH1: callFrame.functionName});
callFrame.setMissingDebugInfoDetails({details, resources});
} else {
callFrame.setMissingDebugInfoDetails({
resources: [],
details: i18nString(UIStrings.failedToLoadDebugSymbolsForFunction, {PH1: callFrame.functionName}),
});
}
}
return callFrame;
}))
.then(callFrames => callFrames.flat());
}
modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
this.#debuggerModelToData.set(debuggerModel, new ModelData(debuggerModel, this.#workspace));
debuggerModel.addEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this);
debuggerModel.addEventListener(SDK.DebuggerModel.Events.ParsedScriptSource, this.parsedScriptSource, this);
debuggerModel.addEventListener(SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this);
debuggerModel.setEvaluateOnCallFrameCallback(this.evaluateOnCallFrame.bind(this));
debuggerModel.setExpandCallFramesCallback(this.expandCallFrames.bind(this));
}
modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
debuggerModel.removeEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this);
debuggerModel.removeEventListener(SDK.DebuggerModel.Events.ParsedScriptSource, this.parsedScriptSource, this);
debuggerModel.removeEventListener(SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this);
debuggerModel.setEvaluateOnCallFrameCallback(null);
debuggerModel.setExpandCallFramesCallback(null);
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (modelData) {
modelData.dispose();
this.#debuggerModelToData.delete(debuggerModel);
}
this.#rawModuleHandles.forEach((rawModuleHandle, rawModuleId) => {
const scripts = rawModuleHandle.scripts.filter(script => script.debuggerModel !== debuggerModel);
if (scripts.length === 0) {
rawModuleHandle.plugin.removeRawModule(rawModuleId).catch(error => {
Common.Console.Console.instance().error(
i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
});
this.#rawModuleHandles.delete(rawModuleId);
} else {
rawModuleHandle.scripts = scripts;
}
});
}
private globalObjectCleared(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void {
const debuggerModel = event.data;
this.modelRemoved(debuggerModel);
this.modelAdded(debuggerModel);
}
addPlugin(plugin: DebuggerLanguagePlugin): void {
this.#plugins.push(plugin);
for (const debuggerModel of this.#debuggerModelToData.keys()) {
for (const script of debuggerModel.scripts()) {
if (this.hasPluginForScript(script)) {
continue;
}
this.parsedScriptSource({data: script});
}
}
}
removePlugin(plugin: DebuggerLanguagePlugin): void {
this.#plugins = this.#plugins.filter(p => p !== plugin);
const scripts = new Set<SDK.Script.Script>();
this.#rawModuleHandles.forEach((rawModuleHandle, rawModuleId) => {
if (rawModuleHandle.plugin !== plugin) {
return;
}
rawModuleHandle.scripts.forEach(script => scripts.add(script));
this.#rawModuleHandles.delete(rawModuleId);
});
for (const script of scripts) {
const modelData = (this.#debuggerModelToData.get(script.debuggerModel) as ModelData);
modelData.removeScript(script);
// Let's see if we have another #plugin that's happy to
// take this orphaned script now. This is important to
// get right, since the same #plugin might race during
// unregister/register and we might already have the
// new instance of the #plugin added before we remove
// the previous instance.
this.parsedScriptSource({data: script});
}
}
hasPluginForScript(script: SDK.Script.Script): boolean {
const rawModuleId = rawModuleIdForScript(script);
const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
return rawModuleHandle !== undefined && rawModuleHandle.scripts.includes(script);
}
/**
* Returns the responsible language #plugin and the raw module ID for a script.
*
* This ensures that the `addRawModule` call finishes first such that the
* caller can immediately issue calls to the returned #plugin without the
* risk of racing with the `addRawModule` call. The returned #plugin will be
* set to undefined to indicate that there's no #plugin for the script.
*/
private async rawModuleIdAndPluginForScript(script: SDK.Script.Script): Promise<{
rawModuleId: string,
plugin: DebuggerLanguagePlugin|null,
}> {
const rawModuleId = rawModuleIdForScript(script);
const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
if (rawModuleHandle) {
await rawModuleHandle.addRawModulePromise;
if (rawModuleHandle === this.#rawModuleHandles.get(rawModuleId)) {
return {rawModuleId, plugin: rawModuleHandle.plugin};
}
}
return {rawModuleId, plugin: null};
}
uiSourceCodeForURL(debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString):
Workspace.UISourceCode.UISourceCode|null {
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (modelData) {
return modelData.getProject().uiSourceCodeForURL(url);
}
return null;
}
async rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location):
Promise<Workspace.UISourceCode.UILocation|null> {
const script = rawLocation.script();
if (!script) {
return null;
}
const {rawModuleId, plugin} = await this.rawModuleIdAndPluginForScript(script);
if (!plugin) {
return null;
}
const pluginLocation = {
rawModuleId,
// RawLocation.#columnNumber is the byte offset in the full raw wasm module. Plugins expect the offset in the code
// section, so subtract the offset of the code section in the module here.
codeOffset: rawLocation.columnNumber - (script.codeOffset() || 0),
inlineFrameIndex: rawLocation.inlineFrameIndex,
};
try {
const sourceLocations = await plugin.rawLocationToSourceLocation(pluginLocation);
for (const sourceLocation of sourceLocations) {
const uiSourceCode = this.uiSourceCodeForURL(
script.debuggerModel, sourceLocation.sourceFileURL as Platform.DevToolsPath.UrlString);
if (!uiSourceCode) {
continue;
}
// Absence of column information is indicated by the value `-1` in talking to language #plugins.
return uiSourceCode.uiLocation(
sourceLocation.lineNumber, sourceLocation.columnNumber >= 0 ? sourceLocation.columnNumber : undefined);
}
} catch (error) {
Common.Console.Console.instance().error(
i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
}
return null;
}
uiLocationToRawLocationRanges(
uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
columnNumber: number|undefined = -1): Promise<{
start: SDK.DebuggerModel.Location,
end: SDK.DebuggerModel.Location,
}[]|null> {
const locationPromises: Promise<{
start: SDK.DebuggerModel.Location,
end: SDK.DebuggerModel.Location,
}[]>[] = [];
this.scriptsForUISourceCode(uiSourceCode).forEach(script => {
const rawModuleId = rawModuleIdForScript(script);
const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
if (!rawModuleHandle) {
return;
}
const {plugin} = rawModuleHandle;
locationPromises.push(getLocations(rawModuleId, plugin, script));
});
if (locationPromises.length === 0) {
return Promise.resolve(null);
}
return Promise.all(locationPromises).then(locations => locations.flat()).catch(error => {
Common.Console.Console.instance().error(
i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
return null;
});
async function getLocations(
rawModuleId: string, plugin: DebuggerLanguagePlugin, script: SDK.Script.Script): Promise<{
start: SDK.DebuggerModel.Location,
end: SDK.DebuggerModel.Location,
}[]> {
const pluginLocation = {rawModuleId, sourceFileURL: uiSourceCode.url(), lineNumber, columnNumber};
const rawLocations = await plugin.sourceLocationToRawLocation(pluginLocation);
if (!rawLocations) {
return [];
}
return rawLocations.map(
m => ({
start: new SDK.DebuggerModel.Location(
script.debuggerModel, script.scriptId, 0, Number(m.startOffset) + (script.codeOffset() || 0)),
end: new SDK.DebuggerModel.Location(
script.debuggerModel, script.scriptId, 0, Number(m.endOffset) + (script.codeOffset() || 0)),
}));
}
}
async uiLocationToRawLocations(
uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
columnNumber?: number): Promise<SDK.DebuggerModel.Location[]|null> {
const locationRanges = await this.uiLocationToRawLocationRanges(uiSourceCode, lineNumber, columnNumber);
if (!locationRanges) {
return null;
}
return locationRanges.map(({start}) => start);
}
async uiLocationRangeToRawLocationRanges(
uiSourceCode: Workspace.UISourceCode.UISourceCode,
textRange: TextUtils.TextRange.TextRange): Promise<SDK.DebuggerModel.LocationRange[]|null> {
const locationRangesPromises = [];
for (let line = textRange.startLine; line <= textRange.endLine; ++line) {
locationRangesPromises.push(this.uiLocationToRawLocationRanges(uiSourceCode, line));
}
const ranges = [];
for (const locationRanges of await Promise.all(locationRangesPromises)) {
if (locationRanges === null) {
return null;
}
for (const range of locationRanges) {
const [startLocation, endLocation] = await Promise.all([
this.rawLocationToUILocation(range.start),
this.rawLocationToUILocation(range.end),
]);
if (startLocation === null || endLocation === null) {
continue;
}
// Report all ranges that somehow intersect with the `textRange`. It's the
// responsibility of the caller to filter / clamp these ranges appropriately.
const overlap = textRange.intersection(new TextUtils.TextRange.TextRange(
startLocation.lineNumber, startLocation.columnNumber ?? 0, endLocation.lineNumber,
endLocation.columnNumber ?? Infinity));
if (!overlap.isEmpty()) {
ranges.push(range);
}
}
}
return ranges;
}
scriptsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] {
for (const modelData of this.#debuggerModelToData.values()) {
const scripts = modelData.uiSourceCodeToScripts.get(uiSourceCode);
if (scripts) {
return scripts;
}
}
return [];
}
setDebugInfoURL(script: SDK.Script.Script, externalURL: Platform.DevToolsPath.UrlString): void {
if (this.hasPluginForScript(script)) {
return;
}
script.debugSymbols = {type: Protocol.Debugger.DebugSymbolsType.ExternalDWARF, externalURL};
this.parsedScriptSource({data: script});
void script.debuggerModel.setDebugInfoURL(script, externalURL);
}
private parsedScriptSource(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): void {
const script = event.data;
if (!script.sourceURL) {
return;
}
for (const plugin of this.#plugins) {
if (!plugin.handleScript(script)) {
continue;
}
const rawModuleId = rawModuleIdForScript(script);
let rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
if (!rawModuleHandle) {
const sourceFileURLsPromise =
(async(): Promise<Platform.DevToolsPath.UrlString[]|{missingSymbolFiles: string[]}> => {
const console = Common.Console.Console.instance();
const url = script.sourceURL;
const symbolsUrl = (script.debugSymbols && script.debugSymbols.externalURL) || '';
if (symbolsUrl) {
console.log(
i18nString(UIStrings.loadingDebugSymbolsForVia, {PH1: plugin.name, PH2: url, PH3: symbolsUrl}));
} else {
console.log(i18nString(UIStrings.loadingDebugSymbolsFor, {PH1: plugin.name, PH2: url}));
}
try {
const code = (!symbolsUrl && url.startsWith('wasm://')) ? await script.getWasmBytecode() : undefined;
const addModuleResult = await plugin.addRawModule(rawModuleId, symbolsUrl, {url, code});
// Check that the handle isn't stale by now. This works because the code that assigns to
// `rawModuleHandle` below will run before this code because of the `await` in the preceding
// line. This is primarily to avoid logging the message below, which would give the developer
// the misleading information that we're done, while in reality it was a stale call that finished.
if (rawModuleHandle !== this.#rawModuleHandles.get(rawModuleId)) {
return [];
}
if ('missingSymbolFiles' in addModuleResult) {
return {missingSymbolFiles: addModuleResult.missingSymbolFiles};
}
const sourceFileURLs = addModuleResult as Platform.DevToolsPath.UrlString[];
if (sourceFileURLs.length === 0) {
console.warn(i18nString(UIStrings.loadedDebugSymbolsForButDidnt, {PH1: plugin.name, PH2: url}));
} else {
console.log(i18nString(
UIStrings.loadedDebugSymbolsForFound, {PH1: plugin.name, PH2: url, PH3: sourceFileURLs.length}));
}
return sourceFileURLs;
} catch (error) {
console.error(i18nString(
UIStrings.failedToLoadDebugSymbolsFor, {PH1: plugin.name, PH2: url, PH3: error.message}));
this.#rawModuleHandles.delete(rawModuleId);
return [];
}
})();
rawModuleHandle = {rawModuleId, plugin, scripts: [script], addRawModulePromise: sourceFileURLsPromise};
this.#rawModuleHandles.set(rawModuleId, rawModuleHandle);
} else {
rawModuleHandle.scripts.push(script);
}
// Wait for the addRawModule call to finish and
// update the #project. It's important to check
// for the DebuggerModel again, which may disappear
// in the meantime...
void rawModuleHandle.addRawModulePromise.then(sourceFileURLs => {
if (!('missingSymbolFiles' in sourceFileURLs)) {
// The script might have disappeared meanwhile...
if (script.debuggerModel.scriptForId(script.scriptId) === script) {
const modelData = this.#debuggerModelToData.get(script.debuggerModel);
if (modelData) { // The DebuggerModel could have disappeared meanwhile...
modelData.addSourceFiles(script, sourceFileURLs);
void this.#debuggerWorkspaceBinding.updateLocations(script);
}
}
}
});
return;
}
}
private debuggerResumed(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void {
const resumedFrames =
Array.from(this.callFrameByStopId.values()).filter(callFrame => callFrame.debuggerModel === event.data);
for (const callFrame of resumedFrames) {
const stopId = this.stopIdByCallFrame.get(callFrame);
assertNotNullOrUndefined(stopId);
this.stopIdByCallFrame.delete(callFrame);
this.callFrameByStopId.delete(stopId);
}
}
getSourcesForScript(script: SDK.Script.Script):
Promise<Array<Platform.DevToolsPath.UrlString>|{missingSymbolFiles: string[]}|undefined> {
const rawModuleId = rawModuleIdForScript(script);
const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
if (rawModuleHandle) {
return rawModuleHandle.addRawModulePromise;
}
return Promise.resolve(undefined);
}
async resolveScopeChain(callFrame: SDK.DebuggerModel.CallFrame): Promise<SourceScope[]|null> {
const script = callFrame.script;
const {rawModuleId, plugin} = await this.rawModuleIdAndPluginForScript(script);
if (!plugin) {
return null;
}
const location = {
rawModuleId,
codeOffset: callFrame.location().columnNumber - (script.codeOffset() || 0),
inlineFrameIndex: callFrame.inlineFrameIndex,
};
const stopId = this.stopIdForCallFrame(callFrame);
try {
const sourceMapping = await plugin.rawLocationToSourceLocation(location);
if (sourceMapping.length === 0) {
return null;
}
const scopes = new Map<string, SourceScope>();
const variables = await plugin.listVariablesInScope(location);
for (const variable of variables || []) {
let scope = scopes.get(variable.scope);
if (!scope) {
const {type, typeName, icon} = await plugin.getScopeInfo(variable.scope);
scope = new SourceScope(callFrame, stopId, type, typeName, icon, plugin);
scopes.set(variable.scope, scope);
}
scope.object().variables.push(variable);
}
return Array.from(scopes.values());
} catch (error) {
Common.Console.Console.instance().error(
i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
return null;
}
}
async getFunctionInfo(script: SDK.Script.Script, location: SDK.DebuggerModel.Location):
Promise<{frames: Array<Chrome.DevTools.FunctionInfo>}|{missingSymbolFiles: string[]}|null> {
const {rawModuleId, plugin} = await this.rawModuleIdAndPluginForScript(script);
if (!plugin) {
return null;
}
const rawLocation: Chrome.DevTools.RawLocation = {
rawModuleId,
codeOffset: location.columnNumber - (script.codeOffset() || 0),
inlineFrameIndex: 0,
};
try {
const functionInfo = await plugin.getFunctionInfo(rawLocation);
return functionInfo;
} catch (error) {
Common.Console.Console.instance().warn(i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
return {frames: []};
}
}
async getInlinedFunctionRanges(rawLocation: SDK.DebuggerModel.Location): Promise<{
start: SDK.DebuggerModel.Location,
end: SDK.DebuggerModel.Location,
}[]> {
const script = rawLocation.script();
if (!script) {
return [];
}
const {rawModuleId, plugin} = await this.rawModuleIdAndPluginForScript(script);
if (!plugin) {
return [];
}
const pluginLocation = {
rawModuleId,
// RawLocation.#columnNumber is the byte offset in the full raw wasm module. Plugins expect the offset in the code
// section, so subtract the offset of the code section in the module here.
codeOffset: rawLocation.columnNumber - (script.codeOffset() || 0),
};
try {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// @ts-ignore
const locations = await plugin.getInlinedFunctionRanges(pluginLocation);
return locations.map(
m => ({
start: new SDK.DebuggerModel.Location(
script.debuggerModel, script.scriptId, 0, Number(m.startOffset) + (script.codeOffset() || 0)),
end: new SDK.DebuggerModel.Location(
script.debuggerModel, script.scriptId, 0, Number(m.endOffset) + (script.codeOffset() || 0)),
}));
} catch (error) {
Common.Console.Console.instance().warn(i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
return [];
}
}
async getInlinedCalleesRanges(rawLocation: SDK.DebuggerModel.Location): Promise<{
start: SDK.DebuggerModel.Location,
end: SDK.DebuggerModel.Location,
}[]> {
const script = rawLocation.script();
if (!script) {
return [];
}
const {rawModuleId, plugin} = await this.rawModuleIdAndPluginForScript(script);
if (!plugin) {
return [];
}
const pluginLocation = {
rawModuleId,
// RawLocation.#columnNumber is the byte offset in the full raw wasm module. Plugins expect the offset in the code
// section, so subtract the offset of the code section in the module here.
codeOffset: rawLocation.columnNumber - (script.codeOffset() || 0),
};
try {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// @ts-ignore
const locations = await plugin.getInlinedCalleesRanges(pluginLocation);
return locations.map(
m => ({
start: new SDK.DebuggerModel.Location(
script.debuggerModel, script.scriptId, 0, Number(m.startOffset) + (script.codeOffset() || 0)),
end: new SDK.DebuggerModel.Location(
script.debuggerModel, script.scriptId, 0, Number(m.endOffset) + (script.codeOffset() || 0)),
}));
} catch (error) {
Common.Console.Console.instance().warn(i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
return [];
}
}
async getMappedLines(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<Set<number>|null> {
const rawModuleIds =
await Promise.all(this.scriptsForUISourceCode(uiSourceCode).map(s => this.rawModuleIdAndPluginForScript(s)));
let mappedLines: Set<number>|null = null;
for (const {rawModuleId, plugin} of rawModuleIds) {
if (!plugin) {
continue;
}
const lines = await plugin.getMappedLines(rawModuleId, uiSourceCode.url());
if (lines === undefined) {
continue;
}
if (mappedLines === null) {
mappedLines = new Set(lines);
} else {
lines.forEach(l => (mappedLines as Set<number>).add(l));
}
}
return mappedLines;
}
}
class ModelData {
project: ContentProviderBasedProject;
readonly uiSourceCodeToScripts: Map<Workspace.UISourceCode.UISourceCode, SDK.Script.Script[]>;
constructor(debuggerModel: SDK.DebuggerModel.DebuggerModel, workspace: Workspace.Workspace.WorkspaceImpl) {
this.project = new ContentProviderBasedProject(
workspace, 'language_plugins::' + debuggerModel.target().id(), Workspace.Workspace.projectTypes.Network, '',
false /* isServiceProject */);
NetworkProject.setTargetForProject(this.project, debuggerModel.target());
this.uiSourceCodeToScripts = new Map();
}
addSourceFiles(script: SDK.Script.Script, urls: Platform.DevToolsPath.UrlString[]): void {
const initiator = script.createPageResourceLoadInitiator();
for (const url of urls) {
let uiSourceCode = this.project.uiSourceCodeForURL(url);
if (!uiSourceCode) {
uiSourceCode = this.project.createUISourceCode(url, Common.ResourceType.resourceTypes.SourceMapScript);
NetworkProject.setInitialFrameAttribution(uiSourceCode, script.frameId);
// Bind the uiSourceCode to the script first before we add the
// uiSourceCode to the #project and thereby notify the rest of
// the system about the new source file.
// https://crbug.com/1150295 is an example where the breakpoint
// resolution logic kicks in right after adding the uiSourceCode
// and at that point we already need to have the mapping in place
// otherwise we will not get the breakpoint right.
this.uiSourceCodeToScripts.set(uiSourceCode, [script]);
const contentProvider = new SDK.CompilerSourceMappingContentProvider.CompilerSourceMappingContentProvider(
url, Common.ResourceType.resourceTypes.SourceMapScript, initiator);
const mimeType = Common.ResourceType.ResourceType.mimeFromURL(url) || 'text/javascript';
this.project.addUISourceCodeWithProvider(uiSourceCode, contentProvider, null, mimeType);
} else {
// The same uiSourceCode can be provided by different scripts,
// but we don't expect that to happen frequently.
const scripts = (this.uiSourceCodeToScripts.get(uiSourceCode) as SDK.Script.Script[]);
if (!scripts.includes(script)) {
scripts.push(script);
}
}
}
}
removeScript(script: SDK.Script.Script): void {
this.uiSourceCodeToScripts.forEach((scripts, uiSourceCode) => {
scripts = scripts.filter(s => s !== script);
if (scripts.length === 0) {
this.uiSourceCodeToScripts.delete(uiSourceCode);
this.project.removeUISourceCode(uiSourceCode.url());
} else {
this.uiSourceCodeToScripts.set(uiSourceCode, scripts);
}
});
}
dispose(): void {
this.project.dispose();
}
getProject(): ContentProviderBasedProject {
return this.project;
}
}
export interface DebuggerLanguagePlugin extends Chrome.DevTools.LanguageExtensionPlugin {
name: string;
handleScript(script: SDK.Script.Script): boolean;
}