1 /* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 import { EXIT_CODE as EXIT_CODE_BASE } from "resource://gre/modules/BackgroundTasksManager.sys.mjs";
7 import { AppConstants as AC } from "resource://gre/modules/AppConstants.sys.mjs";
8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
12 DISABLED_BY_POLICY: EXIT_CODE_BASE.LAST_RESERVED + 1,
13 INVALID_ARGUMENT: EXIT_CODE_BASE.LAST_RESERVED + 2,
14 MUTEX_NOT_LOCKABLE: EXIT_CODE_BASE.LAST_RESERVED + 3,
18 ChromeUtils.defineESModuleGetters(lazy, {
19 setTimeout: "resource://gre/modules/Timer.sys.mjs",
20 BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
21 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
22 // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
23 ShellService: "resource:///modules/ShellService.sys.mjs",
25 XPCOMUtils.defineLazyServiceGetters(lazy, {
26 AlertsService: ["@mozilla.org/alerts-service;1", "nsIAlertsService"],
28 ChromeUtils.defineLazyGetter(lazy, "log", () => {
29 let { ConsoleAPI } = ChromeUtils.importESModule(
30 "resource://gre/modules/Console.sys.mjs"
32 let consoleOptions = {
34 maxLogLevelPref: "app.defaultagent.loglevel",
35 prefix: "DefaultAgent",
37 return new ConsoleAPI(consoleOptions);
40 // Should be slightly longer than kNotificationTimeoutMs and kGleanSendWait below
41 // (divided by 1000 to convert millseconds to seconds) to not cause race
44 // Additionally, should be less than the Windows Scheduled Task timeout
45 // execTimeLimitBStr in ScheduledTask.cpp.
47 // Current bounds are 11 hours 55 minutes 10 seconds and 12 hours 5 minutes.
48 export const backgroundTaskTimeoutSec = 12 * 60 * 60;
50 // 11 hours 55 minutes in milliseconds.
51 const kNotificationTimeoutMs = 11 * 60 * 60 * 1000 + 55 * 60 * 1000;
52 // 10 seconds in milliseconds.
53 const kGleanSendWait = 10000;
55 const kNotificationShown = Object.freeze({
56 notShown: "not-shown",
61 const kNotificationAction = Object.freeze({
62 dismissedByTimeout: "dismissed-by-timeout",
63 dismissedByButton: "dismissed-by-button",
64 dismissedToActionCenter: "dismissed-to-action-center",
65 makeFirefoxDefaultButton: "make-firefox-default-button",
66 toastClicked: "toast-clicked",
67 noAction: "no-action",
70 // We expect to be given a command string in argv[1], perhaps followed by other
71 // arguments depending on the command. The valid commands are:
72 // register-task [unique-token]
73 // Create a Windows scheduled task that will launch this binary with the
74 // do-task command every 24 hours, starting from 24 hours after register-task
75 // is run. unique-token is required and should be some string that uniquely
76 // identifies this installation of the product; typically this will be the
77 // install path hash that's used for the update directory, the AppUserModelID,
78 // and other related purposes.
79 // update-task [unique-token]
80 // Update an existing task registration, without changing its schedule. This
81 // should be called during updates of the application, in case this program
82 // has been updated and any of the task parameters have changed. The unique
83 // token argument is required and should be the same one that was passed in
84 // when the task was registered.
85 // unregister-task [unique-token]
86 // Removes the previously created task. The unique token argument is required
87 // and should be the same one that was passed in when the task was registered.
88 // uninstall [unique-token]
89 // Removes the previously created task, and also removes all registry entries
90 // running the task may have created. The unique token argument is required
91 // and should be the same one that was passed in when the task was registered.
92 // do-task [app-user-model-id]
93 // Actually performs the default agent task, which currently means generating
94 // and sending our telemetry ping and possibly showing a notification to the
95 // user if their browser has switched from Firefox to Edge with Blink.
96 // set-default-browser-user-choice [app-user-model-id] [[.file1 ProgIDRoot1]
98 // Set the default browser via the UserChoice registry keys. Additional
99 // optional file extensions to register can be specified as additional
100 // argument pairs: the first element is the file extension, the second element
101 // is the root of a ProgID, which will be suffixed with `-$AUMI`.
102 export async function runBackgroundTask(commandLine) {
103 Services.fog.initializeFOG(
105 "firefox.desktop.background.defaultagent"
108 let defaultAgent = Cc["@mozilla.org/default-agent;1"].getService(
112 let command = commandLine.getArgument(0);
114 // The uninstall and unregister commands are allowed even if the policy
115 // disabling the task is set, so that uninstalls and updates always work.
116 // Similarly, debug commands are always allowed.
119 let token = commandLine.getArgument(1);
120 lazy.log.info(`Uninstalling for token "${token}"`);
121 defaultAgent.uninstall(token);
122 return EXIT_CODE.SUCCESS;
124 case "unregister-task": {
125 let token = commandLine.getArgument(1);
126 lazy.log.info(`Unregistering task for token "${token}"`);
127 defaultAgent.unregisterTask(token);
128 return EXIT_CODE.SUCCESS;
132 // We check for disablement by policy because that's assumed to be static.
133 // But we don't check for disablement by remote settings so that
134 // `register-task` and `update-task` can proceed as part of the update
135 // cycle, waiting for remote (re-)enablement.
136 if (defaultAgent.agentDisabled()) {
137 lazy.log.warn("Default Agent disabled, exiting without running.");
138 return EXIT_CODE.DISABLED_BY_POLICY;
142 case "register-task": {
143 let token = commandLine.getArgument(1);
144 lazy.log.info(`Registering task for token "${token}"`);
145 defaultAgent.registerTask(token);
146 return EXIT_CODE.SUCCESS;
148 case "update-task": {
149 let token = commandLine.getArgument(1);
150 lazy.log.info(`Updating task for token "${token}"`);
151 defaultAgent.updateTask(token);
152 return EXIT_CODE.SUCCESS;
155 let aumid = commandLine.getArgument(1);
156 let force = commandLine.findFlag("force", true) != -1;
158 lazy.log.info(`Running do-task with AUMID "${aumid}"`);
160 let cppFallback = false;
162 await lazy.BackgroundTasksUtils.enableNimbus(commandLine);
164 lazy.NimbusFeatures.defaultAgent.getVariable("cppFallback");
166 lazy.log.error(`Error enabling nimbus: ${e}`);
171 lazy.log.info("Running JS do-task.");
172 await runWithRegistryLocked(async () => {
173 await doTask(defaultAgent, force);
176 lazy.log.info("Running C++ do-task.");
177 defaultAgent.doTask(aumid, force);
181 lazy.log.error(e.message);
184 if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
185 return EXIT_CODE.MUTEX_NOT_LOCKABLE;
188 return EXIT_CODE.EXCEPTION;
191 // Bug 1857333: We wait for arbitrary time for Glean to submit telemetry.
192 lazy.log.info("Pinged glean, waiting for submission.");
193 await new Promise(resolve => lazy.setTimeout(resolve, kGleanSendWait));
195 return EXIT_CODE.SUCCESS;
199 return EXIT_CODE.INVALID_ARGUMENT;
202 // Throws if unable to lock mutex (therefore function isn't run).
203 async function runWithRegistryLocked(aMutexGuardedFunction) {
204 const kVendor = Services.appinfo.vendor || "";
205 const kRegistryMutexName = `${kVendor}${AC.MOZ_APP_BASENAME}DefaultBrowserAgentRegistryMutex`;
206 let mutexFactory = Cc["@mozilla.org/windows-mutex-factory;1"].getService(
207 Ci.nsIWindowsMutexFactory
210 let mutex = mutexFactory.createMutex(kRegistryMutexName);
211 mutex.tryLock(kRegistryMutexName);
212 lazy.log.debug(`Locked named mutex: ${kRegistryMutexName}`);
214 await aMutexGuardedFunction();
217 lazy.log.debug(`Unlocked named mutex: ${kRegistryMutexName}`);
221 async function doTask(defaultAgent, force) {
222 if (!defaultAgent.appRanRecently() && !force) {
223 lazy.log.warn("Main app has not ran recently, exiting without running.");
224 throw new Error("App hasn't ran recently");
227 let browser = defaultAgent.getDefaultBrowser();
228 lazy.log.debug(`Default browser: ${browser}`);
229 let previousBrowser = defaultAgent.getReplacePreviousDefaultBrowser(browser);
230 lazy.log.debug(`Previous browser: ${previousBrowser}`);
231 let defaultPdfHandler = defaultAgent.getDefaultPdfHandler();
232 lazy.log.debug(`Default PDF Handler: ${defaultPdfHandler}`);
234 let notificationTelemetry = {
235 shown: kNotificationShown.notShown,
236 action: kNotificationAction.noAction,
238 if ((browser == "edge-chrome" && previousBrowser == "firefox") || force) {
239 lazy.log.info("Showing default browser intervention notification.");
241 const alertName = "default_agent_intervention";
242 let notification = showNotification(alertName);
243 let timeout = makeTimeout(alertName);
245 notificationTelemetry = await Promise.race([notification, timeout]);
247 lazy.log.debug(`Notification telemetry: ${notificationTelemetry}`);
250 notificationTelemetry.action ==
251 kNotificationAction.makeFirefoxDefaultButton ||
252 notificationTelemetry.action == kNotificationAction.toastClicked
254 await lazy.ShellService.setDefaultBrowser(false).catch(e => {
255 lazy.log.error(`setDefaultBrowser failed: ${e}`);
259 defaultAgent.sendPing(
263 notificationTelemetry.shown,
264 notificationTelemetry.action
268 async function showNotification(name) {
269 let notificationTelemetry = {
270 shown: kNotificationShown.error,
271 action: kNotificationAction.noAction,
274 // Bug 1868714: We disable the notification server to defer on changes
275 // necessary for it to work with Background Tasks.
277 lazy.log.debug("Disabling notification server.");
278 Services.prefs.setBoolPref(
279 "alerts.useSystemBackend.windows.notificationserver.enabled",
283 const l10n = new Localization([
284 "branding/brand.ftl",
285 // Background tasks are only used in a context where browser refs are
286 // present; that it's in toolkit instead of browser is a historical
287 // artifact of the default agent having previously been a
288 // standalone application.
289 // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
290 "browser/backgroundtasks/defaultagent.ftl",
292 let [title, body, yesButtonText, noButtonText] = await l10n.formatValues([
293 { id: "default-browser-notification-header-text" },
294 { id: "default-browser-notification-body-text" },
295 { id: "default-browser-notification-yes-button-text" },
296 { id: "default-browser-notification-no-button-text" },
299 let yesAction = "yes-action";
300 let noAction = "no-action";
302 let alert = makeAlert({
309 title: yesButtonText,
318 const { observer, shownPromise } = makeObserver({ yesAction, noAction });
320 lazy.AlertsService.showAlert(alert, observer);
322 notificationTelemetry = await shownPromise.promise;
325 lazy.log.error(e.message);
328 // Reset the pref so we can assume the default value in the future.
329 lazy.log.debug("Reenabling notification server.");
330 Services.prefs.clearUserPref(
331 "alerts.useSystemBackend.windows.notificationserver.enabled"
335 return notificationTelemetry;
338 function makeAlert(options) {
339 let winalert = Cc["@mozilla.org/windows-alert-notification;1"].createInstance(
340 Ci.nsIWindowsAlertNotification
342 winalert.handleActions = true;
343 winalert.imagePlacement = winalert.eIcon;
345 let alert = winalert.QueryInterface(Ci.nsIAlertNotification);
346 let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
349 "chrome://branding/content/about-logo@2x.png",
352 true /* aTextClickable */,
358 null /* aInPrivateBrowsing */,
359 true /* aRequireInteraction */
362 alert.actions = options.actions;
367 function makeObserver(actions) {
368 let shownPromise = Promise.withResolvers();
370 // We'll receive multiple callbacks which individually might indicate an
371 // interaction. Only log the first one to disambiguate and reduce noise.
372 let firstInteraction = true;
373 let logFirstInteraction = message => {
374 if (firstInteraction) {
375 lazy.log.debug(message);
376 firstInteraction = false;
380 let observer = (subject, topic, data) => {
382 case "alertactioncallback":
384 case actions.yesAction:
386 'Notification "yes" button clicked, setting default browser.'
388 shownPromise.resolve({
389 shown: kNotificationShown.shown,
390 action: kNotificationAction.makeFirefoxDefaultButton,
393 case actions.noAction:
394 logFirstInteraction("Notification dismissed by button.");
395 shownPromise.resolve({
396 shown: kNotificationShown.shown,
397 action: kNotificationAction.dismissedByButton,
401 lazy.log.error(`Unrecognized notification action ${data}`);
402 throw new Error(`Unexpected notification action received: ${data}`);
405 case "alertclickcallback":
407 "Notification body clicked, setting default browser."
409 shownPromise.resolve({
410 shown: kNotificationShown.shown,
411 action: kNotificationAction.toastClicked,
415 lazy.log.error("Error showing notification.");
416 shownPromise.resolve({
417 shown: kNotificationShown.error,
418 action: kNotificationAction.noAction,
421 case "alertfinished":
422 logFirstInteraction("Notification dismissed from action center.");
423 shownPromise.resolve({
424 shown: kNotificationShown.shown,
425 action: kNotificationAction.dismissedToActionCenter,
431 return { observer, shownPromise };
434 function makeTimeout(alertName) {
435 return new Promise(resolve => {
436 // If the notification hasn't been activated or dismissed within 12 hours,
437 // stop waiting for it.
438 let timeoutMs = kNotificationTimeoutMs;
440 // Allow overriding the notification timeout fron an environment variable.
441 const envTimeoutKey = "MOZ_NOTIFICATION_TIMEOUT_MS";
442 if (Services.env.exists(envTimeoutKey)) {
443 let envTimeoutValue = Services.env.get(envTimeoutKey);
444 if (!isNaN(envTimeoutValue)) {
445 timeoutMs = Number(envTimeoutValue);
448 `Environment variable ${envTimeoutKey}=${envTimeoutValue} is not a number.`
452 lazy.log.info(`Registering notification timeout in ${timeoutMs}ms`);
454 lazy.setTimeout(() => {
455 lazy.log.warn(`Notification timed out after ${timeoutMs}ms`);
457 lazy.AlertsService.closeAlert(alertName);
460 shown: kNotificationShown.shown,
461 action: kNotificationAction.dismissedByTimeout,