Bug 1435959 - Fix missing network requests in netmonitor panel for oop extensions. r=ochameau
authorLuca Greco <lgreco@mozilla.com>
Mon, 19 Feb 2018 18:00:35 +0100
changeset 407420 e561c88fc549429f597dd0c3b93cbbcbedfd009f
parent 407419 5e9db9c66ccdbe20d8ebb8c498a4385c4dc54ea0
child 407421 2ea235b4dedb3f3141f1fd845df0c20935361a16
push id60917
push userluca.greco@alcacoop.it
push dateFri, 09 Mar 2018 20:32:48 +0000
treeherderautoland@e561c88fc549 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1435959
milestone60.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 1435959 - Fix missing network requests in netmonitor panel for oop extensions. r=ochameau MozReview-Commit-ID: F8jzwBveACm
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_addon_debugging_netmonitor.js
devtools/client/framework/ToolboxProcess.jsm
devtools/server/actors/webconsole.js
devtools/server/actors/webextension.js
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -33,16 +33,17 @@ support-files =
   serviceWorker.js
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
   ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js
   ../../../../../toolkit/components/extensions/test/mochitest/redirection.sjs
   ../../../../../toolkit/components/reader/test/readerModeNonArticle.html
   ../../../../../toolkit/components/reader/test/readerModeArticle.html
 
