Bug 1287007 - Track message manager / browser swaps draft
authorRob Wu <rob@robwu.nl>
Fri, 09 Sep 2016 23:04:04 -0700
changeset 428439 206a577e675029584758314c892745dd6b368aec
parent 428438 d71c4f0301c46fdbfa3257e97c03200ef9bfe9e5
child 428440 8797b10128a9f2123c53b9d57000f8e7e80231f7
push id33305
push userbmo:rob@robwu.nl
push dateSun, 23 Oct 2016 20:56:25 +0000
bugs1287007, 1301837
milestone52.0a1
Bug 1287007 - Track message manager / browser swaps Neither the message manager nor the XUL browser is guaranteed to be constant during a ProxyContext's lifetime. Add a new class to follow the `<browser>` belonging to the current docshell and update the ProxyContext properties as needed. NOTE: The `BrowserDocshellFollower` class assumes that docshells are swapped using `newBrowser.swapDocShells(oldBrowser)`. If this assumption turns out to be false, then the tracker will lose track of the `<browser>`. See bugzil.la/1301837 for more details. Also, renamed `messageManager` to `currentMessageManager` because the `messageManager` property is overwritten by the `setContentWindow` hack in WannabeChildAPIManager in ExtensionChild.jsm. browser/components/extensions/test/browser/browser_ext_currentWindow.js provides test coverage for this feature once the `test` API goes through a ChildAPIManager instead of directly through a WannabeChildAPIManager. Why? Because that test calls `test.onMessage.addListener` in the script that is loaded in a popup page. Popups are loaded in two stages: First the content is preloaded in a `<browser>`, and then when the popup is shown a new `<browser>` is created and the docshells are swapped. When the script runs while the popup script is being preloaded, the `ParentAPIManager` receives the IPC message with the target set to the `<browser>` used for preloading. When the API response is ready, `target.messageManager.sendAsyncMessage` is called. Meanwhile the docshells have been swapped, the message manager is gone and this fails. With this patch, the message manager is correctly tracked and this test passes. MozReview-Commit-ID: C5Z0ZJRXKyw
toolkit/components/extensions/Extension.jsm
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -228,23 +228,56 @@ let ProxyMessenger = {
     }
 
     // Note: No special handling for sendNativeMessage / connectNative because
     // native messaging runs in the chrome process, so it never needs a proxy.
     return null;
   },
 };
 
