browser/experiments/Experiments.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = [
     8   "Experiments",
     9   "ExperimentsProvider",
    10 ];
    12 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    14 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    15 Cu.import("resource://gre/modules/Services.jsm");
    16 Cu.import("resource://gre/modules/Task.jsm");
    17 Cu.import("resource://gre/modules/Promise.jsm");
    18 Cu.import("resource://gre/modules/osfile.jsm");
    19 Cu.import("resource://gre/modules/Log.jsm");
    20 Cu.import("resource://gre/modules/Preferences.jsm");
    21 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
    23 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
    24                                   "resource://gre/modules/UpdateChannel.jsm");
    25 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
    26                                   "resource://gre/modules/AddonManager.jsm");
    27 XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
    28                                   "resource://gre/modules/AddonManager.jsm");
    29 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryPing",
    30                                   "resource://gre/modules/TelemetryPing.jsm");
    31 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
    32                                   "resource://gre/modules/TelemetryLog.jsm");
    33 XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
    34                                   "resource://services-common/utils.js");
    35 XPCOMUtils.defineLazyModuleGetter(this, "Metrics",
    36                                   "resource://gre/modules/Metrics.jsm");
    38 // CertUtils.jsm doesn't expose a single "CertUtils" object like a normal .jsm
    39 // would.
    40 XPCOMUtils.defineLazyGetter(this, "CertUtils",
    41   function() {
    42     var mod = {};
    43     Cu.import("resource://gre/modules/CertUtils.jsm", mod);
    44     return mod;
    45   });
    47 XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
    48                                    "@mozilla.org/xre/app-info;1",
    49                                    "nsICrashReporter");
    51 const FILE_CACHE                = "experiments.json";
    52 const EXPERIMENTS_CHANGED_TOPIC = "experiments-changed";
    53 const MANIFEST_VERSION          = 1;
    54 const CACHE_VERSION             = 1;
    56 const KEEP_HISTORY_N_DAYS       = 180;
    57 const MIN_EXPERIMENT_ACTIVE_SECONDS = 60;
    59 const PREF_BRANCH               = "experiments.";
    60 const PREF_ENABLED              = "enabled"; // experiments.enabled
    61 const PREF_ACTIVE_EXPERIMENT    = "activeExperiment"; // whether we have an active experiment
    62 const PREF_LOGGING              = "logging";
    63 const PREF_LOGGING_LEVEL        = PREF_LOGGING + ".level"; // experiments.logging.level
    64 const PREF_LOGGING_DUMP         = PREF_LOGGING + ".dump"; // experiments.logging.dump
    65 const PREF_MANIFEST_URI         = "manifest.uri"; // experiments.logging.manifest.uri
    66 const PREF_MANIFEST_CHECKCERT   = "manifest.cert.checkAttributes"; // experiments.manifest.cert.checkAttributes
    67 const PREF_MANIFEST_REQUIREBUILTIN = "manifest.cert.requireBuiltin"; // experiments.manifest.cert.requireBuiltin
    68 const PREF_FORCE_SAMPLE = "force-sample-value"; // experiments.force-sample-value
    70 const PREF_HEALTHREPORT_ENABLED = "datareporting.healthreport.service.enabled";
    72 const PREF_BRANCH_TELEMETRY     = "toolkit.telemetry.";
    73 const PREF_TELEMETRY_ENABLED    = "enabled";
    75 const URI_EXTENSION_STRINGS     = "chrome://mozapps/locale/extensions/extensions.properties";
    76 const STRING_TYPE_NAME          = "type.%ID%.name";
    78 const TELEMETRY_LOG = {
    79   // log(key, [kind, experimentId, details])
    80   ACTIVATION_KEY: "EXPERIMENT_ACTIVATION",
    81   ACTIVATION: {
    82     // Successfully activated.
    83     ACTIVATED: "ACTIVATED",
    84     // Failed to install the add-on.
    85     INSTALL_FAILURE: "INSTALL_FAILURE",
    86     // Experiment does not meet activation requirements. Details will
    87     // be provided.
    88     REJECTED: "REJECTED",
    89   },
    91   // log(key, [kind, experimentId, optionalDetails...])
    92   TERMINATION_KEY: "EXPERIMENT_TERMINATION",
    93   TERMINATION: {
    94     // The Experiments service was disabled.
    95     SERVICE_DISABLED: "SERVICE_DISABLED",
    96     // Add-on uninstalled.
    97     ADDON_UNINSTALLED: "ADDON_UNINSTALLED",
    98     // The experiment disabled itself.
    99     FROM_API: "FROM_API",
   100     // The experiment expired (e.g. by exceeding the end date).
   101     EXPIRED: "EXPIRED",
   102     // Disabled after re-evaluating conditions. If this is specified,
   103     // details will be provided.
   104     RECHECK: "RECHECK",
   105   },
   106 };
   108 const gPrefs = new Preferences(PREF_BRANCH);
   109 const gPrefsTelemetry = new Preferences(PREF_BRANCH_TELEMETRY);
   110 let gExperimentsEnabled = false;
   111 let gAddonProvider = null;
   112 let gExperiments = null;
   113 let gLogAppenderDump = null;
   114 let gPolicyCounter = 0;
   115 let gExperimentsCounter = 0;
   116 let gExperimentEntryCounter = 0;
   117 let gPreviousProviderCounter = 0;
   119 // Tracks active AddonInstall we know about so we can deny external
   120 // installs.
   121 let gActiveInstallURLs = new Set();
   123 // Tracks add-on IDs that are being uninstalled by us. This allows us
   124 // to differentiate between expected uninstalled and user-driven uninstalls.
   125 let gActiveUninstallAddonIDs = new Set();
   127 let gLogger;
   128 let gLogDumping = false;
   130 function configureLogging() {
   131   if (!gLogger) {
   132     gLogger = Log.repository.getLogger("Browser.Experiments");
   133     gLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
   134   }
   135   gLogger.level = gPrefs.get(PREF_LOGGING_LEVEL, Log.Level.Warn);
   137   let logDumping = gPrefs.get(PREF_LOGGING_DUMP, false);
   138   if (logDumping != gLogDumping) {
   139     if (logDumping) {
   140       gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
   141       gLogger.addAppender(gLogAppenderDump);
   142     } else {
   143       gLogger.removeAppender(gLogAppenderDump);
   144       gLogAppenderDump = null;
   145     }
   146     gLogDumping = logDumping;
   147   }
   148 }
   150 // Takes an array of promises and returns a promise that is resolved once all of
   151 // them are rejected or resolved.
   152 function allResolvedOrRejected(promises) {
   153   if (!promises.length) {
   154     return Promise.resolve([]);
   155   }
   157   let countdown = promises.length;
   158   let deferred = Promise.defer();
   160   for (let p of promises) {
   161     let helper = () => {
   162       if (--countdown == 0) {
   163         deferred.resolve();
   164       }
   165     };
   166     Promise.resolve(p).then(helper, helper);
   167   }
   169   return deferred.promise;
   170 }
   172 // Loads a JSON file using OS.file. file is a string representing the path
   173 // of the file to be read, options contains additional options to pass to
   174 // OS.File.read.
   175 // Returns a Promise resolved with the json payload or rejected with
   176 // OS.File.Error or JSON.parse() errors.
   177 function loadJSONAsync(file, options) {
   178   return Task.spawn(function() {
   179     let rawData = yield OS.File.read(file, options);
   180     // Read json file into a string
   181     let data;
   182     try {
   183       // Obtain a converter to read from a UTF-8 encoded input stream.
   184       let converter = new TextDecoder();
   185       data = JSON.parse(converter.decode(rawData));
   186     } catch (ex) {
   187       gLogger.error("Experiments: Could not parse JSON: " + file + " " + ex);
   188       throw ex;
   189     }
   190     throw new Task.Result(data);
   191   });
   192 }
   194 function telemetryEnabled() {
   195   return gPrefsTelemetry.get(PREF_TELEMETRY_ENABLED, false);
   196 }
   198 // Returns a promise that is resolved with the AddonInstall for that URL.
   199 function addonInstallForURL(url, hash) {
   200   let deferred = Promise.defer();
   201   AddonManager.getInstallForURL(url, install => deferred.resolve(install),
   202                                 "application/x-xpinstall", hash);
   203   return deferred.promise;
   204 }
   206 // Returns a promise that is resolved with an Array<Addon> of the installed
   207 // experiment addons.
   208 function installedExperimentAddons() {
   209   let deferred = Promise.defer();
   210   AddonManager.getAddonsByTypes(["experiment"], (addons) => {
   211     deferred.resolve([a for (a of addons) if (!a.appDisabled)]);
   212   });
   213   return deferred.promise;
   214 }
   216 // Takes an Array<Addon> and returns a promise that is resolved when the
   217 // addons are uninstalled.
   218 function uninstallAddons(addons) {
   219   let ids = new Set([a.id for (a of addons)]);
   220   let deferred = Promise.defer();
   222   let listener = {};
   223   listener.onUninstalled = addon => {
   224     if (!ids.has(addon.id)) {
   225       return;
   226     }
   228     ids.delete(addon.id);
   229     if (ids.size == 0) {
   230       AddonManager.removeAddonListener(listener);
   231       deferred.resolve();
   232     }
   233   };
   235   AddonManager.addAddonListener(listener);
   237   for (let addon of addons) {
   238     // Disabling the add-on before uninstalling is necessary to cause tests to
   239     // pass. This might be indicative of a bug in XPIProvider.
   240     // TODO follow up in bug 992396.
   241     addon.userDisabled = true;
   242     addon.uninstall();
   243   }
   245   return deferred.promise;
   246 }
   248 /**
   249  * The experiments module.
   250  */
   252 let Experiments = {
   253   /**
   254    * Provides access to the global `Experiments.Experiments` instance.
   255    */
   256   instance: function () {
   257     if (!gExperiments) {
   258       gExperiments = new Experiments.Experiments();
   259     }
   261     return gExperiments;
   262   },
   263 };
   265 /*
   266  * The policy object allows us to inject fake enviroment data from the
   267  * outside by monkey-patching.
   268  */
   270 Experiments.Policy = function () {
   271   this._log = Log.repository.getLoggerWithMessagePrefix(
   272     "Browser.Experiments.Policy",
   273     "Policy #" + gPolicyCounter++ + "::");
   275   // Set to true to ignore hash verification on downloaded XPIs. This should
   276   // not be used outside of testing.
   277   this.ignoreHashes = false;
   278 };
   280 Experiments.Policy.prototype = {
   281   now: function () {
   282     return new Date();
   283   },
   285   random: function () {
   286     let pref = gPrefs.get(PREF_FORCE_SAMPLE);
   287     if (pref !== undefined) {
   288       let val = Number.parseFloat(pref);
   289       this._log.debug("random sample forced: " + val);
   290       if (isNaN(val) || val < 0) {
   291         return 0;
   292       }
   293       if (val > 1) {
   294         return 1;
   295       }
   296       return val;
   297     }
   298     return Math.random();
   299   },
   301   futureDate: function (offset) {
   302     return new Date(this.now().getTime() + offset);
   303   },
   305   oneshotTimer: function (callback, timeout, thisObj, name) {
   306     return CommonUtils.namedTimer(callback, timeout, thisObj, name);
   307   },
   309   updatechannel: function () {
   310     return UpdateChannel.get();
   311   },
   313   locale: function () {
   314     let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
   315     return chrome.getSelectedLocale("global");
   316   },
   318   /*
   319    * @return Promise<> Resolved with the payload data.
   320    */
   321   healthReportPayload: function () {
   322     return Task.spawn(function*() {
   323       let reporter = Cc["@mozilla.org/datareporting/service;1"]
   324             .getService(Ci.nsISupports)
   325             .wrappedJSObject
   326             .healthReporter;
   327       yield reporter.onInit();
   328       let payload = yield reporter.collectAndObtainJSONPayload();
   329       throw new Task.Result(payload);
   330     });
   331   },
   333   telemetryPayload: function () {
   334     return TelemetryPing.getPayload();
   335   },
   336 };
   338 function AlreadyShutdownError(message="already shut down") {
   339   this.name = "AlreadyShutdownError";
   340   this.message = message;
   341 }
   343 AlreadyShutdownError.prototype = new Error();
   344 AlreadyShutdownError.prototype.constructor = AlreadyShutdownError;
   346 /**
   347  * Manages the experiments and provides an interface to control them.
   348  */
   350 Experiments.Experiments = function (policy=new Experiments.Policy()) {
   351   this._log = Log.repository.getLoggerWithMessagePrefix(
   352     "Browser.Experiments.Experiments",
   353     "Experiments #" + gExperimentsCounter++ + "::");
   354   this._log.trace("constructor");
   356   this._policy = policy;
   358   // This is a Map of (string -> ExperimentEntry), keyed with the experiment id.
   359   // It holds both the current experiments and history.
   360   // Map() preserves insertion order, which means we preserve the manifest order.
   361   // This is null until we've successfully completed loading the cache from
   362   // disk the first time.
   363   this._experiments = null;
   364   this._refresh = false;
   365   this._terminateReason = null; // or TELEMETRY_LOG.TERMINATION....
   366   this._dirty = false;
   368   // Loading the cache happens once asynchronously on startup
   369   this._loadTask = null;
   371   // The _main task handles all other actions:
   372   // * refreshing the manifest off the network (if _refresh)
   373   // * disabling/enabling experiments
   374   // * saving the cache (if _dirty)
   375   this._mainTask = null;
   377   // Timer for re-evaluating experiment status.
   378   this._timer = null;
   380   this._shutdown = false;
   382   // We need to tell when we first evaluated the experiments to fire an
   383   // experiments-changed notification when we only loaded completed experiments.
   384   this._firstEvaluate = true;
   386   this.init();
   387 };
   389 Experiments.Experiments.prototype = {
   390   QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsIObserver]),
   392   init: function () {
   393     this._shutdown = false;
   394     configureLogging();
   396     gExperimentsEnabled = gPrefs.get(PREF_ENABLED, false);
   397     this._log.trace("enabled=" + gExperimentsEnabled + ", " + this.enabled);
   399     gPrefs.observe(PREF_LOGGING, configureLogging);
   400     gPrefs.observe(PREF_MANIFEST_URI, this.updateManifest, this);
   401     gPrefs.observe(PREF_ENABLED, this._toggleExperimentsEnabled, this);
   403     gPrefsTelemetry.observe(PREF_TELEMETRY_ENABLED, this._telemetryStatusChanged, this);
   405     AsyncShutdown.profileBeforeChange.addBlocker("Experiments.jsm shutdown",
   406       this.uninit.bind(this));
   408     this._registerWithAddonManager();
   410     let deferred = Promise.defer();
   412     this._loadTask = this._loadFromCache();
   413     this._loadTask.then(
   414       () => {
   415         this._log.trace("_loadTask finished ok");
   416         this._loadTask = null;
   417         this._run().then(deferred.resolve, deferred.reject);
   418       },
   419       (e) => {
   420         this._log.error("_loadFromCache caught error: " + e);
   421         deferred.reject(e);
   422       }
   423     );
   425     return deferred.promise;
   426   },
   428   /**
   429    * Uninitialize this instance.
   430    *
   431    * This function is susceptible to race conditions. If it is called multiple
   432    * times before the previous uninit() has completed or if it is called while
   433    * an init() operation is being performed, the object may get in bad state
   434    * and/or deadlock could occur.
   435    *
   436    * @return Promise<>
   437    *         The promise is fulfilled when all pending tasks are finished.
   438    */
   439   uninit: Task.async(function* () {
   440     this._log.trace("uninit: started");
   441     yield this._loadTask;
   442     this._log.trace("uninit: finished with _loadTask");
   444     if (!this._shutdown) {
   445       this._log.trace("uninit: no previous shutdown");
   446       this._unregisterWithAddonManager();
   448       gPrefs.ignore(PREF_LOGGING, configureLogging);
   449       gPrefs.ignore(PREF_MANIFEST_URI, this.updateManifest, this);
   450       gPrefs.ignore(PREF_ENABLED, this._toggleExperimentsEnabled, this);
   452       gPrefsTelemetry.ignore(PREF_TELEMETRY_ENABLED, this._telemetryStatusChanged, this);
   454       if (this._timer) {
   455         this._timer.clear();
   456       }
   457     }
   459     this._shutdown = true;
   460     if (this._mainTask) {
   461       try {
   462         this._log.trace("uninit: waiting on _mainTask");
   463         yield this._mainTask;
   464       } catch (e if e instanceof AlreadyShutdownError) {
   465         // We error out of tasks after shutdown via that exception.
   466       }
   467     }
   469     this._log.info("Completed uninitialization.");
   470   }),
   472   _registerWithAddonManager: function (previousExperimentsProvider) {
   473     this._log.trace("Registering instance with Addon Manager.");
   475     AddonManager.addAddonListener(this);
   476     AddonManager.addInstallListener(this);
   478     if (!gAddonProvider) {
   479       // The properties of this AddonType should be kept in sync with the
   480       // experiment AddonType registered in XPIProvider.
   481       this._log.trace("Registering previous experiment add-on provider.");
   482       gAddonProvider = previousExperimentsProvider || new Experiments.PreviousExperimentProvider(this);
   483       AddonManagerPrivate.registerProvider(gAddonProvider, [
   484           new AddonManagerPrivate.AddonType("experiment",
   485                                             URI_EXTENSION_STRINGS,
   486                                             STRING_TYPE_NAME,
   487                                             AddonManager.VIEW_TYPE_LIST,
   488                                             11000,
   489                                             AddonManager.TYPE_UI_HIDE_EMPTY),
   490       ]);
   491     }
   493   },
   495   _unregisterWithAddonManager: function () {
   496     this._log.trace("Unregistering instance with Addon Manager.");
   498     if (gAddonProvider) {
   499       this._log.trace("Unregistering previous experiment add-on provider.");
   500       AddonManagerPrivate.unregisterProvider(gAddonProvider);
   501       gAddonProvider = null;
   502     }
   504     AddonManager.removeInstallListener(this);
   505     AddonManager.removeAddonListener(this);
   506   },
   508   /*
   509    * Change the PreviousExperimentsProvider that this instance uses.
   510    * For testing only.
   511    */
   512   _setPreviousExperimentsProvider: function (provider) {
   513     this._unregisterWithAddonManager();
   514     this._registerWithAddonManager(provider);
   515   },
   517   /**
   518    * Throws an exception if we've already shut down.
   519    */
   520   _checkForShutdown: function() {
   521     if (this._shutdown) {
   522       throw new AlreadyShutdownError("uninit() already called");
   523     }
   524   },
   526   /**
   527    * Whether the experiments feature is enabled.
   528    */
   529   get enabled() {
   530     return gExperimentsEnabled;
   531   },
   533   /**
   534    * Toggle whether the experiments feature is enabled or not.
   535    */
   536   set enabled(enabled) {
   537     this._log.trace("set enabled(" + enabled + ")");
   538     gPrefs.set(PREF_ENABLED, enabled);
   539   },
   541   _toggleExperimentsEnabled: Task.async(function* (enabled) {
   542     this._log.trace("_toggleExperimentsEnabled(" + enabled + ")");
   543     let wasEnabled = gExperimentsEnabled;
   544     gExperimentsEnabled = enabled && telemetryEnabled();
   546     if (wasEnabled == gExperimentsEnabled) {
   547       return;
   548     }
   550     if (gExperimentsEnabled) {
   551       yield this.updateManifest();
   552     } else {
   553       yield this.disableExperiment(TELEMETRY_LOG.TERMINATION.SERVICE_DISABLED);
   554       if (this._timer) {
   555         this._timer.clear();
   556       }
   557     }
   558   }),
   560   _telemetryStatusChanged: function () {
   561     this._toggleExperimentsEnabled(gExperimentsEnabled);
   562   },
   564   /**
   565    * Returns a promise that is resolved with an array of `ExperimentInfo` objects,
   566    * which provide info on the currently and recently active experiments.
   567    * The array is in chronological order.
   568    *
   569    * The experiment info is of the form:
   570    * {
   571    *   id: <string>,
   572    *   name: <string>,
   573    *   description: <string>,
   574    *   active: <boolean>,
   575    *   endDate: <integer>, // epoch ms
   576    *   detailURL: <string>,
   577    *   ... // possibly extended later
   578    * }
   579    *
   580    * @return Promise<Array<ExperimentInfo>> Array of experiment info objects.
   581    */
   582   getExperiments: function () {
   583     return Task.spawn(function*() {
   584       yield this._loadTask;
   585       let list = [];
   587       for (let [id, experiment] of this._experiments) {
   588         if (!experiment.startDate) {
   589           // We only collect experiments that are or were active.
   590           continue;
   591         }
   593         list.push({
   594           id: id,
   595           name: experiment._name,
   596           description: experiment._description,
   597           active: experiment.enabled,
   598           endDate: experiment.endDate.getTime(),
   599           detailURL: experiment._homepageURL,
   600 	  branch: experiment.branch,
   601         });
   602       }
   604       // Sort chronologically, descending.
   605       list.sort((a, b) => b.endDate - a.endDate);
   606       return list;
   607     }.bind(this));
   608   },
   610   /**
   611    * Returns the ExperimentInfo for the active experiment, or null
   612    * if there is none.
   613    */
   614   getActiveExperiment: function () {
   615     let experiment = this._getActiveExperiment();
   616     if (!experiment) {
   617       return null;
   618     }
   620     let info = {
   621       id: experiment.id,
   622       name: experiment._name,
   623       description: experiment._description,
   624       active: experiment.enabled,
   625       endDate: experiment.endDate.getTime(),
   626       detailURL: experiment._homepageURL,
   627     };
   629     return info;
   630   },
   632   /**
   633    * Experiment "branch" support. If an experiment has multiple branches, it
   634    * can record the branch with the experiment system and it will
   635    * automatically be included in data reporting (FHR/telemetry payloads).
   636    */
   638   /**
   639    * Set the experiment branch for the specified experiment ID.
   640    * @returns Promise<>
   641    */
   642   setExperimentBranch: Task.async(function*(id, branchstr) {
   643     yield this._loadTask;
   644     let e = this._experiments.get(id);
   645     if (!e) {
   646       throw new Error("Experiment not found");
   647     }
   648     e.branch = String(branchstr);
   649     this._dirty = true;
   650     Services.obs.notifyObservers(null, EXPERIMENTS_CHANGED_TOPIC, null);
   651     yield this._run();
   652   }),
   653   /**
   654    * Get the branch of the specified experiment. If the experiment is unknown,
   655    * throws an error.
   656    *
   657    * @param id The ID of the experiment. Pass null for the currently running
   658    *           experiment.
   659    * @returns Promise<string|null>
   660    * @throws Error if the specified experiment ID is unknown, or if there is no
   661    *         current experiment.
   662    */
   663   getExperimentBranch: Task.async(function*(id=null) {
   664     yield this._loadTask;
   665     let e;
   666     if (id) {
   667       e = this._experiments.get(id);
   668       if (!e) {
   669         throw new Error("Experiment not found");
   670       }
   671     } else {
   672       e = this._getActiveExperiment();
   673       if (e === null) {
   674 	throw new Error("No active experiment");
   675       }
   676     }
   677     return e.branch;
   678   }),
   680   /**
   681    * Determine whether another date has the same UTC day as now().
   682    */
   683   _dateIsTodayUTC: function (d) {
   684     let now = this._policy.now();
   686     return stripDateToMidnight(now).getTime() == stripDateToMidnight(d).getTime();
   687   },
   689   /**
   690    * Obtain the entry of the most recent active experiment that was active
   691    * today.
   692    *
   693    * If no experiment was active today, this resolves to nothing.
   694    *
   695    * Assumption: Only a single experiment can be active at a time.
   696    *
   697    * @return Promise<object>
   698    */
   699   lastActiveToday: function () {
   700     return Task.spawn(function* getMostRecentActiveExperimentTask() {
   701       let experiments = yield this.getExperiments();
   703       // Assumption: Ordered chronologically, descending, with active always
   704       // first.
   705       for (let experiment of experiments) {
   706         if (experiment.active) {
   707           return experiment;
   708         }
   710         if (experiment.endDate && this._dateIsTodayUTC(experiment.endDate)) {
   711           return experiment;
   712         }
   713       }
   714       return null;
   715     }.bind(this));
   716   },
   718   _run: function() {
   719     this._log.trace("_run");
   720     this._checkForShutdown();
   721     if (!this._mainTask) {
   722       this._mainTask = Task.spawn(this._main.bind(this));
   723       this._mainTask.then(
   724         () => {
   725           this._log.trace("_main finished, scheduling next run");
   726           this._mainTask = null;
   727           this._scheduleNextRun();
   728         },
   729         (e) => {
   730           this._log.error("_main caught error: " + e);
   731           this._mainTask = null;
   732         }
   733       );
   734     }
   735     return this._mainTask;
   736   },
   738   _main: function*() {
   739     do {
   740       this._log.trace("_main iteration");
   741       yield this._loadTask;
   742       if (!gExperimentsEnabled) {
   743         this._refresh = false;
   744       }
   746       if (this._refresh) {
   747         yield this._loadManifest();
   748       }
   749       yield this._evaluateExperiments();
   750       if (this._dirty) {
   751         yield this._saveToCache();
   752       }
   753       // If somebody called .updateManifest() or disableExperiment()
   754       // while we were running, go again right now.
   755     }
   756     while (this._refresh || this._terminateReason);
   757   },
   759   _loadManifest: function*() {
   760     this._log.trace("_loadManifest");
   761     let uri = Services.urlFormatter.formatURLPref(PREF_BRANCH + PREF_MANIFEST_URI);
   763     this._checkForShutdown();
   765     this._refresh = false;
   766     try {
   767       let responseText = yield this._httpGetRequest(uri);
   768       this._log.trace("_loadManifest() - responseText=\"" + responseText + "\"");
   770       if (this._shutdown) {
   771         return;
   772       }
   774       let data = JSON.parse(responseText);
   775       this._updateExperiments(data);
   776     } catch (e) {
   777       this._log.error("_loadManifest - failure to fetch/parse manifest (continuing anyway): " + e);
   778     }
   779   },
   781   /**
   782    * Fetch an updated list of experiments and trigger experiment updates.
   783    * Do only use when experiments are enabled.
   784    *
   785    * @return Promise<>
   786    *         The promise is resolved when the manifest and experiment list is updated.
   787    */
   788   updateManifest: function () {
   789     this._log.trace("updateManifest()");
   791     if (!gExperimentsEnabled) {
   792       return Promise.reject(new Error("experiments are disabled"));
   793     }
   795     if (this._shutdown) {
   796       return Promise.reject(Error("uninit() alrady called"));
   797     }
   799     this._refresh = true;
   800     return this._run();
   801   },
   803   notify: function (timer) {
   804     this._log.trace("notify()");
   805     this._checkForShutdown();
   806     return this._run();
   807   },
   809   // START OF ADD-ON LISTENERS
   811   onUninstalled: function (addon) {
   812     this._log.trace("onUninstalled() - addon id: " + addon.id);
   813     if (gActiveUninstallAddonIDs.has(addon.id)) {
   814       this._log.trace("matches pending uninstall");
   815       return;
   816     }
   817     let activeExperiment = this._getActiveExperiment();
   818     if (!activeExperiment || activeExperiment._addonId != addon.id) {
   819       return;
   820     }
   822     this.disableExperiment(TELEMETRY_LOG.TERMINATION.ADDON_UNINSTALLED);
   823   },
   825   onInstallStarted: function (install) {
   826     if (install.addon.type != "experiment") {
   827       return;
   828     }
   830     this._log.trace("onInstallStarted() - " + install.addon.id);
   831     if (install.addon.appDisabled) {
   832       // This is a PreviousExperiment
   833       return;
   834     }
   836     // We want to be in control of all experiment add-ons: reject installs
   837     // for add-ons that we don't know about.
   839     // We have a race condition of sorts to worry about here. We have 2
   840     // onInstallStarted listeners. This one (the global one) and the one
   841     // created as part of ExperimentEntry._installAddon. Because of the order
   842     // they are registered in, this one likely executes first. Unfortunately,
   843     // this means that the add-on ID is not yet set on the ExperimentEntry.
   844     // So, we can't just look at this._trackedAddonIds because the new experiment
   845     // will have its add-on ID set to null. We work around this by storing a
   846     // identifying field - the source URL of the install - in a module-level
   847     // variable (so multiple Experiments instances doesn't cancel each other
   848     // out).
   850     if (this._trackedAddonIds.has(install.addon.id)) {
   851       this._log.info("onInstallStarted allowing install because add-on ID " +
   852                      "tracked by us.");
   853       return;
   854     }
   856     if (gActiveInstallURLs.has(install.sourceURI.spec)) {
   857       this._log.info("onInstallStarted allowing install because install " +
   858                      "tracked by us.");
   859       return;
   860     }
   862     this._log.warn("onInstallStarted cancelling install of unknown " +
   863                    "experiment add-on: " + install.addon.id);
   864     return false;
   865   },
   867   // END OF ADD-ON LISTENERS.
   869   _getExperimentByAddonId: function (addonId) {
   870     for (let [, entry] of this._experiments) {
   871       if (entry._addonId === addonId) {
   872         return entry;
   873       }
   874     }
   876     return null;
   877   },
   879   /*
   880    * Helper function to make HTTP GET requests. Returns a promise that is resolved with
   881    * the responseText when the request is complete.
   882    */
   883   _httpGetRequest: function (url) {
   884     this._log.trace("httpGetRequest(" + url + ")");
   885     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
   886     try {
   887       xhr.open("GET", url);
   888     } catch (e) {
   889       this._log.error("httpGetRequest() - Error opening request to " + url + ": " + e);
   890       return Promise.reject(new Error("Experiments - Error opening XHR for " + url));
   891     }
   893     let deferred = Promise.defer();
   895     let log = this._log;
   896     xhr.onerror = function (e) {
   897       log.error("httpGetRequest::onError() - Error making request to " + url + ": " + e.error);
   898       deferred.reject(new Error("Experiments - XHR error for " + url + " - " + e.error));
   899     };
   901     xhr.onload = function (event) {
   902       if (xhr.status !== 200 && xhr.state !== 0) {
   903         log.error("httpGetRequest::onLoad() - Request to " + url + " returned status " + xhr.status);
   904         deferred.reject(new Error("Experiments - XHR status for " + url + " is " + xhr.status));
   905         return;
   906       }
   908       let certs = null;
   909       if (gPrefs.get(PREF_MANIFEST_CHECKCERT, true)) {
   910         certs = CertUtils.readCertPrefs(PREF_BRANCH + "manifest.certs.");
   911       }
   912       try {
   913         let allowNonBuiltin = !gPrefs.get(PREF_MANIFEST_REQUIREBUILTIN, true);
   914         CertUtils.checkCert(xhr.channel, allowNonBuiltin, certs);
   915       }
   916       catch (e) {
   917         log.error("manifest fetch failed certificate checks", [e]);
   918         deferred.reject(new Error("Experiments - manifest fetch failed certificate checks: " + e));
   919         return;
   920       }
   922       deferred.resolve(xhr.responseText);
   923     };
   925     if (xhr.channel instanceof Ci.nsISupportsPriority) {
   926       xhr.channel.priority = Ci.nsISupportsPriority.PRIORITY_LOWEST;
   927     }
   929     xhr.send(null);
   930     return deferred.promise;
   931   },
   933   /*
   934    * Path of the cache file we use in the profile.
   935    */
   936   get _cacheFilePath() {
   937     return OS.Path.join(OS.Constants.Path.profileDir, FILE_CACHE);
   938   },
   940   /*
   941    * Part of the main task to save the cache to disk, called from _main.
   942    */
   943   _saveToCache: function* () {
   944     this._log.trace("_saveToCache");
   945     let path = this._cacheFilePath;
   946     let textData = JSON.stringify({
   947       version: CACHE_VERSION,
   948       data: [e[1].toJSON() for (e of this._experiments.entries())],
   949     });
   951     let encoder = new TextEncoder();
   952     let data = encoder.encode(textData);
   953     let options = { tmpPath: path + ".tmp", compression: "lz4" };
   954     yield OS.File.writeAtomic(path, data, options);
   955     this._dirty = false;
   956     this._log.debug("_saveToCache saved to " + path);
   957   },
   959   /*
   960    * Task function, load the cached experiments manifest file from disk.
   961    */
   962   _loadFromCache: Task.async(function* () {
   963     this._log.trace("_loadFromCache");
   964     let path = this._cacheFilePath;
   965     try {
   966       let result = yield loadJSONAsync(path, { compression: "lz4" });
   967       this._populateFromCache(result);
   968     } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
   969       // No cached manifest yet.
   970       this._experiments = new Map();
   971     }
   972   }),
   974   _populateFromCache: function (data) {
   975     this._log.trace("populateFromCache() - data: " + JSON.stringify(data));
   977     // If the user has a newer cache version than we can understand, we fail
   978     // hard; no experiments should be active in this older client.
   979     if (CACHE_VERSION !== data.version) {
   980       throw new Error("Experiments::_populateFromCache() - invalid cache version");
   981     }
   983     let experiments = new Map();
   984     for (let item of data.data) {
   985       let entry = new Experiments.ExperimentEntry(this._policy);
   986       if (!entry.initFromCacheData(item)) {
   987         continue;
   988       }
   989       experiments.set(entry.id, entry);
   990     }
   992     this._experiments = experiments;
   993   },
   995   /*
   996    * Update the experiment entries from the experiments
   997    * array in the manifest
   998    */
   999   _updateExperiments: function (manifestObject) {
  1000     this._log.trace("_updateExperiments() - experiments: " + JSON.stringify(manifestObject));
  1002     if (manifestObject.version !== MANIFEST_VERSION) {
  1003       this._log.warning("updateExperiments() - unsupported version " + manifestObject.version);
  1006     let experiments = new Map(); // The new experiments map
  1008     // Collect new and updated experiments.
  1009     for (let data of manifestObject.experiments) {
  1010       let entry = this._experiments.get(data.id);
  1012       if (entry) {
  1013         if (!entry.updateFromManifestData(data)) {
  1014           this._log.error("updateExperiments() - Invalid manifest data for " + data.id);
  1015           continue;
  1017       } else {
  1018         entry = new Experiments.ExperimentEntry(this._policy);
  1019         if (!entry.initFromManifestData(data)) {
  1020           continue;
  1024       if (entry.shouldDiscard()) {
  1025         continue;
  1028       experiments.set(entry.id, entry);
  1031     // Make sure we keep experiments that are or were running.
  1032     // We remove them after KEEP_HISTORY_N_DAYS.
  1033     for (let [id, entry] of this._experiments) {
  1034       if (experiments.has(id)) {
  1035         continue;
  1038       if (!entry.startDate || entry.shouldDiscard()) {
  1039         this._log.trace("updateExperiments() - discarding entry for " + id);
  1040         continue;
  1043       experiments.set(id, entry);
  1046     this._experiments = experiments;
  1047     this._dirty = true;
  1048   },
  1050   getActiveExperimentID: function() {
  1051     if (!this._experiments) {
  1052       return null;
  1054     let e = this._getActiveExperiment();
  1055     if (!e) {
  1056       return null;
  1058     return e.id;
  1059   },
  1061   getActiveExperimentBranch: function() {
  1062     if (!this._experiments) {
  1063       return null;
  1065     let e = this._getActiveExperiment();
  1066     if (!e) {
  1067       return null;
  1069     return e.branch;
  1070   },
  1072   _getActiveExperiment: function () {
  1073     let enabled = [experiment for ([,experiment] of this._experiments) if (experiment._enabled)];
  1075     if (enabled.length == 1) {
  1076       return enabled[0];
  1079     if (enabled.length > 1) {
  1080       this._log.error("getActiveExperimentId() - should not have more than 1 active experiment");
  1081       throw new Error("have more than 1 active experiment");
  1084     return null;
  1085   },
  1087   /**
  1088    * Disables all active experiments.
  1090    * @return Promise<> Promise that will get resolved once the task is done or failed.
  1091    */
  1092   disableExperiment: function (reason) {
  1093     if (!reason) {
  1094       throw new Error("Must specify a termination reason.");
  1097     this._log.trace("disableExperiment()");
  1098     this._terminateReason = reason;
  1099     return this._run();
  1100   },
  1102   /**
  1103    * The Set of add-on IDs that we know about from manifests.
  1104    */
  1105   get _trackedAddonIds() {
  1106     if (!this._experiments) {
  1107       return new Set();
  1110     return new Set([e._addonId for ([,e] of this._experiments) if (e._addonId)]);
  1111   },
  1113   /*
  1114    * Task function to check applicability of experiments, disable the active
  1115    * experiment if needed and activate the first applicable candidate.
  1116    */
  1117   _evaluateExperiments: function*() {
  1118     this._log.trace("_evaluateExperiments");
  1120     this._checkForShutdown();
  1122     // The first thing we do is reconcile our state against what's in the
  1123     // Addon Manager. It's possible that the Addon Manager knows of experiment
  1124     // add-ons that we don't. This could happen if an experiment gets installed
  1125     // when we're not listening or if there is a bug in our synchronization
  1126     // code.
  1127     //
  1128     // We have a few options of what to do with unknown experiment add-ons
  1129     // coming from the Addon Manager. Ideally, we'd convert these to
  1130     // ExperimentEntry instances and stuff them inside this._experiments.
  1131     // However, since ExperimentEntry contain lots of metadata from the
  1132     // manifest and trying to make up data could be error prone, it's safer
  1133     // to not try. Furthermore, if an experiment really did come from us, we
  1134     // should have some record of it. In the end, we decide to discard all
  1135     // knowledge for these unknown experiment add-ons.
  1136     let installedExperiments = yield installedExperimentAddons();
  1137     let expectedAddonIds = this._trackedAddonIds;
  1138     let unknownAddons = [a for (a of installedExperiments) if (!expectedAddonIds.has(a.id))];
  1139     if (unknownAddons.length) {
  1140       this._log.warn("_evaluateExperiments() - unknown add-ons in AddonManager: " +
  1141                      [a.id for (a of unknownAddons)].join(", "));
  1143       yield uninstallAddons(unknownAddons);
  1146     let activeExperiment = this._getActiveExperiment();
  1147     let activeChanged = false;
  1148     let now = this._policy.now();
  1150     if (!activeExperiment) {
  1151       // Avoid this pref staying out of sync if there were e.g. crashes.
  1152       gPrefs.set(PREF_ACTIVE_EXPERIMENT, false);
  1155     // Ensure the active experiment is in the proper state. This may install,
  1156     // uninstall, upgrade, or enable the experiment add-on. What exactly is
  1157     // abstracted away from us by design.
  1158     if (activeExperiment) {
  1159       let changes;
  1160       let shouldStopResult = yield activeExperiment.shouldStop();
  1161       if (shouldStopResult.shouldStop) {
  1162         let expireReasons = ["endTime", "maxActiveSeconds"];
  1163         let kind, reason;
  1165         if (expireReasons.indexOf(shouldStopResult.reason[0]) != -1) {
  1166           kind = TELEMETRY_LOG.TERMINATION.EXPIRED;
  1167           reason = null;
  1168         } else {
  1169           kind = TELEMETRY_LOG.TERMINATION.RECHECK;
  1170           reason = shouldStopResult.reason;
  1172         changes = yield activeExperiment.stop(kind, reason);
  1174       else if (this._terminateReason) {
  1175         changes = yield activeExperiment.stop(this._terminateReason);
  1177       else {
  1178         changes = yield activeExperiment.reconcileAddonState();
  1181       if (changes) {
  1182         this._dirty = true;
  1183         activeChanged = true;
  1186       if (!activeExperiment._enabled) {
  1187         activeExperiment = null;
  1188         activeChanged = true;
  1192     this._terminateReason = null;
  1194     if (!activeExperiment && gExperimentsEnabled) {
  1195       for (let [id, experiment] of this._experiments) {
  1196         let applicable;
  1197         let reason = null;
  1198         try {
  1199           applicable = yield experiment.isApplicable();
  1201         catch (e) {
  1202           applicable = false;
  1203           reason = e;
  1206         if (!applicable && reason && reason[0] != "was-active") {
  1207           // Report this from here to avoid over-reporting.
  1208           let desc = TELEMETRY_LOG.ACTIVATION;
  1209           let data = [TELEMETRY_LOG.ACTIVATION.REJECTED, id];
  1210           data = data.concat(reason);
  1211           TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY, data);
  1214         if (!applicable) {
  1215           continue;
  1218         this._log.debug("evaluateExperiments() - activating experiment " + id);
  1219         try {
  1220           yield experiment.start();
  1221           activeChanged = true;
  1222           activeExperiment = experiment;
  1223           this._dirty = true;
  1224           break;
  1225         } catch (e) {
  1226           // On failure, clean up the best we can and try the next experiment.
  1227           this._log.error("evaluateExperiments() - Unable to start experiment: " + e.message);
  1228           experiment._enabled = false;
  1229           yield experiment.reconcileAddonState();
  1234     gPrefs.set(PREF_ACTIVE_EXPERIMENT, activeExperiment != null);
  1236     if (activeChanged || this._firstEvaluate) {
  1237       Services.obs.notifyObservers(null, EXPERIMENTS_CHANGED_TOPIC, null);
  1238       this._firstEvaluate = false;
  1241     if ("@mozilla.org/toolkit/crash-reporter;1" in Cc && activeExperiment) {
  1242       try {
  1243         gCrashReporter.annotateCrashReport("ActiveExperiment", activeExperiment.id);
  1244       } catch (e) {
  1245         // It's ok if crash reporting is disabled.
  1248   },
  1250   /*
  1251    * Schedule the soonest re-check of experiment applicability that is needed.
  1252    */
  1253   _scheduleNextRun: function () {
  1254     this._checkForShutdown();
  1256     if (this._timer) {
  1257       this._timer.clear();
  1260     if (!gExperimentsEnabled || this._experiments.length == 0) {
  1261       return;
  1264     let time = null;
  1265     let now = this._policy.now().getTime();
  1267     for (let [id, experiment] of this._experiments) {
  1268       let scheduleTime = experiment.getScheduleTime();
  1269       if (scheduleTime > now) {
  1270         if (time !== null) {
  1271           time = Math.min(time, scheduleTime);
  1272         } else {
  1273           time = scheduleTime;
  1278     if (time === null) {
  1279       // No schedule time found.
  1280       return;
  1283     this._log.trace("scheduleExperimentEvaluation() - scheduling for "+time+", now: "+now);
  1284     this._policy.oneshotTimer(this.notify, time - now, this, "_timer");
  1285   },
  1286 };
  1289 /*
  1290  * Represents a single experiment.
  1291  */
  1293 Experiments.ExperimentEntry = function (policy) {
  1294   this._policy = policy || new Experiments.Policy();
  1295   this._log = Log.repository.getLoggerWithMessagePrefix(
  1296     "Browser.Experiments.Experiments",
  1297     "ExperimentEntry #" + gExperimentEntryCounter++ + "::");
  1299   // Is the experiment supposed to be running.
  1300   this._enabled = false;
  1301   // When this experiment was started, if ever.
  1302   this._startDate = null;
  1303   // When this experiment was ended, if ever.
  1304   this._endDate = null;
  1305   // The condition data from the manifest.
  1306   this._manifestData = null;
  1307   // For an active experiment, signifies whether we need to update the xpi.
  1308   this._needsUpdate = false;
  1309   // A random sample value for comparison against the manifest conditions.
  1310   this._randomValue = null;
  1311   // When this entry was last changed for respecting history retention duration.
  1312   this._lastChangedDate = null;
  1313   // Has this experiment failed to activate before?
  1314   this._failedStart = false;
  1315   // The experiment branch
  1316   this._branch = null;
  1318   // We grab these from the addon after download.
  1319   this._name = null;
  1320   this._description = null;
  1321   this._homepageURL = null;
  1322   this._addonId = null;
  1323 };
  1325 Experiments.ExperimentEntry.prototype = {
  1326   MANIFEST_REQUIRED_FIELDS: new Set([
  1327     "id",
  1328     "xpiURL",
  1329     "xpiHash",
  1330     "startTime",
  1331     "endTime",
  1332     "maxActiveSeconds",
  1333     "appName",
  1334     "channel",
  1335   ]),
  1337   MANIFEST_OPTIONAL_FIELDS: new Set([
  1338     "maxStartTime",
  1339     "minVersion",
  1340     "maxVersion",
  1341     "version",
  1342     "minBuildID",
  1343     "maxBuildID",
  1344     "buildIDs",
  1345     "os",
  1346     "locale",
  1347     "sample",
  1348     "disabled",
  1349     "frozen",
  1350     "jsfilter",
  1351   ]),
  1353   SERIALIZE_KEYS: new Set([
  1354     "_enabled",
  1355     "_manifestData",
  1356     "_needsUpdate",
  1357     "_randomValue",
  1358     "_failedStart",
  1359     "_name",
  1360     "_description",
  1361     "_homepageURL",
  1362     "_addonId",
  1363     "_startDate",
  1364     "_endDate",
  1365     "_branch",
  1366   ]),
  1368   DATE_KEYS: new Set([
  1369     "_startDate",
  1370     "_endDate",
  1371   ]),
  1373   UPGRADE_KEYS: new Map([
  1374     ["_branch", null],
  1375   ]),
  1377   ADDON_CHANGE_NONE: 0,
  1378   ADDON_CHANGE_INSTALL: 1,
  1379   ADDON_CHANGE_UNINSTALL: 2,
  1380   ADDON_CHANGE_ENABLE: 4,
  1382   /*
  1383    * Initialize entry from the manifest.
  1384    * @param data The experiment data from the manifest.
  1385    * @return boolean Whether initialization succeeded.
  1386    */
  1387   initFromManifestData: function (data) {
  1388     if (!this._isManifestDataValid(data)) {
  1389       return false;
  1392     this._manifestData = data;
  1394     this._randomValue = this._policy.random();
  1395     this._lastChangedDate = this._policy.now();
  1397     return true;
  1398   },
  1400   get enabled() {
  1401     return this._enabled;
  1402   },
  1404   get id() {
  1405     return this._manifestData.id;
  1406   },
  1408   get branch() {
  1409     return this._branch;
  1410   },
  1412   set branch(v) {
  1413     this._branch = v;
  1414   },
  1416   get startDate() {
  1417     return this._startDate;
  1418   },
  1420   get endDate() {
  1421     if (!this._startDate) {
  1422       return null;
  1425     let endTime = 0;
  1427     if (!this._enabled) {
  1428       return this._endDate;
  1431     let maxActiveMs = 1000 * this._manifestData.maxActiveSeconds;
  1432     endTime = Math.min(1000 * this._manifestData.endTime,
  1433                        this._startDate.getTime() + maxActiveMs);
  1435     return new Date(endTime);
  1436   },
  1438   get needsUpdate() {
  1439     return this._needsUpdate;
  1440   },
  1442   /*
  1443    * Initialize entry from the cache.
  1444    * @param data The entry data from the cache.
  1445    * @return boolean Whether initialization succeeded.
  1446    */
  1447   initFromCacheData: function (data) {
  1448     for (let [key, dval] of this.UPGRADE_KEYS) {
  1449       if (!(key in data)) {
  1450         data[key] = dval;
  1454     for (let key of this.SERIALIZE_KEYS) {
  1455       if (!(key in data) && !this.DATE_KEYS.has(key)) {
  1456         this._log.error("initFromCacheData() - missing required key " + key);
  1457         return false;
  1459     };
  1461     if (!this._isManifestDataValid(data._manifestData)) {
  1462       return false;
  1465     // Dates are restored separately from epoch ms, everything else is just
  1466     // copied in.
  1468     this.SERIALIZE_KEYS.forEach(key => {
  1469       if (!this.DATE_KEYS.has(key)) {
  1470         this[key] = data[key];
  1472     });
  1474     this.DATE_KEYS.forEach(key => {
  1475       if (key in data) {
  1476         let date = new Date();
  1477         date.setTime(data[key]);
  1478         this[key] = date;
  1480     });
  1482     this._lastChangedDate = this._policy.now();
  1484     return true;
  1485   },
  1487   /*
  1488    * Returns a JSON representation of this object.
  1489    */
  1490   toJSON: function () {
  1491     let obj = {};
  1493     // Dates are serialized separately as epoch ms.
  1495     this.SERIALIZE_KEYS.forEach(key => {
  1496       if (!this.DATE_KEYS.has(key)) {
  1497         obj[key] = this[key];
  1499     });
  1501     this.DATE_KEYS.forEach(key => {
  1502       if (this[key]) {
  1503         obj[key] = this[key].getTime();
  1505     });
  1507     return obj;
  1508   },
  1510   /*
  1511    * Update from the experiment data from the manifest.
  1512    * @param data The experiment data from the manifest.
  1513    * @return boolean Whether updating succeeded.
  1514    */
  1515   updateFromManifestData: function (data) {
  1516     let old = this._manifestData;
  1518     if (!this._isManifestDataValid(data)) {
  1519       return false;
  1522     if (this._enabled) {
  1523       if (old.xpiHash !== data.xpiHash) {
  1524         // A changed hash means we need to update active experiments.
  1525         this._needsUpdate = true;
  1527     } else if (this._failedStart &&
  1528                (old.xpiHash !== data.xpiHash) ||
  1529                (old.xpiURL !== data.xpiURL)) {
  1530       // Retry installation of previously invalid experiments
  1531       // if hash or url changed.
  1532       this._failedStart = false;
  1535     this._manifestData = data;
  1536     this._lastChangedDate = this._policy.now();
  1538     return true;
  1539   },
  1541   /*
  1542    * Is this experiment applicable?
  1543    * @return Promise<> Resolved if the experiment is applicable.
  1544    *                   If it is not applicable it is rejected with
  1545    *                   a Promise<string> which contains the reason.
  1546    */
  1547   isApplicable: function () {
  1548     let versionCmp = Cc["@mozilla.org/xpcom/version-comparator;1"]
  1549                               .getService(Ci.nsIVersionComparator);
  1550     let app = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
  1551     let runtime = Cc["@mozilla.org/xre/app-info;1"]
  1552                     .getService(Ci.nsIXULRuntime);
  1554     let locale = this._policy.locale();
  1555     let channel = this._policy.updatechannel();
  1556     let data = this._manifestData;
  1558     let now = this._policy.now() / 1000; // The manifest times are in seconds.
  1559     let minActive = MIN_EXPERIMENT_ACTIVE_SECONDS;
  1560     let maxActive = data.maxActiveSeconds || 0;
  1561     let startSec = (this.startDate || 0) / 1000;
  1563     this._log.trace("isApplicable() - now=" + now
  1564                     + ", randomValue=" + this._randomValue
  1565                     + ", data=" + JSON.stringify(this._manifestData));
  1567     // Not applicable if it already ran.
  1569     if (!this.enabled && this._endDate) {
  1570       return Promise.reject(["was-active"]);
  1573     // Define and run the condition checks.
  1575     let simpleChecks = [
  1576       { name: "failedStart",
  1577         condition: () => !this._failedStart },
  1578       { name: "disabled",
  1579         condition: () => !data.disabled },
  1580       { name: "frozen",
  1581         condition: () => !data.frozen || this._enabled },
  1582       { name: "startTime",
  1583         condition: () => now >= data.startTime },
  1584       { name: "endTime",
  1585         condition: () => now < data.endTime },
  1586       { name: "maxStartTime",
  1587         condition: () => !data.maxStartTime || now <= data.maxStartTime },
  1588       { name: "maxActiveSeconds",
  1589         condition: () => !this._startDate || now <= (startSec + maxActive) },
  1590       { name: "appName",
  1591         condition: () => !data.appName || data.appName.indexOf(app.name) != -1 },
  1592       { name: "minBuildID",
  1593         condition: () => !data.minBuildID || app.platformBuildID >= data.minBuildID },
  1594       { name: "maxBuildID",
  1595         condition: () => !data.maxBuildID || app.platformBuildID <= data.maxBuildID },
  1596       { name: "buildIDs",
  1597         condition: () => !data.buildIDs || data.buildIDs.indexOf(app.platformBuildID) != -1 },
  1598       { name: "os",
  1599         condition: () => !data.os || data.os.indexOf(runtime.OS) != -1 },
  1600       { name: "channel",
  1601         condition: () => !data.channel || data.channel.indexOf(channel) != -1 },
  1602       { name: "locale",
  1603         condition: () => !data.locale || data.locale.indexOf(locale) != -1 },
  1604       { name: "sample",
  1605         condition: () => data.sample === undefined || this._randomValue <= data.sample },
  1606       { name: "version",
  1607         condition: () => !data.version || data.version.indexOf(app.version) != -1 },
  1608       { name: "minVersion",
  1609         condition: () => !data.minVersion || versionCmp.compare(app.version, data.minVersion) >= 0 },
  1610       { name: "maxVersion",
  1611         condition: () => !data.maxVersion || versionCmp.compare(app.version, data.maxVersion) <= 0 },
  1612     ];
  1614     for (let check of simpleChecks) {
  1615       let result = check.condition();
  1616       if (!result) {
  1617         this._log.debug("isApplicable() - id="
  1618                         + data.id + " - test '" + check.name + "' failed");
  1619         return Promise.reject([check.name]);
  1623     if (data.jsfilter) {
  1624       return this._runFilterFunction(data.jsfilter);
  1627     return Promise.resolve(true);
  1628   },
  1630   /*
  1631    * Run the jsfilter function from the manifest in a sandbox and return the
  1632    * result (forced to boolean).
  1633    */
  1634   _runFilterFunction: function (jsfilter) {
  1635     this._log.trace("runFilterFunction() - filter: " + jsfilter);
  1637     return Task.spawn(function ExperimentEntry_runFilterFunction_task() {
  1638       const nullprincipal = Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal);
  1639       let options = {
  1640         sandboxName: "telemetry experiments jsfilter sandbox",
  1641         wantComponents: false,
  1642       };
  1644       let sandbox = Cu.Sandbox(nullprincipal);
  1645       let context = {};
  1646       context.healthReportPayload = yield this._policy.healthReportPayload();
  1647       context.telemetryPayload    = yield this._policy.telemetryPayload();
  1649       try {
  1650         Cu.evalInSandbox(jsfilter, sandbox);
  1651       } catch (e) {
  1652         this._log.error("runFilterFunction() - failed to eval jsfilter: " + e.message);
  1653         throw ["jsfilter-evalfailed"];
  1656       // You can't insert arbitrarily complex objects into a sandbox, so
  1657       // we serialize everything through JSON.
  1658       sandbox._hr = JSON.stringify(yield this._policy.healthReportPayload());
  1659       Object.defineProperty(sandbox, "_t",
  1660         { get: () => JSON.stringify(this._policy.telemetryPayload()) });
  1662       let result = false;
  1663       try {
  1664         result = !!Cu.evalInSandbox("filter({healthReportPayload: JSON.parse(_hr), telemetryPayload: JSON.parse(_t)})", sandbox);
  1666       catch (e) {
  1667         this._log.debug("runFilterFunction() - filter function failed: "
  1668                       + e.message + ", " + e.stack);
  1669         throw ["jsfilter-threw", e.message];
  1671       finally {
  1672         Cu.nukeSandbox(sandbox);
  1675       if (!result) {
  1676         throw ["jsfilter-false"];
  1679       throw new Task.Result(true);
  1680     }.bind(this));
  1681   },
  1683   /*
  1684    * Start running the experiment.
  1686    * @return Promise<> Resolved when the operation is complete.
  1687    */
  1688   start: Task.async(function* () {
  1689     this._log.trace("start() for " + this.id);
  1691     this._enabled = true;
  1692     return yield this.reconcileAddonState();
  1693   }),
  1695   // Async install of the addon for this experiment, part of the start task above.
  1696   _installAddon: Task.async(function* () {
  1697     let deferred = Promise.defer();
  1699     let hash = this._policy.ignoreHashes ? null : this._manifestData.xpiHash;
  1701     let install = yield addonInstallForURL(this._manifestData.xpiURL, hash);
  1702     gActiveInstallURLs.add(install.sourceURI.spec);
  1704     let failureHandler = (install, handler) => {
  1705       let message = "AddonInstall " + handler + " for " + this.id + ", state=" +
  1706                    (install.state || "?") + ", error=" + install.error;
  1707       this._log.error("_installAddon() - " + message);
  1708       this._failedStart = true;
  1709       gActiveInstallURLs.delete(install.sourceURI.spec);
  1711       TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
  1712                       [TELEMETRY_LOG.ACTIVATION.INSTALL_FAILURE, this.id]);
  1714       deferred.reject(new Error(message));
  1715     };
  1717     let listener = {
  1718       _expectedID: null,
  1720       onDownloadEnded: install => {
  1721         this._log.trace("_installAddon() - onDownloadEnded for " + this.id);
  1723         if (install.existingAddon) {
  1724           this._log.warn("_installAddon() - onDownloadEnded, addon already installed");
  1727         if (install.addon.type !== "experiment") {
  1728           this._log.error("_installAddon() - onDownloadEnded, wrong addon type");
  1729           install.cancel();
  1731       },
  1733       onInstallStarted: install => {
  1734         this._log.trace("_installAddon() - onInstallStarted for " + this.id);
  1736         if (install.existingAddon) {
  1737           this._log.warn("_installAddon() - onInstallStarted, addon already installed");
  1740         if (install.addon.type !== "experiment") {
  1741           this._log.error("_installAddon() - onInstallStarted, wrong addon type");
  1742           return false;
  1744       },
  1746       onInstallEnded: install => {
  1747         this._log.trace("_installAddon() - install ended for " + this.id);
  1748         gActiveInstallURLs.delete(install.sourceURI.spec);
  1750         this._lastChangedDate = this._policy.now();
  1751         this._startDate = this._policy.now();
  1752         this._enabled = true;
  1754         TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
  1755                        [TELEMETRY_LOG.ACTIVATION.ACTIVATED, this.id]);
  1757         let addon = install.addon;
  1758         this._name = addon.name;
  1759         this._addonId = addon.id;
  1760         this._description = addon.description || "";
  1761         this._homepageURL = addon.homepageURL || "";
  1763         // Experiment add-ons default to userDisabled=true. Enable if needed.
  1764         if (addon.userDisabled) {
  1765           this._log.trace("Add-on is disabled. Enabling.");
  1766           listener._expectedID = addon.id;
  1767           AddonManager.addAddonListener(listener);
  1768           addon.userDisabled = false;
  1769         } else {
  1770           this._log.trace("Add-on is enabled. start() completed.");
  1771           deferred.resolve();
  1773       },
  1775       onEnabled: addon => {
  1776         this._log.info("onEnabled() for " + addon.id);
  1778         if (addon.id != listener._expectedID) {
  1779           return;
  1782         AddonManager.removeAddonListener(listener);
  1783         deferred.resolve();
  1784       },
  1785     };
  1787     ["onDownloadCancelled", "onDownloadFailed", "onInstallCancelled", "onInstallFailed"]
  1788       .forEach(what => {
  1789         listener[what] = install => failureHandler(install, what)
  1790       });
  1792     install.addListener(listener);
  1793     install.install();
  1795     return yield deferred.promise;
  1796   }),
  1798   /**
  1799    * Stop running the experiment if it is active.
  1801    * @param terminationKind (optional)
  1802    *        The termination kind, e.g. ADDON_UNINSTALLED or EXPIRED.
  1803    * @param terminationReason (optional)
  1804    *        The termination reason details for termination kind RECHECK.
  1805    * @return Promise<> Resolved when the operation is complete.
  1806    */
  1807   stop: Task.async(function* (terminationKind, terminationReason) {
  1808     this._log.trace("stop() - id=" + this.id + ", terminationKind=" + terminationKind);
  1809     if (!this._enabled) {
  1810       throw new Error("Must not call stop() on an inactive experiment.");
  1813     this._enabled = false;
  1814     let now = this._policy.now();
  1815     this._lastChangedDate = now;
  1816     this._endDate = now;
  1818     let changes = yield this.reconcileAddonState();
  1819     this._logTermination(terminationKind, terminationReason);
  1821     if (terminationKind == TELEMETRY_LOG.TERMINATION.ADDON_UNINSTALLED) {
  1822       changes |= this.ADDON_CHANGE_UNINSTALL;
  1825     return changes;
  1826   }),
  1828   /**
  1829    * Reconcile the state of the add-on against what it's supposed to be.
  1831    * If we are active, ensure the add-on is enabled and up to date.
  1833    * If we are inactive, ensure the add-on is not installed.
  1834    */
  1835   reconcileAddonState: Task.async(function* () {
  1836     this._log.trace("reconcileAddonState()");
  1838     if (!this._enabled) {
  1839       if (!this._addonId) {
  1840         this._log.trace("reconcileAddonState() - Experiment is not enabled and " +
  1841                         "has no add-on. Doing nothing.");
  1842         return this.ADDON_CHANGE_NONE;
  1845       let addon = yield this._getAddon();
  1846       if (!addon) {
  1847         this._log.trace("reconcileAddonState() - Inactive experiment has no " +
  1848                         "add-on. Doing nothing.");
  1849         return this.ADDON_CHANGE_NONE;
  1852       this._log.info("reconcileAddonState() - Uninstalling add-on for inactive " +
  1853                      "experiment: " + addon.id);
  1854       gActiveUninstallAddonIDs.add(addon.id);
  1855       yield uninstallAddons([addon]);
  1856       gActiveUninstallAddonIDs.delete(addon.id);
  1857       return this.ADDON_CHANGE_UNINSTALL;
  1860     // If we get here, we're supposed to be active.
  1862     let changes = 0;
  1864     // That requires an add-on.
  1865     let currentAddon = yield this._getAddon();
  1867     // If we have an add-on but it isn't up to date, uninstall it
  1868     // (to prepare for reinstall).
  1869     if (currentAddon && this._needsUpdate) {
  1870       this._log.info("reconcileAddonState() - Uninstalling add-on because update " +
  1871                      "needed: " + currentAddon.id);
  1872       gActiveUninstallAddonIDs.add(currentAddon.id);
  1873       yield uninstallAddons([currentAddon]);
  1874       gActiveUninstallAddonIDs.delete(currentAddon.id);
  1875       changes |= this.ADDON_CHANGE_UNINSTALL;
  1878     if (!currentAddon || this._needsUpdate) {
  1879       this._log.info("reconcileAddonState() - Installing add-on.");
  1880       yield this._installAddon();
  1881       changes |= this.ADDON_CHANGE_INSTALL;
  1884     let addon = yield this._getAddon();
  1885     if (!addon) {
  1886       throw new Error("Could not obtain add-on for experiment that should be " +
  1887                       "enabled.");
  1890     // If we have the add-on and it is enabled, we are done.
  1891     if (!addon.userDisabled) {
  1892       return changes;
  1895     let deferred = Promise.defer();
  1897     // Else we need to enable it.
  1898     let listener = {
  1899       onEnabled: enabledAddon => {
  1900         if (enabledAddon.id != addon.id) {
  1901           return;
  1904         AddonManager.removeAddonListener(listener);
  1905         deferred.resolve();
  1906       },
  1907     };
  1909     this._log.info("Activating add-on: " + addon.id);
  1910     AddonManager.addAddonListener(listener);
  1911     addon.userDisabled = false;
  1912     yield deferred.promise;
  1913     changes |= this.ADDON_CHANGE_ENABLE;
  1915     this._log.info("Add-on has been enabled: " + addon.id);
  1916     return changes;
  1917    }),
  1919   /**
  1920    * Obtain the underlying Addon from the Addon Manager.
  1922    * @return Promise<Addon|null>
  1923    */
  1924   _getAddon: function () {
  1925     if (!this._addonId) {
  1926       return Promise.resolve(null);
  1929     let deferred = Promise.defer();
  1931     AddonManager.getAddonByID(this._addonId, (addon) => {
  1932       if (addon && addon.appDisabled) {
  1933         // Don't return PreviousExperiments.
  1934         addon = null;
  1937       deferred.resolve(addon);
  1938     });
  1940     return deferred.promise;
  1941   },
  1943   _logTermination: function (terminationKind, terminationReason) {
  1944     if (terminationKind === undefined) {
  1945       return;
  1948     if (!(terminationKind in TELEMETRY_LOG.TERMINATION)) {
  1949       this._log.warn("stop() - unknown terminationKind " + terminationKind);
  1950       return;
  1953     let data = [terminationKind, this.id];
  1954     if (terminationReason) {
  1955       data = data.concat(terminationReason);
  1958     TelemetryLog.log(TELEMETRY_LOG.TERMINATION_KEY, data);
  1959   },
  1961   /**
  1962    * Determine whether an active experiment should be stopped.
  1963    */
  1964   shouldStop: function () {
  1965     if (!this._enabled) {
  1966       throw new Error("shouldStop must not be called on disabled experiments.");
  1969     let data = this._manifestData;
  1970     let now = this._policy.now() / 1000; // The manifest times are in seconds.
  1971     let maxActiveSec = data.maxActiveSeconds || 0;
  1973     let deferred = Promise.defer();
  1974     this.isApplicable().then(
  1975       () => deferred.resolve({shouldStop: false}),
  1976       reason => deferred.resolve({shouldStop: true, reason: reason})
  1977     );
  1979     return deferred.promise;
  1980   },
  1982   /*
  1983    * Should this be discarded from the cache due to age?
  1984    */
  1985   shouldDiscard: function () {
  1986     let limit = this._policy.now();
  1987     limit.setDate(limit.getDate() - KEEP_HISTORY_N_DAYS);
  1988     return (this._lastChangedDate < limit);
  1989   },
  1991   /*
  1992    * Get next date (in epoch-ms) to schedule a re-evaluation for this.
  1993    * Returns 0 if it doesn't need one.
  1994    */
  1995   getScheduleTime: function () {
  1996     if (this._enabled) {
  1997       let now = this._policy.now();
  1998       let startTime = this._startDate.getTime();
  1999       let maxActiveTime = startTime + 1000 * this._manifestData.maxActiveSeconds;
  2000       return Math.min(1000 * this._manifestData.endTime,  maxActiveTime);
  2003     if (this._endDate) {
  2004       return this._endDate.getTime();
  2007     return 1000 * this._manifestData.startTime;
  2008   },
  2010   /*
  2011    * Perform sanity checks on the experiment data.
  2012    */
  2013   _isManifestDataValid: function (data) {
  2014     this._log.trace("isManifestDataValid() - data: " + JSON.stringify(data));
  2016     for (let key of this.MANIFEST_REQUIRED_FIELDS) {
  2017       if (!(key in data)) {
  2018         this._log.error("isManifestDataValid() - missing required key: " + key);
  2019         return false;
  2023     for (let key in data) {
  2024       if (!this.MANIFEST_OPTIONAL_FIELDS.has(key) &&
  2025           !this.MANIFEST_REQUIRED_FIELDS.has(key)) {
  2026         this._log.error("isManifestDataValid() - unknown key: " + key);
  2027         return false;
  2031     return true;
  2032   },
  2033 };
  2037 /**
  2038  * Strip a Date down to its UTC midnight.
  2040  * This will return a cloned Date object. The original is unchanged.
  2041  */
  2042 let stripDateToMidnight = function (d) {
  2043   let m = new Date(d);
  2044   m.setUTCHours(0, 0, 0, 0);
  2046   return m;
  2047 };
  2049 function ExperimentsLastActiveMeasurement1() {
  2050   Metrics.Measurement.call(this);
  2052 function ExperimentsLastActiveMeasurement2() {
  2053   Metrics.Measurement.call(this);
  2056 const FIELD_DAILY_LAST_TEXT = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
  2058 ExperimentsLastActiveMeasurement1.prototype = Object.freeze({
  2059   __proto__: Metrics.Measurement.prototype,
  2061   name: "info",
  2062   version: 1,
  2064   fields: {
  2065     lastActive: FIELD_DAILY_LAST_TEXT,
  2067 });
  2068 ExperimentsLastActiveMeasurement2.prototype = Object.freeze({
  2069   __proto__: Metrics.Measurement.prototype,
  2071   name: "info",
  2072   version: 2,
  2074   fields: {
  2075     lastActive: FIELD_DAILY_LAST_TEXT,
  2076     lastActiveBranch: FIELD_DAILY_LAST_TEXT,
  2078 });
  2080 this.ExperimentsProvider = function () {
  2081   Metrics.Provider.call(this);
  2083   this._experiments = null;
  2084 };
  2086 ExperimentsProvider.prototype = Object.freeze({
  2087   __proto__: Metrics.Provider.prototype,
  2089   name: "org.mozilla.experiments",
  2091   measurementTypes: [
  2092     ExperimentsLastActiveMeasurement1,
  2093     ExperimentsLastActiveMeasurement2,
  2094   ],
  2096   _OBSERVERS: [
  2097     EXPERIMENTS_CHANGED_TOPIC,
  2098   ],
  2100   postInit: function () {
  2101     for (let o of this._OBSERVERS) {
  2102       Services.obs.addObserver(this, o, false);
  2105     return Promise.resolve();
  2106   },
  2108   onShutdown: function () {
  2109     for (let o of this._OBSERVERS) {
  2110       Services.obs.removeObserver(this, o);
  2113     return Promise.resolve();
  2114   },
  2116   observe: function (subject, topic, data) {
  2117     switch (topic) {
  2118       case EXPERIMENTS_CHANGED_TOPIC:
  2119         this.recordLastActiveExperiment();
  2120         break;
  2122   },
  2124   collectDailyData: function () {
  2125     return this.recordLastActiveExperiment();
  2126   },
  2128   recordLastActiveExperiment: function () {
  2129     if (!gExperimentsEnabled) {
  2130       return Promise.resolve();
  2133     if (!this._experiments) {
  2134       this._experiments = Experiments.instance();
  2137     let m = this.getMeasurement(ExperimentsLastActiveMeasurement2.prototype.name,
  2138                                 ExperimentsLastActiveMeasurement2.prototype.version);
  2140     return this.enqueueStorageOperation(() => {
  2141       return Task.spawn(function* recordTask() {
  2142         let todayActive = yield this._experiments.lastActiveToday();
  2143         if (!todayActive) {
  2144           this._log.info("No active experiment on this day: " +
  2145                          this._experiments._policy.now());
  2146           return;
  2149         this._log.info("Recording last active experiment: " + todayActive.id);
  2150         yield m.setDailyLastText("lastActive", todayActive.id,
  2151                                  this._experiments._policy.now());
  2152         let branch = todayActive.branch;
  2153         if (branch) {
  2154           yield m.setDailyLastText("lastActiveBranch", branch,
  2155                                    this._experiments._policy.now());
  2157       }.bind(this));
  2158     });
  2159   },
  2160 });
  2162 /**
  2163  * An Add-ons Manager provider that knows about old experiments.
  2165  * This provider exposes read-only add-ons corresponding to previously-active
  2166  * experiments. The existence of this provider (and the add-ons it knows about)
  2167  * facilitates the display of old experiments in the Add-ons Manager UI with
  2168  * very little custom code in that component.
  2169  */
  2170 this.Experiments.PreviousExperimentProvider = function (experiments) {
  2171   this._experiments = experiments;
  2172   this._experimentList = [];
  2173   this._log = Log.repository.getLoggerWithMessagePrefix(
  2174     "Browser.Experiments.Experiments",
  2175     "PreviousExperimentProvider #" + gPreviousProviderCounter++ + "::");
  2178 this.Experiments.PreviousExperimentProvider.prototype = Object.freeze({
  2179   startup: function () {
  2180     this._log.trace("startup()");
  2181     Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
  2182   },
  2184   shutdown: function () {
  2185     this._log.trace("shutdown()");
  2186     Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
  2187   },
  2189   observe: function (subject, topic, data) {
  2190     switch (topic) {
  2191       case EXPERIMENTS_CHANGED_TOPIC:
  2192         this._updateExperimentList();
  2193         break;
  2195   },
  2197   getAddonByID: function (id, cb) {
  2198     for (let experiment of this._experimentList) {
  2199       if (experiment.id == id) {
  2200         cb(new PreviousExperimentAddon(experiment));
  2201         return;
  2205     cb(null);
  2206   },
  2208   getAddonsByTypes: function (types, cb) {
  2209     if (types && types.length > 0 && types.indexOf("experiment") == -1) {
  2210       cb([]);
  2211       return;
  2214     cb([new PreviousExperimentAddon(e) for (e of this._experimentList)]);
  2215   },
  2217   _updateExperimentList: function () {
  2218     return this._experiments.getExperiments().then((experiments) => {
  2219       let list = [e for (e of experiments) if (!e.active)];
  2221       let newMap = new Map([[e.id, e] for (e of list)]);
  2222       let oldMap = new Map([[e.id, e] for (e of this._experimentList)]);
  2224       let added = [e.id for (e of list) if (!oldMap.has(e.id))];
  2225       let removed = [e.id for (e of this._experimentList) if (!newMap.has(e.id))];
  2227       for (let id of added) {
  2228         this._log.trace("updateExperimentList() - adding " + id);
  2229         let wrapper = new PreviousExperimentAddon(newMap.get(id));
  2230         AddonManagerPrivate.callInstallListeners("onExternalInstall", null, wrapper, null, false);
  2231         AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false);
  2234       for (let id of removed) {
  2235         this._log.trace("updateExperimentList() - removing " + id);
  2236         let wrapper = new PreviousExperimentAddon(oldMap.get(id));
  2237         AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false);
  2240       this._experimentList = list;
  2242       for (let id of added) {
  2243         let wrapper = new PreviousExperimentAddon(newMap.get(id));
  2244         AddonManagerPrivate.callAddonListeners("onInstalled", wrapper);
  2247       for (let id of removed) {
  2248         let wrapper = new PreviousExperimentAddon(oldMap.get(id));
  2249         AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
  2252       return this._experimentList;
  2253     });
  2254   },
  2255 });
  2257 /**
  2258  * An add-on that represents a previously-installed experiment.
  2259  */
  2260 function PreviousExperimentAddon(experiment) {
  2261   this._id = experiment.id;
  2262   this._name = experiment.name;
  2263   this._endDate = experiment.endDate;
  2264   this._description = experiment.description;
  2267 PreviousExperimentAddon.prototype = Object.freeze({
  2268   // BEGIN REQUIRED ADDON PROPERTIES
  2270   get appDisabled() {
  2271     return true;
  2272   },
  2274   get blocklistState() {
  2275     Ci.nsIBlocklistService.STATE_NOT_BLOCKED
  2276   },
  2278   get creator() {
  2279     return new AddonManagerPrivate.AddonAuthor("");
  2280   },
  2282   get foreignInstall() {
  2283     return false;
  2284   },
  2286   get id() {
  2287     return this._id;
  2288   },
  2290   get isActive() {
  2291     return false;
  2292   },
  2294   get isCompatible() {
  2295     return true;
  2296   },
  2298   get isPlatformCompatible() {
  2299     return true;
  2300   },
  2302   get name() {
  2303     return this._name;
  2304   },
  2306   get pendingOperations() {
  2307     return AddonManager.PENDING_NONE;
  2308   },
  2310   get permissions() {
  2311     return 0;
  2312   },
  2314   get providesUpdatesSecurely() {
  2315     return true;
  2316   },
  2318   get scope() {
  2319     return AddonManager.SCOPE_PROFILE;
  2320   },
  2322   get type() {
  2323     return "experiment";
  2324   },
  2326   get userDisabled() {
  2327     return true;
  2328   },
  2330   get version() {
  2331     return null;
  2332   },
  2334   // END REQUIRED PROPERTIES
  2336   // BEGIN OPTIONAL PROPERTIES
  2338   get description() {
  2339     return this._description;
  2340   },
  2342   get updateDate() {
  2343     return new Date(this._endDate);
  2344   },
  2346   // END OPTIONAL PROPERTIES
  2348   // BEGIN REQUIRED METHODS
  2350   isCompatibleWith: function (appVersion, platformVersion) {
  2351     return true;
  2352   },
  2354   findUpdates: function (listener, reason, appVersion, platformVersion) {
  2355     AddonManagerPrivate.callNoUpdateListeners(this, listener, reason,
  2356                                               appVersion, platformVersion);
  2357   },
  2359   // END REQUIRED METHODS
  2361   /**
  2362    * The end-date of the experiment, required for the Addon Manager UI.
  2363    */
  2365    get endDate() {
  2366      return this._endDate;
  2367    },
  2369 });

mercurial