browser/experiments/Experiments.jsm

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

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

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

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

mercurial