Bug 1411565 - about:debugging connect to remote runtime using url parameters draft
authorJulian Descottes <jdescottes@mozilla.com>
Mon, 23 Oct 2017 10:15:40 +0200
changeset 686232 3ff651ffa561af3b59b8baaa7e04b3aed03ea229
parent 686146 4985a43af0fb4b3bb64be6bf99b1d0cbef82eced
child 737334 3acfa4f52ad18ce7518d0c2dc64e4dffd852d4d5
push id86133
push userjdescottes@mozilla.com
push dateWed, 25 Oct 2017 16:13:53 +0000
bugs1411565
milestone58.0a1
Bug 1411565 - about:debugging connect to remote runtime using url parameters MozReview-Commit-ID: DOekSCb96XC
devtools/client/aboutdebugging/components/aboutdebugging.js
devtools/client/aboutdebugging/components/addons/panel.js
devtools/client/aboutdebugging/components/addons/target.js
devtools/client/aboutdebugging/components/tabs/panel.js
devtools/client/aboutdebugging/components/tabs/target.js
devtools/client/aboutdebugging/components/target-list.js
devtools/client/aboutdebugging/initializer.js
devtools/client/aboutdebugging/modules/addon.js
devtools/client/aboutdebugging/modules/connect.js
devtools/client/aboutdebugging/modules/moz.build
--- a/devtools/client/aboutdebugging/components/aboutdebugging.js
+++ b/devtools/client/aboutdebugging/components/aboutdebugging.js
@@ -46,16 +46,17 @@ const panels = [{
 
 const defaultPanelId = "addons";
 
 module.exports = createClass({
   displayName: "AboutDebuggingApp",
 
   propTypes: {
     client: PropTypes.instanceOf(DebuggerClient).isRequired,
+    connect: PropTypes.object.isRequired,
     telemetry: PropTypes.instanceOf(Telemetry).isRequired
   },
 
   getInitialState() {
     return {
       selectedPanelId: defaultPanelId
     };
   },
@@ -78,24 +79,24 @@ module.exports = createClass({
     });
   },
 
   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/addons/panel.js
+++ b/devtools/client/aboutdebugging/components/addons/panel.js
@@ -27,16 +27,17 @@ const REMOTE_ENABLED_PREF = "devtools.de
 const WEB_EXT_URL = "https://developer.mozilla.org/Add-ons" +
                     "/WebExtensions/Getting_started_with_web-ext";
 
 module.exports = createClass({
   displayName: "AddonsPanel",
 
   propTypes: {
     client: PropTypes.instanceOf(DebuggerClient).isRequired,
+    connect: PropTypes.object,
     id: PropTypes.string.isRequired
   },
 
   getInitialState() {
     return {
       extensions: [],
       debugDisabled: false,
     };
@@ -75,23 +76,24 @@ module.exports = createClass({
     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,
+            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);
       });
@@ -121,17 +123,17 @@ module.exports = createClass({
   /**
    * 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);
 
@@ -147,16 +149,17 @@ module.exports = createClass({
     }),
     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")),
@@ -166,15 +169,16 @@ module.exports = createClass({
       )
     ),
     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
@@ -122,31 +122,33 @@ function warningMessages(warnings = []) 
   });
 }
 
 module.exports = createClass({
   displayName: "AddonTarget",
 
   propTypes: {
     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
   },
 
   debug() {
-    let { target } = this.props;
-    debugAddon(target.addonID);
+    let { target, client, connect } = this.props;
+    debugAddon(target, connect, client);
   },
 
   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");
 
 module.exports = createClass({
   displayName: "TabsPanel",
 
   propTypes: {
     client: PropTypes.instanceOf(DebuggerClient).isRequired,
+    connect: PropTypes.object,
     id: PropTypes.string.isRequired
   },
 
   getInitialState() {
     return {
       tabs: []
     };
   },
@@ -66,32 +67,33 @@ module.exports = createClass({
           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,26 +13,32 @@ const Services = require("Services");
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 module.exports = createClass({
   displayName: "TabTarget",
 
   propTypes: {
     target: PropTypes.shape({
+      connect: PropTypes.object,
       icon: PropTypes.string,
       outerWindowID: PropTypes.number.isRequired,
       title: PropTypes.string,
       url: PropTypes.string.isRequired
     }).isRequired
   },
 
   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=${host}&port=${port}`;
+    }
+    window.open(url);
   },
 
   render() {
     let { target } = this.props;
 
     return dom.div({ className: "target-container" },
       dom.img({
         className: "target-icon",
--- a/devtools/client/aboutdebugging/components/target-list.js
+++ b/devtools/client/aboutdebugging/components/target-list.js
@@ -18,32 +18,42 @@ const LocaleCompare = (a, b) => {
   return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
 };
 
 module.exports = createClass({
   displayName: "TargetList",
 
   propTypes: {
     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/initializer.js
+++ b/devtools/client/aboutdebugging/initializer.js
@@ -1,66 +1,57 @@
 /* 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;
     }
 
-    if (!DebuggerServer.initialized) {
-      DebuggerServer.init();
-    }
-    DebuggerServer.allowChromeProcess = true;
-    // We want a full featured server for about:debugging. Especially the
-    // "browser actors" like addons.
-    DebuggerServer.registerActors({ root: true, browser: true, tab: true });
+    let {connect, client} = await createClient();
 
-    this.client = new DebuggerClient(DebuggerServer.connectPipe());
+    if (client !== null) {
+      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,30 +4,67 @@
 
 "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");
 
+var {TargetFactory} = require("devtools/client/framework/target");
+var {Toolbox} = require("devtools/client/framework/toolbox");
+
+var {gDevTools} = require("devtools/client/framework/devtools");
+
 let toolbox = null;
 
-exports.debugAddon = function (addonID) {
+/**
+ * Start debugging the addon corresponding the provided addonTarget.
+ *
+ * @param {Object} addonTarget
+ *        See PropTypes of components/addons/target.js.
+ * @param {Object} connect
+ *        Connection info about the current about:debugging target.
+ * @param {DebuggerClient} client
+ *        Required for remote debugging.
+ */
+exports.debugAddon = function (addonTarget, connect, client) {
   if (toolbox) {
     toolbox.close();
   }
 
+  if (connect.type === "REMOTE") {
+    debugRemoteAddon(addonTarget.form, client);
+  } else if (connect.type === "LOCAL") {
+    debugLocalAddon(addonTarget.addonID);
+  }
+};
+
+async function debugLocalAddon(addonID) {
   toolbox = BrowserToolboxProcess.init({
     addonID,
     onClose: () => {
       toolbox = null;
     }
   });
-};
+}
+
+async function debugRemoteAddon(addonForm, client) {
+  let options = {
+    form: addonForm,
+    chrome: true,
+    client,
+    isTabActor: addonForm.isWebExtension
+  };
+
+  let target = await TargetFactory.forRemoteTab(options);
+
+  let hostType = Toolbox.HostType.WINDOW;
+  gDevTools.showToolbox(target, "webconsole", hostType);
+}
 
 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,106 @@
+/* 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";
+
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+  "devtools/shared/client/debugger-client", true);
+loader.lazyRequireGetter(this, "DebuggerServer",
+  "devtools/server/main", true);
+
+// 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 connect object describing the current about:debugging target.
+ *
+ * @returns {Object}
+ *          - type {String} target connection type, from the TYPE const.
+ *          - params {Object} various connection parameters that might be needed to create
+ *            the connection
+ */
+function getConnectData() {
+  let connect = {};
+  let href = window.location.href;
+
+  // URL constructor doesn't support about: scheme
+  let url = new window.URL(href.replace("about:", "http://"));
+
+  let params = url.searchParams;
+  if (params.has("host") && params.has("port")) {
+    // Remote debugging: host + port => about:debugging?host=localhost&port=9000
+    connect.type = TYPE.REMOTE;
+    connect.params = {
+      host: params.get("host"),
+      port: params.get("port"),
+    };
+  } else {
+    // TODO: support remote device debugging using parameters such as 'runtime' and 'type'
+    connect.type = TYPE.LOCAL;
+  }
+
+  return connect;
+}
+
+/**
+ * Connect to an existing DebuggerServer listening on the provided host and port.
+ * Returns a promise that resolves a DebuggerTransport.
+ */
+function createRemoteTransport(host, port) {
+  Services.prefs.setCharPref("devtools.debugger.remote-host", host);
+  Services.prefs.setIntPref("devtools.debugger.remote-port", port);
+  return DebuggerClient.socketConnect({ host, port });
+}
+
+/**
+ * Create a DebuggerServer and connect to it.
+ * Returns a DebuggerTransport.
+ */
+function createLocalTransport() {
+  // Current Firefox instance debugging
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init();
+    DebuggerServer.addBrowserActors();
+  }
+  DebuggerServer.allowChromeProcess = true;
+
+  // We want a full featured server for about:debugging. Especially the
+  // "browser actors" like addons.
+  DebuggerServer.registerActors({ root: true, browser: true, tab: true });
+
+  return DebuggerServer.connectPipe();
+}
+
+/**
+ * Create a client to debug the about:debugging target.
+ *
+ * @returns {Object}
+ *          - client: {DebuggerClient}
+ *          - connect: {Object}
+ */
+exports.createClient = async function () {
+  let connect = getConnectData();
+
+  let transport;
+  if (connect.type == TYPE.REMOTE) {
+    let {host, port} = connect.params;
+    transport = await createRemoteTransport(host, port);
+  } else {
+    transport = await createLocalTransport();
+  }
+
+  return {
+    client: new DebuggerClient(transport),
+    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',
 )