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 */ 34 function InspectorBackendClass() 35 { 36 this._lastCallbackId = 1; 37 this._pendingResponsesCount = 0; 38 this._callbacks = {}; 39 this._domainDispatchers = {}; 40 this._eventArgs = {}; 41 this._replyArgs = {}; 42 this._hasErrorData = {}; 43 44 this.dumpInspectorTimeStats = false; 45 this.dumpInspectorProtocolMessages = false; 46 this._initialized = false; 47 } 48 49 InspectorBackendClass.prototype = { 50 /** 51 * @return {number} 52 */ 53 nextCallbackId: function() 54 { 55 return this._lastCallbackId++; 56 }, 57 58 _wrap: function(callback, method) 59 { 60 var callbackId = this.nextCallbackId(); 61 if (!callback) 62 callback = function() {}; 63 64 this._callbacks[callbackId] = callback; 65 callback.methodName = method; 66 if (this.dumpInspectorTimeStats) 67 callback.sendRequestTime = Date.now(); 68 69 return callbackId; 70 }, 71 72 _getAgent: function(domain) 73 { 74 var agentName = domain + "Agent"; 75 if (!window[agentName]) 76 window[agentName] = {}; 77 return window[agentName]; 78 }, 79 80 registerCommand: function(method, signature, replyArgs, hasErrorData) 81 { 82 var domainAndMethod = method.split("."); 83 var agent = this._getAgent(domainAndMethod[0]); 84 85 agent[domainAndMethod[1]] = this._sendMessageToBackend.bind(this, method, signature); 86 agent[domainAndMethod[1]]["invoke"] = this._invoke.bind(this, method, signature); 87 this._replyArgs[method] = replyArgs; 88 if (hasErrorData) 89 this._hasErrorData[method] = true; 90 91 this._initialized = true; 92 }, 93 94 registerEnum: function(type, values) 95 { 96 var domainAndMethod = type.split("."); 97 var agent = this._getAgent(domainAndMethod[0]); 98 99 agent[domainAndMethod[1]] = values; 100 101 this._initialized = true; 102 }, 103 104 registerEvent: function(eventName, params) 105 { 106 this._eventArgs[eventName] = params; 107 108 this._initialized = true; 109 }, 110 111 _invoke: function(method, signature, args, callback) 112 { 113 this._wrapCallbackAndSendMessageObject(method, args, callback); 114 }, 115 116 _sendMessageToBackend: function(method, signature, vararg) 117 { 118 var args = Array.prototype.slice.call(arguments, 2); 119 var callback = (args.length && typeof args[args.length - 1] === "function") ? args.pop() : null; 120 121 var params = {}; 122 var hasParams = false; 123 for (var i = 0; i < signature.length; ++i) { 124 var param = signature[i]; 125 var paramName = param["name"]; 126 var typeName = param["type"]; 127 var optionalFlag = param["optional"]; 128 129 if (!args.length && !optionalFlag) { 130 console.error("Protocol Error: Invalid number of arguments for method '" + method + "' call. It must have the following arguments '" + JSON.stringify(signature) + "'."); 131 return; 132 } 133 134 var value = args.shift(); 135 if (optionalFlag && typeof value === "undefined") { 136 continue; 137 } 138 139 if (typeof value !== typeName) { 140 console.error("Protocol Error: Invalid type of argument '" + paramName + "' for method '" + method + "' call. It must be '" + typeName + "' but it is '" + typeof value + "'."); 141 return; 142 } 143 144 params[paramName] = value; 145 hasParams = true; 146 } 147 148 if (args.length === 1 && !callback) { 149 if (typeof args[0] !== "undefined") { 150 console.error("Protocol Error: Optional callback argument for method '" + method + "' call must be a function but its type is '" + typeof args[0] + "'."); 151 return; 152 } 153 } 154 155 this._wrapCallbackAndSendMessageObject(method, hasParams ? params : null, callback); 156 }, 157 158 _wrapCallbackAndSendMessageObject: function(method, params, callback) 159 { 160 var messageObject = {}; 161 messageObject.method = method; 162 if (params) 163 messageObject.params = params; 164 messageObject.id = this._wrap(callback, method); 165 166 if (this.dumpInspectorProtocolMessages) 167 console.log("frontend: " + JSON.stringify(messageObject)); 168 169 ++this._pendingResponsesCount; 170 this.sendMessageObjectToBackend(messageObject); 171 }, 172 173 sendMessageObjectToBackend: function(messageObject) 174 { 175 var message = JSON.stringify(messageObject); 176 InspectorFrontendHost.sendMessageToBackend(message); 177 }, 178 179 registerDomainDispatcher: function(domain, dispatcher) 180 { 181 this._domainDispatchers[domain] = dispatcher; 182 }, 183 184 dispatch: function(message) 185 { 186 if (this.dumpInspectorProtocolMessages) 187 console.log("backend: " + ((typeof message === "string") ? message : JSON.stringify(message))); 188 189 var messageObject = (typeof message === "string") ? JSON.parse(message) : message; 190 191 if ("id" in messageObject) { // just a response for some request 192 if (messageObject.error) { 193 if (messageObject.error.code !== -32000) 194 this.reportProtocolError(messageObject); 195 } 196 197 var callback = this._callbacks[messageObject.id]; 198 if (callback) { 199 var argumentsArray = [ null ]; 200 if (messageObject.error) { 201 argumentsArray[0] = messageObject.error.message; 202 } 203 if (this._hasErrorData[callback.methodName]) { 204 argumentsArray.push(null); 205 if (messageObject.error) 206 argumentsArray[1] = messageObject.error.data; 207 } 208 if (messageObject.result) { 209 var paramNames = this._replyArgs[callback.methodName]; 210 if (paramNames) { 211 for (var i = 0; i < paramNames.length; ++i) 212 argumentsArray.push(messageObject.result[paramNames[i]]); 213 } 214 } 215 216 var processingStartTime; 217 if (this.dumpInspectorTimeStats && callback.methodName) 218 processingStartTime = Date.now(); 219 220 callback.apply(null, argumentsArray); 221 --this._pendingResponsesCount; 222 delete this._callbacks[messageObject.id]; 223 224 if (this.dumpInspectorTimeStats && callback.methodName) 225 console.log("time-stats: " + callback.methodName + " = " + (processingStartTime - callback.sendRequestTime) + " + " + (Date.now() - processingStartTime)); 226 } 227 228 if (this._scripts && !this._pendingResponsesCount) 229 this.runAfterPendingDispatches(); 230 231 return; 232 } else { 233 var method = messageObject.method.split("."); 234 var domainName = method[0]; 235 var functionName = method[1]; 236 if (!(domainName in this._domainDispatchers)) { 237 console.error("Protocol Error: the message is for non-existing domain '" + domainName + "'"); 238 return; 239 } 240 var dispatcher = this._domainDispatchers[domainName]; 241 if (!(functionName in dispatcher)) { 242 console.error("Protocol Error: Attempted to dispatch an unimplemented method '" + messageObject.method + "'"); 243 return; 244 } 245 246 if (!this._eventArgs[messageObject.method]) { 247 console.error("Protocol Error: Attempted to dispatch an unspecified method '" + messageObject.method + "'"); 248 return; 249 } 250 251 var params = []; 252 if (messageObject.params) { 253 var paramNames = this._eventArgs[messageObject.method]; 254 for (var i = 0; i < paramNames.length; ++i) 255 params.push(messageObject.params[paramNames[i]]); 256 } 257 258 var processingStartTime; 259 if (this.dumpInspectorTimeStats) 260 processingStartTime = Date.now(); 261 262 dispatcher[functionName].apply(dispatcher, params); 263 264 if (this.dumpInspectorTimeStats) 265 console.log("time-stats: " + messageObject.method + " = " + (Date.now() - processingStartTime)); 266 } 267 }, 268 269 reportProtocolError: function(messageObject) 270 { 271 console.error("Request with id = " + messageObject.id + " failed. " + messageObject.error); 272 }, 273 274 /** 275 * @param {string=} script 276 */ 277 runAfterPendingDispatches: function(script) 278 { 279 if (!this._scripts) 280 this._scripts = []; 281 282 if (script) 283 this._scripts.push(script); 284 285 if (!this._pendingResponsesCount) { 286 var scripts = this._scripts; 287 this._scripts = [] 288 for (var id = 0; id < scripts.length; ++id) 289 scripts[id].call(this); 290 } 291 }, 292 293 loadFromJSONIfNeeded: function(jsonUrl) 294 { 295 if (this._initialized) 296 return; 297 298 var xhr = new XMLHttpRequest(); 299 xhr.open("GET", jsonUrl, false); 300 xhr.send(null); 301 302 var schema = JSON.parse(xhr.responseText); 303 var code = InspectorBackendClass._generateCommands(schema); 304 eval(code); 305 } 306 } 307 308 /** 309 * @param {*} schema 310 * @return {string} 311 */ 312 InspectorBackendClass._generateCommands = function(schema) { 313 var jsTypes = { integer: "number", array: "object" }; 314 var rawTypes = {}; 315 var result = []; 316 317 var domains = schema["domains"] || []; 318 for (var i = 0; i < domains.length; ++i) { 319 var domain = domains[i]; 320 for (var j = 0; domain.types && j < domain.types.length; ++j) { 321 var type = domain.types[j]; 322 rawTypes[domain.domain + "." + type.id] = jsTypes[type.type] || type.type; 323 } 324 } 325 326 function toUpperCase(groupIndex, group0, group1) 327 { 328 return [group0, group1][groupIndex].toUpperCase(); 329 } 330 function generateEnum(enumName, items) 331 { 332 var members = [] 333 for (var m = 0; m < items.length; ++m) { 334 var value = items[m]; 335 var name = value.replace(/-(\w)/g, toUpperCase.bind(null, 1)).toTitleCase(); 336 name = name.replace(/HTML|XML|WML|API/ig, toUpperCase.bind(null, 0)); 337 members.push(name + ": \"" + value +"\""); 338 } 339 return "InspectorBackend.registerEnum(\"" + enumName + "\", {" + members.join(", ") + "});"; 340 } 341 342 for (var i = 0; i < domains.length; ++i) { 343 var domain = domains[i]; 344 345 var types = domain["types"] || []; 346 for (var j = 0; j < types.length; ++j) { 347 var type = types[j]; 348 if ((type["type"] === "string") && type["enum"]) 349 result.push(generateEnum(domain.domain + "." + type.id, type["enum"])); 350 else if (type["type"] === "object") { 351 var properties = type["properties"] || []; 352 for (var k = 0; k < properties.length; ++k) { 353 var property = properties[k]; 354 if ((property["type"] === "string") && property["enum"]) 355 result.push(generateEnum(domain.domain + "." + type.id + property["name"].toTitleCase(), property["enum"])); 356 } 357 } 358 } 359 360 var commands = domain["commands"] || []; 361 for (var j = 0; j < commands.length; ++j) { 362 var command = commands[j]; 363 var parameters = command["parameters"]; 364 var paramsText = []; 365 for (var k = 0; parameters && k < parameters.length; ++k) { 366 var parameter = parameters[k]; 367 368 var type; 369 if (parameter.type) 370 type = jsTypes[parameter.type] || parameter.type; 371 else { 372 var ref = parameter["$ref"]; 373 if (ref.indexOf(".") !== -1) 374 type = rawTypes[ref]; 375 else 376 type = rawTypes[domain.domain + "." + ref]; 377 } 378 379 var text = "{\"name\": \"" + parameter.name + "\", \"type\": \"" + type + "\", \"optional\": " + (parameter.optional ? "true" : "false") + "}"; 380 paramsText.push(text); 381 } 382 383 var returnsText = []; 384 var returns = command["returns"] || []; 385 for (var k = 0; k < returns.length; ++k) { 386 var parameter = returns[k]; 387 returnsText.push("\"" + parameter.name + "\""); 388 } 389 var hasErrorData = String(Boolean(command.error)); 390 result.push("InspectorBackend.registerCommand(\"" + domain.domain + "." + command.name + "\", [" + paramsText.join(", ") + "], [" + returnsText.join(", ") + "], " + hasErrorData + ");"); 391 } 392 393 for (var j = 0; domain.events && j < domain.events.length; ++j) { 394 var event = domain.events[j]; 395 var paramsText = []; 396 for (var k = 0; event.parameters && k < event.parameters.length; ++k) { 397 var parameter = event.parameters[k]; 398 paramsText.push("\"" + parameter.name + "\""); 399 } 400 result.push("InspectorBackend.registerEvent(\"" + domain.domain + "." + event.name + "\", [" + paramsText.join(", ") + "]);"); 401 } 402 403 result.push("InspectorBackend.register" + domain.domain + "Dispatcher = InspectorBackend.registerDomainDispatcher.bind(InspectorBackend, \"" + domain.domain + "\");"); 404 } 405 return result.join("\n"); 406 } 407 408 InspectorBackend = new InspectorBackendClass(); 409