summaryrefslogtreecommitdiffstats
path: root/devtools/shared/protocol/Front.js
blob: 1298f3a075852eb9b6e40f08a9b7d927e7b78554 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var { settleAll } = require("resource://devtools/shared/DevToolsUtils.js");
var EventEmitter = require("resource://devtools/shared/event-emitter.js");

var { Pool } = require("resource://devtools/shared/protocol/Pool.js");
var {
  getStack,
  callFunctionWithAsyncStack,
} = require("resource://devtools/shared/platform/stack.js");

/**
 * Base class for client-side actor fronts.
 *
 * @param [DevToolsClient|null] conn
 *   The conn must either be DevToolsClient or null. Must have
 *   addActorPool, removeActorPool, and poolFor.
 *   conn can be null if the subclass provides a conn property.
 * @param [Target|null] target
 *   If we are instantiating a target-scoped front, this is a reference to the front's
 *   Target instance, otherwise this is null.
 * @param [Front|null] parentFront
 *   The parent front. This is only available if the Front being initialized is a child
 *   of a parent front.
 * @constructor
 */
class Front extends Pool {
  constructor(conn = null, targetFront = null, parentFront = null) {
    super(conn);
    if (!conn) {
      throw new Error("Front without conn");
    }
    this.actorID = null;
    // The targetFront attribute represents the debuggable context. Only target-scoped
    // fronts and their children fronts will have the targetFront attribute set.
    this.targetFront = targetFront;
    // The parentFront attribute points to its parent front. Only children of
    // target-scoped fronts will have the parentFront attribute set.
    this.parentFront = parentFront;
    this._requests = [];

    // Front listener functions registered via `watchFronts`
    this._frontCreationListeners = null;
    this._frontDestructionListeners = null;

    // List of optional listener for each event, that is processed immediatly on packet
    // receival, before emitting event via EventEmitter on the Front.
    // These listeners are register via Front.before function.
    // Map(Event Name[string] => Event Listener[function])
    this._beforeListeners = new Map();

    // This flag allows to check if the `initialize` method has resolved.
    // Used to avoid notifying about initialized fronts in `watchFronts`.
    this._initializeResolved = false;
  }

  /**
   * Return the parent front.
   */
  getParent() {
    return this.parentFront && this.parentFront.actorID
      ? this.parentFront
      : null;
  }

  destroy() {
    // Prevent destroying twice if a `forwardCancelling` event has already been received
    // and already called `baseFrontClassDestroy`
    this.baseFrontClassDestroy();

    // Keep `clearEvents` out of baseFrontClassDestroy as we still want TargetMixin to be
    // able to emit `target-destroyed` after we called baseFrontClassDestroy from DevToolsClient.purgeRequests.
    this.clearEvents();
  }

  // This method is also called from `DevToolsClient`, when a connector is destroyed
  // and we should:
  // - reject all pending request made to the remote process/target/thread.
  // - avoid trying to do new request against this remote context.
  // - unmanage this front, so that DevToolsClient.getFront no longer returns it.
  //
  // When a connector is destroyed a `forwardCancelling` RDP event is sent by the server.
  // This is done in a distinct method from `destroy` in order to do all that immediately,
  // even if `Front.destroy` is overloaded by an async method.
  baseFrontClassDestroy() {
    // Reject all outstanding requests, they won't make sense after
    // the front is destroyed.
    while (this._requests.length) {
      const { deferred, to, type, stack } = this._requests.shift();
      // Note: many tests are ignoring `Connection closed` promise rejections,
      // via PromiseTestUtils.allowMatchingRejectionsGlobally.
      // Do not update the message without updating the tests.
      const msg =
        "Connection closed, pending request to " +
        to +
        ", type " +
        type +
        " failed" +
        "\n\nRequest stack:\n" +
        stack.formattedStack;
      deferred.reject(new Error(msg));
    }

    if (this.actorID) {
      super.destroy();
      this.actorID = null;
    }
    this._isDestroyed = true;

    this.targetFront = null;
    this.parentFront = null;
    this._frontCreationListeners = null;
    this._frontDestructionListeners = null;
    this._beforeListeners = null;
  }

