UNPKG

chrome-devtools-frontend

Version:
1,109 lines (905 loc) • 36.4 kB
/* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import {NodeURL} from './NodeURL.js'; import type * as Platform from '../platform/platform.js'; import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; import type * as Protocol from '../../generated/protocol.js'; export const DevToolsStubErrorCode = -32015; // TODO(dgozman): we are not reporting generic errors in tests, but we should // instead report them and just have some expected errors in test expectations. const GenericError = -32000; const ConnectionClosedErrorCode = -32001; type MessageParams = { // eslint-disable-next-line @typescript-eslint/no-explicit-any [x: string]: any, }; type ProtocolDomainName = ProtocolProxyApi.ProtocolDomainName; export interface MessageError { code: number; message: string; data?: string|null; } export type Message = { sessionId?: string, url?: Platform.DevToolsPath.UrlString, id?: number, error?: MessageError|null, result?: Object|null, method?: QualifiedName, params?: MessageParams|null, }; interface EventMessage extends Message { method: QualifiedName; params?: MessageParams|null; } /** A qualified name, e.g. Domain.method */ export type QualifiedName = string&{qualifiedEventNameTag: string | undefined}; /** A qualified name, e.g. method */ export type UnqualifiedName = string&{unqualifiedEventNameTag: string | undefined}; export const splitQualifiedName = (string: QualifiedName): [string, UnqualifiedName] => { const [domain, eventName] = string.split('.'); return [domain, eventName as UnqualifiedName]; }; export const qualifyName = (domain: string, name: UnqualifiedName): QualifiedName => { return `${domain}.${name}` as QualifiedName; }; type EventParameterNames = Map<QualifiedName, string[]>; type ReadonlyEventParameterNames = ReadonlyMap<QualifiedName, string[]>; interface CommandParameter { name: string; type: string; optional: boolean; } type Callback = (error: MessageError|null, arg1: Object|null) => void; interface CallbackWithDebugInfo { callback: Callback; method: string; } export class InspectorBackend { readonly agentPrototypes: Map<ProtocolDomainName, _AgentPrototype> = new Map(); #initialized: boolean = false; #eventParameterNamesForDomain = new Map<ProtocolDomainName, EventParameterNames>(); private getOrCreateEventParameterNamesForDomain(domain: ProtocolDomainName): EventParameterNames { let map = this.#eventParameterNamesForDomain.get(domain); if (!map) { map = new Map(); this.#eventParameterNamesForDomain.set(domain, map); } return map; } getOrCreateEventParameterNamesForDomainForTesting(domain: ProtocolDomainName): EventParameterNames { return this.getOrCreateEventParameterNamesForDomain(domain); } getEventParameterNames(): ReadonlyMap<ProtocolDomainName, ReadonlyEventParameterNames> { return this.#eventParameterNamesForDomain; } static reportProtocolError(error: string, messageObject: Object): void { console.error(error + ': ' + JSON.stringify(messageObject)); } static reportProtocolWarning(error: string, messageObject: Object): void { console.warn(error + ': ' + JSON.stringify(messageObject)); } isInitialized(): boolean { return this.#initialized; } private agentPrototype(domain: ProtocolDomainName): _AgentPrototype { let prototype = this.agentPrototypes.get(domain); if (!prototype) { prototype = new _AgentPrototype(domain); this.agentPrototypes.set(domain, prototype); } return prototype; } registerCommand(method: QualifiedName, parameters: CommandParameter[], replyArgs: string[]): void { const [domain, command] = splitQualifiedName(method); this.agentPrototype(domain as ProtocolDomainName).registerCommand(command, parameters, replyArgs); this.#initialized = true; } registerEnum(type: QualifiedName, values: Object): void { const [domain, name] = splitQualifiedName(type); // @ts-ignore globalThis global namespace pollution if (!globalThis.Protocol[domain]) { // @ts-ignore globalThis global namespace pollution globalThis.Protocol[domain] = {}; } // @ts-ignore globalThis global namespace pollution globalThis.Protocol[domain][name] = values; this.#initialized = true; } registerEvent(eventName: QualifiedName, params: string[]): void { const domain = eventName.split('.')[0]; const eventParameterNames = this.getOrCreateEventParameterNamesForDomain(domain as ProtocolDomainName); eventParameterNames.set(eventName, params); this.#initialized = true; } } let connectionFactory: () => Connection; export class Connection { onMessage!: ((arg0: Object) => void)|null; constructor() { } setOnMessage(_onMessage: (arg0: (Object|string)) => void): void { } setOnDisconnect(_onDisconnect: (arg0: string) => void): void { } sendRawMessage(_message: string): void { } disconnect(): Promise<void> { throw new Error('not implemented'); } static setFactory(factory: () => Connection): void { connectionFactory = factory; } static getFactory(): () => Connection { return connectionFactory; } } type SendRawMessageCallback = (...args: unknown[]) => void; export const test = { /** * This will get called for every protocol message. * ProtocolClient.test.dumpProtocol = console.log */ dumpProtocol: null as ((arg0: string) => void) | null, /** * Runs a function when no protocol activity is present. * ProtocolClient.test.deprecatedRunAfterPendingDispatches(() => console.log('done')) */ deprecatedRunAfterPendingDispatches: null as ((arg0: () => void) => void) | null, /** * Sends a raw message over main connection. * ProtocolClient.test.sendRawMessage('Page.enable', {}, console.log) */ sendRawMessage: null as ((method: QualifiedName, args: Object|null, arg2: SendRawMessageCallback) => void) | null, /** * Set to true to not log any errors. */ suppressRequestErrors: false as boolean, /** * Set to get notified about any messages sent over protocol. */ onMessageSent: null as ((message: {domain: string, method: string, params: Object, id: number, sessionId?: string}, target: TargetBase|null) => void) | null, /** * Set to get notified about any messages received over protocol. */ onMessageReceived: null as ((message: Object, target: TargetBase|null) => void) | null, }; const LongPollingMethods = new Set<string>(['CSS.takeComputedStyleUpdates']); export class SessionRouter { readonly #connectionInternal: Connection; #lastMessageId: number; #pendingResponsesCount: number; readonly #pendingLongPollingMessageIds: Set<number>; readonly #sessions: Map<string, { target: TargetBase, callbacks: Map<number, CallbackWithDebugInfo>, proxyConnection: ((Connection | undefined)|null), }>; #pendingScripts: (() => void)[]; constructor(connection: Connection) { this.#connectionInternal = connection; this.#lastMessageId = 1; this.#pendingResponsesCount = 0; this.#pendingLongPollingMessageIds = new Set(); this.#sessions = new Map(); this.#pendingScripts = []; test.deprecatedRunAfterPendingDispatches = this.deprecatedRunAfterPendingDispatches.bind(this); test.sendRawMessage = this.sendRawMessageForTesting.bind(this); this.#connectionInternal.setOnMessage(this.onMessage.bind(this)); this.#connectionInternal.setOnDisconnect(reason => { const session = this.#sessions.get(''); if (session) { session.target.dispose(reason); } }); } registerSession(target: TargetBase, sessionId: string, proxyConnection?: Connection|null): void { // Only the Audits panel uses proxy connections. If it is ever possible to have multiple active at the // same time, it should be tested thoroughly. if (proxyConnection) { for (const session of this.#sessions.values()) { if (session.proxyConnection) { console.error('Multiple simultaneous proxy connections are currently unsupported'); break; } } } this.#sessions.set(sessionId, {target, callbacks: new Map(), proxyConnection}); } unregisterSession(sessionId: string): void { const session = this.#sessions.get(sessionId); if (!session) { return; } for (const callback of session.callbacks.values()) { SessionRouter.dispatchUnregisterSessionError(callback); } this.#sessions.delete(sessionId); } private getTargetBySessionId(sessionId: string): TargetBase|null { const session = this.#sessions.get(sessionId ? sessionId : ''); if (!session) { return null; } return session.target; } private nextMessageId(): number { return this.#lastMessageId++; } connection(): Connection { return this.#connectionInternal; } sendMessage(sessionId: string, domain: string, method: QualifiedName, params: Object|null, callback: Callback): void { const messageId = this.nextMessageId(); const messageObject: Message = { id: messageId, method: method, }; if (params) { messageObject.params = params; } if (sessionId) { messageObject.sessionId = sessionId; } if (test.dumpProtocol) { test.dumpProtocol('frontend: ' + JSON.stringify(messageObject)); } if (test.onMessageSent) { const paramsObject = JSON.parse(JSON.stringify(params || {})); test.onMessageSent( {domain, method, params: (paramsObject as Object), id: messageId, sessionId}, this.getTargetBySessionId(sessionId)); } ++this.#pendingResponsesCount; if (LongPollingMethods.has(method)) { this.#pendingLongPollingMessageIds.add(messageId); } const session = this.#sessions.get(sessionId); if (!session) { return; } session.callbacks.set(messageId, {callback, method}); this.#connectionInternal.sendRawMessage(JSON.stringify(messageObject)); } private sendRawMessageForTesting(method: QualifiedName, params: Object|null, callback: Callback|null, sessionId = ''): void { const domain = method.split('.')[0]; this.sendMessage(sessionId, domain, method, params, callback || ((): void => {})); } private onMessage(message: string|Object): void { if (test.dumpProtocol) { test.dumpProtocol('backend: ' + ((typeof message === 'string') ? message : JSON.stringify(message))); } if (test.onMessageReceived) { const messageObjectCopy = JSON.parse((typeof message === 'string') ? message : JSON.stringify(message)); test.onMessageReceived(messageObjectCopy, this.getTargetBySessionId(messageObjectCopy.sessionId)); } const messageObject = ((typeof message === 'string') ? JSON.parse(message) : message) as Message; // Send all messages to proxy connections. let suppressUnknownMessageErrors = false; for (const session of this.#sessions.values()) { if (!session.proxyConnection) { continue; } if (!session.proxyConnection.onMessage) { InspectorBackend.reportProtocolError( 'Protocol Error: the session has a proxyConnection with no _onMessage', messageObject); continue; } session.proxyConnection.onMessage(messageObject); suppressUnknownMessageErrors = true; } const sessionId = messageObject.sessionId || ''; const session = this.#sessions.get(sessionId); if (!session) { if (!suppressUnknownMessageErrors) { InspectorBackend.reportProtocolError('Protocol Error: the message with wrong session id', messageObject); } return; } // If this message is directly for the target controlled by the proxy connection, don't handle it. if (session.proxyConnection) { return; } if (session.target.getNeedsNodeJSPatching()) { NodeURL.patch(messageObject); } if (messageObject.id !== undefined) { // just a response for some request const callback = session.callbacks.get(messageObject.id); session.callbacks.delete(messageObject.id); if (!callback) { if (messageObject.error?.code === ConnectionClosedErrorCode) { // Ignore the errors that are sent as responses after the session closes. return; } if (!suppressUnknownMessageErrors) { InspectorBackend.reportProtocolError('Protocol Error: the message with wrong id', messageObject); } return; } callback.callback(messageObject.error || null, messageObject.result || null); --this.#pendingResponsesCount; this.#pendingLongPollingMessageIds.delete(messageObject.id); if (this.#pendingScripts.length && !this.hasOutstandingNonLongPollingRequests()) { this.deprecatedRunAfterPendingDispatches(); } } else { if (messageObject.method === undefined) { InspectorBackend.reportProtocolError('Protocol Error: the message without method', messageObject); return; } // This cast is justified as we just checked for the presence of messageObject.method. const eventMessage = messageObject as EventMessage; session.target.dispatch(eventMessage); } } private hasOutstandingNonLongPollingRequests(): boolean { return this.#pendingResponsesCount - this.#pendingLongPollingMessageIds.size > 0; } private deprecatedRunAfterPendingDispatches(script?: (() => void)): void { if (script) { this.#pendingScripts.push(script); } // Execute all promises. window.setTimeout(() => { if (!this.hasOutstandingNonLongPollingRequests()) { this.executeAfterPendingDispatches(); } else { this.deprecatedRunAfterPendingDispatches(); } }, 0); } private executeAfterPendingDispatches(): void { if (!this.hasOutstandingNonLongPollingRequests()) { const scripts = this.#pendingScripts; this.#pendingScripts = []; for (let id = 0; id < scripts.length; ++id) { scripts[id](); } } } static dispatchConnectionError(callback: Callback, method: string): void { const error = { message: `Connection is closed, can\'t dispatch pending call to ${method}`, code: ConnectionClosedErrorCode, data: null, }; window.setTimeout(() => callback(error, null), 0); } static dispatchUnregisterSessionError({callback, method}: CallbackWithDebugInfo): void { const error = { message: `Session is unregistering, can\'t dispatch pending call to ${method}`, code: ConnectionClosedErrorCode, data: null, }; window.setTimeout(() => callback(error, null), 0); } } /** * Make sure that `Domain` in get/set is only ever instantiated with one protocol domain * name, because if `Domain` allows multiple domains, the type is unsound. */ interface AgentsMap extends Map<ProtocolDomainName, ProtocolProxyApi.ProtocolApi[ProtocolDomainName]> { get<Domain extends ProtocolDomainName>(key: Domain): ProtocolProxyApi.ProtocolApi[Domain]|undefined; set<Domain extends ProtocolDomainName>(key: Domain, value: ProtocolProxyApi.ProtocolApi[Domain]): this; } /** * Make sure that `Domain` in get/set is only ever instantiated with one protocol domain * name, because if `Domain` allows multiple domains, the type is unsound. */ interface DispatcherMap extends Map<ProtocolDomainName, ProtocolProxyApi.ProtocolDispatchers[ProtocolDomainName]> { get<Domain extends ProtocolDomainName>(key: Domain): DispatcherManager<Domain>|undefined; set<Domain extends ProtocolDomainName>(key: Domain, value: DispatcherManager<Domain>): this; } export class TargetBase { needsNodeJSPatching: boolean; readonly sessionId: string; routerInternal: SessionRouter|null; #agents: AgentsMap = new Map(); #dispatchers: DispatcherMap = new Map(); constructor( needsNodeJSPatching: boolean, parentTarget: TargetBase|null, sessionId: string, connection: Connection|null) { this.needsNodeJSPatching = needsNodeJSPatching; this.sessionId = sessionId; if ((!parentTarget && connection) || (!parentTarget && sessionId) || (connection && sessionId)) { throw new Error('Either connection or sessionId (but not both) must be supplied for a child target'); } let router: SessionRouter; if (sessionId && parentTarget && parentTarget.routerInternal) { router = parentTarget.routerInternal; } else if (connection) { router = new SessionRouter(connection); } else { router = new SessionRouter(connectionFactory()); } this.routerInternal = router; router.registerSession(this, this.sessionId); for (const [domain, agentPrototype] of inspectorBackend.agentPrototypes) { const agent = Object.create((agentPrototype as _AgentPrototype)); agent.target = this; this.#agents.set(domain, agent); } for (const [domain, eventParameterNames] of inspectorBackend.getEventParameterNames().entries()) { this.#dispatchers.set(domain, new DispatcherManager(eventParameterNames)); } } dispatch(eventMessage: EventMessage): void { const [domainName, method] = splitQualifiedName(eventMessage.method); const dispatcher = this.#dispatchers.get(domainName as ProtocolDomainName); if (!dispatcher) { InspectorBackend.reportProtocolError( `Protocol Error: the message ${eventMessage.method} is for non-existing domain '${domainName}'`, eventMessage); return; } dispatcher.dispatch(method, eventMessage); } dispose(_reason: string): void { if (!this.routerInternal) { return; } this.routerInternal.unregisterSession(this.sessionId); this.routerInternal = null; } isDisposed(): boolean { return !this.routerInternal; } markAsNodeJSForTest(): void { this.needsNodeJSPatching = true; } router(): SessionRouter|null { return this.routerInternal; } // Agent accessors, keep alphabetically sorted. /** * Make sure that `Domain` is only ever instantiated with one protocol domain * name, because if `Domain` allows multiple domains, the type is unsound. */ private getAgent<Domain extends ProtocolDomainName>(domain: Domain): ProtocolProxyApi.ProtocolApi[Domain] { const agent = this.#agents.get<Domain>(domain); if (!agent) { throw new Error('Accessing undefined agent'); } return agent; } accessibilityAgent(): ProtocolProxyApi.AccessibilityApi { return this.getAgent('Accessibility'); } animationAgent(): ProtocolProxyApi.AnimationApi { return this.getAgent('Animation'); } auditsAgent(): ProtocolProxyApi.AuditsApi { return this.getAgent('Audits'); } browserAgent(): ProtocolProxyApi.BrowserApi { return this.getAgent('Browser'); } backgroundServiceAgent(): ProtocolProxyApi.BackgroundServiceApi { return this.getAgent('BackgroundService'); } cacheStorageAgent(): ProtocolProxyApi.CacheStorageApi { return this.getAgent('CacheStorage'); } cssAgent(): ProtocolProxyApi.CSSApi { return this.getAgent('CSS'); } databaseAgent(): ProtocolProxyApi.DatabaseApi { return this.getAgent('Database'); } debuggerAgent(): ProtocolProxyApi.DebuggerApi { return this.getAgent('Debugger'); } deviceOrientationAgent(): ProtocolProxyApi.DeviceOrientationApi { return this.getAgent('DeviceOrientation'); } domAgent(): ProtocolProxyApi.DOMApi { return this.getAgent('DOM'); } domdebuggerAgent(): ProtocolProxyApi.DOMDebuggerApi { return this.getAgent('DOMDebugger'); } domsnapshotAgent(): ProtocolProxyApi.DOMSnapshotApi { return this.getAgent('DOMSnapshot'); } domstorageAgent(): ProtocolProxyApi.DOMStorageApi { return this.getAgent('DOMStorage'); } emulationAgent(): ProtocolProxyApi.EmulationApi { return this.getAgent('Emulation'); } eventBreakpointsAgent(): ProtocolProxyApi.EventBreakpointsApi { return this.getAgent('EventBreakpoints'); } fetchAgent(): ProtocolProxyApi.FetchApi { return this.getAgent('Fetch'); } heapProfilerAgent(): ProtocolProxyApi.HeapProfilerApi { return this.getAgent('HeapProfiler'); } indexedDBAgent(): ProtocolProxyApi.IndexedDBApi { return this.getAgent('IndexedDB'); } inputAgent(): ProtocolProxyApi.InputApi { return this.getAgent('Input'); } ioAgent(): ProtocolProxyApi.IOApi { return this.getAgent('IO'); } inspectorAgent(): ProtocolProxyApi.InspectorApi { return this.getAgent('Inspector'); } layerTreeAgent(): ProtocolProxyApi.LayerTreeApi { return this.getAgent('LayerTree'); } logAgent(): ProtocolProxyApi.LogApi { return this.getAgent('Log'); } mediaAgent(): ProtocolProxyApi.MediaApi { return this.getAgent('Media'); } memoryAgent(): ProtocolProxyApi.MemoryApi { return this.getAgent('Memory'); } networkAgent(): ProtocolProxyApi.NetworkApi { return this.getAgent('Network'); } overlayAgent(): ProtocolProxyApi.OverlayApi { return this.getAgent('Overlay'); } pageAgent(): ProtocolProxyApi.PageApi { return this.getAgent('Page'); } preloadAgent(): ProtocolProxyApi.PreloadApi { return this.getAgent('Preload'); } profilerAgent(): ProtocolProxyApi.ProfilerApi { return this.getAgent('Profiler'); } performanceAgent(): ProtocolProxyApi.PerformanceApi { return this.getAgent('Performance'); } runtimeAgent(): ProtocolProxyApi.RuntimeApi { return this.getAgent('Runtime'); } securityAgent(): ProtocolProxyApi.SecurityApi { return this.getAgent('Security'); } serviceWorkerAgent(): ProtocolProxyApi.ServiceWorkerApi { return this.getAgent('ServiceWorker'); } storageAgent(): ProtocolProxyApi.StorageApi { return this.getAgent('Storage'); } systemInfo(): ProtocolProxyApi.SystemInfoApi { return this.getAgent('SystemInfo'); } targetAgent(): ProtocolProxyApi.TargetApi { return this.getAgent('Target'); } tracingAgent(): ProtocolProxyApi.TracingApi { return this.getAgent('Tracing'); } webAudioAgent(): ProtocolProxyApi.WebAudioApi { return this.getAgent('WebAudio'); } webAuthnAgent(): ProtocolProxyApi.WebAuthnApi { return this.getAgent('WebAuthn'); } // Dispatcher registration and de-registration, keep alphabetically sorted. /** * Make sure that `Domain` is only ever instantiated with one protocol domain * name, because if `Domain` allows multiple domains, the type is unsound. */ private registerDispatcher<Domain extends ProtocolDomainName>( domain: Domain, dispatcher: ProtocolProxyApi.ProtocolDispatchers[Domain]): void { const manager = this.#dispatchers.get(domain); if (!manager) { return; } manager.addDomainDispatcher(dispatcher); } /** * Make sure that `Domain` is only ever instantiated with one protocol domain * name, because if `Domain` allows multiple domains, the type is unsound. */ private unregisterDispatcher<Domain extends ProtocolDomainName>( domain: Domain, dispatcher: ProtocolProxyApi.ProtocolDispatchers[Domain]): void { const manager = this.#dispatchers.get(domain); if (!manager) { return; } manager.removeDomainDispatcher(dispatcher); } registerAccessibilityDispatcher(dispatcher: ProtocolProxyApi.AccessibilityDispatcher): void { this.registerDispatcher('Accessibility', dispatcher); } registerAnimationDispatcher(dispatcher: ProtocolProxyApi.AnimationDispatcher): void { this.registerDispatcher('Animation', dispatcher); } registerAuditsDispatcher(dispatcher: ProtocolProxyApi.AuditsDispatcher): void { this.registerDispatcher('Audits', dispatcher); } registerCSSDispatcher(dispatcher: ProtocolProxyApi.CSSDispatcher): void { this.registerDispatcher('CSS', dispatcher); } registerDatabaseDispatcher(dispatcher: ProtocolProxyApi.DatabaseDispatcher): void { this.registerDispatcher('Database', dispatcher); } registerBackgroundServiceDispatcher(dispatcher: ProtocolProxyApi.BackgroundServiceDispatcher): void { this.registerDispatcher('BackgroundService', dispatcher); } registerDebuggerDispatcher(dispatcher: ProtocolProxyApi.DebuggerDispatcher): void { this.registerDispatcher('Debugger', dispatcher); } unregisterDebuggerDispatcher(dispatcher: ProtocolProxyApi.DebuggerDispatcher): void { this.unregisterDispatcher('Debugger', dispatcher); } registerDOMDispatcher(dispatcher: ProtocolProxyApi.DOMDispatcher): void { this.registerDispatcher('DOM', dispatcher); } registerDOMStorageDispatcher(dispatcher: ProtocolProxyApi.DOMStorageDispatcher): void { this.registerDispatcher('DOMStorage', dispatcher); } registerFetchDispatcher(dispatcher: ProtocolProxyApi.FetchDispatcher): void { this.registerDispatcher('Fetch', dispatcher); } registerHeapProfilerDispatcher(dispatcher: ProtocolProxyApi.HeapProfilerDispatcher): void { this.registerDispatcher('HeapProfiler', dispatcher); } registerInspectorDispatcher(dispatcher: ProtocolProxyApi.InspectorDispatcher): void { this.registerDispatcher('Inspector', dispatcher); } registerLayerTreeDispatcher(dispatcher: ProtocolProxyApi.LayerTreeDispatcher): void { this.registerDispatcher('LayerTree', dispatcher); } registerLogDispatcher(dispatcher: ProtocolProxyApi.LogDispatcher): void { this.registerDispatcher('Log', dispatcher); } registerMediaDispatcher(dispatcher: ProtocolProxyApi.MediaDispatcher): void { this.registerDispatcher('Media', dispatcher); } registerNetworkDispatcher(dispatcher: ProtocolProxyApi.NetworkDispatcher): void { this.registerDispatcher('Network', dispatcher); } registerOverlayDispatcher(dispatcher: ProtocolProxyApi.OverlayDispatcher): void { this.registerDispatcher('Overlay', dispatcher); } registerPageDispatcher(dispatcher: ProtocolProxyApi.PageDispatcher): void { this.registerDispatcher('Page', dispatcher); } registerPreloadDispatcher(dispatcher: ProtocolProxyApi.PreloadDispatcher): void { this.registerDispatcher('Preload', dispatcher); } registerProfilerDispatcher(dispatcher: ProtocolProxyApi.ProfilerDispatcher): void { this.registerDispatcher('Profiler', dispatcher); } registerRuntimeDispatcher(dispatcher: ProtocolProxyApi.RuntimeDispatcher): void { this.registerDispatcher('Runtime', dispatcher); } registerSecurityDispatcher(dispatcher: ProtocolProxyApi.SecurityDispatcher): void { this.registerDispatcher('Security', dispatcher); } registerServiceWorkerDispatcher(dispatcher: ProtocolProxyApi.ServiceWorkerDispatcher): void { this.registerDispatcher('ServiceWorker', dispatcher); } registerStorageDispatcher(dispatcher: ProtocolProxyApi.StorageDispatcher): void { this.registerDispatcher('Storage', dispatcher); } registerTargetDispatcher(dispatcher: ProtocolProxyApi.TargetDispatcher): void { this.registerDispatcher('Target', dispatcher); } registerTracingDispatcher(dispatcher: ProtocolProxyApi.TracingDispatcher): void { this.registerDispatcher('Tracing', dispatcher); } registerWebAudioDispatcher(dispatcher: ProtocolProxyApi.WebAudioDispatcher): void { this.registerDispatcher('WebAudio', dispatcher); } registerWebAuthnDispatcher(dispatcher: ProtocolProxyApi.WebAuthnDispatcher): void { this.registerDispatcher('WebAuthn', dispatcher); } getNeedsNodeJSPatching(): boolean { return this.needsNodeJSPatching; } } /** * This is a class that serves as the prototype for a domains #agents (every target * has it's own set of #agents). The InspectorBackend keeps an instance of this class * per domain, and each TargetBase creates its #agents (via Object.create) and installs * this instance as prototype. * * The reasons this is done is so that on the prototypes we can install the implementations * of the invoke_enable, etc. methods that the front-end uses. */ // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/naming-convention class _AgentPrototype { replyArgs: { [x: string]: string[], }; commandParameters: { [x: string]: CommandParameter[], }; readonly domain: string; target!: TargetBase; constructor(domain: string) { this.replyArgs = {}; this.domain = domain; this.commandParameters = {}; } registerCommand(methodName: UnqualifiedName, parameters: CommandParameter[], replyArgs: string[]): void { const domainAndMethod = qualifyName(this.domain, methodName); function sendMessagePromise(this: _AgentPrototype, ...args: unknown[]): Promise<unknown> { return _AgentPrototype.prototype.sendMessageToBackendPromise.call(this, domainAndMethod, parameters, args); } // @ts-ignore Method code generation this[methodName] = sendMessagePromise; this.commandParameters[domainAndMethod] = parameters; function invoke( this: _AgentPrototype, request: Object|undefined = {}): Promise<Protocol.ProtocolResponseWithError> { return this.invoke(domainAndMethod, request); } // @ts-ignore Method code generation this['invoke_' + methodName] = invoke; this.replyArgs[domainAndMethod] = replyArgs; } private prepareParameters( method: string, parameters: CommandParameter[], args: unknown[], errorCallback: (arg0: string) => void): Object |null { const params: {[x: string]: unknown} = {}; let hasParams = false; for (const param of parameters) { const paramName = param.name; const typeName = param.type; const optionalFlag = param.optional; if (!args.length && !optionalFlag) { errorCallback( `Protocol Error: Invalid number of arguments for method '${method}' call. ` + `It must have the following arguments ${JSON.stringify(parameters)}'.`); return null; } const value = args.shift(); if (optionalFlag && typeof value === 'undefined') { continue; } if (typeof value !== typeName) { errorCallback( `Protocol Error: Invalid type of argument '${paramName}' for method '${method}' call. ` + `It must be '${typeName}' but it is '${typeof value}'.`); return null; } params[paramName] = value; hasParams = true; } if (args.length) { errorCallback(`Protocol Error: Extra ${args.length} arguments in a call to method '${method}'.`); return null; } return hasParams ? params : null; } private sendMessageToBackendPromise(method: QualifiedName, parameters: CommandParameter[], args: unknown[]): Promise<unknown> { let errorMessage; function onError(message: string): void { console.error(message); errorMessage = message; } const params = this.prepareParameters(method, parameters, args, onError); if (errorMessage) { return Promise.resolve(null); } return new Promise(resolve => { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any const callback: Callback = (error: MessageError|null, result: any|null): void => { if (error) { if (!test.suppressRequestErrors && error.code !== DevToolsStubErrorCode && error.code !== GenericError && error.code !== ConnectionClosedErrorCode) { console.error('Request ' + method + ' failed. ' + JSON.stringify(error)); } resolve(null); return; } const args = this.replyArgs[method]; resolve(result && args.length ? result[args[0]] : undefined); }; const router = this.target.router(); if (!router) { SessionRouter.dispatchConnectionError(callback, method); } else { router.sendMessage(this.target.sessionId, this.domain, method, params, callback); } }); } private invoke(method: QualifiedName, request: Object|null): Promise<Protocol.ProtocolResponseWithError> { return new Promise(fulfill => { const callback: Callback = (error: MessageError|undefined|null, result: Object|null): void => { if (error && !test.suppressRequestErrors && error.code !== DevToolsStubErrorCode && error.code !== GenericError && error.code !== ConnectionClosedErrorCode) { console.error('Request ' + method + ' failed. ' + JSON.stringify(error)); } const errorMessage = error?.message; fulfill({...result, getError: (): string | undefined => errorMessage}); }; const router = this.target.router(); if (!router) { SessionRouter.dispatchConnectionError(callback, method); } else { router.sendMessage(this.target.sessionId, this.domain, method, request, callback); } }); } } /** * A `DispatcherManager` has a collection of #dispatchers that implement one of the * `ProtocolProxyApi.{Foo}Dispatcher` interfaces. Each target uses one of these per * domain to manage the registered #dispatchers. The class knows the parameter names * of the events via `#eventArgs`, which is a map managed by the inspector back-end * so that there is only one map per domain that is shared among all DispatcherManagers. */ class DispatcherManager<Domain extends ProtocolDomainName> { #eventArgs: ReadonlyEventParameterNames; #dispatchers: ProtocolProxyApi.ProtocolDispatchers[Domain][] = []; constructor(eventArgs: ReadonlyEventParameterNames) { this.#eventArgs = eventArgs; } addDomainDispatcher(dispatcher: ProtocolProxyApi.ProtocolDispatchers[Domain]): void { this.#dispatchers.push(dispatcher); } removeDomainDispatcher(dispatcher: ProtocolProxyApi.ProtocolDispatchers[Domain]): void { const index = this.#dispatchers.indexOf(dispatcher); if (index === -1) { return; } this.#dispatchers.splice(index, 1); } dispatch(event: UnqualifiedName, messageObject: EventMessage): void { if (!this.#dispatchers.length) { return; } if (!this.#eventArgs.has(messageObject.method)) { InspectorBackend.reportProtocolWarning( `Protocol Warning: Attempted to dispatch an unspecified event '${messageObject.method}'`, messageObject); return; } const messageParams = {...messageObject.params}; for (let index = 0; index < this.#dispatchers.length; ++index) { const dispatcher = this.#dispatchers[index]; if (event in dispatcher) { const f = dispatcher[event as string as keyof ProtocolProxyApi.ProtocolDispatchers[Domain]]; // @ts-ignore Can't type check the dispatch. f.call(dispatcher, messageParams); } } } } export const inspectorBackend = new InspectorBackend();