toolkit/modules/AsyncShutdown.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 /**
     6  * Managing safe shutdown of asynchronous services.
     7  *
     8  * Firefox shutdown is composed of phases that take place
     9  * sequentially. Typically, each shutdown phase removes some
    10  * capabilities from the application. For instance, at the end of
    11  * phase profileBeforeChange, no service is permitted to write to the
    12  * profile directory (with the exception of Telemetry). Consequently,
    13  * if any service has requested I/O to the profile directory before or
    14  * during phase profileBeforeChange, the system must be informed that
    15  * these requests need to be completed before the end of phase
    16  * profileBeforeChange. Failing to inform the system of this
    17  * requirement can (and has been known to) cause data loss.
    18  *
    19  * Example: At some point during shutdown, the Add-On Manager needs to
    20  * ensure that all add-ons have safely written their data to disk,
    21  * before writing its own data. Since the data is saved to the
    22  * profile, this must be completed during phase profileBeforeChange.
    23  *
    24  * AsyncShutdown.profileBeforeChange.addBlocker(
    25  *   "Add-on manager: shutting down",
    26  *   function condition() {
    27  *     // Do things.
    28  *     // Perform I/O that must take place during phase profile-before-change
    29  *     return promise;
    30  *   }
    31  * });
    32  *
    33  * In this example, function |condition| will be called at some point
    34  * during phase profileBeforeChange and phase profileBeforeChange
    35  * itself is guaranteed to not terminate until |promise| is either
    36  * resolved or rejected.
    37  */
    39 "use strict";
    41 const Cu = Components.utils;
    42 const Cc = Components.classes;
    43 const Ci = Components.interfaces;
    44 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    45 Cu.import("resource://gre/modules/Services.jsm", this);
    47 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    48   "resource://gre/modules/Promise.jsm");
    49 XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
    50   "@mozilla.org/xpcom/debug;1", "nsIDebug");
    51 Object.defineProperty(this, "gCrashReporter", {
    52   get: function() {
    53     delete this.gCrashReporter;
    54     try {
    55       let reporter = Cc["@mozilla.org/xre/app-info;1"].
    56             getService(Ci.nsICrashReporter);
    57       return this.gCrashReporter = reporter;
    58     } catch (ex) {
    59       return this.gCrashReporter = null;
    60     }
    61   },
    62   configurable: true
    63 });
    65 // Display timeout warnings after 10 seconds
    66 const DELAY_WARNING_MS = 10 * 1000;
    69 // Crash the process if shutdown is really too long
    70 // (allowing for sleep).
    71 const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
    72 let DELAY_CRASH_MS = 60 * 1000; // One minute
    73 try {
    74   DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
    75 } catch (ex) {
    76   // Ignore errors
    77 }
    78 Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
    79   DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
    80 }, false);
    83 /**
    84  * Display a warning.
    85  *
    86  * As this code is generally used during shutdown, there are chances
    87  * that the UX will not be available to display warnings on the
    88  * console. We therefore use dump() rather than Cu.reportError().
    89  */
    90 function log(msg, prefix = "", error = null) {
    91   dump(prefix + msg + "\n");
    92   if (error) {
    93     dump(prefix + error + "\n");
    94     if (typeof error == "object" && "stack" in error) {
    95       dump(prefix + error.stack + "\n");
    96     }
    97   }
    98 }
    99 function warn(msg, error = null) {
   100   return log(msg, "WARNING: ", error);
   101 }
   102 function err(msg, error = null) {
   103   return log(msg, "ERROR: ", error);
   104 }
   106 // Utility function designed to get the current state of execution
   107 // of a blocker.
   108 // We are a little paranoid here to ensure that in case of evaluation
   109 // error we do not block the AsyncShutdown.
   110 function safeGetState(state) {
   111   if (!state) {
   112     return "(none)";
   113   }
   114   let data, string;
   115   try {
   116     // Evaluate state(), normalize the result into something that we can
   117     // safely stringify or upload.
   118     string = JSON.stringify(state());
   119     data = JSON.parse(string);
   120     // Simplify the rest of the code by ensuring that we can simply
   121     // concatenate the result to a message.
   122     if (data && typeof data == "object") {
   123       data.toString = function() {
   124         return string;
   125       };
   126     }
   127     return data;
   128   } catch (ex) {
   129     if (string) {
   130       return string;
   131     }
   132     try {
   133       return "Error getting state: " + ex + " at " + ex.stack;
   134     } catch (ex2) {
   135       return "Error getting state but could not display error";
   136     }
   137   }
   138 }
   140 /**
   141  * Countdown for a given duration, skipping beats if the computer is too busy,
   142  * sleeping or otherwise unavailable.
   143  *
   144  * @param {number} delay An approximate delay to wait in milliseconds (rounded
   145  * up to the closest second).
   146  *
   147  * @return Deferred
   148  */
   149 function looseTimer(delay) {
   150   let DELAY_BEAT = 1000;
   151   let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   152   let beats = Math.ceil(delay / DELAY_BEAT);
   153   let deferred = Promise.defer();
   154   timer.initWithCallback(function() {
   155     if (beats <= 0) {
   156       deferred.resolve();
   157     }
   158     --beats;
   159   }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
   160   // Ensure that the timer is both canceled once we are done with it
   161   // and not garbage-collected until then.
   162   deferred.promise.then(() => timer.cancel(), () => timer.cancel());
   163   return deferred;
   164 }
   166 this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
   168 /**
   169  * {string} topic -> phase
   170  */
   171 let gPhases = new Map();
   173 this.AsyncShutdown = {
   174   /**
   175    * Access function getPhase. For testing purposes only.
   176    */
   177   get _getPhase() {
   178     let accepted = false;
   179     try {
   180       accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing");
   181     } catch (ex) {
   182       // Ignore errors
   183     }
   184     if (accepted) {
   185       return getPhase;
   186     }
   187     return undefined;
   188   }
   189 };
   191 /**
   192  * Register a new phase.
   193  *
   194  * @param {string} topic The notification topic for this Phase.
   195  * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications}
   196  */
   197 function getPhase(topic) {
   198   let phase = gPhases.get(topic);
   199   if (phase) {
   200     return phase;
   201   }
   202   let spinner = new Spinner(topic);
   203   phase = Object.freeze({
   204     /**
   205      * Register a blocker for the completion of a phase.
   206      *
   207      * @param {string} name The human-readable name of the blocker. Used
   208      * for debugging/error reporting. Please make sure that the name
   209      * respects the following model: "Some Service: some action in progress" -
   210      * for instance "OS.File: flushing all pending I/O";
   211      * @param {function|promise|*} condition A condition blocking the
   212      * completion of the phase. Generally, this is a function
   213      * returning a promise. This function is evaluated during the
   214      * phase and the phase is guaranteed to not terminate until the
   215      * resulting promise is either resolved or rejected. If
   216      * |condition| is not a function but another value |v|, it behaves
   217      * as if it were a function returning |v|.
   218      * @param {function*} state Optionally, a function returning
   219      * information about the current state of the blocker as an
   220      * object. Used for providing more details when logging errors or
   221      * crashing.
   222      *
   223      * Examples:
   224      * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
   225      *      promise); // profileBeforeChange will not complete until
   226      *                // promise is resolved or rejected
   227      *
   228      * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback",
   229      *     function callback() {
   230      *       // ...
   231      *       // Execute this code during profileBeforeChange
   232      *       return promise;
   233      *       // profileBeforeChange will not complete until promise
   234      *       // is resolved or rejected
   235      * });
   236      *
   237      * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback",
   238      *     function callback() {
   239      *       // ...
   240      *       // Execute this code during profileBeforeChange
   241      *       // No specific guarantee about completion of profileBeforeChange
   242      * });
   243      *
   244      */
   245     addBlocker: function(name, condition, state = null) {
   246       if (typeof name != "string") {
   247         throw new TypeError("Expected a human-readable name as first argument");
   248       }
   249       if (state && typeof state != "function") {
   250         throw new TypeError("Expected nothing or a function as third argument");
   251       }
   252       spinner.addBlocker({name: name, condition: condition, state: state});
   253     }
   254   });
   255   gPhases.set(topic, phase);
   256   return phase;
   257 }
   259 /**
   260  * Utility class used to spin the event loop until all blockers for a
   261  * Phase are satisfied.
   262  *
   263  * @param {string} topic The xpcom notification for that phase.
   264  */
   265 function Spinner(topic) {
   266   this._topic = topic;
   267   this._conditions = new Set(); // set to |null| once it is too late to register
   268   Services.obs.addObserver(this, topic, false);
   269 }
   271 Spinner.prototype = {
   272   /**
   273    * Register a new condition for this phase.
   274    *
   275    * @param {object} condition A Condition that must be fulfilled before
   276    * we complete this Phase.
   277    * Must contain fields:
   278    * - {string} name The human-readable name of the condition. Used
   279    * for debugging/error reporting.
   280    * - {function} action An action that needs to be completed
   281    * before we proceed to the next runstate. If |action| returns a promise,
   282    * we wait until the promise is resolved/rejected before proceeding
   283    * to the next runstate.
   284    */
   285   addBlocker: function(condition) {
   286     if (!this._conditions) {
   287       throw new Error("Phase " + this._topic +
   288                       " has already begun, it is too late to register" +
   289                       " completion condition '" + condition.name + "'.");
   290     }
   291     this._conditions.add(condition);
   292   },
   294   observe: function() {
   295     let topic = this._topic;
   296     Services.obs.removeObserver(this, topic);
   298     let conditions = this._conditions;
   299     this._conditions = null; // Too late to register
   301     if (conditions.size == 0) {
   302       // No need to spin anything
   303       return;
   304     }
   306     // The promises for which we are waiting.
   307     let allPromises = [];
   309     // Information to determine and report to the user which conditions
   310     // are not satisfied yet.
   311     let allMonitors = [];
   313     for (let {condition, name, state} of conditions) {
   314       // Gather all completion conditions
   316       try {
   317         if (typeof condition == "function") {
   318           // Normalize |condition| to the result of the function.
   319           try {
   320             condition = condition(topic);
   321           } catch (ex) {
   322             condition = Promise.reject(ex);
   323           }
   324         }
   325         // Normalize to a promise. Of course, if |condition| was not a
   326         // promise in the first place (in particular if the above
   327         // function returned |undefined| or failed), that new promise
   328         // isn't going to be terribly interesting, but it will behave
   329         // as a promise.
   330         condition = Promise.resolve(condition);
   332         // If the promise takes too long to be resolved/rejected,
   333         // we need to notify the user.
   334         //
   335         // If it takes way too long, we need to crash.
   337         let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   338         timer.initWithCallback(function() {
   339           let msg = "A phase completion condition is" +
   340             " taking too long to complete." +
   341             " Condition: " + monitor.name +
   342             " Phase: " + topic +
   343             " State: " + safeGetState(state);
   344           warn(msg);
   345         }, DELAY_WARNING_MS, Ci.nsITimer.TYPE_ONE_SHOT);
   347         let monitor = {
   348           isFrozen: true,
   349           name: name,
   350           state: state
   351         };
   352         condition = condition.then(function onSuccess() {
   353             timer.cancel(); // As a side-effect, this prevents |timer| from
   354                             // being garbage-collected too early.
   355             monitor.isFrozen = false;
   356           }, function onError(error) {
   357             timer.cancel();
   358             let msg = "A completion condition encountered an error" +
   359                 " while we were spinning the event loop." +
   360                 " Condition: " + name +
   361                 " Phase: " + topic +
   362                 " State: " + safeGetState(state);
   363             warn(msg, error);
   364             monitor.isFrozen = false;
   365         });
   366         allMonitors.push(monitor);
   367         allPromises.push(condition);
   369       } catch (error) {
   370           let msg = "A completion condition encountered an error" +
   371                 " while we were initializing the phase." +
   372                 " Condition: " + name +
   373                 " Phase: " + topic +
   374                 " State: " + safeGetState(state);
   375           warn(msg, error);
   376       }
   378     }
   379     conditions = null;
   381     let promise = Promise.all(allPromises);
   382     allPromises = null;
   384     promise = promise.then(null, function onError(error) {
   385       // I don't think that this can happen.
   386       // However, let's be overcautious with async/shutdown error reporting.
   387       let msg = "An uncaught error appeared while completing the phase." +
   388             " Phase: " + topic;
   389       warn(msg, error);
   390     });
   392     let satisfied = false; // |true| once we have satisfied all conditions
   394     // If after DELAY_CRASH_MS (approximately one minute, adjusted to take
   395     // into account sleep and otherwise busy computer) we have not finished
   396     // this shutdown phase, we assume that the shutdown is somehow frozen,
   397     // presumably deadlocked. At this stage, the only thing we can do to
   398     // avoid leaving the user's computer in an unstable (and battery-sucking)
   399     // situation is report the issue and crash.
   400     let timeToCrash = looseTimer(DELAY_CRASH_MS);
   401     timeToCrash.promise.then(
   402       function onTimeout() {
   403         // Report the problem as best as we can, then crash.
   404         let frozen = [];
   405         let states = [];
   406         for (let {name, isFrozen, state} of allMonitors) {
   407           if (isFrozen) {
   408             frozen.push({name: name, state: safeGetState(state)});
   409           }
   410         }
   412         let msg = "At least one completion condition failed to complete" +
   413               " within a reasonable amount of time. Causing a crash to" +
   414               " ensure that we do not leave the user with an unresponsive" +
   415               " process draining resources." +
   416               " Conditions: " + JSON.stringify(frozen) +
   417               " Phase: " + topic;
   418         err(msg);
   419         if (gCrashReporter && gCrashReporter.enabled) {
   420           let data = {
   421             phase: topic,
   422             conditions: frozen
   423           };
   424           gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
   425             JSON.stringify(data));
   426         } else {
   427           warn("No crash reporter available");
   428         }
   430         let error = new Error();
   431         gDebug.abort(error.fileName, error.lineNumber + 1);
   432       },
   433       function onSatisfied() {
   434         // The promise has been rejected, which means that we have satisfied
   435         // all completion conditions.
   436       });
   438     promise = promise.then(function() {
   439       satisfied = true;
   440       timeToCrash.reject();
   441     }/* No error is possible here*/);
   443     // Now, spin the event loop
   444     let thread = Services.tm.mainThread;
   445     while(!satisfied) {
   446       thread.processNextEvent(true);
   447     }
   448   }
   449 };
   452 // List of well-known runstates
   453 // Ideally, runstates should be registered from the component that decides
   454 // when they start/stop. For compatibility with existing startup/shutdown
   455 // mechanisms, we register a few runstates here.
   457 this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
   458 this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
   459 this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change2");
   460 this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
   461 Object.freeze(this.AsyncShutdown);

mercurial