Backed out 2 changesets (bug 1855992) for causing talos failures @ mozilla::net:...
[gecko.git] / remote / marionette / server.sys.mjs
blob36e7a9d6398205ccaf60e869dcaa34c76475e08f
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
9   Command: "chrome://remote/content/marionette/message.sys.mjs",
10   DebuggerTransport: "chrome://remote/content/marionette/transport.sys.mjs",
11   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
12   GeckoDriver: "chrome://remote/content/marionette/driver.sys.mjs",
13   Log: "chrome://remote/content/shared/Log.sys.mjs",
14   MarionettePrefs: "chrome://remote/content/marionette/prefs.sys.mjs",
15   Message: "chrome://remote/content/marionette/message.sys.mjs",
16   PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
17   Response: "chrome://remote/content/marionette/message.sys.mjs",
18 });
20 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
21   lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
23 ChromeUtils.defineLazyGetter(lazy, "ServerSocket", () => {
24   return Components.Constructor(
25     "@mozilla.org/network/server-socket;1",
26     "nsIServerSocket",
27     "initSpecialConnection"
28   );
29 });
31 const { KeepWhenOffline, LoopbackOnly } = Ci.nsIServerSocket;
33 const PROTOCOL_VERSION = 3;
35 /**
36  * Bootstraps Marionette and handles incoming client connections.
37  *
38  * Starting the Marionette server will open a TCP socket sporting the
39  * debugger transport interface on the provided `port`.  For every
40  * new connection, a {@link TCPConnection} is created.
41  */
42 export class TCPListener {
43   /**
44    * @param {number} port
45    *     Port for server to listen to.
46    */
47   constructor(port) {
48     this.port = port;
49     this.socket = null;
50     this.conns = new Set();
51     this.nextConnID = 0;
52     this.alive = false;
53   }
55   /**
56    * Function produces a {@link GeckoDriver}.
57    *
58    * Determines the application to initialise the driver with.
59    *
60    * @returns {GeckoDriver}
61    *     A driver instance.
62    */
63   driverFactory() {
64     return new lazy.GeckoDriver(this);
65   }
67   async setAcceptConnections(value) {
68     if (value) {
69       if (!this.socket) {
70         await lazy.PollPromise(
71           (resolve, reject) => {
72             try {
73               const flags = KeepWhenOffline | LoopbackOnly;
74               const backlog = 1;
75               this.socket = new lazy.ServerSocket(this.port, flags, backlog);
76               resolve();
77             } catch (e) {
78               lazy.logger.debug(
79                 `Could not bind to port ${this.port} (${e.name})`
80               );
81               reject();
82             }
83           },
84           { interval: 250, timeout: 5000 }
85         );
87         // Since PollPromise doesn't throw when timeout expires,
88         // we can end up in the situation when the socket is undefined.
89         if (!this.socket) {
90           throw new Error(`Could not bind to port ${this.port}`);
91         }
93         this.port = this.socket.port;
95         this.socket.asyncListen(this);
96         lazy.logger.info(`Listening on port ${this.port}`);
97       }
98     } else if (this.socket) {
99       // Note that closing the server socket will not close currently active
100       // connections.
101       this.socket.close();
102       this.socket = null;
103       lazy.logger.info(`Stopped listening on port ${this.port}`);
104     }
105   }
107   /**
108    * Bind this listener to {@link #port} and start accepting incoming
109    * socket connections on {@link #onSocketAccepted}.
110    *
111    * The marionette.port preference will be populated with the value
112    * of {@link #port}.
113    */
114   async start() {
115     if (this.alive) {
116       return;
117     }
119     // Start socket server and listening for connection attempts
120     await this.setAcceptConnections(true);
121     lazy.MarionettePrefs.port = this.port;
122     this.alive = true;
123   }
125   async stop() {
126     if (!this.alive) {
127       return;
128     }
130     // Shutdown server socket, and no longer listen for new connections
131     await this.setAcceptConnections(false);
132     this.alive = false;
133   }
135   onSocketAccepted(serverSocket, clientSocket) {
136     let input = clientSocket.openInputStream(0, 0, 0);
137     let output = clientSocket.openOutputStream(0, 0, 0);
138     let transport = new lazy.DebuggerTransport(input, output);
140     // Only allow a single active WebDriver session at a time
141     const hasActiveSession = [...this.conns].find(
142       conn => !!conn.driver.currentSession
143     );
144     if (hasActiveSession) {
145       lazy.logger.warn(
146         "Connection attempt denied because an active session has been found"
147       );
149       // Ideally we should stop the server to listen for new connection
150       // attempts, but the current architecture doesn't allow us to do that.
151       // As such just close the transport if no further connections are allowed.
152       transport.close();
153       return;
154     }
156     let conn = new TCPConnection(
157       this.nextConnID++,
158       transport,
159       this.driverFactory.bind(this)
160     );
161     conn.onclose = this.onConnectionClosed.bind(this);
162     this.conns.add(conn);
164     lazy.logger.debug(
165       `Accepted connection ${conn.id} ` +
166         `from ${clientSocket.host}:${clientSocket.port}`
167     );
168     conn.sayHello();
169     transport.ready();
170   }
172   onConnectionClosed(conn) {
173     lazy.logger.debug(`Closed connection ${conn.id}`);
174     this.conns.delete(conn);
175   }
179  * Marionette client connection.
181  * Dispatches packets received to their correct service destinations
182  * and sends back the service endpoint's return values.
184  * @param {number} connID
185  *     Unique identifier of the connection this dispatcher should handle.
186  * @param {DebuggerTransport} transport
187  *     Debugger transport connection to the client.
188  * @param {function(): GeckoDriver} driverFactory
189  *     Factory function that produces a {@link GeckoDriver}.
190  */
191 export class TCPConnection {
192   constructor(connID, transport, driverFactory) {
193     this.id = connID;
194     this.conn = transport;
196     // transport hooks are TCPConnection#onPacket
197     // and TCPConnection#onClosed
198     this.conn.hooks = this;
200     // callback for when connection is closed
201     this.onclose = null;
203     // last received/sent message ID
204     this.lastID = 0;
206     this.driver = driverFactory();
207   }
209   #log(msg) {
210     let dir = msg.origin == lazy.Message.Origin.Client ? "->" : "<-";
211     lazy.logger.debug(`${this.id} ${dir} ${msg.toString()}`);
212   }
214   /**
215    * Debugger transport callback that cleans up
216    * after a connection is closed.
217    */
218   onClosed() {
219     this.driver.deleteSession();
220     if (this.onclose) {
221       this.onclose(this);
222     }
223   }
225   /**
226    * Callback that receives data packets from the client.
227    *
228    * If the message is a Response, we look up the command previously
229    * issued to the client and run its callback, if any.  In case of
230    * a Command, the corresponding is executed.
231    *
232    * @param {Array.<number, number, ?, ?>} data
233    *     A four element array where the elements, in sequence, signifies
234    *     message type, message ID, method name or error, and parameters
235    *     or result.
236    */
237   onPacket(data) {
238     // unable to determine how to respond
239     if (!Array.isArray(data)) {
240       let e = new TypeError(
241         "Unable to unmarshal packet data: " + JSON.stringify(data)
242       );
243       lazy.error.report(e);
244       return;
245     }
247     // return immediately with any error trying to unmarshal message
248     let msg;
249     try {
250       msg = lazy.Message.fromPacket(data);
251       msg.origin = lazy.Message.Origin.Client;
252       this.#log(msg);
253     } catch (e) {
254       let resp = this.createResponse(data[1]);
255       resp.sendError(e);
256       return;
257     }
259     // execute new command
260     if (msg instanceof lazy.Command) {
261       (async () => {
262         await this.execute(msg);
263       })();
264     } else {
265       lazy.logger.fatal("Cannot process messages other than Command");
266     }
267   }
269   /**
270    * Executes a Marionette command and sends back a response when it
271    * has finished executing.
272    *
273    * If the command implementation sends the response itself by calling
274    * <code>resp.send()</code>, the response is guaranteed to not be
275    * sent twice.
276    *
277    * Errors thrown in commands are marshaled and sent back, and if they
278    * are not {@link WebDriverError} instances, they are additionally
279    * propagated and reported to {@link Components.utils.reportError}.
280    *
281    * @param {Command} cmd
282    *     Command to execute.
283    */
284   async execute(cmd) {
285     let resp = this.createResponse(cmd.id);
286     let sendResponse = () => resp.sendConditionally(resp => !resp.sent);
287     let sendError = resp.sendError.bind(resp);
289     await this.despatch(cmd, resp)
290       .then(sendResponse, sendError)
291       .catch(lazy.error.report);
292   }
294   /**
295    * Despatches command to appropriate Marionette service.
296    *
297    * @param {Command} cmd
298    *     Command to run.
299    * @param {Response} resp
300    *     Mutable response where the command's return value will be
301    *     assigned.
302    *
303    * @throws {Error}
304    *     A command's implementation may throw at any time.
305    */
306   async despatch(cmd, resp) {
307     const startTime = Cu.now();
309     let fn = this.driver.commands[cmd.name];
310     if (typeof fn == "undefined") {
311       throw new lazy.error.UnknownCommandError(cmd.name);
312     }
314     if (cmd.name != "WebDriver:NewSession") {
315       lazy.assert.session(this.driver.currentSession);
316     }
318     let rv = await fn.bind(this.driver)(cmd);
320     // Bug 1819029: Some older commands cannot return a response wrapped within
321     // a value field because it would break compatibility with geckodriver and
322     // Marionette client. It's unlikely that we are going to fix that.
323     //
324     // Warning: No more commands should be added to this list!
325     const commandsNoValueResponse = [
326       "Marionette:Quit",
327       "WebDriver:FindElements",
328       "WebDriver:FindElementsFromShadowRoot",
329       "WebDriver:CloseChromeWindow",
330       "WebDriver:CloseWindow",
331       "WebDriver:FullscreenWindow",
332       "WebDriver:GetCookies",
333       "WebDriver:GetElementRect",
334       "WebDriver:GetTimeouts",
335       "WebDriver:GetWindowHandles",
336       "WebDriver:GetWindowRect",
337       "WebDriver:MaximizeWindow",
338       "WebDriver:MinimizeWindow",
339       "WebDriver:NewSession",
340       "WebDriver:NewWindow",
341       "WebDriver:SetWindowRect",
342     ];
344     if (rv != null) {
345       // By default the Response' constructor sets the body to `{ value: null }`.
346       // As such we only want to override the value if it's neither `null` nor
347       // `undefined`.
348       if (commandsNoValueResponse.includes(cmd.name)) {
349         resp.body = rv;
350       } else {
351         resp.body.value = rv;
352       }
353     }
355     if (Services.profiler?.IsActive()) {
356       ChromeUtils.addProfilerMarker(
357         "Marionette: Command",
358         { startTime, category: "Remote-Protocol" },
359         `${cmd.name} (${cmd.id})`
360       );
361     }
362   }
364   /**
365    * Fail-safe creation of a new instance of {@link Response}.
366    *
367    * @param {number} msgID
368    *     Message ID to respond to.  If it is not a number, -1 is used.
369    *
370    * @returns {Response}
371    *     Response to the message with `msgID`.
372    */
373   createResponse(msgID) {
374     if (typeof msgID != "number") {
375       msgID = -1;
376     }
377     return new lazy.Response(msgID, this.send.bind(this));
378   }
380   sendError(err, cmdID) {
381     let resp = new lazy.Response(cmdID, this.send.bind(this));
382     resp.sendError(err);
383   }
385   /**
386    * When a client connects we send across a JSON Object defining the
387    * protocol level.
388    *
389    * This is the only message sent by Marionette that does not follow
390    * the regular message format.
391    */
392   sayHello() {
393     let whatHo = {
394       applicationType: "gecko",
395       marionetteProtocol: PROTOCOL_VERSION,
396     };
397     this.sendRaw(whatHo);
398   }
400   /**
401    * Delegates message to client based on the provided  {@code cmdID}.
402    * The message is sent over the debugger transport socket.
403    *
404    * The command ID is a unique identifier assigned to the client's request
405    * that is used to distinguish the asynchronous responses.
406    *
407    * Whilst responses to commands are synchronous and must be sent in the
408    * correct order.
409    *
410    * @param {Message} msg
411    *     The command or response to send.
412    */
413   send(msg) {
414     msg.origin = lazy.Message.Origin.Server;
415     if (msg instanceof lazy.Response) {
416       this.sendToClient(msg);
417     } else {
418       lazy.logger.fatal("Cannot send messages other than Response");
419     }
420   }
422   // Low-level methods:
424   /**
425    * Send given response to the client over the debugger transport socket.
426    *
427    * @param {Response} resp
428    *     The response to send back to the client.
429    */
430   sendToClient(resp) {
431     this.sendMessage(resp);
432   }
434   /**
435    * Marshal message to the Marionette message format and send it.
436    *
437    * @param {Message} msg
438    *     The message to send.
439    */
440   sendMessage(msg) {
441     this.#log(msg);
442     let payload = msg.toPacket();
443     this.sendRaw(payload);
444   }
446   /**
447    * Send the given payload over the debugger transport socket to the
448    * connected client.
449    *
450    * @param {Object<string, ?>} payload
451    *     The payload to ship.
452    */
453   sendRaw(payload) {
454     this.conn.send(payload);
455   }
457   toString() {
458     return `[object TCPConnection ${this.id}]`;
459   }