Bug 1215117 - Make console input field work inside a worker toolbox;r=ejpbruel
authorBrian Grinstead <bgrinstead@mozilla.com>
Mon, 26 Oct 2015 09:13:11 -0700
changeset 269464 0e616440737c12028953b5095ee7222d7e4d79a4
parent 269463 03eec3bcf2933e13f3f147b72b3e722c36a42e52
child 269465 b39a7e4289d8de57151b826dec3902130f9d0634
push id15876
push userbgrinstead@mozilla.com
push dateMon, 26 Oct 2015 16:15:54 +0000
treeherderfx-team@0e616440737c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersejpbruel
bugs1215117
milestone44.0a1
Bug 1215117 - Make console input field work inside a worker toolbox;r=ejpbruel
devtools/client/debugger/test/mochitest/browser.ini
devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js
devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js
devtools/client/debugger/test/mochitest/browser_dbg_worker-console.js
devtools/client/debugger/test/mochitest/head.js
devtools/client/framework/target.js
devtools/client/webconsole/webconsole.js
devtools/server/actors/webconsole.js
devtools/server/actors/worker.js
devtools/server/main.js
devtools/server/worker.js
devtools/shared/client/main.js
devtools/shared/webconsole/client.js
devtools/shared/webconsole/moz.build
devtools/shared/webconsole/worker-utils.js
devtools/shared/worker/loader.js
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -569,16 +569,18 @@ skip-if = e10s && debug
 [browser_dbg_variables-view-reexpand-03.js]
 skip-if = e10s && debug
 [browser_dbg_variables-view-webidl.js]
 skip-if = e10s && debug
 [browser_dbg_watch-expressions-01.js]
 skip-if = e10s && debug
 [browser_dbg_watch-expressions-02.js]
 skip-if = e10s && debug
+[browser_dbg_worker-console.js]
+skip-if = e10s && debug
 [browser_dbg_worker-window.js]
 skip-if = e10s && debug
 [browser_dbg_WorkerActor.attach.js]
 skip-if = e10s && debug
 [browser_dbg_WorkerActor.attachThread.js]
 skip-if = e10s && debug
 [browser_dbg_split-console-keypress.js]
 skip-if = e10s && debug
--- a/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_break-on-next-console.js
@@ -28,17 +28,17 @@ function test() {
       .then(() => closeDebuggerAndFinish(gPanel));
   });
 
   let testConsole = Task.async(function*() {
     info("Starting testConsole");
 
     let oncePaused = gTarget.once("thread-paused");
     EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
-    let jsterm = yield getSplitConsole();
+    let jsterm = yield getSplitConsole(gDevTools.getToolbox(gPanel.target));
     let executed = jsterm.execute("1+1");
     yield oncePaused;
 
     let updatedFrame = yield waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
     let variables = gDebugger.DebuggerView.Variables;
 
     is(variables._store.length, 3, "Correct number of scopes available");
     is(variables.getScopeAtIndex(0).name, "With scope [Object]",
@@ -49,25 +49,9 @@ function test() {
         "Paused with correct scope (2)");
 
     let onceResumed = gTarget.once("thread-resumed");
     EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
     yield onceResumed;
 
     yield executed;
   });
-
-  function getSplitConsole() {
-    return new Promise(resolve => {
-      let toolbox = gDevTools.getToolbox(gPanel.target);
-      toolbox.once("webconsole-ready", () => {
-        ok(toolbox.splitConsole, "Split console is shown.");
-        let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
-        resolve(jsterm);
-      });
-      EventUtils.synthesizeKey("VK_ESCAPE", {}, gDebugger);
-    });
-  }
 }
-
-registerCleanupFunction(() => {
-  Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
-});
--- a/devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_split-console-keypress.js
@@ -89,24 +89,9 @@ function test() {
     gPanel.target.on("thread-resumed", () => {
       is(gThreadClient.paused, false,
         "Should not be paused after resume");
       // Final test: did we preserve console inputNode focus during resume?
       is(consoleLostFocus, false, "Resume - console should keep focus");
       closeDebuggerAndFinish(gPanel);
     });
   });
-
-  function getSplitConsole(toolbox, theDebugger) {
-    return new Promise(resolve => {
-      toolbox.once("webconsole-ready", () => {
-        let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
-        resolve(jsterm);
-      });
-      EventUtils.synthesizeKey("VK_ESCAPE", {}, theDebugger);
-    });
-  }
 }
