--- 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;