1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4"use strict"; 5 6const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm"); 7const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 8const { XPCOMUtils } = ChromeUtils.import( 9 "resource://gre/modules/XPCOMUtils.jsm" 10); 11const { PromiseUtils } = ChromeUtils.import( 12 "resource://gre/modules/PromiseUtils.jsm" 13); 14const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm"); 15 16XPCOMUtils.defineLazyModuleGetters(this, { 17 AddonRollouts: "resource://normandy/lib/AddonRollouts.jsm", 18 AddonStudies: "resource://normandy/lib/AddonStudies.jsm", 19 CleanupManager: "resource://normandy/lib/CleanupManager.jsm", 20 LogManager: "resource://normandy/lib/LogManager.jsm", 21 NormandyMigrations: "resource://normandy/NormandyMigrations.jsm", 22 PreferenceExperiments: "resource://normandy/lib/PreferenceExperiments.jsm", 23 PreferenceRollouts: "resource://normandy/lib/PreferenceRollouts.jsm", 24 RecipeRunner: "resource://normandy/lib/RecipeRunner.jsm", 25 ShieldPreferences: "resource://normandy/lib/ShieldPreferences.jsm", 26 TelemetryUtils: "resource://gre/modules/TelemetryUtils.jsm", 27 TelemetryEvents: "resource://normandy/lib/TelemetryEvents.jsm", 28 ExperimentManager: "resource://nimbus/lib/ExperimentManager.jsm", 29 RemoteSettingsExperimentLoader: 30 "resource://nimbus/lib/RemoteSettingsExperimentLoader.jsm", 31}); 32 33var EXPORTED_SYMBOLS = ["Normandy"]; 34 35const UI_AVAILABLE_NOTIFICATION = "sessionstore-windows-restored"; 36const BOOTSTRAP_LOGGER_NAME = "app.normandy.bootstrap"; 37const SHIELD_INIT_NOTIFICATION = "shield-init-complete"; 38 39const STARTUP_EXPERIMENT_PREFS_BRANCH = "app.normandy.startupExperimentPrefs."; 40const STARTUP_ROLLOUT_PREFS_BRANCH = "app.normandy.startupRolloutPrefs."; 41const PREF_LOGGING_LEVEL = "app.normandy.logging.level"; 42 43// Logging 44const log = Log.repository.getLogger(BOOTSTRAP_LOGGER_NAME); 45log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); 46log.level = Services.prefs.getIntPref(PREF_LOGGING_LEVEL, Log.Level.Warn); 47 48var Normandy = { 49 studyPrefsChanged: {}, 50 rolloutPrefsChanged: {}, 51 defaultPrefsHaveBeenApplied: PromiseUtils.defer(), 52 uiAvailableNotificationObserved: PromiseUtils.defer(), 53 54 /** Initialization that needs to happen before the first paint on startup. */ 55 async init({ runAsync = true } = {}) { 56 // It is important to register the listener for the UI before the first 57 // await, to avoid missing it. 58 Services.obs.addObserver(this, UI_AVAILABLE_NOTIFICATION); 59 60 // Listen for when Telemetry is disabled or re-enabled. 61 Services.obs.addObserver( 62 this, 63 TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC 64 ); 65 66 // It is important this happens before the first `await`. Note that this 67 // also happens before migrations are applied. 68 this.rolloutPrefsChanged = this.applyStartupPrefs( 69 STARTUP_ROLLOUT_PREFS_BRANCH 70 ); 71 this.studyPrefsChanged = this.applyStartupPrefs( 72 STARTUP_EXPERIMENT_PREFS_BRANCH 73 ); 74 this.defaultPrefsHaveBeenApplied.resolve(); 75 76 await NormandyMigrations.applyAll(); 77 78 // Wait for the UI to be ready, or time out after 5 minutes. 79 if (runAsync) { 80 await Promise.race([ 81 this.uiAvailableNotificationObserved, 82 new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000)), 83 ]); 84 } 85 86 // Remove observer for UI notifications. It will error if the notification 87 // was already removed, which is fine. 88 try { 89 Services.obs.removeObserver(this, UI_AVAILABLE_NOTIFICATION); 90 } catch (e) {} 91 92 await this.finishInit(); 93 }, 94 95 async observe(subject, topic, data) { 96 if (topic === UI_AVAILABLE_NOTIFICATION) { 97 Services.obs.removeObserver(this, UI_AVAILABLE_NOTIFICATION); 98 this.uiAvailableNotificationObserved.resolve(); 99 } else if (topic === TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC) { 100 await Promise.all( 101 [ 102 PreferenceExperiments, 103 PreferenceRollouts, 104 AddonStudies, 105 AddonRollouts, 106 ].map(service => service.onTelemetryDisabled()) 107 ); 108 } 109 }, 110 111 async finishInit() { 112 try { 113 TelemetryEvents.init(); 114 } catch (err) { 115 log.error("Failed to initialize telemetry events:", err); 116 } 117 118 await PreferenceRollouts.recordOriginalValues(this.rolloutPrefsChanged); 119 await PreferenceExperiments.recordOriginalValues(this.studyPrefsChanged); 120 121 // Setup logging and listen for changes to logging prefs 122 LogManager.configure( 123 Services.prefs.getIntPref(PREF_LOGGING_LEVEL, Log.Level.Warn) 124 ); 125 Services.prefs.addObserver(PREF_LOGGING_LEVEL, LogManager.configure); 126 CleanupManager.addCleanupHandler(() => 127 Services.prefs.removeObserver(PREF_LOGGING_LEVEL, LogManager.configure) 128 ); 129 130 try { 131 await ExperimentManager.onStartup(); 132 } catch (err) { 133 log.error("Failed to initialize ExperimentManager:", err); 134 } 135 136 try { 137 await RemoteSettingsExperimentLoader.init(); 138 } catch (err) { 139 log.error("Failed to initialize RemoteSettingsExperimentLoader:", err); 140 } 141 142 try { 143 await AddonStudies.init(); 144 } catch (err) { 145 log.error("Failed to initialize addon studies:", err); 146 } 147 148 try { 149 await PreferenceRollouts.init(); 150 } catch (err) { 151 log.error("Failed to initialize preference rollouts:", err); 152 } 153 154 try { 155 await AddonRollouts.init(); 156 } catch (err) { 157 log.error("Failed to initialize addon rollouts:", err); 158 } 159 160 try { 161 await PreferenceExperiments.init(); 162 } catch (err) { 163 log.error("Failed to initialize preference experiments:", err); 164 } 165 166 try { 167 ShieldPreferences.init(); 168 } catch (err) { 169 log.error("Failed to initialize preferences UI:", err); 170 } 171 172 await RecipeRunner.init(); 173 Services.obs.notifyObservers(null, SHIELD_INIT_NOTIFICATION); 174 }, 175 176 async uninit() { 177 await CleanupManager.cleanup(); 178 // Note that Service.pref.removeObserver and Service.obs.removeObserver have 179 // oppositely ordered parameters. 180 Services.prefs.removeObserver(PREF_LOGGING_LEVEL, LogManager.configure); 181 for (const topic of [ 182 TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC, 183 UI_AVAILABLE_NOTIFICATION, 184 ]) { 185 try { 186 Services.obs.removeObserver(this, topic); 187 } catch (e) { 188 // topic must have already been removed or never added 189 } 190 } 191 }, 192 193 /** 194 * Copy a preference subtree from one branch to another, being careful about 195 * types, and return the values the target branch originally had. Prefs will 196 * be read from the user branch and applied to the default branch. 197 * 198 * @param sourcePrefix 199 * The pref prefix to read prefs from. 200 * @returns 201 * The original values that each pref had on the default branch. 202 */ 203 applyStartupPrefs(sourcePrefix) { 204 // Note that this is called before Normandy's migrations are applied. This 205 // currently has no effect, but future changes should be careful to be 206 // backwards compatible. 207 const originalValues = {}; 208 const sourceBranch = Services.prefs.getBranch(sourcePrefix); 209 const targetBranch = Services.prefs.getDefaultBranch(""); 210 211 for (const prefName of sourceBranch.getChildList("")) { 212 const sourcePrefType = sourceBranch.getPrefType(prefName); 213 const targetPrefType = targetBranch.getPrefType(prefName); 214 215 if ( 216 targetPrefType !== Services.prefs.PREF_INVALID && 217 targetPrefType !== sourcePrefType 218 ) { 219 Cu.reportError( 220 new Error( 221 `Error setting startup pref ${prefName}; pref type does not match.` 222 ) 223 ); 224 continue; 225 } 226 227 // record the value of the default branch before setting it 228 try { 229 switch (targetPrefType) { 230 case Services.prefs.PREF_STRING: { 231 originalValues[prefName] = targetBranch.getCharPref(prefName); 232 break; 233 } 234 case Services.prefs.PREF_INT: { 235 originalValues[prefName] = targetBranch.getIntPref(prefName); 236 break; 237 } 238 case Services.prefs.PREF_BOOL: { 239 originalValues[prefName] = targetBranch.getBoolPref(prefName); 240 break; 241 } 242 case Services.prefs.PREF_INVALID: { 243 originalValues[prefName] = null; 244 break; 245 } 246 default: { 247 // This should never happen 248 log.error( 249 `Error getting startup pref ${prefName}; unknown value type ${sourcePrefType}.` 250 ); 251 } 252 } 253 } catch (e) { 254 if (e.result === Cr.NS_ERROR_UNEXPECTED) { 255 // There is a value for the pref on the user branch but not on the default branch. This is ok. 256 originalValues[prefName] = null; 257 } else { 258 // Unexpected error, report it and move on 259 Cu.reportError(e); 260 continue; 261 } 262 } 263 264 // now set the new default value 265 switch (sourcePrefType) { 266 case Services.prefs.PREF_STRING: { 267 targetBranch.setCharPref( 268 prefName, 269 sourceBranch.getCharPref(prefName) 270 ); 271 break; 272 } 273 case Services.prefs.PREF_INT: { 274 targetBranch.setIntPref(prefName, sourceBranch.getIntPref(prefName)); 275 break; 276 } 277 case Services.prefs.PREF_BOOL: { 278 targetBranch.setBoolPref( 279 prefName, 280 sourceBranch.getBoolPref(prefName) 281 ); 282 break; 283 } 284 default: { 285 // This should never happen. 286 Cu.reportError( 287 new Error( 288 `Error getting startup pref ${prefName}; unexpected value type ${sourcePrefType}.` 289 ) 290 ); 291 } 292 } 293 } 294 295 return originalValues; 296 }, 297}; 298