toolkit/devtools/server/actors/webbrowser.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/devtools/server/actors/webbrowser.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1448 @@
     1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +"use strict";
    1.11 +
    1.12 +let {Ci,Cu} = require("chrome");
    1.13 +let {createExtraActors, appendExtraActors} = require("devtools/server/actors/common");
    1.14 +let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
    1.15 +
    1.16 +let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
    1.17 +Cu.import("resource://gre/modules/Services.jsm");
    1.18 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.19 +
    1.20 +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
    1.21 +
    1.22 +// Assumptions on events module:
    1.23 +// events needs to be dispatched synchronously,
    1.24 +// by calling the listeners in the order or registration.
    1.25 +XPCOMUtils.defineLazyGetter(this, "events", () => {
    1.26 +  return devtools.require("sdk/event/core");
    1.27 +});
    1.28 +
    1.29 +// Also depends on following symbols, shared by common scope with main.js:
    1.30 +// DebuggerServer, CommonCreateExtraActors, CommonAppendExtraActors, ActorPool,
    1.31 +// ThreadActor
    1.32 +
    1.33 +/**
    1.34 + * Browser-specific actors.
    1.35 + */
    1.36 +
    1.37 +/**
    1.38 + * Yield all windows of type |aWindowType|, from the oldest window to the
    1.39 + * youngest, using nsIWindowMediator::getEnumerator. We're usually
    1.40 + * interested in "navigator:browser" windows.
    1.41 + */
    1.42 +function allAppShellDOMWindows(aWindowType)
    1.43 +{
    1.44 +  let e = Services.wm.getEnumerator(aWindowType);
    1.45 +  while (e.hasMoreElements()) {
    1.46 +    yield e.getNext();
    1.47 +  }
    1.48 +}
    1.49 +
    1.50 +/**
    1.51 + * Retrieve the window type of the top-level window |aWindow|.
    1.52 + */
    1.53 +function appShellDOMWindowType(aWindow) {
    1.54 +  /* This is what nsIWindowMediator's enumerator checks. */
    1.55 +  return aWindow.document.documentElement.getAttribute('windowtype');
    1.56 +}
    1.57 +
    1.58 +/**
    1.59 + * Send Debugger:Shutdown events to all "navigator:browser" windows.
    1.60 + */
    1.61 +function sendShutdownEvent() {
    1.62 +  for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
    1.63 +    let evt = win.document.createEvent("Event");
    1.64 +    evt.initEvent("Debugger:Shutdown", true, false);
    1.65 +    win.document.documentElement.dispatchEvent(evt);
    1.66 +  }
    1.67 +}
    1.68 +
    1.69 +/**
    1.70 + * Construct a root actor appropriate for use in a server running in a
    1.71 + * browser. The returned root actor:
    1.72 + * - respects the factories registered with DebuggerServer.addGlobalActor,
    1.73 + * - uses a BrowserTabList to supply tab actors,
    1.74 + * - sends all navigator:browser window documents a Debugger:Shutdown event
    1.75 + *   when it exits.
    1.76 + *
    1.77 + * * @param aConnection DebuggerServerConnection
    1.78 + *        The conection to the client.
    1.79 + */
    1.80 +function createRootActor(aConnection)
    1.81 +{
    1.82 +  return new RootActor(aConnection,
    1.83 +                       {
    1.84 +                         tabList: new BrowserTabList(aConnection),
    1.85 +                         addonList: new BrowserAddonList(aConnection),
    1.86 +                         globalActorFactories: DebuggerServer.globalActorFactories,
    1.87 +                         onShutdown: sendShutdownEvent
    1.88 +                       });
    1.89 +}
    1.90 +
    1.91 +/**
    1.92 + * A live list of BrowserTabActors representing the current browser tabs,
    1.93 + * to be provided to the root actor to answer 'listTabs' requests.
    1.94 + *
    1.95 + * This object also takes care of listening for TabClose events and
    1.96 + * onCloseWindow notifications, and exiting the BrowserTabActors concerned.
    1.97 + *
    1.98 + * (See the documentation for RootActor for the definition of the "live
    1.99 + * list" interface.)
   1.100 + *
   1.101 + * @param aConnection DebuggerServerConnection
   1.102 + *     The connection in which this list's tab actors may participate.
   1.103 + *
   1.104 + * Some notes:
   1.105 + *
   1.106 + * This constructor is specific to the desktop browser environment; it
   1.107 + * maintains the tab list by tracking XUL windows and their XUL documents'
   1.108 + * "tabbrowser", "tab", and "browser" elements. What's entailed in maintaining
   1.109 + * an accurate list of open tabs in this context?
   1.110 + *
   1.111 + * - Opening and closing XUL windows:
   1.112 + *
   1.113 + * An nsIWindowMediatorListener is notified when new XUL windows (i.e., desktop
   1.114 + * windows) are opened and closed. It is not notified of individual content
   1.115 + * browser tabs coming and going within such a XUL window. That seems
   1.116 + * reasonable enough; it's concerned with XUL windows, not tab elements in the
   1.117 + * window's XUL document.
   1.118 + *
   1.119 + * However, even if we attach TabOpen and TabClose event listeners to each XUL
   1.120 + * window as soon as it is created:
   1.121 + *
   1.122 + * - we do not receive a TabOpen event for the initial empty tab of a new XUL
   1.123 + *   window; and
   1.124 + *
   1.125 + * - we do not receive TabClose events for the tabs of a XUL window that has
   1.126 + *   been closed.
   1.127 + *
   1.128 + * This means that TabOpen and TabClose events alone are not sufficient to
   1.129 + * maintain an accurate list of live tabs and mark tab actors as closed
   1.130 + * promptly. Our nsIWindowMediatorListener onCloseWindow handler must find and
   1.131 + * exit all actors for tabs that were in the closing window.
   1.132 + *
   1.133 + * Since this is a bit hairy, we don't make each individual attached tab actor
   1.134 + * responsible for noticing when it has been closed; we watch for that, and
   1.135 + * promise to call each actor's 'exit' method when it's closed, regardless of
   1.136 + * how we learn the news.
   1.137 + *
   1.138 + * - nsIWindowMediator locks
   1.139 + *
   1.140 + * nsIWindowMediator holds a lock protecting its list of top-level windows
   1.141 + * while it calls nsIWindowMediatorListener methods. nsIWindowMediator's
   1.142 + * GetEnumerator method also tries to acquire that lock. Thus, enumerating
   1.143 + * windows from within a listener method deadlocks (bug 873589). Rah. One
   1.144 + * can sometimes work around this by leaving the enumeration for a later
   1.145 + * tick.
   1.146 + *
   1.147 + * - Dragging tabs between windows:
   1.148 + *
   1.149 + * When a tab is dragged from one desktop window to another, we receive a
   1.150 + * TabOpen event for the new tab, and a TabClose event for the old tab; tab XUL
   1.151 + * elements do not really move from one document to the other (although their
   1.152 + * linked browser's content window objects do).
   1.153 + *
   1.154 + * However, while we could thus assume that each tab stays with the XUL window
   1.155 + * it belonged to when it was created, I'm not sure this is behavior one should
   1.156 + * rely upon. When a XUL window is closed, we take the less efficient, more
   1.157 + * conservative approach of simply searching the entire table for actors that
   1.158 + * belong to the closing XUL window, rather than trying to somehow track which
   1.159 + * XUL window each tab belongs to.
   1.160 + */
   1.161 +function BrowserTabList(aConnection)
   1.162 +{
   1.163 +  this._connection = aConnection;
   1.164 +
   1.165 +  /*
   1.166 +   * The XUL document of a tabbed browser window has "tab" elements, whose
   1.167 +   * 'linkedBrowser' JavaScript properties are "browser" elements; those
   1.168 +   * browsers' 'contentWindow' properties are wrappers on the tabs' content
   1.169 +   * window objects.
   1.170 +   *
   1.171 +   * This map's keys are "browser" XUL elements; it maps each browser element
   1.172 +   * to the tab actor we've created for its content window, if we've created
   1.173 +   * one. This map serves several roles:
   1.174 +   *
   1.175 +   * - During iteration, we use it to find actors we've created previously.
   1.176 +   *
   1.177 +   * - On a TabClose event, we use it to find the tab's actor and exit it.
   1.178 +   *
   1.179 +   * - When the onCloseWindow handler is called, we iterate over it to find all
   1.180 +   *   tabs belonging to the closing XUL window, and exit them.
   1.181 +   *
   1.182 +   * - When it's empty, and the onListChanged hook is null, we know we can
   1.183 +   *   stop listening for events and notifications.
   1.184 +   *
   1.185 +   * We listen for TabClose events and onCloseWindow notifications in order to
   1.186 +   * send onListChanged notifications, but also to tell actors when their
   1.187 +   * referent has gone away and remove entries for dead browsers from this map.
   1.188 +   * If that code is working properly, neither this map nor the actors in it
   1.189 +   * should ever hold dead tabs alive.
   1.190 +   */
   1.191 +  this._actorByBrowser = new Map();
   1.192 +
   1.193 +  /* The current onListChanged handler, or null. */
   1.194 +  this._onListChanged = null;
   1.195 +
   1.196 +  /*
   1.197 +   * True if we've been iterated over since we last called our onListChanged
   1.198 +   * hook.
   1.199 +   */
   1.200 +  this._mustNotify = false;
   1.201 +
   1.202 +  /* True if we're testing, and should throw if consistency checks fail. */
   1.203 +  this._testing = false;
   1.204 +}
   1.205 +
   1.206 +BrowserTabList.prototype.constructor = BrowserTabList;
   1.207 +
   1.208 +
   1.209 +/**
   1.210 + * Get the selected browser for the given navigator:browser window.
   1.211 + * @private
   1.212 + * @param aWindow nsIChromeWindow
   1.213 + *        The navigator:browser window for which you want the selected browser.
   1.214 + * @return nsIDOMElement|null
   1.215 + *         The currently selected xul:browser element, if any. Note that the
   1.216 + *         browser window might not be loaded yet - the function will return
   1.217 + *         |null| in such cases.
   1.218 + */
   1.219 +BrowserTabList.prototype._getSelectedBrowser = function(aWindow) {
   1.220 +  return aWindow.gBrowser ? aWindow.gBrowser.selectedBrowser : null;
   1.221 +};
   1.222 +
   1.223 +BrowserTabList.prototype._getChildren = function(aWindow) {
   1.224 +  return aWindow.gBrowser.browsers;
   1.225 +};
   1.226 +
   1.227 +BrowserTabList.prototype.getList = function() {
   1.228 +  let topXULWindow = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
   1.229 +
   1.230 +  // As a sanity check, make sure all the actors presently in our map get
   1.231 +  // picked up when we iterate over all windows' tabs.
   1.232 +  let initialMapSize = this._actorByBrowser.size;
   1.233 +  let foundCount = 0;
   1.234 +
   1.235 +  // To avoid mysterious behavior if tabs are closed or opened mid-iteration,
   1.236 +  // we update the map first, and then make a second pass over it to yield
   1.237 +  // the actors. Thus, the sequence yielded is always a snapshot of the
   1.238 +  // actors that were live when we began the iteration.
   1.239 +
   1.240 +  let actorPromises = [];
   1.241 +
   1.242 +  // Iterate over all navigator:browser XUL windows.
   1.243 +  for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
   1.244 +    let selectedBrowser = this._getSelectedBrowser(win);
   1.245 +    if (!selectedBrowser) {
   1.246 +      continue;
   1.247 +    }
   1.248 +
   1.249 +    // For each tab in this XUL window, ensure that we have an actor for
   1.250 +    // it, reusing existing actors where possible. We actually iterate
   1.251 +    // over 'browser' XUL elements, and BrowserTabActor uses
   1.252 +    // browser.contentWindow as the debuggee global.
   1.253 +    for (let browser of this._getChildren(win)) {
   1.254 +      // Do we have an existing actor for this browser? If not, create one.
   1.255 +      let actor = this._actorByBrowser.get(browser);
   1.256 +      if (actor) {
   1.257 +        actorPromises.push(promise.resolve(actor));
   1.258 +        foundCount++;
   1.259 +      } else if (browser.isRemoteBrowser) {
   1.260 +        actor = new RemoteBrowserTabActor(this._connection, browser);
   1.261 +        this._actorByBrowser.set(browser, actor);
   1.262 +        let promise = actor.connect().then((form) => {
   1.263 +          actor._form = form;
   1.264 +          return actor;
   1.265 +        });
   1.266 +        actorPromises.push(promise);
   1.267 +      } else {
   1.268 +        actor = new BrowserTabActor(this._connection, browser, win.gBrowser);
   1.269 +        this._actorByBrowser.set(browser, actor);
   1.270 +        actorPromises.push(promise.resolve(actor));
   1.271 +      }
   1.272 +
   1.273 +      // Set the 'selected' properties on all actors correctly.
   1.274 +      actor.selected = (win === topXULWindow && browser === selectedBrowser);
   1.275 +    }
   1.276 +  }
   1.277 +
   1.278 +  if (this._testing && initialMapSize !== foundCount)
   1.279 +    throw Error("_actorByBrowser map contained actors for dead tabs");
   1.280 +
   1.281 +  this._mustNotify = true;
   1.282 +  this._checkListening();
   1.283 +
   1.284 +  return promise.all(actorPromises);
   1.285 +};
   1.286 +
   1.287 +Object.defineProperty(BrowserTabList.prototype, 'onListChanged', {
   1.288 +  enumerable: true, configurable:true,
   1.289 +  get: function() { return this._onListChanged; },
   1.290 +  set: function(v) {
   1.291 +    if (v !== null && typeof v !== 'function') {
   1.292 +      throw Error("onListChanged property may only be set to 'null' or a function");
   1.293 +    }
   1.294 +    this._onListChanged = v;
   1.295 +    this._checkListening();
   1.296 +  }
   1.297 +});
   1.298 +
   1.299 +/**
   1.300 + * The set of tabs has changed somehow. Call our onListChanged handler, if
   1.301 + * one is set, and if we haven't already called it since the last iteration.
   1.302 + */
   1.303 +BrowserTabList.prototype._notifyListChanged = function() {
   1.304 +  if (!this._onListChanged)
   1.305 +    return;
   1.306 +  if (this._mustNotify) {
   1.307 +    this._onListChanged();
   1.308 +    this._mustNotify = false;
   1.309 +  }
   1.310 +};
   1.311 +
   1.312 +/**
   1.313 + * Exit |aActor|, belonging to |aBrowser|, and notify the onListChanged
   1.314 + * handle if needed.
   1.315 + */
   1.316 +BrowserTabList.prototype._handleActorClose = function(aActor, aBrowser) {
   1.317 +  if (this._testing) {
   1.318 +    if (this._actorByBrowser.get(aBrowser) !== aActor) {
   1.319 +      throw Error("BrowserTabActor not stored in map under given browser");
   1.320 +    }
   1.321 +    if (aActor.browser !== aBrowser) {
   1.322 +      throw Error("actor's browser and map key don't match");
   1.323 +    }
   1.324 +  }
   1.325 +
   1.326 +  this._actorByBrowser.delete(aBrowser);
   1.327 +  aActor.exit();
   1.328 +
   1.329 +  this._notifyListChanged();
   1.330 +  this._checkListening();
   1.331 +};
   1.332 +
   1.333 +/**
   1.334 + * Make sure we are listening or not listening for activity elsewhere in
   1.335 + * the browser, as appropriate. Other than setting up newly created XUL
   1.336 + * windows, all listener / observer connection and disconnection should
   1.337 + * happen here.
   1.338 + */
   1.339 +BrowserTabList.prototype._checkListening = function() {
   1.340 +  /*
   1.341 +   * If we have an onListChanged handler that we haven't sent an announcement
   1.342 +   * to since the last iteration, we need to watch for tab creation.
   1.343 +   *
   1.344 +   * Oddly, we don't need to watch for 'close' events here. If our actor list
   1.345 +   * is empty, then either it was empty the last time we iterated, and no
   1.346 +   * close events are possible, or it was not empty the last time we
   1.347 +   * iterated, but all the actors have since been closed, and we must have
   1.348 +   * sent a notification already when they closed.
   1.349 +   */
   1.350 +  this._listenForEventsIf(this._onListChanged && this._mustNotify,
   1.351 +                          "_listeningForTabOpen", ["TabOpen", "TabSelect"]);
   1.352 +
   1.353 +  /* If we have live actors, we need to be ready to mark them dead. */
   1.354 +  this._listenForEventsIf(this._actorByBrowser.size > 0,
   1.355 +                          "_listeningForTabClose", ["TabClose"]);
   1.356 +
   1.357 +  /*
   1.358 +   * We must listen to the window mediator in either case, since that's the
   1.359 +   * only way to find out about tabs that come and go when top-level windows
   1.360 +   * are opened and closed.
   1.361 +   */
   1.362 +  this._listenToMediatorIf((this._onListChanged && this._mustNotify) ||
   1.363 +                           (this._actorByBrowser.size > 0));
   1.364 +};
   1.365 +
   1.366 +/*
   1.367 + * Add or remove event listeners for all XUL windows.
   1.368 + *
   1.369 + * @param aShouldListen boolean
   1.370 + *    True if we should add event handlers; false if we should remove them.
   1.371 + * @param aGuard string
   1.372 + *    The name of a guard property of 'this', indicating whether we're
   1.373 + *    already listening for those events.
   1.374 + * @param aEventNames array of strings
   1.375 + *    An array of event names.
   1.376 + */
   1.377 +BrowserTabList.prototype._listenForEventsIf = function(aShouldListen, aGuard, aEventNames) {
   1.378 +  if (!aShouldListen !== !this[aGuard]) {
   1.379 +    let op = aShouldListen ? "addEventListener" : "removeEventListener";
   1.380 +    for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
   1.381 +      for (let name of aEventNames) {
   1.382 +        win[op](name, this, false);
   1.383 +      }
   1.384 +    }
   1.385 +    this[aGuard] = aShouldListen;
   1.386 +  }
   1.387 +};
   1.388 +
   1.389 +/**
   1.390 + * Implement nsIDOMEventListener.
   1.391 + */
   1.392 +BrowserTabList.prototype.handleEvent = DevToolsUtils.makeInfallible(function(aEvent) {
   1.393 +  switch (aEvent.type) {
   1.394 +  case "TabOpen":
   1.395 +  case "TabSelect":
   1.396 +    /* Don't create a new actor; iterate will take care of that. Just notify. */
   1.397 +    this._notifyListChanged();
   1.398 +    this._checkListening();
   1.399 +    break;
   1.400 +  case "TabClose":
   1.401 +    let browser = aEvent.target.linkedBrowser;
   1.402 +    let actor = this._actorByBrowser.get(browser);
   1.403 +    if (actor) {
   1.404 +      this._handleActorClose(actor, browser);
   1.405 +    }
   1.406 +    break;
   1.407 +  }
   1.408 +}, "BrowserTabList.prototype.handleEvent");
   1.409 +
   1.410 +/*
   1.411 + * If |aShouldListen| is true, ensure we've registered a listener with the
   1.412 + * window mediator. Otherwise, ensure we haven't registered a listener.
   1.413 + */
   1.414 +BrowserTabList.prototype._listenToMediatorIf = function(aShouldListen) {
   1.415 +  if (!aShouldListen !== !this._listeningToMediator) {
   1.416 +    let op = aShouldListen ? "addListener" : "removeListener";
   1.417 +    Services.wm[op](this);
   1.418 +    this._listeningToMediator = aShouldListen;
   1.419 +  }
   1.420 +};
   1.421 +
   1.422 +/**
   1.423 + * nsIWindowMediatorListener implementation.
   1.424 + *
   1.425 + * See _onTabClosed for explanation of why we needn't actually tweak any
   1.426 + * actors or tables here.
   1.427 + *
   1.428 + * An nsIWindowMediatorListener's methods get passed all sorts of windows; we
   1.429 + * only care about the tab containers. Those have 'getBrowser' methods.
   1.430 + */
   1.431 +BrowserTabList.prototype.onWindowTitleChange = () => { };
   1.432 +
   1.433 +BrowserTabList.prototype.onOpenWindow = DevToolsUtils.makeInfallible(function(aWindow) {
   1.434 +  let handleLoad = DevToolsUtils.makeInfallible(() => {
   1.435 +    /* We don't want any further load events from this window. */
   1.436 +    aWindow.removeEventListener("load", handleLoad, false);
   1.437 +
   1.438 +    if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType)
   1.439 +      return;
   1.440 +
   1.441 +    // Listen for future tab activity.
   1.442 +    if (this._listeningForTabOpen) {
   1.443 +      aWindow.addEventListener("TabOpen", this, false);
   1.444 +      aWindow.addEventListener("TabSelect", this, false);
   1.445 +    }
   1.446 +    if (this._listeningForTabClose) {
   1.447 +      aWindow.addEventListener("TabClose", this, false);
   1.448 +    }
   1.449 +
   1.450 +    // As explained above, we will not receive a TabOpen event for this
   1.451 +    // document's initial tab, so we must notify our client of the new tab
   1.452 +    // this will have.
   1.453 +    this._notifyListChanged();
   1.454 +  });
   1.455 +
   1.456 +  /*
   1.457 +   * You can hardly do anything at all with a XUL window at this point; it
   1.458 +   * doesn't even have its document yet. Wait until its document has
   1.459 +   * loaded, and then see what we've got. This also avoids
   1.460 +   * nsIWindowMediator enumeration from within listeners (bug 873589).
   1.461 +   */
   1.462 +  aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.463 +                   .getInterface(Ci.nsIDOMWindow);
   1.464 +
   1.465 +  aWindow.addEventListener("load", handleLoad, false);
   1.466 +}, "BrowserTabList.prototype.onOpenWindow");
   1.467 +
   1.468 +BrowserTabList.prototype.onCloseWindow = DevToolsUtils.makeInfallible(function(aWindow) {
   1.469 +  aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
   1.470 +                   .getInterface(Ci.nsIDOMWindow);
   1.471 +
   1.472 +  if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType)
   1.473 +    return;
   1.474 +
   1.475 +  /*
   1.476 +   * nsIWindowMediator deadlocks if you call its GetEnumerator method from
   1.477 +   * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so
   1.478 +   * handle the close in a different tick.
   1.479 +   */
   1.480 +  Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
   1.481 +    /*
   1.482 +     * Scan the entire map for actors representing tabs that were in this
   1.483 +     * top-level window, and exit them.
   1.484 +     */
   1.485 +    for (let [browser, actor] of this._actorByBrowser) {
   1.486 +      /* The browser document of a closed window has no default view. */
   1.487 +      if (!browser.ownerDocument.defaultView) {
   1.488 +        this._handleActorClose(actor, browser);
   1.489 +      }
   1.490 +    }
   1.491 +  }, "BrowserTabList.prototype.onCloseWindow's delayed body"), 0);
   1.492 +}, "BrowserTabList.prototype.onCloseWindow");
   1.493 +
   1.494 +/**
   1.495 + * Creates a tab actor for handling requests to a browser tab, like
   1.496 + * attaching and detaching. TabActor respects the actor factories
   1.497 + * registered with DebuggerServer.addTabActor.
   1.498 + *
   1.499 + * This class is subclassed by BrowserTabActor and
   1.500 + * ContentActor. Subclasses are expected to implement a getter
   1.501 + * the docShell properties.
   1.502 + *
   1.503 + * @param aConnection DebuggerServerConnection
   1.504 + *        The conection to the client.
   1.505 + * @param aChromeEventHandler
   1.506 + *        An object on which listen for DOMWindowCreated and pageshow events.
   1.507 + */
   1.508 +function TabActor(aConnection)
   1.509 +{
   1.510 +  this.conn = aConnection;
   1.511 +  this._tabActorPool = null;
   1.512 +  // A map of actor names to actor instances provided by extensions.
   1.513 +  this._extraActors = {};
   1.514 +  this._exited = false;
   1.515 +
   1.516 +  this.traits = { reconfigure: true };
   1.517 +}
   1.518 +
   1.519 +// XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
   1.520 +// *complete* mess, needs to be rethought asap.
   1.521 +
   1.522 +TabActor.prototype = {
   1.523 +  traits: null,
   1.524 +
   1.525 +  get exited() { return this._exited; },
   1.526 +  get attached() { return !!this._attached; },
   1.527 +
   1.528 +  _tabPool: null,
   1.529 +  get tabActorPool() { return this._tabPool; },
   1.530 +
   1.531 +  _contextPool: null,
   1.532 +  get contextActorPool() { return this._contextPool; },
   1.533 +
   1.534 +  _pendingNavigation: null,
   1.535 +
   1.536 +  // A constant prefix that will be used to form the actor ID by the server.
   1.537 +  actorPrefix: "tab",
   1.538 +
   1.539 +  /**
   1.540 +   * An object on which listen for DOMWindowCreated and pageshow events.
   1.541 +   */
   1.542 +  get chromeEventHandler() {
   1.543 +    // TODO: bug 992778, fix docShell.chromeEventHandler in child processes
   1.544 +    return this.docShell.chromeEventHandler ||
   1.545 +           this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
   1.546 +                        .getInterface(Ci.nsIContentFrameMessageManager);
   1.547 +  },
   1.548 +
   1.549 +  /**
   1.550 +   * Getter for the nsIMessageManager associated to the tab.
   1.551 +   */
   1.552 +  get messageManager() {
   1.553 +    return this.docShell
   1.554 +      .QueryInterface(Ci.nsIInterfaceRequestor)
   1.555 +      .getInterface(Ci.nsIContentFrameMessageManager);
   1.556 +  },
   1.557 +
   1.558 +  /**
   1.559 +   * Getter for the tab's doc shell.
   1.560 +   */
   1.561 +  get docShell() {
   1.562 +    throw "The docShell getter should be implemented by a subclass of TabActor";
   1.563 +  },
   1.564 +
   1.565 +  /**
   1.566 +   * Getter for the tab content's DOM window.
   1.567 +   */
   1.568 +  get window() {
   1.569 +    return this.docShell
   1.570 +      .QueryInterface(Ci.nsIInterfaceRequestor)
   1.571 +      .getInterface(Ci.nsIDOMWindow);
   1.572 +  },
   1.573 +
   1.574 +  /**
   1.575 +   * Getter for the nsIWebProgress for watching this window.
   1.576 +   */
   1.577 +  get webProgress() {
   1.578 +    return this.docShell
   1.579 +      .QueryInterface(Ci.nsIInterfaceRequestor)
   1.580 +      .getInterface(Ci.nsIWebProgress);
   1.581 +  },
   1.582 +
   1.583 +  /**
   1.584 +   * Getter for the nsIWebNavigation for the tab.
   1.585 +   */
   1.586 +  get webNavigation() {
   1.587 +    return this.docShell
   1.588 +      .QueryInterface(Ci.nsIInterfaceRequestor)
   1.589 +      .getInterface(Ci.nsIWebNavigation);
   1.590 +  },
   1.591 +
   1.592 +  /**
   1.593 +   * Getter for the tab's document.
   1.594 +   */
   1.595 +  get contentDocument() {
   1.596 +    return this.webNavigation.document;
   1.597 +  },
   1.598 +
   1.599 +  /**
   1.600 +   * Getter for the tab title.
   1.601 +   * @return string
   1.602 +   *         Tab title.
   1.603 +   */
   1.604 +  get title() {
   1.605 +    return this.contentDocument.contentTitle;
   1.606 +  },
   1.607 +
   1.608 +  /**
   1.609 +   * Getter for the tab URL.
   1.610 +   * @return string
   1.611 +   *         Tab URL.
   1.612 +   */
   1.613 +  get url() {
   1.614 +    if (this.webNavigation.currentURI) {
   1.615 +      return this.webNavigation.currentURI.spec;
   1.616 +    }
   1.617 +    // Abrupt closing of the browser window may leave callbacks without a
   1.618 +    // currentURI.
   1.619 +    return null;
   1.620 +  },
   1.621 +
   1.622 +  form: function BTA_form() {
   1.623 +    dbg_assert(!this.exited,
   1.624 +               "grip() shouldn't be called on exited browser actor.");
   1.625 +    dbg_assert(this.actorID,
   1.626 +               "tab should have an actorID.");
   1.627 +
   1.628 +    let windowUtils = this.window
   1.629 +      .QueryInterface(Ci.nsIInterfaceRequestor)
   1.630 +      .getInterface(Ci.nsIDOMWindowUtils);
   1.631 +
   1.632 +    let response = {
   1.633 +      actor: this.actorID,
   1.634 +      title: this.title,
   1.635 +      url: this.url,
   1.636 +      outerWindowID: windowUtils.outerWindowID
   1.637 +    };
   1.638 +
   1.639 +    // Walk over tab actors added by extensions and add them to a new ActorPool.
   1.640 +    let actorPool = new ActorPool(this.conn);
   1.641 +    this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
   1.642 +    if (!actorPool.isEmpty()) {
   1.643 +      this._tabActorPool = actorPool;
   1.644 +      this.conn.addActorPool(this._tabActorPool);
   1.645 +    }
   1.646 +
   1.647 +    this._appendExtraActors(response);
   1.648 +    return response;
   1.649 +  },
   1.650 +
   1.651 +  /**
   1.652 +   * Called when the actor is removed from the connection.
   1.653 +   */
   1.654 +  disconnect: function BTA_disconnect() {
   1.655 +    this._detach();
   1.656 +    this._extraActors = null;
   1.657 +    this._exited = true;
   1.658 +  },
   1.659 +
   1.660 +  /**
   1.661 +   * Called by the root actor when the underlying tab is closed.
   1.662 +   */
   1.663 +  exit: function BTA_exit() {
   1.664 +    if (this.exited) {
   1.665 +      return;
   1.666 +    }
   1.667 +
   1.668 +    if (this._detach()) {
   1.669 +      this.conn.send({ from: this.actorID,
   1.670 +                       type: "tabDetached" });
   1.671 +    }
   1.672 +
   1.673 +    this._exited = true;
   1.674 +  },
   1.675 +
   1.676 +  /* Support for DebuggerServer.addTabActor. */
   1.677 +  _createExtraActors: createExtraActors,
   1.678 +  _appendExtraActors: appendExtraActors,
   1.679 +
   1.680 +  /**
   1.681 +   * Does the actual work of attching to a tab.
   1.682 +   */
   1.683 +  _attach: function BTA_attach() {
   1.684 +    if (this._attached) {
   1.685 +      return;
   1.686 +    }
   1.687 +
   1.688 +    // Create a pool for tab-lifetime actors.
   1.689 +    dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
   1.690 +    this._tabPool = new ActorPool(this.conn);
   1.691 +    this.conn.addActorPool(this._tabPool);
   1.692 +
   1.693 +    // ... and a pool for context-lifetime actors.
   1.694 +    this._pushContext();
   1.695 +
   1.696 +    this._progressListener = new DebuggerProgressListener(this);
   1.697 +    this._progressListener.watch(this.docShell);
   1.698 +
   1.699 +    this._attached = true;
   1.700 +  },
   1.701 +
   1.702 +  /**
   1.703 +   * Creates a thread actor and a pool for context-lifetime actors. It then sets
   1.704 +   * up the content window for debugging.
   1.705 +   */
   1.706 +  _pushContext: function BTA_pushContext() {
   1.707 +    dbg_assert(!this._contextPool, "Can't push multiple contexts");
   1.708 +
   1.709 +    this._contextPool = new ActorPool(this.conn);
   1.710 +    this.conn.addActorPool(this._contextPool);
   1.711 +
   1.712 +    this.threadActor = new ThreadActor(this, this.window);
   1.713 +    this._contextPool.addActor(this.threadActor);
   1.714 +  },
   1.715 +
   1.716 +  /**
   1.717 +   * Exits the current thread actor and removes the context-lifetime actor pool.
   1.718 +   * The content window is no longer being debugged after this call.
   1.719 +   */
   1.720 +  _popContext: function BTA_popContext() {
   1.721 +    dbg_assert(!!this._contextPool, "No context to pop.");
   1.722 +
   1.723 +    this.conn.removeActorPool(this._contextPool);
   1.724 +    this._contextPool = null;
   1.725 +    this.threadActor.exit();
   1.726 +    this.threadActor = null;
   1.727 +  },
   1.728 +
   1.729 +  /**
   1.730 +   * Does the actual work of detaching from a tab.
   1.731 +   *
   1.732 +   * @returns false if the tab wasn't attached or true of detahing succeeds.
   1.733 +   */
   1.734 +  _detach: function BTA_detach() {
   1.735 +    if (!this.attached) {
   1.736 +      return false;
   1.737 +    }
   1.738 +
   1.739 +    // Check for docShell availability, as it can be already gone
   1.740 +    // during Firefox shutdown.
   1.741 +    if (this.docShell) {
   1.742 +      this._progressListener.unwatch(this.docShell);
   1.743 +    }
   1.744 +    this._progressListener = null;
   1.745 +
   1.746 +    this._popContext();
   1.747 +
   1.748 +    // Shut down actors that belong to this tab's pool.
   1.749 +    this.conn.removeActorPool(this._tabPool);
   1.750 +    this._tabPool = null;
   1.751 +    if (this._tabActorPool) {
   1.752 +      this.conn.removeActorPool(this._tabActorPool);
   1.753 +      this._tabActorPool = null;
   1.754 +    }
   1.755 +
   1.756 +    this._attached = false;
   1.757 +    return true;
   1.758 +  },
   1.759 +
   1.760 +  // Protocol Request Handlers
   1.761 +
   1.762 +  onAttach: function BTA_onAttach(aRequest) {
   1.763 +    if (this.exited) {
   1.764 +      return { type: "exited" };
   1.765 +    }
   1.766 +
   1.767 +    this._attach();
   1.768 +
   1.769 +    return {
   1.770 +      type: "tabAttached",
   1.771 +      threadActor: this.threadActor.actorID,
   1.772 +      cacheEnabled: this._getCacheEnabled(),
   1.773 +      javascriptEnabled: this._getJavascriptEnabled(),
   1.774 +      traits: this.traits,
   1.775 +    };
   1.776 +  },
   1.777 +
   1.778 +  onDetach: function BTA_onDetach(aRequest) {
   1.779 +    if (!this._detach()) {
   1.780 +      return { error: "wrongState" };
   1.781 +    }
   1.782 +
   1.783 +    return { type: "detached" };
   1.784 +  },
   1.785 +
   1.786 +  /**
   1.787 +   * Reload the page in this tab.
   1.788 +   */
   1.789 +  onReload: function(aRequest) {
   1.790 +    // Wait a tick so that the response packet can be dispatched before the
   1.791 +    // subsequent navigation event packet.
   1.792 +    Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
   1.793 +      this.window.location.reload();
   1.794 +    }, "TabActor.prototype.onReload's delayed body"), 0);
   1.795 +    return {};
   1.796 +  },
   1.797 +
   1.798 +  /**
   1.799 +   * Navigate this tab to a new location
   1.800 +   */
   1.801 +  onNavigateTo: function(aRequest) {
   1.802 +    // Wait a tick so that the response packet can be dispatched before the
   1.803 +    // subsequent navigation event packet.
   1.804 +    Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
   1.805 +      this.window.location = aRequest.url;
   1.806 +    }, "TabActor.prototype.onNavigateTo's delayed body"), 0);
   1.807 +    return {};
   1.808 +  },
   1.809 +
   1.810 +  /**
   1.811 +   * Reconfigure options.
   1.812 +   */
   1.813 +  onReconfigure: function (aRequest) {
   1.814 +    let options = aRequest.options || {};
   1.815 +
   1.816 +    this._toggleJsOrCache(options);
   1.817 +    return {};
   1.818 +  },
   1.819 +
   1.820 +  /**
   1.821 +   * Handle logic to enable/disable JS/cache.
   1.822 +   */
   1.823 +  _toggleJsOrCache: function(options) {
   1.824 +    // Wait a tick so that the response packet can be dispatched before the
   1.825 +    // subsequent navigation event packet.
   1.826 +    let reload = false;
   1.827 +
   1.828 +    if (typeof options.javascriptEnabled !== "undefined" &&
   1.829 +        options.javascriptEnabled !== this._getJavascriptEnabled()) {
   1.830 +      this._setJavascriptEnabled(options.javascriptEnabled);
   1.831 +      reload = true;
   1.832 +    }
   1.833 +    if (typeof options.cacheEnabled !== "undefined" &&
   1.834 +        options.cacheEnabled !== this._getCacheEnabled()) {
   1.835 +      this._setCacheEnabled(options.cacheEnabled);
   1.836 +      reload = true;
   1.837 +    }
   1.838 +
   1.839 +    // Reload if:
   1.840 +    //  - there's an explicit `performReload` flag and it's true
   1.841 +    //  - there's no `performReload` flag, but it makes sense to do so
   1.842 +    let hasExplicitReloadFlag = "performReload" in options;
   1.843 +    if ((hasExplicitReloadFlag && options.performReload) ||
   1.844 +       (!hasExplicitReloadFlag && reload)) {
   1.845 +      this.onReload();
   1.846 +    }
   1.847 +  },
   1.848 +
   1.849 +  /**
   1.850 +   * Disable or enable the cache via docShell.
   1.851 +   */
   1.852 +  _setCacheEnabled: function(allow) {
   1.853 +    let enable =  Ci.nsIRequest.LOAD_NORMAL;
   1.854 +    let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
   1.855 +                  Ci.nsIRequest.INHIBIT_CACHING;
   1.856 +    if (this.docShell) {
   1.857 +      this.docShell.defaultLoadFlags = allow ? enable : disable;
   1.858 +    }
   1.859 +  },
   1.860 +
   1.861 +  /**
   1.862 +   * Disable or enable JS via docShell.
   1.863 +   */
   1.864 +  _setJavascriptEnabled: function(allow) {
   1.865 +    if (this.docShell) {
   1.866 +      this.docShell.allowJavascript = allow;
   1.867 +    }
   1.868 +  },
   1.869 +
   1.870 +  /**
   1.871 +   * Return cache allowed status.
   1.872 +   */
   1.873 +  _getCacheEnabled: function() {
   1.874 +    if (!this.docShell) {
   1.875 +      // The tab is already closed.
   1.876 +      return null;
   1.877 +    }
   1.878 +
   1.879 +    let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
   1.880 +                  Ci.nsIRequest.INHIBIT_CACHING;
   1.881 +    return this.docShell.defaultLoadFlags !== disable;
   1.882 +  },
   1.883 +
   1.884 +  /**
   1.885 +   * Return JS allowed status.
   1.886 +   */
   1.887 +  _getJavascriptEnabled: function() {
   1.888 +    if (!this.docShell) {
   1.889 +      // The tab is already closed.
   1.890 +      return null;
   1.891 +    }
   1.892 +
   1.893 +    return this.docShell.allowJavascript;
   1.894 +  },
   1.895 +
   1.896 +  /**
   1.897 +   * Prepare to enter a nested event loop by disabling debuggee events.
   1.898 +   */
   1.899 +  preNest: function BTA_preNest() {
   1.900 +    if (!this.window) {
   1.901 +      // The tab is already closed.
   1.902 +      return;
   1.903 +    }
   1.904 +    let windowUtils = this.window
   1.905 +                          .QueryInterface(Ci.nsIInterfaceRequestor)
   1.906 +                          .getInterface(Ci.nsIDOMWindowUtils);
   1.907 +    windowUtils.suppressEventHandling(true);
   1.908 +    windowUtils.suspendTimeouts();
   1.909 +  },
   1.910 +
   1.911 +  /**
   1.912 +   * Prepare to exit a nested event loop by enabling debuggee events.
   1.913 +   */
   1.914 +  postNest: function BTA_postNest(aNestData) {
   1.915 +    if (!this.window) {
   1.916 +      // The tab is already closed.
   1.917 +      return;
   1.918 +    }
   1.919 +    let windowUtils = this.window
   1.920 +                          .QueryInterface(Ci.nsIInterfaceRequestor)
   1.921 +                          .getInterface(Ci.nsIDOMWindowUtils);
   1.922 +    windowUtils.resumeTimeouts();
   1.923 +    windowUtils.suppressEventHandling(false);
   1.924 +    if (this._pendingNavigation) {
   1.925 +      this._pendingNavigation.resume();
   1.926 +      this._pendingNavigation = null;
   1.927 +    }
   1.928 +  },
   1.929 +
   1.930 +  /**
   1.931 +   * Handle location changes, by clearing the previous debuggees and enabling
   1.932 +   * debugging, which may have been disabled temporarily by the
   1.933 +   * DebuggerProgressListener.
   1.934 +   */
   1.935 +  _windowReady: function (window) {
   1.936 +    let isTopLevel = window == this.window;
   1.937 +    dumpn("window-ready: " + window.location + " isTopLevel:" + isTopLevel);
   1.938 +
   1.939 +    events.emit(this, "window-ready", {
   1.940 +      window: window,
   1.941 +      isTopLevel: isTopLevel
   1.942 +    });
   1.943 +
   1.944 +    // TODO bug 997119: move that code to ThreadActor by listening to window-ready
   1.945 +    let threadActor = this.threadActor;
   1.946 +    if (isTopLevel) {
   1.947 +      threadActor.clearDebuggees();
   1.948 +      if (threadActor.dbg) {
   1.949 +        threadActor.dbg.enabled = true;
   1.950 +        threadActor.global = window;
   1.951 +        threadActor.maybePauseOnExceptions();
   1.952 +      }
   1.953 +    }
   1.954 +
   1.955 +    // Refresh the debuggee list when a new window object appears (top window or
   1.956 +    // iframe).
   1.957 +    if (threadActor.attached) {
   1.958 +      threadActor.findGlobals();
   1.959 +    }
   1.960 +  },
   1.961 +
   1.962 +  /**
   1.963 +   * Start notifying server codebase and client about a new document
   1.964 +   * being loaded in the currently targeted context.
   1.965 +   */
   1.966 +  _willNavigate: function (window, newURI, request) {
   1.967 +    let isTopLevel = window == this.window;
   1.968 +
   1.969 +    // will-navigate event needs to be dispatched synchronously,
   1.970 +    // by calling the listeners in the order or registration.
   1.971 +    // This event fires once navigation starts,
   1.972 +    // (all pending user prompts are dealt with),
   1.973 +    // but before the first request starts.
   1.974 +    events.emit(this, "will-navigate", {
   1.975 +      window: window,
   1.976 +      isTopLevel: isTopLevel,
   1.977 +      newURI: newURI,
   1.978 +      request: request
   1.979 +    });
   1.980 +
   1.981 +
   1.982 +    // We don't do anything for inner frames in TabActor.
   1.983 +    // (we will only update thread actor on window-ready)
   1.984 +    if (!isTopLevel) {
   1.985 +      return;
   1.986 +    }
   1.987 +
   1.988 +    // Proceed normally only if the debuggee is not paused.
   1.989 +    // TODO bug 997119: move that code to ThreadActor by listening to will-navigate
   1.990 +    let threadActor = this.threadActor;
   1.991 +    if (request && threadActor.state == "paused") {
   1.992 +      request.suspend();
   1.993 +      threadActor.onResume();
   1.994 +      threadActor.dbg.enabled = false;
   1.995 +      this._pendingNavigation = request;
   1.996 +    }
   1.997 +    threadActor.disableAllBreakpoints();
   1.998 +
   1.999 +    this.conn.send({
  1.1000 +      from: this.actorID,
  1.1001 +      type: "tabNavigated",
  1.1002 +      url: newURI,
  1.1003 +      nativeConsoleAPI: true,
  1.1004 +      state: "start"
  1.1005 +    });
  1.1006 +  },
  1.1007 +
  1.1008 +  /**
  1.1009 +   * Notify server and client about a new document done loading in the current
  1.1010 +   * targeted context.
  1.1011 +   */
  1.1012 +  _navigate: function (window) {
  1.1013 +    let isTopLevel = window == this.window;
  1.1014 +
  1.1015 +    // navigate event needs to be dispatched synchronously,
  1.1016 +    // by calling the listeners in the order or registration.
  1.1017 +    // This event is fired once the document is loaded,
  1.1018 +    // after the load event, it's document ready-state is 'complete'.
  1.1019 +    events.emit(this, "navigate", {
  1.1020 +      window: window,
  1.1021 +      isTopLevel: isTopLevel
  1.1022 +    });
  1.1023 +
  1.1024 +    // We don't do anything for inner frames in TabActor.
  1.1025 +    // (we will only update thread actor on window-ready)
  1.1026 +    if (!isTopLevel) {
  1.1027 +      return;
  1.1028 +    }
  1.1029 +
  1.1030 +    // TODO bug 997119: move that code to ThreadActor by listening to navigate
  1.1031 +    let threadActor = this.threadActor;
  1.1032 +    if (threadActor.state == "running") {
  1.1033 +      threadActor.dbg.enabled = true;
  1.1034 +    }
  1.1035 +
  1.1036 +    this.conn.send({
  1.1037 +      from: this.actorID,
  1.1038 +      type: "tabNavigated",
  1.1039 +      url: this.url,
  1.1040 +      title: this.title,
  1.1041 +      nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
  1.1042 +      state: "stop"
  1.1043 +    });
  1.1044 +  },
  1.1045 +
  1.1046 +  /**
  1.1047 +   * Tells if the window.console object is native or overwritten by script in
  1.1048 +   * the page.
  1.1049 +   *
  1.1050 +   * @param nsIDOMWindow aWindow
  1.1051 +   *        The window object you want to check.
  1.1052 +   * @return boolean
  1.1053 +   *         True if the window.console object is native, or false otherwise.
  1.1054 +   */
  1.1055 +  hasNativeConsoleAPI: function BTA_hasNativeConsoleAPI(aWindow) {
  1.1056 +    let isNative = false;
  1.1057 +    try {
  1.1058 +      // We are very explicitly examining the "console" property of
  1.1059 +      // the non-Xrayed object here.
  1.1060 +      let console = aWindow.wrappedJSObject.console;
  1.1061 +      isNative = console instanceof aWindow.Console;
  1.1062 +    }
  1.1063 +    catch (ex) { }
  1.1064 +    return isNative;
  1.1065 +  }
  1.1066 +};
  1.1067 +
  1.1068 +/**
  1.1069 + * The request types this actor can handle.
  1.1070 + */
  1.1071 +TabActor.prototype.requestTypes = {
  1.1072 +  "attach": TabActor.prototype.onAttach,
  1.1073 +  "detach": TabActor.prototype.onDetach,
  1.1074 +  "reload": TabActor.prototype.onReload,
  1.1075 +  "navigateTo": TabActor.prototype.onNavigateTo,
  1.1076 +  "reconfigure": TabActor.prototype.onReconfigure
  1.1077 +};
  1.1078 +
  1.1079 +/**
  1.1080 + * Creates a tab actor for handling requests to a single in-process
  1.1081 + * <browser> tab. Most of the implementation comes from TabActor.
  1.1082 + *
  1.1083 + * @param aConnection DebuggerServerConnection
  1.1084 + *        The conection to the client.
  1.1085 + * @param aBrowser browser
  1.1086 + *        The browser instance that contains this tab.
  1.1087 + * @param aTabBrowser tabbrowser
  1.1088 + *        The tabbrowser that can receive nsIWebProgressListener events.
  1.1089 + */
  1.1090 +function BrowserTabActor(aConnection, aBrowser, aTabBrowser)
  1.1091 +{
  1.1092 +  TabActor.call(this, aConnection, aBrowser);
  1.1093 +  this._browser = aBrowser;
  1.1094 +  this._tabbrowser = aTabBrowser;
  1.1095 +}
  1.1096 +
  1.1097 +BrowserTabActor.prototype = Object.create(TabActor.prototype);
  1.1098 +
  1.1099 +BrowserTabActor.prototype.constructor = BrowserTabActor;
  1.1100 +
  1.1101 +Object.defineProperty(BrowserTabActor.prototype, "docShell", {
  1.1102 +  get: function() {
  1.1103 +    return this._browser.docShell;
  1.1104 +  },
  1.1105 +  enumerable: true,
  1.1106 +  configurable: false
  1.1107 +});
  1.1108 +
  1.1109 +Object.defineProperty(BrowserTabActor.prototype, "title", {
  1.1110 +  get: function() {
  1.1111 +    let title = this.contentDocument.title || this._browser.contentTitle;
  1.1112 +    // If contentTitle is empty (e.g. on a not-yet-restored tab), but there is a
  1.1113 +    // tabbrowser (i.e. desktop Firefox, but not Fennec), we can use the label
  1.1114 +    // as the title.
  1.1115 +    if (!title && this._tabbrowser) {
  1.1116 +      let tab = this._tabbrowser._getTabForContentWindow(this.window);
  1.1117 +      if (tab) {
  1.1118 +        title = tab.label;
  1.1119 +      }
  1.1120 +    }
  1.1121 +    return title;
  1.1122 +  },
  1.1123 +  enumerable: true,
  1.1124 +  configurable: false
  1.1125 +});
  1.1126 +
  1.1127 +Object.defineProperty(BrowserTabActor.prototype, "browser", {
  1.1128 +  get: function() {
  1.1129 +    return this._browser;
  1.1130 +  },
  1.1131 +  enumerable: true,
  1.1132 +  configurable: false
  1.1133 +});
  1.1134 +
  1.1135 +BrowserTabActor.prototype.disconnect = function() {
  1.1136 +  TabActor.prototype.disconnect.call(this);
  1.1137 +  this._browser = null;
  1.1138 +  this._tabbrowser = null;
  1.1139 +};
  1.1140 +
  1.1141 +BrowserTabActor.prototype.exit = function() {
  1.1142 +  TabActor.prototype.exit.call(this);
  1.1143 +  this._browser = null;
  1.1144 +  this._tabbrowser = null;
  1.1145 +};
  1.1146 +
  1.1147 +/**
  1.1148 + * This actor is a shim that connects to a ContentActor in a remote
  1.1149 + * browser process. All RDP packets get forwarded using the message
  1.1150 + * manager.
  1.1151 + *
  1.1152 + * @param aConnection The main RDP connection.
  1.1153 + * @param aBrowser XUL <browser> element to connect to.
  1.1154 + */
  1.1155 +function RemoteBrowserTabActor(aConnection, aBrowser)
  1.1156 +{
  1.1157 +  this._conn = aConnection;
  1.1158 +  this._browser = aBrowser;
  1.1159 +  this._form = null;
  1.1160 +}
  1.1161 +
  1.1162 +RemoteBrowserTabActor.prototype = {
  1.1163 +  connect: function() {
  1.1164 +    return DebuggerServer.connectToChild(this._conn, this._browser);
  1.1165 +  },
  1.1166 +
  1.1167 +  form: function() {
  1.1168 +    return this._form;
  1.1169 +  },
  1.1170 +
  1.1171 +  exit: function() {
  1.1172 +    this._browser = null;
  1.1173 +  },
  1.1174 +};
  1.1175 +
  1.1176 +function BrowserAddonList(aConnection)
  1.1177 +{
  1.1178 +  this._connection = aConnection;
  1.1179 +  this._actorByAddonId = new Map();
  1.1180 +  this._onListChanged = null;
  1.1181 +}
  1.1182 +
  1.1183 +BrowserAddonList.prototype.getList = function() {
  1.1184 +  var deferred = promise.defer();
  1.1185 +  AddonManager.getAllAddons((addons) => {
  1.1186 +    for (let addon of addons) {
  1.1187 +      let actor = this._actorByAddonId.get(addon.id);
  1.1188 +      if (!actor) {
  1.1189 +        actor = new BrowserAddonActor(this._connection, addon);
  1.1190 +        this._actorByAddonId.set(addon.id, actor);
  1.1191 +      }
  1.1192 +    }
  1.1193 +    deferred.resolve([actor for ([_, actor] of this._actorByAddonId)]);
  1.1194 +  });
  1.1195 +  return deferred.promise;
  1.1196 +}
  1.1197 +
  1.1198 +Object.defineProperty(BrowserAddonList.prototype, "onListChanged", {
  1.1199 +  enumerable: true, configurable: true,
  1.1200 +  get: function() { return this._onListChanged; },
  1.1201 +  set: function(v) {
  1.1202 +    if (v !== null && typeof v != "function") {
  1.1203 +      throw Error("onListChanged property may only be set to 'null' or a function");
  1.1204 +    }
  1.1205 +    this._onListChanged = v;
  1.1206 +    if (this._onListChanged) {
  1.1207 +      AddonManager.addAddonListener(this);
  1.1208 +    } else {
  1.1209 +      AddonManager.removeAddonListener(this);
  1.1210 +    }
  1.1211 +  }
  1.1212 +});
  1.1213 +
  1.1214 +BrowserAddonList.prototype.onInstalled = function (aAddon) {
  1.1215 +  this._onListChanged();
  1.1216 +};
  1.1217 +
  1.1218 +BrowserAddonList.prototype.onUninstalled = function (aAddon) {
  1.1219 +  this._actorByAddonId.delete(aAddon.id);
  1.1220 +  this._onListChanged();
  1.1221 +};
  1.1222 +
  1.1223 +function BrowserAddonActor(aConnection, aAddon) {
  1.1224 +  this.conn = aConnection;
  1.1225 +  this._addon = aAddon;
  1.1226 +  this._contextPool = null;
  1.1227 +  this._threadActor = null;
  1.1228 +  this._global = null;
  1.1229 +  AddonManager.addAddonListener(this);
  1.1230 +}
  1.1231 +
  1.1232 +BrowserAddonActor.prototype = {
  1.1233 +  actorPrefix: "addon",
  1.1234 +
  1.1235 +  get exited() {
  1.1236 +    return !this._addon;
  1.1237 +  },
  1.1238 +
  1.1239 +  get id() {
  1.1240 +    return this._addon.id;
  1.1241 +  },
  1.1242 +
  1.1243 +  get url() {
  1.1244 +    return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined;
  1.1245 +  },
  1.1246 +
  1.1247 +  get attached() {
  1.1248 +    return this._threadActor;
  1.1249 +  },
  1.1250 +
  1.1251 +  get global() {
  1.1252 +    return this._global;
  1.1253 +  },
  1.1254 +
  1.1255 +  form: function BAA_form() {
  1.1256 +    dbg_assert(this.actorID, "addon should have an actorID.");
  1.1257 +
  1.1258 +    return {
  1.1259 +      actor: this.actorID,
  1.1260 +      id: this.id,
  1.1261 +      name: this._addon.name,
  1.1262 +      url: this.url,
  1.1263 +      debuggable: this._addon.isDebuggable,
  1.1264 +    };
  1.1265 +  },
  1.1266 +
  1.1267 +  disconnect: function BAA_disconnect() {
  1.1268 +    this._addon = null;
  1.1269 +    this._global = null;
  1.1270 +    AddonManager.removeAddonListener(this);
  1.1271 +  },
  1.1272 +
  1.1273 +  setOptions: function BAA_setOptions(aOptions) {
  1.1274 +    if ("global" in aOptions) {
  1.1275 +      this._global = aOptions.global;
  1.1276 +    }
  1.1277 +  },
  1.1278 +
  1.1279 +  onDisabled: function BAA_onDisabled(aAddon) {
  1.1280 +    if (aAddon != this._addon) {
  1.1281 +      return;
  1.1282 +    }
  1.1283 +
  1.1284 +    this._global = null;
  1.1285 +  },
  1.1286 +
  1.1287 +  onUninstalled: function BAA_onUninstalled(aAddon) {
  1.1288 +    if (aAddon != this._addon) {
  1.1289 +      return;
  1.1290 +    }
  1.1291 +
  1.1292 +    if (this.attached) {
  1.1293 +      this.onDetach();
  1.1294 +      this.conn.send({ from: this.actorID, type: "tabDetached" });
  1.1295 +    }
  1.1296 +
  1.1297 +    this.disconnect();
  1.1298 +  },
  1.1299 +
  1.1300 +  onAttach: function BAA_onAttach() {
  1.1301 +    if (this.exited) {
  1.1302 +      return { type: "exited" };
  1.1303 +    }
  1.1304 +
  1.1305 +    if (!this.attached) {
  1.1306 +      this._contextPool = new ActorPool(this.conn);
  1.1307 +      this.conn.addActorPool(this._contextPool);
  1.1308 +
  1.1309 +      this._threadActor = new AddonThreadActor(this.conn, this,
  1.1310 +                                               this._addon.id);
  1.1311 +      this._contextPool.addActor(this._threadActor);
  1.1312 +    }
  1.1313 +
  1.1314 +    return { type: "tabAttached", threadActor: this._threadActor.actorID };
  1.1315 +  },
  1.1316 +
  1.1317 +  onDetach: function BAA_onDetach() {
  1.1318 +    if (!this.attached) {
  1.1319 +      return { error: "wrongState" };
  1.1320 +    }
  1.1321 +
  1.1322 +    this.conn.removeActorPool(this._contextPool);
  1.1323 +    this._contextPool = null;
  1.1324 +
  1.1325 +    this._threadActor = null;
  1.1326 +
  1.1327 +    return { type: "detached" };
  1.1328 +  },
  1.1329 +
  1.1330 +  preNest: function() {
  1.1331 +    let e = Services.wm.getEnumerator(null);
  1.1332 +    while (e.hasMoreElements()) {
  1.1333 +      let win = e.getNext();
  1.1334 +      let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1335 +                           .getInterface(Ci.nsIDOMWindowUtils);
  1.1336 +      windowUtils.suppressEventHandling(true);
  1.1337 +      windowUtils.suspendTimeouts();
  1.1338 +    }
  1.1339 +  },
  1.1340 +
  1.1341 +  postNest: function() {
  1.1342 +    let e = Services.wm.getEnumerator(null);
  1.1343 +    while (e.hasMoreElements()) {
  1.1344 +      let win = e.getNext();
  1.1345 +      let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1346 +                           .getInterface(Ci.nsIDOMWindowUtils);
  1.1347 +      windowUtils.resumeTimeouts();
  1.1348 +      windowUtils.suppressEventHandling(false);
  1.1349 +    }
  1.1350 +  }
  1.1351 +};
  1.1352 +
  1.1353 +BrowserAddonActor.prototype.requestTypes = {
  1.1354 +  "attach": BrowserAddonActor.prototype.onAttach,
  1.1355 +  "detach": BrowserAddonActor.prototype.onDetach
  1.1356 +};
  1.1357 +
  1.1358 +/**
  1.1359 + * The DebuggerProgressListener object is an nsIWebProgressListener which
  1.1360 + * handles onStateChange events for the inspected browser. If the user tries to
  1.1361 + * navigate away from a paused page, the listener makes sure that the debuggee
  1.1362 + * is resumed before the navigation begins.
  1.1363 + *
  1.1364 + * @param TabActor aTabActor
  1.1365 + *        The tab actor associated with this listener.
  1.1366 + */
  1.1367 +function DebuggerProgressListener(aTabActor) {
  1.1368 +  this._tabActor = aTabActor;
  1.1369 +  this._onWindowCreated = this.onWindowCreated.bind(this);
  1.1370 +}
  1.1371 +
  1.1372 +DebuggerProgressListener.prototype = {
  1.1373 +  QueryInterface: XPCOMUtils.generateQI([
  1.1374 +    Ci.nsIWebProgressListener,
  1.1375 +    Ci.nsISupportsWeakReference,
  1.1376 +    Ci.nsISupports,
  1.1377 +  ]),
  1.1378 +
  1.1379 +  watch: function DPL_watch(docShell) {
  1.1380 +    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1381 +                              .getInterface(Ci.nsIWebProgress);
  1.1382 +    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATUS |
  1.1383 +                                          Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
  1.1384 +                                          Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
  1.1385 +
  1.1386 +    // TODO: fix docShell.chromeEventHandler in child processes!
  1.1387 +    let chromeEventHandler = docShell.chromeEventHandler ||
  1.1388 +                             docShell.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1389 +                                     .getInterface(Ci.nsIContentFrameMessageManager);
  1.1390 +
  1.1391 +    // Watch for globals being created in this docshell tree.
  1.1392 +    chromeEventHandler.addEventListener("DOMWindowCreated",
  1.1393 +                                        this._onWindowCreated, true);
  1.1394 +    chromeEventHandler.addEventListener("pageshow",
  1.1395 +                                        this._onWindowCreated, true);
  1.1396 +  },
  1.1397 +
  1.1398 +  unwatch: function DPL_unwatch(docShell) {
  1.1399 +    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1400 +                              .getInterface(Ci.nsIWebProgress);
  1.1401 +    webProgress.removeProgressListener(this);
  1.1402 +
  1.1403 +    // TODO: fix docShell.chromeEventHandler in child processes!
  1.1404 +    let chromeEventHandler = docShell.chromeEventHandler ||
  1.1405 +                             docShell.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1406 +                                     .getInterface(Ci.nsIContentFrameMessageManager);
  1.1407 +    chromeEventHandler.removeEventListener("DOMWindowCreated",
  1.1408 +                                           this._onWindowCreated, true);
  1.1409 +    chromeEventHandler.removeEventListener("pageshow",
  1.1410 +                                           this._onWindowCreated, true);
  1.1411 +  },
  1.1412 +
  1.1413 +  onWindowCreated:
  1.1414 +  DevToolsUtils.makeInfallible(function DPL_onWindowCreated(evt) {
  1.1415 +    // Ignore any event if the tab actor isn't attached.
  1.1416 +    if (!this._tabActor.attached) {
  1.1417 +      return;
  1.1418 +    }
  1.1419 +
  1.1420 +    // pageshow events for non-persisted pages have already been handled by a
  1.1421 +    // prior DOMWindowCreated event.
  1.1422 +    if (evt.type == "pageshow" && !evt.persisted) {
  1.1423 +      return;
  1.1424 +    }
  1.1425 +
  1.1426 +    let window = evt.target.defaultView;
  1.1427 +    this._tabActor._windowReady(window);
  1.1428 +  }, "DebuggerProgressListener.prototype.onWindowCreated"),
  1.1429 +
  1.1430 +  onStateChange:
  1.1431 +  DevToolsUtils.makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) {
  1.1432 +    // Ignore any event if the tab actor isn't attached.
  1.1433 +    if (!this._tabActor.attached) {
  1.1434 +      return;
  1.1435 +    }
  1.1436 +
  1.1437 +    let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START;
  1.1438 +    let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP;
  1.1439 +    let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
  1.1440 +    let isWindow = aFlag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
  1.1441 +
  1.1442 +    let window = aProgress.DOMWindow;
  1.1443 +    if (isDocument && isStart) {
  1.1444 +      let newURI = aRequest instanceof Ci.nsIChannel ? aRequest.URI.spec : null;
  1.1445 +      this._tabActor._willNavigate(window, newURI, aRequest);
  1.1446 +    }
  1.1447 +    if (isWindow && isStop) {
  1.1448 +      this._tabActor._navigate(window);
  1.1449 +    }
  1.1450 +  }, "DebuggerProgressListener.prototype.onStateChange")
  1.1451 +};

mercurial