Bug 1450948 - Convert ChromeActor to protocol.js r=ochameau draft
authoryulia <ystartsev@mozilla.com>
Tue, 24 Apr 2018 15:30:38 +0200
changeset 787833 4003f1c84108aed4f37bca2d3c8cfb32d5ac40e9
parent 787539 6eeb97ca94f40189d5aa552da9e0b0b11bfa0441
child 787834 0d2a86cde36a377cb0467d419b1f9fe04ce4c310
child 787837 a66c3e735b368001fa99cd1f29eb25e54c74b8ed
child 787839 1121d8482cd56f18ca8608b8e6db467f87d97bac
child 788335 80421fa98d9f988cf0b73508a811b41568012465
push id107802
push userbmo:ystartsev@mozilla.com
push dateWed, 25 Apr 2018 13:14:31 +0000
reviewersochameau
bugs1450948
milestone61.0a1
Bug 1450948 - Convert ChromeActor to protocol.js r=ochameau MozReview-Commit-ID: 1pwYUXGiEdT * fix types in tab.js MozReview-Commit-ID: 2PAU8IeEKDV
devtools/server/actors/chrome.js
devtools/server/actors/tab.js
devtools/server/actors/webextension.js
devtools/shared/protocol.js
devtools/shared/specs/index.js
devtools/shared/specs/moz.build
devtools/shared/specs/tab.js
--- a/devtools/server/actors/chrome.js
+++ b/devtools/server/actors/chrome.js
@@ -5,16 +5,20 @@
 "use strict";
 
 const { Ci } = require("chrome");
 const Services = require("Services");
 const { DebuggerServer } = require("../main");
 const { getChildDocShells, TabActor } = require("./tab");
 const makeDebugger = require("./utils/make-debugger");
 
+const { extend } = require("devtools/shared/extend");
+const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol");
+const { tabSpec } = require("devtools/shared/specs/tab");
+
 /**
  * Creates a TabActor for debugging all the chrome content in the
  * current process. Most of the implementation is inherited from TabActor.
  * ChromeActor is a child of RootActor, it can be instanciated via
  * RootActor.getProcess request.
  * ChromeActor exposes all tab actors via its form() request, like TabActor.
  *
  * History lecture:
@@ -25,17 +29,27 @@ const makeDebugger = require("./utils/ma
  * So we are now exposing a process actor that offers the same API as TabActor
  * by inheriting its functionality.
  * Global actors are now only the actors that are meant to be global,
  * and are no longer related to any specific scope/document.
  *
  * @param connection DebuggerServerConnection
  *        The connection to the client.
  */
