Bug 1465635 - Merge all target fronts with Target class. r=yulia,jdescottes
authorAlexandre Poirot <poirot.alex@gmail.com>
Sat, 02 Feb 2019 11:24:32 +0000
changeset 456567 1dfe4f2eb472d1dd9251297f9476e786c2cd751a
parent 456566 fead89ec2d1bb342ed1feb0616ec241411baed8a
child 456568 aff30c79fa91fa4b444cc76f1d9e2a4ac487b6af
push id77340
push userapoirot@mozilla.com
push dateSat, 02 Feb 2019 11:26:18 +0000
treeherderautoland@756f2a2d2017 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyulia, jdescottes
bugs1465635
milestone67.0a1
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
Bug 1465635 - Merge all target fronts with Target class. r=yulia,jdescottes This patch makes it so that all target fronts inherits from a Target class mixin. We are using a mixin as fronts should inherit from a custom Front class, which is augmented with its own RDP request and events defined in its spec. (This is done via FrontClassWithSpec(spec)) Depends on D15830 Differential Revision: https://phabricator.services.mozilla.com/D15831
browser/components/extensions/parent/ext-devtools.js
devtools/client/framework/target.js
devtools/client/framework/test/browser_two_tabs.js
devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
devtools/client/shared/test/browser_dbg_listtabs-03.js
devtools/server/tests/unit/head_dbg.js
devtools/shared/fronts/targets/addon.js
devtools/shared/fronts/targets/browsing-context.js
devtools/shared/fronts/targets/content-process.js
devtools/shared/fronts/targets/moz.build
devtools/shared/fronts/targets/target-mixin.js
devtools/shared/fronts/targets/worker.js
devtools/shared/protocol.js
devtools/shared/specs/targets/worker.js
--- a/browser/components/extensions/parent/ext-devtools.js
+++ b/browser/components/extensions/parent/ext-devtools.js
@@ -97,17 +97,17 @@ global.getToolboxEvalOptions = function(
 
   if (selectedNode && selectedNode.nodeFront) {
     // If there is a selected node in the inspector, we hand over
     // its actor id to the eval request in order to provide the "$0" binding.
     options.toolboxSelectedNodeActorID = selectedNode.nodeFront.actorID;
   }
 
   // Provide the console actor ID to implement the "inspect" binding.
-  options.toolboxConsoleActorID = toolbox.target.form.consoleActor;
+  options.toolboxConsoleActorID = toolbox.target.activeConsole.actor;
 
   return options;
 };
 
 /**
  * The DevToolsPage represents the "devtools_page" related to a particular
  * Toolbox and WebExtension.
  *
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -1,31 +1,24 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const EventEmitter = require("devtools/shared/event-emitter");
-const Services = require("Services");
-
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "DebuggerClient",
   "devtools/shared/client/debugger-client", true);
-loader.lazyRequireGetter(this, "gDevTools",
-  "devtools/client/framework/devtools", true);
-loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", true);
 
 const targets = new WeakMap();
-const promiseTargets = new WeakMap();
 
 /**
  * Functions for creating Targets
  */
