зеркало из https://github.com/mozilla/gecko-dev.git
1096 строки
34 KiB
JavaScript
1096 строки
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";
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"PromiseUtils",
|
|
"resource://gre/modules/PromiseUtils.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.
|
|
const isContent =
|
|
// eslint-disable-next-line mozilla/use-services
|
|
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();
|
|
// Once all the tracked promises have been resolved we are done. Once Wait()
|
|
// resolves, it should not be possible anymore to add further promises.
|
|
// This covers for a possibly rare case, where something may try to add a
|
|
// blocker after wait() is done, that would never be awaited for.
|
|
this._done = false;
|
|
}
|
|
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.
|
|
this._done = true;
|
|
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) {
|
|
if (this._done) {
|
|
throw new Error("Wait is complete, cannot add further promises.");
|
|
}
|
|
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);
|
|
}
|
|
)
|
|
.finally(() => {
|
|
this._indirections.delete(key);
|
|
// Normally the promise is resolved or rejected, but if its global
|
|
// goes away, only finally may be invoked. In all the other cases this
|
|
// is a no-op since the promise has been fulfilled already.
|
|
indirection.reject(
|
|
new Error("Promise not fulfilled, did it lost its global?")
|
|
);
|
|
});
|
|
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`.
|
|
stack = [];
|
|
while (frame != null) {
|
|
stack.push(frame.filename + ":" + frame.name + ":" + frame.lineNumber);
|
|
frame = frame.caller;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
|
|
AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
|
|
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) {
|
|
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
|
|
AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
|
|
AsyncShutdown.xpcomWillShutdown = getPhase("xpcom-will-shutdown");
|
|
|
|
AsyncShutdown.Barrier = Barrier;
|
|
|
|
Object.freeze(AsyncShutdown);
|