-function ChromeActor(connection) {
+
+/**
+ * Protocol.js expects only the prototype object, and does not maintain the prototype
+ * chain when it constructs the ActorClass. For this reason we are using `extend` to
+ * maintain the properties of TabActor.prototype
+ * */
+
+const chromePrototype = extend({}, TabActor.prototype);
+
+chromePrototype.initialize = function(connection) {
+  Actor.prototype.initialize.call(this, connection);
   TabActor.call(this, connection);
 
   // This creates a Debugger instance for chrome debugging all globals.
   this.makeDebugger = makeDebugger.bind(null, {
     findDebuggees: dbg => dbg.findAllGlobals(),
     shouldAddNewGlobalAsDebuggee: () => true
   });
 
@@ -64,62 +78,57 @@ function ChromeActor(connection) {
   // On XPCShell, there is no window/docshell
   let docShell = window ? window.QueryInterface(Ci.nsIInterfaceRequestor)
                                 .getInterface(Ci.nsIDocShell)
                         : null;
   Object.defineProperty(this, "docShell", {
     value: docShell,
     configurable: true
   });
-}
-exports.ChromeActor = ChromeActor;
+};
 
-ChromeActor.prototype = Object.create(TabActor.prototype);
-
-ChromeActor.prototype.constructor = ChromeActor;
-
-ChromeActor.prototype.isRootActor = true;
+chromePrototype.isRootActor = true;
 
 /**
  * Getter for the list of all docshells in this tabActor
  * @return {Array}
  */
-Object.defineProperty(ChromeActor.prototype, "docShells", {
+Object.defineProperty(chromePrototype, "docShells", {
   get: function() {
     // Iterate over all top-level windows and all their docshells.
     let docShells = [];
     let e = Services.ww.getWindowEnumerator();
     while (e.hasMoreElements()) {
       let window = e.getNext();
       let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIWebNavigation)
                            .QueryInterface(Ci.nsIDocShell);
       docShells = docShells.concat(getChildDocShells(docShell));
     }
 
     return docShells;
   }
 });
 
-ChromeActor.prototype.observe = function(subject, topic, data) {
+chromePrototype.observe = function(subject, topic, data) {
   TabActor.prototype.observe.call(this, subject, topic, data);
   if (!this.attached) {
     return;
   }
 
   subject.QueryInterface(Ci.nsIDocShell);
 
   if (topic == "chrome-webnavigation-create") {
     this._onDocShellCreated(subject);
   } else if (topic == "chrome-webnavigation-destroy") {
     this._onDocShellDestroy(subject);
   }
 };
 
-ChromeActor.prototype._attach = function() {
+chromePrototype._attach = function() {
   if (this.attached) {
     return false;
   }
 
   TabActor.prototype._attach.call(this);
 
   // Listen for any new/destroyed chrome docshell
   Services.obs.addObserver(this, "chrome-webnavigation-create");
@@ -135,17 +144,17 @@ ChromeActor.prototype._attach = function
     if (docShell == this.docShell) {
       continue;
     }
     this._progressListener.watch(docShell);
   }
   return undefined;
 };
 
-ChromeActor.prototype._detach = function() {
+chromePrototype._detach = function() {
   if (!this.attached) {
     return false;
   }
 
   Services.obs.removeObserver(this, "chrome-webnavigation-create");
   Services.obs.removeObserver(this, "chrome-webnavigation-destroy");
 
   // Iterate over all top-level windows.
@@ -165,34 +174,38 @@ ChromeActor.prototype._detach = function
   return undefined;
 };
 
 /* ThreadActor hooks. */
 
 /**
  * Prepare to enter a nested event loop by disabling debuggee events.
  */
-ChromeActor.prototype.preNest = function() {
+chromePrototype.preNest = function() {
   // Disable events in all open windows.
   let e = Services.wm.getEnumerator(null);
   while (e.hasMoreElements()) {
     let win = e.getNext();
     let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils);
     windowUtils.suppressEventHandling(true);
     windowUtils.suspendTimeouts();
   }
 };
 
 /**
  * Prepare to exit a nested event loop by enabling debuggee events.
  */
-ChromeActor.prototype.postNest = function(nestData) {
+chromePrototype.postNest = function(nestData) {
   // Enable events in all open windows.
   let e = Services.wm.getEnumerator(null);
   while (e.hasMoreElements()) {
     let win = e.getNext();
     let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils);
     windowUtils.resumeTimeouts();
     windowUtils.suppressEventHandling(false);
   }
 };
+
+chromePrototype.typeName = "Chrome";
+exports.chromePrototype = chromePrototype;
+exports.ChromeActor = ActorClassWithSpec(tabSpec, chromePrototype);
--- a/devtools/server/actors/tab.js
+++ b/devtools/server/actors/tab.js
@@ -606,17 +606,17 @@ TabActor.prototype = {
   },
 
   _unwatchDocShell(docShell) {
     if (this._progressListener) {
       this._progressListener.unwatch(docShell);
     }
   },
 
-  onSwitchToFrame(request) {
+  switchToFrame(request) {
     let windowId = request.windowId;
     let win;
 
     try {
       win = Services.wm.getOuterWindowWithId(windowId);
     } catch (e) {
       // ignore
     }
@@ -628,22 +628,22 @@ TabActor.prototype = {
     }
 
     // Reply first before changing the document
     DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));
 
     return {};
   },
 
-  onListFrames(request) {
+  listFrames(request) {
     let windows = this._docShellsToWindows(this.docShells);
     return { frames: windows };
   },
 
-  onListWorkers(request) {
+  listWorkers(request) {
     if (!this.attached) {
       return { error: "wrongState" };
     }
 
     if (this._workerActorList === null) {
       this._workerActorList = new WorkerActorList(this.conn, {
         type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
         window: this.window
@@ -664,17 +664,17 @@ TabActor.prototype = {
 
       return {
         "from": this.actorID,
         "workers": actors.map((actor) => actor.form())
       };
     });
   },
 
-  onLogInPage(request) {
+  logInPage(request) {
     let {text, category, flags} = request;
     let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
     let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
     scriptError.initWithWindowID(text, null, null, 0, 0, flags,
                                  category, getInnerId(this.window));
     Services.console.logMessage(scriptError);
     return {};
   },
@@ -934,86 +934,86 @@ TabActor.prototype = {
     this.conn.send({ from: this.actorID,
                      type: "tabDetached" });
 
     return true;
   },
 
   // Protocol Request Handlers
 
-  onAttach(request) {
+  attach(request) {
     if (this.exited) {
       return { type: "exited" };
     }
 
     this._attach();
 
     return {
       type: "tabAttached",
       threadActor: this.threadActor.actorID,
       cacheDisabled: this._getCacheDisabled(),
       javascriptEnabled: this._getJavascriptEnabled(),
       traits: this.traits,
     };
   },
 
-  onDetach(request) {
+  detach(request) {
     if (!this._detach()) {
       return { error: "wrongState" };
     }
 
     return { type: "detached" };
   },
 
   /**
    * Bring the tab's window to front.
    */
-  onFocus() {
+  focus() {
     if (this.window) {
       this.window.focus();
     }
     return {};
   },
 
   /**
    * Reload the page in this tab.
    */
-  onReload(request) {
+  reload(request) {
     let force = request && request.options && request.options.force;
     // Wait a tick so that the response packet can be dispatched before the
     // subsequent navigation event packet.
     Services.tm.dispatchToMainThread(DevToolsUtils.makeInfallible(() => {
       // This won't work while the browser is shutting down and we don't really
       // care.
       if (Services.startup.shuttingDown) {
         return;
       }
       this.webNavigation.reload(force ?
         Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE :
         Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
-    }, "TabActor.prototype.onReload's delayed body"));
+    }, "TabActor.prototype.reload's delayed body"));
     return {};
   },
 
   /**
    * Navigate this tab to a new location
    */
-  onNavigateTo(request) {
+  navigateTo(request) {
     // Wait a tick so that the response packet can be dispatched before the
     // subsequent navigation event packet.
     Services.tm.dispatchToMainThread(DevToolsUtils.makeInfallible(() => {
       this.window.location = request.url;
-    }, "TabActor.prototype.onNavigateTo's delayed body"));
+    }, "TabActor.prototype.navigateTo's delayed body"));
     return {};
   },
 
   /**
    * Reconfigure options.
    */
-  onReconfigure(request) {
+  reconfigure(request) {
     let options = request.options || {};
 
     if (!this.docShell) {
       // The tab is already closed.
       return {};
     }
     this._toggleDevToolsSettings(options);
 
@@ -1073,17 +1073,17 @@ TabActor.prototype = {
     }
 
     // Reload if:
     //  - there's an explicit `performReload` flag and it's true
     //  - there's no `performReload` flag, but it makes sense to do so
     let hasExplicitReloadFlag = "performReload" in options;
     if ((hasExplicitReloadFlag && options.performReload) ||
        (!hasExplicitReloadFlag && reload)) {
-      this.onReload();
+      this.reload();
     }
   },
 
   /**
    * Opposite of the _toggleDevToolsSettings method, that reset document state
    * when closing the toolbox.
    */
   _restoreDocumentSettings() {
@@ -1459,27 +1459,27 @@ TabActor.prototype = {
     }
   },
 };
 
 /**
  * The request types this actor can handle.
  */
 TabActor.prototype.requestTypes = {
-  "attach": TabActor.prototype.onAttach,
-  "detach": TabActor.prototype.onDetach,
-  "focus": TabActor.prototype.onFocus,
-  "reload": TabActor.prototype.onReload,
-  "navigateTo": TabActor.prototype.onNavigateTo,
-  "reconfigure": TabActor.prototype.onReconfigure,
+  "attach": TabActor.prototype.attach,
+  "detach": TabActor.prototype.detach,
+  "focus": TabActor.prototype.focus,
+  "reload": TabActor.prototype.reload,
+  "navigateTo": TabActor.prototype.navigateTo,
+  "reconfigure": TabActor.prototype.reconfigure,
   "ensureCSSErrorReportingEnabled": TabActor.prototype.ensureCSSErrorReportingEnabled,
-  "switchToFrame": TabActor.prototype.onSwitchToFrame,
-  "listFrames": TabActor.prototype.onListFrames,
-  "listWorkers": TabActor.prototype.onListWorkers,
-  "logInPage": TabActor.prototype.onLogInPage,
+  "switchToFrame": TabActor.prototype.switchToFrame,
+  "listFrames": TabActor.prototype.listFrames,
+  "listWorkers": TabActor.prototype.listWorkers,
+  "logInPage": TabActor.prototype.logInPage,
 };
 
 exports.TabActor = TabActor;
 
 /**
  * The DebuggerProgressListener object is an nsIWebProgressListener which
  * handles onStateChange events for the inspected browser. If the user tries to
  * navigate away from a paused page, the listener makes sure that the debuggee
--- a/devtools/server/actors/webextension.js
+++ b/devtools/server/actors/webextension.js
@@ -1,24 +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 { extend } = require("devtools/shared/extend");
 const { Ci, Cu, Cc } = require("chrome");
 const Services = require("Services");
 
-const { ChromeActor } = require("./chrome");
+const { ChromeActor, chromePrototype } = require("./chrome");
 const makeDebugger = require("./utils/make-debugger");
+const { ActorClassWithSpec } = require("devtools/shared/protocol");
+const { tabSpec } = require("devtools/shared/specs/tab");
 
 loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
 loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/thread", true);
 loader.lazyRequireGetter(this, "ChromeUtils");
-
 const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet.";
 
 /**
  * Creates a TabActor for debugging all the contexts associated to a target WebExtensions
  * add-on running in a child extension process.
  * Most of the implementation is inherited from ChromeActor (which inherits most of its
  * implementation from TabActor).
  * WebExtensionChildActor is created by a WebExtensionParentActor counterpart, when its
@@ -48,19 +50,21 @@ const FALLBACK_DOC_MESSAGE = "Your addon
  * @param {nsIMessageSender} chromeGlobal.
  *        The chromeGlobal where this actor has been injected by the
  *        DebuggerServer.connectToFrame method.
  * @param {string} prefix
  *        the custom RDP prefix to use.
  * @param {string} addonId
  *        the addonId of the target WebExtension.
  */
-function WebExtensionChildActor(conn, chromeGlobal, prefix, addonId) {
-  ChromeActor.call(this, conn);
+
+const webExtensionChildPrototype = extend({}, chromePrototype);
 
+webExtensionChildPrototype.initialize = function(conn, chromeGlobal, prefix, addonId) {
+  chromePrototype.initialize.call(this, conn);
   this._chromeGlobal = chromeGlobal;
   this._prefix = prefix;
   this.id = addonId;
 
   // Redefine the messageManager getter to return the chromeGlobal
   // as the messageManager for this actor (which is the browser XUL
   // element used by the parent actor running in the main process to
   // connect to the extension process).
@@ -98,33 +102,29 @@ function WebExtensionChildActor(conn, ch
 
   // Try to discovery an existent extension page to attach (which will provide the initial
   // URL shown in the window tittle when the addon debugger is opened).
   let extensionWindow = this._searchForExtensionWindow();
 
   if (extensionWindow) {
     this._setWindow(extensionWindow);
   }
-}
-exports.WebExtensionChildActor = WebExtensionChildActor;
+};
 
-WebExtensionChildActor.prototype = Object.create(ChromeActor.prototype);
-
-WebExtensionChildActor.prototype.actorPrefix = "webExtension";
-WebExtensionChildActor.prototype.constructor = WebExtensionChildActor;
+webExtensionChildPrototype.typeName = "webExtension";
 
 // NOTE: This is needed to catch in the webextension webconsole all the
 // errors raised by the WebExtension internals that are not currently
 // associated with any window.
-WebExtensionChildActor.prototype.isRootActor = true;
+webExtensionChildPrototype.isRootActor = true;
 
 /**
  * Called when the actor is removed from the connection.
  */
-WebExtensionChildActor.prototype.exit = function() {
+webExtensionChildPrototype.exit = function() {
   if (this._chromeGlobal) {
     let chromeGlobal = this._chromeGlobal;
     this._chromeGlobal = null;
 
     chromeGlobal.removeMessageListener("debug:webext_parent_exit", this._onParentExit);
 
     chromeGlobal.sendAsyncMessage("debug:webext_child_exit", {
       actor: this.actorID
@@ -134,17 +134,17 @@ WebExtensionChildActor.prototype.exit = 
   this.addon = null;
   this.id = null;
 
   return ChromeActor.prototype.exit.apply(this);
 };
 
 // Private helpers.
 
-WebExtensionChildActor.prototype._createFallbackWindow = function() {
+webExtensionChildPrototype._createFallbackWindow = function() {
   if (this.fallbackWindow) {
     // Skip if there is already an existent fallback window.
     return;
   }
 
   // Create an empty hidden window as a fallback (e.g. the background page could be
   // not defined for the target add-on or not yet when the actor instance has been
   // created).
@@ -153,48 +153,48 @@ WebExtensionChildActor.prototype._create
   // Save the reference to the fallback DOMWindow.
   this.fallbackWindow = this.fallbackWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
                                            .getInterface(Ci.nsIDOMWindow);
 
   // Insert the fallback doc message.
   this.fallbackWindow.document.body.innerText = FALLBACK_DOC_MESSAGE;
 };
 
-WebExtensionChildActor.prototype._destroyFallbackWindow = function() {
+webExtensionChildPrototype._destroyFallbackWindow = function() {
   if (this.fallbackWebNav) {
     // Explicitly close the fallback windowless browser to prevent it to leak
     // (and to prevent it to freeze devtools xpcshell tests).
     this.fallbackWebNav.loadURI("about:blank", 0, null, null, null);
     this.fallbackWebNav.close();
 
     this.fallbackWebNav = null;
     this.fallbackWindow = null;
   }
 };
 
 // Discovery an extension page to use as a default target window.
 // NOTE: This currently fail to discovery an extension page running in a
 // windowless browser when running in non-oop mode, and the background page
 // is set later using _onNewExtensionWindow.
-WebExtensionChildActor.prototype._searchForExtensionWindow = function() {
+webExtensionChildPrototype._searchForExtensionWindow = function() {
   let e = Services.ww.getWindowEnumerator(null);
   while (e.hasMoreElements()) {
     let window = e.getNext();
 
     if (window.document.nodePrincipal.addonId == this.id) {
       return window;
     }
   }
 
   return undefined;
 };
 
 // Customized ChromeActor/TabActor hooks.
 
-WebExtensionChildActor.prototype._onDocShellDestroy = function(docShell) {
+webExtensionChildPrototype._onDocShellDestroy = function(docShell) {
   // Stop watching this docshell (the unwatch() method will check if we
   // started watching it before).
   this._unwatchDocShell(docShell);
 
   // Let the _onDocShellDestroy notify that the docShell has been destroyed.
   let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIWebProgress);
   this._notifyDocShellDestroy(webProgress);
@@ -203,23 +203,23 @@ WebExtensionChildActor.prototype._onDocS
   // currently attached, switch to the fallback window
   if (this.attached && docShell == this.docShell) {
     // Creates a fallback window if it doesn't exist yet.
     this._createFallbackWindow();
     this._changeTopLevelDocument(this.fallbackWindow);
   }
 };
 
-WebExtensionChildActor.prototype._onNewExtensionWindow = function(window) {
+webExtensionChildPrototype._onNewExtensionWindow = function(window) {
   if (!this.window || this.window === this.fallbackWindow) {
     this._changeTopLevelDocument(window);
   }
 };
 
-WebExtensionChildActor.prototype._attach = function() {
+webExtensionChildPrototype._attach = function() {
   // NOTE: we need to be sure that `this.window` can return a
   // window before calling the ChromeActor.onAttach, or the TabActor
   // will not be subscribed to the child doc shell updates.
 
   if (!this.window || this.window.document.nodePrincipal.addonId !== this.id) {
     // Discovery an existent extension page to attach.
     let extensionWindow = this._searchForExtensionWindow();
 
@@ -230,28 +230,28 @@ WebExtensionChildActor.prototype._attach
       this._setWindow(extensionWindow);
     }
   }
 
   // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
   ChromeActor.prototype._attach.apply(this);
 };
 
-WebExtensionChildActor.prototype._detach = function() {
+webExtensionChildPrototype._detach = function() {
   // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
   ChromeActor.prototype._detach.apply(this);
 
   // Stop watching for new extension windows.
   this._destroyFallbackWindow();
 };
 
 /**
  * Return the json details related to a docShell.
  */
-WebExtensionChildActor.prototype._docShellToWindow = function(docShell) {
+webExtensionChildPrototype._docShellToWindow = function(docShell) {
   const baseWindowDetails = ChromeActor.prototype._docShellToWindow.call(this, docShell);
 
   let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                             .getInterface(Ci.nsIWebProgress);
   let window = webProgress.DOMWindow;
 
   // Collect the addonID from the document origin attributes and its sameType top level
   // frame.
@@ -266,44 +266,44 @@ WebExtensionChildActor.prototype._docShe
     addonID,
     sameTypeRootAddonID,
   });
 };
 
 /**
  * Return an array of the json details related to an array/iterator of docShells.
  */
-WebExtensionChildActor.prototype._docShellsToWindows = function(docshells) {
+webExtensionChildPrototype._docShellsToWindows = function(docshells) {
   return ChromeActor.prototype._docShellsToWindows.call(this, docshells)
                     .filter(windowDetails => {
                       // Filter the docShells based on the addon id of the window or
                       // its sameType top level frame.
                       return windowDetails.addonID === this.id ||
                              windowDetails.sameTypeRootAddonID === this.id;
                     });
 };
 
-WebExtensionChildActor.prototype.isExtensionWindow = function(window) {
+webExtensionChildPrototype.isExtensionWindow = function(window) {
   return window.document.nodePrincipal.addonId == this.id;
 };
 
-WebExtensionChildActor.prototype.isExtensionWindowDescendent = function(window) {
+webExtensionChildPrototype.isExtensionWindowDescendent = function(window) {
   // Check if the source is coming from a descendant docShell of an extension window.
   let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIDocShell);
   let rootWin = docShell.sameTypeRootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
                                              .getInterface(Ci.nsIDOMWindow);
   return this.isExtensionWindow(rootWin);
 };
 
 /**
  * Return true if the given source is associated with this addon and should be
  * added to the visible sources (retrieved and used by the webbrowser actor module).
  */
-WebExtensionChildActor.prototype._allowSource = function(source) {
+webExtensionChildPrototype._allowSource = function(source) {
   // Use the source.element to detect the allowed source, if any.
   if (source.element) {
     let domEl = unwrapDebuggerObjectGlobal(source.element);
     return (this.isExtensionWindow(domEl.ownerGlobal) ||
             this.isExtensionWindowDescendent(domEl.ownerGlobal));
   }
 
   // Fallback to check the uri if there is no source.element associated to the source.
@@ -340,17 +340,17 @@ WebExtensionChildActor.prototype._allowS
     return false;
   }
 };
 
 /**
  * Return true if the given global is associated with this addon and should be
  * added as a debuggee, false otherwise.
  */
-WebExtensionChildActor.prototype._shouldAddNewGlobalAsDebuggee = function(newGlobal) {
+webExtensionChildPrototype._shouldAddNewGlobalAsDebuggee = function(newGlobal) {
   const global = unwrapDebuggerObjectGlobal(newGlobal);
 
   if (global instanceof Ci.nsIDOMWindow) {
     try {
       global.document;
     } catch (e) {
       // The global might be a sandbox with a window object in its proto chain. If the
       // window navigated away since the sandbox was created, it can throw a security
@@ -383,15 +383,17 @@ WebExtensionChildActor.prototype._should
     // Unable to retrieve the sandbox metadata.
   }
 
   return false;
 };
 
 // Handlers for the messages received from the parent actor.
 
-WebExtensionChildActor.prototype._onParentExit = function(msg) {
+webExtensionChildPrototype._onParentExit = function(msg) {
   if (msg.json.actor !== this.actorID) {
     return;
   }
 
   this.exit();
 };
+
+exports.WebExtensionChildActor = ActorClassWithSpec(tabSpec, webExtensionChildPrototype);
--- a/devtools/shared/protocol.js
+++ b/devtools/shared/protocol.js
@@ -1105,20 +1105,16 @@ var generateActorSpec = function(actorDe
 };
 exports.generateActorSpec = generateActorSpec;
 
 /**
  * Generates request handlers as described by the given actor specification on
  * the given actor prototype. Returns the actor prototype.
  */
 var generateRequestHandlers = function(actorSpec, actorProto) {
-  if (actorProto._actorSpec) {
-    throw new Error("actorProto called twice on the same actor prototype!");
-  }
-
   actorProto.typeName = actorSpec.typeName;
 
   // Generate request handlers for each method definition
   actorProto.requestTypes = Object.create(null);
   actorSpec.methods.forEach(spec => {
     let handler = function(packet, conn) {
       try {
         let args;
--- a/devtools/shared/specs/index.js
+++ b/devtools/shared/specs/index.js
@@ -200,16 +200,21 @@ const Types = exports.__TypesForTests = 
     front: "devtools/shared/fronts/stylesheets",
   },
   {
     types: ["symbolIterator"],
     spec: "devtools/shared/specs/symbol-iterator",
     front: null,
   },
   {
+    types: ["tab"],
+    spec: "devtools/shared/specs/tab",
+    front: null,
+  },
+  {
     types: ["timeline"],
     spec: "devtools/shared/specs/timeline",
     front: "devtools/shared/fronts/timeline",
   },
   {
     types: ["audionode", "webaudio"],
     spec: "devtools/shared/specs/webaudio",
     front: "devtools/shared/fronts/webaudio",
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -36,15 +36,16 @@ DevToolsModules(
     'reflow.js',
     'script.js',
     'source.js',
     'storage.js',
     'string.js',
     'styles.js',
     'stylesheets.js',
     'symbol-iterator.js',
+    'tab.js',
     'timeline.js',
     'webaudio.js',
     'webextension-inspected-window.js',
     'webextension-parent.js',
     'webgl.js',
     'worker.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/tab.js
@@ -0,0 +1,111 @@
+/* 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 {types, generateActorSpec, RetVal, Option} = require("devtools/shared/protocol");
+
+types.addDictType("tab.attach", {
+  type: "string",
+  threadActor: "number",
+  cacheDisabled: "boolean",
+  javascriptEnabled: "boolean",
+  traits: "json"
+});
+
+types.addDictType("tab.detach", {
+  error: "nullable:string",
+  type: "nullable:string"
+});
+
+types.addDictType("tab.switchtoframe", {
+  error: "nullable:string",
+  message: "nullable:string"
+});
+
+types.addDictType("tab.listframes", {
+  frames: "array:tab.window"
+});
+
+types.addDictType("tab.window", {
+  id: "string",
+  parentID: "nullable:string",
+  url: "string",
+  title: "string"
+});
+
+types.addDictType("tab.workers", {
+  error: "nullable:string"
+});
+
+types.addDictType("tab.reload", {
+  force: "boolean"
+});
+
+types.addDictType("tab.reconfigure", {
+  javascriptEnabled: "nullable:boolean",
+  cacheDisabled: "nullable:boolean",
+  serviceWorkersTestingEnabled: "nullable:boolean",
+  performReload: "nullable:boolean"
+});
+
+const tabSpec = generateActorSpec({
+  typeName: "tab",
+
+  methods: {
+    attach: {
+      request: {},
+      response: RetVal("tab.attach")
+    },
+    detach: {
+      request: {},
+      response: RetVal("tab.detach")
+    },
+    focus: {
+      request: {},
+      response: {}
+    },
+    reload: {
+      request: {
+        options: Option(0, "tab.reload"),
+      },
+      response: {}
+    },
+    navigateTo: {
+      request: {
+        url: Option(0, "string"),
+      },
+      response: {}
+    },
+    reconfigure: {
+      request: {
+        options: Option(0, "tab.reconfigure")
+      },
+      response: {}
+    },
+    switchToFrame: {
+      request: {
+        windowId: Option(0, "string")
+      },
+      response: RetVal("tab.switchtoframe")
+    },
+    listFrames: {
+      request: {},
+      response: RetVal("tab.listframes")
+    },
+    listWorkers: {
+      request: {},
+      response: RetVal("tab.workers")
+    },
+    logInPage: {
+      request: {
+        text: Option(0, "string"),
+        category: Option(0, "string"),
+        flags: Option(0, "string")
+      },
+      response: {}
+    }
+  },
+});
+
+exports.tabSpec = tabSpec;