Wed, 31 Dec 2014 06:09:35 +0100
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 | }); |