Bug 1522253: Add a standard mechanism for requesting data from the other process. r=Gijs
authorDave Townsend <dtownsend@oxymoronical.com>
Wed, 13 Feb 2019 17:29:53 +0000
changeset 458926 b745576a1639532f77909f59940dca2009a71452
parent 458925 9a2412c232a5897fe21cd5965e350d6665ca2310
child 458927 7897f18e59f57e831e4a88e8669361b629984075
push id35551
push usershindli@mozilla.com
push dateWed, 13 Feb 2019 21:34:09 +0000
treeherdermozilla-central@08f794a4928e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1522253
milestone67.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 1522253: Add a standard mechanism for requesting data from the other process. r=Gijs As we add more specific methods to RemotePageManager some of them will have to call the main process to get the results. This adds a standard mechanism for doing so. Calling sendRequest will return a promise that is resolved or rejected when the main process responds. Differential Revision: https://phabricator.services.mozilla.com/D19412
toolkit/components/remotepagemanager/MessagePort.jsm
toolkit/components/remotepagemanager/RemotePageManagerChild.jsm
toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
--- a/toolkit/components/remotepagemanager/MessagePort.jsm
+++ b/toolkit/components/remotepagemanager/MessagePort.jsm
@@ -6,16 +6,18 @@
 
 var EXPORTED_SYMBOLS = ["MessagePort", "MessageListener"];
 
 const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.defineModuleGetter(this, "AsyncPrefs",
   "resource://gre/modules/AsyncPrefs.jsm");
 ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "PromiseUtils",
+  "resource://gre/modules/PromiseUtils.jsm");
 
 /*
  * Used for all kinds of permissions checks which requires explicit
  * whitelisting of specific permissions granted through RPM.
  *
  * Please note that prefs that one wants to update need to be
  * whitelisted within AsyncPrefs.jsm.
  */
