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/. */
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",
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",
27 "initSpecialConnection"
31 const { KeepWhenOffline, LoopbackOnly } = Ci.nsIServerSocket;
33 const PROTOCOL_VERSION = 3;
36 * Bootstraps Marionette and handles incoming client connections.
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.
42 export class TCPListener {
44 * @param {number} port
45 * Port for server to listen to.
50 this.conns = new Set();
56 * Function produces a {@link GeckoDriver}.
58 * Determines the application to initialise the driver with.
60 * @returns {GeckoDriver}
64 return new lazy.GeckoDriver(this);
67 async setAcceptConnections(value) {
70 await lazy.PollPromise(
71 (resolve, reject) => {
73 const flags = KeepWhenOffline | LoopbackOnly;
75 this.socket = new lazy.ServerSocket(this.port, flags, backlog);
79 `Could not bind to port ${this.port} (${e.name})`
84 { interval: 250, timeout: 5000 }
87 // Since PollPromise doesn't throw when timeout expires,
88 // we can end up in the situation when the socket is undefined.
90 throw new Error(`Could not bind to port ${this.port}`);
93 this.port = this.socket.port;
95 this.socket.asyncListen(this);
96 lazy.logger.info(`Listening on port ${this.port}`);
98 } else if (this.socket) {
99 // Note that closing the server socket will not close currently active
103 lazy.logger.info(`Stopped listening on port ${this.port}`);
108 * Bind this listener to {@link #port} and start accepting incoming
109 * socket connections on {@link #onSocketAccepted}.
111 * The marionette.port preference will be populated with the value
119 // Start socket server and listening for connection attempts
120 await this.setAcceptConnections(true);
121 lazy.MarionettePrefs.port = this.port;
130 // Shutdown server socket, and no longer listen for new connections
131 await this.setAcceptConnections(false);
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
144 if (hasActiveSession) {
146 "Connection attempt denied because an active session has been found"
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.
156 let conn = new TCPConnection(
159 this.driverFactory.bind(this)
161 conn.onclose = this.onConnectionClosed.bind(this);
162 this.conns.add(conn);
165 `Accepted connection ${conn.id} ` +
166 `from ${clientSocket.host}:${clientSocket.port}`
172 onConnectionClosed(conn) {
173 lazy.logger.debug(`Closed connection ${conn.id}`);
174 this.conns.delete(conn);
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}.
191 export class TCPConnection {
192 constructor(connID, transport, driverFactory) {
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
203 // last received/sent message ID
206 this.driver = driverFactory();
210 let dir = msg.origin == lazy.Message.Origin.Client ? "->" : "<-";
211 lazy.logger.debug(`${this.id} ${dir} ${msg.toString()}`);
215 * Debugger transport callback that cleans up
216 * after a connection is closed.
219 this.driver.deleteSession();
226 * Callback that receives data packets from the client.
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.
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
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)
243 lazy.error.report(e);
247 // return immediately with any error trying to unmarshal message
250 msg = lazy.Message.fromPacket(data);
251 msg.origin = lazy.Message.Origin.Client;
254 let resp = this.createResponse(data[1]);
259 // execute new command
260 if (msg instanceof lazy.Command) {
262 await this.execute(msg);
265 lazy.logger.fatal("Cannot process messages other than Command");
270 * Executes a Marionette command and sends back a response when it
271 * has finished executing.
273 * If the command implementation sends the response itself by calling
274 * <code>resp.send()</code>, the response is guaranteed to not be
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}.
281 * @param {Command} cmd
282 * Command to execute.
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);
295 * Despatches command to appropriate Marionette service.
297 * @param {Command} cmd
299 * @param {Response} resp
300 * Mutable response where the command's return value will be
304 * A command's implementation may throw at any time.
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);
314 if (cmd.name != "WebDriver:NewSession") {
315 lazy.assert.session(this.driver.currentSession);
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.
324 // Warning: No more commands should be added to this list!
325 const commandsNoValueResponse = [
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",
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
348 if (commandsNoValueResponse.includes(cmd.name)) {
351 resp.body.value = rv;
355 if (Services.profiler?.IsActive()) {
356 ChromeUtils.addProfilerMarker(
357 "Marionette: Command",
358 { startTime, category: "Remote-Protocol" },
359 `${cmd.name} (${cmd.id})`
365 * Fail-safe creation of a new instance of {@link Response}.
367 * @param {number} msgID
368 * Message ID to respond to. If it is not a number, -1 is used.
370 * @returns {Response}
371 * Response to the message with `msgID`.
373 createResponse(msgID) {
374 if (typeof msgID != "number") {
377 return new lazy.Response(msgID, this.send.bind(this));
380 sendError(err, cmdID) {
381 let resp = new lazy.Response(cmdID, this.send.bind(this));
386 * When a client connects we send across a JSON Object defining the
389 * This is the only message sent by Marionette that does not follow
390 * the regular message format.
394 applicationType: "gecko",
395 marionetteProtocol: PROTOCOL_VERSION,
397 this.sendRaw(whatHo);
401 * Delegates message to client based on the provided {@code cmdID}.
402 * The message is sent over the debugger transport socket.
404 * The command ID is a unique identifier assigned to the client's request
405 * that is used to distinguish the asynchronous responses.
407 * Whilst responses to commands are synchronous and must be sent in the
410 * @param {Message} msg
411 * The command or response to send.
414 msg.origin = lazy.Message.Origin.Server;
415 if (msg instanceof lazy.Response) {
416 this.sendToClient(msg);
418 lazy.logger.fatal("Cannot send messages other than Response");
422 // Low-level methods:
425 * Send given response to the client over the debugger transport socket.
427 * @param {Response} resp
428 * The response to send back to the client.
431 this.sendMessage(resp);
435 * Marshal message to the Marionette message format and send it.
437 * @param {Message} msg
438 * The message to send.
442 let payload = msg.toPacket();
443 this.sendRaw(payload);
447 * Send the given payload over the debugger transport socket to the
450 * @param {Object<string, ?>} payload
451 * The payload to ship.
454 this.conn.send(payload);
458 return `[object TCPConnection ${this.id}]`;