-const TargetFactory = exports.TargetFactory = {
+exports.TargetFactory = {
 
   /**
    * Construct a Target. The target will be cached for each Tab so that we create only
    * one per tab.
    *
    * @param {XULTab} tab
    *        The tab to use in creating a new target.
    *
@@ -37,16 +30,20 @@ const TargetFactory = exports.TargetFact
       return target;
     }
     const promise = this.createTargetForTab(tab);
     // Immediately set the target's promise in cache to prevent race
     targets.set(tab, promise);
     target = await promise;
     // Then replace the promise with the target object
     targets.set(tab, target);
+    target.attachTab(tab);
+    target.once("close", () => {
+      targets.delete(tab);
+    });
     return target;
   },
 
   /**
    * Instantiate a target for the given tab.
    *
    * This will automatically:
    * - spawn a DebuggerServer in the parent process,
@@ -82,749 +79,51 @@ const TargetFactory = exports.TargetFact
 
     createLocalServer();
     const client = createLocalClient();
 
     // Connect the local client to the local server
     await client.connect();
 
     // Fetch the FrameTargetActor's Front
-    const front = await client.mainRoot.getTab({ tab });
-
-    return new Target({
-      client,
-      activeTab: front,
-      // A local Target will never perform chrome debugging.
-      chrome: false,
-      tab,
-    });
+    return client.mainRoot.getTab({ tab });
   },
 
   /**
    * Return a promise of a Target for a remote tab.
    * @param {Object} options
    *        The options object has the following properties:
    *        {
    *          activeTab: front for this tab target,
    *          client: a DebuggerClient instance
    *                  (caller owns this and is responsible for closing),
    *          chrome: true if the remote target is the whole process
    *        }
    *
    * @return A promise of a target object
    */
-  forRemoteTab: function(options) {
-    let targetPromise = promiseTargets.get(options);
-    if (targetPromise == null) {
-      const target = new Target(options);
-      targetPromise = target.attach().then(() => target);
-      targetPromise.catch(e => {
-        console.error("Exception while attaching target", e);
-      });
-      promiseTargets.set(options, targetPromise);
+  forRemoteTab: function({ activeTab, client, chrome }) {
+    const target = activeTab;
+    if (chrome) {
+      target.forceChrome();
     }
+    const targetPromise = target.attach().then(() => target);
+    targetPromise.catch(e => {
+      console.error("Exception while attaching target", e);
+    });
     return targetPromise;
   },
 
   forWorker: function(workerTargetFront) {
-    let target = targets.get(workerTargetFront);
-    if (target == null) {
-      target = new Target({
-        client: workerTargetFront.client,
-        activeTab: workerTargetFront,
-        chrome: false,
-      });
-      targets.set(workerTargetFront, target);
-    }
-    return target;
+    return workerTargetFront;
   },
 
   /**
    * Creating a target for a tab that is being closed is a problem because it
    * allows a leak as a result of coming after the close event which normally
    * clears things up. This function allows us to ask if there is a known
    * target for a tab without creating a target
    * @return true/false
    */
   isKnownTab: function(tab) {
     return targets.has(tab);
   },
 };
-
-/**
- * A Target represents something that we can debug. Targets are generally
- * read-only. Any changes that you wish to make to a target should be done via
- * a Tool that attaches to the target. i.e. a Target is just a pointer saying
- * "the thing to debug is over there".
- *
- * Providing a generalized abstraction of a web-page or web-browser (available
- * either locally or remotely) is beyond the scope of this class (and maybe
- * also beyond the scope of this universe) However Target does attempt to
- * abstract some common events and read-only properties common to many Tools.
- *
- * Supported read-only properties:
- * - name, url
- *
- * Target extends EventEmitter and provides support for the following events:
- * - close: The target window has been closed. All tools attached to this
- *          target should close. This event is not currently cancelable.
- * - navigate: The target window has navigated to a different URL
- *
- * Optional events:
- * - will-navigate: The target window will navigate to a different URL
- * - hidden: The target is not visible anymore (for TargetTab, another tab is
- *           selected)
- * - visible: The target is visible (for TargetTab, tab is selected)
- *
- * Comparing Targets: 2 instances of a Target object can point at the same
- * thing, so t1 !== t2 and t1 != t2 even when they represent the same object.
- * To compare to targets use 't1.equals(t2)'.
- */
-
-/**
- * A Target represents a debuggable context. It can be a browser tab, a tab on
- * a remote device, like a tab on Firefox for Android. But it can also be an add-on,
- * as well as firefox parent process, or just one of its content process.
- * A Target is related to a given TargetActor, for which we pass the form as
- * argument.
- *
- * For now, only workers are having a distinct Target class called WorkerTarget.
- *
- * @param {Front} activeTab
- *                  If we already have a front for this target, pass it here.
- * @param {DebuggerClient} client
- *                  The DebuggerClient instance to be used to debug this target.
- * @param {Boolean} chrome
- *                  True, if we allow to see privileged resources like JSM, xpcom,
- *                  frame scripts...
- * @param {xul:tab} tab (optional)
- *                  If the target is a local Firefox tab, a reference to the firefox
- *                  frontend tab object.
- */
-class Target extends EventEmitter {
-  constructor({ client, chrome, activeTab, tab = null }) {
-    if (!activeTab) {
-      throw new Error("Cannot instanciate target without a non-null activeTab");
-    }
-
-    super();
-
-    this.destroy = this.destroy.bind(this);
-    this._onTabNavigated = this._onTabNavigated.bind(this);
-    this.activeConsole = null;
-    this.activeTab = activeTab;
-
-    this._url = this.form.url;
-    this._title = this.form.title;
-
-    this._client = client;
-    this._chrome = chrome;
-
-    // When debugging local tabs, we also have a reference to the Firefox tab
-    // This is used to:
-    // * distinguish local tabs from remote (see target.isLocalTab)
-    // * being able to hookup into Firefox UI (see Hosts)
-    if (tab) {
-      this._tab = tab;
-      this._setupListeners();
-    }
-
-    // isBrowsingContext is true for all target connected to an actor that inherits from
-    // BrowsingContextTargetActor. It happens to be the case for almost all targets but:
-    // * legacy add-ons (old bootstrapped add-ons)
-    // * content process (browser content toolbox)
-    // * xpcshell debugging (it uses ParentProcessTargetActor, which inherits from
-    //                       BrowsingContextActor, but doesn't have any valid browsing
-    //                       context to attach to.)
-    // Starting with FF64, BrowsingContextTargetActor exposes a traits to help identify
-    // the target actors inheriting from it. It also help identify the xpcshell debugging
-    // target actor that doesn't have any valid browsing context.
-    // (Once FF63 is no longer supported, we can remove the `else` branch and only look
-    // for the traits)
-    if (this.form.traits && ("isBrowsingContext" in this.form.traits)) {
-      this._isBrowsingContext = this.form.traits.isBrowsingContext;
-    } else {
-      this._isBrowsingContext = !this.isLegacyAddon && !this.isContentProcess && !this.isWorkerTarget;
-    }
-
-    // Cache of already created targed-scoped fronts
-    // [typeName:string => Front instance]
-    this.fronts = new Map();
-    // Temporary fix for bug #1493131 - inspector has a different life cycle
-    // than most other fronts because it is closely related to the toolbox.
-    // TODO: remove once inspector is separated from the toolbox
-    this._inspector = null;
-  }
-
-  /**
-   * Returns a promise for the protocol description from the root actor. Used
-   * internally with `target.actorHasMethod`. Takes advantage of caching if
-   * definition was fetched previously with the corresponding actor information.
-   * Actors are lazily loaded, so not only must the tool using a specific actor
-   * be in use, the actors are only registered after invoking a method (for
-   * performance reasons, added in bug 988237), so to use these actor detection
-   * methods, one must already be communicating with a specific actor of that
-   * type.
-   *
-   * @return {Promise}
-   * {
-   *   "category": "actor",
-   *   "typeName": "longstractor",
-   *   "methods": [{
-   *     "name": "substring",
-   *     "request": {
-   *       "type": "substring",
-   *       "start": {
-   *         "_arg": 0,
-   *         "type": "primitive"
-   *       },
-   *       "end": {
-   *         "_arg": 1,
-   *         "type": "primitive"
-   *       }
-   *     },
-   *     "response": {
-   *       "substring": {
-   *         "_retval": "primitive"
-   *       }
-   *     }
-   *   }],
-   *  "events": {}
-   * }
-   */
-  async getActorDescription(actorName) {
-    if (this._protocolDescription &&
-        this._protocolDescription.types[actorName]) {
-      return this._protocolDescription.types[actorName];
-    }
-    const description = await this.client.mainRoot.protocolDescription();
-    this._protocolDescription = description;
-    return description.types[actorName];
-  }
-
-  /**
-   * Returns a boolean indicating whether or not the specific actor
-   * type exists.
-   *
-   * @param {String} actorName
-   * @return {Boolean}
-   */
-  hasActor(actorName) {
-    if (this.form) {
-      return !!this.form[actorName + "Actor"];
-    }
-    return false;
-  }
-
-  /**
-   * Queries the protocol description to see if an actor has
-   * an available method. The actor must already be lazily-loaded (read
-   * the restrictions in the `getActorDescription` comments),
-   * so this is for use inside of tool. Returns a promise that
-   * resolves to a boolean.
-   *
-   * @param {String} actorName
-   * @param {String} methodName
-   * @return {Promise}
-   */
-  actorHasMethod(actorName, methodName) {
-    return this.getActorDescription(actorName).then(desc => {
-      if (desc && desc.methods) {
-        return !!desc.methods.find(method => method.name === methodName);
-      }
-      return false;
-    });
-  }
-
-  /**
-   * Returns a trait from the root actor.
-   *
-   * @param {String} traitName
-   * @return {Mixed}
-   */
-  getTrait(traitName) {
-    // If the targeted actor exposes traits and has a defined value for this
-    // traits, override the root actor traits
-    if (this.form.traits && traitName in this.form.traits) {
-      return this.form.traits[traitName];
-    }
-
-    return this.client.traits[traitName];
-  }
-
-  get tab() {
-    return this._tab;
-  }
-
-  get form() {
-    return this.activeTab.targetForm;
-  }
-
-  // Get a promise of the RootActor's form
-  get root() {
-    return this.client.mainRoot.rootForm;
-  }
-
-  // Temporary fix for bug #1493131 - inspector has a different life cycle
-  // than most other fronts because it is closely related to the toolbox.
-  // TODO: remove once inspector is separated from the toolbox
-  async getInspector(typeName) {
-    // the front might have been destroyed and no longer have an actor ID
-    if (this._inspector && this._inspector.actorID) {
-      return this._inspector;
-    }
-    this._inspector = await getFront(this.client, "inspector", this.form);
-    this.emit("inspector", this._inspector);
-    return this._inspector;
-  }
-
-  // Run callback on every front of this type that currently exists, and on every
-  // instantiation of front type in the future.
-  onFront(typeName, callback) {
-    const front = this.fronts.get(typeName);
-    if (front) {
-      return callback(front);
-    }
-    return this.on(typeName, callback);
-  }
-
-  // Get a Front for a target-scoped actor.
-  // i.e. an actor served by RootActor.listTabs or RootActorActor.getTab requests
-  async getFront(typeName) {
-    let front = this.fronts.get(typeName);
-    // the front might have been destroyed and no longer have an actor ID
-    if (front && front.actorID || front && typeof front.then === "function") {
-      return front;
-    }
-    front = getFront(this.client, typeName, this.form);
-    this.fronts.set(typeName, front);
-    // replace the placeholder with the instance of the front once it has loaded
-    front = await front;
-    this.emit(typeName, front);
-    this.fronts.set(typeName, front);
-    return front;
-  }
-
-  getCachedFront(typeName) {
-    // do not wait for async fronts;
-    const front = this.fronts.get(typeName);
-    // ensure that the front is a front, and not async front
-    if (front && front.actorID) {
-      return front;
-    }
-    return null;
-  }
-
-  get client() {
-    return this._client;
-  }
-
-  // Tells us if we are debugging content document
-  // or if we are debugging chrome stuff.
-  // Allows to controls which features are available against
-  // a chrome or a content document.
-  get chrome() {
-    return this._chrome;
-  }
-
-  // Tells us if the related actor implements BrowsingContextTargetActor
-  // interface and requires to call `attach` request before being used and
-  // `detach` during cleanup.
-  get isBrowsingContext() {
-    return this._isBrowsingContext;
-  }
-
-  get name() {
-    if (this.isAddon) {
-      return this.form.name;
-    }
-    return this.title;
-  }
-
-  get url() {
-    return this._url;
-  }
-
-  get title() {
-    return this._title || this.url;
-  }
-
-  get isAddon() {
-    return this.isLegacyAddon || this.isWebExtension;
-  }
-
-  get isWorkerTarget() {
-    return this.activeTab.typeName === "workerTarget";
-  }
-
-  get isLegacyAddon() {
-    return !!(this.form && this.form.actor &&
-      this.form.actor.match(/conn\d+\.addon(Target)?\d+/));
-  }
-
-  get isWebExtension() {
-    return !!(this.form && this.form.actor && (
-      this.form.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
-      this.form.actor.match(/child\d+\/webExtension(Target)?\d+/)
-    ));
-  }
-
-  get isContentProcess() {
-    // browser content toolbox's form will be of the form:
-    //   server0.conn0.content-process0/contentProcessTarget7
-    // while xpcshell debugging will be:
-    //   server1.conn0.contentProcessTarget7
-    return !!(this.form && this.form.actor &&
-      this.form.actor.match(/conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/));
-  }
-
-  get isLocalTab() {
-    return !!this._tab;
-  }
-
-  get isMultiProcess() {
-    return !this.window;
-  }
-
-  get canRewind() {
-    return this.activeTab.traits.canRewind;
-  }
-
-  isReplayEnabled() {
-    return Services.prefs.getBoolPref("devtools.recordreplay.mvp.enabled")
-      && this.canRewind
-      && this.isLocalTab;
-  }
-
-  getExtensionPathName(url) {
-    // Return the url if the target is not a webextension.
-    if (!this.isWebExtension) {
-      throw new Error("Target is not a WebExtension");
-    }
-
-    try {
-      const parsedURL = new URL(url);
-      // Only moz-extension URL should be shortened into the URL pathname.
-      if (parsedURL.protocol !== "moz-extension:") {
-        return url;
-      }
-      return parsedURL.pathname;
-    } catch (e) {
-      // Return the url if unable to resolve the pathname.
-      return url;
-    }
-  }
-
-  /**
-   * For local tabs, returns the tab's contentPrincipal, which can be used as a
-   * `triggeringPrincipal` when opening links.  However, this is a hack as it is not
-   * correct for subdocuments and it won't work for remote debugging.  Bug 1467945 hopes
-   * to devise a better approach.
-   */
-  get contentPrincipal() {
-    if (!this.isLocalTab) {
-      return null;
-    }
-    return this.tab.linkedBrowser.contentPrincipal;
-  }
-
-  /**
-   * Attach the target and its console actor.
-   *
-   * This method will mainly call `attach` request on the target actor as well
-   * as the console actor.
-   * See DebuggerClient.attachTarget and DebuggerClient.attachConsole for more info.
-   * It also starts listenings to events the target actor will start emitting
-   * after being attached, like `tabDetached` and `frameUpdate`
-   */
-  attach() {
-    if (this._attach) {
-      return this._attach;
-    }
-
-    // Attach the target actor
-    const attachBrowsingContextTarget = async () => {
-      await this.activeTab.attach();
-
-      this.activeTab.on("tabNavigated", this._onTabNavigated);
-      this._onFrameUpdate = packet => {
-        this.emit("frame-update", packet);
-      };
-      this.activeTab.on("frameUpdate", this._onFrameUpdate);
-    };
-
-    // Attach the console actor
-    const attachConsole = async () => {
-      const [, consoleClient] = await this._client.attachConsole(
-        this.form.consoleActor, []);
-      this.activeConsole = consoleClient;
-
-      this._onInspectObject = packet => this.emit("inspect-object", packet);
-      this.activeConsole.on("inspectObject", this._onInspectObject);
-    };
-
-    this._attach = (async () => {
-      // 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();
-
-      // 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 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 ` +
-          `Addon (legacy).`);
-      }
-
-      // _setupRemoteListeners has to be called after the potential call to `attachTarget`
-      // as it depends on `activeTab` which is set by this method.
-      this._setupRemoteListeners();
-
-      // But all target actor have a console actor to attach
-      return attachConsole();
-    })();
-
-    return this._attach;
-  }
-
-  /**
-   * Listen to the different events.
-   */
-  _setupListeners() {
-    this.tab.addEventListener("TabClose", this);
-    this.tab.ownerDocument.defaultView.addEventListener("unload", this);
-    this.tab.addEventListener("TabRemotenessChange", this);
-  }
-
-  /**
-   * Teardown event listeners.
-   */
-  _teardownListeners() {
-    if (this._tab.ownerDocument.defaultView) {
-      this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
-    }
-    this._tab.removeEventListener("TabClose", this);
-    this._tab.removeEventListener("TabRemotenessChange", this);
-  }
-
-  /**
-   * Event listener for tabNavigated packet sent by activeTab's front.
-   */
-  _onTabNavigated(packet) {
-    const event = Object.create(null);
-    event.url = packet.url;
-    event.title = packet.title;
-    event.nativeConsoleAPI = packet.nativeConsoleAPI;
-    event.isFrameSwitching = packet.isFrameSwitching;
-
-    // Keep the title unmodified when a developer toolbox switches frame
-    // for a tab (Bug 1261687), but always update the title when the target
-    // is a WebExtension (where the addon name is always included in the title
-    // and the url is supposed to be updated every time the selected frame changes).
-    if (!packet.isFrameSwitching || this.isWebExtension) {
-      this._url = packet.url;
-      this._title = packet.title;
-    }
-
-    // Send any stored event payload (DOMWindow or nsIRequest) for backwards
-    // compatibility with non-remotable tools.
-    if (packet.state == "start") {
-      this.emit("will-navigate", event);
-    } else {
-      this.emit("navigate", event);
-    }
-  }
-
-  /**
-   * Setup listeners for remote debugging, updating existing ones as necessary.
-   */
-  _setupRemoteListeners() {
-    this.client.addListener("closed", this.destroy);
-
-    this.activeTab.on("tabDetached", this.destroy);
-
-    // These events should be ultimately listened from the thread client as
-    // they are coming from it and no longer go through the Target Actor/Front.
-    this._onSourceUpdated = packet => this.emit("source-updated", packet);
-    this.activeTab.on("newSource", this._onSourceUpdated);
-    this.activeTab.on("updatedSource", this._onSourceUpdated);
-  }
-
-  /**
-   * Teardown listeners for remote debugging.
-   */
-  _teardownRemoteListeners() {
-    // Remove listeners set in _setupRemoteListeners
-    this.client.removeListener("closed", this.destroy);
-    this.activeTab.off("tabDetached", this.destroy);
-    this.activeTab.off("newSource", this._onSourceUpdated);
-    this.activeTab.off("updatedSource", this._onSourceUpdated);
-
-    // Remove listeners set in attachTarget
-    if (this.isBrowsingContext) {
-      this.activeTab.off("tabNavigated", this._onTabNavigated);
-      this.activeTab.off("frameUpdate", this._onFrameUpdate);
-    }
-
-    // Remove listeners set in attachConsole
-    if (this.activeConsole && this._onInspectObject) {
-      this.activeConsole.off("inspectObject", this._onInspectObject);
-    }
-  }
-
-  /**
-   * Handle tabs events.
-   */
-  handleEvent(event) {
-    switch (event.type) {
-      case "TabClose":
-      case "unload":
-        this.destroy();
-        break;
-      case "TabRemotenessChange":
-        this.onRemotenessChange();
-        break;
-    }
-  }
-
-  /**
-   * Automatically respawn the toolbox when the tab changes between being
-   * loaded within the parent process and loaded from a content process.
-   * Process change can go in both ways.
-   */
-  onRemotenessChange() {
-    // Responsive design do a crazy dance around tabs and triggers
-    // remotenesschange events. But we should ignore them as at the end
-    // the content doesn't change its remoteness.
-    if (this._tab.isResponsiveDesignMode) {
-      return;
-    }
-
-    // Save a reference to the tab as it will be nullified on destroy
-    const tab = this._tab;
-    const onToolboxDestroyed = async (target) => {
-      if (target != this) {
-        return;
-      }
-      gDevTools.off("toolbox-destroyed", target);
-
-      // Recreate a fresh target instance as the current one is now destroyed
-      const newTarget = await TargetFactory.forTab(tab);
-      gDevTools.showToolbox(newTarget);
-    };
-    gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
-  }
-
-  /**
-   * Target is not alive anymore.
-   */
-  destroy() {
-    // If several things call destroy then we give them all the same
-    // destruction promise so we're sure to destroy only once
-    if (this._destroyer) {
-      return this._destroyer;
-    }
-
-    this._destroyer = (async () => {
-      // Before taking any action, notify listeners that destruction is imminent.
-      this.emit("close");
-
-      for (let [, front] of this.fronts) {
-        front = await front;
-        await front.destroy();
-      }
-
-      if (this._tab) {
-        this._teardownListeners();
-      }
-
-      this._teardownRemoteListeners();
-
-      if (this.isLocalTab) {
-        // We started with a local tab and created the client ourselves, so we
-        // should close it.
-        await this._client.close();
-      } else {
-        // The client was handed to us, so we are not responsible for closing
-        // it. We just need to detach from the tab, if already attached.
-        // |detach| may fail if the connection is already dead, so proceed with
-        // cleanup directly after this.
-        try {
-          await this.activeTab.detach();
-        } catch (e) {
-          console.warn(`Error while detaching target: ${e.message}`);
-        }
-      }
-
-      this._cleanup();
-    })();
-
-    return this._destroyer;
-  }
-
-  /**
-   * Clean up references to what this target points to.
-   */
-  _cleanup() {
-    if (this._tab) {
-      targets.delete(this._tab);
-    } else {
-      promiseTargets.delete(this.form);
-    }
-
-    this.activeTab = null;
-    this.activeConsole = null;
-    this._client = null;
-    this._tab = null;
-    this._attach = null;
-    this._title = null;
-    this._url = null;
-  }
-
-  toString() {
-    const id = this._tab ? this._tab : (this.form && this.form.actor);
-    return `Target:${id}`;
-  }
-
-  /**
-   * Log an error of some kind to the tab's console.
-   *
-   * @param {String} text
-   *                 The text to log.
-   * @param {String} category
-   *                 The category of the message.  @see nsIScriptError.
-   * @returns {Promise}
-   */
-  logErrorInPage(text, category) {
-    if (this.activeTab.traits.logInPage) {
-      const errorFlag = 0;
-      return this.activeTab.logInPage({ text, category, flags: errorFlag });
-    }
-    return Promise.resolve();
-  }
-
-  /**
-   * Log a warning of some kind to the tab's console.
-   *
-   * @param {String} text
-   *                 The text to log.
-   * @param {String} category
-   *                 The category of the message.  @see nsIScriptError.
-   * @returns {Promise}
-   */
-  logWarningInPage(text, category) {
-    if (this.activeTab.traits.logInPage) {
-      const warningFlag = 1;
-      return this.activeTab.logInPage({ text, category, flags: warningFlag });
-    }
-    return Promise.resolve();
-  }
-}
-exports.Target = Target;
--- a/devtools/client/framework/test/browser_two_tabs.js
+++ b/devtools/client/framework/test/browser_two_tabs.js
@@ -25,20 +25,20 @@ add_task(async () => {
   await client.connect();
 
   const tabs = await client.mainRoot.listTabs();
   const targetFront1 = tabs.find(a => a.url === TAB_URL_1);
   const targetFront2 = tabs.find(a => a.url === TAB_URL_2);
 
   await checkGetTab(client, tab1, tab2, targetFront1, targetFront2);
   await checkGetTabFailures(client);
-  await checkSelectedTargetActor(client, targetFront2);
+  await checkSelectedTargetActor(targetFront2);
 
   await removeTab(tab2);
-  await checkFirstTargetActor(client, targetFront1);
+  await checkFirstTargetActor(targetFront1);
 
   await removeTab(tab1);
   await client.close();
 });
 
 async function checkGetTab(client, tab1, tab2, targetFront1, targetFront2) {
   let front = await client.mainRoot.getTab({tab: tab1});
   is(targetFront1, front,
@@ -71,27 +71,21 @@ async function checkGetTabFailures(clien
   try {
     await client.mainRoot.getTab({ outerWindowID: -999 });
     ok(false, "getTab unexpectedly succeed with a wrong outerWindowID");
   } catch (error) {
     is(error, "Protocol error (noTab): Unable to find tab with outerWindowID '-999'");
   }
 }
 
-async function checkSelectedTargetActor(client, targetFront2) {
+async function checkSelectedTargetActor(targetFront2) {
   // Send a naive request to the second target actor to check if it works
-  const response = await client.request({
-    to: targetFront2.targetForm.consoleActor,
-    type: "startListeners",
-    listeners: [],
-  });
+  await targetFront2.attach();
+  const response = await targetFront2.activeConsole.startListeners([]);
   ok("startedListeners" in response, "Actor from the selected tab should respond to the request.");
 }
 
-async function checkFirstTargetActor(client, targetFront1) {
+async function checkFirstTargetActor(targetFront1) {
   // then send a request to the first target actor to check if it still works
-  const response = await client.request({
-    to: targetFront1.targetForm.consoleActor,
-    type: "startListeners",
-    listeners: [],
-  });
+  await targetFront1.attach();
+  const response = await targetFront1.activeConsole.startListeners([]);
   ok("startedListeners" in response, "Actor from the first tab should still respond.");
 }
--- a/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
+++ b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
@@ -154,17 +154,17 @@ add_task(async function testSidebarDOMNo
     await toolbox.target.getFront("webExtensionInspectedWindow");
   const sidebar = inspector.getPanel(SIDEBAR_ID);
   const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
 
   const expression = "({ body: document.body })";
 
   const evalResult = await inspectedWindowFront.eval(fakeExtCallerInfo, expression, {
     evalResultAsGrip: true,
-    toolboxConsoleActorID: toolbox.target.form.consoleActor,
+    toolboxConsoleActorID: toolbox.target.activeConsole.actor,
   });
 
   sidebar.setObjectValueGrip(evalResult.valueGrip);
 
   // Wait the DOM node to be rendered inside the component.
   await waitForObjectInspector(sidebarPanelContent, "node");
 
   // Wait for the object to be expanded so we only target the "body" property node, and
--- a/devtools/client/shared/test/browser_dbg_listtabs-03.js
+++ b/devtools/client/shared/test/browser_dbg_listtabs-03.js
@@ -24,28 +24,26 @@ add_task(async function test() {
   is(type, "browser", "Root actor should identify itself as a browser.");
   const tab = await addTab(TAB1_URL);
 
   let tabs = await client.mainRoot.listTabs();
   is(tabs.length, 2, "Should be two tabs");
   const tabFront = tabs.filter(a => a.url == TAB1_URL).pop();
   ok(tabFront, "Should have an actor for the tab");
 
-  let response = await tabFront.attach();
-  is(response.type, "tabAttached", "Should have attached");
+  await tabFront.attach();
 
   const previousActorID = tabFront.actorID;
-  response = await tabFront.detach();
+  let response = await tabFront.detach();
   is(response.type, "detached", "Should have detached");
 
   tabs = await client.mainRoot.listTabs();
   const newFront = tabs.find(a => a.url == TAB1_URL);
   is(newFront.actorID, previousActorID, "Should have the same actor for the same tab");
   isnot(newFront, tabFront, "But the front should be a new one");
 
-  response = await newFront.attach();
-  is(response.type, "tabAttached", "Should have attached");
+  await newFront.attach();
   response = await newFront.detach();
   is(response.type, "detached", "Should have detached");
 
   await removeTab(tab);
   await client.close();
 });
--- a/devtools/server/tests/unit/head_dbg.js
+++ b/devtools/server/tests/unit/head_dbg.js
@@ -385,19 +385,16 @@ async function getTestTab(client, title)
   return null;
 }
 
 // Attach to |client|'s tab whose title is |title|; and return the targetFront instance
 // referring to that tab.
 async function attachTestTab(client, title) {
   const targetFront = await getTestTab(client, title);
   await targetFront.attach();
-  const response = await targetFront.attach();
-  Assert.equal(response.type, "tabAttached");
-  Assert.ok(typeof response.threadActor === "string");
   return targetFront;
 }
 
 // Attach to |client|'s tab whose title is |title|, and then attach to
 // that tab's thread. Pass |callback| the thread attach response packet, a
 // TargetFront referring to the tab, and a ThreadClient referring to the
 // thread.
 async function attachTestThread(client, title, callback = () => {}) {
--- a/devtools/shared/fronts/targets/addon.js
+++ b/devtools/shared/fronts/targets/addon.js
@@ -70,21 +70,27 @@ class AddonTargetFront extends FrontClas
       front.form(form);
       this.manage(front);
       return front;
     }
     return this;
   }
 
   async attach() {
-    const response = await super.attach();
+    if (this._attach) {
+      return this._attach;
+    }
+    this._attach = (async () => {
+      const response = await super.attach();
 
-    this.threadActor = response.threadActor;
+      this.threadActor = response.threadActor;
 
-    return response;
+      return this.attachConsole();
+    })();
+    return this._attach;
   }
 
   reconfigure() {
     // Toolbox and options panel are calling this method but Addon Target can't be
     // reconfigured. So we ignore this call here.
     return Promise.resolve();
   }
 
--- a/devtools/shared/fronts/targets/browsing-context.js
+++ b/devtools/shared/fronts/targets/browsing-context.js
@@ -1,48 +1,50 @@
 /* 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 {browsingContextTargetSpec} = require("devtools/shared/specs/targets/browsing-context");
 const { FrontClassWithSpec, registerFront } = require("devtools/shared/protocol");
+const { TargetMixin } = require("./target-mixin");
 
 loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
 
-class BrowsingContextTargetFront extends FrontClassWithSpec(browsingContextTargetSpec) {
+class BrowsingContextTargetFront extends
+  TargetMixin(FrontClassWithSpec(browsingContextTargetSpec)) {
   constructor(client) {
     super(client);
 
     this.thread = null;
 
     // Cache the value of some target properties that are being returned by `attach`
     // request and then keep them up-to-date in `reconfigure` request.
     this.configureOptions = {
       javascriptEnabled: null,
     };
 
     // RootFront.listTabs is going to update this state via `setIsSelected`  method
     this._selected = false;
 
-    // TODO: remove once ThreadClient becomes a front
-    this.client = client;
+    this._onTabNavigated = this._onTabNavigated.bind(this);
+    this._onFrameUpdate = this._onFrameUpdate.bind(this);
   }
 
   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;
 
     this.outerWindowID = json.outerWindowID;
     this.favicon = json.favicon;
-    this.title = json.title;
-    this.url = json.url;
+    this._title = json.title;
+    this._url = json.url;
   }
 
   // Reports if the related tab is selected. Only applies to BrowsingContextTarget
   // issued from RootFront.listTabs.
   get selected() {
     return this._selected;
   }
 
@@ -56,37 +58,87 @@ class BrowsingContextTargetFront extends
    *
    * @param object options
    *        Configuration options.
    */
   attachThread(options = {}) {
     if (this.thread) {
       return Promise.resolve([{}, this.thread]);
     }
-
     const packet = {
       to: this._threadActor,
       type: "attach",
       options,
     };
     return this.client.request(packet).then(response => {
       this.thread = new ThreadClient(this, this._threadActor);
       this.client.registerClient(this.thread);
       return [response, this.thread];
     });
   }
 
-  async attach() {
-    const response = await super.attach();
+  /**
+   * Event listener for `frameUpdate` event.
+   */
+  _onFrameUpdate(packet) {
+    this.emit("frame-update", packet);
+  }
+
+  /**
+   * Event listener for `tabNavigated` event.
+   */
+  _onTabNavigated(packet) {
+    const event = Object.create(null);
+    event.url = packet.url;
+    event.title = packet.title;
+    event.nativeConsoleAPI = packet.nativeConsoleAPI;
+    event.isFrameSwitching = packet.isFrameSwitching;
+
+    // Keep the title unmodified when a developer toolbox switches frame
+    // for a tab (Bug 1261687), but always update the title when the target
+    // is a WebExtension (where the addon name is always included in the title
+    // and the url is supposed to be updated every time the selected frame changes).
+    if (!packet.isFrameSwitching || this.isWebExtension) {
+      this._url = packet.url;
+      this._title = packet.title;
+    }
 
-    this._threadActor = response.threadActor;
-    this.configureOptions.javascriptEnabled = response.javascriptEnabled;
-    this.traits = response.traits || {};
+    // Send any stored event payload (DOMWindow or nsIRequest) for backwards
+    // compatibility with non-remotable tools.
+    if (packet.state == "start") {
+      this.emit("will-navigate", event);
+    } else {
+      this.emit("navigate", event);
+    }
+  }
 
-    return response;
+  async attach() {
+    if (this._attach) {
+      return this._attach;
+    }
+    this._attach = (async () => {
+      // All Browsing context inherited target emit a few event that are being
+      // translated on the target class. Listen for them before attaching as they
+      // can start firing on attach call.
+      this.on("tabNavigated", this._onTabNavigated);
+      this.on("frameUpdate", this._onFrameUpdate);
+
+      const response = await super.attach();
+
+      this._threadActor = response.threadActor;
+      this.configureOptions.javascriptEnabled = response.javascriptEnabled;
+      this.traits = response.traits || {};
+
+      // xpcshell tests from devtools/server/tests/unit/ are implementing
+      // fake BrowsingContextTargetActor which do not expose any console actor.
+      if (this.targetForm.consoleActor) {
+        await this.attachConsole();
+      }
+    })();
+    return this._attach;
   }
 
   async reconfigure({ options }) {
     const response = await super.reconfigure({ options });
 
     if (typeof options.javascriptEnabled != "undefined") {
       this.configureOptions.javascriptEnabled = options.javascriptEnabled;
     }
@@ -106,16 +158,18 @@ class BrowsingContextTargetFront extends
     if (this.thread) {
       try {
         await this.thread.detach();
       } catch (e) {
         console.warn(`Error while detaching the thread front: ${e.message}`);
       }
     }
 
-    this.destroy();
+    // Remove listeners set in attach
+    this.off("tabNavigated", this._onTabNavigated);
+    this.off("frameUpdate", this._onFrameUpdate);
 
     return response;
   }
 }
 
 exports.BrowsingContextTargetFront = BrowsingContextTargetFront;
-registerFront(BrowsingContextTargetFront);
+registerFront(exports.BrowsingContextTargetFront);
--- a/devtools/shared/fronts/targets/content-process.js
+++ b/devtools/shared/fronts/targets/content-process.js
@@ -1,39 +1,48 @@
 /* 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 {contentProcessTargetSpec} = require("devtools/shared/specs/targets/content-process");
 const { FrontClassWithSpec, registerFront } = require("devtools/shared/protocol");
+const { TargetMixin } = require("./target-mixin");
 
-class ContentProcessTargetFront extends FrontClassWithSpec(contentProcessTargetSpec) {
+class ContentProcessTargetFront extends
+  TargetMixin(FrontClassWithSpec(contentProcessTargetSpec)) {
   constructor(client) {
     super(client);
 
-    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;
     this.chromeDebugger = json.chromeDebugger;
   }
 
   attachThread() {
     return this.client.attachThread(this.chromeDebugger);
   }
 
+  attach() {
+    // All target actors have a console actor to attach.
+    // All but xpcshell test actors... which is using a ContentProcessTargetActor
+    if (this.targetForm.consoleActor) {
+      return this.attachConsole();
+    }
+    return Promise.resolve();
+  }
+
   reconfigure() {
     // Toolbox and options panel are calling this method but Worker Target can't be
     // reconfigured. So we ignore this call here.
     return Promise.resolve();
   }
 }
 
 exports.ContentProcessTargetFront = ContentProcessTargetFront;
-registerFront(ContentProcessTargetFront);
+registerFront(exports.ContentProcessTargetFront);
--- a/devtools/shared/fronts/targets/moz.build
+++ b/devtools/shared/fronts/targets/moz.build
@@ -3,10 +3,11 @@
 # 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',
     'browsing-context.js',
     'content-process.js',
+    'target-mixin.js',
     'worker.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/targets/target-mixin.js
@@ -0,0 +1,571 @@
+/* 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 Services = require("Services");
+
+// We are requiring a module from client whereas this module is from shared.
+// This shouldn't happen, but Fronts should rather be part of client anyway.
+// Otherwise gDevTools is only used for local tabs and should propably only
+// used by a subclass, specific to local tabs.
+loader.lazyRequireGetter(this, "gDevTools",
+  "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "TargetFactory",
+  "devtools/client/framework/target", true);
+loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", true);
+
+/**
+ * A Target represents a debuggable context. It can be a browser tab, a tab on
+ * a remote device, like a tab on Firefox for Android. But it can also be an add-on,
+ * as well as firefox parent process, or just one of its content process.
+ * A Target is related to a given TargetActor, for which we derive this class.
+ *
+ * Providing a generalized abstraction of a web-page or web-browser (available
+ * either locally or remotely) is beyond the scope of this class (and maybe
+ * also beyond the scope of this universe) However Target does attempt to
+ * abstract some common events and read-only properties common to many Tools.
+ *
+ * Supported read-only properties:
+ * - name, url
+ *
+ * Target extends EventEmitter and provides support for the following events:
+ * - close: The target window has been closed. All tools attached to this
+ *          target should close. This event is not currently cancelable.
+ *
+ * Optional events only dispatched by BrowsingContextTarget:
+ * - will-navigate: The target window will navigate to a different URL
+ * - navigate: The target window has navigated to a different URL
+ */
+function TargetMixin(parentClass) {
+  class Target extends parentClass {
+    constructor(client, form) {
+      super(client, form);
+
+      this._forceChrome = false;
+
+      this.destroy = this.destroy.bind(this);
+      this.activeConsole = null;
+
+      this._client = client;
+
+      // Cache of already created targed-scoped fronts
+      // [typeName:string => Front instance]
+      this.fronts = new Map();
+      // Temporary fix for bug #1493131 - inspector has a different life cycle
+      // than most other fronts because it is closely related to the toolbox.
+      // TODO: remove once inspector is separated from the toolbox
+      this._inspector = null;
+
+      this._setupRemoteListeners();
+    }
+
+    attachTab(tab) {
+      // When debugging local tabs, we also have a reference to the Firefox tab
+      // This is used to:
+      // * distinguish local tabs from remote (see target.isLocalTab)
+      // * being able to hookup into Firefox UI (see Hosts)
+      this._tab = tab;
+      this._setupListeners();
+    }
+
+    /**
+     * Returns a promise for the protocol description from the root actor. Used
+     * internally with `target.actorHasMethod`. Takes advantage of caching if
+     * definition was fetched previously with the corresponding actor information.
+     * Actors are lazily loaded, so not only must the tool using a specific actor
+     * be in use, the actors are only registered after invoking a method (for
+     * performance reasons, added in bug 988237), so to use these actor detection
+     * methods, one must already be communicating with a specific actor of that
+     * type.
+     *
+     * @return {Promise}
+     * {
+     *   "category": "actor",
+     *   "typeName": "longstractor",
+     *   "methods": [{
+     *     "name": "substring",
+     *     "request": {
+     *       "type": "substring",
+     *       "start": {
+     *         "_arg": 0,
+     *         "type": "primitive"
+     *       },
+     *       "end": {
+     *         "_arg": 1,
+     *         "type": "primitive"
+     *       }
+     *     },
+     *     "response": {
+     *       "substring": {
+     *         "_retval": "primitive"
+     *       }
+     *     }
+     *   }],
+     *  "events": {}
+     * }
+     */
+    async getActorDescription(actorName) {
+      if (this._protocolDescription &&
+          this._protocolDescription.types[actorName]) {
+        return this._protocolDescription.types[actorName];
+      }
+      const description = await this.client.mainRoot.protocolDescription();
+      this._protocolDescription = description;
+      return description.types[actorName];
+    }
+
+    /**
+     * Returns a boolean indicating whether or not the specific actor
+     * type exists.
+     *
+     * @param {String} actorName
+     * @return {Boolean}
+     */
+    hasActor(actorName) {
+      if (this.targetForm) {
+        return !!this.targetForm[actorName + "Actor"];
+      }
+      return false;
+    }
+
+    /**
+     * Queries the protocol description to see if an actor has
+     * an available method. The actor must already be lazily-loaded (read
+     * the restrictions in the `getActorDescription` comments),
+     * so this is for use inside of tool. Returns a promise that
+     * resolves to a boolean.
+     *
+     * @param {String} actorName
+     * @param {String} methodName
+     * @return {Promise}
+     */
+    actorHasMethod(actorName, methodName) {
+      return this.getActorDescription(actorName).then(desc => {
+        if (desc && desc.methods) {
+          return !!desc.methods.find(method => method.name === methodName);
+        }
+        return false;
+      });
+    }
+
+    /**
+     * Returns a trait from the root actor.
+     *
+     * @param {String} traitName
+     * @return {Mixed}
+     */
+    getTrait(traitName) {
+      // If the targeted actor exposes traits and has a defined value for this
+      // traits, override the root actor traits
+      if (this.targetForm.traits && traitName in this.targetForm.traits) {
+        return this.targetForm.traits[traitName];
+      }
+
+      return this.client.traits[traitName];
+    }
+
+    get tab() {
+      return this._tab;
+    }
+
+    // Get a promise of the RootActor's form
+    get root() {
+      return this.client.mainRoot.rootForm;
+    }
+
+    // Temporary fix for bug #1493131 - inspector has a different life cycle
+    // than most other fronts because it is closely related to the toolbox.
+    // TODO: remove once inspector is separated from the toolbox
+    async getInspector() {
+      // the front might have been destroyed and no longer have an actor ID
+      if (this._inspector && this._inspector.actorID) {
+        return this._inspector;
+      }
+      this._inspector = await getFront(this.client, "inspector", this.targetForm);
+      this.emit("inspector", this._inspector);
+      return this._inspector;
+    }
+
+    // Run callback on every front of this type that currently exists, and on every
+    // instantiation of front type in the future.
+    onFront(typeName, callback) {
+      const front = this.fronts.get(typeName);
+      if (front) {
+        return callback(front);
+      }
+      return this.on(typeName, callback);
+    }
+
+    // Get a Front for a target-scoped actor.
+    // i.e. an actor served by RootActor.listTabs or RootActorActor.getTab requests
+    async getFront(typeName) {
+      let front = this.fronts.get(typeName);
+      // the front might have been destroyed and no longer have an actor ID
+      if (front && front.actorID || front && typeof front.then === "function") {
+        return front;
+      }
+      front = getFront(this.client, typeName, this.targetForm);
+      this.fronts.set(typeName, front);
+      // replace the placeholder with the instance of the front once it has loaded
+      front = await front;
+      this.emit(typeName, front);
+      this.fronts.set(typeName, front);
+      return front;
+    }
+
+    getCachedFront(typeName) {
+      // do not wait for async fronts;
+      const front = this.fronts.get(typeName);
+      // ensure that the front is a front, and not async front
+      if (front && front.actorID) {
+        return front;
+      }
+      return null;
+    }
+
+    get client() {
+      return this._client;
+    }
+
+    // Tells us if we are debugging content document
+    // or if we are debugging chrome stuff.
+    // Allows to controls which features are available against
+    // a chrome or a content document.
+    get chrome() {
+      return this.isAddon || this.isContentProcess || this.isParentProcess ||
+        this.isWindowTarget || this._forceChrome;
+    }
+
+    forceChrome() {
+      this._forceChrome = true;
+    }
+
+    // Tells us if the related actor implements BrowsingContextTargetActor
+    // interface and requires to call `attach` request before being used and
+    // `detach` during cleanup.
+    get isBrowsingContext() {
+      return this.typeName === "browsingContextTarget";
+    }
+
+    get name() {
+      if (this.isAddon) {
+        return this.targetForm.name;
+      }
+      return this.title;
+    }
+
+    get title() {
+      return this._title || this.url;
+    }
+
+    get url() {
+      return this._url;
+    }
+
+    get isAddon() {
+      return this.isLegacyAddon || this.isWebExtension;
+    }
+
+    get isWorkerTarget() {
+      return this.typeName === "workerTarget";
+    }
+
+    get isLegacyAddon() {
+      return !!(this.targetForm && this.targetForm.actor &&
+        this.targetForm.actor.match(/conn\d+\.addon(Target)?\d+/));
+    }
+
+    get isWebExtension() {
+      return !!(this.targetForm && this.targetForm.actor && (
+        this.targetForm.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
+        this.targetForm.actor.match(/child\d+\/webExtension(Target)?\d+/)
+      ));
+    }
+
+    get isContentProcess() {
+      // browser content toolbox's form will be of the form:
+      //   server0.conn0.content-process0/contentProcessTarget7
+      // while xpcshell debugging will be:
+      //   server1.conn0.contentProcessTarget7
+      return !!(this.targetForm && this.targetForm.actor &&
+        this.targetForm.actor.match(
+          /conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/));
+    }
+
+    get isParentProcess() {
+      return !!(this.targetForm && this.targetForm.actor &&
+        this.targetForm.actor.match(/conn\d+\.parentProcessTarget\d+/));
+    }
+
+    get isWindowTarget() {
+      return !!(this.targetForm && this.targetForm.actor &&
+        this.targetForm.actor.match(/conn\d+\.chromeWindowTarget\d+/));
+    }
+
+    get isLocalTab() {
+      return !!this._tab;
+    }
+
+    get isMultiProcess() {
+      return !this.window;
+    }
+
+    get canRewind() {
+      return this.traits.canRewind;
+    }
+
+    isReplayEnabled() {
+      return Services.prefs.getBoolPref("devtools.recordreplay.mvp.enabled")
+        && this.canRewind
+        && this.isLocalTab;
+    }
+
+    getExtensionPathName(url) {
+      // Return the url if the target is not a webextension.
+      if (!this.isWebExtension) {
+        throw new Error("Target is not a WebExtension");
+      }
+
+      try {
+        const parsedURL = new URL(url);
+        // Only moz-extension URL should be shortened into the URL pathname.
+        if (parsedURL.protocol !== "moz-extension:") {
+          return url;
+        }
+        return parsedURL.pathname;
+      } catch (e) {
+        // Return the url if unable to resolve the pathname.
+        return url;
+      }
+    }
+
+    /**
+     * For local tabs, returns the tab's contentPrincipal, which can be used as a
+     * `triggeringPrincipal` when opening links.  However, this is a hack as it is not
+     * correct for subdocuments and it won't work for remote debugging.  Bug 1467945 hopes
+     * to devise a better approach.
+     */
+    get contentPrincipal() {
+      if (!this.isLocalTab) {
+        return null;
+      }
+      return this.tab.linkedBrowser.contentPrincipal;
+    }
+
+    // Attach the console actor
+    async attachConsole() {
+      const [, consoleClient] = await this._client.attachConsole(
+        this.targetForm.consoleActor, []);
+      this.activeConsole = consoleClient;
+
+      this._onInspectObject = packet => this.emit("inspect-object", packet);
+      this.activeConsole.on("inspectObject", this._onInspectObject);
+    }
+
+    /**
+     * Listen to the different events.
+     */
+    _setupListeners() {
+      this.tab.addEventListener("TabClose", this);
+      this.tab.ownerDocument.defaultView.addEventListener("unload", this);
+      this.tab.addEventListener("TabRemotenessChange", this);
+    }
+
+    /**
+     * Teardown event listeners.
+     */
+    _teardownListeners() {
+      if (this._tab.ownerDocument.defaultView) {
+        this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
+      }
+      this._tab.removeEventListener("TabClose", this);
+      this._tab.removeEventListener("TabRemotenessChange", this);
+    }
+
+    /**
+     * Setup listeners for remote debugging, updating existing ones as necessary.
+     */
+    _setupRemoteListeners() {
+      this.client.addListener("closed", this.destroy);
+
+      this.on("tabDetached", this.destroy);
+
+      // These events should be ultimately listened from the thread client as
+      // they are coming from it and no longer go through the Target Actor/Front.
+      this._onSourceUpdated = packet => this.emit("source-updated", packet);
+      this.on("newSource", this._onSourceUpdated);
+      this.on("updatedSource", this._onSourceUpdated);
+    }
+
+    /**
+     * Teardown listeners for remote debugging.
+     */
+    _teardownRemoteListeners() {
+      // Remove listeners set in _setupRemoteListeners
+      this.client.removeListener("closed", this.destroy);
+      this.off("tabDetached", this.destroy);
+      this.off("newSource", this._onSourceUpdated);
+      this.off("updatedSource", this._onSourceUpdated);
+
+      // Remove listeners set in attachConsole
+      if (this.activeConsole && this._onInspectObject) {
+        this.activeConsole.off("inspectObject", this._onInspectObject);
+      }
+    }
+
+    /**
+     * Handle tabs events.
+     */
+    handleEvent(event) {
+      switch (event.type) {
+        case "TabClose":
+        case "unload":
+          this.destroy();
+          break;
+        case "TabRemotenessChange":
+          this.onRemotenessChange();
+          break;
+      }
+    }
+
+    /**
+     * Automatically respawn the toolbox when the tab changes between being
+     * loaded within the parent process and loaded from a content process.
+     * Process change can go in both ways.
+     */
+    onRemotenessChange() {
+      // Responsive design do a crazy dance around tabs and triggers
+      // remotenesschange events. But we should ignore them as at the end
+      // the content doesn't change its remoteness.
+      if (this._tab.isResponsiveDesignMode) {
+        return;
+      }
+
+      // Save a reference to the tab as it will be nullified on destroy
+      const tab = this._tab;
+      const onToolboxDestroyed = async (target) => {
+        if (target != this) {
+          return;
+        }
+        gDevTools.off("toolbox-destroyed", target);
+
+        // Recreate a fresh target instance as the current one is now destroyed
+        const newTarget = await TargetFactory.forTab(tab);
+        gDevTools.showToolbox(newTarget);
+      };
+      gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
+    }
+
+    /**
+     * Target is not alive anymore.
+     */
+    destroy() {
+      // If several things call destroy then we give them all the same
+      // destruction promise so we're sure to destroy only once
+      if (this._destroyer) {
+        return this._destroyer;
+      }
+
+      this._destroyer = (async () => {
+        // Before taking any action, notify listeners that destruction is imminent.
+        this.emit("close");
+
+        for (let [, front] of this.fronts) {
+          front = await front;
+          await front.destroy();
+        }
+
+        if (this._tab) {
+          this._teardownListeners();
+        }
+
+        this._teardownRemoteListeners();
+
+        if (this.isLocalTab) {
+          // We started with a local tab and created the client ourselves, so we
+          // should close it.
+          await this._client.close();
+
+        // Not all targets supports attach/detach. For example content process doesn't.
+        // Also ensure that the front is still active before trying to do the request.
+        } else if (this.detach && this.actorID) {
+          // The client was handed to us, so we are not responsible for closing
+          // it. We just need to detach from the tab, if already attached.
+          // |detach| may fail if the connection is already dead, so proceed with
+          // cleanup directly after this.
+          try {
+            await this.detach();
+          } catch (e) {
+            console.warn(`Error while detaching target: ${e.message}`);
+          }
+        }
+
+        // Do that very last in order to let a chance to call detach.
+        super.destroy();
+
+        this._cleanup();
+      })();
+
+      return this._destroyer;
+    }
+
+    /**
+     * Clean up references to what this target points to.
+     */
+    _cleanup() {
+      this.activeConsole = null;
+      this._client = null;
+      this._tab = null;
+
+      // All target front subclasses set this variable in their `attach` method.
+      // None of them overload destroy, so clean this up from here.
+      this._attach = null;
+
+      this._title = null;
+      this._url = null;
+    }
+
+    toString() {
+      const id = this._tab ? this._tab : (this.targetForm && this.targetForm.actor);
+      return `Target:${id}`;
+    }
+
+    /**
+     * Log an error of some kind to the tab's console.
+     *
+     * @param {String} text
+     *                 The text to log.
+     * @param {String} category
+     *                 The category of the message.  @see nsIScriptError.
+     * @returns {Promise}
+     */
+    logErrorInPage(text, category) {
+      if (this.traits.logInPage) {
+        const errorFlag = 0;
+        return this.logInPage({ text, category, flags: errorFlag });
+      }
+      return Promise.resolve();
+    }
+
+    /**
+     * Log a warning of some kind to the tab's console.
+     *
+     * @param {String} text
+     *                 The text to log.
+     * @param {String} category
+     *                 The category of the message.  @see nsIScriptError.
+     * @returns {Promise}
+     */
+    logWarningInPage(text, category) {
+      if (this.traits.logInPage) {
+        const warningFlag = 1;
+        return this.logInPage({ text, category, flags: warningFlag });
+      }
+      return Promise.resolve();
+    }
+  }
+  return Target;
+}
+exports.TargetMixin = TargetMixin;
--- a/devtools/shared/fronts/targets/worker.js
+++ b/devtools/shared/fronts/targets/worker.js
@@ -1,76 +1,83 @@
 /* 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 {workerTargetSpec} = require("devtools/shared/specs/targets/worker");
 const { FrontClassWithSpec, registerFront } = require("devtools/shared/protocol");
+const { TargetMixin } = require("./target-mixin");
 
 loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
 
-class WorkerTargetFront extends FrontClassWithSpec(workerTargetSpec) {
+class WorkerTargetFront extends
+  TargetMixin(FrontClassWithSpec(workerTargetSpec)) {
   constructor(client) {
     super(client);
 
     this.thread = null;
     this.traits = {};
 
-    // TODO: remove once ThreadClient becomes a front
-    this.client = client;
-
     this._isClosed = false;
 
-    this.destroy = this.destroy.bind(this);
-    this.on("close", this.destroy);
+    // The actor sends a "close" event, which is translated to "worker-close" by
+    // the specification in order to not conflict with Target's "close" event.
+    // This event is similar to tabDetached and means that the worker is destroyed.
+    // So that we should destroy the target in order to significate that the target
+    // is no longer debuggable.
+    this.once("worker-close", this.destroy.bind(this));
   }
 
   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;
-    this.url = json.url;
+    this._url = json.url;
     this.type = json.type;
     this.scope = json.scope;
     this.fetch = json.fetch;
   }
 
   get isClosed() {
     return this._isClosed;
   }
 
   destroy() {
-    this.off("close", this.destroy);
     this._isClosed = true;
 
     if (this.thread) {
       this.client.unregisterClient(this.thread);
+      this.thread = null;
     }
 
-    this.unmanage(this);
-
     super.destroy();
   }
 
   async attach() {
-    const response = await super.attach();
+    if (this._attach) {
+      return this._attach;
+    }
+    this._attach = (async () => {
+      const response = await super.attach();
 
-    this.url = response.url;
+      this._url = response.url;
 
-    // Immediately call `connect` in other to fetch console and thread actors
-    // that will be later used by Target.
-    const connectResponse = await this.connect({});
-    // Set the console actor ID on the form to expose it to Target.attach's attachConsole
-    this.targetForm.consoleActor = connectResponse.consoleActor;
-    this.threadActor = connectResponse.threadActor;
+      // Immediately call `connect` in other to fetch console and thread actors
+      // that will be later used by Target.
+      const connectResponse = await this.connect({});
+      // Set the console actor ID on the form to expose it to Target.attachConsole
+      this.targetForm.consoleActor = connectResponse.consoleActor;
+      this.threadActor = connectResponse.threadActor;
 
-    return response;
+      return this.attachConsole();
+    })();
+    return this._attach;
   }
 
   async detach() {
     if (this.isClosed) {
       return {};
     }
     let response;
     try {
@@ -106,9 +113,9 @@ class WorkerTargetFront extends FrontCla
     this.thread = new ThreadClient(this, this.threadActor);
     this.client.registerClient(this.thread);
 
     return [attachResponse, this.thread];
   }
 }
 
 exports.WorkerTargetFront = WorkerTargetFront;
-registerFront(WorkerTargetFront);
+registerFront(exports.WorkerTargetFront);
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -1507,17 +1507,17 @@ var generateRequestMethods = function(ac
         return undefined;
       }
 
       return this.request(packet).then(response => {
         let ret;
         try {
           ret = spec.response.read(response, this);
         } catch (ex) {
-          console.error("Error reading response to: " + name);
+          console.error("Error reading response to: " + name + "\n" + ex);
           throw ex;
         }
         return ret;
       });
     };
 
     // Release methods should call the destroy function on return.
     if (spec.release) {
--- a/devtools/shared/specs/targets/worker.js
+++ b/devtools/shared/specs/targets/worker.js
@@ -27,17 +27,18 @@ const workerTargetSpec = generateActorSp
       request: {},
       response: RetVal("json"),
     },
   },
 
   events: {
     // WorkerTargetActor still uses old sendActorEvent function,
     // but it should use emit instead.
-    close: {
+    // Do not emit a `close` event as Target class emit this event on destroy
+    "worker-close": {
       type: "close",
     },
     // newSource is being sent by ThreadActor in the name of its parent,
     // i.e. WorkerTargetActor
     newSource: {
       type: "newSource",
       source: Option(0, "json"),
     },