chrome-devtools-frontend
Version:
Chrome DevTools UI
614 lines (560 loc) • 25.1 kB
text/typescript
// 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;
}