summaryrefslogtreecommitdiffstats
path: root/remote/shared/messagehandler/MessageHandler.sys.mjs
blob: ed15ed29b8094e37bb3071ee3334efbe2a72fa26 (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
/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  error: "chrome://remote/content/shared/messagehandler/Errors.sys.mjs",
  EventsDispatcher:
    "chrome://remote/content/shared/messagehandler/EventsDispatcher.sys.mjs",
  Log: "chrome://remote/content/shared/Log.sys.mjs",
  ModuleCache:
    "chrome://remote/content/shared/messagehandler/ModuleCache.sys.mjs",
});

XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());

/**
 * A ContextDescriptor object provides information to decide if a broadcast or
 * a session data item should be applied to a specific MessageHandler context.
 *
 * @typedef {object} ContextDescriptor
 * @property {ContextDescriptorType} type
 *     The type of context
 * @property {string=} id
 *     Unique id of a given context for the provided type.
 *     For ContextDescriptorType.All, id can be ommitted.
 *     For ContextDescriptorType.TopBrowsingContext, the id should be the
 *     browserId corresponding to a top-level browsing context.
 */

/**
 * Enum of ContextDescriptor types.
 *
 * @enum {string}
 */
export const ContextDescriptorType = {
  All: "All",
  TopBrowsingContext: "TopBrowsingContext",
};

/**
 * A ContextInfo identifies a given context that can be linked to a MessageHandler
 * instance. It should be used to identify events coming from this context.
 *
 * It can either be provided by the MessageHandler itself, when the event is
 * emitted from the context it relates to.
 *
 * Or it can be assembled manually, for instance when emitting an event which
 * relates to a window global from the root layer (eg browsingContext.contextCreated).
 *
 * @typedef {object} ContextInfo
 * @property {string} contextId
 *     Unique id of the MessageHandler corresponding to this context.
 * @property {string} type
 *     One of MessageHandler.type.
 */

/**
 * MessageHandler instances are dedicated to handle both Commands and Events
 * to enable automation and introspection for remote control protocols.
 *
 * MessageHandler instances are designed to form a network, where each instance
 * should allow to inspect a specific context (eg. a BrowsingContext, a Worker,
 * etc). Those instances might live in different processes and threads but
 * should be linked together by the usage of a single sessionId, shared by all
 * the instances of a single MessageHandler network.
 *
 * MessageHandler instances will be dynamically spawned depending on which
 * Command or which Event needs to be processed and should therefore not be
 * explicitly created by consumers, nor used directly.
 *
 * The only exception is the ROOT MessageHandler. This MessageHandler will be
 * the entry point to send commands to the rest of the network. It will also
 * emit all the relevant events captured by the network.
 *
 * However, even to create this ROOT MessageHandler, consumers should use the
 * RootMessageHandlerRegistry. This singleton will ensure that MessageHandler
 * instances are properly registered and can be retrieved based on a given
 * session id as well as some other context information.
 */
export class MessageHandler extends EventEmitter {
  #context;
  #contextId;
  #eventsDispatcher;
  #moduleCache;
  #registry;
  #sessionId;

  /**
   * Create a new MessageHandler instance.
   *
   * @param {string} sessionId
   *     ID of the session the handler is used for.
   * @param {object} context
   *     The context linked to this MessageHandler instance.
   * @param {MessageHandlerRegistry} registry
   *     The MessageHandlerRegistry which owns this MessageHandler instance.
   */
  constructor(sessionId, context, registry) {
    super();

    this.#moduleCache = new lazy.ModuleCache(this);

    this.#sessionId = sessionId;
    this.#context = context;
    this.#contextId = this.constructor.getIdFromContext(context);
    this.#eventsDispatcher = new lazy.EventsDispatcher(this);
    this.#registry = registry;
  }

  get context() {
    return this.#context;
  }

  get contextId() {
    return this.#contextId;
  }

  get eventsDispatcher() {
    return this.#eventsDispatcher;
  }

  get moduleCache() {
    return this.#moduleCache;
  }

  get name() {
    return [this.sessionId, this.constructor.type, this.contextId].join("-");
  }

  get registry() {
    return this.#registry;
  }

  get sessionId() {
    return this.#sessionId;
  }

  destroy() {
    lazy.logger.trace(
      `MessageHandler ${this.constructor.type} for session ${this.sessionId} is being destroyed`
    );
    this.#eventsDispatcher.destroy();
    this.#moduleCache.destroy();

    // At least the MessageHandlerRegistry will be expecting this event in order
    // to remove the instance from the registry when destroyed.
    this.emit("message-handler-destroyed", this);
  }

  /**
   * Emit a message handler event.
   *
   * Such events should bubble up to the root of a MessageHandler network.
   *
   * @param {string} name
   *     Name of the event. Protocol level events should be of the
   *     form [module name].[event name].
   * @param {object} data
   *     The event's data.
   * @param {ContextInfo=} contextInfo
   *     The event's context info, used to identify the origin of the event.
   *     If not provided, the context info of the current MessageHandler will be
   *     used.
   */
  emitEvent(name, data, contextInfo) {
    // If no contextInfo field is provided on the event, extract it from the
    // MessageHandler instance.
    contextInfo = contextInfo || this.#getContextInfo();

    // Events are emitted both under their own name for consumers listening to
    // a specific and as `message-handler-event` for consumers which need to
    // catch all events.
    this.emit(name, data, contextInfo);
    this.emit("message-handler-event", {
      name,
      contextInfo,
      data,
      sessionId: this.sessionId,
    });
  }

