target-configuration.js (19540B)
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 { Actor } = require("resource://devtools/shared/protocol.js"); 8 const { 9 targetConfigurationSpec, 10 } = require("resource://devtools/shared/specs/target-configuration.js"); 11 12 const { SessionDataHelpers } = ChromeUtils.importESModule( 13 "resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs", 14 { global: "contextual" } 15 ); 16 const { isBrowsingContextPartOfContext } = ChromeUtils.importESModule( 17 "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", 18 { global: "contextual" } 19 ); 20 loader.lazyRequireGetter( 21 this, 22 "TRACER_LOG_METHODS", 23 "resource://devtools/shared/specs/tracer.js", 24 true 25 ); 26 const { SUPPORTED_DATA } = SessionDataHelpers; 27 const { TARGET_CONFIGURATION } = SUPPORTED_DATA; 28 const LOG_DISABLED = -1; 29 30 // List of options supported by this target configuration actor. 31 /* eslint sort-keys: "error" */ 32 const SUPPORTED_OPTIONS = { 33 // Disable network request caching. 34 cacheDisabled: true, 35 // Enable color scheme simulation. 36 colorSchemeSimulation: true, 37 // Enable custom formatters 38 customFormatters: true, 39 // Set a custom user agent 40 customUserAgent: true, 41 // Is the tracer experimental feature manually enabled by the user? 42 isTracerFeatureEnabled: true, 43 // Enable JavaScript 44 javascriptEnabled: true, 45 // Force a custom device pixel ratio (used in RDM). Set to null to restore origin ratio. 46 overrideDPPX: true, 47 // Enable print simulation mode. 48 printSimulationEnabled: true, 49 // Override navigator.maxTouchPoints (used in RDM and doesn't apply if RDM isn't enabled) 50 rdmPaneMaxTouchPoints: true, 51 // Page orientation (used in RDM and doesn't apply if RDM isn't enabled) 52 rdmPaneOrientation: true, 53 // Enable allocation tracking, if set, contains an object defining the tracking configurations 54 recordAllocations: true, 55 // Reload the page when the touch simulation state changes (only works alongside touchEventsOverride) 56 reloadOnTouchSimulationToggle: true, 57 // Restore focus in the page after closing DevTools. 58 restoreFocus: true, 59 // Enable service worker testing over HTTP (instead of HTTPS only). 60 serviceWorkersTestingEnabled: true, 61 // Set the current tab offline 62 setTabOffline: true, 63 // Enable touch events simulation 64 touchEventsOverride: true, 65 // Used to configure and start/stop the JavaScript tracer 66 tracerOptions: true, 67 // Use simplified highlighters when prefers-reduced-motion is enabled. 68 useSimpleHighlightersForReducedMotion: true, 69 }; 70 /* eslint-disable sort-keys */ 71 72 /** 73 * This actor manages the configuration flags which apply to DevTools targets. 74 * 75 * Configuration flags should be applied to all concerned targets when the 76 * configuration is updated, and new targets should also be able to read the 77 * flags when they are created. The flags will be forwarded to the WatcherActor 78 * and stored as TARGET_CONFIGURATION data entries. 79 * Some flags will be set directly set from this actor, in the parent process 80 * (see _updateParentProcessConfiguration), and others will be set from the target actor, 81 * in the content process. 82 * 83 * @class 84 */ 85 class TargetConfigurationActor extends Actor { 86 constructor(watcherActor) { 87 super(watcherActor.conn, targetConfigurationSpec); 88 this.watcherActor = watcherActor; 89 90 this._onBrowsingContextAttached = 91 this._onBrowsingContextAttached.bind(this); 92 // We need to be notified of new browsing context being created so we can re-set flags 93 // we already set on the "previous" browsing context. We're using this event as it's 94 // emitted very early in the document lifecycle (i.e. before any script on the page is 95 // executed), which is not the case for "window-global-created" for example. 96 Services.obs.addObserver( 97 this._onBrowsingContextAttached, 98 "browsing-context-attached" 99 ); 100 101 // When we perform a bfcache navigation, the current browsing context gets 102 // replaced with a browsing which was previously stored in bfcache and we 103 // should update our reference accordingly. 104 this._onBfCacheNavigation = this._onBfCacheNavigation.bind(this); 105 this.watcherActor.on( 106 "bf-cache-navigation-pageshow", 107 this._onBfCacheNavigation 108 ); 109 110 this._browsingContext = this.watcherActor.browserElement?.browsingContext; 111 } 112 113 // Value of `logging.console` pref, before starting recording JS Traces 114 #consolePrefValue; 115 // Value of `logging.PageMessages` pref, before starting recording JS Traces 116 #pageMessagesPrefValue; 117 118 form() { 119 return { 120 actor: this.actorID, 121 configuration: this._getConfiguration(), 122 traits: { supportedOptions: SUPPORTED_OPTIONS }, 123 }; 124 } 125 126 /** 127 * Returns whether or not this actor should handle the flag that should be set on the 128 * BrowsingContext in the parent process. 129 * 130 * @returns {boolean} 131 */ 132 _shouldHandleConfigurationInParentProcess() { 133 // Only handle parent process configuration if the watcherActor is tied to a 134 // browser element. 135 // For now, the Browser Toolbox and Web Extension are having a unique target 136 // which applies the configuration by itself on new documents. 137 return this.watcherActor.sessionContext.type == "browser-element"; 138 } 139 140 /** 141 * Event handler for attached browsing context. This will be called when 142 * a new browsing context is created that we might want to handle 143 * (e.g. when navigating to a page with Cross-Origin-Opener-Policy header) 144 */ 145 _onBrowsingContextAttached(browsingContext) { 146 if (!this._shouldHandleConfigurationInParentProcess()) { 147 return; 148 } 149 150 // We only want to set flags on top-level browsing context. The platform 151 // will take care of propagating it to the entire browsing contexts tree. 152 if (browsingContext.parent) { 153 return; 154 } 155 156 // Only process BrowsingContexts which are related to the debugged scope. 157 // As this callback fires very early, the BrowsingContext may not have 158 // any WindowGlobal yet and so we ignore all checks dones against the WindowGlobal 159 // if there is none. Meaning we might accept more BrowsingContext than expected. 160 if ( 161 !isBrowsingContextPartOfContext( 162 browsingContext, 163 this.watcherActor.sessionContext, 164 { acceptNoWindowGlobal: true, forceAcceptTopLevelTarget: true } 165 ) 166 ) { 167 return; 168 } 169 170 const rdmEnabledInPreviousBrowsingContext = this._browsingContext.inRDMPane; 171 172 // Before replacing the target browsing context, restore the configuration 173 // on the previous one if they share the same browser. 174 if ( 175 this._browsingContext && 176 this._browsingContext.browserId === browsingContext.browserId && 177 !this._browsingContext.isDiscarded 178 ) { 179 // For now this should always be true as long as we already had a browsing 180 // context set, but the same logic should be used when supporting EFT on 181 // toolboxes with several top level browsing contexts: when a new browsing 182 // context attaches, only reset the browsing context with the same browserId 183 this._restoreParentProcessConfiguration(); 184 } 185 186 // We need to store the browsing context as this.watcherActor.browserElement.browsingContext 187 // can still refer to the previous browsing context at this point. 188 this._browsingContext = browsingContext; 189 190 // If `inRDMPane` was set in the previous browsing context, set it again on the new one, 191 // otherwise some RDM-related configuration won't be applied (e.g. orientation). 192 if (rdmEnabledInPreviousBrowsingContext) { 193 this._browsingContext.inRDMPane = true; 194 } 195 this._updateParentProcessConfiguration(this._getConfiguration()); 196 } 197 198 _onBfCacheNavigation({ windowGlobal } = {}) { 199 if (windowGlobal) { 200 this._onBrowsingContextAttached(windowGlobal.browsingContext); 201 } 202 } 203 204 _getConfiguration() { 205 const targetConfigurationData = 206 this.watcherActor.getSessionDataForType(TARGET_CONFIGURATION); 207 if (!targetConfigurationData) { 208 return {}; 209 } 210 211 const cfgMap = {}; 212 for (const { key, value } of targetConfigurationData) { 213 cfgMap[key] = value; 214 } 215 return cfgMap; 216 } 217 218 /** 219 * 220 * @param {object} configuration 221 * @returns Promise<Object> Applied configuration object 222 */ 223 async updateConfiguration(configuration) { 224 const cfgArray = Object.keys(configuration) 225 .filter(key => { 226 if (!SUPPORTED_OPTIONS[key]) { 227 console.warn(`Unsupported option for TargetConfiguration: ${key}`); 228 return false; 229 } 230 return true; 231 }) 232 .map(key => ({ key, value: configuration[key] })); 233 234 this._updateParentProcessConfiguration(configuration); 235 await this.watcherActor.addOrSetDataEntry( 236 TARGET_CONFIGURATION, 237 cfgArray, 238 "add" 239 ); 240 return this._getConfiguration(); 241 } 242 243 /** 244 * 245 * @param {object} configuration: See `updateConfiguration` 246 */ 247 _updateParentProcessConfiguration(configuration) { 248 // Process "tracerOptions" for all session types, as this isn't specific to tab debugging 249 if ("tracerOptions" in configuration) { 250 this._setTracerOptions(configuration.tracerOptions); 251 } 252 253 if (!this._shouldHandleConfigurationInParentProcess()) { 254 return; 255 } 256 257 let shouldReload = false; 258 for (const [key, value] of Object.entries(configuration)) { 259 switch (key) { 260 case "colorSchemeSimulation": 261 this._setColorSchemeSimulation(value); 262 break; 263 case "customUserAgent": 264 this._setCustomUserAgent(value); 265 break; 266 case "javascriptEnabled": 267 if (value !== undefined) { 268 // This flag requires a reload in order to take full effect, 269 // so reload if it has changed. 270 if (value != this.isJavascriptEnabled()) { 271 shouldReload = true; 272 } 273 this._setJavascriptEnabled(value); 274 } 275 break; 276 case "overrideDPPX": 277 this._setDPPXOverride(value); 278 break; 279 case "printSimulationEnabled": 280 this._setPrintSimulationEnabled(value); 281 break; 282 case "rdmPaneMaxTouchPoints": 283 this._setRDMPaneMaxTouchPoints(value); 284 break; 285 case "rdmPaneOrientation": 286 this._setRDMPaneOrientation(value); 287 break; 288 case "serviceWorkersTestingEnabled": 289 this._setServiceWorkersTestingEnabled(value); 290 break; 291 case "touchEventsOverride": 292 this._setTouchEventsOverride(value); 293 break; 294 case "cacheDisabled": 295 this._setCacheDisabled(value); 296 break; 297 case "setTabOffline": 298 this._setTabOffline(value); 299 break; 300 } 301 } 302 303 if (shouldReload) { 304 this._browsingContext.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); 305 } 306 } 307 308 _restoreParentProcessConfiguration() { 309 // Always process tracer options as this isn't specific to tab debugging 310 if (this.#consolePrefValue !== undefined) { 311 this._setTracerOptions(); 312 } 313 314 if (!this._shouldHandleConfigurationInParentProcess()) { 315 return; 316 } 317 318 this._setServiceWorkersTestingEnabled(false); 319 this._setPrintSimulationEnabled(false); 320 if (this._resetCacheDisabledOnDestroy) { 321 this._setCacheDisabled(false); 322 } 323 this._setTabOffline(false); 324 325 // Restore the color scheme simulation only if it was explicitly updated 326 // by this actor. This will avoid side effects caused when destroying additional 327 // targets (e.g. RDM target, WebExtension target, …). 328 // TODO: We may want to review other configuration values to see if we should use 329 // the same pattern (Bug 1701553). 330 if (this._resetColorSchemeSimulationOnDestroy) { 331 this._setColorSchemeSimulation(null); 332 } 333 334 // Restore the user agent only if it was explicitly updated by this specific actor. 335 if (this._initialUserAgent !== undefined) { 336 this._setCustomUserAgent(this._initialUserAgent); 337 } 338 339 // Restore the origin device pixel ratio only if it was explicitly updated by this 340 // specific actor. 341 if (this._initialDPPXOverride !== undefined) { 342 this._setDPPXOverride(this._initialDPPXOverride); 343 } 344 345 if (this._initialJavascriptEnabled !== undefined) { 346 this._setJavascriptEnabled(this._initialJavascriptEnabled); 347 } 348 349 if (this._initialTouchEventsOverride !== undefined) { 350 this._setTouchEventsOverride(this._initialTouchEventsOverride); 351 } 352 } 353 354 /** 355 * Disable or enable the service workers testing features. 356 */ 357 _setServiceWorkersTestingEnabled(enabled) { 358 if (this._browsingContext.serviceWorkersTestingEnabled != enabled) { 359 this._browsingContext.serviceWorkersTestingEnabled = enabled; 360 } 361 } 362 363 /** 364 * Disable or enable the print simulation. 365 */ 366 _setPrintSimulationEnabled(enabled) { 367 const value = enabled ? "print" : ""; 368 if (this._browsingContext.mediumOverride != value) { 369 this._browsingContext.mediumOverride = value; 370 } 371 } 372 373 /** 374 * Disable or enable the color-scheme simulation. 375 */ 376 _setColorSchemeSimulation(override) { 377 const value = override || "none"; 378 if (this._browsingContext.prefersColorSchemeOverride != value) { 379 this._browsingContext.prefersColorSchemeOverride = value; 380 this._resetColorSchemeSimulationOnDestroy = true; 381 } 382 } 383 384 /** 385 * Set a custom user agent on the page 386 * 387 * @param {string} userAgent: The user agent to set on the page. If null, will reset the 388 * user agent to its original value. 389 * @returns {boolean} Whether the user agent was changed or not. 390 */ 391 _setCustomUserAgent(userAgent = "") { 392 if (this._browsingContext.customUserAgent === userAgent) { 393 return; 394 } 395 396 if (this._initialUserAgent === undefined) { 397 this._initialUserAgent = this._browsingContext.customUserAgent; 398 } 399 400 this._browsingContext.customUserAgent = userAgent; 401 } 402 403 isJavascriptEnabled() { 404 return this._browsingContext.allowJavascript; 405 } 406 407 _setJavascriptEnabled(allow) { 408 if (this._initialJavascriptEnabled === undefined) { 409 this._initialJavascriptEnabled = this._browsingContext.allowJavascript; 410 } 411 if (allow !== undefined) { 412 this._browsingContext.allowJavascript = allow; 413 } 414 } 415 416 /* DPPX override */ 417 _setDPPXOverride(dppx) { 418 if (this._browsingContext.overrideDPPX === dppx) { 419 return; 420 } 421 422 if (!dppx && this._initialDPPXOverride) { 423 dppx = this._initialDPPXOverride; 424 } else if (dppx !== undefined && this._initialDPPXOverride === undefined) { 425 this._initialDPPXOverride = this._browsingContext.overrideDPPX; 426 } 427 428 if (dppx !== undefined) { 429 this._browsingContext.overrideDPPX = dppx; 430 } 431 } 432 433 /** 434 * Set the touchEventsOverride on the browsing context. 435 * 436 * @param {string} flag: See BrowsingContext.webidl `TouchEventsOverride` enum for values. 437 */ 438 _setTouchEventsOverride(flag) { 439 if (this._browsingContext.touchEventsOverride === flag) { 440 return; 441 } 442 443 if (!flag && this._initialTouchEventsOverride) { 444 flag = this._initialTouchEventsOverride; 445 } else if ( 446 flag !== undefined && 447 this._initialTouchEventsOverride === undefined 448 ) { 449 this._initialTouchEventsOverride = 450 this._browsingContext.touchEventsOverride; 451 } 452 453 if (flag !== undefined) { 454 this._browsingContext.touchEventsOverride = flag; 455 } 456 } 457 458 /** 459 * Overrides navigator.maxTouchPoints. 460 * Note that we don't need to reset the original value when the actor is destroyed, 461 * as it's directly handled by the platform when RDM is closed. 462 * 463 * @param {Integer} maxTouchPoints 464 */ 465 _setRDMPaneMaxTouchPoints(maxTouchPoints) { 466 this._browsingContext.setRDMPaneMaxTouchPoints(maxTouchPoints); 467 } 468 469 /** 470 * Set an orientation and an angle on the browsing context. This will be applied only 471 * if Responsive Design Mode is enabled. 472 * 473 * @param {object} options 474 * @param {string} options.type: The orientation type of the rotated device. 475 * @param {number} options.angle: The rotated angle of the device. 476 */ 477 _setRDMPaneOrientation({ type, angle }) { 478 if (this._browsingContext.inRDMPane) { 479 this._browsingContext.setOrientationOverride(type, angle); 480 } 481 } 482 483 /** 484 * Disable or enable the cache via the browsing context. 485 * 486 * @param {boolean} disabled: The state the cache should be changed to 487 */ 488 _setCacheDisabled(disabled) { 489 const value = disabled 490 ? Ci.nsIRequest.LOAD_BYPASS_CACHE 491 : Ci.nsIRequest.LOAD_NORMAL; 492 if (this._browsingContext.defaultLoadFlags != value) { 493 this._browsingContext.defaultLoadFlags = value; 494 this._resetCacheDisabledOnDestroy = true; 495 } 496 } 497 498 /** 499 * Set the browsing context to offline. 500 * 501 * @param {boolean} offline: Whether the network throttling is set to offline 502 */ 503 _setTabOffline(offline) { 504 if (!this._browsingContext.isDiscarded) { 505 this._browsingContext.forceOffline = offline; 506 } 507 } 508 509 destroy() { 510 Services.obs.removeObserver( 511 this._onBrowsingContextAttached, 512 "browsing-context-attached" 513 ); 514 this.watcherActor.off( 515 "bf-cache-navigation-pageshow", 516 this._onBfCacheNavigation 517 ); 518 // Avoid trying to restore if the related context is already being destroyed 519 if (this._browsingContext && !this._browsingContext.isDiscarded) { 520 this._restoreParentProcessConfiguration(); 521 } 522 super.destroy(); 523 } 524 525 /** 526 * Called when the tracer is toggled on/off by the frontend. 527 * Note that when `options` is defined, it is meant to be enabled. 528 * It may not actually be tracing yet depending on the passed options. 529 * 530 * @param {object} options 531 */ 532 _setTracerOptions(options) { 533 if (!options) { 534 if (this.#consolePrefValue === LOG_DISABLED) { 535 Services.prefs.clearUserPref("logging.console"); 536 } else { 537 Services.prefs.setIntPref("logging.console", this.#consolePrefValue); 538 } 539 this.#consolePrefValue = undefined; 540 if (this.#pageMessagesPrefValue === LOG_DISABLED) { 541 Services.prefs.clearUserPref("logging.PageMessages"); 542 } else { 543 Services.prefs.setIntPref( 544 "logging.PageMessages", 545 this.#pageMessagesPrefValue 546 ); 547 } 548 this.#pageMessagesPrefValue = undefined; 549 return; 550 } 551 552 // Only enable the MOZ_LOG's when recording to the profiler, 553 // otherwise it would pollute firefox stdout unexpectedly. 554 if (options.logMethod != TRACER_LOG_METHODS.PROFILER) { 555 return; 556 } 557 558 // Enable `MOZ_LOG=console:5` via the logging.console so that all console API calls 559 // are stored in the profiler when recording JS Traces via the profiler. 560 // 561 // We do this from here as TargetConfiguration runs in the parent process, 562 // where we can set preferences. Whereas the profiler tracer actor runs in the content process. 563 const LOG_VERBOSE = 5; 564 this.#consolePrefValue = Services.prefs.getIntPref( 565 "logging.console", 566 LOG_DISABLED 567 ); 568 Services.prefs.setIntPref("logging.console", LOG_VERBOSE); 569 this.#pageMessagesPrefValue = Services.prefs.getIntPref( 570 "logging.PageMessages", 571 LOG_DISABLED 572 ); 573 Services.prefs.setIntPref("logging.PageMessages", LOG_VERBOSE); 574 } 575 } 576 577 exports.TargetConfigurationActor = TargetConfigurationActor;