forked from mirrors/gecko-dev
		
	 691543ee89
			
		
	
	
		691543ee89
		
	
	
	
	
		
			
			Automatic changes by ESLint, except for manual corrections for .xml files. Differential Revision: https://phabricator.services.mozilla.com/D4439 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			1031 lines
		
	
	
	
		
			34 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1031 lines
		
	
	
	
		
			34 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| /**
 | |
|  * Managing safe shutdown of asynchronous services.
 | |
|  *
 | |
|  * Firefox shutdown is composed of phases that take place
 | |
|  * sequentially. Typically, each shutdown phase removes some
 | |
|  * capabilities from the application. For instance, at the end of
 | |
|  * phase profileBeforeChange, no service is permitted to write to the
 | |
|  * profile directory (with the exception of Telemetry). Consequently,
 | |
|  * if any service has requested I/O to the profile directory before or
 | |
|  * during phase profileBeforeChange, the system must be informed that
 | |
|  * these requests need to be completed before the end of phase
 | |
|  * profileBeforeChange. Failing to inform the system of this
 | |
|  * requirement can (and has been known to) cause data loss.
 | |
|  *
 | |
|  * Example: At some point during shutdown, the Add-On Manager needs to
 | |
|  * ensure that all add-ons have safely written their data to disk,
 | |
|  * before writing its own data. Since the data is saved to the
 | |
|  * profile, this must be completed during phase profileBeforeChange.
 | |
|  *
 | |
|  * AsyncShutdown.profileBeforeChange.addBlocker(
 | |
|  *   "Add-on manager: shutting down",
 | |
|  *   function condition() {
 | |
|  *     // Do things.
 | |
|  *     // Perform I/O that must take place during phase profile-before-change
 | |
|  *     return promise;
 | |
|  *   }
 | |
|  * });
 | |
|  *
 | |
|  * In this example, function |condition| will be called at some point
 | |
|  * during phase profileBeforeChange and phase profileBeforeChange
 | |
|  * itself is guaranteed to not terminate until |promise| is either
 | |
|  * resolved or rejected.
 | |
|  */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
 | |
| ChromeUtils.import("resource://gre/modules/Services.jsm", this);
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(this, "PromiseUtils",
 | |
|   "resource://gre/modules/PromiseUtils.jsm");
 | |
| ChromeUtils.defineModuleGetter(this, "Task",
 | |
|   "resource://gre/modules/Task.jsm");
 | |
| XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
 | |
|   "@mozilla.org/xpcom/debug;1", "nsIDebug2");
 | |
| Object.defineProperty(this, "gCrashReporter", {
 | |
|   get() {
 | |
|     delete this.gCrashReporter;
 | |
|     try {
 | |
|       let reporter = Cc["@mozilla.org/xre/app-info;1"].
 | |
|             getService(Ci.nsICrashReporter);
 | |
|       return this.gCrashReporter = reporter;
 | |
|     } catch (ex) {
 | |
|       return this.gCrashReporter = null;
 | |
|     }
 | |
|   },
 | |
|   configurable: true,
 | |
| });
 | |
| 
 | |
| // `true` if this is a content process, `false` otherwise.
 | |
| // It would be nicer to go through `Services.appinfo`, but some tests need to be
 | |
| // able to replace that field with a custom implementation before it is first
 | |
| // called.
 | |
| // eslint-disable-next-line mozilla/use-services
 | |
| const isContent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
 | |
| 
 | |
| // Display timeout warnings after 10 seconds
 | |
| const DELAY_WARNING_MS = 10 * 1000;
 | |
| 
 | |
| 
 | |
| // Crash the process if shutdown is really too long
 | |
| // (allowing for sleep).
 | |
| const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
 | |
| var DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS,
 | |
|                                                60 * 1000); // One minute
 | |
| Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
 | |
|   DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
 | |
| });
 | |
| 
 | |
| /**
 | |
|  * A set of Promise that supports waiting.
 | |
|  *
 | |
|  * Promise items may be added or removed during the wait. The wait will
 | |
|  * resolve once all Promise items have been resolved or removed.
 | |
|  */
 | |
| function PromiseSet() {
 | |
|   /**
 | |
|    * key: the Promise passed pass the client of the `PromiseSet`.
 | |
|    * value: an indirection on top of `key`, as an object with
 | |
|    *   the following fields:
 | |
|    *   - indirection: a Promise resolved if `key` is resolved or
 | |
|    *     if `resolve` is called
 | |
|    *   - resolve: a function used to resolve the indirection.
 | |
|    */
 | |
|   this._indirections = new Map();
 | |
| }
 | |