@@ -121,28 +123,117 @@ class MessageListener {
  */
 class MessagePort {
   constructor(messageManager, portID) {
     this.messageManager = messageManager;
     this.portID = portID;
     this.destroyed = false;
     this.listener = new MessageListener();
 
+    // This is a sparse array of pending requests. The id of each request is
+    // simply its index in the array. The next id is the current length of the
+    // array (which includes the count of missing indexes).
+    this.requests = [];
+
     this.message = this.message.bind(this);
+    this.receiveRequest = this.receiveRequest.bind(this);
+    this.receiveResponse = this.receiveResponse.bind(this);
+    this.addMessageListeners();
+  }
+
+  addMessageListeners() {
     this.messageManager.addMessageListener("RemotePage:Message", this.message);
+    this.messageManager.addMessageListener("RemotePage:Request", this.receiveRequest);
+    this.messageManager.addMessageListener("RemotePage:Response", this.receiveResponse);
+  }
+
+  removeMessageListeners() {
+    this.messageManager.removeMessageListener("RemotePage:Message", this.message);
+    this.messageManager.removeMessageListener("RemotePage:Request", this.receiveRequest);
+    this.messageManager.removeMessageListener("RemotePage:Response", this.receiveResponse);
   }
 
   // Called when the message manager used to connect to the other process has
   // changed, i.e. when a tab is detached.
   swapMessageManager(messageManager) {
-    this.messageManager.removeMessageListener("RemotePage:Message", this.message);
-
+    this.removeMessageListeners();
     this.messageManager = messageManager;
+    this.addMessageListeners();
+  }
 
-    this.messageManager.addMessageListener("RemotePage:Message", this.message);
+  // Sends a request to the other process and returns a promise that completes
+  // once the other process has responded to the request or some error occurs.
+  sendRequest(name, data = null) {
+    if (this.destroyed) {
+      return Promise.reject(new Error("Message port has been destroyed"));
+    }
+
+    let deferred = PromiseUtils.defer();
+    this.requests.push(deferred);
+
+    this.messageManager.sendAsyncMessage("RemotePage:Request", {
+      portID: this.portID,
+      requestID: this.requests.length - 1,
+      name,
+      data,
+    });
+
+    return deferred.promise;
+  }
+
+  // Handles an IPC message to perform a request of some kind.
+  async receiveRequest({ data: messagedata }) {
+    if (this.destroyed || (messagedata.portID != this.portID)) {
+      return;
+    }
+
+    let data = {
+      portID: this.portID,
+      requestID: messagedata.requestID,
+    };
+
+    try {
+      data.resolve = await this.handleRequest(messagedata.name, messagedata.data);
+    } catch (e) {
+      data.reject = e;
+    }
+
+    this.messageManager.sendAsyncMessage("RemotePage:Response", data);
+  }
+
+  // Handles an IPC message with the response of a request.
+  receiveResponse({ data: messagedata }) {
+    if (this.destroyed || (messagedata.portID != this.portID)) {
+      return;
+    }
+
+    let deferred = this.requests[messagedata.requestID];
+    if (!deferred) {
+      Cu.reportError("Received a response to an unknown request.");
+      return;
+    }
+
+    delete this.requests[messagedata.requestID];
+
+    if ("resolve" in messagedata) {
+      deferred.resolve(messagedata.resolve);
+    } else if ("reject" in messagedata) {
+      deferred.reject(messagedata.reject);
+    } else {
+      deferred.reject(new Error("Internal RPM error."));
+    }
+  }
+
+  // Handles an IPC message containing any message.
+  message({ data: messagedata }) {
+    if (this.destroyed || (messagedata.portID != this.portID)) {
+      return;
+    }
+
+    this.handleMessage(messagedata);
   }
 
   /* Adds a listener for messages. Many callbacks can be registered for the
    * same message if necessary. An attempt to register the same callback for the
    * same message twice will be ignored. When called the callback is passed an
    * object with these properties:
    *   target: This message port
    *   name:   The message name
@@ -179,22 +270,30 @@ class MessagePort {
       data,
     });
   }
 
   // Called to destroy this port
   destroy() {
     try {
       // This can fail in the child process if the tab has already been closed
-      this.messageManager.removeMessageListener("RemotePage:Message", this.message);
+      this.removeMessageListeners();
     } catch (e) { }
+
+    for (let deferred of this.requests) {
+      if (deferred) {
+        deferred.reject(new Error("Message port has been destroyed"));
+      }
+    }
+
     this.messageManager = null;
     this.destroyed = true;
     this.portID = null;
     this.listener = null;
+    this.requests = [];
   }
 
   getBoolPref(aPref) {
     let principal = this.window.document.nodePrincipal;
     if (!RPMAccessManager.checkAllowAccess(principal, "getBoolPref", aPref)) {
       throw new Error("RPMAccessManager does not allow access to getBoolPref");
     }
     return Services.prefs.getBoolPref(aPref);
--- a/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm
+++ b/toolkit/components/remotepagemanager/RemotePageManagerChild.jsm
@@ -62,23 +62,23 @@ class ChildMessagePort extends MessagePo
 
     // Tell the main process to set up its side of the message pipe.
     this.messageManager.sendAsyncMessage("RemotePage:InitPort", {
       portID,
       url: window.document.documentURI.replace(/[\#|\?].*$/, ""),
     });
   }
 
-  // Called when a message is received from the message manager. This could
-  // have come from any port in the message manager so verify the port ID.
-  message({ data: messagedata }) {
-    if (this.destroyed || (messagedata.portID != this.portID)) {
-      return;
-    }
+  // Called when the content process is requesting some data.
+  async handleRequest(name, data) {
+    throw new Error(`Unknown request ${name}.`);
+  }
 
+  // Called when a message is received from the message manager.
+  handleMessage(messagedata) {
     let message = {
       name: messagedata.name,
       data: messagedata.data,
     };
     this.listener.callListeners(Cu.cloneInto(message, this.window));
   }
 
   destroy() {
--- a/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
+++ b/toolkit/components/remotepagemanager/RemotePageManagerParent.jsm
@@ -229,23 +229,23 @@ class ChromeMessagePort extends MessageP
     this.listener.callListeners({
       target: this.publicPort,
       name: "RemotePage:Unload",
       data: null,
     });
     this.destroy();
   }
 
-  // Called when a message is received from the message manager. This could
-  // have come from any port in the message manager so verify the port ID.
-  message({ data: messagedata }) {
-    if (this.destroyed || (messagedata.portID != this.portID)) {
-      return;
-    }
+  // Called when the content process is requesting some data.
+  async handleRequest(name, data) {
+    throw new Error(`Unknown request ${name}.`);
+  }
 
+  // Called when a message is received from the message manager.
+  handleMessage(messagedata) {
     let message = {
       target: this.publicPort,
       name: messagedata.name,
       data: messagedata.data,
     };
     this.listener.callListeners(message);
 
     if (messagedata.name == "RemotePage:Unload")