/* * Copyright (C) 2011 Google Inc. All rights reserved. * Copyright (C) 2013, 2015, 2016 Apple Inc. All rights reserved. * Copyright (C) 2014 University of Washington. * * 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. */ InspectorBackendClass = class InspectorBackendClass { constructor() { this._registeredDomains = {}; this._activeDomains = {}; this._customTracer = null; this._defaultTracer = new WI.LoggingProtocolTracer; this._activeTracers = [this._defaultTracer]; // FIXME: Web Inspector: release unused backend domains/events/commands once the debuggable type is known this._supportedDomainsForTargetType = new Multimap; this._supportedCommandParameters = new Map; this._supportedEventParameters = new Map; WI.settings.protocolAutoLogMessages.addEventListener(WI.Setting.Event.Changed, this._startOrStopAutomaticTracing, this); WI.settings.protocolAutoLogTimeStats.addEventListener(WI.Setting.Event.Changed, this._startOrStopAutomaticTracing, this); this._startOrStopAutomaticTracing(); this.currentDispatchState = { event: null, request: null, response: null, }; } // Public // This should only be used for getting enum values (`InspectorBackend.Enum.Domain.Type.Item`). // Domain/Command/Event feature checking should use one of the `has*` functions below. get Enum() { // Enums should not be conditionally enabled by debuggable and/or target type. return this._registeredDomains; } // It's still possible to set this flag on InspectorBackend to just // dump protocol traffic as it happens. For more complex uses of // protocol data, install a subclass of WI.ProtocolTracer. set dumpInspectorProtocolMessages(value) { // Implicitly cause automatic logging to start if it's allowed. WI.settings.protocolAutoLogMessages.value = value; this._defaultTracer.dumpMessagesToConsole = value; } get dumpInspectorProtocolMessages() { return WI.settings.protocolAutoLogMessages.value; } set dumpInspectorTimeStats(value) { WI.settings.protocolAutoLogTimeStats.value = value; if (!this.dumpInspectorProtocolMessages) this.dumpInspectorProtocolMessages = true; this._defaultTracer.dumpTimingDataToConsole = value; } get dumpInspectorTimeStats() { return WI.settings.protocolAutoLogTimeStats.value; } set filterMultiplexingBackendInspectorProtocolMessages(value) { WI.settings.protocolFilterMultiplexingBackendMessages.value = value; this._defaultTracer.filterMultiplexingBackend = value; } get filterMultiplexingBackendInspectorProtocolMessages() { return WI.settings.protocolFilterMultiplexingBackendMessages.value; } set customTracer(tracer) { console.assert(!tracer || tracer instanceof WI.ProtocolTracer, tracer); console.assert(!tracer || tracer !== this._defaultTracer, tracer); // Bail early if no state change is to be made. if (!tracer && !this._customTracer) return; if (tracer === this._customTracer) return; if (tracer === this._defaultTracer) return; if (this._customTracer) this._customTracer.logFinished(); this._customTracer = tracer; this._activeTracers = [this._defaultTracer]; if (this._customTracer) { this._customTracer.logStarted(); this._activeTracers.push(this._customTracer); } } get activeTracers() { return this._activeTracers; } registerDomain(domainName, targetTypes) { targetTypes = targetTypes || WI.TargetType.all; for (let targetType of targetTypes) this._supportedDomainsForTargetType.add(targetType, domainName); this._registeredDomains[domainName] = new InspectorBackend.Domain(domainName); } registerVersion(domainName, version) { let domain = this._registeredDomains[domainName]; domain.VERSION = version; } registerEnum(qualifiedName, enumValues) { let [domainName, enumName] = qualifiedName.split("."); let domain = this._registeredDomains[domainName]; domain._addEnum(enumName, enumValues); } registerCommand(qualifiedName, targetTypes, callSignature, replySignature) { let [domainName, commandName] = qualifiedName.split("."); let domain = this._registeredDomains[domainName]; domain._addCommand(targetTypes, new InspectorBackend.Command(qualifiedName, commandName, callSignature, replySignature)); } registerEvent(qualifiedName, targetTypes, signature) { let [domainName, eventName] = qualifiedName.split("."); let domain = this._registeredDomains[domainName]; domain._addEvent(targetTypes, new InspectorBackend.Event(qualifiedName, eventName, signature)); } registerDispatcher(domainName, dispatcher) { let domain = this._registeredDomains[domainName]; domain._addDispatcher(dispatcher); } activateDomain(domainName, debuggableTypes) { // FIXME: Web Inspector: remove "extra domains" concept now that domains can be added based on the debuggable type // Ask `WI.sharedApp` (if it exists) as it may have a different debuggable type if extra // domains were activated, which is the only other time this will be called. let currentDebuggableType = WI.sharedApp?.debuggableType || InspectorFrontendHost.debuggableInfo.debuggableType; if (debuggableTypes && !debuggableTypes.includes(currentDebuggableType)) return; console.assert(domainName in this._registeredDomains); console.assert(!(domainName in this._activeDomains)); let domain = this._registeredDomains[domainName]; this._activeDomains[domainName] = domain; let supportedTargetTypes = WI.DebuggableType.supportedTargetTypes(currentDebuggableType); for (let [targetType, command] of domain._supportedCommandsForTargetType) { if (!supportedTargetTypes.has(targetType)) continue; let parameters = this._supportedCommandParameters.get(command._qualifiedName); if (!parameters) { parameters = new Set; this._supportedCommandParameters.set(command._qualifiedName, parameters); } parameters.addAll(command._callSignature.map((item) => item.name)); } for (let [targetType, event] of domain._supportedEventsForTargetType) { if (!supportedTargetTypes.has(targetType)) continue; let parameters = this._supportedEventParameters.get(event._qualifiedName); if (!parameters) { parameters = new Set; this._supportedEventParameters.set(event._qualifiedName, parameters); } parameters.addAll(event._parameterNames); } } dispatch(message) { InspectorBackend.backendConnection.dispatch(message); } runAfterPendingDispatches(callback) { if (!WI.mainTarget) { callback(); return; } // FIXME: Should this respect pending dispatches in all connections? WI.mainTarget.connection.runAfterPendingDispatches(callback); } supportedDomainsForTargetType(type) { console.assert(WI.TargetType.all.includes(type), "Unknown target type", type); return this._supportedDomainsForTargetType.get(type) || new Set; } hasDomain(domainName) { console.assert(!domainName.includes(".") && !domainName.endsWith("Agent")); return domainName in this._activeDomains; } hasCommand(qualifiedName, parameterName) { console.assert(qualifiedName.includes(".") && !qualifiedName.includes("Agent.")); let parameters = this._supportedCommandParameters.get(qualifiedName); if (!parameters) return false; return parameterName === undefined || parameters.has(parameterName); } hasEvent(qualifiedName, parameterName) { console.assert(qualifiedName.includes(".") && !qualifiedName.includes("Agent.")); let parameters = this._supportedEventParameters.get(qualifiedName); if (!parameters) return false; return parameterName === undefined || parameters.has(parameterName); } getVersion(domainName) { console.assert(!domainName.includes(".") && !domainName.endsWith("Agent")); let domain = this._activeDomains[domainName]; if (domain && "VERSION" in domain) return domain.VERSION; return -Infinity; } invokeCommand(qualifiedName, targetType, connection, commandArguments, callback) { let [domainName, commandName] = qualifiedName.split("."); let domain = this._activeDomains[domainName]; return domain._invokeCommand(commandName, targetType, connection, commandArguments, callback); } // Private _makeAgent(domainName, target) { let domain = this._activeDomains[domainName]; return domain._makeAgent(target); } _startOrStopAutomaticTracing() { this._defaultTracer.dumpMessagesToConsole = this.dumpInspectorProtocolMessages; this._defaultTracer.dumpTimingDataToConsole = this.dumpTimingDataToConsole; this._defaultTracer.filterMultiplexingBackend = this.filterMultiplexingBackendInspectorProtocolMessages; } }; InspectorBackend = new InspectorBackendClass; InspectorBackend.Domain = class InspectorBackendDomain { constructor(domainName) { this._domainName = domainName; // Enums are stored directly on the Domain instance using their unqualified // type name as the property. Thus, callers can write: Domain.EnumType. this._dispatcher = null; // FIXME: Web Inspector: release unused backend domains/events/commands once the debuggable type is known this._supportedCommandsForTargetType = new Multimap; this._supportedEventsForTargetType = new Multimap; } // Private _addEnum(enumName, enumValues) { console.assert(!(enumName in this)); this[enumName] = enumValues; } _addCommand(targetTypes, command) { targetTypes = targetTypes || WI.TargetType.all; for (let type of targetTypes) this._supportedCommandsForTargetType.add(type, command); } _addEvent(targetTypes, event) { targetTypes = targetTypes || WI.TargetType.all; for (let type of targetTypes) this._supportedEventsForTargetType.add(type, event); } _addDispatcher(dispatcher) { console.assert(!this._dispatcher); this._dispatcher = dispatcher; } _makeAgent(target) { let commands = this._supportedCommandsForTargetType.get(target.type) || new Set; let events = this._supportedEventsForTargetType.get(target.type) || new Set; return new InspectorBackend.Agent(target, commands, events, this._dispatcher); } _invokeCommand(commandName, targetType, connection, commandArguments, callback) { let commands = this._supportedCommandsForTargetType.get(targetType); for (let command of commands) { if (command._commandName === commandName) return command._makeCallable(connection).invoke(commandArguments, callback); } console.assert(); } }; InspectorBackend.Agent = class InspectorBackendAgent { constructor(target, commands, events, dispatcher) { // Commands are stored directly on the Agent instance using their unqualified // method name as the property. Thus, callers can write: DomainAgent.commandName(). for (let command of commands) { this[command._commandName] = command._makeCallable(target.connection); target._supportedCommandParameters.set(command._qualifiedName, command); } this._events = {}; for (let event of events) { this._events[event._eventName] = event; target._supportedEventParameters.set(event._qualifiedName, event); } this._dispatcher = dispatcher ? new dispatcher(target) : null; } }; InspectorBackend.Dispatcher = class InspectorBackendDispatcher { constructor(target) { console.assert(target instanceof WI.Target); this._target = target; } }; InspectorBackend.Command = class InspectorBackendCommand { constructor(qualifiedName, commandName, callSignature, replySignature) { this._qualifiedName = qualifiedName; this._commandName = commandName; this._callSignature = callSignature || []; this._replySignature = replySignature || []; } // Private _hasParameter(parameterName) { return this._callSignature.some((item) => item.name === parameterName); } _makeCallable(connection) { let instance = new InspectorBackend.Callable(this, connection); function callable() { console.assert(this instanceof InspectorBackend.Agent); return instance._invokeWithArguments.call(instance, Array.from(arguments)); } callable._instance = instance; Object.setPrototypeOf(callable, InspectorBackend.Callable.prototype); return callable; } }; InspectorBackend.Event = class InspectorBackendEvent { constructor(qualifiedName, eventName, parameterNames) { this._qualifiedName = qualifiedName; this._eventName = eventName; this._parameterNames = parameterNames || []; } // Private _hasParameter(parameterName) { return this._parameterNames.includes(parameterName); } }; // InspectorBackend.Callable can't use ES6 classes because of its trampoline nature. // But we can use strict mode to get stricter handling of the code inside its functions. InspectorBackend.Callable = function(command, connection) { "use strict"; this._command = command; this._connection = connection; this._instance = this; }; // As part of the workaround to make commands callable, these functions use `this._instance`. // `this` could refer to the callable trampoline, or the InspectorBackend.Callable instance. InspectorBackend.Callable.prototype = { __proto__: Function.prototype, // Public invoke(commandArguments, callback) { "use strict"; let command = this._instance._command; let connection = this._instance._connection; function deliverFailure(message) { console.error(`Protocol Error: ${message}`); if (callback) setTimeout(callback.bind(null, message), 0); else return Promise.reject(new Error(message)); } if (typeof commandArguments !== "object") return deliverFailure(`invoke expects an object for command arguments but its type is '${typeof commandArguments}'.`); let parameters = {}; for (let {name, type, optional} of command._callSignature) { if (!(name in commandArguments) && !optional) return deliverFailure(`Missing argument '${name}' for command '${command._qualifiedName}'.`); let value = commandArguments[name]; if (optional && value === undefined) continue; if (typeof value !== type) return deliverFailure(`Invalid type of argument '${name}' for command '${command._qualifiedName}' call. It must be '${type}' but it is '${typeof value}'.`); parameters[name] = value; } if (typeof callback === "function") connection._sendCommandToBackendWithCallback(command, parameters, callback); else return connection._sendCommandToBackendExpectingPromise(command, parameters); }, // Private _invokeWithArguments(commandArguments) { "use strict"; let command = this._instance._command; let connection = this._instance._connection; let callback = typeof commandArguments.lastValue === "function" ? commandArguments.pop() : null; function deliverFailure(message) { console.error(`Protocol Error: ${message}`); if (callback) setTimeout(callback.bind(null, message), 0); else return Promise.reject(new Error(message)); } let parameters = {}; for (let {name, type, optional} of command._callSignature) { if (!commandArguments.length && !optional) return deliverFailure(`Invalid number of arguments for command '${command._qualifiedName}'.`); let value = commandArguments.shift(); if (optional && value === undefined) continue; if (typeof value !== type) return deliverFailure(`Invalid type of argument '${name}' for command '${command._qualifiedName}' call. It must be '${type}' but it is '${typeof value}'.`); parameters[name] = value; } if (!callback && commandArguments.length === 1 && commandArguments[0] !== undefined) return deliverFailure(`Protocol Error: Optional callback argument for command '${command._qualifiedName}' call must be a function but its type is '${typeof commandArguments[0]}'.`); if (callback) connection._sendCommandToBackendWithCallback(command, parameters, callback); else return connection._sendCommandToBackendExpectingPromise(command, parameters); } };