2 * Copyright (C) 2011 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
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
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.
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.
34 function InspectorBackendClass()
36 this._lastCallbackId = 1;
37 this._pendingResponsesCount = 0;
39 this._domainDispatchers = {};
42 this._hasErrorData = {};
44 this.dumpInspectorTimeStats = false;
45 this.dumpInspectorProtocolMessages = false;
46 this._initialized = false;
49 InspectorBackendClass.prototype = {
53 nextCallbackId: function()
55 return this._lastCallbackId++;
58 _wrap: function(callback, method)
60 var callbackId = this.nextCallbackId();
62 callback = function() {};
64 this._callbacks[callbackId] = callback;
65 callback.methodName = method;
66 if (this.dumpInspectorTimeStats)
67 callback.sendRequestTime = Date.now();
72 _getAgent: function(domain)
74 var agentName = domain + "Agent";
75 if (!window[agentName])
76 window[agentName] = {};
77 return window[agentName];
80 registerCommand: function(method, signature, replyArgs, hasErrorData)
82 var domainAndMethod = method.split(".");
83 var agent = this._getAgent(domainAndMethod[0]);
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;
89 this._hasErrorData[method] = true;
91 this._initialized = true;
94 registerEnum: function(type, values)
96 var domainAndMethod = type.split(".");
97 var agent = this._getAgent(domainAndMethod[0]);
99 agent[domainAndMethod[1]] = values;
101 this._initialized = true;
104 registerEvent: function(eventName, params)
106 this._eventArgs[eventName] = params;
108 this._initialized = true;
111 _invoke: function(method, signature, args, callback)
113 this._wrapCallbackAndSendMessageObject(method, args, callback);
116 _sendMessageToBackend: function(method, signature, vararg)
118 var args = Array.prototype.slice.call(arguments, 2);
119 var callback = (args.length && typeof args[args.length - 1] === "function") ? args.pop() : null;
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"];
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) + "'.");
134 var value = args.shift();
135 if (optionalFlag && typeof value === "undefined") {
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 + "'.");
144 params[paramName] = value;
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] + "'.");
155 this._wrapCallbackAndSendMessageObject(method, hasParams ? params : null, callback);
158 _wrapCallbackAndSendMessageObject: function(method, params, callback)
160 var messageObject = {};
161 messageObject.method = method;
163 messageObject.params = params;
164 messageObject.id = this._wrap(callback, method);
166 if (this.dumpInspectorProtocolMessages)
167 console.log("frontend: " + JSON.stringify(messageObject));
169 ++this._pendingResponsesCount;
170 this.sendMessageObjectToBackend(messageObject);
173 sendMessageObjectToBackend: function(messageObject)
175 var message = JSON.stringify(messageObject);
176 InspectorFrontendHost.sendMessageToBackend(message);
179 registerDomainDispatcher: function(domain, dispatcher)
181 this._domainDispatchers[domain] = dispatcher;
184 dispatch: function(message)
186 if (this.dumpInspectorProtocolMessages)
187 console.log("backend: " + ((typeof message === "string") ? message : JSON.stringify(message)));
189 var messageObject = (typeof message === "string") ? JSON.parse(message) : message;
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);
197 var callback = this._callbacks[messageObject.id];
199 var argumentsArray = [ null ];
200 if (messageObject.error) {
201 argumentsArray[0] = messageObject.error.message;
203 if (this._hasErrorData[callback.methodName]) {
204 argumentsArray.push(null);
205 if (messageObject.error)
206 argumentsArray[1] = messageObject.error.data;
208 if (messageObject.result) {
209 var paramNames = this._replyArgs[callback.methodName];
211 for (var i = 0; i < paramNames.length; ++i)
212 argumentsArray.push(messageObject.result[paramNames[i]]);
216 var processingStartTime;
217 if (this.dumpInspectorTimeStats && callback.methodName)
218 processingStartTime = Date.now();
220 callback.apply(null, argumentsArray);
221 --this._pendingResponsesCount;
222 delete this._callbacks[messageObject.id];
224 if (this.dumpInspectorTimeStats && callback.methodName)
225 console.log("time-stats: " + callback.methodName + " = " + (processingStartTime - callback.sendRequestTime) + " + " + (Date.now() - processingStartTime));
228 if (this._scripts && !this._pendingResponsesCount)
229 this.runAfterPendingDispatches();
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 " + messageObject.method + " is for non-existing domain '" + domainName + "'");
240 var dispatcher = this._domainDispatchers[domainName];
241 if (!(functionName in dispatcher)) {
242 console.error("Protocol Error: Attempted to dispatch an unimplemented method '" + messageObject.method + "'");
246 if (!this._eventArgs[messageObject.method]) {
247 console.error("Protocol Error: Attempted to dispatch an unspecified method '" + messageObject.method + "'");
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]]);
258 var processingStartTime;
259 if (this.dumpInspectorTimeStats)
260 processingStartTime = Date.now();
262 dispatcher[functionName].apply(dispatcher, params);
264 if (this.dumpInspectorTimeStats)
265 console.log("time-stats: " + messageObject.method + " = " + (Date.now() - processingStartTime));
269 reportProtocolError: function(messageObject)
271 console.error("Request with id = " + messageObject.id + " failed. " + JSON.stringify(messageObject.error));
275 * @param {string=} script
277 runAfterPendingDispatches: function(script)
283 this._scripts.push(script);
285 if (!this._pendingResponsesCount) {
286 var scripts = this._scripts;
288 for (var id = 0; id < scripts.length; ++id)
289 scripts[id].call(this);
293 loadFromJSONIfNeeded: function(jsonUrl)
295 if (this._initialized)
298 var xhr = new XMLHttpRequest();
299 xhr.open("GET", jsonUrl, false);
302 var schema = JSON.parse(xhr.responseText);
303 var code = InspectorBackendClass._generateCommands(schema);
308 * @param {function(T)} clientCallback
309 * @param {string} errorPrefix
310 * @param {function(new:T,S)=} constructor
311 * @param {T=} defaultValue
312 * @return {function(?string, S)}
315 wrapClientCallback: function(clientCallback, errorPrefix, constructor, defaultValue)
318 * @param {?string} error
322 function callbackWrapper(error, value)
325 console.error(errorPrefix + error);
326 clientCallback(defaultValue);
330 clientCallback(new constructor(value));
332 clientCallback(value);
334 return callbackWrapper;
342 InspectorBackendClass._generateCommands = function(schema) {
343 var jsTypes = { integer: "number", array: "object" };
347 var domains = schema["domains"] || [];
348 for (var i = 0; i < domains.length; ++i) {
349 var domain = domains[i];
350 for (var j = 0; domain.types && j < domain.types.length; ++j) {
351 var type = domain.types[j];
352 rawTypes[domain.domain + "." + type.id] = jsTypes[type.type] || type.type;
356 function toUpperCase(groupIndex, group0, group1)
358 return [group0, group1][groupIndex].toUpperCase();
360 function generateEnum(enumName, items)
363 for (var m = 0; m < items.length; ++m) {
364 var value = items[m];
365 var name = value.replace(/-(\w)/g, toUpperCase.bind(null, 1)).toTitleCase();
366 name = name.replace(/HTML|XML|WML|API/ig, toUpperCase.bind(null, 0));
367 members.push(name + ": \"" + value +"\"");
369 return "InspectorBackend.registerEnum(\"" + enumName + "\", {" + members.join(", ") + "});";
372 for (var i = 0; i < domains.length; ++i) {
373 var domain = domains[i];
375 var types = domain["types"] || [];
376 for (var j = 0; j < types.length; ++j) {
378 if ((type["type"] === "string") && type["enum"])
379 result.push(generateEnum(domain.domain + "." + type.id, type["enum"]));
380 else if (type["type"] === "object") {
381 var properties = type["properties"] || [];
382 for (var k = 0; k < properties.length; ++k) {
383 var property = properties[k];
384 if ((property["type"] === "string") && property["enum"])
385 result.push(generateEnum(domain.domain + "." + type.id + property["name"].toTitleCase(), property["enum"]));
390 var commands = domain["commands"] || [];
391 for (var j = 0; j < commands.length; ++j) {
392 var command = commands[j];
393 var parameters = command["parameters"];
395 for (var k = 0; parameters && k < parameters.length; ++k) {
396 var parameter = parameters[k];
400 type = jsTypes[parameter.type] || parameter.type;
402 var ref = parameter["$ref"];
403 if (ref.indexOf(".") !== -1)
404 type = rawTypes[ref];
406 type = rawTypes[domain.domain + "." + ref];
409 var text = "{\"name\": \"" + parameter.name + "\", \"type\": \"" + type + "\", \"optional\": " + (parameter.optional ? "true" : "false") + "}";
410 paramsText.push(text);
413 var returnsText = [];
414 var returns = command["returns"] || [];
415 for (var k = 0; k < returns.length; ++k) {
416 var parameter = returns[k];
417 returnsText.push("\"" + parameter.name + "\"");
419 var hasErrorData = String(Boolean(command.error));
420 result.push("InspectorBackend.registerCommand(\"" + domain.domain + "." + command.name + "\", [" + paramsText.join(", ") + "], [" + returnsText.join(", ") + "], " + hasErrorData + ");");
423 for (var j = 0; domain.events && j < domain.events.length; ++j) {
424 var event = domain.events[j];
426 for (var k = 0; event.parameters && k < event.parameters.length; ++k) {
427 var parameter = event.parameters[k];
428 paramsText.push("\"" + parameter.name + "\"");
430 result.push("InspectorBackend.registerEvent(\"" + domain.domain + "." + event.name + "\", [" + paramsText.join(", ") + "]);");
433 result.push("InspectorBackend.register" + domain.domain + "Dispatcher = InspectorBackend.registerDomainDispatcher.bind(InspectorBackend, \"" + domain.domain + "\");");
435 return result.join("\n");
438 InspectorBackend = new InspectorBackendClass();