summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/app/TelemetryEnvironment.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/app/TelemetryEnvironment.jsm')
-rw-r--r--toolkit/components/telemetry/app/TelemetryEnvironment.jsm2128
1 files changed, 2128 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/app/TelemetryEnvironment.jsm b/toolkit/components/telemetry/app/TelemetryEnvironment.jsm
new file mode 100644
index 0000000000..8e82ad49bb
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryEnvironment.jsm
@@ -0,0 +1,2128 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["TelemetryEnvironment"];
+
+const myScope = this;
+
+const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm", this);
+const { ObjectUtils } = ChromeUtils.import(
+ "resource://gre/modules/ObjectUtils.jsm"
+);
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+if (AppConstants.MOZ_GLEAN) {
+ Cu.importGlobalProperties(["Glean"]);
+}
+
+const Utils = TelemetryUtils;
+
+const { AddonManager, AddonManagerPrivate } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AttributionCode",
+ "resource:///modules/AttributionCode.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ProfileAge",
+ "resource://gre/modules/ProfileAge.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "WindowsVersionInfo",
+ "resource://gre/modules/components-utils/WindowsVersionInfo.jsm"
+);
+
+// The maximum length of a string (e.g. description) in the addons section.
+const MAX_ADDON_STRING_LENGTH = 100;
+// The maximum length of a string value in the settings.attribution object.
+const MAX_ATTRIBUTION_STRING_LENGTH = 100;
+// The maximum lengths for the experiment id and branch in the experiments section.
+const MAX_EXPERIMENT_ID_LENGTH = 100;
+const MAX_EXPERIMENT_BRANCH_LENGTH = 100;
+const MAX_EXPERIMENT_TYPE_LENGTH = 20;
+const MAX_EXPERIMENT_ENROLLMENT_ID_LENGTH = 40;
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+// eslint-disable-next-line no-unused-vars
+var Policy = {
+ now: () => new Date(),
+ _intlLoaded: false,
+ _browserDelayedStartup() {
+ if (Policy._intlLoaded) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve => {
+ let startupTopic = "browser-delayed-startup-finished";
+ Services.obs.addObserver(function observer(subject, topic) {
+ if (topic == startupTopic) {
+ Services.obs.removeObserver(observer, startupTopic);
+ resolve();
+ }
+ }, startupTopic);
+ });
+ },
+};
+
+// This is used to buffer calls to setExperimentActive and friends, so that we
+// don't prematurely initialize our environment if it is called early during
+// startup.
+var gActiveExperimentStartupBuffer = new Map();
+
+var gGlobalEnvironment;
+function getGlobal() {
+ if (!gGlobalEnvironment) {
+ gGlobalEnvironment = new EnvironmentCache();
+ }
+ return gGlobalEnvironment;
+}
+
+var TelemetryEnvironment = {
+ get currentEnvironment() {
+ return getGlobal().currentEnvironment;
+ },
+
+ onInitialized() {
+ return getGlobal().onInitialized();
+ },
+
+ delayedInit() {
+ return getGlobal().delayedInit();
+ },
+
+ registerChangeListener(name, listener) {
+ return getGlobal().registerChangeListener(name, listener);
+ },
+
+ unregisterChangeListener(name) {
+ return getGlobal().unregisterChangeListener(name);
+ },
+
+ /**
+ * Add an experiment annotation to the environment.
+ * If an annotation with the same id already exists, it will be overwritten.
+ * This triggers a new subsession, subject to throttling.
+ *
+ * @param {String} id The id of the active experiment.
+ * @param {String} branch The experiment branch.
+ * @param {Object} [options] Optional object with options.
+ * @param {String} [options.type=false] The specific experiment type.
+ * @param {String} [options.enrollmentId=undefined] The id of the enrollment.
+ */
+ setExperimentActive(id, branch, options = {}) {
+ if (gGlobalEnvironment) {
+ gGlobalEnvironment.setExperimentActive(id, branch, options);
+ } else {
+ gActiveExperimentStartupBuffer.set(id, { branch, options });
+ }
+ },
+
+ /**
+ * Remove an experiment annotation from the environment.
+ * If the annotation exists, a new subsession will triggered.
+ *
+ * @param {String} id The id of the active experiment.
+ */
+ setExperimentInactive(id) {
+ if (gGlobalEnvironment) {
+ gGlobalEnvironment.setExperimentInactive(id);
+ } else {
+ gActiveExperimentStartupBuffer.delete(id);
+ }
+ },
+
+ /**
+ * Returns an object containing the data for the active experiments.
+ *
+ * The returned object is of the format:
+ *
+ * {
+ * "<experiment id>": { branch: "<branch>" },
+ * // …
+ * }
+ */
+ getActiveExperiments() {
+ if (gGlobalEnvironment) {
+ return gGlobalEnvironment.getActiveExperiments();
+ }
+
+ const result = {};
+ for (const [id, { branch }] of gActiveExperimentStartupBuffer.entries()) {
+ result[id] = branch;
+ }
+ return result;
+ },
+
+ shutdown() {
+ return getGlobal().shutdown();
+ },
+
+ // Policy to use when saving preferences. Exported for using them in tests.
+ // Reports "<user-set>" if there is a value set on the user branch
+ RECORD_PREF_STATE: 1,
+
+ // Reports the value set on the user branch, if one is set
+ RECORD_PREF_VALUE: 2,
+
+ // Reports the active value (set on either the user or default branch)
+ // for this pref, if one is set
+ RECORD_DEFAULTPREF_VALUE: 3,
+
+ // Reports "<set>" if a value for this pref is defined on either the user
+ // or default branch
+ RECORD_DEFAULTPREF_STATE: 4,
+
+ // Testing method
+ async testWatchPreferences(prefMap) {
+ return getGlobal()._watchPreferences(prefMap);
+ },
+
+ /**
+ * Intended for use in tests only.
+ *
+ * In multiple tests we need a way to shut and re-start telemetry together
+ * with TelemetryEnvironment. This is problematic due to the fact that
+ * TelemetryEnvironment is a singleton. We, therefore, need this helper
+ * method to be able to re-set TelemetryEnvironment.
+ */
+ testReset() {
+ return getGlobal().reset();
+ },
+
+ /**
+ * Intended for use in tests only.
+ */
+ testCleanRestart() {
+ getGlobal().shutdown();
+ gGlobalEnvironment = null;
+ gActiveExperimentStartupBuffer = new Map();
+ return getGlobal();
+ },
+};
+
+const RECORD_PREF_STATE = TelemetryEnvironment.RECORD_PREF_STATE;
+const RECORD_PREF_VALUE = TelemetryEnvironment.RECORD_PREF_VALUE;
+const RECORD_DEFAULTPREF_VALUE = TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE;
+const RECORD_DEFAULTPREF_STATE = TelemetryEnvironment.RECORD_DEFAULTPREF_STATE;
+const DEFAULT_ENVIRONMENT_PREFS = new Map([
+ ["app.feedback.baseURL", { what: RECORD_PREF_VALUE }],
+ ["app.support.baseURL", { what: RECORD_PREF_VALUE }],
+ ["accessibility.browsewithcaret", { what: RECORD_PREF_VALUE }],
+ ["accessibility.force_disabled", { what: RECORD_PREF_VALUE }],
+ ["app.normandy.test-prefs.bool", { what: RECORD_PREF_VALUE }],
+ ["app.normandy.test-prefs.integer", { what: RECORD_PREF_VALUE }],
+ ["app.normandy.test-prefs.string", { what: RECORD_PREF_VALUE }],
+ ["app.shield.optoutstudies.enabled", { what: RECORD_PREF_VALUE }],
+ ["app.update.interval", { what: RECORD_PREF_VALUE }],
+ ["app.update.service.enabled", { what: RECORD_PREF_VALUE }],
+ ["app.update.silent", { what: RECORD_PREF_VALUE }],
+ ["browser.cache.disk.enable", { what: RECORD_PREF_VALUE }],
+ ["browser.cache.disk.capacity", { what: RECORD_PREF_VALUE }],
+ ["browser.cache.memory.enable", { what: RECORD_PREF_VALUE }],
+ ["browser.cache.offline.enable", { what: RECORD_PREF_VALUE }],
+ ["browser.formfill.enable", { what: RECORD_PREF_VALUE }],
+ ["browser.newtabpage.enabled", { what: RECORD_PREF_VALUE }],
+ ["browser.shell.checkDefaultBrowser", { what: RECORD_PREF_VALUE }],
+ ["browser.search.ignoredJAREngines", { what: RECORD_DEFAULTPREF_VALUE }],
+ ["browser.search.region", { what: RECORD_PREF_VALUE }],
+ ["browser.search.suggest.enabled", { what: RECORD_PREF_VALUE }],
+ ["browser.search.widget.inNavBar", { what: RECORD_DEFAULTPREF_VALUE }],
+ ["browser.startup.homepage", { what: RECORD_PREF_STATE }],
+ ["browser.startup.page", { what: RECORD_PREF_VALUE }],
+ ["browser.urlbar.suggest.searches", { what: RECORD_PREF_VALUE }],
+ ["devtools.chrome.enabled", { what: RECORD_PREF_VALUE }],
+ ["devtools.debugger.enabled", { what: RECORD_PREF_VALUE }],
+ ["devtools.debugger.remote-enabled", { what: RECORD_PREF_VALUE }],
+ ["dom.ipc.plugins.enabled", { what: RECORD_PREF_VALUE }],
+ ["dom.ipc.plugins.sandbox-level.flash", { what: RECORD_PREF_VALUE }],
+ ["dom.ipc.processCount", { what: RECORD_PREF_VALUE }],
+ ["dom.max_script_run_time", { what: RECORD_PREF_VALUE }],
+ ["extensions.autoDisableScopes", { what: RECORD_PREF_VALUE }],
+ ["extensions.enabledScopes", { what: RECORD_PREF_VALUE }],
+ ["extensions.blocklist.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.formautofill.addresses.enabled", { what: RECORD_PREF_VALUE }],
+ [
+ "extensions.formautofill.addresses.capture.enabled",
+ { what: RECORD_PREF_VALUE },
+ ],
+ ["extensions.formautofill.creditCards.enabled", { what: RECORD_PREF_VALUE }],
+ [
+ "extensions.formautofill.creditCards.available",
+ { what: RECORD_PREF_VALUE },
+ ],
+ ["extensions.formautofill.creditCards.used", { what: RECORD_PREF_VALUE }],
+ ["extensions.strictCompatibility", { what: RECORD_PREF_VALUE }],
+ ["extensions.update.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.update.url", { what: RECORD_PREF_VALUE }],
+ ["extensions.update.background.url", { what: RECORD_PREF_VALUE }],
+ ["extensions.screenshots.disabled", { what: RECORD_PREF_VALUE }],
+ ["general.config.filename", { what: RECORD_DEFAULTPREF_STATE }],
+ ["general.smoothScroll", { what: RECORD_PREF_VALUE }],
+ ["gfx.direct2d.disabled", { what: RECORD_PREF_VALUE }],
+ ["gfx.direct2d.force-enabled", { what: RECORD_PREF_VALUE }],
+ ["gfx.webrender.all", { what: RECORD_PREF_VALUE }],
+ ["gfx.webrender.all.qualified", { what: RECORD_PREF_VALUE }],
+ ["gfx.webrender.force-disabled", { what: RECORD_PREF_VALUE }],
+ ["layers.acceleration.disabled", { what: RECORD_PREF_VALUE }],
+ ["layers.acceleration.force-enabled", { what: RECORD_PREF_VALUE }],
+ ["layers.async-pan-zoom.enabled", { what: RECORD_PREF_VALUE }],
+ ["layers.async-video-oop.enabled", { what: RECORD_PREF_VALUE }],
+ ["layers.async-video.enabled", { what: RECORD_PREF_VALUE }],
+ ["layers.componentalpha.enabled", { what: RECORD_PREF_VALUE }],
+ ["layers.d3d11.disable-warp", { what: RECORD_PREF_VALUE }],
+ ["layers.d3d11.force-warp", { what: RECORD_PREF_VALUE }],
+ [
+ "layers.offmainthreadcomposition.force-disabled",
+ { what: RECORD_PREF_VALUE },
+ ],
+ ["layers.prefer-d3d9", { what: RECORD_PREF_VALUE }],
+ ["layers.prefer-opengl", { what: RECORD_PREF_VALUE }],
+ ["layout.css.devPixelsPerPx", { what: RECORD_PREF_VALUE }],
+ ["marionette.enabled", { what: RECORD_PREF_VALUE }],
+ ["network.proxy.autoconfig_url", { what: RECORD_PREF_STATE }],
+ ["network.proxy.http", { what: RECORD_PREF_STATE }],
+ ["network.proxy.ssl", { what: RECORD_PREF_STATE }],
+ ["network.trr.mode", { what: RECORD_PREF_VALUE }],
+ ["pdfjs.disabled", { what: RECORD_PREF_VALUE }],
+ ["places.history.enabled", { what: RECORD_PREF_VALUE }],
+ ["plugins.show_infobar", { what: RECORD_PREF_VALUE }],
+ ["privacy.fuzzyfox.enabled", { what: RECORD_PREF_VALUE }],
+ ["privacy.trackingprotection.enabled", { what: RECORD_PREF_VALUE }],
+ ["privacy.donottrackheader.enabled", { what: RECORD_PREF_VALUE }],
+ ["security.enterprise_roots.auto-enabled", { what: RECORD_PREF_VALUE }],
+ ["security.enterprise_roots.enabled", { what: RECORD_PREF_VALUE }],
+ ["security.pki.mitm_detected", { what: RECORD_PREF_VALUE }],
+ ["security.mixed_content.block_active_content", { what: RECORD_PREF_VALUE }],
+ ["security.mixed_content.block_display_content", { what: RECORD_PREF_VALUE }],
+ ["security.tls.version.enable-deprecated", { what: RECORD_PREF_VALUE }],
+ ["signon.management.page.breach-alerts.enabled", { what: RECORD_PREF_VALUE }],
+ ["signon.autofillForms", { what: RECORD_PREF_VALUE }],
+ ["signon.generation.enabled", { what: RECORD_PREF_VALUE }],
+ ["signon.rememberSignons", { what: RECORD_PREF_VALUE }],
+ ["toolkit.telemetry.pioneerId", { what: RECORD_PREF_STATE }],
+ ["widget.content.allow-gtk-dark-theme", { what: RECORD_DEFAULTPREF_VALUE }],
+ ["widget.content.gtk-theme-override", { what: RECORD_PREF_STATE }],
+ [
+ "widget.content.gtk-high-contrast.enabled",
+ { what: RECORD_DEFAULTPREF_VALUE },
+ ],
+ ["xpinstall.signatures.required", { what: RECORD_PREF_VALUE }],
+]);
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+const PREF_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_DISTRIBUTOR = "app.distributor";
+const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel";
+const PREF_APP_PARTNER_BRANCH = "app.partner.";
+const PREF_PARTNER_ID = "mozilla.partner.id";
+
+const COMPOSITOR_CREATED_TOPIC = "compositor:created";
+const COMPOSITOR_PROCESS_ABORTED_TOPIC = "compositor:process-aborted";
+const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
+ "distribution-customization-complete";
+const GFX_FEATURES_READY_TOPIC = "gfx-features-ready";
+const SEARCH_ENGINE_MODIFIED_TOPIC = "browser-search-engine-modified";
+const SEARCH_SERVICE_TOPIC = "browser-search-service";
+const SESSIONSTORE_WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored";
+const PREF_CHANGED_TOPIC = "nsPref:changed";
+const BLOCKLIST_LOADED_TOPIC = "plugin-blocklist-loaded";
+const AUTO_UPDATE_PREF_CHANGE_TOPIC = "auto-update-config-change";
+const SERVICES_INFO_CHANGE_TOPIC = "sync-ui-state:update";
+
+/**
+ * Enforces the parameter to a boolean value.
+ * @param aValue The input value.
+ * @return {Boolean|Object} If aValue is a boolean or a number, returns its truthfulness
+ * value. Otherwise, return null.
+ */
+function enforceBoolean(aValue) {
+ if (typeof aValue !== "number" && typeof aValue !== "boolean") {
+ return null;
+ }
+ return Boolean(aValue);
+}
+
+/**
+ * Get the current browser locale.
+ * @return a string with the locale or null on failure.
+ */
+function getBrowserLocale() {
+ try {
+ return Services.locale.appLocaleAsBCP47;
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Get the current OS locale.
+ * @return a string with the OS locale or null on failure.
+ */
+function getSystemLocale() {
+ try {
+ return Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ ).systemLocale;
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Get the current OS locales.
+ * @return an array of strings with the OS locales or null on failure.
+ */
+function getSystemLocales() {
+ try {
+ return Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ ).systemLocales;
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Get the current OS regional preference locales.
+ * @return an array of strings with the OS regional preference locales or null on failure.
+ */
+function getRegionalPrefsLocales() {
+ try {
+ return Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ ).regionalPrefsLocales;
+ } catch (e) {
+ return null;
+ }
+}
+
+function getIntlSettings() {
+ return {
+ requestedLocales: Services.locale.requestedLocales,
+ availableLocales: Services.locale.availableLocales,
+ appLocales: Services.locale.appLocalesAsBCP47,
+ systemLocales: getSystemLocales(),
+ regionalPrefsLocales: getRegionalPrefsLocales(),
+ acceptLanguages: Services.prefs
+ .getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString)
+ .data.split(",")
+ .map(str => str.trim()),
+ };
+}
+
+/**
+ * Safely get a sysinfo property and return its value. If the property is not
+ * available, return aDefault.
+ *
+ * @param aPropertyName the property name to get.
+ * @param aDefault the value to return if aPropertyName is not available.
+ * @return The property value, if available, or aDefault.
+ */
+function getSysinfoProperty(aPropertyName, aDefault) {
+ try {
+ // |getProperty| may throw if |aPropertyName| does not exist.
+ return Services.sysinfo.getProperty(aPropertyName);
+ } catch (e) {}
+
+ return aDefault;
+}
+
+/**
+ * Safely get a gfxInfo field and return its value. If the field is not available, return
+ * aDefault.
+ *
+ * @param aPropertyName the property name to get.
+ * @param aDefault the value to return if aPropertyName is not available.
+ * @return The property value, if available, or aDefault.
+ */
+function getGfxField(aPropertyName, aDefault) {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ try {
+ // Accessing the field may throw if |aPropertyName| does not exist.
+ let gfxProp = gfxInfo[aPropertyName];
+ if (gfxProp !== undefined && gfxProp !== "") {
+ return gfxProp;
+ }
+ } catch (e) {}
+
+ return aDefault;
+}
+
+/**
+ * Returns a substring of the input string.
+ *
+ * @param {String} aString The input string.
+ * @param {Integer} aMaxLength The maximum length of the returned substring. If this is
+ * greater than the length of the input string, we return the whole input string.
+ * @return {String} The substring or null if the input string is null.
+ */
+function limitStringToLength(aString, aMaxLength) {
+ if (typeof aString !== "string") {
+ return null;
+ }
+ return aString.substring(0, aMaxLength);
+}
+
+/**
+ * Force a value to be a string.
+ * Only if the value is null, null is returned instead.
+ */
+function forceToStringOrNull(aValue) {
+ if (aValue === null) {
+ return null;
+ }
+
+ return String(aValue);
+}
+
+/**
+ * Get the information about a graphic adapter.
+ *
+ * @param aSuffix A suffix to add to the properties names.
+ * @return An object containing the adapter properties.
+ */
+function getGfxAdapter(aSuffix = "") {
+ // Note that gfxInfo, and so getGfxField, might return "Unknown" for the RAM on failures,
+ // not null.
+ let memoryMB = parseInt(getGfxField("adapterRAM" + aSuffix, null), 10);
+ if (Number.isNaN(memoryMB)) {
+ memoryMB = null;
+ }
+
+ return {
+ description: getGfxField("adapterDescription" + aSuffix, null),
+ vendorID: getGfxField("adapterVendorID" + aSuffix, null),
+ deviceID: getGfxField("adapterDeviceID" + aSuffix, null),
+ subsysID: getGfxField("adapterSubsysID" + aSuffix, null),
+ RAM: memoryMB,
+ driver: getGfxField("adapterDriver" + aSuffix, null),
+ driverVendor: getGfxField("adapterDriverVendor" + aSuffix, null),
+ driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
+ driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
+ };
+}
+
+/**
+ * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
+ * is owned by a parent environment object and is an addon listener.
+ */
+function EnvironmentAddonBuilder(environment) {
+ this._environment = environment;
+
+ // The pending task blocks addon manager shutdown. It can either be the initial load
+ // or a change load.
+ this._pendingTask = null;
+
+ // Have we added an observer to listen for blocklist changes that still needs to be
+ // removed:
+ this._blocklistObserverAdded = false;
+
+ // Set to true once initial load is complete and we're watching for changes.
+ this._loaded = false;
+
+ // The state reported by the shutdown blocker if we hang shutdown.
+ this._shutdownState = "Initial";
+}
+EnvironmentAddonBuilder.prototype = {
+ /**
+ * Get the initial set of addons.
+ * @returns Promise<void> when the initial load is complete.
+ */
+ async init() {
+ AddonManager.beforeShutdown.addBlocker(
+ "EnvironmentAddonBuilder",
+ () => this._shutdownBlocker(),
+ { fetchState: () => this._shutdownState }
+ );
+
+ this._pendingTask = (async () => {
+ try {
+ this._shutdownState = "Awaiting _updateAddons";
+ // Gather initial addons details
+ await this._updateAddons(true);
+
+ if (!this._environment._addonsAreFull) {
+ // The addon database has not been loaded, wait for it to
+ // initialize and gather full data as soon as it does.
+ this._shutdownState = "Awaiting AddonManagerPrivate.databaseReady";
+ await AddonManagerPrivate.databaseReady;
+
+ // Now gather complete addons details.
+ this._shutdownState = "Awaiting second _updateAddons";
+ await this._updateAddons();
+ }
+ } catch (err) {
+ this._environment._log.error("init - Exception in _updateAddons", err);
+ } finally {
+ this._pendingTask = null;
+ this._shutdownState = "_pendingTask init complete. No longer blocking.";
+ }
+ })();
+
+ return this._pendingTask;
+ },
+
+ /**
+ * Register an addon listener and watch for changes.
+ */
+ watchForChanges() {
+ this._loaded = true;
+ AddonManager.addAddonListener(this);
+ },
+
+ // AddonListener
+ onEnabled(addon) {
+ this._onAddonChange(addon);
+ },
+ onDisabled(addon) {
+ this._onAddonChange(addon);
+ },
+ onInstalled(addon) {
+ this._onAddonChange(addon);
+ },
+ onUninstalling(addon) {
+ this._onAddonChange(addon);
+ },
+ onUninstalled(addon) {
+ this._onAddonChange(addon);
+ },
+
+ _onAddonChange(addon) {
+ if (addon && addon.isBuiltin && !addon.isSystem) {
+ return;
+ }
+ this._environment._log.trace("_onAddonChange");
+ this._checkForChanges("addons-changed");
+ },
+
+ // nsIObserver
+ observe(aSubject, aTopic, aData) {
+ this._environment._log.trace("observe - Topic " + aTopic);
+ if (aTopic == BLOCKLIST_LOADED_TOPIC) {
+ Services.obs.removeObserver(this, BLOCKLIST_LOADED_TOPIC);
+ this._blocklistObserverAdded = false;
+ let plugins = this._getActivePlugins();
+ let gmpPluginsPromise = this._getActiveGMPlugins();
+ gmpPluginsPromise.then(
+ gmpPlugins => {
+ let { addons } = this._environment._currentEnvironment;
+ addons.activePlugins = plugins;
+ addons.activeGMPlugins = gmpPlugins;
+ },
+ err => {
+ this._environment._log.error(
+ "blocklist observe: Error collecting plugins",
+ err
+ );
+ }
+ );
+ }
+ },
+
+ _checkForChanges(changeReason) {
+ if (this._pendingTask) {
+ this._environment._log.trace(
+ "_checkForChanges - task already pending, dropping change with reason " +
+ changeReason
+ );
+ return;
+ }
+
+ this._shutdownState = "_checkForChanges awaiting _updateAddons";
+ this._pendingTask = this._updateAddons().then(
+ result => {
+ this._pendingTask = null;
+ this._shutdownState = "No longer blocking, _updateAddons resolved";
+ if (result.changed) {
+ this._environment._onEnvironmentChange(
+ changeReason,
+ result.oldEnvironment
+ );
+ }
+ },
+ err => {
+ this._pendingTask = null;
+ this._shutdownState = "No longer blocking, _updateAddons rejected";
+ this._environment._log.error(
+ "_checkForChanges: Error collecting addons",
+ err
+ );
+ }
+ );
+ },
+
+ _shutdownBlocker() {
+ if (this._loaded) {
+ AddonManager.removeAddonListener(this);
+ if (this._blocklistObserverAdded) {
+ Services.obs.removeObserver(this, BLOCKLIST_LOADED_TOPIC);
+ }
+ }
+
+ // At startup, _pendingTask is set to a Promise that does not resolve
+ // until the addons database has been read so complete details about
+ // addons are available. Returning it here will cause it to block
+ // profileBeforeChange, guranteeing that full information will be
+ // available by the time profileBeforeChangeTelemetry is fired.
+ return this._pendingTask;
+ },
+
+ /**
+ * Collect the addon data for the environment.
+ *
+ * This should only be called from _pendingTask; otherwise we risk
+ * running this during addon manager shutdown.
+ *
+ * @param {boolean} [atStartup]
+ * True if this is the first check we're performing at startup. In that
+ * situation, we defer some more expensive initialization.
+ *
+ * @returns Promise<Object> This returns a Promise resolved with a status object with the following members:
+ * changed - Whether the environment changed.
+ * oldEnvironment - Only set if a change occured, contains the environment data before the change.
+ */
+ async _updateAddons(atStartup) {
+ this._environment._log.trace("_updateAddons");
+
+ let addons = {
+ activeAddons: await this._getActiveAddons(),
+ theme: await this._getActiveTheme(),
+ activePlugins: this._getActivePlugins(atStartup),
+ activeGMPlugins: await this._getActiveGMPlugins(atStartup),
+ };
+
+ let result = {
+ changed:
+ !this._environment._currentEnvironment.addons ||
+ !ObjectUtils.deepEqual(
+ addons.activeAddons,
+ this._environment._currentEnvironment.addons.activeAddons
+ ),
+ };
+
+ if (result.changed) {
+ this._environment._log.trace("_updateAddons: addons differ");
+ result.oldEnvironment = Cu.cloneInto(
+ this._environment._currentEnvironment,
+ myScope
+ );
+ }
+ this._environment._currentEnvironment.addons = addons;
+
+ return result;
+ },
+
+ /**
+ * Get the addon data in object form.
+ * @return Promise<object> containing the addon data.
+ */
+ async _getActiveAddons() {
+ // Request addons, asynchronously.
+ let { addons: allAddons, fullData } = await AddonManager.getActiveAddons([
+ "extension",
+ "service",
+ ]);
+
+ this._environment._addonsAreFull = fullData;
+ let activeAddons = {};
+ for (let addon of allAddons) {
+ // Don't collect any information about the new built-in search webextensions
+ if (addon.isBuiltin && !addon.isSystem) {
+ continue;
+ }
+ // Weird addon data in the wild can lead to exceptions while collecting
+ // the data.
+ try {
+ // Make sure to have valid dates.
+ let updateDate = new Date(Math.max(0, addon.updateDate));
+
+ activeAddons[addon.id] = {
+ version: limitStringToLength(addon.version, MAX_ADDON_STRING_LENGTH),
+ scope: addon.scope,
+ type: addon.type,
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ isSystem: addon.isSystem,
+ isWebExtension: addon.isWebExtension,
+ multiprocessCompatible: true,
+ };
+
+ // getActiveAddons() gives limited data during startup and full
+ // data after the addons database is loaded.
+ if (fullData) {
+ let installDate = new Date(Math.max(0, addon.installDate));
+ Object.assign(activeAddons[addon.id], {
+ blocklisted:
+ addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ description: limitStringToLength(
+ addon.description,
+ MAX_ADDON_STRING_LENGTH
+ ),
+ name: limitStringToLength(addon.name, MAX_ADDON_STRING_LENGTH),
+ userDisabled: enforceBoolean(addon.userDisabled),
+ appDisabled: addon.appDisabled,
+ foreignInstall: enforceBoolean(addon.foreignInstall),
+ hasBinaryComponents: false,
+ installDay: Utils.millisecondsToDays(installDate.getTime()),
+ signedState: addon.signedState,
+ });
+ }
+ } catch (ex) {
+ this._environment._log.error(
+ "_getActiveAddons - An addon was discarded due to an error",
+ ex
+ );
+ continue;
+ }
+ }
+
+ return activeAddons;
+ },
+
+ /**
+ * Get the currently active theme data in object form.
+ * @return Promise<object> containing the active theme data.
+ */
+ async _getActiveTheme() {
+ // Request themes, asynchronously.
+ let { addons: themes } = await AddonManager.getActiveAddons(["theme"]);
+
+ let activeTheme = {};
+ // We only store information about the active theme.
+ let theme = themes.find(theme => theme.isActive);
+ if (theme) {
+ // Make sure to have valid dates.
+ let installDate = new Date(Math.max(0, theme.installDate));
+ let updateDate = new Date(Math.max(0, theme.updateDate));
+
+ activeTheme = {
+ id: theme.id,
+ blocklisted:
+ theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ description: limitStringToLength(
+ theme.description,
+ MAX_ADDON_STRING_LENGTH
+ ),
+ name: limitStringToLength(theme.name, MAX_ADDON_STRING_LENGTH),
+ userDisabled: enforceBoolean(theme.userDisabled),
+ appDisabled: theme.appDisabled,
+ version: limitStringToLength(theme.version, MAX_ADDON_STRING_LENGTH),
+ scope: theme.scope,
+ foreignInstall: enforceBoolean(theme.foreignInstall),
+ hasBinaryComponents: false,
+ installDay: Utils.millisecondsToDays(installDate.getTime()),
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ };
+ }
+
+ return activeTheme;
+ },
+
+ /**
+ * Get the plugins data in object form.
+ *
+ * @param {boolean} [atStartup]
+ * True if this is the first check we're performing at startup. In that
+ * situation, we defer some more expensive initialization.
+ *
+ * @return Object containing the plugins data.
+ */
+ _getActivePlugins(atStartup) {
+ // If we haven't yet loaded the blocklist, pass back dummy data for now,
+ // and add an observer to update this data as soon as we get it.
+ if (atStartup || !Services.blocklist.isLoaded) {
+ if (!this._blocklistObserverAdded) {
+ Services.obs.addObserver(this, BLOCKLIST_LOADED_TOPIC);
+ this._blocklistObserverAdded = true;
+ }
+ return [
+ {
+ name: "dummy",
+ version: "0.1",
+ description: "Blocklist unavailable",
+ blocklisted: false,
+ disabled: true,
+ clicktoplay: false,
+ mimeTypes: ["text/there.is.only.blocklist"],
+ updateDay: Utils.millisecondsToDays(Date.now()),
+ },
+ ];
+ }
+ let pluginTags = Cc["@mozilla.org/plugin/host;1"]
+ .getService(Ci.nsIPluginHost)
+ .getPluginTags();
+
+ let activePlugins = [];
+ for (let tag of pluginTags) {
+ // Skip plugins which are not active.
+ if (tag.disabled) {
+ continue;
+ }
+
+ try {
+ // Make sure to have a valid date.
+ let updateDate = new Date(Math.max(0, tag.lastModifiedTime));
+
+ activePlugins.push({
+ name: limitStringToLength(tag.name, MAX_ADDON_STRING_LENGTH),
+ version: limitStringToLength(tag.version, MAX_ADDON_STRING_LENGTH),
+ description: limitStringToLength(
+ tag.description,
+ MAX_ADDON_STRING_LENGTH
+ ),
+ blocklisted: tag.blocklisted,
+ disabled: tag.disabled,
+ clicktoplay: tag.clicktoplay,
+ mimeTypes: tag.getMimeTypes(),
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ });
+ } catch (ex) {
+ this._environment._log.error(
+ "_getActivePlugins - A plugin was discarded due to an error",
+ ex
+ );
+ continue;
+ }
+ }
+
+ return activePlugins;
+ },
+
+ /**
+ * Get the GMPlugins data in object form.
+ *
+ * @param {boolean} [atStartup]
+ * True if this is the first check we're performing at startup. In that
+ * situation, we defer some more expensive initialization.
+ *
+ * @return Object containing the GMPlugins data.
+ *
+ * This should only be called from _pendingTask; otherwise we risk
+ * running this during addon manager shutdown.
+ */
+ async _getActiveGMPlugins(atStartup) {
+ // If we haven't yet loaded the blocklist, pass back dummy data for now,
+ // and add an observer to update this data as soon as we get it.
+ if (atStartup || !Services.blocklist.isLoaded) {
+ if (!this._blocklistObserverAdded) {
+ Services.obs.addObserver(this, BLOCKLIST_LOADED_TOPIC);
+ this._blocklistObserverAdded = true;
+ }
+ return {
+ "dummy-gmp": {
+ version: "0.1",
+ userDisabled: false,
+ applyBackgroundUpdates: 1,
+ },
+ };
+ }
+ // Request plugins, asynchronously.
+ let allPlugins = await AddonManager.getAddonsByTypes(["plugin"]);
+
+ let activeGMPlugins = {};
+ for (let plugin of allPlugins) {
+ // Only get info for active GMplugins.
+ if (!plugin.isGMPlugin || !plugin.isActive) {
+ continue;
+ }
+
+ try {
+ activeGMPlugins[plugin.id] = {
+ version: plugin.version,
+ userDisabled: enforceBoolean(plugin.userDisabled),
+ applyBackgroundUpdates: plugin.applyBackgroundUpdates,
+ };
+ } catch (ex) {
+ this._environment._log.error(
+ "_getActiveGMPlugins - A GMPlugin was discarded due to an error",
+ ex
+ );
+ continue;
+ }
+ }
+
+ return activeGMPlugins;
+ },
+};
+
+function EnvironmentCache() {
+ this._log = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ "TelemetryEnvironment::"
+ );
+ this._log.trace("constructor");
+
+ this._shutdown = false;
+ // Don't allow querying the search service too early to prevent
+ // impacting the startup performance.
+ this._canQuerySearch = false;
+ // To guard against slowing down startup, defer gathering heavy environment
+ // entries until the session is restored.
+ this._sessionWasRestored = false;
+
+ // A map of listeners that will be called on environment changes.
+ this._changeListeners = new Map();
+
+ // A map of watched preferences which trigger an Environment change when
+ // modified. Every entry contains a recording policy (RECORD_PREF_*).
+ this._watchedPrefs = DEFAULT_ENVIRONMENT_PREFS;
+
+ this._currentEnvironment = {
+ build: this._getBuild(),
+ partner: this._getPartner(),
+ system: this._getSystem(),
+ };
+
+ this._addObservers();
+
+ // Build the remaining asynchronous parts of the environment. Don't register change listeners
+ // until the initial environment has been built.
+
+ let p = [this._updateSettings()];
+ this._addonBuilder = new EnvironmentAddonBuilder(this);
+ p.push(this._addonBuilder.init());
+
+ this._currentEnvironment.profile = {};
+ p.push(this._updateProfile());
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ p.push(this._loadAttributionAsync());
+ }
+ p.push(this._loadAutoUpdateAsync());
+ p.push(this._loadIntlData());
+
+ for (const [
+ id,
+ { branch, options },
+ ] of gActiveExperimentStartupBuffer.entries()) {
+ this.setExperimentActive(id, branch, options);
+ }
+ gActiveExperimentStartupBuffer = null;
+
+ let setup = () => {
+ this._initTask = null;
+ this._startWatchingPrefs();
+ this._addonBuilder.watchForChanges();
+ this._updateGraphicsFeatures();
+ return this.currentEnvironment;
+ };
+
+ this._initTask = Promise.all(p).then(
+ () => setup(),
+ err => {
+ // log errors but eat them for consumers
+ this._log.error("EnvironmentCache - error while initializing", err);
+ return setup();
+ }
+ );
+
+ // Addons may contain partial or full data depending on whether the Addons DB
+ // has had a chance to load. Do we have full data yet?
+ this._addonsAreFull = false;
+}
+EnvironmentCache.prototype = {
+ /**
+ * The current environment data. The returned data is cloned to avoid
+ * unexpected sharing or mutation.
+ * @returns object
+ */
+ get currentEnvironment() {
+ return Cu.cloneInto(this._currentEnvironment, myScope);
+ },
+
+ /**
+ * Wait for the current enviroment to be fully initialized.
+ * @returns Promise<object>
+ */
+ onInitialized() {
+ if (this._initTask) {
+ return this._initTask;
+ }
+ return Promise.resolve(this.currentEnvironment);
+ },
+
+ /**
+ * This gets called when the delayed init completes.
+ */
+ async delayedInit() {
+ this._processData = await Services.sysinfo.processInfo;
+ let processData = await Services.sysinfo.processInfo;
+ // Remove isWow64 and isWowARM64 from processData
+ // to strip it down to just CPU info
+ delete processData.isWow64;
+ delete processData.isWowARM64;
+
+ let oldEnv = null;
+ if (!this._initTask) {
+ oldEnv = this.currentEnvironment;
+ }
+
+ this._cpuData = this._getCPUData();
+ // Augment the return value from the promises with cached values
+ this._cpuData = { ...processData, ...this._cpuData };
+
+ this._currentEnvironment.system.cpu = this._getCPUData();
+
+ if (AppConstants.platform == "win") {
+ this._hddData = await Services.sysinfo.diskInfo;
+ if (AppConstants.MOZ_GLEAN) {
+ Glean.fogValidation.profileDiskIsSsd.set(
+ this._hddData.profile.type == "SSD"
+ );
+ }
+ let osData = await Services.sysinfo.osInfo;
+
+ if (!this._initTask) {
+ // We've finished creating the initial env, so notify for the update
+ // This is all a bit awkward because `currentEnvironment` clones
+ // the object, which we need to pass to the notification, but we
+ // should only notify once we've updated the current environment...
+ // Ideally, _onEnvironmentChange should somehow deal with all this
+ // instead of all the consumers.
+ oldEnv = this.currentEnvironment;
+ }
+
+ this._osData = this._getOSData();
+
+ // Augment the return values from the promises with cached values
+ this._osData = Object.assign(osData, this._osData);
+
+ this._currentEnvironment.system.os = this._getOSData();
+ this._currentEnvironment.system.hdd = this._getHDDData();
+
+ // Windows only values stored in processData
+ this._currentEnvironment.system.isWow64 = this._getProcessData().isWow64;
+ this._currentEnvironment.system.isWowARM64 = this._getProcessData().isWowARM64;
+ }
+
+ if (!this._initTask) {
+ this._onEnvironmentChange("system-info", oldEnv);
+ }
+ },
+
+ /**
+ * Register a listener for environment changes.
+ * @param name The name of the listener. If a new listener is registered
+ * with the same name, the old listener will be replaced.
+ * @param listener function(reason, oldEnvironment) - Will receive a reason for
+ the change and the environment data before the change.
+ */
+ registerChangeListener(name, listener) {
+ this._log.trace("registerChangeListener for " + name);
+ if (this._shutdown) {
+ this._log.warn("registerChangeListener - already shutdown");
+ return;
+ }
+ this._changeListeners.set(name, listener);
+ },
+
+ /**
+ * Unregister from listening to environment changes.
+ * It's fine to call this on an unitialized TelemetryEnvironment.
+ * @param name The name of the listener to remove.
+ */
+ unregisterChangeListener(name) {
+ this._log.trace("unregisterChangeListener for " + name);
+ if (this._shutdown) {
+ this._log.warn("registerChangeListener - already shutdown");
+ return;
+ }
+ this._changeListeners.delete(name);
+ },
+
+ setExperimentActive(id, branch, options) {
+ this._log.trace(`setExperimentActive - id: ${id}, branch: ${branch}`);
+ // Make sure both the id and the branch have sane lengths.
+ const saneId = limitStringToLength(id, MAX_EXPERIMENT_ID_LENGTH);
+ const saneBranch = limitStringToLength(
+ branch,
+ MAX_EXPERIMENT_BRANCH_LENGTH
+ );
+ if (!saneId || !saneBranch) {
+ this._log.error(
+ "setExperimentActive - the provided arguments are not strings."
+ );
+ return;
+ }
+
+ // Warn the user about any content truncation.
+ if (saneId.length != id.length || saneBranch.length != branch.length) {
+ this._log.warn(
+ "setExperimentActive - the experiment id or branch were truncated."
+ );
+ }
+
+ // Truncate the experiment type if present.
+ if (options.hasOwnProperty("type")) {
+ let type = limitStringToLength(options.type, MAX_EXPERIMENT_TYPE_LENGTH);
+ if (type.length != options.type.length) {
+ options.type = type;
+ this._log.warn(
+ "setExperimentActive - the experiment type was truncated."
+ );
+ }
+ }
+
+ // Truncate the enrollment id if present.
+ if (options.hasOwnProperty("enrollmentId")) {
+ let enrollmentId = limitStringToLength(
+ options.enrollmentId,
+ MAX_EXPERIMENT_ENROLLMENT_ID_LENGTH
+ );
+ if (enrollmentId.length != options.enrollmentId.length) {
+ options.enrollmentId = enrollmentId;
+ this._log.warn(
+ "setExperimentActive - the enrollment id was truncated."
+ );
+ }
+ }
+
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ // Add the experiment annotation.
+ let experiments = this._currentEnvironment.experiments || {};
+ experiments[saneId] = { branch: saneBranch };
+ if (options.hasOwnProperty("type")) {
+ experiments[saneId].type = options.type;
+ }
+ if (options.hasOwnProperty("enrollmentId")) {
+ experiments[saneId].enrollmentId = options.enrollmentId;
+ }
+ this._currentEnvironment.experiments = experiments;
+ // Notify of the change.
+ this._onEnvironmentChange("experiment-annotation-changed", oldEnvironment);
+ },
+
+ setExperimentInactive(id) {
+ this._log.trace("setExperimentInactive");
+ let experiments = this._currentEnvironment.experiments || {};
+ if (id in experiments) {
+ // Only attempt to notify if a previous annotation was found and removed.
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ // Remove the experiment annotation.
+ delete this._currentEnvironment.experiments[id];
+ // Notify of the change.
+ this._onEnvironmentChange(
+ "experiment-annotation-changed",
+ oldEnvironment
+ );
+ }
+ },
+
+ getActiveExperiments() {
+ return Cu.cloneInto(this._currentEnvironment.experiments || {}, myScope);
+ },
+
+ shutdown() {
+ this._log.trace("shutdown");
+ this._shutdown = true;
+ },
+
+ /**
+ * Only used in tests, set the preferences to watch.
+ * @param aPreferences A map of preferences names and their recording policy.
+ */
+ async _watchPreferences(aPreferences) {
+ this._stopWatchingPrefs();
+ this._watchedPrefs = aPreferences;
+ await this._updateSettings();
+ this._startWatchingPrefs();
+ },
+
+ /**
+ * Get an object containing the values for the watched preferences. Depending on the
+ * policy, the value for a preference or whether it was changed by user is reported.
+ *
+ * @return An object containing the preferences values.
+ */
+ _getPrefData() {
+ let prefData = {};
+ for (let [pref, policy] of this._watchedPrefs.entries()) {
+ let prefValue = this._getPrefValue(pref, policy.what);
+
+ if (prefValue === undefined) {
+ continue;
+ }
+
+ prefData[pref] = prefValue;
+ }
+ return prefData;
+ },
+
+ /**
+ * Get the value of a preference given the preference name and the policy.
+ * @param pref Name of the preference.
+ * @param what Policy of the preference.
+ *
+ * @returns The value we need to store for this preference. It can be undefined
+ * or null if the preference is invalid or has a value set by the user.
+ */
+ _getPrefValue(pref, what) {
+ // Check the policy for the preference and decide if we need to store its value
+ // or whether it changed from the default value.
+ let prefType = Services.prefs.getPrefType(pref);
+
+ if (
+ what == TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE ||
+ what == TelemetryEnvironment.RECORD_DEFAULTPREF_STATE
+ ) {
+ // For default prefs, make sure they exist
+ if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
+ return undefined;
+ }
+ } else if (!Services.prefs.prefHasUserValue(pref)) {
+ // For user prefs, make sure they are set
+ return undefined;
+ }
+
+ if (what == TelemetryEnvironment.RECORD_DEFAULTPREF_STATE) {
+ return "<set>";
+ } else if (what == TelemetryEnvironment.RECORD_PREF_STATE) {
+ return "<user-set>";
+ } else if (prefType == Ci.nsIPrefBranch.PREF_STRING) {
+ return Services.prefs.getStringPref(pref);
+ } else if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
+ return Services.prefs.getBoolPref(pref);
+ } else if (prefType == Ci.nsIPrefBranch.PREF_INT) {
+ return Services.prefs.getIntPref(pref);
+ } else if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
+ return null;
+ }
+ throw new Error(
+ `Unexpected preference type ("${prefType}") for "${pref}".`
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+
+ /**
+ * Start watching the preferences.
+ */
+ _startWatchingPrefs() {
+ this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
+
+ Services.prefs.addObserver("", this, true);
+ },
+
+ _onPrefChanged(aData) {
+ this._log.trace("_onPrefChanged");
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ this._currentEnvironment.settings.userPrefs[aData] = this._getPrefValue(
+ aData,
+ this._watchedPrefs.get(aData).what
+ );
+ this._onEnvironmentChange("pref-changed", oldEnvironment);
+ },
+
+ /**
+ * Do not receive any more change notifications for the preferences.
+ */
+ _stopWatchingPrefs() {
+ this._log.trace("_stopWatchingPrefs");
+
+ Services.prefs.removeObserver("", this);
+ },
+
+ _addObservers() {
+ // Watch the search engine change and service topics.
+ Services.obs.addObserver(this, SESSIONSTORE_WINDOWS_RESTORED_TOPIC);
+ Services.obs.addObserver(this, COMPOSITOR_CREATED_TOPIC);
+ Services.obs.addObserver(this, COMPOSITOR_PROCESS_ABORTED_TOPIC);
+ Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
+ Services.obs.addObserver(this, GFX_FEATURES_READY_TOPIC);
+ Services.obs.addObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
+ Services.obs.addObserver(this, SEARCH_SERVICE_TOPIC);
+ Services.obs.addObserver(this, AUTO_UPDATE_PREF_CHANGE_TOPIC);
+ Services.obs.addObserver(this, SERVICES_INFO_CHANGE_TOPIC);
+ },
+
+ _removeObservers() {
+ Services.obs.removeObserver(this, SESSIONSTORE_WINDOWS_RESTORED_TOPIC);
+ Services.obs.removeObserver(this, COMPOSITOR_CREATED_TOPIC);
+ Services.obs.removeObserver(this, COMPOSITOR_PROCESS_ABORTED_TOPIC);
+ try {
+ Services.obs.removeObserver(
+ this,
+ DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
+ );
+ } catch (ex) {}
+ Services.obs.removeObserver(this, GFX_FEATURES_READY_TOPIC);
+ Services.obs.removeObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
+ Services.obs.removeObserver(this, SEARCH_SERVICE_TOPIC);
+ Services.obs.removeObserver(this, AUTO_UPDATE_PREF_CHANGE_TOPIC);
+ Services.obs.removeObserver(this, SERVICES_INFO_CHANGE_TOPIC);
+ },
+
+ observe(aSubject, aTopic, aData) {
+ this._log.trace("observe - aTopic: " + aTopic + ", aData: " + aData);
+ switch (aTopic) {
+ case SEARCH_ENGINE_MODIFIED_TOPIC:
+ if (
+ aData != "engine-default" &&
+ aData != "engine-default-private" &&
+ aData != "engine-changed"
+ ) {
+ return;
+ }
+ if (
+ aData == "engine-changed" &&
+ aSubject.QueryInterface(Ci.nsISearchEngine) &&
+ Services.search.defaultEngine != aSubject
+ ) {
+ return;
+ }
+ // Record the new default search choice and send the change notification.
+ this._onSearchEngineChange();
+ break;
+ case SEARCH_SERVICE_TOPIC:
+ if (aData != "init-complete") {
+ return;
+ }
+ // Now that the search engine init is complete, record the default search choice.
+ this._canQuerySearch = true;
+ this._updateSearchEngine();
+ break;
+ case GFX_FEATURES_READY_TOPIC:
+ case COMPOSITOR_CREATED_TOPIC:
+ // Full graphics information is not available until we have created at
+ // least one off-main-thread-composited window. Thus we wait for the
+ // first compositor to be created and then query nsIGfxInfo again.
+ this._updateGraphicsFeatures();
+ break;
+ case COMPOSITOR_PROCESS_ABORTED_TOPIC:
+ // Our compositor process has been killed for whatever reason, so refresh
+ // our reported graphics features and trigger an environment change.
+ this._onCompositorProcessAborted();
+ break;
+ case DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC:
+ // Distribution customizations are applied after final-ui-startup. query
+ // partner prefs again when they are ready.
+ this._updatePartner();
+ Services.obs.removeObserver(this, aTopic);
+ break;
+ case SESSIONSTORE_WINDOWS_RESTORED_TOPIC:
+ this._sessionWasRestored = true;
+ // Make sure to initialize the search service once we've done restoring
+ // the windows, so that we don't risk loosing search data.
+ Services.search.init();
+ // The default browser check could take some time, so just call it after
+ // the session was restored.
+ this._updateDefaultBrowser();
+ break;
+ case PREF_CHANGED_TOPIC:
+ let options = this._watchedPrefs.get(aData);
+ if (options && !options.requiresRestart) {
+ this._onPrefChanged(aData);
+ }
+ break;
+ case AUTO_UPDATE_PREF_CHANGE_TOPIC:
+ this._currentEnvironment.settings.update.autoDownload = aData == "true";
+ break;
+ case SERVICES_INFO_CHANGE_TOPIC:
+ this._updateServicesInfo();
+ break;
+ }
+ },
+
+ /**
+ * Update the default search engine value.
+ */
+ async _updateSearchEngine() {
+ if (!this._canQuerySearch) {
+ this._log.trace("_updateSearchEngine - ignoring early call");
+ return;
+ }
+
+ this._log.trace(
+ "_updateSearchEngine - isInitialized: " + Services.search.isInitialized
+ );
+ if (!Services.search.isInitialized) {
+ return;
+ }
+
+ // Make sure we have a settings section.
+ this._currentEnvironment.settings = this._currentEnvironment.settings || {};
+
+ // Update the search engine entry in the current environment.
+ const defaultEngineInfo = await Services.search.getDefaultEngineInfo();
+ this._currentEnvironment.settings.defaultSearchEngine =
+ defaultEngineInfo.defaultSearchEngine;
+ this._currentEnvironment.settings.defaultSearchEngineData = {
+ ...defaultEngineInfo.defaultSearchEngineData,
+ };
+ if ("defaultPrivateSearchEngine" in defaultEngineInfo) {
+ this._currentEnvironment.settings.defaultPrivateSearchEngine =
+ defaultEngineInfo.defaultPrivateSearchEngine;
+ }
+ if ("defaultPrivateSearchEngineData" in defaultEngineInfo) {
+ this._currentEnvironment.settings.defaultPrivateSearchEngineData = {
+ ...defaultEngineInfo.defaultPrivateSearchEngineData,
+ };
+ }
+ },
+
+ /**
+ * Update the default search engine value and trigger the environment change.
+ */
+ async _onSearchEngineChange() {
+ this._log.trace("_onSearchEngineChange");
+
+ // Finally trigger the environment change notification.
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ await this._updateSearchEngine();
+ this._onEnvironmentChange("search-engine-changed", oldEnvironment);
+ },
+
+ /**
+ * Refresh the Telemetry environment and trigger an environment change due to
+ * a change in compositor process (normally this will mean we've fallen back
+ * from out-of-process to in-process compositing).
+ */
+ _onCompositorProcessAborted() {
+ this._log.trace("_onCompositorProcessAborted");
+
+ // Trigger the environment change notification.
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ this._updateGraphicsFeatures();
+ this._onEnvironmentChange("gfx-features-changed", oldEnvironment);
+ },
+
+ /**
+ * Update the graphics features object.
+ */
+ _updateGraphicsFeatures() {
+ let gfxData = this._currentEnvironment.system.gfx;
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ gfxData.features = gfxInfo.getFeatures();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getFeatures() caught error", e);
+ }
+ },
+
+ /**
+ * Update the partner prefs.
+ */
+ _updatePartner() {
+ this._currentEnvironment.partner = this._getPartner();
+ },
+
+ /**
+ * Get the build data in object form.
+ * @return Object containing the build data.
+ */
+ _getBuild() {
+ let buildData = {
+ applicationId: Services.appinfo.ID || null,
+ applicationName: Services.appinfo.name || null,
+ architecture: Services.sysinfo.get("arch"),
+ buildId: Services.appinfo.appBuildID || null,
+ version: Services.appinfo.version || null,
+ vendor: Services.appinfo.vendor || null,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY || null,
+ platformVersion: Services.appinfo.platformVersion || null,
+ xpcomAbi: Services.appinfo.XPCOMABI,
+ updaterAvailable: AppConstants.MOZ_UPDATER,
+ };
+
+ return buildData;
+ },
+
+ /**
+ * Determine if we're the default browser.
+ * @returns null on error, true if we are the default browser, or false otherwise.
+ */
+ _isDefaultBrowser() {
+ let isDefault = (service, ...args) => {
+ try {
+ return !!service.isDefaultBrowser(...args);
+ } catch (ex) {
+ this._log.error(
+ "_isDefaultBrowser - Could not determine if default browser",
+ ex
+ );
+ return null;
+ }
+ };
+
+ if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
+ this._log.info(
+ "_isDefaultBrowser - Could not obtain browser shell service"
+ );
+ return null;
+ }
+
+ try {
+ let { ShellService } = ChromeUtils.import(
+ "resource:///modules/ShellService.jsm"
+ );
+ // This uses the same set of flags used by the pref pane.
+ return isDefault(ShellService, false, true);
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not obtain shell service JSM");
+ }
+
+ try {
+ let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIShellService
+ );
+ // This uses the same set of flags used by the pref pane.
+ return isDefault(shellService, true);
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not obtain shell service", ex);
+ return null;
+ }
+ },
+
+ _updateDefaultBrowser() {
+ if (AppConstants.platform === "android") {
+ return;
+ }
+ // Make sure to have a settings section.
+ this._currentEnvironment.settings = this._currentEnvironment.settings || {};
+ this._currentEnvironment.settings.isDefaultBrowser = this
+ ._sessionWasRestored
+ ? this._isDefaultBrowser()
+ : null;
+ },
+
+ /**
+ * Update the cached settings data.
+ */
+ async _updateSettings() {
+ let updateChannel = null;
+ try {
+ updateChannel = Utils.getUpdateChannel();
+ } catch (e) {}
+
+ this._currentEnvironment.settings = {
+ blocklistEnabled: Services.prefs.getBoolPref(
+ PREF_BLOCKLIST_ENABLED,
+ true
+ ),
+ e10sEnabled: Services.appinfo.browserTabsRemoteAutostart,
+ e10sMultiProcesses: Services.appinfo.maxWebProcessCount,
+ fissionEnabled: Services.appinfo.fissionAutostart,
+ telemetryEnabled: Utils.isTelemetryEnabled,
+ locale: getBrowserLocale(),
+ // We need to wait for browser-delayed-startup-finished to ensure that the locales
+ // have settled, once that's happened we can get the intl data directly.
+ intl: Policy._intlLoaded ? getIntlSettings() : {},
+ update: {
+ channel: updateChannel,
+ enabled: !Services.policies || Services.policies.isAllowed("appUpdate"),
+ },
+ userPrefs: this._getPrefData(),
+ sandbox: this._getSandboxData(),
+ };
+
+ // Services.appinfo.launcherProcessState is not available in all build
+ // configurations, in which case an exception may be thrown.
+ try {
+ this._currentEnvironment.settings.launcherProcessState =
+ Services.appinfo.launcherProcessState;
+ } catch (e) {}
+
+ this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
+ AddonManager.checkCompatibility;
+
+ this._updateAttribution();
+ this._updateDefaultBrowser();
+ await this._updateSearchEngine();
+ this._updateAutoDownload();
+ },
+
+ _getSandboxData() {
+ let effectiveContentProcessLevel = null;
+ try {
+ let sandboxSettings = Cc[
+ "@mozilla.org/sandbox/sandbox-settings;1"
+ ].getService(Ci.mozISandboxSettings);
+ effectiveContentProcessLevel =
+ sandboxSettings.effectiveContentSandboxLevel;
+ } catch (e) {}
+ return {
+ effectiveContentProcessLevel,
+ };
+ },
+
+ /**
+ * Update the cached profile data.
+ * @returns Promise<> resolved when the I/O is complete.
+ */
+ async _updateProfile() {
+ let profileAccessor = await ProfileAge();
+
+ let creationDate = await profileAccessor.created;
+ let resetDate = await profileAccessor.reset;
+ let firstUseDate = await profileAccessor.firstUse;
+
+ this._currentEnvironment.profile.creationDate = Utils.millisecondsToDays(
+ creationDate
+ );
+ if (resetDate) {
+ this._currentEnvironment.profile.resetDate = Utils.millisecondsToDays(
+ resetDate
+ );
+ }
+ if (firstUseDate) {
+ this._currentEnvironment.profile.firstUseDate = Utils.millisecondsToDays(
+ firstUseDate
+ );
+ }
+ },
+
+ /**
+ * Load the attribution data object and updates the environment.
+ * @returns Promise<> resolved when the I/O is complete.
+ */
+ async _loadAttributionAsync() {
+ try {
+ await AttributionCode.getAttrDataAsync();
+ } catch (e) {
+ // The AttributionCode.jsm module might not be always available
+ // (e.g. tests). Gracefully handle this.
+ return;
+ }
+ this._updateAttribution();
+ },
+
+ /**
+ * Update the environment with the cached attribution data.
+ */
+ _updateAttribution() {
+ let data = null;
+ try {
+ data = AttributionCode.getCachedAttributionData();
+ } catch (e) {
+ // The AttributionCode.jsm module might not be always available
+ // (e.g. tests). Gracefully handle this.
+ }
+
+ if (!data || !Object.keys(data).length) {
+ return;
+ }
+
+ let attributionData = {};
+ for (let key in data) {
+ attributionData[key] = limitStringToLength(
+ data[key],
+ MAX_ATTRIBUTION_STRING_LENGTH
+ );
+ }
+ this._currentEnvironment.settings.attribution = attributionData;
+ },
+
+ /**
+ * Load the auto update pref and adds it to the environment
+ */
+ async _loadAutoUpdateAsync() {
+ if (AppConstants.MOZ_UPDATER) {
+ this._updateAutoDownloadCache = await UpdateUtils.getAppUpdateAutoEnabled();
+ } else {
+ this._updateAutoDownloadCache = false;
+ }
+ this._updateAutoDownload();
+ },
+
+ /**
+ * Update the environment with the cached value for whether updates can auto-
+ * download.
+ */
+ _updateAutoDownload() {
+ if (this._updateAutoDownloadCache === undefined) {
+ return;
+ }
+ this._currentEnvironment.settings.update.autoDownload = this._updateAutoDownloadCache;
+ },
+
+ /**
+ * Get i18n data about the system.
+ * @return A promise of completion.
+ */
+ async _loadIntlData() {
+ // Wait for the startup topic.
+ await Policy._browserDelayedStartup();
+ this._currentEnvironment.settings.intl = getIntlSettings();
+ Policy._intlLoaded = true;
+ },
+ // This exists as a separate function for testing.
+ async _getFxaSignedInUser() {
+ return fxAccounts.getSignedInUser();
+ },
+
+ async _updateServicesInfo() {
+ let syncEnabled = false;
+ let accountEnabled = false;
+ let weaveService = Cc["@mozilla.org/weave/service;1"].getService()
+ .wrappedJSObject;
+ syncEnabled = weaveService && weaveService.enabled;
+ if (syncEnabled) {
+ // All sync users are account users, definitely.
+ accountEnabled = true;
+ } else {
+ // Not all account users are sync users. See if they're signed into FxA.
+ try {
+ let user = await this._getFxaSignedInUser();
+ if (user) {
+ accountEnabled = true;
+ }
+ } catch (e) {
+ // We don't know. This might be a transient issue which will clear
+ // itself up later, but the information in telemetry is quite possibly stale
+ // (this is called from a change listener), so clear it out to avoid
+ // reporting data which might be wrong until we can figure it out.
+ delete this._currentEnvironment.services;
+ this._log.error("_updateServicesInfo() caught error", e);
+ return;
+ }
+ }
+ this._currentEnvironment.services = {
+ accountEnabled,
+ syncEnabled,
+ };
+ },
+
+ /**
+ * Get the partner data in object form.
+ * @return Object containing the partner data.
+ */
+ _getPartner() {
+ let partnerData = {
+ distributionId: Services.prefs.getStringPref(PREF_DISTRIBUTION_ID, null),
+ distributionVersion: Services.prefs.getStringPref(
+ PREF_DISTRIBUTION_VERSION,
+ null
+ ),
+ partnerId: Services.prefs.getStringPref(PREF_PARTNER_ID, null),
+ distributor: Services.prefs.getStringPref(PREF_DISTRIBUTOR, null),
+ distributorChannel: Services.prefs.getStringPref(
+ PREF_DISTRIBUTOR_CHANNEL,
+ null
+ ),
+ };
+
+ // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data.
+ let partnerBranch = Services.prefs.getBranch(PREF_APP_PARTNER_BRANCH);
+ partnerData.partnerNames = partnerBranch.getChildList("");
+
+ return partnerData;
+ },
+
+ _cpuData: null,
+ /**
+ * Get the CPU information.
+ * @return Object containing the CPU information data.
+ */
+ _getCPUData() {
+ if (this._cpuData) {
+ return this._cpuData;
+ }
+
+ this._cpuData = {};
+
+ const CPU_EXTENSIONS = [
+ "hasMMX",
+ "hasSSE",
+ "hasSSE2",
+ "hasSSE3",
+ "hasSSSE3",
+ "hasSSE4A",
+ "hasSSE4_1",
+ "hasSSE4_2",
+ "hasAVX",
+ "hasAVX2",
+ "hasAES",
+ "hasEDSP",
+ "hasARMv6",
+ "hasARMv7",
+ "hasNEON",
+ "hasUserCET",
+ ];
+
+ // Enumerate the available CPU extensions.
+ let availableExts = [];
+ for (let ext of CPU_EXTENSIONS) {
+ if (getSysinfoProperty(ext, false)) {
+ availableExts.push(ext);
+ }
+ }
+
+ this._cpuData.extensions = availableExts;
+
+ return this._cpuData;
+ },
+
+ _processData: null,
+ /**
+ * Get the process information.
+ * @return Object containing the process information data.
+ */
+ _getProcessData() {
+ if (this._processData) {
+ return this._processData;
+ }
+ return {};
+ },
+
+ /**
+ * Get the device information, if we are on a portable device.
+ * @return Object containing the device information data, or null if
+ * not a portable device.
+ */
+ _getDeviceData() {
+ if (AppConstants.platform !== "android") {
+ return null;
+ }
+
+ return {
+ model: getSysinfoProperty("device", null),
+ manufacturer: getSysinfoProperty("manufacturer", null),
+ hardware: getSysinfoProperty("hardware", null),
+ isTablet: getSysinfoProperty("tablet", null),
+ };
+ },
+
+ _osData: null,
+ /**
+ * Get the OS information.
+ * @return Object containing the OS data.
+ */
+ _getOSData() {
+ if (this._osData) {
+ return this._osData;
+ }
+ this._osData = {
+ name: forceToStringOrNull(getSysinfoProperty("name", null)),
+ version: forceToStringOrNull(getSysinfoProperty("version", null)),
+ locale: forceToStringOrNull(getSystemLocale()),
+ };
+
+ if (AppConstants.platform == "android") {
+ this._osData.kernelVersion = forceToStringOrNull(
+ getSysinfoProperty("kernel_version", null)
+ );
+ } else if (AppConstants.platform === "win") {
+ // The path to the "UBR" key, queried to get additional version details on Windows.
+ const WINDOWS_UBR_KEY_PATH =
+ "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
+
+ let versionInfo = WindowsVersionInfo.get({ throwOnError: false });
+ this._osData.servicePackMajor = versionInfo.servicePackMajor;
+ this._osData.servicePackMinor = versionInfo.servicePackMinor;
+ this._osData.windowsBuildNumber = versionInfo.buildNumber;
+ // We only need the UBR if we're at or above Windows 10.
+ if (
+ typeof this._osData.version === "string" &&
+ Services.vc.compare(this._osData.version, "10") >= 0
+ ) {
+ // Query the UBR key and only add it to the environment if it's available.
+ // |readRegKey| doesn't throw, but rather returns 'undefined' on error.
+ let ubr = WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ WINDOWS_UBR_KEY_PATH,
+ "UBR",
+ Ci.nsIWindowsRegKey.WOW64_64
+ );
+ this._osData.windowsUBR = ubr !== undefined ? ubr : null;
+ }
+ }
+
+ return this._osData;
+ },
+
+ _hddData: null,
+ /**
+ * Get the HDD information.
+ * @return Object containing the HDD data.
+ */
+ _getHDDData() {
+ if (this._hddData) {
+ return this._hddData;
+ }
+ let nullData = { model: null, revision: null, type: null };
+ return { profile: nullData, binary: nullData, system: nullData };
+ },
+
+ /**
+ * Get registered security product information.
+ * @return Object containing the security product data
+ */
+ _getSecurityAppData() {
+ const maxStringLength = 256;
+
+ const keys = [
+ ["registeredAntiVirus", "antivirus"],
+ ["registeredAntiSpyware", "antispyware"],
+ ["registeredFirewall", "firewall"],
+ ];
+
+ let result = {};
+
+ for (let [inKey, outKey] of keys) {
+ let prop = getSysinfoProperty(inKey, null);
+ if (prop) {
+ prop = limitStringToLength(prop, maxStringLength).split(";");
+ }
+
+ result[outKey] = prop;
+ }
+
+ return result;
+ },
+
+ /**
+ * Get the GFX information.
+ * @return Object containing the GFX data.
+ */
+ _getGFXData() {
+ let gfxData = {
+ D2DEnabled: getGfxField("D2DEnabled", null),
+ DWriteEnabled: getGfxField("DWriteEnabled", null),
+ ContentBackend: getGfxField("ContentBackend", null),
+ Headless: getGfxField("isHeadless", null),
+ EmbeddedInFirefoxReality: getGfxField("EmbeddedInFirefoxReality", null),
+ // The following line is disabled due to main thread jank and will be enabled
+ // again as part of bug 1154500.
+ // DWriteVersion: getGfxField("DWriteVersion", null),
+ adapters: [],
+ monitors: [],
+ features: {},
+ };
+
+ if (AppConstants.platform !== "android") {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ try {
+ gfxData.monitors = gfxInfo.getMonitors();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getMonitors() caught error", e);
+ }
+ }
+
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ gfxData.features = gfxInfo.getFeatures();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getFeatures() caught error", e);
+ }
+
+ // GfxInfo does not yet expose a way to iterate through all the adapters.
+ gfxData.adapters.push(getGfxAdapter(""));
+ gfxData.adapters[0].GPUActive = true;
+
+ // If we have a second adapter add it to the gfxData.adapters section.
+ let hasGPU2 = getGfxField("adapterDeviceID2", null) !== null;
+ if (!hasGPU2) {
+ this._log.trace("_getGFXData - Only one display adapter detected.");
+ return gfxData;
+ }
+
+ this._log.trace("_getGFXData - Two display adapters detected.");
+
+ gfxData.adapters.push(getGfxAdapter("2"));
+ gfxData.adapters[1].GPUActive = getGfxField("isGPU2Active", null);
+
+ return gfxData;
+ },
+
+ /**
+ * Get the system data in object form.
+ * @return Object containing the system data.
+ */
+ _getSystem() {
+ let memoryMB = getSysinfoProperty("memsize", null);
+ if (memoryMB) {
+ // Send RAM size in megabytes. Rounding because sysinfo doesn't
+ // always provide RAM in multiples of 1024.
+ memoryMB = Math.round(memoryMB / 1024 / 1024);
+ }
+
+ let virtualMB = getSysinfoProperty("virtualmemsize", null);
+ if (virtualMB) {
+ // Send the total virtual memory size in megabytes. Rounding because
+ // sysinfo doesn't always provide RAM in multiples of 1024.
+ virtualMB = Math.round(virtualMB / 1024 / 1024);
+ }
+
+ let data = {
+ memoryMB,
+ virtualMaxMB: virtualMB,
+ cpu: this._getCPUData(),
+ os: this._getOSData(),
+ hdd: this._getHDDData(),
+ gfx: this._getGFXData(),
+ appleModelId: getSysinfoProperty("appleModelId", null),
+ };
+
+ if (AppConstants.platform === "win") {
+ data = { ...this._getProcessData(), ...data };
+ } else if (AppConstants.platform == "android") {
+ data.device = this._getDeviceData();
+ }
+
+ // Windows 8+
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ data.sec = this._getSecurityAppData();
+ }
+
+ return data;
+ },
+
+ _onEnvironmentChange(what, oldEnvironment) {
+ this._log.trace("_onEnvironmentChange for " + what);
+
+ // We are already skipping change events in _checkChanges if there is a pending change task running.
+ if (this._shutdown) {
+ this._log.trace("_onEnvironmentChange - Already shut down.");
+ return;
+ }
+
+ if (ObjectUtils.deepEqual(this._currentEnvironment, oldEnvironment)) {
+ Services.telemetry.scalarAdd("telemetry.environment_didnt_change", 1);
+ }
+
+ for (let [name, listener] of this._changeListeners) {
+ try {
+ this._log.debug("_onEnvironmentChange - calling " + name);
+ listener(what, oldEnvironment);
+ } catch (e) {
+ this._log.error(
+ "_onEnvironmentChange - listener " + name + " caught error",
+ e
+ );
+ }
+ }
+ },
+
+ reset() {
+ this._shutdown = false;
+ },
+};