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