  async manage(front, form, ctx) {
    if (!front.actorID) {
      throw new Error(
        "Can't manage front without an actor ID.\n" +
          "Ensure server supports " +
          front.typeName +
          "."
      );
    }

    if (front.parentFront && front.parentFront !== this) {
      throw new Error(
        `${this.actorID} (${this.typeName}) can't manage ${front.actorID}
        (${front.typeName}) since it has a different parentFront ${
          front.parentFront
            ? front.parentFront.actorID + "(" + front.parentFront.typeName + ")"
            : "<no parentFront>"
        }`
      );
    }

    super.manage(front);

    if (typeof front.initialize == "function") {
      await front.initialize();
    }
    front._initializeResolved = true;

    // Ensure calling form() *before* notifying about this front being just created.
    // We exprect the front to be fully initialized, especially via its form attributes.
    // But do that *after* calling manage() so that the front is already registered
    // in Pools and can be fetched by its ID, in case a child actor, created in form()
    // tries to get a reference to its parent via the actor ID.
    if (form) {
      front.form(form, ctx);
    }

    // Call listeners registered via `watchFronts` method
    // (ignore if this front has been destroyed)
    if (this._frontCreationListeners) {
      this._frontCreationListeners.emit(front.typeName, front);
    }
  }

  async unmanage(front) {
    super.unmanage(front);

    // Call listeners registered via `watchFronts` method
    if (this._frontDestructionListeners) {
      this._frontDestructionListeners.emit(front.typeName, front);
    }
  }

  /*
   * Listen for the creation and/or destruction of fronts matching one of the provided types.
   *
   * @param {String} typeName
   *        Actor type to watch.
   * @param {Function} onAvailable (optional)
   *        Callback fired when a front has been just created or was already available.
   *        The function is called with one arguments, the front.
   * @param {Function} onDestroy (optional)
   *        Callback fired in case of front destruction.
   *        The function is called with the same argument than onAvailable.
   */
  watchFronts(typeName, onAvailable, onDestroy) {
    if (this.isDestroyed()) {
      // The front was already destroyed, bail out.
      console.error(
        `Tried to call watchFronts for the '${typeName}' type on an ` +
          `already destroyed front '${this.typeName}'.`
      );
      return;
    }

    if (onAvailable) {
      // First fire the callback on fronts with the correct type and which have
      // been initialized. If initialize() is still in progress, the front will
      // be emitted via _frontCreationListeners shortly after.
      for (const front of this.poolChildren()) {
        if (front.typeName == typeName && front._initializeResolved) {
          onAvailable(front);
        }
      }

      if (!this._frontCreationListeners) {
        this._frontCreationListeners = new EventEmitter();
      }
      // Then register the callback for fronts instantiated in the future
      this._frontCreationListeners.on(typeName, onAvailable);
    }

    if (onDestroy) {
      if (!this._frontDestructionListeners) {
        this._frontDestructionListeners = new EventEmitter();
      }
      this._frontDestructionListeners.on(typeName, onDestroy);
    }
  }

  /**
   * Stop listening for the creation and/or destruction of a given type of fronts.
   * See `watchFronts()` for documentation of the arguments.
   */
  unwatchFronts(typeName, onAvailable, onDestroy) {
    if (this.isDestroyed()) {
      // The front was already destroyed, bail out.
      console.error(
        `Tried to call unwatchFronts for the '${typeName}' type on an ` +
          `already destroyed front '${this.typeName}'.`
      );
      return;
    }

    if (onAvailable && this._frontCreationListeners) {
      this._frontCreationListeners.off(typeName, onAvailable);
    }
    if (onDestroy && this._frontDestructionListeners) {
      this._frontDestructionListeners.off(typeName, onDestroy);
    }
  }

  /**
   * Register an event listener that will be called immediately on packer receival.
   * The given callback is going to be called before emitting the event via EventEmitter
   * API on the Front. Event emitting will be delayed if the callback is async.
   * Only one such listener can be registered per type of event.
   *
   * @param String type
   *   Event emitted by the actor to intercept.
   * @param Function callback
   *   Function that will process the event.
   */
  before(type, callback) {
    if (this._beforeListeners.has(type)) {
      throw new Error(
        `Can't register multiple before listeners for "${type}".`
      );
    }
    this._beforeListeners.set(type, callback);
  }

