# HG changeset patch # User Luca Greco # Date 1519059635 -3600 # Node ID e561c88fc549429f597dd0c3b93cbbcbedfd009f # Parent 6a7b06113e975a64ef38ff1261f3e3b9b2667640 Bug 1435959 - Fix missing network requests in netmonitor panel for oop extensions. r=ochameau MozReview-Commit-ID: F8jzwBveACm diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini --- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -1,9 +1,10 @@ [DEFAULT] tags = webextensions in-process-webextensions +[browser_ext_addon_debugging_netmonitor.js] [browser_ext_autocompletepopup.js] [browser_ext_legacy_extension_context_contentscript.js] [browser_ext_windows_allowScriptsToClose.js] [include:browser-common.ini] [parent:browser-common.ini] diff --git a/browser/components/extensions/test/browser/browser_ext_addon_debugging_netmonitor.js b/browser/components/extensions/test/browser/browser_ext_addon_debugging_netmonitor.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(); +}); diff --git a/devtools/client/framework/ToolboxProcess.jsm b/devtools/client/framework/ToolboxProcess.jsm --- a/devtools/client/framework/ToolboxProcess.jsm +++ b/devtools/client/framework/ToolboxProcess.jsm @@ -342,31 +342,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(); } diff --git a/devtools/server/actors/webconsole.js b/devtools/server/actors/webconsole.js --- 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 diff --git a/devtools/server/actors/webextension.js b/devtools/server/actors/webextension.js --- a/devtools/server/actors/webextension.js +++ b/devtools/server/actors/webextension.js @@ -54,16 +54,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