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