UNPKG

chrome-devtools-frontend

Version:
614 lines (560 loc) • 25.1 kB
// Copyright 2023 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 './Formatters.js'; import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; import * as Formatters from './CustomFormatters.js'; import { DEFAULT_MODULE_CONFIGURATIONS, findModuleConfiguration, type ModuleConfigurations, resolveSourcePathToURL, } from './ModuleConfiguration.js'; import type * as SymbolsBackend from './SymbolsBackend.js'; import createSymbolsBackend from './SymbolsBackend.js'; import type {HostInterface} from './WorkerRPC.js'; function mapVector<T, ApiT>(vector: SymbolsBackend.Vector<ApiT>, callback: (apiElement: ApiT) => T): T[] { const elements: T[] = []; for (let i = 0; i < vector.size(); ++i) { const element = vector.get(i); elements.push(callback(element)); } return elements; } interface ScopeInfo { type: 'GLOBAL'|'LOCAL'|'PARAMETER'; typeName: string; icon?: string; } type LazyFSNode = FS.FSNode&{contents: {cacheLength: () => void, length: number}}; function mapEnumerator(apiEnumerator: SymbolsBackend.Enumerator): Formatters.Enumerator { return {typeId: apiEnumerator.typeId, value: apiEnumerator.value, name: apiEnumerator.name}; } function mapFieldInfo(apiFieldInfo: SymbolsBackend.FieldInfo): Formatters.FieldInfo { return {typeId: apiFieldInfo.typeId, offset: apiFieldInfo.offset, name: apiFieldInfo.name}; } class ModuleInfo { readonly fileNameToUrl = new Map<string, string>(); readonly urlToFileName = new Map<string, string>(); readonly dwarfSymbolsPlugin: SymbolsBackend.DWARFSymbolsPlugin; constructor( readonly symbolsUrl: string, readonly symbolsFileName: string, readonly symbolsDwpFileName: string|undefined, readonly backend: SymbolsBackend.Module) { this.dwarfSymbolsPlugin = new backend.DWARFSymbolsPlugin(); } stringifyScope(scope: SymbolsBackend.VariableScope): 'GLOBAL'|'LOCAL'|'PARAMETER' { switch (scope) { case this.backend.VariableScope.GLOBAL: return 'GLOBAL'; case this.backend.VariableScope.LOCAL: return 'LOCAL'; case this.backend.VariableScope.PARAMETER: return 'PARAMETER'; } throw new Error(`InternalError: Invalid scope ${scope}`); } stringifyErrorCode(errorCode: SymbolsBackend.ErrorCode): string { switch (errorCode) { case this.backend.ErrorCode.PROTOCOL_ERROR: return 'ProtocolError:'; case this.backend.ErrorCode.MODULE_NOT_FOUND_ERROR: return 'ModuleNotFoundError:'; case this.backend.ErrorCode.INTERNAL_ERROR: return 'InternalError'; case this.backend.ErrorCode.EVAL_ERROR: return 'EvalError'; } throw new Error(`InternalError: Invalid error code ${errorCode}`); } } export function createEmbindPool(): { flush(): void, manage<T extends SymbolsBackend.EmbindObject|undefined>(object: T): T, unmanage<T extends SymbolsBackend.EmbindObject>(object: T): boolean, } { class EmbindObjectPool { private objectPool: SymbolsBackend.EmbindObject[] = []; flush(): void { for (const object of this.objectPool.reverse()) { object.delete(); } this.objectPool = []; } manage<T extends SymbolsBackend.EmbindObject|undefined>(object: T): T { if (typeof object !== 'undefined') { this.objectPool.push(object); } return object; } unmanage<T extends SymbolsBackend.EmbindObject>(object: T): boolean { const index = this.objectPool.indexOf(object); if (index > -1) { this.objectPool.splice(index, 1); object.delete(); return true; } return false; } } const pool = new EmbindObjectPool(); const manage = pool.manage.bind(pool); const unmanage = pool.unmanage.bind(pool); const flush = pool.flush.bind(pool); return {manage, unmanage, flush}; } // Cache the underlying WebAssembly module after the first instantiation // so that subsequent calls to `createSymbolsBackend()` are faster, which // greatly speeds up the test suite. let symbolsBackendModulePromise: undefined|Promise<WebAssembly.Module>; function instantiateWasm( imports: WebAssembly.Imports, callback: (module: WebAssembly.Module) => void, resourceLoader: ResourceLoader, ): Emscripten.WebAssemblyExports { if (!symbolsBackendModulePromise) { symbolsBackendModulePromise = resourceLoader.createSymbolsBackendModulePromise(); } symbolsBackendModulePromise.then(module => WebAssembly.instantiate(module, imports)) .then(callback) .catch(console.error); return []; } export type RawModule = Chrome.DevTools.RawModule&{dwp?: ArrayBuffer}; export interface ResourceLoader { loadSymbols(rawModuleId: string, rawModule: RawModule, url: URL, filesystem: typeof FS, hostInterface: HostInterface): Promise<{symbolsFileName: string, symbolsDwpFileName?: string}>; createSymbolsBackendModulePromise(): Promise<WebAssembly.Module>; possiblyMissingSymbols?: string[]; } export class DWARFLanguageExtensionPlugin implements Chrome.DevTools.LanguageExtensionPlugin { private moduleInfos = new Map<string, Promise<ModuleInfo|undefined>>(); private lazyObjects = new Formatters.LazyObjectStore(); constructor( readonly moduleConfigurations: ModuleConfigurations, readonly resourceLoader: ResourceLoader, readonly hostInterface: HostInterface) { this.moduleConfigurations = moduleConfigurations; } private async newModuleInfo(rawModuleId: string, symbolsHint: string, rawModule: RawModule): Promise<ModuleInfo> { const {flush, manage} = createEmbindPool(); try { const rawModuleURL = new URL(rawModule.url); const {pathSubstitutions} = findModuleConfiguration(this.moduleConfigurations, rawModuleURL); const symbolsURL = symbolsHint ? resolveSourcePathToURL([], symbolsHint, rawModuleURL) : rawModuleURL; const instantiateWasmWrapper = (imports: Emscripten.WebAssemblyImports, callback: (module: WebAssembly.Module) => void): Emscripten.WebAssemblyExports => { // Emscripten type definitions are incorrect, we're getting passed a WebAssembly.Imports object here. return instantiateWasm(imports as unknown as WebAssembly.Imports, callback, this.resourceLoader); }; const backend = await createSymbolsBackend({instantiateWasm: instantiateWasmWrapper}); const {symbolsFileName, symbolsDwpFileName} = await this.resourceLoader.loadSymbols(rawModuleId, rawModule, symbolsURL, backend.FS, this.hostInterface); const moduleInfo = new ModuleInfo(symbolsURL.href, symbolsFileName, symbolsDwpFileName, backend); const addRawModuleResponse = manage(moduleInfo.dwarfSymbolsPlugin.AddRawModule(rawModuleId, symbolsFileName)); mapVector(manage(addRawModuleResponse.sources), fileName => { const fileURL = resolveSourcePathToURL(pathSubstitutions, fileName, symbolsURL); moduleInfo.fileNameToUrl.set(fileName, fileURL.href); moduleInfo.urlToFileName.set(fileURL.href, fileName); }); // Set up lazy dwo files if we are running on a worker if (typeof global === 'undefined' && typeof importScripts === 'function' && typeof XMLHttpRequest !== 'undefined') { mapVector(manage(addRawModuleResponse.dwos), dwoFile => { const absolutePath = dwoFile.startsWith('/') ? dwoFile : '/' + dwoFile; const pathSplit = absolutePath.split('/'); const fileName = pathSplit.pop() as string; const parentDirectory = pathSplit.join('/'); // Sometimes these stick around. try { backend.FS.unlink(absolutePath); } catch { } // Ensure directory exists if (parentDirectory.length > 1) { // TypeScript doesn't know about createPath // @ts-expect-error doesn't exit on types backend.FS.createPath('/', parentDirectory.substring(1), true, true); } const dwoURL = new URL(dwoFile, symbolsURL).href; const node = backend.FS.createLazyFile(parentDirectory, fileName, dwoURL, true, false) as LazyFSNode; const cacheLength = node.contents.cacheLength; const wrapper = (): void => { try { cacheLength.apply(node.contents); void this.hostInterface.reportResourceLoad(dwoURL, {success: true, size: node.contents.length}); } catch (e) { void this.hostInterface.reportResourceLoad(dwoURL, {success: false, errorMessage: (e as Error).message}); // Rethrow any error fetching the content as errno 44 (EEXIST) // TypeScript doesn't know about the ErrnoError constructor // @ts-expect-error doesn't exit on types throw new backend.FS.ErrnoError(44); } }; node.contents.cacheLength = wrapper; }); } return moduleInfo; } finally { flush(); } } async addRawModule(rawModuleId: string, symbolsUrl: string, rawModule: RawModule): Promise<string[]> { // This complex logic makes sure that addRawModule / removeRawModule calls are // handled sequentially for the same rawModuleId, and thus this looks symmetrical // to the removeRawModule() method below. The idea is that we chain our operation // on any previous operation for the same rawModuleId, and thereby end up with a // single sequence of events. const originalPromise = Promise.resolve(this.moduleInfos.get(rawModuleId)); const moduleInfoPromise = originalPromise.then(moduleInfo => { if (moduleInfo) { throw new Error(`InternalError: Duplicate module with ID '${rawModuleId}'`); } return this.newModuleInfo(rawModuleId, symbolsUrl, rawModule); }); // This looks a bit odd, but it's important that the operation is chained via // the `_moduleInfos` map *and* at the same time resolves to it's original // value in case of an error (i.e. if someone tried to add the same rawModuleId // twice, this will retain the original value in that case instead of having all // users get the internal error). this.moduleInfos.set(rawModuleId, moduleInfoPromise.catch(() => originalPromise)); const moduleInfo = await moduleInfoPromise; return [...moduleInfo.urlToFileName.keys()]; } private async getModuleInfo(rawModuleId: string): Promise<ModuleInfo> { const moduleInfo = await this.moduleInfos.get(rawModuleId); if (!moduleInfo) { throw new Error(`InternalError: Unknown module with raw module ID ${rawModuleId}`); } return moduleInfo; } async removeRawModule(rawModuleId: string): Promise<void> { const originalPromise = Promise.resolve(this.moduleInfos.get(rawModuleId)); const moduleInfoPromise = originalPromise.then(moduleInfo => { if (!moduleInfo) { throw new Error(`InternalError: No module with ID '${rawModuleId}'`); } return undefined; }); this.moduleInfos.set(rawModuleId, moduleInfoPromise.catch(() => originalPromise)); await moduleInfoPromise; } async sourceLocationToRawLocation(sourceLocation: Chrome.DevTools.SourceLocation): Promise<Chrome.DevTools.RawLocationRange[]> { const {flush, manage} = createEmbindPool(); const moduleInfo = await this.getModuleInfo(sourceLocation.rawModuleId); const sourceFile = moduleInfo.urlToFileName.get(sourceLocation.sourceFileURL); if (!sourceFile) { throw new Error(`InternalError: Unknown URL ${sourceLocation.sourceFileURL}`); } try { const rawLocations = manage(moduleInfo.dwarfSymbolsPlugin.SourceLocationToRawLocation( sourceLocation.rawModuleId, sourceFile, sourceLocation.lineNumber, sourceLocation.columnNumber)); const error = manage(rawLocations.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { const {rawModuleId, startOffset, endOffset} = manage(rawLocation); return {rawModuleId, startOffset, endOffset}; }); return locations; } finally { flush(); } } async rawLocationToSourceLocation(rawLocation: Chrome.DevTools.RawLocation): Promise<Chrome.DevTools.SourceLocation[]> { const {flush, manage} = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const sourceLocations = moduleInfo.dwarfSymbolsPlugin.RawLocationToSourceLocation( rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0); const error = manage(sourceLocations.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const locations = mapVector(manage(sourceLocations.sourceLocation), sourceLocation => { const sourceFileURL = moduleInfo.fileNameToUrl.get(sourceLocation.sourceFile); if (!sourceFileURL) { throw new Error(`InternalError: Unknown source file ${sourceLocation.sourceFile}`); } const {rawModuleId, lineNumber, columnNumber} = manage(sourceLocation); return { rawModuleId, sourceFileURL, lineNumber, columnNumber, }; }); return locations; } finally { flush(); } } async getScopeInfo(type: string): Promise<ScopeInfo> { switch (type) { case 'GLOBAL': return { type, typeName: 'Global', icon: 'data:null', }; case 'LOCAL': return { type, typeName: 'Local', icon: 'data:null', }; case 'PARAMETER': return { type, typeName: 'Parameter', icon: 'data:null', }; } throw new Error(`InternalError: Invalid scope type '${type}`); } async listVariablesInScope(rawLocation: Chrome.DevTools.RawLocation): Promise<Chrome.DevTools.Variable[]> { const {flush, manage} = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const variables = manage(moduleInfo.dwarfSymbolsPlugin.ListVariablesInScope( rawLocation.rawModuleId, rawLocation.codeOffset, rawLocation.inlineFrameIndex || 0)); const error = manage(variables.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const apiVariables = mapVector(manage(variables.variable), variable => { const {scope, name, type} = manage(variable); return {scope: moduleInfo.stringifyScope(scope), name, type, nestedName: name.split('::')}; }); return apiVariables; } finally { flush(); } } async getFunctionInfo(rawLocation: Chrome.DevTools.RawLocation): Promise<{frames: Chrome.DevTools.FunctionInfo[], missingSymbolFiles: string[]}> { const {flush, manage} = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const functionInfo = manage(moduleInfo.dwarfSymbolsPlugin.GetFunctionInfo(rawLocation.rawModuleId, rawLocation.codeOffset)); const error = manage(functionInfo.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const apiFunctionInfos = mapVector(manage(functionInfo.functionNames), functionName => { return {name: functionName}; }); let apiMissingSymbolFiles = mapVector(manage(functionInfo.missingSymbolFiles), x => x); if (apiMissingSymbolFiles.length && this.resourceLoader.possiblyMissingSymbols) { apiMissingSymbolFiles = apiMissingSymbolFiles.concat(this.resourceLoader.possiblyMissingSymbols); } return { frames: apiFunctionInfos, missingSymbolFiles: apiMissingSymbolFiles.map(x => new URL(x, moduleInfo.symbolsUrl).href) }; } finally { flush(); } } async getInlinedFunctionRanges(rawLocation: Chrome.DevTools.RawLocation): Promise<Chrome.DevTools.RawLocationRange[]> { const {flush, manage} = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const rawLocations = manage( moduleInfo.dwarfSymbolsPlugin.GetInlinedFunctionRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); const error = manage(rawLocations.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { const {rawModuleId, startOffset, endOffset} = manage(rawLocation); return {rawModuleId, startOffset, endOffset}; }); return locations; } finally { flush(); } } async getInlinedCalleesRanges(rawLocation: Chrome.DevTools.RawLocation): Promise<Chrome.DevTools.RawLocationRange[]> { const {flush, manage} = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawLocation.rawModuleId); try { const rawLocations = manage( moduleInfo.dwarfSymbolsPlugin.GetInlinedCalleesRanges(rawLocation.rawModuleId, rawLocation.codeOffset)); const error = manage(rawLocations.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const locations = mapVector(manage(rawLocations.rawLocationRanges), rawLocation => { const {rawModuleId, startOffset, endOffset} = manage(rawLocation); return {rawModuleId, startOffset, endOffset}; }); return locations; } finally { flush(); } } async getValueInfo(expression: string, context: Chrome.DevTools.RawLocation, stopId: unknown): Promise<{ typeInfos: Formatters.TypeInfo[], root: Formatters.TypeInfo, location?: number, data?: number[], displayValue?: string, memoryAddress?: number, }|null> { const {manage, unmanage, flush} = createEmbindPool(); const moduleInfo = await this.getModuleInfo(context.rawModuleId); try { const apiRawLocation = manage(new moduleInfo.backend.RawLocation()); apiRawLocation.rawModuleId = context.rawModuleId; apiRawLocation.codeOffset = context.codeOffset; apiRawLocation.inlineFrameIndex = context.inlineFrameIndex || 0; const wasm = new Formatters.HostWasmInterface(this.hostInterface, stopId); const proxy = new Formatters.DebuggerProxy(wasm, moduleInfo.backend); const typeInfoResult = manage(moduleInfo.dwarfSymbolsPlugin.EvaluateExpression(apiRawLocation, expression, proxy)); const error = manage(typeInfoResult.error); if (error) { if (error.code === moduleInfo.backend.ErrorCode.MODULE_NOT_FOUND_ERROR) { // Let's not throw when the module gets unloaded - that is quite common path that // we hit when the source-scope pane still keeps asynchronously updating while we // unload the wasm module. return null; } // TODO(crbug.com/1271147) Instead of throwing, we whould create an AST error node with the message // so that it is properly surfaced to the user. This should then make the special handling of // MODULE_NOT_FOUND_ERROR unnecessary. throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const typeInfos = mapVector(manage(typeInfoResult.typeInfos), typeInfo => fromApiTypeInfo(manage(typeInfo))); const root = fromApiTypeInfo(manage(typeInfoResult.root)); const {location, displayValue, memoryAddress} = typeInfoResult; const data = typeInfoResult.data ? mapVector(manage(typeInfoResult.data), n => n) : undefined; return {typeInfos, root, location, data, displayValue, memoryAddress}; function fromApiTypeInfo(apiTypeInfo: SymbolsBackend.TypeInfo): Formatters.TypeInfo { const apiMembers = manage(apiTypeInfo.members); const members = mapVector(apiMembers, fieldInfo => mapFieldInfo(manage(fieldInfo))); const apiEnumerators = manage(apiTypeInfo.enumerators); const enumerators = mapVector(apiEnumerators, enumerator => mapEnumerator(manage(enumerator))); unmanage(apiEnumerators); const typeNames = mapVector(manage(apiTypeInfo.typeNames), e => e); unmanage(apiMembers); const {typeId, size, arraySize, alignment, canExpand, isPointer, hasValue} = apiTypeInfo; const formatter = Formatters.CustomFormatters.get({ typeNames, typeId, size, alignment, isPointer, canExpand, arraySize: arraySize ?? 0, hasValue, members, enumerators, }); return { typeNames, isPointer, typeId, size, alignment, canExpand: canExpand && !formatter, arraySize: arraySize ?? 0, hasValue: hasValue || Boolean(formatter), members, enumerators, }; } } finally { flush(); } } async getMappedLines(rawModuleId: string, sourceFileURL: string): Promise<number[]> { const {flush, manage} = createEmbindPool(); const moduleInfo = await this.getModuleInfo(rawModuleId); const sourceFile = moduleInfo.urlToFileName.get(sourceFileURL); if (!sourceFile) { throw new Error(`InternalError: Unknown URL ${sourceFileURL}`); } try { const mappedLines = manage(moduleInfo.dwarfSymbolsPlugin.GetMappedLines(rawModuleId, sourceFile)); const error = manage(mappedLines.error); if (error) { throw new Error(`${moduleInfo.stringifyErrorCode(error.code)}: ${error.message}`); } const lines = mapVector(manage(mappedLines.MappedLines), l => l); return lines; } finally { flush(); } } async evaluate(expression: string, context: SymbolsBackend.RawLocation, stopId: unknown): Promise<Chrome.DevTools.RemoteObject|Chrome.DevTools.ForeignObject|null> { const valueInfo = await this.getValueInfo(expression, context, stopId); if (!valueInfo) { return null; } const wasm = new Formatters.HostWasmInterface(this.hostInterface, stopId); const cxxObject = await Formatters.CXXValue.create(this.lazyObjects, wasm, wasm.view, valueInfo); if (!cxxObject) { return { type: 'undefined' as Chrome.DevTools.RemoteObjectType, hasChildren: false, description: '<optimized out>', }; } return await cxxObject.asRemoteObject(); } async getProperties(objectId: Chrome.DevTools.RemoteObjectId): Promise<Chrome.DevTools.PropertyDescriptor[]> { const remoteObject = this.lazyObjects.get(objectId); if (!remoteObject) { return []; } const properties = await remoteObject.getProperties(); const descriptors = []; for (const {name, property} of properties) { descriptors.push({name, value: await property.asRemoteObject()}); } return descriptors; } async releaseObject(objectId: Chrome.DevTools.RemoteObjectId): Promise<void> { this.lazyObjects.release(objectId); } } export async function createPlugin( hostInterface: HostInterface, resourceLoader: ResourceLoader, moduleConfigurations: ModuleConfigurations = DEFAULT_MODULE_CONFIGURATIONS, logPluginApiCalls = false): Promise<DWARFLanguageExtensionPlugin> { const plugin = new DWARFLanguageExtensionPlugin(moduleConfigurations, resourceLoader, hostInterface); if (logPluginApiCalls) { const pluginLoggingProxy = { get: function<Key extends keyof DWARFLanguageExtensionPlugin>(target: DWARFLanguageExtensionPlugin, key: Key): DWARFLanguageExtensionPlugin[Key] { if (typeof target[key] === 'function') { return function(): unknown { const args = [...arguments]; const jsonArgs = args.map(x => { try { return JSON.stringify(x); } catch { return x.toString(); } }) .join(', '); // eslint-disable-next-line no-console console.info(`${key}(${jsonArgs})`); // @ts-expect-error TypeScript does not play well with `arguments` return (target[key] as (...args: any[]) => void).apply(target, arguments); } as unknown as DWARFLanguageExtensionPlugin[Key]; } return Reflect.get(target, key); }, }; return new Proxy(plugin, pluginLoggingProxy); } return plugin; }