browser/experiments/Experiments.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/experiments/Experiments.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,2369 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = [
    1.11 +  "Experiments",
    1.12 +  "ExperimentsProvider",
    1.13 +];
    1.14 +
    1.15 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.16 +
    1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.18 +Cu.import("resource://gre/modules/Services.jsm");
    1.19 +Cu.import("resource://gre/modules/Task.jsm");
    1.20 +Cu.import("resource://gre/modules/Promise.jsm");
    1.21 +Cu.import("resource://gre/modules/osfile.jsm");
    1.22 +Cu.import("resource://gre/modules/Log.jsm");
    1.23 +Cu.import("resource://gre/modules/Preferences.jsm");
    1.24 +Cu.import("resource://gre/modules/AsyncShutdown.jsm");
    1.25 +
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
    1.27 +                                  "resource://gre/modules/UpdateChannel.jsm");
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
    1.29 +                                  "resource://gre/modules/AddonManager.jsm");
    1.30 +XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
    1.31 +                                  "resource://gre/modules/AddonManager.jsm");
    1.32 +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryPing",
    1.33 +                                  "resource://gre/modules/TelemetryPing.jsm");
    1.34 +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryLog",
    1.35 +                                  "resource://gre/modules/TelemetryLog.jsm");
    1.36 +XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
    1.37 +                                  "resource://services-common/utils.js");
    1.38 +XPCOMUtils.defineLazyModuleGetter(this, "Metrics",
    1.39 +                                  "resource://gre/modules/Metrics.jsm");
    1.40 +
    1.41 +// CertUtils.jsm doesn't expose a single "CertUtils" object like a normal .jsm
    1.42 +// would.
    1.43 +XPCOMUtils.defineLazyGetter(this, "CertUtils",
    1.44 +  function() {
    1.45 +    var mod = {};
    1.46 +    Cu.import("resource://gre/modules/CertUtils.jsm", mod);
    1.47 +    return mod;
    1.48 +  });
    1.49 +
    1.50 +XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
    1.51 +                                   "@mozilla.org/xre/app-info;1",
    1.52 +                                   "nsICrashReporter");
    1.53 +
    1.54 +const FILE_CACHE                = "experiments.json";
    1.55 +const EXPERIMENTS_CHANGED_TOPIC = "experiments-changed";
    1.56 +const MANIFEST_VERSION          = 1;
    1.57 +const CACHE_VERSION             = 1;
    1.58 +
    1.59 +const KEEP_HISTORY_N_DAYS       = 180;
    1.60 +const MIN_EXPERIMENT_ACTIVE_SECONDS = 60;
    1.61 +
    1.62 +const PREF_BRANCH               = "experiments.";
    1.63 +const PREF_ENABLED              = "enabled"; // experiments.enabled
    1.64 +const PREF_ACTIVE_EXPERIMENT    = "activeExperiment"; // whether we have an active experiment
    1.65 +const PREF_LOGGING              = "logging";
    1.66 +const PREF_LOGGING_LEVEL        = PREF_LOGGING + ".level"; // experiments.logging.level
    1.67 +const PREF_LOGGING_DUMP         = PREF_LOGGING + ".dump"; // experiments.logging.dump
    1.68 +const PREF_MANIFEST_URI         = "manifest.uri"; // experiments.logging.manifest.uri
    1.69 +const PREF_MANIFEST_CHECKCERT   = "manifest.cert.checkAttributes"; // experiments.manifest.cert.checkAttributes
    1.70 +const PREF_MANIFEST_REQUIREBUILTIN = "manifest.cert.requireBuiltin"; // experiments.manifest.cert.requireBuiltin
    1.71 +const PREF_FORCE_SAMPLE = "force-sample-value"; // experiments.force-sample-value
    1.72 +
    1.73 +const PREF_HEALTHREPORT_ENABLED = "datareporting.healthreport.service.enabled";
    1.74 +
    1.75 +const PREF_BRANCH_TELEMETRY     = "toolkit.telemetry.";
    1.76 +const PREF_TELEMETRY_ENABLED    = "enabled";
    1.77 +
    1.78 +const URI_EXTENSION_STRINGS     = "chrome://mozapps/locale/extensions/extensions.properties";
    1.79 +const STRING_TYPE_NAME          = "type.%ID%.name";
    1.80 +
    1.81 +const TELEMETRY_LOG = {
    1.82 +  // log(key, [kind, experimentId, details])
    1.83 +  ACTIVATION_KEY: "EXPERIMENT_ACTIVATION",
    1.84 +  ACTIVATION: {
    1.85 +    // Successfully activated.
    1.86 +    ACTIVATED: "ACTIVATED",
    1.87 +    // Failed to install the add-on.
    1.88 +    INSTALL_FAILURE: "INSTALL_FAILURE",
    1.89 +    // Experiment does not meet activation requirements. Details will
    1.90 +    // be provided.
    1.91 +    REJECTED: "REJECTED",
    1.92 +  },
    1.93 +
    1.94 +  // log(key, [kind, experimentId, optionalDetails...])
    1.95 +  TERMINATION_KEY: "EXPERIMENT_TERMINATION",
    1.96 +  TERMINATION: {
    1.97 +    // The Experiments service was disabled.
    1.98 +    SERVICE_DISABLED: "SERVICE_DISABLED",
    1.99 +    // Add-on uninstalled.
   1.100 +    ADDON_UNINSTALLED: "ADDON_UNINSTALLED",
   1.101 +    // The experiment disabled itself.
   1.102 +    FROM_API: "FROM_API",
   1.103 +    // The experiment expired (e.g. by exceeding the end date).
   1.104 +    EXPIRED: "EXPIRED",
   1.105 +    // Disabled after re-evaluating conditions. If this is specified,
   1.106 +    // details will be provided.
   1.107 +    RECHECK: "RECHECK",
   1.108 +  },
   1.109 +};
   1.110 +
   1.111 +const gPrefs = new Preferences(PREF_BRANCH);
   1.112 +const gPrefsTelemetry = new Preferences(PREF_BRANCH_TELEMETRY);
   1.113 +let gExperimentsEnabled = false;
   1.114 +let gAddonProvider = null;
   1.115 +let gExperiments = null;
   1.116 +let gLogAppenderDump = null;
   1.117 +let gPolicyCounter = 0;
   1.118 +let gExperimentsCounter = 0;
   1.119 +let gExperimentEntryCounter = 0;
   1.120 +let gPreviousProviderCounter = 0;
   1.121 +
   1.122 +// Tracks active AddonInstall we know about so we can deny external
   1.123 +// installs.
   1.124 +let gActiveInstallURLs = new Set();
   1.125 +
   1.126 +// Tracks add-on IDs that are being uninstalled by us. This allows us
   1.127 +// to differentiate between expected uninstalled and user-driven uninstalls.
   1.128 +let gActiveUninstallAddonIDs = new Set();
   1.129 +
   1.130 +let gLogger;
   1.131 +let gLogDumping = false;
   1.132 +
   1.133 +function configureLogging() {
   1.134 +  if (!gLogger) {
   1.135 +    gLogger = Log.repository.getLogger("Browser.Experiments");
   1.136 +    gLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
   1.137 +  }
   1.138 +  gLogger.level = gPrefs.get(PREF_LOGGING_LEVEL, Log.Level.Warn);
   1.139 +
   1.140 +  let logDumping = gPrefs.get(PREF_LOGGING_DUMP, false);
   1.141 +  if (logDumping != gLogDumping) {
   1.142 +    if (logDumping) {
   1.143 +      gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
   1.144 +      gLogger.addAppender(gLogAppenderDump);
   1.145 +    } else {
   1.146 +      gLogger.removeAppender(gLogAppenderDump);
   1.147 +      gLogAppenderDump = null;
   1.148 +    }
   1.149 +    gLogDumping = logDumping;
   1.150 +  }
   1.151 +}
   1.152 +
   1.153 +// Takes an array of promises and returns a promise that is resolved once all of
   1.154 +// them are rejected or resolved.
   1.155 +function allResolvedOrRejected(promises) {
   1.156 +  if (!promises.length) {
   1.157 +    return Promise.resolve([]);
   1.158 +  }
   1.159 +
   1.160 +  let countdown = promises.length;
   1.161 +  let deferred = Promise.defer();
   1.162 +
   1.163 +  for (let p of promises) {
   1.164 +    let helper = () => {
   1.165 +      if (--countdown == 0) {
   1.166 +        deferred.resolve();
   1.167 +      }
   1.168 +    };
   1.169 +    Promise.resolve(p).then(helper, helper);
   1.170 +  }
   1.171 +
   1.172 +  return deferred.promise;
   1.173 +}
   1.174 +
   1.175 +// Loads a JSON file using OS.file. file is a string representing the path
   1.176 +// of the file to be read, options contains additional options to pass to
   1.177 +// OS.File.read.
   1.178 +// Returns a Promise resolved with the json payload or rejected with
   1.179 +// OS.File.Error or JSON.parse() errors.
   1.180 +function loadJSONAsync(file, options) {
   1.181 +  return Task.spawn(function() {
   1.182 +    let rawData = yield OS.File.read(file, options);
   1.183 +    // Read json file into a string
   1.184 +    let data;
   1.185 +    try {
   1.186 +      // Obtain a converter to read from a UTF-8 encoded input stream.
   1.187 +      let converter = new TextDecoder();
   1.188 +      data = JSON.parse(converter.decode(rawData));
   1.189 +    } catch (ex) {
   1.190 +      gLogger.error("Experiments: Could not parse JSON: " + file + " " + ex);
   1.191 +      throw ex;
   1.192 +    }
   1.193 +    throw new Task.Result(data);
   1.194 +  });
   1.195 +}
   1.196 +
   1.197 +function telemetryEnabled() {
   1.198 +  return gPrefsTelemetry.get(PREF_TELEMETRY_ENABLED, false);
   1.199 +}
   1.200 +
   1.201 +// Returns a promise that is resolved with the AddonInstall for that URL.
   1.202 +function addonInstallForURL(url, hash) {
   1.203 +  let deferred = Promise.defer();
   1.204 +  AddonManager.getInstallForURL(url, install => deferred.resolve(install),
   1.205 +                                "application/x-xpinstall", hash);
   1.206 +  return deferred.promise;
   1.207 +}
   1.208 +
   1.209 +// Returns a promise that is resolved with an Array<Addon> of the installed
   1.210 +// experiment addons.
   1.211 +function installedExperimentAddons() {
   1.212 +  let deferred = Promise.defer();
   1.213 +  AddonManager.getAddonsByTypes(["experiment"], (addons) => {
   1.214 +    deferred.resolve([a for (a of addons) if (!a.appDisabled)]);
   1.215 +  });
   1.216 +  return deferred.promise;
   1.217 +}
   1.218 +
   1.219 +// Takes an Array<Addon> and returns a promise that is resolved when the
   1.220 +// addons are uninstalled.
   1.221 +function uninstallAddons(addons) {
   1.222 +  let ids = new Set([a.id for (a of addons)]);
   1.223 +  let deferred = Promise.defer();
   1.224 +
   1.225 +  let listener = {};
   1.226 +  listener.onUninstalled = addon => {
   1.227 +    if (!ids.has(addon.id)) {
   1.228 +      return;
   1.229 +    }
   1.230 +
   1.231 +    ids.delete(addon.id);
   1.232 +    if (ids.size == 0) {
   1.233 +      AddonManager.removeAddonListener(listener);
   1.234 +      deferred.resolve();
   1.235 +    }
   1.236 +  };
   1.237 +
   1.238 +  AddonManager.addAddonListener(listener);
   1.239 +
   1.240 +  for (let addon of addons) {
   1.241 +    // Disabling the add-on before uninstalling is necessary to cause tests to
   1.242 +    // pass. This might be indicative of a bug in XPIProvider.
   1.243 +    // TODO follow up in bug 992396.
   1.244 +    addon.userDisabled = true;
   1.245 +    addon.uninstall();
   1.246 +  }
   1.247 +
   1.248 +  return deferred.promise;
   1.249 +}
   1.250 +
   1.251 +/**
   1.252 + * The experiments module.
   1.253 + */
   1.254 +
   1.255 +let Experiments = {
   1.256 +  /**
   1.257 +   * Provides access to the global `Experiments.Experiments` instance.
   1.258 +   */
   1.259 +  instance: function () {
   1.260 +    if (!gExperiments) {
   1.261 +      gExperiments = new Experiments.Experiments();
   1.262 +    }
   1.263 +
   1.264 +    return gExperiments;
   1.265 +  },
   1.266 +};
   1.267 +
   1.268 +/*
   1.269 + * The policy object allows us to inject fake enviroment data from the
   1.270 + * outside by monkey-patching.
   1.271 + */
   1.272 +
   1.273 +Experiments.Policy = function () {
   1.274 +  this._log = Log.repository.getLoggerWithMessagePrefix(
   1.275 +    "Browser.Experiments.Policy",
   1.276 +    "Policy #" + gPolicyCounter++ + "::");
   1.277 +
   1.278 +  // Set to true to ignore hash verification on downloaded XPIs. This should
   1.279 +  // not be used outside of testing.
   1.280 +  this.ignoreHashes = false;
   1.281 +};
   1.282 +
   1.283 +Experiments.Policy.prototype = {
   1.284 +  now: function () {
   1.285 +    return new Date();
   1.286 +  },
   1.287 +
   1.288 +  random: function () {
   1.289 +    let pref = gPrefs.get(PREF_FORCE_SAMPLE);
   1.290 +    if (pref !== undefined) {
   1.291 +      let val = Number.parseFloat(pref);
   1.292 +      this._log.debug("random sample forced: " + val);
   1.293 +      if (isNaN(val) || val < 0) {
   1.294 +        return 0;
   1.295 +      }
   1.296 +      if (val > 1) {
   1.297 +        return 1;
   1.298 +      }
   1.299 +      return val;
   1.300 +    }
   1.301 +    return Math.random();
   1.302 +  },
   1.303 +
   1.304 +  futureDate: function (offset) {
   1.305 +    return new Date(this.now().getTime() + offset);
   1.306 +  },
   1.307 +
   1.308 +  oneshotTimer: function (callback, timeout, thisObj, name) {
   1.309 +    return CommonUtils.namedTimer(callback, timeout, thisObj, name);
   1.310 +  },
   1.311 +
   1.312 +  updatechannel: function () {
   1.313 +    return UpdateChannel.get();
   1.314 +  },
   1.315 +
   1.316 +  locale: function () {
   1.317 +    let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
   1.318 +    return chrome.getSelectedLocale("global");
   1.319 +  },
   1.320 +
   1.321 +  /*
   1.322 +   * @return Promise<> Resolved with the payload data.
   1.323 +   */
   1.324 +  healthReportPayload: function () {
   1.325 +    return Task.spawn(function*() {
   1.326 +      let reporter = Cc["@mozilla.org/datareporting/service;1"]
   1.327 +            .getService(Ci.nsISupports)
   1.328 +            .wrappedJSObject
   1.329 +            .healthReporter;
   1.330 +      yield reporter.onInit();
   1.331 +      let payload = yield reporter.collectAndObtainJSONPayload();
   1.332 +      throw new Task.Result(payload);
   1.333 +    });
   1.334 +  },
   1.335 +
   1.336 +  telemetryPayload: function () {
   1.337 +    return TelemetryPing.getPayload();
   1.338 +  },
   1.339 +};
   1.340 +
   1.341 +function AlreadyShutdownError(message="already shut down") {
   1.342 +  this.name = "AlreadyShutdownError";
   1.343 +  this.message = message;
   1.344 +}
   1.345 +
   1.346 +AlreadyShutdownError.prototype = new Error();
   1.347 +AlreadyShutdownError.prototype.constructor = AlreadyShutdownError;
   1.348 +
   1.349 +/**
   1.350 + * Manages the experiments and provides an interface to control them.
   1.351 + */
   1.352 +
   1.353 +Experiments.Experiments = function (policy=new Experiments.Policy()) {
   1.354 +  this._log = Log.repository.getLoggerWithMessagePrefix(
   1.355 +    "Browser.Experiments.Experiments",
   1.356 +    "Experiments #" + gExperimentsCounter++ + "::");
   1.357 +  this._log.trace("constructor");
   1.358 +
   1.359 +  this._policy = policy;
   1.360 +
   1.361 +  // This is a Map of (string -> ExperimentEntry), keyed with the experiment id.
   1.362 +  // It holds both the current experiments and history.
   1.363 +  // Map() preserves insertion order, which means we preserve the manifest order.
   1.364 +  // This is null until we've successfully completed loading the cache from
   1.365 +  // disk the first time.
   1.366 +  this._experiments = null;
   1.367 +  this._refresh = false;
   1.368 +  this._terminateReason = null; // or TELEMETRY_LOG.TERMINATION....
   1.369 +  this._dirty = false;
   1.370 +
   1.371 +  // Loading the cache happens once asynchronously on startup
   1.372 +  this._loadTask = null;
   1.373 +
   1.374 +  // The _main task handles all other actions:
   1.375 +  // * refreshing the manifest off the network (if _refresh)
   1.376 +  // * disabling/enabling experiments
   1.377 +  // * saving the cache (if _dirty)
   1.378 +  this._mainTask = null;
   1.379 +
   1.380 +  // Timer for re-evaluating experiment status.
   1.381 +  this._timer = null;
   1.382 +
   1.383 +  this._shutdown = false;
   1.384 +
   1.385 +  // We need to tell when we first evaluated the experiments to fire an
   1.386 +  // experiments-changed notification when we only loaded completed experiments.
   1.387 +  this._firstEvaluate = true;
   1.388 +
   1.389 +  this.init();
   1.390 +};
   1.391 +
   1.392 +Experiments.Experiments.prototype = {
   1.393 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsIObserver]),
   1.394 +
   1.395 +  init: function () {
   1.396 +    this._shutdown = false;
   1.397 +    configureLogging();
   1.398 +
   1.399 +    gExperimentsEnabled = gPrefs.get(PREF_ENABLED, false);
   1.400 +    this._log.trace("enabled=" + gExperimentsEnabled + ", " + this.enabled);
   1.401 +
   1.402 +    gPrefs.observe(PREF_LOGGING, configureLogging);
   1.403 +    gPrefs.observe(PREF_MANIFEST_URI, this.updateManifest, this);
   1.404 +    gPrefs.observe(PREF_ENABLED, this._toggleExperimentsEnabled, this);
   1.405 +
   1.406 +    gPrefsTelemetry.observe(PREF_TELEMETRY_ENABLED, this._telemetryStatusChanged, this);
   1.407 +
   1.408 +    AsyncShutdown.profileBeforeChange.addBlocker("Experiments.jsm shutdown",
   1.409 +      this.uninit.bind(this));
   1.410 +
   1.411 +    this._registerWithAddonManager();
   1.412 +
   1.413 +    let deferred = Promise.defer();
   1.414 +
   1.415 +    this._loadTask = this._loadFromCache();
   1.416 +    this._loadTask.then(
   1.417 +      () => {
   1.418 +        this._log.trace("_loadTask finished ok");
   1.419 +        this._loadTask = null;
   1.420 +        this._run().then(deferred.resolve, deferred.reject);
   1.421 +      },
   1.422 +      (e) => {
   1.423 +        this._log.error("_loadFromCache caught error: " + e);
   1.424 +        deferred.reject(e);
   1.425 +      }
   1.426 +    );
   1.427 +
   1.428 +    return deferred.promise;
   1.429 +  },
   1.430 +
   1.431 +  /**
   1.432 +   * Uninitialize this instance.
   1.433 +   *
   1.434 +   * This function is susceptible to race conditions. If it is called multiple
   1.435 +   * times before the previous uninit() has completed or if it is called while
   1.436 +   * an init() operation is being performed, the object may get in bad state
   1.437 +   * and/or deadlock could occur.
   1.438 +   *
   1.439 +   * @return Promise<>
   1.440 +   *         The promise is fulfilled when all pending tasks are finished.
   1.441 +   */
   1.442 +  uninit: Task.async(function* () {
   1.443 +    this._log.trace("uninit: started");
   1.444 +    yield this._loadTask;
   1.445 +    this._log.trace("uninit: finished with _loadTask");
   1.446 +
   1.447 +    if (!this._shutdown) {
   1.448 +      this._log.trace("uninit: no previous shutdown");
   1.449 +      this._unregisterWithAddonManager();
   1.450 +
   1.451 +      gPrefs.ignore(PREF_LOGGING, configureLogging);
   1.452 +      gPrefs.ignore(PREF_MANIFEST_URI, this.updateManifest, this);
   1.453 +      gPrefs.ignore(PREF_ENABLED, this._toggleExperimentsEnabled, this);
   1.454 +
   1.455 +      gPrefsTelemetry.ignore(PREF_TELEMETRY_ENABLED, this._telemetryStatusChanged, this);
   1.456 +
   1.457 +      if (this._timer) {
   1.458 +        this._timer.clear();
   1.459 +      }
   1.460 +    }
   1.461 +
   1.462 +    this._shutdown = true;
   1.463 +    if (this._mainTask) {
   1.464 +      try {
   1.465 +        this._log.trace("uninit: waiting on _mainTask");
   1.466 +        yield this._mainTask;
   1.467 +      } catch (e if e instanceof AlreadyShutdownError) {
   1.468 +        // We error out of tasks after shutdown via that exception.
   1.469 +      }
   1.470 +    }
   1.471 +
   1.472 +    this._log.info("Completed uninitialization.");
   1.473 +  }),
   1.474 +
   1.475 +  _registerWithAddonManager: function (previousExperimentsProvider) {
   1.476 +    this._log.trace("Registering instance with Addon Manager.");
   1.477 +
   1.478 +    AddonManager.addAddonListener(this);
   1.479 +    AddonManager.addInstallListener(this);
   1.480 +
   1.481 +    if (!gAddonProvider) {
   1.482 +      // The properties of this AddonType should be kept in sync with the
   1.483 +      // experiment AddonType registered in XPIProvider.
   1.484 +      this._log.trace("Registering previous experiment add-on provider.");
   1.485 +      gAddonProvider = previousExperimentsProvider || new Experiments.PreviousExperimentProvider(this);
   1.486 +      AddonManagerPrivate.registerProvider(gAddonProvider, [
   1.487 +          new AddonManagerPrivate.AddonType("experiment",
   1.488 +                                            URI_EXTENSION_STRINGS,
   1.489 +                                            STRING_TYPE_NAME,
   1.490 +                                            AddonManager.VIEW_TYPE_LIST,
   1.491 +                                            11000,
   1.492 +                                            AddonManager.TYPE_UI_HIDE_EMPTY),
   1.493 +      ]);
   1.494 +    }
   1.495 +
   1.496 +  },
   1.497 +
   1.498 +  _unregisterWithAddonManager: function () {
   1.499 +    this._log.trace("Unregistering instance with Addon Manager.");
   1.500 +
   1.501 +    if (gAddonProvider) {
   1.502 +      this._log.trace("Unregistering previous experiment add-on provider.");
   1.503 +      AddonManagerPrivate.unregisterProvider(gAddonProvider);
   1.504 +      gAddonProvider = null;
   1.505 +    }
   1.506 +
   1.507 +    AddonManager.removeInstallListener(this);
   1.508 +    AddonManager.removeAddonListener(this);
   1.509 +  },
   1.510 +
   1.511 +  /*
   1.512 +   * Change the PreviousExperimentsProvider that this instance uses.
   1.513 +   * For testing only.
   1.514 +   */
   1.515 +  _setPreviousExperimentsProvider: function (provider) {
   1.516 +    this._unregisterWithAddonManager();
   1.517 +    this._registerWithAddonManager(provider);
   1.518 +  },
   1.519 +
   1.520 +  /**
   1.521 +   * Throws an exception if we've already shut down.
   1.522 +   */
   1.523 +  _checkForShutdown: function() {
   1.524 +    if (this._shutdown) {
   1.525 +      throw new AlreadyShutdownError("uninit() already called");
   1.526 +    }
   1.527 +  },
   1.528 +
   1.529 +  /**
   1.530 +   * Whether the experiments feature is enabled.
   1.531 +   */
   1.532 +  get enabled() {
   1.533 +    return gExperimentsEnabled;
   1.534 +  },
   1.535 +
   1.536 +  /**
   1.537 +   * Toggle whether the experiments feature is enabled or not.
   1.538 +   */
   1.539 +  set enabled(enabled) {
   1.540 +    this._log.trace("set enabled(" + enabled + ")");
   1.541 +    gPrefs.set(PREF_ENABLED, enabled);
   1.542 +  },
   1.543 +
   1.544 +  _toggleExperimentsEnabled: Task.async(function* (enabled) {
   1.545 +    this._log.trace("_toggleExperimentsEnabled(" + enabled + ")");
   1.546 +    let wasEnabled = gExperimentsEnabled;
   1.547 +    gExperimentsEnabled = enabled && telemetryEnabled();
   1.548 +
   1.549 +    if (wasEnabled == gExperimentsEnabled) {
   1.550 +      return;
   1.551 +    }
   1.552 +
   1.553 +    if (gExperimentsEnabled) {
   1.554 +      yield this.updateManifest();
   1.555 +    } else {
   1.556 +      yield this.disableExperiment(TELEMETRY_LOG.TERMINATION.SERVICE_DISABLED);
   1.557 +      if (this._timer) {
   1.558 +        this._timer.clear();
   1.559 +      }
   1.560 +    }
   1.561 +  }),
   1.562 +
   1.563 +  _telemetryStatusChanged: function () {
   1.564 +    this._toggleExperimentsEnabled(gExperimentsEnabled);
   1.565 +  },
   1.566 +
   1.567 +  /**
   1.568 +   * Returns a promise that is resolved with an array of `ExperimentInfo` objects,
   1.569 +   * which provide info on the currently and recently active experiments.
   1.570 +   * The array is in chronological order.
   1.571 +   *
   1.572 +   * The experiment info is of the form:
   1.573 +   * {
   1.574 +   *   id: <string>,
   1.575 +   *   name: <string>,
   1.576 +   *   description: <string>,
   1.577 +   *   active: <boolean>,
   1.578 +   *   endDate: <integer>, // epoch ms
   1.579 +   *   detailURL: <string>,
   1.580 +   *   ... // possibly extended later
   1.581 +   * }
   1.582 +   *
   1.583 +   * @return Promise<Array<ExperimentInfo>> Array of experiment info objects.
   1.584 +   */
   1.585 +  getExperiments: function () {
   1.586 +    return Task.spawn(function*() {
   1.587 +      yield this._loadTask;
   1.588 +      let list = [];
   1.589 +
   1.590 +      for (let [id, experiment] of this._experiments) {
   1.591 +        if (!experiment.startDate) {
   1.592 +          // We only collect experiments that are or were active.
   1.593 +          continue;
   1.594 +        }
   1.595 +
   1.596 +        list.push({
   1.597 +          id: id,
   1.598 +          name: experiment._name,
   1.599 +          description: experiment._description,
   1.600 +          active: experiment.enabled,
   1.601 +          endDate: experiment.endDate.getTime(),
   1.602 +          detailURL: experiment._homepageURL,
   1.603 +	  branch: experiment.branch,
   1.604 +        });
   1.605 +      }
   1.606 +
   1.607 +      // Sort chronologically, descending.
   1.608 +      list.sort((a, b) => b.endDate - a.endDate);
   1.609 +      return list;
   1.610 +    }.bind(this));
   1.611 +  },
   1.612 +
   1.613 +  /**
   1.614 +   * Returns the ExperimentInfo for the active experiment, or null
   1.615 +   * if there is none.
   1.616 +   */
   1.617 +  getActiveExperiment: function () {
   1.618 +    let experiment = this._getActiveExperiment();
   1.619 +    if (!experiment) {
   1.620 +      return null;
   1.621 +    }
   1.622 +
   1.623 +    let info = {
   1.624 +      id: experiment.id,
   1.625 +      name: experiment._name,
   1.626 +      description: experiment._description,
   1.627 +      active: experiment.enabled,
   1.628 +      endDate: experiment.endDate.getTime(),
   1.629 +      detailURL: experiment._homepageURL,
   1.630 +    };
   1.631 +
   1.632 +    return info;
   1.633 +  },
   1.634 +
   1.635 +  /**
   1.636 +   * Experiment "branch" support. If an experiment has multiple branches, it
   1.637 +   * can record the branch with the experiment system and it will
   1.638 +   * automatically be included in data reporting (FHR/telemetry payloads).
   1.639 +   */
   1.640 +
   1.641 +  /**
   1.642 +   * Set the experiment branch for the specified experiment ID.
   1.643 +   * @returns Promise<>
   1.644 +   */
   1.645 +  setExperimentBranch: Task.async(function*(id, branchstr) {
   1.646 +    yield this._loadTask;
   1.647 +    let e = this._experiments.get(id);
   1.648 +    if (!e) {
   1.649 +      throw new Error("Experiment not found");
   1.650 +    }
   1.651 +    e.branch = String(branchstr);
   1.652 +    this._dirty = true;
   1.653 +    Services.obs.notifyObservers(null, EXPERIMENTS_CHANGED_TOPIC, null);
   1.654 +    yield this._run();
   1.655 +  }),
   1.656 +  /**
   1.657 +   * Get the branch of the specified experiment. If the experiment is unknown,
   1.658 +   * throws an error.
   1.659 +   *
   1.660 +   * @param id The ID of the experiment. Pass null for the currently running
   1.661 +   *           experiment.
   1.662 +   * @returns Promise<string|null>
   1.663 +   * @throws Error if the specified experiment ID is unknown, or if there is no
   1.664 +   *         current experiment.
   1.665 +   */
   1.666 +  getExperimentBranch: Task.async(function*(id=null) {
   1.667 +    yield this._loadTask;
   1.668 +    let e;
   1.669 +    if (id) {
   1.670 +      e = this._experiments.get(id);
   1.671 +      if (!e) {
   1.672 +        throw new Error("Experiment not found");
   1.673 +      }
   1.674 +    } else {
   1.675 +      e = this._getActiveExperiment();
   1.676 +      if (e === null) {
   1.677 +	throw new Error("No active experiment");
   1.678 +      }
   1.679 +    }
   1.680 +    return e.branch;
   1.681 +  }),
   1.682 +
   1.683 +  /**
   1.684 +   * Determine whether another date has the same UTC day as now().
   1.685 +   */
   1.686 +  _dateIsTodayUTC: function (d) {
   1.687 +    let now = this._policy.now();
   1.688 +
   1.689 +    return stripDateToMidnight(now).getTime() == stripDateToMidnight(d).getTime();
   1.690 +  },
   1.691 +
   1.692 +  /**
   1.693 +   * Obtain the entry of the most recent active experiment that was active
   1.694 +   * today.
   1.695 +   *
   1.696 +   * If no experiment was active today, this resolves to nothing.
   1.697 +   *
   1.698 +   * Assumption: Only a single experiment can be active at a time.
   1.699 +   *
   1.700 +   * @return Promise<object>
   1.701 +   */
   1.702 +  lastActiveToday: function () {
   1.703 +    return Task.spawn(function* getMostRecentActiveExperimentTask() {
   1.704 +      let experiments = yield this.getExperiments();
   1.705 +
   1.706 +      // Assumption: Ordered chronologically, descending, with active always
   1.707 +      // first.
   1.708 +      for (let experiment of experiments) {
   1.709 +        if (experiment.active) {
   1.710 +          return experiment;
   1.711 +        }
   1.712 +
   1.713 +        if (experiment.endDate && this._dateIsTodayUTC(experiment.endDate)) {
   1.714 +          return experiment;
   1.715 +        }
   1.716 +      }
   1.717 +      return null;
   1.718 +    }.bind(this));
   1.719 +  },
   1.720 +
   1.721 +  _run: function() {
   1.722 +    this._log.trace("_run");
   1.723 +    this._checkForShutdown();
   1.724 +    if (!this._mainTask) {
   1.725 +      this._mainTask = Task.spawn(this._main.bind(this));
   1.726 +      this._mainTask.then(
   1.727 +        () => {
   1.728 +          this._log.trace("_main finished, scheduling next run");
   1.729 +          this._mainTask = null;
   1.730 +          this._scheduleNextRun();
   1.731 +        },
   1.732 +        (e) => {
   1.733 +          this._log.error("_main caught error: " + e);
   1.734 +          this._mainTask = null;
   1.735 +        }
   1.736 +      );
   1.737 +    }
   1.738 +    return this._mainTask;
   1.739 +  },
   1.740 +
   1.741 +  _main: function*() {
   1.742 +    do {
   1.743 +      this._log.trace("_main iteration");
   1.744 +      yield this._loadTask;
   1.745 +      if (!gExperimentsEnabled) {
   1.746 +        this._refresh = false;
   1.747 +      }
   1.748 +
   1.749 +      if (this._refresh) {
   1.750 +        yield this._loadManifest();
   1.751 +      }
   1.752 +      yield this._evaluateExperiments();
   1.753 +      if (this._dirty) {
   1.754 +        yield this._saveToCache();
   1.755 +      }
   1.756 +      // If somebody called .updateManifest() or disableExperiment()
   1.757 +      // while we were running, go again right now.
   1.758 +    }
   1.759 +    while (this._refresh || this._terminateReason);
   1.760 +  },
   1.761 +
   1.762 +  _loadManifest: function*() {
   1.763 +    this._log.trace("_loadManifest");
   1.764 +    let uri = Services.urlFormatter.formatURLPref(PREF_BRANCH + PREF_MANIFEST_URI);
   1.765 +
   1.766 +    this._checkForShutdown();
   1.767 +
   1.768 +    this._refresh = false;
   1.769 +    try {
   1.770 +      let responseText = yield this._httpGetRequest(uri);
   1.771 +      this._log.trace("_loadManifest() - responseText=\"" + responseText + "\"");
   1.772 +
   1.773 +      if (this._shutdown) {
   1.774 +        return;
   1.775 +      }
   1.776 +
   1.777 +      let data = JSON.parse(responseText);
   1.778 +      this._updateExperiments(data);
   1.779 +    } catch (e) {
   1.780 +      this._log.error("_loadManifest - failure to fetch/parse manifest (continuing anyway): " + e);
   1.781 +    }
   1.782 +  },
   1.783 +
   1.784 +  /**
   1.785 +   * Fetch an updated list of experiments and trigger experiment updates.
   1.786 +   * Do only use when experiments are enabled.
   1.787 +   *
   1.788 +   * @return Promise<>
   1.789 +   *         The promise is resolved when the manifest and experiment list is updated.
   1.790 +   */
   1.791 +  updateManifest: function () {
   1.792 +    this._log.trace("updateManifest()");
   1.793 +
   1.794 +    if (!gExperimentsEnabled) {
   1.795 +      return Promise.reject(new Error("experiments are disabled"));
   1.796 +    }
   1.797 +
   1.798 +    if (this._shutdown) {
   1.799 +      return Promise.reject(Error("uninit() alrady called"));
   1.800 +    }
   1.801 +
   1.802 +    this._refresh = true;
   1.803 +    return this._run();
   1.804 +  },
   1.805 +
   1.806 +  notify: function (timer) {
   1.807 +    this._log.trace("notify()");
   1.808 +    this._checkForShutdown();
   1.809 +    return this._run();
   1.810 +  },
   1.811 +
   1.812 +  // START OF ADD-ON LISTENERS
   1.813 +
   1.814 +  onUninstalled: function (addon) {
   1.815 +    this._log.trace("onUninstalled() - addon id: " + addon.id);
   1.816 +    if (gActiveUninstallAddonIDs.has(addon.id)) {
   1.817 +      this._log.trace("matches pending uninstall");
   1.818 +      return;
   1.819 +    }
   1.820 +    let activeExperiment = this._getActiveExperiment();
   1.821 +    if (!activeExperiment || activeExperiment._addonId != addon.id) {
   1.822 +      return;
   1.823 +    }
   1.824 +
   1.825 +    this.disableExperiment(TELEMETRY_LOG.TERMINATION.ADDON_UNINSTALLED);
   1.826 +  },
   1.827 +
   1.828 +  onInstallStarted: function (install) {
   1.829 +    if (install.addon.type != "experiment") {
   1.830 +      return;
   1.831 +    }
   1.832 +
   1.833 +    this._log.trace("onInstallStarted() - " + install.addon.id);
   1.834 +    if (install.addon.appDisabled) {
   1.835 +      // This is a PreviousExperiment
   1.836 +      return;
   1.837 +    }
   1.838 +
   1.839 +    // We want to be in control of all experiment add-ons: reject installs
   1.840 +    // for add-ons that we don't know about.
   1.841 +
   1.842 +    // We have a race condition of sorts to worry about here. We have 2
   1.843 +    // onInstallStarted listeners. This one (the global one) and the one
   1.844 +    // created as part of ExperimentEntry._installAddon. Because of the order
   1.845 +    // they are registered in, this one likely executes first. Unfortunately,
   1.846 +    // this means that the add-on ID is not yet set on the ExperimentEntry.
   1.847 +    // So, we can't just look at this._trackedAddonIds because the new experiment
   1.848 +    // will have its add-on ID set to null. We work around this by storing a
   1.849 +    // identifying field - the source URL of the install - in a module-level
   1.850 +    // variable (so multiple Experiments instances doesn't cancel each other
   1.851 +    // out).
   1.852 +
   1.853 +    if (this._trackedAddonIds.has(install.addon.id)) {
   1.854 +      this._log.info("onInstallStarted allowing install because add-on ID " +
   1.855 +                     "tracked by us.");
   1.856 +      return;
   1.857 +    }
   1.858 +
   1.859 +    if (gActiveInstallURLs.has(install.sourceURI.spec)) {
   1.860 +      this._log.info("onInstallStarted allowing install because install " +
   1.861 +                     "tracked by us.");
   1.862 +      return;
   1.863 +    }
   1.864 +
   1.865 +    this._log.warn("onInstallStarted cancelling install of unknown " +
   1.866 +                   "experiment add-on: " + install.addon.id);
   1.867 +    return false;
   1.868 +  },
   1.869 +
   1.870 +  // END OF ADD-ON LISTENERS.
   1.871 +
   1.872 +  _getExperimentByAddonId: function (addonId) {
   1.873 +    for (let [, entry] of this._experiments) {
   1.874 +      if (entry._addonId === addonId) {
   1.875 +        return entry;
   1.876 +      }
   1.877 +    }
   1.878 +
   1.879 +    return null;
   1.880 +  },
   1.881 +
   1.882 +  /*
   1.883 +   * Helper function to make HTTP GET requests. Returns a promise that is resolved with
   1.884 +   * the responseText when the request is complete.
   1.885 +   */
   1.886 +  _httpGetRequest: function (url) {
   1.887 +    this._log.trace("httpGetRequest(" + url + ")");
   1.888 +    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
   1.889 +    try {
   1.890 +      xhr.open("GET", url);
   1.891 +    } catch (e) {
   1.892 +      this._log.error("httpGetRequest() - Error opening request to " + url + ": " + e);
   1.893 +      return Promise.reject(new Error("Experiments - Error opening XHR for " + url));
   1.894 +    }
   1.895 +
   1.896 +    let deferred = Promise.defer();
   1.897 +
   1.898 +    let log = this._log;
   1.899 +    xhr.onerror = function (e) {
   1.900 +      log.error("httpGetRequest::onError() - Error making request to " + url + ": " + e.error);
   1.901 +      deferred.reject(new Error("Experiments - XHR error for " + url + " - " + e.error));
   1.902 +    };
   1.903 +
   1.904 +    xhr.onload = function (event) {
   1.905 +      if (xhr.status !== 200 && xhr.state !== 0) {
   1.906 +        log.error("httpGetRequest::onLoad() - Request to " + url + " returned status " + xhr.status);
   1.907 +        deferred.reject(new Error("Experiments - XHR status for " + url + " is " + xhr.status));
   1.908 +        return;
   1.909 +      }
   1.910 +
   1.911 +      let certs = null;
   1.912 +      if (gPrefs.get(PREF_MANIFEST_CHECKCERT, true)) {
   1.913 +        certs = CertUtils.readCertPrefs(PREF_BRANCH + "manifest.certs.");
   1.914 +      }
   1.915 +      try {
   1.916 +        let allowNonBuiltin = !gPrefs.get(PREF_MANIFEST_REQUIREBUILTIN, true);
   1.917 +        CertUtils.checkCert(xhr.channel, allowNonBuiltin, certs);
   1.918 +      }
   1.919 +      catch (e) {
   1.920 +        log.error("manifest fetch failed certificate checks", [e]);
   1.921 +        deferred.reject(new Error("Experiments - manifest fetch failed certificate checks: " + e));
   1.922 +        return;
   1.923 +      }
   1.924 +
   1.925 +      deferred.resolve(xhr.responseText);
   1.926 +    };
   1.927 +
   1.928 +    if (xhr.channel instanceof Ci.nsISupportsPriority) {
   1.929 +      xhr.channel.priority = Ci.nsISupportsPriority.PRIORITY_LOWEST;
   1.930 +    }
   1.931 +
   1.932 +    xhr.send(null);
   1.933 +    return deferred.promise;
   1.934 +  },
   1.935 +
   1.936 +  /*
   1.937 +   * Path of the cache file we use in the profile.
   1.938 +   */
   1.939 +  get _cacheFilePath() {
   1.940 +    return OS.Path.join(OS.Constants.Path.profileDir, FILE_CACHE);
   1.941 +  },
   1.942 +
   1.943 +  /*
   1.944 +   * Part of the main task to save the cache to disk, called from _main.
   1.945 +   */
   1.946 +  _saveToCache: function* () {
   1.947 +    this._log.trace("_saveToCache");
   1.948 +    let path = this._cacheFilePath;
   1.949 +    let textData = JSON.stringify({
   1.950 +      version: CACHE_VERSION,
   1.951 +      data: [e[1].toJSON() for (e of this._experiments.entries())],
   1.952 +    });
   1.953 +
   1.954 +    let encoder = new TextEncoder();
   1.955 +    let data = encoder.encode(textData);
   1.956 +    let options = { tmpPath: path + ".tmp", compression: "lz4" };
   1.957 +    yield OS.File.writeAtomic(path, data, options);
   1.958 +    this._dirty = false;
   1.959 +    this._log.debug("_saveToCache saved to " + path);
   1.960 +  },
   1.961 +
   1.962 +  /*
   1.963 +   * Task function, load the cached experiments manifest file from disk.
   1.964 +   */
   1.965 +  _loadFromCache: Task.async(function* () {
   1.966 +    this._log.trace("_loadFromCache");
   1.967 +    let path = this._cacheFilePath;
   1.968 +    try {
   1.969 +      let result = yield loadJSONAsync(path, { compression: "lz4" });
   1.970 +      this._populateFromCache(result);
   1.971 +    } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
   1.972 +      // No cached manifest yet.
   1.973 +      this._experiments = new Map();
   1.974 +    }
   1.975 +  }),
   1.976 +
   1.977 +  _populateFromCache: function (data) {
   1.978 +    this._log.trace("populateFromCache() - data: " + JSON.stringify(data));
   1.979 +
   1.980 +    // If the user has a newer cache version than we can understand, we fail
   1.981 +    // hard; no experiments should be active in this older client.
   1.982 +    if (CACHE_VERSION !== data.version) {
   1.983 +      throw new Error("Experiments::_populateFromCache() - invalid cache version");
   1.984 +    }
   1.985 +
   1.986 +    let experiments = new Map();
   1.987 +    for (let item of data.data) {
   1.988 +      let entry = new Experiments.ExperimentEntry(this._policy);
   1.989 +      if (!entry.initFromCacheData(item)) {
   1.990 +        continue;
   1.991 +      }
   1.992 +      experiments.set(entry.id, entry);
   1.993 +    }
   1.994 +
   1.995 +    this._experiments = experiments;
   1.996 +  },
   1.997 +
   1.998 +  /*
   1.999 +   * Update the experiment entries from the experiments
  1.1000 +   * array in the manifest
  1.1001 +   */
  1.1002 +  _updateExperiments: function (manifestObject) {
  1.1003 +    this._log.trace("_updateExperiments() - experiments: " + JSON.stringify(manifestObject));
  1.1004 +
  1.1005 +    if (manifestObject.version !== MANIFEST_VERSION) {
  1.1006 +      this._log.warning("updateExperiments() - unsupported version " + manifestObject.version);
  1.1007 +    }
  1.1008 +
  1.1009 +    let experiments = new Map(); // The new experiments map
  1.1010 +
  1.1011 +    // Collect new and updated experiments.
  1.1012 +    for (let data of manifestObject.experiments) {
  1.1013 +      let entry = this._experiments.get(data.id);
  1.1014 +
  1.1015 +      if (entry) {
  1.1016 +        if (!entry.updateFromManifestData(data)) {
  1.1017 +          this._log.error("updateExperiments() - Invalid manifest data for " + data.id);
  1.1018 +          continue;
  1.1019 +        }
  1.1020 +      } else {
  1.1021 +        entry = new Experiments.ExperimentEntry(this._policy);
  1.1022 +        if (!entry.initFromManifestData(data)) {
  1.1023 +          continue;
  1.1024 +        }
  1.1025 +      }
  1.1026 +
  1.1027 +      if (entry.shouldDiscard()) {
  1.1028 +        continue;
  1.1029 +      }
  1.1030 +
  1.1031 +      experiments.set(entry.id, entry);
  1.1032 +    }
  1.1033 +
  1.1034 +    // Make sure we keep experiments that are or were running.
  1.1035 +    // We remove them after KEEP_HISTORY_N_DAYS.
  1.1036 +    for (let [id, entry] of this._experiments) {
  1.1037 +      if (experiments.has(id)) {
  1.1038 +        continue;
  1.1039 +      }
  1.1040 +
  1.1041 +      if (!entry.startDate || entry.shouldDiscard()) {
  1.1042 +        this._log.trace("updateExperiments() - discarding entry for " + id);
  1.1043 +        continue;
  1.1044 +      }
  1.1045 +
  1.1046 +      experiments.set(id, entry);
  1.1047 +    }
  1.1048 +
  1.1049 +    this._experiments = experiments;
  1.1050 +    this._dirty = true;
  1.1051 +  },
  1.1052 +
  1.1053 +  getActiveExperimentID: function() {
  1.1054 +    if (!this._experiments) {
  1.1055 +      return null;
  1.1056 +    }
  1.1057 +    let e = this._getActiveExperiment();
  1.1058 +    if (!e) {
  1.1059 +      return null;
  1.1060 +    }
  1.1061 +    return e.id;
  1.1062 +  },
  1.1063 +
  1.1064 +  getActiveExperimentBranch: function() {
  1.1065 +    if (!this._experiments) {
  1.1066 +      return null;
  1.1067 +    }
  1.1068 +    let e = this._getActiveExperiment();
  1.1069 +    if (!e) {
  1.1070 +      return null;
  1.1071 +    }
  1.1072 +    return e.branch;
  1.1073 +  },
  1.1074 +
  1.1075 +  _getActiveExperiment: function () {
  1.1076 +    let enabled = [experiment for ([,experiment] of this._experiments) if (experiment._enabled)];
  1.1077 +
  1.1078 +    if (enabled.length == 1) {
  1.1079 +      return enabled[0];
  1.1080 +    }
  1.1081 +
  1.1082 +    if (enabled.length > 1) {
  1.1083 +      this._log.error("getActiveExperimentId() - should not have more than 1 active experiment");
  1.1084 +      throw new Error("have more than 1 active experiment");
  1.1085 +    }
  1.1086 +
  1.1087 +    return null;
  1.1088 +  },
  1.1089 +
  1.1090 +  /**
  1.1091 +   * Disables all active experiments.
  1.1092 +   *
  1.1093 +   * @return Promise<> Promise that will get resolved once the task is done or failed.
  1.1094 +   */
  1.1095 +  disableExperiment: function (reason) {
  1.1096 +    if (!reason) {
  1.1097 +      throw new Error("Must specify a termination reason.");
  1.1098 +    }
  1.1099 +
  1.1100 +    this._log.trace("disableExperiment()");
  1.1101 +    this._terminateReason = reason;
  1.1102 +    return this._run();
  1.1103 +  },
  1.1104 +
  1.1105 +  /**
  1.1106 +   * The Set of add-on IDs that we know about from manifests.
  1.1107 +   */
  1.1108 +  get _trackedAddonIds() {
  1.1109 +    if (!this._experiments) {
  1.1110 +      return new Set();
  1.1111 +    }
  1.1112 +
  1.1113 +    return new Set([e._addonId for ([,e] of this._experiments) if (e._addonId)]);
  1.1114 +  },
  1.1115 +
  1.1116 +  /*
  1.1117 +   * Task function to check applicability of experiments, disable the active
  1.1118 +   * experiment if needed and activate the first applicable candidate.
  1.1119 +   */
  1.1120 +  _evaluateExperiments: function*() {
  1.1121 +    this._log.trace("_evaluateExperiments");
  1.1122 +
  1.1123 +    this._checkForShutdown();
  1.1124 +
  1.1125 +    // The first thing we do is reconcile our state against what's in the
  1.1126 +    // Addon Manager. It's possible that the Addon Manager knows of experiment
  1.1127 +    // add-ons that we don't. This could happen if an experiment gets installed
  1.1128 +    // when we're not listening or if there is a bug in our synchronization
  1.1129 +    // code.
  1.1130 +    //
  1.1131 +    // We have a few options of what to do with unknown experiment add-ons
  1.1132 +    // coming from the Addon Manager. Ideally, we'd convert these to
  1.1133 +    // ExperimentEntry instances and stuff them inside this._experiments.
  1.1134 +    // However, since ExperimentEntry contain lots of metadata from the
  1.1135 +    // manifest and trying to make up data could be error prone, it's safer
  1.1136 +    // to not try. Furthermore, if an experiment really did come from us, we
  1.1137 +    // should have some record of it. In the end, we decide to discard all
  1.1138 +    // knowledge for these unknown experiment add-ons.
  1.1139 +    let installedExperiments = yield installedExperimentAddons();
  1.1140 +    let expectedAddonIds = this._trackedAddonIds;
  1.1141 +    let unknownAddons = [a for (a of installedExperiments) if (!expectedAddonIds.has(a.id))];
  1.1142 +    if (unknownAddons.length) {
  1.1143 +      this._log.warn("_evaluateExperiments() - unknown add-ons in AddonManager: " +
  1.1144 +                     [a.id for (a of unknownAddons)].join(", "));
  1.1145 +
  1.1146 +      yield uninstallAddons(unknownAddons);
  1.1147 +    }
  1.1148 +
  1.1149 +    let activeExperiment = this._getActiveExperiment();
  1.1150 +    let activeChanged = false;
  1.1151 +    let now = this._policy.now();
  1.1152 +
  1.1153 +    if (!activeExperiment) {
  1.1154 +      // Avoid this pref staying out of sync if there were e.g. crashes.
  1.1155 +      gPrefs.set(PREF_ACTIVE_EXPERIMENT, false);
  1.1156 +    }
  1.1157 +
  1.1158 +    // Ensure the active experiment is in the proper state. This may install,
  1.1159 +    // uninstall, upgrade, or enable the experiment add-on. What exactly is
  1.1160 +    // abstracted away from us by design.
  1.1161 +    if (activeExperiment) {
  1.1162 +      let changes;
  1.1163 +      let shouldStopResult = yield activeExperiment.shouldStop();
  1.1164 +      if (shouldStopResult.shouldStop) {
  1.1165 +        let expireReasons = ["endTime", "maxActiveSeconds"];
  1.1166 +        let kind, reason;
  1.1167 +
  1.1168 +        if (expireReasons.indexOf(shouldStopResult.reason[0]) != -1) {
  1.1169 +          kind = TELEMETRY_LOG.TERMINATION.EXPIRED;
  1.1170 +          reason = null;
  1.1171 +        } else {
  1.1172 +          kind = TELEMETRY_LOG.TERMINATION.RECHECK;
  1.1173 +          reason = shouldStopResult.reason;
  1.1174 +        }
  1.1175 +        changes = yield activeExperiment.stop(kind, reason);
  1.1176 +      }
  1.1177 +      else if (this._terminateReason) {
  1.1178 +        changes = yield activeExperiment.stop(this._terminateReason);
  1.1179 +      }
  1.1180 +      else {
  1.1181 +        changes = yield activeExperiment.reconcileAddonState();
  1.1182 +      }
  1.1183 +
  1.1184 +      if (changes) {
  1.1185 +        this._dirty = true;
  1.1186 +        activeChanged = true;
  1.1187 +      }
  1.1188 +
  1.1189 +      if (!activeExperiment._enabled) {
  1.1190 +        activeExperiment = null;
  1.1191 +        activeChanged = true;
  1.1192 +      }
  1.1193 +    }
  1.1194 +
  1.1195 +    this._terminateReason = null;
  1.1196 +
  1.1197 +    if (!activeExperiment && gExperimentsEnabled) {
  1.1198 +      for (let [id, experiment] of this._experiments) {
  1.1199 +        let applicable;
  1.1200 +        let reason = null;
  1.1201 +        try {
  1.1202 +          applicable = yield experiment.isApplicable();
  1.1203 +        }
  1.1204 +        catch (e) {
  1.1205 +          applicable = false;
  1.1206 +          reason = e;
  1.1207 +        }
  1.1208 +
  1.1209 +        if (!applicable && reason && reason[0] != "was-active") {
  1.1210 +          // Report this from here to avoid over-reporting.
  1.1211 +          let desc = TELEMETRY_LOG.ACTIVATION;
  1.1212 +          let data = [TELEMETRY_LOG.ACTIVATION.REJECTED, id];
  1.1213 +          data = data.concat(reason);
  1.1214 +          TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY, data);
  1.1215 +        }
  1.1216 +
  1.1217 +        if (!applicable) {
  1.1218 +          continue;
  1.1219 +        }
  1.1220 +
  1.1221 +        this._log.debug("evaluateExperiments() - activating experiment " + id);
  1.1222 +        try {
  1.1223 +          yield experiment.start();
  1.1224 +          activeChanged = true;
  1.1225 +          activeExperiment = experiment;
  1.1226 +          this._dirty = true;
  1.1227 +          break;
  1.1228 +        } catch (e) {
  1.1229 +          // On failure, clean up the best we can and try the next experiment.
  1.1230 +          this._log.error("evaluateExperiments() - Unable to start experiment: " + e.message);
  1.1231 +          experiment._enabled = false;
  1.1232 +          yield experiment.reconcileAddonState();
  1.1233 +        }
  1.1234 +      }
  1.1235 +    }
  1.1236 +
  1.1237 +    gPrefs.set(PREF_ACTIVE_EXPERIMENT, activeExperiment != null);
  1.1238 +
  1.1239 +    if (activeChanged || this._firstEvaluate) {
  1.1240 +      Services.obs.notifyObservers(null, EXPERIMENTS_CHANGED_TOPIC, null);
  1.1241 +      this._firstEvaluate = false;
  1.1242 +    }
  1.1243 +
  1.1244 +    if ("@mozilla.org/toolkit/crash-reporter;1" in Cc && activeExperiment) {
  1.1245 +      try {
  1.1246 +        gCrashReporter.annotateCrashReport("ActiveExperiment", activeExperiment.id);
  1.1247 +      } catch (e) {
  1.1248 +        // It's ok if crash reporting is disabled.
  1.1249 +      }
  1.1250 +    }
  1.1251 +  },
  1.1252 +
  1.1253 +  /*
  1.1254 +   * Schedule the soonest re-check of experiment applicability that is needed.
  1.1255 +   */
  1.1256 +  _scheduleNextRun: function () {
  1.1257 +    this._checkForShutdown();
  1.1258 +
  1.1259 +    if (this._timer) {
  1.1260 +      this._timer.clear();
  1.1261 +    }
  1.1262 +
  1.1263 +    if (!gExperimentsEnabled || this._experiments.length == 0) {
  1.1264 +      return;
  1.1265 +    }
  1.1266 +
  1.1267 +    let time = null;
  1.1268 +    let now = this._policy.now().getTime();
  1.1269 +
  1.1270 +    for (let [id, experiment] of this._experiments) {
  1.1271 +      let scheduleTime = experiment.getScheduleTime();
  1.1272 +      if (scheduleTime > now) {
  1.1273 +        if (time !== null) {
  1.1274 +          time = Math.min(time, scheduleTime);
  1.1275 +        } else {
  1.1276 +          time = scheduleTime;
  1.1277 +        }
  1.1278 +      }
  1.1279 +    }
  1.1280 +
  1.1281 +    if (time === null) {
  1.1282 +      // No schedule time found.
  1.1283 +      return;
  1.1284 +    }
  1.1285 +
  1.1286 +    this._log.trace("scheduleExperimentEvaluation() - scheduling for "+time+", now: "+now);
  1.1287 +    this._policy.oneshotTimer(this.notify, time - now, this, "_timer");
  1.1288 +  },
  1.1289 +};
  1.1290 +
  1.1291 +
  1.1292 +/*
  1.1293 + * Represents a single experiment.
  1.1294 + */
  1.1295 +
  1.1296 +Experiments.ExperimentEntry = function (policy) {
  1.1297 +  this._policy = policy || new Experiments.Policy();
  1.1298 +  this._log = Log.repository.getLoggerWithMessagePrefix(
  1.1299 +    "Browser.Experiments.Experiments",
  1.1300 +    "ExperimentEntry #" + gExperimentEntryCounter++ + "::");
  1.1301 +
  1.1302 +  // Is the experiment supposed to be running.
  1.1303 +  this._enabled = false;
  1.1304 +  // When this experiment was started, if ever.
  1.1305 +  this._startDate = null;
  1.1306 +  // When this experiment was ended, if ever.
  1.1307 +  this._endDate = null;
  1.1308 +  // The condition data from the manifest.
  1.1309 +  this._manifestData = null;
  1.1310 +  // For an active experiment, signifies whether we need to update the xpi.
  1.1311 +  this._needsUpdate = false;
  1.1312 +  // A random sample value for comparison against the manifest conditions.
  1.1313 +  this._randomValue = null;
  1.1314 +  // When this entry was last changed for respecting history retention duration.
  1.1315 +  this._lastChangedDate = null;
  1.1316 +  // Has this experiment failed to activate before?
  1.1317 +  this._failedStart = false;
  1.1318 +  // The experiment branch
  1.1319 +  this._branch = null;
  1.1320 +
  1.1321 +  // We grab these from the addon after download.
  1.1322 +  this._name = null;
  1.1323 +  this._description = null;
  1.1324 +  this._homepageURL = null;
  1.1325 +  this._addonId = null;
  1.1326 +};
  1.1327 +
  1.1328 +Experiments.ExperimentEntry.prototype = {
  1.1329 +  MANIFEST_REQUIRED_FIELDS: new Set([
  1.1330 +    "id",
  1.1331 +    "xpiURL",
  1.1332 +    "xpiHash",
  1.1333 +    "startTime",
  1.1334 +    "endTime",
  1.1335 +    "maxActiveSeconds",
  1.1336 +    "appName",
  1.1337 +    "channel",
  1.1338 +  ]),
  1.1339 +
  1.1340 +  MANIFEST_OPTIONAL_FIELDS: new Set([
  1.1341 +    "maxStartTime",
  1.1342 +    "minVersion",
  1.1343 +    "maxVersion",
  1.1344 +    "version",
  1.1345 +    "minBuildID",
  1.1346 +    "maxBuildID",
  1.1347 +    "buildIDs",
  1.1348 +    "os",
  1.1349 +    "locale",
  1.1350 +    "sample",
  1.1351 +    "disabled",
  1.1352 +    "frozen",
  1.1353 +    "jsfilter",
  1.1354 +  ]),
  1.1355 +
  1.1356 +  SERIALIZE_KEYS: new Set([
  1.1357 +    "_enabled",
  1.1358 +    "_manifestData",
  1.1359 +    "_needsUpdate",
  1.1360 +    "_randomValue",
  1.1361 +    "_failedStart",
  1.1362 +    "_name",
  1.1363 +    "_description",
  1.1364 +    "_homepageURL",
  1.1365 +    "_addonId",
  1.1366 +    "_startDate",
  1.1367 +    "_endDate",
  1.1368 +    "_branch",
  1.1369 +  ]),
  1.1370 +
  1.1371 +  DATE_KEYS: new Set([
  1.1372 +    "_startDate",
  1.1373 +    "_endDate",
  1.1374 +  ]),
  1.1375 +
  1.1376 +  UPGRADE_KEYS: new Map([
  1.1377 +    ["_branch", null],
  1.1378 +  ]),
  1.1379 +
  1.1380 +  ADDON_CHANGE_NONE: 0,
  1.1381 +  ADDON_CHANGE_INSTALL: 1,
  1.1382 +  ADDON_CHANGE_UNINSTALL: 2,
  1.1383 +  ADDON_CHANGE_ENABLE: 4,
  1.1384 +
  1.1385 +  /*
  1.1386 +   * Initialize entry from the manifest.
  1.1387 +   * @param data The experiment data from the manifest.
  1.1388 +   * @return boolean Whether initialization succeeded.
  1.1389 +   */
  1.1390 +  initFromManifestData: function (data) {
  1.1391 +    if (!this._isManifestDataValid(data)) {
  1.1392 +      return false;
  1.1393 +    }
  1.1394 +
  1.1395 +    this._manifestData = data;
  1.1396 +
  1.1397 +    this._randomValue = this._policy.random();
  1.1398 +    this._lastChangedDate = this._policy.now();
  1.1399 +
  1.1400 +    return true;
  1.1401 +  },
  1.1402 +
  1.1403 +  get enabled() {
  1.1404 +    return this._enabled;
  1.1405 +  },
  1.1406 +
  1.1407 +  get id() {
  1.1408 +    return this._manifestData.id;
  1.1409 +  },
  1.1410 +
  1.1411 +  get branch() {
  1.1412 +    return this._branch;
  1.1413 +  },
  1.1414 +
  1.1415 +  set branch(v) {
  1.1416 +    this._branch = v;
  1.1417 +  },
  1.1418 +
  1.1419 +  get startDate() {
  1.1420 +    return this._startDate;
  1.1421 +  },
  1.1422 +
  1.1423 +  get endDate() {
  1.1424 +    if (!this._startDate) {
  1.1425 +      return null;
  1.1426 +    }
  1.1427 +
  1.1428 +    let endTime = 0;
  1.1429 +
  1.1430 +    if (!this._enabled) {
  1.1431 +      return this._endDate;
  1.1432 +    }
  1.1433 +
  1.1434 +    let maxActiveMs = 1000 * this._manifestData.maxActiveSeconds;
  1.1435 +    endTime = Math.min(1000 * this._manifestData.endTime,
  1.1436 +                       this._startDate.getTime() + maxActiveMs);
  1.1437 +
  1.1438 +    return new Date(endTime);
  1.1439 +  },
  1.1440 +
  1.1441 +  get needsUpdate() {
  1.1442 +    return this._needsUpdate;
  1.1443 +  },
  1.1444 +
  1.1445 +  /*
  1.1446 +   * Initialize entry from the cache.
  1.1447 +   * @param data The entry data from the cache.
  1.1448 +   * @return boolean Whether initialization succeeded.
  1.1449 +   */
  1.1450 +  initFromCacheData: function (data) {
  1.1451 +    for (let [key, dval] of this.UPGRADE_KEYS) {
  1.1452 +      if (!(key in data)) {
  1.1453 +        data[key] = dval;
  1.1454 +      }
  1.1455 +    }
  1.1456 +
  1.1457 +    for (let key of this.SERIALIZE_KEYS) {
  1.1458 +      if (!(key in data) && !this.DATE_KEYS.has(key)) {
  1.1459 +        this._log.error("initFromCacheData() - missing required key " + key);
  1.1460 +        return false;
  1.1461 +      }
  1.1462 +    };
  1.1463 +
  1.1464 +    if (!this._isManifestDataValid(data._manifestData)) {
  1.1465 +      return false;
  1.1466 +    }
  1.1467 +
  1.1468 +    // Dates are restored separately from epoch ms, everything else is just
  1.1469 +    // copied in.
  1.1470 +
  1.1471 +    this.SERIALIZE_KEYS.forEach(key => {
  1.1472 +      if (!this.DATE_KEYS.has(key)) {
  1.1473 +        this[key] = data[key];
  1.1474 +      }
  1.1475 +    });
  1.1476 +
  1.1477 +    this.DATE_KEYS.forEach(key => {
  1.1478 +      if (key in data) {
  1.1479 +        let date = new Date();
  1.1480 +        date.setTime(data[key]);
  1.1481 +        this[key] = date;
  1.1482 +      }
  1.1483 +    });
  1.1484 +
  1.1485 +    this._lastChangedDate = this._policy.now();
  1.1486 +
  1.1487 +    return true;
  1.1488 +  },
  1.1489 +
  1.1490 +  /*
  1.1491 +   * Returns a JSON representation of this object.
  1.1492 +   */
  1.1493 +  toJSON: function () {
  1.1494 +    let obj = {};
  1.1495 +
  1.1496 +    // Dates are serialized separately as epoch ms.
  1.1497 +
  1.1498 +    this.SERIALIZE_KEYS.forEach(key => {
  1.1499 +      if (!this.DATE_KEYS.has(key)) {
  1.1500 +        obj[key] = this[key];
  1.1501 +      }
  1.1502 +    });
  1.1503 +
  1.1504 +    this.DATE_KEYS.forEach(key => {
  1.1505 +      if (this[key]) {
  1.1506 +        obj[key] = this[key].getTime();
  1.1507 +      }
  1.1508 +    });
  1.1509 +
  1.1510 +    return obj;
  1.1511 +  },
  1.1512 +
  1.1513 +  /*
  1.1514 +   * Update from the experiment data from the manifest.
  1.1515 +   * @param data The experiment data from the manifest.
  1.1516 +   * @return boolean Whether updating succeeded.
  1.1517 +   */
  1.1518 +  updateFromManifestData: function (data) {
  1.1519 +    let old = this._manifestData;
  1.1520 +
  1.1521 +    if (!this._isManifestDataValid(data)) {
  1.1522 +      return false;
  1.1523 +    }
  1.1524 +
  1.1525 +    if (this._enabled) {
  1.1526 +      if (old.xpiHash !== data.xpiHash) {
  1.1527 +        // A changed hash means we need to update active experiments.
  1.1528 +        this._needsUpdate = true;
  1.1529 +      }
  1.1530 +    } else if (this._failedStart &&
  1.1531 +               (old.xpiHash !== data.xpiHash) ||
  1.1532 +               (old.xpiURL !== data.xpiURL)) {
  1.1533 +      // Retry installation of previously invalid experiments
  1.1534 +      // if hash or url changed.
  1.1535 +      this._failedStart = false;
  1.1536 +    }
  1.1537 +
  1.1538 +    this._manifestData = data;
  1.1539 +    this._lastChangedDate = this._policy.now();
  1.1540 +
  1.1541 +    return true;
  1.1542 +  },
  1.1543 +
  1.1544 +  /*
  1.1545 +   * Is this experiment applicable?
  1.1546 +   * @return Promise<> Resolved if the experiment is applicable.
  1.1547 +   *                   If it is not applicable it is rejected with
  1.1548 +   *                   a Promise<string> which contains the reason.
  1.1549 +   */
  1.1550 +  isApplicable: function () {
  1.1551 +    let versionCmp = Cc["@mozilla.org/xpcom/version-comparator;1"]
  1.1552 +                              .getService(Ci.nsIVersionComparator);
  1.1553 +    let app = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
  1.1554 +    let runtime = Cc["@mozilla.org/xre/app-info;1"]
  1.1555 +                    .getService(Ci.nsIXULRuntime);
  1.1556 +
  1.1557 +    let locale = this._policy.locale();
  1.1558 +    let channel = this._policy.updatechannel();
  1.1559 +    let data = this._manifestData;
  1.1560 +
  1.1561 +    let now = this._policy.now() / 1000; // The manifest times are in seconds.
  1.1562 +    let minActive = MIN_EXPERIMENT_ACTIVE_SECONDS;
  1.1563 +    let maxActive = data.maxActiveSeconds || 0;
  1.1564 +    let startSec = (this.startDate || 0) / 1000;
  1.1565 +
  1.1566 +    this._log.trace("isApplicable() - now=" + now
  1.1567 +                    + ", randomValue=" + this._randomValue
  1.1568 +                    + ", data=" + JSON.stringify(this._manifestData));
  1.1569 +
  1.1570 +    // Not applicable if it already ran.
  1.1571 +
  1.1572 +    if (!this.enabled && this._endDate) {
  1.1573 +      return Promise.reject(["was-active"]);
  1.1574 +    }
  1.1575 +
  1.1576 +    // Define and run the condition checks.
  1.1577 +
  1.1578 +    let simpleChecks = [
  1.1579 +      { name: "failedStart",
  1.1580 +        condition: () => !this._failedStart },
  1.1581 +      { name: "disabled",
  1.1582 +        condition: () => !data.disabled },
  1.1583 +      { name: "frozen",
  1.1584 +        condition: () => !data.frozen || this._enabled },
  1.1585 +      { name: "startTime",
  1.1586 +        condition: () => now >= data.startTime },
  1.1587 +      { name: "endTime",
  1.1588 +        condition: () => now < data.endTime },
  1.1589 +      { name: "maxStartTime",
  1.1590 +        condition: () => !data.maxStartTime || now <= data.maxStartTime },
  1.1591 +      { name: "maxActiveSeconds",
  1.1592 +        condition: () => !this._startDate || now <= (startSec + maxActive) },
  1.1593 +      { name: "appName",
  1.1594 +        condition: () => !data.appName || data.appName.indexOf(app.name) != -1 },
  1.1595 +      { name: "minBuildID",
  1.1596 +        condition: () => !data.minBuildID || app.platformBuildID >= data.minBuildID },
  1.1597 +      { name: "maxBuildID",
  1.1598 +        condition: () => !data.maxBuildID || app.platformBuildID <= data.maxBuildID },
  1.1599 +      { name: "buildIDs",
  1.1600 +        condition: () => !data.buildIDs || data.buildIDs.indexOf(app.platformBuildID) != -1 },
  1.1601 +      { name: "os",
  1.1602 +        condition: () => !data.os || data.os.indexOf(runtime.OS) != -1 },
  1.1603 +      { name: "channel",
  1.1604 +        condition: () => !data.channel || data.channel.indexOf(channel) != -1 },
  1.1605 +      { name: "locale",
  1.1606 +        condition: () => !data.locale || data.locale.indexOf(locale) != -1 },
  1.1607 +      { name: "sample",
  1.1608 +        condition: () => data.sample === undefined || this._randomValue <= data.sample },
  1.1609 +      { name: "version",
  1.1610 +        condition: () => !data.version || data.version.indexOf(app.version) != -1 },
  1.1611 +      { name: "minVersion",
  1.1612 +        condition: () => !data.minVersion || versionCmp.compare(app.version, data.minVersion) >= 0 },
  1.1613 +      { name: "maxVersion",
  1.1614 +        condition: () => !data.maxVersion || versionCmp.compare(app.version, data.maxVersion) <= 0 },
  1.1615 +    ];
  1.1616 +
  1.1617 +    for (let check of simpleChecks) {
  1.1618 +      let result = check.condition();
  1.1619 +      if (!result) {
  1.1620 +        this._log.debug("isApplicable() - id="
  1.1621 +                        + data.id + " - test '" + check.name + "' failed");
  1.1622 +        return Promise.reject([check.name]);
  1.1623 +      }
  1.1624 +    }
  1.1625 +
  1.1626 +    if (data.jsfilter) {
  1.1627 +      return this._runFilterFunction(data.jsfilter);
  1.1628 +    }
  1.1629 +
  1.1630 +    return Promise.resolve(true);
  1.1631 +  },
  1.1632 +
  1.1633 +  /*
  1.1634 +   * Run the jsfilter function from the manifest in a sandbox and return the
  1.1635 +   * result (forced to boolean).
  1.1636 +   */
  1.1637 +  _runFilterFunction: function (jsfilter) {
  1.1638 +    this._log.trace("runFilterFunction() - filter: " + jsfilter);
  1.1639 +
  1.1640 +    return Task.spawn(function ExperimentEntry_runFilterFunction_task() {
  1.1641 +      const nullprincipal = Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal);
  1.1642 +      let options = {
  1.1643 +        sandboxName: "telemetry experiments jsfilter sandbox",
  1.1644 +        wantComponents: false,
  1.1645 +      };
  1.1646 +
  1.1647 +      let sandbox = Cu.Sandbox(nullprincipal);
  1.1648 +      let context = {};
  1.1649 +      context.healthReportPayload = yield this._policy.healthReportPayload();
  1.1650 +      context.telemetryPayload    = yield this._policy.telemetryPayload();
  1.1651 +
  1.1652 +      try {
  1.1653 +        Cu.evalInSandbox(jsfilter, sandbox);
  1.1654 +      } catch (e) {
  1.1655 +        this._log.error("runFilterFunction() - failed to eval jsfilter: " + e.message);
  1.1656 +        throw ["jsfilter-evalfailed"];
  1.1657 +      }
  1.1658 +
  1.1659 +      // You can't insert arbitrarily complex objects into a sandbox, so
  1.1660 +      // we serialize everything through JSON.
  1.1661 +      sandbox._hr = JSON.stringify(yield this._policy.healthReportPayload());
  1.1662 +      Object.defineProperty(sandbox, "_t",
  1.1663 +        { get: () => JSON.stringify(this._policy.telemetryPayload()) });
  1.1664 +
  1.1665 +      let result = false;
  1.1666 +      try {
  1.1667 +        result = !!Cu.evalInSandbox("filter({healthReportPayload: JSON.parse(_hr), telemetryPayload: JSON.parse(_t)})", sandbox);
  1.1668 +      }
  1.1669 +      catch (e) {
  1.1670 +        this._log.debug("runFilterFunction() - filter function failed: "
  1.1671 +                      + e.message + ", " + e.stack);
  1.1672 +        throw ["jsfilter-threw", e.message];
  1.1673 +      }
  1.1674 +      finally {
  1.1675 +        Cu.nukeSandbox(sandbox);
  1.1676 +      }
  1.1677 +
  1.1678 +      if (!result) {
  1.1679 +        throw ["jsfilter-false"];
  1.1680 +      }
  1.1681 +
  1.1682 +      throw new Task.Result(true);
  1.1683 +    }.bind(this));
  1.1684 +  },
  1.1685 +
  1.1686 +  /*
  1.1687 +   * Start running the experiment.
  1.1688 +   *
  1.1689 +   * @return Promise<> Resolved when the operation is complete.
  1.1690 +   */
  1.1691 +  start: Task.async(function* () {
  1.1692 +    this._log.trace("start() for " + this.id);
  1.1693 +
  1.1694 +    this._enabled = true;
  1.1695 +    return yield this.reconcileAddonState();
  1.1696 +  }),
  1.1697 +
  1.1698 +  // Async install of the addon for this experiment, part of the start task above.
  1.1699 +  _installAddon: Task.async(function* () {
  1.1700 +    let deferred = Promise.defer();
  1.1701 +
  1.1702 +    let hash = this._policy.ignoreHashes ? null : this._manifestData.xpiHash;
  1.1703 +
  1.1704 +    let install = yield addonInstallForURL(this._manifestData.xpiURL, hash);
  1.1705 +    gActiveInstallURLs.add(install.sourceURI.spec);
  1.1706 +
  1.1707 +    let failureHandler = (install, handler) => {
  1.1708 +      let message = "AddonInstall " + handler + " for " + this.id + ", state=" +
  1.1709 +                   (install.state || "?") + ", error=" + install.error;
  1.1710 +      this._log.error("_installAddon() - " + message);
  1.1711 +      this._failedStart = true;
  1.1712 +      gActiveInstallURLs.delete(install.sourceURI.spec);
  1.1713 +
  1.1714 +      TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
  1.1715 +                      [TELEMETRY_LOG.ACTIVATION.INSTALL_FAILURE, this.id]);
  1.1716 +
  1.1717 +      deferred.reject(new Error(message));
  1.1718 +    };
  1.1719 +
  1.1720 +    let listener = {
  1.1721 +      _expectedID: null,
  1.1722 +
  1.1723 +      onDownloadEnded: install => {
  1.1724 +        this._log.trace("_installAddon() - onDownloadEnded for " + this.id);
  1.1725 +
  1.1726 +        if (install.existingAddon) {
  1.1727 +          this._log.warn("_installAddon() - onDownloadEnded, addon already installed");
  1.1728 +        }
  1.1729 +
  1.1730 +        if (install.addon.type !== "experiment") {
  1.1731 +          this._log.error("_installAddon() - onDownloadEnded, wrong addon type");
  1.1732 +          install.cancel();
  1.1733 +        }
  1.1734 +      },
  1.1735 +
  1.1736 +      onInstallStarted: install => {
  1.1737 +        this._log.trace("_installAddon() - onInstallStarted for " + this.id);
  1.1738 +
  1.1739 +        if (install.existingAddon) {
  1.1740 +          this._log.warn("_installAddon() - onInstallStarted, addon already installed");
  1.1741 +        }
  1.1742 +
  1.1743 +        if (install.addon.type !== "experiment") {
  1.1744 +          this._log.error("_installAddon() - onInstallStarted, wrong addon type");
  1.1745 +          return false;
  1.1746 +        }
  1.1747 +      },
  1.1748 +
  1.1749 +      onInstallEnded: install => {
  1.1750 +        this._log.trace("_installAddon() - install ended for " + this.id);
  1.1751 +        gActiveInstallURLs.delete(install.sourceURI.spec);
  1.1752 +
  1.1753 +        this._lastChangedDate = this._policy.now();
  1.1754 +        this._startDate = this._policy.now();
  1.1755 +        this._enabled = true;
  1.1756 +
  1.1757 +        TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
  1.1758 +                       [TELEMETRY_LOG.ACTIVATION.ACTIVATED, this.id]);
  1.1759 +
  1.1760 +        let addon = install.addon;
  1.1761 +        this._name = addon.name;
  1.1762 +        this._addonId = addon.id;
  1.1763 +        this._description = addon.description || "";
  1.1764 +        this._homepageURL = addon.homepageURL || "";
  1.1765 +
  1.1766 +        // Experiment add-ons default to userDisabled=true. Enable if needed.
  1.1767 +        if (addon.userDisabled) {
  1.1768 +          this._log.trace("Add-on is disabled. Enabling.");
  1.1769 +          listener._expectedID = addon.id;
  1.1770 +          AddonManager.addAddonListener(listener);
  1.1771 +          addon.userDisabled = false;
  1.1772 +        } else {
  1.1773 +          this._log.trace("Add-on is enabled. start() completed.");
  1.1774 +          deferred.resolve();
  1.1775 +        }
  1.1776 +      },
  1.1777 +
  1.1778 +      onEnabled: addon => {
  1.1779 +        this._log.info("onEnabled() for " + addon.id);
  1.1780 +
  1.1781 +        if (addon.id != listener._expectedID) {
  1.1782 +          return;
  1.1783 +        }
  1.1784 +
  1.1785 +        AddonManager.removeAddonListener(listener);
  1.1786 +        deferred.resolve();
  1.1787 +      },
  1.1788 +    };
  1.1789 +
  1.1790 +    ["onDownloadCancelled", "onDownloadFailed", "onInstallCancelled", "onInstallFailed"]
  1.1791 +      .forEach(what => {
  1.1792 +        listener[what] = install => failureHandler(install, what)
  1.1793 +      });
  1.1794 +
  1.1795 +    install.addListener(listener);
  1.1796 +    install.install();
  1.1797 +
  1.1798 +    return yield deferred.promise;
  1.1799 +  }),
  1.1800 +
  1.1801 +  /**
  1.1802 +   * Stop running the experiment if it is active.
  1.1803 +   *
  1.1804 +   * @param terminationKind (optional)
  1.1805 +   *        The termination kind, e.g. ADDON_UNINSTALLED or EXPIRED.
  1.1806 +   * @param terminationReason (optional)
  1.1807 +   *        The termination reason details for termination kind RECHECK.
  1.1808 +   * @return Promise<> Resolved when the operation is complete.
  1.1809 +   */
  1.1810 +  stop: Task.async(function* (terminationKind, terminationReason) {
  1.1811 +    this._log.trace("stop() - id=" + this.id + ", terminationKind=" + terminationKind);
  1.1812 +    if (!this._enabled) {
  1.1813 +      throw new Error("Must not call stop() on an inactive experiment.");
  1.1814 +    }
  1.1815 +
  1.1816 +    this._enabled = false;
  1.1817 +    let now = this._policy.now();
  1.1818 +    this._lastChangedDate = now;
  1.1819 +    this._endDate = now;
  1.1820 +
  1.1821 +    let changes = yield this.reconcileAddonState();
  1.1822 +    this._logTermination(terminationKind, terminationReason);
  1.1823 +
  1.1824 +    if (terminationKind == TELEMETRY_LOG.TERMINATION.ADDON_UNINSTALLED) {
  1.1825 +      changes |= this.ADDON_CHANGE_UNINSTALL;
  1.1826 +    }
  1.1827 +
  1.1828 +    return changes;
  1.1829 +  }),
  1.1830 +
  1.1831 +  /**
  1.1832 +   * Reconcile the state of the add-on against what it's supposed to be.
  1.1833 +   *
  1.1834 +   * If we are active, ensure the add-on is enabled and up to date.
  1.1835 +   *
  1.1836 +   * If we are inactive, ensure the add-on is not installed.
  1.1837 +   */
  1.1838 +  reconcileAddonState: Task.async(function* () {
  1.1839 +    this._log.trace("reconcileAddonState()");
  1.1840 +
  1.1841 +    if (!this._enabled) {
  1.1842 +      if (!this._addonId) {
  1.1843 +        this._log.trace("reconcileAddonState() - Experiment is not enabled and " +
  1.1844 +                        "has no add-on. Doing nothing.");
  1.1845 +        return this.ADDON_CHANGE_NONE;
  1.1846 +      }
  1.1847 +
  1.1848 +      let addon = yield this._getAddon();
  1.1849 +      if (!addon) {
  1.1850 +        this._log.trace("reconcileAddonState() - Inactive experiment has no " +
  1.1851 +                        "add-on. Doing nothing.");
  1.1852 +        return this.ADDON_CHANGE_NONE;
  1.1853 +      }
  1.1854 +
  1.1855 +      this._log.info("reconcileAddonState() - Uninstalling add-on for inactive " +
  1.1856 +                     "experiment: " + addon.id);
  1.1857 +      gActiveUninstallAddonIDs.add(addon.id);
  1.1858 +      yield uninstallAddons([addon]);
  1.1859 +      gActiveUninstallAddonIDs.delete(addon.id);
  1.1860 +      return this.ADDON_CHANGE_UNINSTALL;
  1.1861 +    }
  1.1862 +
  1.1863 +    // If we get here, we're supposed to be active.
  1.1864 +
  1.1865 +    let changes = 0;
  1.1866 +
  1.1867 +    // That requires an add-on.
  1.1868 +    let currentAddon = yield this._getAddon();
  1.1869 +
  1.1870 +    // If we have an add-on but it isn't up to date, uninstall it
  1.1871 +    // (to prepare for reinstall).
  1.1872 +    if (currentAddon && this._needsUpdate) {
  1.1873 +      this._log.info("reconcileAddonState() - Uninstalling add-on because update " +
  1.1874 +                     "needed: " + currentAddon.id);
  1.1875 +      gActiveUninstallAddonIDs.add(currentAddon.id);
  1.1876 +      yield uninstallAddons([currentAddon]);
  1.1877 +      gActiveUninstallAddonIDs.delete(currentAddon.id);
  1.1878 +      changes |= this.ADDON_CHANGE_UNINSTALL;
  1.1879 +    }
  1.1880 +
  1.1881 +    if (!currentAddon || this._needsUpdate) {
  1.1882 +      this._log.info("reconcileAddonState() - Installing add-on.");
  1.1883 +      yield this._installAddon();
  1.1884 +      changes |= this.ADDON_CHANGE_INSTALL;
  1.1885 +    }
  1.1886 +
  1.1887 +    let addon = yield this._getAddon();
  1.1888 +    if (!addon) {
  1.1889 +      throw new Error("Could not obtain add-on for experiment that should be " +
  1.1890 +                      "enabled.");
  1.1891 +    }
  1.1892 +
  1.1893 +    // If we have the add-on and it is enabled, we are done.
  1.1894 +    if (!addon.userDisabled) {
  1.1895 +      return changes;
  1.1896 +    }
  1.1897 +
  1.1898 +    let deferred = Promise.defer();
  1.1899 +
  1.1900 +    // Else we need to enable it.
  1.1901 +    let listener = {
  1.1902 +      onEnabled: enabledAddon => {
  1.1903 +        if (enabledAddon.id != addon.id) {
  1.1904 +          return;
  1.1905 +        }
  1.1906 +
  1.1907 +        AddonManager.removeAddonListener(listener);
  1.1908 +        deferred.resolve();
  1.1909 +      },
  1.1910 +    };
  1.1911 +
  1.1912 +    this._log.info("Activating add-on: " + addon.id);
  1.1913 +    AddonManager.addAddonListener(listener);
  1.1914 +    addon.userDisabled = false;
  1.1915 +    yield deferred.promise;
  1.1916 +    changes |= this.ADDON_CHANGE_ENABLE;
  1.1917 +
  1.1918 +    this._log.info("Add-on has been enabled: " + addon.id);
  1.1919 +    return changes;
  1.1920 +   }),
  1.1921 +
  1.1922 +  /**
  1.1923 +   * Obtain the underlying Addon from the Addon Manager.
  1.1924 +   *
  1.1925 +   * @return Promise<Addon|null>
  1.1926 +   */
  1.1927 +  _getAddon: function () {
  1.1928 +    if (!this._addonId) {
  1.1929 +      return Promise.resolve(null);
  1.1930 +    }
  1.1931 +
  1.1932 +    let deferred = Promise.defer();
  1.1933 +
  1.1934 +    AddonManager.getAddonByID(this._addonId, (addon) => {
  1.1935 +      if (addon && addon.appDisabled) {
  1.1936 +        // Don't return PreviousExperiments.
  1.1937 +        addon = null;
  1.1938 +      }
  1.1939 +
  1.1940 +      deferred.resolve(addon);
  1.1941 +    });
  1.1942 +
  1.1943 +    return deferred.promise;
  1.1944 +  },
  1.1945 +
  1.1946 +  _logTermination: function (terminationKind, terminationReason) {
  1.1947 +    if (terminationKind === undefined) {
  1.1948 +      return;
  1.1949 +    }
  1.1950 +
  1.1951 +    if (!(terminationKind in TELEMETRY_LOG.TERMINATION)) {
  1.1952 +      this._log.warn("stop() - unknown terminationKind " + terminationKind);
  1.1953 +      return;
  1.1954 +    }
  1.1955 +
  1.1956 +    let data = [terminationKind, this.id];
  1.1957 +    if (terminationReason) {
  1.1958 +      data = data.concat(terminationReason);
  1.1959 +    }
  1.1960 +
  1.1961 +    TelemetryLog.log(TELEMETRY_LOG.TERMINATION_KEY, data);
  1.1962 +  },
  1.1963 +
  1.1964 +  /**
  1.1965 +   * Determine whether an active experiment should be stopped.
  1.1966 +   */
  1.1967 +  shouldStop: function () {
  1.1968 +    if (!this._enabled) {
  1.1969 +      throw new Error("shouldStop must not be called on disabled experiments.");
  1.1970 +    }
  1.1971 +
  1.1972 +    let data = this._manifestData;
  1.1973 +    let now = this._policy.now() / 1000; // The manifest times are in seconds.
  1.1974 +    let maxActiveSec = data.maxActiveSeconds || 0;
  1.1975 +
  1.1976 +    let deferred = Promise.defer();
  1.1977 +    this.isApplicable().then(
  1.1978 +      () => deferred.resolve({shouldStop: false}),
  1.1979 +      reason => deferred.resolve({shouldStop: true, reason: reason})
  1.1980 +    );
  1.1981 +
  1.1982 +    return deferred.promise;
  1.1983 +  },
  1.1984 +
  1.1985 +  /*
  1.1986 +   * Should this be discarded from the cache due to age?
  1.1987 +   */
  1.1988 +  shouldDiscard: function () {
  1.1989 +    let limit = this._policy.now();
  1.1990 +    limit.setDate(limit.getDate() - KEEP_HISTORY_N_DAYS);
  1.1991 +    return (this._lastChangedDate < limit);
  1.1992 +  },
  1.1993 +
  1.1994 +  /*
  1.1995 +   * Get next date (in epoch-ms) to schedule a re-evaluation for this.
  1.1996 +   * Returns 0 if it doesn't need one.
  1.1997 +   */
  1.1998 +  getScheduleTime: function () {
  1.1999 +    if (this._enabled) {
  1.2000 +      let now = this._policy.now();
  1.2001 +      let startTime = this._startDate.getTime();
  1.2002 +      let maxActiveTime = startTime + 1000 * this._manifestData.maxActiveSeconds;
  1.2003 +      return Math.min(1000 * this._manifestData.endTime,  maxActiveTime);
  1.2004 +    }
  1.2005 +
  1.2006 +    if (this._endDate) {
  1.2007 +      return this._endDate.getTime();
  1.2008 +    }
  1.2009 +
  1.2010 +    return 1000 * this._manifestData.startTime;
  1.2011 +  },
  1.2012 +
  1.2013 +  /*
  1.2014 +   * Perform sanity checks on the experiment data.
  1.2015 +   */
  1.2016 +  _isManifestDataValid: function (data) {
  1.2017 +    this._log.trace("isManifestDataValid() - data: " + JSON.stringify(data));
  1.2018 +
  1.2019 +    for (let key of this.MANIFEST_REQUIRED_FIELDS) {
  1.2020 +      if (!(key in data)) {
  1.2021 +        this._log.error("isManifestDataValid() - missing required key: " + key);
  1.2022 +        return false;
  1.2023 +      }
  1.2024 +    }
  1.2025 +
  1.2026 +    for (let key in data) {
  1.2027 +      if (!this.MANIFEST_OPTIONAL_FIELDS.has(key) &&
  1.2028 +          !this.MANIFEST_REQUIRED_FIELDS.has(key)) {
  1.2029 +        this._log.error("isManifestDataValid() - unknown key: " + key);
  1.2030 +        return false;
  1.2031 +      }
  1.2032 +    }
  1.2033 +
  1.2034 +    return true;
  1.2035 +  },
  1.2036 +};
  1.2037 +
  1.2038 +
  1.2039 +
  1.2040 +/**
  1.2041 + * Strip a Date down to its UTC midnight.
  1.2042 + *
  1.2043 + * This will return a cloned Date object. The original is unchanged.
  1.2044 + */
  1.2045 +let stripDateToMidnight = function (d) {
  1.2046 +  let m = new Date(d);
  1.2047 +  m.setUTCHours(0, 0, 0, 0);
  1.2048 +
  1.2049 +  return m;
  1.2050 +};
  1.2051 +
  1.2052 +function ExperimentsLastActiveMeasurement1() {
  1.2053 +  Metrics.Measurement.call(this);
  1.2054 +}
  1.2055 +function ExperimentsLastActiveMeasurement2() {
  1.2056 +  Metrics.Measurement.call(this);
  1.2057 +}
  1.2058 +
  1.2059 +const FIELD_DAILY_LAST_TEXT = {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT};
  1.2060 +
  1.2061 +ExperimentsLastActiveMeasurement1.prototype = Object.freeze({
  1.2062 +  __proto__: Metrics.Measurement.prototype,
  1.2063 +
  1.2064 +  name: "info",
  1.2065 +  version: 1,
  1.2066 +
  1.2067 +  fields: {
  1.2068 +    lastActive: FIELD_DAILY_LAST_TEXT,
  1.2069 +  }
  1.2070 +});
  1.2071 +ExperimentsLastActiveMeasurement2.prototype = Object.freeze({
  1.2072 +  __proto__: Metrics.Measurement.prototype,
  1.2073 +
  1.2074 +  name: "info",
  1.2075 +  version: 2,
  1.2076 +
  1.2077 +  fields: {
  1.2078 +    lastActive: FIELD_DAILY_LAST_TEXT,
  1.2079 +    lastActiveBranch: FIELD_DAILY_LAST_TEXT,
  1.2080 +  }
  1.2081 +});
  1.2082 +
  1.2083 +this.ExperimentsProvider = function () {
  1.2084 +  Metrics.Provider.call(this);
  1.2085 +
  1.2086 +  this._experiments = null;
  1.2087 +};
  1.2088 +
  1.2089 +ExperimentsProvider.prototype = Object.freeze({
  1.2090 +  __proto__: Metrics.Provider.prototype,
  1.2091 +
  1.2092 +  name: "org.mozilla.experiments",
  1.2093 +
  1.2094 +  measurementTypes: [
  1.2095 +    ExperimentsLastActiveMeasurement1,
  1.2096 +    ExperimentsLastActiveMeasurement2,
  1.2097 +  ],
  1.2098 +
  1.2099 +  _OBSERVERS: [
  1.2100 +    EXPERIMENTS_CHANGED_TOPIC,
  1.2101 +  ],
  1.2102 +
  1.2103 +  postInit: function () {
  1.2104 +    for (let o of this._OBSERVERS) {
  1.2105 +      Services.obs.addObserver(this, o, false);
  1.2106 +    }
  1.2107 +
  1.2108 +    return Promise.resolve();
  1.2109 +  },
  1.2110 +
  1.2111 +  onShutdown: function () {
  1.2112 +    for (let o of this._OBSERVERS) {
  1.2113 +      Services.obs.removeObserver(this, o);
  1.2114 +    }
  1.2115 +
  1.2116 +    return Promise.resolve();
  1.2117 +  },
  1.2118 +
  1.2119 +  observe: function (subject, topic, data) {
  1.2120 +    switch (topic) {
  1.2121 +      case EXPERIMENTS_CHANGED_TOPIC:
  1.2122 +        this.recordLastActiveExperiment();
  1.2123 +        break;
  1.2124 +    }
  1.2125 +  },
  1.2126 +
  1.2127 +  collectDailyData: function () {
  1.2128 +    return this.recordLastActiveExperiment();
  1.2129 +  },
  1.2130 +
  1.2131 +  recordLastActiveExperiment: function () {
  1.2132 +    if (!gExperimentsEnabled) {
  1.2133 +      return Promise.resolve();
  1.2134 +    }
  1.2135 +
  1.2136 +    if (!this._experiments) {
  1.2137 +      this._experiments = Experiments.instance();
  1.2138 +    }
  1.2139 +
  1.2140 +    let m = this.getMeasurement(ExperimentsLastActiveMeasurement2.prototype.name,
  1.2141 +                                ExperimentsLastActiveMeasurement2.prototype.version);
  1.2142 +
  1.2143 +    return this.enqueueStorageOperation(() => {
  1.2144 +      return Task.spawn(function* recordTask() {
  1.2145 +        let todayActive = yield this._experiments.lastActiveToday();
  1.2146 +        if (!todayActive) {
  1.2147 +          this._log.info("No active experiment on this day: " +
  1.2148 +                         this._experiments._policy.now());
  1.2149 +          return;
  1.2150 +        }
  1.2151 +
  1.2152 +        this._log.info("Recording last active experiment: " + todayActive.id);
  1.2153 +        yield m.setDailyLastText("lastActive", todayActive.id,
  1.2154 +                                 this._experiments._policy.now());
  1.2155 +        let branch = todayActive.branch;
  1.2156 +        if (branch) {
  1.2157 +          yield m.setDailyLastText("lastActiveBranch", branch,
  1.2158 +                                   this._experiments._policy.now());
  1.2159 +        }
  1.2160 +      }.bind(this));
  1.2161 +    });
  1.2162 +  },
  1.2163 +});
  1.2164 +
  1.2165 +/**
  1.2166 + * An Add-ons Manager provider that knows about old experiments.
  1.2167 + *
  1.2168 + * This provider exposes read-only add-ons corresponding to previously-active
  1.2169 + * experiments. The existence of this provider (and the add-ons it knows about)
  1.2170 + * facilitates the display of old experiments in the Add-ons Manager UI with
  1.2171 + * very little custom code in that component.
  1.2172 + */
  1.2173 +this.Experiments.PreviousExperimentProvider = function (experiments) {
  1.2174 +  this._experiments = experiments;
  1.2175 +  this._experimentList = [];
  1.2176 +  this._log = Log.repository.getLoggerWithMessagePrefix(
  1.2177 +    "Browser.Experiments.Experiments",
  1.2178 +    "PreviousExperimentProvider #" + gPreviousProviderCounter++ + "::");
  1.2179 +}
  1.2180 +
  1.2181 +this.Experiments.PreviousExperimentProvider.prototype = Object.freeze({
  1.2182 +  startup: function () {
  1.2183 +    this._log.trace("startup()");
  1.2184 +    Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
  1.2185 +  },
  1.2186 +
  1.2187 +  shutdown: function () {
  1.2188 +    this._log.trace("shutdown()");
  1.2189 +    Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
  1.2190 +  },
  1.2191 +
  1.2192 +  observe: function (subject, topic, data) {
  1.2193 +    switch (topic) {
  1.2194 +      case EXPERIMENTS_CHANGED_TOPIC:
  1.2195 +        this._updateExperimentList();
  1.2196 +        break;
  1.2197 +    }
  1.2198 +  },
  1.2199 +
  1.2200 +  getAddonByID: function (id, cb) {
  1.2201 +    for (let experiment of this._experimentList) {
  1.2202 +      if (experiment.id == id) {
  1.2203 +        cb(new PreviousExperimentAddon(experiment));
  1.2204 +        return;
  1.2205 +      }
  1.2206 +    }
  1.2207 +
  1.2208 +    cb(null);
  1.2209 +  },
  1.2210 +
  1.2211 +  getAddonsByTypes: function (types, cb) {
  1.2212 +    if (types && types.length > 0 && types.indexOf("experiment") == -1) {
  1.2213 +      cb([]);
  1.2214 +      return;
  1.2215 +    }
  1.2216 +
  1.2217 +    cb([new PreviousExperimentAddon(e) for (e of this._experimentList)]);
  1.2218 +  },
  1.2219 +
  1.2220 +  _updateExperimentList: function () {
  1.2221 +    return this._experiments.getExperiments().then((experiments) => {
  1.2222 +      let list = [e for (e of experiments) if (!e.active)];
  1.2223 +
  1.2224 +      let newMap = new Map([[e.id, e] for (e of list)]);
  1.2225 +      let oldMap = new Map([[e.id, e] for (e of this._experimentList)]);
  1.2226 +
  1.2227 +      let added = [e.id for (e of list) if (!oldMap.has(e.id))];
  1.2228 +      let removed = [e.id for (e of this._experimentList) if (!newMap.has(e.id))];
  1.2229 +
  1.2230 +      for (let id of added) {
  1.2231 +        this._log.trace("updateExperimentList() - adding " + id);
  1.2232 +        let wrapper = new PreviousExperimentAddon(newMap.get(id));
  1.2233 +        AddonManagerPrivate.callInstallListeners("onExternalInstall", null, wrapper, null, false);
  1.2234 +        AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false);
  1.2235 +      }
  1.2236 +
  1.2237 +      for (let id of removed) {
  1.2238 +        this._log.trace("updateExperimentList() - removing " + id);
  1.2239 +        let wrapper = new PreviousExperimentAddon(oldMap.get(id));
  1.2240 +        AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false);
  1.2241 +      }
  1.2242 +
  1.2243 +      this._experimentList = list;
  1.2244 +
  1.2245 +      for (let id of added) {
  1.2246 +        let wrapper = new PreviousExperimentAddon(newMap.get(id));
  1.2247 +        AddonManagerPrivate.callAddonListeners("onInstalled", wrapper);
  1.2248 +      }
  1.2249 +
  1.2250 +      for (let id of removed) {
  1.2251 +        let wrapper = new PreviousExperimentAddon(oldMap.get(id));
  1.2252 +        AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
  1.2253 +      }
  1.2254 +
  1.2255 +      return this._experimentList;
  1.2256 +    });
  1.2257 +  },
  1.2258 +});
  1.2259 +
  1.2260 +/**
  1.2261 + * An add-on that represents a previously-installed experiment.
  1.2262 + */
  1.2263 +function PreviousExperimentAddon(experiment) {
  1.2264 +  this._id = experiment.id;
  1.2265 +  this._name = experiment.name;
  1.2266 +  this._endDate = experiment.endDate;
  1.2267 +  this._description = experiment.description;
  1.2268 +}
  1.2269 +
  1.2270 +PreviousExperimentAddon.prototype = Object.freeze({
  1.2271 +  // BEGIN REQUIRED ADDON PROPERTIES
  1.2272 +
  1.2273 +  get appDisabled() {
  1.2274 +    return true;
  1.2275 +  },
  1.2276 +
  1.2277 +  get blocklistState() {
  1.2278 +    Ci.nsIBlocklistService.STATE_NOT_BLOCKED
  1.2279 +  },
  1.2280 +
  1.2281 +  get creator() {
  1.2282 +    return new AddonManagerPrivate.AddonAuthor("");
  1.2283 +  },
  1.2284 +
  1.2285 +  get foreignInstall() {
  1.2286 +    return false;
  1.2287 +  },
  1.2288 +
  1.2289 +  get id() {
  1.2290 +    return this._id;
  1.2291 +  },
  1.2292 +
  1.2293 +  get isActive() {
  1.2294 +    return false;
  1.2295 +  },
  1.2296 +
  1.2297 +  get isCompatible() {
  1.2298 +    return true;
  1.2299 +  },
  1.2300 +
  1.2301 +  get isPlatformCompatible() {
  1.2302 +    return true;
  1.2303 +  },
  1.2304 +
  1.2305 +  get name() {
  1.2306 +    return this._name;
  1.2307 +  },
  1.2308 +
  1.2309 +  get pendingOperations() {
  1.2310 +    return AddonManager.PENDING_NONE;
  1.2311 +  },
  1.2312 +
  1.2313 +  get permissions() {
  1.2314 +    return 0;
  1.2315 +  },
  1.2316 +
  1.2317 +  get providesUpdatesSecurely() {
  1.2318 +    return true;
  1.2319 +  },
  1.2320 +
  1.2321 +  get scope() {
  1.2322 +    return AddonManager.SCOPE_PROFILE;
  1.2323 +  },
  1.2324 +
  1.2325 +  get type() {
  1.2326 +    return "experiment";
  1.2327 +  },
  1.2328 +
  1.2329 +  get userDisabled() {
  1.2330 +    return true;
  1.2331 +  },
  1.2332 +
  1.2333 +  get version() {
  1.2334 +    return null;
  1.2335 +  },
  1.2336 +
  1.2337 +  // END REQUIRED PROPERTIES
  1.2338 +
  1.2339 +  // BEGIN OPTIONAL PROPERTIES
  1.2340 +
  1.2341 +  get description() {
  1.2342 +    return this._description;
  1.2343 +  },
  1.2344 +
  1.2345 +  get updateDate() {
  1.2346 +    return new Date(this._endDate);
  1.2347 +  },
  1.2348 +
  1.2349 +  // END OPTIONAL PROPERTIES
  1.2350 +
  1.2351 +  // BEGIN REQUIRED METHODS
  1.2352 +
  1.2353 +  isCompatibleWith: function (appVersion, platformVersion) {
  1.2354 +    return true;
  1.2355 +  },
  1.2356 +
  1.2357 +  findUpdates: function (listener, reason, appVersion, platformVersion) {
  1.2358 +    AddonManagerPrivate.callNoUpdateListeners(this, listener, reason,
  1.2359 +                                              appVersion, platformVersion);
  1.2360 +  },
  1.2361 +
  1.2362 +  // END REQUIRED METHODS
  1.2363 +
  1.2364 +  /**
  1.2365 +   * The end-date of the experiment, required for the Addon Manager UI.
  1.2366 +   */
  1.2367 +
  1.2368 +   get endDate() {
  1.2369 +     return this._endDate;
  1.2370 +   },
  1.2371 +
  1.2372 +});

mercurial