InspectorBackend.js 15 KB

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