  /**
   * @typedef {object} CommandDestination
   * @property {string} type
   *     One of MessageHandler.type.
   * @property {string=} id
   *     Unique context identifier. The format depends on the type.
   *     For WINDOW_GLOBAL destinations, this is a browsing context id.
   *     Optional, should only be provided if `contextDescriptor` is missing.
   * @property {ContextDescriptor=} contextDescriptor
   *     Descriptor used to match several contexts, which will all receive the
   *     command.
   *     Optional, should only be provided if `id` is missing.
   */

  /**
   * @typedef {object} Command
   * @property {string} commandName
   *     The name of the command to execute.
   * @property {string} moduleName
   *     The name of the module.
   * @property {object} params
   *     Optional command parameters.
   * @property {CommandDestination} destination
   *     The destination describing a debuggable context.
   * @property {boolean=} retryOnAbort
   *     Optional. When true, commands will be retried upon AbortError, which
   *     can occur when the underlying JSWindowActor pair is destroyed.
   *     Defaults to `false`.
   */

  /**
   * Retrieve all module classes matching the moduleName and destination.
   * See `getAllModuleClasses` (ModuleCache.jsm) for more details.
   *
   * @param {string} moduleName
   *     The name of the module.
   * @param {Destination} destination
   *     The destination.
   * @returns {Array.<class<Module>|null>}
   *     An array of Module classes.
   */
  getAllModuleClasses(moduleName, destination) {
    return this.#moduleCache.getAllModuleClasses(moduleName, destination);
  }

  /**
   * Handle a command, either in one of the modules owned by this MessageHandler
   * or in a another MessageHandler after forwarding the command.
   *
   * @param {Command} command
   *     The command that should be either handled in this layer or forwarded to
   *     the next layer leading to the destination.
   * @returns {Promise} A Promise that will resolve with the return value of the
   *     command once it has been executed.
   */
  handleCommand(command) {
    const { moduleName, commandName, params, destination } = command;
    lazy.logger.trace(
      `Received command ${moduleName}.${commandName} for destination ${destination.type}`
    );

    if (!this.supportsCommand(moduleName, commandName, destination)) {
      throw new lazy.error.UnsupportedCommandError(
        `${moduleName}.${commandName} not supported for destination ${destination?.type}`
      );
    }

    const module = this.#moduleCache.getModuleInstance(moduleName, destination);
    if (module && module.supportsMethod(commandName)) {
      return module[commandName](params, destination);
    }

    return this.forwardCommand(command);
  }

  toString() {
    return `[object ${this.constructor.name} ${this.name}]`;
  }

  /**
   * Apply the initial session data items provided to this MessageHandler on
   * startup. Implementation is specific to each MessageHandler class.
   *
   * By default the implementation is a no-op.
   *
   * @param {Array<SessionDataItem>} sessionDataItems
   *     Initial session data items for this MessageHandler.
   */
  async applyInitialSessionDataItems(sessionDataItems) {}

  /**
   * Returns the module path corresponding to this MessageHandler class.
   *
   * Needs to be implemented in the sub class.
   */
  static get modulePath() {
    throw new Error("Not implemented");
  }

  /**
   * Returns the type corresponding to this MessageHandler class.
   *
   * Needs to be implemented in the sub class.
   */
  static get type() {
    throw new Error("Not implemented");
  }

  /**
   * Returns the id corresponding to a context compatible with this
   * MessageHandler class.
   *
   * Needs to be implemented in the sub class.
   */
  static getIdFromContext(context) {
    throw new Error("Not implemented");
  }

  /**
   * Forward a command to other MessageHandlers.
   *
   * Needs to be implemented in the sub class.
   */
  forwardCommand(command) {
    throw new Error("Not implemented");
  }

  /**
   * Check if contextDescriptor matches the context linked
   * to this MessageHandler instance.
   *
   * Needs to be implemented in the sub class.
   */
  matchesContext(contextDescriptor) {
    throw new Error("Not implemented");
  }

  /**
   * Check if the given command is supported in the module
   * for the destination
   *
   * @param {string} moduleName
   *     The name of the module.
   * @param {string} commandName
   *     The name of the command.
   * @param {Destination} destination
   *     The destination.
   * @returns {boolean}
   *     True if the command is supported.
   */
  supportsCommand(moduleName, commandName, destination) {
    return this.getAllModuleClasses(moduleName, destination).some(cls =>
      cls.supportsMethod(commandName)
    );
  }

  /**
   * Return the context information for this MessageHandler instance, which
   * can be used to identify the origin of an event.
   *
   * @returns {ContextInfo}
   *     The context information for this MessageHandler.
   */
  #getContextInfo() {
    return {
      contextId: this.contextId,
      type: this.constructor.type,
    };
  }
}