339 lines
11 KiB
JavaScript
339 lines
11 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
|
DevToolsSocketStatus:
|
|
"resource://devtools/shared/security/DevToolsSocketStatus.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) {
|
|
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;
|
|
let maxRuntime;
|
|
if (lazy.DevToolsSocketStatus.hasSocketOpened()) {
|
|
lazy.log.info(
|
|
`Setting background task timeout period to indefinite because a DevTools server is listening.`
|
|
);
|
|
minimumReached = true;
|
|
maxRuntime = new Promise(() => {});
|
|
} else {
|
|
minRuntime = new Promise(resolve =>
|
|
lazy.setTimeout(() => {
|
|
minimumReached = true;
|
|
resolve(true);
|
|
}, timingSettings.minTaskRuntimeMS)
|
|
);
|
|
maxRuntime = new Promise(resolve =>
|
|
lazy.setTimeout(() => {
|
|
lazy.log.error(`Background task named '${name}' timed out`);
|
|
resolve(EXIT_CODE.TIMEOUT);
|
|
}, timingSettings.maxTaskRuntimeSec * 1000)
|
|
);
|
|
}
|
|
exitCode = await Promise.race([
|
|
maxRuntime,
|
|
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,
|
|
};
|