☠☠ backed out by 293882a12e97 ☠ ☠ | |
author | Luca Greco <lgreco@mozilla.com> |
Tue, 21 Mar 2017 15:55:35 +0100 | |
changeset 358786 | 9b16857ca48d5057aba8240b41546e8592e990bb |
parent 358785 | d4ebca627094a76823a037acec723b921bd65952 |
child 358787 | 9859873385bc5722792131a3dcb9caced44f0261 |
push id | 42849 |
push user | ryanvm@gmail.com |
push date | Wed, 17 May 2017 17:09:54 +0000 |
treeherder | autoland@2255bb2a4215 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | ochameau |
bugs | 1302702 |
milestone | 55.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
|
--- a/devtools/client/framework/connect/connect.js +++ b/devtools/client/framework/connect/connect.js @@ -159,18 +159,19 @@ var onConnectionReady = Task.async(funct } }); /** * Build one button for an add-on actor. */ function buildAddonLink(addon, parent) { let a = document.createElement("a"); - a.onclick = function () { - openToolbox(addon, true, "jsdebugger", false); + a.onclick = async function () { + const isTabActor = addon.isWebExtension; + openToolbox(addon, true, "webconsole", isTabActor); }; a.textContent = addon.name; a.title = addon.id; a.href = "#"; parent.appendChild(a); }
--- a/devtools/client/framework/target.js +++ b/devtools/client/framework/target.js @@ -346,41 +346,41 @@ TabTarget.prototype = { return this._url; }, get isRemote() { return !this.isLocalTab; }, get isAddon() { - return !!(this._form && this._form.actor && ( - this._form.actor.match(/conn\d+\.addon\d+/) || - this._form.actor.match(/conn\d+\.webExtension\d+/) - )); + return !!(this._form && this._form.actor && + this._form.actor.match(/conn\d+\.addon\d+/)) || this.isWebExtension; }, get isWebExtension() { - return !!(this._form && this._form.actor && - this._form.actor.match(/conn\d+\.webExtension\d+/)); + return !!(this._form && this._form.actor && ( + this._form.actor.match(/conn\d+\.webExtension\d+/) || + this._form.actor.match(/child\d+\/webExtension\d+/) + )); }, get isLocalTab() { return !!this._tab; }, get isMultiProcess() { return !this.window; }, /** * Adds remote protocol capabilities to the target, so that it can be used * for tools that support the Remote Debugging Protocol even for local * connections. */ - makeRemote: function () { + makeRemote: async function () { if (this._remote) { return this._remote.promise; } this._remote = defer(); if (this.isLocalTab) { // Since a remote protocol connection will be made, let's start the @@ -393,16 +393,32 @@ TabTarget.prototype = { // directly with actors living in the child process. // We also need browser actors for actor registry which enabled addons // to register custom actors. DebuggerServer.registerActors({ root: true, browser: true, tab: false }); this._client = new DebuggerClient(DebuggerServer.connectPipe()); // A local TabTarget will never perform chrome debugging. this._chrome = false; + } else if (this._form.isWebExtension && + this.client.mainRoot.traits.webExtensionAddonConnect) { + // The addonActor form is related to a WebExtensionParentActor instance, + // which isn't a tab actor on its own, it is an actor living in the parent process + // with access to the addon metadata, it can control the addon (e.g. reloading it) + // and listen to the AddonManager events related to the lifecycle of the addon + // (e.g. when the addon is disabled or uninstalled ). + // To retrieve the TabActor instance, we call its "connect" method, + // (which fetches the TabActor form from a WebExtensionChildActor instance). + let {form} = await this._client.request({ + to: this._form.actor, type: "connect", + }); + + this._form = form; + this._url = form.url; + this._title = form.title; } this._setupRemoteListeners(); let attachTab = () => { this._client.attachTab(this._form.actor, (response, tabClient) => { if (!tabClient) { this._remote.reject("Unable to attach to the tab");
--- a/devtools/client/framework/toolbox-process-window.js +++ b/devtools/client/framework/toolbox-process-window.js @@ -41,27 +41,21 @@ var connect = Task.async(function*() { }); gClient = new DebuggerClient(transport); yield gClient.connect(); let addonID = getParameterByName("addonID"); if (addonID) { let { addons } = yield gClient.listAddons(); let addonActor = addons.filter(addon => addon.id === addonID).pop(); - openToolbox({ - form: addonActor, - chrome: true, - isTabActor: addonActor.isWebExtension ? true : false - }); + let isTabActor = addonActor.isWebExtension; + openToolbox({form: addonActor, chrome: true, isTabActor}); } else { let response = yield gClient.getProcess(); - openToolbox({ - form: response.form, - chrome: true - }); + openToolbox({form: response.form, chrome: true}); } }); // Certain options should be toggled since we can assume chrome debugging here function setPrefDefaults() { Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true); Services.prefs.setBoolPref("devtools.performance.ui.show-platform-data", true); Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
--- a/devtools/server/actors/moz.build +++ b/devtools/server/actors/moz.build @@ -57,16 +57,17 @@ DevToolsModules( 'styles.js', 'stylesheets.js', 'tab.js', 'timeline.js', 'webaudio.js', 'webbrowser.js', 'webconsole.js', 'webextension-inspected-window.js', + 'webextension-parent.js', 'webextension.js', 'webgl.js', 'window.js', 'worker-list.js', 'worker.js', ) with Files('animation.js'):
--- a/devtools/server/actors/root.js +++ b/devtools/server/actors/root.js @@ -185,17 +185,20 @@ RootActor.prototype = { // Whether or not `getProfile()` supports specifying a `startTime` // and `endTime` to filter out samples. Fx40+ profilerDataFilterable: true, // Whether or not the MemoryActor's heap snapshot abilities are // fully equipped to handle heap snapshots for the memory tool. Fx44+ heapSnapshots: true, // Whether or not the timeline actor can emit DOMContentLoaded and Load // markers, currently in use by the network monitor. Fx45+ - documentLoadingMarkers: true + documentLoadingMarkers: true, + // Whether or not the webextension addon actor have to be connected + // to retrieve the extension child process tab actors. + webExtensionAddonConnect: true, }, /** * Return a 'hello' packet as specified by the Remote Debugging Protocol. */ sayHello: function () { return { from: this.actorID,
--- a/devtools/server/actors/tab.js +++ b/devtools/server/actors/tab.js @@ -592,16 +592,22 @@ TabActor.prototype = { // We watch for all child docshells under the current document, this._progressListener.watch(this.docShell); // And list all already existing ones. this._updateChildDocShells(); }, + _unwatchDocShell(docShell) { + if (this._progressListener) { + this._progressListener.unwatch(docShell); + } + }, + onSwitchToFrame(request) { let windowId = request.windowId; let win; try { win = Services.wm.getOuterWindowWithId(windowId); } catch (e) { // ignore @@ -695,61 +701,93 @@ TabActor.prototype = { if (this._isRootDocShell(docShell)) { this._progressListener.watch(docShell); } this._notifyDocShellsUpdate([docShell]); }); }, _onDocShellDestroy(docShell) { + // Stop watching this docshell (the unwatch() method will check if we + // started watching it before). + this._unwatchDocShell(docShell); + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebProgress); this._notifyDocShellDestroy(webProgress); + + if (webProgress.DOMWindow == this._originalWindow) { + // If the original top level document we connected to is removed, + // we try to switch to any other top level document + let rootDocShells = this.docShells + .filter(d => { + return d != this.docShell && + this._isRootDocShell(d); + }); + if (rootDocShells.length > 0) { + let newRoot = rootDocShells[0]; + this._originalWindow = newRoot.DOMWindow; + this._changeTopLevelDocument(this._originalWindow); + } else { + // If for some reason (typically during Firefox shutdown), the original + // document is destroyed, and there is no other top level docshell, + // we detach the tab actor to unregister all listeners and prevent any + // exception + this.exit(); + } + return; + } + + // If the currently targeted context is destroyed, + // and we aren't on the top-level document, + // we have to switch to the top-level one. + if (webProgress.DOMWindow == this.window && + this.window != this._originalWindow) { + this._changeTopLevelDocument(this._originalWindow); + } }, _isRootDocShell(docShell) { // Should report as root docshell: // - New top level window's docshells, when using ChromeActor against a // process. It allows tracking iframes of the newly opened windows // like Browser console or new browser windows. // - MozActivities or window.open frames on B2G, where a new root docshell // is spawn in the child process of the app. return !docShell.parent; }, + _docShellToWindow(docShell) { + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + let window = webProgress.DOMWindow; + let id = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + let parentID = undefined; + // Ignore the parent of the original document on non-e10s firefox, + // as we get the xul window as parent and don't care about it. + if (window.parent && window != this._originalWindow) { + parentID = window.parent + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + } + + return { + id, + parentID, + url: window.location.href, + title: window.document.title, + }; + }, + // Convert docShell list to windows objects list being sent to the client _docShellsToWindows(docshells) { - return docshells.map(docShell => { - let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); - let window = webProgress.DOMWindow; - let id = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .outerWindowID; - let parentID = undefined; - // Ignore the parent of the original document on non-e10s firefox, - // as we get the xul window as parent and don't care about it. - if (window.parent && window != this._originalWindow) { - parentID = window.parent - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .outerWindowID; - } - - // Collect the addonID from the document origin attributes. - let addonID = window.document.nodePrincipal.addonId; - - return { - id, - parentID, - addonID, - url: window.location.href, - title: window.document.title, - }; - }); + return docshells.map(docShell => this._docShellToWindow(docShell)); }, _notifyDocShellsUpdate(docshells) { let windows = this._docShellsToWindows(docshells); // Do not send the `frameUpdate` event if the windows array is empty. if (windows.length == 0) { return; @@ -775,51 +813,16 @@ TabActor.prototype = { this.conn.send({ from: this.actorID, type: "frameUpdate", frames: [{ id, destroy: true }] }); - - // Stop watching this docshell (the unwatch() method will check if we - // started watching it before). - webProgress.QueryInterface(Ci.nsIDocShell); - this._progressListener.unwatch(webProgress); - - if (webProgress.DOMWindow == this._originalWindow) { - // If the original top level document we connected to is removed, - // we try to switch to any other top level document - let rootDocShells = this.docShells - .filter(d => { - return d != this.docShell && - this._isRootDocShell(d); - }); - if (rootDocShells.length > 0) { - let newRoot = rootDocShells[0]; - this._originalWindow = newRoot.DOMWindow; - this._changeTopLevelDocument(this._originalWindow); - } else { - // If for some reason (typically during Firefox shutdown), the original - // document is destroyed, and there is no other top level docshell, - // we detach the tab actor to unregister all listeners and prevent any - // exception - this.exit(); - } - return; - } - - // If the currently targeted context is destroyed, - // and we aren't on the top-level document, - // we have to switch to the top-level one. - if (webProgress.DOMWindow == this.window && - this.window != this._originalWindow) { - this._changeTopLevelDocument(this._originalWindow); - } }, _notifyDocShellDestroyAll() { this.conn.send({ from: this.actorID, type: "frameUpdate", destroyAll: true }); @@ -861,17 +864,17 @@ TabActor.prototype = { _detach() { if (!this.attached) { return false; } // Check for docShell availability, as it can be already gone // during Firefox shutdown. if (this.docShell) { - this._progressListener.unwatch(this.docShell); + this._unwatchDocShell(this.docShell); this._restoreDocumentSettings(); } if (this._progressListener) { this._progressListener.destroy(); this._progressListener = null; this._originalWindow = null; // Removes the observers being set in _watchDocShells
--- a/devtools/server/actors/webbrowser.js +++ b/devtools/server/actors/webbrowser.js @@ -9,17 +9,17 @@ var { Ci } = require("chrome"); var Services = require("Services"); var promise = require("promise"); var { DebuggerServer } = require("devtools/server/main"); var DevToolsUtils = require("devtools/shared/DevToolsUtils"); loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true); loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true); -loader.lazyRequireGetter(this, "WebExtensionActor", "devtools/server/actors/webextension", true); +loader.lazyRequireGetter(this, "WebExtensionParentActor", "devtools/server/actors/webextension-parent", true); loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker-list", true); loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "devtools/server/actors/worker-list", true); loader.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true); loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); /** * Browser-specific actors. */ @@ -830,26 +830,28 @@ function BrowserAddonList(connection) { BrowserAddonList.prototype.getList = function () { let deferred = promise.defer(); AddonManager.getAllAddons((addons) => { for (let addon of addons) { let actor = this._actorByAddonId.get(addon.id); if (!actor) { if (addon.isWebExtension) { - actor = new WebExtensionActor(this._connection, addon); + actor = new WebExtensionParentActor(this._connection, addon); } else { actor = new BrowserAddonActor(this._connection, addon); } this._actorByAddonId.set(addon.id, actor); } } + deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor)); }); + return deferred.promise; }; Object.defineProperty(BrowserAddonList.prototype, "onListChanged", { enumerable: true, configurable: true, get() { return this._onListChanged;
new file mode 100644 --- /dev/null +++ b/devtools/server/actors/webextension-parent.js @@ -0,0 +1,210 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {DebuggerServer} = require("devtools/server/main"); +const protocol = require("devtools/shared/protocol"); +const {webExtensionSpec} = require("devtools/shared/specs/webextension-parent"); + +loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); +loader.lazyImporter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm"); + +/** + * Creates the actor that represents the addon in the parent process, which connects + * itself to a WebExtensionChildActor counterpart which is created in the + * extension process (or in the main process if the WebExtensions OOP mode is disabled). + * + * The WebExtensionParentActor subscribes itself as an AddonListener on the AddonManager + * and forwards this events to child actor (e.g. on addon reload or when the addon is + * uninstalled completely) and connects to the child extension process using a `browser` + * element provided by the extension internals (it is not related to any single extension, + * but it will be created automatically to the currently selected "WebExtensions OOP mode" + * and it persist across the extension reloads (it is destroyed once the actor exits). + * WebExtensionActor is a child of RootActor, it can be retrieved via + * RootActor.listAddons request. + * + * @param {DebuggerServerConnection} conn + * The connection to the client. + * @param {AddonWrapper} addon + * The target addon. + */ +const WebExtensionParentActor = protocol.ActorClassWithSpec(webExtensionSpec, { + initialize(conn, addon) { + this.conn = conn; + this.addon = addon; + this.id = addon.id; + this._childFormPromise = null; + + AddonManager.addAddonListener(this); + }, + + destroy() { + AddonManager.removeAddonListener(this); + + this.addon = null; + this._childFormPromise = null; + + if (this._destroyProxyChildActor) { + this._destroyProxyChildActor(); + delete this._destroyProxyChildActor; + } + }, + + setOptions() { + // NOTE: not used anymore for webextensions, still used in the legacy addons, + // addon manager is currently going to call it automatically on every addon. + }, + + reload() { + return this.addon.reload().then(() => { + return {}; + }); + }, + + form() { + return { + actor: this.actorID, + id: this.id, + name: this.addon.name, + iconURL: this.addon.iconURL, + debuggable: this.addon.isDebuggable, + temporarilyInstalled: this.addon.temporarilyInstalled, + isWebExtension: true, + }; + }, + + connect() { + if (this._childFormPormise) { + return this._childFormPromise; + } + + let proxy = new ProxyChildActor(this.conn, this); + this._childFormPromise = proxy.connect().then(form => { + // Merge into the child actor form, some addon metadata + // (e.g. the addon name shown in the addon debugger window title). + return Object.assign(form, { + id: this.addon.id, + name: this.addon.name, + iconURL: this.addon.iconURL, + // Set the isOOP attribute on the connected child actor form. + isOOP: proxy.isOOP, + }); + }); + this._destroyProxyChildActor = () => proxy.destroy(); + + return this._childFormPromise; + }, + + // ProxyChildActor callbacks. + + onProxyChildActorDestroy() { + // Invalidate the cached child actor and form Promise + // if the child actor exits. + this._childFormPromise = null; + delete this._destroyProxyChildActor; + }, + + // AddonManagerListener callbacks. + + onInstalled(addon) { + if (addon.id != this.id) { + return; + } + + // Update the AddonManager's addon object on reload/update. + this.addon = addon; + }, + + onUninstalled(addon) { + if (addon != this.addon) { + return; + } + + this.destroy(); + }, +}); + +exports.WebExtensionParentActor = WebExtensionParentActor; + +function ProxyChildActor(connection, parentActor) { + this._conn = connection; + this._parentActor = parentActor; + this.addonId = parentActor.id; + + this._onChildExit = this._onChildExit.bind(this); + + this._form = null; + this._browser = null; + this._childActorID = null; +} + +ProxyChildActor.prototype = { + /** + * Connect the webextension child actor. + */ + async connect() { + if (this._browser) { + throw new Error("This actor is already connected to the extension process"); + } + + // Called when the debug browser element has been destroyed + // (no actor is using it anymore to connect the child extension process). + const onDestroy = this.destroy.bind(this); + + this._browser = await ExtensionParent.DebugUtils.getExtensionProcessBrowser(this); + + this._form = await DebuggerServer.connectToChild(this._conn, this._browser, onDestroy, + {addonId: this.addonId}); + + this._childActorID = this._form.actor; + + // Exit the proxy child actor if the child actor has been destroyed. + this._mm.addMessageListener("debug:webext_child_exit", this._onChildExit); + + return this._form; + }, + + get isOOP() { + return this._browser ? this._browser.isRemoteBrowser : undefined; + }, + + get _mm() { + return this._browser && ( + this._browser.messageManager || + this._browser.frameLoader.messageManager); + }, + + destroy() { + if (this._mm) { + this._mm.removeMessageListener("debug:webext_child_exit", this._onChildExit); + + this._mm.sendAsyncMessage("debug:webext_parent_exit", { + actor: this._childActorID, + }); + + ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this); + } + + if (this._parentActor) { + this._parentActor.onProxyChildActorDestroy(); + } + + this._parentActor = null; + this._browser = null; + this._childActorID = null; + this._form = null; + }, + + /** + * Handle the child actor exit. + */ + _onChildExit(msg) { + if (msg.json.actor !== this._childActorID) { + return; + } + + this.destroy(); + }, +};
--- a/devtools/server/actors/webextension.js +++ b/devtools/server/actors/webextension.js @@ -1,333 +1,374 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; -const { Ci, Cu } = require("chrome"); +const { Ci, Cu, Cc } = require("chrome"); const Services = require("Services"); + const { ChromeActor } = require("./chrome"); const makeDebugger = require("./utils/make-debugger"); -var DevToolsUtils = require("devtools/shared/DevToolsUtils"); -var { assert } = DevToolsUtils; - loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id"); loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true); -loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); -loader.lazyImporter(this, "XPIProvider", "resource://gre/modules/addons/XPIProvider.jsm"); - const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet."; /** * Creates a TabActor for debugging all the contexts associated to a target WebExtensions - * add-on. + * add-on running in a child extension process. * Most of the implementation is inherited from ChromeActor (which inherits most of its * implementation from TabActor). - * WebExtensionActor is a child of RootActor, it can be retrieved via - * RootActor.listAddons request. - * WebExtensionActor exposes all tab actors via its form() request, like TabActor. + * WebExtensionChildActor is created by a WebExtensionParentActor counterpart, when its + * parent actor's `connect` method has been called (on the listAddons RDP package), + * it runs in the same process that the extension is running into (which can be the main + * process if the extension is running in non-oop mode, or the child extension process + * if the extension is running in oop-mode). + * + * A WebExtensionChildActor contains all tab actors, like a regular ChromeActor + * or TabActor. * * History lecture: - * The add-on actors used to not inherit TabActor because of the different way the + * - The add-on actors used to not inherit TabActor because of the different way the * add-on APIs where exposed to the add-on itself, and for this reason the Addon Debugger * has only a sub-set of the feature available in the Tab or in the Browser Toolbox. - * In a WebExtensions add-on all the provided contexts (background and popup pages etc.), + * - In a WebExtensions add-on all the provided contexts (background, popups etc.), * besides the Content Scripts which run in the content process, hooked to an existent * tab, by creating a new WebExtensionActor which inherits from ChromeActor, we can * provide a full features Addon Toolbox (which is basically like a BrowserToolbox which * filters the visible sources and frames to the one that are related to the target * add-on). + * - When the WebExtensions OOP mode has been introduced, this actor has been refactored + * and moved from the main process to the new child extension process. * - * @param conn DebuggerServerConnection + * @param {DebuggerServerConnection} conn * The connection to the client. - * @param addon AddonWrapper - * The target addon. + * @param {nsIMessageSender} chromeGlobal. + * The chromeGlobal where this actor has been injected by the + * DebuggerServer.connectToChild method. + * @param {string} prefix + * the custom RDP prefix to use. + * @param {string} addonId + * the addonId of the target WebExtension. */ -function WebExtensionActor(conn, addon) { +function WebExtensionChildActor(conn, chromeGlobal, prefix, addonId) { ChromeActor.call(this, conn); - this.id = addon.id; - this.addon = addon; + this._chromeGlobal = chromeGlobal; + this._prefix = prefix; + this.id = addonId; // 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 // (retrieved and used in the related webconsole child actor). this.consoleAPIListenerOptions = { - addonId: addon.id, + addonId: this.id, }; + this.aps = Cc["@mozilla.org/addons/policy-service;1"] + .getService(Ci.nsIAddonPolicyService); + // This creates a Debugger instance for debugging all the add-on globals. this.makeDebugger = makeDebugger.bind(null, { findDebuggees: dbg => { return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee); }, shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this), }); - // Discover the preferred debug global for the target addon - this.preferredTargetWindow = null; - this._findAddonPreferredTargetWindow(); + // Try to discovery an existent extension page to attach (which will provide the initial + // URL shown in the window tittle when the addon debugger is opened). + let extensionWindow = this._searchForExtensionWindow(); - AddonManager.addAddonListener(this); + if (extensionWindow) { + this._setWindow(extensionWindow); + } } -exports.WebExtensionActor = WebExtensionActor; +exports.WebExtensionChildActor = WebExtensionChildActor; -WebExtensionActor.prototype = Object.create(ChromeActor.prototype); +WebExtensionChildActor.prototype = Object.create(ChromeActor.prototype); -WebExtensionActor.prototype.actorPrefix = "webExtension"; -WebExtensionActor.prototype.constructor = WebExtensionActor; +WebExtensionChildActor.prototype.actorPrefix = "webExtension"; +WebExtensionChildActor.prototype.constructor = WebExtensionChildActor; // NOTE: This is needed to catch in the webextension webconsole all the // errors raised by the WebExtension internals that are not currently // associated with any window. -WebExtensionActor.prototype.isRootActor = true; - -WebExtensionActor.prototype.form = function () { - assert(this.actorID, "addon should have an actorID."); - - let baseForm = ChromeActor.prototype.form.call(this); - - return Object.assign(baseForm, { - actor: this.actorID, - id: this.id, - name: this.addon.name, - url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined, - iconURL: this.addon.iconURL, - debuggable: this.addon.isDebuggable, - temporarilyInstalled: this.addon.temporarilyInstalled, - isWebExtension: this.addon.isWebExtension, - }); -}; - -WebExtensionActor.prototype._attach = function () { - // NOTE: we need to be sure that `this.window` can return a - // window before calling the ChromeActor.onAttach, or the TabActor - // will not be subscribed to the child doc shell updates. - - // If a preferredTargetWindow exists, set it as the target for this actor - // when the client request to attach this actor. - if (this.preferredTargetWindow) { - this._setWindow(this.preferredTargetWindow); - } else { - this._createFallbackWindow(); - } - - // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell - ChromeActor.prototype._attach.apply(this); -}; - -WebExtensionActor.prototype._detach = function () { - this._destroyFallbackWindow(); - - // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners. - ChromeActor.prototype._detach.apply(this); -}; +WebExtensionChildActor.prototype.isRootActor = true; /** * Called when the actor is removed from the connection. */ -WebExtensionActor.prototype.exit = function () { - AddonManager.removeAddonListener(this); +WebExtensionChildActor.prototype.exit = function () { + if (this._chromeGlobal) { + let chromeGlobal = this._chromeGlobal; + this._chromeGlobal = null; - this.preferredTargetWindow = null; + chromeGlobal.removeMessageListener("debug:webext_parent_exit", this._onParentExit); + + chromeGlobal.sendAsyncMessage("debug:webext_child_exit", { + actor: this.actorID + }); + } + this.addon = null; this.id = null; return ChromeActor.prototype.exit.apply(this); }; -// Addon Specific Remote Debugging requestTypes and methods. - -/** - * Reloads the addon. - */ -WebExtensionActor.prototype.onReload = function () { - return this.addon.reload() - .then(() => { - // send an empty response - return {}; - }); -}; - -/** - * Set the preferred global for the add-on (called from the AddonManager). - */ -WebExtensionActor.prototype.setOptions = function (addonOptions) { - if ("global" in addonOptions) { - // Set the proposed debug global as the preferred target window - // (the actor will eventually set it as the target once it is attached) - this.preferredTargetWindow = addonOptions.global; - } -}; - -// AddonManagerListener callbacks. +// Private helpers. -WebExtensionActor.prototype.onInstalled = function (addon) { - if (addon.id != this.id) { - return; - } - - // Update the AddonManager's addon object on reload/update. - this.addon = addon; -}; - -WebExtensionActor.prototype.onUninstalled = function (addon) { - if (addon != this.addon) { - return; - } - - this.exit(); -}; - -WebExtensionActor.prototype.onPropertyChanged = function (addon, changedPropNames) { - if (addon != this.addon) { - return; - } - - // Refresh the preferred debug global on disabled/reloaded/upgraded addon. - if (changedPropNames.includes("debugGlobal")) { - this._findAddonPreferredTargetWindow(); - } -}; - -// Private helpers - -WebExtensionActor.prototype._createFallbackWindow = function () { +WebExtensionChildActor.prototype._createFallbackWindow = function () { if (this.fallbackWindow) { // Skip if there is already an existent fallback window. return; } // Create an empty hidden window as a fallback (e.g. the background page could be // not defined for the target add-on or not yet when the actor instance has been // created). this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true); - this.fallbackWebNav.loadURI( - `data:text/html;charset=utf-8,${FALLBACK_DOC_MESSAGE}`, - 0, null, null, null - ); - - this.fallbackDocShell = this.fallbackWebNav - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDocShell); - Object.defineProperty(this, "docShell", { - value: this.fallbackDocShell, - configurable: true - }); + // Save the reference to the fallback DOMWindow. + this.fallbackWindow = this.fallbackWebNav.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); - // Save the reference to the fallback DOMWindow - this.fallbackWindow = this.fallbackDocShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindow); + // Insert the fallback doc message. + this.fallbackWindow.document.body.innerText = FALLBACK_DOC_MESSAGE; }; -WebExtensionActor.prototype._destroyFallbackWindow = function () { +WebExtensionChildActor.prototype._destroyFallbackWindow = function () { if (this.fallbackWebNav) { // Explicitly close the fallback windowless browser to prevent it to leak // (and to prevent it to freeze devtools xpcshell tests). this.fallbackWebNav.loadURI("about:blank", 0, null, null, null); this.fallbackWebNav.close(); this.fallbackWebNav = null; this.fallbackWindow = null; } }; -/** - * Discover the preferred debug global and switch to it if the addon has been attached. - */ -WebExtensionActor.prototype._findAddonPreferredTargetWindow = function () { - return new Promise(resolve => { - let activeAddon = XPIProvider.activeAddons.get(this.id); +// Discovery an extension page to use as a default target window. +// NOTE: This currently fail to discovery an extension page running in a +// windowless browser when running in non-oop mode, and the background page +// is set later using _onNewExtensionWindow. +WebExtensionChildActor.prototype._searchForExtensionWindow = function () { + let e = Services.ww.getWindowEnumerator(null); + while (e.hasMoreElements()) { + let window = e.getNext(); + + if (window.document.nodePrincipal.addonId == this.id) { + return window; + } + } + + return undefined; +}; + +// Customized ChromeActor/TabActor hooks. - if (!activeAddon) { - // The addon is not active, the background page is going to be destroyed, - // navigate to the fallback window (if it already exists). - resolve(null); - } else { - AddonManager.getAddonByInstanceID(activeAddon.instanceID) - .then(privateWrapper => { - let targetWindow = privateWrapper.getDebugGlobal(); +WebExtensionChildActor.prototype._onDocShellDestroy = function (docShell) { + // Stop watching this docshell (the unwatch() method will check if we + // started watching it before). + this._unwatchDocShell(docShell); + + // Let the _onDocShellDestroy notify that the docShell has been destroyed. + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + this._notifyDocShellDestroy(webProgress); + + // If the destroyed docShell was the current docShell and the actor is + // currently attached, switch to the fallback window + if (this.attached && docShell == this.docShell) { + // Creates a fallback window if it doesn't exist yet. + this._createFallbackWindow(); + this._changeTopLevelDocument(this.fallbackWindow); + } +}; + +WebExtensionChildActor.prototype._onNewExtensionWindow = function (window) { + if (!this.window || this.window === this.fallbackWindow) { + this._changeTopLevelDocument(window); + } +}; - // Do not use the preferred global if it is not a DOMWindow as expected. - if (!(targetWindow instanceof Ci.nsIDOMWindow)) { - targetWindow = null; - } +WebExtensionChildActor.prototype._attach = function () { + // NOTE: we need to be sure that `this.window` can return a + // window before calling the ChromeActor.onAttach, or the TabActor + // will not be subscribed to the child doc shell updates. - resolve(targetWindow); - }); + if (!this.window || this.window.document.nodePrincipal.addonId !== this.id) { + // Discovery an existent extension page to attach. + let extensionWindow = this._searchForExtensionWindow(); + + if (!extensionWindow) { + this._createFallbackWindow(); + this._setWindow(this.fallbackWindow); + } else { + this._setWindow(extensionWindow); } - }).then(preferredTargetWindow => { - this.preferredTargetWindow = preferredTargetWindow; + } + + // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell + ChromeActor.prototype._attach.apply(this); +}; + +WebExtensionChildActor.prototype._detach = function () { + // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners. + ChromeActor.prototype._detach.apply(this); + + // Stop watching for new extension windows. + this._destroyFallbackWindow(); +}; - if (!preferredTargetWindow) { - // Create a fallback window if no preferred target window has been found. - this._createFallbackWindow(); - } else if (this.attached) { - // Change the top level document if the actor is already attached. - this._changeTopLevelDocument(preferredTargetWindow); - } +/** + * Return the json details related to a docShell. + */ +WebExtensionChildActor.prototype._docShellToWindow = function (docShell) { + const baseWindowDetails = ChromeActor.prototype._docShellToWindow.call(this, docShell); + + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + let window = webProgress.DOMWindow; + + // Collect the addonID from the document origin attributes and its sameType top level + // frame. + let addonID = window.document.nodePrincipal.addonId; + let sameTypeRootAddonID = docShell.QueryInterface(Ci.nsIDocShellTreeItem) + .sameTypeRootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .document.nodePrincipal.addonId; + + return Object.assign(baseWindowDetails, { + addonID, + sameTypeRootAddonID, }); }; /** * Return an array of the json details related to an array/iterator of docShells. */ -WebExtensionActor.prototype._docShellsToWindows = function (docshells) { +WebExtensionChildActor.prototype._docShellsToWindows = function (docshells) { return ChromeActor.prototype._docShellsToWindows.call(this, docshells) .filter(windowDetails => { - // filter the docShells based on the addon id - return windowDetails.addonID == this.id; + // Filter the docShells based on the addon id of the window or + // its sameType top level frame. + return windowDetails.addonID === this.id || + windowDetails.sameTypeRootAddonID === this.id; }); }; +WebExtensionChildActor.prototype.isExtensionWindow = function (window) { + return window.document.nodePrincipal.addonId == this.id; +}; + +WebExtensionChildActor.prototype.isExtensionWindowDescendent = function (window) { + // Check if the source is coming from a descendant docShell of an extension window. + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + let rootWin = docShell.sameTypeRootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + return this.isExtensionWindow(rootWin); +}; + /** * Return true if the given source is associated with this addon and should be * added to the visible sources (retrieved and used by the webbrowser actor module). */ -WebExtensionActor.prototype._allowSource = function (source) { +WebExtensionChildActor.prototype._allowSource = function (source) { + // Use the source.element to detect the allowed source, if any. + if (source.element) { + let domEl = unwrapDebuggerObjectGlobal(source.element); + return (this.isExtensionWindow(domEl.ownerGlobal) || + this.isExtensionWindowDescendent(domEl.ownerGlobal)); + } + + // Fallback to check the uri if there is no source.element associated to the source. + + // Retrieve the first component of source.url in the form "url1 -> url2 -> ...". + let url = source.url.split(" -> ").pop(); + + // Filter out the code introduced by evaluating code in the webconsole. + if (url === "debugger eval code") { + return false; + } + + let uri; + + // Try to decode the url. try { - let uri = Services.io.newURI(source.url); - let addonID = mapURIToAddonID(uri); + uri = Services.io.newURI(url); + } catch (err) { + Cu.reportError(`Unexpected invalid url: ${url}`); + return false; + } + + // Filter out resource and chrome sources (which are related to the loaded internals). + if (["resource", "chrome", "file"].includes(uri.scheme)) { + return false; + } + + try { + let addonID = this.aps.extensionURIToAddonId(uri); return addonID == this.id; - } catch (e) { + } catch (err) { + // extensionURIToAddonId raises an exception on non-extension URLs. return false; } }; /** * Return true if the given global is associated with this addon and should be * added as a debuggee, false otherwise. */ -WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) { +WebExtensionChildActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) { const global = unwrapDebuggerObjectGlobal(newGlobal); if (global instanceof Ci.nsIDOMWindow) { - return global.document.nodePrincipal.addonId == this.id; + // Filter out any global which contains a XUL document. + if (global.document instanceof Ci.nsIDOMXULDocument) { + return false; + } + + // Change top level document as a simulated frame switching. + if (global.document.ownerGlobal && this.isExtensionWindow(global)) { + this._onNewExtensionWindow(global.document.ownerGlobal); + } + + return global.document.ownerGlobal && + this.isExtensionWindowDescendent(global.document.ownerGlobal); } try { // This will fail for non-Sandbox objects, hence the try-catch block. let metadata = Cu.getSandboxMetadata(global); if (metadata) { return metadata.addonID === this.id; } } catch (e) { // Unable to retrieve the sandbox metadata. } return false; }; -/** - * Override WebExtensionActor requestTypes: - * - redefined `reload`, which should reload the target addon - * (instead of the entire browser as the regular ChromeActor does). - */ -WebExtensionActor.prototype.requestTypes.reload = WebExtensionActor.prototype.onReload; +// Handlers for the messages received from the parent actor. + +WebExtensionChildActor.prototype._onParentExit = function (msg) { + if (msg.json.actor !== this.actorID) { + return; + } + + this.exit(); +};
--- a/devtools/server/child.js +++ b/devtools/server/child.js @@ -12,39 +12,48 @@ try { // Encapsulate in its own scope to allows loading this frame script more than once. (function () { const Cu = Components.utils; const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { dumpn } = DevToolsUtils; const { DebuggerServer, ActorPool } = require("devtools/server/main"); - const { ContentActor } = require("devtools/server/actors/childtab"); if (!DebuggerServer.initialized) { DebuggerServer.init(); } // We want a special server without any root actor and only tab actors. // We are going to spawn a ContentActor instance in the next few lines, // it is going to act like a root actor without being one. DebuggerServer.registerActors({ root: false, browser: false, tab: true }); let connections = new Map(); let onConnect = DevToolsUtils.makeInfallible(function (msg) { removeMessageListener("debug:connect", onConnect); let mm = msg.target; let prefix = msg.data.prefix; + let addonId = msg.data.addonId; let conn = DebuggerServer.connectToParent(prefix, mm); conn.parentMessageManager = mm; connections.set(prefix, conn); - let actor = new ContentActor(conn, chromeGlobal, prefix); + let actor; + + if (addonId) { + const { WebExtensionChildActor } = require("devtools/server/actors/webextension"); + actor = new WebExtensionChildActor(conn, chromeGlobal, prefix, addonId); + } else { + const { ContentActor } = require("devtools/server/actors/childtab"); + actor = new ContentActor(conn, chromeGlobal, prefix); + } + let actorPool = new ActorPool(conn); actorPool.addActor(actor); conn.addActorPool(actorPool); sendAsyncMessage("debug:actor", {actor: actor.form(), prefix: prefix}); }); addMessageListener("debug:connect", onConnect);
--- a/devtools/server/main.js +++ b/devtools/server/main.js @@ -1004,17 +1004,17 @@ var DebuggerServer = { * @param function [onDestroy] * Optional function to invoke when the child process closes * or the connection shuts down. (Need to forget about the * related TabActor) * @return object * A promise object that is resolved once the connection is * established. */ - connectToChild(connection, frame, onDestroy) { + connectToChild(connection, frame, onDestroy, {addonId} = {}) { 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); let trackMessageManager = () => { @@ -1117,16 +1117,19 @@ var DebuggerServer = { }); if (childTransport) { childTransport.swapBrowser(mm); } }; let destroy = DevToolsUtils.makeInfallible(function () { + events.off(connection, "closed", destroy); + Services.obs.removeObserver(onMessageManagerClose, "message-manager-close"); + // provides hook to actor modules that need to exchange messages // between e10s parent and child processes parentModules.forEach(mod => { if (mod.onDisconnected) { mod.onDisconnected(); } }); // TODO: Remove this deprecated path once it's no longer needed by add-ons. @@ -1163,18 +1166,16 @@ var DebuggerServer = { } if (onDestroy) { onDestroy(mm); } // Cleanup all listeners untrackMessageManager(); - Services.obs.removeObserver(onMessageManagerClose, "message-manager-close"); - events.off(connection, "closed", destroy); }); // Listen for various messages and frame events trackMessageManager(); // Listen for app process exit let onMessageManagerClose = function (subject, topic, data) { if (subject == mm) { @@ -1183,17 +1184,17 @@ var DebuggerServer = { }; Services.obs.addObserver(onMessageManagerClose, "message-manager-close"); // Listen for connection close to cleanup things // when user unplug the device or we lose the connection somehow. events.on(connection, "closed", destroy); - mm.sendAsyncMessage("debug:connect", { prefix }); + mm.sendAsyncMessage("debug:connect", { prefix, addonId }); return deferred.promise; }, /** * Create a new debugger connection for the given transport. Called after * connectPipe(), from connectToParent, or from an incoming socket * connection handler.
--- a/devtools/shared/specs/moz.build +++ b/devtools/shared/specs/moz.build @@ -38,11 +38,12 @@ DevToolsModules( 'storage.js', 'string.js', 'styleeditor.js', 'styles.js', 'stylesheets.js', 'timeline.js', 'webaudio.js', 'webextension-inspected-window.js', + 'webextension-parent.js', 'webgl.js', 'worker.js' )
new file mode 100644 --- /dev/null +++ b/devtools/shared/specs/webextension-parent.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const {RetVal, generateActorSpec} = require("devtools/shared/protocol"); + +const webExtensionSpec = generateActorSpec({ + typeName: "webExtensionAddon", + + methods: { + reload: { + request: { }, + response: { addon: RetVal("json") }, + }, + + connect: { + request: { }, + response: { form: RetVal("json") }, + }, + }, +}); + +exports.webExtensionSpec = webExtensionSpec;