inspector.js (11923B)
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 /** 8 * Here's the server side of the remote inspector. 9 * 10 * The WalkerActor is the client's view of the debuggee's DOM. It's gives 11 * the client a tree of NodeActor objects. 12 * 13 * The walker presents the DOM tree mostly unmodified from the source DOM 14 * tree, but with a few key differences: 15 * 16 * - Empty text nodes are ignored. This is pretty typical of developer 17 * tools, but maybe we should reconsider that on the server side. 18 * - iframes with documents loaded have the loaded document as the child, 19 * the walker provides one big tree for the whole document tree. 20 * 21 * There are a few ways to get references to NodeActors: 22 * 23 * - When you first get a WalkerActor reference, it comes with a free 24 * reference to the root document's node. 25 * - Given a node, you can ask for children, siblings, and parents. 26 * - You can issue querySelector and querySelectorAll requests to find 27 * other elements. 28 * - Requests that return arbitrary nodes from the tree (like querySelector 29 * and querySelectorAll) will also return any nodes the client hasn't 30 * seen in order to have a complete set of parents. 31 * 32 * Once you have a NodeFront, you should be able to answer a few questions 33 * without further round trips, like the node's name, namespace/tagName, 34 * attributes, etc. Other questions (like a text node's full nodeValue) 35 * might require another round trip. 36 * 37 * The protocol guarantees that the client will always know the parent of 38 * any node that is returned by the server. This means that some requests 39 * (like querySelector) will include the extra nodes needed to satisfy this 40 * requirement. The client keeps track of this parent relationship, so the 41 * node fronts form a tree that is a subset of the actual DOM tree. 42 * 43 * 44 * We maintain this guarantee to support the ability to release subtrees on 45 * the client - when a node is disconnected from the DOM tree we want to be 46 * able to free the client objects for all the children nodes. 47 * 48 * So to be able to answer "all the children of a given node that we have 49 * seen on the client side", we guarantee that every time we've seen a node, 50 * we connect it up through its parents. 51 */ 52 53 const { Actor } = require("resource://devtools/shared/protocol.js"); 54 const { 55 inspectorSpec, 56 } = require("resource://devtools/shared/specs/inspector.js"); 57 58 const { setTimeout } = ChromeUtils.importESModule( 59 "resource://gre/modules/Timer.sys.mjs", 60 { global: "contextual" } 61 ); 62 const { 63 LongStringActor, 64 } = require("resource://devtools/server/actors/string.js"); 65 66 loader.lazyRequireGetter( 67 this, 68 "InspectorActorUtils", 69 "resource://devtools/server/actors/inspector/utils.js" 70 ); 71 loader.lazyRequireGetter( 72 this, 73 "WalkerActor", 74 "resource://devtools/server/actors/inspector/walker.js", 75 true 76 ); 77 loader.lazyRequireGetter( 78 this, 79 "EyeDropper", 80 "resource://devtools/server/actors/highlighters/eye-dropper.js", 81 true 82 ); 83 loader.lazyRequireGetter( 84 this, 85 "PageStyleActor", 86 "resource://devtools/server/actors/page-style.js", 87 true 88 ); 89 loader.lazyRequireGetter( 90 this, 91 ["CustomHighlighterActor", "isTypeRegistered", "HighlighterEnvironment"], 92 "resource://devtools/server/actors/highlighters.js", 93 true 94 ); 95 loader.lazyRequireGetter( 96 this, 97 "CompatibilityActor", 98 "resource://devtools/server/actors/compatibility/compatibility.js", 99 true 100 ); 101 102 const SVG_NS = "http://www.w3.org/2000/svg"; 103 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 104 105 /** 106 * Server side of the inspector actor, which is used to create 107 * inspector-related actors, including the walker. 108 */ 109 class InspectorActor extends Actor { 110 constructor(conn, targetActor) { 111 super(conn, inspectorSpec); 112 this.targetActor = targetActor; 113 114 this._onColorPicked = this._onColorPicked.bind(this); 115 this._onColorPickCanceled = this._onColorPickCanceled.bind(this); 116 this.destroyEyeDropper = this.destroyEyeDropper.bind(this); 117 } 118 119 highlightersState = { 120 fadingViewportSizeHiglighter: null, 121 }; 122 123 destroy() { 124 super.destroy(); 125 this.destroyEyeDropper(); 126 127 this._compatibility = null; 128 this._pageStylePromise = null; 129 this._walkerPromise = null; 130 this.walker = null; 131 this.targetActor = null; 132 } 133 134 get window() { 135 return this.targetActor.window; 136 } 137 138 getWalker(options = {}) { 139 if (this._walkerPromise) { 140 return this._walkerPromise; 141 } 142 143 this._walkerPromise = new Promise(resolve => { 144 const domReady = () => { 145 const targetActor = this.targetActor; 146 this.walker = new WalkerActor(this.conn, targetActor, options); 147 this.manage(this.walker); 148 this.walker.once("destroyed", () => { 149 this._walkerPromise = null; 150 this._pageStylePromise = null; 151 }); 152 resolve(this.walker); 153 }; 154 155 if (this.window.document.readyState === "loading") { 156 // Expose an abort controller for DOMContentLoaded to remove the 157 // listener unconditionally, even if the race hits the timeout. 158 const abortController = new AbortController(); 159 Promise.race([ 160 new Promise(r => { 161 this.window.addEventListener("DOMContentLoaded", r, { 162 capture: true, 163 once: true, 164 signal: abortController.signal, 165 }); 166 }), 167 // The DOMContentLoaded event will never be emitted on documents stuck 168 // in the loading state, for instance if document.write was called 169 // without calling document.close. 170 // TODO: It is not clear why we are waiting for the event overall, see 171 // Bug 1766279 to actually stop listening to the event altogether. 172 new Promise(r => setTimeout(r, 500)), 173 ]) 174 .then(domReady) 175 .finally(() => abortController.abort()); 176 } else { 177 domReady(); 178 } 179 }); 180 181 return this._walkerPromise; 182 } 183 184 getPageStyle() { 185 if (this._pageStylePromise) { 186 return this._pageStylePromise; 187 } 188 189 this._pageStylePromise = this.getWalker().then(() => { 190 const pageStyle = new PageStyleActor(this); 191 this.manage(pageStyle); 192 return pageStyle; 193 }); 194 return this._pageStylePromise; 195 } 196 197 getCompatibility() { 198 if (this._compatibility) { 199 return this._compatibility; 200 } 201 202 this._compatibility = new CompatibilityActor(this); 203 this.manage(this._compatibility); 204 return this._compatibility; 205 } 206 207 /** 208 * If consumers need to display several highlighters at the same time or 209 * different types of highlighters, then this method should be used, passing 210 * the type name of the highlighter needed as argument. 211 * A new instance will be created everytime the method is called, so it's up 212 * to the consumer to release it when it is not needed anymore 213 * 214 * @param {string} type The type of highlighter to create 215 * @return {Highlighter} The highlighter actor instance or null if the 216 * typeName passed doesn't match any available highlighter 217 */ 218 async getHighlighterByType(typeName) { 219 if (isTypeRegistered(typeName)) { 220 const highlighterActor = new CustomHighlighterActor(this, typeName); 221 if (highlighterActor.instance.isReady) { 222 await highlighterActor.instance.isReady; 223 } 224 225 return highlighterActor; 226 } 227 return null; 228 } 229 230 /** 231 * Get the node's image data if any (for canvas and img nodes). 232 * Returns an imageData object with the actual data being a LongStringActor 233 * and a size json object. 234 * The image data is transmitted as a base64 encoded png data-uri. 235 * The method rejects if the node isn't an image or if the image is missing 236 * 237 * Accepts a maxDim request parameter to resize images that are larger. This 238 * is important as the resizing occurs server-side so that image-data being 239 * transfered in the longstring back to the client will be that much smaller 240 */ 241 getImageDataFromURL(url, maxDim) { 242 const img = new this.window.Image(); 243 img.src = url; 244 245 // imageToImageData waits for the image to load. 246 return InspectorActorUtils.imageToImageData(img, maxDim).then(imageData => { 247 return { 248 data: new LongStringActor(this.conn, imageData.data), 249 size: imageData.size, 250 }; 251 }); 252 } 253 254 /** 255 * Resolve a URL to its absolute form, in the scope of a given content window. 256 * 257 * @param {string} url. 258 * @param {NodeActor} node If provided, the owner window of this node will be 259 * used to resolve the URL. Otherwise, the top-level content window will be 260 * used instead. 261 * @return {string} url. 262 */ 263 resolveRelativeURL(url, node) { 264 const document = InspectorActorUtils.isNodeDead(node) 265 ? this.window.document 266 : InspectorActorUtils.nodeDocument(node.rawNode); 267 268 if (!document) { 269 return url; 270 } 271 272 const baseURI = Services.io.newURI(document.baseURI); 273 return Services.io.newURI(url, null, baseURI).spec; 274 } 275 276 /** 277 * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper. 278 * Note that for now, a new instance is created every time to deal with page navigation. 279 */ 280 createEyeDropper() { 281 this.destroyEyeDropper(); 282 this._highlighterEnv = new HighlighterEnvironment(); 283 this._highlighterEnv.initFromTargetActor(this.targetActor); 284 this._eyeDropper = new EyeDropper(this._highlighterEnv); 285 return this._eyeDropper.isReady; 286 } 287 288 /** 289 * Destroy the current eye-dropper highlighter instance. 290 */ 291 destroyEyeDropper() { 292 if (this._eyeDropper) { 293 this.cancelPickColorFromPage(); 294 this._eyeDropper.destroy(); 295 this._eyeDropper = null; 296 this._highlighterEnv.destroy(); 297 this._highlighterEnv = null; 298 } 299 } 300 301 /** 302 * Pick a color from the page using the eye-dropper. This method doesn't return anything 303 * but will cause events to be sent to the front when a color is picked or when the user 304 * cancels the picker. 305 * 306 * @param {object} options 307 */ 308 async pickColorFromPage(options) { 309 await this.createEyeDropper(); 310 this._eyeDropper.show(this.window.document.documentElement, options); 311 this._eyeDropper.once("selected", this._onColorPicked); 312 this._eyeDropper.once("canceled", this._onColorPickCanceled); 313 this.targetActor.once("will-navigate", this.destroyEyeDropper); 314 } 315 316 /** 317 * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper 318 * highlighter is for the user to click in the page and select a color. If you need to 319 * dismiss the eye-dropper programatically instead, use this method. 320 */ 321 cancelPickColorFromPage() { 322 if (this._eyeDropper) { 323 this._eyeDropper.hide(); 324 this._eyeDropper.off("selected", this._onColorPicked); 325 this._eyeDropper.off("canceled", this._onColorPickCanceled); 326 this.targetActor.off("will-navigate", this.destroyEyeDropper); 327 } 328 } 329 330 /** 331 * Check if the current document supports highlighters using a canvasFrame anonymous 332 * content container. 333 * It is impossible to detect the feature programmatically as some document types simply 334 * don't render the canvasFrame without throwing any error. 335 */ 336 supportsHighlighters() { 337 const doc = this.targetActor.window.document; 338 const ns = doc.documentElement.namespaceURI; 339 340 // XUL documents do not support insertAnonymousContent(). 341 if (ns === XUL_NS) { 342 return false; 343 } 344 345 // SVG documents do not render the canvasFrame (see Bug 1157592). 346 if (ns === SVG_NS) { 347 return false; 348 } 349 350 return true; 351 } 352 353 _onColorPicked(color) { 354 this.emit("color-picked", color); 355 } 356 357 _onColorPickCanceled() { 358 this.emit("color-pick-canceled"); 359 } 360 } 361 362 exports.InspectorActor = InspectorActor;