Bug 1107706: Part 3: Add a command processor to Marionette
☠☠ backed out by 4c2c50594967 ☠ ☠
authorAndreas Tolfsen <ato@mozilla.com>
Tue, 17 Mar 2015 14:49:27 +0000
changeset 265675 502320aec21f189c534f1cf3739dee38088996c1
parent 265674 60b58aed6d27d4855c60ca81a68406c629ea4f51
child 265676 593a7917f9170a833d2418d8742e9a74c5afe758
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 3: Add a command processor to Marionette The command processor receives messages, processes them, and wraps the execution of the command implementations. This allows commands to throw errors without worrying about the side effects. This patch also introduces a Response object which correspondingly wraps the replies to the client. This shifts the responsibility of managing the correctness of the packets from the commands to this.
testing/marionette/command.js
testing/marionette/jar.mn
new file mode 100644
--- /dev/null
+++ b/testing/marionette/command.js
@@ -0,0 +1,168 @@
+/* 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 {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+Cu.import("chrome://marionette/content/error.js");
+
+this.EXPORTED_SYMBOLS = ["CommandProcessor", "Response"];
+const logger = Log.repository.getLogger("Marionette");
+
+/**
+ * Represents the response returned from the remote end after execution
+ * of its corresponding command.
+ *
+ * The Response is a mutable object passed to each command for
+ * modification through the available setters.  The response is sent
+ * implicitly by CommandProcessor when a command is finished executing,
+ * and any modifications made subsequent to this will have no effect.
+ *
+ * @param {number} cmdId
+ *     UUID tied to the corresponding command request this is
+ *     a response for.
+ * @param {function(number)} okHandler
+ *     Callback function called on successful responses with no body.
+ * @param {function(Object, number)} respHandler
+ *     Callback function called on successful responses with body.
+ * @param {Object=} msg
+ *     A message to populate the response, containing the properties
+ *     "sessionId", "status", and "value".
+ * @param {function(Map)=} sanitizer
+ *     Run before sending message.
+ */
+this.Response = function(cmdId, okHandler, respHandler, msg, sanitizer) {
+  const removeEmpty = function(map) {
+    let rv = {};
+    for (let [key, value] of map) {
+      if (typeof value == "undefined") {
+        value = null;
+      }
+      rv[key] = value;
+    }
+    return rv;
+  };
+
+  this.id = cmdId;
+  this.ok = true;
+  this.okHandler = okHandler;
+  this.respHandler = respHandler;
+  this.sanitizer = sanitizer || removeEmpty;
+
+  this.data = new Map([
+    ["sessionId", msg.sessionId ? msg.sessionId : null],
+    ["status", msg.status ? msg.status : 0 /* success */],
+    ["value", msg.value ? msg.value : undefined],
+  ]);
+};
+
+Response.prototype = {
+  get name() { return this.data.get("name"); },
+  set name(n) { this.data.set("name", n); },
+  get sessionId() { return this.data.get("sessionId"); },
+  set sessionId(id) { this.data.set("sessionId", id); },
+  get status() { return this.data.get("status"); },
+  set status(ns) { this.data.set("status", ns); },
+  get value() { return this.data.get("value"); },
+  set value(val) {
+    this.data.set("value", val);
+    this.ok = false;
+  }
+};
+
+Response.prototype.send = function() {
+  if (this.sent) {
+    logger.warn("Skipped sending response to command ID " +
+      this.id + " because response has already been sent");
+    return;
+  }
+
+  if (this.ok) {
+    this.okHandler(this.id);
+  } else {
+    let rawData = this.sanitizer(this.data);
+    this.respHandler(rawData, this.id);
+  }
+};
+
+/**
+ * @param {(Error|Object)} err
+ *     The error to send, either an instance of the Error prototype,
+ *     or an object with the properties "message", "code", and "stack".
+ */
+Response.prototype.sendError = function(err) {
+  this.status = "code" in err ? err.code : new UnknownError().code;
+  this.value = error.toJSON(err);
+  this.send();
+
+  // propagate errors that are implementation problems
+  if (!error.isWebDriverError(err)) {
+    throw err;
+  }
+};
+
+/**
+ * The command processor receives messages on execute(payload, …)
+ * from the dispatcher, processes them, and wraps the functions that
+ * it executes from the WebDriver implementation, driver.
+ *
+ * @param {GeckoDriver} driver
+ *     Reference to the driver implementation.
+ */
+this.CommandProcessor = function(driver) {
+  this.driver = driver;
+};
+
+/**
+ * Executes a WebDriver command based on the received payload,
+ * which is expected to be an object with a "parameters" property
+ * that is a simple key/value collection of arguments.
+ *
+ * The respHandler function will be called with the JSON object to
+ * send back to the client.
+ *
+ * The cmdId is the UUID tied to this request that prevents
+ * the dispatcher from sending responses in the wrong order.
+ *
+ * @param {Object} payload
+ *     Message as received from client.
+ * @param {function(number)} okHandler
+ *     Callback function called on successful responses with no body.
+ * @param {function(Object, number)} respHandler
+ *     Callback function called on successful responses with body.
+ * @param {number} cmdId
+ *     The unique identifier for the command to execute.
+ */
+CommandProcessor.prototype.execute = function(payload, okHandler, respHandler, cmdId) {
+  let cmd = payload;
+  let resp = new Response(
+    cmdId, okHandler, respHandler, {sessionId: this.driver.sessionId});
+  let sendResponse = resp.send.bind(resp);
+  let sendError = resp.sendError.bind(resp);
+
+  // Ideally handlers shouldn't have to care about the command ID,
+  // but some methods (newSession, executeScript, et al.) have not
+  // yet been converted to use the new form of request dispatching.
+  cmd.id = cmdId;
+
+  // For as long as the old form of request dispatching is in use,
+  // we need to tell ListenerProxy what the current command ID is
+  // so that individual commands in driver.js can define it explicitly.
+  this.driver.listener.curCmdId = cmd.id;
+
+  let req = Task.spawn(function*() {
+    let fn = this.driver.commands[cmd.name];
+    if (typeof fn == "undefined") {
+      throw new UnknownCommandError(cmd.name);
+    }
+
+    yield fn.bind(this.driver)(cmd, resp);
+  }.bind(this));
+
+  req.then(sendResponse, sendError).catch(error.report);
+};
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -10,16 +10,20 @@ 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)
 #endif
 
 % content specialpowers %content/