diff options
Diffstat (limited to 'toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs')
-rw-r--r-- | toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs b/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs new file mode 100644 index 0000000000..dcf17625f7 --- /dev/null +++ b/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs @@ -0,0 +1,327 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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 = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + let consoleOptions = { + // tip: set maxLogLevel to "debug" and use log.debug() to create detailed + // messages during development. See LOG_LEVELS in Console.sys.mjs for details. + maxLogLevel: "error", + maxLogLevelPref: "toolkit.backgroundtasks.loglevel", + prefix: "BackgroundTasksManager", + }; + return new ConsoleAPI(consoleOptions); +}); + +ChromeUtils.defineLazyGetter(lazy, "DevToolsStartup", () => { + return Cc["@mozilla.org/devtools/startup-clh;1"].getService( + Ci.nsICommandLineHandler + ).wrappedJSObject; +}); + +// The default timing settings can be overriden by the preferences +// toolkit.backgroundtasks.defaultTimeoutSec and +// toolkit.backgroundtasks.defaultMinTaskRuntimeMS for all background tasks +// and individually per module by +// export const backgroundTaskTimeoutSec = X; +// export const backgroundTaskMinRuntimeMS = Y; +let timingSettings = { + minTaskRuntimeMS: 500, + maxTaskRuntimeSec: 600, // 10 minutes. +}; + +// Map resource://testing-common/ to the shared test modules directory. This is +// a transliteration of `register_modules_protocol_handler` from +// https://searchfox.org/mozilla-central/rev/f081504642a115cb8236bea4d8250e5cb0f39b02/testing/xpcshell/head.js#358-389. +function registerModulesProtocolHandler() { + let _TESTING_MODULES_URI = Services.env.get( + "XPCSHELL_TESTING_MODULES_URI", + "" + ); + if (!_TESTING_MODULES_URI) { + return false; + } + + let protocolHandler = Services.io + .getProtocolHandler("resource") + .QueryInterface(Ci.nsIResProtocolHandler); + + protocolHandler.setSubstitution( + "testing-common", + Services.io.newURI(_TESTING_MODULES_URI) + ); + // Log loudly so that when testing, we always actually use the + // console logging mechanism and therefore deterministically load that code. + lazy.log.error( + `Substitution set: resource://testing-common aliases ${_TESTING_MODULES_URI}` + ); + + return true; +} + +function locationsForBackgroundTaskNamed(name) { + const subModules = [ + "resource:///modules", // App-specific first. + "resource://gre/modules", // Toolkit/general second. + ]; + + if (registerModulesProtocolHandler()) { + subModules.push("resource://testing-common"); // Test-only third. + } + + let locations = []; + for (const subModule of subModules) { + let URI = `${subModule}/backgroundtasks/BackgroundTask_${name}.sys.mjs`; + locations.push(URI); + } + + return locations; +} + +/** + * Find an ES module named like `backgroundtasks/BackgroundTask_${name}.sys.mjs`, + * import it, and return the whole module. + * + * When testing, allow to load from `XPCSHELL_TESTING_MODULES_URI`, + * which is registered at `resource://testing-common`, the standard + * location for test-only modules. + * + * @return {Object} The imported module. + * @throws NS_ERROR_NOT_AVAILABLE if a background task with the given `name` is + * not found. + */ +function findBackgroundTaskModule(name) { + for (const URI of locationsForBackgroundTaskNamed(name)) { + lazy.log.debug(`Looking for background task at URI: ${URI}`); + + try { + const taskModule = ChromeUtils.importESModule(URI); + lazy.log.info(`Found background task at URI: ${URI}`); + return taskModule; + } catch (ex) { + if (ex.result != Cr.NS_ERROR_FILE_NOT_FOUND) { + throw ex; + } + } + } + + lazy.log.warn(`No backgroundtask named '${name}' registered`); + throw new Components.Exception( + `No backgroundtask named '${name}' registered`, + Cr.NS_ERROR_NOT_AVAILABLE + ); +} + +export class BackgroundTasksManager { + get helpInfo() { + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + + if (bts.isBackgroundTaskMode) { + return lazy.DevToolsStartup.jsdebuggerHelpInfo; + } + + return ""; + } + + handle(commandLine) { + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + + if (!bts.isBackgroundTaskMode) { + lazy.log.info( + `${Services.appinfo.processID}: !isBackgroundTaskMode, exiting` + ); + return; + } + + const name = bts.backgroundTaskName(); + lazy.log.info( + `${Services.appinfo.processID}: Preparing to run background task named '${name}'` + + ` (with ${commandLine.length} arguments)` + ); + + if (!("@mozilla.org/devtools/startup-clh;1" in Cc)) { + return; + } + + // Check this before the devtools startup flow handles and removes it. + const CASE_INSENSITIVE = false; + if ( + commandLine.findFlag("jsdebugger", CASE_INSENSITIVE) < 0 && + commandLine.findFlag("start-debugger-server", CASE_INSENSITIVE) < 0 + ) { + lazy.log.info( + `${Services.appinfo.processID}: No devtools flag found; not preparing devtools thread` + ); + return; + } + + const waitFlag = + commandLine.findFlag("wait-for-jsdebugger", CASE_INSENSITIVE) != -1; + if (waitFlag) { + function onDevtoolsThreadReady(subject, topic, data) { + lazy.log.info( + `${Services.appinfo.processID}: Setting breakpoints for background task named '${name}'` + + ` (with ${commandLine.length} arguments)` + ); + + const threadActor = subject.wrappedJSObject; + threadActor.setBreakpointOnLoad(locationsForBackgroundTaskNamed(name)); + + Services.obs.removeObserver(onDevtoolsThreadReady, topic); + } + + Services.obs.addObserver(onDevtoolsThreadReady, "devtools-thread-ready"); + } + + const DevToolsStartup = Cc[ + "@mozilla.org/devtools/startup-clh;1" + ].getService(Ci.nsICommandLineHandler); + DevToolsStartup.handle(commandLine); + } + + async runBackgroundTaskNamed(name, commandLine) { + function addMarker(markerName) { + return ChromeUtils.addProfilerMarker(markerName, undefined, name); + } + addMarker("BackgroundTasksManager:AfterRunBackgroundTaskNamed"); + + lazy.log.info( + `${Services.appinfo.processID}: Running background task named '${name}'` + + ` (with ${commandLine.length} arguments)` + ); + lazy.log.debug( + `${Services.appinfo.processID}: Background task using profile` + + ` '${Services.dirsvc.get("ProfD", Ci.nsIFile).path}'` + ); + + let exitCode = EXIT_CODE.NOT_FOUND; + try { + let taskModule = findBackgroundTaskModule(name); + addMarker("BackgroundTasksManager:AfterFindRunBackgroundTask"); + + // Get timing configuration. First check for default preferences, + // then for per module overrides. + timingSettings.minTaskRuntimeMS = Services.prefs.getIntPref( + "toolkit.backgroundtasks.defaultMinTaskRuntimeMS", + timingSettings.minTaskRuntimeMS + ); + if (taskModule.backgroundTaskMinRuntimeMS) { + timingSettings.minTaskRuntimeMS = taskModule.backgroundTaskMinRuntimeMS; + } + timingSettings.maxTaskRuntimeSec = Services.prefs.getIntPref( + "toolkit.backgroundtasks.defaultTimeoutSec", + timingSettings.maxTaskRuntimeSec + ); + if (taskModule.backgroundTaskTimeoutSec) { + timingSettings.maxTaskRuntimeSec = taskModule.backgroundTaskTimeoutSec; + } + + try { + let minimumReached = false; + let minRuntime = new Promise(resolve => + lazy.setTimeout(() => { + minimumReached = true; + resolve(true); + }, timingSettings.minTaskRuntimeMS) + ); + exitCode = await Promise.race([ + new Promise(resolve => + lazy.setTimeout(() => { + lazy.log.error(`Background task named '${name}' timed out`); + resolve(EXIT_CODE.TIMEOUT); + }, timingSettings.maxTaskRuntimeSec * 1000) + ), + taskModule.runBackgroundTask(commandLine), + ]); + if (!minimumReached) { + lazy.log.debug( + `Backgroundtask named '${name}' waiting for minimum runtime.` + ); + await minRuntime; + } + lazy.log.info( + `Backgroundtask named '${name}' completed with exit code ${exitCode}` + ); + } catch (e) { + lazy.log.error(`Backgroundtask named '${name}' threw exception`, e); + exitCode = EXIT_CODE.EXCEPTION; + } + } finally { + addMarker("BackgroundTasksManager:AfterAwaitRunBackgroundTask"); + + lazy.log.info(`Invoking Services.startup.quit(..., ${exitCode})`); + Services.startup.quit(Ci.nsIAppStartup.eForceQuit, exitCode); + } + + return exitCode; + } + + classID = Components.ID("{4d48c536-e16f-4699-8f9c-add4f28f92f0}"); + QueryInterface = ChromeUtils.generateQI([ + "nsIBackgroundTasksManager", + "nsICommandLineHandler", + ]); +} + +/** + * Background tasks should standard exit code conventions where 0 denotes + * success and non-zero denotes failure and/or an error. In addition, since + * background tasks have limited channels to communicate with consumers, the + * special values `NOT_FOUND` (integer 2) and `THREW_EXCEPTION` (integer 3) are + * distinguished. + * + * If you extend this to add background task-specific exit codes, use exit codes + * greater than 10 to allow for additional shared exit codes to be added here. + * Exit codes should be between 0 and 127 to be safe across platforms. + */ +export const EXIT_CODE = { + /** + * The task succeeded. + * + * The `runBackgroundTask(...)` promise resolved to 0. + */ + SUCCESS: 0, + + /** + * The task with the specified name could not be found or imported. + * + * The corresponding `runBackgroundTask` method could not be found. + */ + NOT_FOUND: 2, + + /** + * The task failed with an uncaught exception. + * + * The `runBackgroundTask(...)` promise rejected with an exception. + */ + EXCEPTION: 3, + + /** + * The task took too long and timed out. + * + * The default timeout is controlled by the pref: + * "toolkit.backgroundtasks.defaultTimeoutSec", but tasks can override this + * by exporting a non-zero `backgroundTaskTimeoutSec` value. + */ + TIMEOUT: 4, + + /** + * The last exit code reserved by this structure. Use codes larger than this + * code for background task-specific exit codes. + */ + LAST_RESERVED: 10, +}; |