-
-registerCleanupFunction(() => {
-  // We don't want the open split console to confuse other tests..
-  Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
-});
new file mode 100644
--- /dev/null
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_worker-console.js
@@ -0,0 +1,145 @@
+// Check to make sure that a worker can be attached to a toolbox
+// and that the console works.
+
+var TAB_URL = EXAMPLE_URL + "doc_WorkerActor.attachThread-tab.html";
+var WORKER_URL = "code_WorkerActor.attachThread-worker.js";
+
+function* initWorkerDebugger(TAB_URL, WORKER_URL) {
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init();
+    DebuggerServer.addBrowserActors();
+  }
+
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  yield connect(client);
+
+  let tab = yield addTab(TAB_URL);
+  let { tabs } = yield listTabs(client);
+  let [, tabClient] = yield attachTab(client, findTab(tabs, TAB_URL));
+
+  yield createWorkerInTab(tab, WORKER_URL);
+
+  let { workers } = yield listWorkers(tabClient);
+  let [, workerClient] = yield attachWorker(tabClient,
+                                             findWorker(workers, WORKER_URL));
+
+  let toolbox = yield gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+                                            "jsdebugger",
+                                            Toolbox.HostType.WINDOW);
+
+  let debuggerPanel = toolbox.getCurrentPanel();
+  let gDebugger = debuggerPanel.panelWin;
+
+  return {client,tab,tabClient,workerClient,toolbox,gDebugger};
+}
+
+add_task(function* testNormalExecution() {
+  let {client,tab,tabClient,workerClient,toolbox,gDebugger} =
+    yield initWorkerDebugger(TAB_URL, WORKER_URL);
+
+  let jsterm = yield getSplitConsole(toolbox);
+  let executed = yield jsterm.execute("this.location.toString()");
+  ok(executed.textContent.includes(WORKER_URL),
+      "Evaluating the global's location works");
+
+  yield gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+  terminateWorkerInTab(tab, WORKER_URL);
+  yield waitForWorkerClose(workerClient);
+  yield close(client);
+  yield removeTab(tab);
+});
+
+add_task(function* testWhilePaused() {
+  let {client,tab,tabClient,workerClient,toolbox,gDebugger} =
+    yield initWorkerDebugger(TAB_URL, WORKER_URL);
+
+  let gTarget = gDebugger.gTarget;
+  let gResumeButton = gDebugger.document.getElementById("resume");
+  let gResumeKey = gDebugger.document.getElementById("resumeKey");
+
+  // Execute some basic math to make sure evaluations are working.
+  let jsterm = yield getSplitConsole(toolbox);
+  let executed = yield jsterm.execute("10000+1");
+  ok(executed.textContent.includes("10001"), "Text for message appeared correct");
+
+  // Pause the worker by waiting for next execution and then sending a message to
+  // it from the main thread.
+  let oncePaused = gTarget.once("thread-paused");
+  EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+  once(gDebugger.gClient, "willInterrupt").then(() => {
+    info("Posting message to worker, then waiting for a pause");
+    postMessageToWorkerInTab(tab, WORKER_URL, "ping");
+  });
+  yield oncePaused;
+
+  let command1 = jsterm.execute("10000+2");
+  let command2 = jsterm.execute("10000+3");
+  let command3 = jsterm.execute("foobar"); // throw an error
+
+  info ("Trying to get the result of command1");
+  executed = yield command1;
+  ok(executed.textContent.includes("10002"),
+      "command1 executed successfully");
+
+  info ("Trying to get the result of command2");
+  executed = yield command2;
+  ok(executed.textContent.includes("10003"),
+      "command2 executed successfully");
+
+  info ("Trying to get the result of command3")
+  executed = yield command3;
+  // XXXworkers This is failing until Bug 1215120 is resolved.
+  todo(executed.textContent.includes("ReferenceError: foobar is not defined"),
+      "command3 executed successfully");
+
+  let onceResumed = gTarget.once("thread-resumed");
+  EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+  yield onceResumed;
+
+  yield gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+  terminateWorkerInTab(tab, WORKER_URL);
+  yield waitForWorkerClose(workerClient);
+  yield close(client);
+  yield removeTab(tab);
+});
+
+// Test to see if creating the pause from the console works.
+add_task(function* testPausedByConsole() {
+  let {client,tab,tabClient,workerClient,toolbox,gDebugger} =
+    yield initWorkerDebugger(TAB_URL, WORKER_URL);
+
+  let gTarget = gDebugger.gTarget;
+  let gResumeButton = gDebugger.document.getElementById("resume");
+  let gResumeKey = gDebugger.document.getElementById("resumeKey");
+
+  let jsterm = yield getSplitConsole(toolbox);
+  let executed = yield jsterm.execute("10000+1");
+  ok(executed.textContent.includes("10001"),
+      "Text for message appeared correct");
+
+  let oncePaused = gTarget.once("thread-paused");
+  EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+  let pausedExecution = jsterm.execute("10000+2");
+
+  info("Executed a command with 'break on next' active, waiting for pause");
+  yield oncePaused;
+
+  executed = yield jsterm.execute("10000+3");
+  ok(executed.textContent.includes("10003"),
+      "Text for message appeared correct");
+
+  info("Waiting for a resume");
+  let onceResumed = gTarget.once("thread-resumed");
+  EventUtils.sendMouseEvent({ type: "mousedown" }, gResumeButton, gDebugger);
+  yield onceResumed;
+
+  executed = yield pausedExecution;
+  ok(executed.textContent.includes("10002"),
+      "Text for message appeared correct");
+
+  yield gDevTools.closeToolbox(TargetFactory.forWorker(workerClient));
+  terminateWorkerInTab(tab, WORKER_URL);
+  yield waitForWorkerClose(workerClient);
+  yield close(client);
+  yield removeTab(tab);
+});
--- a/devtools/client/debugger/test/mochitest/head.js
+++ b/devtools/client/debugger/test/mochitest/head.js
@@ -1185,8 +1185,33 @@ function afterDispatch(store, type) {
       predicate: action => (
         action.type === type &&
         action.status ? action.status === "done" : true
       ),
       run: resolve
     });
   });
 }
