1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/framework/target.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,661 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {Cc, Ci, Cu} = require("chrome"); 1.11 +const {Promise: promise} = require("resource://gre/modules/Promise.jsm"); 1.12 +const EventEmitter = require("devtools/toolkit/event-emitter"); 1.13 + 1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.15 +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", 1.16 + "resource://gre/modules/devtools/dbg-server.jsm"); 1.17 +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", 1.18 + "resource://gre/modules/devtools/dbg-client.jsm"); 1.19 + 1.20 +const targets = new WeakMap(); 1.21 +const promiseTargets = new WeakMap(); 1.22 + 1.23 +/** 1.24 + * Functions for creating Targets 1.25 + */ 1.26 +exports.TargetFactory = { 1.27 + /** 1.28 + * Construct a Target 1.29 + * @param {XULTab} tab 1.30 + * The tab to use in creating a new target. 1.31 + * 1.32 + * @return A target object 1.33 + */ 1.34 + forTab: function TF_forTab(tab) { 1.35 + let target = targets.get(tab); 1.36 + if (target == null) { 1.37 + target = new TabTarget(tab); 1.38 + targets.set(tab, target); 1.39 + } 1.40 + return target; 1.41 + }, 1.42 + 1.43 + /** 1.44 + * Return a promise of a Target for a remote tab. 1.45 + * @param {Object} options 1.46 + * The options object has the following properties: 1.47 + * { 1.48 + * form: the remote protocol form of a tab, 1.49 + * client: a DebuggerClient instance 1.50 + * (caller owns this and is responsible for closing), 1.51 + * chrome: true if the remote target is the whole process 1.52 + * } 1.53 + * 1.54 + * @return A promise of a target object 1.55 + */ 1.56 + forRemoteTab: function TF_forRemoteTab(options) { 1.57 + let targetPromise = promiseTargets.get(options); 1.58 + if (targetPromise == null) { 1.59 + let target = new TabTarget(options); 1.60 + targetPromise = target.makeRemote().then(() => target); 1.61 + promiseTargets.set(options, targetPromise); 1.62 + } 1.63 + return targetPromise; 1.64 + }, 1.65 + 1.66 + /** 1.67 + * Creating a target for a tab that is being closed is a problem because it 1.68 + * allows a leak as a result of coming after the close event which normally 1.69 + * clears things up. This function allows us to ask if there is a known 1.70 + * target for a tab without creating a target 1.71 + * @return true/false 1.72 + */ 1.73 + isKnownTab: function TF_isKnownTab(tab) { 1.74 + return targets.has(tab); 1.75 + }, 1.76 + 1.77 + /** 1.78 + * Construct a Target 1.79 + * @param {nsIDOMWindow} window 1.80 + * The chromeWindow to use in creating a new target 1.81 + * @return A target object 1.82 + */ 1.83 + forWindow: function TF_forWindow(window) { 1.84 + let target = targets.get(window); 1.85 + if (target == null) { 1.86 + target = new WindowTarget(window); 1.87 + targets.set(window, target); 1.88 + } 1.89 + return target; 1.90 + }, 1.91 + 1.92 + /** 1.93 + * Get all of the targets known to the local browser instance 1.94 + * @return An array of target objects 1.95 + */ 1.96 + allTargets: function TF_allTargets() { 1.97 + let windows = []; 1.98 + let wm = Cc["@mozilla.org/appshell/window-mediator;1"] 1.99 + .getService(Ci.nsIWindowMediator); 1.100 + let en = wm.getXULWindowEnumerator(null); 1.101 + while (en.hasMoreElements()) { 1.102 + windows.push(en.getNext()); 1.103 + } 1.104 + 1.105 + return windows.map(function(window) { 1.106 + return TargetFactory.forWindow(window); 1.107 + }); 1.108 + }, 1.109 +}; 1.110 + 1.111 +/** 1.112 + * The 'version' property allows the developer tools equivalent of browser 1.113 + * detection. Browser detection is evil, however while we don't know what we 1.114 + * will need to detect in the future, it is an easy way to postpone work. 1.115 + * We should be looking to use 'supports()' in place of version where 1.116 + * possible. 1.117 + */ 1.118 +function getVersion() { 1.119 + // FIXME: return something better 1.120 + return 20; 1.121 +} 1.122 + 1.123 +/** 1.124 + * A better way to support feature detection, but we're not yet at a place 1.125 + * where we have the features well enough defined for this to make lots of 1.126 + * sense. 1.127 + */ 1.128 +function supports(feature) { 1.129 + // FIXME: return something better 1.130 + return false; 1.131 +}; 1.132 + 1.133 +/** 1.134 + * A Target represents something that we can debug. Targets are generally 1.135 + * read-only. Any changes that you wish to make to a target should be done via 1.136 + * a Tool that attaches to the target. i.e. a Target is just a pointer saying 1.137 + * "the thing to debug is over there". 1.138 + * 1.139 + * Providing a generalized abstraction of a web-page or web-browser (available 1.140 + * either locally or remotely) is beyond the scope of this class (and maybe 1.141 + * also beyond the scope of this universe) However Target does attempt to 1.142 + * abstract some common events and read-only properties common to many Tools. 1.143 + * 1.144 + * Supported read-only properties: 1.145 + * - name, isRemote, url 1.146 + * 1.147 + * Target extends EventEmitter and provides support for the following events: 1.148 + * - close: The target window has been closed. All tools attached to this 1.149 + * target should close. This event is not currently cancelable. 1.150 + * - navigate: The target window has navigated to a different URL 1.151 + * 1.152 + * Optional events: 1.153 + * - will-navigate: The target window will navigate to a different URL 1.154 + * - hidden: The target is not visible anymore (for TargetTab, another tab is selected) 1.155 + * - visible: The target is visible (for TargetTab, tab is selected) 1.156 + * 1.157 + * Target also supports 2 functions to help allow 2 different versions of 1.158 + * Firefox debug each other. The 'version' property is the equivalent of 1.159 + * browser detection - simple and easy to implement but gets fragile when things 1.160 + * are not quite what they seem. The 'supports' property is the equivalent of 1.161 + * feature detection - harder to setup, but more robust long-term. 1.162 + * 1.163 + * Comparing Targets: 2 instances of a Target object can point at the same 1.164 + * thing, so t1 !== t2 and t1 != t2 even when they represent the same object. 1.165 + * To compare to targets use 't1.equals(t2)'. 1.166 + */ 1.167 +function Target() { 1.168 + throw new Error("Use TargetFactory.newXXX or Target.getXXX to create a Target in place of 'new Target()'"); 1.169 +} 1.170 + 1.171 +Object.defineProperty(Target.prototype, "version", { 1.172 + get: getVersion, 1.173 + enumerable: true 1.174 +}); 1.175 + 1.176 + 1.177 +/** 1.178 + * A TabTarget represents a page living in a browser tab. Generally these will 1.179 + * be web pages served over http(s), but they don't have to be. 1.180 + */ 1.181 +function TabTarget(tab) { 1.182 + EventEmitter.decorate(this); 1.183 + this.destroy = this.destroy.bind(this); 1.184 + this._handleThreadState = this._handleThreadState.bind(this); 1.185 + this.on("thread-resumed", this._handleThreadState); 1.186 + this.on("thread-paused", this._handleThreadState); 1.187 + // Only real tabs need initialization here. Placeholder objects for remote 1.188 + // targets will be initialized after a makeRemote method call. 1.189 + if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) { 1.190 + this._tab = tab; 1.191 + this._setupListeners(); 1.192 + } else { 1.193 + this._form = tab.form; 1.194 + this._client = tab.client; 1.195 + this._chrome = tab.chrome; 1.196 + } 1.197 +} 1.198 + 1.199 +TabTarget.prototype = { 1.200 + _webProgressListener: null, 1.201 + 1.202 + supports: supports, 1.203 + get version() { return getVersion(); }, 1.204 + 1.205 + get tab() { 1.206 + return this._tab; 1.207 + }, 1.208 + 1.209 + get form() { 1.210 + return this._form; 1.211 + }, 1.212 + 1.213 + get root() { 1.214 + return this._root; 1.215 + }, 1.216 + 1.217 + get client() { 1.218 + return this._client; 1.219 + }, 1.220 + 1.221 + get chrome() { 1.222 + return this._chrome; 1.223 + }, 1.224 + 1.225 + get window() { 1.226 + // Be extra careful here, since this may be called by HS_getHudByWindow 1.227 + // during shutdown. 1.228 + if (this._tab && this._tab.linkedBrowser) { 1.229 + return this._tab.linkedBrowser.contentWindow; 1.230 + } 1.231 + return null; 1.232 + }, 1.233 + 1.234 + get name() { 1.235 + return this._tab ? this._tab.linkedBrowser.contentDocument.title : 1.236 + this._form.title; 1.237 + }, 1.238 + 1.239 + get url() { 1.240 + return this._tab ? this._tab.linkedBrowser.contentDocument.location.href : 1.241 + this._form.url; 1.242 + }, 1.243 + 1.244 + get isRemote() { 1.245 + return !this.isLocalTab; 1.246 + }, 1.247 + 1.248 + get isAddon() { 1.249 + return !!(this._form && this._form.addonActor); 1.250 + }, 1.251 + 1.252 + get isLocalTab() { 1.253 + return !!this._tab; 1.254 + }, 1.255 + 1.256 + get isThreadPaused() { 1.257 + return !!this._isThreadPaused; 1.258 + }, 1.259 + 1.260 + /** 1.261 + * Adds remote protocol capabilities to the target, so that it can be used 1.262 + * for tools that support the Remote Debugging Protocol even for local 1.263 + * connections. 1.264 + */ 1.265 + makeRemote: function TabTarget_makeRemote() { 1.266 + if (this._remote) { 1.267 + return this._remote.promise; 1.268 + } 1.269 + 1.270 + this._remote = promise.defer(); 1.271 + 1.272 + if (this.isLocalTab) { 1.273 + // Since a remote protocol connection will be made, let's start the 1.274 + // DebuggerServer here, once and for all tools. 1.275 + if (!DebuggerServer.initialized) { 1.276 + DebuggerServer.init(); 1.277 + DebuggerServer.addBrowserActors(); 1.278 + } 1.279 + 1.280 + this._client = new DebuggerClient(DebuggerServer.connectPipe()); 1.281 + // A local TabTarget will never perform chrome debugging. 1.282 + this._chrome = false; 1.283 + } 1.284 + 1.285 + this._setupRemoteListeners(); 1.286 + 1.287 + let attachTab = () => { 1.288 + this._client.attachTab(this._form.actor, (aResponse, aTabClient) => { 1.289 + if (!aTabClient) { 1.290 + this._remote.reject("Unable to attach to the tab"); 1.291 + return; 1.292 + } 1.293 + this.activeTab = aTabClient; 1.294 + this.threadActor = aResponse.threadActor; 1.295 + this._remote.resolve(null); 1.296 + }); 1.297 + }; 1.298 + 1.299 + if (this.isLocalTab) { 1.300 + this._client.connect((aType, aTraits) => { 1.301 + this._client.listTabs(aResponse => { 1.302 + this._root = aResponse; 1.303 + 1.304 + let windowUtils = this.window 1.305 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.306 + .getInterface(Ci.nsIDOMWindowUtils); 1.307 + let outerWindow = windowUtils.outerWindowID; 1.308 + aResponse.tabs.some((tab) => { 1.309 + if (tab.outerWindowID === outerWindow) { 1.310 + this._form = tab; 1.311 + return true; 1.312 + } 1.313 + return false; 1.314 + }); 1.315 + if (!this._form) { 1.316 + this._form = aResponse.tabs[aResponse.selected]; 1.317 + } 1.318 + attachTab(); 1.319 + }); 1.320 + }); 1.321 + } else if (!this.chrome) { 1.322 + // In the remote debugging case, the protocol connection will have been 1.323 + // already initialized in the connection screen code. 1.324 + attachTab(); 1.325 + } else { 1.326 + // Remote chrome debugging doesn't need anything at this point. 1.327 + this._remote.resolve(null); 1.328 + } 1.329 + 1.330 + return this._remote.promise; 1.331 + }, 1.332 + 1.333 + /** 1.334 + * Listen to the different events. 1.335 + */ 1.336 + _setupListeners: function TabTarget__setupListeners() { 1.337 + this._webProgressListener = new TabWebProgressListener(this); 1.338 + this.tab.linkedBrowser.addProgressListener(this._webProgressListener); 1.339 + this.tab.addEventListener("TabClose", this); 1.340 + this.tab.parentNode.addEventListener("TabSelect", this); 1.341 + this.tab.ownerDocument.defaultView.addEventListener("unload", this); 1.342 + }, 1.343 + 1.344 + /** 1.345 + * Teardown event listeners. 1.346 + */ 1.347 + _teardownListeners: function TabTarget__teardownListeners() { 1.348 + if (this._webProgressListener) { 1.349 + this._webProgressListener.destroy(); 1.350 + } 1.351 + 1.352 + this._tab.ownerDocument.defaultView.removeEventListener("unload", this); 1.353 + this._tab.removeEventListener("TabClose", this); 1.354 + this._tab.parentNode.removeEventListener("TabSelect", this); 1.355 + }, 1.356 + 1.357 + /** 1.358 + * Setup listeners for remote debugging, updating existing ones as necessary. 1.359 + */ 1.360 + _setupRemoteListeners: function TabTarget__setupRemoteListeners() { 1.361 + this.client.addListener("closed", this.destroy); 1.362 + 1.363 + this._onTabDetached = (aType, aPacket) => { 1.364 + // We have to filter message to ensure that this detach is for this tab 1.365 + if (aPacket.from == this._form.actor) { 1.366 + this.destroy(); 1.367 + } 1.368 + }; 1.369 + this.client.addListener("tabDetached", this._onTabDetached); 1.370 + 1.371 + this._onTabNavigated = function onRemoteTabNavigated(aType, aPacket) { 1.372 + let event = Object.create(null); 1.373 + event.url = aPacket.url; 1.374 + event.title = aPacket.title; 1.375 + event.nativeConsoleAPI = aPacket.nativeConsoleAPI; 1.376 + // Send any stored event payload (DOMWindow or nsIRequest) for backwards 1.377 + // compatibility with non-remotable tools. 1.378 + if (aPacket.state == "start") { 1.379 + event._navPayload = this._navRequest; 1.380 + this.emit("will-navigate", event); 1.381 + this._navRequest = null; 1.382 + } else { 1.383 + event._navPayload = this._navWindow; 1.384 + this.emit("navigate", event); 1.385 + this._navWindow = null; 1.386 + } 1.387 + }.bind(this); 1.388 + this.client.addListener("tabNavigated", this._onTabNavigated); 1.389 + }, 1.390 + 1.391 + /** 1.392 + * Teardown listeners for remote debugging. 1.393 + */ 1.394 + _teardownRemoteListeners: function TabTarget__teardownRemoteListeners() { 1.395 + this.client.removeListener("closed", this.destroy); 1.396 + this.client.removeListener("tabNavigated", this._onTabNavigated); 1.397 + this.client.removeListener("tabDetached", this._onTabDetached); 1.398 + }, 1.399 + 1.400 + /** 1.401 + * Handle tabs events. 1.402 + */ 1.403 + handleEvent: function (event) { 1.404 + switch (event.type) { 1.405 + case "TabClose": 1.406 + case "unload": 1.407 + this.destroy(); 1.408 + break; 1.409 + case "TabSelect": 1.410 + if (this.tab.selected) { 1.411 + this.emit("visible", event); 1.412 + } else { 1.413 + this.emit("hidden", event); 1.414 + } 1.415 + break; 1.416 + } 1.417 + }, 1.418 + 1.419 + /** 1.420 + * Handle script status. 1.421 + */ 1.422 + _handleThreadState: function(event) { 1.423 + switch (event) { 1.424 + case "thread-resumed": 1.425 + this._isThreadPaused = false; 1.426 + break; 1.427 + case "thread-paused": 1.428 + this._isThreadPaused = true; 1.429 + break; 1.430 + } 1.431 + }, 1.432 + 1.433 + /** 1.434 + * Target is not alive anymore. 1.435 + */ 1.436 + destroy: function() { 1.437 + // If several things call destroy then we give them all the same 1.438 + // destruction promise so we're sure to destroy only once 1.439 + if (this._destroyer) { 1.440 + return this._destroyer.promise; 1.441 + } 1.442 + 1.443 + this._destroyer = promise.defer(); 1.444 + 1.445 + // Before taking any action, notify listeners that destruction is imminent. 1.446 + this.emit("close"); 1.447 + 1.448 + // First of all, do cleanup tasks that pertain to both remoted and 1.449 + // non-remoted targets. 1.450 + this.off("thread-resumed", this._handleThreadState); 1.451 + this.off("thread-paused", this._handleThreadState); 1.452 + 1.453 + if (this._tab) { 1.454 + this._teardownListeners(); 1.455 + } 1.456 + 1.457 + let cleanupAndResolve = () => { 1.458 + this._cleanup(); 1.459 + this._destroyer.resolve(null); 1.460 + }; 1.461 + // If this target was not remoted, the promise will be resolved before the 1.462 + // function returns. 1.463 + if (this._tab && !this._client) { 1.464 + cleanupAndResolve(); 1.465 + } else if (this._client) { 1.466 + // If, on the other hand, this target was remoted, the promise will be 1.467 + // resolved after the remote connection is closed. 1.468 + this._teardownRemoteListeners(); 1.469 + 1.470 + if (this.isLocalTab) { 1.471 + // We started with a local tab and created the client ourselves, so we 1.472 + // should close it. 1.473 + this._client.close(cleanupAndResolve); 1.474 + } else { 1.475 + // The client was handed to us, so we are not responsible for closing 1.476 + // it. We just need to detach from the tab, if already attached. 1.477 + if (this.activeTab) { 1.478 + this.activeTab.detach(cleanupAndResolve); 1.479 + } else { 1.480 + cleanupAndResolve(); 1.481 + } 1.482 + } 1.483 + } 1.484 + 1.485 + return this._destroyer.promise; 1.486 + }, 1.487 + 1.488 + /** 1.489 + * Clean up references to what this target points to. 1.490 + */ 1.491 + _cleanup: function TabTarget__cleanup() { 1.492 + if (this._tab) { 1.493 + targets.delete(this._tab); 1.494 + } else { 1.495 + promiseTargets.delete(this._form); 1.496 + } 1.497 + this.activeTab = null; 1.498 + this._client = null; 1.499 + this._tab = null; 1.500 + this._form = null; 1.501 + this._remote = null; 1.502 + }, 1.503 + 1.504 + toString: function() { 1.505 + return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor)); 1.506 + }, 1.507 +}; 1.508 + 1.509 + 1.510 +/** 1.511 + * WebProgressListener for TabTarget. 1.512 + * 1.513 + * @param object aTarget 1.514 + * The TabTarget instance to work with. 1.515 + */ 1.516 +function TabWebProgressListener(aTarget) { 1.517 + this.target = aTarget; 1.518 +} 1.519 + 1.520 +TabWebProgressListener.prototype = { 1.521 + target: null, 1.522 + 1.523 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), 1.524 + 1.525 + onStateChange: function TWPL_onStateChange(progress, request, flag, status) { 1.526 + let isStart = flag & Ci.nsIWebProgressListener.STATE_START; 1.527 + let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; 1.528 + let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK; 1.529 + let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST; 1.530 + 1.531 + // Skip non-interesting states. 1.532 + if (!isStart || !isDocument || !isRequest || !isNetwork) { 1.533 + return; 1.534 + } 1.535 + 1.536 + // emit event if the top frame is navigating 1.537 + if (this.target && this.target.window == progress.DOMWindow) { 1.538 + // Emit the event if the target is not remoted or store the payload for 1.539 + // later emission otherwise. 1.540 + if (this.target._client) { 1.541 + this.target._navRequest = request; 1.542 + } else { 1.543 + this.target.emit("will-navigate", request); 1.544 + } 1.545 + } 1.546 + }, 1.547 + 1.548 + onProgressChange: function() {}, 1.549 + onSecurityChange: function() {}, 1.550 + onStatusChange: function() {}, 1.551 + 1.552 + onLocationChange: function TWPL_onLocationChange(webProgress, request, URI, flags) { 1.553 + if (this.target && 1.554 + !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { 1.555 + let window = webProgress.DOMWindow; 1.556 + // Emit the event if the target is not remoted or store the payload for 1.557 + // later emission otherwise. 1.558 + if (this.target._client) { 1.559 + this.target._navWindow = window; 1.560 + } else { 1.561 + this.target.emit("navigate", window); 1.562 + } 1.563 + } 1.564 + }, 1.565 + 1.566 + /** 1.567 + * Destroy the progress listener instance. 1.568 + */ 1.569 + destroy: function TWPL_destroy() { 1.570 + if (this.target.tab) { 1.571 + try { 1.572 + this.target.tab.linkedBrowser.removeProgressListener(this); 1.573 + } catch (ex) { 1.574 + // This can throw when a tab crashes in e10s. 1.575 + } 1.576 + } 1.577 + this.target._webProgressListener = null; 1.578 + this.target._navRequest = null; 1.579 + this.target._navWindow = null; 1.580 + this.target = null; 1.581 + } 1.582 +}; 1.583 + 1.584 + 1.585 +/** 1.586 + * A WindowTarget represents a page living in a xul window or panel. Generally 1.587 + * these will have a chrome: URL 1.588 + */ 1.589 +function WindowTarget(window) { 1.590 + EventEmitter.decorate(this); 1.591 + this._window = window; 1.592 + this._setupListeners(); 1.593 +} 1.594 + 1.595 +WindowTarget.prototype = { 1.596 + supports: supports, 1.597 + get version() { return getVersion(); }, 1.598 + 1.599 + get window() { 1.600 + return this._window; 1.601 + }, 1.602 + 1.603 + get name() { 1.604 + return this._window.document.title; 1.605 + }, 1.606 + 1.607 + get url() { 1.608 + return this._window.document.location.href; 1.609 + }, 1.610 + 1.611 + get isRemote() { 1.612 + return false; 1.613 + }, 1.614 + 1.615 + get isLocalTab() { 1.616 + return false; 1.617 + }, 1.618 + 1.619 + get isThreadPaused() { 1.620 + return !!this._isThreadPaused; 1.621 + }, 1.622 + 1.623 + /** 1.624 + * Listen to the different events. 1.625 + */ 1.626 + _setupListeners: function() { 1.627 + this._handleThreadState = this._handleThreadState.bind(this); 1.628 + this.on("thread-paused", this._handleThreadState); 1.629 + this.on("thread-resumed", this._handleThreadState); 1.630 + }, 1.631 + 1.632 + _handleThreadState: function(event) { 1.633 + switch (event) { 1.634 + case "thread-resumed": 1.635 + this._isThreadPaused = false; 1.636 + break; 1.637 + case "thread-paused": 1.638 + this._isThreadPaused = true; 1.639 + break; 1.640 + } 1.641 + }, 1.642 + 1.643 + /** 1.644 + * Target is not alive anymore. 1.645 + */ 1.646 + destroy: function() { 1.647 + if (!this._destroyed) { 1.648 + this._destroyed = true; 1.649 + 1.650 + this.off("thread-paused", this._handleThreadState); 1.651 + this.off("thread-resumed", this._handleThreadState); 1.652 + this.emit("close"); 1.653 + 1.654 + targets.delete(this._window); 1.655 + this._window = null; 1.656 + } 1.657 + 1.658 + return promise.resolve(null); 1.659 + }, 1.660 + 1.661 + toString: function() { 1.662 + return 'WindowTarget:' + this.window; 1.663 + }, 1.664 +};