Bug 1637473 [wpt PR 23557] - De-flaky pointerevents/pointerevent_capture_mouse.html...
[gecko.git] / remote / Connection.jsm
blob2cc7fa417aba39708d2d2f8bf53340f1d457707c
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 EXPORTED_SYMBOLS = ["Connection"];
9 const { XPCOMUtils } = ChromeUtils.import(
10   "resource://gre/modules/XPCOMUtils.jsm"
13 const { truncate } = ChromeUtils.import("chrome://remote/content/Format.jsm");
14 const { Log } = ChromeUtils.import("chrome://remote/content/Log.jsm");
15 const { UnknownMethodError } = ChromeUtils.import(
16   "chrome://remote/content/Error.jsm"
19 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
20 XPCOMUtils.defineLazyServiceGetter(
21   this,
22   "UUIDGen",
23   "@mozilla.org/uuid-generator;1",
24   "nsIUUIDGenerator"
27 class Connection {
28   /**
29    * @param WebSocketTransport transport
30    * @param httpd.js's Connection httpdConnection
31    */
32   constructor(transport, httpdConnection) {
33     this.id = UUIDGen.generateUUID().toString();
34     this.transport = transport;
35     this.httpdConnection = httpdConnection;
37     this.transport.hooks = this;
38     this.transport.ready();
40     this.defaultSession = null;
41     this.sessions = new Map();
42   }
44   /**
45    * Register a new Session to forward the messages to.
46    * Session without any `id` attribute will be considered to be the
47    * default one, to which messages without `sessionId` attribute are
48    * forwarded to. Only one such session can be registered.
49    *
50    * @param Session session
51    */
52   registerSession(session) {
53     if (!session.id) {
54       if (this.defaultSession) {
55         throw new Error(
56           "Default session is already set on Connection," +
57             "can't register another one."
58         );
59       }
60       this.defaultSession = session;
61     }
62     this.sessions.set(session.id, session);
63   }
65   send(body) {
66     const payload = JSON.stringify(body, null, Log.verbose ? "\t" : null);
67     log.trace(truncate`<-(connection ${this.id}) ${payload}`);
68     this.transport.send(JSON.parse(payload));
69   }
71   /**
72    * Send an error back to the client.
73    *
74    * @param Number id
75    *        Id of the packet which lead to an error.
76    * @param Error e
77    *        Error object with `message` and `stack` attributes.
78    * @param Number sessionId (Optional)
79    *        Id of the session used to send this packet.
80    *        This will be null if that was the default session.
81    */
82   onError(id, e, sessionId) {
83     const error = {
84       message: e.message,
85       data: e.stack,
86     };
87     this.send({ id, sessionId, error });
88   }
90   /**
91    * Send the result of a call to a Domain's function.
92    *
93    * @param Number id
94    *        The request id being sent by the client to call the domain's method.
95    * @param Object result
96    *        A JSON-serializable value which is the actual result.
97    * @param Number sessionId
98    *        The sessionId from which this packet is emitted.
99    *        This will be undefined for the default session.
100    */
101   onResult(id, result, sessionId) {
102     this.sendResult(id, result, sessionId);
104     // When a client attaches to a secondary target via
105     // `Target.attachToTarget`, and it executes a command via
106     // `Target.sendMessageToTarget`, we should emit an event back with the
107     // result including the `sessionId` attribute of this secondary target's
108     // session. `Target.attachToTarget` creates the secondary session and
109     // returns the session ID.
110     if (sessionId) {
111       // Temporarily disabled due to spamming of the console (bug 1598468).
112       // Event should only be sent on protocol messages (eg. attachedToTarget)
113       // this.sendEvent("Target.receivedMessageFromTarget", {
114       //   sessionId,
115       //   // receivedMessageFromTarget is expected to send a raw CDP packet
116       //   // in the `message` property and it to be already serialized to a
117       //   // string
118       //   message: JSON.stringify({
119       //     id,
120       //     result,
121       //   }),
122       // });
123     }
124   }
126   sendResult(id, result, sessionId) {
127     this.send({
128       sessionId, // this will be undefined for the default session
129       id,
130       result,
131     });
132   }
134   /**
135    * Send an event coming from a Domain to the CDP client.
136    *
137    * @param String method
138    *        The event name. This is composed by a domain name,
139    *        a dot character followed by the event name.
140    *        e.g. `Target.targetCreated`
141    * @param Object params
142    *        A JSON-serializable value which is the payload
143    *        associated with this event.
144    * @param Number sessionId
145    *        The sessionId from which this packet is emitted.
146    *        This will be undefined for the default session.
147    */
148   onEvent(method, params, sessionId) {
149     this.sendEvent(method, params, sessionId);
151     // When a client attaches to a secondary target via
152     // `Target.attachToTarget`, we should emit an event back with the
153     // result including the `sessionId` attribute of this secondary target's
154     // session. `Target.attachToTarget` creates the secondary session and
155     // returns the session ID.
156     if (sessionId) {
157       // Temporarily disabled due to spamming of the console (bug 1598468).
158       // Event should only be sent on protocol messages (eg. attachedToTarget)
159       // this.sendEvent("Target.receivedMessageFromTarget", {
160       //   sessionId,
161       //   message: JSON.stringify({
162       //     method,
163       //     params,
164       //   }),
165       // });
166     }
167   }
169   sendEvent(method, params, sessionId) {
170     this.send({
171       sessionId, // this will be undefined for the default session
172       method,
173       params,
174     });
175   }
177   // transport hooks
179   /**
180    * Receive a packet from the WebSocket layer.
181    * This packet is sent by a CDP client and is meant to execute
182    * a particular function on a given Domain.
183    *
184    * @param Object packet
185    *        JSON-serializable object sent by the client
186    */
187   async onPacket(packet) {
188     log.trace(`(connection ${this.id})-> ${JSON.stringify(packet)}`);
190     try {
191       const { id, method, params, sessionId } = packet;
193       // First check for mandatory field in the packets
194       if (typeof id == "undefined") {
195         throw new TypeError("Message missing 'id' field");
196       }
197       if (typeof method == "undefined") {
198         throw new TypeError("Message missing 'method' field");
199       }
201       // Extract the domain name and the method name out of `method` attribute
202       const { domain, command } = Connection.splitMethod(method);
204       // If a `sessionId` field is passed, retrieve the session to which we
205       // should forward this packet. Otherwise send it to the default session.
206       let session;
207       if (!sessionId) {
208         if (!this.defaultSession) {
209           throw new Error(`Connection is missing a default Session.`);
210         }
211         session = this.defaultSession;
212       } else {
213         session = this.sessions.get(sessionId);
214         if (!session) {
215           throw new Error(`Session '${sessionId}' doesn't exists.`);
216         }
217       }
219       // Bug 1600317 - Workaround to deny internal methods to be called
220       if (command.startsWith("_")) {
221         throw new UnknownMethodError(command);
222       }
224       // Finally, instruct the targeted session to execute the command
225       const result = await session.execute(id, domain, command, params);
226       this.onResult(id, result, sessionId);
227     } catch (e) {
228       this.onError(packet.id, e, packet.sessionId);
229     }
230   }
232   /**
233    * Interpret a given CDP packet for a given Session.
234    *
235    * @param String sessionId
236    *               ID of the session for which we should execute a command.
237    * @param String message
238    *               JSON payload of the CDP packet stringified to a string.
239    *               The CDP packet is about executing a Domain's function.
240    */
241   sendMessageToTarget(sessionId, message) {
242     const session = this.sessions.get(sessionId);
243     if (!session) {
244       throw new Error(`Session '${sessionId}' doesn't exists.`);
245     }
246     // `message` is received from `Target.sendMessageToTarget` where the
247     // message attribute is a stringify JSON payload which represent a CDP
248     // packet.
249     const packet = JSON.parse(message);
251     // The CDP packet sent by the client shouldn't have a sessionId attribute
252     // as it is passed as another argument of `Target.sendMessageToTarget`.
253     // Set it here in order to reuse the codepath of flatten session, where
254     // the client sends CDP packets with a `sessionId` attribute instead
255     // of going through the old and probably deprecated
256     // `Target.sendMessageToTarget` API.
257     packet.sessionId = sessionId;
258     this.onPacket(packet);
259   }
261   /**
262    * Instruct the connection to close.
263    * This will ask the transport to shutdown the WebSocket connection
264    * and destroy all active sessions.
265    */
266   close() {
267     this.transport.close();
269     // In addition to the WebSocket transport, we also have to close the Connection
270     // used internaly within httpd.js. Otherwise the server doesn't shut down correctly
271     // and keep these Connection instances alive.
272     this.httpdConnection.close();
273   }
275   /**
276    * This is called by the `transport` when the connection is closed.
277    * Cleanup all the registered sessions.
278    */
279   onClosed(status) {
280     for (const session of this.sessions.values()) {
281       session.destructor();
282     }
283     this.sessions.clear();
284   }
286   /**
287    * Splits a method, e.g. "Browser.getVersion",
288    * into domain ("Browser") and command ("getVersion") components.
289    */
290   static splitMethod(s) {
291     const ss = s.split(".");
292     if (ss.length != 2 || ss[0].length == 0 || ss[1].length == 0) {
293       throw new TypeError(`Invalid method format: "${s}"`);
294     }
295     return {
296       domain: ss[0],
297       command: ss[1],
298     };
299   }
301   toString() {
302     return `[object Connection ${this.id}]`;
303   }