toolkit/devtools/server/actors/webbrowser.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 "use strict";
michael@0 8
michael@0 9 let {Ci,Cu} = require("chrome");
michael@0 10 let {createExtraActors, appendExtraActors} = require("devtools/server/actors/common");
michael@0 11 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
michael@0 12
michael@0 13 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 14 Cu.import("resource://gre/modules/Services.jsm");
michael@0 15 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 16
michael@0 17 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
michael@0 18
michael@0 19 // Assumptions on events module:
michael@0 20 // events needs to be dispatched synchronously,
michael@0 21 // by calling the listeners in the order or registration.
michael@0 22 XPCOMUtils.defineLazyGetter(this, "events", () => {
michael@0 23 return devtools.require("sdk/event/core");
michael@0 24 });
michael@0 25
michael@0 26 // Also depends on following symbols, shared by common scope with main.js:
michael@0 27 // DebuggerServer, CommonCreateExtraActors, CommonAppendExtraActors, ActorPool,
michael@0 28 // ThreadActor
michael@0 29
michael@0 30 /**
michael@0 31 * Browser-specific actors.
michael@0 32 */
michael@0 33
michael@0 34 /**
michael@0 35 * Yield all windows of type |aWindowType|, from the oldest window to the
michael@0 36 * youngest, using nsIWindowMediator::getEnumerator. We're usually
michael@0 37 * interested in "navigator:browser" windows.
michael@0 38 */
michael@0 39 function allAppShellDOMWindows(aWindowType)
michael@0 40 {
michael@0 41 let e = Services.wm.getEnumerator(aWindowType);
michael@0 42 while (e.hasMoreElements()) {
michael@0 43 yield e.getNext();
michael@0 44 }
michael@0 45 }
michael@0 46
michael@0 47 /**
michael@0 48 * Retrieve the window type of the top-level window |aWindow|.
michael@0 49 */
michael@0 50 function appShellDOMWindowType(aWindow) {
michael@0 51 /* This is what nsIWindowMediator's enumerator checks. */
michael@0 52 return aWindow.document.documentElement.getAttribute('windowtype');
michael@0 53 }
michael@0 54
michael@0 55 /**
michael@0 56 * Send Debugger:Shutdown events to all "navigator:browser" windows.
michael@0 57 */
michael@0 58 function sendShutdownEvent() {
michael@0 59 for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
michael@0 60 let evt = win.document.createEvent("Event");
michael@0 61 evt.initEvent("Debugger:Shutdown", true, false);
michael@0 62 win.document.documentElement.dispatchEvent(evt);
michael@0 63 }
michael@0 64 }
michael@0 65
michael@0 66 /**
michael@0 67 * Construct a root actor appropriate for use in a server running in a
michael@0 68 * browser. The returned root actor:
michael@0 69 * - respects the factories registered with DebuggerServer.addGlobalActor,
michael@0 70 * - uses a BrowserTabList to supply tab actors,
michael@0 71 * - sends all navigator:browser window documents a Debugger:Shutdown event
michael@0 72 * when it exits.
michael@0 73 *
michael@0 74 * * @param aConnection DebuggerServerConnection
michael@0 75 * The conection to the client.
michael@0 76 */
michael@0 77 function createRootActor(aConnection)
michael@0 78 {
michael@0 79 return new RootActor(aConnection,
michael@0 80 {
michael@0 81 tabList: new BrowserTabList(aConnection),
michael@0 82 addonList: new BrowserAddonList(aConnection),
michael@0 83 globalActorFactories: DebuggerServer.globalActorFactories,
michael@0 84 onShutdown: sendShutdownEvent
michael@0 85 });
michael@0 86 }
michael@0 87
michael@0 88 /**
michael@0 89 * A live list of BrowserTabActors representing the current browser tabs,
michael@0 90 * to be provided to the root actor to answer 'listTabs' requests.
michael@0 91 *
michael@0 92 * This object also takes care of listening for TabClose events and
michael@0 93 * onCloseWindow notifications, and exiting the BrowserTabActors concerned.
michael@0 94 *
michael@0 95 * (See the documentation for RootActor for the definition of the "live
michael@0 96 * list" interface.)
michael@0 97 *
michael@0 98 * @param aConnection DebuggerServerConnection
michael@0 99 * The connection in which this list's tab actors may participate.
michael@0 100 *
michael@0 101 * Some notes:
michael@0 102 *
michael@0 103 * This constructor is specific to the desktop browser environment; it
michael@0 104 * maintains the tab list by tracking XUL windows and their XUL documents'
michael@0 105 * "tabbrowser", "tab", and "browser" elements. What's entailed in maintaining
michael@0 106 * an accurate list of open tabs in this context?
michael@0 107 *
michael@0 108 * - Opening and closing XUL windows:
michael@0 109 *
michael@0 110 * An nsIWindowMediatorListener is notified when new XUL windows (i.e., desktop
michael@0 111 * windows) are opened and closed. It is not notified of individual content
michael@0 112 * browser tabs coming and going within such a XUL window. That seems
michael@0 113 * reasonable enough; it's concerned with XUL windows, not tab elements in the
michael@0 114 * window's XUL document.
michael@0 115 *
michael@0 116 * However, even if we attach TabOpen and TabClose event listeners to each XUL
michael@0 117 * window as soon as it is created:
michael@0 118 *
michael@0 119 * - we do not receive a TabOpen event for the initial empty tab of a new XUL
michael@0 120 * window; and
michael@0 121 *
michael@0 122 * - we do not receive TabClose events for the tabs of a XUL window that has
michael@0 123 * been closed.
michael@0 124 *
michael@0 125 * This means that TabOpen and TabClose events alone are not sufficient to
michael@0 126 * maintain an accurate list of live tabs and mark tab actors as closed
michael@0 127 * promptly. Our nsIWindowMediatorListener onCloseWindow handler must find and
michael@0 128 * exit all actors for tabs that were in the closing window.
michael@0 129 *
michael@0 130 * Since this is a bit hairy, we don't make each individual attached tab actor
michael@0 131 * responsible for noticing when it has been closed; we watch for that, and
michael@0 132 * promise to call each actor's 'exit' method when it's closed, regardless of
michael@0 133 * how we learn the news.
michael@0 134 *
michael@0 135 * - nsIWindowMediator locks
michael@0 136 *
michael@0 137 * nsIWindowMediator holds a lock protecting its list of top-level windows
michael@0 138 * while it calls nsIWindowMediatorListener methods. nsIWindowMediator's
michael@0 139 * GetEnumerator method also tries to acquire that lock. Thus, enumerating
michael@0 140 * windows from within a listener method deadlocks (bug 873589). Rah. One
michael@0 141 * can sometimes work around this by leaving the enumeration for a later
michael@0 142 * tick.
michael@0 143 *
michael@0 144 * - Dragging tabs between windows:
michael@0 145 *
michael@0 146 * When a tab is dragged from one desktop window to another, we receive a
michael@0 147 * TabOpen event for the new tab, and a TabClose event for the old tab; tab XUL
michael@0 148 * elements do not really move from one document to the other (although their
michael@0 149 * linked browser's content window objects do).
michael@0 150 *
michael@0 151 * However, while we could thus assume that each tab stays with the XUL window
michael@0 152 * it belonged to when it was created, I'm not sure this is behavior one should
michael@0 153 * rely upon. When a XUL window is closed, we take the less efficient, more
michael@0 154 * conservative approach of simply searching the entire table for actors that
michael@0 155 * belong to the closing XUL window, rather than trying to somehow track which
michael@0 156 * XUL window each tab belongs to.
michael@0 157 */
michael@0 158 function BrowserTabList(aConnection)
michael@0 159 {
michael@0 160 this._connection = aConnection;
michael@0 161
michael@0 162 /*
michael@0 163 * The XUL document of a tabbed browser window has "tab" elements, whose
michael@0 164 * 'linkedBrowser' JavaScript properties are "browser" elements; those
michael@0 165 * browsers' 'contentWindow' properties are wrappers on the tabs' content
michael@0 166 * window objects.
michael@0 167 *
michael@0 168 * This map's keys are "browser" XUL elements; it maps each browser element
michael@0 169 * to the tab actor we've created for its content window, if we've created
michael@0 170 * one. This map serves several roles:
michael@0 171 *
michael@0 172 * - During iteration, we use it to find actors we've created previously.
michael@0 173 *
michael@0 174 * - On a TabClose event, we use it to find the tab's actor and exit it.
michael@0 175 *
michael@0 176 * - When the onCloseWindow handler is called, we iterate over it to find all
michael@0 177 * tabs belonging to the closing XUL window, and exit them.
michael@0 178 *
michael@0 179 * - When it's empty, and the onListChanged hook is null, we know we can
michael@0 180 * stop listening for events and notifications.
michael@0 181 *
michael@0 182 * We listen for TabClose events and onCloseWindow notifications in order to
michael@0 183 * send onListChanged notifications, but also to tell actors when their
michael@0 184 * referent has gone away and remove entries for dead browsers from this map.
michael@0 185 * If that code is working properly, neither this map nor the actors in it
michael@0 186 * should ever hold dead tabs alive.
michael@0 187 */
michael@0 188 this._actorByBrowser = new Map();
michael@0 189
michael@0 190 /* The current onListChanged handler, or null. */
michael@0 191 this._onListChanged = null;
michael@0 192
michael@0 193 /*
michael@0 194 * True if we've been iterated over since we last called our onListChanged
michael@0 195 * hook.
michael@0 196 */
michael@0 197 this._mustNotify = false;
michael@0 198
michael@0 199 /* True if we're testing, and should throw if consistency checks fail. */
michael@0 200 this._testing = false;
michael@0 201 }
michael@0 202
michael@0 203 BrowserTabList.prototype.constructor = BrowserTabList;
michael@0 204
michael@0 205
michael@0 206 /**
michael@0 207 * Get the selected browser for the given navigator:browser window.
michael@0 208 * @private
michael@0 209 * @param aWindow nsIChromeWindow
michael@0 210 * The navigator:browser window for which you want the selected browser.
michael@0 211 * @return nsIDOMElement|null
michael@0 212 * The currently selected xul:browser element, if any. Note that the
michael@0 213 * browser window might not be loaded yet - the function will return
michael@0 214 * |null| in such cases.
michael@0 215 */
michael@0 216 BrowserTabList.prototype._getSelectedBrowser = function(aWindow) {
michael@0 217 return aWindow.gBrowser ? aWindow.gBrowser.selectedBrowser : null;
michael@0 218 };
michael@0 219
michael@0 220 BrowserTabList.prototype._getChildren = function(aWindow) {
michael@0 221 return aWindow.gBrowser.browsers;
michael@0 222 };
michael@0 223
michael@0 224 BrowserTabList.prototype.getList = function() {
michael@0 225 let topXULWindow = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
michael@0 226
michael@0 227 // As a sanity check, make sure all the actors presently in our map get
michael@0 228 // picked up when we iterate over all windows' tabs.
michael@0 229 let initialMapSize = this._actorByBrowser.size;
michael@0 230 let foundCount = 0;
michael@0 231
michael@0 232 // To avoid mysterious behavior if tabs are closed or opened mid-iteration,
michael@0 233 // we update the map first, and then make a second pass over it to yield
michael@0 234 // the actors. Thus, the sequence yielded is always a snapshot of the
michael@0 235 // actors that were live when we began the iteration.
michael@0 236
michael@0 237 let actorPromises = [];
michael@0 238
michael@0 239 // Iterate over all navigator:browser XUL windows.
michael@0 240 for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
michael@0 241 let selectedBrowser = this._getSelectedBrowser(win);
michael@0 242 if (!selectedBrowser) {
michael@0 243 continue;
michael@0 244 }
michael@0 245
michael@0 246 // For each tab in this XUL window, ensure that we have an actor for
michael@0 247 // it, reusing existing actors where possible. We actually iterate
michael@0 248 // over 'browser' XUL elements, and BrowserTabActor uses
michael@0 249 // browser.contentWindow as the debuggee global.
michael@0 250 for (let browser of this._getChildren(win)) {
michael@0 251 // Do we have an existing actor for this browser? If not, create one.
michael@0 252 let actor = this._actorByBrowser.get(browser);
michael@0 253 if (actor) {
michael@0 254 actorPromises.push(promise.resolve(actor));
michael@0 255 foundCount++;
michael@0 256 } else if (browser.isRemoteBrowser) {
michael@0 257 actor = new RemoteBrowserTabActor(this._connection, browser);
michael@0 258 this._actorByBrowser.set(browser, actor);
michael@0 259 let promise = actor.connect().then((form) => {
michael@0 260 actor._form = form;
michael@0 261 return actor;
michael@0 262 });
michael@0 263 actorPromises.push(promise);
michael@0 264 } else {
michael@0 265 actor = new BrowserTabActor(this._connection, browser, win.gBrowser);
michael@0 266 this._actorByBrowser.set(browser, actor);
michael@0 267 actorPromises.push(promise.resolve(actor));
michael@0 268 }
michael@0 269
michael@0 270 // Set the 'selected' properties on all actors correctly.
michael@0 271 actor.selected = (win === topXULWindow && browser === selectedBrowser);
michael@0 272 }
michael@0 273 }
michael@0 274
michael@0 275 if (this._testing && initialMapSize !== foundCount)
michael@0 276 throw Error("_actorByBrowser map contained actors for dead tabs");
michael@0 277
michael@0 278 this._mustNotify = true;
michael@0 279 this._checkListening();
michael@0 280
michael@0 281 return promise.all(actorPromises);
michael@0 282 };
michael@0 283
michael@0 284 Object.defineProperty(BrowserTabList.prototype, 'onListChanged', {
michael@0 285 enumerable: true, configurable:true,
michael@0 286 get: function() { return this._onListChanged; },
michael@0 287 set: function(v) {
michael@0 288 if (v !== null && typeof v !== 'function') {
michael@0 289 throw Error("onListChanged property may only be set to 'null' or a function");
michael@0 290 }
michael@0 291 this._onListChanged = v;
michael@0 292 this._checkListening();
michael@0 293 }
michael@0 294 });
michael@0 295
michael@0 296 /**
michael@0 297 * The set of tabs has changed somehow. Call our onListChanged handler, if
michael@0 298 * one is set, and if we haven't already called it since the last iteration.
michael@0 299 */
michael@0 300 BrowserTabList.prototype._notifyListChanged = function() {
michael@0 301 if (!this._onListChanged)
michael@0 302 return;
michael@0 303 if (this._mustNotify) {
michael@0 304 this._onListChanged();
michael@0 305 this._mustNotify = false;
michael@0 306 }
michael@0 307 };
michael@0 308
michael@0 309 /**
michael@0 310 * Exit |aActor|, belonging to |aBrowser|, and notify the onListChanged
michael@0 311 * handle if needed.
michael@0 312 */
michael@0 313 BrowserTabList.prototype._handleActorClose = function(aActor, aBrowser) {
michael@0 314 if (this._testing) {
michael@0 315 if (this._actorByBrowser.get(aBrowser) !== aActor) {
michael@0 316 throw Error("BrowserTabActor not stored in map under given browser");
michael@0 317 }
michael@0 318 if (aActor.browser !== aBrowser) {
michael@0 319 throw Error("actor's browser and map key don't match");
michael@0 320 }
michael@0 321 }
michael@0 322
michael@0 323 this._actorByBrowser.delete(aBrowser);
michael@0 324 aActor.exit();
michael@0 325
michael@0 326 this._notifyListChanged();
michael@0 327 this._checkListening();
michael@0 328 };
michael@0 329
michael@0 330 /**
michael@0 331 * Make sure we are listening or not listening for activity elsewhere in
michael@0 332 * the browser, as appropriate. Other than setting up newly created XUL
michael@0 333 * windows, all listener / observer connection and disconnection should
michael@0 334 * happen here.
michael@0 335 */
michael@0 336 BrowserTabList.prototype._checkListening = function() {
michael@0 337 /*
michael@0 338 * If we have an onListChanged handler that we haven't sent an announcement
michael@0 339 * to since the last iteration, we need to watch for tab creation.
michael@0 340 *
michael@0 341 * Oddly, we don't need to watch for 'close' events here. If our actor list
michael@0 342 * is empty, then either it was empty the last time we iterated, and no
michael@0 343 * close events are possible, or it was not empty the last time we
michael@0 344 * iterated, but all the actors have since been closed, and we must have
michael@0 345 * sent a notification already when they closed.
michael@0 346 */
michael@0 347 this._listenForEventsIf(this._onListChanged && this._mustNotify,
michael@0 348 "_listeningForTabOpen", ["TabOpen", "TabSelect"]);
michael@0 349
michael@0 350 /* If we have live actors, we need to be ready to mark them dead. */
michael@0 351 this._listenForEventsIf(this._actorByBrowser.size > 0,
michael@0 352 "_listeningForTabClose", ["TabClose"]);
michael@0 353
michael@0 354 /*
michael@0 355 * We must listen to the window mediator in either case, since that's the
michael@0 356 * only way to find out about tabs that come and go when top-level windows
michael@0 357 * are opened and closed.
michael@0 358 */
michael@0 359 this._listenToMediatorIf((this._onListChanged && this._mustNotify) ||
michael@0 360 (this._actorByBrowser.size > 0));
michael@0 361 };
michael@0 362
michael@0 363 /*
michael@0 364 * Add or remove event listeners for all XUL windows.
michael@0 365 *
michael@0 366 * @param aShouldListen boolean
michael@0 367 * True if we should add event handlers; false if we should remove them.
michael@0 368 * @param aGuard string
michael@0 369 * The name of a guard property of 'this', indicating whether we're
michael@0 370 * already listening for those events.
michael@0 371 * @param aEventNames array of strings
michael@0 372 * An array of event names.
michael@0 373 */
michael@0 374 BrowserTabList.prototype._listenForEventsIf = function(aShouldListen, aGuard, aEventNames) {
michael@0 375 if (!aShouldListen !== !this[aGuard]) {
michael@0 376 let op = aShouldListen ? "addEventListener" : "removeEventListener";
michael@0 377 for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
michael@0 378 for (let name of aEventNames) {
michael@0 379 win[op](name, this, false);
michael@0 380 }
michael@0 381 }
michael@0 382 this[aGuard] = aShouldListen;
michael@0 383 }
michael@0 384 };
michael@0 385
michael@0 386 /**
michael@0 387 * Implement nsIDOMEventListener.
michael@0 388 */
michael@0 389 BrowserTabList.prototype.handleEvent = DevToolsUtils.makeInfallible(function(aEvent) {
michael@0 390 switch (aEvent.type) {
michael@0 391 case "TabOpen":
michael@0 392 case "TabSelect":
michael@0 393 /* Don't create a new actor; iterate will take care of that. Just notify. */
michael@0 394 this._notifyListChanged();
michael@0 395 this._checkListening();
michael@0 396 break;
michael@0 397 case "TabClose":
michael@0 398 let browser = aEvent.target.linkedBrowser;
michael@0 399 let actor = this._actorByBrowser.get(browser);
michael@0 400 if (actor) {
michael@0 401 this._handleActorClose(actor, browser);
michael@0 402 }
michael@0 403 break;
michael@0 404 }
michael@0 405 }, "BrowserTabList.prototype.handleEvent");
michael@0 406
michael@0 407 /*
michael@0 408 * If |aShouldListen| is true, ensure we've registered a listener with the
michael@0 409 * window mediator. Otherwise, ensure we haven't registered a listener.
michael@0 410 */
michael@0 411 BrowserTabList.prototype._listenToMediatorIf = function(aShouldListen) {
michael@0 412 if (!aShouldListen !== !this._listeningToMediator) {
michael@0 413 let op = aShouldListen ? "addListener" : "removeListener";
michael@0 414 Services.wm[op](this);
michael@0 415 this._listeningToMediator = aShouldListen;
michael@0 416 }
michael@0 417 };
michael@0 418
michael@0 419 /**
michael@0 420 * nsIWindowMediatorListener implementation.
michael@0 421 *
michael@0 422 * See _onTabClosed for explanation of why we needn't actually tweak any
michael@0 423 * actors or tables here.
michael@0 424 *
michael@0 425 * An nsIWindowMediatorListener's methods get passed all sorts of windows; we
michael@0 426 * only care about the tab containers. Those have 'getBrowser' methods.
michael@0 427 */
michael@0 428 BrowserTabList.prototype.onWindowTitleChange = () => { };
michael@0 429
michael@0 430 BrowserTabList.prototype.onOpenWindow = DevToolsUtils.makeInfallible(function(aWindow) {
michael@0 431 let handleLoad = DevToolsUtils.makeInfallible(() => {
michael@0 432 /* We don't want any further load events from this window. */
michael@0 433 aWindow.removeEventListener("load", handleLoad, false);
michael@0 434
michael@0 435 if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType)
michael@0 436 return;
michael@0 437
michael@0 438 // Listen for future tab activity.
michael@0 439 if (this._listeningForTabOpen) {
michael@0 440 aWindow.addEventListener("TabOpen", this, false);
michael@0 441 aWindow.addEventListener("TabSelect", this, false);
michael@0 442 }
michael@0 443 if (this._listeningForTabClose) {
michael@0 444 aWindow.addEventListener("TabClose", this, false);
michael@0 445 }
michael@0 446
michael@0 447 // As explained above, we will not receive a TabOpen event for this
michael@0 448 // document's initial tab, so we must notify our client of the new tab
michael@0 449 // this will have.
michael@0 450 this._notifyListChanged();
michael@0 451 });
michael@0 452
michael@0 453 /*
michael@0 454 * You can hardly do anything at all with a XUL window at this point; it
michael@0 455 * doesn't even have its document yet. Wait until its document has
michael@0 456 * loaded, and then see what we've got. This also avoids
michael@0 457 * nsIWindowMediator enumeration from within listeners (bug 873589).
michael@0 458 */
michael@0 459 aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 460 .getInterface(Ci.nsIDOMWindow);
michael@0 461
michael@0 462 aWindow.addEventListener("load", handleLoad, false);
michael@0 463 }, "BrowserTabList.prototype.onOpenWindow");
michael@0 464
michael@0 465 BrowserTabList.prototype.onCloseWindow = DevToolsUtils.makeInfallible(function(aWindow) {
michael@0 466 aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 467 .getInterface(Ci.nsIDOMWindow);
michael@0 468
michael@0 469 if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType)
michael@0 470 return;
michael@0 471
michael@0 472 /*
michael@0 473 * nsIWindowMediator deadlocks if you call its GetEnumerator method from
michael@0 474 * a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so
michael@0 475 * handle the close in a different tick.
michael@0 476 */
michael@0 477 Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
michael@0 478 /*
michael@0 479 * Scan the entire map for actors representing tabs that were in this
michael@0 480 * top-level window, and exit them.
michael@0 481 */
michael@0 482 for (let [browser, actor] of this._actorByBrowser) {
michael@0 483 /* The browser document of a closed window has no default view. */
michael@0 484 if (!browser.ownerDocument.defaultView) {
michael@0 485 this._handleActorClose(actor, browser);
michael@0 486 }
michael@0 487 }
michael@0 488 }, "BrowserTabList.prototype.onCloseWindow's delayed body"), 0);
michael@0 489 }, "BrowserTabList.prototype.onCloseWindow");
michael@0 490
michael@0 491 /**
michael@0 492 * Creates a tab actor for handling requests to a browser tab, like
michael@0 493 * attaching and detaching. TabActor respects the actor factories
michael@0 494 * registered with DebuggerServer.addTabActor.
michael@0 495 *
michael@0 496 * This class is subclassed by BrowserTabActor and
michael@0 497 * ContentActor. Subclasses are expected to implement a getter
michael@0 498 * the docShell properties.
michael@0 499 *
michael@0 500 * @param aConnection DebuggerServerConnection
michael@0 501 * The conection to the client.
michael@0 502 * @param aChromeEventHandler
michael@0 503 * An object on which listen for DOMWindowCreated and pageshow events.
michael@0 504 */
michael@0 505 function TabActor(aConnection)
michael@0 506 {
michael@0 507 this.conn = aConnection;
michael@0 508 this._tabActorPool = null;
michael@0 509 // A map of actor names to actor instances provided by extensions.
michael@0 510 this._extraActors = {};
michael@0 511 this._exited = false;
michael@0 512
michael@0 513 this.traits = { reconfigure: true };
michael@0 514 }
michael@0 515
michael@0 516 // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
michael@0 517 // *complete* mess, needs to be rethought asap.
michael@0 518
michael@0 519 TabActor.prototype = {
michael@0 520 traits: null,
michael@0 521
michael@0 522 get exited() { return this._exited; },
michael@0 523 get attached() { return !!this._attached; },
michael@0 524
michael@0 525 _tabPool: null,
michael@0 526 get tabActorPool() { return this._tabPool; },
michael@0 527
michael@0 528 _contextPool: null,
michael@0 529 get contextActorPool() { return this._contextPool; },
michael@0 530
michael@0 531 _pendingNavigation: null,
michael@0 532
michael@0 533 // A constant prefix that will be used to form the actor ID by the server.
michael@0 534 actorPrefix: "tab",
michael@0 535
michael@0 536 /**
michael@0 537 * An object on which listen for DOMWindowCreated and pageshow events.
michael@0 538 */
michael@0 539 get chromeEventHandler() {
michael@0 540 // TODO: bug 992778, fix docShell.chromeEventHandler in child processes
michael@0 541 return this.docShell.chromeEventHandler ||
michael@0 542 this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 543 .getInterface(Ci.nsIContentFrameMessageManager);
michael@0 544 },
michael@0 545
michael@0 546 /**
michael@0 547 * Getter for the nsIMessageManager associated to the tab.
michael@0 548 */
michael@0 549 get messageManager() {
michael@0 550 return this.docShell
michael@0 551 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 552 .getInterface(Ci.nsIContentFrameMessageManager);
michael@0 553 },
michael@0 554
michael@0 555 /**
michael@0 556 * Getter for the tab's doc shell.
michael@0 557 */
michael@0 558 get docShell() {
michael@0 559 throw "The docShell getter should be implemented by a subclass of TabActor";
michael@0 560 },
michael@0 561
michael@0 562 /**
michael@0 563 * Getter for the tab content's DOM window.
michael@0 564 */
michael@0 565 get window() {
michael@0 566 return this.docShell
michael@0 567 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 568 .getInterface(Ci.nsIDOMWindow);
michael@0 569 },
michael@0 570
michael@0 571 /**
michael@0 572 * Getter for the nsIWebProgress for watching this window.
michael@0 573 */
michael@0 574 get webProgress() {
michael@0 575 return this.docShell
michael@0 576 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 577 .getInterface(Ci.nsIWebProgress);
michael@0 578 },
michael@0 579
michael@0 580 /**
michael@0 581 * Getter for the nsIWebNavigation for the tab.
michael@0 582 */
michael@0 583 get webNavigation() {
michael@0 584 return this.docShell
michael@0 585 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 586 .getInterface(Ci.nsIWebNavigation);
michael@0 587 },
michael@0 588
michael@0 589 /**
michael@0 590 * Getter for the tab's document.
michael@0 591 */
michael@0 592 get contentDocument() {
michael@0 593 return this.webNavigation.document;
michael@0 594 },
michael@0 595
michael@0 596 /**
michael@0 597 * Getter for the tab title.
michael@0 598 * @return string
michael@0 599 * Tab title.
michael@0 600 */
michael@0 601 get title() {
michael@0 602 return this.contentDocument.contentTitle;
michael@0 603 },
michael@0 604
michael@0 605 /**
michael@0 606 * Getter for the tab URL.
michael@0 607 * @return string
michael@0 608 * Tab URL.
michael@0 609 */
michael@0 610 get url() {
michael@0 611 if (this.webNavigation.currentURI) {
michael@0 612 return this.webNavigation.currentURI.spec;
michael@0 613 }
michael@0 614 // Abrupt closing of the browser window may leave callbacks without a
michael@0 615 // currentURI.
michael@0 616 return null;
michael@0 617 },
michael@0 618
michael@0 619 form: function BTA_form() {
michael@0 620 dbg_assert(!this.exited,
michael@0 621 "grip() shouldn't be called on exited browser actor.");
michael@0 622 dbg_assert(this.actorID,
michael@0 623 "tab should have an actorID.");
michael@0 624
michael@0 625 let windowUtils = this.window
michael@0 626 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 627 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 628
michael@0 629 let response = {
michael@0 630 actor: this.actorID,
michael@0 631 title: this.title,
michael@0 632 url: this.url,
michael@0 633 outerWindowID: windowUtils.outerWindowID
michael@0 634 };
michael@0 635
michael@0 636 // Walk over tab actors added by extensions and add them to a new ActorPool.
michael@0 637 let actorPool = new ActorPool(this.conn);
michael@0 638 this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
michael@0 639 if (!actorPool.isEmpty()) {
michael@0 640 this._tabActorPool = actorPool;
michael@0 641 this.conn.addActorPool(this._tabActorPool);
michael@0 642 }
michael@0 643
michael@0 644 this._appendExtraActors(response);
michael@0 645 return response;
michael@0 646 },
michael@0 647
michael@0 648 /**
michael@0 649 * Called when the actor is removed from the connection.
michael@0 650 */
michael@0 651 disconnect: function BTA_disconnect() {
michael@0 652 this._detach();
michael@0 653 this._extraActors = null;
michael@0 654 this._exited = true;
michael@0 655 },
michael@0 656
michael@0 657 /**
michael@0 658 * Called by the root actor when the underlying tab is closed.
michael@0 659 */
michael@0 660 exit: function BTA_exit() {
michael@0 661 if (this.exited) {
michael@0 662 return;
michael@0 663 }
michael@0 664
michael@0 665 if (this._detach()) {
michael@0 666 this.conn.send({ from: this.actorID,
michael@0 667 type: "tabDetached" });
michael@0 668 }
michael@0 669
michael@0 670 this._exited = true;
michael@0 671 },
michael@0 672
michael@0 673 /* Support for DebuggerServer.addTabActor. */
michael@0 674 _createExtraActors: createExtraActors,
michael@0 675 _appendExtraActors: appendExtraActors,
michael@0 676
michael@0 677 /**
michael@0 678 * Does the actual work of attching to a tab.
michael@0 679 */
michael@0 680 _attach: function BTA_attach() {
michael@0 681 if (this._attached) {
michael@0 682 return;
michael@0 683 }
michael@0 684
michael@0 685 // Create a pool for tab-lifetime actors.
michael@0 686 dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
michael@0 687 this._tabPool = new ActorPool(this.conn);
michael@0 688 this.conn.addActorPool(this._tabPool);
michael@0 689
michael@0 690 // ... and a pool for context-lifetime actors.
michael@0 691 this._pushContext();
michael@0 692
michael@0 693 this._progressListener = new DebuggerProgressListener(this);
michael@0 694 this._progressListener.watch(this.docShell);
michael@0 695
michael@0 696 this._attached = true;
michael@0 697 },
michael@0 698
michael@0 699 /**
michael@0 700 * Creates a thread actor and a pool for context-lifetime actors. It then sets
michael@0 701 * up the content window for debugging.
michael@0 702 */
michael@0 703 _pushContext: function BTA_pushContext() {
michael@0 704 dbg_assert(!this._contextPool, "Can't push multiple contexts");
michael@0 705
michael@0 706 this._contextPool = new ActorPool(this.conn);
michael@0 707 this.conn.addActorPool(this._contextPool);
michael@0 708
michael@0 709 this.threadActor = new ThreadActor(this, this.window);
michael@0 710 this._contextPool.addActor(this.threadActor);
michael@0 711 },
michael@0 712
michael@0 713 /**
michael@0 714 * Exits the current thread actor and removes the context-lifetime actor pool.
michael@0 715 * The content window is no longer being debugged after this call.
michael@0 716 */
michael@0 717 _popContext: function BTA_popContext() {
michael@0 718 dbg_assert(!!this._contextPool, "No context to pop.");
michael@0 719
michael@0 720 this.conn.removeActorPool(this._contextPool);
michael@0 721 this._contextPool = null;
michael@0 722 this.threadActor.exit();
michael@0 723 this.threadActor = null;
michael@0 724 },
michael@0 725
michael@0 726 /**
michael@0 727 * Does the actual work of detaching from a tab.
michael@0 728 *
michael@0 729 * @returns false if the tab wasn't attached or true of detahing succeeds.
michael@0 730 */
michael@0 731 _detach: function BTA_detach() {
michael@0 732 if (!this.attached) {
michael@0 733 return false;
michael@0 734 }
michael@0 735
michael@0 736 // Check for docShell availability, as it can be already gone
michael@0 737 // during Firefox shutdown.
michael@0 738 if (this.docShell) {
michael@0 739 this._progressListener.unwatch(this.docShell);
michael@0 740 }
michael@0 741 this._progressListener = null;
michael@0 742
michael@0 743 this._popContext();
michael@0 744
michael@0 745 // Shut down actors that belong to this tab's pool.
michael@0 746 this.conn.removeActorPool(this._tabPool);
michael@0 747 this._tabPool = null;
michael@0 748 if (this._tabActorPool) {
michael@0 749 this.conn.removeActorPool(this._tabActorPool);
michael@0 750 this._tabActorPool = null;
michael@0 751 }
michael@0 752
michael@0 753 this._attached = false;
michael@0 754 return true;
michael@0 755 },
michael@0 756
michael@0 757 // Protocol Request Handlers
michael@0 758
michael@0 759 onAttach: function BTA_onAttach(aRequest) {
michael@0 760 if (this.exited) {
michael@0 761 return { type: "exited" };
michael@0 762 }
michael@0 763
michael@0 764 this._attach();
michael@0 765
michael@0 766 return {
michael@0 767 type: "tabAttached",
michael@0 768 threadActor: this.threadActor.actorID,
michael@0 769 cacheEnabled: this._getCacheEnabled(),
michael@0 770 javascriptEnabled: this._getJavascriptEnabled(),
michael@0 771 traits: this.traits,
michael@0 772 };
michael@0 773 },
michael@0 774
michael@0 775 onDetach: function BTA_onDetach(aRequest) {
michael@0 776 if (!this._detach()) {
michael@0 777 return { error: "wrongState" };
michael@0 778 }
michael@0 779
michael@0 780 return { type: "detached" };
michael@0 781 },
michael@0 782
michael@0 783 /**
michael@0 784 * Reload the page in this tab.
michael@0 785 */
michael@0 786 onReload: function(aRequest) {
michael@0 787 // Wait a tick so that the response packet can be dispatched before the
michael@0 788 // subsequent navigation event packet.
michael@0 789 Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
michael@0 790 this.window.location.reload();
michael@0 791 }, "TabActor.prototype.onReload's delayed body"), 0);
michael@0 792 return {};
michael@0 793 },
michael@0 794
michael@0 795 /**
michael@0 796 * Navigate this tab to a new location
michael@0 797 */
michael@0 798 onNavigateTo: function(aRequest) {
michael@0 799 // Wait a tick so that the response packet can be dispatched before the
michael@0 800 // subsequent navigation event packet.
michael@0 801 Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
michael@0 802 this.window.location = aRequest.url;
michael@0 803 }, "TabActor.prototype.onNavigateTo's delayed body"), 0);
michael@0 804 return {};
michael@0 805 },
michael@0 806
michael@0 807 /**
michael@0 808 * Reconfigure options.
michael@0 809 */
michael@0 810 onReconfigure: function (aRequest) {
michael@0 811 let options = aRequest.options || {};
michael@0 812
michael@0 813 this._toggleJsOrCache(options);
michael@0 814 return {};
michael@0 815 },
michael@0 816
michael@0 817 /**
michael@0 818 * Handle logic to enable/disable JS/cache.
michael@0 819 */
michael@0 820 _toggleJsOrCache: function(options) {
michael@0 821 // Wait a tick so that the response packet can be dispatched before the
michael@0 822 // subsequent navigation event packet.
michael@0 823 let reload = false;
michael@0 824
michael@0 825 if (typeof options.javascriptEnabled !== "undefined" &&
michael@0 826 options.javascriptEnabled !== this._getJavascriptEnabled()) {
michael@0 827 this._setJavascriptEnabled(options.javascriptEnabled);
michael@0 828 reload = true;
michael@0 829 }
michael@0 830 if (typeof options.cacheEnabled !== "undefined" &&
michael@0 831 options.cacheEnabled !== this._getCacheEnabled()) {
michael@0 832 this._setCacheEnabled(options.cacheEnabled);
michael@0 833 reload = true;
michael@0 834 }
michael@0 835
michael@0 836 // Reload if:
michael@0 837 // - there's an explicit `performReload` flag and it's true
michael@0 838 // - there's no `performReload` flag, but it makes sense to do so
michael@0 839 let hasExplicitReloadFlag = "performReload" in options;
michael@0 840 if ((hasExplicitReloadFlag && options.performReload) ||
michael@0 841 (!hasExplicitReloadFlag && reload)) {
michael@0 842 this.onReload();
michael@0 843 }
michael@0 844 },
michael@0 845
michael@0 846 /**
michael@0 847 * Disable or enable the cache via docShell.
michael@0 848 */
michael@0 849 _setCacheEnabled: function(allow) {
michael@0 850 let enable = Ci.nsIRequest.LOAD_NORMAL;
michael@0 851 let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
michael@0 852 Ci.nsIRequest.INHIBIT_CACHING;
michael@0 853 if (this.docShell) {
michael@0 854 this.docShell.defaultLoadFlags = allow ? enable : disable;
michael@0 855 }
michael@0 856 },
michael@0 857
michael@0 858 /**
michael@0 859 * Disable or enable JS via docShell.
michael@0 860 */
michael@0 861 _setJavascriptEnabled: function(allow) {
michael@0 862 if (this.docShell) {
michael@0 863 this.docShell.allowJavascript = allow;
michael@0 864 }
michael@0 865 },
michael@0 866
michael@0 867 /**
michael@0 868 * Return cache allowed status.
michael@0 869 */
michael@0 870 _getCacheEnabled: function() {
michael@0 871 if (!this.docShell) {
michael@0 872 // The tab is already closed.
michael@0 873 return null;
michael@0 874 }
michael@0 875
michael@0 876 let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
michael@0 877 Ci.nsIRequest.INHIBIT_CACHING;
michael@0 878 return this.docShell.defaultLoadFlags !== disable;
michael@0 879 },
michael@0 880
michael@0 881 /**
michael@0 882 * Return JS allowed status.
michael@0 883 */
michael@0 884 _getJavascriptEnabled: function() {
michael@0 885 if (!this.docShell) {
michael@0 886 // The tab is already closed.
michael@0 887 return null;
michael@0 888 }
michael@0 889
michael@0 890 return this.docShell.allowJavascript;
michael@0 891 },
michael@0 892
michael@0 893 /**
michael@0 894 * Prepare to enter a nested event loop by disabling debuggee events.
michael@0 895 */
michael@0 896 preNest: function BTA_preNest() {
michael@0 897 if (!this.window) {
michael@0 898 // The tab is already closed.
michael@0 899 return;
michael@0 900 }
michael@0 901 let windowUtils = this.window
michael@0 902 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 903 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 904 windowUtils.suppressEventHandling(true);
michael@0 905 windowUtils.suspendTimeouts();
michael@0 906 },
michael@0 907
michael@0 908 /**
michael@0 909 * Prepare to exit a nested event loop by enabling debuggee events.
michael@0 910 */
michael@0 911 postNest: function BTA_postNest(aNestData) {
michael@0 912 if (!this.window) {
michael@0 913 // The tab is already closed.
michael@0 914 return;
michael@0 915 }
michael@0 916 let windowUtils = this.window
michael@0 917 .QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 918 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 919 windowUtils.resumeTimeouts();
michael@0 920 windowUtils.suppressEventHandling(false);
michael@0 921 if (this._pendingNavigation) {
michael@0 922 this._pendingNavigation.resume();
michael@0 923 this._pendingNavigation = null;
michael@0 924 }
michael@0 925 },
michael@0 926
michael@0 927 /**
michael@0 928 * Handle location changes, by clearing the previous debuggees and enabling
michael@0 929 * debugging, which may have been disabled temporarily by the
michael@0 930 * DebuggerProgressListener.
michael@0 931 */
michael@0 932 _windowReady: function (window) {
michael@0 933 let isTopLevel = window == this.window;
michael@0 934 dumpn("window-ready: " + window.location + " isTopLevel:" + isTopLevel);
michael@0 935
michael@0 936 events.emit(this, "window-ready", {
michael@0 937 window: window,
michael@0 938 isTopLevel: isTopLevel
michael@0 939 });
michael@0 940
michael@0 941 // TODO bug 997119: move that code to ThreadActor by listening to window-ready
michael@0 942 let threadActor = this.threadActor;
michael@0 943 if (isTopLevel) {
michael@0 944 threadActor.clearDebuggees();
michael@0 945 if (threadActor.dbg) {
michael@0 946 threadActor.dbg.enabled = true;
michael@0 947 threadActor.global = window;
michael@0 948 threadActor.maybePauseOnExceptions();
michael@0 949 }
michael@0 950 }
michael@0 951
michael@0 952 // Refresh the debuggee list when a new window object appears (top window or
michael@0 953 // iframe).
michael@0 954 if (threadActor.attached) {
michael@0 955 threadActor.findGlobals();
michael@0 956 }
michael@0 957 },
michael@0 958
michael@0 959 /**
michael@0 960 * Start notifying server codebase and client about a new document
michael@0 961 * being loaded in the currently targeted context.
michael@0 962 */
michael@0 963 _willNavigate: function (window, newURI, request) {
michael@0 964 let isTopLevel = window == this.window;
michael@0 965
michael@0 966 // will-navigate event needs to be dispatched synchronously,
michael@0 967 // by calling the listeners in the order or registration.
michael@0 968 // This event fires once navigation starts,
michael@0 969 // (all pending user prompts are dealt with),
michael@0 970 // but before the first request starts.
michael@0 971 events.emit(this, "will-navigate", {
michael@0 972 window: window,
michael@0 973 isTopLevel: isTopLevel,
michael@0 974 newURI: newURI,
michael@0 975 request: request
michael@0 976 });
michael@0 977
michael@0 978
michael@0 979 // We don't do anything for inner frames in TabActor.
michael@0 980 // (we will only update thread actor on window-ready)
michael@0 981 if (!isTopLevel) {
michael@0 982 return;
michael@0 983 }
michael@0 984
michael@0 985 // Proceed normally only if the debuggee is not paused.
michael@0 986 // TODO bug 997119: move that code to ThreadActor by listening to will-navigate
michael@0 987 let threadActor = this.threadActor;
michael@0 988 if (request && threadActor.state == "paused") {
michael@0 989 request.suspend();
michael@0 990 threadActor.onResume();
michael@0 991 threadActor.dbg.enabled = false;
michael@0 992 this._pendingNavigation = request;
michael@0 993 }
michael@0 994 threadActor.disableAllBreakpoints();
michael@0 995
michael@0 996 this.conn.send({
michael@0 997 from: this.actorID,
michael@0 998 type: "tabNavigated",
michael@0 999 url: newURI,
michael@0 1000 nativeConsoleAPI: true,
michael@0 1001 state: "start"
michael@0 1002 });
michael@0 1003 },
michael@0 1004
michael@0 1005 /**
michael@0 1006 * Notify server and client about a new document done loading in the current
michael@0 1007 * targeted context.
michael@0 1008 */
michael@0 1009 _navigate: function (window) {
michael@0 1010 let isTopLevel = window == this.window;
michael@0 1011
michael@0 1012 // navigate event needs to be dispatched synchronously,
michael@0 1013 // by calling the listeners in the order or registration.
michael@0 1014 // This event is fired once the document is loaded,
michael@0 1015 // after the load event, it's document ready-state is 'complete'.
michael@0 1016 events.emit(this, "navigate", {
michael@0 1017 window: window,
michael@0 1018 isTopLevel: isTopLevel
michael@0 1019 });
michael@0 1020
michael@0 1021 // We don't do anything for inner frames in TabActor.
michael@0 1022 // (we will only update thread actor on window-ready)
michael@0 1023 if (!isTopLevel) {
michael@0 1024 return;
michael@0 1025 }
michael@0 1026
michael@0 1027 // TODO bug 997119: move that code to ThreadActor by listening to navigate
michael@0 1028 let threadActor = this.threadActor;
michael@0 1029 if (threadActor.state == "running") {
michael@0 1030 threadActor.dbg.enabled = true;
michael@0 1031 }
michael@0 1032
michael@0 1033 this.conn.send({
michael@0 1034 from: this.actorID,
michael@0 1035 type: "tabNavigated",
michael@0 1036 url: this.url,
michael@0 1037 title: this.title,
michael@0 1038 nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
michael@0 1039 state: "stop"
michael@0 1040 });
michael@0 1041 },
michael@0 1042
michael@0 1043 /**
michael@0 1044 * Tells if the window.console object is native or overwritten by script in
michael@0 1045 * the page.
michael@0 1046 *
michael@0 1047 * @param nsIDOMWindow aWindow
michael@0 1048 * The window object you want to check.
michael@0 1049 * @return boolean
michael@0 1050 * True if the window.console object is native, or false otherwise.
michael@0 1051 */
michael@0 1052 hasNativeConsoleAPI: function BTA_hasNativeConsoleAPI(aWindow) {
michael@0 1053 let isNative = false;
michael@0 1054 try {
michael@0 1055 // We are very explicitly examining the "console" property of
michael@0 1056 // the non-Xrayed object here.
michael@0 1057 let console = aWindow.wrappedJSObject.console;
michael@0 1058 isNative = console instanceof aWindow.Console;
michael@0 1059 }
michael@0 1060 catch (ex) { }
michael@0 1061 return isNative;
michael@0 1062 }
michael@0 1063 };
michael@0 1064
michael@0 1065 /**
michael@0 1066 * The request types this actor can handle.
michael@0 1067 */
michael@0 1068 TabActor.prototype.requestTypes = {
michael@0 1069 "attach": TabActor.prototype.onAttach,
michael@0 1070 "detach": TabActor.prototype.onDetach,
michael@0 1071 "reload": TabActor.prototype.onReload,
michael@0 1072 "navigateTo": TabActor.prototype.onNavigateTo,
michael@0 1073 "reconfigure": TabActor.prototype.onReconfigure
michael@0 1074 };
michael@0 1075
michael@0 1076 /**
michael@0 1077 * Creates a tab actor for handling requests to a single in-process
michael@0 1078 * <browser> tab. Most of the implementation comes from TabActor.
michael@0 1079 *
michael@0 1080 * @param aConnection DebuggerServerConnection
michael@0 1081 * The conection to the client.
michael@0 1082 * @param aBrowser browser
michael@0 1083 * The browser instance that contains this tab.
michael@0 1084 * @param aTabBrowser tabbrowser
michael@0 1085 * The tabbrowser that can receive nsIWebProgressListener events.
michael@0 1086 */
michael@0 1087 function BrowserTabActor(aConnection, aBrowser, aTabBrowser)
michael@0 1088 {
michael@0 1089 TabActor.call(this, aConnection, aBrowser);
michael@0 1090 this._browser = aBrowser;
michael@0 1091 this._tabbrowser = aTabBrowser;
michael@0 1092 }
michael@0 1093
michael@0 1094 BrowserTabActor.prototype = Object.create(TabActor.prototype);
michael@0 1095
michael@0 1096 BrowserTabActor.prototype.constructor = BrowserTabActor;
michael@0 1097
michael@0 1098 Object.defineProperty(BrowserTabActor.prototype, "docShell", {
michael@0 1099 get: function() {
michael@0 1100 return this._browser.docShell;
michael@0 1101 },
michael@0 1102 enumerable: true,
michael@0 1103 configurable: false
michael@0 1104 });
michael@0 1105
michael@0 1106 Object.defineProperty(BrowserTabActor.prototype, "title", {
michael@0 1107 get: function() {
michael@0 1108 let title = this.contentDocument.title || this._browser.contentTitle;
michael@0 1109 // If contentTitle is empty (e.g. on a not-yet-restored tab), but there is a
michael@0 1110 // tabbrowser (i.e. desktop Firefox, but not Fennec), we can use the label
michael@0 1111 // as the title.
michael@0 1112 if (!title && this._tabbrowser) {
michael@0 1113 let tab = this._tabbrowser._getTabForContentWindow(this.window);
michael@0 1114 if (tab) {
michael@0 1115 title = tab.label;
michael@0 1116 }
michael@0 1117 }
michael@0 1118 return title;
michael@0 1119 },
michael@0 1120 enumerable: true,
michael@0 1121 configurable: false
michael@0 1122 });
michael@0 1123
michael@0 1124 Object.defineProperty(BrowserTabActor.prototype, "browser", {
michael@0 1125 get: function() {
michael@0 1126 return this._browser;
michael@0 1127 },
michael@0 1128 enumerable: true,
michael@0 1129 configurable: false
michael@0 1130 });
michael@0 1131
michael@0 1132 BrowserTabActor.prototype.disconnect = function() {
michael@0 1133 TabActor.prototype.disconnect.call(this);
michael@0 1134 this._browser = null;
michael@0 1135 this._tabbrowser = null;
michael@0 1136 };
michael@0 1137
michael@0 1138 BrowserTabActor.prototype.exit = function() {
michael@0 1139 TabActor.prototype.exit.call(this);
michael@0 1140 this._browser = null;
michael@0 1141 this._tabbrowser = null;
michael@0 1142 };
michael@0 1143
michael@0 1144 /**
michael@0 1145 * This actor is a shim that connects to a ContentActor in a remote
michael@0 1146 * browser process. All RDP packets get forwarded using the message
michael@0 1147 * manager.
michael@0 1148 *
michael@0 1149 * @param aConnection The main RDP connection.
michael@0 1150 * @param aBrowser XUL <browser> element to connect to.
michael@0 1151 */
michael@0 1152 function RemoteBrowserTabActor(aConnection, aBrowser)
michael@0 1153 {
michael@0 1154 this._conn = aConnection;
michael@0 1155 this._browser = aBrowser;
michael@0 1156 this._form = null;
michael@0 1157 }
michael@0 1158
michael@0 1159 RemoteBrowserTabActor.prototype = {
michael@0 1160 connect: function() {
michael@0 1161 return DebuggerServer.connectToChild(this._conn, this._browser);
michael@0 1162 },
michael@0 1163
michael@0 1164 form: function() {
michael@0 1165 return this._form;
michael@0 1166 },
michael@0 1167
michael@0 1168 exit: function() {
michael@0 1169 this._browser = null;
michael@0 1170 },
michael@0 1171 };
michael@0 1172
michael@0 1173 function BrowserAddonList(aConnection)
michael@0 1174 {
michael@0 1175 this._connection = aConnection;
michael@0 1176 this._actorByAddonId = new Map();
michael@0 1177 this._onListChanged = null;
michael@0 1178 }
michael@0 1179
michael@0 1180 BrowserAddonList.prototype.getList = function() {
michael@0 1181 var deferred = promise.defer();
michael@0 1182 AddonManager.getAllAddons((addons) => {
michael@0 1183 for (let addon of addons) {
michael@0 1184 let actor = this._actorByAddonId.get(addon.id);
michael@0 1185 if (!actor) {
michael@0 1186 actor = new BrowserAddonActor(this._connection, addon);
michael@0 1187 this._actorByAddonId.set(addon.id, actor);
michael@0 1188 }
michael@0 1189 }
michael@0 1190 deferred.resolve([actor for ([_, actor] of this._actorByAddonId)]);
michael@0 1191 });
michael@0 1192 return deferred.promise;
michael@0 1193 }
michael@0 1194
michael@0 1195 Object.defineProperty(BrowserAddonList.prototype, "onListChanged", {
michael@0 1196 enumerable: true, configurable: true,
michael@0 1197 get: function() { return this._onListChanged; },
michael@0 1198 set: function(v) {
michael@0 1199 if (v !== null && typeof v != "function") {
michael@0 1200 throw Error("onListChanged property may only be set to 'null' or a function");
michael@0 1201 }
michael@0 1202 this._onListChanged = v;
michael@0 1203 if (this._onListChanged) {
michael@0 1204 AddonManager.addAddonListener(this);
michael@0 1205 } else {
michael@0 1206 AddonManager.removeAddonListener(this);
michael@0 1207 }
michael@0 1208 }
michael@0 1209 });
michael@0 1210
michael@0 1211 BrowserAddonList.prototype.onInstalled = function (aAddon) {
michael@0 1212 this._onListChanged();
michael@0 1213 };
michael@0 1214
michael@0 1215 BrowserAddonList.prototype.onUninstalled = function (aAddon) {
michael@0 1216 this._actorByAddonId.delete(aAddon.id);
michael@0 1217 this._onListChanged();
michael@0 1218 };
michael@0 1219
michael@0 1220 function BrowserAddonActor(aConnection, aAddon) {
michael@0 1221 this.conn = aConnection;
michael@0 1222 this._addon = aAddon;
michael@0 1223 this._contextPool = null;
michael@0 1224 this._threadActor = null;
michael@0 1225 this._global = null;
michael@0 1226 AddonManager.addAddonListener(this);
michael@0 1227 }
michael@0 1228
michael@0 1229 BrowserAddonActor.prototype = {
michael@0 1230 actorPrefix: "addon",
michael@0 1231
michael@0 1232 get exited() {
michael@0 1233 return !this._addon;
michael@0 1234 },
michael@0 1235
michael@0 1236 get id() {
michael@0 1237 return this._addon.id;
michael@0 1238 },
michael@0 1239
michael@0 1240 get url() {
michael@0 1241 return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined;
michael@0 1242 },
michael@0 1243
michael@0 1244 get attached() {
michael@0 1245 return this._threadActor;
michael@0 1246 },
michael@0 1247
michael@0 1248 get global() {
michael@0 1249 return this._global;
michael@0 1250 },
michael@0 1251
michael@0 1252 form: function BAA_form() {
michael@0 1253 dbg_assert(this.actorID, "addon should have an actorID.");
michael@0 1254
michael@0 1255 return {
michael@0 1256 actor: this.actorID,
michael@0 1257 id: this.id,
michael@0 1258 name: this._addon.name,
michael@0 1259 url: this.url,
michael@0 1260 debuggable: this._addon.isDebuggable,
michael@0 1261 };
michael@0 1262 },
michael@0 1263
michael@0 1264 disconnect: function BAA_disconnect() {
michael@0 1265 this._addon = null;
michael@0 1266 this._global = null;
michael@0 1267 AddonManager.removeAddonListener(this);
michael@0 1268 },
michael@0 1269
michael@0 1270 setOptions: function BAA_setOptions(aOptions) {
michael@0 1271 if ("global" in aOptions) {
michael@0 1272 this._global = aOptions.global;
michael@0 1273 }
michael@0 1274 },
michael@0 1275
michael@0 1276 onDisabled: function BAA_onDisabled(aAddon) {
michael@0 1277 if (aAddon != this._addon) {
michael@0 1278 return;
michael@0 1279 }
michael@0 1280
michael@0 1281 this._global = null;
michael@0 1282 },
michael@0 1283
michael@0 1284 onUninstalled: function BAA_onUninstalled(aAddon) {
michael@0 1285 if (aAddon != this._addon) {
michael@0 1286 return;
michael@0 1287 }
michael@0 1288
michael@0 1289 if (this.attached) {
michael@0 1290 this.onDetach();
michael@0 1291 this.conn.send({ from: this.actorID, type: "tabDetached" });
michael@0 1292 }
michael@0 1293
michael@0 1294 this.disconnect();
michael@0 1295 },
michael@0 1296
michael@0 1297 onAttach: function BAA_onAttach() {
michael@0 1298 if (this.exited) {
michael@0 1299 return { type: "exited" };
michael@0 1300 }
michael@0 1301
michael@0 1302 if (!this.attached) {
michael@0 1303 this._contextPool = new ActorPool(this.conn);
michael@0 1304 this.conn.addActorPool(this._contextPool);
michael@0 1305
michael@0 1306 this._threadActor = new AddonThreadActor(this.conn, this,
michael@0 1307 this._addon.id);
michael@0 1308 this._contextPool.addActor(this._threadActor);
michael@0 1309 }
michael@0 1310
michael@0 1311 return { type: "tabAttached", threadActor: this._threadActor.actorID };
michael@0 1312 },
michael@0 1313
michael@0 1314 onDetach: function BAA_onDetach() {
michael@0 1315 if (!this.attached) {
michael@0 1316 return { error: "wrongState" };
michael@0 1317 }
michael@0 1318
michael@0 1319 this.conn.removeActorPool(this._contextPool);
michael@0 1320 this._contextPool = null;
michael@0 1321
michael@0 1322 this._threadActor = null;
michael@0 1323
michael@0 1324 return { type: "detached" };
michael@0 1325 },
michael@0 1326
michael@0 1327 preNest: function() {
michael@0 1328 let e = Services.wm.getEnumerator(null);
michael@0 1329 while (e.hasMoreElements()) {
michael@0 1330 let win = e.getNext();
michael@0 1331 let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1332 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 1333 windowUtils.suppressEventHandling(true);
michael@0 1334 windowUtils.suspendTimeouts();
michael@0 1335 }
michael@0 1336 },
michael@0 1337
michael@0 1338 postNest: function() {
michael@0 1339 let e = Services.wm.getEnumerator(null);
michael@0 1340 while (e.hasMoreElements()) {
michael@0 1341 let win = e.getNext();
michael@0 1342 let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1343 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 1344 windowUtils.resumeTimeouts();
michael@0 1345 windowUtils.suppressEventHandling(false);
michael@0 1346 }
michael@0 1347 }
michael@0 1348 };
michael@0 1349
michael@0 1350 BrowserAddonActor.prototype.requestTypes = {
michael@0 1351 "attach": BrowserAddonActor.prototype.onAttach,
michael@0 1352 "detach": BrowserAddonActor.prototype.onDetach
michael@0 1353 };
michael@0 1354
michael@0 1355 /**
michael@0 1356 * The DebuggerProgressListener object is an nsIWebProgressListener which
michael@0 1357 * handles onStateChange events for the inspected browser. If the user tries to
michael@0 1358 * navigate away from a paused page, the listener makes sure that the debuggee
michael@0 1359 * is resumed before the navigation begins.
michael@0 1360 *
michael@0 1361 * @param TabActor aTabActor
michael@0 1362 * The tab actor associated with this listener.
michael@0 1363 */
michael@0 1364 function DebuggerProgressListener(aTabActor) {
michael@0 1365 this._tabActor = aTabActor;
michael@0 1366 this._onWindowCreated = this.onWindowCreated.bind(this);
michael@0 1367 }
michael@0 1368
michael@0 1369 DebuggerProgressListener.prototype = {
michael@0 1370 QueryInterface: XPCOMUtils.generateQI([
michael@0 1371 Ci.nsIWebProgressListener,
michael@0 1372 Ci.nsISupportsWeakReference,
michael@0 1373 Ci.nsISupports,
michael@0 1374 ]),
michael@0 1375
michael@0 1376 watch: function DPL_watch(docShell) {
michael@0 1377 let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1378 .getInterface(Ci.nsIWebProgress);
michael@0 1379 webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATUS |
michael@0 1380 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
michael@0 1381 Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
michael@0 1382
michael@0 1383 // TODO: fix docShell.chromeEventHandler in child processes!
michael@0 1384 let chromeEventHandler = docShell.chromeEventHandler ||
michael@0 1385 docShell.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1386 .getInterface(Ci.nsIContentFrameMessageManager);
michael@0 1387
michael@0 1388 // Watch for globals being created in this docshell tree.
michael@0 1389 chromeEventHandler.addEventListener("DOMWindowCreated",
michael@0 1390 this._onWindowCreated, true);
michael@0 1391 chromeEventHandler.addEventListener("pageshow",
michael@0 1392 this._onWindowCreated, true);
michael@0 1393 },
michael@0 1394
michael@0 1395 unwatch: function DPL_unwatch(docShell) {
michael@0 1396 let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1397 .getInterface(Ci.nsIWebProgress);
michael@0 1398 webProgress.removeProgressListener(this);
michael@0 1399
michael@0 1400 // TODO: fix docShell.chromeEventHandler in child processes!
michael@0 1401 let chromeEventHandler = docShell.chromeEventHandler ||
michael@0 1402 docShell.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1403 .getInterface(Ci.nsIContentFrameMessageManager);
michael@0 1404 chromeEventHandler.removeEventListener("DOMWindowCreated",
michael@0 1405 this._onWindowCreated, true);
michael@0 1406 chromeEventHandler.removeEventListener("pageshow",
michael@0 1407 this._onWindowCreated, true);
michael@0 1408 },
michael@0 1409
michael@0 1410 onWindowCreated:
michael@0 1411 DevToolsUtils.makeInfallible(function DPL_onWindowCreated(evt) {
michael@0 1412 // Ignore any event if the tab actor isn't attached.
michael@0 1413 if (!this._tabActor.attached) {
michael@0 1414 return;
michael@0 1415 }
michael@0 1416
michael@0 1417 // pageshow events for non-persisted pages have already been handled by a
michael@0 1418 // prior DOMWindowCreated event.
michael@0 1419 if (evt.type == "pageshow" && !evt.persisted) {
michael@0 1420 return;
michael@0 1421 }
michael@0 1422
michael@0 1423 let window = evt.target.defaultView;
michael@0 1424 this._tabActor._windowReady(window);
michael@0 1425 }, "DebuggerProgressListener.prototype.onWindowCreated"),
michael@0 1426
michael@0 1427 onStateChange:
michael@0 1428 DevToolsUtils.makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) {
michael@0 1429 // Ignore any event if the tab actor isn't attached.
michael@0 1430 if (!this._tabActor.attached) {
michael@0 1431 return;
michael@0 1432 }
michael@0 1433
michael@0 1434 let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START;
michael@0 1435 let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP;
michael@0 1436 let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
michael@0 1437 let isWindow = aFlag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
michael@0 1438
michael@0 1439 let window = aProgress.DOMWindow;
michael@0 1440 if (isDocument && isStart) {
michael@0 1441 let newURI = aRequest instanceof Ci.nsIChannel ? aRequest.URI.spec : null;
michael@0 1442 this._tabActor._willNavigate(window, newURI, aRequest);
michael@0 1443 }
michael@0 1444 if (isWindow && isStop) {
michael@0 1445 this._tabActor._navigate(window);
michael@0 1446 }
michael@0 1447 }, "DebuggerProgressListener.prototype.onStateChange")
michael@0 1448 };

mercurial