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/. */
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");
13 callFunctionWithAsyncStack,
14 } = require("resource://devtools/shared/platform/stack.js");
17 * Base class for client-side actor fronts.
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
31 class Front extends Pool {
32 constructor(conn = null, targetFront = null, parentFront = null) {
35 throw new Error("Front without conn");
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;
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;
62 * Return the parent front.
65 return this.parentFront && this.parentFront.actorID
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.
80 // This method is also called from `DevToolsClient`, when a connector is destroyed
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.
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.
98 "Connection closed, pending request to " +
103 "\n\nRequest stack:\n" +
104 stack.formattedStack;
105 deferred.reject(new Error(msg));
112 this._isDestroyed = true;
114 this.targetFront = null;
115 this.parentFront = null;
116 this._frontCreationListeners = null;
117 this._frontDestructionListeners = null;
118 this._beforeListeners = null;
121 async manage(front, form, ctx) {
122 if (!front.actorID) {
124 "Can't manage front without an actor ID.\n" +
125 "Ensure server supports " +
131 if (front.parentFront && front.parentFront !== this) {
133 `${this.actorID} (${this.typeName}) can't manage ${front.actorID}
134 (${front.typeName}) since it has a different parentFront ${
136 ? front.parentFront.actorID + "(" + front.parentFront.typeName + ")"
144 if (typeof front.initialize == "function") {
145 await front.initialize();
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.
155 front.form(form, ctx);
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);
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);
175 * Listen for the creation and/or destruction of fronts matching one of the provided types.
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.
186 watchFronts(typeName, onAvailable, onDestroy) {
187 if (this.isDestroyed()) {
188 // The front was already destroyed, bail out.
190 `Tried to call watchFronts for the '${typeName}' type on an ` +
191 `already destroyed front '${this.typeName}'.`
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) {
206 if (!this._frontCreationListeners) {
207 this._frontCreationListeners = new EventEmitter();
209 // Then register the callback for fronts instantiated in the future
210 this._frontCreationListeners.on(typeName, onAvailable);
214 if (!this._frontDestructionListeners) {
215 this._frontDestructionListeners = new EventEmitter();
217 this._frontDestructionListeners.on(typeName, onDestroy);
222 * Stop listening for the creation and/or destruction of a given type of fronts.
223 * See `watchFronts()` for documentation of the arguments.
225 unwatchFronts(typeName, onAvailable, onDestroy) {
226 if (this.isDestroyed()) {
227 // The front was already destroyed, bail out.
229 `Tried to call unwatchFronts for the '${typeName}' type on an ` +
230 `already destroyed front '${this.typeName}'.`
235 if (onAvailable && this._frontCreationListeners) {
236 this._frontCreationListeners.off(typeName, onAvailable);
238 if (onDestroy && this._frontDestructionListeners) {
239 this._frontDestructionListeners.off(typeName, onDestroy);
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.
250 * Event emitted by the actor to intercept.
251 * @param Function callback
252 * Function that will process the event.
254 before(type, callback) {
255 if (this._beforeListeners.has(type)) {
257 `Can't register multiple before listeners for "${type}".`
260 this._beforeListeners.set(type, callback);
264 return "[Front for " + this.typeName + "/" + this.actorID + "]";
268 * Update the actor from its representation.
269 * Subclasses should override this.
274 * Send a packet on the connection.
278 this.conn._transport.send(packet);
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);
289 * Send a two-way request on the connection.
292 const deferred = Promise.withResolvers();
293 // Save packet basics for debugging
294 const { to, type } = packet;
295 this._requests.push({
297 to: to || this.actorID,
302 return deferred.promise;
306 * Handler for incoming packets from the client's actor.
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.
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);
321 args = event.request.read(packet, this);
323 console.error("Error reading event: " + packet.type);
324 console.exception(ex);
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
330 const beforeEvent = this._beforeListeners.get(event.name);
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") {
337 super.emit(event.name, ...args);
338 ChromeUtils.addProfilerMarker(
339 "DevTools:RDP Front",
341 `${this.typeName}.${event.name}`
348 super.emit(event.name, ...args);
349 ChromeUtils.addProfilerMarker(
350 "DevTools:RDP Front",
352 `${this.typeName}.${event.name}`
357 // Remaining packets must be responses.
358 if (this._requests.length === 0) {
360 "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
361 const err = Error(msg);
366 const { deferred, stack } = this._requests.shift();
367 callFunctionWithAsyncStack(
371 if (packet.error && packet.message) {
373 "Protocol error (" + packet.error + "): " + packet.message;
375 message = packet.error;
377 message += " from: " + this.actorID;
378 if (packet.fileName) {
379 const { fileName, columnNumber, lineNumber } = packet;
380 message += ` (${fileName}:${lineNumber}:${columnNumber})`;
382 const packetError = new Error(message);
383 deferred.reject(packetError);
385 deferred.resolve(packet);
394 return !!this._requests.length;
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.
403 * Resolved when all requests have settled.
405 waitForRequestsToSettle() {
406 return settleAll(this._requests.map(({ deferred }) => deferred.promise));
410 exports.Front = Front;