Bug 1346316 - Expose content frame global in Browser Content Toolbox. r=ochameau draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Tue, 18 Apr 2017 16:31:21 -0500
changeset 564714 bc096bdba034ab4d6a2a34404f1d8d6bd3826980
parent 563757 2b6a66a98e253ba158f3960f1c68ad49b2ebcdb4
child 564715 52db792735e11042f46e86ccd6b81cf571702091
push id54673
push userbmo:jryans@gmail.com
push dateTue, 18 Apr 2017 21:57:02 +0000
reviewersochameau
bugs1346316
milestone55.0a1
Bug 1346316 - Expose content frame global in Browser Content Toolbox. r=ochameau This makes the Browser Content Toolbox much more convenient by exposing the same environment a frame script for the currently selected browser would have access to, which simplifies exploring the content process state in many cases, since you no longer have to go digging around for the right objects. MozReview-Commit-ID: KReOqlwuY2b
devtools/client/framework/devtools-browser.js
devtools/server/actors/child-process.js
devtools/server/actors/root.js
devtools/server/content-server.jsm
devtools/server/main.js
devtools/shared/client/main.js
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -339,30 +339,30 @@ var gDevToolsBrowser = exports.gDevTools
       return onNewNode.then(() => {
         // Now that the node has been selected, wait until the inspector is
         // fully updated.
         return inspector.once("inspector-updated");
       });
     });
   },
 
