|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const {Cc, Ci, Cu} = require("chrome"); |
|
8 const {Promise: promise} = require("resource://gre/modules/Promise.jsm"); |
|
9 const EventEmitter = require("devtools/toolkit/event-emitter"); |
|
10 |
|
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
12 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", |
|
13 "resource://gre/modules/devtools/dbg-server.jsm"); |
|
14 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", |
|
15 "resource://gre/modules/devtools/dbg-client.jsm"); |
|
16 |
|
17 const targets = new WeakMap(); |
|
18 const promiseTargets = new WeakMap(); |
|
19 |
|
20 /** |
|
21 * Functions for creating Targets |
|
22 */ |
|
23 exports.TargetFactory = { |
|
24 /** |
|
25 * Construct a Target |
|
26 * @param {XULTab} tab |
|
27 * The tab to use in creating a new target. |
|
28 * |
|
29 * @return A target object |
|
30 */ |
|
31 forTab: function TF_forTab(tab) { |
|
32 let target = targets.get(tab); |
|
33 if (target == null) { |
|
34 target = new TabTarget(tab); |
|
35 targets.set(tab, target); |
|
36 } |
|
37 return target; |
|
38 }, |
|
39 |
|
40 /** |
|
41 * Return a promise of a Target for a remote tab. |
|
42 * @param {Object} options |
|
43 * The options object has the following properties: |
|
44 * { |
|
45 * form: the remote protocol form of a tab, |
|
46 * client: a DebuggerClient instance |
|
47 * (caller owns this and is responsible for closing), |
|
48 * chrome: true if the remote target is the whole process |
|
49 * } |
|
50 * |
|
51 * @return A promise of a target object |
|
52 */ |
|
53 forRemoteTab: function TF_forRemoteTab(options) { |
|
54 let targetPromise = promiseTargets.get(options); |
|
55 if (targetPromise == null) { |
|
56 let target = new TabTarget(options); |
|
57 targetPromise = target.makeRemote().then(() => target); |
|
58 promiseTargets.set(options, targetPromise); |
|
59 } |
|
60 return targetPromise; |
|
61 }, |
|
62 |
|
63 /** |
|
64 * Creating a target for a tab that is being closed is a problem because it |
|
65 * allows a leak as a result of coming after the close event which normally |
|
66 * clears things up. This function allows us to ask if there is a known |
|
67 * target for a tab without creating a target |
|
68 * @return true/false |
|
69 */ |
|
70 isKnownTab: function TF_isKnownTab(tab) { |
|
71 return targets.has(tab); |
|
72 }, |
|
73 |
|
74 /** |
|
75 * Construct a Target |
|
76 * @param {nsIDOMWindow} window |
|
77 * The chromeWindow to use in creating a new target |
|
78 * @return A target object |
|
79 */ |
|
80 forWindow: function TF_forWindow(window) { |
|
81 let target = targets.get(window); |
|
82 if (target == null) { |
|
83 target = new WindowTarget(window); |
|
84 targets.set(window, target); |
|
85 } |
|
86 return target; |
|
87 }, |
|
88 |
|
89 /** |
|
90 * Get all of the targets known to the local browser instance |
|
91 * @return An array of target objects |
|
92 */ |
|
93 allTargets: function TF_allTargets() { |
|
94 let windows = []; |
|
95 let wm = Cc["@mozilla.org/appshell/window-mediator;1"] |
|
96 .getService(Ci.nsIWindowMediator); |
|
97 let en = wm.getXULWindowEnumerator(null); |
|
98 while (en.hasMoreElements()) { |
|
99 windows.push(en.getNext()); |
|
100 } |
|
101 |
|
102 return windows.map(function(window) { |
|
103 return TargetFactory.forWindow(window); |
|
104 }); |
|
105 }, |
|
106 }; |
|
107 |
|
108 /** |
|
109 * The 'version' property allows the developer tools equivalent of browser |
|
110 * detection. Browser detection is evil, however while we don't know what we |
|
111 * will need to detect in the future, it is an easy way to postpone work. |
|
112 * We should be looking to use 'supports()' in place of version where |
|
113 * possible. |
|
114 */ |
|
115 function getVersion() { |
|
116 // FIXME: return something better |
|
117 return 20; |
|
118 } |
|
119 |
|
120 /** |
|
121 * A better way to support feature detection, but we're not yet at a place |
|
122 * where we have the features well enough defined for this to make lots of |
|
123 * sense. |
|
124 */ |
|
125 function supports(feature) { |
|
126 // FIXME: return something better |
|
127 return false; |
|
128 }; |
|
129 |
|
130 /** |
|
131 * A Target represents something that we can debug. Targets are generally |
|
132 * read-only. Any changes that you wish to make to a target should be done via |
|
133 * a Tool that attaches to the target. i.e. a Target is just a pointer saying |
|
134 * "the thing to debug is over there". |
|
135 * |
|
136 * Providing a generalized abstraction of a web-page or web-browser (available |
|
137 * either locally or remotely) is beyond the scope of this class (and maybe |
|
138 * also beyond the scope of this universe) However Target does attempt to |
|
139 * abstract some common events and read-only properties common to many Tools. |
|
140 * |
|
141 * Supported read-only properties: |
|
142 * - name, isRemote, url |
|
143 * |
|
144 * Target extends EventEmitter and provides support for the following events: |
|
145 * - close: The target window has been closed. All tools attached to this |
|
146 * target should close. This event is not currently cancelable. |
|
147 * - navigate: The target window has navigated to a different URL |
|
148 * |
|
149 * Optional events: |
|
150 * - will-navigate: The target window will navigate to a different URL |
|
151 * - hidden: The target is not visible anymore (for TargetTab, another tab is selected) |
|
152 * - visible: The target is visible (for TargetTab, tab is selected) |
|
153 * |
|
154 * Target also supports 2 functions to help allow 2 different versions of |
|
155 * Firefox debug each other. The 'version' property is the equivalent of |
|
156 * browser detection - simple and easy to implement but gets fragile when things |
|
157 * are not quite what they seem. The 'supports' property is the equivalent of |
|
158 * feature detection - harder to setup, but more robust long-term. |
|
159 * |
|
160 * Comparing Targets: 2 instances of a Target object can point at the same |
|
161 * thing, so t1 !== t2 and t1 != t2 even when they represent the same object. |
|
162 * To compare to targets use 't1.equals(t2)'. |
|
163 */ |
|
164 function Target() { |
|
165 throw new Error("Use TargetFactory.newXXX or Target.getXXX to create a Target in place of 'new Target()'"); |
|
166 } |
|
167 |
|
168 Object.defineProperty(Target.prototype, "version", { |
|
169 get: getVersion, |
|
170 enumerable: true |
|
171 }); |
|
172 |
|
173 |
|
174 /** |
|
175 * A TabTarget represents a page living in a browser tab. Generally these will |
|
176 * be web pages served over http(s), but they don't have to be. |
|
177 */ |
|
178 function TabTarget(tab) { |
|
179 EventEmitter.decorate(this); |
|
180 this.destroy = this.destroy.bind(this); |
|
181 this._handleThreadState = this._handleThreadState.bind(this); |
|
182 this.on("thread-resumed", this._handleThreadState); |
|
183 this.on("thread-paused", this._handleThreadState); |
|
184 // Only real tabs need initialization here. Placeholder objects for remote |
|
185 // targets will be initialized after a makeRemote method call. |
|
186 if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) { |
|
187 this._tab = tab; |
|
188 this._setupListeners(); |
|
189 } else { |
|
190 this._form = tab.form; |
|
191 this._client = tab.client; |
|
192 this._chrome = tab.chrome; |
|
193 } |
|
194 } |
|
195 |
|
196 TabTarget.prototype = { |
|
197 _webProgressListener: null, |
|
198 |
|
199 supports: supports, |
|
200 get version() { return getVersion(); }, |
|
201 |
|
202 get tab() { |
|
203 return this._tab; |
|
204 }, |
|
205 |
|
206 get form() { |
|
207 return this._form; |
|
208 }, |
|
209 |
|
210 get root() { |
|
211 return this._root; |
|
212 }, |
|
213 |
|
214 get client() { |
|
215 return this._client; |
|
216 }, |
|
217 |
|
218 get chrome() { |
|
219 return this._chrome; |
|
220 }, |
|
221 |
|
222 get window() { |
|
223 // Be extra careful here, since this may be called by HS_getHudByWindow |
|
224 // during shutdown. |
|
225 if (this._tab && this._tab.linkedBrowser) { |
|
226 return this._tab.linkedBrowser.contentWindow; |
|
227 } |
|
228 return null; |
|
229 }, |
|
230 |
|
231 get name() { |
|
232 return this._tab ? this._tab.linkedBrowser.contentDocument.title : |
|
233 this._form.title; |
|
234 }, |
|
235 |
|
236 get url() { |
|
237 return this._tab ? this._tab.linkedBrowser.contentDocument.location.href : |
|
238 this._form.url; |
|
239 }, |
|
240 |
|
241 get isRemote() { |
|
242 return !this.isLocalTab; |
|
243 }, |
|
244 |
|
245 get isAddon() { |
|
246 return !!(this._form && this._form.addonActor); |
|
247 }, |
|
248 |
|
249 get isLocalTab() { |
|
250 return !!this._tab; |
|
251 }, |
|
252 |
|
253 get isThreadPaused() { |
|
254 return !!this._isThreadPaused; |
|
255 }, |
|
256 |
|
257 /** |
|
258 * Adds remote protocol capabilities to the target, so that it can be used |
|
259 * for tools that support the Remote Debugging Protocol even for local |
|
260 * connections. |
|
261 */ |
|
262 makeRemote: function TabTarget_makeRemote() { |
|
263 if (this._remote) { |
|
264 return this._remote.promise; |
|
265 } |
|
266 |
|
267 this._remote = promise.defer(); |
|
268 |
|
269 if (this.isLocalTab) { |
|
270 // Since a remote protocol connection will be made, let's start the |
|
271 // DebuggerServer here, once and for all tools. |
|
272 if (!DebuggerServer.initialized) { |
|
273 DebuggerServer.init(); |
|
274 DebuggerServer.addBrowserActors(); |
|
275 } |
|
276 |
|
277 this._client = new DebuggerClient(DebuggerServer.connectPipe()); |
|
278 // A local TabTarget will never perform chrome debugging. |
|
279 this._chrome = false; |
|
280 } |
|
281 |
|
282 this._setupRemoteListeners(); |
|
283 |
|
284 let attachTab = () => { |
|
285 this._client.attachTab(this._form.actor, (aResponse, aTabClient) => { |
|
286 if (!aTabClient) { |
|
287 this._remote.reject("Unable to attach to the tab"); |
|
288 return; |
|
289 } |
|
290 this.activeTab = aTabClient; |
|
291 this.threadActor = aResponse.threadActor; |
|
292 this._remote.resolve(null); |
|
293 }); |
|
294 }; |
|
295 |
|
296 if (this.isLocalTab) { |
|
297 this._client.connect((aType, aTraits) => { |
|
298 this._client.listTabs(aResponse => { |
|
299 this._root = aResponse; |
|
300 |
|
301 let windowUtils = this.window |
|
302 .QueryInterface(Ci.nsIInterfaceRequestor) |
|
303 .getInterface(Ci.nsIDOMWindowUtils); |
|
304 let outerWindow = windowUtils.outerWindowID; |
|
305 aResponse.tabs.some((tab) => { |
|
306 if (tab.outerWindowID === outerWindow) { |
|
307 this._form = tab; |
|
308 return true; |
|
309 } |
|
310 return false; |
|
311 }); |
|
312 if (!this._form) { |
|
313 this._form = aResponse.tabs[aResponse.selected]; |
|
314 } |
|
315 attachTab(); |
|
316 }); |
|
317 }); |
|
318 } else if (!this.chrome) { |
|
319 // In the remote debugging case, the protocol connection will have been |
|
320 // already initialized in the connection screen code. |
|
321 attachTab(); |
|
322 } else { |
|
323 // Remote chrome debugging doesn't need anything at this point. |
|
324 this._remote.resolve(null); |
|
325 } |
|
326 |
|
327 return this._remote.promise; |
|
328 }, |
|
329 |
|
330 /** |
|
331 * Listen to the different events. |
|
332 */ |
|
333 _setupListeners: function TabTarget__setupListeners() { |
|
334 this._webProgressListener = new TabWebProgressListener(this); |
|
335 this.tab.linkedBrowser.addProgressListener(this._webProgressListener); |
|
336 this.tab.addEventListener("TabClose", this); |
|
337 this.tab.parentNode.addEventListener("TabSelect", this); |
|
338 this.tab.ownerDocument.defaultView.addEventListener("unload", this); |
|
339 }, |
|
340 |
|
341 /** |
|
342 * Teardown event listeners. |
|
343 */ |
|
344 _teardownListeners: function TabTarget__teardownListeners() { |
|
345 if (this._webProgressListener) { |
|
346 this._webProgressListener.destroy(); |
|
347 } |
|
348 |
|
349 this._tab.ownerDocument.defaultView.removeEventListener("unload", this); |
|
350 this._tab.removeEventListener("TabClose", this); |
|
351 this._tab.parentNode.removeEventListener("TabSelect", this); |
|
352 }, |
|
353 |
|
354 /** |
|
355 * Setup listeners for remote debugging, updating existing ones as necessary. |
|
356 */ |
|
357 _setupRemoteListeners: function TabTarget__setupRemoteListeners() { |
|
358 this.client.addListener("closed", this.destroy); |
|
359 |
|
360 this._onTabDetached = (aType, aPacket) => { |
|
361 // We have to filter message to ensure that this detach is for this tab |
|
362 if (aPacket.from == this._form.actor) { |
|
363 this.destroy(); |
|
364 } |
|
365 }; |
|
366 this.client.addListener("tabDetached", this._onTabDetached); |
|
367 |
|
368 this._onTabNavigated = function onRemoteTabNavigated(aType, aPacket) { |
|
369 let event = Object.create(null); |
|
370 event.url = aPacket.url; |
|
371 event.title = aPacket.title; |
|
372 event.nativeConsoleAPI = aPacket.nativeConsoleAPI; |
|
373 // Send any stored event payload (DOMWindow or nsIRequest) for backwards |
|
374 // compatibility with non-remotable tools. |
|
375 if (aPacket.state == "start") { |
|
376 event._navPayload = this._navRequest; |
|
377 this.emit("will-navigate", event); |
|
378 this._navRequest = null; |
|
379 } else { |
|
380 event._navPayload = this._navWindow; |
|
381 this.emit("navigate", event); |
|
382 this._navWindow = null; |
|
383 } |
|
384 }.bind(this); |
|
385 this.client.addListener("tabNavigated", this._onTabNavigated); |
|
386 }, |
|
387 |
|
388 /** |
|
389 * Teardown listeners for remote debugging. |
|
390 */ |
|
391 _teardownRemoteListeners: function TabTarget__teardownRemoteListeners() { |
|
392 this.client.removeListener("closed", this.destroy); |
|
393 this.client.removeListener("tabNavigated", this._onTabNavigated); |
|
394 this.client.removeListener("tabDetached", this._onTabDetached); |
|
395 }, |
|
396 |
|
397 /** |
|
398 * Handle tabs events. |
|
399 */ |
|
400 handleEvent: function (event) { |
|
401 switch (event.type) { |
|
402 case "TabClose": |
|
403 case "unload": |
|
404 this.destroy(); |
|
405 break; |
|
406 case "TabSelect": |
|
407 if (this.tab.selected) { |
|
408 this.emit("visible", event); |
|
409 } else { |
|
410 this.emit("hidden", event); |
|
411 } |
|
412 break; |
|
413 } |
|
414 }, |
|
415 |
|
416 /** |
|
417 * Handle script status. |
|
418 */ |
|
419 _handleThreadState: function(event) { |
|
420 switch (event) { |
|
421 case "thread-resumed": |
|
422 this._isThreadPaused = false; |
|
423 break; |
|
424 case "thread-paused": |
|
425 this._isThreadPaused = true; |
|
426 break; |
|
427 } |
|
428 }, |
|
429 |
|
430 /** |
|
431 * Target is not alive anymore. |
|
432 */ |
|
433 destroy: function() { |
|
434 // If several things call destroy then we give them all the same |
|
435 // destruction promise so we're sure to destroy only once |
|
436 if (this._destroyer) { |
|
437 return this._destroyer.promise; |
|
438 } |
|
439 |
|
440 this._destroyer = promise.defer(); |
|
441 |
|
442 // Before taking any action, notify listeners that destruction is imminent. |
|
443 this.emit("close"); |
|
444 |
|
445 // First of all, do cleanup tasks that pertain to both remoted and |
|
446 // non-remoted targets. |
|
447 this.off("thread-resumed", this._handleThreadState); |
|
448 this.off("thread-paused", this._handleThreadState); |
|
449 |
|
450 if (this._tab) { |
|
451 this._teardownListeners(); |
|
452 } |
|
453 |
|
454 let cleanupAndResolve = () => { |
|
455 this._cleanup(); |
|
456 this._destroyer.resolve(null); |
|
457 }; |
|
458 // If this target was not remoted, the promise will be resolved before the |
|
459 // function returns. |
|
460 if (this._tab && !this._client) { |
|
461 cleanupAndResolve(); |
|
462 } else if (this._client) { |
|
463 // If, on the other hand, this target was remoted, the promise will be |
|
464 // resolved after the remote connection is closed. |
|
465 this._teardownRemoteListeners(); |
|
466 |
|
467 if (this.isLocalTab) { |
|
468 // We started with a local tab and created the client ourselves, so we |
|
469 // should close it. |
|
470 this._client.close(cleanupAndResolve); |
|
471 } else { |
|
472 // The client was handed to us, so we are not responsible for closing |
|
473 // it. We just need to detach from the tab, if already attached. |
|
474 if (this.activeTab) { |
|
475 this.activeTab.detach(cleanupAndResolve); |
|
476 } else { |
|
477 cleanupAndResolve(); |
|
478 } |
|
479 } |
|
480 } |
|
481 |
|
482 return this._destroyer.promise; |
|
483 }, |
|
484 |
|
485 /** |
|
486 * Clean up references to what this target points to. |
|
487 */ |
|
488 _cleanup: function TabTarget__cleanup() { |
|
489 if (this._tab) { |
|
490 targets.delete(this._tab); |
|
491 } else { |
|
492 promiseTargets.delete(this._form); |
|
493 } |
|
494 this.activeTab = null; |
|
495 this._client = null; |
|
496 this._tab = null; |
|
497 this._form = null; |
|
498 this._remote = null; |
|
499 }, |
|
500 |
|
501 toString: function() { |
|
502 return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor)); |
|
503 }, |
|
504 }; |
|
505 |
|
506 |
|
507 /** |
|
508 * WebProgressListener for TabTarget. |
|
509 * |
|
510 * @param object aTarget |
|
511 * The TabTarget instance to work with. |
|
512 */ |
|
513 function TabWebProgressListener(aTarget) { |
|
514 this.target = aTarget; |
|
515 } |
|
516 |
|
517 TabWebProgressListener.prototype = { |
|
518 target: null, |
|
519 |
|
520 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), |
|
521 |
|
522 onStateChange: function TWPL_onStateChange(progress, request, flag, status) { |
|
523 let isStart = flag & Ci.nsIWebProgressListener.STATE_START; |
|
524 let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; |
|
525 let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK; |
|
526 let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST; |
|
527 |
|
528 // Skip non-interesting states. |
|
529 if (!isStart || !isDocument || !isRequest || !isNetwork) { |
|
530 return; |
|
531 } |
|
532 |
|
533 // emit event if the top frame is navigating |
|
534 if (this.target && this.target.window == progress.DOMWindow) { |
|
535 // Emit the event if the target is not remoted or store the payload for |
|
536 // later emission otherwise. |
|
537 if (this.target._client) { |
|
538 this.target._navRequest = request; |
|
539 } else { |
|
540 this.target.emit("will-navigate", request); |
|
541 } |
|
542 } |
|
543 }, |
|
544 |
|
545 onProgressChange: function() {}, |
|
546 onSecurityChange: function() {}, |
|
547 onStatusChange: function() {}, |
|
548 |
|
549 onLocationChange: function TWPL_onLocationChange(webProgress, request, URI, flags) { |
|
550 if (this.target && |
|
551 !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { |
|
552 let window = webProgress.DOMWindow; |
|
553 // Emit the event if the target is not remoted or store the payload for |
|
554 // later emission otherwise. |
|
555 if (this.target._client) { |
|
556 this.target._navWindow = window; |
|
557 } else { |
|
558 this.target.emit("navigate", window); |
|
559 } |
|
560 } |
|
561 }, |
|
562 |
|
563 /** |
|
564 * Destroy the progress listener instance. |
|
565 */ |
|
566 destroy: function TWPL_destroy() { |
|
567 if (this.target.tab) { |
|
568 try { |
|
569 this.target.tab.linkedBrowser.removeProgressListener(this); |
|
570 } catch (ex) { |
|
571 // This can throw when a tab crashes in e10s. |
|
572 } |
|
573 } |
|
574 this.target._webProgressListener = null; |
|
575 this.target._navRequest = null; |
|
576 this.target._navWindow = null; |
|
577 this.target = null; |
|
578 } |
|
579 }; |
|
580 |
|
581 |
|
582 /** |
|
583 * A WindowTarget represents a page living in a xul window or panel. Generally |
|
584 * these will have a chrome: URL |
|
585 */ |
|
586 function WindowTarget(window) { |
|
587 EventEmitter.decorate(this); |
|
588 this._window = window; |
|
589 this._setupListeners(); |
|
590 } |
|
591 |
|
592 WindowTarget.prototype = { |
|
593 supports: supports, |
|
594 get version() { return getVersion(); }, |
|
595 |
|
596 get window() { |
|
597 return this._window; |
|
598 }, |
|
599 |
|
600 get name() { |
|
601 return this._window.document.title; |
|
602 }, |
|
603 |
|
604 get url() { |
|
605 return this._window.document.location.href; |
|
606 }, |
|
607 |
|
608 get isRemote() { |
|
609 return false; |
|
610 }, |
|
611 |
|
612 get isLocalTab() { |
|
613 return false; |
|
614 }, |
|
615 |
|
616 get isThreadPaused() { |
|
617 return !!this._isThreadPaused; |
|
618 }, |
|
619 |
|
620 /** |
|
621 * Listen to the different events. |
|
622 */ |
|
623 _setupListeners: function() { |
|
624 this._handleThreadState = this._handleThreadState.bind(this); |
|
625 this.on("thread-paused", this._handleThreadState); |
|
626 this.on("thread-resumed", this._handleThreadState); |
|
627 }, |
|
628 |
|
629 _handleThreadState: function(event) { |
|
630 switch (event) { |
|
631 case "thread-resumed": |
|
632 this._isThreadPaused = false; |
|
633 break; |
|
634 case "thread-paused": |
|
635 this._isThreadPaused = true; |
|
636 break; |
|
637 } |
|
638 }, |
|
639 |
|
640 /** |
|
641 * Target is not alive anymore. |
|
642 */ |
|
643 destroy: function() { |
|
644 if (!this._destroyed) { |
|
645 this._destroyed = true; |
|
646 |
|
647 this.off("thread-paused", this._handleThreadState); |
|
648 this.off("thread-resumed", this._handleThreadState); |
|
649 this.emit("close"); |
|
650 |
|
651 targets.delete(this._window); |
|
652 this._window = null; |
|
653 } |
|
654 |
|
655 return promise.resolve(null); |
|
656 }, |
|
657 |
|
658 toString: function() { |
|
659 return 'WindowTarget:' + this.window; |
|
660 }, |
|
661 }; |