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