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