  toString() {
    return "[Front for " + this.typeName + "/" + this.actorID + "]";
  }

  /**
   * Update the actor from its representation.
   * Subclasses should override this.
   */
  form(form) {}

  /**
   * Send a packet on the connection.
   */
  send(packet) {
    if (packet.to) {
      this.conn._transport.send(packet);
    } else {
      packet.to = this.actorID;
      // The connection might be closed during the promise resolution
      if (this.conn && this.conn._transport) {
        this.conn._transport.send(packet);
      }
    }
  }

  /**
   * Send a two-way request on the connection.
   */
  request(packet) {
    const deferred = Promise.withResolvers();
    // Save packet basics for debugging
    const { to, type } = packet;
    this._requests.push({
      deferred,
      to: to || this.actorID,
      type,
      stack: getStack(),
    });
    this.send(packet);
    return deferred.promise;
  }

  /**
   * Handler for incoming packets from the client's actor.
   */
  onPacket(packet) {
    if (this.isDestroyed()) {
      // If the Front was already destroyed, all the requests have been purged
      // and rejected with detailed error messages in baseFrontClassDestroy.
      return;
    }

    // Pick off event packets
    const type = packet.type || undefined;
    if (this._clientSpec.events && this._clientSpec.events.has(type)) {
      const event = this._clientSpec.events.get(packet.type);
      let args;
      try {
        args = event.request.read(packet, this);
      } catch (ex) {
        console.error("Error reading event: " + packet.type);
        console.exception(ex);
        throw ex;
      }
      // Check for "pre event" callback to be processed before emitting events on fronts
      // Use event.name instead of packet.type to use specific event name instead of RDP
      // packet's type.
      const beforeEvent = this._beforeListeners.get(event.name);
      if (beforeEvent) {
        const result = beforeEvent.apply(this, args);
        // Check to see if the beforeEvent returned a promise -- if so,
        // wait for their resolution before emitting. Otherwise, emit synchronously.
        if (result && typeof result.then == "function") {
          result.then(() => {
            super.emit(event.name, ...args);
            ChromeUtils.addProfilerMarker(
              "DevTools:RDP Front",
              null,
              `${this.typeName}.${event.name}`
            );
          });
          return;
        }
      }

      super.emit(event.name, ...args);
      ChromeUtils.addProfilerMarker(
        "DevTools:RDP Front",
        null,
        `${this.typeName}.${event.name}`
      );
      return;
    }

    // Remaining packets must be responses.
    if (this._requests.length === 0) {
      const msg =
        "Unexpected packet " + this.actorID + ", " + JSON.stringify(packet);
      const err = Error(msg);
      console.error(err);
      throw err;
    }

    const { deferred, stack } = this._requests.shift();
    callFunctionWithAsyncStack(
      () => {
        if (packet.error) {
          let message;
          if (packet.error && packet.message) {
            message =
              "Protocol error (" + packet.error + "): " + packet.message;
          } else {
            message = packet.error;
          }
          message += " from: " + this.actorID;
          if (packet.fileName) {
            const { fileName, columnNumber, lineNumber } = packet;
            message += ` (${fileName}:${lineNumber}:${columnNumber})`;
          }
          const packetError = new Error(message);
          deferred.reject(packetError);
        } else {
          deferred.resolve(packet);
        }
      },
      stack,
      "DevTools RDP"
    );
  }

  hasRequests() {
    return !!this._requests.length;
  }

  /**
   * Wait for all current requests from this front to settle.  This is especially useful
   * for tests and other utility environments that may not have events or mechanisms to
   * await the completion of requests without this utility.
   *
   * @return Promise
   *         Resolved when all requests have settled.
   */
  waitForRequestsToSettle() {
    return settleAll(this._requests.map(({ deferred }) => deferred.promise));
  }
}

exports.Front = Front;