Backed out 7 changesets (bug 1506546) fo failing devtools at devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_system_addons.js on a CLOSED TREE
authorAndreea Pavel <apavel@mozilla.com>
Tue, 27 Nov 2018 14:37:14 +0200
changeset 504684 c19baae766e96ffbc7bf20534e1ba62b64871fe3
parent 504683 2f8233496ee8d83688e27d266e5dd61ba29ad5b1
child 504685 c779bf180e26500b0a907c55f992be2121fc4ee9
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1506546
milestone65.0a1
backs outfbe3f2183f32134a867db07f07352fb33428ea3c
8972a2f1401568be3e4af284ac02af215f330209
00fe26234b3da593bda143f9f7836b94c33f128e
7d8e650e25c2fc0070c7ca5e4a9d851b561d9734
a293a37483b91f1040015918719a3092dc9be2da
7b75250a4f1274929a2e02d33f2d28fce8bd5f5b
23ad29cb776e431cba6d99d862aa7eb840d611ea
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 7 changesets (bug 1506546) fo failing devtools at devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_system_addons.js on a CLOSED TREE Backed out changeset fbe3f2183f32 (bug 1506546) Backed out changeset 8972a2f14015 (bug 1506546) Backed out changeset 00fe26234b3d (bug 1506546) Backed out changeset 7d8e650e25c2 (bug 1506546) Backed out changeset a293a37483b9 (bug 1506546) Backed out changeset 7b75250a4f12 (bug 1506546) Backed out changeset 23ad29cb776e (bug 1506546)
devtools/client/aboutdebugging-new/src/actions/debug-targets.js
devtools/client/aboutdebugging-new/src/components/debugtarget/TemporaryExtensionAction.js
devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
devtools/client/aboutdebugging-new/src/modules/extensions-helper.js
devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_usb_runtime.js
devtools/client/aboutdebugging-new/test/browser/mocks/head-client-wrapper-mock.js
devtools/client/aboutdebugging/components/addons/Panel.js
devtools/client/aboutdebugging/components/addons/Target.js
devtools/client/aboutdebugging/modules/addon.js
devtools/client/debugger/test/mochitest/head.js
devtools/client/framework/connect/connect.js
devtools/client/framework/target.js
devtools/client/framework/toolbox-process-window.js
devtools/client/shared/test/browser_dbg_addon-console.js
devtools/client/shared/test/browser_dbg_listaddons.js
devtools/client/shared/test/helper_addons.js
devtools/server/tests/mochitest/test_webextension-addon-debugging-connect.html
devtools/server/tests/mochitest/webextension-helpers.js
devtools/server/tests/unit/test_addon_events.js
devtools/server/tests/unit/test_addon_reload.js
devtools/server/tests/unit/test_addons_actor.js
devtools/shared/client/addon-client.js
devtools/shared/client/debugger-client.js
devtools/shared/client/moz.build
devtools/shared/fronts/root.js
devtools/shared/fronts/targets/addon.js
devtools/shared/specs/index.js
devtools/shared/specs/root.js
devtools/shared/specs/targets/addon.js
--- a/devtools/client/aboutdebugging-new/src/actions/debug-targets.js
+++ b/devtools/client/aboutdebugging-new/src/actions/debug-targets.js
@@ -98,23 +98,22 @@ function pushServiceWorker(actor) {
     try {
       await clientWrapper.request({ to: actor, type: "push" });
     } catch (e) {
       console.error(e);
     }
   };
 }
 
