diff --git a/testing/marionette/dispatcher.js b/testing/marionette/dispatcher.js new file mode 100644 index 00000000000..5e6ec8e7653 --- /dev/null +++ b/testing/marionette/dispatcher.js @@ -0,0 +1,280 @@ +/* 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/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +Cu.import("chrome://marionette/content/command.js"); +Cu.import("chrome://marionette/content/emulator.js"); +Cu.import("chrome://marionette/content/error.js"); +Cu.import("chrome://marionette/content/driver.js"); + +this.EXPORTED_SYMBOLS = ["Dispatcher"]; + +const logger = Log.repository.getLogger("Marionette"); +const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + +/** + * Manages a Marionette connection, and dispatches packets received to + * their correct destinations. + * + * @param {number} connId + * Unique identifier of the connection this dispatcher should handle. + * @param {DebuggerTransport} transport + * Debugger transport connection to the client. + * @param {function(Emulator): GeckoDriver} driverFactory + * A factory function that takes an Emulator as argument and produces + * a GeckoDriver. + */ +this.Dispatcher = function(connId, transport, driverFactory) { + this.id = connId; + this.conn = transport; + + // Marionette uses a protocol based on the debugger server, which + // requires passing back actor ID's with responses. Unlike the debugger + // server, we don't actually have multiple actors, so just use a dummy + // value of "0". + this.actorId = "0"; + + // callback for when connection is closed + this.onclose = null; + + // transport hooks are Dispatcher.prototype.onPacket + // and Dispatcher.prototype.onClosed + this.conn.hooks = this; + + this.emulator = new Emulator(msg => this.sendResponse(msg, -1)); + this.driver = driverFactory(this.emulator); + this.commandProcessor = new CommandProcessor(this.driver); +}; + +/** + * Debugger transport callback that dispatches the request. + * Request handlers defined in this.requests take presedence + * over those defined in this.driver.commands. + */ +Dispatcher.prototype.onPacket = function(packet) { + logger.debug(`${this.id} -> ${packet.toSource()}`); + + if (this.requests && this.requests[packet.name]) { + this.requests[packet.name].bind(this)(packet); + } else { + let id = this.beginNewCommand(); + let ok = this.sendOk.bind(this); + let send = this.send.bind(this); + this.commandProcessor.execute(packet, ok, send, id); + } +}; + +/** + * Debugger transport callback that cleans up + * after a connection is closed. + */ +Dispatcher.prototype.onClosed = function(status) { + this.driver.sessionTearDown(); + if (this.onclose) { + this.onclose(this); + } +}; + +// Dispatcher specific command handlers: + +Dispatcher.prototype.getMarionetteID = function() { + let id = this.beginNewCommand(); + this.sendResponse({from: "root", id: this.actorId}, id); +}; + +Dispatcher.prototype.emulatorCmdResult = function(msg) { + switch (this.driver.context) { + case Context.CONTENT: + this.driver.sendAsync("emulatorCmdResult", msg); + break; + case Context.CHROME: + let cb = this.emulator.popCallback(msg.id); + if (!cb) { + return; + } + cb.result(msg); + break; + } +}; + +/** + * Quits Firefox with the provided flags and tears down the current + * session. + */ +Dispatcher.prototype.quitApplication = function(msg) { + let id = this.beginNewCommand(); + + if (this.driver.appName != "Firefox") { + this.sendError({ + "message": "In app initiated quit only supported on Firefox", + "status": 500 + }, id); + return; + } + + let flags = Ci.nsIAppStartup.eAttemptQuit; + for (let k of msg.parameters.flags) { + flags |= Ci.nsIAppStartup[k]; + } + + this.driver.sessionTearDown(); + Services.startup.quit(flags); +}; + +// Convenience methods: + +Dispatcher.prototype.sayHello = function() { + let id = this.beginNewCommand(); + let yo = {from: "root", applicationType: "gecko", traits: []}; + this.sendResponse(yo, id); +}; + +Dispatcher.prototype.sendOk = function(cmdId) { + this.sendResponse({from: this.actorId, ok: true}, cmdId); +}; + +Dispatcher.prototype.sendError = function(err, cmdId) { + let packet = { + from: this.actorId, + status: err.status, + sessionId: this.driver.sessionId, + error: err + }; + this.sendResponse(packet, cmdId); +}; + +/** + * Marshals and sends message to either client or emulator based on the + * provided {@code cmdId}. + * + * This routine produces a Marionette protocol packet, which is different + * to a WebDriver protocol response in that it contains an extra key + * {@code from} for the debugger transport actor ID. It also replaces the + * key {@code value} with {@code error} when {@code msg.status} isn't + * {@code 0}. + * + * @param {Object} msg + * Object with the properties {@code value}, {@code status}, and + * {@code sessionId}. + * @param {UUID} cmdId + * The unique identifier for the command the message is a response to. + */ +Dispatcher.prototype.send = function(msg, cmdId) { + let packet = { + from: this.actorId, + value: msg.value, + status: msg.status, + sessionId: msg.sessionId, + }; + + if (typeof packet.value == "undefined") { + packet.value = null; + } + + // the Marionette protocol sends errors using the "error" + // key instead of, as Selenium, "value" + if (!error.isSuccess(msg.status)) { + packet.error = packet.value; + delete packet.value; + } + + this.sendResponse(packet, cmdId); +}; + +// Low-level methods: + +/** + * Delegates message to client or emulator based on the provided + * {@code cmdId}. The message is sent over the debugger transport socket. + * + * The command ID is a unique identifier assigned to the client's request + * that is used to distinguish the asynchronous responses. + * + * Whilst responses to commands are synchronous and must be sent in the + * correct order, emulator callbacks are more transparent and can be sent + * at any time. These callbacks won't change the current command state. + * + * @param {Object} payload + * The payload to send. + * @param {UUID} cmdId + * The unique identifier for this payload. {@code -1} signifies + * that it's an emulator callback. + */ +Dispatcher.prototype.sendResponse = function(payload, cmdId) { + if (emulator.isCallback(cmdId)) { + this.sendToEmulator(payload); + } else { + this.sendToClient(payload, cmdId); + this.commandId = null; + } +}; + +/** + * Send message to emulator over the debugger transport socket. + * Notably this skips out-of-sync command checks. + */ +Dispatcher.prototype.sendToEmulator = function(payload) { + this.sendRaw("emulator", payload); +}; + +/** + * Send given payload as-is to the connected client over the debugger + * transport socket. + * + * If {@code cmdId} evaluates to false, the current command state isn't + * set, or the response is out-of-sync, a warning is logged and this + * routine will return (no-op). + */ +Dispatcher.prototype.sendToClient = function(payload, cmdId) { + if (!cmdId) { + logger.warn("Got response with no command ID"); + return; + } else if (this.commandId === null) { + logger.warn(`No current command, ignoring response: ${payload.toSource}`); + return; + } else if (this.isOutOfSync(cmdId)) { + logger.warn(`Ignoring out-of-sync response with command ID: ${cmdId}`); + return; + } + this.sendRaw("client", payload); +}; + +/** + * Sends payload as-is over debugger transport socket to client, + * and logs it. + */ +Dispatcher.prototype.sendRaw = function(dest, payload) { + logger.debug(`${this.id} ${dest} <- ${payload.toSource()}`); + this.conn.send(payload); +}; + +/** + * Begins a new command by generating a unique identifier and assigning + * it to the current command state {@code Dispatcher.prototype.commandId}. + * + * @return {UUID} + * The generated unique identifier for the current command. + */ +Dispatcher.prototype.beginNewCommand = function() { + let uuid = uuidGen.generateUUID().toString(); + this.commandId = uuid; + return uuid; +}; + +Dispatcher.prototype.isOutOfSync = function(cmdId) { + return this.commandId !== cmdId; +}; + +Dispatcher.prototype.requests = { + getMarionetteID: Dispatcher.prototype.getMarionetteID, + emulatorCmdResult: Dispatcher.prototype.emulatorCmdResult, + quitApplication: Dispatcher.prototype.quitApplication +}; diff --git a/testing/marionette/jar.mn b/testing/marionette/jar.mn index 1a6119ececc..f969edea097 100644 --- a/testing/marionette/jar.mn +++ b/testing/marionette/jar.mn @@ -15,7 +15,6 @@ marionette.jar: content/EventUtils.js (EventUtils.js) content/ChromeUtils.js (ChromeUtils.js) content/error.js (error.js) - content/cmdproc.js (cmdproc.js) content/command.js (command.js) content/dispatcher.js (dispatcher.js) content/emulator.js (emulator.js)