Bug 817580 - Expose tab actors for apps in child processes. r=jimb
authorAlexandre Poirot <poirot.alex@gmail.com>
Wed, 24 Jul 2013 11:28:32 -0400
changeset 139803 738b7898b6c79352aa9a918c1fae0c5dd5c3531e
parent 139802 fedda0ce50c8153b3ed000c42debdcfc93339ecf
child 139804 36323782eaa34b5638fc03fedab8aa0317259abc
push id25004
push userryanvm@gmail.com
push dateWed, 24 Jul 2013 22:27:03 +0000
treeherdermozilla-central@7d66decedd21 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimb
bugs817580
milestone25.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 817580 - Expose tab actors for apps in child processes. r=jimb
b2g/chrome/content/settings.js
b2g/chrome/content/shell.js
toolkit/devtools/server/actors/childtab.js
toolkit/devtools/server/actors/webapps.js
toolkit/devtools/server/actors/webconsole.js
toolkit/devtools/server/child.js
toolkit/devtools/server/dbg-server.jsm
toolkit/devtools/server/main.js
toolkit/devtools/webconsole/WebConsoleUtils.jsm
--- a/b2g/chrome/content/settings.js
+++ b/b2g/chrome/content/settings.js
@@ -193,17 +193,21 @@ Components.utils.import('resource://gre/
   lock.set('deviceinfo.product_model', product_model, null, null);
 })();
 
 // =================== Debugger ====================
 SettingsListener.observe('devtools.debugger.remote-enabled', false, function(value) {
   Services.prefs.setBoolPref('devtools.debugger.remote-enabled', value);
   // This preference is consulted during startup
   Services.prefs.savePrefFile(null);
-  value ? RemoteDebugger.start() : RemoteDebugger.stop();
+  try {
+    value ? RemoteDebugger.start() : RemoteDebugger.stop();
+  } catch(e) {
+    dump("Error while initializing devtools: " + e + "\n" + e.stack + "\n");
+  }
 
 #ifdef MOZ_WIDGET_GONK
   let enableAdb = value;
 
   try {
     if (Services.prefs.getBoolPref('marionette.defaultPrefs.enabled')) {
       // Marionette is enabled. Force adb on, since marionette requires remote
       // debugging to be disabled (we don't want adb to track the remote debugger
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -972,22 +972,20 @@ let RemoteDebugger = {
   },
 
   // Start the debugger server.
   start: function debugger_start() {
     if (!DebuggerServer.initialized) {
       // Ask for remote connections.
       DebuggerServer.init(this.prompt.bind(this));
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
-#ifndef MOZ_WIDGET_GONK
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
       DebuggerServer.addGlobalActor(DebuggerServer.ChromeDebuggerActor, "chromeDebugger");
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
-#endif
       if ("nsIProfiler" in Ci) {
         DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
       }
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
       DebuggerServer.addActors('chrome://browser/content/dbg-browser-actors.js');
       DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
     }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/childtab.js
@@ -0,0 +1,78 @@
+/* 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";
+
+/**
+ * Tab actor for documents living in a child process.
+ *
+ * Depends on BrowserTabActor, defined in webbrowser.js actor.
+ */
+
+/**
+ * Creates a tab actor for handling requests to the single tab, like
+ * attaching and detaching. ContentTabActor respects the actor factories
+ * registered with DebuggerServer.addTabActor.
+ *
+ * @param connection DebuggerServerConnection
+ *        The conection to the client.
+ * @param browser browser
+ *        The browser instance that contains this tab.
+ */
+function ContentTabActor(connection, browser)
+{
+  BrowserTabActor.call(this, connection, browser);
+}
+
+ContentTabActor.prototype = Object.create(BrowserTabActor.prototype);
+
+ContentTabActor.prototype.constructor = ContentTabActor;
+
+Object.defineProperty(ContentTabActor.prototype, "title", {
+  get: function() {
+    return this.browser.title;
+  },
+  enumerable: true,
+  configurable: false
+});
+
+Object.defineProperty(ContentTabActor.prototype, "url", {
+  get: function() {
+    return this.browser.document.documentURI;
+  },
+  enumerable: true,
+  configurable: false
+});
+
+Object.defineProperty(ContentTabActor.prototype, "contentWindow", {
+  get: function() {
+    return this.browser;
+  },
+  enumerable: true,
+  configurable: false
+});
+
+// Override grip just to rename this._tabActorPool to this._tabActorPool2
+// in order to prevent it to be cleaned on detach.
+// We have to keep tab actors alive as we keep the ContentTabActor
+// alive after detach and reuse it for multiple debug sessions.
+ContentTabActor.prototype.grip = function () {
+  let response = {
+    'actor': this.actorID,
+    'title': this.title,
+    'url': this.url
+  };
+
+  // Walk over tab actors added by extensions and add them to a new ActorPool.
+  let actorPool = new ActorPool(this.conn);
+  this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
+  if (!actorPool.isEmpty()) {
+    this._tabActorPool2 = actorPool;
+    this.conn.addActorPool(this._tabActorPool2);
+  }
+
+  this._appendExtraActors(response);
+  return response;
+};
+
--- a/toolkit/devtools/server/actors/webapps.js
+++ b/toolkit/devtools/server/actors/webapps.js
@@ -3,16 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 let Cu = Components.utils;
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 
+let promise;
+
 function debug(aMsg) {
   /*
   Cc["@mozilla.org/consoleservice;1"]
     .getService(Ci.nsIConsoleService)
     .logStringMessage("--*-- WebappsActor : " + aMsg);
   */
 }
 
@@ -24,17 +26,21 @@ function WebappsActor(aConnection) {
   debug("init");
   // Load actor dependencies lazily as this actor require extra environnement
   // preparation to work (like have a profile setup in xpcshell tests)
 
   Cu.import("resource://gre/modules/Webapps.jsm");
   Cu.import("resource://gre/modules/AppsUtils.jsm");
   Cu.import("resource://gre/modules/FileUtils.jsm");
   Cu.import('resource://gre/modules/Services.jsm');
-  let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
+  promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
+
+  // Keep reference of already created app actors.
+  // key: app frame message manager, value: ContentTabActor's grip() value
+  this._appActorsMap = new Map();
 }
 
 WebappsActor.prototype = {
   actorPrefix: "webapps",
 
   _registerApp: function wa_actorRegisterApp(aApp, aId, aDir) {
     debug("registerApp");
     let reg = DOMApplicationRegistry;
@@ -370,24 +376,211 @@ WebappsActor.prototype = {
     if (!app) {
       return { error: "missingParameter",
                message: "No application for " + manifestURL };
     }
 
     reg.close(app);
 
     return {};
+  },
+
+  _appFrames: function () {
+    // Register the system app
+    let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
+    let systemAppFrame = chromeWindow.shell.contentBrowser;
+    yield systemAppFrame;
+
+    // Register apps hosted in the system app. i.e. the homescreen, all regular
+    // apps and the keyboard.
+    // Bookmark apps and other system app internal frames like captive portal
+    // are also hosted in system app, but they are not using mozapp attribute.
+    let frames = systemAppFrame.contentDocument.querySelectorAll("iframe[mozapp]");
+    for (let i = 0; i < frames.length; i++) {
+      yield frames[i];
+    }
+  },
+
+  listRunningApps: function (aRequest) {
+    debug("listRunningApps\n");
+
+    let apps = [];
+
+    for each (let frame in this._appFrames()) {
+      let manifestURL = frame.getAttribute("mozapp");
+      apps.push(manifestURL);
+    }
+
+    return { apps: apps };
+  },
+
+  _connectToApp: function (aFrame) {
+    let defer = Promise.defer();
+
+    let mm = aFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
+    mm.loadFrameScript("resource://gre/modules/devtools/server/child.js", false);
+
+    let childTransport, prefix;
+
+    let onActorCreated = makeInfallible(function (msg) {
+      mm.removeMessageListener("debug:actor", onActorCreated);
+
+      dump("***** Got debug:actor\n");
+      let { actor, appId } = msg.json;
+      prefix = msg.json.prefix;
+
+      // Pipe Debugger message from/to parent/child via the message manager
+      childTransport = new ChildDebuggerTransport(mm, prefix);
+      childTransport.hooks = {
+        onPacket: this.conn.send.bind(this.conn),
+        onClosed: function () {}
+      };
+      childTransport.ready();
+
+      this.conn.setForwarding(prefix, childTransport);
+
+      debug("establishing forwarding for app with prefix " + prefix);
+
+      this._appActorsMap.set(mm, actor);
+
+      defer.resolve(actor);
+    }).bind(this);
+    mm.addMessageListener("debug:actor", onActorCreated);
+
+    let onMessageManagerDisconnect = makeInfallible(function (subject, topic, data) {
+      if (subject == mm) {
+        Services.obs.removeObserver(onMessageManagerDisconnect, topic);
+        if (childTransport) {
+          // If we have a child transport, the actor has already
+          // been created. We need to stop using this message manager.
+          childTransport.close();
+          this.conn.cancelForwarding(prefix);
+        } else {
+          // Otherwise, the app has been closed before the actor
+          // had a chance to be created, so we are not able to create
+          // the actor.
+          defer.resolve(null);
+        }
+        this._appActorsMap.delete(mm);
+      }
+    }).bind(this);
+    Services.obs.addObserver(onMessageManagerDisconnect,
+                             "message-manager-disconnect", false);
+
+    let prefixStart = this.conn.prefix + "child";
+    mm.sendAsyncMessage("debug:connect", { prefix: prefixStart });
+
+    return defer.promise;
+  },
+
+  getAppActor: function ({ manifestURL }) {
+    debug("getAppActor\n");
+
+    let appFrame = null;
+    for each (let frame in this._appFrames()) {
+      if (frame.getAttribute("mozapp") == manifestURL) {
+        appFrame = frame;
+        break;
+      }
+    }
+
+    if (!appFrame) {
+      return { error: "appNotFound",
+               message: "Unable to find any opened app whose manifest " +
+                        "is '" + manifestURL + "'" };
+    }
+
+    // Only create a new actor, if we haven't already
+    // instanciated one for this connection.
+    let mm = appFrame.QueryInterface(Ci.nsIFrameLoaderOwner)
+                     .frameLoader
+                     .messageManager;
+    let actor = this._appActorsMap.get(mm);
+    if (!actor) {
+      return this._connectToApp(appFrame)
+                 .then(function (actor) ({ actor: actor }));
+    }
+
+    return { actor: actor };
+  },
+
+  watchApps: function () {
+    this._framesByOrigin = {};
+    let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
+    let systemAppFrame = chromeWindow.getContentWindow();
+    systemAppFrame.addEventListener("appwillopen", this);
+    systemAppFrame.addEventListener("appterminated", this);
+
+    return {};
+  },
+
+  unwatchApps: function () {
+    this._framesByOrigin = null;
+    let chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');
+    let systemAppFrame = chromeWindow.getContentWindow();
+    systemAppFrame.removeEventListener("appwillopen", this);
+    systemAppFrame.removeEventListener("appterminated", this);
+
+    return {};
+  },
+
+  handleEvent: function (event) {
+    let frame;
+    let origin = event.detail.origin;
+    switch(event.type) {
+      case "appwillopen":
+        frame = event.target;
+        // Ignore the event if we already received an appwillopen for this app
+        // (appwillopen is also fired when the app has been moved to background
+        // and get back to foreground)
+        let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner)
+                         .frameLoader
+                         .messageManager;
+        if (this._appActorsMap.has(mm)) {
+          return;
+        }
+
+        // Workaround to be able to get the related frame on `appterminated`.
+        // `appterminated` event being dispatched by gaia only comes app origin
+        // whereas we need to get the its manifest URL, that we can fetch
+        // on the app frame.
+        this._framesByOrigin[origin] = frame;
+
+        this.conn.send({ from: this.actorID,
+                         type: "appOpen",
+                         manifestURL: frame.getAttribute("mozapp")
+                       });
+        break;
+
+      case "appterminated":
+        // Get the related app frame out of this event
+        // TODO: eventually fire the event on the frame or at least use
+        // manifestURL as key (and propagate manifestURL via event detail)
+        frame = this._framesByOrigin[origin];
+        delete this._framesByOrigin[origin];
+        if (frame) {
+          let manifestURL = frame.getAttribute("mozapp");
+          this.conn.send({ from: this.actorID,
+                           type: "appClose",
+                           manifestURL: manifestURL
+                         });
+        }
+        break;
+    }
   }
 };
 
 /**
  * The request types this actor can handle.
  */
 WebappsActor.prototype.requestTypes = {
   "install": WebappsActor.prototype.install,
   "getAll": WebappsActor.prototype.getAll,
   "launch": WebappsActor.prototype.launch,
   "close": WebappsActor.prototype.close,
-  "uninstall": WebappsActor.prototype.uninstall
+  "uninstall": WebappsActor.prototype.uninstall,
+  "listRunningApps": WebappsActor.prototype.listRunningApps,
+  "getAppActor": WebappsActor.prototype.getAppActor,
+  "watchApps": WebappsActor.prototype.watchApps,
+  "unwatchApps": WebappsActor.prototype.unwatchApps
 };
 
 DebuggerServer.addGlobalActor(WebappsActor, "webappsActor");
-
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -49,28 +49,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
  *        The connection to the client, DebuggerServerConnection.
  * @param object [aParentActor]
  *        Optional, the parent actor.
  */
 function WebConsoleActor(aConnection, aParentActor)
 {
   this.conn = aConnection;
 
-  if (aParentActor instanceof BrowserTabActor &&
-      aParentActor.browser instanceof Ci.nsIDOMWindow) {
-    // B2G tab actor |this.browser| points to a DOM chrome window, not
+  if (aParentActor.browser instanceof Ci.nsIDOMWindow) {
+    // B2G tab actor |this.browser| points to a DOM window, not
     // a xul:browser element.
     //
-    // TODO: bug 802246 - b2g has only one tab actor, the shell.xul, which is
+    // TODO: bug 802246 - b2g has tab actor which is
     // not properly supported by the console actor - see bug for details.
     //
-    // Below we work around the problem: selecting the shell.xul tab actor
+    // Below we work around the problem: selecting a b2g tab actor
     // behaves as if the user picked the global console actor.
-    //this._window = aParentActor.browser;
-    this._window = Services.wm.getMostRecentWindow("navigator:browser");
+    this._window = aParentActor.browser;
     this._isGlobalActor = true;
   }
   else if (aParentActor instanceof BrowserTabActor &&
            aParentActor.browser instanceof Ci.nsIDOMElement) {
     // Firefox for desktop tab actor |this.browser| points to the xul:browser
     // element.
     this._window = aParentActor.browser.contentWindow;
   }
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/child.js
@@ -0,0 +1,34 @@
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
+
+if (!DebuggerServer.initialized) {
+  DebuggerServer.init();
+}
+
+// In case of apps being loaded in parent process, DebuggerServer is already
+// initialized, but child specific actors are not registered.
+// Otherwise, for apps in child process, we need to load actors the first
+// time we load child.js
+DebuggerServer.addChildActors();
+
+let onConnect = DevToolsUtils.makeInfallible(function (msg) {
+  removeMessageListener("debug:connect", onConnect);
+
+  let mm = msg.target;
+
+  let prefix = msg.data.prefix + docShell.appId;
+
+  let conn = DebuggerServer.connectToParent(prefix, mm);
+
+  let actor = new DebuggerServer.ContentTabActor(conn, content);
+  let actorPool = new ActorPool(conn);
+  actorPool.addActor(actor);
+  conn.addActorPool(actorPool);
+
+  sendAsyncMessage("debug:actor", {actor: actor.grip(),
+                                   appId: docShell.appId,
+                                   prefix: prefix});
+});
+
+addMessageListener("debug:connect", onConnect);
--- a/toolkit/devtools/server/dbg-server.jsm
+++ b/toolkit/devtools/server/dbg-server.jsm
@@ -10,17 +10,17 @@
  * shield it from the debuggee. This way, when debugging chrome globals,
  * debugger and debuggee will be in separate compartments.
  */
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
-this.EXPORTED_SYMBOLS = ["DebuggerServer"];
+this.EXPORTED_SYMBOLS = ["DebuggerServer", "ActorPool"];
 
 var loadSubScript =
   "function loadSubScript(aURL)\n" +
   "{\n" +
   "const Ci = Components.interfaces;\n" +
   "const Cc = Components.classes;\n" +
   "  try {\n" +
   "    let loader = Cc[\"@mozilla.org/moz/jssubscript-loader;1\"]\n" +
@@ -36,8 +36,9 @@ var loadSubScript =
 var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
                       .createInstance(Ci.nsIPrincipal);
 
 var gGlobal = Cu.Sandbox(systemPrincipal);
 Cu.evalInSandbox(loadSubScript, gGlobal, "1.8");
 gGlobal.loadSubScript("resource://gre/modules/devtools/server/main.js");
 
 this.DebuggerServer = gGlobal.DebuggerServer;
+this.ActorPool = gGlobal.ActorPool;
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -291,16 +291,36 @@ var DebuggerServer = {
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
 
     this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
     this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
     this.registerModule("devtools/server/actors/inspector");
   },
 
   /**
+   * Install tab actors in documents loaded in content childs
+   */
+  addChildActors: function () {
+    // In case of apps being loaded in parent process, DebuggerServer is already
+    // initialized and browser actors are already loaded,
+    // but childtab.js hasn't been loaded yet.
+    if (!("BrowserTabActor" in this)) {
+      this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
+      this.addActors("resource://gre/modules/devtools/server/actors/script.js");
+      this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
+      this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
+      this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
+      this.registerModule("devtools/server/actors/inspector");
+    }
+    if (!("ContentTabActor" in DebuggerServer)) {
+      this.addActors("resource://gre/modules/devtools/server/actors/childtab.js");
+    }
+  },
+
+  /**
    * Listens on the given port for remote debugger connections.
    *
    * @param aPort int
    *        The port to listen on.
    */
   openListener: function DS_openListener(aPort) {
     if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
       return false;
@@ -388,16 +408,32 @@ var DebuggerServer = {
     //
     // But every time you use this, you will feel the shame of having
     // used a property that starts with a '_'.
     clientTransport._serverConnection = connection;
 
     return clientTransport;
   },
 
+  /**
+   * In a content child process, create a new connection that exchanges
+   * nsIMessageSender messages with our parent process.
+   *
+   * @param aPrefix
+   *    The prefix we should use in our nsIMessageSender message names and
+   *    actor names. This connection will use messages named
+   *    "debug:<prefix>:packet", and all its actors will have names
+   *    beginning with "<prefix>:".
+   */
+  connectToParent: function(aPrefix, aMessageManager) {
+    this._checkInit();
+
+    let transport = new ChildDebuggerTransport(aMessageManager, aPrefix);
+    return this._onConnection(transport, aPrefix, true);
+  },
 
   // nsIServerSocketListener implementation
 
   onSocketAccepted:
   makeInfallible(function DS_onSocketAccepted(aSocket, aTransport) {
     if (!this._allowConnection()) {
       return;
     }
@@ -431,34 +467,36 @@ var DebuggerServer = {
    * connectPipe(), from connectToParent, or from an incoming socket
    * connection handler.
    *
    * If present, |aForwardingPrefix| is a forwarding prefix that a parent
    * server is using to recognizes messages intended for this server. Ensure
    * that all our actors have names beginning with |aForwardingPrefix + ':'|.
    * In particular, the root actor's name will be |aForwardingPrefix + ':root'|.
    */
-  _onConnection: function DS_onConnection(aTransport, aForwardingPrefix) {
+  _onConnection: function DS_onConnection(aTransport, aForwardingPrefix, aNoRootActor = false) {
     let connID;
     if (aForwardingPrefix) {
       connID = aForwardingPrefix + ":";
     } else {
       connID = "conn" + this._nextConnID++ + '.';
     }
     let conn = new DebuggerServerConnection(connID, aTransport);
     this._connections[connID] = conn;
 
     // Create a root actor for the connection and send the hello packet.
-    conn.rootActor = this.createRootActor(conn);
-    if (aForwardingPrefix)
-      conn.rootActor.actorID = aForwardingPrefix + ":root";
-    else
-      conn.rootActor.actorID = "root";
-    conn.addActor(conn.rootActor);
-    aTransport.send(conn.rootActor.sayHello());
+    if (!aNoRootActor) {
+      conn.rootActor = this.createRootActor(conn);
+      if (aForwardingPrefix)
+        conn.rootActor.actorID = aForwardingPrefix + ":root";
+      else
+        conn.rootActor.actorID = "root";
+      conn.addActor(conn.rootActor);
+      aTransport.send(conn.rootActor.sayHello());
+    }
     aTransport.ready();
 
     return conn;
   },
 
   /**
    * Remove the connection from the debugging server.
    */
--- a/toolkit/devtools/webconsole/WebConsoleUtils.jsm
+++ b/toolkit/devtools/webconsole/WebConsoleUtils.jsm
@@ -1818,18 +1818,20 @@ NetworkMonitor.prototype = {
    */
   init: function NM_init()
   {
     this.responsePipeSegmentSize = Services.prefs
                                    .getIntPref("network.buffer.cache.size");
 
     gActivityDistributor.addObserver(this);
 
-    Services.obs.addObserver(this._httpResponseExaminer,
-                             "http-on-examine-response", false);
+    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+      Services.obs.addObserver(this._httpResponseExaminer,
+                               "http-on-examine-response", false);
+    }
   },
 
   /**
    * Observe notifications for the http-on-examine-response topic, coming from
    * the nsIObserverService.
    *
    * @private
    * @param nsIHttpChannel aSubject
@@ -2325,18 +2327,20 @@ NetworkMonitor.prototype = {
   },
 
   /**
    * Suspend Web Console activity. This is called when all Web Consoles are
    * closed.
    */
   destroy: function NM_destroy()
   {
-    Services.obs.removeObserver(this._httpResponseExaminer,
-                                "http-on-examine-response");
+    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) {
+      Services.obs.removeObserver(this._httpResponseExaminer,
+                                  "http-on-examine-response");
+    }
 
     gActivityDistributor.removeObserver(this);
 
     this.openRequests = {};
     this.openResponses = {};
     this.owner = null;
     this.window = null;
   },