573 lines
19 KiB
JavaScript
573 lines
19 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const { Actor } = require("resource://devtools/shared/protocol.js");
|
|
const {
|
|
targetConfigurationSpec,
|
|
} = require("resource://devtools/shared/specs/target-configuration.js");
|
|
|
|
const { SessionDataHelpers } = ChromeUtils.importESModule(
|
|
"resource://devtools/server/actors/watcher/SessionDataHelpers.sys.mjs",
|
|
{ global: "contextual" }
|
|
);
|
|
const { isBrowsingContextPartOfContext } = ChromeUtils.importESModule(
|
|
"resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
|
|
{ global: "contextual" }
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"TRACER_LOG_METHODS",
|
|
"resource://devtools/shared/specs/tracer.js",
|
|
true
|
|
);
|
|
const { SUPPORTED_DATA } = SessionDataHelpers;
|
|
const { TARGET_CONFIGURATION } = SUPPORTED_DATA;
|
|
const LOG_DISABLED = -1;
|
|
|
|
// List of options supported by this target configuration actor.
|
|
/* eslint sort-keys: "error" */
|
|
const SUPPORTED_OPTIONS = {
|
|
// Disable network request caching.
|
|
cacheDisabled: true,
|
|
// Enable color scheme simulation.
|
|
colorSchemeSimulation: true,
|
|
// Enable custom formatters
|
|
customFormatters: true,
|
|
// Set a custom user agent
|
|
customUserAgent: true,
|
|
// Is the tracer experimental feature manually enabled by the user?
|
|
isTracerFeatureEnabled: true,
|
|
// Enable JavaScript
|
|
javascriptEnabled: true,
|
|
// Force a custom device pixel ratio (used in RDM). Set to null to restore origin ratio.
|
|
overrideDPPX: true,
|
|
// Enable print simulation mode.
|
|
printSimulationEnabled: true,
|
|
// Override navigator.maxTouchPoints (used in RDM and doesn't apply if RDM isn't enabled)
|
|
rdmPaneMaxTouchPoints: true,
|
|
// Page orientation (used in RDM and doesn't apply if RDM isn't enabled)
|
|
rdmPaneOrientation: true,
|
|
// Enable allocation tracking, if set, contains an object defining the tracking configurations
|
|
recordAllocations: true,
|
|
// Reload the page when the touch simulation state changes (only works alongside touchEventsOverride)
|
|
reloadOnTouchSimulationToggle: true,
|
|
// Restore focus in the page after closing DevTools.
|
|
restoreFocus: true,
|
|
// Enable service worker testing over HTTP (instead of HTTPS only).
|
|
serviceWorkersTestingEnabled: true,
|
|
// Set the current tab offline
|
|
setTabOffline: true,
|
|
// Enable touch events simulation
|
|
touchEventsOverride: true,
|
|
// Used to configure and start/stop the JavaScript tracer
|
|
tracerOptions: true,
|
|
// Use simplified highlighters when prefers-reduced-motion is enabled.
|
|
useSimpleHighlightersForReducedMotion: true,
|
|
};
|
|
/* eslint-disable sort-keys */
|
|
|
|
/**
|
|
* This actor manages the configuration flags which apply to DevTools targets.
|
|
*
|
|
* Configuration flags should be applied to all concerned targets when the
|
|
* configuration is updated, and new targets should also be able to read the
|
|
* flags when they are created. The flags will be forwarded to the WatcherActor
|
|
* and stored as TARGET_CONFIGURATION data entries.
|
|
* Some flags will be set directly set from this actor, in the parent process
|
|
* (see _updateParentProcessConfiguration), and others will be set from the target actor,
|
|
* in the content process.
|
|
*
|
|
* @constructor
|
|
*
|
|
*/
|
|
class TargetConfigurationActor extends Actor {
|
|
constructor(watcherActor) {
|
|
super(watcherActor.conn, targetConfigurationSpec);
|
|
this.watcherActor = watcherActor;
|
|
|
|
this._onBrowsingContextAttached =
|
|
this._onBrowsingContextAttached.bind(this);
|
|
// We need to be notified of new browsing context being created so we can re-set flags
|
|
// we already set on the "previous" browsing context. We're using this event as it's
|
|
// emitted very early in the document lifecycle (i.e. before any script on the page is
|
|
// executed), which is not the case for "window-global-created" for example.
|
|
Services.obs.addObserver(
|
|
this._onBrowsingContextAttached,
|
|
"browsing-context-attached"
|
|
);
|
|
|
|
// When we perform a bfcache navigation, the current browsing context gets
|
|
// replaced with a browsing which was previously stored in bfcache and we
|
|
// should update our reference accordingly.
|
|
this._onBfCacheNavigation = this._onBfCacheNavigation.bind(this);
|
|
this.watcherActor.on(
|
|
"bf-cache-navigation-pageshow",
|
|
this._onBfCacheNavigation
|
|
);
|
|
|
|
this._browsingContext = this.watcherActor.browserElement?.browsingContext;
|
|
}
|
|
|
|
// Value of `logging.console` pref, before starting recording JS Traces
|
|
#consolePrefValue;
|
|
// Value of `logging.PageMessages` pref, before starting recording JS Traces
|
|
#pageMessagesPrefValue;
|
|
|
|
form() {
|
|
return {
|
|
actor: this.actorID,
|
|
configuration: this._getConfiguration(),
|
|
traits: { supportedOptions: SUPPORTED_OPTIONS },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not this actor should handle the flag that should be set on the
|
|
* BrowsingContext in the parent process.
|
|
*
|
|
* @returns {Boolean}
|
|
*/
|
|
_shouldHandleConfigurationInParentProcess() {
|
|
// Only handle parent process configuration if the watcherActor is tied to a
|
|
// browser element.
|
|
// For now, the Browser Toolbox and Web Extension are having a unique target
|
|
// which applies the configuration by itself on new documents.
|
|
return this.watcherActor.sessionContext.type == "browser-element";
|
|
}
|
|
|
|
/**
|
|
* Event handler for attached browsing context. This will be called when
|
|
* a new browsing context is created that we might want to handle
|
|
* (e.g. when navigating to a page with Cross-Origin-Opener-Policy header)
|
|
*/
|
|
_onBrowsingContextAttached(browsingContext) {
|
|
if (!this._shouldHandleConfigurationInParentProcess()) {
|
|
return;
|
|
}
|
|
|
|
// We only want to set flags on top-level browsing context. The platform
|
|
// will take care of propagating it to the entire browsing contexts tree.
|
|
if (browsingContext.parent) {
|
|
return;
|
|
}
|
|
|
|
// Only process BrowsingContexts which are related to the debugged scope.
|
|
// As this callback fires very early, the BrowsingContext may not have
|
|
// any WindowGlobal yet and so we ignore all checks dones against the WindowGlobal
|
|
// if there is none. Meaning we might accept more BrowsingContext than expected.
|
|
if (
|
|
!isBrowsingContextPartOfContext(
|
|
browsingContext,
|
|
this.watcherActor.sessionContext,
|
|
{ acceptNoWindowGlobal: true, forceAcceptTopLevelTarget: true }
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const rdmEnabledInPreviousBrowsingContext = this._browsingContext.inRDMPane;
|
|
|
|
// Before replacing the target browsing context, restore the configuration
|
|
// on the previous one if they share the same browser.
|
|
if (
|
|
this._browsingContext &&
|
|
this._browsingContext.browserId === browsingContext.browserId &&
|
|
!this._browsingContext.isDiscarded
|
|
) {
|
|
// For now this should always be true as long as we already had a browsing
|
|
// context set, but the same logic should be used when supporting EFT on
|
|
// toolboxes with several top level browsing contexts: when a new browsing
|
|
// context attaches, only reset the browsing context with the same browserId
|
|
this._restoreParentProcessConfiguration();
|
|
}
|
|
|
|
// We need to store the browsing context as this.watcherActor.browserElement.browsingContext
|
|
// can still refer to the previous browsing context at this point.
|
|
this._browsingContext = browsingContext;
|
|
|
|
// If `inRDMPane` was set in the previous browsing context, set it again on the new one,
|
|
// otherwise some RDM-related configuration won't be applied (e.g. orientation).
|
|
if (rdmEnabledInPreviousBrowsingContext) {
|
|
this._browsingContext.inRDMPane = true;
|
|
}
|
|
this._updateParentProcessConfiguration(this._getConfiguration());
|
|
}
|
|
|
|
_onBfCacheNavigation({ windowGlobal } = {}) {
|
|
if (windowGlobal) {
|
|
this._onBrowsingContextAttached(windowGlobal.browsingContext);
|
|
}
|
|
}
|
|
|
|
_getConfiguration() {
|
|
const targetConfigurationData =
|
|
this.watcherActor.getSessionDataForType(TARGET_CONFIGURATION);
|
|
if (!targetConfigurationData) {
|
|
return {};
|
|
}
|
|
|
|
const cfgMap = {};
|
|
for (const { key, value } of targetConfigurationData) {
|
|
cfgMap[key] = value;
|
|
}
|
|
return cfgMap;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Object} configuration
|
|
* @returns Promise<Object> Applied configuration object
|
|
*/
|
|
async updateConfiguration(configuration) {
|
|
const cfgArray = Object.keys(configuration)
|
|
.filter(key => {
|
|
if (!SUPPORTED_OPTIONS[key]) {
|
|
console.warn(`Unsupported option for TargetConfiguration: ${key}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
.map(key => ({ key, value: configuration[key] }));
|
|
|
|
this._updateParentProcessConfiguration(configuration);
|
|
await this.watcherActor.addOrSetDataEntry(
|
|
TARGET_CONFIGURATION,
|
|
cfgArray,
|
|
"add"
|
|
);
|
|
return this._getConfiguration();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Object} configuration: See `updateConfiguration`
|
|
*/
|
|
_updateParentProcessConfiguration(configuration) {
|
|
// Process "tracerOptions" for all session types, as this isn't specific to tab debugging
|
|
if ("tracerOptions" in configuration) {
|
|
this._setTracerOptions(configuration.tracerOptions);
|
|
}
|
|
|
|
if (!this._shouldHandleConfigurationInParentProcess()) {
|
|
return;
|
|
}
|
|
|
|
let shouldReload = false;
|
|
for (const [key, value] of Object.entries(configuration)) {
|
|
switch (key) {
|
|
case "colorSchemeSimulation":
|
|
this._setColorSchemeSimulation(value);
|
|
break;
|
|
case "customUserAgent":
|
|
this._setCustomUserAgent(value);
|
|
break;
|
|
case "javascriptEnabled":
|
|
if (value !== undefined) {
|
|
// This flag requires a reload in order to take full effect,
|
|
// so reload if it has changed.
|
|
if (value != this.isJavascriptEnabled()) {
|
|
shouldReload = true;
|
|
}
|
|
this._setJavascriptEnabled(value);
|
|
}
|
|
break;
|
|
case "overrideDPPX":
|
|
this._setDPPXOverride(value);
|
|
break;
|
|
case "printSimulationEnabled":
|
|
this._setPrintSimulationEnabled(value);
|
|
break;
|
|
case "rdmPaneMaxTouchPoints":
|
|
this._setRDMPaneMaxTouchPoints(value);
|
|
break;
|
|
case "rdmPaneOrientation":
|
|
this._setRDMPaneOrientation(value);
|
|
break;
|
|
case "serviceWorkersTestingEnabled":
|
|
this._setServiceWorkersTestingEnabled(value);
|
|
break;
|
|
case "touchEventsOverride":
|
|
this._setTouchEventsOverride(value);
|
|
break;
|
|
case "cacheDisabled":
|
|
this._setCacheDisabled(value);
|
|
break;
|
|
case "setTabOffline":
|
|
this._setTabOffline(value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (shouldReload) {
|
|
this._browsingContext.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
|
|
}
|
|
}
|
|
|
|
_restoreParentProcessConfiguration() {
|
|
// Always process tracer options as this isn't specific to tab debugging
|
|
if (this.#consolePrefValue !== undefined) {
|
|
this._setTracerOptions();
|
|
}
|
|
|
|
if (!this._shouldHandleConfigurationInParentProcess()) {
|
|
return;
|
|
}
|
|
|
|
this._setServiceWorkersTestingEnabled(false);
|
|
this._setPrintSimulationEnabled(false);
|
|
this._setCacheDisabled(false);
|
|
this._setTabOffline(false);
|
|
|
|
// Restore the color scheme simulation only if it was explicitly updated
|
|
// by this actor. This will avoid side effects caused when destroying additional
|
|
// targets (e.g. RDM target, WebExtension target, …).
|
|
// TODO: We may want to review other configuration values to see if we should use
|
|
// the same pattern (Bug 1701553).
|
|
if (this._resetColorSchemeSimulationOnDestroy) {
|
|
this._setColorSchemeSimulation(null);
|
|
}
|
|
|
|
// Restore the user agent only if it was explicitly updated by this specific actor.
|
|
if (this._initialUserAgent !== undefined) {
|
|
this._setCustomUserAgent(this._initialUserAgent);
|
|
}
|
|
|
|
// Restore the origin device pixel ratio only if it was explicitly updated by this
|
|
// specific actor.
|
|
if (this._initialDPPXOverride !== undefined) {
|
|
this._setDPPXOverride(this._initialDPPXOverride);
|
|
}
|
|
|
|
if (this._initialJavascriptEnabled !== undefined) {
|
|
this._setJavascriptEnabled(this._initialJavascriptEnabled);
|
|
}
|
|
|
|
if (this._initialTouchEventsOverride !== undefined) {
|
|
this._setTouchEventsOverride(this._initialTouchEventsOverride);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable or enable the service workers testing features.
|
|
*/
|
|
_setServiceWorkersTestingEnabled(enabled) {
|
|
if (this._browsingContext.serviceWorkersTestingEnabled != enabled) {
|
|
this._browsingContext.serviceWorkersTestingEnabled = enabled;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable or enable the print simulation.
|
|
*/
|
|
_setPrintSimulationEnabled(enabled) {
|
|
const value = enabled ? "print" : "";
|
|
if (this._browsingContext.mediumOverride != value) {
|
|
this._browsingContext.mediumOverride = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable or enable the color-scheme simulation.
|
|
*/
|
|
_setColorSchemeSimulation(override) {
|
|
const value = override || "none";
|
|
if (this._browsingContext.prefersColorSchemeOverride != value) {
|
|
this._browsingContext.prefersColorSchemeOverride = value;
|
|
this._resetColorSchemeSimulationOnDestroy = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set a custom user agent on the page
|
|
*
|
|
* @param {String} userAgent: The user agent to set on the page. If null, will reset the
|
|
* user agent to its original value.
|
|
* @returns {Boolean} Whether the user agent was changed or not.
|
|
*/
|
|
_setCustomUserAgent(userAgent = "") {
|
|
if (this._browsingContext.customUserAgent === userAgent) {
|
|
return;
|
|
}
|
|
|
|
if (this._initialUserAgent === undefined) {
|
|
this._initialUserAgent = this._browsingContext.customUserAgent;
|
|
}
|
|
|
|
this._browsingContext.customUserAgent = userAgent;
|
|
}
|
|
|
|
isJavascriptEnabled() {
|
|
return this._browsingContext.allowJavascript;
|
|
}
|
|
|
|
_setJavascriptEnabled(allow) {
|
|
if (this._initialJavascriptEnabled === undefined) {
|
|
this._initialJavascriptEnabled = this._browsingContext.allowJavascript;
|
|
}
|
|
if (allow !== undefined) {
|
|
this._browsingContext.allowJavascript = allow;
|
|
}
|
|
}
|
|
|
|
/* DPPX override */
|
|
_setDPPXOverride(dppx) {
|
|
if (this._browsingContext.overrideDPPX === dppx) {
|
|
return;
|
|
}
|
|
|
|
if (!dppx && this._initialDPPXOverride) {
|
|
dppx = this._initialDPPXOverride;
|
|
} else if (dppx !== undefined && this._initialDPPXOverride === undefined) {
|
|
this._initialDPPXOverride = this._browsingContext.overrideDPPX;
|
|
}
|
|
|
|
if (dppx !== undefined) {
|
|
this._browsingContext.overrideDPPX = dppx;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the touchEventsOverride on the browsing context.
|
|
*
|
|
* @param {String} flag: See BrowsingContext.webidl `TouchEventsOverride` enum for values.
|
|
*/
|
|
_setTouchEventsOverride(flag) {
|
|
if (this._browsingContext.touchEventsOverride === flag) {
|
|
return;
|
|
}
|
|
|
|
if (!flag && this._initialTouchEventsOverride) {
|
|
flag = this._initialTouchEventsOverride;
|
|
} else if (
|
|
flag !== undefined &&
|
|
this._initialTouchEventsOverride === undefined
|
|
) {
|
|
this._initialTouchEventsOverride =
|
|
this._browsingContext.touchEventsOverride;
|
|
}
|
|
|
|
if (flag !== undefined) {
|
|
this._browsingContext.touchEventsOverride = flag;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Overrides navigator.maxTouchPoints.
|
|
* Note that we don't need to reset the original value when the actor is destroyed,
|
|
* as it's directly handled by the platform when RDM is closed.
|
|
*
|
|
* @param {Integer} maxTouchPoints
|
|
*/
|
|
_setRDMPaneMaxTouchPoints(maxTouchPoints) {
|
|
this._browsingContext.setRDMPaneMaxTouchPoints(maxTouchPoints);
|
|
}
|
|
|
|
/**
|
|
* Set an orientation and an angle on the browsing context. This will be applied only
|
|
* if Responsive Design Mode is enabled.
|
|
*
|
|
* @param {Object} options
|
|
* @param {String} options.type: The orientation type of the rotated device.
|
|
* @param {Number} options.angle: The rotated angle of the device.
|
|
*/
|
|
_setRDMPaneOrientation({ type, angle }) {
|
|
this._browsingContext.setRDMPaneOrientation(type, angle);
|
|
}
|
|
|
|
/**
|
|
* Disable or enable the cache via the browsing context.
|
|
*
|
|
* @param {Boolean} disabled: The state the cache should be changed to
|
|
*/
|
|
_setCacheDisabled(disabled) {
|
|
const value = disabled
|
|
? Ci.nsIRequest.LOAD_BYPASS_CACHE
|
|
: Ci.nsIRequest.LOAD_NORMAL;
|
|
if (this._browsingContext.defaultLoadFlags != value) {
|
|
this._browsingContext.defaultLoadFlags = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the browsing context to offline.
|
|
*
|
|
* @param {Boolean} offline: Whether the network throttling is set to offline
|
|
*/
|
|
_setTabOffline(offline) {
|
|
if (!this._browsingContext.isDiscarded) {
|
|
this._browsingContext.forceOffline = offline;
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
Services.obs.removeObserver(
|
|
this._onBrowsingContextAttached,
|
|
"browsing-context-attached"
|
|
);
|
|
this.watcherActor.off(
|
|
"bf-cache-navigation-pageshow",
|
|
this._onBfCacheNavigation
|
|
);
|
|
// Avoid trying to restore if the related context is already being destroyed
|
|
if (this._browsingContext && !this._browsingContext.isDiscarded) {
|
|
this._restoreParentProcessConfiguration();
|
|
}
|
|
super.destroy();
|
|
}
|
|
|
|
/**
|
|
* Called when the tracer is toggled on/off by the frontend.
|
|
* Note that when `options` is defined, it is meant to be enabled.
|
|
* It may not actually be tracing yet depending on the passed options.
|
|
*
|
|
* @param {Object} options
|
|
*/
|
|
_setTracerOptions(options) {
|
|
if (!options) {
|
|
if (this.#consolePrefValue === LOG_DISABLED) {
|
|
Services.prefs.clearUserPref("logging.console");
|
|
} else {
|
|
Services.prefs.setIntPref("logging.console", this.#consolePrefValue);
|
|
}
|
|
this.#consolePrefValue = undefined;
|
|
if (this.#pageMessagesPrefValue === LOG_DISABLED) {
|
|
Services.prefs.clearUserPref("logging.PageMessages");
|
|
} else {
|
|
Services.prefs.setIntPref(
|
|
"logging.PageMessages",
|
|
this.#pageMessagesPrefValue
|
|
);
|
|
}
|
|
this.#pageMessagesPrefValue = undefined;
|
|
return;
|
|
}
|
|
|
|
// Only enable the MOZ_LOG's when recording to the profiler,
|
|
// otherwise it would pollute firefox stdout unexpectedly.
|
|
if (options.logMethod != TRACER_LOG_METHODS.PROFILER) {
|
|
return;
|
|
}
|
|
|
|
// Enable `MOZ_LOG=console:5` via the logging.console so that all console API calls
|
|
// are stored in the profiler when recording JS Traces via the profiler.
|
|
//
|
|
// We do this from here as TargetConfiguration runs in the parent process,
|
|
// where we can set preferences. Whereas the profiler tracer actor runs in the content process.
|
|
const LOG_VERBOSE = 5;
|
|
this.#consolePrefValue = Services.prefs.getIntPref(
|
|
"logging.console",
|
|
LOG_DISABLED
|
|
);
|
|
Services.prefs.setIntPref("logging.console", LOG_VERBOSE);
|
|
this.#pageMessagesPrefValue = Services.prefs.getIntPref(
|
|
"logging.PageMessages",
|
|
LOG_DISABLED
|
|
);
|
|
Services.prefs.setIntPref("logging.PageMessages", LOG_VERBOSE);
|
|
}
|
|
}
|
|
|
|
exports.TargetConfigurationActor = TargetConfigurationActor;
|