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 269584 0e616440737c12028953b5095ee7222d7e4d79a4
parent 269583 03eec3bcf2933e13f3f147b72b3e722c36a42e52
child 269585 b39a7e4289d8de57151b826dec3902130f9d0634
push id29586
push usercbook@mozilla.com
push dateTue, 27 Oct 2015 09:52:52 +0000
treeherdermozilla-central@e36bf6850a93 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersejpbruel
bugs1215117
milestone44.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 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;
       },