browser/devtools/framework/target.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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 };

mercurial