UNPKG

chrome-devtools-frontend

Version:
1,407 lines (1,218 loc) • 37.3 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'; /** * @typedef {string} */ export const ProtocolError = Symbol('Protocol.Error'); 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; export class InspectorBackend { constructor() { /** @type {!Map<string, !_AgentPrototype>} */ this._agentPrototypes = new Map(); /** @type {!Map<string, !_DispatcherPrototype>} */ this._dispatcherPrototypes = new Map(); this._initialized = false; } /** * @param {string} error * @param {!Object} messageObject */ static reportProtocolError(error, messageObject) { console.error(error + ': ' + JSON.stringify(messageObject)); } /** * @param {string} error * @param {!Object} messageObject */ static reportProtocolWarning(error, messageObject) { console.warn(error + ': ' + JSON.stringify(messageObject)); } /** * @return {boolean} */ isInitialized() { return this._initialized; } /** * @param {string} domain */ _addAgentGetterMethodToProtocolTargetPrototype(domain) { let upperCaseLength = 0; while (upperCaseLength < domain.length && domain[upperCaseLength].toLowerCase() !== domain[upperCaseLength]) { ++upperCaseLength; } const methodName = domain.substr(0, upperCaseLength).toLowerCase() + domain.slice(upperCaseLength) + 'Agent'; /** * @this {TargetBase} */ function agentGetter() { return this._agents[domain]; } // @ts-ignore Method code generation TargetBase.prototype[methodName] = agentGetter; /** * @this {TargetBase} * @param {!_DispatcherPrototype} dispatcher */ function registerDispatcher(dispatcher) { this.registerDispatcher(domain, dispatcher); } // @ts-ignore Method code generation TargetBase.prototype['register' + domain + 'Dispatcher'] = registerDispatcher; /** * @this {TargetBase} * @param {!_DispatcherPrototype} dispatcher */ function unregisterDispatcher(dispatcher) { this.unregisterDispatcher(domain, dispatcher); } // @ts-ignore Method code generation TargetBase.prototype['unregister' + domain + 'Dispatcher'] = unregisterDispatcher; } /** * @param {string} domain * @return {!_AgentPrototype} */ _agentPrototype(domain) { if (!this._agentPrototypes.has(domain)) { this._agentPrototypes.set(domain, new _AgentPrototype(domain)); this._addAgentGetterMethodToProtocolTargetPrototype(domain); } return /** @type {!_AgentPrototype} */ (this._agentPrototypes.get(domain)); } /** * @param {string} domain * @return {!_DispatcherPrototype} */ _dispatcherPrototype(domain) { if (!this._dispatcherPrototypes.has(domain)) { this._dispatcherPrototypes.set(domain, new _DispatcherPrototype()); } return /** @type {!_DispatcherPrototype} */ (this._dispatcherPrototypes.get(domain)); } /** * @param {string} method * @param {!Array.<!{name: string, type: string, optional: boolean}>} signature * @param {!Array.<string>} replyArgs */ registerCommand(method, signature, replyArgs) { const domainAndMethod = method.split('.'); this._agentPrototype(domainAndMethod[0]).registerCommand(domainAndMethod[1], signature, replyArgs); this._initialized = true; } /** * @param {string} type * @param {!Object} values */ registerEnum(type, values) { const domainAndName = type.split('.'); const domain = domainAndName[0]; // @ts-ignore Protocol global namespace pollution if (!Protocol[domain]) { // @ts-ignore Protocol global namespace pollution Protocol[domain] = {}; } // @ts-ignore Protocol global namespace pollution Protocol[domain][domainAndName[1]] = values; this._initialized = true; } /** * @param {string} eventName * @param {!Object} params */ registerEvent(eventName, params) { const domain = eventName.split('.')[0]; this._dispatcherPrototype(domain).registerEvent(eventName, params); this._initialized = true; } /** * @param {function((T|undefined)):void} clientCallback * @param {string} errorPrefix * @param {function(new:T,S):void=} constructor * @param {T=} defaultValue * @return {function(?string, S):void} * @template T,S */ wrapClientCallback(clientCallback, errorPrefix, constructor, defaultValue) { /** * @param {?string} error * @param {S} value * @template S */ function callbackWrapper(error, value) { if (error) { console.error(errorPrefix + error); clientCallback(defaultValue); return; } if (constructor) { // @ts-ignore Special casting clientCallback(new constructor(value)); } else { // @ts-ignore Special casting clientCallback(value); } } return callbackWrapper; } } /** @type {function():!Connection} */ let _factory; /** * @interface */ export class Connection { constructor() { /** @type {?function(!Object):void} */ this._onMessage; } /** * @param {function((!Object|string)):void} onMessage */ setOnMessage(onMessage) { } /** * @param {function(string):void} onDisconnect */ setOnDisconnect(onDisconnect) { } /** * @param {string} message */ sendRawMessage(message) { } /** * @return {!Promise<void>} */ disconnect() { throw new Error('not implemented'); } /** * @param {function():!Connection} factory */ static setFactory(factory) { _factory = factory; } /** * @return {function():!Connection} */ static getFactory() { return _factory; } } /** @typedef {function(...*):void} */ let SendRawMessageCallback; // eslint-disable-line no-unused-vars export const test = { /** * This will get called for every protocol message. * ProtocolClient.test.dumpProtocol = console.log * @type {?function(string):void} */ dumpProtocol: null, /** * Runs a function when no protocol activity is present. * ProtocolClient.test.deprecatedRunAfterPendingDispatches(() => console.log('done')) * @type {?function(function():void=):void} */ deprecatedRunAfterPendingDispatches: null, /** * Sends a raw message over main connection. * ProtocolClient.test.sendRawMessage('Page.enable', {}, console.log) * @type {?function(string, ?Object, ?SendRawMessageCallback):void} */ sendRawMessage: null, /** * Set to true to not log any errors. */ suppressRequestErrors: false, /** * Set to get notified about any messages sent over protocol. * @type {?function({domain: string, method: string, params: !Object, id: number}, ?TargetBase):void} */ onMessageSent: null, /** * Set to get notified about any messages received over protocol. * @type {?function(!Object, ?TargetBase):void} */ onMessageReceived: null, }; const LongPollingMethods = new Set(['CSS.takeComputedStyleUpdates']); export class SessionRouter { /** * @param {!Connection} connection */ constructor(connection) { this._connection = connection; this._lastMessageId = 1; this._pendingResponsesCount = 0; /** @type {!Set<number>} */ this._pendingLongPollingMessageIds = new Set(); this._domainToLogger = new Map(); /** @type {!Map<string, {target: !TargetBase, callbacks: !Map<number, !_CallbackWithDebugInfo>, proxyConnection: (?Connection|undefined)}>} */ this._sessions = new Map(); /** @type {!Array<function():void>} */ this._pendingScripts = []; test.deprecatedRunAfterPendingDispatches = this._deprecatedRunAfterPendingDispatches.bind(this); test.sendRawMessage = this._sendRawMessageForTesting.bind(this); this._connection.setOnMessage(this._onMessage.bind(this)); this._connection.setOnDisconnect(reason => { const session = this._sessions.get(''); if (session) { session.target.dispose(reason); } }); } /** * @param {!TargetBase} target * @param {string} sessionId * @param {?Connection=} proxyConnection */ registerSession(target, sessionId, proxyConnection) { // 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}); } /** * @param {string} sessionId */ unregisterSession(sessionId) { const session = this._sessions.get(sessionId); if (!session) { return; } for (const callback of session.callbacks.values()) { SessionRouter.dispatchUnregisterSessionError(callback); } this._sessions.delete(sessionId); } /** * @param {string} sessionId * @return {?TargetBase} */ _getTargetBySessionId(sessionId) { const session = this._sessions.get(sessionId ? sessionId : ''); if (!session) { return null; } return session.target; } /** * @return {number} */ _nextMessageId() { return this._lastMessageId++; } /** * @return {!Connection} */ connection() { return this._connection; } /** * @param {string} sessionId * @param {string} domain * @param {string} method * @param {?Object} params * @param {!_Callback} callback */ sendMessage(sessionId, domain, method, params, callback) { const messageObject = {}; const messageId = this._nextMessageId(); messageObject.id = messageId; messageObject.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: /** @type {!Object} */ (paramsObject), id: messageId}, 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._connection.sendRawMessage(JSON.stringify(messageObject)); } /** * @param {string} method * @param {?Object} params * @param {?function(...*):void} callback */ _sendRawMessageForTesting(method, params, callback) { const domain = method.split('.')[0]; this.sendMessage('', domain, method, params, callback || (() => {})); } /** * @param {!Object|string} message */ _onMessage(message) { 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( /** @type {!Object} */ (messageObjectCopy), this._getTargetBySessionId(messageObjectCopy.sessionId)); } const messageObject = /** @type {!{sessionId: string, url: string, id: number, error: ?Object, result: ?Object}|!{sessionId: string, url: string, method: string, params: !Array<string>}} */ ((typeof message === 'string') ? JSON.parse(message) : 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._needsNodeJSPatching) { NodeURL.patch(messageObject); } if ('id' in messageObject) { // just a response for some request const callback = session.callbacks.get(messageObject.id); session.callbacks.delete(messageObject.id); if (!callback) { if (!suppressUnknownMessageErrors) { InspectorBackend.reportProtocolError('Protocol Error: the message with wrong id', messageObject); } return; } callback.callback(messageObject.error, messageObject.result); --this._pendingResponsesCount; this._pendingLongPollingMessageIds.delete(messageObject.id); if (this._pendingScripts.length && !this._hasOutstandingNonLongPollingRequests()) { this._deprecatedRunAfterPendingDispatches(); } } else { if (!('method' in messageObject)) { InspectorBackend.reportProtocolError('Protocol Error: the message without method', messageObject); return; } const method = messageObject.method.split('.'); const domainName = method[0]; if (!(domainName in session.target._dispatchers)) { InspectorBackend.reportProtocolError( `Protocol Error: the message ${messageObject.method} is for non-existing domain '${domainName}'`, messageObject); return; } session.target._dispatchers[domainName].dispatch( method[1], /** @type {{method: string, params: ?Array<string>}} */ (messageObject)); } } _hasOutstandingNonLongPollingRequests() { return this._pendingResponsesCount - this._pendingLongPollingMessageIds.size > 0; } /** * @param {function():void=} script */ _deprecatedRunAfterPendingDispatches(script) { if (script) { this._pendingScripts.push(script); } // Execute all promises. setTimeout(() => { if (!this._hasOutstandingNonLongPollingRequests()) { this._executeAfterPendingDispatches(); } else { this._deprecatedRunAfterPendingDispatches(); } }, 0); } _executeAfterPendingDispatches() { if (!this._hasOutstandingNonLongPollingRequests()) { const scripts = this._pendingScripts; this._pendingScripts = []; for (let id = 0; id < scripts.length; ++id) { scripts[id](); } } } /** * @param {!_Callback} callback * @param {string} method */ static dispatchConnectionError(callback, method) { const error = { message: `Connection is closed, can\'t dispatch pending call to ${method}`, code: _ConnectionClosedErrorCode, data: null }; setTimeout(() => callback(error, null), 0); } /** * @param {!_CallbackWithDebugInfo} callbackWithDebugInfo */ static dispatchUnregisterSessionError({callback, method}) { const error = { message: `Session is unregistering, can\'t dispatch pending call to ${method}`, code: _ConnectionClosedErrorCode, data: null }; setTimeout(() => callback(error, null), 0); } } export class TargetBase { /** * @param {boolean} needsNodeJSPatching * @param {?TargetBase} parentTarget * @param {string} sessionId * @param {?Connection} connection */ constructor(needsNodeJSPatching, parentTarget, sessionId, connection) { 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'); } /** @type {!SessionRouter} */ let router; if (sessionId && parentTarget && parentTarget._router) { router = parentTarget._router; } else if (connection) { router = new SessionRouter(connection); } else { router = new SessionRouter(_factory()); } /** @type {?SessionRouter} */ this._router = router; router.registerSession(this, this._sessionId); /** @type {!Object<string,!_AgentPrototype>} */ this._agents = {}; for (const [domain, agentPrototype] of inspectorBackend._agentPrototypes) { this._agents[domain] = Object.create(/** @type {!_AgentPrototype} */ (agentPrototype)); this._agents[domain]._target = this; } /** @type {!Object<string,_DispatcherPrototype>} */ this._dispatchers = {}; for (const [domain, dispatcherPrototype] of inspectorBackend._dispatcherPrototypes) { this._dispatchers[domain] = Object.create(/** @type {!_DispatcherPrototype} */ (dispatcherPrototype)); this._dispatchers[domain]._dispatchers = []; } } /** * @param {string} domain * @param {!Object} dispatcher */ registerDispatcher(domain, dispatcher) { if (!this._dispatchers[domain]) { return; } this._dispatchers[domain].addDomainDispatcher(dispatcher); } /** * @param {string} domain * @param {!Object} dispatcher */ unregisterDispatcher(domain, dispatcher) { if (!this._dispatchers[domain]) { return; } this._dispatchers[domain].removeDomainDispatcher(dispatcher); } /** * @param {string} reason */ dispose(reason) { if (!this._router) { return; } this._router.unregisterSession(this._sessionId); this._router = null; } /** * @return {boolean} */ isDisposed() { return !this._router; } markAsNodeJSForTest() { this._needsNodeJSPatching = true; } /** * @return {?SessionRouter} */ router() { return this._router; } // Agent accessors, keep alphabetically sorted. /** * @return {!ProtocolProxyApi.AccessibilityApi} */ accessibilityAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.AnimationApi} */ animationAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.ApplicationCacheApi} */ applicationCacheAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.AuditsApi} */ auditsAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.BackgroundServiceApi} */ backgroundServiceAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.CacheStorageApi} */ cacheStorageAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.CSSApi} */ cssAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.DatabaseApi} */ databaseAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.DebuggerApi} */ debuggerAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.DeviceOrientationApi} */ deviceOrientationAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.DOMApi} */ domAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.DOMDebuggerApi} */ domdebuggerAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.DOMSnapshotApi} */ domsnapshotAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.DOMStorageApi} */ domstorageAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.EmulationApi} */ emulationAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.HeapProfilerApi} */ heapProfilerAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.IndexedDBApi} */ indexedDBAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.InputApi} */ inputAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.IOApi} */ ioAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.InspectorApi} */ inspectorAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.LayerTreeApi} */ layerTreeAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.LogApi} */ logAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.MediaApi} */ mediaAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.MemoryApi} */ memoryAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.NetworkApi} */ networkAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.OverlayApi} */ overlayAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.PageApi} */ pageAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.ProfilerApi} */ profilerAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.PerformanceApi} */ performanceAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.RuntimeApi} */ runtimeAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.SecurityApi} */ securityAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.ServiceWorkerApi} */ serviceWorkerAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.StorageApi} */ storageAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.TargetApi} */ targetAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.TracingApi} */ tracingAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.WebAudioApi} */ webAudioAgent() { throw new Error('Implemented in InspectorBackend.js'); } /** * @return {!ProtocolProxyApi.WebAuthnApi} */ webAuthnAgent() { throw new Error('Implemented in InspectorBackend.js'); } // Dispatcher registration, keep alphabetically sorted. /** * @param {!ProtocolProxyApi.AnimationDispatcher} dispatcher */ registerAnimationDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.ApplicationCacheDispatcher} dispatcher */ registerApplicationCacheDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.AuditsDispatcher} dispatcher */ registerAuditsDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.CSSDispatcher} dispatcher */ registerCSSDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.DatabaseDispatcher} dispatcher */ registerDatabaseDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.BackgroundServiceDispatcher} dispatcher */ registerBackgroundServiceDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.DebuggerDispatcher} dispatcher */ registerDebuggerDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.DebuggerDispatcher} dispatcher */ unregisterDebuggerDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.DOMDispatcher} dispatcher */ registerDOMDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.DOMStorageDispatcher} dispatcher */ registerDOMStorageDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.HeapProfilerDispatcher} dispatcher */ registerHeapProfilerDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.InspectorDispatcher} dispatcher */ registerInspectorDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.LayerTreeDispatcher} dispatcher */ registerLayerTreeDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.LogDispatcher} dispatcher */ registerLogDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.MediaDispatcher} dispatcher */ registerMediaDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.NetworkDispatcher} dispatcher */ registerNetworkDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.OverlayDispatcher} dispatcher */ registerOverlayDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.PageDispatcher} dispatcher */ registerPageDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.ProfilerDispatcher} dispatcher */ registerProfilerDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.RuntimeDispatcher} dispatcher */ registerRuntimeDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.SecurityDispatcher} dispatcher */ registerSecurityDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.ServiceWorkerDispatcher} dispatcher */ registerServiceWorkerDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.StorageDispatcher} dispatcher */ registerStorageDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.TargetDispatcher} dispatcher */ registerTargetDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.TracingDispatcher} dispatcher */ registerTracingDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } /** * @param {!ProtocolProxyApi.WebAudioDispatcher} dispatcher */ registerWebAudioDispatcher(dispatcher) { throw new Error('Implemented in InspectorBackend.js'); } } class _AgentPrototype { /** * @param {string} domain */ constructor(domain) { /** @type {!Object<string, !Array<string>>} */ this._replyArgs = {}; this._domain = domain; /** @type {!TargetBase} */ this._target; } /** * @param {string} methodName * @param {!Array.<!{name: string, type: string, optional: boolean}>} signature * @param {!Array.<string>} replyArgs */ registerCommand(methodName, signature, replyArgs) { const domainAndMethod = this._domain + '.' + methodName; /** * @param {...*} vararg * @this {_AgentPrototype} * @return {!Promise.<*>} */ function sendMessagePromise(vararg) { const params = Array.prototype.slice.call(arguments); return _AgentPrototype.prototype._sendMessageToBackendPromise.call(this, domainAndMethod, signature, params); } // @ts-ignore Method code generation this[methodName] = sendMessagePromise; /** * @param {!Object=} request * @return {!Promise<?Object>} * @this {_AgentPrototype} */ function invoke(request = {}) { return this._invoke(domainAndMethod, request); } // @ts-ignore Method code generation this['invoke_' + methodName] = invoke; this._replyArgs[domainAndMethod] = replyArgs; } /** * @param {string} method * @param {!Array.<!{name: string, type: string, optional: boolean}>} signature * @param {!Array.<*>} args * @param {function(string):void} errorCallback * @return {?Object} */ _prepareParameters(method, signature, args, errorCallback) { /** @type {!Object<string, *>} */ const params = {}; let hasParams = false; for (const param of signature) { 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(signature)}'.`); 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; } /** * @param {string} method * @param {!Array<!{name: string, type: string, optional: boolean}>} signature * @param {!Array<*>} args * @return {!Promise<?>} */ _sendMessageToBackendPromise(method, signature, args) { let errorMessage; /** * @param {string} message */ function onError(message) { console.error(message); errorMessage = message; } const params = this._prepareParameters(method, signature, args, onError); if (errorMessage) { return Promise.resolve(null); } return new Promise(resolve => { /** * @param {*} error * @param {*} result */ const callback = (error, result) => { 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); }; if (!this._target._router) { SessionRouter.dispatchConnectionError(callback, method); } else { this._target._router.sendMessage(this._target._sessionId, this._domain, method, params, callback); } }); } /** * @param {string} method * @param {?Object} request * @return {!Promise<!Object>} */ _invoke(method, request) { return new Promise(fulfill => { /** * @param {*} error * @param {*} result */ const callback = (error, result) => { if (error && !test.suppressRequestErrors && error.code !== DevToolsStubErrorCode && error.code !== _GenericError && error.code !== _ConnectionClosedErrorCode) { console.error('Request ' + method + ' failed. ' + JSON.stringify(error)); } if (!result) { result = {}; } if (error) { // TODO(crbug.com/1011811): Remove Old lookup of ProtocolError result[ProtocolError] = error.message; result.getError = () => { return error.message; }; } else { result.getError = () => { return undefined; }; } fulfill(result); }; if (!this._target._router) { SessionRouter.dispatchConnectionError(callback, method); } else { this._target._router.sendMessage(this._target._sessionId, this._domain, method, request, callback); } }); } } class _DispatcherPrototype { constructor() { /** @type {!Object<string, *>} */ this._eventArgs = {}; /** @type {!Array<*>} */ this._dispatchers; } /** * @param {string} eventName * @param {!Object} params */ registerEvent(eventName, params) { this._eventArgs[eventName] = params; } /** * @param {!Object} dispatcher */ addDomainDispatcher(dispatcher) { this._dispatchers.push(dispatcher); } /** * @param {!Object} dispatcher */ removeDomainDispatcher(dispatcher) { const index = this._dispatchers.indexOf(dispatcher); if (index === -1) { return; } this._dispatchers.splice(index, 1); } /** * @param {string} functionName * @param {!{method: string, params: ?Object.<string, *>|undefined}} messageObject */ dispatch(functionName, messageObject) { if (!this._dispatchers.length) { return; } if (!this._eventArgs[messageObject.method]) { InspectorBackend.reportProtocolWarning( `Protocol Warning: Attempted to dispatch an unspecified method '${messageObject.method}'`, messageObject); return; } const messageArgument = {...messageObject.params}; for (let index = 0; index < this._dispatchers.length; ++index) { const dispatcher = this._dispatchers[index]; if (functionName in dispatcher) { dispatcher[functionName].call(dispatcher, messageArgument); } } } } /** * Takes error and result. * @typedef {function(?Object, ?Object):void} */ // @ts-ignore typedef export let _Callback; /** * Takes error and result. * @typedef {!{callback: function(?Object, ?Object):void, method: string}} */ // @ts-ignore typedef export let _CallbackWithDebugInfo; export const inspectorBackend = new InspectorBackend();