+class BrowserDocshellFollower {
+  /**
+   * Follows the <browser> belonging to the `xulBrowser`'s current docshell.
+   *
+   * @param {XULElement} xulBrowser A <browser> tag.
+   * @param {function} onBrowserChange Called when the <browser> changes.
+   */
+  constructor(xulBrowser, onBrowserChange) {
+    this.xulBrowser = xulBrowser;
+    this.onBrowserChange = onBrowserChange;
+
+    xulBrowser.addEventListener("SwapDocShells", this);
+  }
+
+  destroy() {
+    this.xulBrowser.removeEventListener("SwapDocShells", this);
+    this.xulBrowser = null;
+  }
+
+  handleEvent({detail: otherBrowser}) {
+    this.xulBrowser.removeEventListener("SwapDocShells", this);
+    this.xulBrowser = otherBrowser;
+    this.xulBrowser.addEventListener("SwapDocShells", this);
+    this.onBrowserChange(otherBrowser);
+  }
+}
+
 class ProxyContext extends BaseContext {
-  constructor(envType, extension, params, messageManager, principal) {
+  constructor(envType, extension, params, xulBrowser, principal) {
     super(envType, extension);
 
     this.uri = NetUtil.newURI(params.url);
 
-    this.messageManager = messageManager;
+    // This message manager is used by ParentAPIManager to send messages and to
+    // close the ProxyContext if the underlying message manager closes. This
+    // message manager object may change when `xulBrowser` swaps docshells, e.g.
+    // when a tab is moved to a different window.
+    this.currentMessageManager = xulBrowser.messageManager;
+    this._docShellTracker = new BrowserDocshellFollower(xulBrowser,
+        this.onBrowserChange.bind(this));
     this.principal_ = principal;
 
     this.apiObj = {};
     GlobalManager.injectInObject(this, false, this.apiObj);
 
     this.listenerProxies = new Map();
 
     this.sandbox = Cu.Sandbox(principal, {});
@@ -255,35 +288,49 @@ class ProxyContext extends BaseContext {
   get principal() {
     return this.principal_;
   }
 
   get cloneScope() {
     return this.sandbox;
   }
 
+  onBrowserChange(browser) {
+    // Make sure that the message manager is set. Otherwise the ProxyContext may
+    // never be destroyed because the ParentAPIManager would fail to detect that
+    // the message manager is closed.
+    if (!browser.messageManager) {
+      throw new Error("BrowserDocshellFollower: The new browser has no message manager");
+    }
+
+    this.currentMessageManager = browser.messageManager;
+  }
+
   shutdown() {
     this.unload();
   }
 
   unload() {
     if (this.unloaded) {
       return;
     }
+    this._docShellTracker.destroy();
     super.unload();
     Management.emit("proxy-context-unload", this);
   }
 }
 
 // The parent ProxyContext of an ExtensionContext in ExtensionChild.jsm.
 class ExtensionChildProxyContext extends ProxyContext {
   constructor(envType, extension, params, xulBrowser) {
-    super(envType, extension, params, xulBrowser.messageManager, extension.principal);
+    super(envType, extension, params, xulBrowser, extension.principal);
 
     this.viewType = params.viewType;
+    // WARNING: The xulBrowser may change when docShells are swapped, e.g. when
+    // the tab moves to a different window.
     this.xulBrowser = xulBrowser;
 
     // TODO(robwu): Remove this once all APIs can run in a separate process.
     if (params.cloneScopeInProcess) {
       this.sandbox = params.cloneScopeInProcess;
     }
   }
 
@@ -304,16 +351,21 @@ class ExtensionChildProxyContext extends
     if (!Management.global.TabManager) {
       return;  // Not yet supported on Android.
     }
     let {gBrowser} = this.xulBrowser.ownerGlobal;
     let tab = gBrowser && gBrowser.getTabForBrowser(this.xulBrowser);
     return tab && Management.global.TabManager.getId(tab);
   }
 
+  onBrowserChange(browser) {
+    super.onBrowserChange(browser);
+    this.xulBrowser = browser;
+  }
+
   shutdown() {
     Management.emit("page-shutdown", this);
     super.shutdown();
   }
 }
 
 function findPathInObject(obj, path, printErrors = true) {
   for (let elt of path.split(".")) {
@@ -355,17 +407,17 @@ var ParentAPIManager = {
     Services.mm.addMessageListener("API:AddListener", this);
     Services.mm.addMessageListener("API:RemoveListener", this);
   },
 
   // "message-manager-close" observer.
   observe(subject, topic, data) {
     let mm = subject;
     for (let [childId, context] of this.proxyContexts) {
-      if (context.messageManager == mm) {
+      if (context.currentMessageManager == mm) {
         this.closeProxyContext(childId);
       }
     }
   },
 
   shutdownExtension(extensionId) {
     for (let [childId, context] of this.proxyContexts) {
       if (context.extension.id == extensionId) {
@@ -417,17 +469,17 @@ var ParentAPIManager = {
       // frame is also the same addon.
       if (principal.URI.prePath != extension.baseURI.prePath ||
           !target.contentPrincipal.subsumes(principal)) {
         Cu.reportError(`Refused to create privileged WebExtension context for ${principal.URI.spec}`);
         return;
       }
       context = new ExtensionChildProxyContext(envType, extension, data, target);
     } else if (envType == "content_parent") {
-      context = new ProxyContext(envType, extension, data, target.messageManager, principal);
+      context = new ProxyContext(envType, extension, data, target, principal);
     } else {
       Cu.reportError(`Invalid WebExtension context envType: ${envType}`);
       return;
     }
     this.proxyContexts.set(childId, context);
   },
 
   closeProxyContext(childId) {
@@ -440,53 +492,60 @@ var ParentAPIManager = {
   },
 
   call(data, target) {
     let context = this.proxyContexts.get(data.childId);
     if (!context) {
       Cu.reportError("WebExtension context not found!");
       return;
     }
+    if (context.currentMessageManager !== target.messageManager) {
+      Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
+    }
+
     function callback(...cbArgs) {
       let lastError = context.lastError;
 
-      target.messageManager.sendAsyncMessage("API:CallResult", {
+      context.currentMessageManager.sendAsyncMessage("API:CallResult", {
         childId: data.childId,
         callId: data.callId,
         args: cbArgs,
         lastError: lastError ? lastError.message : null,
       });
     }
 
     let args = data.args;
     args = Cu.cloneInto(args, context.sandbox);
     if (data.callId) {
       args = args.concat(callback);
     }
     try {
       findPathInObject(context.apiObj, data.path)(...args);
     } catch (e) {
       let msg = e.message || "API failed";
-      target.messageManager.sendAsyncMessage("API:CallResult", {
+      context.currentMessageManager.sendAsyncMessage("API:CallResult", {
         childId: data.childId,
         callId: data.callId,
         lastError: msg,
       });
     }
   },
 
   addListener(data, target) {
     let context = this.proxyContexts.get(data.childId);
     if (!context) {
       Cu.reportError("WebExtension context not found!");
       return;
     }
+    if (context.currentMessageManager !== target.messageManager) {
+      Cu.reportError("WebExtension warning: Message manager unexpectedly changed");
+    }
 
     function listener(...listenerArgs) {
-      target.messageManager.sendAsyncMessage("API:RunListener", {
+      context.currentMessageManager.sendAsyncMessage("API:RunListener", {
         childId: data.childId,
         path: data.path,
         args: listenerArgs,
       });
     }
 
     context.listenerProxies.set(data.path, listener);