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) { function buildAddonLink(addon, parent) {
let a = document.createElement("a"); let a = document.createElement("a");
a.onclick = function () { a.onclick = async function () {
openToolbox(addon, true, "jsdebugger", false); const isTabActor = addon.isWebExtension;
openToolbox(addon, true, "webconsole", isTabActor);
}; };
a.textContent = addon.name; a.textContent = addon.name;

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

@ -351,15 +351,15 @@ TabTarget.prototype = {
}, },
get isAddon() { get isAddon() {
return !!(this._form && this._form.actor && ( return !!(this._form && this._form.actor &&
this._form.actor.match(/conn\d+\.addon\d+/) || this._form.actor.match(/conn\d+\.addon\d+/)) || this.isWebExtension;
this._form.actor.match(/conn\d+\.webExtension\d+/)
));
}, },
get isWebExtension() { get isWebExtension() {
return !!(this._form && this._form.actor && return !!(this._form && this._form.actor && (
this._form.actor.match(/conn\d+\.webExtension\d+/)); this._form.actor.match(/conn\d+\.webExtension\d+/) ||
this._form.actor.match(/child\d+\/webExtension\d+/)
));
}, },
get isLocalTab() { get isLocalTab() {
@ -375,7 +375,7 @@ TabTarget.prototype = {
* for tools that support the Remote Debugging Protocol even for local * for tools that support the Remote Debugging Protocol even for local
* connections. * connections.
*/ */
makeRemote: function () { makeRemote: async function () {
if (this._remote) { if (this._remote) {
return this._remote.promise; return this._remote.promise;
} }
@ -398,6 +398,22 @@ TabTarget.prototype = {
this._client = new DebuggerClient(DebuggerServer.connectPipe()); this._client = new DebuggerClient(DebuggerServer.connectPipe());
// A local TabTarget will never perform chrome debugging. // A local TabTarget will never perform chrome debugging.
this._chrome = false; 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(); this._setupRemoteListeners();

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

@ -46,17 +46,11 @@ var connect = Task.async(function*() {
if (addonID) { if (addonID) {
let { addons } = yield gClient.listAddons(); let { addons } = yield gClient.listAddons();
let addonActor = addons.filter(addon => addon.id === addonID).pop(); let addonActor = addons.filter(addon => addon.id === addonID).pop();
openToolbox({ let isTabActor = addonActor.isWebExtension;
form: addonActor, openToolbox({form: addonActor, chrome: true, isTabActor});
chrome: true,
isTabActor: addonActor.isWebExtension ? true : false
});
} else { } else {
let response = yield gClient.getProcess(); let response = yield gClient.getProcess();
openToolbox({ openToolbox({form: response.form, chrome: true});
form: response.form,
chrome: true
});
} }
}); });

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

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

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

@ -190,7 +190,10 @@ RootActor.prototype = {
heapSnapshots: true, heapSnapshots: true,
// Whether or not the timeline actor can emit DOMContentLoaded and Load // Whether or not the timeline actor can emit DOMContentLoaded and Load
// markers, currently in use by the network monitor. Fx45+ // 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(); this._updateChildDocShells();
}, },
_unwatchDocShell(docShell) {
if (this._progressListener) {
this._progressListener.unwatch(docShell);
}
},
onSwitchToFrame(request) { onSwitchToFrame(request) {
let windowId = request.windowId; let windowId = request.windowId;
let win; let win;
@ -700,9 +706,43 @@ TabActor.prototype = {
}, },
_onDocShellDestroy(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) let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress); .getInterface(Ci.nsIWebProgress);
this._notifyDocShellDestroy(webProgress); 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) { _isRootDocShell(docShell) {
@ -715,36 +755,34 @@ TabActor.prototype = {
return !docShell.parent; 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 // Convert docShell list to windows objects list being sent to the client
_docShellsToWindows(docshells) { _docShellsToWindows(docshells) {
return docshells.map(docShell => { return docshells.map(docShell => this._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;
}
// 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,
};
});
}, },
_notifyDocShellsUpdate(docshells) { _notifyDocShellsUpdate(docshells) {
@ -780,41 +818,6 @@ TabActor.prototype = {
destroy: true 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() { _notifyDocShellDestroyAll() {
@ -866,7 +869,7 @@ TabActor.prototype = {
// Check for docShell availability, as it can be already gone // Check for docShell availability, as it can be already gone
// during Firefox shutdown. // during Firefox shutdown.
if (this.docShell) { if (this.docShell) {
this._progressListener.unwatch(this.docShell); this._unwatchDocShell(this.docShell);
this._restoreDocumentSettings(); this._restoreDocumentSettings();
} }
if (this._progressListener) { 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, "RootActor", "devtools/server/actors/root", true);
loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", 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, "WorkerActorList", "devtools/server/actors/worker-list", true);
loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "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.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true);
@ -835,7 +835,7 @@ BrowserAddonList.prototype.getList = function () {
let actor = this._actorByAddonId.get(addon.id); let actor = this._actorByAddonId.get(addon.id);
if (!actor) { if (!actor) {
if (addon.isWebExtension) { if (addon.isWebExtension) {
actor = new WebExtensionActor(this._connection, addon); actor = new WebExtensionParentActor(this._connection, addon);
} else { } else {
actor = new BrowserAddonActor(this._connection, addon); actor = new BrowserAddonActor(this._connection, addon);
} }
@ -843,8 +843,10 @@ BrowserAddonList.prototype.getList = function () {
this._actorByAddonId.set(addon.id, actor); this._actorByAddonId.set(addon.id, actor);
} }
} }
deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor)); deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor));
}); });
return deferred.promise; 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"; "use strict";
const { Ci, Cu } = require("chrome"); const { Ci, Cu, Cc } = require("chrome");
const Services = require("Services"); const Services = require("Services");
const { ChromeActor } = require("./chrome"); const { ChromeActor } = require("./chrome");
const makeDebugger = require("./utils/make-debugger"); 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, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true); 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."; 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 * 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 * Most of the implementation is inherited from ChromeActor (which inherits most of its
* implementation from TabActor). * implementation from TabActor).
* WebExtensionActor is a child of RootActor, it can be retrieved via * WebExtensionChildActor is created by a WebExtensionParentActor counterpart, when its
* RootActor.listAddons request. * parent actor's `connect` method has been called (on the listAddons RDP package),
* WebExtensionActor exposes all tab actors via its form() request, like TabActor. * 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: * 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 * 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. * 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 * 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 * 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 * 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 * filters the visible sources and frames to the one that are related to the target
* add-on). * 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. * The connection to the client.
* @param addon AddonWrapper * @param {nsIMessageSender} chromeGlobal.
* The target addon. * 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); ChromeActor.call(this, conn);
this.id = addon.id; this._chromeGlobal = chromeGlobal;
this.addon = addon; this._prefix = prefix;
this.id = addonId;
// Bind the _allowSource helper to this, it is used in the // Bind the _allowSource helper to this, it is used in the
// TabActor to lazily create the TabSources instance. // TabActor to lazily create the TabSources instance.
this._allowSource = this._allowSource.bind(this); 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 // Set the consoleAPIListener filtering options
// (retrieved and used in the related webconsole child actor). // (retrieved and used in the related webconsole child actor).
this.consoleAPIListenerOptions = { 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 creates a Debugger instance for debugging all the add-on globals.
this.makeDebugger = makeDebugger.bind(null, { this.makeDebugger = makeDebugger.bind(null, {
findDebuggees: dbg => { findDebuggees: dbg => {
@ -69,135 +83,50 @@ function WebExtensionActor(conn, addon) {
shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this), shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this),
}); });
// Discover the preferred debug global for the target addon // Try to discovery an existent extension page to attach (which will provide the initial
this.preferredTargetWindow = null; // URL shown in the window tittle when the addon debugger is opened).
this._findAddonPreferredTargetWindow(); 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"; WebExtensionChildActor.prototype.actorPrefix = "webExtension";
WebExtensionActor.prototype.constructor = WebExtensionActor; WebExtensionChildActor.prototype.constructor = WebExtensionChildActor;
// NOTE: This is needed to catch in the webextension webconsole all the // NOTE: This is needed to catch in the webextension webconsole all the
// errors raised by the WebExtension internals that are not currently // errors raised by the WebExtension internals that are not currently
// associated with any window. // associated with any window.
WebExtensionActor.prototype.isRootActor = true; WebExtensionChildActor.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);
};
/** /**
* Called when the actor is removed from the connection. * Called when the actor is removed from the connection.
*/ */
WebExtensionActor.prototype.exit = function () { WebExtensionChildActor.prototype.exit = function () {
AddonManager.removeAddonListener(this); 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.addon = null;
this.id = null; this.id = null;
return ChromeActor.prototype.exit.apply(this); return ChromeActor.prototype.exit.apply(this);
}; };
// Addon Specific Remote Debugging requestTypes and methods. // Private helpers.
/** WebExtensionChildActor.prototype._createFallbackWindow = function () {
* 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 () {
if (this.fallbackWindow) { if (this.fallbackWindow) {
// Skip if there is already an existent fallback window. // Skip if there is already an existent fallback window.
return; 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 // not defined for the target add-on or not yet when the actor instance has been
// created). // created).
this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true); 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 // Save the reference to the fallback DOMWindow.
.QueryInterface(Ci.nsIInterfaceRequestor) this.fallbackWindow = this.fallbackWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell); .getInterface(Ci.nsIDOMWindow);
Object.defineProperty(this, "docShell", { // Insert the fallback doc message.
value: this.fallbackDocShell, this.fallbackWindow.document.body.innerText = FALLBACK_DOC_MESSAGE;
configurable: true
});
// Save the reference to the fallback DOMWindow
this.fallbackWindow = this.fallbackDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
}; };
WebExtensionActor.prototype._destroyFallbackWindow = function () { WebExtensionChildActor.prototype._destroyFallbackWindow = function () {
if (this.fallbackWebNav) { if (this.fallbackWebNav) {
// Explicitly close the fallback windowless browser to prevent it to leak // Explicitly close the fallback windowless browser to prevent it to leak
// (and to prevent it to freeze devtools xpcshell tests). // (and to prevent it to freeze devtools xpcshell tests).
@ -238,65 +157,173 @@ WebExtensionActor.prototype._destroyFallbackWindow = function () {
} }
}; };
/** // Discovery an extension page to use as a default target window.
* Discover the preferred debug global and switch to it if the addon has been attached. // NOTE: This currently fail to discovery an extension page running in a
*/ // windowless browser when running in non-oop mode, and the background page
WebExtensionActor.prototype._findAddonPreferredTargetWindow = function () { // is set later using _onNewExtensionWindow.
return new Promise(resolve => { WebExtensionChildActor.prototype._searchForExtensionWindow = function () {
let activeAddon = XPIProvider.activeAddons.get(this.id); let e = Services.ww.getWindowEnumerator(null);
while (e.hasMoreElements()) {
let window = e.getNext();
if (!activeAddon) { if (window.document.nodePrincipal.addonId == this.id) {
// The addon is not active, the background page is going to be destroyed, return window;
// 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);
});
} }
}).then(preferredTargetWindow => { }
this.preferredTargetWindow = preferredTargetWindow;
if (!preferredTargetWindow) { return undefined;
// Create a fallback window if no preferred target window has been found. };
// 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(); this._createFallbackWindow();
} else if (this.attached) { this._setWindow(this.fallbackWindow);
// Change the top level document if the actor is already attached. } else {
this._changeTopLevelDocument(preferredTargetWindow); 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. * 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) return ChromeActor.prototype._docShellsToWindows.call(this, docshells)
.filter(windowDetails => { .filter(windowDetails => {
// filter the docShells based on the addon id // Filter the docShells based on the addon id of the window or
return windowDetails.addonID == this.id; // 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 * 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). * 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 { try {
let uri = Services.io.newURI(source.url); uri = Services.io.newURI(url);
let addonID = mapURIToAddonID(uri); } 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; return addonID == this.id;
} catch (e) { } catch (err) {
// extensionURIToAddonId raises an exception on non-extension URLs.
return false; 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 * Return true if the given global is associated with this addon and should be
* added as a debuggee, false otherwise. * added as a debuggee, false otherwise.
*/ */
WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) { WebExtensionChildActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) {
const global = unwrapDebuggerObjectGlobal(newGlobal); const global = unwrapDebuggerObjectGlobal(newGlobal);
if (global instanceof Ci.nsIDOMWindow) { 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 { try {
@ -325,9 +363,12 @@ WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal)
return false; return false;
}; };
/** // Handlers for the messages received from the parent actor.
* Override WebExtensionActor requestTypes:
* - redefined `reload`, which should reload the target addon WebExtensionChildActor.prototype._onParentExit = function (msg) {
* (instead of the entire browser as the regular ChromeActor does). if (msg.json.actor !== this.actorID) {
*/ return;
WebExtensionActor.prototype.requestTypes.reload = WebExtensionActor.prototype.onReload; }
this.exit();
};

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

@ -17,7 +17,6 @@ try {
const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const { dumpn } = DevToolsUtils; const { dumpn } = DevToolsUtils;
const { DebuggerServer, ActorPool } = require("devtools/server/main"); const { DebuggerServer, ActorPool } = require("devtools/server/main");
const { ContentActor } = require("devtools/server/actors/childtab");
if (!DebuggerServer.initialized) { if (!DebuggerServer.initialized) {
DebuggerServer.init(); DebuggerServer.init();
@ -34,12 +33,22 @@ try {
let mm = msg.target; let mm = msg.target;
let prefix = msg.data.prefix; let prefix = msg.data.prefix;
let addonId = msg.data.addonId;
let conn = DebuggerServer.connectToParent(prefix, mm); let conn = DebuggerServer.connectToParent(prefix, mm);
conn.parentMessageManager = mm; conn.parentMessageManager = mm;
connections.set(prefix, conn); 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); let actorPool = new ActorPool(conn);
actorPool.addActor(actor); actorPool.addActor(actor);
conn.addActorPool(actorPool); conn.addActorPool(actorPool);

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

@ -1009,7 +1009,7 @@ var DebuggerServer = {
* A promise object that is resolved once the connection is * A promise object that is resolved once the connection is
* established. * established.
*/ */
connectToChild(connection, frame, onDestroy) { connectToChild(connection, frame, onDestroy, {addonId} = {}) {
let deferred = SyncPromise.defer(); let deferred = SyncPromise.defer();
// Get messageManager from XUL browser (which might be a specialized tunnel for RDM) // Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
@ -1122,6 +1122,9 @@ var DebuggerServer = {
}; };
let destroy = DevToolsUtils.makeInfallible(function () { 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 // provides hook to actor modules that need to exchange messages
// between e10s parent and child processes // between e10s parent and child processes
parentModules.forEach(mod => { parentModules.forEach(mod => {
@ -1168,8 +1171,6 @@ var DebuggerServer = {
// Cleanup all listeners // Cleanup all listeners
untrackMessageManager(); untrackMessageManager();
Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
events.off(connection, "closed", destroy);
}); });
// Listen for various messages and frame events // Listen for various messages and frame events
@ -1188,7 +1189,7 @@ var DebuggerServer = {
// when user unplug the device or we lose the connection somehow. // when user unplug the device or we lose the connection somehow.
events.on(connection, "closed", destroy); events.on(connection, "closed", destroy);
mm.sendAsyncMessage("debug:connect", { prefix }); mm.sendAsyncMessage("debug:connect", { prefix, addonId });
return deferred.promise; return deferred.promise;
}, },

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

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