diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/components/asyncshutdown/AsyncShutdown.jsm | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/asyncshutdown/AsyncShutdown.jsm')
-rw-r--r-- | toolkit/components/asyncshutdown/AsyncShutdown.jsm | 1041 |
1 files changed, 1041 insertions, 0 deletions
diff --git a/toolkit/components/asyncshutdown/AsyncShutdown.jsm b/toolkit/components/asyncshutdown/AsyncShutdown.jsm new file mode 100644 index 000000000..62ac36f42 --- /dev/null +++ b/toolkit/components/asyncshutdown/AsyncShutdown.jsm @@ -0,0 +1,1041 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Managing safe shutdown of asynchronous services. + * + * Firefox shutdown is composed of phases that take place + * sequentially. Typically, each shutdown phase removes some + * capabilities from the application. For instance, at the end of + * phase profileBeforeChange, no service is permitted to write to the + * profile directory (with the exception of Telemetry). Consequently, + * if any service has requested I/O to the profile directory before or + * during phase profileBeforeChange, the system must be informed that + * these requests need to be completed before the end of phase + * profileBeforeChange. Failing to inform the system of this + * requirement can (and has been known to) cause data loss. + * + * Example: At some point during shutdown, the Add-On Manager needs to + * ensure that all add-ons have safely written their data to disk, + * before writing its own data. Since the data is saved to the + * profile, this must be completed during phase profileBeforeChange. + * + * AsyncShutdown.profileBeforeChange.addBlocker( + * "Add-on manager: shutting down", + * function condition() { + * // Do things. + * // Perform I/O that must take place during phase profile-before-change + * return promise; + * } + * }); + * + * In this example, function |condition| will be called at some point + * during phase profileBeforeChange and phase profileBeforeChange + * itself is guaranteed to not terminate until |promise| is either + * resolved or rejected. + */ + +"use strict"; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", + "resource://gre/modules/PromiseUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "gDebug", + "@mozilla.org/xpcom/debug;1", "nsIDebug2"); +Object.defineProperty(this, "gCrashReporter", { + get: function() { + delete this.gCrashReporter; + try { + let reporter = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsICrashReporter); + return this.gCrashReporter = reporter; + } catch (ex) { + return this.gCrashReporter = null; + } + }, + configurable: true +}); + +// `true` if this is a content process, `false` otherwise. +// It would be nicer to go through `Services.appInfo`, but some tests need to be +// able to replace that field with a custom implementation before it is first +// called. +const isContent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT; + +// Display timeout warnings after 10 seconds +const DELAY_WARNING_MS = 10 * 1000; + + +// Crash the process if shutdown is really too long +// (allowing for sleep). +const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout"; +var DELAY_CRASH_MS = 60 * 1000; // One minute +try { + DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS); +} catch (ex) { + // Ignore errors +} +Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() { + DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS); +}, false); + +/** + * A set of Promise that supports waiting. + * + * Promise items may be added or removed during the wait. The wait will + * resolve once all Promise items have been resolved or removed. + */ +function PromiseSet() { + /** + * key: the Promise passed pass the client of the `PromiseSet`. + * value: an indirection on top of `key`, as an object with + * the following fields: + * - indirection: a Promise resolved if `key` is resolved or + * if `resolve` is called + * - resolve: a function used to resolve the indirection. + */ + this._indirections = new Map(); +} +PromiseSet.prototype = { + /** + * Wait until all Promise have been resolved or removed. + * + * Note that calling `wait()` causes Promise to be removed from the + * Set once they are resolved. + * + * @return {Promise} Resolved once all Promise have been resolved or removed, + * or rejected after at least one Promise has rejected. + */ + wait: function() { + // Pick an arbitrary element in the map, if any exists. + let entry = this._indirections.entries().next(); + if (entry.done) { + // No indirections left, we are done. + return Promise.resolve(); + } + + let [, indirection] = entry.value; + let promise = indirection.promise; + promise = promise.then(() => + // At this stage, the entry has been cleaned up. + this.wait() + ); + return promise; + }, + + /** + * Add a new Promise to the set. + * + * Calls to wait (including ongoing calls) will only return once + * `key` has either resolved or been removed. + */ + add: function(key) { + this._ensurePromise(key); + let indirection = PromiseUtils.defer(); + key.then( + x => { + // Clean up immediately. + // This needs to be done before the call to `resolve`, otherwise + // `wait()` may loop forever. + this._indirections.delete(key); + indirection.resolve(x); + }, + err => { + this._indirections.delete(key); + indirection.reject(err); + }); + this._indirections.set(key, indirection); + }, + + /** + * Remove a Promise from the set. + * + * Calls to wait (including ongoing calls) will ignore this promise, + * unless it is added again. + */ + delete: function(key) { + this._ensurePromise(key); + let value = this._indirections.get(key); + if (!value) { + return false; + } + this._indirections.delete(key); + value.resolve(); + return true; + }, + + _ensurePromise: function(key) { + if (!key || typeof key != "object") { + throw new Error("Expected an object"); + } + if ((!("then" in key)) || typeof key.then != "function") { + throw new Error("Expected a Promise"); + } + }, + +}; + + +/** + * 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) { + 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 +} +Services.prefs.addObserver(PREF_DEBUG_LOG, function() { + DEBUG_LOG = Services.prefs.getBoolPref(PREF_DEBUG_LOG); +}, false); + +function debug(msg, error=null) { + if (DEBUG_LOG) { + log(msg, "DEBUG: ", error); + } +} +function warn(msg, error = null) { + log(msg, "WARNING: ", error); +} +function fatalerr(msg, error = null) { + log(msg, "FATAL ERROR: ", error); +} + +// Utility function designed to get the current state of execution +// of a blocker. +// We are a little paranoid here to ensure that in case of evaluation +// error we do not block the AsyncShutdown. +function safeGetState(fetchState) { + if (!fetchState) { + return "(none)"; + } + let data, string; + try { + // Evaluate fetchState(), normalize the result into something that we can + // safely stringify or upload. + let state = fetchState(); + if (!state) { + return "(none)"; + } + string = JSON.stringify(state); + data = JSON.parse(string); + // Simplify the rest of the code by ensuring that we can simply + // concatenate the result to a message. + if (data && typeof data == "object") { + data.toString = function() { + return string; + }; + } + return data; + } catch (ex) { + + // Make sure that this causes test failures + Promise.reject(ex); + + if (string) { + return string; + } + try { + return "Error getting state: " + ex + " at " + ex.stack; + } catch (ex2) { + return "Error getting state but could not display error"; + } + } +} + +/** + * Countdown for a given duration, skipping beats if the computer is too busy, + * sleeping or otherwise unavailable. + * + * @param {number} delay An approximate delay to wait in milliseconds (rounded + * up to the closest second). + * + * @return Deferred + */ +function looseTimer(delay) { + let DELAY_BEAT = 1000; + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let beats = Math.ceil(delay / DELAY_BEAT); + let deferred = Promise.defer(); + timer.initWithCallback(function() { + if (beats <= 0) { + deferred.resolve(); + } + --beats; + }, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP); + // Ensure that the timer is both canceled once we are done with it + // and not garbage-collected until then. + deferred.promise.then(() => timer.cancel(), () => timer.cancel()); + return deferred; +} + +/** + * Given an nsIStackFrame object, find the caller filename, line number, + * and stack if necessary, and return them as an object. + * + * @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) { + 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 + } + + 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"); + } + + 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>", + } + } +} + +this.EXPORTED_SYMBOLS = ["AsyncShutdown"]; + +/** + * {string} topic -> phase + */ +var gPhases = new Map(); + +this.AsyncShutdown = { + /** + * Access function getPhase. For testing purposes only. + */ + get _getPhase() { + let accepted = false; + try { + accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing"); + } catch (ex) { + // Ignore errors + } + if (accepted) { + return getPhase; + } + return undefined; + } +}; + +/** + * Register a new phase. + * + * @param {string} topic The notification topic for this Phase. + * @see {https://developer.mozilla.org/en-US/docs/Observer_Notifications} + */ +function getPhase(topic) { + let phase = gPhases.get(topic); + if (phase) { + return phase; + } + let spinner = new Spinner(topic); + phase = Object.freeze({ + /** + * Register a blocker for the completion of a phase. + * + * @param {string} name The human-readable name of the blocker. Used + * for debugging/error reporting. Please make sure that the name + * respects the following model: "Some Service: some action in progress" - + * for instance "OS.File: flushing all pending I/O"; + * @param {function|promise|*} condition A condition blocking the + * completion of the phase. Generally, this is a function + * returning a promise. This function is evaluated during the + * phase and the phase is guaranteed to not terminate until the + * resulting promise is either resolved or rejected. If + * |condition| is not a function but another value |v|, it behaves + * as if it were a function returning |v|. + * @param {object*} details Optionally, an object with details + * that may be useful for error reporting, as a subset of of the following + * fields: + * - fetchState (strongly recommended) A function returning + * information about the current state of the blocker as an + * object. Used for providing more details when logging errors or + * crashing. + * - stack. A string containing stack information. This module can + * generally infer stack information if it is not provided. + * - lineNumber A number containing the line number for the caller. + * This module can generally infer this information if it is not + * provided. + * - filename A string containing the filename for the caller. This + * module can generally infer the information if it is not provided. + * + * Examples: + * AsyncShutdown.profileBeforeChange.addBlocker("Module: just a promise", + * promise); // profileBeforeChange will not complete until + * // promise is resolved or rejected + * + * AsyncShutdown.profileBeforeChange.addBlocker("Module: a callback", + * function callback() { + * // ... + * // Execute this code during profileBeforeChange + * return promise; + * // profileBeforeChange will not complete until promise + * // is resolved or rejected + * }); + * + * AsyncShutdown.profileBeforeChange.addBlocker("Module: trivial callback", + * function callback() { + * // ... + * // Execute this code during profileBeforeChange + * // No specific guarantee about completion of profileBeforeChange + * }); + */ + addBlocker: function(name, condition, details = null) { + spinner.addBlocker(name, condition, details); + }, + /** + * Remove the blocker for a condition. + * + * If several blockers have been registered for the same + * condition, remove all these blockers. If no blocker has been + * registered for this condition, this is a noop. + * + * @return {boolean} true if a blocker has been removed, false + * otherwise. Note that a result of false may mean either that + * the blocker has never been installed or that the phase has + * completed and the blocker has already been resolved. + */ + removeBlocker: function(condition) { + return spinner.removeBlocker(condition); + }, + + get name() { + return spinner.name; + }, + + /** + * Trigger the phase without having to broadcast a + * notification. For testing purposes only. + */ + get _trigger() { + let accepted = false; + try { + accepted = Services.prefs.getBoolPref("toolkit.asyncshutdown.testing"); + } catch (ex) { + // Ignore errors + } + if (accepted) { + return () => spinner.observe(); + } + return undefined; + } + }); + gPhases.set(topic, phase); + return phase; +} + +/** + * Utility class used to spin the event loop until all blockers for a + * Phase are satisfied. + * + * @param {string} topic The xpcom notification for that phase. + */ +function Spinner(topic) { + this._barrier = new Barrier(topic); + this._topic = topic; + Services.obs.addObserver(this, topic, false); +} + +Spinner.prototype = { + /** + * Register a new condition for this phase. + * + * See the documentation of `addBlocker` in property `client` + * of instances of `Barrier`. + */ + addBlocker: function(name, condition, details) { + this._barrier.client.addBlocker(name, condition, details); + }, + /** + * Remove the blocker for a condition. + * + * See the documentation of `removeBlocker` in rpoperty `client` + * of instances of `Barrier` + * + * @return {boolean} true if a blocker has been removed, false + * otherwise. Note that a result of false may mean either that + * the blocker has never been installed or that the phase has + * completed and the blocker has already been resolved. + */ + removeBlocker: function(condition) { + return this._barrier.client.removeBlocker(condition); + }, + + get name() { + return this._barrier.client.name; + }, + + // nsIObserver.observe + observe: function() { + let topic = this._topic; + debug(`Starting phase ${ topic }`); + Services.obs.removeObserver(this, topic); + + let satisfied = false; // |true| once we have satisfied all conditions + 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 waiting for notification"); + 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. + Promise.reject(ex); + } + } + debug(`Finished phase ${ topic }`); + } +}; + +/** + * A mechanism used to register blockers that prevent some action from + * happening. + * + * An instance of |Barrier| provides a capability |client| that + * clients can use to register blockers. The barrier is resolved once + * all registered blockers have been resolved. The owner of the + * |Barrier| may wait for the resolution of the barrier and obtain + * information on which blockers have not been resolved yet. + * + * @param {string} name The name of the blocker. Used mainly for error- + * reporting. + */ +function Barrier(name) { + if (!name) { + throw new TypeError("Instances of Barrier need a (non-empty) name"); + } + + + /** + * The set of all Promise for which we need to wait before the barrier + * is lifted. Note that this set may be changed while we are waiting. + * + * Set to `null` once the wait is complete. + */ + this._waitForMe = new PromiseSet(); + + /** + * A map from conditions, as passed by users during the call to `addBlocker`, + * to `promise`, as present in `this._waitForMe`. + * + * Used to let users perform cleanup through `removeBlocker`. + * Set to `null` once the wait is complete. + * + * Key: condition (any, as passed by user) + * Value: promise used as a key in `this._waitForMe`. Note that there is + * no guarantee that the key is still present in `this._waitForMe`. + */ + this._conditionToPromise = new Map(); + + /** + * A map from Promise, as present in `this._waitForMe` or + * `this._conditionToPromise`, to information on blockers. + * + * Key: Promise (as present in this._waitForMe or this._conditionToPromise). + * Value: { + * trigger: function, + * promise, + * name, + * fetchState: function, + * stack, + * filename, + * 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; + + /** + * `true` once we have started waiting. + */ + this._isStarted = false; + + /** + * The capability of adding blockers. This object may safely be returned + * or passed to clients. + */ + this.client = { + /** + * The name of the barrier owning this client. + */ + get name() { + return name; + }, + + /** + * Register a blocker for the completion of this barrier. + * + * @param {string} name The human-readable name of the blocker. Used + * for debugging/error reporting. Please make sure that the name + * respects the following model: "Some Service: some action in progress" - + * for instance "OS.File: flushing all pending I/O"; + * @param {function|promise|*} condition A condition blocking the + * completion of the phase. Generally, this is a function + * returning a promise. This function is evaluated during the + * phase and the phase is guaranteed to not terminate until the + * resulting promise is either resolved or rejected. If + * |condition| is not a function but another value |v|, it behaves + * as if it were a function returning |v|. + * @param {object*} details Optionally, an object with details + * that may be useful for error reporting, as a subset of of the following + * fields: + * - fetchState (strongly recommended) A function returning + * information about the current state of the blocker as an + * object. Used for providing more details when logging errors or + * crashing. + * - stack. A string containing stack information. This module can + * generally infer stack information if it is not provided. + * - lineNumber A number containing the line number for the caller. + * This module can generally infer this information if it is not + * provided. + * - filename A string containing the filename for the caller. This + * module can generally infer the information if it is not provided. + */ + addBlocker: (name, condition, details) => { + if (typeof name != "string") { + throw new TypeError("Expected a human-readable name as first argument"); + } + if (details && typeof details == "function") { + details = { + fetchState: details + }; + } else if (!details) { + details = {}; + } + if (typeof details != "object") { + throw new TypeError("Expected an object as third argument to `addBlocker`, got " + details); + } + 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 + // condition is complete, and never throws. + let trigger; + + // A promise resolved once the condition is complete. + let promise; + if (typeof condition == "function") { + promise = new Promise((resolve, reject) => { + trigger = () => { + try { + resolve(condition()); + } catch (ex) { + reject(ex); + } + } + }); + } else { + // If `condition` is not a function, `trigger` is not particularly + // interesting, and `condition` needs to be normalized to a promise. + trigger = () => {}; + promise = Promise.resolve(condition); + } + + // Make sure that `promise` never rejects. + promise = promise.then(null, error => { + let msg = `A blocker encountered an error while we were waiting. + 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, + promise: promise, + name: name, + fetchState: fetchState, + getOrigin: () => getOrigin(topFrame, filename, lineNumber, stack), + }; + + this._waitForMe.add(promise); + this._promiseToBlocker.set(promise, blocker); + this._conditionToPromise.set(condition, promise); + + // As conditions may hold lots of memory, we attempt to cleanup + // as soon as we are done (which might be in the next tick, if + // we have been passed a resolved promise). + promise = promise.then(() => { + debug(`Completed blocker ${ name } for phase ${ this._name }`); + this._removeBlocker(condition); + }); + + if (this._isStarted) { + // The wait has already started. The blocker should be + // notified asap. We do it out of band as clients probably + // expect `addBlocker` to return immediately. + Promise.resolve().then(trigger); + } + }, + + /** + * Remove the blocker for a condition. + * + * If several blockers have been registered for the same + * condition, remove all these blockers. If no blocker has been + * registered for this condition, this is a noop. + * + * @return {boolean} true if at least one blocker has been + * removed, false otherwise. + */ + removeBlocker: (condition) => { + return this._removeBlocker(condition); + } + }; +} +Barrier.prototype = Object.freeze({ + /** + * The current state of the barrier, as a JSON-serializable object + * designed for error-reporting. + */ + get state() { + if (!this._isStarted) { + return "Not started"; + } + if (!this._waitForMe) { + return "Complete"; + } + let frozen = []; + for (let blocker of this._promiseToBlocker.values()) { + let {name, fetchState} = blocker; + let {stack, filename, lineNumber} = blocker.getOrigin(); + frozen.push({ + name: name, + state: safeGetState(fetchState), + filename: filename, + lineNumber: lineNumber, + stack: stack + }); + } + return frozen; + }, + + /** + * Wait until all currently registered blockers are complete. + * + * Once this method has been called, any attempt to register a new blocker + * for this barrier will cause an error. + * + * Successive calls to this method always return the same value. + * + * @param {object=} options Optionally, an object that may contain + * the following fields: + * {number} warnAfterMS If provided and > 0, print a warning if the barrier + * has not been resolved after the given number of milliseconds. + * {number} crashAfterMS If provided and > 0, crash the process if the barrier + * has not been resolved after the give number of milliseconds (rounded up + * to the next second). To avoid crashing simply because the computer is busy + * or going to sleep, we actually wait for ceil(crashAfterMS/1000) successive + * periods of at least one second. Upon crashing, if a crash reporter is present, + * prepare a crash report with the state of this barrier. + * + * + * @return {Promise} A promise satisfied once all blockers are complete. + */ + wait: function(options = {}) { + // This method only implements caching on top of _wait() + if (this._promise) { + return this._promise; + } + return this._promise = this._wait(options); + }, + _wait: function(options) { + + // Sanity checks + if (this._isStarted) { + throw new TypeError("Internal error: already started " + this._name); + } + if (!this._waitForMe || !this._conditionToPromise || !this._promiseToBlocker) { + throw new TypeError("Internal error: already finished " + this._name); + } + + let topic = this._name; + + // Notify blockers + for (let blocker of this._promiseToBlocker.values()) { + blocker.trigger(); // We have guarantees that this method will never throw + } + + this._isStarted = true; + + // Now, wait + let promise = this._waitForMe.wait(); + + promise = promise.then(null, function onError(error) { + // I don't think that this can happen. + // However, let's be overcautious with async/shutdown error reporting. + let msg = "An uncaught error appeared while completing the phase." + + " Phase: " + topic; + warn(msg, error); + }); + + promise = promise.then(() => { + // Cleanup memory + this._waitForMe = null; + this._promiseToBlocker = null; + this._conditionToPromise = null; + }); + + // Now handle warnings and crashes + let warnAfterMS = DELAY_WARNING_MS; + if (options && "warnAfterMS" in options) { + if (typeof options.warnAfterMS == "number" + || options.warnAfterMS == null) { + // Change the delay or deactivate warnAfterMS + warnAfterMS = options.warnAfterMS; + } else { + throw new TypeError("Wrong option value for warnAfterMS"); + } + } + + if (warnAfterMS && warnAfterMS > 0) { + // If the promise takes too long to be resolved/rejected, + // we need to notify the user. + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(() => { + let msg = "At least one completion condition is taking too long to complete." + + " Conditions: " + JSON.stringify(this.state) + + " Barrier: " + topic; + warn(msg); + }, warnAfterMS, Ci.nsITimer.TYPE_ONE_SHOT); + + promise = promise.then(function onSuccess() { + timer.cancel(); + // As a side-effect, this prevents |timer| from + // being garbage-collected too early. + }); + } + + let crashAfterMS = DELAY_CRASH_MS; + if (options && "crashAfterMS" in options) { + if (typeof options.crashAfterMS == "number" + || options.crashAfterMS == null) { + // Change the delay or deactivate crashAfterMS + crashAfterMS = options.crashAfterMS; + } else { + throw new TypeError("Wrong option value for crashAfterMS"); + } + } + + if (crashAfterMS > 0) { + let timeToCrash = null; + + // If after |crashAfterMS| milliseconds (adjusted to take into + // account sleep and otherwise busy computer) we have not finished + // this shutdown phase, we assume that the shutdown is somehow + // frozen, presumably deadlocked. At this stage, the only thing we + // can do to avoid leaving the user's computer in an unstable (and + // battery-sucking) situation is report the issue and crash. + timeToCrash = looseTimer(crashAfterMS); + timeToCrash.promise.then( + function onTimeout() { + // Report the problem as best as we can, then crash. + let state = this.state; + + // If you change the following message, please make sure + // that any information on the topic and state appears + // within the first 200 characters of the message. This + // helps automatically sort oranges. + let msg = "AsyncShutdown timeout in " + topic + + " Conditions: " + JSON.stringify(state) + + " At least one completion condition failed to complete" + + " within a reasonable amount of time. Causing a crash to" + + " ensure that we do not leave the user with an unresponsive" + + " process draining resources."; + fatalerr(msg); + if (gCrashReporter && gCrashReporter.enabled) { + let data = { + phase: topic, + conditions: state + }; + gCrashReporter.annotateCrashReport("AsyncShutdownTimeout", + JSON.stringify(data)); + } else { + warn("No crash reporter available"); + } + + // To help sorting out bugs, we want to make sure that the + // call to nsIDebug2.abort points to a guilty client, rather + // than to AsyncShutdown itself. We pick a client that is + // still blocking and use its filename/lineNumber, + // which have been determined during the call to `addBlocker`. + let filename = "?"; + let lineNumber = -1; + for (let blocker of this._promiseToBlocker.values()) { + ({filename, lineNumber} = blocker.getOrigin()); + break; + } + gDebug.abort(filename, lineNumber); + }.bind(this), + function onSatisfied() { + // The promise has been rejected, which means that we have satisfied + // all completion conditions. + }); + + promise = promise.then(function() { + timeToCrash.reject(); + }/* No error is possible here*/); + } + + return promise; + }, + + _removeBlocker: function(condition) { + if (!this._waitForMe || !this._promiseToBlocker || !this._conditionToPromise) { + // We have already cleaned up everything. + return false; + } + + let promise = this._conditionToPromise.get(condition); + if (!promise) { + // The blocker has already been removed + return false; + } + this._conditionToPromise.delete(condition); + this._promiseToBlocker.delete(promise); + return this._waitForMe.delete(promise); + }, + +}); + + + +// List of well-known phases +// Ideally, phases should be registered from the component that decides +// when they start/stop. For compatibility with existing startup/shutdown +// mechanisms, we register a few phases here. + +// Parent process +if (!isContent) { + this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown"); + this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change"); + this.AsyncShutdown.placesClosingInternalConnection = getPhase("places-will-close-connection"); + this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change-telemetry"); +} + +// Notifications that fire in the parent and content process, but should +// only have phases in the parent process. +if (!isContent) { + this.AsyncShutdown.quitApplicationGranted = getPhase("quit-application-granted"); +} + +// Don't add a barrier for content-child-shutdown because this +// makes it easier to cause shutdown hangs. + +// All processes +this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown"); +this.AsyncShutdown.xpcomWillShutdown = getPhase("xpcom-will-shutdown"); + +this.AsyncShutdown.Barrier = Barrier; + +Object.freeze(this.AsyncShutdown); |