+
+// Return a promise with a reference to jsterm, opening the split
+// console if necessary.  This cleans up the split console pref so
+// it won't pollute other tests.
+function getSplitConsole(toolbox, win) {
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+  });
+
+  if (!win) {
+    win = toolbox.doc.defaultView;
+  }
+
+  if (!toolbox.splitConsole) {
+    EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+  }
+
+  return new Promise(resolve => {
+    toolbox.getPanelWhenReady("webconsole").then(() => {
+      ok(toolbox.splitConsole, "Split console is shown.");
+      let jsterm = toolbox.getPanel("webconsole").hud.jsterm;
+      resolve(jsterm);
+    });
+  });
+}
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -727,18 +727,24 @@ WorkerTarget.prototype = {
   get isTabActor() {
     return true;
   },
 
   get url() {
     return this._workerClient.url;
   },
 
+  get isWorkerTarget() {
+    return true;
+  },
+
   get form() {
-    return {};
+    return {
+      consoleActor: this._workerClient.consoleActor
+    };
   },
 
   get activeTab() {
     return this._workerClient;
   },
 
   get client() {
     return this._workerClient.client;
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -5028,23 +5028,27 @@ WebConsoleConnectionProxy.prototype = {
       this._connectTimer.cancel();
       this._connectTimer = null;
     }, () => {
       this._connectTimer = null;
     });
 
     let client = this.client = this.target.client;
 
-    client.addListener("logMessage", this._onLogMessage);
-    client.addListener("pageError", this._onPageError);
-    client.addListener("consoleAPICall", this._onConsoleAPICall);
-    client.addListener("fileActivity", this._onFileActivity);
-    client.addListener("reflowActivity", this._onReflowActivity);
-    client.addListener("serverLogCall", this._onServerLogCall);
-    client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
+    if (this.target.isWorkerTarget) {
+      // XXXworkers: Not Console API yet inside of workers (Bug 1209353).
+    } else {
+      client.addListener("logMessage", this._onLogMessage);
+      client.addListener("pageError", this._onPageError);
+      client.addListener("consoleAPICall", this._onConsoleAPICall);
+      client.addListener("fileActivity", this._onFileActivity);
+      client.addListener("reflowActivity", this._onReflowActivity);
+      client.addListener("serverLogCall", this._onServerLogCall);
+      client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
+    }
     this.target.on("will-navigate", this._onTabNavigated);
     this.target.on("navigate", this._onTabNavigated);
 
     this._consoleActor = this.target.form.consoleActor;
     if (this.target.isTabActor) {
       let tab = this.target.form;
       this.owner.onLocationChange(tab.url, tab.title);
     }
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1,55 +1,42 @@
 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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");
 const { Cc, Ci, Cu } = require("chrome");
 const { DebuggerServer, ActorPool } = require("devtools/server/main");
 const { EnvironmentActor, ThreadActor } = require("devtools/server/actors/script");
 const { ObjectActor, LongStringActor, createValueGrip, stringIsLong } = require("devtools/server/actors/object");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-                                  "resource://gre/modules/Services.jsm");
