1/*
2 * Copyright (C) 2011 Google Inc. All rights reserved.
3 * Copyright (C) 2013 Apple Inc. All rights reserved.
4 * Copyright (C) 2014 University of Washington.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are
8 * met:
9 *
10 *     * Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 *     * Redistributions in binary form must reproduce the above
13 * copyright notice, this list of conditions and the following disclaimer
14 * in the documentation and/or other materials provided with the
15 * distribution.
16 *     * Neither the name of Google Inc. nor the names of its
17 * contributors may be used to endorse or promote products derived from
18 * this software without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32
33function InspectorBackendClass()
34{
35    this._lastSequenceId = 1;
36    this._pendingResponsesCount = 0;
37    this._callbackData = new Map;
38    this._agents = {};
39    this._deferredScripts = [];
40
41    this.dumpInspectorTimeStats = false;
42    this.dumpInspectorProtocolMessages = false;
43    this.warnForLongMessageHandling = false;
44    this.longMessageHandlingThreshold = 10; // milliseconds.
45}
46
47InspectorBackendClass.prototype = {
48
49    // Public
50
51    registerCommand: function(qualifiedName, callSignature, replySignature)
52    {
53        var [domainName, commandName] = qualifiedName.split(".");
54        var agent = this._agentForDomain(domainName);
55        agent.addCommand(InspectorBackend.Command.create(this, qualifiedName, callSignature, replySignature));
56    },
57
58    registerEnum: function(qualifiedName, enumValues)
59    {
60        var [domainName, enumName] = qualifiedName.split(".");
61        var agent = this._agentForDomain(domainName);
62        agent.addEnum(enumName, enumValues);
63    },
64
65    registerEvent: function(qualifiedName, signature)
66    {
67        var [domainName, eventName] = qualifiedName.split(".");
68        var agent = this._agentForDomain(domainName);
69        agent.addEvent(new InspectorBackend.Event(eventName, signature));
70    },
71
72    registerDomainDispatcher: function(domainName, dispatcher)
73    {
74        var agent = this._agentForDomain(domainName);
75        agent.dispatcher = dispatcher;
76    },
77
78    dispatch: function(message)
79    {
80        if (this.dumpInspectorProtocolMessages)
81            console.log("backend: " + ((typeof message === "string") ? message : JSON.stringify(message)));
82
83        var messageObject = (typeof message === "string") ? JSON.parse(message) : message;
84
85        if ("id" in messageObject)
86            this._dispatchCallback(messageObject);
87        else
88            this._dispatchEvent(messageObject);
89    },
90
91    runAfterPendingDispatches: function(script)
92    {
93        console.assert(script);
94        console.assert(typeof script === "function");
95
96        if (!this._pendingResponsesCount)
97            script.call(this);
98        else
99            this._deferredScripts.push(script);
100    },
101
102    // Private
103
104    _agentForDomain: function(domainName)
105    {
106        if (this._agents[domainName])
107            return this._agents[domainName];
108
109        var agent = new InspectorBackend.Agent(domainName);
110        this._agents[domainName] = agent;
111        window[domainName + "Agent"] = agent;
112        return agent;
113    },
114
115    _willSendMessageToBackend: function(command, callback)
116    {
117        ++this._pendingResponsesCount;
118        var sequenceId = this._lastSequenceId++;
119
120        if (callback && typeof callback === "function") {
121            var callbackData =  {
122                "callback": callback,
123                "command": command,
124            };
125
126            if (this.dumpInspectorTimeStats)
127                callbackData.sendRequestTime = Date.now();
128
129            this._callbackData.set(sequenceId, callbackData);
130        }
131
132        return sequenceId;
133    },
134
135    _dispatchCallback: function(messageObject)
136    {
137        --this._pendingResponsesCount;
138        console.assert(this._pendingResponsesCount >= 0);
139
140        if (messageObject["error"]) {
141            if (messageObject["error"].code !== -32000)
142                this._reportProtocolError(messageObject);
143        }
144
145        var callbackData = this._callbackData.get(messageObject["id"]);
146        if (callbackData && typeof callbackData.callback === "function") {
147            var command = callbackData.command;
148            var callback = callbackData.callback;
149            var callbackArguments = [];
150
151            callbackArguments.push(messageObject["error"] ? messageObject["error"].message : null);
152
153            if (messageObject["result"]) {
154                // FIXME: this should be indicated by invoking the command differently, rather
155                // than by setting a magical property on the callback. <webkit.org/b/132386>
156                if (callback.expectsResultObject) {
157                    // The callback expects results as an object with properties, this is useful
158                    // for backwards compatibility with renamed or different parameters.
159                    callbackArguments.push(messageObject["result"]);
160                } else {
161                    for (var parameterName of command.replySignature)
162                        callbackArguments.push(messageObject["result"][parameterName]);
163                }
164            }
165
166            var processingStartTime;
167            if (this.dumpInspectorTimeStats)
168                processingStartTime = Date.now();
169
170            try {
171                callback.apply(null, callbackArguments);
172            } catch (e) {
173                console.error("Uncaught exception in inspector page while dispatching callback for command " + command.qualifiedName + ": ", e);
174            }
175
176            var processingDuration = Date.now() - processingStartTime;
177            if (this.warnForLongMessageHandling && processingDuration > this.longMessageHandlingThreshold)
178                console.warn("InspectorBackend: took " + processingDuration + "ms to handle response for command: " + command.qualifiedName);
179
180            if (this.dumpInspectorTimeStats)
181                console.log("time-stats: Handling: " + processingDuration + "ms; RTT: " + (processingStartTime - callbackData.sendRequestTime) + "ms; (command " + command.qualifiedName + ")");
182
183            this._callbackData.delete(messageObject["id"]);
184        }
185
186        if (this._deferredScripts.length && !this._pendingResponsesCount)
187            this._flushPendingScripts();
188    },
189
190    _dispatchEvent: function(messageObject)
191    {
192        var qualifiedName = messageObject["method"];
193        var [domainName, eventName] = qualifiedName.split(".");
194        if (!(domainName in this._agents)) {
195            console.error("Protocol Error: Attempted to dispatch method '" + eventName + "' for non-existing domain '" + domainName + "'");
196            return;
197        }
198
199        var agent = this._agentForDomain(domainName);
200        var event = agent.getEvent(eventName);
201        if (!event) {
202            console.error("Protocol Error: Attempted to dispatch an unspecified method '" + qualifiedName + "'");
203            return;
204        }
205
206        var eventArguments = [];
207        if (messageObject["params"]) {
208            var parameterNames = event.parameterNames;
209            for (var i = 0; i < parameterNames.length; ++i)
210                eventArguments.push(messageObject["params"][parameterNames[i]]);
211        }
212
213        var processingStartTime;
214        if (this.dumpInspectorTimeStats)
215            processingStartTime = Date.now();
216
217        try {
218            agent.dispatchEvent(eventName, eventArguments);
219        } catch (e) {
220            console.error("Uncaught exception in inspector page while handling event " + qualifiedName + ": ", e);
221        }
222
223        var processingDuration = Date.now() - processingStartTime;
224        if (this.warnForLongMessageHandling && processingDuration > this.longMessageHandlingThreshold)
225            console.warn("InspectorBackend: took " + processingDuration + "ms to handle event: " + messageObject["method"]);
226
227        if (this.dumpInspectorTimeStats)
228            console.log("time-stats: Handling: " + processingDuration + "ms (event " + messageObject["method"] + ")");
229    },
230
231    _invokeCommand: function(command, parameters, callback)
232    {
233        var messageObject = {};
234        messageObject["method"] = command.qualifiedName;
235
236        if (parameters)
237            messageObject["params"] = parameters;
238
239        // We always assign an id as a sequence identifier.
240        // Callback data is saved only if a callback is actually passed.
241        messageObject["id"] = this._willSendMessageToBackend(command, callback);
242
243        var stringifiedMessage = JSON.stringify(messageObject);
244        if (this.dumpInspectorProtocolMessages)
245            console.log("frontend: " + stringifiedMessage);
246
247        InspectorFrontendHost.sendMessageToBackend(stringifiedMessage);
248    },
249
250    _reportProtocolError: function(messageObject)
251    {
252        console.error("Request with id = " + messageObject["id"] + " failed. " + JSON.stringify(messageObject["error"]));
253    },
254
255    _flushPendingScripts: function()
256    {
257        console.assert(!this._pendingResponsesCount);
258
259        var scriptsToRun = this._deferredScripts;
260        this._deferredScripts = [];
261        for (var script of scriptsToRun)
262            script.call(this);
263    }
264}
265
266InspectorBackend = new InspectorBackendClass();
267
268InspectorBackend.Agent = function(domainName)
269{
270    this._domainName = domainName;
271
272    // Commands are stored directly on the Agent instance using their unqualified
273    // method name as the property. Thus, callers can write: FooAgent.methodName().
274    // Enums are stored similarly based on the unqualified type name.
275    this._events = {};
276}
277
278InspectorBackend.Agent.prototype = {
279    get domainName()
280    {
281        return this._domainName;
282    },
283
284    set dispatcher(value)
285    {
286        this._dispatcher = value;
287    },
288
289    addEnum: function(enumName, enumValues)
290    {
291        this[enumName] = enumValues;
292    },
293
294    addCommand: function(command)
295    {
296        this[command.commandName] = command;
297    },
298
299    addEvent: function(event)
300    {
301        this._events[event.eventName] = event;
302    },
303
304    getEvent: function(eventName)
305    {
306        return this._events[eventName];
307    },
308
309    hasEvent: function(eventName)
310    {
311        return eventName in this._events;
312    },
313
314    dispatchEvent: function(eventName, eventArguments)
315    {
316        if (!(eventName in this._dispatcher)) {
317            console.error("Protocol Error: Attempted to dispatch an unimplemented method '" + this._domainName + "." + eventName + "'");
318            return false;
319        }
320
321        this._dispatcher[eventName].apply(this._dispatcher, eventArguments);
322        return true;
323    }
324}
325
326InspectorBackend.Command = function(backend, qualifiedName, callSignature, replySignature)
327{
328    this._backend = backend;
329    this._instance = this;
330
331    var [domainName, commandName] = qualifiedName.split(".");
332    this._qualifiedName = qualifiedName;
333    this._commandName = commandName;
334    this._callSignature = callSignature || [];
335    this._replySignature = replySignature || [];
336}
337
338InspectorBackend.Command.create = function(backend, commandName, callSignature, replySignature)
339{
340    var instance = new InspectorBackend.Command(backend, commandName, callSignature, replySignature);
341
342    function callable() {
343        instance._invokeWithArguments.apply(instance, arguments);
344    }
345    callable._instance = instance;
346    callable.__proto__ = InspectorBackend.Command.prototype;
347    return callable;
348}
349
350// As part of the workaround to make commands callable, these functions use |this._instance|.
351// |this| could refer to the callable trampoline, or the InspectorBackend.Command instance.
352InspectorBackend.Command.prototype = {
353    __proto__: Function.prototype,
354
355    // Public
356
357    get qualifiedName()
358    {
359        return this._instance._qualifiedName;
360    },
361
362    get commandName()
363    {
364        return this._instance._commandName;
365    },
366
367    get callSignature()
368    {
369        return this._instance._callSignature;
370    },
371
372    get replySignature()
373    {
374        return this._instance._replySignature;
375    },
376
377    invoke: function(commandArguments, callback)
378    {
379        var instance = this._instance;
380        instance._backend._invokeCommand(instance, commandArguments, callback);
381    },
382
383    promise: function()
384    {
385        var instance = this._instance;
386        var promiseArguments = Array.prototype.slice.call(arguments);
387        return new Promise(function(resolve, reject) {
388            function convertToPromiseCallback(error, payload) {
389                return error ? reject(error) : resolve(payload);
390            }
391            promiseArguments.push(convertToPromiseCallback);
392            instance._invokeWithArguments.apply(instance, promiseArguments);
393        });
394    },
395
396    supports: function(parameterName)
397    {
398        var instance = this._instance;
399        return instance.callSignature.some(function(parameter) {
400            return parameter["name"] === parameterName;
401        });
402    },
403
404    // Private
405
406    _invokeWithArguments: function()
407    {
408        var instance = this._instance;
409        var commandArguments = Array.prototype.slice.call(arguments);
410        var callback = typeof commandArguments.lastValue === "function" ? commandArguments.pop() : null;
411
412        var parameters = {};
413        for (var i = 0; i < instance.callSignature.length; ++i) {
414            var parameter = instance.callSignature[i];
415            var parameterName = parameter["name"];
416            var typeName = parameter["type"];
417            var optionalFlag = parameter["optional"];
418
419            if (!commandArguments.length && !optionalFlag) {
420                console.error("Protocol Error: Invalid number of arguments for method '" + instance.qualifiedName + "' call. It must have the following arguments '" + JSON.stringify(signature) + "'.");
421                return;
422            }
423
424            var value = commandArguments.shift();
425            if (optionalFlag && typeof value === "undefined")
426                continue;
427
428            if (typeof value !== typeName) {
429                console.error("Protocol Error: Invalid type of argument '" + parameterName + "' for method '" + instance.qualifiedName + "' call. It must be '" + typeName + "' but it is '" + typeof value + "'.");
430                return;
431            }
432
433            parameters[parameterName] = value;
434        }
435
436        if (commandArguments.length === 1 && !callback) {
437            if (typeof commandArguments[0] !== "undefined") {
438                console.error("Protocol Error: Optional callback argument for method '" + instance.qualifiedName + "' call must be a function but its type is '" + typeof args[0] + "'.");
439                return;
440            }
441        }
442
443        instance._backend._invokeCommand(instance, Object.keys(parameters).length ? parameters : null, callback);
444    },
445}
446
447InspectorBackend.Event = function(eventName, parameterNames)
448{
449    this.eventName = eventName;
450    this.parameterNames = parameterNames;
451}
452