mirror of https://github.com/roytam1/UXP.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1020 lines
34 KiB
1020 lines
34 KiB
/* 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. 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 = 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); |
|
}, 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 = Services.prefs.getBoolPref(PREF_DEBUG_LOG, false); |
|
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 = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing", false); |
|
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 = 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, 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"); |
|
} |
|
|
|
// 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);
|
|
|