Backed out 3 changesets (bug 1883476, bug 1826375) for causing windows build bustages...
[gecko.git] / toolkit / mozapps / defaultagent / BackgroundTask_defaultagent.sys.mjs
blobe6eca7b0bec0fbbe5fa83e017bb88b04d0a3a17c
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";
10 const EXIT_CODE = {
11   ...EXIT_CODE_BASE,
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,
17 const lazy = {};
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",
24 });
25 XPCOMUtils.defineLazyServiceGetters(lazy, {
26   AlertsService: ["@mozilla.org/alerts-service;1", "nsIAlertsService"],
27 });
28 ChromeUtils.defineLazyGetter(lazy, "log", () => {
29   let { ConsoleAPI } = ChromeUtils.importESModule(
30     "resource://gre/modules/Console.sys.mjs"
31   );
32   let consoleOptions = {
33     maxLogLevel: "error",
34     maxLogLevelPref: "app.defaultagent.loglevel",
35     prefix: "DefaultAgent",
36   };
37   return new ConsoleAPI(consoleOptions);
38 });
40 // Should be slightly longer than kNotificationTimeoutMs and kGleanSendWait below
41 // (divided by 1000 to convert millseconds to seconds) to not cause race
42 // between timeouts.
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",
57   shown: "shown",
58   error: "error",
59 });
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",
68 });
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]
97 // ...]
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(
104     undefined,
105     "firefox.desktop.background.defaultagent"
106   );
108   let defaultAgent = Cc["@mozilla.org/default-agent;1"].getService(
109     Ci.nsIDefaultAgent
110   );
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.
117   switch (command) {
118     case "uninstall": {
119       let token = commandLine.getArgument(1);
120       lazy.log.info(`Uninstalling for token "${token}"`);
121       defaultAgent.uninstall(token);
122       return EXIT_CODE.SUCCESS;
123     }
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;
129     }
130   }
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;
139   }
141   switch (command) {
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;
147     }
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;
153     }
154     case "do-task": {
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;
161       try {
162         await lazy.BackgroundTasksUtils.enableNimbus(commandLine);
163         cppFallback =
164           lazy.NimbusFeatures.defaultAgent.getVariable("cppFallback");
165       } catch (e) {
166         lazy.log.error(`Error enabling nimbus: ${e}`);
167       }
169       try {
170         if (!cppFallback) {
171           lazy.log.info("Running JS do-task.");
172           await runWithRegistryLocked(async () => {
173             await doTask(defaultAgent, force);
174           });
175         } else {
176           lazy.log.info("Running C++ do-task.");
177           defaultAgent.doTask(aumid, force);
178         }
179       } catch (e) {
180         if (e.message) {
181           lazy.log.error(e.message);
182         }
184         if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
185           return EXIT_CODE.MUTEX_NOT_LOCKABLE;
186         }
188         return EXIT_CODE.EXCEPTION;
189       }
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;
196     }
197   }
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
208   );
210   let mutex = mutexFactory.createMutex(kRegistryMutexName);
211   mutex.tryLock(kRegistryMutexName);
212   lazy.log.debug(`Locked named mutex: ${kRegistryMutexName}`);
213   try {
214     await aMutexGuardedFunction();
215   } finally {
216     mutex.unlock();
217     lazy.log.debug(`Unlocked named mutex: ${kRegistryMutexName}`);
218   }
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");
225   }
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,
237   };
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]);
246   }
247   lazy.log.debug(`Notification telemetry: ${notificationTelemetry}`);
249   if (
250     notificationTelemetry.action ==
251       kNotificationAction.makeFirefoxDefaultButton ||
252     notificationTelemetry.action == kNotificationAction.toastClicked
253   ) {
254     await lazy.ShellService.setDefaultBrowser(false).catch(e => {
255       lazy.log.error(`setDefaultBrowser failed: ${e}`);
256     });
257   }
259   defaultAgent.sendPing(
260     browser,
261     previousBrowser,
262     defaultPdfHandler,
263     notificationTelemetry.shown,
264     notificationTelemetry.action
265   );
268 async function showNotification(name) {
269   let notificationTelemetry = {
270     shown: kNotificationShown.error,
271     action: kNotificationAction.noAction,
272   };
274   // Bug 1868714: We disable the notification server to defer on changes
275   // necessary for it to work with Background Tasks.
276   try {
277     lazy.log.debug("Disabling notification server.");
278     Services.prefs.setBoolPref(
279       "alerts.useSystemBackend.windows.notificationserver.enabled",
280       false
281     );
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",
291     ]);
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" },
297     ]);
299     let yesAction = "yes-action";
300     let noAction = "no-action";
302     let alert = makeAlert({
303       name,
304       title,
305       body,
306       actions: [
307         {
308           action: yesAction,
309           title: yesButtonText,
310         },
311         {
312           action: noAction,
313           title: noButtonText,
314         },
315       ],
316     });
318     const { observer, shownPromise } = makeObserver({ yesAction, noAction });
320     lazy.AlertsService.showAlert(alert, observer);
322     notificationTelemetry = await shownPromise.promise;
323   } catch (e) {
324     if (e.message) {
325       lazy.log.error(e.message);
326     }
327   } finally {
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"
332     );
333   }
335   return notificationTelemetry;
338 function makeAlert(options) {
339   let winalert = Cc["@mozilla.org/windows-alert-notification;1"].createInstance(
340     Ci.nsIWindowsAlertNotification
341   );
342   winalert.handleActions = true;
343   winalert.imagePlacement = winalert.eIcon;
345   let alert = winalert.QueryInterface(Ci.nsIAlertNotification);
346   let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
347   alert.init(
348     options.name,
349     "chrome://branding/content/about-logo@2x.png",
350     options.title,
351     options.body,
352     true /* aTextClickable */,
353     null /* aCookie */,
354     null /* aDir */,
355     null /* aLang */,
356     null /* aData */,
357     systemPrincipal,
358     null /* aInPrivateBrowsing */,
359     true /* aRequireInteraction */
360   );
362   alert.actions = options.actions;
364   return alert;
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;
377     }
378   };
380   let observer = (subject, topic, data) => {
381     switch (topic) {
382       case "alertactioncallback":
383         switch (data) {
384           case actions.yesAction:
385             logFirstInteraction(
386               'Notification "yes" button clicked, setting default browser.'
387             );
388             shownPromise.resolve({
389               shown: kNotificationShown.shown,
390               action: kNotificationAction.makeFirefoxDefaultButton,
391             });
392             break;
393           case actions.noAction:
394             logFirstInteraction("Notification dismissed by button.");
395             shownPromise.resolve({
396               shown: kNotificationShown.shown,
397               action: kNotificationAction.dismissedByButton,
398             });
399             break;
400           default:
401             lazy.log.error(`Unrecognized notification action ${data}`);
402             throw new Error(`Unexpected notification action received: ${data}`);
403         }
404         break;
405       case "alertclickcallback":
406         logFirstInteraction(
407           "Notification body clicked, setting default browser."
408         );
409         shownPromise.resolve({
410           shown: kNotificationShown.shown,
411           action: kNotificationAction.toastClicked,
412         });
413         break;
414       case "alerterror":
415         lazy.log.error("Error showing notification.");
416         shownPromise.resolve({
417           shown: kNotificationShown.error,
418           action: kNotificationAction.noAction,
419         });
420         break;
421       case "alertfinished":
422         logFirstInteraction("Notification dismissed from action center.");
423         shownPromise.resolve({
424           shown: kNotificationShown.shown,
425           action: kNotificationAction.dismissedToActionCenter,
426         });
427         break;
428     }
429   };
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);
446       } else {
447         lazy.log.error(
448           `Environment variable ${envTimeoutKey}=${envTimeoutValue} is not a number.`
449         );
450       }
451     }
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);
459       resolve({
460         shown: kNotificationShown.shown,
461         action: kNotificationAction.dismissedByTimeout,
462       });
463     }, timeoutMs);
464   });