/* 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/. */ const lazy = {}; const PREF_APP_UPDATE_COMPULSORY_RESTART = "app.update.compulsory_restart"; let deferredRestartTasks = null; ChromeUtils.defineESModuleGetters(lazy, { ScheduledTask: "resource://gre/modules/ScheduledTask.sys.mjs", InfoBar: "resource:///modules/asrouter/InfoBar.sys.mjs", }); // Forcibly close Firefox, without waiting for beforeunload handlers. function forceRestart() { Services.startup.quit( Services.startup.eForceQuit | Services.startup.eRestart ); console.error(`Firefox is restarting`); } function infobarDispatchCallback(action, _selectedBrowser) { if (action?.type === "USER_ACTION" && action.data?.type === "RESTART_APP") { forceRestart(); } } // Display the notification message function showNotificationToolbar(restartZonedDateTime) { const datetime = restartZonedDateTime.epochMilliseconds; const message = { weight: 100, id: "COMPULSORY_RESTART_SCHEDULED", content: { priority: 3, type: "universal", dismissable: false, text: { string_id: "compulsory-restart-message", }, buttons: [ { label: { string_id: "policy-update-now", }, action: { type: "RESTART_APP", dismiss: false, }, }, ], attributes: { datetime, }, }, template: "infobar", targeting: "true", groups: [], }; const win = Services.wm.getMostRecentBrowserWindow(); if (!win) { return; } lazy.InfoBar.showInfoBarMessage( win.gBrowser.selectedBrowser, message, infobarDispatchCallback ); } /** * Disarm deferred tasks and clear the state for the next test. */ export function testingOnly_resetTasks() { if (!Cu.isInAutomation) { throw new Error("this method only usable in testing"); } deferredRestartTasks?.notificationTask?.disarm(); deferredRestartTasks?.restartTask?.disarm(); deferredRestartTasks = null; } /** * Check on whether the deferred tasks have been created and armed. */ export function testingOnly_getTaskStatus() { if (!Cu.isInAutomation) { throw new Error("this method only usable in testing"); } if (deferredRestartTasks) { const res = { notificationTask: deferredRestartTasks.notificationTask?.isArmed, restartTask: deferredRestartTasks.restartTask?.isArmed, }; return res; } return null; } // Example settings: // {NotificationPeriodHours: 72, RestartTimeOfDay: {Hour: 18, Minute: 45}} /** * Params: * nowInstant: A Temporal.Instant object representing the current time. * notificationPeriodHours: The minimum amount of time in hours between when an update has been staged and when a warning will be displayed * restartTimeOfDay: The time of day, in local time, that the restart will take place, in the form of an object like: {hour: 3, minute: 14} * * Returns: * { * notificationZonedDateTime: a Temporal.ZonedDateTime object representing the time when the notification bar should start to be shown. * restartZonedDateTime: a Temporal.ZonedDateTime object representing the time when the current Firefox instance will forcibly quit * } */ export function calculateSchedule( nowInstant, notificationPeriodHours, restartTimeOfDay ) { // The notification time is nowInstant + notificationPeriodHours const notificationDelay = Temporal.Duration.from({ hours: notificationPeriodHours, }); const notificationInstant = nowInstant.add(notificationDelay); const notificationZonedDateTime = notificationInstant.toZonedDateTimeISO( Temporal.Now.timeZoneId() ); // Figure out the compulsory restart time. The earliest is 1 hour past the notification time, the latest is 25 hours past the notification time. // restart time = if (there is an upcoming `restartTimeOfDay` on the notification day, an it's more than an hour in the future) then (notification day at restart time) else (notification day + 1 at restart time) const restartTime = Temporal.PlainTime.from({ hour: restartTimeOfDay.Hour, minute: restartTimeOfDay.Minute, }); let restartZonedDateTime = notificationZonedDateTime.withPlainTime(restartTime); // Make sure the user has at least 1 hour notification before restart time. // If not, postpone the restart by 24 hours. if ( Temporal.Duration.compare( notificationZonedDateTime.until(restartZonedDateTime), Temporal.Duration.from({ hours: 1 }) ) < 0 ) { // it's less than 1 hour until the restart time. restartZonedDateTime = restartZonedDateTime.add( Temporal.Duration.from({ hours: 24 }) ); } return { notificationZonedDateTime, restartZonedDateTime }; } // Create scheduled tasks with requested date/time export function createScheduledRestartTasks( restartZonedDateTime, notificationZonedDateTime ) { const notificationTask = new lazy.ScheduledTask(() => { showNotificationToolbar(restartZonedDateTime); }, notificationZonedDateTime.epochMilliseconds); const restartTask = new lazy.ScheduledTask( forceRestart, restartZonedDateTime.epochMilliseconds ); notificationTask.arm(); restartTask.arm(); return { notificationTask, restartTask }; } // Read the policy from prefs and parse the JSON. export function getCompulsoryRestartPolicy() { const compulsoryRestartSettingStr = Services.prefs.getStringPref( PREF_APP_UPDATE_COMPULSORY_RESTART, null ); if (compulsoryRestartSettingStr) { const compulsoryRestartSetting = JSON.parse(compulsoryRestartSettingStr); if ( typeof compulsoryRestartSetting?.NotificationPeriodHours === "number" && typeof compulsoryRestartSetting?.RestartTimeOfDay === "object" && typeof compulsoryRestartSetting.RestartTimeOfDay.Hour === "number" && typeof compulsoryRestartSetting.RestartTimeOfDay.Minute === "number" ) { return compulsoryRestartSetting; } } return null; } // This is the main entry point into this module. // This function is called when an update is staged, to set timers for // when to show the notification and when to force a restart. export function handleCompulsoryUpdatePolicy() { if (!deferredRestartTasks) { const compulsoryRestartSetting = getCompulsoryRestartPolicy(); if (compulsoryRestartSetting) { const now = Temporal.Now.instant(); const { restartZonedDateTime, notificationZonedDateTime } = calculateSchedule( now, compulsoryRestartSetting.NotificationPeriodHours, compulsoryRestartSetting.RestartTimeOfDay ); if (restartZonedDateTime && notificationZonedDateTime) { deferredRestartTasks = createScheduledRestartTasks( restartZonedDateTime, notificationZonedDateTime ); } else { console.error( `Invalid restart settings: ${JSON.stringify(compulsoryRestartSetting)}` ); } } } } const observer = { observe: (_subject, topic, _data) => { switch (topic) { case "update-downloaded": case "update-staged": handleCompulsoryUpdatePolicy(); break; } }, }; export const UpdatePolicyEnforcer = { registerObservers() { Services.obs.addObserver(observer, "update-downloaded"); Services.obs.addObserver(observer, "update-staged"); }, };