summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry/TelemetryEnvironment.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/telemetry/TelemetryEnvironment.jsm')
-rw-r--r--toolkit/components/telemetry/TelemetryEnvironment.jsm1459
1 files changed, 1459 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/TelemetryEnvironment.jsm b/toolkit/components/telemetry/TelemetryEnvironment.jsm
new file mode 100644
index 000000000..e2453649c
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -0,0 +1,1459 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = [
+ "TelemetryEnvironment",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const myScope = this;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
+Cu.import("resource://gre/modules/ObjectUtils.jsm");
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+const Utils = TelemetryUtils;
+
+XPCOMUtils.defineLazyModuleGetter(this, "AttributionCode",
+ "resource:///modules/AttributionCode.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
+ "resource://gre/modules/ctypes.jsm");
+if (AppConstants.platform !== "gonk") {
+ Cu.import("resource://gre/modules/AddonManager.jsm");
+ XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
+ "resource://gre/modules/LightweightThemeManager.jsm");
+}
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
+ "resource://gre/modules/ProfileAge.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.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;
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+var Policy = {
+ now: () => new Date(),
+};
+
+var gGlobalEnvironment;
+function getGlobal() {
+ if (!gGlobalEnvironment) {
+ gGlobalEnvironment = new EnvironmentCache();
+ }
+ return gGlobalEnvironment;
+}
+
+this.TelemetryEnvironment = {
+ get currentEnvironment() {
+ return getGlobal().currentEnvironment;
+ },
+
+ onInitialized: function() {
+ return getGlobal().onInitialized();
+ },
+
+ delayedInit: function() {
+ return getGlobal().delayedInit();
+ },
+
+ registerChangeListener: function(name, listener) {
+ return getGlobal().registerChangeListener(name, listener);
+ },
+
+ unregisterChangeListener: function(name) {
+ return getGlobal().unregisterChangeListener(name);
+ },
+
+ shutdown: function() {
+ return getGlobal().shutdown();
+ },
+
+ // Policy to use when saving preferences. Exported for using them in tests.
+ RECORD_PREF_STATE: 1, // Don't record the preference value
+ RECORD_PREF_VALUE: 2, // We only record user-set prefs.
+
+ // Testing method
+ testWatchPreferences: function(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: function() {
+ return getGlobal().reset();
+ },
+
+ /**
+ * Intended for use in tests only.
+ */
+ testCleanRestart: function() {
+ getGlobal().shutdown();
+ gGlobalEnvironment = null;
+ return getGlobal();
+ },
+};
+
+const RECORD_PREF_STATE = TelemetryEnvironment.RECORD_PREF_STATE;
+const RECORD_PREF_VALUE = TelemetryEnvironment.RECORD_PREF_VALUE;
+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.update.auto", {what: RECORD_PREF_VALUE}],
+ ["app.update.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}],
+ ["app.update.url", {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.newtab.url", {what: RECORD_PREF_STATE}],
+ ["browser.newtabpage.enabled", {what: RECORD_PREF_VALUE}],
+ ["browser.newtabpage.enhanced", {what: RECORD_PREF_VALUE}],
+ ["browser.shell.checkDefaultBrowser", {what: RECORD_PREF_VALUE}],
+ ["browser.search.suggest.enabled", {what: RECORD_PREF_VALUE}],
+ ["browser.startup.homepage", {what: RECORD_PREF_STATE}],
+ ["browser.startup.page", {what: RECORD_PREF_VALUE}],
+ ["browser.tabs.animate", {what: RECORD_PREF_VALUE}],
+ ["browser.urlbar.suggest.searches", {what: RECORD_PREF_VALUE}],
+ ["browser.urlbar.userMadeSearchSuggestionsChoice", {what: RECORD_PREF_VALUE}],
+ // Record "Zoom Text Only" pref in Firefox 50 to 52 (Bug 979323).
+ ["browser.zoom.full", {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.asyncInit.enabled", {what: RECORD_PREF_VALUE}],
+ ["dom.ipc.plugins.enabled", {what: RECORD_PREF_VALUE}],
+ ["dom.ipc.processCount", {what: RECORD_PREF_VALUE, requiresRestart: true}],
+ ["dom.max_script_run_time", {what: RECORD_PREF_VALUE}],
+ ["experiments.manifest.uri", {what: RECORD_PREF_VALUE}],
+ ["extensions.autoDisableScopes", {what: RECORD_PREF_VALUE}],
+ ["extensions.enabledScopes", {what: RECORD_PREF_VALUE}],
+ ["extensions.blocklist.enabled", {what: RECORD_PREF_VALUE}],
+ ["extensions.blocklist.url", {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}],
+ ["general.smoothScroll", {what: RECORD_PREF_VALUE}],
+ ["gfx.direct2d.disabled", {what: RECORD_PREF_VALUE}],
+ ["gfx.direct2d.force-enabled", {what: RECORD_PREF_VALUE}],
+ ["gfx.direct2d.use1_1", {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}],
+ ["network.proxy.autoconfig_url", {what: RECORD_PREF_STATE}],
+ ["network.proxy.http", {what: RECORD_PREF_STATE}],
+ ["network.proxy.ssl", {what: RECORD_PREF_STATE}],
+ ["pdfjs.disabled", {what: RECORD_PREF_VALUE}],
+ ["places.history.enabled", {what: RECORD_PREF_VALUE}],
+ ["privacy.trackingprotection.enabled", {what: RECORD_PREF_VALUE}],
+ ["privacy.donottrackheader.enabled", {what: RECORD_PREF_VALUE}],
+ ["services.sync.serverURL", {what: RECORD_PREF_STATE}],
+ ["security.mixed_content.block_active_content", {what: RECORD_PREF_VALUE}],
+ ["security.mixed_content.block_display_content", {what: RECORD_PREF_VALUE}],
+ ["security.sandbox.content.level", {what: RECORD_PREF_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_HOTFIX_LASTVERSION = "extensions.hotfix.lastVersion";
+const PREF_APP_PARTNER_BRANCH = "app.partner.";
+const PREF_PARTNER_ID = "mozilla.partner.id";
+const PREF_UPDATE_ENABLED = "app.update.enabled";
+const PREF_UPDATE_AUTODOWNLOAD = "app.update.auto";
+const PREF_SEARCH_COHORT = "browser.search.cohort";
+const PREF_E10S_COHORT = "e10s.rollout.cohort";
+
+const COMPOSITOR_CREATED_TOPIC = "compositor:created";
+const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC = "distribution-customization-complete";
+const EXPERIMENTS_CHANGED_TOPIC = "experiments-changed";
+const GFX_FEATURES_READY_TOPIC = "gfx-features-ready";
+const SEARCH_ENGINE_MODIFIED_TOPIC = "browser-search-engine-modified";
+const SEARCH_SERVICE_TOPIC = "browser-search-service";
+
+/**
+ * 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 (new Boolean(aValue)).valueOf();
+}
+
+/**
+ * Get the current browser.
+ * @return a string with the locale or null on failure.
+ */
+function getBrowserLocale() {
+ try {
+ return Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIXULChromeRegistry).
+ getSelectedLocale('global');
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Get the current OS locale.
+ * @return a string with the OS locale or null on failure.
+ */
+function getSystemLocale() {
+ try {
+ return Services.locale.getLocaleComponentForUserAgent();
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Asynchronously get a list of addons of the specified type from the AddonManager.
+ * @param aTypes An array containing the types of addons to request.
+ * @return Promise<Array> resolved when AddonManager has finished, returning an
+ * array of addons.
+ */
+function promiseGetAddonsByTypes(aTypes) {
+ return new Promise((resolve) =>
+ AddonManager.getAddonsByTypes(aTypes, (addons) => resolve(addons)));
+}
+
+/**
+ * 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),
+ driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
+ driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
+ };
+}
+
+/**
+ * Gets the service pack and build information on Windows platforms. The initial version
+ * was copied from nsUpdateService.js.
+ *
+ * @return An object containing the service pack major and minor versions, along with the
+ * build number.
+ */
+function getWindowsVersionInfo() {
+ const UNKNOWN_VERSION_INFO = {servicePackMajor: null, servicePackMinor: null, buildNumber: null};
+
+ if (AppConstants.platform !== "win") {
+ return UNKNOWN_VERSION_INFO;
+ }
+
+ const BYTE = ctypes.uint8_t;
+ const WORD = ctypes.uint16_t;
+ const DWORD = ctypes.uint32_t;
+ const WCHAR = ctypes.char16_t;
+ const BOOL = ctypes.int;
+
+ // This structure is described at:
+ // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx
+ const SZCSDVERSIONLENGTH = 128;
+ const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW',
+ [
+ {dwOSVersionInfoSize: DWORD},
+ {dwMajorVersion: DWORD},
+ {dwMinorVersion: DWORD},
+ {dwBuildNumber: DWORD},
+ {dwPlatformId: DWORD},
+ {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)},
+ {wServicePackMajor: WORD},
+ {wServicePackMinor: WORD},
+ {wSuiteMask: WORD},
+ {wProductType: BYTE},
+ {wReserved: BYTE}
+ ]);
+
+ let kernel32 = ctypes.open("kernel32");
+ try {
+ let GetVersionEx = kernel32.declare("GetVersionExW",
+ ctypes.default_abi,
+ BOOL,
+ OSVERSIONINFOEXW.ptr);
+ let winVer = OSVERSIONINFOEXW();
+ winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
+
+ if (0 === GetVersionEx(winVer.address())) {
+ throw ("Failure in GetVersionEx (returned 0)");
+ }
+
+ return {
+ servicePackMajor: winVer.wServicePackMajor,
+ servicePackMinor: winVer.wServicePackMinor,
+ buildNumber: winVer.dwBuildNumber,
+ };
+ } catch (e) {
+ return UNKNOWN_VERSION_INFO;
+ } finally {
+ kernel32.close();
+ }
+}
+
+/**
+ * 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;
+
+ // Set to true once initial load is complete and we're watching for changes.
+ this._loaded = false;
+}
+EnvironmentAddonBuilder.prototype = {
+ /**
+ * Get the initial set of addons.
+ * @returns Promise<void> when the initial load is complete.
+ */
+ init: function() {
+ // Some tests don't initialize the addon manager. This accounts for the
+ // unfortunate reality of life.
+ try {
+ AddonManager.shutdown.addBlocker("EnvironmentAddonBuilder",
+ () => this._shutdownBlocker());
+ } catch (err) {
+ return Promise.reject(err);
+ }
+
+ this._pendingTask = this._updateAddons().then(
+ () => { this._pendingTask = null; },
+ (err) => {
+ this._environment._log.error("init - Exception in _updateAddons", err);
+ this._pendingTask = null;
+ }
+ );
+
+ return this._pendingTask;
+ },
+
+ /**
+ * Register an addon listener and watch for changes.
+ */
+ watchForChanges: function() {
+ this._loaded = true;
+ AddonManager.addAddonListener(this);
+ Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
+ },
+
+ // AddonListener
+ onEnabled: function() {
+ this._onAddonChange();
+ },
+ onDisabled: function() {
+ this._onAddonChange();
+ },
+ onInstalled: function() {
+ this._onAddonChange();
+ },
+ onUninstalling: function() {
+ this._onAddonChange();
+ },
+
+ _onAddonChange: function() {
+ this._environment._log.trace("_onAddonChange");
+ this._checkForChanges("addons-changed");
+ },
+
+ // nsIObserver
+ observe: function (aSubject, aTopic, aData) {
+ this._environment._log.trace("observe - Topic " + aTopic);
+ this._checkForChanges("experiment-changed");
+ },
+
+ _checkForChanges: function(changeReason) {
+ if (this._pendingTask) {
+ this._environment._log.trace("_checkForChanges - task already pending, dropping change with reason " + changeReason);
+ return;
+ }
+
+ this._pendingTask = this._updateAddons().then(
+ (result) => {
+ this._pendingTask = null;
+ if (result.changed) {
+ this._environment._onEnvironmentChange(changeReason, result.oldEnvironment);
+ }
+ },
+ (err) => {
+ this._pendingTask = null;
+ this._environment._log.error("_checkForChanges: Error collecting addons", err);
+ });
+ },
+
+ _shutdownBlocker: function() {
+ if (this._loaded) {
+ AddonManager.removeAddonListener(this);
+ Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
+ }
+ 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.
+ *
+ * @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.
+ */
+ _updateAddons: Task.async(function* () {
+ this._environment._log.trace("_updateAddons");
+ let personaId = null;
+ if (AppConstants.platform !== "gonk") {
+ let theme = LightweightThemeManager.currentTheme;
+ if (theme) {
+ personaId = theme.id;
+ }
+ }
+
+ let addons = {
+ activeAddons: yield this._getActiveAddons(),
+ theme: yield this._getActiveTheme(),
+ activePlugins: this._getActivePlugins(),
+ activeGMPlugins: yield this._getActiveGMPlugins(),
+ activeExperiment: this._getActiveExperiment(),
+ persona: personaId,
+ };
+
+ let result = {
+ changed: !this._environment._currentEnvironment.addons ||
+ !ObjectUtils.deepEqual(addons, this._environment._currentEnvironment.addons),
+ };
+
+ 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.
+ */
+ _getActiveAddons: Task.async(function* () {
+ // Request addons, asynchronously.
+ let allAddons = yield promiseGetAddonsByTypes(["extension", "service"]);
+
+ let activeAddons = {};
+ for (let addon of allAddons) {
+ // Skip addons which are not active.
+ if (!addon.isActive) {
+ continue;
+ }
+
+ // Weird addon data in the wild can lead to exceptions while collecting
+ // the data.
+ try {
+ // Make sure to have valid dates.
+ let installDate = new Date(Math.max(0, addon.installDate));
+ let updateDate = new Date(Math.max(0, addon.updateDate));
+
+ 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,
+ version: limitStringToLength(addon.version, MAX_ADDON_STRING_LENGTH),
+ scope: addon.scope,
+ type: addon.type,
+ foreignInstall: enforceBoolean(addon.foreignInstall),
+ hasBinaryComponents: addon.hasBinaryComponents,
+ installDay: Utils.millisecondsToDays(installDate.getTime()),
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ signedState: addon.signedState,
+ isSystem: addon.isSystem,
+ };
+
+ if (addon.signedState !== undefined)
+ activeAddons[addon.id].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.
+ */
+ _getActiveTheme: Task.async(function* () {
+ // Request themes, asynchronously.
+ let themes = yield promiseGetAddonsByTypes(["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: theme.hasBinaryComponents,
+ installDay: Utils.millisecondsToDays(installDate.getTime()),
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ };
+ }
+
+ return activeTheme;
+ }),
+
+ /**
+ * Get the plugins data in object form.
+ * @return Object containing the plugins data.
+ */
+ _getActivePlugins: function () {
+ 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.
+ * @return Object containing the GMPlugins data.
+ *
+ * This should only be called from _pendingTask; otherwise we risk
+ * running this during addon manager shutdown.
+ */
+ _getActiveGMPlugins: Task.async(function* () {
+ // Request plugins, asynchronously.
+ let allPlugins = yield promiseGetAddonsByTypes(["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;
+ }),
+
+ /**
+ * Get the active experiment data in object form.
+ * @return Object containing the active experiment data.
+ */
+ _getActiveExperiment: function () {
+ let experimentInfo = {};
+ try {
+ let scope = {};
+ Cu.import("resource:///modules/experiments/Experiments.jsm", scope);
+ let experiments = scope.Experiments.instance();
+ let activeExperiment = experiments.getActiveExperimentID();
+ if (activeExperiment) {
+ experimentInfo.id = activeExperiment;
+ experimentInfo.branch = experiments.getActiveExperimentBranch();
+ }
+ } catch (e) {
+ // If this is not Firefox, the import will fail.
+ }
+
+ return experimentInfo;
+ },
+};
+
+function EnvironmentCache() {
+ this._log = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME, "TelemetryEnvironment::");
+ this._log.trace("constructor");
+
+ this._shutdown = false;
+ this._delayedInitFinished = 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._updateSettings();
+ // Fill in the default search engine, if the search provider is already initialized.
+ this._updateSearchEngine();
+ this._addObservers();
+
+ // Build the remaining asynchronous parts of the environment. Don't register change listeners
+ // until the initial environment has been built.
+
+ let p = [];
+ if (AppConstants.platform === "gonk") {
+ this._addonBuilder = {
+ watchForChanges: function() {}
+ };
+ } else {
+ this._addonBuilder = new EnvironmentAddonBuilder(this);
+ p = [ this._addonBuilder.init() ];
+ }
+
+ this._currentEnvironment.profile = {};
+ p.push(this._updateProfile());
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ p.push(this._updateAttribution());
+ }
+
+ 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();
+ });
+}
+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: function() {
+ if (this._initTask) {
+ return this._initTask;
+ }
+ return Promise.resolve(this.currentEnvironment);
+ },
+
+ /**
+ * This gets called when the delayed init completes.
+ */
+ delayedInit: function() {
+ this._delayedInitFinished = true;
+ },
+
+ /**
+ * 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: function (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: function (name) {
+ this._log.trace("unregisterChangeListener for " + name);
+ if (this._shutdown) {
+ this._log.warn("registerChangeListener - already shutdown");
+ return;
+ }
+ this._changeListeners.delete(name);
+ },
+
+ shutdown: function() {
+ 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.
+ */
+ _watchPreferences: function (aPreferences) {
+ this._stopWatchingPrefs();
+ this._watchedPrefs = aPreferences;
+ 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: function () {
+ let prefData = {};
+ for (let [pref, policy] of this._watchedPrefs.entries()) {
+ // Only record preferences if they are non-default
+ if (!Preferences.isSet(pref)) {
+ continue;
+ }
+
+ // Check the policy for the preference and decide if we need to store its value
+ // or whether it changed from the default value.
+ let prefValue = undefined;
+ if (policy.what == TelemetryEnvironment.RECORD_PREF_STATE) {
+ prefValue = "<user-set>";
+ } else {
+ prefValue = Preferences.get(pref, null);
+ }
+ prefData[pref] = prefValue;
+ }
+ return prefData;
+ },
+
+ /**
+ * Start watching the preferences.
+ */
+ _startWatchingPrefs: function () {
+ this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
+
+ for (let [pref, options] of this._watchedPrefs) {
+ if (!("requiresRestart" in options) || !options.requiresRestart) {
+ Preferences.observe(pref, this._onPrefChanged, this);
+ }
+ }
+ },
+
+ _onPrefChanged: function() {
+ this._log.trace("_onPrefChanged");
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ this._updateSettings();
+ this._onEnvironmentChange("pref-changed", oldEnvironment);
+ },
+
+ /**
+ * Do not receive any more change notifications for the preferences.
+ */
+ _stopWatchingPrefs: function () {
+ this._log.trace("_stopWatchingPrefs");
+
+ for (let [pref, options] of this._watchedPrefs) {
+ if (!("requiresRestart" in options) || !options.requiresRestart) {
+ Preferences.ignore(pref, this._onPrefChanged, this);
+ }
+ }
+ },
+
+ _addObservers: function () {
+ // Watch the search engine change and service topics.
+ Services.obs.addObserver(this, COMPOSITOR_CREATED_TOPIC, false);
+ Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC, false);
+ Services.obs.addObserver(this, GFX_FEATURES_READY_TOPIC, false);
+ Services.obs.addObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC, false);
+ Services.obs.addObserver(this, SEARCH_SERVICE_TOPIC, false);
+ },
+
+ _removeObservers: function () {
+ Services.obs.removeObserver(this, COMPOSITOR_CREATED_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);
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ this._log.trace("observe - aTopic: " + aTopic + ", aData: " + aData);
+ switch (aTopic) {
+ case SEARCH_ENGINE_MODIFIED_TOPIC:
+ if (aData != "engine-current") {
+ 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._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 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;
+ }
+ },
+
+ /**
+ * Get the default search engine.
+ * @return {String} Returns the search engine identifier, "NONE" if no default search
+ * engine is defined or "UNDEFINED" if no engine identifier or name can be found.
+ */
+ _getDefaultSearchEngine: function () {
+ let engine;
+ try {
+ engine = Services.search.defaultEngine;
+ } catch (e) {}
+
+ let name;
+ if (!engine) {
+ name = "NONE";
+ } else if (engine.identifier) {
+ name = engine.identifier;
+ } else if (engine.name) {
+ name = "other-" + engine.name;
+ } else {
+ name = "UNDEFINED";
+ }
+
+ return name;
+ },
+
+ /**
+ * Update the default search engine value.
+ */
+ _updateSearchEngine: function () {
+ if (!Services.search) {
+ // Just ignore cases where the search service is not implemented.
+ 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.
+ this._currentEnvironment.settings.defaultSearchEngine = this._getDefaultSearchEngine();
+ this._currentEnvironment.settings.defaultSearchEngineData =
+ Services.search.getDefaultEngineInfo();
+
+ // Record the cohort identifier used for search defaults A/B testing.
+ if (Services.prefs.prefHasUserValue(PREF_SEARCH_COHORT))
+ this._currentEnvironment.settings.searchCohort = Services.prefs.getCharPref(PREF_SEARCH_COHORT);
+ },
+
+ /**
+ * Update the default search engine value and trigger the environment change.
+ */
+ _onSearchEngineChange: function () {
+ this._log.trace("_onSearchEngineChange");
+
+ // Finally trigger the environment change notification.
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
+ this._updateSearchEngine();
+ this._onEnvironmentChange("search-engine-changed", oldEnvironment);
+ },
+
+ /**
+ * Update the graphics features object.
+ */
+ _updateGraphicsFeatures: function () {
+ 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: function() {
+ this._currentEnvironment.partner = this._getPartner();
+ },
+
+ /**
+ * Get the build data in object form.
+ * @return Object containing the build data.
+ */
+ _getBuild: function () {
+ 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,
+ platformVersion: Services.appinfo.platformVersion || null,
+ xpcomAbi: Services.appinfo.XPCOMABI,
+ hotfixVersion: Preferences.get(PREF_HOTFIX_LASTVERSION, null),
+ };
+
+ // Add |architecturesInBinary| only for Mac Universal builds.
+ if ("@mozilla.org/xpcom/mac-utils;1" in Cc) {
+ let macUtils = Cc["@mozilla.org/xpcom/mac-utils;1"].getService(Ci.nsIMacUtils);
+ if (macUtils && macUtils.isUniversalBinary) {
+ buildData.architecturesInBinary = macUtils.architecturesInBinary;
+ }
+ }
+
+ return buildData;
+ },
+
+ /**
+ * Determine if we're the default browser.
+ * @returns null on error, true if we are the default browser, or false otherwise.
+ */
+ _isDefaultBrowser: function () {
+ if (AppConstants.platform === "gonk") {
+ return true;
+ }
+
+ if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
+ this._log.info("_isDefaultBrowser - Could not obtain browser shell service");
+ return null;
+ }
+
+ let shellService;
+ try {
+ let scope = {};
+ Cu.import("resource:///modules/ShellService.jsm", scope);
+ shellService = scope.ShellService;
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not obtain shell service JSM");
+ }
+
+ if (!shellService) {
+ try {
+ shellService = Cc["@mozilla.org/browser/shell-service;1"]
+ .getService(Ci.nsIShellService);
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not obtain shell service", ex);
+ return null;
+ }
+ }
+
+ try {
+ // This uses the same set of flags used by the pref pane.
+ return shellService.isDefaultBrowser(false, true) ? true : false;
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not determine if default browser", ex);
+ return null;
+ }
+ },
+
+ /**
+ * Update the cached settings data.
+ */
+ _updateSettings: function () {
+ let updateChannel = null;
+ try {
+ updateChannel = UpdateUtils.getUpdateChannel(false);
+ } catch (e) {}
+
+ this._currentEnvironment.settings = {
+ blocklistEnabled: Preferences.get(PREF_BLOCKLIST_ENABLED, true),
+ e10sEnabled: Services.appinfo.browserTabsRemoteAutostart,
+ e10sCohort: Preferences.get(PREF_E10S_COHORT, "unknown"),
+ telemetryEnabled: Utils.isTelemetryEnabled,
+ locale: getBrowserLocale(),
+ update: {
+ channel: updateChannel,
+ enabled: Preferences.get(PREF_UPDATE_ENABLED, true),
+ autoDownload: Preferences.get(PREF_UPDATE_AUTODOWNLOAD, true),
+ },
+ userPrefs: this._getPrefData(),
+ };
+
+ if (AppConstants.platform !== "gonk") {
+ this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
+ AddonManager.checkCompatibility;
+ }
+
+ if (AppConstants.platform !== "android") {
+ this._currentEnvironment.settings.isDefaultBrowser =
+ this._isDefaultBrowser();
+ }
+
+ this._updateSearchEngine();
+ },
+
+ /**
+ * Update the cached profile data.
+ * @returns Promise<> resolved when the I/O is complete.
+ */
+ _updateProfile: Task.async(function* () {
+ const logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, "ProfileAge - ");
+ let profileAccessor = new ProfileAge(null, logger);
+
+ let creationDate = yield profileAccessor.created;
+ let resetDate = yield profileAccessor.reset;
+
+ this._currentEnvironment.profile.creationDate =
+ Utils.millisecondsToDays(creationDate);
+ if (resetDate) {
+ this._currentEnvironment.profile.resetDate =
+ Utils.millisecondsToDays(resetDate);
+ }
+ }),
+
+ /**
+ * Update the cached attribution data object.
+ * @returns Promise<> resolved when the I/O is complete.
+ */
+ _updateAttribution: Task.async(function* () {
+ let data = yield AttributionCode.getAttrDataAsync();
+ if (Object.keys(data).length > 0) {
+ this._currentEnvironment.settings.attribution = {};
+ for (let key in data) {
+ this._currentEnvironment.settings.attribution[key] =
+ limitStringToLength(data[key], MAX_ATTRIBUTION_STRING_LENGTH);
+ }
+ }
+ }),
+
+ /**
+ * Get the partner data in object form.
+ * @return Object containing the partner data.
+ */
+ _getPartner: function () {
+ let partnerData = {
+ distributionId: Preferences.get(PREF_DISTRIBUTION_ID, null),
+ distributionVersion: Preferences.get(PREF_DISTRIBUTION_VERSION, null),
+ partnerId: Preferences.get(PREF_PARTNER_ID, null),
+ distributor: Preferences.get(PREF_DISTRIBUTOR, null),
+ distributorChannel: Preferences.get(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;
+ },
+
+ /**
+ * Get the CPU information.
+ * @return Object containing the CPU information data.
+ */
+ _getCpuData: function () {
+ let cpuData = {
+ count: getSysinfoProperty("cpucount", null),
+ cores: getSysinfoProperty("cpucores", null),
+ vendor: getSysinfoProperty("cpuvendor", null),
+ family: getSysinfoProperty("cpufamily", null),
+ model: getSysinfoProperty("cpumodel", null),
+ stepping: getSysinfoProperty("cpustepping", null),
+ l2cacheKB: getSysinfoProperty("cpucachel2", null),
+ l3cacheKB: getSysinfoProperty("cpucachel3", null),
+ speedMHz: getSysinfoProperty("cpuspeed", null),
+ };
+
+ const CPU_EXTENSIONS = ["hasMMX", "hasSSE", "hasSSE2", "hasSSE3", "hasSSSE3",
+ "hasSSE4A", "hasSSE4_1", "hasSSE4_2", "hasAVX", "hasAVX2",
+ "hasEDSP", "hasARMv6", "hasARMv7", "hasNEON"];
+
+ // Enumerate the available CPU extensions.
+ let availableExts = [];
+ for (let ext of CPU_EXTENSIONS) {
+ if (getSysinfoProperty(ext, false)) {
+ availableExts.push(ext);
+ }
+ }
+
+ cpuData.extensions = availableExts;
+
+ return cpuData;
+ },
+
+ /**
+ * 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: function () {
+ if (!["gonk", "android"].includes(AppConstants.platform)) {
+ return null;
+ }
+
+ return {
+ model: getSysinfoProperty("device", null),
+ manufacturer: getSysinfoProperty("manufacturer", null),
+ hardware: getSysinfoProperty("hardware", null),
+ isTablet: getSysinfoProperty("tablet", null),
+ };
+ },
+
+ /**
+ * Get the OS information.
+ * @return Object containing the OS data.
+ */
+ _getOSData: function () {
+ let data = {
+ name: forceToStringOrNull(getSysinfoProperty("name", null)),
+ version: forceToStringOrNull(getSysinfoProperty("version", null)),
+ locale: forceToStringOrNull(getSystemLocale()),
+ };
+
+ if (["gonk", "android"].includes(AppConstants.platform)) {
+ data.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 = getWindowsVersionInfo();
+ data.servicePackMajor = versionInfo.servicePackMajor;
+ data.servicePackMinor = versionInfo.servicePackMinor;
+ // We only need the build number and UBR if we're at or above Windows 10.
+ if (typeof(data.version) === 'string' &&
+ Services.vc.compare(data.version, "10") >= 0) {
+ data.windowsBuildNumber = versionInfo.buildNumber;
+ // 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);
+ data.windowsUBR = (ubr !== undefined) ? ubr : null;
+ }
+ data.installYear = getSysinfoProperty("installYear", null);
+ }
+
+ return data;
+ },
+
+ /**
+ * Get the HDD information.
+ * @return Object containing the HDD data.
+ */
+ _getHDDData: function () {
+ return {
+ profile: { // hdd where the profile folder is located
+ model: getSysinfoProperty("profileHDDModel", null),
+ revision: getSysinfoProperty("profileHDDRevision", null),
+ },
+ binary: { // hdd where the application binary is located
+ model: getSysinfoProperty("binHDDModel", null),
+ revision: getSysinfoProperty("binHDDRevision", null),
+ },
+ system: { // hdd where the system files are located
+ model: getSysinfoProperty("winHDDModel", null),
+ revision: getSysinfoProperty("winHDDRevision", null),
+ },
+ };
+ },
+
+ /**
+ * Get the GFX information.
+ * @return Object containing the GFX data.
+ */
+ _getGFXData: function () {
+ let gfxData = {
+ D2DEnabled: getGfxField("D2DEnabled", null),
+ DWriteEnabled: getGfxField("DWriteEnabled", null),
+ ContentBackend: getGfxField("ContentBackend", 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 (!["gonk", "android", "linux"].includes(AppConstants.platform)) {
+ 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: function () {
+ 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: memoryMB,
+ virtualMaxMB: virtualMB,
+ cpu: this._getCpuData(),
+ os: this._getOSData(),
+ hdd: this._getHDDData(),
+ gfx: this._getGFXData(),
+ };
+
+ if (AppConstants.platform === "win") {
+ data.isWow64 = getSysinfoProperty("isWow64", null);
+ } else if (["gonk", "android"].includes(AppConstants.platform)) {
+ data.device = this._getDeviceData();
+ }
+
+ return data;
+ },
+
+ _onEnvironmentChange: function (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;
+ }
+
+ 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: function () {
+ this._shutdown = false;
+ this._delayedInitFinished = false;
+ }
+};