| PromiseSet.prototype = {
 | |
|   /**
 | |
|    * Wait until all Promise have been resolved or removed.
 | |
|    *
 | |
|    * Note that calling `wait()` causes Promise to be removed from the
 | |
|    * Set once they are resolved.
 | |
|    *
 | |
|    * @return {Promise} Resolved once all Promise have been resolved or removed,
 | |
|    * or rejected after at least one Promise has rejected.
 | |
|    */
 | |
|   wait() {
 | |
|     // Pick an arbitrary element in the map, if any exists.
 | |
|     let entry = this._indirections.entries().next();
 | |
|     if (entry.done) {
 | |
|       // No indirections left, we are done.
 | |
|       return Promise.resolve();
 | |
|     }
 | |
| 
 | |
|     let [, indirection] = entry.value;
 | |
|     let promise = indirection.promise;
 | |
|     promise = promise.then(() =>
 | |
|       // At this stage, the entry has been cleaned up.
 | |
|       this.wait()
 | |
|     );
 | |
|     return promise;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Add a new Promise to the set.
 | |
|    *
 | |
|    * Calls to wait (including ongoing calls) will only return once
 | |
|    * `key` has either resolved or been removed.
 | |
|    */
 | |
|   add(key) {
 | |
|     this._ensurePromise(key);
 | |
|     let indirection = PromiseUtils.defer();
 | |
|     key.then(
 | |
|       x => {
 | |
|         // Clean up immediately.
 | |
|         // This needs to be done before the call to `resolve`, otherwise
 | |
|         // `wait()` may loop forever.
 | |
|         this._indirections.delete(key);
 | |
|         indirection.resolve(x);
 | |
|       },
 | |
|       err => {
 | |
|         this._indirections.delete(key);
 | |
|         indirection.reject(err);
 | |
|       });
 | |
|     this._indirections.set(key, indirection);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Remove a Promise from the set.
 | |
|    *
 | |
|    * Calls to wait (including ongoing calls) will ignore this promise,
 | |
|    * unless it is added again.
 | |
|    */
 | |
|   delete(key) {
 | |
|     this._ensurePromise(key);
 | |
|     let value = this._indirections.get(key);
 | |
|     if (!value) {
 | |
|       return false;
 | |
|     }
 | |
|     this._indirections.delete(key);
 | |
|     value.resolve();
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   _ensurePromise(key) {
 | |
|     if (!key || typeof key != "object") {
 | |
|       throw new Error("Expected an object");
 | |
|     }
 | |
|     if ((!("then" in key)) || typeof key.then != "function") {
 | |
|       throw new Error("Expected a Promise");
 | |
|     }
 | |
|   },
 | |
| 
 | |
| };
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Display a warning.
 | |
|  *
 | |
|  * As this code is generally used during shutdown, there are chances
 | |
|  * that the UX will not be available to display warnings on the
 | |
|  * console. We therefore use dump() rather than Cu.reportError().
 | |
|  */
 | |
| function log(msg, prefix = "", error = null) {
 | |
|   try {
 | |
|     dump(prefix + msg + "\n");
 | |
|     if (error) {
 | |
|       dump(prefix + error + "\n");
 | |
|       if (typeof error == "object" && "stack" in error) {
 | |
|         dump(prefix + error.stack + "\n");
 | |
|       }
 | |
|     }
 | |
|   } catch (ex) {
 | |
|     dump("INTERNAL ERROR in AsyncShutdown: cannot log message.\n");
 | |
|   }
 | |
| }
 | |
| const PREF_DEBUG_LOG = "toolkit.asyncshutdown.log";
 | |
| var DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG, false);
 | |
| Services.prefs.addObserver(PREF_DEBUG_LOG, function() {
 | |
|   DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
 | |
| });
 | |
| 
 | |
| function debug(msg, error = null) {
 | |
|   if (DEBUG_LOG) {
 | |
|     log(msg, "DEBUG: ", error);
 | |
|   }
 | |
| }
 | |
| function warn(msg, error = null) {
 | |
|   log(msg, "WARNING: ", error);
 | |
| }
 | |
| function fatalerr(msg, error = null) {
 | |
|   log(msg, "FATAL ERROR: ", error);
 | |
| }
 | |
| 
 | |
| // Utility function designed to get the current state of execution
 | |
| // of a blocker.
 | |
| // We are a little paranoid here to ensure that in case of evaluation
 | |
| // error we do not block the AsyncShutdown.
 | |
| function safeGetState(fetchState) {
 | |
|   if (!fetchState) {
 | |
|     return "(none)";
 | |
|   }
 | |
|   let data, string;
 | |
|   try {
 | |
|     // Evaluate fetchState(), normalize the result into something that we can
 | |
|     // safely stringify or upload.
 | |
|     let state = fetchState();
 | |
|     if (!state) {
 | |
|       return "(none)";
 | |
|     }
 | |
|     string = JSON.stringify(state);
 | |
|     data = JSON.parse(string);
 | |
|     // Simplify the rest of the code by ensuring that we can simply
 | |
|     // concatenate the result to a message.
 | |
|     if (data && typeof data == "object") {
 | |
|       data.toString = function() {
 | |
|         return string;
 | |
|       };
 | |
|     }
 | |
|     return data;
 | |
|   } catch (ex) {
 | |
| 
 | |
|     // Make sure that this causes test failures
 | |
|     Promise.reject(ex);
 | |
| 
 | |
|     if (string) {
 | |
|       return string;
 | |
|     }
 | |
|     try {
 | |
|       return "Error getting state: " + ex + " at " + ex.stack;
 | |
|     } catch (ex2) {
 | |
|       return "Error getting state but could not display error";
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Countdown for a given duration, skipping beats if the computer is too busy,
 | |
|  * sleeping or otherwise unavailable.
 | |
|  *
 | |
|  * @param {number} delay An approximate delay to wait in milliseconds (rounded
 | |
|  * up to the closest second).
 | |
|  *
 | |
|  * @return Deferred
 | |
|  */
 | |
| function looseTimer(delay) {
 | |
|   let DELAY_BEAT = 1000;
 | |
|   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | |
|   let beats = Math.ceil(delay / DELAY_BEAT);
 | |
|   let deferred = PromiseUtils.defer();
 | |
|   timer.initWithCallback(function() {
 | |
|     if (beats <= 0) {
 | |
|       deferred.resolve();
 | |
|     }
 | |
|     --beats;
 | |
|   }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
 | |
|   // Ensure that the timer is both canceled once we are done with it
 | |
|   // and not garbage-collected until then.
 | |
|   deferred.promise.then(() => timer.cancel(), () => timer.cancel());
 | |
|   return deferred;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Given an nsIStackFrame object, find the caller filename, line number,
 | |
|  * and stack if necessary, and return them as an object.
 | |
|  *
 | |
|  * @param {nsIStackFrame} topFrame Top frame of the call stack.
 | |
|  * @param {string} filename Pre-supplied filename or null if unknown.
 | |
|  * @param {number} lineNumber Pre-supplied line number or null if unknown.
 | |
|  * @param {string} stack Pre-supplied stack or null if unknown.
 | |
|  *
 | |
|  * @return object
 | |
|  */
 | |
| function getOrigin(topFrame, filename = null, lineNumber = null, stack = null) {
 | |
|   try {
 | |
|     // Determine the filename and line number of the caller.
 | |
|     let frame = topFrame;
 | |
| 
 | |
|     for (; frame && frame.filename == topFrame.filename; frame = frame.caller) {
 | |
|       // Climb up the stack
 | |
|     }
 | |
| 
 | |
|     if (filename == null) {
 | |
|       filename = frame ? frame.filename : "?";
 | |
|     }
 | |
|     if (lineNumber == null) {
 | |
|       lineNumber = frame ? frame.lineNumber : 0;
 | |
|     }
 | |
|     if (stack == null) {
 | |
|       // Now build the rest of the stack as a string, using Task.jsm's rewriting
 | |
|       // to ensure that we do not lose information at each call to `Task.spawn`.
 | |
|       let frames = [];
 | |
|       while (frame != null) {
 | |
|         frames.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber);
 | |
|         frame = frame.caller;
 | |
|       }
 | |
|       stack = frames.join("\n");
 | |
|       // Avoid loading Task.jsm if there's no task on the stack.
 | |
|       if (stack.includes("/Task.jsm:")) {
 | |
|         stack = Task.Debugging.generateReadableStack(stack);
 | |
|       }
 | |
|       stack = stack.split("\n");
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       filename,
 | |
|       lineNumber,
 | |
|       stack,
 | |
|     };
 | |
|   } catch (ex) {
 | |
|     return {
 | |
|       filename: "<internal error: could not get origin>",
 | |
|       lineNumber: -1,
 | |
|       stack: "<internal error: could not get origin>",
 | |
|     };
 | |
|   }
 | |
| }
 | |
| 
 | |
| var EXPORTED_SYMBOLS = ["AsyncShutdown"];
 | |
| 
 | |
| /**
 | |
|  * {string} topic -> phase
 | |
|  */
 | |
| var gPhases = new Map();
 | |
| 
 | |
| var AsyncShutdown = {
 | |
|   /**
 | |
|    * Access function getPhase. For testing purposes only.
 | |
|    */
 | |
|   get _getPhase() {
 | |
|     let accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing", false);
 | |
|     if (accepted) {
 | |
|       return getPhase;
 | |
|     }
 | |
|     return undefined;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * This constant is used as the amount of milliseconds to allow shutdown to be
 | |
|    * blocked until we crash the process forcibly and is read from the
 | |
|    * 'toolkit.asyncshutdown.crash_timeout' pref.
 | |
|    */
 | |
|   get DELAY_CRASH_MS() {
 | |
|     return DELAY_CRASH_MS;
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Register a new phase.
 | |
|  *
 | |
|  * @param {string} topic The notification topic for this Phase.
 | |
|  * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications}
 | |
|  */
 | |
| function getPhase(topic) {
 | |
|   let phase = gPhases.get(topic);
 | |
|   if (phase) {
 | |
|     return phase;
 | |
|   }
 | |
|   let spinner = new Spinner(topic);
 | |
|   phase = Object.freeze({
 | |
|     /**
 | |
|      * Register a blocker for the completion of a phase.
 | |
|      *
 | |
|      * @param {string} name The human-readable name of the blocker. Used
 | |
|      * for debugging/error reporting. Please make sure that the name
 | |
|      * respects the following model: "Some Service: some action in progress" -
 | |
|      * for instance "OS.File: flushing all pending I/O";
 | |
|      * @param {function|promise|*} condition A condition blocking the
 | |
|      * completion of the phase. Generally, this is a function
 | |
|      * returning a promise. This function is evaluated during the
 | |
|      * phase and the phase is guaranteed to not terminate until the
 | |
|      * resulting promise is either resolved or rejected. If
 | |
|      * |condition| is not a function but another value |v|, it behaves
 | |
|      * as if it were a function returning |v|.
 | |
|      * @param {object*} details Optionally, an object with details
 | |
|      * that may be useful for error reporting, as a subset of of the following
 | |
|      * fields:
 | |
|      * - fetchState (strongly recommended) A function returning
 | |
|      *    information about the current state of the blocker as an
 | |
|      *    object. Used for providing more details when logging errors or
 | |
|      *    crashing.
 | |
|      * - stack. A string containing stack information. This module can
 | |
|      *    generally infer stack information if it is not provided.
 | |
|      * - lineNumber A number containing the line number for the caller.
 | |
|      *    This module can generally infer this information if it is not
 | |
|      *    provided.
 | |
|      * - filename A string containing the filename for the caller. This
 | |
|      *    module can generally infer  the information if it is not provided.
 | |
|      *
 | |
|      * Examples:
 | |
|      * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
 | |
|      *      promise); // profileBeforeChange will not complete until
 | |
|      *                // promise is resolved or rejected
 | |
|      *
 | |
|      * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback",
 | |
|      *     function callback() {
 | |
|      *       // ...
 | |
|      *       // Execute this code during profileBeforeChange
 | |
|      *       return promise;
 | |
|      *       // profileBeforeChange will not complete until promise
 | |
|      *       // is resolved or rejected
 | |
|      * });
 | |
|      *
 | |
|      * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback",
 | |
|      *     function callback() {
 | |
|      *       // ...
 | |
|      *       // Execute this code during profileBeforeChange
 | |
|      *       // No specific guarantee about completion of profileBeforeChange
 | |
|      * });
 | |
|      */
 | |
|     addBlocker(name, condition, details = null) {
 | |
|       spinner.addBlocker(name, condition, details);
 | |
|     },
 | |
|     /**
 | |
|      * Remove the blocker for a condition.
 | |
|      *
 | |
|      * If several blockers have been registered for the same
 | |
|      * condition, remove all these blockers. If no blocker has been
 | |
|      * registered for this condition, this is a noop.
 | |
|      *
 | |
|      * @return {boolean} true if a blocker has been removed, false
 | |
|      * otherwise. Note that a result of false may mean either that
 | |
|      * the blocker has never been installed or that the phase has
 | |
|      * completed and the blocker has already been resolved.
 | |
|      */
 | |
|     removeBlocker(condition) {
 | |
|       return spinner.removeBlocker(condition);
 | |
|     },
 | |
| 
 | |
|     get name() {
 | |
|       return spinner.name;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Trigger the phase without having to broadcast a
 | |
|      * notification. For testing purposes only.
 | |
|      */
 | |
|     get _trigger() {
 | |
|       let accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing", false);
 | |
|       if (accepted) {
 | |
|         return () => spinner.observe();
 | |
|       }
 | |
|       return undefined;
 | |
|     },
 | |
|   });
 | |
|   gPhases.set(topic, phase);
 | |
|   return phase;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Utility class used to spin the event loop until all blockers for a
 | |
|  * Phase are satisfied.
 | |
|  *
 | |
|  * @param {string} topic The xpcom notification for that phase.
 | |
|  */
 | |
| function Spinner(topic) {
 | |
|   this._barrier = new Barrier(topic);
 | |
|   this._topic = topic;
 | |
|   Services.obs.addObserver(this, topic);
 | |
| }
 | |
| 
 | |
| Spinner.prototype = {
 | |
|   /**
 | |
|    * Register a new condition for this phase.
 | |
|    *
 | |
|    * See the documentation of `addBlocker` in property `client`
 | |
|    * of instances of `Barrier`.
 | |
|    */
 | |
|   addBlocker(name, condition, details) {
 | |
|     this._barrier.client.addBlocker(name, condition, details);
 | |
|   },
 | |
|   /**
 | |
|    * Remove the blocker for a condition.
 | |
|    *
 | |
|    * See the documentation of `removeBlocker` in rpoperty `client`
 | |
|    * of instances of `Barrier`
 | |
|    *
 | |
|    * @return {boolean} true if a blocker has been removed, false
 | |
|    * otherwise. Note that a result of false may mean either that
 | |
|    * the blocker has never been installed or that the phase has
 | |
|    * completed and the blocker has already been resolved.
 | |
|    */
 | |
|   removeBlocker(condition) {
 | |
|     return this._barrier.client.removeBlocker(condition);
 | |
|   },
 | |
| 
 | |
|   get name() {
 | |
|     return this._barrier.client.name;
 | |
|   },
 | |
| 
 | |
|   // nsIObserver.observe
 | |
|   observe() {
 | |
|     let topic = this._topic;
 | |
|     debug(`Starting phase ${ topic }`);
 | |
|     Services.obs.removeObserver(this, topic);
 | |
| 
 | |
|     let satisfied = false; // |true| once we have satisfied all conditions
 | |
|     let promise;
 | |
|     try {
 | |
|       promise = this._barrier.wait({
 | |
|         warnAfterMS: DELAY_WARNING_MS,
 | |
|         crashAfterMS: DELAY_CRASH_MS,
 | |
|       }).catch(
 | |
|         // Additional precaution to be entirely sure that we cannot reject.
 | |
|       );
 | |
|     } catch (ex) {
 | |
|       debug("Error waiting for notification");
 | |
|       throw ex;
 | |
|     }
 | |
| 
 | |
|     // Now, spin the event loop
 | |
|     debug("Spinning the event loop");
 | |
|     promise.then(() => satisfied = true); // This promise cannot reject
 | |
|     let thread = Services.tm.mainThread;
 | |
|     while (!satisfied) {
 | |
|       try {
 | |
|         thread.processNextEvent(true);
 | |
|       } catch (ex) {
 | |
|         // An uncaught error should not stop us, but it should still
 | |
|         // be reported and cause tests to fail.
 | |
|         Promise.reject(ex);
 | |
|       }
 | |
|     }
 | |
|     debug(`Finished phase ${ topic }`);
 | |
|   },
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * A mechanism used to register blockers that prevent some action from
 | |
|  * happening.
 | |
|  *
 | |
|  * An instance of |Barrier| provides a capability |client| that
 | |
|  * clients can use to register blockers. The barrier is resolved once
 | |
|  * all registered blockers have been resolved. The owner of the
 | |
|  * |Barrier| may wait for the resolution of the barrier and obtain
 | |
|  * information on which blockers have not been resolved yet.
 | |
|  *
 | |
|  * @param {string} name The name of the blocker. Used mainly for error-
 | |
|  *     reporting.
 | |
|  */
 | |
| function Barrier(name) {
 | |
|   if (!name) {
 | |
|     throw new TypeError("Instances of Barrier need a (non-empty) name");
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * The set of all Promise for which we need to wait before the barrier
 | |
|    * is lifted. Note that this set may be changed while we are waiting.
 | |
|    *
 | |
|    * Set to `null` once the wait is complete.
 | |
|    */
 | |
|   this._waitForMe = new PromiseSet();
 | |
| 
 | |
|   /**
 | |
|    * A map from conditions, as passed by users during the call to `addBlocker`,
 | |
|    * to `promise`, as present in `this._waitForMe`.
 | |
|    *
 | |
|    * Used to let users perform cleanup through `removeBlocker`.
 | |
|    * Set to `null` once the wait is complete.
 | |
|    *
 | |
|    * Key: condition (any, as passed by user)
 | |
|    * Value: promise used as a key in `this._waitForMe`. Note that there is
 | |
|    * no guarantee that the key is still present in `this._waitForMe`.
 | |
|    */
 | |
|   this._conditionToPromise = new Map();
 | |
| 
 | |
|   /**
 | |
|    * A map from Promise, as present in `this._waitForMe` or
 | |
|    * `this._conditionToPromise`, to information on blockers.
 | |
|    *
 | |
|    * Key: Promise (as present in this._waitForMe or this._conditionToPromise).
 | |
|    * Value:  {
 | |
|    *  trigger: function,
 | |
|    *  promise,
 | |
|    *  name,
 | |
|    *  fetchState: function,
 | |
|    *  stack,
 | |
|    *  filename,
 | |
|    *  lineNumber
 | |
|    * };
 | |
|    */
 | |
|   this._promiseToBlocker = new Map();
 | |
| 
 | |
|   /**
 | |
|    * The name of the barrier.
 | |
|    */
 | |
|   if (typeof name != "string") {
 | |
|     throw new TypeError("The name of the barrier must be a string");
 | |
|   }
 | |
|   this._name = name;
 | |
| 
 | |
|   /**
 | |
|    * A cache for the promise returned by wait().
 | |
|    */
 | |
|   this._promise = null;
 | |
| 
 | |
|   /**
 | |
|    * `true` once we have started waiting.
 | |
|    */
 | |
|   this._isStarted = false;
 | |
| 
 | |
|   /**
 | |
|    * The capability of adding blockers. This object may safely be returned
 | |
|    * or passed to clients.
 | |
|    */
 | |
|   this.client = {
 | |
|     /**
 | |
|      * The name of the barrier owning this client.
 | |
|      */
 | |
|     get name() {
 | |
|       return name;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Register a blocker for the completion of this barrier.
 | |
|      *
 | |
|      * @param {string} name The human-readable name of the blocker. Used
 | |
|      * for debugging/error reporting. Please make sure that the name
 | |
|      * respects the following model: "Some Service: some action in progress" -
 | |
|      * for instance "OS.File: flushing all pending I/O";
 | |
|      * @param {function|promise|*} condition A condition blocking the
 | |
|      * completion of the phase. Generally, this is a function
 | |
|      * returning a promise. This function is evaluated during the
 | |
|      * phase and the phase is guaranteed to not terminate until the
 | |
|      * resulting promise is either resolved or rejected. If
 | |
|      * |condition| is not a function but another value |v|, it behaves
 | |
|      * as if it were a function returning |v|.
 | |
|      * @param {object*} details Optionally, an object with details
 | |
|      * that may be useful for error reporting, as a subset of of the following
 | |
|      * fields:
 | |
|      * - fetchState (strongly recommended) A function returning
 | |
|      *    information about the current state of the blocker as an
 | |
|      *    object. Used for providing more details when logging errors or
 | |
|      *    crashing.
 | |
|      * - stack. A string containing stack information. This module can
 | |
|      *    generally infer stack information if it is not provided.
 | |
|      * - lineNumber A number containing the line number for the caller.
 | |
|      *    This module can generally infer this information if it is not
 | |
|      *    provided.
 | |
|      * - filename A string containing the filename for the caller. This
 | |
|      *    module can generally infer  the information if it is not provided.
 | |
|      */
 | |
|     addBlocker: (name, condition, details) => {
 | |
|       if (typeof name != "string") {
 | |
|         throw new TypeError("Expected a human-readable name as first argument");
 | |
|       }
 | |
|       if (details && typeof details == "function") {
 | |
|         details = {
 | |
|           fetchState: details,
 | |
|         };
 | |
|       } else if (!details) {
 | |
|         details = {};
 | |
|       }
 | |
|       if (typeof details != "object") {
 | |
|         throw new TypeError("Expected an object as third argument to `addBlocker`, got " + details);
 | |
|       }
 | |
|       if (!this._waitForMe) {
 | |
|         throw new Error(`Phase "${ this._name }" is finished, it is too late to register completion condition "${ name }"`);
 | |
|       }
 | |
|       debug(`Adding blocker ${ name } for phase ${ this._name }`);
 | |
| 
 | |
|       // Normalize the details
 | |
| 
 | |
|       let fetchState = details.fetchState || null;
 | |
|       if (fetchState != null && typeof fetchState != "function") {
 | |
|         throw new TypeError("Expected a function for option `fetchState`");
 | |
|       }
 | |
|       let filename = details.filename || null;
 | |
|       let lineNumber = details.lineNumber || null;
 | |
|       let stack = details.stack || null;
 | |
| 
 | |
|       // Split the condition between a trigger function and a promise.
 | |
| 
 | |
|       // The function to call to notify the blocker that we have started waiting.
 | |
|       // This function returns a promise resolved/rejected once the
 | |
|       // condition is complete, and never throws.
 | |
|       let trigger;
 | |
| 
 | |
|       // A promise resolved once the condition is complete.
 | |
|       let promise;
 | |
|       if (typeof condition == "function") {
 | |
|         promise = new Promise((resolve, reject) => {
 | |
|           trigger = () => {
 | |
|             try {
 | |
|               resolve(condition());
 | |
|             } catch (ex) {
 | |
|               reject(ex);
 | |
|             }
 | |
|           };
 | |
|         });
 | |
|       } else {
 | |
|         // If `condition` is not a function, `trigger` is not particularly
 | |
|         // interesting, and `condition` needs to be normalized to a promise.
 | |
|         trigger = () => {};
 | |
|         promise = Promise.resolve(condition);
 | |
|       }
 | |
| 
 | |
|       // Make sure that `promise` never rejects.
 | |
|       promise = promise.catch(error => {
 | |
|         let msg = `A blocker encountered an error while we were waiting.
 | |
|           Blocker:  ${ name }
 | |
|           Phase: ${ this._name }
 | |
|           State: ${ safeGetState(fetchState) }`;
 | |
|         warn(msg, error);
 | |
| 
 | |
|         // The error should remain uncaught, to ensure that it
 | |
|         // still causes tests to fail.
 | |
|         Promise.reject(error);
 | |
|       }).catch(
 | |
|         // Added as a last line of defense, in case `warn`, `this._name` or
 | |
|         // `safeGetState` somehow throws an error.
 | |
|       );
 | |
| 
 | |
|       let topFrame = null;
 | |
|       if (filename == null || lineNumber == null || stack == null) {
 | |
|         topFrame = Components.stack;
 | |
|       }
 | |
| 
 | |
|       let blocker = {
 | |
|         trigger,
 | |
|         promise,
 | |
|         name,
 | |
|         fetchState,
 | |
|         getOrigin: () => getOrigin(topFrame, filename, lineNumber, stack),
 | |
|       };
 | |
| 
 | |
|       this._waitForMe.add(promise);
 | |
|       this._promiseToBlocker.set(promise, blocker);
 | |
|       this._conditionToPromise.set(condition, promise);
 | |
| 
 | |
|       // As conditions may hold lots of memory, we attempt to cleanup
 | |
|       // as soon as we are done (which might be in the next tick, if
 | |
|       // we have been passed a resolved promise).
 | |
|       promise = promise.then(() => {
 | |
|         debug(`Completed blocker ${ name } for phase ${ this._name }`);
 | |
|         this._removeBlocker(condition);
 | |
|       });
 | |
| 
 | |
|       if (this._isStarted) {
 | |
|         // The wait has already started. The blocker should be
 | |
|         // notified asap. We do it out of band as clients probably
 | |
|         // expect `addBlocker` to return immediately.
 | |
|         Promise.resolve().then(trigger);
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Remove the blocker for a condition.
 | |
|      *
 | |
|      * If several blockers have been registered for the same
 | |
|      * condition, remove all these blockers. If no blocker has been
 | |
|      * registered for this condition, this is a noop.
 | |
|      *
 | |
|      * @return {boolean} true if at least one blocker has been
 | |
|      * removed, false otherwise.
 | |
|      */
 | |
|     removeBlocker: (condition) => {
 | |
|       return this._removeBlocker(condition);
 | |
|     },
 | |
|   };
 | |
| }
 | |
| Barrier.prototype = Object.freeze({
 | |
|   /**
 | |
|    * The current state of the barrier, as a JSON-serializable object
 | |
|    * designed for error-reporting.
 | |
|    */
 | |
|   get state() {
 | |
|     if (!this._isStarted) {
 | |
|       return "Not started";
 | |
|     }
 | |
|     if (!this._waitForMe) {
 | |
|       return "Complete";
 | |
|     }
 | |
|     let frozen = [];
 | |
|     for (let blocker of this._promiseToBlocker.values()) {
 | |
|       let {name, fetchState} = blocker;
 | |
|       let {stack, filename, lineNumber} = blocker.getOrigin();
 | |
|       frozen.push({
 | |
|         name,
 | |
|         state: safeGetState(fetchState),
 | |
|         filename,
 | |
|         lineNumber,
 | |
|         stack,
 | |
|       });
 | |
|     }
 | |
|     return frozen;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Wait until all currently registered blockers are complete.
 | |
|    *
 | |
|    * Once this method has been called, any attempt to register a new blocker
 | |
|    * for this barrier will cause an error.
 | |
|    *
 | |
|    * Successive calls to this method always return the same value.
 | |
|    *
 | |
|    * @param {object=}  options Optionally, an object  that may contain
 | |
|    * the following fields:
 | |
|    *  {number} warnAfterMS If provided and > 0, print a warning if the barrier
 | |
|    *   has not been resolved after the given number of milliseconds.
 | |
|    *  {number} crashAfterMS If provided and > 0, crash the process if the barrier
 | |
|    *   has not been resolved after the give number of milliseconds (rounded up
 | |
|    *   to the next second). To avoid crashing simply because the computer is busy
 | |
|    *   or going to sleep, we actually wait for ceil(crashAfterMS/1000) successive
 | |
|    *   periods of at least one second. Upon crashing, if a crash reporter is present,
 | |
|    *   prepare a crash report with the state of this barrier.
 | |
|    *
 | |
|    *
 | |
|    * @return {Promise} A promise satisfied once all blockers are complete.
 | |
|    */
 | |
|   wait(options = {}) {
 | |
|     // This method only implements caching on top of _wait()
 | |
|     if (this._promise) {
 | |
|       return this._promise;
 | |
|     }
 | |
|     return this._promise = this._wait(options);
 | |
|   },
 | |
|   _wait(options) {
 | |
| 
 | |
|     // Sanity checks
 | |
|     if (this._isStarted) {
 | |
|       throw new TypeError("Internal error: already started " + this._name);
 | |
|     }
 | |
|     if (!this._waitForMe || !this._conditionToPromise || !this._promiseToBlocker) {
 | |
|       throw new TypeError("Internal error: already finished " + this._name);
 | |
|     }
 | |
| 
 | |
|     let topic = this._name;
 | |
| 
 | |
|     // Notify blockers
 | |
|     for (let blocker of this._promiseToBlocker.values()) {
 | |
|       blocker.trigger(); // We have guarantees that this method will never throw
 | |
|     }
 | |
| 
 | |
|     this._isStarted = true;
 | |
| 
 | |
|     // Now, wait
 | |
|     let promise = this._waitForMe.wait();
 | |
| 
 | |
|     promise = promise.catch(function onError(error) {
 | |
|       // I don't think that this can happen.
 | |
|       // However, let's be overcautious with async/shutdown error reporting.
 | |
|       let msg = "An uncaught error appeared while completing the phase." +
 | |
|         " Phase: " + topic;
 | |
|       warn(msg, error);
 | |
|     });
 | |
| 
 | |
|     promise = promise.then(() => {
 | |
|       // Cleanup memory
 | |
|       this._waitForMe = null;
 | |
|       this._promiseToBlocker = null;
 | |
|       this._conditionToPromise = null;
 | |
|     });
 | |
| 
 | |
|     // Now handle warnings and crashes
 | |
|     let warnAfterMS = DELAY_WARNING_MS;
 | |
|     if (options && "warnAfterMS" in options) {
 | |
|       if (typeof options.warnAfterMS == "number"
 | |
|          || options.warnAfterMS == null) {
 | |
|         // Change the delay or deactivate warnAfterMS
 | |
|         warnAfterMS = options.warnAfterMS;
 | |
|       } else {
 | |
|         throw new TypeError("Wrong option value for warnAfterMS");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (warnAfterMS && warnAfterMS > 0) {
 | |
|       // If the promise takes too long to be resolved/rejected,
 | |
|       // we need to notify the user.
 | |
|       let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | |
|       timer.initWithCallback(() => {
 | |
|         let msg = "At least one completion condition is taking too long to complete." +
 | |
|         " Conditions: " + JSON.stringify(this.state) +
 | |
|         " Barrier: " + topic;
 | |
|         warn(msg);
 | |
|       }, warnAfterMS, Ci.nsITimer.TYPE_ONE_SHOT);
 | |
| 
 | |
|       promise = promise.then(function onSuccess() {
 | |
|         timer.cancel();
 | |
|         // As a side-effect, this prevents |timer| from
 | |
|         // being garbage-collected too early.
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     let crashAfterMS = DELAY_CRASH_MS;
 | |
|     if (options && "crashAfterMS" in options) {
 | |
|       if (typeof options.crashAfterMS == "number"
 | |
|          || options.crashAfterMS == null) {
 | |
|         // Change the delay or deactivate crashAfterMS
 | |
|         crashAfterMS = options.crashAfterMS;
 | |
|       } else {
 | |
|         throw new TypeError("Wrong option value for crashAfterMS");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (crashAfterMS > 0) {
 | |
|       let timeToCrash = null;
 | |
| 
 | |
|       // If after |crashAfterMS| milliseconds (adjusted to take into
 | |
|       // account sleep and otherwise busy computer) we have not finished
 | |
|       // this shutdown phase, we assume that the shutdown is somehow
 | |
|       // frozen, presumably deadlocked. At this stage, the only thing we
 | |
|       // can do to avoid leaving the user's computer in an unstable (and
 | |
|       // battery-sucking) situation is report the issue and crash.
 | |
|       timeToCrash = looseTimer(crashAfterMS);
 | |
|       timeToCrash.promise.then(
 | |
|         () => {
 | |
|           // Report the problem as best as we can, then crash.
 | |
|           let state = this.state;
 | |
| 
 | |
|           // If you change the following message, please make sure
 | |
|           // that any information on the topic and state appears
 | |
|           // within the first 200 characters of the message. This
 | |
|           // helps automatically sort oranges.
 | |
|           let msg = "AsyncShutdown timeout in " + topic +
 | |
|             " Conditions: " + JSON.stringify(state) +
 | |
|             " At least one completion condition failed to complete" +
 | |
|             " within a reasonable amount of time. Causing a crash to" +
 | |
|             " ensure that we do not leave the user with an unresponsive" +
 | |
|             " process draining resources.";
 | |
|           fatalerr(msg);
 | |
|           if (gCrashReporter && gCrashReporter.enabled) {
 | |
|             let data = {
 | |
|               phase: topic,
 | |
|               conditions: state,
 | |
|             };
 | |
|             gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
 | |
|               JSON.stringify(data));
 | |
|           } else {
 | |
|             warn("No crash reporter available");
 | |
|           }
 | |
| 
 | |
|           // To help sorting out bugs, we want to make sure that the
 | |
|           // call to nsIDebug2.abort points to a guilty client, rather
 | |
|           // than to AsyncShutdown itself. We pick a client that is
 | |
|           // still blocking and use its filename/lineNumber,
 | |
|           // which have been determined during the call to `addBlocker`.
 | |
|           let filename = "?";
 | |
|           let lineNumber = -1;
 | |
|           for (let blocker of this._promiseToBlocker.values()) {
 | |
|             ({filename, lineNumber} = blocker.getOrigin());
 | |
|             break;
 | |
|           }
 | |
|           gDebug.abort(filename, lineNumber);
 | |
|         },
 | |
|         function onSatisfied() {
 | |
|           // The promise has been rejected, which means that we have satisfied
 | |
|           // all completion conditions.
 | |
|         });
 | |
| 
 | |
|       promise = promise.then(function() {
 | |
|         timeToCrash.reject();
 | |
|       }/* No error is possible here*/);
 | |
|     }
 | |
| 
 | |
|     return promise;
 | |
|   },
 | |
| 
 | |
|   _removeBlocker(condition) {
 | |
|     if (!this._waitForMe || !this._promiseToBlocker || !this._conditionToPromise) {
 | |
|       // We have already cleaned up everything.
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     let promise = this._conditionToPromise.get(condition);
 | |
|     if (!promise) {
 | |
|       // The blocker has already been removed
 | |
|       return false;
 | |
|     }
 | |
|     this._conditionToPromise.delete(condition);
 | |
|     this._promiseToBlocker.delete(promise);
 | |
|     return this._waitForMe.delete(promise);
 | |
|   },
 | |
| 
 | |
| });
 | |
| 
 | |
| 
 | |
| 
 | |
| // List of well-known phases
 | |
| // Ideally, phases should be registered from the component that decides
 | |
| // when they start/stop. For compatibility with existing startup/shutdown
 | |
| // mechanisms, we register a few phases here.
 | |
| 
 | |
| // Parent process
 | |
| if (!isContent) {
 | |
|   this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
 | |
|   this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
 | |
|   this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change-telemetry");
 | |
| }
 | |
| 
 | |
| // Notifications that fire in the parent and content process, but should
 | |
| // only have phases in the parent process.
 | |
| if (!isContent) {
 | |
|   this.AsyncShutdown.quitApplicationGranted = getPhase("quit-application-granted");
 | |
| }
 | |
| 
 | |
| // Don't add a barrier for content-child-shutdown because this
 | |
| // makes it easier to cause shutdown hangs.
 | |
| 
 | |
| // All processes
 | |
| this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
 | |
| this.AsyncShutdown.xpcomWillShutdown = getPhase("xpcom-will-shutdown");
 | |
| 
 | |
| this.AsyncShutdown.Barrier = Barrier;
 | |
| 
 | |
| Object.freeze(this.AsyncShutdown);
 |