tab.js (10194B)
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 "use strict"; 5 6 const { 7 tabDescriptorSpec, 8 } = require("resource://devtools/shared/specs/descriptors/tab.js"); 9 const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js"); 10 11 loader.lazyRequireGetter( 12 this, 13 "gDevTools", 14 "resource://devtools/client/framework/devtools.js", 15 true 16 ); 17 loader.lazyRequireGetter( 18 this, 19 "WindowGlobalTargetFront", 20 "resource://devtools/client/fronts/targets/window-global.js", 21 true 22 ); 23 const { 24 FrontClassWithSpec, 25 registerFront, 26 } = require("resource://devtools/shared/protocol.js"); 27 const { 28 DescriptorMixin, 29 } = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js"); 30 31 const POPUP_DEBUG_PREF = "devtools.popups.debug"; 32 33 /** 34 * DescriptorFront for tab targets. 35 * 36 * @fires remoteness-change 37 * Fired only for target switching, when the debugged tab is a local tab. 38 * TODO: This event could move to the server in order to support 39 * remoteness change for remote debugging. 40 */ 41 class TabDescriptorFront extends DescriptorMixin( 42 FrontClassWithSpec(tabDescriptorSpec) 43 ) { 44 constructor(client, targetFront, parentFront) { 45 super(client, targetFront, parentFront); 46 47 // The tab descriptor can be configured to create either local tab targets 48 // (eg, regular tab toolbox) or browsing context targets (eg tab remote 49 // debugging). 50 this._localTab = null; 51 52 // Flag to prevent the server from trying to spawn targets by the watcher actor. 53 this._disableTargetSwitching = false; 54 55 this._onTargetDestroyed = this._onTargetDestroyed.bind(this); 56 this._handleTabEvent = this._handleTabEvent.bind(this); 57 58 // When the target is created from the server side, 59 // it is not created via TabDescriptor.getTarget. 60 // Instead, it is retrieved by the TargetCommand which 61 // will call TabDescriptor.setTarget from TargetCommand.onTargetAvailable 62 if (this.isServerTargetSwitchingEnabled()) { 63 this._targetFrontPromise = new Promise( 64 r => (this._resolveTargetFrontPromise = r) 65 ); 66 } 67 } 68 69 descriptorType = DESCRIPTOR_TYPES.TAB; 70 71 form(json) { 72 this.actorID = json.actor; 73 this._form = json; 74 this.traits = json.traits || {}; 75 } 76 77 /** 78 * Destroy the front. 79 * 80 * @param Boolean If true, it means that we destroy the front when receiving the descriptor-destroyed 81 * event from the server. 82 */ 83 destroy({ isServerDestroyEvent = false } = {}) { 84 if (this.isDestroyed()) { 85 return; 86 } 87 88 // The descriptor may be destroyed first by the frontend. 89 // When closing the tab, the toolbox document is almost immediately removed from the DOM. 90 // The `unload` event fires and toolbox destroys itself, as well as its related client. 91 // 92 // In such case, we emit the descriptor-destroyed event 93 if (!isServerDestroyEvent) { 94 this.emit("descriptor-destroyed"); 95 } 96 if (this.isLocalTab) { 97 this._teardownLocalTabListeners(); 98 } 99 super.destroy(); 100 } 101 102 getWatcher() { 103 const isPopupDebuggingEnabled = Services.prefs.getBoolPref( 104 POPUP_DEBUG_PREF, 105 false 106 ); 107 return super.getWatcher({ 108 isServerTargetSwitchingEnabled: this.isServerTargetSwitchingEnabled(), 109 isPopupDebuggingEnabled, 110 }); 111 } 112 113 setLocalTab(localTab) { 114 this._localTab = localTab; 115 this._setupLocalTabListeners(); 116 } 117 118 get isTabDescriptor() { 119 return true; 120 } 121 122 get isLocalTab() { 123 return !!this._localTab; 124 } 125 126 get localTab() { 127 return this._localTab; 128 } 129 130 _setupLocalTabListeners() { 131 this.localTab.addEventListener("TabClose", this._handleTabEvent); 132 this.localTab.addEventListener("TabRemotenessChange", this._handleTabEvent); 133 } 134 135 _teardownLocalTabListeners() { 136 this.localTab.removeEventListener("TabClose", this._handleTabEvent); 137 this.localTab.removeEventListener( 138 "TabRemotenessChange", 139 this._handleTabEvent 140 ); 141 } 142 143 isServerTargetSwitchingEnabled() { 144 return !this._disableTargetSwitching; 145 } 146 147 /** 148 * Called by CommandsFactory, when the WebExtension codebase instantiates 149 * a commands. We have to flag the TabDescriptor for them as they don't support 150 * target switching and gets severely broken when enabling server target which 151 * introduce target switching for all navigations and reloads 152 */ 153 setIsForWebExtension() { 154 this.disableTargetSwitching(); 155 } 156 157 /** 158 * Method used by the WebExtension which still need to disable server side targets, 159 * and also a few xpcshell tests which are using legacy API and don't support watcher actor. 160 */ 161 disableTargetSwitching() { 162 this._disableTargetSwitching = true; 163 // Delete these two attributes which have to be set early from the constructor, 164 // but we don't know yet if target switch should be disabled. 165 delete this._targetFrontPromise; 166 delete this._resolveTargetFrontPromise; 167 } 168 169 get isZombieTab() { 170 return this._form.isZombieTab; 171 } 172 173 get browserId() { 174 return this._form.browserId; 175 } 176 177 get selected() { 178 return this._form.selected; 179 } 180 181 get title() { 182 return this._form.title; 183 } 184 185 get url() { 186 return this._form.url; 187 } 188 189 get favicon() { 190 // Note: the favicon is not part of the default form() payload, it will be 191 // added in `retrieveFavicon`. 192 return this._form.favicon; 193 } 194 195 _createTabTarget(form) { 196 const front = new WindowGlobalTargetFront(this._client, null, this); 197 198 // As these fronts aren't instantiated by protocol.js, we have to set their actor ID 199 // manually like that: 200 front.actorID = form.actor; 201 front.form(form); 202 this.manage(front); 203 return front; 204 } 205 206 _onTargetDestroyed() { 207 // Clear the cached targetFront when the target is destroyed. 208 // Note that we are also checking that _targetFront has a valid actorID 209 // in getTarget, this acts as an additional security to avoid races. 210 this._targetFront = null; 211 } 212 213 /** 214 * Safely retrieves the favicon via getFavicon() and populates this._form.favicon. 215 * 216 * We could let callers explicitly retrieve the favicon instead of inserting it in the 217 * form dynamically. 218 */ 219 async retrieveFavicon() { 220 try { 221 this._form.favicon = await this.getFavicon(); 222 } catch (e) { 223 // We might request the data for a tab which is going to be destroyed. 224 // In this case the TargetFront will be destroyed. Otherwise log an error. 225 if (!this.isDestroyed()) { 226 console.error("Failed to retrieve the favicon for " + this.url, e); 227 } 228 } 229 } 230 231 /** 232 * Top-level targets created on the server will not be created and managed 233 * by a descriptor front. Instead they are created by the Watcher actor. 234 * On the client side we manually re-establish a link between the descriptor 235 * and the new top-level target. 236 */ 237 setTarget(targetFront) { 238 // Completely ignore the previous target. 239 // We might nullify the _targetFront unexpectely due to previous target 240 // being destroyed after the new is created 241 if (this._targetFront) { 242 this._targetFront.off("target-destroyed", this._onTargetDestroyed); 243 } 244 this._targetFront = targetFront; 245 246 targetFront.on("target-destroyed", this._onTargetDestroyed); 247 248 if (this.isServerTargetSwitchingEnabled()) { 249 this._resolveTargetFrontPromise(targetFront); 250 251 // Set a new promise in order to: 252 // 1) Avoid leaking the targetFront we just resolved into the previous promise. 253 // 2) Never return an empty target from `getTarget` 254 // 255 // About the second point: 256 // There is a race condition where we call `onTargetDestroyed` (which clears `this.targetFront`) 257 // a bit before calling `setTarget`. So that `this.targetFront` could be null, 258 // while we now a new target will eventually come when calling `setTarget`. 259 // Setting a new promise will help wait for the next target while `_targetFront` is null. 260 // Note that `getTarget` first look into `_targetFront` before checking for `_targetFrontPromise`. 261 this._targetFrontPromise = new Promise( 262 r => (this._resolveTargetFrontPromise = r) 263 ); 264 } 265 } 266 getCachedTarget() { 267 return this._targetFront; 268 } 269 async getTarget() { 270 if (this._targetFront && !this._targetFront.isDestroyed()) { 271 return this._targetFront; 272 } 273 274 if (this._targetFrontPromise) { 275 return this._targetFrontPromise; 276 } 277 278 this._targetFrontPromise = (async () => { 279 let newTargetFront = null; 280 try { 281 const targetForm = await super.getTarget(); 282 newTargetFront = this._createTabTarget(targetForm); 283 this.setTarget(newTargetFront); 284 } catch (e) { 285 console.log( 286 `Request to connect to TabDescriptor "${this.id}" failed: ${e}` 287 ); 288 } 289 290 this._targetFrontPromise = null; 291 return newTargetFront; 292 })(); 293 return this._targetFrontPromise; 294 } 295 296 /** 297 * Handle tabs events. 298 */ 299 async _handleTabEvent(event) { 300 switch (event.type) { 301 case "TabClose": { 302 // Always destroy the toolbox opened for this local tab descriptor. 303 // When the toolbox is in a Window Host, it won't be removed from the 304 // DOM when the tab is closed. 305 const toolbox = gDevTools.getToolboxForDescriptorFront(this); 306 if (toolbox) { 307 // Toolbox.destroy will call target.destroy eventually. 308 await toolbox.destroy(); 309 } 310 break; 311 } 312 case "TabRemotenessChange": 313 this._onRemotenessChange(); 314 break; 315 } 316 } 317 318 /** 319 * Automatically respawn the toolbox when the tab changes between being 320 * loaded within the parent process and loaded from a content process. 321 * Process change can go in both ways. 322 */ 323 async _onRemotenessChange() { 324 // In a near future, this client side code should be replaced by actor code, 325 // notifying about new tab targets. 326 this.emit("remoteness-change", this._targetFront); 327 } 328 } 329 330 registerFront(TabDescriptorFront);