Bug 1240912 - Toolbox remains connected across frame swaps. r=ochameau draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Tue, 28 Jun 2016 20:10:55 -0500
changeset 402163 193b55a8beda265514bde4f4530e8f4daf13fc2f
parent 402162 7f4043a6226d5ba95b51b96ae487297fe446a699
child 402164 a026299204727d4ed9a4097f695852f468242f12
push id26614
push userbmo:jryans@gmail.com
push dateThu, 18 Aug 2016 01:07:09 +0000
reviewersochameau
bugs1240912
milestone51.0a1
Bug 1240912 - Toolbox remains connected across frame swaps. r=ochameau It is now possible keep the toolbox open while toggling RDM open and closed. The toolbox will follow the frame as it moves and update the message manager it uses internally with each move. MozReview-Commit-ID: DvLzCmOXfnb
devtools/client/framework/toolbox-init.js
devtools/client/responsive.html/browser/swap.js
devtools/client/responsive.html/test/browser/browser.ini
devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js
devtools/server/main.js
devtools/shared/transport/transport.js
--- a/devtools/client/framework/toolbox-init.js
+++ b/devtools/client/framework/toolbox-init.js
@@ -69,16 +69,16 @@ if (url.search.length > 1) {
     let toolbox = yield gDevTools.showToolbox(target, tool, Toolbox.HostType.CUSTOM, options);
 
     // Watch for toolbox.xul unload in order to cleanup things when we close
     // about:devtools-toolbox tabs
     function onUnload() {
       window.removeEventListener("unload", onUnload);
       toolbox.destroy();
     }
-    window.addEventListener("unload", onUnload, true);
+    window.addEventListener("unload", onUnload);
     toolbox.on("destroy", function () {
       window.removeEventListener("unload", onUnload);
     });
   }).catch(error => {
     console.error("Exception while loading the toolbox", error);
   });
 }
--- a/devtools/client/responsive.html/browser/swap.js
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -30,16 +30,30 @@ const { tunnelToInnerBrowser } = require
  *        container page.  It is called with the outer browser that loaded the
  *        container page.
  */
 function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
   let gBrowser = tab.ownerDocument.defaultView.gBrowser;
   let innerBrowser;
   let tunnel;
 
