Bug 1153828 - Merge message listeners in AsyncContentSender; r?automatedtester draft
authorAndreas Tolfsen <ato@mozilla.com>
Thu, 21 Jan 2016 19:27:23 +0000
changeset 325489 5449471881b172ef6b6cc3021caf694a24b9df21
parent 325488 bd8bb6298d90770f97843e9d7dc711cc0f87d02f
child 513448 2ac65c368d8a788e6bdc5443d99f17d9434ae3d4
push id9979
push useratolfsen@mozilla.com
push dateMon, 25 Jan 2016 17:07:13 +0000
reviewersautomatedtester
bugs1153828
milestone47.0a1
Bug 1153828 - Merge message listeners in AsyncContentSender; r?automatedtester This change reduces the number of content frame script message senders from three to one by imposing a message format.
testing/marionette/driver.js
testing/marionette/listener.js
testing/marionette/proxy.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -1213,17 +1213,18 @@ GeckoDriver.prototype.executeWithCallbac
  *     URL to navigate to.
  */
 GeckoDriver.prototype.get = function(cmd, resp) {
   let url = cmd.parameters.url;
 
   switch (this.context) {
     case Context.CONTENT:
       let get = this.listener.get({url: url, pageTimeout: this.pageTimeout});
-      let id = this.listener.curId;
+      // TODO(ato): Bug 1242595
+      let id = this.listener.activeMessageId;
 
       // If a remoteness update interrupts our page load, this will never return
       // We need to re-issue this request to correctly poll for readyState and
       // send errors.
       this.curBrowser.pendingCommands.push(() => {
         cmd.parameters.command_id = id;
         this.mm.broadcastAsyncMessage(
             "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -399,55 +399,55 @@ function deleteSession(msg) {
 
 /*
  * Helper methods
  */
 
 /**
  * Generic method to send a message to the server
  */
-function sendToServer(name, data, objs, id) {
-  if (!data) {
-    data = {}
-  }
+function sendToServer(path, data = {}, objs, id) {
   if (id) {
     data.command_id = id;
   }
-  sendAsyncMessage(name, data, objs);
+  sendAsyncMessage(path, data, objs);
 }
 
 /**
  * Send response back to server
  */
-function sendResponse(value, command_id) {
-  sendToServer("Marionette:done", value, null, command_id);
+function sendResponse(value, id) {
+  let path = proxy.AsyncContentSender.makeReplyPath(id);
+  sendToServer(path, value, null, id);
 }
 
 /**
  * Send ack back to server
  */
-function sendOk(command_id) {
-  sendToServer("Marionette:ok", null, null, command_id);
+function sendOk(id) {
+  let path = proxy.AsyncContentSender.makeReplyPath(id);
+  sendToServer(path, {}, null, id);
+}
+
+/**
+ * Send error message to server
+ */
+function sendError(err, id) {
+  let path = proxy.AsyncContentSender.makeReplyPath(id);
+  sendToServer(path, {error: null}, {error: err}, id);
 }
 
 /**
  * Send log message to server
  */
 function sendLog(msg) {
   sendToServer("Marionette:log", {message: msg});
 }
 
 /**
- * Send error message to server
- */
-function sendError(err, cmdId) {
-  sendToServer("Marionette:error", null, {error: err}, cmdId);
-}
-
-/**
  * Clear test values after completion of test
  */
 function resetValues() {
   sandboxes = {};
   curContainer = { frame: content, shadowRoot: null };
   actions.mouseEventsOnly = false;
 }
 
--- a/testing/marionette/proxy.js
+++ b/testing/marionette/proxy.js
@@ -1,27 +1,22 @@
 /* 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("chrome://marionette/content/modal.js");
 
 this.EXPORTED_SYMBOLS = ["proxy"];
 
-const MARIONETTE_OK = "Marionette:ok";
-const MARIONETTE_DONE = "Marionette:done";
-const MARIONETTE_ERROR = "Marionette:error";
-
-const logger = Log.repository.getLogger("Marionette");
-const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+const uuidgen = Cc["@mozilla.org/uuid-generator;1"]
+    .getService(Ci.nsIUUIDGenerator);
 
 // Proxy handler that traps requests to get a property.  Will prioritise
 // properties that exist on the object's own prototype.
 var ownPriorityGetterTrap = {
   get: (obj, prop) => {
     if (obj.hasOwnProperty(prop)) {
       return obj[prop];
     }
@@ -30,132 +25,135 @@ var ownPriorityGetterTrap = {
 };
 
 this.proxy = {};
 
 /**
  * Creates a transparent interface between the chrome- and content
  * contexts.
  *
- * Calls to this object will be proxied via the message manager to the active
- * browsing context (content) and responses will be provided back as
- * promises.
+ * Calls to this object will be proxied via the message manager to a
+ * content frame script, and responses are returend as promises.
  *
  * The argument sequence is serialised and passed as an array, unless it
  * consists of a single object type that isn't null, in which case it's
  * passed literally.  The latter specialisation is temporary to achieve
  * backwards compatibility with listener.js.
  *
  * @param {function(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn
- *     Function returning the current message manager.
+ *     Closure function returning the current message manager.
  * @param {function(string, Object, number)} sendAsyncFn
- *     Callback for sending async messages to the current listener.
+ *     Callback for sending async messages.
  */
 proxy.toListener = function(mmFn, sendAsyncFn) {
-  let sender = new AsyncContentSender(mmFn, sendAsyncFn);
+  let sender = new proxy.AsyncContentSender(mmFn, sendAsyncFn);
   return new Proxy(sender, ownPriorityGetterTrap);
 };
 
 /**
- * The AsyncContentSender allows one to make synchronous calls to the
- * message listener of the content frame of the current browsing context.
- *
- * Presumptions about the responses from content space are made so we
- * can provide a nicer API on top of the message listener primitives that
- * make calls from chrome- to content space seem synchronous by leveraging
- * promises.
+ * With the AsyncContentSender it is possible to make asynchronous calls
+ * to the message listener in a frame script.
  *
- * The promise is guaranteed not to resolve until the execution of the
- * command in content space is complete.
+ * The responses from content are expected to be JSON Objects, where an
+ * {@code error} key indicates that an error occured, and a {@code value}
+ * entry that the operation was successful.  It is the value of the
+ * {@code value} key that is returned to the consumer through a promise.
  */
-this.AsyncContentSender = class {
+proxy.AsyncContentSender = class {
   constructor(mmFn, sendAsyncFn) {
-    this.curId = null;
     this.sendAsync = sendAsyncFn;
+    // TODO(ato): Bug 1242595
+    this.activeMessageId = null;
+
     this.mmFn_ = mmFn;
-    this._listeners = [];
+    this.listeners_ = new Map();
+    this.dialogueObserver_ = null;
   }
 
   get mm() {
     return this.mmFn_();
   }
 
-  removeListeners() {
-    this._listeners.map(l => this.mm.removeMessageListener(l[0], l[1]));
-    this._listeners = [];
-  }
-
   /**
    * Call registered function in the frame script environment of the
    * current browsing context's content frame.
    *
    * @param {string} name
-   *     Function to call in the listener, e.g. for "Marionette:foo8",
-   *     use "foo".
-   * @param {Array} args
+   *     Function to call in the listener, e.g. for the message listener
+   *     "Marionette:foo8", use "foo".
+   * @param {Array.<?>=} args
    *     Argument list to pass the function.  If args has a single entry
    *     that is an object, we assume it's an old style dispatch, and
    *     the object will passed literally.
    *
    * @return {Promise}
    *     A promise that resolves to the result of the command.
    */
-  send(name, args) {
-    if (this._listeners[0]) {
-      // A prior (probably timed-out) request has left listeners behind.
-      // Remove them before proceeding.
-      logger.warn("A previous failed command left content listeners behind!");
-      this.removeListeners();
-    }
-
-    this.curId = uuidgen.generateUUID().toString();
-
-    let proxy = new Promise((resolve, reject) => {
-      let removeListeners = (n, fn) => {
-        let rmFn = msg => {
-          if (this.curId !== msg.json.command_id) {
-            logger.warn("Skipping out-of-sync response from listener: " +
-                `Expected response to ${name} with ID ${this.curId}, ` +
-                "but got: " + msg.name + msg.json.toSource());
-            return;
-          }
+  send(name, args = []) {
+    let uuid = uuidgen.generateUUID().toString();
+    // TODO(ato): Bug 1242595
+    this.activeMessageId = uuid;
 
-          this.removeListeners();
-          modal.removeHandler(handleDialog);
-
-          fn(msg);
-          this.curId = null;
-        };
-
-        this._listeners.push([n, rmFn]);
-        return rmFn;
+    return new Promise((resolve, reject) => {
+      let path = proxy.AsyncContentSender.makeReplyPath(uuid);
+      let cb = msg => {
+        this.activeMessageId = null;
+        if ("error" in msg.json) {
+          reject(msg.objects.error);
+        } else {
+          resolve(msg.json.value);
+        }
       };
-
-      let okListener = () => resolve();
-      let valListener = msg => resolve(msg.json.value);
-      let errListener = msg => reject(msg.objects.error);
-
-      let handleDialog = (subject, topic) => {
-        this.removeListeners()
-        modal.removeHandler(handleDialog);
-        this.sendAsync("cancelRequest");
+      this.dialogueObserver_ = (subject, topic) => {
+        this.cancelAll();
         resolve();
       };
 
-      // start content process listeners, and install observers for global-
-      // and tab modal dialogues
-      this.mm.addMessageListener(MARIONETTE_OK, removeListeners(MARIONETTE_OK, okListener));
-      this.mm.addMessageListener(MARIONETTE_DONE, removeListeners(MARIONETTE_DONE, valListener));
-      this.mm.addMessageListener(MARIONETTE_ERROR, removeListeners(MARIONETTE_ERROR, errListener));
-      modal.addHandler(handleDialog);
+      // start content message listener
+      // and install observers for global- and tab modal dialogues
+      this.addListener_(path, cb);
+      modal.addHandler(this.dialogueObserver_);
+
+      this.sendAsync(name, marshal(args), uuid);
+    });
+  }
+
+  cancelAll() {
+    this.removeAllListeners_();
+    modal.removeHandler(this.dialogueObserver_);
+    this.sendAsync("cancelRequest");
+  }
 
-      this.sendAsync(name, marshal(args), this.curId);
-    });
+  addListener_(path, callback) {
+    let autoRemover = msg => {
+      this.removeListener_(path);
+      modal.removeHandler(this.dialogueObserver_);
+      callback(msg);
+    };
+
+    this.mm.addMessageListener(path, autoRemover);
+    this.listeners_.set(path, autoRemover);
+  }
 
-    return proxy;
+  removeListener_(path) {
+    let l = this.listeners_.get(path);
+    this.mm.removeMessageListener(path, l[1]);
+    return this.listeners_.delete(path);
+  }
+
+  removeAllListeners_() {
+    let ok = true;
+    for (let [p, cb] of this.listeners_) {
+      ok |= this.removeListener_(p);
+    }
+    return ok;
+  }
+
+  static makeReplyPath(uuid) {
+    return "Marionette:asyncReply:" + uuid;
   }
 };
 
 /**
  * Creates a transparent interface from the content- to the chrome context.
  *
  * Calls to this object will be proxied via the frame's sendSyncMessage
  * (nsISyncMessageSender) function.  Since the message is synchronous,
@@ -165,30 +163,30 @@ this.AsyncContentSender = class {
  *
  *     let chrome = proxy.toChrome(sendSyncMessage.bind(this));
  *     let cookie = chrome.getCookie("foo");
  *
  * @param {nsISyncMessageSender} sendSyncMessageFn
  *     The frame message manager's sendSyncMessage function.
  */
 proxy.toChrome = function(sendSyncMessageFn) {
-  let sender = new SyncChromeSender(sendSyncMessageFn);
+  let sender = new proxy.SyncChromeSender(sendSyncMessageFn);
   return new Proxy(sender, ownPriorityGetterTrap);
 };
 
 /**
  * The SyncChromeSender sends synchronous RPC messages to the chrome
  * context, using a frame's sendSyncMessage (nsISyncMessageSender) function.
  *
  * Example on how to use from a frame content script:
  *
  *     let sender = new SyncChromeSender(sendSyncMessage.bind(this));
  *     let res = sender.send("addCookie", cookie);
  */
-this.SyncChromeSender = class {
+proxy.SyncChromeSender = class {
   constructor(sendSyncMessage) {
     this.sendSyncMessage_ = sendSyncMessage;
   }
 
   send(func, args) {
     let name = "Marionette:" + func;
     return this.sendSyncMessage_(name, marshal(args));
   }