blob: b466212e450a1d23026a0d7bf779ecbbf30635d5 [file] [log] [blame]
/*
* 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];
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()
{
return this._activeDomains;
}
// 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 || Object.values(WI.TargetType);
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
if (debuggableTypes && !debuggableTypes.includes(InspectorFrontendHost.debuggableType()))
return;
console.assert(domainName in this._registeredDomains);
console.assert(!(domainName in this._activeDomains));
let domain = this._registeredDomains[domainName];
this._activeDomains[domainName] = domain;
for (let command of domain._supportedCommandsForTargetType.values()) {
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 event of domain._supportedEventsForTargetType.values()) {
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(Object.values(WI.TargetType).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;
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 || Object.values(WI.TargetType);
for (let type of targetTypes)
this._supportedCommandsForTargetType.add(type, command);
}
_addEvent(targetTypes, event)
{
targetTypes = targetTypes || Object.values(WI.TargetType);
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);
}
};