Bug 1248489 - Bullet-proofing AsyncShutdown;r?froydnj draft
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Fri, 19 Feb 2016 12:51:04 +0100
changeset 332084 0c2a2a5c829ab63bb7f4e6a1f24be117f7fc9407
parent 331663 0629918a09ae87808efdda432d7852371ba37db6
child 514542 dd930debe4a4ea73370220f64635c5d9e2e22dc6
push id11160
push userdteller@mozilla.com
push dateFri, 19 Feb 2016 11:52:08 +0000
reviewersfroydnj
bugs1248489
milestone47.0a1
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
toolkit/components/asyncshutdown/AsyncShutdown.jsm
--- 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,