Bug 1211489: Provide message sequencing in Marionette
authorAndreas Tolfsen <ato@mozilla.com>
Sat, 26 Sep 2015 17:12:01 +0100
changeset 309406 01480efcc2813129fe123ffba854ec0048353af8
parent 309405 349b2d9ac5aa1a4558a92725c552b5a5b61ad73f
child 309407 633bfeaa021b75241fba50ab3544c46b9a84c00c
push id5513
push userraliiev@mozilla.com
push dateMon, 25 Jan 2016 13:55:34 +0000
treeherdermozilla-beta@5ee97dd05b5c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1211489, 1207125
milestone45.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 1211489: Provide message sequencing in Marionette Message sequencing allows Marionette to provide an asynchronous, parallel pipelining user-facing interface, limit chances of payload race conditions, and remove stylistic inconsistencies in how commands and responses are dispatched internally. Clients that deliver a blocking WebDriver interface are still be expected to not send further command requests before the response from the last command has come back, but if they still happen to do so because of programming error or otherwise, no harm will be done. This will guard against bugs such as bug 1207125. This patch formalises the command and response concepts, and applies these concepts to emulator callbacks. Through the new message format, Marionette is able to provide two-way parallel communication. In other words, the server will be able to instruct the client to perform a command in a non ad-hoc way. runEmulatorCmd and runEmulatorShell are both turned into command instructions originating from the server. This resolves a lot of technical debt in the server code because they are no longer special-cased to circumvent the dispatching technique used for all other commands; commands may originate from either the client or the server providing parallel pipelining enforced through message sequencing: client server | | msgid=1 |----------->| | command | | | msgid=2 |<-----------| | command | | | msgid=2 |----------->| | response | | | msgid=1 |<-----------| | response | | | The protocol now consists of a "Command" message and the corresponding "Response" message. A "Response" message must always be sent in reply to a "Command" message. This bumps the Marionette protocol level to 3. r=dburns r=jgriffin
testing/marionette/command.js
testing/marionette/dispatcher.js
testing/marionette/driver.js
testing/marionette/emulator.js
testing/marionette/error.js
testing/marionette/frame-manager.js
testing/marionette/jar.mn
testing/marionette/listener.js
testing/marionette/message.js
testing/marionette/server.js
deleted file mode 100644
--- a/testing/marionette/command.js
+++ /dev/null
@@ -1,163 +0,0 @@
-/* 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";
-
-var {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");
-
-const validator = {
-  exclusionary: {
-    "capabilities": ["error", "value"],
-    "error": ["value", "sessionId", "capabilities"],
-    "sessionId": ["error", "value"],
-    "value": ["error", "sessionId", "capabilities"],
-  },
-
-  set: function(obj, prop, val) {
-    let tests = this.exclusionary[prop];
-    if (tests) {
-      for (let t of tests) {
-        if (obj.hasOwnProperty(t)) {
-          throw new TypeError(`${t} set, cannot set ${prop}`);
-        }
-      }
-    }
-
-    obj[prop] = val;
-    return true;
-  },
-};
-
-/**
- * The response body is exposed as an argument to commands.
- * Commands can set fields on the body through defining properties.
- *
- * Setting properties invokes a validator that performs tests for
- * mutually exclusionary fields on the input against the existing data
- * in the body.
- *
- * For example setting the {@code error} property on the body when
- * {@code value}, {@code sessionId}, or {@code capabilities} have been
- * set previously will cause an error.
- */
-this.ResponseBody = () => new Proxy({}, validator);
-
-/**
- * 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.  To send data in a response,
- * you modify the body property on the response.  The body property can
- * also be replaced completely.
- *
- * The response is sent implicitly by CommandProcessor when a command
- * has finished executing, and any modifications made subsequent to that
- * will have no effect.
- *
- * @param {number} cmdId
- *     UUID tied to the corresponding command request this is
- *     a response for.
- * @param {function(Object, number)} respHandler
- *     Callback function called on responses.
- */
-this.Response = function(cmdId, respHandler) {
-  this.id = cmdId;
-  this.respHandler = respHandler;
-  this.sent = false;
-  this.body = ResponseBody();
-};
-
-Response.prototype.send = function() {
-  if (this.sent) {
-    throw new RangeError("Response has already been sent: " + this.toString());
-  }
-  this.respHandler(this.body, this.id);
-  this.sent = true;
-};
-
-Response.prototype.sendError = function(err) {
-  let wd = error.isWebDriverError(err);
-  let we = wd ? err : new WebDriverError(err.message);
-
-  this.body.error = we.status;
-  this.body.message = we.message || null;
-  this.body.stacktrace = we.stack || null;
-
-  this.send();
-
-  // propagate errors that are implementation problems
-  if (!wd) {
-    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(Object, number)} respHandler
- *     Callback function called on responses.
- * @param {number} cmdId
- *     The unique identifier for the command to execute.
- */
-CommandProcessor.prototype.execute = function(payload, respHandler, cmdId) {
-  let cmd = payload;
-  let resp = new Response(cmdId, respHandler);
-  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;
-
-  let req = Task.spawn(function*() {
-    let fn = this.driver.commands[cmd.name];
-    if (typeof fn == "undefined") {
-      throw new UnknownCommandError(cmd.name);
-    }
-
-    let rv = yield fn.bind(this.driver)(cmd, resp);
-
-    if (typeof rv != "undefined") {
-      if (typeof rv != "object") {
-        resp.body = {value: rv};
-      } else {
-        resp.body = rv;
-      }
-    }
-  }.bind(this));
-
-  req.then(sendResponse, sendError).catch(error.report);
-};
--- a/testing/marionette/dispatcher.js
+++ b/testing/marionette/dispatcher.js
@@ -1,238 +1,244 @@
 /* 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";
 
-var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const {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/driver.js");
 Cu.import("chrome://marionette/content/emulator.js");
 Cu.import("chrome://marionette/content/error.js");
-Cu.import("chrome://marionette/content/driver.js");
+Cu.import("chrome://marionette/content/message.js");
 
 this.EXPORTED_SYMBOLS = ["Dispatcher"];
 
-const PROTOCOL_VERSION = 2;
+const PROTOCOL_VERSION = 3;
 
 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.
- * @param {function()} stopSignal
- *     Signal to stop the Marionette server.
  */
-this.Dispatcher = function(connId, transport, driverFactory, stopSignal) {
-  this.id = connId;
+this.Dispatcher = function(connId, transport, driverFactory) {
+  this.connId = connId;
   this.conn = transport;
 
+  // transport hooks are Dispatcher#onPacket
+  // and Dispatcher#onClosed
+  this.conn.hooks = this;
+
   // 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.send(msg, -1));
-  this.driver = driverFactory(this.emulator);
-  this.commandProcessor = new CommandProcessor(this.driver);
-
-  this.stopSignal_ = stopSignal;
-};
+  // last received/sent message ID
+  this.lastId = 0;
 
-/**
- * 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) {
-  if (logger.level <= Log.Level.Debug) {
-    logger.debug(this.id + " -> " + JSON.stringify(packet));
-  }
+  this.emulator = new Emulator(this.sendEmulator.bind(this));
+  this.driver = driverFactory(this.emulator);
 
-  if (this.requests && this.requests[packet.name]) {
-    this.requests[packet.name].bind(this)(packet);
-  } else {
-    let id = this.beginNewCommand();
-    let send = this.send.bind(this);
-    this.commandProcessor.execute(packet, send, id);
-  }
+  // lookup of commands sent by server to client by message ID
+  this.commands_ = new Map();
 };
 
 /**
  * Debugger transport callback that cleans up
  * after a connection is closed.
  */
-Dispatcher.prototype.onClosed = function(status) {
+Dispatcher.prototype.onClosed = function(reason) {
   this.driver.sessionTearDown();
   if (this.onclose) {
     this.onclose(this);
   }
 };
 
-// Dispatcher specific command handlers:
+/**
+ * Callback that receives data packets from the client.
+ *
+ * If the message is a Response, we look up the command previously issued
+ * to the client and run its callback, if any.  In case of a Command,
+ * the corresponding is executed.
+ *
+ * @param {Array.<number, number, ?, ?>} data
+ *     A four element array where the elements, in sequence, signifies
+ *     message type, message ID, method name or error, and parameters
+ *     or result.
+ */
+Dispatcher.prototype.onPacket = function(data) {
+  let msg = Message.fromMsg(data);
+  msg.origin = MessageOrigin.Client;
+  this.log_(msg);
 
-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;
+  if (msg instanceof Response) {
+    let cmd = this.commands_.get(msg.id);
+    this.commands_.delete(msg.id);
+    cmd.onresponse(msg);
+  } else if (msg instanceof Command) {
+    this.lastId = msg.id;
+    this.execute(msg);
   }
 };
 
 /**
- * Quits Firefox with the provided flags and tears down the current
- * session.
+ * Executes a WebDriver command and sends back a response when it has
+ * finished executing.
+ *
+ * Commands implemented in GeckoDriver and registered in its
+ * {@code GeckoDriver.commands} attribute.  The return values from
+ * commands are expected to be Promises.  If the resolved value of said
+ * promise is not an object, the response body will be wrapped in an object
+ * under a "value" field.
+ *
+ * If the command implementation sends the response itself by calling
+ * {@code resp.send()}, the response is guaranteed to not be sent twice.
+ *
+ * Errors thrown in commands are marshaled and sent back, and if they
+ * are not WebDriverError instances, they are additionally propagated and
+ * reported to {@code Components.utils.reportError}.
+ *
+ * @param {Command} cmd
+ *     The requested command to execute.
  */
-Dispatcher.prototype.quitApplication = function(msg) {
-  let id = this.beginNewCommand();
-
-  if (this.driver.appName != "Firefox") {
-    this.sendError(new WebDriverError("In app initiated quit only supported in Firefox"));
-    return;
-  }
-
-  let flags = Ci.nsIAppStartup.eAttemptQuit;
-  for (let k of msg.parameters.flags) {
-    flags |= Ci.nsIAppStartup[k];
-  }
+Dispatcher.prototype.execute = function(cmd) {
+  let resp = new Response(cmd.id, this.send.bind(this));
+  let sendResponse = () => resp.sendConditionally(resp => !resp.sent);
+  let sendError = resp.sendError.bind(resp);
 
-  this.stopSignal_();
-  this.sendOk(id);
+  let req = Task.spawn(function*() {
+    let fn = this.driver.commands[cmd.name];
+    if (typeof fn == "undefined") {
+      throw new UnknownCommandError(cmd.name);
+    }
 
-  this.driver.sessionTearDown();
-  Services.startup.quit(flags);
-};
-
-// Convenience methods:
+    let rv = yield fn.bind(this.driver)(cmd, resp);
 
-Dispatcher.prototype.sayHello = function() {
-  let id = this.beginNewCommand();
-  let whatHo = {
-    applicationType: "gecko",
-    marionetteProtocol: PROTOCOL_VERSION,
-  };
-  this.send(whatHo, id);
-};
+    if (typeof rv != "undefined") {
+      if (typeof rv != "object") {
+        resp.body = {value: rv};
+      } else {
+        resp.body = rv;
+      }
+    }
+  }.bind(this));
 
-Dispatcher.prototype.sendOk = function(cmdId) {
-  this.send({}, cmdId);
+  req.then(sendResponse, sendError).catch(error.report);
 };
 
 Dispatcher.prototype.sendError = function(err, cmdId) {
   let resp = new Response(cmdId, this.send.bind(this));
   resp.sendError(err);
 };
 
+// Convenience methods:
+
+/**
+ * When a client connects we send across a JSON Object defining the
+ * protocol level.
+ *
+ * This is the only message sent by Marionette that does not follow
+ * the regular message format.
+ */
+Dispatcher.prototype.sayHello = function() {
+  let whatHo = {
+    applicationType: "gecko",
+    marionetteProtocol: PROTOCOL_VERSION,
+  };
+  this.sendRaw(whatHo);
+};
+
+Dispatcher.prototype.sendEmulator = function(name, params, resCb, errCb) {
+  let cmd = new Command(++this.lastId, name, params);
+  cmd.onresult = resCb;
+  cmd.onerror = errCb;
+  this.send(cmd);
+};
+
 /**
  * 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.
+ * @param {Command,Response} msg
+ *     The command or response to send.
  */
-Dispatcher.prototype.send = function(payload, cmdId) {
-  if (emulator.isCallback(cmdId)) {
-    this.sendToEmulator(payload);
-  } else {
-    this.sendToClient(payload, cmdId);
-    this.commandId = null;
+Dispatcher.prototype.send = function(msg) {
+  msg.origin = MessageOrigin.Server;
+  if (msg instanceof Command) {
+    this.commands_.set(msg.id, msg);
+    this.sendToEmulator(msg);
+  } else if (msg instanceof Response) {
+    this.sendToClient(msg);
   }
 };
 
 // Low-level methods:
 
 /**
- * Send message to emulator over the debugger transport socket.
- * Notably this skips out-of-sync command checks.
+ * Send command to emulator over the debugger transport socket.
+ *
+ * @param {Command} cmd
+ *     The command to issue to the emulator.
  */
-Dispatcher.prototype.sendToEmulator = function(payload) {
-  this.sendRaw("emulator", payload);
+Dispatcher.prototype.sendToEmulator = function(cmd) {
+  this.sendMessage(cmd);
 };
 
 /**
- * Send given payload as-is to the connected client over the debugger
- * transport socket.
+ * Send given response to the 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).
+ * @param {Response} resp
+ *     The response to send back to the client.
  */
-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;
-  }
+Dispatcher.prototype.sendToClient = function(resp) {
   this.driver.responseCompleted();
-  this.sendRaw("client", payload);
+  this.sendMessage(resp);
 };
 
 /**
- * Sends payload as-is over debugger transport socket to client,
- * and logs it.
+ * Marshal message to the Marionette message format and send it.
+ *
+ * @param {Command,Response} msg
+ *     The message to send.
  */
-Dispatcher.prototype.sendRaw = function(dest, payload) {
-  if (logger.level <= Log.Level.Debug) {
-    logger.debug(this.id + " " + dest + " <- " + JSON.stringify(payload));
-  }
-  this.conn.send(payload);
+Dispatcher.prototype.sendMessage = function(msg) {
+  this.log_(msg);
+  let payload = msg.toMsg();
+  this.sendRaw(payload);
 };
 
 /**
- * Begins a new command by generating a unique identifier and assigning
- * it to the current command state {@code Dispatcher.prototype.commandId}.
+ * Send the given payload over the debugger transport socket to the
+ * connected client.
  *
- * @return {UUID}
- *     The generated unique identifier for the current command.
+ * @param {Object} payload
+ *     The payload to ship.
  */
-Dispatcher.prototype.beginNewCommand = function() {
-  let uuid = uuidGen.generateUUID().toString();
-  this.commandId = uuid;
-  return uuid;
+Dispatcher.prototype.sendRaw = function(payload) {
+  this.conn.send(payload);
 };
 
-Dispatcher.prototype.isOutOfSync = function(cmdId) {
-  return this.commandId !== cmdId;
+Dispatcher.prototype.log_ = function(msg) {
+  if (logger.level > Log.Level.Debug) {
+    return;
+  }
+  let a = (msg.origin == MessageOrigin.Client ? " -> " : " <- ");
+  logger.debug(this.connId + a + msg);
 };
-
-Dispatcher.prototype.requests = {
-  emulatorCmdResult: Dispatcher.prototype.emulatorCmdResult,
-  quitApplication: Dispatcher.prototype.quitApplication
-};
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -19,17 +19,16 @@ Cu.import("resource://gre/modules/XPCOMU
 var {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 this.DevToolsUtils = devtools.require("devtools/shared/DevToolsUtils");
 
 XPCOMUtils.defineLazyServiceGetter(
     this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2");
 
 Cu.import("chrome://marionette/content/actions.js");
 Cu.import("chrome://marionette/content/elements.js");
-Cu.import("chrome://marionette/content/emulator.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/modal.js");
 Cu.import("chrome://marionette/content/proxy.js");
 Cu.import("chrome://marionette/content/simpletest.js");
 
 loader.loadSubScript("chrome://marionette/content/common.js");
 
 // preserve this import order:
@@ -90,22 +89,27 @@ this.Context.fromString = function(s) {
  * Throughout this prototype, functions with the argument {@code cmd}'s
  * documentation refers to the contents of the {@code cmd.parameters}
  * object.
  *
  * @param {string} appName
  *     Description of the product, for example "B2G" or "Firefox".
  * @param {string} device
  *     Device this driver should assume.
+ * @param {function()} stopSignal
+ *     Signal to stop the Marionette server.
  * @param {Emulator=} emulator
  *     Reference to the emulator connection, if running on an emulator.
  */
-this.GeckoDriver = function(appName, device, emulator) {
+this.GeckoDriver = function(appName, device, stopSignal, emulator) {
   this.appName = appName;
+  this.stopSignal_ = stopSignal;
   this.emulator = emulator;
+  // TODO(ato): hack
+  this.emulator.sendToListener = this.sendAsync.bind(this);
 
   this.sessionId = null;
   // holds list of BrowserObjs
   this.browsers = {};
   // points to current browser
   this.curBrowser = null;
   this.context = Context.CONTENT;
   this.scriptTimeout = null;
@@ -159,16 +163,17 @@ this.GeckoDriver = function(appName, dev
     "appBuildId" : Services.appinfo.appBuildID,
     "device": device,
     "version": Services.appinfo.version,
   };
 
   this.mm = globalMessageManager;
   this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this));
 
+  // always keep weak reference to current dialogue
   this.dialog = null;
   let handleDialog = (subject, topic) => {
     let winr;
     if (topic == modal.COMMON_DIALOG_LOADED) {
       winr = Cu.getWeakReference(subject);
     }
     this.dialog = new modal.Dialog(() => this.curBrowser, winr);
   };
@@ -1078,21 +1083,16 @@ GeckoDriver.prototype.executeWithCallbac
   }
 
   let win = this.getCurrentWindow();
   let origOnError = win.onerror;
   that.timeout = scriptTimeout;
 
   let res = yield new Promise(function(resolve, reject) {
     let chromeAsyncReturnFunc = function(val) {
-      if (that.emulator.cbs.length > 0) {
-        that.emulator.cbs = [];
-        throw new WebDriverError("Emulator callback still pending when finish() called");
-      }
-
       if (cmd.id == that.sandboxes[sandboxName].command_id) {
         if (that.timer !== null) {
           that.timer.cancel();
           that.timer = null;
         }
 
         win.onerror = origOnError;
 
@@ -1128,30 +1128,21 @@ GeckoDriver.prototype.executeWithCallbac
           this.testName);
       this.createExecuteSandbox(win, marionette, sandboxName);
     }
     if (!this.sandboxes[sandboxName]) {
       return;
     }
 
     this.sandboxes[sandboxName].command_id = cmd.id;
-    this.sandboxes[sandboxName].runEmulatorCmd = (cmd, cb) => {
-      let ecb = new EmulatorCallback();
-      ecb.onresult = cb;
-      ecb.onerror = chromeAsyncError;
-      this.emulator.pushCallback(ecb);
-      this.emulator.send({emulator_cmd: cmd, id: ecb.id});
-    };
-    this.sandboxes[sandboxName].runEmulatorShell = (args, cb) => {
-      let ecb = new EmulatorCallback();
-      ecb.onresult = cb;
-      ecb.onerror = chromeAsyncError;
-      this.emulator.pushCallback(ecb);
-      this.emulator.send({emulator_shell: args, id: ecb.id});
-    };
+    this.sandboxes[sandboxName].runEmulatorCmd =
+        (cmd, cb) => this.emulator.command(cmd, cb, chromeAsyncError);
+    this.sandboxes[sandboxName].runEmulatorShell =
+        (args, cb) => this.emulator.shell(args, cb, chromeAsyncError);
+
     this.applyArgumentsToSandbox(win, this.sandboxes[sandboxName], args);
 
     // NB: win.onerror is not hooked by default due to the inability to
     // differentiate content exceptions from chrome exceptions. See bug
     // 1128760 for more details. A debug_script flag can be set to
     // reenable onerror hooking to help debug test scripts.
     if (cmd.parameters.debug_script) {
       win.onerror = function(msg, url, line) {
@@ -2796,16 +2787,37 @@ GeckoDriver.prototype.sendKeysToDialog =
       cmd.parameters.value,
       () => {},
       e => { throw e; },
       this.command_id,
       true /* ignore visibility check */);
 };
 
 /**
+ * Quits Firefox with the provided flags and tears down the current
+ * session.
+ */
+GeckoDriver.prototype.quitApplication = function(cmd, resp) {
+  if (this.appName != "Firefox") {
+    throw new WebDriverError("In app initiated quit only supported in Firefox");
+  }
+
+  let flags = Ci.nsIAppStartup.eAttemptQuit;
+  for (let k of cmd.parameters.flags) {
+    flags |= Ci.nsIAppStartup[k];
+  }
+
+  this.stopSignal_();
+  resp.send();
+
+  this.sessionTearDown();
+  Services.startup.quit(flags);
+};
+
+/**
  * Helper function to convert an outerWindowID into a UID that Marionette
  * tracks.
  */
 GeckoDriver.prototype.generateFrameId = function(id) {
   let uid = id + (this.appName == "B2G" ? "-b2g" : "");
   return uid;
 };
 
@@ -2830,21 +2842,16 @@ GeckoDriver.prototype.receiveMessage = f
 
     case "Marionette:shareData":
       // log messages from tests
       if (message.json.log) {
         this.marionetteLog.addLogs(message.json.log);
       }
       break;
 
-    case "Marionette:runEmulatorCmd":
-    case "Marionette:runEmulatorShell":
-      this.emulator.send(message.json);
-      break;
-
     case "Marionette:switchToModalOrigin":
       this.curBrowser.frameManager.switchToModalOrigin(message);
       this.mm = this.curBrowser.frameManager
           .currentRemoteFrame.messageManager.get();
       break;
 
     case "Marionette:switchedToFrame":
       if (message.json.restorePrevious) {
@@ -3017,17 +3024,18 @@ GeckoDriver.prototype.commands = {
   "getScreenOrientation": GeckoDriver.prototype.getScreenOrientation,
   "setScreenOrientation": GeckoDriver.prototype.setScreenOrientation,
   "getWindowSize": GeckoDriver.prototype.getWindowSize,
   "setWindowSize": GeckoDriver.prototype.setWindowSize,
   "maximizeWindow": GeckoDriver.prototype.maximizeWindow,
   "dismissDialog": GeckoDriver.prototype.dismissDialog,
   "acceptDialog": GeckoDriver.prototype.acceptDialog,
   "getTextFromDialog": GeckoDriver.prototype.getTextFromDialog,
-  "sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog
+  "sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog,
+  "quitApplication": GeckoDriver.prototype.quitApplication,
 };
 
 /**
  * Creates a BrowserObj.  BrowserObjs handle interactions with the
  * browser, according to the current environment (desktop, b2g, etc.).
  *
  * @param {nsIDOMWindow} win
  *     The window whose browser needs to be accessed.
--- a/testing/marionette/emulator.js
+++ b/testing/marionette/emulator.js
@@ -1,122 +1,111 @@
 /* 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";
 
-var {classes: Cc, interfaces: Ci} = Components;
-const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-this.EXPORTED_SYMBOLS = ["emulator", "Emulator", "EmulatorCallback"];
-
-this.emulator = {};
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-/**
- * Determines if command ID is an emulator callback.
- */
-this.emulator.isCallback = function(cmdId) {
-  return cmdId < 0;
-};
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const logger = Log.repository.getLogger("Marionette");
+
+this.EXPORTED_SYMBOLS = ["Emulator"];
 
 /**
  * Represents the connection between Marionette and the emulator it's
  * running on.
  *
  * When injected scripts call the JS routines {@code runEmulatorCmd} or
  * {@code runEmulatorShell}, the second argument to those is a callback
  * which is stored in cbs.  They are later retreived by their unique ID
  * using popCallback.
  *
- * @param {function(Object)} sendFn
+ * @param {function(Object)} sendToEmulatorFn
  *     Callback function that sends a message to the emulator.
+ * @param {function(Object)} sendToEmulatorFn
+ *     Callback function that sends a message asynchronously to the
+ *     current listener.
  */
-this.Emulator = function(sendFn) {
-  this.send = sendFn;
-  this.cbs = [];
+this.Emulator = function(sendToEmulatorFn) {
+  this.sendToEmulator = sendToEmulatorFn;
 };
 
 /**
- * Pops a callback off the stack if found.  Otherwise this is a no-op.
+ * Instruct the client to run an Android emulator command.
  *
- * @param {number} id
- *     Unique ID associated with the callback.
- *
- * @return {?function(Object)}
- *     Callback function that takes an emulator response message as
- *     an argument.
+ * @param {string} cmd
+ *     The command to run.
+ * @param {function(?)} resCb
+ *     Callback on a result response from the emulator.
+ * @param {function(?)} errCb
+ *     Callback on an error in running the command.
  */
-Emulator.prototype.popCallback = function(id) {
-  let f, fi;
-  for (let i = 0; i < this.cbs.length; ++i) {
-    if (this.cbs[i].id == id) {
-      f = this.cbs[i];
-      fi = i;
-    }
-  }
-
-  if (!f) {
-    return null;
-  }
-
-  this.cbs.splice(fi, 1);
-  return f;
+Emulator.prototype.command = function(cmd, resCb, errCb) {
+  assertDefined(cmd, "runEmulatorCmd");
+  this.sendToEmulator(
+      "runEmulatorCmd", {emulator_cmd: cmd}, resCb, errCb);
 };
 
 /**
- * Pushes callback on to the stack.
+ * Instruct the client to execute Android emulator shell arguments.
  *
- * @param {function(Object)} cb
- *     Callback function that takes an emulator response message as
- *     an argument.
+ * @param {Array.<string>} args
+ *     The shell instruction for the emulator to execute.
+ * @param {function(?)} resCb
+ *     Callback on a result response from the emulator.
+ * @param {function(?)} errCb
+ *     Callback on an error in executing the shell arguments.
  */
-Emulator.prototype.pushCallback = function(cb) {
-  cb.send_ = this.sendFn;
-  this.cbs.push(cb);
+Emulator.prototype.shell = function(args, resCb, errCb) {
+  assertDefined(args, "runEmulatorShell");
+  this.sendToEmulator(
+      "runEmulatorShell", {emulator_shell: args}, resCb, errCb);
+};
+
+Emulator.prototype.processMessage = function(msg) {
+  let resCb = this.resultCallback(msg.json.id);
+  let errCb = this.errorCallback(msg.json.id);
+
+  switch (msg.name) {
+    case "Marionette:runEmulatorCmd":
+      this.command(msg.json.command, resCb, errCb);
+      break;
+
+    case "Marionette:runEmulatorShell":
+      this.shell(msg.json.arguments, resCb, errCb);
+      break;
+  }
 };
 
-/**
- * Encapsulates a callback to the emulator and provides an execution
- * environment for them.
- *
- * Each callback is assigned a unique identifier, id, that can be used
- * to retrieve them from Emulator's stack using popCallback.
- *
- * The onresult event listener is triggered when a result arrives on
- * the callback.
- *
- * The onerror event listener is triggered when an error occurs during
- * the execution of that callback.
- */
-this.EmulatorCallback = function() {
-  this.id = uuidGen.generateUUID().toString();
-  this.onresult = null;
-  this.onerror = null;
-  this.send_ = null;
+Emulator.prototype.resultCallback = function(msgId) {
+  return res => this.sendResult({result: res, id: msgId});
+};
+
+Emulator.prototype.errorCallback = function(msgId) {
+  return err => this.sendResult({error: err, id: msgId});
+};
+
+Emulator.prototype.sendResult = function(msg) {
+  // sendToListener set explicitly in GeckoDriver's ctor
+  this.sendToListener("emulatorCmdResult", msg);
 };
 
-EmulatorCallback.prototype.command = function(cmd, cb) {
-  this.onresult = cb;
-  this.send_({emulator_cmd: cmd, id: this.id});
-};
-
-EmulatorCallback.prototype.shell = function(args, cb) {
-  this.onresult = cb;
-  this.send_({emulator_shell: args, id: this.id});
+/** Receives IPC messages from the listener. */
+Emulator.prototype.receiveMessage = function(msg) {
+  try {
+    this.processMessage(msg);
+  } catch (e) {
+    this.sendResult({error: `${e.name}: ${e.message}`, id: msg.json.id});
+  }
 };
 
-EmulatorCallback.prototype.result = function(msg) {
-  if (this.send_ === null) {
-    throw new TypeError(
-      "EmulatorCallback must be registered with Emulator to fire");
-  }
+Emulator.prototype.QueryInterface = XPCOMUtils.generateQI(
+    [Ci.nsIMessageListener, Ci.nsISupportsWeakReference]);
 
-  try {
-    if (!this.onresult) {
-      return;
-    }
-    this.onresult(msg.result);
-  } catch (e) {
-    if (this.onerror) {
-      this.onerror(e);
-    }
+function assertDefined(arg, action) {
+  if (typeof arg == "undefined") {
+    throw new TypeError("Not enough arguments to " + action);
   }
-};
+}
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -105,16 +105,34 @@ error.stringify = function(err) {
     }
     return s;
   } catch (e) {
     return "<unprintable error>";
   }
 };
 
 /**
+ * Marshal an Error to a JSON structure.
+ *
+ * @param {Error} err
+ *     The Error to serialise.
+ *
+ * @return {Object.<string, Object>}
+ *     JSON structure with the keys "error", "message", and "stacktrace".
+ */
+error.toJson = function(err) {
+  let json = {
+    error: err.status,
+    message: err.message || null,
+    stacktrace: err.stack || null,
+  };
+  return json;
+};
+
+/**
  * WebDriverError is the prototypal parent of all WebDriver errors.
  * It should not be used directly, as it does not correspond to a real
  * error in the specification.
  */
 this.WebDriverError = function(msg) {
   Error.call(this, msg);
   this.name = "WebDriverError";
   this.message = msg;
@@ -211,17 +229,17 @@ this.JavaScriptError = function(err, fnN
   this.stack = trace;
 };
 JavaScriptError.prototype = Object.create(WebDriverError.prototype);
 
 this.NoAlertOpenError = function(msg) {
   WebDriverError.call(this, msg);
   this.name = "NoAlertOpenError";
   this.status = "no such alert";
-}
+};
 NoAlertOpenError.prototype = Object.create(WebDriverError.prototype);
 
 this.NoSuchElementError = function(msg) {
   WebDriverError.call(this, msg);
   this.name = "NoSuchElementError";
   this.status = "no such element";
 };
 NoSuchElementError.prototype = Object.create(WebDriverError.prototype);
--- a/testing/marionette/frame-manager.js
+++ b/testing/marionette/frame-manager.js
@@ -180,18 +180,18 @@ FrameManager.prototype = {
    *     ChromeMessageBroadcaster or ChromeMessageSender.
    */
   addMessageManagerListeners: function FM_addMessageManagerListeners(mm) {
     mm.addWeakMessageListener("Marionette:ok", this.server);
     mm.addWeakMessageListener("Marionette:done", this.server);
     mm.addWeakMessageListener("Marionette:error", this.server);
     mm.addWeakMessageListener("Marionette:emitTouchEvent", this.server);
     mm.addWeakMessageListener("Marionette:log", this.server);
-    mm.addWeakMessageListener("Marionette:runEmulatorCmd", this.server);
-    mm.addWeakMessageListener("Marionette:runEmulatorShell", this.server);
+    mm.addWeakMessageListener("Marionette:runEmulatorCmd", this.server.emulator);
+    mm.addWeakMessageListener("Marionette:runEmulatorShell", this.server.emulator);
     mm.addWeakMessageListener("Marionette:shareData", this.server);
     mm.addWeakMessageListener("Marionette:switchToModalOrigin", this.server);
     mm.addWeakMessageListener("Marionette:switchedToFrame", this.server);
     mm.addWeakMessageListener("Marionette:getVisibleCookies", this.server);
     mm.addWeakMessageListener("Marionette:register", this.server);
     mm.addWeakMessageListener("Marionette:listenersAttached", this.server);
     mm.addWeakMessageListener("Marionette:getFiles", this.server);
     mm.addWeakMessageListener("MarionetteFrame:handleModal", this);
@@ -212,18 +212,18 @@ FrameManager.prototype = {
    *     ChromeMessageBroadcaster or ChromeMessageSender.
    */
   removeMessageManagerListeners: function FM_removeMessageManagerListeners(mm) {
     mm.removeWeakMessageListener("Marionette:ok", this.server);
     mm.removeWeakMessageListener("Marionette:done", this.server);
     mm.removeWeakMessageListener("Marionette:error", this.server);
     mm.removeWeakMessageListener("Marionette:log", this.server);
     mm.removeWeakMessageListener("Marionette:shareData", this.server);
-    mm.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server);
-    mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.server);
+    mm.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server.emulator);
+    mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.server.emulator);
     mm.removeWeakMessageListener("Marionette:switchedToFrame", this.server);
     mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.server);
     mm.removeWeakMessageListener("Marionette:listenersAttached", this.server);
     mm.removeWeakMessageListener("Marionette:register", this.server);
     mm.removeWeakMessageListener("Marionette:getFiles", this.server);
     mm.removeWeakMessageListener("MarionetteFrame:handleModal", this);
     mm.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this);
   }
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -11,17 +11,17 @@ marionette.jar:
   content/elements.js (elements.js)
   content/sendkeys.js (sendkeys.js)
   content/common.js (common.js)
   content/simpletest.js (simpletest.js)
   content/frame-manager.js (frame-manager.js)
   content/EventUtils.js  (EventUtils.js)
   content/ChromeUtils.js  (ChromeUtils.js)
   content/error.js (error.js)
-  content/command.js (command.js)
+  content/message.js (message.js)
   content/dispatcher.js (dispatcher.js)
   content/emulator.js (emulator.js)
   content/modal.js (modal.js)
   content/proxy.js (proxy.js)
   content/capture.js (capture.js)
   content/cookies.js (cookies.js)
 #ifdef ENABLE_TESTS
   content/test.xul  (client/marionette/chrome/test.xul)
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -1898,50 +1898,58 @@ function deleteAllCookies() {
 }
 
 function getAppCacheStatus(msg) {
   sendResponse({ value: curContainer.frame.applicationCache.status },
                msg.json.command_id);
 }
 
 // emulator callbacks
-var _emu_cb_id = 0;
 var _emu_cbs = {};
 
 function runEmulatorCmd(cmd, callback) {
+  logger.info("listener runEmulatorCmd cmd=" + cmd);
   if (callback) {
-    _emu_cbs[_emu_cb_id] = callback;
+    _emu_cbs[asyncTestCommandId] = callback;
   }
-  sendAsyncMessage("Marionette:runEmulatorCmd", {emulator_cmd: cmd, id: _emu_cb_id});
-  _emu_cb_id += 1;
+  sendAsyncMessage("Marionette:runEmulatorCmd",
+      {command: cmd, id: asyncTestCommandId});
 }
 
 function runEmulatorShell(args, callback) {
   if (callback) {
-    _emu_cbs[_emu_cb_id] = callback;
+    _emu_cbs[asyncTestCommandId] = callback;
   }
-  sendAsyncMessage("Marionette:runEmulatorShell", {emulator_shell: args, id: _emu_cb_id});
-  _emu_cb_id += 1;
+  sendAsyncMessage("Marionette:runEmulatorShell",
+      {arguments: args, id: asyncTestCommandId});
 }
 
 function emulatorCmdResult(msg) {
-  let message = msg.json;
+  let {error, result, id} = msg.json;
+
+  if (error) {
+    let err = new JavaScriptError(error);
+    sendError(err, id);
+    return;
+  }
+
   if (!sandboxes[sandboxName]) {
     return;
   }
-  let cb = _emu_cbs[message.id];
-  delete _emu_cbs[message.id];
+  let cb = _emu_cbs[id];
+  delete _emu_cbs[id];
   if (!cb) {
     return;
   }
+
   try {
-    cb(message.result);
+    cb(result);
   } catch (e) {
-    sendError(e, -1);
-    return;
+    let err = new JavaScriptError(e);
+    sendError(err, id);
   }
 }
 
 function importScript(msg) {
   let command_id = msg.json.command_id;
   let file;
   if (importedScripts.exists()) {
     file = FileUtils.openFileOutputStream(importedScripts,
new file mode 100644
--- /dev/null
+++ b/testing/marionette/message.js
@@ -0,0 +1,288 @@
+/* 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";
+
+var {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 = [
+  "Command",
+  "Message",
+  "MessageOrigin",
+  "Response",
+];
+
+const logger = Log.repository.getLogger("Marionette");
+
+this.MessageOrigin = {
+  Client: 0,
+  Server: 1,
+};
+
+this.Message = {};
+
+/**
+ * Converts a data packet into a Command or Response type.
+ *
+ * @param {Array.<number, number, ?, ?>} data
+ *     A four element array where the elements, in sequence, signifies
+ *     message type, message ID, method name or error, and parameters
+ *     or result.
+ *
+ * @return {(Command,Response)}
+ *     Based on the message type, a Command or Response instance.
+ *
+ * @throws {TypeError}
+ *     If the message type is not recognised.
+ */
+Message.fromMsg = function(data) {
+  switch (data[0]) {
+    case Command.TYPE:
+      return Command.fromMsg(data);
+
+    case Response.TYPE:
+      return Response.fromMsg(data);
+
+    default:
+      throw new TypeError(
+          "Unrecognised message type in packet: " + JSON.stringify(data));
+  }
+};
+
+/**
+ * A command is a request from the client to run a series of remote end
+ * steps and return a fitting response.
+ *
+ * The command can be synthesised from the message passed over the
+ * Marionette socket using the {@code fromMsg} function.  The format of
+ * a message is:
+ *
+ *     [type, id, name, params]
+ *
+ * where
+ *
+ *   type:
+ *     Must be zero (integer). Zero means that this message is a command.
+ *
+ *   id:
+ *     Number used as a sequence number.  The server replies with a
+ *     requested id.
+ *
+ *   name:
+ *     String representing the command name with an associated set of
+ *     remote end steps.
+ *
+ *   params:
+ *     Object of command function arguments.  The keys of this object
+ *     must be strings, but the values can be arbitrary values.
+ *
+ * A command has an associated message {@code id} that prevents the
+ * dispatcher from sending responses in the wrong order.
+ *
+ * The command may also have optional error- and result handlers that
+ * are called when the client returns with a response.  These are
+ * {@code function onerror({Object})}, {@code function onresult({Object})},
+ * and {@code function onresult({Response})}.
+ *
+ * @param {number} msgId
+ *     Message ID unique identifying this message.
+ * @param {string} name
+ *     Command name.
+ * @param {Object<string, ?>} params
+ *     Command parameters.
+ */
+this.Command = class {
+  constructor(msgId, name, params={}) {
+    this.id = msgId;
+    this.name = name;
+    this.parameters = params;
+
+    this.onerror = null;
+    this.onresult = null;
+
+    this.origin = MessageOrigin.Client;
+    this.sent = false;
+  }
+
+  /**
+   * Calls the error- or result handler associated with this command.
+   * This function can be replaced with a custom response handler.
+   *
+   * @param {Response} resp
+   *     The response to pass on to the result or error to the
+   *     {@code onerror} or {@code onresult} handlers to.
+   */
+  onresponse(resp) {
+    if (resp.error && this.onerror) {
+      this.onerror(resp.error);
+    } else if (resp.body && this.onresult) {
+      this.onresult(resp.body);
+    }
+  }
+
+  toMsg() {
+    return [Command.TYPE, this.id, this.name, this.parameters];
+  }
+
+  toString() {
+    return "Command {id: " + this.id + ", " +
+        "name: " + JSON.stringify(this.name) + ", " +
+        "parameters: " + JSON.stringify(this.parameters) + "}"
+  }
+
+  static fromMsg(msg) {
+    return new Command(msg[1], msg[2], msg[3]);
+  }
+};
+
+Command.TYPE = 0;
+
+
+const validator = {
+  exclusionary: {
+    "capabilities": ["error", "value"],
+    "error": ["value", "sessionId", "capabilities"],
+    "sessionId": ["error", "value"],
+    "value": ["error", "sessionId", "capabilities"],
+  },
+
+  set: function(obj, prop, val) {
+    let tests = this.exclusionary[prop];
+    if (tests) {
+      for (let t of tests) {
+        if (obj.hasOwnProperty(t)) {
+          throw new TypeError(`${t} set, cannot set ${prop}`);
+        }
+      }
+    }
+
+    obj[prop] = val;
+    return true;
+  },
+};
+
+/**
+ * The response body is exposed as an argument to commands.
+ * Commands can set fields on the body through defining properties.
+ *
+ * Setting properties invokes a validator that performs tests for
+ * mutually exclusionary fields on the input against the existing data
+ * in the body.
+ *
+ * For example setting the {@code error} property on the body when
+ * {@code value}, {@code sessionId}, or {@code capabilities} have been
+ * set previously will cause an error.
+ */
+this.ResponseBody = () => new Proxy({}, validator);
+
+/**
+ * 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.  To send data in a response,
+ * you modify the body property on the response.  The body property can
+ * also be replaced completely.
+ *
+ * The response is sent implicitly by CommandProcessor when a command
+ * has finished executing, and any modifications made subsequent to that
+ * will have no effect.
+ *
+ * @param {number} msgId
+ *     Message ID tied to the corresponding command request this is a
+ *     response for.
+ * @param {function(Response|Message)} respHandler
+ *     Function callback called on sending the response.
+ */
+this.Response = class {
+  constructor(msgId, respHandler) {
+    this.id = msgId;
+
+    this.error = null;
+    this.body = ResponseBody();
+
+    this.origin = MessageOrigin.Server;
+    this.sent = false;
+
+    this.respHandler_ = respHandler;
+  }
+
+  /**
+   * Sends response conditionally, given a predicate.
+   *
+   * @param {function(Response): boolean} predicate
+   *     A predicate taking a Response object and returning a boolean.
+   */
+  sendConditionally(predicate) {
+    if (predicate(this)) {
+      this.send();
+    }
+  }
+
+  /**
+   * Sends response using the response handler provided on construction.
+   *
+   * @throws {RangeError}
+   *     If the response has already been sent.
+   */
+  send() {
+    if (this.sent) {
+      throw new RangeError("Response has already been sent: " + this);
+    }
+    this.respHandler_(this);
+    this.sent = true;
+  }
+
+  /**
+   * Send given Error to client.
+   *
+   * Turns the response into an error response, clears any previously
+   * set body data, and sends it using the response handler provided
+   * on construction.
+   *
+   * @param {Error} err
+   *     The Error instance to send.
+   *
+   * @throws {Error}
+   *     If the {@code error} is not a WebDriverError, the error is
+   *     propagated.
+   */
+  sendError(err) {
+    let wd = error.isWebDriverError(err);
+    let we = wd ? err : new WebDriverError(err.message);
+
+    this.error = error.toJson(err);
+    this.body = null;
+    this.send();
+
+    // propagate errors that are implementation problems
+    if (!wd) {
+      throw err;
+    }
+  }
+
+  toMsg() {
+    return [Response.TYPE, this.id, this.error, this.body];
+  }
+
+  toString() {
+    return "Response {id: " + this.id + ", " +
+        "error: " + JSON.stringify(this.error) + ", " +
+        "body: " + JSON.stringify(this.body) + "}";
+  }
+
+  static fromMsg(msg) {
+    let resp = new Response(msg[1], null);
+    resp.error = msg[2];
+    resp.body = msg[3];
+    return resp;
+  }
+};
+
+Response.TYPE = 1;
--- a/testing/marionette/server.js
+++ b/testing/marionette/server.js
@@ -88,17 +88,18 @@ MarionetteServer.prototype.driverFactory
 
   if (bypassOffline) {
     logger.debug("Bypassing offline status");
     Services.prefs.setBoolPref("network.gonk.manage-offline-status", false);
     Services.io.manageOfflineStatus = false;
     Services.io.offline = false;
   }
 
-  return new GeckoDriver(appName, device, emulator);
+  let stopSignal = () => this.stop();
+  return new GeckoDriver(appName, device, stopSignal, emulator);
 };
 
 MarionetteServer.prototype.start = function() {
   if (this.alive) {
     return;
   }
   let flags = Ci.nsIServerSocket.KeepWhenOffline;
   if (this.forceLocal) {
@@ -124,30 +125,27 @@ MarionetteServer.prototype.closeListener
 
 MarionetteServer.prototype.onSocketAccepted = function(
     serverSocket, clientSocket) {
   let input = clientSocket.openInputStream(0, 0, 0);
   let output = clientSocket.openOutputStream(0, 0, 0);
   let transport = new DebuggerTransport(input, output);
   let connId = "conn" + this.nextConnId++;
 
-  let stopSignal = () => this.stop();
-  let dispatcher = new Dispatcher(connId, transport, this.driverFactory, stopSignal);
+  let dispatcher = new Dispatcher(connId, transport, this.driverFactory.bind(this));
   dispatcher.onclose = this.onConnectionClosed.bind(this);
   this.conns[connId] = dispatcher;
 
   logger.info(`Accepted connection ${connId} from ${clientSocket.host}:${clientSocket.port}`);
-
-  // Create a root actor for the connection and send the hello packet
   dispatcher.sayHello();
   transport.ready();
 };
 
 MarionetteServer.prototype.onConnectionClosed = function(conn) {
-  let id = conn.id;
+  let id = conn.connId;
   delete this.conns[id];
   logger.info(`Closed connection ${id}`);
 };
 
 function isMulet() {
   try {
     return Services.prefs.getBoolPref("b2g.is_mulet");
   } catch (e) {