Bug 1880216 - Migrate Fenix docs into Sphinx. r=owlish,geckoview-reviewers,android...
[gecko.git] / devtools / shared / protocol / Front.js
blobd3649e7bd13cbcc6c7b6aef7b442bb6797e66ad4
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 var { settleAll } = require("resource://devtools/shared/DevToolsUtils.js");
8 var EventEmitter = require("resource://devtools/shared/event-emitter.js");
10 var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
11 var {
12   getStack,
13   callFunctionWithAsyncStack,
14 } = require("resource://devtools/shared/platform/stack.js");
16 /**
17  * Base class for client-side actor fronts.
18  *
19  * @param [DevToolsClient|null] conn
20  *   The conn must either be DevToolsClient or null. Must have
21  *   addActorPool, removeActorPool, and poolFor.
22  *   conn can be null if the subclass provides a conn property.
23  * @param [Target|null] target
24  *   If we are instantiating a target-scoped front, this is a reference to the front's
25  *   Target instance, otherwise this is null.
26  * @param [Front|null] parentFront
27  *   The parent front. This is only available if the Front being initialized is a child
28  *   of a parent front.
29  * @constructor
30  */
31 class Front extends Pool {
32   constructor(conn = null, targetFront = null, parentFront = null) {
33     super(conn);
34     if (!conn) {
35       throw new Error("Front without conn");
36     }
37     this.actorID = null;
38     // The targetFront attribute represents the debuggable context. Only target-scoped
39     // fronts and their children fronts will have the targetFront attribute set.
40     this.targetFront = targetFront;
41     // The parentFront attribute points to its parent front. Only children of
42     // target-scoped fronts will have the parentFront attribute set.
43     this.parentFront = parentFront;
44     this._requests = [];
46     // Front listener functions registered via `watchFronts`
47     this._frontCreationListeners = null;
48     this._frontDestructionListeners = null;
50     // List of optional listener for each event, that is processed immediatly on packet
51     // receival, before emitting event via EventEmitter on the Front.
52     // These listeners are register via Front.before function.
53     // Map(Event Name[string] => Event Listener[function])
54     this._beforeListeners = new Map();
56     // This flag allows to check if the `initialize` method has resolved.
57     // Used to avoid notifying about initialized fronts in `watchFronts`.
58     this._initializeResolved = false;
59   }
61   /**
62    * Return the parent front.
63    */
64   getParent() {
65     return this.parentFront && this.parentFront.actorID
66       ? this.parentFront
67       : null;
68   }
70   destroy() {
71     // Prevent destroying twice if a `forwardCancelling` event has already been received
72     // and already called `baseFrontClassDestroy`
73     this.baseFrontClassDestroy();
75     // Keep `clearEvents` out of baseFrontClassDestroy as we still want TargetMixin to be
76     // able to emit `target-destroyed` after we called baseFrontClassDestroy from DevToolsClient.purgeRequests.
77     this.clearEvents();
78   }
80   // This method is also called from `DevToolsClient`, when a connector is destroyed
81   // and we should:
82   // - reject all pending request made to the remote process/target/thread.
83   // - avoid trying to do new request against this remote context.
84   // - unmanage this front, so that DevToolsClient.getFront no longer returns it.
85   //
86   // When a connector is destroyed a `forwardCancelling` RDP event is sent by the server.
87   // This is done in a distinct method from `destroy` in order to do all that immediately,
88   // even if `Front.destroy` is overloaded by an async method.
89   baseFrontClassDestroy() {
90     // Reject all outstanding requests, they won't make sense after
91     // the front is destroyed.
92     while (this._requests.length) {
93       const { deferred, to, type, stack } = this._requests.shift();
94       // Note: many tests are ignoring `Connection closed` promise rejections,
95       // via PromiseTestUtils.allowMatchingRejectionsGlobally.
96       // Do not update the message without updating the tests.
97       const msg =
98         "Connection closed, pending request to " +
99         to +
100         ", type " +
101         type +
102         " failed" +
103         "\n\nRequest stack:\n" +
104         stack.formattedStack;
105       deferred.reject(new Error(msg));
106     }
108     if (this.actorID) {
109       super.destroy();
110       this.actorID = null;
111     }
112     this._isDestroyed = true;
114     this.targetFront = null;
115     this.parentFront = null;
116     this._frontCreationListeners = null;
117     this._frontDestructionListeners = null;
118     this._beforeListeners = null;
119   }
121   async manage(front, form, ctx) {
122     if (!front.actorID) {
123       throw new Error(
124         "Can't manage front without an actor ID.\n" +
125           "Ensure server supports " +
126           front.typeName +
127           "."
128       );
129     }
131     if (front.parentFront && front.parentFront !== this) {
132       throw new Error(
133         `${this.actorID} (${this.typeName}) can't manage ${front.actorID}
134         (${front.typeName}) since it has a different parentFront ${
135           front.parentFront
136             ? front.parentFront.actorID + "(" + front.parentFront.typeName + ")"
137             : "<no parentFront>"
138         }`
139       );
140     }
142     super.manage(front);
144     if (typeof front.initialize == "function") {
145       await front.initialize();
146     }
147     front._initializeResolved = true;
149     // Ensure calling form() *before* notifying about this front being just created.
150     // We exprect the front to be fully initialized, especially via its form attributes.
151     // But do that *after* calling manage() so that the front is already registered
152     // in Pools and can be fetched by its ID, in case a child actor, created in form()
153     // tries to get a reference to its parent via the actor ID.
154     if (form) {
155       front.form(form, ctx);
156     }
158     // Call listeners registered via `watchFronts` method
159     // (ignore if this front has been destroyed)
160     if (this._frontCreationListeners) {
161       this._frontCreationListeners.emit(front.typeName, front);
162     }
163   }
165   async unmanage(front) {
166     super.unmanage(front);
168     // Call listeners registered via `watchFronts` method
169     if (this._frontDestructionListeners) {
170       this._frontDestructionListeners.emit(front.typeName, front);
171     }
172   }
174   /*
175    * Listen for the creation and/or destruction of fronts matching one of the provided types.
176    *
177    * @param {String} typeName
178    *        Actor type to watch.
179    * @param {Function} onAvailable (optional)
180    *        Callback fired when a front has been just created or was already available.
181    *        The function is called with one arguments, the front.
182    * @param {Function} onDestroy (optional)
183    *        Callback fired in case of front destruction.
184    *        The function is called with the same argument than onAvailable.
185    */
186   watchFronts(typeName, onAvailable, onDestroy) {
187     if (this.isDestroyed()) {
188       // The front was already destroyed, bail out.
189       console.error(
190         `Tried to call watchFronts for the '${typeName}' type on an ` +
191           `already destroyed front '${this.typeName}'.`
192       );
193       return;
194     }
196     if (onAvailable) {
197       // First fire the callback on fronts with the correct type and which have
198       // been initialized. If initialize() is still in progress, the front will
199       // be emitted via _frontCreationListeners shortly after.
200       for (const front of this.poolChildren()) {
201         if (front.typeName == typeName && front._initializeResolved) {
202           onAvailable(front);
203         }
204       }
206       if (!this._frontCreationListeners) {
207         this._frontCreationListeners = new EventEmitter();
208       }
209       // Then register the callback for fronts instantiated in the future
210       this._frontCreationListeners.on(typeName, onAvailable);
211     }
213     if (onDestroy) {
214       if (!this._frontDestructionListeners) {
215         this._frontDestructionListeners = new EventEmitter();
216       }
217       this._frontDestructionListeners.on(typeName, onDestroy);
218     }
219   }
221   /**
222    * Stop listening for the creation and/or destruction of a given type of fronts.
223    * See `watchFronts()` for documentation of the arguments.
224    */
225   unwatchFronts(typeName, onAvailable, onDestroy) {
226     if (this.isDestroyed()) {
227       // The front was already destroyed, bail out.
228       console.error(
229         `Tried to call unwatchFronts for the '${typeName}' type on an ` +
230           `already destroyed front '${this.typeName}'.`
231       );
232       return;
233     }
235     if (onAvailable && this._frontCreationListeners) {
236       this._frontCreationListeners.off(typeName, onAvailable);
237     }
238     if (onDestroy && this._frontDestructionListeners) {
239       this._frontDestructionListeners.off(typeName, onDestroy);
240     }
241   }
243   /**
244    * Register an event listener that will be called immediately on packer receival.
245    * The given callback is going to be called before emitting the event via EventEmitter
246    * API on the Front. Event emitting will be delayed if the callback is async.
247    * Only one such listener can be registered per type of event.
248    *
249    * @param String type
250    *   Event emitted by the actor to intercept.
251    * @param Function callback
252    *   Function that will process the event.
253    */
254   before(type, callback) {
255     if (this._beforeListeners.has(type)) {
256       throw new Error(
257         `Can't register multiple before listeners for "${type}".`
258       );
259     }
260     this._beforeListeners.set(type, callback);
261   }
263   toString() {
264     return "[Front for " + this.typeName + "/" + this.actorID + "]";
265   }
267   /**
268    * Update the actor from its representation.
269    * Subclasses should override this.
270    */
271   form() {}
273   /**
274    * Send a packet on the connection.
275    */
276   send(packet) {
277     if (packet.to) {
278       this.conn._transport.send(packet);
279     } else {
280       packet.to = this.actorID;
281       // The connection might be closed during the promise resolution
282       if (this.conn && this.conn._transport) {
283         this.conn._transport.send(packet);
284       }
285     }
286   }
288   /**
289    * Send a two-way request on the connection.
290    */
291   request(packet) {
292     const deferred = Promise.withResolvers();
293     // Save packet basics for debugging
294     const { to, type } = packet;
295     this._requests.push({
296       deferred,
297       to: to || this.actorID,
298       type,
299       stack: getStack(),
300     });
301     this.send(packet);
302     return deferred.promise;
303   }
305   /**
306    * Handler for incoming packets from the client's actor.
307    */
308   onPacket(packet) {
309     if (this.isDestroyed()) {
310       // If the Front was already destroyed, all the requests have been purged
311       // and rejected with detailed error messages in baseFrontClassDestroy.
312       return;
313     }
315     // Pick off event packets
316     const type = packet.type || undefined;
317     if (this._clientSpec.events && this._clientSpec.events.has(type)) {
318       const event = this._clientSpec.events.get(packet.type);
319       let args;
320       try {
321         args = event.request.read(packet, this);
322       } catch (ex) {
323         console.error("Error reading event: " + packet.type);
324         console.exception(ex);
325         throw ex;
326       }
327       // Check for "pre event" callback to be processed before emitting events on fronts
328       // Use event.name instead of packet.type to use specific event name instead of RDP
329       // packet's type.
330       const beforeEvent = this._beforeListeners.get(event.name);
331       if (beforeEvent) {
332         const result = beforeEvent.apply(this, args);
333         // Check to see if the beforeEvent returned a promise -- if so,
334         // wait for their resolution before emitting. Otherwise, emit synchronously.
335         if (result && typeof result.then == "function") {
336           result.then(() => {
337             super.emit(event.name, ...args);
338             ChromeUtils.addProfilerMarker(
339               "DevTools:RDP Front",
340               null,
341               `${this.typeName}.${event.name}`
342             );
343           });
344           return;
345         }
346       }
348       super.emit(event.name, ...args);
349       ChromeUtils.addProfilerMarker(
350         "DevTools:RDP Front",
351         null,
352         `${this.typeName}.${event.name}`
353       );
354       return;
355     }
357     // Remaining packets must be responses.
358     if (this._requests.length === 0) {
359       const msg =
360         "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
361       const err = Error(msg);
362       console.error(err);
363       throw err;
364     }
366     const { deferred, stack } = this._requests.shift();
367     callFunctionWithAsyncStack(
368       () => {
369         if (packet.error) {
370           let message;
371           if (packet.error && packet.message) {
372             message =
373               "Protocol error (" + packet.error + "): " + packet.message;
374           } else {
375             message = packet.error;
376           }
377           message += " from: " + this.actorID;
378           if (packet.fileName) {
379             const { fileName, columnNumber, lineNumber } = packet;
380             message += ` (${fileName}:${lineNumber}:${columnNumber})`;
381           }
382           const packetError = new Error(message);
383           deferred.reject(packetError);
384         } else {
385           deferred.resolve(packet);
386         }
387       },
388       stack,
389       "DevTools RDP"
390     );
391   }
393   hasRequests() {
394     return !!this._requests.length;
395   }
397   /**
398    * Wait for all current requests from this front to settle.  This is especially useful
399    * for tests and other utility environments that may not have events or mechanisms to
400    * await the completion of requests without this utility.
401    *
402    * @return Promise
403    *         Resolved when all requests have settled.
404    */
405   waitForRequestsToSettle() {
406     return settleAll(this._requests.map(({ deferred }) => deferred.promise));
407   }
410 exports.Front = Front;