forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			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,
 | |
| };
 | 