-function reloadTemporaryExtension(id) {
+function reloadTemporaryExtension(actor) {
   return async (_, getState) => {
     const clientWrapper = getCurrentClient(getState().runtimes);
 
     try {
-      const addonTargetFront = await clientWrapper.getAddon({ id });
-      await addonTargetFront.reload();
+      await clientWrapper.request({ to: actor, type: "reload" });
     } catch (e) {
       console.error(e);
     }
   };
 }
 
 function removeTemporaryExtension(id) {
   return async () => {
@@ -145,17 +144,17 @@ function requestTabs() {
 function requestExtensions() {
   return async (dispatch, getState) => {
     dispatch({ type: REQUEST_EXTENSIONS_START });
 
     const runtime = getCurrentRuntime(getState().runtimes);
     const clientWrapper = getCurrentClient(getState().runtimes);
 
     try {
-      const addons = await clientWrapper.listAddons();
+      const { addons } = await clientWrapper.listAddons();
       let extensions = addons.filter(a => a.debuggable);
 
       // Filter out system addons unless the dedicated preference is set to true.
       if (!getState().ui.showSystemAddons) {
         extensions = extensions.filter(e => !e.isSystem);
       }
 
       if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
--- a/devtools/client/aboutdebugging-new/src/components/debugtarget/TemporaryExtensionAction.js
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/TemporaryExtensionAction.js
@@ -24,17 +24,17 @@ class TemporaryExtensionAction extends P
     return {
       dispatch: PropTypes.func.isRequired,
       target: Types.debugTarget.isRequired,
     };
   }
 
   reload() {
     const { dispatch, target } = this.props;
-    dispatch(Actions.reloadTemporaryExtension(target.id));
+    dispatch(Actions.reloadTemporaryExtension(target.details.actor));
   }
 
   remove() {
     const { dispatch, target } = this.props;
     dispatch(Actions.removeTemporaryExtension(target.id));
   }
 
   render() {
--- a/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
+++ b/devtools/client/aboutdebugging-new/src/modules/client-wrapper.js
@@ -92,21 +92,17 @@ class ClientWrapper {
     }
   }
 
   async listTabs(options) {
     return this.client.listTabs(options);
   }
 
   async listAddons() {
-    return this.client.mainRoot.listAddons();
-  }
-
-  async getAddon({ id }) {
-    return this.client.mainRoot.getAddon({ id });
+    return this.client.listAddons();
   }
 
   async listWorkers() {
     const { other, service, shared } = await this.client.mainRoot.listAllWorkers();
 
     return {
       otherWorkers: other,
       serviceWorkers: service,
--- a/devtools/client/aboutdebugging-new/src/modules/extensions-helper.js
+++ b/devtools/client/aboutdebugging-new/src/modules/extensions-helper.js
@@ -48,23 +48,24 @@ exports.debugLocalAddon = async function
  * Start debugging an addon in a remote instance of Firefox.
  *
  * @param {String} id
  *        The addon id to debug.
  * @param {DebuggerClient} client
  *        Required for remote debugging.
  */
 exports.debugRemoteAddon = async function(id, client) {
-  const addonTargetFront = await client.mainRoot.getAddon({ id });
+  const { addons } = await client.listAddons();
+  const addonForm = addons.find(addon => addon.id === id);
 
   // Close previous addon debugging toolbox.
   closeToolbox();
 
   const options = {
-    activeTab: addonTargetFront,
+    form: addonForm,
     chrome: true,
     client,
   };
 
   const target = await TargetFactory.forRemoteTab(options);
 
   const hostType = Toolbox.HostType.WINDOW;
   remoteAddonToolbox = await gDevTools.showToolbox(target, null, hostType);
--- a/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_usb_runtime.js
+++ b/devtools/client/aboutdebugging-new/test/browser/browser_aboutdebugging_addons_usb_runtime.js
@@ -29,31 +29,31 @@ add_task(async function() {
 
   const extensionPane = getDebugTargetPane("Extensions", document);
   info("Check an empty target pane message is displayed");
   ok(extensionPane.querySelector(".js-debug-target-list-empty"),
     "Extensions list is empty");
 
   info("Add an extension to the remote client");
   const addon = { name: "Test extension name", debuggable: true };
-  usbClient.listAddons = () => [addon];
+  usbClient.listAddons = () => ({ addons: [addon] });
   usbClient._eventEmitter.emit("addonListChanged");
 
   info("Wait until the extension appears");
   await waitUntil(() => !extensionPane.querySelector(".js-debug-target-list-empty"));
 
   const extensionTarget = findDebugTargetByText("Test extension name", document);
   ok(extensionTarget, "Extension target appeared for the USB runtime");
 
   // The goal here is to check that USB runtimes addons are only updated when the USB
   // runtime is sending addonListChanged events. The reason for this test is because the
   // previous implementation was updating the USB runtime extensions list when the _local_
   // AddonManager was updated.
   info("Remove the extension from the remote client WITHOUT sending an event");
-  usbClient.listAddons = () => [];
+  usbClient.listAddons = () => ({ addons: [] });
 
   info("Simulate an addon update on the ThisFirefox client");
   usbMocks.thisFirefoxClient._eventEmitter.emit("addonListChanged");
 
   // To avoid wait for a set period of time we trigger another async update, adding a new
   // tab. We assume that if the addon update mechanism had started, it would also be done
   // when the new tab was processed.
   info("Wait until the tab target for 'http://some.random/url.com' appears");
--- a/devtools/client/aboutdebugging-new/test/browser/mocks/head-client-wrapper-mock.js
+++ b/devtools/client/aboutdebugging-new/test/browser/mocks/head-client-wrapper-mock.js
@@ -57,17 +57,17 @@ function createClientMock() {
     // Return default preference value or null if no match.
     getPreference: (prefName) => {
       if (prefName in DEFAULT_PREFERENCES) {
         return DEFAULT_PREFERENCES[prefName];
       }
       return null;
     },
     // Empty array of addons
-    listAddons: () => [],
+    listAddons: () => ({ addons: [] }),
     // Empty array of tabs
     listTabs: () => ({ tabs: []}),
     // Empty arrays of workers
     listWorkers: () => ({
       otherWorkers: [],
       serviceWorkers: [],
       sharedWorkers: [],
     }),
--- a/devtools/client/aboutdebugging/components/addons/Panel.js
+++ b/devtools/client/aboutdebugging/components/addons/Panel.js
@@ -95,22 +95,24 @@ class AddonsPanel extends Component {
   }
 
   updateShowSystemStatus() {
     const showSystemAddons = Services.prefs.getBoolPref(SYSTEM_ENABLED_PREF, false);
     this.setState({ showSystemAddons });
   }
 
   updateAddonsList() {
-    this.props.client.mainRoot.listAddons()
-      .then(addons => {
+    this.props.client.listAddons()
+      .then(({addons}) => {
         const extensions = addons.filter(addon => addon.debuggable).map(addon => {
           return {
-            addonTargetFront: addon,
+            addonTargetActor: addon.actor,
             addonID: addon.id,
+            // Forward the whole addon actor form for potential remote debugging.
+            form: addon,
             icon: addon.iconURL || ExtensionIcon,
             isSystem: addon.isSystem,
             manifestURL: addon.manifestURL,
             name: addon.name,
             temporarilyInstalled: addon.temporarilyInstalled,
             url: addon.url,
             warnings: addon.warnings,
           };
--- a/devtools/client/aboutdebugging/components/addons/Target.js
+++ b/devtools/client/aboutdebugging/components/addons/Target.js
@@ -8,16 +8,17 @@
 
 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 {
   debugLocalAddon,
   debugRemoteAddon,
   getExtensionUuid,
+  isLegacyTemporaryExtension,
   isTemporaryID,
   parseFileUri,
   uninstallAddon,
 } = require("../../modules/addon");
 const Services = require("Services");
 
 loader.lazyRequireGetter(this, "DebuggerClient",
   "devtools/shared/client/debugger-client", true);
@@ -118,17 +119,17 @@ function infoMessages(target) {
   }
 
   return messages;
 }
 
 function warningMessages(target) {
   let messages = [];
 
-  if (target.addonTargetFront.isLegacyTemporaryExtension()) {
+  if (isLegacyTemporaryExtension(target.form)) {
     messages.push(dom.li(
       {
         className: "addon-target-warning-message addon-target-message",
       },
       Strings.GetStringFromName("legacyExtensionWarning"),
       " ",
       dom.a(
         {
@@ -151,18 +152,19 @@ function warningMessages(target) {
 
 class AddonTarget extends Component {
   static get propTypes() {
     return {
       client: PropTypes.instanceOf(DebuggerClient).isRequired,
       connect: PropTypes.object,
       debugDisabled: PropTypes.bool,
       target: PropTypes.shape({
-        addonTargetFront: PropTypes.object.isRequired,
+        addonTargetActor: 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,
     };
   }
@@ -185,20 +187,23 @@ class AddonTarget extends Component {
   }
 
   uninstall() {
     const { target } = this.props;
     uninstallAddon(target.addonID);
   }
 
   async reload() {
-    const { target } = this.props;
+    const { client, target } = this.props;
     const { AboutDebugging } = window;
     try {
-      await target.addonTargetFront.reload();
+      await client.request({
+        to: target.addonTargetActor,
+        type: "reload",
+      });
       AboutDebugging.emit("addon-reload");
     } catch (e) {
       throw new Error("Error reloading addon " + target.addonID + ": " + e.message);
     }
   }
 
   render() {
     const { target, debugDisabled } = this.props;
--- a/devtools/client/aboutdebugging/modules/addon.js
+++ b/devtools/client/aboutdebugging/modules/addon.js
@@ -20,16 +20,29 @@ const {
  * devtools/client/aboutdebugging-new/src/modules/extensions-helper.js
  * The only methods implemented here are the ones used in the old aboutdebugging only.
  */
 
 exports.isTemporaryID = function(addonID) {
   return AddonManagerPrivate.isTemporaryInstallID(addonID);
 };
 
+exports.isLegacyTemporaryExtension = function(addonForm) {
+  if (!addonForm.type) {
+    // If about:debugging is connected to an older then 59 remote Firefox, and type is
+    // not available on the addon/webextension actors, return false to avoid showing
+    // irrelevant warning messages.
+    return false;
+  }
+  return addonForm.type == "extension" &&
+         addonForm.temporarilyInstalled &&
+         !addonForm.isWebExtension &&
+         !addonForm.isAPIExtension;
+};
+
 /**
  * See JSDoc in devtools/client/aboutdebugging-new/src/modules/extensions-helper for all
  * the methods exposed below.
  */
 
 exports.debugLocalAddon = debugLocalAddon;
 exports.debugRemoteAddon = debugRemoteAddon;
 exports.getExtensionUuid = getExtensionUuid;
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -158,16 +158,29 @@ function getTargetActorForUrl(aClient, a
   aClient.listTabs().then(aResponse => {
     let targetActor = aResponse.tabs.filter(aGrip => aGrip.url == aUrl).pop();
     deferred.resolve(targetActor);
   });
 
   return deferred.promise;
 }
 
+function getAddonActorForId(aClient, aAddonId) {
+  info("Get addon actor for ID: " + aAddonId);
+  let deferred = promise.defer();
+
+  aClient.listAddons().then(aResponse => {
+    let addonTargetActor = aResponse.addons.filter(aGrip => aGrip.id == aAddonId).pop();
+    info("got addon actor for ID: " + aAddonId);
+    deferred.resolve(addonTargetActor);
+  });
+
+  return deferred.promise;
+}
+
 async function attachTargetActorForUrl(aClient, aUrl) {
   let grip = await getTargetActorForUrl(aClient, aUrl);
   let [ response, front ] = await aClient.attachTarget(grip.actor);
   return [grip, response, front];
 }
 
 async function attachThreadActorForUrl(aClient, aUrl) {
   let [grip, response] = await attachTargetActorForUrl(aClient, aUrl);
@@ -759,16 +772,28 @@ function hideVarPopupByScrollingEditor(a
   editor.setFirstVisibleLine(0);
   return popupHiding.then(waitForTick);
 }
 
 function reopenVarPopup(...aArgs) {
   return hideVarPopup.apply(this, aArgs).then(() => openVarPopup.apply(this, aArgs));
 }
 
+function attachAddonActorForId(aClient, aAddonId) {
+  let deferred = promise.defer();
+
+  getAddonActorForId(aClient, aAddonId).then(aGrip => {
+    aClient.attachAddon(aGrip).then(([aResponse]) => {
+      deferred.resolve([aGrip, aResponse]);
+    });
+  });
+
+  return deferred.promise;
+}
+
 function doResume(aPanel) {
   const threadClient = aPanel.panelWin.gThreadClient;
   return threadClient.resume();
 }
 
 function doInterrupt(aPanel) {
   const threadClient = aPanel.panelWin.gThreadClient;
   return threadClient.interrupt();
--- a/devtools/client/framework/connect/connect.js
+++ b/devtools/client/framework/connect/connect.js
@@ -75,17 +75,20 @@ var submit = async function() {
 /**
  * Connection is ready. List actors and build buttons.
  */
 var onConnectionReady = async function([aType, aTraits]) {
   clearTimeout(gConnectionTimeout);
 
   let addons = [];
   try {
-    addons = await gClient.mainRoot.listAddons();
+    const response = await gClient.listAddons();
+    if (!response.error && response.addons.length > 0) {
+      addons = response.addons;
+    }
   } catch (e) {
     // listAddons throws if the runtime doesn't support addons
   }
 
   let parent = document.getElementById("addonTargetActors");
   if (addons.length > 0) {
     // Add one entry for each add-on.
     for (const addon of addons) {
@@ -155,17 +158,17 @@ var onConnectionReady = async function([
 };
 
 /**
  * Build one button for an add-on.
  */
 function buildAddonLink(addon, parent) {
   const a = document.createElement("a");
   a.onclick = async function() {
-    openToolbox(null, true, "webconsole", addon);
+    openToolbox(addon, true, "webconsole");
   };
 
   a.textContent = addon.name;
   a.title = addon.id;
   a.href = "#";
 
   parent.appendChild(a);
 }
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -557,29 +557,38 @@ Target.prototype = {
           this.client.mainRoot.traits.webExtensionAddonConnect) {
         // The addonTargetActor form is related to a WebExtensionActor instance,
         // which isn't a target 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 target actor instance, we call its "connect" method, (which
         // fetches the target actor form from a WebExtensionTargetActor instance).
-        this.activeTab = await this.activeTab.connect();
+        const {form} = await this._client.request({
+          to: this.form.actor, type: "connect",
+        });
+
+        this._form = form;
+        this._url = this.form.url;
+        this._title = this.form.title;
       }
 
       // AddonTargetActor and ContentProcessTargetActor don't inherit from
       // BrowsingContextTargetActor (i.e. this.isBrowsingContext=false) and don't need
       // to be attached via DebuggerClient.attachTarget.
       if (this.isBrowsingContext) {
         await attachBrowsingContextTarget();
+      } else if (this.isLegacyAddon) {
+        const [, addonTargetFront] = await this._client.attachAddon(this.form);
+        this.activeTab = addonTargetFront;
 
-      // Addon Worker and Content process targets are the first targets to have their
-      // front already instantiated. The plan is to have all targets to have their front
-      // passed as constructor argument.
-      } else if (this.isWorkerTarget || this.isLegacyAddon) {
+      // Worker and Content process targets are the first target to have their front already
+      // instantiated. The plan is to have all targets to have their front passed as
+      // constructor argument.
+      } else if (this.isWorkerTarget) {
         // Worker is the first front to be completely migrated to have only its attach
         // method being called from Target.attach. Other fronts should be refactored.
         await this.activeTab.attach();
       } else if (this.isContentProcess) {
         // ContentProcessTarget is the only one target without any attach request.
       } else {
         throw new Error(`Unsupported type of target. Expected target of one of the` +
           ` following types: BrowsingContext, ContentProcess, Worker or ` +
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -84,18 +84,19 @@ var connect = async function() {
     webSocket,
   });
   gClient = new DebuggerClient(transport);
   appendStatusMessage("Start protocol client for connection");
   await gClient.connect();
 
   appendStatusMessage("Get root form for toolbox");
   if (addonID) {
-    const addonTargetFront = await gClient.mainRoot.getAddon({ id: addonID });
-    await openToolbox({activeTab: addonTargetFront, chrome: true});
+    const { addons } = await gClient.listAddons();
+    const addonTargetActor = addons.filter(addon => addon.id === addonID).pop();
+    await openToolbox({form: addonTargetActor, chrome: true});
   } else {
     const front = await gClient.mainRoot.getMainProcess();
     await openToolbox({activeTab: front, chrome: true});
   }
 };
 
 // Certain options should be toggled since we can assume chrome debugging here
 function setPrefDefaults() {
--- a/devtools/client/shared/test/browser_dbg_addon-console.js
+++ b/devtools/client/shared/test/browser_dbg_addon-console.js
@@ -58,20 +58,20 @@ AddonDebugger.prototype = {
     document.documentElement.appendChild(this.frame);
     window.addEventListener("message", this._onMessage);
 
     const transport = DebuggerServer.connectPipe();
     this.client = new DebuggerClient(transport);
 
     yield this.client.connect();
 
-    const addonTargetFront = yield this.client.mainRoot.getAddon({ id: addonId });
+    const addonTargetActor = yield getAddonActorForId(this.client, addonId);
 
     const targetOptions = {
-      activeTab: addonTargetFront,
+      form: addonTargetActor,
       client: this.client,
       chrome: true,
     };
 
     const toolboxOptions = {
       customIframe: this.frame,
     };
 
--- a/devtools/client/shared/test/browser_dbg_listaddons.js
+++ b/devtools/client/shared/test/browser_dbg_listaddons.js
@@ -16,17 +16,17 @@ var { DebuggerClient } = require("devtoo
 /**
  * Make sure the listAddons request works as specified.
  */
 const ADDON1_ID = "jid1-oBAwBoE5rSecNg@jetpack";
 const ADDON1_PATH = "addon1.xpi";
 const ADDON2_ID = "jid1-qjtzNGV8xw5h2A@jetpack";
 const ADDON2_PATH = "addon2.xpi";
 
-var gAddon1, gAddon1Front, gAddon2, gClient;
+var gAddon1, gAddon1Actor, gAddon2, gClient;
 
 function test() {
   DebuggerServer.init();
   DebuggerServer.registerAllActors();
 
   const transport = DebuggerServer.connectPipe();
   gClient = new DebuggerClient(transport);
   gClient.connect().then(([aType, aTraits]) => {
@@ -50,69 +50,69 @@ function testFirstAddon() {
   let addonListChanged = false;
   gClient.mainRoot.once("addonListChanged").then(() => {
     addonListChanged = true;
   });
 
   return addTemporaryAddon(ADDON1_PATH).then(addon => {
     gAddon1 = addon;
 
-    return gClient.mainRoot.getAddon({ id: ADDON1_ID }).then(front => {
+    return getAddonActorForId(gClient, ADDON1_ID).then(grip => {
       ok(!addonListChanged, "Should not yet be notified that list of addons changed.");
-      ok(front, "Should find an addon actor for addon1.");
-      gAddon1Front = front;
+      ok(grip, "Should find an addon actor for addon1.");
+      gAddon1Actor = grip.actor;
     });
   });
 }
 
 function testSecondAddon() {
   let addonListChanged = false;
   gClient.mainRoot.once("addonListChanged").then(() => {
     addonListChanged = true;
   });
 
   return addTemporaryAddon(ADDON2_PATH).then(addon => {
     gAddon2 = addon;
 
-    return gClient.mainRoot.getAddon({ id: ADDON1_ID }).then(front1 => {
-      return gClient.mainRoot.getAddon({ id: ADDON2_ID }).then(front2 => {
+    return getAddonActorForId(gClient, ADDON1_ID).then(fistGrip => {
+      return getAddonActorForId(gClient, ADDON2_ID).then(secondGrip => {
         ok(addonListChanged, "Should be notified that list of addons changed.");
-        is(front1, gAddon1Front, "First addon's actor shouldn't have changed.");
-        ok(front2, "Should find a addon actor for the second addon.");
+        is(fistGrip.actor, gAddon1Actor, "First addon's actor shouldn't have changed.");
+        ok(secondGrip, "Should find a addon actor for the second addon.");
       });
     });
   });
 }
 
 function testRemoveFirstAddon() {
   let addonListChanged = false;
   gClient.mainRoot.once("addonListChanged").then(() => {
     addonListChanged = true;
   });
 
   return removeAddon(gAddon1).then(() => {
-    return gClient.mainRoot.getAddon({ id: ADDON1_ID }).then(front => {
+    return getAddonActorForId(gClient, ADDON1_ID).then(grip => {
       ok(addonListChanged, "Should be notified that list of addons changed.");
-      ok(!front, "Shouldn't find a addon actor for the first addon anymore.");
+      ok(!grip, "Shouldn't find a addon actor for the first addon anymore.");
     });
   });
 }
 
 function testRemoveSecondAddon() {
   let addonListChanged = false;
   gClient.mainRoot.once("addonListChanged").then(() => {
     addonListChanged = true;
   });
 
   return removeAddon(gAddon2).then(() => {
-    return gClient.mainRoot.getAddon({ id: ADDON2_ID }).then(front => {
+    return getAddonActorForId(gClient, ADDON2_ID).then(grip => {
       ok(addonListChanged, "Should be notified that list of addons changed.");
-      ok(!front, "Shouldn't find a addon actor for the second addon anymore.");
+      ok(!grip, "Shouldn't find a addon actor for the second addon anymore.");
     });
   });
 }
 
 registerCleanupFunction(function() {
   gAddon1 = null;
-  gAddon1Front = null;
+  gAddon1Actor = null;
   gAddon2 = null;
   gClient = null;
 });
--- a/devtools/client/shared/test/helper_addons.js
+++ b/devtools/client/shared/test/helper_addons.js
@@ -30,16 +30,29 @@ function getAddonURIFromPath(path) {
 
 function addTemporaryAddon(path) {
   const addonFile = getAddonURIFromPath(path).file;
   info("Installing addon: " + addonFile.path);
 
   return AddonManager.installTemporaryAddon(addonFile);
 }
 
+function getAddonActorForId(client, addonId) {
+  info("Get addon actor for ID: " + addonId);
+  const deferred = getDeferredPromise().defer();
+
+  client.listAddons().then(response => {
+    const addonTargetActor = response.addons.filter(grip => grip.id == addonId).pop();
+    info("got addon actor for ID: " + addonId);
+    deferred.resolve(addonTargetActor);
+  });
+
+  return deferred.promise;
+}
+
 function removeAddon(addon) {
   info("Removing addon.");
 
   const deferred = getDeferredPromise().defer();
 
   const listener = {
     onUninstalled: function(uninstalledAddon) {
       if (uninstalledAddon != addon) {
--- a/devtools/server/tests/mochitest/test_webextension-addon-debugging-connect.html
+++ b/devtools/server/tests/mochitest/test_webextension-addon-debugging-connect.html
@@ -32,22 +32,23 @@ async function test_connect_addon(oopMod
   await extension.awaitMessage("background page ready");
 
   // Connect a DebuggerClient.
   const transport = DebuggerServer.connectPipe();
   const client = new DebuggerClient(transport);
   await client.connect();
 
   // List addons and assertions on the expected addon actor.
-  const addonTargetFront = await client.mainRoot.getAddon({ id: extension.id });
-  ok(addonTargetFront, "The expected webextension addon actor has been found");
+  const {addons} = await client.mainRoot.listAddons();
+  const addonTargetActor = addons.filter(actor => actor.id === extension.id).pop();
+  ok(addonTargetActor, "The expected webextension addon actor has been found");
 
   // Connect to the target addon actor and wait for the updated list of frames.
   const addonTarget = await TargetFactory.forRemoteTab({
-    activeTab: addonTargetFront,
+    form: addonTargetActor,
     client,
     chrome: true,
   });
   is(addonTarget.form.isOOP, oopMode,
      "Got the expected oop mode in the webextension actor form");
   const frames = await waitForFramesUpdated(addonTarget);
   const backgroundPageFrame = frames.filter((frame) => {
     return frame.url && frame.url.endsWith("/_generated_background_page.html");
--- a/devtools/server/tests/mochitest/webextension-helpers.js
+++ b/devtools/server/tests/mochitest/webextension-helpers.js
@@ -93,41 +93,46 @@ function collectFrameUpdates({client}, m
 }
 
 async function attachAddon(addonId) {
   const transport = DebuggerServer.connectPipe();
   const client = new DebuggerClient(transport);
 
   await client.connect();
 
-  const addonTargetFront = await client.mainRoot.getAddon({ id: addonId });
+  const {addons} = await client.mainRoot.listAddons();
+  const addonTargetActor = addons.filter(actor => actor.id === addonId).pop();
 
-  if (!addonTargetFront) {
+  if (!addonTargetActor) {
     client.close();
     throw new Error(`No WebExtension Actor found for ${addonId}`);
   }
 
   const addonTarget = await TargetFactory.forRemoteTab({
-    activeTab: addonTargetFront,
+    form: addonTargetActor,
     client,
     chrome: true,
   });
 
   return addonTarget;
 }
 
 async function reloadAddon({client}, addonId) {
-  const addonTargetFront = await client.mainRoot.getAddon({ id: addonId });
+  const {addons} = await client.mainRoot.listAddons();
+  const addonTargetActor = addons.filter(actor => actor.id === addonId).pop();
 
-  if (!addonTargetFront) {
+  if (!addonTargetActor) {
     client.close();
     throw new Error(`No WebExtension Actor found for ${addonId}`);
   }
 
-  await addonTargetFront.reload();
+  await client.request({
+    to: addonTargetActor.actor,
+    type: "reload",
+  });
 }
 
 // Test helpers related to the AddonManager.
 
 function generateWebExtensionXPI(extDetails) {
   const addonFile = Extension.generateXPI(extDetails);
 
   flushJarCache(addonFile.path);
--- a/devtools/server/tests/unit/test_addon_events.js
+++ b/devtools/server/tests/unit/test_addon_events.js
@@ -13,17 +13,17 @@ add_task(async function testReloadExited
   DebuggerServer.registerAllActors();
 
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   await client.connect();
 
   // Retrieve the current list of addons to be notified of the next list update.
   // We will also call listAddons every time we receive the event "addonListChanged" for
   // the same reason.
-  await client.mainRoot.listAddons();
+  await client.listAddons();
 
   info("Install the addon");
   const addonFile = do_get_file("addons/web-extension", false);
 
   let installedAddon;
   await expectAddonListChanged(client, async () => {
     installedAddon = await AddonManager.installTemporaryAddon(addonFile);
   });
@@ -51,10 +51,10 @@ add_task(async function testReloadExited
 
   await close(client);
 });
 
 async function expectAddonListChanged(client, predicate) {
   const onAddonListChanged = client.mainRoot.once("addonListChanged");
   await predicate();
   await onAddonListChanged;
-  await client.mainRoot.listAddons();
+  await client.listAddons();
 }
--- a/devtools/server/tests/unit/test_addon_reload.js
+++ b/devtools/server/tests/unit/test_addon_reload.js
@@ -29,20 +29,27 @@ function promiseWebExtensionStartup() {
       Management.off("ready", listener);
       resolve(extension);
     };
 
     Management.on("ready", listener);
   });
 }
 
-async function reloadAddon(addonTargetFront) {
+async function findAddonInRootList(client, addonId) {
+  const result = await client.listAddons();
+  const addonTargetActor = result.addons.filter(addon => addon.id === addonId)[0];
+  ok(addonTargetActor, `Found add-on actor for ${addonId}`);
+  return addonTargetActor;
+}
+
+async function reloadAddon(client, addonTargetActor) {
   // The add-on will be re-installed after a successful reload.
   const onInstalled = promiseAddonEvent("onInstalled");
-  await addonTargetFront.reload();
+  await client.request({to: addonTargetActor.actor, type: "reload"});
   await onInstalled;
 }
 
 function getSupportFile(path) {
   const allowMissing = false;
   return do_get_file(path, allowMissing);
 }
 
@@ -62,51 +69,51 @@ add_task(async function testReloadExited
 
   // Install a decoy add-on.
   const addonFile2 = getSupportFile("addons/web-extension2");
   const [installedAddon2] = await Promise.all([
     AddonManager.installTemporaryAddon(addonFile2),
     promiseWebExtensionStartup(),
   ]);
 
-  const addonTargetFront = await client.mainRoot.getAddon({ id: installedAddon.id });
+  const addonTargetActor = await findAddonInRootList(client, installedAddon.id);
 
   await Promise.all([
-    reloadAddon(addonTargetFront),
+    reloadAddon(client, addonTargetActor),
     promiseWebExtensionStartup(),
   ]);
 
   // Uninstall the decoy add-on, which should cause its actor to exit.
   const onUninstalled = promiseAddonEvent("onUninstalled");
   installedAddon2.uninstall();
   await onUninstalled;
 
   // Try to re-list all add-ons after a reload.
   // This was throwing an exception because of the exited actor.
-  const newAddonFront = await client.mainRoot.getAddon({ id: installedAddon.id });
-  equal(newAddonFront.id, addonTargetFront.id);
+  const newAddonActor = await findAddonInRootList(client, installedAddon.id);
+  equal(newAddonActor.id, addonTargetActor.id);
 
-  // The fronts should be the same after the reload
-  equal(newAddonFront, addonTargetFront);
+  // The actor id should be the same after the reload
+  equal(newAddonActor.actor, addonTargetActor.actor);
 
   const onAddonListChanged = client.mainRoot.once("addonListChanged");
 
   // Install an upgrade version of the first add-on.
   const addonUpgradeFile = getSupportFile("addons/web-extension-upgrade");
   const [upgradedAddon] = await Promise.all([
     AddonManager.installTemporaryAddon(addonUpgradeFile),
     promiseWebExtensionStartup(),
   ]);
 
   // Waiting for addonListChanged unsolicited event
   await onAddonListChanged;
 
   // re-list all add-ons after an upgrade.
-  const upgradedAddonFront = await client.mainRoot.getAddon({ id: upgradedAddon.id });
-  equal(upgradedAddonFront.id, addonTargetFront.id);
-  // The fronts should be the same after the upgrade.
-  equal(upgradedAddonFront, addonTargetFront);
+  const upgradedAddonActor = await findAddonInRootList(client, upgradedAddon.id);
+  equal(upgradedAddonActor.id, addonTargetActor.id);
+  // The actor id should be the same after the upgrade.
+  equal(upgradedAddonActor.actor, addonTargetActor.actor);
 
   // The addon metadata has been updated.
-  equal(upgradedAddonFront.name, "Test Addons Actor Upgrade");
+  equal(upgradedAddonActor.name, "Test Addons Actor Upgrade");
 
   await close(client);
 });
--- a/devtools/server/tests/unit/test_addons_actor.js
+++ b/devtools/server/tests/unit/test_addons_actor.js
@@ -24,20 +24,20 @@ add_task(async function testSuccessfulIn
   const usePlatformSeparator = true;
   const addonPath = getFilePath("addons/web-extension",
                                 allowMissing, usePlatformSeparator);
   const installedAddon = await addons.installTemporaryAddon(addonPath);
   equal(installedAddon.id, "test-addons-actor@mozilla.org");
   // The returned object is currently not a proper actor.
   equal(installedAddon.actor, false);
 
-  const addonList = await client.mainRoot.listAddons();
-  ok(addonList && addonList.map(a => a.name),
+  const addonList = await client.listAddons();
+  ok(addonList && addonList.addons && addonList.addons.map(a => a.name),
      "Received list of add-ons");
-  const addon = addonList.find(a => a.id === installedAddon.id);
+  const addon = addonList.addons.filter(a => a.id === installedAddon.id)[0];
   ok(addon, "Test add-on appeared in root install list");
 
   await close(client);
 });
 
 add_task(async function testNonExistantPath() {
   const [client, addons] = await connect();
 
new file mode 100644
--- /dev/null
+++ b/devtools/shared/client/addon-client.js
@@ -0,0 +1,43 @@
+/* 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 {DebuggerClient} = require("devtools/shared/client/debugger-client");
+
+function AddonClient(client, actor) {
+  this._client = client;
+  this._actor = actor;
+  this.request = this._client.request;
+  this.events = [];
+}
+
+AddonClient.prototype = {
+  get actor() {
+    return this._actor;
+  },
+  get _transport() {
+    return this._client._transport;
+  },
+
+  /**
+   * Detach the client from the addon actor.
+   *
+   * @param function onResponse
+   *        Called with the response packet.
+   */
+  detach: DebuggerClient.requester({
+    type: "detach",
+  }, {
+    after: function(response) {
+      if (this._client.activeAddon === this) {
+        this._client.activeAddon = null;
+      }
+      this._client.unregisterClient(this);
+      return response;
+    },
+  }),
+};
+
+module.exports = AddonClient;
--- a/devtools/shared/client/debugger-client.js
+++ b/devtools/shared/client/debugger-client.js
@@ -17,16 +17,17 @@ const {
   UnsolicitedPauses,
 } = require("./constants");
 
 loader.lazyRequireGetter(this, "Authentication", "devtools/shared/security/auth");
 loader.lazyRequireGetter(this, "DebuggerSocket", "devtools/shared/security/socket", true);
 loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
 
 loader.lazyRequireGetter(this, "WebConsoleClient", "devtools/shared/webconsole/client", true);
+loader.lazyRequireGetter(this, "AddonTargetFront", "devtools/shared/fronts/targets/addon", true);
 loader.lazyRequireGetter(this, "RootFront", "devtools/shared/fronts/root", true);
 loader.lazyRequireGetter(this, "BrowsingContextTargetFront", "devtools/shared/fronts/targets/browsing-context", true);
 loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
 loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/object-client");
 loader.lazyRequireGetter(this, "Pool", "devtools/shared/protocol", true);
 loader.lazyRequireGetter(this, "Front", "devtools/shared/protocol", true);
 
 // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
@@ -339,16 +340,24 @@ DebuggerClient.prototype = {
   /*
    * This function exists only to preserve DebuggerClient's interface;
    * new code should say 'client.mainRoot.listTabs()'.
    */
   listTabs: function(options) {
     return this.mainRoot.listTabs(options);
   },
 
+  /*
+   * This function exists only to preserve DebuggerClient's interface;
+   * new code should say 'client.mainRoot.listAddons()'.
+   */
+  listAddons: function() {
+    return this.mainRoot.listAddons();
+  },
+
   getTab: function(filter) {
     return this.mainRoot.getTab(filter);
   },
 
   /**
    * Attach to a target actor:
    *
    *  - start watching for new documents (emits `tabNativated` messages)
@@ -367,16 +376,33 @@ DebuggerClient.prototype = {
       this._frontPool.manage(front);
     }
 
     const response = await front.attach();
     return [response, front];
   },
 
   /**
+   * Attach to an addon target actor.
+   *
+   * @param string addonTargetActor
+   *        The actor ID for the addon to attach.
+   */
+  attachAddon: async function(form) {
+    let front = this._frontPool.actor(form.actor);
+    if (!front) {
+      front = new AddonTargetFront(this, form);
+      this._frontPool.manage(front);
+    }
+
+    const response = await front.attach();
+    return [response, front];
+  },
+
+  /**
    * Attach to a Web Console actor. Depending on the listeners being passed as second
    * arguments, starts listening for:
    * - PageError:
    *   Javascript error happening in the debugged context
    * - ConsoleAPI:
    *   Calls made to console.* API
    * - NetworkActivity:
    *   Http requests made in the debugged context
--- a/devtools/shared/client/moz.build
+++ b/devtools/shared/client/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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-client.js',
     'array-buffer-client.js',
     'breakpoint-client.js',
     'connection-manager.js',
     'constants.js',
     'debugger-client.js',
     'environment-client.js',
     'event-source.js',
     'long-string-client.js',
--- a/devtools/shared/fronts/root.js
+++ b/devtools/shared/fronts/root.js
@@ -211,30 +211,16 @@ const RootFront = protocol.FrontClassWit
     }
 
     return this._getTab(packet);
   }, {
     impl: "_getTab",
   }),
 
   /**
-   * Fetch the target front for a given add-on.
-   * This is just an helper on top of `listAddons` request.
-   *
-   * @param object filter
-   *        A dictionary object with following attribute:
-   *         - id: used to match the add-on to connect to.
-   */
-  async getAddon({ id }) {
-    const addons = await this.listAddons();
-    const addonTargetFront = addons.find(addon => addon.id === id);
-    return addonTargetFront;
-  },
-
-  /**
    * Test request that returns the object passed as first argument.
    *
    * `echo` is special as all the property of the given object have to be passed
    * on the packet object. That's not something that can be achieve by requester helper.
    */
 
   echo(packet) {
     packet.type = "echo";
--- a/devtools/shared/fronts/targets/addon.js
+++ b/devtools/shared/fronts/targets/addon.js
@@ -1,75 +1,26 @@
 /* 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 {addonTargetSpec} = require("devtools/shared/specs/targets/addon");
 const protocol = require("devtools/shared/protocol");
 const {custom} = protocol;
-loader.lazyRequireGetter(this, "BrowsingContextTargetFront", "devtools/shared/fronts/targets/browsing-context", true);
 
 const AddonTargetFront = protocol.FrontClassWithSpec(addonTargetSpec, {
-  initialize: function(client) {
-    protocol.Front.prototype.initialize.call(this, client);
+  initialize: function(client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
 
     this.client = client;
 
     this.traits = {};
   },
 
-  form(json) {
-    this.actorID = json.actor;
-
-    // Save the full form for Target class usage.
-    // Do not use `form` name to avoid colliding with protocol.js's `form` method
-    this.targetForm = json;
-
-    // We used to manipulate the form rather than the front itself.
-    // Expose all form attributes to ease accessing them.
-    for (const name in json) {
-      if (name == "actor") {
-        continue;
-      }
-      this[name] = json[name];
-    }
-  },
-
-  isLegacyTemporaryExtension() {
-    if (!this.type) {
-      // If about:debugging is connected to an older then 59 remote Firefox, and type is
-      // not available on the addon/webextension actors, return false to avoid showing
-      // irrelevant warning messages.
-      return false;
-    }
-    return this.type == "extension" &&
-           this.temporarilyInstalled &&
-           !this.isWebExtension &&
-           !this.isAPIExtension;
-  },
-
-  /**
-   * Returns the actual target front for web extensions.
-   *
-   * AddonTargetActor is used for WebExtensions, but this isn't the final target actor
-   * we want to use for it. AddonTargetActor only expose metadata about the Add-on, like
-   * its name, type, ... Instead, we want to use a WebExtensionTargetActor, which
-   * inherits from BrowsingContextTargetActor. This connect method is used to retrive
-   * the final target actor to use.
-   */
-  connect: custom(async function() {
-    const { form } = await this._connect();
-    const front = new BrowsingContextTargetFront(this.client, form);
-    this.manage(front);
-    return front;
-  }, {
-    impl: "_connect",
-  }),
-
   attach: custom(async function() {
     const response = await this._attach();
 
     this.threadActor = response.threadActor;
 
     return response;
   }, {
     impl: "_attach",
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -235,21 +235,16 @@ const Types = exports.__TypesForTests = 
     front: null,
   },
   {
     types: ["symbolIterator"],
     spec: "devtools/shared/specs/symbol-iterator",
     front: null,
   },
   {
-    types: ["addonTarget"],
-    spec: "devtools/shared/specs/targets/addon",
-    front: "devtools/shared/fronts/targets/addon",
-  },
-  {
     types: ["browsingContextTarget"],
     spec: "devtools/shared/specs/targets/browsing-context",
     front: null,
   },
   {
     types: ["chromeWindowTarget"],
     spec: "devtools/shared/specs/targets/chrome-window",
     front: null,
--- a/devtools/shared/specs/root.js
+++ b/devtools/shared/specs/root.js
@@ -6,16 +6,19 @@
 const { types, generateActorSpec, RetVal, Arg, Option } = require("devtools/shared/protocol");
 
 types.addDictType("root.getTab", {
   tab: "json",
 });
 types.addDictType("root.getWindow", {
   window: "json",
 });
+types.addDictType("root.listAddons", {
+  addons: "array:json",
+});
 types.addDictType("root.listWorkers", {
   workers: "array:workerTarget",
 });
 types.addDictType("root.listServiceWorkerRegistrations", {
   registrations: "array:json",
 });
 types.addDictType("root.listProcesses", {
   processes: "array:json",
@@ -49,19 +52,17 @@ const rootSpecPrototype = {
       request: {
         outerWindowID: Option(0, "number"),
       },
       response: RetVal("root.getWindow"),
     },
 
     listAddons: {
       request: {},
-      response: {
-        addons: RetVal("array:addonTarget"),
-      },
+      response: RetVal("root.listAddons"),
     },
 
     listWorkers: {
       request: {},
       response: RetVal("root.listWorkers"),
     },
 
     listServiceWorkerRegistrations: {
--- a/devtools/shared/specs/targets/addon.js
+++ b/devtools/shared/specs/targets/addon.js
@@ -1,35 +1,33 @@
 /* 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 {Option, RetVal, generateActorSpec} = require("devtools/shared/protocol");
+const {Arg, Option, RetVal, generateActorSpec} = require("devtools/shared/protocol");
 
 const addonTargetSpec = generateActorSpec({
   typeName: "addonTarget",
 
   methods: {
     attach: {
       request: {},
       response: RetVal("json"),
     },
     detach: {
       request: {},
       response: RetVal("json"),
     },
     connect: {
-      request: {},
+      request: {
+        options: Arg(0, "json"),
+      },
       response: RetVal("json"),
     },
-    reload: {
-      request: {},
-      response: {},
-    },
     push: {
       request: {},
       response: RetVal("json"),
     },
   },
 
   events: {
     // newSource is being sent by ThreadActor in the name of its parent,