Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetter(
lazy,
"timerManager",
"@mozilla.org/updates/timer-manager;1",
Ci.nsIUpdateTimerManager
);
ChromeUtils.defineESModuleGetters(lazy, {
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
HeartbeatSurvey:
"moz-src:///browser/components/nimbus/HeartbeatSurvey.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "log", () => {
const { Logger } = ChromeUtils.importESModule(
);
return new Logger("LegacyHeartbeatFeature");
});
const TIMER_NAME = "legacy-heartbeat";
const PREF_CHANGED_TOPIC = "nsPref:changed";
const RUN_INTERVAL_PREF = "app.normandy.run_interval_seconds";
const SHIELD_ENABLED_PREF = "app.normandy.enabled";
const API_URL_PREF = "app.normandy.api_url";
const TIMER_LAST_UPDATE_PREF = `app.update.lastUpdateTime.${TIMER_NAME}`;
/**
* Runs legacy Heartbeat surveys through Nimbus.
*/
export const LegacyHeartbeatFeature = new (class {
#initialized;
#onFeatureUpdate;
#running;
#shutdownRegistered;
init() {
if (this.#initialized) {
return;
}
const backgroundTasks = Cc["@mozilla.org/backgroundtasks;1"]?.getService(
Ci.nsIBackgroundTasks
);
const inShutdown = Services.startup.isInOrBeyondShutdownPhase(
Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
);
if (backgroundTasks?.isBackgroundTaskMode || inShutdown) {
return;
}
this.#onFeatureUpdate = () => {
this.run({ trigger: "featureUpdate" });
};
lazy.NimbusFeatures.legacyHeartbeat.onUpdate(this.#onFeatureUpdate);
this.setTimer();
Services.prefs.addObserver(RUN_INTERVAL_PREF, this);
this.#initialized = true;
if (!this.#shutdownRegistered) {
lazy.AsyncShutdown.appShutdownConfirmed.addBlocker(
"LegacyHeartbeatFeature: cleaning up",
() => this.uninit()
);
this.#shutdownRegistered = true;
}
}
async uninit() {
if (!this.#initialized) {
return;
}
lazy.NimbusFeatures.legacyHeartbeat.offUpdate(this.#onFeatureUpdate);
lazy.timerManager.unregisterTimer(TIMER_NAME);
this.#initialized = false;
this.#onFeatureUpdate = null;
Services.prefs.removeObserver(RUN_INTERVAL_PREF, this);
}
observe(subject, topic, data) {
if (topic === PREF_CHANGED_TOPIC && data === RUN_INTERVAL_PREF) {
this.setTimer();
}
}
checkPrefs() {
if (!Services.prefs.getBoolPref(SHIELD_ENABLED_PREF, false)) {
return false;
}
const apiUrl = Services.prefs.getCharPref(API_URL_PREF, "");
if (!apiUrl) {
return false;
}
if (!apiUrl.startsWith("https://")) {
return false;
}
return true;
}
setTimer() {
const runInterval = Services.prefs.getIntPref(RUN_INTERVAL_PREF, 21600);
lazy.timerManager.registerTimer(TIMER_NAME, () => this.run(), runInterval);
}
async run({ trigger = "timer" } = {}) {
if (this.#running || !this.checkPrefs()) {
return;
}
this.#running = true;
try {
const recipe = this.getHeartbeatRecipe();
if (recipe === null) {
return;
}
const survey = new lazy.HeartbeatSurvey();
await survey.run(recipe);
} catch (e) {
lazy.log.error(e);
} finally {
this.#running = false;
if (trigger != "timer") {
// `run()` was executed outside the scheduled timer.
// Update the last time it ran to make sure it is rescheduled later.
const lastUpdateTime = Math.round(Date.now() / 1000);
Services.prefs.setIntPref(TIMER_LAST_UPDATE_PREF, lastUpdateTime);
}
}
}
getHeartbeatRecipe() {
const survey = lazy.NimbusFeatures.legacyHeartbeat.getVariable("survey");
if (typeof survey == "undefined") {
return null;
}
const { slug } =
lazy.NimbusFeatures.legacyHeartbeat.getEnrollmentMetadata();
return {
id: `nimbus:${slug}`,
arguments: survey,
};
}
})();