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);