Home | History | Annotate | Download | only in front_end
      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