343130840f09028386f235497323004dfc71625f
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Protocol / InspectorBackend.js
1 /*
2  * Copyright (C) 2011 Google Inc. All rights reserved.
3  * Copyright (C) 2013, 2015 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
33 InspectorBackendClass = class InspectorBackendClass
34 {
35     constructor()
36     {
37         this._lastSequenceId = 1;
38         this._pendingResponses = new Map;
39         this._agents = {};
40         this._deferredScripts = [];
41         this._activeTracer = null;
42         this._automaticTracer = null;
43
44         this._dumpInspectorTimeStats = false;
45
46         let setting = WebInspector.autoLogProtocolMessagesSetting = new WebInspector.Setting("auto-collect-protocol-messages", false);
47         setting.addEventListener(WebInspector.Setting.Event.Changed, this._startOrStopAutomaticTracing.bind(this))
48         this._startOrStopAutomaticTracing();
49     }
50
51     // Public
52
53     // It's still possible to set this flag on InspectorBackend to just
54     // dump protocol traffic as it happens. For more complex uses of
55     // protocol data, install a subclass of WebInspector.ProtocolTracer.
56     set dumpInspectorProtocolMessages(value)
57     {
58         // Implicitly cause automatic logging to start if it's allowed.
59         let setting = WebInspector.autoLogProtocolMessagesSetting;
60         setting.value = value;
61
62         if (this.activeTracer !== this._automaticTracer)
63             return;
64
65         if (this.activeTracer)
66             this.activeTracer.dumpMessagesToConsole = value;
67     }
68
69     get dumpInspectorProtocolMessages()
70     {
71         return !!this._automaticTracer;
72     }
73
74     set dumpInspectorTimeStats(value)
75     {
76         if (!this.dumpInspectorProtocolMessages)
77             this.dumpInspectorProtocolMessages = true;
78
79         if (this.activeTracer !== this._automaticTracer)
80             return;
81
82         if (this.activeTracer)
83             this.activeTracer.dumpTimingDataToConsole = value;
84     }
85
86     get dumpInspectorTimeStats()
87     {
88         return this._dumpInspectorTimeStats;
89     }
90
91     set activeTracer(tracer)
92     {
93         console.assert(!tracer || tracer instanceof WebInspector.ProtocolTracer);
94
95         // Bail early if no state change is to be made.
96         if (!tracer && !this._activeTracer)
97             return;
98
99         if (tracer === this._activeTracer)
100             return;
101
102         // Don't allow an automatic tracer to dislodge a custom tracer.
103         if (this._activeTracer && tracer === this._automaticTracer)
104             return;
105
106         if (this.activeTracer)
107             this.activeTracer.logFinished();
108
109         if (this._activeTracer === this._automaticTracer)
110             this._automaticTracer = null;
111
112         this._activeTracer = tracer;
113         if (this.activeTracer)
114             this.activeTracer.logStarted();
115         else {
116             // If the custom tracer was removed and automatic tracing is enabled,
117             // then create a new automatic tracer and install it in its place.
118             this._startOrStopAutomaticTracing();
119         }
120     }
121
122     get activeTracer()
123     {
124         return this._activeTracer || null;
125     }
126
127     registerCommand(qualifiedName, callSignature, replySignature)
128     {
129         var [domainName, commandName] = qualifiedName.split(".");
130         var agent = this._agentForDomain(domainName);
131         agent.addCommand(InspectorBackend.Command.create(this, qualifiedName, callSignature, replySignature));
132     }
133
134     registerEnum(qualifiedName, enumValues)
135     {
136         var [domainName, enumName] = qualifiedName.split(".");
137         var agent = this._agentForDomain(domainName);
138         agent.addEnum(enumName, enumValues);
139     }
140
141     registerEvent(qualifiedName, signature)
142     {
143         var [domainName, eventName] = qualifiedName.split(".");
144         var agent = this._agentForDomain(domainName);
145         agent.addEvent(new InspectorBackend.Event(eventName, signature));
146     }
147
148     registerDomainDispatcher(domainName, dispatcher)
149     {
150         var agent = this._agentForDomain(domainName);
151         agent.dispatcher = dispatcher;
152     }
153
154     dispatch(message)
155     {
156         let messageObject = (typeof message === "string") ? JSON.parse(message) : message;
157
158         if ("id" in messageObject)
159             this._dispatchResponse(messageObject);
160         else
161             this._dispatchEvent(messageObject);
162     }
163
164     runAfterPendingDispatches(script)
165     {
166         console.assert(script);
167         console.assert(typeof script === "function");
168
169         if (!this._pendingResponses.size)
170             script.call(this);
171         else
172             this._deferredScripts.push(script);
173     }
174
175     activateDomain(domainName, activationDebuggableType)
176     {
177         if (!activationDebuggableType || InspectorFrontendHost.debuggableType() === activationDebuggableType) {
178             var agent = this._agents[domainName];
179             agent.activate();
180             return agent;
181         }
182
183         return null;
184     }
185
186     // Private
187
188     _startOrStopAutomaticTracing()
189     {
190         let setting = WebInspector.autoLogProtocolMessagesSetting;
191
192         // Bail if there is no state transition to be made.
193         if (!(setting.value ^ !!this.activeTracer))
194             return;
195
196         if (!setting.value) {
197             if (this.activeTracer === this._automaticTracer)
198                 this.activeTracer = null;
199
200             this._automaticTracer = null;
201         } else {
202             this._automaticTracer = new WebInspector.LoggingProtocolTracer;
203             this._automaticTracer.dumpMessagesToConsole = this.dumpInspectorProtocolMessages;
204             this._automaticTracer.dumpTimingDataToConsole = this.dumpTimingDataToConsole;
205             // This will be ignored if a custom tracer is installed.
206             this.activeTracer = this._automaticTracer;
207         }
208     }
209
210     _agentForDomain(domainName)
211     {
212         if (this._agents[domainName])
213             return this._agents[domainName];
214
215         var agent = new InspectorBackend.Agent(domainName);
216         this._agents[domainName] = agent;
217         return agent;
218     }
219
220     _sendCommandToBackendWithCallback(command, parameters, callback)
221     {
222         let sequenceId = this._lastSequenceId++;
223
224         let messageObject = {
225             "id": sequenceId,
226             "method": command.qualifiedName,
227         };
228
229         if (Object.keys(parameters).length)
230             messageObject["params"] = parameters;
231
232         let responseData = {command, callback};
233
234         if (this.activeTracer)
235             responseData.sendRequestTimestamp = timestamp();
236
237         this._pendingResponses.set(sequenceId, responseData);
238         this._sendMessageToBackend(messageObject);
239     }
240
241     _sendCommandToBackendExpectingPromise(command, parameters)
242     {
243         let sequenceId = this._lastSequenceId++;
244
245         let messageObject = {
246             "id": sequenceId,
247             "method": command.qualifiedName,
248         };
249
250         if (Object.keys(parameters).length)
251             messageObject["params"] = parameters;
252
253         let responseData = {command};
254
255         if (this.activeTracer)
256             responseData.sendRequestTimestamp = timestamp();
257
258         let responsePromise = new Promise(function(resolve, reject) {
259             responseData.promise = {resolve, reject};
260         });
261
262         this._pendingResponses.set(sequenceId, responseData);
263         this._sendMessageToBackend(messageObject);
264
265         return responsePromise;
266     }
267
268     _sendMessageToBackend(messageObject)
269     {
270         let stringifiedMessage = JSON.stringify(messageObject);
271         if (this.activeTracer)
272             this.activeTracer.logFrontendRequest(stringifiedMessage);
273
274         InspectorFrontendHost.sendMessageToBackend(stringifiedMessage);
275     }
276
277     _dispatchResponse(messageObject)
278     {
279         console.assert(this._pendingResponses.size >= 0);
280
281         if (messageObject["error"]) {
282             if (messageObject["error"].code !== -32000)
283                 this._reportProtocolError(messageObject);
284         }
285
286         let sequenceId = messageObject["id"];
287         console.assert(this._pendingResponses.has(sequenceId), sequenceId, this._pendingResponses);
288
289         let responseData = this._pendingResponses.take(sequenceId);
290         let {command, callback, promise} = responseData;
291
292         let processingStartTimestamp;
293         if (this.activeTracer) {
294             processingStartTimestamp = timestamp();
295             this.activeTracer.logWillHandleResponse(JSON.stringify(messageObject));
296         }
297
298         if (typeof callback === "function")
299             this._dispatchResponseToCallback(command, messageObject, callback);
300         else if (typeof promise === "object")
301             this._dispatchResponseToPromise(command, messageObject, promise);
302         else
303             console.error("Received a command response without a corresponding callback or promise.", messageObject, command);
304
305         if (this.activeTracer) {
306             let processingTime = (timestamp() - processingStartTimestamp).toFixed(3);
307             let roundTripTime = (processingStartTimestamp - responseData.sendRequestTimestamp).toFixed(3);
308             this.activeTracer.logDidHandleResponse(JSON.stringify(messageObject), {rtt: roundTripTime, dispatch: processingTime});
309         }
310
311         if (this._deferredScripts.length && !this._pendingResponses.size)
312             this._flushPendingScripts();
313     }
314
315     _dispatchResponseToCallback(command, messageObject, callback)
316     {
317         let callbackArguments = [];
318         callbackArguments.push(messageObject["error"] ? messageObject["error"].message : null);
319
320         if (messageObject["result"]) {
321             for (var parameterName of command.replySignature)
322                 callbackArguments.push(messageObject["result"][parameterName]);
323         }
324
325         try {
326             callback.apply(null, callbackArguments);
327         } catch (e) {
328             console.error("Uncaught exception in inspector page while dispatching callback for command " + command.qualifiedName, e);
329         }
330     }
331
332     _dispatchResponseToPromise(command, messageObject, promise)
333     {
334         let {resolve, reject} = promise;
335         if (messageObject["error"])
336             reject(new Error(messageObject["error"].message));
337         else
338             resolve(messageObject["result"]);
339     }
340
341     _dispatchEvent(messageObject)
342     {
343         let qualifiedName = messageObject["method"];
344         let [domainName, eventName] = qualifiedName.split(".");
345         if (!(domainName in this._agents)) {
346             console.error("Protocol Error: Attempted to dispatch method '" + eventName + "' for non-existing domain '" + domainName + "'");
347             return;
348         }
349
350         let agent = this._agentForDomain(domainName);
351         if (!agent.active) {
352             console.error("Protocol Error: Attempted to dispatch method for domain '" + domainName + "' which exists but is not active.");
353             return;
354         }
355
356         let event = agent.getEvent(eventName);
357         if (!event) {
358             console.error("Protocol Error: Attempted to dispatch an unspecified method '" + qualifiedName + "'");
359             return;
360         }
361
362         let eventArguments = [];
363         if (messageObject["params"])
364             eventArguments = event.parameterNames.map((name) => messageObject["params"][name]);
365
366         let processingStartTimestamp;
367         if (this.activeTracer) {
368             processingStartTimestamp = timestamp();
369             this.activeTracer.logWillHandleEvent(JSON.stringify(messageObject));
370         }
371
372         try {
373             agent.dispatchEvent(eventName, eventArguments);
374         } catch (e) {
375             console.error("Uncaught exception in inspector page while handling event " + qualifiedName, e);
376             if (this.activeTracer)
377                 this.activeTracer.logFrontendException(JSON.stringify(messageObject), e);
378         }
379
380         if (this.activeTracer) {
381             let processingTime = (timestamp() - processingStartTimestamp).toFixed(3);
382             this.activeTracer.logDidHandleEvent(JSON.stringify(messageObject), {dispatch: processingTime});
383         }
384     }
385
386     _reportProtocolError(messageObject)
387     {
388         console.error("Request with id = " + messageObject["id"] + " failed. " + JSON.stringify(messageObject["error"]));
389     }
390
391     _flushPendingScripts()
392     {
393         console.assert(this._pendingResponses.size === 0);
394
395         let scriptsToRun = this._deferredScripts;
396         this._deferredScripts = [];
397         for (let script of scriptsToRun)
398             script.call(this);
399     }
400 };
401
402 InspectorBackend = new InspectorBackendClass;
403
404 InspectorBackend.Agent = class InspectorBackendAgent
405 {
406     constructor(domainName)
407     {
408         this._domainName = domainName;
409
410         // Agents are always created, but are only useable after they are activated.
411         this._active = false;
412
413         // Commands are stored directly on the Agent instance using their unqualified
414         // method name as the property. Thus, callers can write: FooAgent.methodName().
415         // Enums are stored similarly based on the unqualified type name.
416         this._events = {};
417     }
418
419     // Public
420
421     get domainName()
422     {
423         return this._domainName;
424     }
425
426     get active()
427     {
428         return this._active;
429     }
430
431     set dispatcher(value)
432     {
433         this._dispatcher = value;
434     }
435
436     addEnum(enumName, enumValues)
437     {
438         this[enumName] = enumValues;
439     }
440
441     addCommand(command)
442     {
443         this[command.commandName] = command;
444     }
445
446     addEvent(event)
447     {
448         this._events[event.eventName] = event;
449     }
450
451     getEvent(eventName)
452     {
453         return this._events[eventName];
454     }
455
456     hasEvent(eventName)
457     {
458         return eventName in this._events;
459     }
460
461     activate()
462     {
463         this._active = true;
464         window[this._domainName + "Agent"] = this;
465     }
466
467     dispatchEvent(eventName, eventArguments)
468     {
469         if (!(eventName in this._dispatcher)) {
470             console.error("Protocol Error: Attempted to dispatch an unimplemented method '" + this._domainName + "." + eventName + "'");
471             return false;
472         }
473
474         this._dispatcher[eventName].apply(this._dispatcher, eventArguments);
475         return true;
476     }
477 };
478
479 // InspectorBackend.Command can't use ES6 classes because of its trampoline nature.
480 // But we can use strict mode to get stricter handling of the code inside its functions.
481 InspectorBackend.Command = function(backend, qualifiedName, callSignature, replySignature)
482 {
483     'use strict';
484
485     this._backend = backend;
486     this._instance = this;
487
488     var [domainName, commandName] = qualifiedName.split(".");
489     this._qualifiedName = qualifiedName;
490     this._commandName = commandName;
491     this._callSignature = callSignature || [];
492     this._replySignature = replySignature || [];
493 };
494
495 InspectorBackend.Command.create = function(backend, commandName, callSignature, replySignature)
496 {
497     'use strict';
498
499     var instance = new InspectorBackend.Command(backend, commandName, callSignature, replySignature);
500
501     function callable() {
502         return instance._invokeWithArguments.apply(instance, arguments);
503     }
504
505     callable._instance = instance;
506     Object.setPrototypeOf(callable, InspectorBackend.Command.prototype);
507
508     return callable;
509 };
510
511 // As part of the workaround to make commands callable, these functions use |this._instance|.
512 // |this| could refer to the callable trampoline, or the InspectorBackend.Command instance.
513 InspectorBackend.Command.prototype = {
514     __proto__: Function.prototype,
515
516     // Public
517
518     get qualifiedName()
519     {
520         return this._instance._qualifiedName;
521     },
522
523     get commandName()
524     {
525         return this._instance._commandName;
526     },
527
528     get callSignature()
529     {
530         return this._instance._callSignature;
531     },
532
533     get replySignature()
534     {
535         return this._instance._replySignature;
536     },
537
538     invoke: function(commandArguments, callback)
539     {
540         'use strict';
541
542         let instance = this._instance;
543
544         if (typeof callback === "function")
545             instance._backend._sendCommandToBackendWithCallback(instance, commandArguments, callback);
546         else
547             return instance._backend._sendCommandToBackendExpectingPromise(instance, commandArguments);
548     },
549
550     supports: function(parameterName)
551     {
552         'use strict';
553
554         var instance = this._instance;
555         return instance.callSignature.some(function(parameter) {
556             return parameter["name"] === parameterName;
557         });
558     },
559
560     // Private
561
562     _invokeWithArguments: function()
563     {
564         'use strict';
565
566         let instance = this._instance;
567         let commandArguments = Array.from(arguments);
568         let callback = typeof commandArguments.lastValue === "function" ? commandArguments.pop() : null;
569
570         function deliverFailure(message) {
571             console.error(`Protocol Error: ${message}`);
572             if (callback)
573                 setTimeout(callback.bind(null, message), 0);
574             else
575                 return Promise.reject(new Error(message));
576         }
577
578         let parameters = {};
579         for (let parameter of instance.callSignature) {
580             let parameterName = parameter["name"];
581             let typeName = parameter["type"];
582             let optionalFlag = parameter["optional"];
583
584             if (!commandArguments.length && !optionalFlag)
585                 return deliverFailure(`Invalid number of arguments for command '${instance.qualifiedName}'.`);
586
587             let value = commandArguments.shift();
588             if (optionalFlag && value === undefined)
589                 continue;
590
591             if (typeof value !== typeName)
592                 return deliverFailure(`Invalid type of argument '${parameterName}' for command '${instance.qualifiedName}' call. It must be '${typeName}' but it is '${typeof value}'.`);
593
594             parameters[parameterName] = value;
595         }
596
597         if (!callback && commandArguments.length === 1 && commandArguments[0] !== undefined)
598             return deliverFailure(`Protocol Error: Optional callback argument for command '${instance.qualifiedName}' call must be a function but its type is '${typeof args[0]}'.`);
599
600         if (callback)
601             instance._backend._sendCommandToBackendWithCallback(instance, parameters, callback);
602         else
603             return instance._backend._sendCommandToBackendExpectingPromise(instance, parameters);
604     }
605 };
606
607 InspectorBackend.Event = class Event
608 {
609     constructor(eventName, parameterNames)
610     {
611         this.eventName = eventName;
612         this.parameterNames = parameterNames;
613     }
614 };