-  _getContentProcessTarget: function (processId) {
+  _getContentProcessTarget: function (processID, outerWindowID) {
     // Create a DebuggerServer in order to connect locally to it
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
       DebuggerServer.addBrowserActors();
     }
     DebuggerServer.allowChromeProcess = true;
 
     let transport = DebuggerServer.connectPipe();
     let client = new DebuggerClient(transport);
 
     let deferred = defer();
     client.connect().then(() => {
-      client.getProcess(processId)
+      client.getProcess(processID, outerWindowID)
             .then(response => {
               let options = {
                 form: response.form,
                 client: client,
                 chrome: true,
                 isTabActor: false
               };
               return TargetFactory.forRemoteTab(options);
@@ -380,27 +380,29 @@ var gDevToolsBrowser = exports.gDevTools
 
     return deferred.promise;
   },
 
    // Used by menus.js
   openContentProcessToolbox: function (gBrowser) {
     let { childCount } = Services.ppmm;
     // Get the process message manager for the current tab
-    let mm = gBrowser.selectedBrowser.messageManager.processMessageManager;
-    let processId = null;
+    let browser = gBrowser.selectedBrowser;
+    let mm = browser.messageManager.processMessageManager;
+    let outerWindowID = browser.outerWindowID;
+    let processID = null;
     for (let i = 1; i < childCount; i++) {
       let child = Services.ppmm.getChildAt(i);
       if (child == mm) {
-        processId = i;
+        processID = i;
         break;
       }
     }
-    if (processId) {
-      this._getContentProcessTarget(processId)
+    if (processID) {
+      this._getContentProcessTarget(processID, outerWindowID)
           .then(target => {
             // Display a new toolbox, in a new window, with debugger by default
             return gDevTools.showToolbox(target, "jsdebugger",
                                          Toolbox.HostType.WINDOW);
           });
     } else {
       let msg = L10N.getStr("toolbox.noContentProcessForTab.message");
       Services.prompt.alert(null, "", msg);
--- a/devtools/server/actors/child-process.js
+++ b/devtools/server/actors/child-process.js
@@ -7,36 +7,53 @@
 const { Cc, Ci, Cu } = require("chrome");
 
 const { ChromeDebuggerActor } = require("devtools/server/actors/script");
 const { WebConsoleActor } = require("devtools/server/actors/webconsole");
 const makeDebugger = require("devtools/server/actors/utils/make-debugger");
 const { ActorPool } = require("devtools/server/main");
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { TabSources } = require("./utils/TabSources");
+const Services = require("Services");
 
 loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker-list", true);
 
-function ChildProcessActor(connection) {
+function ChildProcessActor(connection, outerWindowID) {
   this.conn = connection;
   this._contextPool = new ActorPool(this.conn);
   this.conn.addActorPool(this._contextPool);
   this.threadActor = null;
 
   // Use a see-everything debugger
   this.makeDebugger = makeDebugger.bind(null, {
     findDebuggees: dbg => dbg.findAllGlobals(),
     shouldAddNewGlobalAsDebuggee: global => true
   });
 
+  let sandboxPrototype = {};
+  if (outerWindowID) {
+    let currentWindow = Services.wm.getOuterWindowWithId(outerWindowID);
+    if (currentWindow) {
+      let contentMM = currentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                                   .getInterface(Ci.nsIDocShell)
+                                   .QueryInterface(Ci.nsIInterfaceRequestor)
+                                   .getInterface(Ci.nsIContentFrameMessageManager);
+      sandboxPrototype = contentMM;
+    }
+  }
+
   // Scope into which the webconsole executes:
-  // An empty sandbox with chrome privileges
+  // If an outerWindowID was supplied, then a chrome sandbox that has the content frame
+  // message manager as its prototype (similar to the frame script environement).
+  // If there's no outerWindowID, then this will be an empty chrome sandbox.
   let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
     .createInstance(Ci.nsIPrincipal);
-  let sandbox = Cu.Sandbox(systemPrincipal);
+  let sandbox = Cu.Sandbox(systemPrincipal, {
+    sandboxPrototype,
+  });
   this._consoleScope = sandbox;
 
   this._workerList = null;
   this._workerActorPool = null;
   this._onWorkerListChanged = this._onWorkerListChanged.bind(this);
 }
 exports.ChildProcessActor = ChildProcessActor;
 
--- a/devtools/server/actors/root.js
+++ b/devtools/server/actors/root.js
@@ -511,17 +511,17 @@ RootActor.prototype = {
     };
   },
 
   onProcessListChanged: function () {
     this.conn.send({ from: this.actorID, type: "processListChanged" });
     this._parameters.processList.onListChanged = null;
   },
 
-  onGetProcess: function (request) {
+  onGetProcess: async function (request) {
     if (!DebuggerServer.allowChromeProcess) {
       return { error: "forbidden",
                message: "You are not allowed to debug chrome." };
     }
     if (("id" in request) && typeof (request.id) != "number") {
       return { error: "wrongParameter",
                message: "getProcess requires a valid `id` attribute." };
     }
@@ -546,20 +546,20 @@ RootActor.prototype = {
     }
     let form = this._processActors.get(id);
     if (form) {
       return { form };
     }
     let onDestroy = () => {
       this._processActors.delete(id);
     };
-    return DebuggerServer.connectToContent(this.conn, mm, onDestroy).then(formResult => {
-      this._processActors.set(id, formResult);
-      return { form: formResult };
-    });
+    form = await DebuggerServer.connectToContent(this.conn, mm, onDestroy,
+                                                 request.outerWindowID);
+    this._processActors.set(id, form);
+    return { form };
   },
 
   /* This is not in the spec, but it's used by tests. */
   onEcho: function (request) {
     /*
      * Request packets are frozen. Copy request, so that
      * DebuggerServerConnection.onPacket can attach a 'from' property.
      */
--- a/devtools/server/content-server.jsm
+++ b/devtools/server/content-server.jsm
@@ -47,30 +47,30 @@ function setupServer(mm) {
   });
 
   return gLoader;
 }
 
 function init(msg) {
   let mm = msg.target;
   mm.QueryInterface(Ci.nsISyncMessageSender);
-  let prefix = msg.data.prefix;
+  let { prefix, outerWindowID } = msg.data;
 
   // Setup a server if none started yet
   let loader = setupServer(mm);
 
   // Connect both parent/child processes debugger servers RDP via message
   // managers
   let { DebuggerServer } = loader.require("devtools/server/main");
   let conn = DebuggerServer.connectToParent(prefix, mm);
   conn.parentMessageManager = mm;
 
   let { ChildProcessActor } =
     loader.require("devtools/server/actors/child-process");
   let { ActorPool } = loader.require("devtools/server/main");
-  let actor = new ChildProcessActor(conn);
+  let actor = new ChildProcessActor(conn, outerWindowID);
   let actorPool = new ActorPool(conn);
   actorPool.addActor(actor);
   conn.addActorPool(actorPool);
 
   let response = { actor: actor.form() };
   mm.sendAsyncMessage("debug:content-process-actor", response);
 }
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -722,17 +722,17 @@ var DebuggerServer = {
 
     let transport = isWorker ?
                     new WorkerDebuggerTransport(scopeOrManager, prefix) :
                     new ChildDebuggerTransport(scopeOrManager, prefix);
 
     return this._onConnection(transport, prefix, true);
   },
 
-  connectToContent(connection, mm, onDestroy) {
+  connectToContent(connection, mm, onDestroy, outerWindowID) {
     let deferred = SyncPromise.defer();
 
     let prefix = connection.allocID("content-process");
     let actor, childTransport;
 
     mm.addMessageListener("debug:content-process-actor", function listener(msg) {
       // Arbitrarily choose the first content process to reply
       // XXX: This code needs to be updated if we use more than one content process
@@ -751,17 +751,18 @@ var DebuggerServer = {
       dumpn("establishing forwarding for process with prefix " + prefix);
 
       actor = msg.json.actor;
 
       deferred.resolve(actor);
     });
 
     mm.sendAsyncMessage("DevTools:InitDebuggerServer", {
-      prefix: prefix
+      prefix,
+      outerWindowID,
     });
 
     function onClose() {
       Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
       events.off(connection, "closed", onClose);
       if (childTransport) {
         // If we have a child transport, the actor has already
         // been created. We need to stop using this message manager.
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -612,27 +612,34 @@ DebuggerClient.prototype = {
       return [response, traceClient];
     });
   },
 
   /**
    * Fetch the ChromeActor for the main process or ChildProcessActor for a
    * a given child process ID.
    *
-   * @param number id
+   * @param number processID
    *        The ID for the process to attach (returned by `listProcesses`).
    *        Connected to the main process if omitted, or is 0.
+   * @param number outerWindowID
+   *        The outerWindowID for the currently selected browser in the process.
+   *        Optional.  If supplied, the chrome scope of the browser will be
+   *        accessible in the console.
    */
-  getProcess: function (id) {
+  getProcess: function (processID, outerWindowID) {
     let packet = {
       to: "root",
       type: "getProcess"
     };
-    if (typeof (id) == "number") {
-      packet.id = id;
+    if (typeof processID == "number") {
+      packet.id = processID;
+    }
+    if (typeof outerWindowID == "number") {
+      packet.outerWindowID = outerWindowID;
     }
     return this.request(packet);
   },
 
   /**
    * Release an object actor.
    *
    * @param string actor