1/* 2 * Copyright (C) 2011 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31/** 32 * @constructor 33 */ 34function InspectorBackendClass() 35{ 36 this._lastCallbackId = 1; 37 this._pendingResponsesCount = 0; 38 this._callbacks = {}; 39 this._domainDispatchers = {}; 40 this._eventArgs = {}; 41 this._replyArgs = {}; 42 43 this.dumpInspectorTimeStats = false; 44 this.dumpInspectorProtocolMessages = false; 45 this._initialized = false; 46} 47 48InspectorBackendClass.prototype = { 49 _wrap: function(callback, method) 50 { 51 var callbackId = this._lastCallbackId++; 52 if (!callback) 53 callback = function() {}; 54 55 this._callbacks[callbackId] = callback; 56 callback.methodName = method; 57 if (this.dumpInspectorTimeStats) 58 callback.sendRequestTime = Date.now(); 59 60 return callbackId; 61 }, 62 63 _getAgent: function(domain) 64 { 65 var agentName = domain + "Agent"; 66 if (!window[agentName]) 67 window[agentName] = {}; 68 return window[agentName]; 69 }, 70 71 registerCommand: function(method, signature, replyArgs) 72 { 73 var domainAndMethod = method.split("."); 74 var agent = this._getAgent(domainAndMethod[0]); 75 76 agent[domainAndMethod[1]] = this._sendMessageToBackend.bind(this, method, signature); 77 agent[domainAndMethod[1]]["invoke"] = this._invoke.bind(this, method, signature); 78 this._replyArgs[method] = replyArgs; 79 80 this._initialized = true; 81 }, 82 83 registerEnum: function(type, values) 84 { 85 var domainAndMethod = type.split("."); 86 var agent = this._getAgent(domainAndMethod[0]); 87 88 agent[domainAndMethod[1]] = values; 89 90 this._initialized = true; 91 }, 92 93 registerEvent: function(eventName, params) 94 { 95 this._eventArgs[eventName] = params; 96 97 this._initialized = true; 98 }, 99 100 _invoke: function(method, signature, args, callback) 101 { 102 this._wrapCallbackAndSendMessageObject(method, args, callback); 103 }, 104 105 _sendMessageToBackend: function(method, signature, vararg) 106 { 107 var args = Array.prototype.slice.call(arguments, 2); 108 var callback = (args.length && typeof args[args.length - 1] === "function") ? args.pop() : null; 109 110 var params = {}; 111 var hasParams = false; 112 for (var i = 0; i < signature.length; ++i) { 113 var param = signature[i]; 114 var paramName = param["name"]; 115 var typeName = param["type"]; 116 var optionalFlag = param["optional"]; 117 118 if (!args.length && !optionalFlag) { 119 console.error("Protocol Error: Invalid number of arguments for method '" + method + "' call. It must have the following arguments '" + JSON.stringify(signature) + "'."); 120 return; 121 } 122 123 var value = args.shift(); 124 if (optionalFlag && typeof value === "undefined") { 125 continue; 126 } 127 128 if (typeof value !== typeName) { 129 console.error("Protocol Error: Invalid type of argument '" + paramName + "' for method '" + method + "' call. It must be '" + typeName + "' but it is '" + typeof value + "'."); 130 return; 131 } 132 133 params[paramName] = value; 134 hasParams = true; 135 } 136 137 if (args.length === 1 && !callback) { 138 if (typeof args[0] !== "undefined") { 139 console.error("Protocol Error: Optional callback argument for method '" + method + "' call must be a function but its type is '" + typeof args[0] + "'."); 140 return; 141 } 142 } 143 144 this._wrapCallbackAndSendMessageObject(method, hasParams ? params : null, callback); 145 }, 146 147 _wrapCallbackAndSendMessageObject: function(method, params, callback) 148 { 149 var messageObject = {}; 150 messageObject.method = method; 151 if (params) 152 messageObject.params = params; 153 messageObject.id = this._wrap(callback, method); 154 155 if (this.dumpInspectorProtocolMessages) 156 console.log("frontend: " + JSON.stringify(messageObject)); 157 158 ++this._pendingResponsesCount; 159 this.sendMessageObjectToBackend(messageObject); 160 }, 161 162 sendMessageObjectToBackend: function(messageObject) 163 { 164 if (this._disconnected) 165 return; 166 var message = JSON.stringify(messageObject); 167 InspectorFrontendHost.sendMessageToBackend(message); 168 }, 169 170 disconnect: function() 171 { 172 this._disconnected = true; 173 }, 174 175 registerDomainDispatcher: function(domain, dispatcher) 176 { 177 this._domainDispatchers[domain] = dispatcher; 178 }, 179 180 dispatch: function(message) 181 { 182 if (this.dumpInspectorProtocolMessages) 183 console.log("backend: " + ((typeof message === "string") ? message : JSON.stringify(message))); 184 185 var messageObject = (typeof message === "string") ? JSON.parse(message) : message; 186 187 if ("id" in messageObject) { // just a response for some request 188 if (messageObject.error) { 189 if (messageObject.error.code !== -32000) 190 this.reportProtocolError(messageObject); 191 } 192 193 var callback = this._callbacks[messageObject.id]; 194 if (callback) { 195 var argumentsArray = []; 196 if (messageObject.result) { 197 var paramNames = this._replyArgs[callback.methodName]; 198 if (paramNames) { 199 for (var i = 0; i < paramNames.length; ++i) 200 argumentsArray.push(messageObject.result[paramNames[i]]); 201 } 202 } 203 204 var processingStartTime; 205 if (this.dumpInspectorTimeStats && callback.methodName) 206 processingStartTime = Date.now(); 207 208 argumentsArray.unshift(messageObject.error ? messageObject.error.message : null); 209 callback.apply(null, argumentsArray); 210 --this._pendingResponsesCount; 211 delete this._callbacks[messageObject.id]; 212 213 if (this.dumpInspectorTimeStats && callback.methodName) 214 console.log("time-stats: " + callback.methodName + " = " + (processingStartTime - callback.sendRequestTime) + " + " + (Date.now() - processingStartTime)); 215 } 216 217 if (this._scripts && !this._pendingResponsesCount) 218 this.runAfterPendingDispatches(); 219 220 return; 221 } else { 222 var method = messageObject.method.split("."); 223 var domainName = method[0]; 224 var functionName = method[1]; 225 if (!(domainName in this._domainDispatchers)) { 226 console.error("Protocol Error: the message is for non-existing domain '" + domainName + "'"); 227 return; 228 } 229 var dispatcher = this._domainDispatchers[domainName]; 230 if (!(functionName in dispatcher)) { 231 console.error("Protocol Error: Attempted to dispatch an unimplemented method '" + messageObject.method + "'"); 232 return; 233 } 234 235 if (!this._eventArgs[messageObject.method]) { 236 console.error("Protocol Error: Attempted to dispatch an unspecified method '" + messageObject.method + "'"); 237 return; 238 } 239 240 var params = []; 241 if (messageObject.params) { 242 var paramNames = this._eventArgs[messageObject.method]; 243 for (var i = 0; i < paramNames.length; ++i) 244 params.push(messageObject.params[paramNames[i]]); 245 } 246 247 var processingStartTime; 248 if (this.dumpInspectorTimeStats) 249 processingStartTime = Date.now(); 250 251 dispatcher[functionName].apply(dispatcher, params); 252 253 if (this.dumpInspectorTimeStats) 254 console.log("time-stats: " + messageObject.method + " = " + (Date.now() - processingStartTime)); 255 } 256 }, 257 258 reportProtocolError: function(messageObject) 259 { 260 console.error("Request with id = " + messageObject.id + " failed. " + messageObject.error); 261 }, 262 263 /** 264 * @param {string=} script 265 */ 266 runAfterPendingDispatches: function(script) 267 { 268 if (!this._scripts) 269 this._scripts = []; 270 271 if (script) 272 this._scripts.push(script); 273 274 if (!this._pendingResponsesCount) { 275 var scripts = this._scripts; 276 this._scripts = [] 277 for (var id = 0; id < scripts.length; ++id) 278 scripts[id].call(this); 279 } 280 }, 281 282 loadFromJSONIfNeeded: function(jsonUrl) 283 { 284 if (this._initialized) 285 return; 286 287 var xhr = new XMLHttpRequest(); 288 xhr.open("GET", jsonUrl, false); 289 xhr.send(null); 290 291 var schema = JSON.parse(xhr.responseText); 292 var code = InspectorBackendClass._generateCommands(schema); 293 eval(code); 294 } 295} 296 297/** 298 * @param {*} schema 299 * @return {string} 300 */ 301InspectorBackendClass._generateCommands = function(schema) { 302 var jsTypes = { integer: "number", array: "object" }; 303 var rawTypes = {}; 304 var result = []; 305 306 var domains = schema["domains"] || []; 307 for (var i = 0; i < domains.length; ++i) { 308 var domain = domains[i]; 309 for (var j = 0; domain.types && j < domain.types.length; ++j) { 310 var type = domain.types[j]; 311 rawTypes[domain.domain + "." + type.id] = jsTypes[type.type] || type.type; 312 } 313 } 314 315 function toUpperCase(groupIndex, group0, group1) 316 { 317 return [group0, group1][groupIndex].toUpperCase(); 318 } 319 function generateEnum(enumName, items) 320 { 321 var members = [] 322 for (var m = 0; m < items.length; ++m) { 323 var value = items[m]; 324 var name = value.replace(/-(\w)/g, toUpperCase.bind(null, 1)).toTitleCase(); 325 name = name.replace(/HTML|XML|WML|API/ig, toUpperCase.bind(null, 0)); 326 members.push(name + ": \"" + value +"\""); 327 } 328 return "InspectorBackend.registerEnum(\"" + enumName + "\", {" + members.join(", ") + "});"; 329 } 330 331 for (var i = 0; i < domains.length; ++i) { 332 var domain = domains[i]; 333 334 var types = domain["types"] || []; 335 for (var j = 0; j < types.length; ++j) { 336 var type = types[j]; 337 if ((type["type"] === "string") && type["enum"]) 338 result.push(generateEnum(domain.domain + "." + type.id, type["enum"])); 339 else if (type["type"] === "object") { 340 var properties = type["properties"] || []; 341 for (var k = 0; k < properties.length; ++k) { 342 var property = properties[k]; 343 if ((property["type"] === "string") && property["enum"]) 344 result.push(generateEnum(domain.domain + "." + type.id + property["name"].toTitleCase(), property["enum"])); 345 } 346 } 347 } 348 349 var commands = domain["commands"] || []; 350 for (var j = 0; j < commands.length; ++j) { 351 var command = commands[j]; 352 var parameters = command["parameters"]; 353 var paramsText = []; 354 for (var k = 0; parameters && k < parameters.length; ++k) { 355 var parameter = parameters[k]; 356 357 var type; 358 if (parameter.type) 359 type = jsTypes[parameter.type] || parameter.type; 360 else { 361 var ref = parameter["$ref"]; 362 if (ref.indexOf(".") !== -1) 363 type = rawTypes[ref]; 364 else 365 type = rawTypes[domain.domain + "." + ref]; 366 } 367 368 var text = "{\"name\": \"" + parameter.name + "\", \"type\": \"" + type + "\", \"optional\": " + (parameter.optional ? "true" : "false") + "}"; 369 paramsText.push(text); 370 } 371 372 var returnsText = []; 373 var returns = command["returns"] || []; 374 for (var k = 0; k < returns.length; ++k) { 375 var parameter = returns[k]; 376 returnsText.push("\"" + parameter.name + "\""); 377 } 378 result.push("InspectorBackend.registerCommand(\"" + domain.domain + "." + command.name + "\", [" + paramsText.join(", ") + "], [" + returnsText.join(", ") + "]);"); 379 } 380 381 for (var j = 0; domain.events && j < domain.events.length; ++j) { 382 var event = domain.events[j]; 383 var paramsText = []; 384 for (var k = 0; event.parameters && k < event.parameters.length; ++k) { 385 var parameter = event.parameters[k]; 386 paramsText.push("\"" + parameter.name + "\""); 387 } 388 result.push("InspectorBackend.registerEvent(\"" + domain.domain + "." + event.name + "\", [" + paramsText.join(", ") + "]);"); 389 } 390 391 result.push("InspectorBackend.register" + domain.domain + "Dispatcher = InspectorBackend.registerDomainDispatcher.bind(InspectorBackend, \"" + domain.domain + "\");"); 392 } 393 return result.join("\n"); 394} 395 396InspectorBackend = new InspectorBackendClass(); 397