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 /* 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/. */
5 "use strict";
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");
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");
17 const targets = new WeakMap();
18 const promiseTargets = new WeakMap();
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 },
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 },
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 },
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 },
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 }
102 return windows.map(function(window) {
103 return TargetFactory.forWindow(window);
104 });
105 },
106 };
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 }
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 };
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 }
168 Object.defineProperty(Target.prototype, "version", {
169 get: getVersion,
170 enumerable: true
171 });
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 }
196 TabTarget.prototype = {
197 _webProgressListener: null,
199 supports: supports,
200 get version() { return getVersion(); },
202 get tab() {
203 return this._tab;
204 },
206 get form() {
207 return this._form;
208 },
210 get root() {
211 return this._root;
212 },
214 get client() {
215 return this._client;
216 },
218 get chrome() {
219 return this._chrome;
220 },
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 },
231 get name() {
232 return this._tab ? this._tab.linkedBrowser.contentDocument.title :
233 this._form.title;
234 },
236 get url() {
237 return this._tab ? this._tab.linkedBrowser.contentDocument.location.href :
238 this._form.url;
239 },
241 get isRemote() {
242 return !this.isLocalTab;
243 },
245 get isAddon() {
246 return !!(this._form && this._form.addonActor);
247 },
249 get isLocalTab() {
250 return !!this._tab;
251 },
253 get isThreadPaused() {
254 return !!this._isThreadPaused;
255 },
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 }
267 this._remote = promise.defer();
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 }
277 this._client = new DebuggerClient(DebuggerServer.connectPipe());
278 // A local TabTarget will never perform chrome debugging.
279 this._chrome = false;
280 }
282 this._setupRemoteListeners();
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 };
296 if (this.isLocalTab) {
297 this._client.connect((aType, aTraits) => {
298 this._client.listTabs(aResponse => {
299 this._root = aResponse;
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 }
327 return this._remote.promise;
328 },
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 },
341 /**
342 * Teardown event listeners.
343 */
344 _teardownListeners: function TabTarget__teardownListeners() {
345 if (this._webProgressListener) {
346 this._webProgressListener.destroy();
347 }
349 this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
350 this._tab.removeEventListener("TabClose", this);
351 this._tab.parentNode.removeEventListener("TabSelect", this);
352 },
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);
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);
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 },
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 },
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 },
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 },
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 }
440 this._destroyer = promise.defer();
442 // Before taking any action, notify listeners that destruction is imminent.
443 this.emit("close");
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);
450 if (this._tab) {
451 this._teardownListeners();
452 }
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();
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 }
482 return this._destroyer.promise;
483 },
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 },
501 toString: function() {
502 return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor));
503 },
504 };
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 }
517 TabWebProgressListener.prototype = {
518 target: null,
520 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
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;
528 // Skip non-interesting states.
529 if (!isStart || !isDocument || !isRequest || !isNetwork) {
530 return;
531 }
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 },
545 onProgressChange: function() {},
546 onSecurityChange: function() {},
547 onStatusChange: function() {},
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 },
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 };
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 }
592 WindowTarget.prototype = {
593 supports: supports,
594 get version() { return getVersion(); },
596 get window() {
597 return this._window;
598 },
600 get name() {
601 return this._window.document.title;
602 },
604 get url() {
605 return this._window.document.location.href;
606 },
608 get isRemote() {
609 return false;
610 },
612 get isLocalTab() {
613 return false;
614 },
616 get isThreadPaused() {
617 return !!this._isThreadPaused;
618 },
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 },
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 },
640 /**
641 * Target is not alive anymore.
642 */
643 destroy: function() {
644 if (!this._destroyed) {
645 this._destroyed = true;
647 this.off("thread-paused", this._handleThreadState);
648 this.off("thread-resumed", this._handleThreadState);
649 this.emit("close");
651 targets.delete(this._window);
652 this._window = null;
653 }
655 return promise.resolve(null);
656 },
658 toString: function() {
659 return 'WindowTarget:' + this.window;
660 },
661 };