Files
tubestation/toolkit/components/backgroundtasks/BackgroundTasksManager.sys.mjs
Jens Stutte 3ea6b78b60 Bug 1832252 - Have a configurable minimum runtime for background tasks. r=nalexander
Background tasks are potentially very short living, such that things launched asynchronously during process startup might not have finished initializing when we are asked to shutdown.

In order to mitigate this, we introduce a configurable `backgroundTaskMinRuntimeMS` (default 500ms) that guarantees that a background task will last at least that time.

Documentation will be added in bug 1833198.

Differential Revision: https://phabricator.services.mozilla.com/D177879
2023-05-15 20:25:21 +00:00

330 lines
10 KiB
JavaScript

/* -*- 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
XPCOMUtils.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);
});
XPCOMUtils.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,
};