Bug 1302702 - Make WebExtension Addon Debugging oop-compatible. r=ochameau

This patch applies all the changes needed to the devtools actors
and the toolbox-process-window, to be able to debug a webextension
running in an extension child process (as well as a webextension
running in the main process).

The devtools actor used to debug a webextension is splitted into
3 actors:

- the WebExtensionActor is the actor that is created when the
  "root.listTabs" RDP request is received, it provides the addon
  metadata (name, icon and addon id) and two RDP methods:

  - reload: used to reload the addon (e.g. from the "about:debugging#addons" page)
  - connectAddonDebuggingActor: which provides the actorID of the actor
    that is connected to the process where the extension is running
    (used by toolbox-process-window.js to connect the toolbox to the needed
    devtools actors, e.g. console, inspector etc.)

- the WebExtensionParentActor is the actor that connects to the
  process where the extension is running and ensures that a
  WebExtensionChildActor instance is created and connected
  (this actor is only the entrypoint to reach the WebExtensionChildActor,
  and so it does not provide any RDP request on its own, it only connect
  itself to its child counterpart and then it returns the RDP "form" of
  the child actor, and the client is then connected directly to the
  child actor)

- the WebExtensionChildActor is the actor that is running in the same
  process of the target extension, and it provides the same requestTypes
  of a tab actor.

By splitting the WebExtensionActor from the WebExtensionParentActor, we are
able to prevent the RemoteDebuggingServer to connect (and create
instances of the WebExtensionChildActor) for every addon listed by
a root.listAddons() request.

MozReview-Commit-ID: L1vxhA6xQkD

--HG--
extra : rebase_source : f9438b4a9842c1dd504edf2fcd87857c670f411f
This commit is contained in:
Luca Greco 2017-03-21 15:55:35 +01:00
Родитель 70a6a66a9e
Коммит db7143751a
13 изменённых файлов: 586 добавлений и 280 удалений

Просмотреть файл

@ -164,8 +164,9 @@ var onConnectionReady = Task.async(function* ([aType, aTraits]) {
*/
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;

Просмотреть файл

@ -351,15 +351,15 @@ TabTarget.prototype = {
},
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() {
@ -375,7 +375,7 @@ TabTarget.prototype = {
* for tools that support the Remote Debugging Protocol even for local
* connections.
*/
makeRemote: function () {
makeRemote: async function () {
if (this._remote) {
return this._remote.promise;
}
@ -398,6 +398,22 @@ TabTarget.prototype = {
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();

Просмотреть файл

@ -46,17 +46,11 @@ var connect = Task.async(function*() {
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});
}
});

Просмотреть файл

@ -62,6 +62,7 @@ DevToolsModules(
'webbrowser.js',
'webconsole.js',
'webextension-inspected-window.js',
'webextension-parent.js',
'webextension.js',
'webgl.js',
'window.js',

Просмотреть файл

@ -190,7 +190,10 @@ RootActor.prototype = {
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,
},
/**

Просмотреть файл

@ -597,6 +597,12 @@ TabActor.prototype = {
this._updateChildDocShells();
},
_unwatchDocShell(docShell) {
if (this._progressListener) {
this._progressListener.unwatch(docShell);
}
},
onSwitchToFrame(request) {
let windowId = request.windowId;
let win;
@ -700,9 +706,43 @@ TabActor.prototype = {
},
_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) {
@ -715,36 +755,34 @@ TabActor.prototype = {
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) {
@ -780,41 +818,6 @@ TabActor.prototype = {
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() {
@ -866,7 +869,7 @@ TabActor.prototype = {
// 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) {

Просмотреть файл

@ -14,7 +14,7 @@ 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);
@ -835,7 +835,7 @@ BrowserAddonList.prototype.getList = function () {
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);
}
@ -843,8 +843,10 @@ BrowserAddonList.prototype.getList = function () {
this._actorByAddonId.set(addon.id, actor);
}
}
deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor));
});
return deferred.promise;
};

Просмотреть файл

@ -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();
},
};

Просмотреть файл

@ -4,63 +4,77 @@
"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 => {
@ -69,135 +83,50 @@ function WebExtensionActor(conn, addon) {
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;
chromeGlobal.removeMessageListener("debug:webext_parent_exit", this._onParentExit);
chromeGlobal.sendAsyncMessage("debug:webext_child_exit", {
actor: this.actorID
});
}
this.preferredTargetWindow = null;
this.addon = null;
this.id = null;
return ChromeActor.prototype.exit.apply(this);
};
// Addon Specific Remote Debugging requestTypes and methods.
// Private helpers.
/**
* 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.
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;
@ -207,26 +136,16 @@ WebExtensionActor.prototype._createFallbackWindow = function () {
// 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);
// Save the reference to the fallback DOMWindow.
this.fallbackWindow = this.fallbackWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
Object.defineProperty(this, "docShell", {
value: this.fallbackDocShell,
configurable: true
});
// 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).
@ -238,65 +157,173 @@ WebExtensionActor.prototype._destroyFallbackWindow = function () {
}
};
/**
* 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 (!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();
// Do not use the preferred global if it is not a DOMWindow as expected.
if (!(targetWindow instanceof Ci.nsIDOMWindow)) {
targetWindow = null;
}
resolve(targetWindow);
});
if (window.document.nodePrincipal.addonId == this.id) {
return window;
}
}).then(preferredTargetWindow => {
this.preferredTargetWindow = preferredTargetWindow;
}
if (!preferredTargetWindow) {
// Create a fallback window if no preferred target window has been found.
return undefined;
};
// Customized ChromeActor/TabActor hooks.
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);
}
};
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.
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();
} else if (this.attached) {
// Change the top level document if the actor is already attached.
this._changeTopLevelDocument(preferredTargetWindow);
this._setWindow(this.fallbackWindow);
} else {
this._setWindow(extensionWindow);
}
}
// 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();
};
/**
* 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;
}
};
@ -305,11 +332,22 @@ WebExtensionActor.prototype._allowSource = function (source) {
* 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 {
@ -325,9 +363,12 @@ WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal)
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();
};

Просмотреть файл

@ -17,7 +17,6 @@ try {
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();
@ -34,12 +33,22 @@ try {
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);

Просмотреть файл

@ -1009,7 +1009,7 @@ var DebuggerServer = {
* 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)
@ -1122,6 +1122,9 @@ var DebuggerServer = {
};
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 => {
@ -1168,8 +1171,6 @@ var DebuggerServer = {
// Cleanup all listeners
untrackMessageManager();
Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
events.off(connection, "closed", destroy);
});
// Listen for various messages and frame events
@ -1188,7 +1189,7 @@ var DebuggerServer = {
// 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;
},

Просмотреть файл

@ -43,6 +43,7 @@ DevToolsModules(
'timeline.js',
'webaudio.js',
'webextension-inspected-window.js',
'webextension-parent.js',
'webgl.js',
'worker.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;