| /* |
| * 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: <https://webkit.org/b/213632> 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: <https://webkit.org/b/201150> 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: <https://webkit.org/b/213632> 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); |
| } |
| }; |