inspector.js (9516B)
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 { 8 FrontClassWithSpec, 9 registerFront, 10 } = require("resource://devtools/shared/protocol.js"); 11 const { 12 inspectorSpec, 13 } = require("resource://devtools/shared/specs/inspector.js"); 14 15 loader.lazyRequireGetter( 16 this, 17 "captureScreenshot", 18 "resource://devtools/client/shared/screenshot.js", 19 true 20 ); 21 const lazy = {}; 22 23 ChromeUtils.defineESModuleGetters(lazy, { 24 TYPES: "resource://devtools/shared/highlighters.mjs", 25 }); 26 27 const SHOW_ALL_ANONYMOUS_CONTENT_PREF = 28 "devtools.inspector.showAllAnonymousContent"; 29 30 /** 31 * Client side of the inspector actor, which is used to create 32 * inspector-related actors, including the walker. 33 */ 34 class InspectorFront extends FrontClassWithSpec(inspectorSpec) { 35 constructor(client, targetFront, parentFront) { 36 super(client, targetFront, parentFront); 37 38 this._client = client; 39 this._highlighters = new Map(); 40 41 // Attribute name from which to retrieve the actorID out of the target actor's form 42 this.formAttributeName = "inspectorActor"; 43 44 // Map of highlighter types to unsettled promises to create a highlighter of that type 45 this._pendingGetHighlighterMap = new Map(); 46 47 this.noopStylesheetListener = () => {}; 48 } 49 50 // async initialization 51 async initialize() { 52 if (this.initialized) { 53 return this.initialized; 54 } 55 56 // Watch STYLESHEET resources to fill the ResourceCommand cache. 57 // StyleRule front's `get parentStyleSheet()` will query the cache to 58 // retrieve the resource corresponding to the parent stylesheet of a rule. 59 const { resourceCommand } = this.targetFront.commands; 60 // Backup resourceCommand, targetFront.commands might be null in `destroy`. 61 this.resourceCommand = resourceCommand; 62 await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], { 63 onAvailable: this.noopStylesheetListener, 64 }); 65 66 // Bail out if the inspector is closed while watchResources was pending 67 if (this.isDestroyed()) { 68 return null; 69 } 70 71 const promises = [this._getWalker(), this._getPageStyle()]; 72 if (this.targetFront.commands.descriptorFront.isTabDescriptor) { 73 promises.push(this._enableViewportSizeOnResizeHighlighter()); 74 } 75 76 this.initialized = await Promise.all(promises); 77 78 return this.initialized; 79 } 80 81 async _getWalker() { 82 const showAllAnonymousContent = Services.prefs.getBoolPref( 83 SHOW_ALL_ANONYMOUS_CONTENT_PREF 84 ); 85 this.walker = await this.getWalker({ 86 showAllAnonymousContent, 87 }); 88 89 // We need to reparent the RootNode of remote iframe Walkers 90 // so that their parent is the NodeFront of the <iframe> 91 // element, coming from another process/target/WalkerFront. 92 await this.walker.reparentRemoteFrame(); 93 } 94 95 hasHighlighter(type) { 96 return this._highlighters.has(type); 97 } 98 99 async _getPageStyle() { 100 this.pageStyle = await super.getPageStyle(); 101 } 102 103 async _enableViewportSizeOnResizeHighlighter() { 104 const highlighter = await this.getOrCreateHighlighterByType( 105 lazy.TYPES.VIEWPORT_SIZE_ON_RESIZE 106 ); 107 await highlighter.show(this); 108 } 109 110 async getCompatibilityFront() { 111 if (!this._compatibility) { 112 this._compatibility = await super.getCompatibility(); 113 } 114 115 return this._compatibility; 116 } 117 118 destroy() { 119 if (this.isDestroyed()) { 120 return; 121 } 122 this._compatibility = null; 123 124 const { resourceCommand } = this; 125 resourceCommand.unwatchResources([resourceCommand.TYPES.STYLESHEET], { 126 onAvailable: this.noopStylesheetListener, 127 }); 128 this.resourceCommand = null; 129 130 this.walker = null; 131 132 // CustomHighlighter fronts are managed by InspectorFront and so will be 133 // automatically destroyed. But we have to clear the `_highlighters` 134 // Map as well as explicitly call `finalize` request on all of them. 135 this.destroyHighlighters(); 136 super.destroy(); 137 } 138 139 destroyHighlighters() { 140 for (const type of this._highlighters.keys()) { 141 if (this._highlighters.has(type)) { 142 const highlighter = this._highlighters.get(type); 143 if (!highlighter.isDestroyed()) { 144 highlighter.finalize(); 145 } 146 this._highlighters.delete(type); 147 } 148 } 149 } 150 151 async getHighlighterByType(typeName) { 152 let highlighter = null; 153 try { 154 highlighter = await super.getHighlighterByType(typeName); 155 } catch (_) { 156 throw new Error( 157 "The target doesn't support " + 158 `creating highlighters by types or ${typeName} is unknown` 159 ); 160 } 161 return highlighter; 162 } 163 164 getKnownHighlighter(type) { 165 return this._highlighters.get(type); 166 } 167 168 /** 169 * Return a highlighter instance of the given type. 170 * If an instance was previously created, return it. Else, create and return a new one. 171 * 172 * Store a promise for the request to create a new highlighter. If another request 173 * comes in before that promise is resolved, wait for it to resolve and return the 174 * highlighter instance it resolved with instead of creating a new request. 175 * 176 * @param {string} type 177 * Highlighter type 178 * @return {Promise} 179 * Promise which resolves with a highlighter instance of the given type 180 */ 181 async getOrCreateHighlighterByType(type) { 182 let front = this._highlighters.get(type); 183 let pendingGetHighlighter = this._pendingGetHighlighterMap.get(type); 184 185 if (!front && !pendingGetHighlighter) { 186 pendingGetHighlighter = (async () => { 187 const highlighter = await this.getHighlighterByType(type); 188 this._highlighters.set(type, highlighter); 189 this._pendingGetHighlighterMap.delete(type); 190 return highlighter; 191 })(); 192 193 this._pendingGetHighlighterMap.set(type, pendingGetHighlighter); 194 } 195 196 if (pendingGetHighlighter) { 197 front = await pendingGetHighlighter; 198 } 199 200 return front; 201 } 202 203 async pickColorFromPage(options) { 204 let screenshot = null; 205 206 // @backward-compat { version 87 } ScreenshotContentActor was only added in 87. 207 // When connecting to older server, the eyedropper will use drawWindow 208 // to retrieve the screenshot of the page (that's a decent fallback, 209 // even if it doesn't handle remote frames). 210 if (this.targetFront.hasActor("screenshotContent")) { 211 try { 212 // We use the screenshot actors as it can retrieve an image of the current viewport, 213 // handling remote frame if need be. 214 const { data } = await captureScreenshot(this.targetFront, { 215 browsingContextID: this.targetFront.browsingContextID, 216 disableFlash: true, 217 ignoreDprForFileScale: true, 218 }); 219 screenshot = data; 220 } catch (e) { 221 // We simply log the error and still call pickColorFromPage as it will default to 222 // use drawWindow in order to get the screenshot of the page (that's a decent 223 // fallback, even if it doesn't handle remote frames). 224 console.error( 225 "Error occured when taking a screenshot for the eyedropper", 226 e 227 ); 228 } 229 } 230 231 await super.pickColorFromPage({ 232 ...options, 233 screenshot, 234 }); 235 236 if (options?.fromMenu) { 237 Glean.devtools.menuEyedropperOpenedCount.add(1); 238 } else { 239 Glean.devtools.eyedropperOpenedCount.add(1); 240 } 241 } 242 243 /** 244 * Given a node grip, return a NodeFront on the right context. 245 * 246 * @param {object} grip: The node grip. 247 * @returns {Promise<NodeFront|null>} A promise that resolves with a NodeFront or null 248 * if the NodeFront couldn't be created/retrieved. 249 */ 250 async getNodeFrontFromNodeGrip(grip) { 251 return this.getNodeActorFromContentDomReference(grip.contentDomReference); 252 } 253 254 async getNodeActorFromContentDomReference(contentDomReference) { 255 const { browsingContextId } = contentDomReference; 256 // If the contentDomReference lives in the same browsing context id than the 257 // current one, we can directly use the current walker. 258 if (this.targetFront.browsingContextID === browsingContextId) { 259 return this.walker.getNodeActorFromContentDomReference( 260 contentDomReference 261 ); 262 } 263 264 // If the contentDomReference has a different browsing context than the current one, 265 // we are either in Fission or in the Multiprocess Browser Toolbox, so we need to 266 // retrieve the walker of the WindowGlobalTarget. 267 // Get the target for this remote frame element 268 269 // Tab and Process Descriptors expose a Watcher, which should be used to 270 // fetch the node's target. 271 let target; 272 const { watcherFront } = this.targetFront.commands; 273 if (watcherFront) { 274 target = await watcherFront.getWindowGlobalTarget(browsingContextId); 275 } else { 276 // For descriptors which don't expose a watcher (e.g. WebExtension) 277 // we used to call RootActor::getBrowsingContextDescriptor, but it was 278 // removed in FF77. 279 // Support for watcher in WebExtension descriptors is Bug 1644341. 280 throw new Error( 281 `Unable to call getNodeActorFromContentDomReference for ${this.targetFront.actorID}` 282 ); 283 } 284 const { walker } = await target.getFront("inspector"); 285 return walker.getNodeActorFromContentDomReference(contentDomReference); 286 } 287 } 288 289 registerFront(InspectorFront);