Bug 1107706: Part 4: Add dispatching mechanism to encapsulate connection
authorAndreas Tolfsen <ato@mozilla.com>
Wed, 18 Mar 2015 12:27:29 +0000
changeset 266309 dbd619c23f57282f53c393780a794de71f848475
parent 266308 a76f6b26484d35189d533b8b3a6f0323304d5078
child 266310 6613699885faa8358c049d902041ae9dbe792a9e
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1107706
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1107706: Part 4: Add dispatching mechanism to encapsulate connection The dispatcher is analogous to the client socket connection, and handles receiving packets and closing connections. It also encompasses some of the functionality needed to establish the devtools and Marionette connection, that previously used to live in MarionetteServerConnection in marionette-server.js. For each connection, recognised commands will be forwarded to the command processor (command.js) unless a handler is defined in Dispatcher.requests.
testing/marionette/dispatcher.js
testing/marionette/jar.mn
new file mode 100644
--- /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
+};
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -10,17 +10,16 @@ marionette.jar:
   content/marionette-sendkeys.js    (marionette-sendkeys.js)
   content/marionette-common.js      (marionette-common.js)
   content/marionette-actions.js     (marionette-actions.js)
   content/marionette-simpletest.js  (marionette-simpletest.js)
   content/marionette-frame-manager.js  (marionette-frame-manager.js)
   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)
 #ifdef ENABLE_TESTS
   content/test.xul  (client/marionette/chrome/test.xul)
   content/test2.xul  (client/marionette/chrome/test2.xul)
   content/test_nested_iframe.xul  (client/marionette/chrome/test_nested_iframe.xul)
   content/test_anonymous_content.xul  (client/marionette/chrome/test_anonymous_content.xul)