+  // Dispatch a custom event each time the _viewport content_ is swapped from one browser
+  // to another.  DevTools server code uses this to follow the content if there is an
+  // active DevTools connection.  While browser.xml does dispatch it's own SwapDocShells
+  // event, this one is easier for DevTools to follow because it's only emitted once per
+  // transition, instead of twice like SwapDocShells.
+  let dispatchDevToolsBrowserSwap = (from, to) => {
+    let CustomEvent = tab.ownerDocument.defaultView.CustomEvent;
+    let event = new CustomEvent("DevTools:BrowserSwap", {
+      detail: to,
+      bubbles: true,
+    });
+    from.dispatchEvent(event);
+  };
+
   return {
 
     start: Task.async(function* () {
       // Freeze navigation temporarily to avoid "blinking" in the location bar.
       freezeNavigationState(tab);
 
       // 1. Create a temporary, hidden tab to load the tool UI.
       let containerTab = gBrowser.addTab(containerURL, {
@@ -67,16 +81,17 @@ function swapToInnerBrowser({ tab, conta
       if (innerBrowser.isRemoteBrowser != tab.linkedBrowser.isRemoteBrowser) {
         throw new Error("The inner browser's remoteness must match the " +
                         "original tab.");
       }
 
       // 4. Swap tab content from the regular browser tab to the browser within
       //    the viewport in the tool UI, preserving all state via
       //    `gBrowser._swapBrowserDocShells`.
+      dispatchDevToolsBrowserSwap(tab.linkedBrowser, innerBrowser);
       gBrowser._swapBrowserDocShells(tab, innerBrowser);
 
       // 5. Force the original browser tab to be non-remote since the tool UI
       //    must be loaded in the parent process, and we're about to swap the
       //    tool UI into this tab.
       gBrowser.updateBrowserRemoteness(tab.linkedBrowser, false);
 
       // 6. Swap the tool UI (with viewport showing the content) into the
@@ -110,27 +125,29 @@ function swapToInnerBrowser({ tab, conta
 
       // 3. Mark the content tab browser's docshell as active so the frame
       //    is created eagerly and will be ready to swap.
       contentBrowser.docShellIsActive = true;
 
       // 4. Swap tab content from the browser within the viewport in the tool UI
       //    to the regular browser tab, preserving all state via
       //    `gBrowser._swapBrowserDocShells`.
+      dispatchDevToolsBrowserSwap(innerBrowser, contentBrowser);
       gBrowser._swapBrowserDocShells(contentTab, innerBrowser);
       innerBrowser = null;
 
       // 5. Force the original browser tab to be remote since web content is
       //    loaded in the child process, and we're about to swap the content
       //    into this tab.
       gBrowser.updateBrowserRemoteness(tab.linkedBrowser, true);
 
       // 6. Swap the content into the original browser tab and close the
       //    temporary tab used to hold the content via
       //    `swapBrowsersAndCloseOther`.
+      dispatchDevToolsBrowserSwap(contentBrowser, tab.linkedBrowser);
       gBrowser.swapBrowsersAndCloseOther(tab, contentTab);
       gBrowser = null;
     },
 
   };
 }
 
 /**
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -21,15 +21,15 @@ support-files =
 [browser_exit_button.js]
 [browser_frame_script_active.js]
 [browser_menu_item_01.js]
 [browser_menu_item_02.js]
 [browser_mouse_resize.js]
 [browser_navigation.js]
 [browser_page_state.js]
 [browser_resize_cmd.js]
-skip-if = true # GCLI target confused after swap, will fix in bug 1240912
 [browser_screenshot_button.js]
 [browser_shutdown_close_sync.js]
 [browser_toolbox_computed_view.js]
 [browser_toolbox_rule_view.js]
+[browser_toolbox_swap_browsers.js]
 [browser_touch_simulation.js]
 [browser_viewport_basics.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_toolbox_swap_browsers.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify that toolbox remains open when opening and closing RDM.
+
+const TEST_URL = "http://example.com/";
+
+function getServerConnectionCount(browser) {
+  ok(browser.isRemoteBrowser, "Content browser is remote");
+  return ContentTask.spawn(browser, {}, function* () {
+    const Cu = Components.utils;
+    const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+    const { DebuggerServer } = require("devtools/server/main");
+    return Object.getOwnPropertyNames(DebuggerServer._connections);
+  });
+}
+
+let checkToolbox = Task.async(function* (tab, browser, location) {
+  let conns = yield getServerConnectionCount(browser);
+  is(conns.length, 2, "Server connection for each tab exists");
+  let target = TargetFactory.forTab(tab);
+  ok(!!gDevTools.getToolbox(target), `Toolbox exists ${location}`);
+});
+
+add_task(function* () {
+  let tab = yield addTab(TEST_URL);
+
+  // Open toolbox outside RDM
+  {
+    let { toolbox } = yield openInspector();
+    yield checkToolbox(tab, tab.linkedBrowser, "outside RDM");
+    let { ui } = yield openRDM(tab);
+    yield checkToolbox(tab, ui.getViewportBrowser(), "after opening RDM");
+    yield closeRDM(tab);
+    yield checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+    yield toolbox.destroy();
+  }
+
+  // Open toolbox inside RDM
+  {
+    let { ui } = yield openRDM(tab);
+    let { toolbox } = yield openInspector();
+    yield checkToolbox(tab, ui.getViewportBrowser(), "inside RDM");
+    yield closeRDM(tab);
+    yield checkToolbox(tab, tab.linkedBrowser, "after closing RDM");
+    yield toolbox.destroy();
+  }
+
+  yield removeTab(tab);
+});
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -980,17 +980,34 @@ var DebuggerServer = {
    */
   connectToChild(connection, frame, onDestroy) {
     let deferred = SyncPromise.defer();
 
     // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
     // or else fallback to asking the frameLoader itself.
     let mm = frame.messageManager || frame.frameLoader.messageManager;
     mm.loadFrameScript("resource://devtools/server/child.js", false);
-    this._childMessageManagers.add(mm);
+
+    let trackMessageManager = () => {
+      frame.addEventListener("DevTools:BrowserSwap", onBrowserSwap);
+      mm.addMessageListener("debug:setup-in-parent", onSetupInParent);
+      if (!actor) {
+        mm.addMessageListener("debug:actor", onActorCreated);
+      }
+      DebuggerServer._childMessageManagers.add(mm);
+    };
+
+    let untrackMessageManager = () => {
+      frame.removeEventListener("DevTools:BrowserSwap", onBrowserSwap);
+      mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
+      if (!actor) {
+        mm.removeMessageListener("debug:actor", onActorCreated);
+      }
+      DebuggerServer._childMessageManagers.delete(mm);
+    };
 
     let actor, childTransport;
     let prefix = connection.allocID("child");
     let netMonitor = null;
 
     // provides hook to actor modules that need to exchange messages
     // between e10s parent and child processes
     let onSetupInParent = function (msg) {
@@ -1019,17 +1036,16 @@ var DebuggerServer = {
         let errorMessage =
           "Exception during actor module setup running in the parent process: ";
         DevToolsUtils.reportException(errorMessage + e);
         dumpn(`ERROR: ${errorMessage}\n\t module: '${module}'\n\t ` +
               `setupParent: '${setupParent}'\n${DevToolsUtils.safeErrorString(e)}`);
         return false;
       }
     };
-    mm.addMessageListener("debug:setup-in-parent", onSetupInParent);
 
     let onActorCreated = DevToolsUtils.makeInfallible(function (msg) {
       if (msg.json.prefix != prefix) {
         return;
       }
       mm.removeMessageListener("debug:actor", onActorCreated);
 
       // Pipe Debugger message from/to parent/child via the message manager
@@ -1044,21 +1060,35 @@ var DebuggerServer = {
 
       dumpn("establishing forwarding for app with prefix " + prefix);
 
       actor = msg.json.actor;
 
       let { NetworkMonitorManager } = require("devtools/shared/webconsole/network-monitor");
       netMonitor = new NetworkMonitorManager(frame, actor.actor);
 
-      events.emit(DebuggerServer, "new-child-process", { mm });
-
       deferred.resolve(actor);
     }).bind(this);
-    mm.addMessageListener("debug:actor", onActorCreated);
+
+    // Listen for browser frame swap
+    let onBrowserSwap = ({ detail: newFrame }) => {
+      // Remove listeners from old frame and mm
+      untrackMessageManager();
+      // Update frame and mm to point to the new browser frame
+      frame = newFrame;
+      // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
+      // or else fallback to asking the frameLoader itself.
+      mm = frame.messageManager || frame.frameLoader.messageManager;
+      // Add listeners to new frame and mm
+      trackMessageManager();
+
+      if (childTransport) {
+        childTransport.swapBrowser(mm);
+      }
+    };
 
     let destroy = DevToolsUtils.makeInfallible(function () {
       // provides hook to actor modules that need to exchange messages
       // between e10s parent and child processes
       DebuggerServer.emit("disconnected-from-child:" + prefix, { mm, prefix });
 
       if (childTransport) {
         // If we have a child transport, the actor has already
@@ -1094,25 +1124,23 @@ var DebuggerServer = {
         netMonitor = null;
       }
 
       if (onDestroy) {
         onDestroy(mm);
       }
 
       // Cleanup all listeners
+      untrackMessageManager();
       Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
-      mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
-      if (!actor) {
-        mm.removeMessageListener("debug:actor", onActorCreated);
-      }
       events.off(connection, "closed", destroy);
+    });
 
-      DebuggerServer._childMessageManagers.delete(mm);
-    });
+    // Listen for various messages and frame events
+    trackMessageManager();
 
     // Listen for app process exit
     let onMessageManagerClose = function (subject, topic, data) {
       if (subject == mm) {
         destroy();
       }
     };
     Services.obs.addObserver(onMessageManagerClose,
--- a/devtools/shared/transport/transport.js
+++ b/devtools/shared/transport/transport.js
@@ -694,87 +694,101 @@
         }
       }
     }
   };
 
   exports.LocalDebuggerTransport = LocalDebuggerTransport;
 
   /**
-   * A transport for the debugging protocol that uses nsIMessageSenders to
+   * A transport for the debugging protocol that uses nsIMessageManagers to
    * exchange packets with servers running in child processes.
    *
-   * In the parent process, |sender| should be the nsIMessageSender for the
-   * child process. In a child process, |sender| should be the child process
+   * In the parent process, |mm| should be the nsIMessageSender for the
+   * child process. In a child process, |mm| should be the child process
    * message manager, which sends packets to the parent.
    *
    * |prefix| is a string included in the message names, to distinguish
    * multiple servers running in the same child process.
    *
    * This transport exchanges messages named 'debug:<prefix>:packet', where
    * <prefix> is |prefix|, whose data is the protocol packet.
    */
-  function ChildDebuggerTransport(sender, prefix) {
+  function ChildDebuggerTransport(mm, prefix) {
     EventEmitter.decorate(this);
 
-    this._sender = sender;
+    this._mm = mm;
     this._messageName = "debug:" + prefix + ":packet";
   }
 
   /*
    * To avoid confusion, we use 'message' to mean something that
    * nsIMessageSender conveys, and 'packet' to mean a remote debugging
    * protocol packet.
    */
   ChildDebuggerTransport.prototype = {
     constructor: ChildDebuggerTransport,
 
     hooks: null,
 
-    ready: function () {
-      this._sender.addMessageListener(this._messageName, this);
+    _addListener() {
+      this._mm.addMessageListener(this._messageName, this);
     },
 
-    close: function () {
+    _removeListener() {
       try {
-        this._sender.removeMessageListener(this._messageName, this);
+        this._mm.removeMessageListener(this._messageName, this);
       } catch (e) {
         if (e.result != Cr.NS_ERROR_NULL_POINTER) {
           throw e;
         }
         // In some cases, especially when using messageManagers in non-e10s mode, we reach
         // this point with a dead messageManager which only throws errors but does not
         // seem to indicate in any other way that it is dead.
       }
+    },
+
+    ready: function () {
+      this._addListener();
+    },
+
+    close: function () {
+      this._removeListener();
       this.emit("onClosed");
       this.hooks.onClosed();
     },
 
     receiveMessage: function ({data}) {
       this.emit("onPacket", data);
       this.hooks.onPacket(data);
     },
 
     send: function (packet) {
       this.emit("send", packet);
       try {
-        this._sender.sendAsyncMessage(this._messageName, packet);
+        this._mm.sendAsyncMessage(this._messageName, packet);
       } catch (e) {
         if (e.result != Cr.NS_ERROR_NULL_POINTER) {
           throw e;
         }
         // In some cases, especially when using messageManagers in non-e10s mode, we reach
         // this point with a dead messageManager which only throws errors but does not
         // seem to indicate in any other way that it is dead.
       }
     },
 
     startBulkSend: function () {
       throw new Error("Can't send bulk data to child processes.");
-    }
+    },
+
+    swapBrowser(mm) {
+      this._removeListener();
+      this._mm = mm;
+      this._addListener();
+    },
   };
 
   exports.ChildDebuggerTransport = ChildDebuggerTransport;
 
   // WorkerDebuggerTransport is defined differently depending on whether we are
   // on the main thread or a worker thread. In the former case, we are required
   // by the devtools loader, and isWorker will be false. Otherwise, we are
   // required by the worker loader, and isWorker will be true.