summaryrefslogtreecommitdiffstats
path: root/application/basilisk/components/extensions/ext-devtools.js
diff options
context:
space:
mode:
Diffstat (limited to 'application/basilisk/components/extensions/ext-devtools.js')
-rw-r--r--application/basilisk/components/extensions/ext-devtools.js312
1 files changed, 312 insertions, 0 deletions
diff --git a/application/basilisk/components/extensions/ext-devtools.js b/application/basilisk/components/extensions/ext-devtools.js
new file mode 100644
index 000000000..6ba9e507a
--- /dev/null
+++ b/application/basilisk/components/extensions/ext-devtools.js
@@ -0,0 +1,312 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* global getTargetTabIdForToolbox */
+
+/**
+ * This module provides helpers used by the other specialized `ext-devtools-*.js` modules
+ * and the implementation of the `devtools_page`.
+ */
+
+XPCOMUtils.defineLazyModuleGetter(this, "gDevTools",
+ "resource://devtools/client/framework/gDevTools.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+Cu.import("resource://gre/modules/ExtensionParent.jsm");
+
+const {
+ HiddenExtensionPage,
+ watchExtensionProxyContextLoad,
+} = ExtensionParent;
+
+// Map[extension -> DevToolsPageDefinition]
+let devtoolsPageDefinitionMap = new Map();
+
+let initDevTools;
+
+/**
+ * Retrieve the devtools target for the devtools extension proxy context
+ * (lazily cloned from the target of the toolbox associated to the context
+ * the first time that it is accessed).
+ *
+ * @param {DevToolsExtensionPageContextParent} context
+ * A devtools extension proxy context.
+ *
+ * @returns {Promise<TabTarget>}
+ * The cloned devtools target associated to the context.
+ */
+global.getDevToolsTargetForContext = (context) => {
+ return Task.spawn(function* asyncGetTabTarget() {
+ if (context.devToolsTarget) {
+ return context.devToolsTarget;
+ }
+
+ if (!context.devToolsToolbox || !context.devToolsToolbox.target) {
+ throw new Error("Unable to get a TabTarget for a context not associated to any toolbox");
+ }
+
+ if (!context.devToolsToolbox.target.isLocalTab) {
+ throw new Error("Unexpected target type: only local tabs are currently supported.");
+ }
+
+ const {TabTarget} = require("devtools/client/framework/target");
+
+ context.devToolsTarget = new TabTarget(context.devToolsToolbox.target.tab);
+ yield context.devToolsTarget.makeRemote();
+
+ return context.devToolsTarget;
+ });
+};
+
+/**
+ * Retrieve the devtools target for the devtools extension proxy context
+ * (lazily cloned from the target of the toolbox associated to the context
+ * the first time that it is accessed).
+ *
+ * @param {Toolbox} toolbox
+ * A devtools toolbox instance.
+ *
+ * @returns {number}
+ * The corresponding WebExtensions tabId.
+ */
+global.getTargetTabIdForToolbox = (toolbox) => {
+ let {target} = toolbox;
+
+ if (!target.isLocalTab) {
+ throw new Error("Unexpected target type: only local tabs are currently supported.");
+ }
+
+ let parentWindow = target.tab.linkedBrowser.ownerDocument.defaultView;
+ let tab = parentWindow.gBrowser.getTabForBrowser(target.tab.linkedBrowser);
+
+ return TabManager.getId(tab);
+};
+
+/**
+ * The DevToolsPage represents the "devtools_page" related to a particular
+ * Toolbox and WebExtension.
+ *
+ * The devtools_page contexts are invisible WebExtensions contexts, similar to the
+ * background page, associated to a single developer toolbox (e.g. If an add-on
+ * registers a devtools_page and the user opens 3 developer toolbox in 3 webpages,
+ * 3 devtools_page contexts will be created for that add-on).
+ *
+ * @param {Extension} extension
+ * The extension that owns the devtools_page.
+ * @param {Object} options
+ * @param {Toolbox} options.toolbox
+ * The developer toolbox instance related to this devtools_page.
+ * @param {string} options.url
+ * The path to the devtools page html page relative to the extension base URL.
+ * @param {DevToolsPageDefinition} options.devToolsPageDefinition
+ * The instance of the devToolsPageDefinition class related to this DevToolsPage.
+ */
+class DevToolsPage extends HiddenExtensionPage {
+ constructor(extension, options) {
+ super(extension, "devtools_page");
+
+ this.url = extension.baseURI.resolve(options.url);
+ this.toolbox = options.toolbox;
+ this.devToolsPageDefinition = options.devToolsPageDefinition;
+
+ this.unwatchExtensionProxyContextLoad = null;
+
+ this.waitForTopLevelContext = new Promise(resolve => {
+ this.resolveTopLevelContext = resolve;
+ });
+ }
+
+ build() {
+ return Task.spawn(function* () {
+ yield this.createBrowserElement();
+
+ // Listening to new proxy contexts.
+ this.unwatchExtensionProxyContextLoad = watchExtensionProxyContextLoad(this, context => {
+ // Keep track of the toolbox and target associated to the context, which is
+ // needed by the API methods implementation.
+ context.devToolsToolbox = this.toolbox;
+
+ if (!this.topLevelContext) {
+ this.topLevelContext = context;
+
+ // Ensure this devtools page is destroyed, when the top level context proxy is
+ // closed.
+ this.topLevelContext.callOnClose(this);
+
+ this.resolveTopLevelContext(context);
+ }
+ });
+
+ extensions.emit("extension-browser-inserted", this.browser, {
+ devtoolsToolboxInfo: {
+ inspectedWindowTabId: getTargetTabIdForToolbox(this.toolbox),
+ },
+ });
+
+ this.browser.loadURI(this.url);
+
+ yield this.waitForTopLevelContext;
+ }.bind(this));
+ }
+
+ close() {
+ if (this.closed) {
+ throw new Error("Unable to shutdown a closed DevToolsPage instance");
+ }
+
+ this.closed = true;
+
+ // Unregister the devtools page instance from the devtools page definition.
+ this.devToolsPageDefinition.forgetForTarget(this.toolbox.target);
+
+ // Unregister it from the resources to cleanup when the context has been closed.
+ if (this.topLevelContext) {
+ this.topLevelContext.forgetOnClose(this);
+ }
+
+ // Stop watching for any new proxy contexts from the devtools page.
+ if (this.unwatchExtensionProxyContextLoad) {
+ this.unwatchExtensionProxyContextLoad();
+ this.unwatchExtensionProxyContextLoad = null;
+ }
+
+ super.shutdown();
+ }
+}
+
+/**
+ * The DevToolsPageDefinitions class represents the "devtools_page" manifest property
+ * of a WebExtension.
+ *
+ * A DevToolsPageDefinition instance is created automatically when a WebExtension
+ * which contains the "devtools_page" manifest property has been loaded, and it is
+ * automatically destroyed when the related WebExtension has been unloaded,
+ * and so there will be at most one DevtoolsPageDefinition per add-on.
+ *
+ * Every time a developer tools toolbox is opened, the DevToolsPageDefinition creates
+ * and keep track of a DevToolsPage instance (which represents the actual devtools_page
+ * instance related to that particular toolbox).
+ *
+ * @param {Extension} extension
+ * The extension that owns the devtools_page.
+ * @param {string} url
+ * The path to the devtools page html page relative to the extension base URL.
+ */
+class DevToolsPageDefinition {
+ constructor(extension, url) {
+ initDevTools();
+
+ this.url = url;
+ this.extension = extension;
+
+ // Map[TabTarget -> DevToolsPage]
+ this.devtoolsPageForTarget = new Map();
+ }
+
+ buildForToolbox(toolbox) {
+ if (this.devtoolsPageForTarget.has(toolbox.target)) {
+ return Promise.reject(new Error("DevtoolsPage has been already created for this toolbox"));
+ }
+
+ const devtoolsPage = new DevToolsPage(this.extension, {
+ toolbox, url: this.url, devToolsPageDefinition: this,
+ });
+ this.devtoolsPageForTarget.set(toolbox.target, devtoolsPage);
+
+ return devtoolsPage.build();
+ }
+
+ shutdownForTarget(target) {
+ if (this.devtoolsPageForTarget.has(target)) {
+ const devtoolsPage = this.devtoolsPageForTarget.get(target);
+ devtoolsPage.close();
+
+ // `devtoolsPage.close()` should remove the instance from the map,
+ // raise an exception if it is still there.
+ if (this.devtoolsPageForTarget.has(target)) {
+ throw new Error(`Leaked DevToolsPage instance for target "${target.toString()}"`);
+ }
+ }
+ }
+
+ forgetForTarget(target) {
+ this.devtoolsPageForTarget.delete(target);
+ }
+
+ shutdown() {
+ for (let target of this.devtoolsPageForTarget.keys()) {
+ this.shutdownForTarget(target);
+ }
+
+ if (this.devtoolsPageForTarget.size > 0) {
+ throw new Error(
+ `Leaked ${this.devtoolsPageForTarget.size} DevToolsPage instances in devtoolsPageForTarget Map`
+ );
+ }
+ }
+}
+
+/* eslint-disable mozilla/balanced-listeners */
+
+let devToolsInitialized = false;
+initDevTools = function() {
+ if (devToolsInitialized) {
+ return;
+ }
+
+ // Create a devtools page context for a new opened toolbox,
+ // based on the registered devtools_page definitions.
+ gDevTools.on("toolbox-created", (evt, toolbox) => {
+ if (!toolbox.target.isLocalTab) {
+ // Only local tabs are currently supported (See Bug 1304378 for additional details
+ // related to remote targets support).
+ let msg = `Ignoring DevTools Toolbox for target "${toolbox.target.toString()}": ` +
+ `"${toolbox.target.name}" ("${toolbox.target.url}"). ` +
+ "Only local tab are currently supported by the WebExtensions DevTools API.";
+ let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+ scriptError.init(msg, null, null, null, null, Ci.nsIScriptError.warningFlag, "content javascript");
+ let consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
+ consoleService.logMessage(scriptError);
+
+ return;
+ }
+
+ for (let devtoolsPage of devtoolsPageDefinitionMap.values()) {
+ devtoolsPage.buildForToolbox(toolbox);
+ }
+ });
+
+ // Destroy a devtools page context for a destroyed toolbox,
+ // based on the registered devtools_page definitions.
+ gDevTools.on("toolbox-destroy", (evt, target) => {
+ if (!target.isLocalTab) {
+ // Only local tabs are currently supported (See Bug 1304378 for additional details
+ // related to remote targets support).
+ return;
+ }
+
+ for (let devtoolsPageDefinition of devtoolsPageDefinitionMap.values()) {
+ devtoolsPageDefinition.shutdownForTarget(target);
+ }
+ });
+
+ devToolsInitialized = true;
+};
+
+// Create and register a new devtools_page definition as specified in the
+// "devtools_page" property in the extension manifest.
+extensions.on("manifest_devtools_page", (type, directive, extension, manifest) => {
+ let devtoolsPageDefinition = new DevToolsPageDefinition(extension, manifest[directive]);
+ devtoolsPageDefinitionMap.set(extension, devtoolsPageDefinition);
+});
+
+// Destroy the registered devtools_page definition on extension shutdown.
+extensions.on("shutdown", (type, extension) => {
+ if (devtoolsPageDefinitionMap.has(extension)) {
+ devtoolsPageDefinitionMap.get(extension).shutdown();
+ devtoolsPageDefinitionMap.delete(extension);
+ }
+});
+/* eslint-enable mozilla/balanced-listeners */