Bug 1370368 - Disconnect port upon pagehide draft
authorRob Wu <rob@robwu.nl>
Fri, 18 May 2018 16:34:01 +0200
changeset 798804 f5825ca851630c3950cc219795e235dc6b75a184
parent 798084 b75acf9652937ce79a9bf02de843c100db0e5ec7
child 798805 8fcf0ff2e2220106261c90ee32fa8482542e2ab2
push id110842
push userbmo:rob@robwu.nl
push dateWed, 23 May 2018 13:45:40 +0000
bugs1370368
milestone62.0a1
Bug 1370368 - Disconnect port upon pagehide MozReview-Commit-ID: HeBcejthn6p
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/test/xpcshell/test_ext_port_disconnect_upon_navigation.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -157,16 +157,17 @@ class Port {
 
     this.disconnectHandler = Object.assign({
       receiveMessage: ({data}) => this.disconnectByOtherEnd(data),
     }, this.handlerBase);
 
     MessageChannel.addListener(this.receiverMMs, "Extension:Port:Disconnect", this.disconnectHandler);
 
     this.context.callOnClose(this);
+    this.registerCloseOnPagehide();
   }
 
   api() {
     let portObj = Cu.createObjectIn(this.context.cloneScope);
 
     let portError = null;
     let publicAPI = {
       name: this.name,
@@ -265,16 +266,38 @@ class Port {
       this.unregisterMessageFuncs.delete(unregister);
       MessageChannel.removeListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
     };
     MessageChannel.addListener(this.receiverMMs, "Extension:Port:PostMessage", handler);
     this.unregisterMessageFuncs.add(unregister);
     return unregister;
   }
 
+  registerCloseOnPagehide() {
+    let {contentWindow} = this.context;
+    if (!contentWindow) {
+      return;
+    }
+
+    let onPageHide = (event) => {
+      if (event.persisted) {
+        // If the page is moved to the bfcache, then scripts in the page will be
+        // suspended and the port becomes unreachable from the extension's
+        // perspective. Explicitly disconnect the port when that happens.
+        this.disconnect();
+      }
+    };
+    let unregister = () => {
+      this.unregisterMessageFuncs.delete(unregister);
+      contentWindow.removeEventListener("pagehide", onPageHide, {mozSystemGroup: true});
+    };
+    contentWindow.addEventListener("pagehide", onPageHide, {mozSystemGroup: true, once: true}, false);
+    this.unregisterMessageFuncs.add(unregister);
+  }
+
   _sendMessage(message, data) {
     let options = {
       recipient: Object.assign({}, this.recipient, {portId: this.id}),
       responseType: MessageChannel.RESPONSE_NONE,
     };
 
     let holder = new StructuredCloneHolder(data);
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_port_disconnect_upon_navigation.js
@@ -0,0 +1,100 @@
+"use strict";
+
+const server = createHttpServer({hosts: ["example.com"]});
+
+server.registerPathHandler("/dummy", (request, response) => {
+  response.setStatusLine(request.httpVersion, 200, "OK");
+  response.setHeader("Content-Type", "text/html; charset=utf-8", false);
+  response.write("<!DOCTYPE html><html></html>");
+});
+
+add_task(async function testConnect_and_navigate_window() {
+  function background() {
+    browser.runtime.onConnect.addListener(port => {
+      let messages = [];
+      port.onMessage.addListener(msg => {
+        messages.push(msg);
+      });
+      port.onDisconnect.addListener(() => {
+        browser.test.assertEq(null, port.error, "Disconnect without error");
+        browser.test.assertEq("pageshown,pagehidden", messages.join(","), "expected messages");
+        browser.test.sendMessage("port-disconnected");
+      });
+      browser.test.sendMessage("port-connected");
+    });
+    browser.test.sendMessage("bg-ready");
+  }
+  function contentScript() {
+    let port = browser.runtime.connect();
+    port.onDisconnect.addListener(() => {
+      // The background page has an onConnect listener and never disconnects
+      // the port.  The port is disconnected when the page unloads, but that
+      // shouldn't trigger the port.onDisconnect event.
+      browser.test.fail("port.onDisconnect should not be fired");
+    });
+
+    // eslint-disable-next-line mozilla/balanced-listeners
+    window.addEventListener("pagehide", () => {
+      browser.test.sendMessage("content-script-hide");
+      try {
+        port.postMessage("pagehidden");
+      } catch (e) {
+        browser.test.sendMessage(`pagehide error: ${e.message}`);
+      }
+    });
+    // eslint-disable-next-line mozilla/balanced-listeners
+    window.addEventListener("pageshow", () => {
+      browser.test.sendMessage("content-script-show");
+      try {
+        port.postMessage("pageshown");
+      } catch (e) {
+        browser.test.sendMessage(`pageshow error: ${e.message}`);
+      }
+    });
+    browser.test.sendMessage("content-script-ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      content_scripts: [{
+        "matches": ["http://example.com/dummy"],
+        "js": ["content_script.js"],
+        "run_at": "document_start",
+      }],
+    },
+
+    files: {
+      "content_script.js": contentScript,
+    },
+  });
+
+  await extension.startup();
+
+  await extension.awaitMessage("bg-ready");
+
+  let contentPage = await ExtensionTestUtils.loadContentPage(
+    "http://example.com/dummy");
+  await extension.awaitMessage("content-script-ready");
+  await extension.awaitMessage("port-connected");
+  await extension.awaitMessage("content-script-show");
+  await contentPage.spawn(null, () => {
+    // Navigate so that the content page is hidden in the bfcache.
+    this.content.location = "data:text/plain;charset=utf-8,something-else";
+  });
+  await extension.awaitMessage("content-script-hide");
+  await extension.awaitMessage("port-disconnected");
+
+  await contentPage.spawn(null, () => {
+    // Navigate back so the content page is resurrected from the bfcache.
+    this.content.history.back();
+  });
+
+  await extension.awaitMessage("content-script-show");
+  await extension.awaitMessage("pageshow error: Attempt to postMessage on disconnected port");
+
+  await contentPage.close();
+  await extension.awaitMessage("content-script-hide");
+  await extension.awaitMessage("pagehide error: Attempt to postMessage on disconnected port");
+  await extension.unload();
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -57,16 +57,17 @@ skip-if = os == "android" # checking for
 [test_ext_localStorage.js]
 [test_ext_management.js]
 skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
 [test_ext_management_uninstall_self.js]
 [test_ext_onmessage_removelistener.js]
 skip-if = true # This test no longer tests what it is meant to test.
 [test_ext_permission_xhr.js]
 [test_ext_persistent_events.js]
+[test_ext_port_disconnect_upon_navigation.js]
 [test_ext_privacy.js]
 [test_ext_privacy_disable.js]
 [test_ext_privacy_update.js]
 [test_ext_proxy_auth.js]
 [test_ext_proxy_config.js]
 [test_ext_proxy_onauthrequired.js]
 [test_ext_proxy_settings.js]
 skip-if = os == "android" # proxy settings are not supported on android