1/* 2 * Copyright (C) 2011 Google Inc. All rights reserved. 3 * Copyright (C) 2013 Apple Inc. All rights reserved. 4 * Copyright (C) 2014 University of Washington. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are 8 * met: 9 * 10 * * Redistributions of source code must retain the above copyright 11 * notice, this list of conditions and the following disclaimer. 12 * * Redistributions in binary form must reproduce the above 13 * copyright notice, this list of conditions and the following disclaimer 14 * in the documentation and/or other materials provided with the 15 * distribution. 16 * * Neither the name of Google Inc. nor the names of its 17 * contributors may be used to endorse or promote products derived from 18 * this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33function InspectorBackendClass() 34{ 35 this._lastSequenceId = 1; 36 this._pendingResponsesCount = 0; 37 this._callbackData = new Map; 38 this._agents = {}; 39 this._deferredScripts = []; 40 41 this.dumpInspectorTimeStats = false; 42 this.dumpInspectorProtocolMessages = false; 43 this.warnForLongMessageHandling = false; 44 this.longMessageHandlingThreshold = 10; // milliseconds. 45} 46 47InspectorBackendClass.prototype = { 48 49 // Public 50 51 registerCommand: function(qualifiedName, callSignature, replySignature) 52 { 53 var [domainName, commandName] = qualifiedName.split("."); 54 var agent = this._agentForDomain(domainName); 55 agent.addCommand(InspectorBackend.Command.create(this, qualifiedName, callSignature, replySignature)); 56 }, 57 58 registerEnum: function(qualifiedName, enumValues) 59 { 60 var [domainName, enumName] = qualifiedName.split("."); 61 var agent = this._agentForDomain(domainName); 62 agent.addEnum(enumName, enumValues); 63 }, 64 65 registerEvent: function(qualifiedName, signature) 66 { 67 var [domainName, eventName] = qualifiedName.split("."); 68 var agent = this._agentForDomain(domainName); 69 agent.addEvent(new InspectorBackend.Event(eventName, signature)); 70 }, 71 72 registerDomainDispatcher: function(domainName, dispatcher) 73 { 74 var agent = this._agentForDomain(domainName); 75 agent.dispatcher = dispatcher; 76 }, 77 78 dispatch: function(message) 79 { 80 if (this.dumpInspectorProtocolMessages) 81 console.log("backend: " + ((typeof message === "string") ? message : JSON.stringify(message))); 82 83 var messageObject = (typeof message === "string") ? JSON.parse(message) : message; 84 85 if ("id" in messageObject) 86 this._dispatchCallback(messageObject); 87 else 88 this._dispatchEvent(messageObject); 89 }, 90 91 runAfterPendingDispatches: function(script) 92 { 93 console.assert(script); 94 console.assert(typeof script === "function"); 95 96 if (!this._pendingResponsesCount) 97 script.call(this); 98 else 99 this._deferredScripts.push(script); 100 }, 101 102 // Private 103 104 _agentForDomain: function(domainName) 105 { 106 if (this._agents[domainName]) 107 return this._agents[domainName]; 108 109 var agent = new InspectorBackend.Agent(domainName); 110 this._agents[domainName] = agent; 111 window[domainName + "Agent"] = agent; 112 return agent; 113 }, 114 115 _willSendMessageToBackend: function(command, callback) 116 { 117 ++this._pendingResponsesCount; 118 var sequenceId = this._lastSequenceId++; 119 120 if (callback && typeof callback === "function") { 121 var callbackData = { 122 "callback": callback, 123 "command": command, 124 }; 125 126 if (this.dumpInspectorTimeStats) 127 callbackData.sendRequestTime = Date.now(); 128 129 this._callbackData.set(sequenceId, callbackData); 130 } 131 132 return sequenceId; 133 }, 134 135 _dispatchCallback: function(messageObject) 136 { 137 --this._pendingResponsesCount; 138 console.assert(this._pendingResponsesCount >= 0); 139 140 if (messageObject["error"]) { 141 if (messageObject["error"].code !== -32000) 142 this._reportProtocolError(messageObject); 143 } 144 145 var callbackData = this._callbackData.get(messageObject["id"]); 146 if (callbackData && typeof callbackData.callback === "function") { 147 var command = callbackData.command; 148 var callback = callbackData.callback; 149 var callbackArguments = []; 150 151 callbackArguments.push(messageObject["error"] ? messageObject["error"].message : null); 152 153 if (messageObject["result"]) { 154 // FIXME: this should be indicated by invoking the command differently, rather 155 // than by setting a magical property on the callback. <webkit.org/b/132386> 156 if (callback.expectsResultObject) { 157 // The callback expects results as an object with properties, this is useful 158 // for backwards compatibility with renamed or different parameters. 159 callbackArguments.push(messageObject["result"]); 160 } else { 161 for (var parameterName of command.replySignature) 162 callbackArguments.push(messageObject["result"][parameterName]); 163 } 164 } 165 166 var processingStartTime; 167 if (this.dumpInspectorTimeStats) 168 processingStartTime = Date.now(); 169 170 try { 171 callback.apply(null, callbackArguments); 172 } catch (e) { 173 console.error("Uncaught exception in inspector page while dispatching callback for command " + command.qualifiedName + ": ", e); 174 } 175 176 var processingDuration = Date.now() - processingStartTime; 177 if (this.warnForLongMessageHandling && processingDuration > this.longMessageHandlingThreshold) 178 console.warn("InspectorBackend: took " + processingDuration + "ms to handle response for command: " + command.qualifiedName); 179 180 if (this.dumpInspectorTimeStats) 181 console.log("time-stats: Handling: " + processingDuration + "ms; RTT: " + (processingStartTime - callbackData.sendRequestTime) + "ms; (command " + command.qualifiedName + ")"); 182 183 this._callbackData.delete(messageObject["id"]); 184 } 185 186 if (this._deferredScripts.length && !this._pendingResponsesCount) 187 this._flushPendingScripts(); 188 }, 189 190 _dispatchEvent: function(messageObject) 191 { 192 var qualifiedName = messageObject["method"]; 193 var [domainName, eventName] = qualifiedName.split("."); 194 if (!(domainName in this._agents)) { 195 console.error("Protocol Error: Attempted to dispatch method '" + eventName + "' for non-existing domain '" + domainName + "'"); 196 return; 197 } 198 199 var agent = this._agentForDomain(domainName); 200 var event = agent.getEvent(eventName); 201 if (!event) { 202 console.error("Protocol Error: Attempted to dispatch an unspecified method '" + qualifiedName + "'"); 203 return; 204 } 205 206 var eventArguments = []; 207 if (messageObject["params"]) { 208 var parameterNames = event.parameterNames; 209 for (var i = 0; i < parameterNames.length; ++i) 210 eventArguments.push(messageObject["params"][parameterNames[i]]); 211 } 212 213 var processingStartTime; 214 if (this.dumpInspectorTimeStats) 215 processingStartTime = Date.now(); 216 217 try { 218 agent.dispatchEvent(eventName, eventArguments); 219 } catch (e) { 220 console.error("Uncaught exception in inspector page while handling event " + qualifiedName + ": ", e); 221 } 222 223 var processingDuration = Date.now() - processingStartTime; 224 if (this.warnForLongMessageHandling && processingDuration > this.longMessageHandlingThreshold) 225 console.warn("InspectorBackend: took " + processingDuration + "ms to handle event: " + messageObject["method"]); 226 227 if (this.dumpInspectorTimeStats) 228 console.log("time-stats: Handling: " + processingDuration + "ms (event " + messageObject["method"] + ")"); 229 }, 230 231 _invokeCommand: function(command, parameters, callback) 232 { 233 var messageObject = {}; 234 messageObject["method"] = command.qualifiedName; 235 236 if (parameters) 237 messageObject["params"] = parameters; 238 239 // We always assign an id as a sequence identifier. 240 // Callback data is saved only if a callback is actually passed. 241 messageObject["id"] = this._willSendMessageToBackend(command, callback); 242 243 var stringifiedMessage = JSON.stringify(messageObject); 244 if (this.dumpInspectorProtocolMessages) 245 console.log("frontend: " + stringifiedMessage); 246 247 InspectorFrontendHost.sendMessageToBackend(stringifiedMessage); 248 }, 249 250 _reportProtocolError: function(messageObject) 251 { 252 console.error("Request with id = " + messageObject["id"] + " failed. " + JSON.stringify(messageObject["error"])); 253 }, 254 255 _flushPendingScripts: function() 256 { 257 console.assert(!this._pendingResponsesCount); 258 259 var scriptsToRun = this._deferredScripts; 260 this._deferredScripts = []; 261 for (var script of scriptsToRun) 262 script.call(this); 263 } 264} 265 266InspectorBackend = new InspectorBackendClass(); 267 268InspectorBackend.Agent = function(domainName) 269{ 270 this._domainName = domainName; 271 272 // Commands are stored directly on the Agent instance using their unqualified 273 // method name as the property. Thus, callers can write: FooAgent.methodName(). 274 // Enums are stored similarly based on the unqualified type name. 275 this._events = {}; 276} 277 278InspectorBackend.Agent.prototype = { 279 get domainName() 280 { 281 return this._domainName; 282 }, 283 284 set dispatcher(value) 285 { 286 this._dispatcher = value; 287 }, 288 289 addEnum: function(enumName, enumValues) 290 { 291 this[enumName] = enumValues; 292 }, 293 294 addCommand: function(command) 295 { 296 this[command.commandName] = command; 297 }, 298 299 addEvent: function(event) 300 { 301 this._events[event.eventName] = event; 302 }, 303 304 getEvent: function(eventName) 305 { 306 return this._events[eventName]; 307 }, 308 309 hasEvent: function(eventName) 310 { 311 return eventName in this._events; 312 }, 313 314 dispatchEvent: function(eventName, eventArguments) 315 { 316 if (!(eventName in this._dispatcher)) { 317 console.error("Protocol Error: Attempted to dispatch an unimplemented method '" + this._domainName + "." + eventName + "'"); 318 return false; 319 } 320 321 this._dispatcher[eventName].apply(this._dispatcher, eventArguments); 322 return true; 323 } 324} 325 326InspectorBackend.Command = function(backend, qualifiedName, callSignature, replySignature) 327{ 328 this._backend = backend; 329 this._instance = this; 330 331 var [domainName, commandName] = qualifiedName.split("."); 332 this._qualifiedName = qualifiedName; 333 this._commandName = commandName; 334 this._callSignature = callSignature || []; 335 this._replySignature = replySignature || []; 336} 337 338InspectorBackend.Command.create = function(backend, commandName, callSignature, replySignature) 339{ 340 var instance = new InspectorBackend.Command(backend, commandName, callSignature, replySignature); 341 342 function callable() { 343 instance._invokeWithArguments.apply(instance, arguments); 344 } 345 callable._instance = instance; 346 callable.__proto__ = InspectorBackend.Command.prototype; 347 return callable; 348} 349 350// As part of the workaround to make commands callable, these functions use |this._instance|. 351// |this| could refer to the callable trampoline, or the InspectorBackend.Command instance. 352InspectorBackend.Command.prototype = { 353 __proto__: Function.prototype, 354 355 // Public 356 357 get qualifiedName() 358 { 359 return this._instance._qualifiedName; 360 }, 361 362 get commandName() 363 { 364 return this._instance._commandName; 365 }, 366 367 get callSignature() 368 { 369 return this._instance._callSignature; 370 }, 371 372 get replySignature() 373 { 374 return this._instance._replySignature; 375 }, 376 377 invoke: function(commandArguments, callback) 378 { 379 var instance = this._instance; 380 instance._backend._invokeCommand(instance, commandArguments, callback); 381 }, 382 383 promise: function() 384 { 385 var instance = this._instance; 386 var promiseArguments = Array.prototype.slice.call(arguments); 387 return new Promise(function(resolve, reject) { 388 function convertToPromiseCallback(error, payload) { 389 return error ? reject(error) : resolve(payload); 390 } 391 promiseArguments.push(convertToPromiseCallback); 392 instance._invokeWithArguments.apply(instance, promiseArguments); 393 }); 394 }, 395 396 supports: function(parameterName) 397 { 398 var instance = this._instance; 399 return instance.callSignature.some(function(parameter) { 400 return parameter["name"] === parameterName; 401 }); 402 }, 403 404 // Private 405 406 _invokeWithArguments: function() 407 { 408 var instance = this._instance; 409 var commandArguments = Array.prototype.slice.call(arguments); 410 var callback = typeof commandArguments.lastValue === "function" ? commandArguments.pop() : null; 411 412 var parameters = {}; 413 for (var i = 0; i < instance.callSignature.length; ++i) { 414 var parameter = instance.callSignature[i]; 415 var parameterName = parameter["name"]; 416 var typeName = parameter["type"]; 417 var optionalFlag = parameter["optional"]; 418 419 if (!commandArguments.length && !optionalFlag) { 420 console.error("Protocol Error: Invalid number of arguments for method '" + instance.qualifiedName + "' call. It must have the following arguments '" + JSON.stringify(signature) + "'."); 421 return; 422 } 423 424 var value = commandArguments.shift(); 425 if (optionalFlag && typeof value === "undefined") 426 continue; 427 428 if (typeof value !== typeName) { 429 console.error("Protocol Error: Invalid type of argument '" + parameterName + "' for method '" + instance.qualifiedName + "' call. It must be '" + typeName + "' but it is '" + typeof value + "'."); 430 return; 431 } 432 433 parameters[parameterName] = value; 434 } 435 436 if (commandArguments.length === 1 && !callback) { 437 if (typeof commandArguments[0] !== "undefined") { 438 console.error("Protocol Error: Optional callback argument for method '" + instance.qualifiedName + "' call must be a function but its type is '" + typeof args[0] + "'."); 439 return; 440 } 441 } 442 443 instance._backend._invokeCommand(instance, Object.keys(parameters).length ? parameters : null, callback); 444 }, 445} 446 447InspectorBackend.Event = function(eventName, parameterNames) 448{ 449 this.eventName = eventName; 450 this.parameterNames = parameterNames; 451} 452