|
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/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 let {Ci,Cu} = require("chrome"); |
|
10 let {createExtraActors, appendExtraActors} = require("devtools/server/actors/common"); |
|
11 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); |
|
12 |
|
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"); |
|
16 |
|
17 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); |
|
18 |
|
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 }); |
|
25 |
|
26 // Also depends on following symbols, shared by common scope with main.js: |
|
27 // DebuggerServer, CommonCreateExtraActors, CommonAppendExtraActors, ActorPool, |
|
28 // ThreadActor |
|
29 |
|
30 /** |
|
31 * Browser-specific actors. |
|
32 */ |
|
33 |
|
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 } |
|
46 |
|
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 } |
|
54 |
|
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 } |
|
65 |
|
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 } |
|
87 |
|
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; |
|
161 |
|
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(); |
|
189 |
|
190 /* The current onListChanged handler, or null. */ |
|
191 this._onListChanged = null; |
|
192 |
|
193 /* |
|
194 * True if we've been iterated over since we last called our onListChanged |
|
195 * hook. |
|
196 */ |
|
197 this._mustNotify = false; |
|
198 |
|
199 /* True if we're testing, and should throw if consistency checks fail. */ |
|
200 this._testing = false; |
|
201 } |
|
202 |
|
203 BrowserTabList.prototype.constructor = BrowserTabList; |
|
204 |
|
205 |
|
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 }; |
|
219 |
|
220 BrowserTabList.prototype._getChildren = function(aWindow) { |
|
221 return aWindow.gBrowser.browsers; |
|
222 }; |
|
223 |
|
224 BrowserTabList.prototype.getList = function() { |
|
225 let topXULWindow = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType); |
|
226 |
|
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; |
|
231 |
|
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. |
|
236 |
|
237 let actorPromises = []; |
|
238 |
|
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 } |
|
245 |
|
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 } |
|
269 |
|
270 // Set the 'selected' properties on all actors correctly. |
|
271 actor.selected = (win === topXULWindow && browser === selectedBrowser); |
|
272 } |
|
273 } |
|
274 |
|
275 if (this._testing && initialMapSize !== foundCount) |
|
276 throw Error("_actorByBrowser map contained actors for dead tabs"); |
|
277 |
|
278 this._mustNotify = true; |
|
279 this._checkListening(); |
|
280 |
|
281 return promise.all(actorPromises); |
|
282 }; |
|
283 |
|
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 }); |
|
295 |
|
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 }; |
|
308 |
|
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 } |
|
322 |
|
323 this._actorByBrowser.delete(aBrowser); |
|
324 aActor.exit(); |
|
325 |
|
326 this._notifyListChanged(); |
|
327 this._checkListening(); |
|
328 }; |
|
329 |
|
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"]); |
|
349 |
|
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"]); |
|
353 |
|
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 }; |
|
362 |
|
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 }; |
|
385 |
|
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"); |
|
406 |
|
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 }; |
|
418 |
|
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 = () => { }; |
|
429 |
|
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); |
|
434 |
|
435 if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType) |
|
436 return; |
|
437 |
|
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 } |
|
446 |
|
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 }); |
|
452 |
|
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); |
|
461 |
|
462 aWindow.addEventListener("load", handleLoad, false); |
|
463 }, "BrowserTabList.prototype.onOpenWindow"); |
|
464 |
|
465 BrowserTabList.prototype.onCloseWindow = DevToolsUtils.makeInfallible(function(aWindow) { |
|
466 aWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
|
467 .getInterface(Ci.nsIDOMWindow); |
|
468 |
|
469 if (appShellDOMWindowType(aWindow) !== DebuggerServer.chromeWindowType) |
|
470 return; |
|
471 |
|
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"); |
|
490 |
|
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; |
|
512 |
|
513 this.traits = { reconfigure: true }; |
|
514 } |
|
515 |
|
516 // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a |
|
517 // *complete* mess, needs to be rethought asap. |
|
518 |
|
519 TabActor.prototype = { |
|
520 traits: null, |
|
521 |
|
522 get exited() { return this._exited; }, |
|
523 get attached() { return !!this._attached; }, |
|
524 |
|
525 _tabPool: null, |
|
526 get tabActorPool() { return this._tabPool; }, |
|
527 |
|
528 _contextPool: null, |
|
529 get contextActorPool() { return this._contextPool; }, |
|
530 |
|
531 _pendingNavigation: null, |
|
532 |
|
533 // A constant prefix that will be used to form the actor ID by the server. |
|
534 actorPrefix: "tab", |
|
535 |
|
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 }, |
|
545 |
|
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 }, |
|
554 |
|
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 }, |
|
561 |
|
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 }, |
|
570 |
|
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 }, |
|
579 |
|
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 }, |
|
588 |
|
589 /** |
|
590 * Getter for the tab's document. |
|
591 */ |
|
592 get contentDocument() { |
|
593 return this.webNavigation.document; |
|
594 }, |
|
595 |
|
596 /** |
|
597 * Getter for the tab title. |
|
598 * @return string |
|
599 * Tab title. |
|
600 */ |
|
601 get title() { |
|
602 return this.contentDocument.contentTitle; |
|
603 }, |
|
604 |
|
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 }, |
|
618 |
|
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."); |
|
624 |
|
625 let windowUtils = this.window |
|
626 .QueryInterface(Ci.nsIInterfaceRequestor) |
|
627 .getInterface(Ci.nsIDOMWindowUtils); |
|
628 |
|
629 let response = { |
|
630 actor: this.actorID, |
|
631 title: this.title, |
|
632 url: this.url, |
|
633 outerWindowID: windowUtils.outerWindowID |
|
634 }; |
|
635 |
|
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 } |
|
643 |
|
644 this._appendExtraActors(response); |
|
645 return response; |
|
646 }, |
|
647 |
|
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 }, |
|
656 |
|
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 } |
|
664 |
|
665 if (this._detach()) { |
|
666 this.conn.send({ from: this.actorID, |
|
667 type: "tabDetached" }); |
|
668 } |
|
669 |
|
670 this._exited = true; |
|
671 }, |
|
672 |
|
673 /* Support for DebuggerServer.addTabActor. */ |
|
674 _createExtraActors: createExtraActors, |
|
675 _appendExtraActors: appendExtraActors, |
|
676 |
|
677 /** |
|
678 * Does the actual work of attching to a tab. |
|
679 */ |
|
680 _attach: function BTA_attach() { |
|
681 if (this._attached) { |
|
682 return; |
|
683 } |
|
684 |
|
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); |
|
689 |
|
690 // ... and a pool for context-lifetime actors. |
|
691 this._pushContext(); |
|
692 |
|
693 this._progressListener = new DebuggerProgressListener(this); |
|
694 this._progressListener.watch(this.docShell); |
|
695 |
|
696 this._attached = true; |
|
697 }, |
|
698 |
|
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"); |
|
705 |
|
706 this._contextPool = new ActorPool(this.conn); |
|
707 this.conn.addActorPool(this._contextPool); |
|
708 |
|
709 this.threadActor = new ThreadActor(this, this.window); |
|
710 this._contextPool.addActor(this.threadActor); |
|
711 }, |
|
712 |
|
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."); |
|
719 |
|
720 this.conn.removeActorPool(this._contextPool); |
|
721 this._contextPool = null; |
|
722 this.threadActor.exit(); |
|
723 this.threadActor = null; |
|
724 }, |
|
725 |
|
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 } |
|
735 |
|
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; |
|
742 |
|
743 this._popContext(); |
|
744 |
|
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 } |
|
752 |
|
753 this._attached = false; |
|
754 return true; |
|
755 }, |
|
756 |
|
757 // Protocol Request Handlers |
|
758 |
|
759 onAttach: function BTA_onAttach(aRequest) { |
|
760 if (this.exited) { |
|
761 return { type: "exited" }; |
|
762 } |
|
763 |
|
764 this._attach(); |
|
765 |
|
766 return { |
|
767 type: "tabAttached", |
|
768 threadActor: this.threadActor.actorID, |
|
769 cacheEnabled: this._getCacheEnabled(), |
|
770 javascriptEnabled: this._getJavascriptEnabled(), |
|
771 traits: this.traits, |
|
772 }; |
|
773 }, |
|
774 |
|
775 onDetach: function BTA_onDetach(aRequest) { |
|
776 if (!this._detach()) { |
|
777 return { error: "wrongState" }; |
|
778 } |
|
779 |
|
780 return { type: "detached" }; |
|
781 }, |
|
782 |
|
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 }, |
|
794 |
|
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 }, |
|
806 |
|
807 /** |
|
808 * Reconfigure options. |
|
809 */ |
|
810 onReconfigure: function (aRequest) { |
|
811 let options = aRequest.options || {}; |
|
812 |
|
813 this._toggleJsOrCache(options); |
|
814 return {}; |
|
815 }, |
|
816 |
|
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; |
|
824 |
|
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 } |
|
835 |
|
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 }, |
|
845 |
|
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 }, |
|
857 |
|
858 /** |
|
859 * Disable or enable JS via docShell. |
|
860 */ |
|
861 _setJavascriptEnabled: function(allow) { |
|
862 if (this.docShell) { |
|
863 this.docShell.allowJavascript = allow; |
|
864 } |
|
865 }, |
|
866 |
|
867 /** |
|
868 * Return cache allowed status. |
|
869 */ |
|
870 _getCacheEnabled: function() { |
|
871 if (!this.docShell) { |
|
872 // The tab is already closed. |
|
873 return null; |
|
874 } |
|
875 |
|
876 let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE | |
|
877 Ci.nsIRequest.INHIBIT_CACHING; |
|
878 return this.docShell.defaultLoadFlags !== disable; |
|
879 }, |
|
880 |
|
881 /** |
|
882 * Return JS allowed status. |
|
883 */ |
|
884 _getJavascriptEnabled: function() { |
|
885 if (!this.docShell) { |
|
886 // The tab is already closed. |
|
887 return null; |
|
888 } |
|
889 |
|
890 return this.docShell.allowJavascript; |
|
891 }, |
|
892 |
|
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 }, |
|
907 |
|
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 }, |
|
926 |
|
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); |
|
935 |
|
936 events.emit(this, "window-ready", { |
|
937 window: window, |
|
938 isTopLevel: isTopLevel |
|
939 }); |
|
940 |
|
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 } |
|
951 |
|
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 }, |
|
958 |
|
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; |
|
965 |
|
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 }); |
|
977 |
|
978 |
|
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 } |
|
984 |
|
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(); |
|
995 |
|
996 this.conn.send({ |
|
997 from: this.actorID, |
|
998 type: "tabNavigated", |
|
999 url: newURI, |
|
1000 nativeConsoleAPI: true, |
|
1001 state: "start" |
|
1002 }); |
|
1003 }, |
|
1004 |
|
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; |
|
1011 |
|
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 }); |
|
1020 |
|
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 } |
|
1026 |
|
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 } |
|
1032 |
|
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 }, |
|
1042 |
|
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 }; |
|
1064 |
|
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 }; |
|
1075 |
|
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 } |
|
1093 |
|
1094 BrowserTabActor.prototype = Object.create(TabActor.prototype); |
|
1095 |
|
1096 BrowserTabActor.prototype.constructor = BrowserTabActor; |
|
1097 |
|
1098 Object.defineProperty(BrowserTabActor.prototype, "docShell", { |
|
1099 get: function() { |
|
1100 return this._browser.docShell; |
|
1101 }, |
|
1102 enumerable: true, |
|
1103 configurable: false |
|
1104 }); |
|
1105 |
|
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 }); |
|
1123 |
|
1124 Object.defineProperty(BrowserTabActor.prototype, "browser", { |
|
1125 get: function() { |
|
1126 return this._browser; |
|
1127 }, |
|
1128 enumerable: true, |
|
1129 configurable: false |
|
1130 }); |
|
1131 |
|
1132 BrowserTabActor.prototype.disconnect = function() { |
|
1133 TabActor.prototype.disconnect.call(this); |
|
1134 this._browser = null; |
|
1135 this._tabbrowser = null; |
|
1136 }; |
|
1137 |
|
1138 BrowserTabActor.prototype.exit = function() { |
|
1139 TabActor.prototype.exit.call(this); |
|
1140 this._browser = null; |
|
1141 this._tabbrowser = null; |
|
1142 }; |
|
1143 |
|
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 } |
|
1158 |
|
1159 RemoteBrowserTabActor.prototype = { |
|
1160 connect: function() { |
|
1161 return DebuggerServer.connectToChild(this._conn, this._browser); |
|
1162 }, |
|
1163 |
|
1164 form: function() { |
|
1165 return this._form; |
|
1166 }, |
|
1167 |
|
1168 exit: function() { |
|
1169 this._browser = null; |
|
1170 }, |
|
1171 }; |
|
1172 |
|
1173 function BrowserAddonList(aConnection) |
|
1174 { |
|
1175 this._connection = aConnection; |
|
1176 this._actorByAddonId = new Map(); |
|
1177 this._onListChanged = null; |
|
1178 } |
|
1179 |
|
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 } |
|
1194 |
|
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 }); |
|
1210 |
|
1211 BrowserAddonList.prototype.onInstalled = function (aAddon) { |
|
1212 this._onListChanged(); |
|
1213 }; |
|
1214 |
|
1215 BrowserAddonList.prototype.onUninstalled = function (aAddon) { |
|
1216 this._actorByAddonId.delete(aAddon.id); |
|
1217 this._onListChanged(); |
|
1218 }; |
|
1219 |
|
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 } |
|
1228 |
|
1229 BrowserAddonActor.prototype = { |
|
1230 actorPrefix: "addon", |
|
1231 |
|
1232 get exited() { |
|
1233 return !this._addon; |
|
1234 }, |
|
1235 |
|
1236 get id() { |
|
1237 return this._addon.id; |
|
1238 }, |
|
1239 |
|
1240 get url() { |
|
1241 return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined; |
|
1242 }, |
|
1243 |
|
1244 get attached() { |
|
1245 return this._threadActor; |
|
1246 }, |
|
1247 |
|
1248 get global() { |
|
1249 return this._global; |
|
1250 }, |
|
1251 |
|
1252 form: function BAA_form() { |
|
1253 dbg_assert(this.actorID, "addon should have an actorID."); |
|
1254 |
|
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 }, |
|
1263 |
|
1264 disconnect: function BAA_disconnect() { |
|
1265 this._addon = null; |
|
1266 this._global = null; |
|
1267 AddonManager.removeAddonListener(this); |
|
1268 }, |
|
1269 |
|
1270 setOptions: function BAA_setOptions(aOptions) { |
|
1271 if ("global" in aOptions) { |
|
1272 this._global = aOptions.global; |
|
1273 } |
|
1274 }, |
|
1275 |
|
1276 onDisabled: function BAA_onDisabled(aAddon) { |
|
1277 if (aAddon != this._addon) { |
|
1278 return; |
|
1279 } |
|
1280 |
|
1281 this._global = null; |
|
1282 }, |
|
1283 |
|
1284 onUninstalled: function BAA_onUninstalled(aAddon) { |
|
1285 if (aAddon != this._addon) { |
|
1286 return; |
|
1287 } |
|
1288 |
|
1289 if (this.attached) { |
|
1290 this.onDetach(); |
|
1291 this.conn.send({ from: this.actorID, type: "tabDetached" }); |
|
1292 } |
|
1293 |
|
1294 this.disconnect(); |
|
1295 }, |
|
1296 |
|
1297 onAttach: function BAA_onAttach() { |
|
1298 if (this.exited) { |
|
1299 return { type: "exited" }; |
|
1300 } |
|
1301 |
|
1302 if (!this.attached) { |
|
1303 this._contextPool = new ActorPool(this.conn); |
|
1304 this.conn.addActorPool(this._contextPool); |
|
1305 |
|
1306 this._threadActor = new AddonThreadActor(this.conn, this, |
|
1307 this._addon.id); |
|
1308 this._contextPool.addActor(this._threadActor); |
|
1309 } |
|
1310 |
|
1311 return { type: "tabAttached", threadActor: this._threadActor.actorID }; |
|
1312 }, |
|
1313 |
|
1314 onDetach: function BAA_onDetach() { |
|
1315 if (!this.attached) { |
|
1316 return { error: "wrongState" }; |
|
1317 } |
|
1318 |
|
1319 this.conn.removeActorPool(this._contextPool); |
|
1320 this._contextPool = null; |
|
1321 |
|
1322 this._threadActor = null; |
|
1323 |
|
1324 return { type: "detached" }; |
|
1325 }, |
|
1326 |
|
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 }, |
|
1337 |
|
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 }; |
|
1349 |
|
1350 BrowserAddonActor.prototype.requestTypes = { |
|
1351 "attach": BrowserAddonActor.prototype.onAttach, |
|
1352 "detach": BrowserAddonActor.prototype.onDetach |
|
1353 }; |
|
1354 |
|
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 } |
|
1368 |
|
1369 DebuggerProgressListener.prototype = { |
|
1370 QueryInterface: XPCOMUtils.generateQI([ |
|
1371 Ci.nsIWebProgressListener, |
|
1372 Ci.nsISupportsWeakReference, |
|
1373 Ci.nsISupports, |
|
1374 ]), |
|
1375 |
|
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); |
|
1382 |
|
1383 // TODO: fix docShell.chromeEventHandler in child processes! |
|
1384 let chromeEventHandler = docShell.chromeEventHandler || |
|
1385 docShell.QueryInterface(Ci.nsIInterfaceRequestor) |
|
1386 .getInterface(Ci.nsIContentFrameMessageManager); |
|
1387 |
|
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 }, |
|
1394 |
|
1395 unwatch: function DPL_unwatch(docShell) { |
|
1396 let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) |
|
1397 .getInterface(Ci.nsIWebProgress); |
|
1398 webProgress.removeProgressListener(this); |
|
1399 |
|
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 }, |
|
1409 |
|
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 } |
|
1416 |
|
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 } |
|
1422 |
|
1423 let window = evt.target.defaultView; |
|
1424 this._tabActor._windowReady(window); |
|
1425 }, "DebuggerProgressListener.prototype.onWindowCreated"), |
|
1426 |
|
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 } |
|
1433 |
|
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; |
|
1438 |
|
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 }; |