UNPKG

chrome-devtools-frontend

Version:
1,242 lines (1,106 loc) • 62.1 kB
// 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 * 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 Workspace from '../workspace/workspace.js'; import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; // eslint-disable-line rulesdir/es_modules_import import {ContentProviderBasedProject} from './ContentProviderBasedProject.js'; import type {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js'; import {NetworkProject} from './NetworkProject.js'; import {assertNotNullOrUndefined} from '../../core/platform/platform.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_); class SourceType { typeInfo: Chrome.DevTools.TypeInfo; members: SourceType[]; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any typeMap: Map<any, SourceType>; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(typeInfo: Chrome.DevTools.TypeInfo, members: SourceType[], typeMap: Map<any, SourceType>) { this.typeInfo = typeInfo; this.members = members; this.typeMap = typeMap; } /** * Create a type graph */ static create(typeInfos: Chrome.DevTools.TypeInfo[]): SourceType|null { if (typeInfos.length === 0) { return null; } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any const typeMap = new Map<any, SourceType>(); for (const typeInfo of typeInfos) { typeMap.set(typeInfo.typeId, new SourceType(typeInfo, [], typeMap)); } for (const sourceType of typeMap.values()) { sourceType.members = sourceType.typeInfo.members.map(({typeId}) => { const memberType = typeMap.get(typeId); if (!memberType) { throw new Error(`Incomplete type information for type ${typeInfos[0].typeNames[0] || '<anonymous>'}`); } return memberType; }); } return typeMap.get(typeInfos[0].typeId) || null; } } /** * 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, }; } async function resolveRemoteObject( // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any callFrame: SDK.DebuggerModel.CallFrame, object: SDK.RemoteObject.RemoteObject): Promise<any> { if (typeof object.value !== 'undefined') { return object.value; } const response = await callFrame.debuggerModel.target().runtimeAgent().invoke_callFunctionOn( {functionDeclaration: 'function() { return this; }', objectId: object.objectId, returnByValue: true}); const {result} = response; if (!result) { return undefined; } return result.value; } export class ValueNode extends SDK.RemoteObject.RemoteObjectImpl { inspectableAddress?: number; callFrame: SDK.DebuggerModel.CallFrame; constructor( callFrame: SDK.DebuggerModel.CallFrame, objectId: Protocol.Runtime.RemoteObjectId|undefined, type: string, subtype: string|undefined, // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any, inspectableAddress?: number, unserializableValue?: string, description?: string, preview?: Protocol.Runtime.ObjectPreview, customPreview?: Protocol.Runtime.CustomPreview, className?: string) { super( callFrame.debuggerModel.runtimeModel(), objectId, type, subtype, value, unserializableValue, description, preview, customPreview, className); this.inspectableAddress = inspectableAddress; this.callFrame = callFrame; } get sourceType(): SourceType { throw new Error('Not Implemented'); } } // Debugger language #plugins present source-language values as trees with mixed dynamic and static structural // information. The static structure is defined by the variable's static type in the source language. Formatters are // able to present source-language values in an arbitrary user-friendly way, which contributes the dynamic structural // information. The classes StaticallyTypedValue and FormatedValueNode respectively implement the static and dynamic // parts in the RemoteObject tree that defines the presentation of the source-language value in the debugger UI. // // struct S { // int i; // struct A { // int j; // } a[3]; // } s // // The RemoteObject tree representing the C struct above could look like the graph below with a formatter for the type // struct A[3], interleaving static and dynamic representations: // // StaticallyTypedValueNode --> s: struct S // \ // |\ // StaticallyTypedValueNode --> | i: int // \ // \ // StaticallyTypedValueNode --> a: struct A[3] // \ // |\ // FormattedValueNode --> | 0: struct A // | \ // | \ // StaticallyTypedValueNode --> | j: int // . // . // . /** Create a new value tree from an expression. */ async function getValueTreeForExpression( callFrame: SDK.DebuggerModel.CallFrame, plugin: DebuggerLanguagePlugin, expression: string, evalOptions: SDK.RuntimeModel.EvaluationOptions): Promise<SDK.RemoteObject.RemoteObject> { const location = getRawLocation(callFrame); let typeInfo; try { typeInfo = await plugin.getTypeInfo(expression, location); } catch (e) { throw FormattingError.makeLocal(callFrame, e.message); } // If there's no type information we cannot represent this expression. if (!typeInfo) { return new SDK.RemoteObject.LocalJSONObject(undefined); } const {base, typeInfos} = typeInfo; const sourceType = SourceType.create(typeInfos); if (!sourceType) { return new SDK.RemoteObject.LocalJSONObject(undefined); } if (sourceType.typeInfo.hasValue && !sourceType.typeInfo.canExpand && base) { // Need to run the formatter for the expression result. return formatSourceValue(callFrame, plugin, sourceType, base, [], evalOptions); } // Create a new value tree with static information for the root. const address = await StaticallyTypedValueNode.getInspectableAddress(callFrame, plugin, base, [], evalOptions); return new StaticallyTypedValueNode(callFrame, plugin, sourceType, base, [], evalOptions, address); } /** Run the formatter for the value defined by the pair of #base and #fieldChain. */ async function formatSourceValue( callFrame: SDK.DebuggerModel.CallFrame, plugin: DebuggerLanguagePlugin, sourceType: SourceType, base: Chrome.DevTools.EvalBase, field: Chrome.DevTools.FieldInfo[], evalOptions: SDK.RuntimeModel.EvaluationOptions): Promise<FormattedValueNode> { const location = getRawLocation(callFrame); let evalCode: { js: string, }|({ js: string, } | null) = await plugin.getFormatter({base, field}, location); if (!evalCode) { evalCode = {js: ''}; } const response = await callFrame.debuggerModel.target().debuggerAgent().invoke_evaluateOnCallFrame({ callFrameId: callFrame.id, expression: evalCode.js, objectGroup: evalOptions.objectGroup, includeCommandLineAPI: evalOptions.includeCommandLineAPI, silent: evalOptions.silent, returnByValue: evalOptions.returnByValue, generatePreview: evalOptions.generatePreview, throwOnSideEffect: evalOptions.throwOnSideEffect, timeout: evalOptions.timeout, }); const error = response.getError(); if (error) { throw new Error(error); } const {result, exceptionDetails} = response; if (exceptionDetails) { throw new FormattingError(callFrame.debuggerModel.runtimeModel().createRemoteObject(result), exceptionDetails); } // Wrap the formatted result into a FormattedValueNode. const object = new FormattedValueNode(callFrame, sourceType, plugin, result, null, evalOptions, undefined); // Check whether the formatter returned a plain object or and object alongisde a formatter tag. const unpackedResultObject = await unpackResultObject(object); const node = unpackedResultObject || object; if (typeof node.value === 'undefined' && node.type !== 'undefined') { node.description = sourceType.typeInfo.typeNames[0]; } return node; async function unpackResultObject(object: FormattedValueNode): Promise<FormattedValueNode|null> { const {tag, value, inspectableAddress, description} = await object.findProperties('tag', 'value', 'inspectableAddress', 'description'); if (!tag || !value) { return null; } const {className, symbol} = await tag.findProperties('className', 'symbol'); if (!className || !symbol) { return null; } const resolvedClassName = className.value; if (typeof resolvedClassName !== 'string' || typeof symbol.objectId === 'undefined') { return null; } const descriptionText = description?.value; if (typeof descriptionText === 'string') { value.description = descriptionText; } value.formatterTag = {symbol: symbol.objectId, className: resolvedClassName}; value.inspectableAddress = inspectableAddress ? inspectableAddress.value : undefined; return value; } } // Formatters produce proper JavaScript objects, which are mirrored as RemoteObjects. To implement interleaving of // formatted and statically typed values, formatters may insert markers in the JavaScript objects. The markers contain // the static type information (`EvalBase`)to create a new StaticallyTypedValueNode tree root. Markers are identified by // their className and the presence of a special Symbol property. Both the class name and the symbol are defined by the // `formatterTag` property. // // A FormattedValueNode is a RemoteObject whose properties can be either FormattedValueNodes or // StaticallyTypedValueNodes. The class hooks into the creation of RemoteObjects for properties to check whether a // property is a marker. class FormattedValueNode extends ValueNode { readonly #plugin: DebuggerLanguagePlugin; readonly #sourceType: SourceType; formatterTag: { className: string, symbol: string, }|null; readonly #evalOptions: SDK.RuntimeModel.EvaluationOptions; constructor( callFrame: SDK.DebuggerModel.CallFrame, sourceType: SourceType, plugin: DebuggerLanguagePlugin, object: Protocol.Runtime.RemoteObject, formatterTag: { className: string, symbol: string, }|null, evalOptions: SDK.RuntimeModel.EvaluationOptions, inspectableAddress: number|undefined) { super( callFrame, object.objectId, object.type, object.subtype, object.value, inspectableAddress, object.unserializableValue, object.description, object.preview, object.customPreview, object.className); this.#plugin = plugin; this.#sourceType = sourceType; // The tag describes how to identify a marker by its className and its identifier symbol's object id. this.formatterTag = formatterTag; this.#evalOptions = evalOptions; } async findProperties(...properties: string[]): Promise<{ [x: string]: FormattedValueNode | undefined, }> { const result: { [x: string]: FormattedValueNode|undefined, } = {}; for (const prop of (await this.getOwnProperties(false)).properties || []) { if (properties.indexOf(prop.name) >= 0) { if (prop.value) { result[prop.name] = (prop.value as FormattedValueNode | undefined); } } } return result; } /** * Hook into RemoteObject creation for properties to check whether a property is a marker. */ async createRemoteObject(newObject: Protocol.Runtime.RemoteObject): Promise<FormattedValueNode|StaticallyTypedValueNode> { // Check if the property RemoteObject is a marker const base = await this.getEvalBaseFromObject(newObject); if (!base) { return new FormattedValueNode( this.callFrame, this.#sourceType, this.#plugin, newObject, this.formatterTag, this.#evalOptions, undefined); } // Property is a marker, check if it's just static type information or if we need to run formatters for the value. const newSourceType = this.#sourceType.typeMap.get(base.rootType.typeId); if (!newSourceType) { throw new Error('Unknown typeId in eval base'); } // The marker refers to a value that needs to be formatted, so run the formatter. if (base.rootType.hasValue && !base.rootType.canExpand && base) { return formatSourceValue(this.callFrame, this.#plugin, newSourceType, base, [], this.#evalOptions); } // The marker is just static information, so start a new subtree with a static type info root. const address = await StaticallyTypedValueNode.getInspectableAddress(this.callFrame, this.#plugin, base, [], this.#evalOptions); return new StaticallyTypedValueNode( this.callFrame, this.#plugin, newSourceType, base, [], this.#evalOptions, address); } /** * Check whether an object is a marker and if so return the EvalBase it contains. */ private async getEvalBaseFromObject(object: Protocol.Runtime.RemoteObject): Promise<Chrome.DevTools.EvalBase|null> { const {objectId} = object; if (!object || !this.formatterTag) { return null; } // A marker is definitively identified by the symbol property. To avoid checking the properties of all objects, // check the className first for an early exit. const {className, symbol} = this.formatterTag; if (className !== object.className) { return null; } const response = await this.debuggerModel().target().runtimeAgent().invoke_callFunctionOn({ functionDeclaration: 'function(sym) { return this[sym]; }', objectId, arguments: [{objectId: symbol as Protocol.Runtime.RemoteObjectId}], }); const {result} = response; if (!result || result.type === 'undefined') { return null; } // The object is a marker, so pull the static type information from its symbol property. The symbol property is not // a formatted value per se, but we wrap it as one to be able to call `findProperties`. const baseObject = new FormattedValueNode( this.callFrame, this.#sourceType, this.#plugin, result, null, this.#evalOptions, undefined); const {payload, rootType} = await baseObject.findProperties('payload', 'rootType'); if (typeof payload === 'undefined' || typeof rootType === 'undefined') { return null; } const value = await resolveRemoteObject(this.callFrame, payload); const {typeId} = await rootType.findProperties('typeId'); if (typeof value === 'undefined' || typeof typeId === 'undefined') { return null; } const newSourceType = this.#sourceType.typeMap.get(typeId.value); if (!newSourceType) { return null; } return {payload: value, rootType: newSourceType.typeInfo}; } } 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); } } // This class implements a `RemoteObject` for source language value whose immediate properties are defined purely by // static type information. Static type information is expressed by an `EvalBase` together with a `#fieldChain`. The // latter is necessary to express navigating through type members. We don't know how to make sense of an `EvalBase`'s // payload here, which is why member navigation is relayed to the formatter via the `#fieldChain`. class StaticallyTypedValueNode extends ValueNode { readonly #variableType: string; readonly #plugin: DebuggerLanguagePlugin; readonly #sourceType: SourceType; readonly #base: Chrome.DevTools.EvalBase|null; readonly #fieldChain: Chrome.DevTools.FieldInfo[]; readonly #evalOptions: SDK.RuntimeModel.EvaluationOptions; constructor( callFrame: SDK.DebuggerModel.CallFrame, plugin: DebuggerLanguagePlugin, sourceType: SourceType, base: Chrome.DevTools.EvalBase|null, fieldChain: Chrome.DevTools.FieldInfo[], evalOptions: SDK.RuntimeModel.EvaluationOptions, inspectableAddress: number|undefined) { const typeName = sourceType.typeInfo.typeNames[0] || '<anonymous>'; const variableType = 'object'; super( callFrame, /* objectId=*/ undefined, /* type=*/ variableType, /* subtype=*/ undefined, /* value=*/ null, inspectableAddress, /* unserializableValue=*/ undefined, /* description=*/ typeName, /* preview=*/ undefined, /* customPreview=*/ undefined, /* className=*/ typeName); this.#variableType = variableType; this.#plugin = plugin; this.#sourceType = sourceType; this.#base = base; this.#fieldChain = fieldChain; this.hasChildrenInternal = true; this.#evalOptions = evalOptions; } get type(): string { return this.#variableType; } get sourceType(): SourceType { return this.#sourceType; } private async expandMember(sourceType: SourceType, fieldInfo: Chrome.DevTools.FieldInfo): Promise<SDK.RemoteObject.RemoteObject> { const fieldChain = this.#fieldChain.concat(fieldInfo); if (sourceType.typeInfo.hasValue && !sourceType.typeInfo.canExpand && this.#base) { return formatSourceValue(this.callFrame, this.#plugin, sourceType, this.#base, fieldChain, this.#evalOptions); } const address = this.inspectableAddress !== undefined ? this.inspectableAddress + fieldInfo.offset : undefined; return new StaticallyTypedValueNode( this.callFrame, this.#plugin, sourceType, this.#base, fieldChain, this.#evalOptions, address); } static async getInspectableAddress( callFrame: SDK.DebuggerModel.CallFrame, plugin: DebuggerLanguagePlugin, base: Chrome.DevTools.EvalBase|null, field: Chrome.DevTools.FieldInfo[], evalOptions: SDK.RuntimeModel.EvaluationOptions): Promise<number|undefined> { if (!base) { return undefined; } const addressCode = await plugin.getInspectableAddress({base, field}); if (!addressCode.js) { return undefined; } const response = await callFrame.debuggerModel.target().debuggerAgent().invoke_evaluateOnCallFrame({ callFrameId: callFrame.id, expression: addressCode.js, objectGroup: evalOptions.objectGroup, includeCommandLineAPI: evalOptions.includeCommandLineAPI, silent: evalOptions.silent, returnByValue: true, generatePreview: evalOptions.generatePreview, throwOnSideEffect: evalOptions.throwOnSideEffect, timeout: evalOptions.timeout, }); const error = response.getError(); if (error) { throw new Error(error); } const {result, exceptionDetails} = response; if (exceptionDetails) { throw new FormattingError(callFrame.debuggerModel.runtimeModel().createRemoteObject(result), exceptionDetails); } const address = result.value; if (!Number.isSafeInteger(address) || address < 0) { console.error(`Inspectable address is not a positive, safe integer: ${address}`); return undefined; } return address; } async doGetProperties(_ownProperties: boolean, accessorPropertiesOnly: boolean, _generatePreview: boolean): Promise<SDK.RemoteObject.GetPropertiesResult> { const {typeInfo} = this.#sourceType; if (accessorPropertiesOnly || !typeInfo.canExpand) { return {properties: [], internalProperties: []} as SDK.RemoteObject.GetPropertiesResult; } if (typeInfo.members.length > 0) { // This value doesn't have a formatter, but we can eagerly expand arrays in the frontend if the size is known. if (typeInfo.arraySize > 0) { const {typeId} = this.#sourceType.typeInfo.members[0]; const properties: SDK.RemoteObject.RemoteObjectProperty[] = []; const elementTypeInfo = this.#sourceType.members[0]; for (let i = 0; i < typeInfo.arraySize; ++i) { const name = `${i}`; const elementField = {name, typeId, offset: elementTypeInfo.typeInfo.size * i}; properties.push(new SDK.RemoteObject.RemoteObjectProperty( name, await this.expandMember(elementTypeInfo, elementField), /* enumerable=*/ false, /* writable=*/ false, /* isOwn=*/ true, /* wasThrown=*/ false)); } return {properties, internalProperties: []} as SDK.RemoteObject.GetPropertiesResult; } // The node is expanded, just make remote objects for its members const members = Promise.all(this.#sourceType.members.map(async (memberTypeInfo, idx) => { const fieldInfo = this.#sourceType.typeInfo.members[idx]; const propertyObject = await this.expandMember(memberTypeInfo, fieldInfo); const name = fieldInfo.name || ''; return new SDK.RemoteObject.RemoteObjectProperty( name, propertyObject, /* enumerable=*/ false, /* writable=*/ false, /* isOwn=*/ true, /* wasThrown=*/ false); })); return {properties: await members, internalProperties: []} as SDK.RemoteObject.GetPropertiesResult; } return {properties: [], internalProperties: []} as SDK.RemoteObject.GetPropertiesResult; } } 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); } get description(): string { return this.type; } 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; } 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; try { sourceVar = await getValueTreeForExpression(this.#callFrame, this.#plugin, variable.name, ({ generatePreview: false, includeCommandLineAPI: true, objectGroup: 'backtrace', returnByValue: false, silent: false, } as SDK.RuntimeModel.EvaluationOptions)); } 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) { 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 objectId(): Protocol.Runtime.RemoteObjectId|undefined { return this.extensionObject.objectId as Protocol.Runtime.RemoteObjectId; } get type(): string { if (this.extensionObject.type === 'array' || this.extensionObject.type === 'null') { return 'object'; } return this.extensionObject.type; } get subtype(): string|undefined { if (this.extensionObject.type === 'array' || this.extensionObject.type === 'null') { return this.extensionObject.type; } return undefined; } get value(): unknown { return this.extensionObject.value; } unserializableValue(): string|undefined { return undefined; } get description(): string|undefined { return this.extensionObject.description; } set description(description: string|undefined) { } get hasChildren(): boolean { return this.extensionObject.hasChildren; } get preview(): Protocol.Runtime.ObjectPreview|undefined { return undefined; } get className(): string|null { return this.extensionObject.className ?? null; } arrayLength(): number { return 0; } arrayBufferByteLength(): number { return 0; } getOwnProperties(_generatePreview: boolean, _nonIndexedPropertiesOnly?: boolean): Promise<SDK.RemoteObject.GetPropertiesResult> { return this.getAllProperties(false, _generatePreview, _nonIndexedPropertiesOnly); } 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}; } release(): void { const {objectId} = this.extensionObject; if (objectId) { assertNotNullOrUndefined(this.plugin.releaseObject); void this.plugin.releaseObject(objectId); } } debuggerModel(): SDK.DebuggerModel.DebuggerModel { return this.callFrame.debuggerModel; } 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} = 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; } try { const object = await getValueTreeForExpression(callFrame, plugin, expression, options); return {object, 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); } 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)) { return; } 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