-XPCOMUtils.defineLazyGetter(this, "NetworkMonitor", () => {
-  return require("devtools/shared/webconsole/network-monitor")
-         .NetworkMonitor;
-});
-XPCOMUtils.defineLazyGetter(this, "NetworkMonitorChild", () => {
-  return require("devtools/shared/webconsole/network-monitor")
-         .NetworkMonitorChild;
-});
-XPCOMUtils.defineLazyGetter(this, "ConsoleProgressListener", () => {
-  return require("devtools/shared/webconsole/network-monitor")
-         .ConsoleProgressListener;
-});
-XPCOMUtils.defineLazyGetter(this, "events", () => {
-  return require("sdk/event/core");
-});
-XPCOMUtils.defineLazyGetter(this, "ServerLoggingListener", () => {
-  return require("devtools/shared/webconsole/server-logger")
-         .ServerLoggingListener;
-});
+loader.lazyRequireGetter(this, "NetworkMonitor", "devtools/shared/webconsole/network-monitor", true);
+loader.lazyRequireGetter(this, "NetworkMonitorChild", "devtools/shared/webconsole/network-monitor", true);
+loader.lazyRequireGetter(this, "ConsoleProgressListener", "devtools/shared/webconsole/network-monitor", true);
+loader.lazyRequireGetter(this, "events", "sdk/event/core");
+loader.lazyRequireGetter(this, "ServerLoggingListener", "devtools/shared/webconsole/server-logger", true);
 
 for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
     "ConsoleAPIListener", "addWebConsoleCommands", "JSPropertyProvider",
     "ConsoleReflowListener", "CONSOLE_WORKER_IDS"]) {
   Object.defineProperty(this, name, {
     get: function(prop) {
       if (prop == "WebConsoleUtils") {
         prop = "Utils";
       }
-      return require("devtools/shared/webconsole/utils")[prop];
+      if (isWorker) {
+        return require("devtools/shared/webconsole/worker-utils")[prop];
+      } else {
+        return require("devtools/shared/webconsole/utils")[prop];
+      }
     }.bind(null, name),
     configurable: true,
     enumerable: true
   });
 }
 
 /**
  * The WebConsoleActor implements capabilities needed for the Web Console
@@ -551,16 +538,21 @@ WebConsoleActor.prototype =
    *
    * @param object aRequest
    *        The JSON request object received from the Web Console client.
    * @return object
    *         The response object which holds the startedListeners array.
    */
   onStartListeners: function WCA_onStartListeners(aRequest)
   {
+    // XXXworkers: Not handling the Console API yet for workers (Bug 1209353).
+    if (isWorker) {
+       aRequest.listeners = [];
+    }
+
     let startedListeners = [];
     let window = !this.parentActor.isRootActor ? this.window : null;
     let appId = null;
     let messageManager = null;
 
     if (this._parentIsContentActor) {
       appId = this.parentActor.docShell.appId;
       messageManager = this.parentActor.messageManager;
@@ -844,18 +836,22 @@ WebConsoleActor.prototype =
     if (evalResult) {
       if ("return" in evalResult) {
         result = evalResult.return;
       } else if ("yield" in evalResult) {
         result = evalResult.yield;
       } else if ("throw" in evalResult) {
         let error = evalResult.throw;
         errorGrip = this.createValueGrip(error);
-        errorMessage = error && (typeof error === "object")
-          ? error.unsafeDereference().toString()
+        // XXXworkers: Calling unsafeDereference() returns an object with no
+        // toString method in workers. See Bug 1215120.
+        let unsafeDereference = error && (typeof error === "object") &&
+                                error.unsafeDereference();
+        errorMessage = unsafeDereference && unsafeDereference.toString
+          ? unsafeDereference.toString()
           : "" + error;
       }
     }
 
     // If a value is encountered that the debugger server doesn't support yet,
     // the console should remain functional.
     let resultGrip;
     try {
@@ -894,18 +890,18 @@ WebConsoleActor.prototype =
     // This is the case of the paused debugger
     if (frameActorId) {
       let frameActor = this.conn.getActor(frameActorId);
       if (frameActor) {
         let frame = frameActor.frame;
         environment = frame.environment;
       }
       else {
-        Cu.reportError("Web Console Actor: the frame actor was not found: " +
-                       frameActorId);
+        DevToolsUtils.reportException("onAutocomplete",
+          Error("The frame actor was not found: " + frameActorId));
       }
     }
     // This is the general case (non-paused debugger)
     else {
       dbgObject = this.dbg.makeGlobalObjectReference(this.evalWindow);
     }
 
     let result = JSPropertyProvider(dbgObject, environment, aRequest.text,
@@ -1038,19 +1034,23 @@ WebConsoleActor.prototype =
       // helpers like cd(), where we users sometimes want to pass a cross-origin
       // window. To circumvent this restriction, we use exportFunction along
       // with a special option designed for this purpose. See bug 1051224.
       obj[name] =
         Cu.exportFunction(obj[name], evalWindow, { allowCrossOriginArguments: true });
     }
     for (let name in helpers.sandbox) {
       let desc = Object.getOwnPropertyDescriptor(helpers.sandbox, name);
-      maybeExport(desc, 'get');
-      maybeExport(desc, 'set');
-      maybeExport(desc, 'value');
+
+      // Workers don't have access to Cu so won't be able to exportFunction.
+      if (!isWorker) {
+        maybeExport(desc, 'get');
+        maybeExport(desc, 'set');
+        maybeExport(desc, 'value');
+      }
       if (desc.value) {
         // Make sure the helpers can be used during eval.
         desc.value = aDebuggerGlobal.makeDebuggeeValue(desc.value);
       }
       Object.defineProperty(helpers.sandbox, name, desc);
     }
     return helpers;
   },
@@ -1133,18 +1133,18 @@ WebConsoleActor.prototype =
     // Find the Debugger.Frame of the given FrameActor.
     let frame = null, frameActor = null;
     if (aOptions.frameActor) {
       frameActor = this.conn.getActor(aOptions.frameActor);
       if (frameActor) {
         frame = frameActor.frame;
       }
       else {
-        Cu.reportError("Web Console Actor: the frame actor was not found: " +
-                       aOptions.frameActor);
+        DevToolsUtils.reportException("evalWithDebugger",
+          Error("The frame actor was not found: " + aOptions.frameActor));
       }
     }
 
     // If we've been given a frame actor in whose scope we should evaluate the
     // expression, be sure to use that frame's Debugger (that is, the JavaScript
     // debugger's Debugger) for the whole operation, not the console's Debugger.
     // (One Debugger will treat a different Debugger's Debugger.Object instances
     // as ordinary objects, not as references to be followed, so mixing
--- a/devtools/server/actors/worker.js
+++ b/devtools/server/actors/worker.js
@@ -37,16 +37,17 @@ function WorkerActor(dbg) {
 }
 
 WorkerActor.prototype = {
   actorPrefix: "worker",
 
   form: function () {
     return {
       actor: this.actorID,
+      consoleActor: this._consoleActor,
       url: this._dbg.url,
       type: this._dbg.type
     };
   },
 
   onAttach: function () {
     if (this._dbg.isClosed) {
       return { error: "closed" };
@@ -82,23 +83,25 @@ WorkerActor.prototype = {
       return {
         type: "connected",
         threadActor: this._threadActor
       };
     }
 
     return DebuggerServer.connectToWorker(
       this.conn, this._dbg, this.actorID, request.options
-    ).then(({ threadActor, transport }) => {
+    ).then(({ threadActor, transport, consoleActor }) => {
       this._threadActor = threadActor;
       this._transport = transport;
+      this._consoleActor = consoleActor;
 
       return {
         type: "connected",
-        threadActor: this._threadActor
+        threadActor: this._threadActor,
+        consoleActor: this._consoleActor
       };
     }, (error) => {
       return { error: error.toString() };
     });
   },
 
   onClose: function () {
     if (this._isAttached) {
--- a/devtools/server/main.js
+++ b/devtools/server/main.js
@@ -828,39 +828,32 @@ var DebuggerServer = {
       aDbg.postMessage(JSON.stringify({
         type: "connect",
         id: aId,
         options: aOptions
       }));
 
       // Steps 3-5 are performed on the worker thread (see worker.js).
 
-      // Step 6: Wait for a response from the worker debugger.
+      // Step 6: Wait for a connection response from the worker debugger.
       let listener = {
         onClose: () => {
           aDbg.removeListener(listener);
 
           reject("closed");
         },
 
         onMessage: (message) => {
           let packet = JSON.parse(message);
-          if (packet.type !== "message" || packet.id !== aId) {
+          if (packet.type !== "connected" || packet.id !== aId) {
             return;
           }
 
-          message = packet.message;
-          if (message.error) {
-            reject(error);
-          }
-
-          if (message.type !== "paused") {
-            return;
-          }
-
+          // The initial connection packet has been received, don't
+          // need to listen any longer
           aDbg.removeListener(listener);
 
           // Step 7: Create a transport for the connection to the worker.
           let transport = new WorkerDebuggerTransport(aDbg, aId);
           transport.ready();
           transport.hooks = {
             onClosed: () => {
               if (!aDbg.isClosed) {
@@ -882,17 +875,18 @@ var DebuggerServer = {
           };
 
           // Ensure that any packets received from the client on the main thread
           // to actors on the worker thread are forwarded to the server on the
           // worker thread.
           aConnection.setForwarding(aId, transport);
 
           resolve({
-            threadActor: message.from,
+            threadActor: packet.threadActor,
+            consoleActor: packet.consoleActor,
             transport: transport
           });
         }
       };
       aDbg.addListener(listener);
     });
   },
 
--- a/devtools/server/worker.js
+++ b/devtools/server/worker.js
@@ -19,16 +19,17 @@ this.rpc = function (method, ...params) 
   return deferred.promise;
 };
 
 loadSubScript("resource://devtools/shared/worker/loader.js");
 
 var Promise = worker.require("promise");
 var { ActorPool } = worker.require("devtools/server/actors/common");
 var { ThreadActor } = worker.require("devtools/server/actors/script");
+var { WebConsoleActor } = worker.require("devtools/server/actors/webconsole");
 var { TabSources } = worker.require("devtools/server/actors/utils/TabSources");
 var makeDebugger = worker.require("devtools/server/actors/utils/make-debugger");
 var { DebuggerServer } = worker.require("devtools/server/main");
 
 DebuggerServer.init();
 DebuggerServer.createRootActor = function () {
   throw new Error("Should never get here!");
 };
@@ -49,44 +50,51 @@ this.addEventListener("message",  functi
     };
 
     // Step 4: Create a thread actor for the connection to the parent.
     let pool = new ActorPool(connection);
     connection.addActorPool(pool);
 
     let sources = null;
 
-    let actor = new ThreadActor({
+    let parent = {
       makeDebugger: makeDebugger.bind(null, {
         findDebuggees: () => {
           return [this.global];
         },
 
         shouldAddNewGlobalAsDebuggee: () => {
           return true;
         },
       }),
 
       get sources() {
         if (sources === null) {
-          sources = new TabSources(actor);
+          sources = new TabSources(threadActor);
         }
         return sources;
-      }
-    }, global);
+      },
 
-    pool.addActor(actor);
+      window: global
+    };
+
+    let threadActor = new ThreadActor(parent, global);
+    pool.addActor(threadActor);
 
-    // Step 5: Attach to the thread actor.
-    //
-    // This will cause a packet to be sent over the connection to the parent.
-    // Because this connection uses WorkerDebuggerTransport internally, this
-    // packet will be sent using WorkerDebuggerGlobalScope.postMessage, causing
-    // an onMessage event to be fired on the WorkerDebugger in the main thread.
-    actor.onAttach({});
+    let consoleActor = new WebConsoleActor(connection, parent);
+    pool.addActor(consoleActor);
+
+    // Step 5: Send a response packet to the parent to notify
+    // it that a connection has been established.
+    postMessage(JSON.stringify({
+      type: "connected",
+      id: packet.id,
+      threadActor: threadActor.actorID,
+      consoleActor: consoleActor.actorID,
+    }));
     break;
 
   case "disconnect":
     connections[packet.id].connection.close();
     break;
 
   case "rpc":
     let deferred = rpcDeferreds[packet.id];
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -1386,30 +1386,46 @@ WorkerClient.prototype = {
     telemetry: "WORKERDETACH"
   }),
 
   attachThread: function(aOptions = {}, aOnResponse = noop) {
     if (this.thread) {
       DevToolsUtils.executeSoon(() => aOnResponse({
         type: "connected",
         threadActor: this.thread._actor,
+        consoleActor: this.consoleActor,
       }, this.thread));
       return;
     }
 
+    // The connect call on server doesn't attach the thread as of version 44.
     this.request({
       to: this._actor,
       type: "connect",
       options: aOptions,
-    }, (aResponse) => {
-      if (!aResponse.error) {
-        this.thread = new ThreadClient(this, aResponse.threadActor);
+    }, (connectReponse) => {
+      if (connectReponse.error) {
+        aOnResponse(connectReponse, null);
+        return;
+      }
+
+      this.request({
+        to: connectReponse.threadActor,
+        type: "attach"
+      }, (attachResponse) => {
+        if (attachResponse.error) {
+          aOnResponse(attachResponse, null);
+        }
+
+        this.thread = new ThreadClient(this, connectReponse.threadActor);
+        this.consoleActor = connectReponse.consoleActor;
         this.client.registerClient(this.thread);
-      }
-      aOnResponse(aResponse, this.thread);
+
+        aOnResponse(connectReponse, this.thread);
+      });
     });
   },
 
   _onClose: function () {
     this.removeListener("close", this._onClose);
 
     this.client.unregisterClient(this);
     this._isClosed = true;
--- a/devtools/shared/webconsole/client.js
+++ b/devtools/shared/webconsole/client.js
@@ -291,16 +291,23 @@ WebConsoleClient.prototype = {
       }
     });
   },
 
   /**
    * Handler for the actors's unsolicited evaluationResult packet.
    */
   onEvaluationResult: function(aNotification, aPacket) {
+    // The client on the main thread can receive notification packets from
+    // multiple webconsole actors: the one on the main thread and the ones
+    // on worker threads.  So make sure we should be handling this request.
+    if (aPacket.from !== this._actor) {
+      return;
+    }
+
     // Find the associated callback based on this ID, and fire it.
     // In a sync evaluation, this would have already been called in
     // direct response to the client.request function.
     let onResponse = this.pendingEvaluationResults.get(aPacket.resultID);
     if (onResponse) {
       onResponse(aPacket);
       this.pendingEvaluationResults.delete(aPacket.resultID);
     } else {
--- a/devtools/shared/webconsole/moz.build
+++ b/devtools/shared/webconsole/moz.build
@@ -10,9 +10,10 @@ if CONFIG['OS_TARGET'] != 'Android':
 
 DevToolsModules(
     'client.js',
     'network-helper.js',
     'network-monitor.js',
     'server-logger-monitor.js',
     'server-logger.js',
     'utils.js',
+    'worker-utils.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/webconsole/worker-utils.js
@@ -0,0 +1,13 @@
+// XXXworkers This file is loaded on the server side for worker debugging.
+// Since the server is running in the worker thread, it doesn't
+// have access to Services / Components.  This functionality
+// is stubbed out to prevent errors, and will need to implemented
+// for Bug 1209353.
+
+exports.Utils = { l10n: function() {} };
+exports.ConsoleServiceListener = function() {};
+exports.ConsoleAPIListener = function() {};
+exports.addWebConsoleCommands = function() {};
+exports.JSPropertyProvider = function() {};
+exports.ConsoleReflowListener = function() {};
+exports.CONSOLE_WORKER_IDS = [];
--- a/devtools/shared/worker/loader.js
+++ b/devtools/shared/worker/loader.js
@@ -437,18 +437,22 @@ var {
       xpcInspector
     };
   } else { // Worker thread
     let requestors = [];
 
     let scope = this;
 
     let xpcInspector = {
+      get eventLoopNestLevel() {
+        return requestors.length;
+      },
+
       get lastNestRequestor() {
-        return requestors.length === 0 ? null : requestors[0];
+        return requestors.length === 0 ? null : requestors[requestors.length - 1];
       },
 
       enterNestedEventLoop: function (requestor) {
         requestors.push(requestor);
         scope.enterEventLoop();
         return requestors.length;
       },