Bug 1411565 - about:debugging connect to remote runtime using url parameters;r=ochameau
This changeset adds basic remote connection functionality to about:debugging.
About:debugging can target a remote firefox instance if the host and port
parameters are passed as URL search params.
The feature is not explicitly exposed at the moment and there is no UI to
connect an instance, and no UI feedback when connected to a remote instance.
When connected, about:debugging should correctly list tabs, workers and addons
for the target instance of Firefox. Debugging features work for all supported
targets.
Known limitations:
- preferences are read from the local Firefox instance (multiprocess, addon
debugging etc...). At the moment the remote instance must be manually
correctly configured
MozReview-Commit-ID: DOekSCb96XC
--- a/devtools/client/aboutdebugging/components/Aboutdebugging.js
+++ b/devtools/client/aboutdebugging/components/Aboutdebugging.js
@@ -46,16 +46,17 @@ const panels = [{
}];
const defaultPanelId = "addons";
class AboutDebuggingApp extends Component {
static get propTypes() {
return {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ connect: PropTypes.object.isRequired,
telemetry: PropTypes.instanceOf(Telemetry).isRequired
};
}
constructor(props) {
super(props);
this.state = {
@@ -84,24 +85,24 @@ class AboutDebuggingApp extends Componen
});
}
selectPanel(panelId) {
window.location.hash = "#" + panelId;
}
render() {
- let { client } = this.props;
+ let { client, connect } = this.props;
let { selectedPanelId } = this.state;
let selectPanel = this.selectPanel;
let selectedPanel = panels.find(p => p.id == selectedPanelId);
let panel;
if (selectedPanel) {
- panel = selectedPanel.component({ client, id: selectedPanel.id });
+ panel = selectedPanel.component({ client, connect, id: selectedPanel.id });
} else {
panel = (
dom.div({ className: "error-page" },
dom.h1({ className: "header-name" },
Strings.GetStringFromName("pageNotFound")
),
dom.h4({ className: "error-page-details" },
Strings.formatStringFromName("doesNotExist", [selectedPanelId], 1))
--- a/devtools/client/aboutdebugging/components/TargetList.js
+++ b/devtools/client/aboutdebugging/components/TargetList.js
@@ -18,33 +18,43 @@ const Strings = Services.strings.createB
const LocaleCompare = (a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
};
class TargetList extends Component {
static get propTypes() {
return {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ connect: PropTypes.object,
debugDisabled: PropTypes.bool,
error: PropTypes.node,
id: PropTypes.string.isRequired,
name: PropTypes.string,
sort: PropTypes.bool,
targetClass: PropTypes.func.isRequired,
targets: PropTypes.arrayOf(PropTypes.object).isRequired
};
}
render() {
- let { client, debugDisabled, error, targetClass, targets, sort } = this.props;
+ let {
+ client,
+ connect,
+ debugDisabled,
+ error,
+ targetClass,
+ targets,
+ sort
+ } = this.props;
+
if (sort) {
targets = targets.sort(LocaleCompare);
}
targets = targets.map(target => {
- return targetClass({ client, target, debugDisabled });
+ return targetClass({ client, connect, target, debugDisabled });
});
let content = "";
if (error) {
content = error;
} else if (targets.length > 0) {
content = dom.ul({ className: "target-list" }, targets);
} else {
--- a/devtools/client/aboutdebugging/components/addons/Panel.js
+++ b/devtools/client/aboutdebugging/components/addons/Panel.js
@@ -27,16 +27,17 @@ const CHROME_ENABLED_PREF = "devtools.ch
const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
const WEB_EXT_URL = "https://developer.mozilla.org/Add-ons" +
"/WebExtensions/Getting_started_with_web-ext";
class AddonsPanel extends Component {
static get propTypes() {
return {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ connect: PropTypes.object,
id: PropTypes.string.isRequired
};
}
constructor(props) {
super(props);
this.state = {
@@ -85,23 +86,25 @@ class AddonsPanel extends Component {
this.setState({ debugDisabled });
}
updateAddonsList() {
this.props.client.listAddons()
.then(({addons}) => {
let extensions = addons.filter(addon => addon.debuggable).map(addon => {
return {
- name: addon.name,
+ addonActor: addon.actor,
+ addonID: addon.id,
+ // Forward the whole addon actor form for potential remote debugging.
+ form: addon,
icon: addon.iconURL || ExtensionIcon,
- addonID: addon.id,
- addonActor: addon.actor,
+ manifestURL: addon.manifestURL,
+ name: addon.name,
temporarilyInstalled: addon.temporarilyInstalled,
url: addon.url,
- manifestURL: addon.manifestURL,
warnings: addon.warnings,
};
});
this.setState({ extensions });
}, error => {
throw new Error("Client error while listing addons: " + error);
});
@@ -131,17 +134,17 @@ class AddonsPanel extends Component {
/**
* Mandatory callback as AddonManager listener.
*/
onDisabled() {
this.updateAddonsList();
}
render() {
- let { client, id } = this.props;
+ let { client, connect, id } = this.props;
let { debugDisabled, extensions: targets } = this.state;
let installedName = Strings.GetStringFromName("extensions");
let temporaryName = Strings.GetStringFromName("temporaryExtensions");
let targetClass = AddonTarget;
const installedTargets = targets.filter((target) => !target.temporarilyInstalled);
const temporaryTargets = targets.filter((target) => target.temporarilyInstalled);
@@ -157,16 +160,17 @@ class AddonsPanel extends Component {
}),
AddonsControls({ debugDisabled }),
dom.div({ id: "temporary-addons" },
TargetList({
id: "temporary-extensions",
name: temporaryName,
targets: temporaryTargets,
client,
+ connect,
debugDisabled,
targetClass,
sort: true
}),
dom.div({ className: "addons-tip"},
dom.span({
className: "addons-web-ext-tip",
}, Strings.GetStringFromName("webExtTip")),
@@ -176,16 +180,17 @@ class AddonsPanel extends Component {
)
),
dom.div({ id: "addons" },
TargetList({
id: "extensions",
name: installedName,
targets: installedTargets,
client,
+ connect,
debugDisabled,
targetClass,
sort: true
})
));
}
}
--- a/devtools/client/aboutdebugging/components/addons/Target.js
+++ b/devtools/client/aboutdebugging/components/addons/Target.js
@@ -4,17 +4,17 @@
/* eslint-env browser */
"use strict";
const { Component } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
-const { debugAddon, isTemporaryID, parseFileUri, uninstallAddon } =
+const { debugLocalAddon, debugRemoteAddon, isTemporaryID, parseFileUri, uninstallAddon } =
require("../../modules/addon");
const Services = require("Services");
loader.lazyImporter(this, "BrowserToolboxProcess",
"resource://devtools/client/framework/ToolboxProcess.jsm");
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
@@ -122,20 +122,22 @@ function warningMessages(warnings = [])
warning);
});
}
class AddonTarget extends Component {
static get propTypes() {
return {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ connect: PropTypes.object,
debugDisabled: PropTypes.bool,
target: PropTypes.shape({
addonActor: PropTypes.string.isRequired,
addonID: PropTypes.string.isRequired,
+ form: PropTypes.object.isRequired,
icon: PropTypes.string,
name: PropTypes.string.isRequired,
temporarilyInstalled: PropTypes.bool,
url: PropTypes.string,
warnings: PropTypes.array,
}).isRequired
};
}
@@ -143,18 +145,23 @@ class AddonTarget extends Component {
constructor(props) {
super(props);
this.debug = this.debug.bind(this);
this.uninstall = this.uninstall.bind(this);
this.reload = this.reload.bind(this);
}
debug() {
- let { target } = this.props;
- debugAddon(target.addonID);
+ let { client, connect, target } = this.props;
+
+ if (connect.type === "REMOTE") {
+ debugRemoteAddon(target.form, client);
+ } else if (connect.type === "LOCAL") {
+ debugLocalAddon(target.addonID);
+ }
}
uninstall() {
let { target } = this.props;
uninstallAddon(target.addonID);
}
reload() {
--- a/devtools/client/aboutdebugging/components/tabs/Panel.js
+++ b/devtools/client/aboutdebugging/components/tabs/Panel.js
@@ -20,16 +20,17 @@ loader.lazyRequireGetter(this, "Debugger
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
class TabsPanel extends Component {
static get propTypes() {
return {
client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ connect: PropTypes.object,
id: PropTypes.string.isRequired
};
}
constructor(props) {
super(props);
this.state = {
@@ -71,32 +72,33 @@ class TabsPanel extends Component {
tab.icon = "chrome://devtools/skin/images/globe.svg";
}
});
this.setState({ tabs });
});
}
render() {
- let { client, id } = this.props;
+ let { client, connect, id } = this.props;
let { tabs } = this.state;
return dom.div({
id: id + "-panel",
className: "panel",
role: "tabpanel",
"aria-labelledby": id + "-header"
},
PanelHeader({
id: id + "-header",
name: Strings.GetStringFromName("tabs")
}),
dom.div({},
TargetList({
client,
+ connect,
id: "tabs",
name: Strings.GetStringFromName("tabs"),
sort: false,
targetClass: TabTarget,
targets: tabs
})
));
}
--- a/devtools/client/aboutdebugging/components/tabs/Target.js
+++ b/devtools/client/aboutdebugging/components/tabs/Target.js
@@ -13,33 +13,39 @@ const dom = require("devtools/client/sha
const Services = require("Services");
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
class TabTarget extends Component {
static get propTypes() {
return {
+ connect: PropTypes.object,
target: PropTypes.shape({
icon: PropTypes.string,
outerWindowID: PropTypes.number.isRequired,
title: PropTypes.string,
url: PropTypes.string.isRequired
}).isRequired
};
}
constructor(props) {
super(props);
this.debug = this.debug.bind(this);
}
debug() {
- let { target } = this.props;
- window.open("about:devtools-toolbox?type=tab&id=" + target.outerWindowID);
+ let { target, connect } = this.props;
+ let url = "about:devtools-toolbox?type=tab&id=" + target.outerWindowID;
+ if (connect.type == "REMOTE") {
+ let {host, port} = connect.params;
+ url += `&host=${encodeURIComponent(host)}&port=${encodeURIComponent(port)}`;
+ }
+ window.open(url);
}
render() {
let { target } = this.props;
return dom.div({ className: "target-container" },
dom.img({
className: "target-icon",
--- a/devtools/client/aboutdebugging/initializer.js
+++ b/devtools/client/aboutdebugging/initializer.js
@@ -1,64 +1,55 @@
/* 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/. */
/* eslint-env browser */
-/* globals DebuggerClient, DebuggerServer, Telemetry */
+/* globals Telemetry */
"use strict";
const { loader } = Components.utils.import(
"resource://devtools/shared/Loader.jsm", {});
const { BrowserLoader } = Components.utils.import(
"resource://devtools/client/shared/browser-loader.js", {});
const { Services } = Components.utils.import(
"resource://gre/modules/Services.jsm", {});
-loader.lazyRequireGetter(this, "DebuggerClient",
- "devtools/shared/client/debugger-client", true);
-loader.lazyRequireGetter(this, "DebuggerServer",
- "devtools/server/main", true);
loader.lazyRequireGetter(this, "Telemetry",
"devtools/client/shared/telemetry");
const { require } = BrowserLoader({
baseURI: "resource://devtools/client/aboutdebugging/",
window
});
const { createFactory } = require("devtools/client/shared/vendor/react");
const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
const AboutDebuggingApp = createFactory(require("./components/Aboutdebugging"));
+const { createClient } = require("./modules/connect");
var AboutDebugging = {
- init() {
+ async init() {
if (!Services.prefs.getBoolPref("devtools.enabled", true)) {
// If DevTools are disabled, navigate to about:devtools.
window.location = "about:devtools?reason=AboutDebugging";
return;
}
- DebuggerServer.init();
- DebuggerServer.allowChromeProcess = true;
- // We want a full featured server for about:debugging. Especially the
- // "browser actors" like addons.
- DebuggerServer.registerAllActors();
+ let {connect, client} = await createClient();
- this.client = new DebuggerClient(DebuggerServer.connectPipe());
+ this.client = client;
+ await this.client.connect();
- this.client.connect().then(() => {
- let client = this.client;
- let telemetry = new Telemetry();
+ let telemetry = new Telemetry();
- render(AboutDebuggingApp({ client, telemetry }),
- document.querySelector("#body"));
- });
+ render(AboutDebuggingApp({ client, connect, telemetry }),
+ document.querySelector("#body"));
},
destroy() {
unmountComponentAtNode(document.querySelector("#body"));
if (this.client) {
this.client.close();
this.client = null;
--- a/devtools/client/aboutdebugging/modules/addon.js
+++ b/devtools/client/aboutdebugging/modules/addon.js
@@ -4,31 +4,79 @@
"use strict";
loader.lazyImporter(this, "BrowserToolboxProcess",
"resource://devtools/client/framework/ToolboxProcess.jsm");
loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
loader.lazyImporter(this, "AddonManagerPrivate", "resource://gre/modules/AddonManager.jsm");
-let toolbox = null;
+var {TargetFactory} = require("devtools/client/framework/target");
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+var {gDevTools} = require("devtools/client/framework/devtools");
-exports.debugAddon = function (addonID) {
- if (toolbox) {
- toolbox.close();
+let browserToolboxProcess = null;
+let remoteAddonToolbox = null;
+function closeToolbox() {
+ if (browserToolboxProcess) {
+ browserToolboxProcess.close();
}
- toolbox = BrowserToolboxProcess.init({
+ if (remoteAddonToolbox) {
+ remoteAddonToolbox.destroy();
+ }
+}
+
+/**
+ * Start debugging an addon in the current instance of Firefox.
+ *
+ * @param {String} addonID
+ * String id of the addon to debug.
+ */
+exports.debugLocalAddon = async function (addonID) {
+ // Close previous addon debugging toolbox.
+ closeToolbox();
+
+ browserToolboxProcess = BrowserToolboxProcess.init({
addonID,
onClose: () => {
- toolbox = null;
+ browserToolboxProcess = null;
}
});
};
+/**
+ * Start debugging an addon in a remote instance of Firefox.
+ *
+ * @param {Object} addonForm
+ * Necessary to create an addon debugging target.
+ * @param {DebuggerClient} client
+ * Required for remote debugging.
+ */
+exports.debugRemoteAddon = async function (addonForm, client) {
+ // Close previous addon debugging toolbox.
+ closeToolbox();
+
+ let options = {
+ form: addonForm,
+ chrome: true,
+ client,
+ isTabActor: addonForm.isWebExtension
+ };
+
+ let target = await TargetFactory.forRemoteTab(options);
+
+ let hostType = Toolbox.HostType.WINDOW;
+ remoteAddonToolbox = await gDevTools.showToolbox(target, null, hostType);
+ remoteAddonToolbox.once("destroy", () => {
+ remoteAddonToolbox = null;
+ });
+};
+
exports.uninstallAddon = async function (addonID) {
let addon = await AddonManager.getAddonByID(addonID);
return addon && addon.uninstall();
};
exports.isTemporaryID = function (addonID) {
return AddonManagerPrivate.isTemporaryInstallID(addonID);
};
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/modules/connect.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+/**
+ * The connect module creates a connection to a debugger server based on the current
+ * context (e.g. URL parameters).
+ */
+
+const { clientFromURL } = require("devtools/client/framework/target-from-url");
+const { DebuggerServer } = require("devtools/server/main");
+
+// Supported connection types
+const TYPE = {
+ // Default, connected to the current instance of Firefox
+ LOCAL: "LOCAL",
+ // Connected to a remote instance of Firefox via host&port settings.
+ REMOTE: "REMOTE",
+};
+
+/**
+ * Create a plain object containing the connection information relevant to aboutdebugging
+ * components.
+ *
+ * @returns {Object}
+ * - type: {String} from TYPE ("LOCAL", "REMOTE")
+ * - params: {Object} additional metadata depending on the type.
+ * - if type === "LOCAL", empty object
+ * - if type === "REMOTE", {host: {String}, port: {String}}
+ */
+function createDescriptorFromURL(url) {
+ let params = url.searchParams;
+
+ let host = params.get("host");
+ let port = params.get("port");
+
+ let descriptor;
+ if (host && port) {
+ descriptor = {
+ type: TYPE.REMOTE,
+ params: {host, port}
+ };
+ } else {
+ descriptor = {
+ type: TYPE.LOCAL,
+ params: {}
+ };
+ }
+
+ return descriptor;
+}
+
+/**
+ * Returns a promise that resolves after creating a debugger client corresponding to the
+ * provided options.
+ *
+ * @returns Promise that resolves an object with the following properties:
+ * - client: a DebuggerClient instance
+ * - connect: a connection descriptor, see doc for createDescriptorFromURL(url).
+ */
+exports.createClient = async function () {
+ let href = window.location.href;
+ let url = new window.URL(href.replace("about:", "http://"));
+
+ let connect = createDescriptorFromURL(url);
+ let client = await clientFromURL(url);
+
+ DebuggerServer.allowChromeProcess = true;
+
+ return {client, connect};
+};
--- a/devtools/client/aboutdebugging/modules/moz.build
+++ b/devtools/client/aboutdebugging/modules/moz.build
@@ -1,8 +1,9 @@
# 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/.
DevToolsModules(
'addon.js',
+ 'connect.js',
'worker.js',
)
--- a/devtools/client/framework/target-from-url.js
+++ b/devtools/client/framework/target-from-url.js
@@ -2,93 +2,85 @@
* 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 { TargetFactory } = require("devtools/client/framework/target");
const { DebuggerServer } = require("devtools/server/main");
const { DebuggerClient } = require("devtools/shared/client/debugger-client");
-const { Task } = require("devtools/shared/task");
/**
* Construct a Target for a given URL object having various query parameters:
*
- * host:
- * {String} The hostname or IP address to connect to.
- * port:
- * {Number} The TCP port to connect to, to use with `host` argument.
- * ws:
- * {Boolean} If true, connect via websocket instread of regular TCP connection.
+ * - host, port & ws: See the documentation for clientFromURL
*
- * type: tab, process, window
- * {String} The type of target to connect to.
+ * - type: tab, process, window
+ * {String} The type of target to connect to.
*
* If type == "tab":
- * id:
- * {Number} the tab outerWindowID
- * chrome: Optional
- * {Boolean} Force the creation of a chrome target. Gives more privileges to
- * the tab actor. Allows chrome execution in the webconsole and see chrome
- * files in the debugger. (handy when contributing to firefox)
+ * - id:
+ * {Number} the tab outerWindowID
+ * - chrome: Optional
+ * {Boolean} Force the creation of a chrome target. Gives more privileges to
+ * the tab actor. Allows chrome execution in the webconsole and see chrome
+ * files in the debugger. (handy when contributing to firefox)
*
* If type == "process":
- * id:
- * {Number} the process id to debug. Default to 0, which is the parent
- * process.
+ * - id:
+ * {Number} the process id to debug. Default to 0, which is the parent process.
*
* If type == "window":
- * id:
- * {Number} the window outerWindowID
+ * - id:
+ * {Number} the window outerWindowID
*
* @param {URL} url
* The url to fetch query params from.
*
* @return A target object
*/
-exports.targetFromURL = Task.async(function* (url) {
+exports.targetFromURL = async function targetFromURL(url) {
+ let client = await clientFromURL(url);
+ await client.connect();
+
let params = url.searchParams;
let type = params.get("type");
if (!type) {
throw new Error("targetFromURL, missing type parameter");
}
let id = params.get("id");
// Allows to spawn a chrome enabled target for any context
// (handy to debug chrome stuff in a child process)
let chrome = params.has("chrome");
- let client = yield createClient(params);
-
- yield client.connect();
-
let form, isTabActor;
if (type === "tab") {
// Fetch target for a remote tab
id = parseInt(id, 10);
if (isNaN(id)) {
throw new Error(`targetFromURL, wrong tab id '${id}', should be a number`);
}
try {
- let response = yield client.getTab({ outerWindowID: id });
+ let response = await client.getTab({ outerWindowID: id });
form = response.tab;
} catch (ex) {
if (ex.error == "noTab") {
throw new Error(`targetFromURL, tab with outerWindowID '${id}' doesn't exist`);
}
throw ex;
}
} else if (type == "process") {
// Fetch target for a remote chrome actor
DebuggerServer.allowChromeProcess = true;
try {
id = parseInt(id, 10);
if (isNaN(id)) {
id = 0;
}
- let response = yield client.getProcess(id);
+ let response = await client.getProcess(id);
form = response.form;
chrome = true;
if (id != 0) {
// Child process are not exposing tab actors and only support debugger+console
isTabActor = false;
}
} catch (ex) {
if (ex.error == "noProcess") {
@@ -99,42 +91,59 @@ exports.targetFromURL = Task.async(funct
} else if (type == "window") {
// Fetch target for a remote window actor
DebuggerServer.allowChromeProcess = true;
try {
id = parseInt(id, 10);
if (isNaN(id)) {
throw new Error("targetFromURL, window requires id parameter");
}
- let response = yield client.mainRoot.getWindow({
+ let response = await client.mainRoot.getWindow({
outerWindowID: id,
});
form = response.window;
chrome = true;
} catch (ex) {
if (ex.error == "notFound") {
throw new Error(`targetFromURL, window with id '${id}' doesn't exist`);
}
throw ex;
}
} else {
throw new Error(`targetFromURL, unsupported type '${type}' parameter`);
}
return TargetFactory.forRemoteTab({ client, form, chrome, isTabActor });
-});
+};
-function* createClient(params) {
+/**
+ * Create a DebuggerClient for a given URL object having various query parameters:
+ *
+ * host:
+ * {String} The hostname or IP address to connect to.
+ * port:
+ * {Number} The TCP port to connect to, to use with `host` argument.
+ * ws:
+ * {Boolean} If true, connect via websocket instead of regular TCP connection.
+ *
+ * @param {URL} url
+ * The url to fetch query params from.
+ * @return a promise that resolves a DebuggerClient object
+ */
+async function clientFromURL(url) {
+ let params = url.searchParams;
let host = params.get("host");
let port = params.get("port");
let webSocket = !!params.get("ws");
let transport;
if (port) {
- transport = yield DebuggerClient.socketConnect({ host, port, webSocket });
+ transport = await DebuggerClient.socketConnect({ host, port, webSocket });
} else {
// Setup a server if we don't have one already running
DebuggerServer.init();
DebuggerServer.registerAllActors();
transport = DebuggerServer.connectPipe();
}
return new DebuggerClient(transport);
}
+
+exports.clientFromURL = clientFromURL;