Bug 1248489 - Bullet-proofing AsyncShutdown;r?froydnj
While investigating
bug 1248489, we discovered that some code paths in
AsyncShutdown could possibly be sensitive to exceptions being thrown
in unexpected places. This patch attempts to make AsyncShutdown more
robust to such exceptions.
MozReview-Commit-ID: 5ImL9YNVgQr
--- a/toolkit/components/asyncshutdown/AsyncShutdown.jsm
+++ b/toolkit/components/asyncshutdown/AsyncShutdown.jsm
@@ -189,22 +189,26 @@ PromiseSet.prototype = {
/**
* 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) {
- dump(prefix + msg + "\n");
- if (error) {
- dump(prefix + error + "\n");
- if (typeof error == "object" && "stack" in error) {
- dump(prefix + error.stack + "\n");
+ 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
@@ -300,45 +304,53 @@ function looseTimer(delay) {
* @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) {
- // Determine the filename and line number of the caller.
- let frame = topFrame;
+ 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
+ }
- 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");
+ }
- if (filename == null) {
- filename = frame ? frame.filename : "?";
- }
- if (lineNumber == null) {
- lineNumber = frame ? frame.lineNumber : 0;
+ 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>",
+ }
}
- 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,
- };
}
this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
/**
* {string} topic -> phase
*/
var gPhases = new Map();
@@ -506,27 +518,37 @@ Spinner.prototype = {
},
get name() {
return this._barrier.client.name;
},
// nsIObserver.observe
observe: function() {
+ debug(`Starting phase ${ topic }`);
let topic = this._topic;
let barrier = this._barrier;
Services.obs.removeObserver(this, topic);
let satisfied = false; // |true| once we have satisfied all conditions
- let promise = this._barrier.wait({
- warnAfterMS: DELAY_WARNING_MS,
- crashAfterMS: DELAY_CRASH_MS
- });
+ 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 in nsIObserve.observe");
+ 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.
@@ -592,16 +614,19 @@ function Barrier(name) {
* 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;
/**
@@ -667,16 +692,19 @@ function Barrier(name) {
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
@@ -708,17 +736,20 @@ function Barrier(name) {
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,