/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ /* 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 { EXIT_CODE as EXIT_CODE_BASE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs"; import { AppConstants as AC } from "resource://gre/modules/AppConstants.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const EXIT_CODE = { ...EXIT_CODE_BASE, DISABLED_BY_POLICY: EXIT_CODE_BASE.LAST_RESERVED + 1, INVALID_ARGUMENT: EXIT_CODE_BASE.LAST_RESERVED + 2, MUTEX_NOT_LOCKABLE: EXIT_CODE_BASE.LAST_RESERVED + 3, }; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { setTimeout: "resource://gre/modules/Timer.sys.mjs", // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit ShellService: "resource:///modules/ShellService.sys.mjs", }); XPCOMUtils.defineLazyServiceGetters(lazy, { AlertsService: ["@mozilla.org/alerts-service;1", "nsIAlertsService"], }); ChromeUtils.defineLazyGetter(lazy, "log", () => { let { ConsoleAPI } = ChromeUtils.importESModule( "resource://gre/modules/Console.sys.mjs" ); let consoleOptions = { maxLogLevel: "error", maxLogLevelPref: "app.defaultagent.loglevel", prefix: "DefaultAgent", }; return new ConsoleAPI(consoleOptions); }); // Should be slightly longer than kNotificationTimeoutMs and kGleanSendWait below // (divided by 1000 to convert millseconds to seconds) to not cause race // between timeouts. // // Additionally, should be less than the Windows Scheduled Task timeout // execTimeLimitBStr in ScheduledTask.cpp. // // Current bounds are 11 hours 55 minutes 10 seconds and 12 hours 5 minutes. export const backgroundTaskTimeoutSec = 12 * 60 * 60; // 11 hours 55 minutes in milliseconds. const kNotificationTimeoutMs = 11 * 60 * 60 * 1000 + 55 * 60 * 1000; // 10 seconds in milliseconds. const kGleanSendWait = 10000; const kNotificationShown = Object.freeze({ notShown: "not-shown", shown: "shown", error: "error", }); const kNotificationAction = Object.freeze({ dismissedByTimeout: "dismissed-by-timeout", dismissedByButton: "dismissed-by-button", dismissedToActionCenter: "dismissed-to-action-center", makeFirefoxDefaultButton: "make-firefox-default-button", toastClicked: "toast-clicked", noAction: "no-action", }); // We expect to be given a command string in argv[1], perhaps followed by other // arguments depending on the command. The valid commands are: // register-task [unique-token] // Create a Windows scheduled task that will launch this binary with the // do-task command every 24 hours, starting from 24 hours after register-task // is run. unique-token is required and should be some string that uniquely // identifies this installation of the product; typically this will be the // install path hash that's used for the update directory, the AppUserModelID, // and other related purposes. // update-task [unique-token] // Update an existing task registration, without changing its schedule. This // should be called during updates of the application, in case this program // has been updated and any of the task parameters have changed. The unique // token argument is required and should be the same one that was passed in // when the task was registered. // unregister-task [unique-token] // Removes the previously created task. The unique token argument is required // and should be the same one that was passed in when the task was registered. // uninstall [unique-token] // Removes the previously created task, and also removes all registry entries // running the task may have created. The unique token argument is required // and should be the same one that was passed in when the task was registered. // do-task [app-user-model-id] // Actually performs the default agent task, which currently means generating // and sending our telemetry ping and possibly showing a notification to the // user if their browser has switched from Firefox to Edge with Blink. // set-default-browser-user-choice [app-user-model-id] [[.file1 ProgIDRoot1] // ...] // Set the default browser via the UserChoice registry keys. Additional // optional file extensions to register can be specified as additional // argument pairs: the first element is the file extension, the second element // is the root of a ProgID, which will be suffixed with `-$AUMI`. export async function runBackgroundTask(commandLine) { Services.fog.initializeFOG( undefined, "firefox.desktop.background.defaultagent" ); let defaultAgent = Cc["@mozilla.org/default-agent;1"].getService( Ci.nsIDefaultAgent ); let command = commandLine.getArgument(0); // The uninstall and unregister commands are allowed even if the policy // disabling the task is set, so that uninstalls and updates always work. // Similarly, debug commands are always allowed. switch (command) { case "uninstall": { let token = commandLine.getArgument(1); lazy.log.info(`Uninstalling for token "${token}"`); defaultAgent.uninstall(token); return EXIT_CODE.SUCCESS; } case "unregister-task": { let token = commandLine.getArgument(1); lazy.log.info(`Unregistering task for token "${token}"`); defaultAgent.unregisterTask(token); return EXIT_CODE.SUCCESS; } } // We check for disablement by policy because that's assumed to be static. // But we don't check for disablement by remote settings so that // `register-task` and `update-task` can proceed as part of the update // cycle, waiting for remote (re-)enablement. if (defaultAgent.agentDisabled()) { lazy.log.warn("Default Agent disabled, exiting without running."); return EXIT_CODE.DISABLED_BY_POLICY; } switch (command) { case "register-task": { let token = commandLine.getArgument(1); lazy.log.info(`Registering task for token "${token}"`); defaultAgent.registerTask(token); return EXIT_CODE.SUCCESS; } case "update-task": { let token = commandLine.getArgument(1); lazy.log.info(`Updating task for token "${token}"`); defaultAgent.updateTask(token); return EXIT_CODE.SUCCESS; } case "do-task": { let aumid = commandLine.getArgument(1); let force = commandLine.findFlag("force", true) != -1; lazy.log.info(`Running do-task with AUMID "${aumid}"`); try { lazy.log.info("Running JS do-task."); await runWithRegistryLocked(async () => { await doTask(defaultAgent, force); }); } catch (e) { if (e.message) { lazy.log.error(e.message); } if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) { return EXIT_CODE.MUTEX_NOT_LOCKABLE; } return EXIT_CODE.EXCEPTION; } // Bug 1857333: We wait for arbitrary time for Glean to submit telemetry. lazy.log.info("Pinged glean, waiting for submission."); await new Promise(resolve => lazy.setTimeout(resolve, kGleanSendWait)); return EXIT_CODE.SUCCESS; } } return EXIT_CODE.INVALID_ARGUMENT; } // Throws if unable to lock mutex (therefore function isn't run). async function runWithRegistryLocked(aMutexGuardedFunction) { const kVendor = Services.appinfo.vendor || ""; const kRegistryMutexName = `${kVendor}${AC.MOZ_APP_BASENAME}DefaultBrowserAgentRegistryMutex`; let mutexFactory = Cc["@mozilla.org/windows-mutex-factory;1"].getService( Ci.nsIWindowsMutexFactory ); let mutex = mutexFactory.createMutex(kRegistryMutexName); mutex.tryLock(kRegistryMutexName); lazy.log.debug(`Locked named mutex: ${kRegistryMutexName}`); try { await aMutexGuardedFunction(); } finally { mutex.unlock(); lazy.log.debug(`Unlocked named mutex: ${kRegistryMutexName}`); } } async function doTask(defaultAgent, force) { if (!defaultAgent.appRanRecently() && !force) { lazy.log.warn("Main app has not ran recently, exiting without running."); throw new Error("App hasn't ran recently"); } let browser = defaultAgent.getDefaultBrowser(); lazy.log.debug(`Default browser: ${browser}`); let previousBrowser = defaultAgent.getReplacePreviousDefaultBrowser(browser); lazy.log.debug(`Previous browser: ${previousBrowser}`); let defaultPdfHandler = defaultAgent.getDefaultPdfHandler(); lazy.log.debug(`Default PDF Handler: ${defaultPdfHandler}`); let notificationTelemetry = { shown: kNotificationShown.notShown, action: kNotificationAction.noAction, }; if ((browser == "edge-chrome" && previousBrowser == "firefox") || force) { lazy.log.info("Showing default browser intervention notification."); const alertName = "default_agent_intervention"; let notification = showNotification(alertName); let timeout = makeTimeout(alertName); notificationTelemetry = await Promise.race([notification, timeout]); } lazy.log.debug(`Notification telemetry: ${notificationTelemetry}`); if ( notificationTelemetry.action == kNotificationAction.makeFirefoxDefaultButton || notificationTelemetry.action == kNotificationAction.toastClicked ) { await lazy.ShellService.setDefaultBrowser(false).catch(e => { lazy.log.error(`setDefaultBrowser failed: ${e}`); }); } defaultAgent.sendPing( browser, previousBrowser, defaultPdfHandler, notificationTelemetry.shown, notificationTelemetry.action ); } async function showNotification(name) { let notificationTelemetry = { shown: kNotificationShown.error, action: kNotificationAction.noAction, }; // Bug 1868714: We disable the notification server to defer on changes // necessary for it to work with Background Tasks. try { lazy.log.debug("Disabling notification server."); Services.prefs.setBoolPref( "alerts.useSystemBackend.windows.notificationserver.enabled", false ); const l10n = new Localization([ "branding/brand.ftl", // Background tasks are only used in a context where browser refs are // present; that it's in toolkit instead of browser is a historical // artifact of the default agent having previously been a // standalone application. // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit "browser/backgroundtasks/defaultagent.ftl", ]); let [title, body, yesButtonText, noButtonText] = await l10n.formatValues([ { id: "default-browser-notification-header-text" }, { id: "default-browser-notification-body-text" }, { id: "default-browser-notification-yes-button-text" }, { id: "default-browser-notification-no-button-text" }, ]); let yesAction = "yes-action"; let noAction = "no-action"; let alert = makeAlert({ name, title, body, actions: [ { action: yesAction, title: yesButtonText, }, { action: noAction, title: noButtonText, }, ], }); const { observer, shownPromise } = makeObserver({ yesAction, noAction }); lazy.AlertsService.showAlert(alert, observer); notificationTelemetry = await shownPromise.promise; } catch (e) { if (e.message) { lazy.log.error(e.message); } } finally { // Reset the pref so we can assume the default value in the future. lazy.log.debug("Reenabling notification server."); Services.prefs.clearUserPref( "alerts.useSystemBackend.windows.notificationserver.enabled" ); } return notificationTelemetry; } function makeAlert(options) { let winalert = Cc["@mozilla.org/windows-alert-notification;1"].createInstance( Ci.nsIWindowsAlertNotification ); winalert.handleActions = true; winalert.imagePlacement = winalert.eIcon; let alert = winalert.QueryInterface(Ci.nsIAlertNotification); let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); alert.init( options.name, "chrome://branding/content/about-logo@2x.png", options.title, options.body, true /* aTextClickable */, null /* aCookie */, null /* aDir */, null /* aLang */, null /* aData */, systemPrincipal, null /* aInPrivateBrowsing */, true /* aRequireInteraction */ ); alert.actions = options.actions; return alert; } function makeObserver(actions) { let shownPromise = Promise.withResolvers(); // We'll receive multiple callbacks which individually might indicate an // interaction. Only log the first one to disambiguate and reduce noise. let firstInteraction = true; let logFirstInteraction = message => { if (firstInteraction) { lazy.log.debug(message); firstInteraction = false; } }; let observer = (subject, topic, data) => { switch (topic) { case "alertactioncallback": switch (data) { case actions.yesAction: logFirstInteraction( 'Notification "yes" button clicked, setting default browser.' ); shownPromise.resolve({ shown: kNotificationShown.shown, action: kNotificationAction.makeFirefoxDefaultButton, }); break; case actions.noAction: logFirstInteraction("Notification dismissed by button."); shownPromise.resolve({ shown: kNotificationShown.shown, action: kNotificationAction.dismissedByButton, }); break; default: lazy.log.error(`Unrecognized notification action ${data}`); throw new Error(`Unexpected notification action received: ${data}`); } break; case "alertclickcallback": logFirstInteraction( "Notification body clicked, setting default browser." ); shownPromise.resolve({ shown: kNotificationShown.shown, action: kNotificationAction.toastClicked, }); break; case "alerterror": lazy.log.error("Error showing notification."); shownPromise.resolve({ shown: kNotificationShown.error, action: kNotificationAction.noAction, }); break; case "alertfinished": logFirstInteraction("Notification dismissed from action center."); shownPromise.resolve({ shown: kNotificationShown.shown, action: kNotificationAction.dismissedToActionCenter, }); break; } }; return { observer, shownPromise }; } function makeTimeout(alertName) { return new Promise(resolve => { // If the notification hasn't been activated or dismissed within 12 hours, // stop waiting for it. let timeoutMs = kNotificationTimeoutMs; // Allow overriding the notification timeout fron an environment variable. const envTimeoutKey = "MOZ_NOTIFICATION_TIMEOUT_MS"; if (Services.env.exists(envTimeoutKey)) { let envTimeoutValue = Services.env.get(envTimeoutKey); if (!isNaN(envTimeoutValue)) { timeoutMs = Number(envTimeoutValue); } else { lazy.log.error( `Environment variable ${envTimeoutKey}=${envTimeoutValue} is not a number.` ); } } lazy.log.info(`Registering notification timeout in ${timeoutMs}ms`); lazy.setTimeout(() => { lazy.log.warn(`Notification timed out after ${timeoutMs}ms`); lazy.AlertsService.closeAlert(alertName); resolve({ shown: kNotificationShown.shown, action: kNotificationAction.dismissedByTimeout, }); }, timeoutMs); }); }