+[browser_ext_addon_debugging_netmonitor.js]
 [browser_ext_browserAction_area.js]
 [browser_ext_browserAction_experiment.js]
 [browser_ext_browserAction_context.js]
 skip-if = os == 'win' || os == 'mac' # Bug 1405453
 [browser_ext_browserAction_contextMenu.js]
 # bug 1369197
 skip-if = os == 'linux'
 [browser_ext_browserAction_disabled.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_addon_debugging_netmonitor.js
@@ -0,0 +1,132 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineModuleGetter(this, "BrowserToolboxProcess",
+                               "resource://devtools/client/framework/ToolboxProcess.jsm");
+
+async function setupToolboxProcessTest(toolboxProcessScript) {
+  // Enable addon debugging.
+  await SpecialPowers.pushPrefEnv({
+    "set": [
+      // Force enabling of addons debugging
+      ["devtools.chrome.enabled", true],
+      ["devtools.debugger.remote-enabled", true],
+      // Disable security prompt
+      ["devtools.debugger.prompt-connection", false],
+      // Enable Browser toolbox test script execution via env variable
+      ["devtools.browser-toolbox.allow-unsafe-script", true],
+    ],
+  });
+
+  let env = Cc["@mozilla.org/process/environment;1"]
+              .getService(Ci.nsIEnvironment);
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", `(${toolboxProcessScript})();`);
+  registerCleanupFunction(() => {
+    env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+  });
+}
+
+add_task(async function test_addon_debugging_netmonitor_panel() {
+  const EXTENSION_ID = "test-monitor-panel@mozilla";
+
+  function background() {
+    let expectedURL;
+    window.doFetchHTTPRequest = async function(urlToFetch) {
+      expectedURL = urlToFetch;
+      await fetch(urlToFetch);
+    };
+    window.testNetworkRequestReceived = async function(requests) {
+      browser.test.log("Addon Debugging Netmonitor panel collected requests: " +
+                       JSON.stringify(requests));
+      browser.test.assertEq(1, requests.length, "Got one request logged");
+      browser.test.assertEq("GET", requests[0].method, "Got a GET request");
+      browser.test.assertEq(expectedURL, requests[0].url, "Got the expected request url");
+
+      browser.test.notifyPass("netmonitor_request_logged");
+    };
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    useAddonManager: "temporary",
+    manifest: {
+      permissions: ["http://mochi.test/"],
+      applications: {
+        gecko: {id: EXTENSION_ID},
+      },
+    },
+  });
+
+  await extension.startup();
+  await extension.awaitMessage("ready");
+
+  // Be careful, this JS function is going to be executed in the addon toolbox,
+  // which lives in another process. So do not try to use any scope variable!
+  const toolboxProcessScript = async function() {
+    /* eslint-disable no-undef */
+    async function waitFor(condition) {
+      while (!condition()) {
+        // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+        await new Promise(done => window.setTimeout(done, 1000));
+      }
+    }
+
+    const console = await toolbox.selectTool("webconsole");
+    const {hud} = console;
+    const {jsterm} = hud;
+
+    const netmonitor = await toolbox.selectTool("netmonitor");
+
+    const expectedURL = "http://mochi.test:8888/?test_netmonitor=1";
+
+    // Call a function defined in the target extension to make it
+    // fetch from an expected http url.
+    await jsterm.execute(`doFetchHTTPRequest("${expectedURL}");`);
+
+    await waitFor(() => {
+      return !netmonitor.panelWin.document.querySelector(".request-list-empty-notice");
+    });
+
+    let {store} = netmonitor.panelWin;
+
+    // NOTE: we need to filter the requests to the ones that we expect until
+    // the network monitor is not yet filtering out the requests that are not
+    // coming from an extension window or a descendent of an extension window,
+    // in both oop and non-oop extension mode (filed as Bug 1442621).
+    function filterRequest(request) {
+      return request.url === expectedURL;
+    }
+
+    let requests;
+
+    await waitFor(() => {
+      requests = Array.from(store.getState().requests.requests.values())
+                      .filter(filterRequest);
+
+      return requests.length > 0;
+    });
+
+    // Call a function defined in the target extension to make assertions
+    // on the network requests collected by the netmonitor panel.
+    await jsterm.execute(`testNetworkRequestReceived(${JSON.stringify(requests)});`);
+    /* eslint-enable no-undef */
+  };
+
+  await setupToolboxProcessTest(toolboxProcessScript);
+  const browserToolboxProcess = new BrowserToolboxProcess({
+    addonID: EXTENSION_ID,
+  });
+
+  await extension.awaitFinish("netmonitor_request_logged");
+
+  let onToolboxClose = browserToolboxProcess.once("close");
+  await browserToolboxProcess.close();
+
+  await onToolboxClose;
+
+  info("Addon Toolbox closed");
+
+  await extension.unload();
+});
--- a/devtools/client/framework/ToolboxProcess.jsm
+++ b/devtools/client/framework/ToolboxProcess.jsm
@@ -341,31 +341,33 @@ BrowserToolboxProcess.prototype = {
   /**
    * Closes the remote debugging server and kills the toolbox process.
    */
   close: async function () {
     if (this.closed) {
       return;
     }
 
+    this.closed = true;
+
     dumpn("Cleaning up the chrome debugging process.");
+
     Services.obs.removeObserver(this.close, "quit-application");
 
     this._dbgProcess.stdout.close();
     await this._dbgProcess.kill();
 
     this._telemetry.toolClosed("jsbrowserdebugger");
     if (this.debuggerServer) {
       this.debuggerServer.off("connectionchange", this._onConnectionChange);
       this.debuggerServer.destroy();
       this.debuggerServer = null;
     }
 
     dumpn("Chrome toolbox is now closed...");
-    this.closed = true;
     this.emit("close", this);
     processes.delete(this);
 
     this._dbgProcess = null;
     this._options = null;
     if (this.loader) {
       this.loader.destroy();
     }
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -81,17 +81,16 @@ function WebConsoleActor(connection, par
             this._onChangedToplevelDocument);
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
                              "last-pb-context-exited");
   }
 
   this.traits = {
-    customNetworkRequest: !this._parentIsContentActor,
     evaluateJSAsync: true,
     transferredResponseSize: true,
     selectedObjectActor: true, // 44+
   };
 }
 
 WebConsoleActor.prototype =
 {
@@ -159,26 +158,16 @@ WebConsoleActor.prototype =
 
   /**
    * List of supported features by the console actor.
    * @type object
    */
   traits: null,
 
   /**
-   * Boolean getter that tells if the parent actor is a ContentActor.
-   *
-   * @private
-   * @type boolean
-   */
-  get _parentIsContentActor() {
-    return this.parentActor.constructor.name == "ContentActor";
-  },
-
-  /**
    * The window or sandbox we work with.
    * Note that even if it is named `window` it refers to the current
    * global we are debugging, which can be a Sandbox for addons
    * or browser content toolbox.
    *
    * @type nsIDOMWindow or Sandbox
    */
   get window() {
@@ -582,17 +571,26 @@ WebConsoleActor.prototype =
    * @return object
    *         The response object which holds the startedListeners array.
    */
   onStartListeners: function (request) {
     let startedListeners = [];
     let window = !this.parentActor.isRootActor ? this.window : null;
     let messageManager = null;
 
-    if (this._parentIsContentActor) {
+    // Check if the actor is running in a child process (but only if
+    // Services.appinfo exists, to prevent onStartListeners to fail
+    // when the target is a Worker).
+    let processBoundary = Services.appinfo && (
+      Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
+    );
+
+    // Retrieve a message manager from the parent actor if this actor is
+    // not currently running in the main process.
+    if (processBoundary) {
       messageManager = this.parentActor.messageManager;
     }
 
     while (request.listeners.length > 0) {
       let listener = request.listeners.shift();
       switch (listener) {
         case "PageError":
           // Workers don't support this message type yet
@@ -624,18 +622,16 @@ WebConsoleActor.prototype =
           if (!this.networkMonitor) {
             // Create a StackTraceCollector that's going to be shared both by
             // the NetworkMonitorChild (getting messages about requests from
             // parent) and by the NetworkMonitor that directly watches service
             // workers requests.
             this.stackTraceCollector = new StackTraceCollector({ window });
             this.stackTraceCollector.init();
 
-            let processBoundary = Services.appinfo.processType !=
-                                  Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
             if (messageManager && processBoundary) {
               // Start a network monitor in the parent process to listen to
               // most requests than happen in parent
               this.networkMonitor =
                 new NetworkMonitorChild(this.parentActor.outerWindowID,
                                         messageManager, this.conn, this);
               this.networkMonitor.init();
               // Spawn also one in the child to listen to service workers
--- a/devtools/server/actors/webextension.js
+++ b/devtools/server/actors/webextension.js
@@ -55,16 +55,28 @@ const FALLBACK_DOC_MESSAGE = "Your addon
  */
 function WebExtensionChildActor(conn, chromeGlobal, prefix, addonId) {
   ChromeActor.call(this, conn);
 
   this._chromeGlobal = chromeGlobal;
   this._prefix = prefix;
   this.id = addonId;
 
+  // Redefine the messageManager getter to return the chromeGlobal
+  // as the messageManager for this actor (which is the browser XUL
+  // element used by the parent actor running in the main process to
+  // connect to the extension process).
+  Object.defineProperty(this, "messageManager", {
+    enumerable: true,
+    configurable: true,
+    get: () => {
+      return this._chromeGlobal;
+    }
+  });
+
   // Bind the _allowSource helper to this, it is used in the
   // TabActor to lazily create the TabSources instance.
   this._allowSource = this._allowSource.bind(this);
   this._onParentExit = this._onParentExit.bind(this);
 
   this._chromeGlobal.addMessageListener("debug:webext_parent_exit", this._onParentExit);
 
   // Set the consoleAPIListener filtering options