toolkit/modules/AsyncShutdown.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/AsyncShutdown.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,461 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +/**
     1.9 + * Managing safe shutdown of asynchronous services.
    1.10 + *
    1.11 + * Firefox shutdown is composed of phases that take place
    1.12 + * sequentially. Typically, each shutdown phase removes some
    1.13 + * capabilities from the application. For instance, at the end of
    1.14 + * phase profileBeforeChange, no service is permitted to write to the
    1.15 + * profile directory (with the exception of Telemetry). Consequently,
    1.16 + * if any service has requested I/O to the profile directory before or
    1.17 + * during phase profileBeforeChange, the system must be informed that
    1.18 + * these requests need to be completed before the end of phase
    1.19 + * profileBeforeChange. Failing to inform the system of this
    1.20 + * requirement can (and has been known to) cause data loss.
    1.21 + *
    1.22 + * Example: At some point during shutdown, the Add-On Manager needs to
    1.23 + * ensure that all add-ons have safely written their data to disk,
    1.24 + * before writing its own data. Since the data is saved to the
    1.25 + * profile, this must be completed during phase profileBeforeChange.
    1.26 + *
    1.27 + * AsyncShutdown.profileBeforeChange.addBlocker(
    1.28 + *   "Add-on manager: shutting down",
    1.29 + *   function condition() {
    1.30 + *     // Do things.
    1.31 + *     // Perform I/O that must take place during phase profile-before-change
    1.32 + *     return promise;
    1.33 + *   }
    1.34 + * });
    1.35 + *
    1.36 + * In this example, function |condition| will be called at some point
    1.37 + * during phase profileBeforeChange and phase profileBeforeChange
    1.38 + * itself is guaranteed to not terminate until |promise| is either
    1.39 + * resolved or rejected.
    1.40 + */
    1.41 +
    1.42 +"use strict";
    1.43 +
    1.44 +const Cu = Components.utils;
    1.45 +const Cc = Components.classes;
    1.46 +const Ci = Components.interfaces;
    1.47 +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
    1.48 +Cu.import("resource://gre/modules/Services.jsm", this);
    1.49 +
    1.50 +XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    1.51 +  "resource://gre/modules/Promise.jsm");
    1.52 +XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
    1.53 +  "@mozilla.org/xpcom/debug;1", "nsIDebug");
    1.54 +Object.defineProperty(this, "gCrashReporter", {
    1.55 +  get: function() {
    1.56 +    delete this.gCrashReporter;
    1.57 +    try {
    1.58 +      let reporter = Cc["@mozilla.org/xre/app-info;1"].
    1.59 +            getService(Ci.nsICrashReporter);
    1.60 +      return this.gCrashReporter = reporter;
    1.61 +    } catch (ex) {
    1.62 +      return this.gCrashReporter = null;
    1.63 +    }
    1.64 +  },
    1.65 +  configurable: true
    1.66 +});
    1.67 +
    1.68 +// Display timeout warnings after 10 seconds
    1.69 +const DELAY_WARNING_MS = 10 * 1000;
    1.70 +
    1.71 +
    1.72 +// Crash the process if shutdown is really too long
    1.73 +// (allowing for sleep).
    1.74 +const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
    1.75 +let DELAY_CRASH_MS = 60 * 1000; // One minute
    1.76 +try {
    1.77 +  DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
    1.78 +} catch (ex) {
    1.79 +  // Ignore errors
    1.80 +}
    1.81 +Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
    1.82 +  DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
    1.83 +}, false);
    1.84 +
    1.85 +
    1.86 +/**
    1.87 + * Display a warning.
    1.88 + *
    1.89 + * As this code is generally used during shutdown, there are chances
    1.90 + * that the UX will not be available to display warnings on the
    1.91 + * console. We therefore use dump() rather than Cu.reportError().
    1.92 + */
    1.93 +function log(msg, prefix = "", error = null) {
    1.94 +  dump(prefix + msg + "\n");
    1.95 +  if (error) {
    1.96 +    dump(prefix + error + "\n");
    1.97 +    if (typeof error == "object" && "stack" in error) {
    1.98 +      dump(prefix + error.stack + "\n");
    1.99 +    }
   1.100 +  }
   1.101 +}
   1.102 +function warn(msg, error = null) {
   1.103 +  return log(msg, "WARNING: ", error);
   1.104 +}
   1.105 +function err(msg, error = null) {
   1.106 +  return log(msg, "ERROR: ", error);
   1.107 +}
   1.108 +
   1.109 +// Utility function designed to get the current state of execution
   1.110 +// of a blocker.
   1.111 +// We are a little paranoid here to ensure that in case of evaluation
   1.112 +// error we do not block the AsyncShutdown.
   1.113 +function safeGetState(state) {
   1.114 +  if (!state) {
   1.115 +    return "(none)";
   1.116 +  }
   1.117 +  let data, string;
   1.118 +  try {
   1.119 +    // Evaluate state(), normalize the result into something that we can
   1.120 +    // safely stringify or upload.
   1.121 +    string = JSON.stringify(state());
   1.122 +    data = JSON.parse(string);
   1.123 +    // Simplify the rest of the code by ensuring that we can simply
   1.124 +    // concatenate the result to a message.
   1.125 +    if (data && typeof data == "object") {
   1.126 +      data.toString = function() {
   1.127 +        return string;
   1.128 +      };
   1.129 +    }
   1.130 +    return data;
   1.131 +  } catch (ex) {
   1.132 +    if (string) {
   1.133 +      return string;
   1.134 +    }
   1.135 +    try {
   1.136 +      return "Error getting state: " + ex + " at " + ex.stack;
   1.137 +    } catch (ex2) {
   1.138 +      return "Error getting state but could not display error";
   1.139 +    }
   1.140 +  }
   1.141 +}
   1.142 +
   1.143 +/**
   1.144 + * Countdown for a given duration, skipping beats if the computer is too busy,
   1.145 + * sleeping or otherwise unavailable.
   1.146 + *
   1.147 + * @param {number} delay An approximate delay to wait in milliseconds (rounded
   1.148 + * up to the closest second).
   1.149 + *
   1.150 + * @return Deferred
   1.151 + */
   1.152 +function looseTimer(delay) {
   1.153 +  let DELAY_BEAT = 1000;
   1.154 +  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   1.155 +  let beats = Math.ceil(delay / DELAY_BEAT);
   1.156 +  let deferred = Promise.defer();
   1.157 +  timer.initWithCallback(function() {
   1.158 +    if (beats <= 0) {
   1.159 +      deferred.resolve();
   1.160 +    }
   1.161 +    --beats;
   1.162 +  }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
   1.163 +  // Ensure that the timer is both canceled once we are done with it
   1.164 +  // and not garbage-collected until then.
   1.165 +  deferred.promise.then(() => timer.cancel(), () => timer.cancel());
   1.166 +  return deferred;
   1.167 +}
   1.168 +
   1.169 +this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
   1.170 +
   1.171 +/**
   1.172 + * {string} topic -> phase
   1.173 + */
   1.174 +let gPhases = new Map();
   1.175 +
   1.176 +this.AsyncShutdown = {
   1.177 +  /**
   1.178 +   * Access function getPhase. For testing purposes only.
   1.179 +   */
   1.180 +  get _getPhase() {
   1.181 +    let accepted = false;
   1.182 +    try {
   1.183 +      accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing");
   1.184 +    } catch (ex) {
   1.185 +      // Ignore errors
   1.186 +    }
   1.187 +    if (accepted) {
   1.188 +      return getPhase;
   1.189 +    }
   1.190 +    return undefined;
   1.191 +  }
   1.192 +};
   1.193 +
   1.194 +/**
   1.195 + * Register a new phase.
   1.196 + *
   1.197 + * @param {string} topic The notification topic for this Phase.
   1.198 + * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications}
   1.199 + */
   1.200 +function getPhase(topic) {
   1.201 +  let phase = gPhases.get(topic);
   1.202 +  if (phase) {
   1.203 +    return phase;
   1.204 +  }
   1.205 +  let spinner = new Spinner(topic);
   1.206 +  phase = Object.freeze({
   1.207 +    /**
   1.208 +     * Register a blocker for the completion of a phase.
   1.209 +     *
   1.210 +     * @param {string} name The human-readable name of the blocker. Used
   1.211 +     * for debugging/error reporting. Please make sure that the name
   1.212 +     * respects the following model: "Some Service: some action in progress" -
   1.213 +     * for instance "OS.File: flushing all pending I/O";
   1.214 +     * @param {function|promise|*} condition A condition blocking the
   1.215 +     * completion of the phase. Generally, this is a function
   1.216 +     * returning a promise. This function is evaluated during the
   1.217 +     * phase and the phase is guaranteed to not terminate until the
   1.218 +     * resulting promise is either resolved or rejected. If
   1.219 +     * |condition| is not a function but another value |v|, it behaves
   1.220 +     * as if it were a function returning |v|.
   1.221 +     * @param {function*} state Optionally, a function returning
   1.222 +     * information about the current state of the blocker as an
   1.223 +     * object. Used for providing more details when logging errors or
   1.224 +     * crashing.
   1.225 +     *
   1.226 +     * Examples:
   1.227 +     * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise",
   1.228 +     *      promise); // profileBeforeChange will not complete until
   1.229 +     *                // promise is resolved or rejected
   1.230 +     *
   1.231 +     * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback",
   1.232 +     *     function callback() {
   1.233 +     *       // ...
   1.234 +     *       // Execute this code during profileBeforeChange
   1.235 +     *       return promise;
   1.236 +     *       // profileBeforeChange will not complete until promise
   1.237 +     *       // is resolved or rejected
   1.238 +     * });
   1.239 +     *
   1.240 +     * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback",
   1.241 +     *     function callback() {
   1.242 +     *       // ...
   1.243 +     *       // Execute this code during profileBeforeChange
   1.244 +     *       // No specific guarantee about completion of profileBeforeChange
   1.245 +     * });
   1.246 +     *
   1.247 +     */
   1.248 +    addBlocker: function(name, condition, state = null) {
   1.249 +      if (typeof name != "string") {
   1.250 +        throw new TypeError("Expected a human-readable name as first argument");
   1.251 +      }
   1.252 +      if (state && typeof state != "function") {
   1.253 +        throw new TypeError("Expected nothing or a function as third argument");
   1.254 +      }
   1.255 +      spinner.addBlocker({name: name, condition: condition, state: state});
   1.256 +    }
   1.257 +  });
   1.258 +  gPhases.set(topic, phase);
   1.259 +  return phase;
   1.260 +}
   1.261 +
   1.262 +/**
   1.263 + * Utility class used to spin the event loop until all blockers for a
   1.264 + * Phase are satisfied.
   1.265 + *
   1.266 + * @param {string} topic The xpcom notification for that phase.
   1.267 + */
   1.268 +function Spinner(topic) {
   1.269 +  this._topic = topic;
   1.270 +  this._conditions = new Set(); // set to |null| once it is too late to register
   1.271 +  Services.obs.addObserver(this, topic, false);
   1.272 +}
   1.273 +
   1.274 +Spinner.prototype = {
   1.275 +  /**
   1.276 +   * Register a new condition for this phase.
   1.277 +   *
   1.278 +   * @param {object} condition A Condition that must be fulfilled before
   1.279 +   * we complete this Phase.
   1.280 +   * Must contain fields:
   1.281 +   * - {string} name The human-readable name of the condition. Used
   1.282 +   * for debugging/error reporting.
   1.283 +   * - {function} action An action that needs to be completed
   1.284 +   * before we proceed to the next runstate. If |action| returns a promise,
   1.285 +   * we wait until the promise is resolved/rejected before proceeding
   1.286 +   * to the next runstate.
   1.287 +   */
   1.288 +  addBlocker: function(condition) {
   1.289 +    if (!this._conditions) {
   1.290 +      throw new Error("Phase " + this._topic +
   1.291 +                      " has already begun, it is too late to register" +
   1.292 +                      " completion condition '" + condition.name + "'.");
   1.293 +    }
   1.294 +    this._conditions.add(condition);
   1.295 +  },
   1.296 +
   1.297 +  observe: function() {
   1.298 +    let topic = this._topic;
   1.299 +    Services.obs.removeObserver(this, topic);
   1.300 +
   1.301 +    let conditions = this._conditions;
   1.302 +    this._conditions = null; // Too late to register
   1.303 +
   1.304 +    if (conditions.size == 0) {
   1.305 +      // No need to spin anything
   1.306 +      return;
   1.307 +    }
   1.308 +
   1.309 +    // The promises for which we are waiting.
   1.310 +    let allPromises = [];
   1.311 +
   1.312 +    // Information to determine and report to the user which conditions
   1.313 +    // are not satisfied yet.
   1.314 +    let allMonitors = [];
   1.315 +
   1.316 +    for (let {condition, name, state} of conditions) {
   1.317 +      // Gather all completion conditions
   1.318 +
   1.319 +      try {
   1.320 +        if (typeof condition == "function") {
   1.321 +          // Normalize |condition| to the result of the function.
   1.322 +          try {
   1.323 +            condition = condition(topic);
   1.324 +          } catch (ex) {
   1.325 +            condition = Promise.reject(ex);
   1.326 +          }
   1.327 +        }
   1.328 +        // Normalize to a promise. Of course, if |condition| was not a
   1.329 +        // promise in the first place (in particular if the above
   1.330 +        // function returned |undefined| or failed), that new promise
   1.331 +        // isn't going to be terribly interesting, but it will behave
   1.332 +        // as a promise.
   1.333 +        condition = Promise.resolve(condition);
   1.334 +
   1.335 +        // If the promise takes too long to be resolved/rejected,
   1.336 +        // we need to notify the user.
   1.337 +        //
   1.338 +        // If it takes way too long, we need to crash.
   1.339 +
   1.340 +        let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   1.341 +        timer.initWithCallback(function() {
   1.342 +          let msg = "A phase completion condition is" +
   1.343 +            " taking too long to complete." +
   1.344 +            " Condition: " + monitor.name +
   1.345 +            " Phase: " + topic +
   1.346 +            " State: " + safeGetState(state);
   1.347 +          warn(msg);
   1.348 +        }, DELAY_WARNING_MS, Ci.nsITimer.TYPE_ONE_SHOT);
   1.349 +
   1.350 +        let monitor = {
   1.351 +          isFrozen: true,
   1.352 +          name: name,
   1.353 +          state: state
   1.354 +        };
   1.355 +        condition = condition.then(function onSuccess() {
   1.356 +            timer.cancel(); // As a side-effect, this prevents |timer| from
   1.357 +                            // being garbage-collected too early.
   1.358 +            monitor.isFrozen = false;
   1.359 +          }, function onError(error) {
   1.360 +            timer.cancel();
   1.361 +            let msg = "A completion condition encountered an error" +
   1.362 +                " while we were spinning the event loop." +
   1.363 +                " Condition: " + name +
   1.364 +                " Phase: " + topic +
   1.365 +                " State: " + safeGetState(state);
   1.366 +            warn(msg, error);
   1.367 +            monitor.isFrozen = false;
   1.368 +        });
   1.369 +        allMonitors.push(monitor);
   1.370 +        allPromises.push(condition);
   1.371 +
   1.372 +      } catch (error) {
   1.373 +          let msg = "A completion condition encountered an error" +
   1.374 +                " while we were initializing the phase." +
   1.375 +                " Condition: " + name +
   1.376 +                " Phase: " + topic +
   1.377 +                " State: " + safeGetState(state);
   1.378 +          warn(msg, error);
   1.379 +      }
   1.380 +
   1.381 +    }
   1.382 +    conditions = null;
   1.383 +
   1.384 +    let promise = Promise.all(allPromises);
   1.385 +    allPromises = null;
   1.386 +
   1.387 +    promise = promise.then(null, function onError(error) {
   1.388 +      // I don't think that this can happen.
   1.389 +      // However, let's be overcautious with async/shutdown error reporting.
   1.390 +      let msg = "An uncaught error appeared while completing the phase." +
   1.391 +            " Phase: " + topic;
   1.392 +      warn(msg, error);
   1.393 +    });
   1.394 +
   1.395 +    let satisfied = false; // |true| once we have satisfied all conditions
   1.396 +
   1.397 +    // If after DELAY_CRASH_MS (approximately one minute, adjusted to take
   1.398 +    // into account sleep and otherwise busy computer) we have not finished
   1.399 +    // this shutdown phase, we assume that the shutdown is somehow frozen,
   1.400 +    // presumably deadlocked. At this stage, the only thing we can do to
   1.401 +    // avoid leaving the user's computer in an unstable (and battery-sucking)
   1.402 +    // situation is report the issue and crash.
   1.403 +    let timeToCrash = looseTimer(DELAY_CRASH_MS);
   1.404 +    timeToCrash.promise.then(
   1.405 +      function onTimeout() {
   1.406 +        // Report the problem as best as we can, then crash.
   1.407 +        let frozen = [];
   1.408 +        let states = [];
   1.409 +        for (let {name, isFrozen, state} of allMonitors) {
   1.410 +          if (isFrozen) {
   1.411 +            frozen.push({name: name, state: safeGetState(state)});
   1.412 +          }
   1.413 +        }
   1.414 +
   1.415 +        let msg = "At least one completion condition failed to complete" +
   1.416 +              " within a reasonable amount of time. Causing a crash to" +
   1.417 +              " ensure that we do not leave the user with an unresponsive" +
   1.418 +              " process draining resources." +
   1.419 +              " Conditions: " + JSON.stringify(frozen) +
   1.420 +              " Phase: " + topic;
   1.421 +        err(msg);
   1.422 +        if (gCrashReporter && gCrashReporter.enabled) {
   1.423 +          let data = {
   1.424 +            phase: topic,
   1.425 +            conditions: frozen
   1.426 +          };
   1.427 +          gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
   1.428 +            JSON.stringify(data));
   1.429 +        } else {
   1.430 +          warn("No crash reporter available");
   1.431 +        }
   1.432 +
   1.433 +        let error = new Error();
   1.434 +        gDebug.abort(error.fileName, error.lineNumber + 1);
   1.435 +      },
   1.436 +      function onSatisfied() {
   1.437 +        // The promise has been rejected, which means that we have satisfied
   1.438 +        // all completion conditions.
   1.439 +      });
   1.440 +
   1.441 +    promise = promise.then(function() {
   1.442 +      satisfied = true;
   1.443 +      timeToCrash.reject();
   1.444 +    }/* No error is possible here*/);
   1.445 +
   1.446 +    // Now, spin the event loop
   1.447 +    let thread = Services.tm.mainThread;
   1.448 +    while(!satisfied) {
   1.449 +      thread.processNextEvent(true);
   1.450 +    }
   1.451 +  }
   1.452 +};
   1.453 +
   1.454 +
   1.455 +// List of well-known runstates
   1.456 +// Ideally, runstates should be registered from the component that decides
   1.457 +// when they start/stop. For compatibility with existing startup/shutdown
   1.458 +// mechanisms, we register a few runstates here.
   1.459 +
   1.460 +this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
   1.461 +this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
   1.462 +this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change2");
   1.463 +this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
   1.464 +Object.freeze(this.AsyncShutdown);

mercurial