summaryrefslogtreecommitdiffstats
path: root/toolkit/components/asyncshutdown/AsyncShutdown.jsm
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/components/asyncshutdown/AsyncShutdown.jsm
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/asyncshutdown/AsyncShutdown.jsm')
-rw-r--r--toolkit/components/asyncshutdown/AsyncShutdown.jsm1041
1 files changed, 1041 insertions, 0 deletions
diff --git a/toolkit/components/asyncshutdown/AsyncShutdown.jsm b/toolkit/components/asyncshutdown/AsyncShutdown.jsm
new file mode 100644
index 000000000..62ac36f42
--- /dev/null
+++ b/toolkit/components/asyncshutdown/AsyncShutdown.jsm
@@ -0,0 +1,1041 @@
+/* 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";
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
+ "@mozilla.org/xpcom/debug;1", "nsIDebug2");
+Object.defineProperty(this, "gCrashReporter", {
+ get: function() {
+ 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.
+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 = 60 * 1000; // One minute
+try {
+ DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
+} catch (ex) {
+ // Ignore errors
+}
+Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
+ DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
+}, false);
+
+/**
+ * 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: function() {
+ // 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: function(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: function(key) {
+ this._ensurePromise(key);
+ let value = this._indirections.get(key);
+ if (!value) {
+ return false;
+ }
+ this._indirections.delete(key);
+ value.resolve();
+ return true;
+ },
+
+ _ensurePromise: function(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 = false;
+try {
+ DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
+} catch (ex) {
+ // Ignore errors
+}
+Services.prefs.addObserver(PREF_DEBUG_LOG, function() {
+ DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG);
+}, false);
+
+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 = Promise.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 = Task.Debugging.generateReadableStack(frames.join("\n")).split("\n");
+ }
+
+ return {
+ filename: filename,
+ lineNumber: lineNumber,
+ stack: stack,
+ };
+ } catch (ex) {
+ return {
+ filename: "<internal error: could not get origin>",
+ lineNumber: -1,
+ stack: "<internal error: could not get origin>",
+ }
+ }
+}
+
+this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
+
+/**
+ * {string} topic -> phase
+ */
+var gPhases = new Map();
+
+this.AsyncShutdown = {
+ /**
+ * Access function getPhase. For testing purposes only.
+ */
+ get _getPhase() {
+ let accepted = false;
+ try {
+ accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing");
+ } catch (ex) {
+ // Ignore errors
+ }
+ if (accepted) {
+ return getPhase;
+ }
+ return undefined;
+ }
+};
+
+/**
+ * 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: function(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: function(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 = false;
+ try {
+ accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing");
+ } catch (ex) {
+ // Ignore errors
+ }
+ 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, false);
+}
+
+Spinner.prototype = {
+ /**
+ * Register a new condition for this phase.
+ *
+ * See the documentation of `addBlocker` in property `client`
+ * of instances of `Barrier`.
+ */
+ addBlocker: function(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: function(condition) {
+ return this._barrier.client.removeBlocker(condition);
+ },
+
+ get name() {
+ return this._barrier.client.name;
+ },
+
+ // nsIObserver.observe
+ observe: function() {
+ 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.then(null, 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: trigger,
+ promise: promise,
+ name: name,
+ fetchState: 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: name,
+ state: safeGetState(fetchState),
+ filename: filename,
+ lineNumber: lineNumber,
+ stack: 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: function(options = {}) {
+ // This method only implements caching on top of _wait()
+ if (this._promise) {
+ return this._promise;
+ }
+ return this._promise = this._wait(options);
+ },
+ _wait: function(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.then(null, 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(
+ function onTimeout() {
+ // 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);
+ }.bind(this),
+ 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: function(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.placesClosingInternalConnection = getPhase("places-will-close-connection");
+ 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);