Bug 918317 - Implementation of nsAsyncShutdown. r=froydnj, r=glandium
This commit is contained in:
843
toolkit/components/asyncshutdown/AsyncShutdown.jsm
Normal file
843
toolkit/components/asyncshutdown/AsyncShutdown.jsm
Normal file
@@ -0,0 +1,843 @@
|
||||
/* 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, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
|
||||
"@mozilla.org/xpcom/debug;1", "nsIDebug");
|
||||
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
|
||||
});
|
||||
|
||||
// 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";
|
||||
let 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);
|
||||
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
function warn(msg, error = null) {
|
||||
return log(msg, "WARNING: ", error);
|
||||
}
|
||||
function fatalerr(msg, error = null) {
|
||||
return 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;
|
||||
}
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
|
||||
|
||||
/**
|
||||
* {string} topic -> phase
|
||||
*/
|
||||
let 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;
|
||||
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
|
||||
});
|
||||
|
||||
// Now, spin 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 conditions registered by clients, as a map.
|
||||
*
|
||||
* Key: condition (function)
|
||||
* Value: Array of {name: string, fetchState: function, filename: string,
|
||||
* lineNumber: number, stack: string}
|
||||
*/
|
||||
this._conditions = new Map();
|
||||
|
||||
/**
|
||||
* Indirections, used to let clients cancel a blocker when they
|
||||
* call removeBlocker().
|
||||
*
|
||||
* Key: condition (function)
|
||||
* Value: Deferred.
|
||||
*/
|
||||
this._indirections = null;
|
||||
|
||||
/**
|
||||
* The name of the barrier.
|
||||
*/
|
||||
this._name = name;
|
||||
|
||||
/**
|
||||
* A cache for the promise returned by wait().
|
||||
*/
|
||||
this._promise = null;
|
||||
|
||||
/**
|
||||
* An array of objects used to monitor the state of each blocker.
|
||||
*/
|
||||
this._monitors = null;
|
||||
|
||||
/**
|
||||
* 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: function(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._conditions) {
|
||||
throw new Error("Phase " + this._name +
|
||||
" has already begun, it is too late to register" +
|
||||
" completion condition '" + name + "'.");
|
||||
}
|
||||
|
||||
let fetchState = details.fetchState || null;
|
||||
let filename = details.filename || "?";
|
||||
let lineNumber = details.lineNumber || -1;
|
||||
let stack = details.stack || undefined;
|
||||
|
||||
if (filename == "?" || lineNumber == -1 || stack === undefined) {
|
||||
// Determine the filename and line number of the caller.
|
||||
let leaf = Components.stack;
|
||||
let frame;
|
||||
for (frame = leaf; frame != null && frame.filename == leaf.filename; frame = frame.caller) {
|
||||
// Climb up the stack
|
||||
}
|
||||
|
||||
if (filename == "?") {
|
||||
filename = frame ? frame.filename : "?";
|
||||
}
|
||||
if (lineNumber == -1) {
|
||||
lineNumber = frame ? frame.lineNumber : -1;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (stack === undefined) {
|
||||
stack = Task.Debugging.generateReadableStack(frames.join("\n")).split("\n");
|
||||
}
|
||||
}
|
||||
|
||||
let set = this._conditions.get(condition);
|
||||
if (!set) {
|
||||
set = [];
|
||||
this._conditions.set(condition, set);
|
||||
}
|
||||
set.push({name: name,
|
||||
fetchState: fetchState,
|
||||
filename: filename,
|
||||
lineNumber: lineNumber,
|
||||
stack: stack});
|
||||
}.bind(this),
|
||||
|
||||
/**
|
||||
* 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: function(condition) {
|
||||
if (this._conditions) {
|
||||
// wait() hasn't been called yet.
|
||||
return this._conditions.delete(condition);
|
||||
}
|
||||
|
||||
if (this._indirections) {
|
||||
// wait() is in progress
|
||||
let deferred = this._indirections.get(condition);
|
||||
if (deferred) {
|
||||
// Unlock the blocker
|
||||
deferred.resolve();
|
||||
}
|
||||
return this._indirections.delete(condition);
|
||||
}
|
||||
// wait() is complete.
|
||||
return false;
|
||||
}.bind(this),
|
||||
};
|
||||
}
|
||||
Barrier.prototype = Object.freeze({
|
||||
/**
|
||||
* The current state of the barrier, as a JSON-serializable object
|
||||
* designed for error-reporting.
|
||||
*/
|
||||
get state() {
|
||||
if (this._conditions) {
|
||||
return "Not started";
|
||||
}
|
||||
if (!this._monitors) {
|
||||
return "Complete";
|
||||
}
|
||||
let frozen = [];
|
||||
for (let {name, isComplete, fetchState, stack, filename, lineNumber} of this._monitors) {
|
||||
if (!isComplete) {
|
||||
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) {
|
||||
let topic = this._name;
|
||||
let conditions = this._conditions;
|
||||
this._conditions = null; // Too late to register
|
||||
if (conditions.size == 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this._indirections = new Map();
|
||||
// The promises for which we are waiting.
|
||||
let allPromises = [];
|
||||
|
||||
// Information to determine and report to the user which conditions
|
||||
// are not satisfied yet.
|
||||
this._monitors = [];
|
||||
|
||||
for (let _condition of conditions.keys()) {
|
||||
for (let current of conditions.get(_condition)) {
|
||||
let condition = _condition; // Avoid capturing the wrong variable
|
||||
let {name, fetchState, stack, filename, lineNumber} = current;
|
||||
|
||||
// An indirection on top of condition, used to let clients
|
||||
// cancel a blocker through removeBlocker.
|
||||
let indirection = Promise.defer();
|
||||
this._indirections.set(condition, indirection);
|
||||
|
||||
// Gather all completion conditions
|
||||
|
||||
try {
|
||||
if (typeof condition == "function") {
|
||||
// Normalize |condition| to the result of the function.
|
||||
try {
|
||||
condition = condition(topic);
|
||||
} catch (ex) {
|
||||
condition = Promise.reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize to a promise. Of course, if |condition| was not a
|
||||
// promise in the first place (in particular if the above
|
||||
// function returned |undefined| or failed), that new promise
|
||||
// isn't going to be terribly interesting, but it will behave
|
||||
// as a promise.
|
||||
condition = Promise.resolve(condition);
|
||||
|
||||
let monitor = {
|
||||
isComplete: false,
|
||||
name: name,
|
||||
fetchState: fetchState,
|
||||
stack: stack,
|
||||
filename: filename,
|
||||
lineNumber: lineNumber
|
||||
};
|
||||
|
||||
condition = condition.then(null, function onError(error) {
|
||||
let msg = "A completion condition encountered an error" +
|
||||
" while we were spinning the event loop." +
|
||||
" Condition: " + name +
|
||||
" Phase: " + topic +
|
||||
" State: " + safeGetState(fetchState);
|
||||
warn(msg, error);
|
||||
|
||||
// The error should remain uncaught, to ensure that it
|
||||
// still causes tests to fail.
|
||||
Promise.reject(error);
|
||||
});
|
||||
condition.then(() => indirection.resolve());
|
||||
|
||||
indirection.promise.then(() => monitor.isComplete = true);
|
||||
this._monitors.push(monitor);
|
||||
allPromises.push(indirection.promise);
|
||||
|
||||
} catch (error) {
|
||||
let msg = "A completion condition encountered an error" +
|
||||
" while we were initializing the phase." +
|
||||
" Condition: " + name +
|
||||
" Phase: " + topic +
|
||||
" State: " + safeGetState(fetchState);
|
||||
warn(msg, error);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
conditions = null;
|
||||
|
||||
let promise = Promise.all(allPromises);
|
||||
allPromises = null;
|
||||
|
||||
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(() => {
|
||||
this._monitors = null;
|
||||
this._indirections = null;
|
||||
}); // Memory cleanup
|
||||
|
||||
|
||||
// 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(function() {
|
||||
let msg = "At least one completion condition is taking too long to complete." +
|
||||
" Conditions: " + JSON.stringify(this.state) +
|
||||
" Barrier: " + topic;
|
||||
warn(msg);
|
||||
}.bind(this), 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 nsIDebug.abort points to a guilty client, rather
|
||||
// than to AsyncShutdown itself. We search through all the
|
||||
// clients until we find one that is guilty and use its
|
||||
// filename/lineNumber, which have been determined during
|
||||
// the call to `addBlocker`.
|
||||
let filename = "?";
|
||||
let lineNumber = -1;
|
||||
for (let monitor of this._monitors) {
|
||||
if (monitor.isComplete) {
|
||||
continue;
|
||||
}
|
||||
filename = monitor.filename;
|
||||
lineNumber = monitor.lineNumber;
|
||||
}
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 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.
|
||||
|
||||
this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
|
||||
this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
|
||||
this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change2");
|
||||
this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
|
||||
this.AsyncShutdown.xpcomThreadsShutdown = getPhase("xpcom-threads-shutdown");
|
||||
|
||||
this.AsyncShutdown.Barrier = Barrier;
|
||||
|
||||
Object.freeze(this.AsyncShutdown);
|
||||
22
toolkit/components/asyncshutdown/moz.build
Normal file
22
toolkit/components/asyncshutdown/moz.build
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
|
||||
|
||||
XPIDL_MODULE = 'toolkit_asyncshutdown'
|
||||
|
||||
XPIDL_SOURCES += [
|
||||
'nsIAsyncShutdown.idl',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'AsyncShutdown.jsm',
|
||||
]
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'nsAsyncShutdown.js',
|
||||
'nsAsyncShutdown.manifest',
|
||||
]
|
||||
269
toolkit/components/asyncshutdown/nsAsyncShutdown.js
Normal file
269
toolkit/components/asyncshutdown/nsAsyncShutdown.js
Normal file
@@ -0,0 +1,269 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* An implementation of nsIAsyncShutdown* based on AsyncShutdown.jsm
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const Cr = Components.results;
|
||||
|
||||
let XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {}).XPCOMUtils;
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||
"resource://gre/modules/AsyncShutdown.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
|
||||
/**
|
||||
* Conversion between nsIPropertyBag and JS object
|
||||
*/
|
||||
let PropertyBagConverter = {
|
||||
// From nsIPropertyBag to JS
|
||||
toObject: function(bag) {
|
||||
if (!(bag instanceof Ci.nsIPropertyBag)) {
|
||||
throw new TypeError("Not a property bag");
|
||||
}
|
||||
let result = {};
|
||||
let enumerator = bag.enumerator;
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let {name, value: property} = enumerator.getNext().QueryInterface(Ci.nsIProperty);
|
||||
let value = this.toValue(property);
|
||||
result[name] = value;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
toValue: function(property) {
|
||||
if (typeof property != "object") {
|
||||
return property;
|
||||
}
|
||||
if (Array.isArray(property)) {
|
||||
return property.map(this.toValue, this);
|
||||
}
|
||||
if (property && property instanceof Ci.nsIPropertyBag) {
|
||||
return this.toObject(property);
|
||||
}
|
||||
return property;
|
||||
},
|
||||
|
||||
// From JS to nsIPropertyBag
|
||||
fromObject: function(obj) {
|
||||
if (obj == null || typeof obj != "object") {
|
||||
throw new TypeError("Invalid object: " + obj);
|
||||
}
|
||||
let bag = Cc["@mozilla.org/hash-property-bag;1"].
|
||||
createInstance(Ci.nsIWritablePropertyBag);
|
||||
for (let k of Object.keys(obj)) {
|
||||
let value = this.fromValue(obj[k]);
|
||||
bag.setProperty(k, value);
|
||||
}
|
||||
return bag;
|
||||
},
|
||||
fromValue: function(value) {
|
||||
if (typeof value == "function") {
|
||||
return null; // Emulating the behavior of JSON.stringify with functions
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(this.fromValue, this);
|
||||
}
|
||||
if (value == null || typeof value != "object") {
|
||||
// Auto-converted to nsIVariant
|
||||
return value;
|
||||
}
|
||||
return this.fromObject(value);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Construct an instance of nsIAsyncShutdownClient from a
|
||||
* AsyncShutdown.Barrier client.
|
||||
*
|
||||
* @param {object} moduleClient A client, as returned from the `client`
|
||||
* property of an instance of `AsyncShutdown.Barrier`. This client will
|
||||
* serve as back-end for methods `addBlocker` and `removeBlocker`.
|
||||
* @constructor
|
||||
*/
|
||||
function nsAsyncShutdownClient(moduleClient) {
|
||||
if (!moduleClient) {
|
||||
throw new TypeError("nsAsyncShutdownClient expects one argument");
|
||||
}
|
||||
this._moduleClient = moduleClient;
|
||||
this._byName = new Map();
|
||||
}
|
||||
nsAsyncShutdownClient.prototype = {
|
||||
_getPromisified: function(xpcomBlocker) {
|
||||
let candidate = this._byName.get(xpcomBlocker.name);
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
if (candidate.xpcom === xpcomBlocker) {
|
||||
return candidate.jsm;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
_setPromisified: function(xpcomBlocker, moduleBlocker) {
|
||||
let candidate = this._byName.get(xpcomBlocker.name);
|
||||
if (!candidate) {
|
||||
this._byName.set(xpcomBlocker.name, {xpcom: xpcomBlocker,
|
||||
jsm: moduleBlocker});
|
||||
return;
|
||||
}
|
||||
if (candidate.xpcom === xpcomBlocker) {
|
||||
return;
|
||||
}
|
||||
throw new Error("We have already registered a distinct blocker with the same name: " + xpcomBlocker.name);
|
||||
},
|
||||
_deletePromisified: function(xpcomBlocker) {
|
||||
let candidate = this._byName.get(xpcomBlocker.name);
|
||||
if (!candidate || candidate.xpcom !== xpcomBlocker) {
|
||||
return false;
|
||||
}
|
||||
this._byName.delete(xpcomBlocker.name);
|
||||
return true;
|
||||
},
|
||||
get jsclient() {
|
||||
return this._moduleClient;
|
||||
},
|
||||
get name() {
|
||||
return this._moduleClient.name;
|
||||
},
|
||||
addBlocker: function(/*nsIAsyncShutdownBlocker*/ xpcomBlocker,
|
||||
fileName, lineNumber, stack) {
|
||||
// We need a Promise-based function with the same behavior as
|
||||
// `xpcomBlocker`. Furthermore, to support `removeBlocker`, we
|
||||
// need to ensure that we always get the same Promise-based
|
||||
// function if we call several `addBlocker`/`removeBlocker` several
|
||||
// times with the same `xpcomBlocker`.
|
||||
//
|
||||
// Ideally, this should be done with a WeakMap() with xpcomBlocker
|
||||
// as a key, but XPConnect NativeWrapped objects cannot serve as
|
||||
// WeakMap keys.
|
||||
//
|
||||
let moduleBlocker = this._getPromisified(xpcomBlocker);
|
||||
if (!moduleBlocker) {
|
||||
moduleBlocker = () => new Promise(
|
||||
// This promise is never resolved. By opposition to AsyncShutdown
|
||||
// blockers, `nsIAsyncShutdownBlocker`s are always lifted by calling
|
||||
// `removeBlocker`.
|
||||
() => xpcomBlocker.blockShutdown(this)
|
||||
);
|
||||
|
||||
this._setPromisified(xpcomBlocker, moduleBlocker);
|
||||
}
|
||||
|
||||
this._moduleClient.addBlocker(xpcomBlocker.name,
|
||||
moduleBlocker,
|
||||
{
|
||||
fetchState: () => {
|
||||
let state = xpcomBlocker.state;
|
||||
if (state) {
|
||||
return PropertyBagConverter.toValue(state);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
filename: fileName,
|
||||
lineNumber: lineNumber,
|
||||
stack: stack,
|
||||
});
|
||||
},
|
||||
|
||||
removeBlocker: function(xpcomBlocker) {
|
||||
let moduleBlocker = this._getPromisified(xpcomBlocker);
|
||||
if (!moduleBlocker) {
|
||||
return false;
|
||||
}
|
||||
this._deletePromisified(xpcomBlocker);
|
||||
return this._moduleClient.removeBlocker(moduleBlocker);
|
||||
},
|
||||
|
||||
/* ........ QueryInterface .............. */
|
||||
QueryInterface : XPCOMUtils.generateQI([Ci.nsIAsyncShutdownBarrier]),
|
||||
classID: Components.ID("{314e9e96-cc37-4d5c-843b-54709ce11426}"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct an instance of nsIAsyncShutdownBarrier from an instance
|
||||
* of AsyncShutdown.Barrier.
|
||||
*
|
||||
* @param {object} moduleBarrier an instance if
|
||||
* `AsyncShutdown.Barrier`. This instance will serve as back-end for
|
||||
* all methods.
|
||||
* @constructor
|
||||
*/
|
||||
function nsAsyncShutdownBarrier(moduleBarrier) {
|
||||
this._client = new nsAsyncShutdownClient(moduleBarrier.client);
|
||||
this._moduleBarrier = moduleBarrier;
|
||||
};
|
||||
nsAsyncShutdownBarrier.prototype = {
|
||||
get state() {
|
||||
return PropertyBagConverter.fromValue(this._moduleBarrier.state);
|
||||
},
|
||||
get client() {
|
||||
return this._client;
|
||||
},
|
||||
wait: function(onReady) {
|
||||
this._moduleBarrier.wait().then(() => {
|
||||
onReady.done();
|
||||
});
|
||||
// By specification, _moduleBarrier.wait() cannot reject.
|
||||
},
|
||||
|
||||
/* ........ QueryInterface .............. */
|
||||
QueryInterface : XPCOMUtils.generateQI([Ci.nsIAsyncShutdownBarrier]),
|
||||
classID: Components.ID("{29a0e8b5-9111-4c09-a0eb-76cd02bf20fa}"),
|
||||
};
|
||||
|
||||
function nsAsyncShutdownService() {
|
||||
// Cache for the getters
|
||||
|
||||
for (let _k of
|
||||
["profileBeforeChange",
|
||||
"profileChangeTeardown",
|
||||
"sendTelemetry",
|
||||
"webWorkersShutdown",
|
||||
"xpcomThreadsShutdown"]) {
|
||||
let k = _k;
|
||||
Object.defineProperty(this, k, {
|
||||
configurable: true,
|
||||
get: function() {
|
||||
delete this[k];
|
||||
let result = new nsAsyncShutdownClient(AsyncShutdown[k]);
|
||||
Object.defineProperty(this, k, {
|
||||
value: result
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hooks for testing purpose
|
||||
this.wrappedJSObject = {
|
||||
_propertyBagConverter: PropertyBagConverter
|
||||
};
|
||||
}
|
||||
nsAsyncShutdownService.prototype = {
|
||||
makeBarrier: function(name) {
|
||||
return new nsAsyncShutdownBarrier(new AsyncShutdown.Barrier(name));
|
||||
},
|
||||
|
||||
/* ........ QueryInterface .............. */
|
||||
QueryInterface : XPCOMUtils.generateQI([Ci.nsIAsyncShutdownService]),
|
||||
classID: Components.ID("{35c496de-a115-475d-93b5-ffa3f3ae6fe3}"),
|
||||
};
|
||||
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
|
||||
nsAsyncShutdownService,
|
||||
nsAsyncShutdownBarrier,
|
||||
nsAsyncShutdownClient,
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
component {35c496de-a115-475d-93b5-ffa3f3ae6fe3} nsAsyncShutdown.js
|
||||
contract @mozilla.org/async-shutdown-service;1 {35c496de-a115-475d-93b5-ffa3f3ae6fe3}
|
||||
175
toolkit/components/asyncshutdown/tests/xpcshell/head.js
Normal file
175
toolkit/components/asyncshutdown/tests/xpcshell/head.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
let Cu = Components.utils;
|
||||
let Cc = Components.classes;
|
||||
let Ci = Components.interfaces;
|
||||
let Cr = Components.results;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
|
||||
|
||||
let asyncShutdownService = Cc["@mozilla.org/async-shutdown-service;1"].
|
||||
getService(Ci.nsIAsyncShutdownService);
|
||||
|
||||
|
||||
Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
|
||||
|
||||
/**
|
||||
* Utility function used to provide the same API for various sources
|
||||
* of async shutdown barriers.
|
||||
*
|
||||
* @param {string} kind One of
|
||||
* - "phase" to test an AsyncShutdown phase;
|
||||
* - "barrier" to test an instance of AsyncShutdown.Barrier;
|
||||
* - "xpcom-barrier" to test an instance of nsIAsyncShutdownBarrier;
|
||||
* - "xpcom-barrier-unwrapped" to test the field `jsclient` of a nsIAsyncShutdownClient.
|
||||
*
|
||||
* @return An object with the following methods:
|
||||
* - addBlocker() - the same method as AsyncShutdown phases and barrier clients
|
||||
* - wait() - trigger the resolution of the lock
|
||||
*/
|
||||
function makeLock(kind) {
|
||||
if (kind == "phase") {
|
||||
let topic = "test-Phase-" + ++makeLock.counter;
|
||||
let phase = AsyncShutdown._getPhase(topic);
|
||||
return {
|
||||
addBlocker: function(...args) {
|
||||
return phase.addBlocker(...args);
|
||||
},
|
||||
removeBlocker: function(blocker) {
|
||||
return phase.removeBlocker(blocker);
|
||||
},
|
||||
wait: function() {
|
||||
Services.obs.notifyObservers(null, topic, null);
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
} else if (kind == "barrier") {
|
||||
let name = "test-Barrier-" + ++makeLock.counter;
|
||||
let barrier = new AsyncShutdown.Barrier(name);
|
||||
return {
|
||||
addBlocker: barrier.client.addBlocker,
|
||||
removeBlocker: barrier.client.removeBlocker,
|
||||
wait: function() {
|
||||
return barrier.wait();
|
||||
}
|
||||
};
|
||||
} else if (kind == "xpcom-barrier") {
|
||||
let name = "test-xpcom-Barrier-" + ++makeLock.counter;
|
||||
let barrier = asyncShutdownService.makeBarrier(name);
|
||||
return {
|
||||
addBlocker: function(name, condition, state) {
|
||||
if (condition == null) {
|
||||
// Slight trick as `null` or `undefined` cannot be used as keys
|
||||
// for `xpcomMap`. Note that this has no incidence on the result
|
||||
// of the test as the XPCOM interface imposes that the condition
|
||||
// is a method, so it cannot be `null`/`undefined`.
|
||||
condition = "<this case can't happen with the xpcom interface>";
|
||||
}
|
||||
let blocker = makeLock.xpcomMap.get(condition);
|
||||
if (!blocker) {
|
||||
blocker = {
|
||||
name: name,
|
||||
state: state,
|
||||
blockShutdown: function(aBarrierClient) {
|
||||
return Task.spawn(function*() {
|
||||
try {
|
||||
if (typeof condition == "function") {
|
||||
yield Promise.resolve(condition());
|
||||
} else {
|
||||
yield Promise.resolve(condition);
|
||||
}
|
||||
} finally {
|
||||
aBarrierClient.removeBlocker(blocker);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
makeLock.xpcomMap.set(condition, blocker);
|
||||
}
|
||||
let {fileName, lineNumber, stack} = (new Error());
|
||||
return barrier.client.addBlocker(blocker, fileName, lineNumber, stack);
|
||||
},
|
||||
removeBlocker: function(condition) {
|
||||
let blocker = makeLock.xpcomMap.get(condition);
|
||||
if (!blocker) {
|
||||
return;
|
||||
}
|
||||
barrier.client.removeBlocker(blocker);
|
||||
},
|
||||
wait: function() {
|
||||
return new Promise(resolve => {
|
||||
barrier.wait(resolve);
|
||||
});
|
||||
}
|
||||
};
|
||||
} else if ("unwrapped-xpcom-barrier") {
|
||||
let name = "unwrapped-xpcom-barrier-" + ++makeLock.counter;
|
||||
let barrier = asyncShutdownService.makeBarrier(name);
|
||||
let client = barrier.client.jsclient;
|
||||
return {
|
||||
addBlocker: client.addBlocker,
|
||||
removeBlocker: client.removeBlocker,
|
||||
wait: function() {
|
||||
return new Promise(resolve => {
|
||||
barrier.wait(resolve);
|
||||
});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new TypeError("Unknown kind " + kind);
|
||||
}
|
||||
}
|
||||
makeLock.counter = 0;
|
||||
makeLock.xpcomMap = new Map(); // Note: Not a WeakMap as we wish to handle non-gc-able keys (e.g. strings)
|
||||
|
||||
/**
|
||||
* An asynchronous task that takes several ticks to complete.
|
||||
*
|
||||
* @param {*=} resolution The value with which the resulting promise will be
|
||||
* resolved once the task is complete. This may be a rejected promise,
|
||||
* in which case the resulting promise will itself be rejected.
|
||||
* @param {object=} outResult An object modified by side-effect during the task.
|
||||
* Initially, its field |isFinished| is set to |false|. Once the task is
|
||||
* complete, its field |isFinished| is set to |true|.
|
||||
*
|
||||
* @return {promise} A promise fulfilled once the task is complete
|
||||
*/
|
||||
function longRunningAsyncTask(resolution = undefined, outResult = {}) {
|
||||
outResult.isFinished = false;
|
||||
if (!("countFinished" in outResult)) {
|
||||
outResult.countFinished = 0;
|
||||
}
|
||||
let deferred = Promise.defer();
|
||||
do_timeout(100, function() {
|
||||
++outResult.countFinished;
|
||||
outResult.isFinished = true;
|
||||
deferred.resolve(resolution);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function get_exn(f) {
|
||||
try {
|
||||
f();
|
||||
return null;
|
||||
} catch (ex) {
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
|
||||
function do_check_exn(exn, constructor) {
|
||||
do_check_neq(exn, null);
|
||||
if (exn.name == constructor) {
|
||||
do_check_eq(exn.constructor.name, constructor);
|
||||
return;
|
||||
}
|
||||
do_print("Wrong error constructor");
|
||||
do_print(exn.constructor.name);
|
||||
do_print(exn.stack);
|
||||
do_check_true(false);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_no_condition() {
|
||||
for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
|
||||
do_print("Testing a barrier with no condition (" + kind + ")");
|
||||
let lock = makeLock(kind);
|
||||
yield lock.wait();
|
||||
do_print("Barrier with no condition didn't lock");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_phase_various_failures() {
|
||||
do_print("Ensure that we cannot add a condition for a phase once notification has been received");
|
||||
for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
|
||||
let lock = makeLock(kind);
|
||||
lock.wait(); // Don't actually wait for the promise to be resolved
|
||||
let exn = get_exn(() => lock.addBlocker("Test", true));
|
||||
do_check_true(!!exn);
|
||||
|
||||
if (kind == "xpcom-barrier") {
|
||||
do_print("Skipping this part of the test that is caught differently by XPConnect");
|
||||
continue;
|
||||
}
|
||||
do_print("Ensure that an incomplete blocker causes a TypeError");
|
||||
|
||||
lock = makeLock(kind);
|
||||
exn = get_exn(() => lock.addBlocker());
|
||||
do_check_exn(exn, "TypeError");
|
||||
|
||||
exn = get_exn(() => lock.addBlocker(null, true));
|
||||
do_check_exn(exn, "TypeError");
|
||||
|
||||
exn = get_exn(() => lock.addBlocker("Test 2", () => true, "not a function"));
|
||||
do_check_exn(exn, "TypeError");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
add_task(function* test_phase_removeBlocker() {
|
||||
do_print("Testing that we can call removeBlocker before, during and after the call to wait()");
|
||||
|
||||
for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
|
||||
|
||||
do_print("Switching to kind " + kind);
|
||||
do_print("Attempt to add then remove a blocker before wait()");
|
||||
let lock = makeLock(kind);
|
||||
let blocker = () => {
|
||||
do_print("This promise will never be resolved");
|
||||
return Promise.defer().promise;
|
||||
};
|
||||
|
||||
lock.addBlocker("Wait forever", blocker);
|
||||
let do_remove_blocker = function(lock, blocker, shouldRemove) {
|
||||
do_print("Attempting to remove blocker " + blocker + ", expecting result " + shouldRemove);
|
||||
if (kind == "xpcom-barrier") {
|
||||
// The xpcom variant always returns `undefined`, so we can't
|
||||
// check its result.
|
||||
lock.removeBlocker(blocker);
|
||||
return;
|
||||
}
|
||||
do_check_eq(lock.removeBlocker(blocker), shouldRemove);
|
||||
};
|
||||
do_remove_blocker(lock, blocker, true);
|
||||
do_remove_blocker(lock, blocker, false);
|
||||
do_print("Attempt to remove non-registered blockers before wait()");
|
||||
do_remove_blocker(lock, "foo", false);
|
||||
do_remove_blocker(lock, null, false);
|
||||
do_print("Waiting (should lift immediately)");
|
||||
yield lock.wait();
|
||||
|
||||
do_print("Attempt to add a blocker then remove it during wait()");
|
||||
lock = makeLock(kind);
|
||||
let blockers = [
|
||||
() => {
|
||||
do_print("This blocker will self-destruct");
|
||||
do_remove_blocker(lock, blockers[0], true);
|
||||
return Promise.defer().promise;
|
||||
},
|
||||
() => {
|
||||
do_print("This blocker will self-destruct twice");
|
||||
do_remove_blocker(lock, blockers[1], true);
|
||||
do_remove_blocker(lock, blockers[1], false);
|
||||
return Promise.defer().promise;
|
||||
},
|
||||
() => {
|
||||
do_print("Attempt to remove non-registered blockers during wait()");
|
||||
do_remove_blocker(lock, "foo", false);
|
||||
do_remove_blocker(lock, null, false);
|
||||
}
|
||||
];
|
||||
for (let i in blockers) {
|
||||
lock.addBlocker("Wait forever again: " + i, blockers[i]);
|
||||
}
|
||||
do_print("Waiting (should lift very quickly)");
|
||||
yield lock.wait();
|
||||
do_remove_blocker(lock, blockers[0], false);
|
||||
|
||||
|
||||
do_print("Attempt to remove a blocker after wait");
|
||||
lock = makeLock(kind);
|
||||
blocker = Promise.resolve;
|
||||
yield lock.wait();
|
||||
do_remove_blocker(lock, blocker, false);
|
||||
|
||||
do_print("Attempt to remove non-registered blocker after wait()");
|
||||
do_remove_blocker(lock, "foo", false);
|
||||
do_remove_blocker(lock, null, false);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
add_task(function* test_state() {
|
||||
do_print("Testing information contained in `state`");
|
||||
|
||||
let BLOCKER_NAME = "test_state blocker " + Math.random();
|
||||
|
||||
// Set up the barrier. Note that we cannot test `barrier.state`
|
||||
// immediately, as it initially contains "Not started"
|
||||
let barrier = new AsyncShutdown.Barrier("test_filename");
|
||||
let deferred = Promise.defer();
|
||||
let {filename, lineNumber} = Components.stack;
|
||||
barrier.client.addBlocker(BLOCKER_NAME,
|
||||
function() {
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
let promiseDone = barrier.wait();
|
||||
|
||||
// Now that we have called `wait()`, the state contains interesting things
|
||||
let state = barrier.state[0];
|
||||
do_print("State: " + JSON.stringify(barrier.state, null, "\t"));
|
||||
Assert.equal(state.filename, filename);
|
||||
Assert.equal(state.lineNumber, lineNumber + 1);
|
||||
Assert.equal(state.name, BLOCKER_NAME);
|
||||
Assert.ok(state.stack.some(x => x.contains("test_state")), "The stack contains the caller function's name");
|
||||
Assert.ok(state.stack.some(x => x.contains(filename)), "The stack contains the calling file's name");
|
||||
|
||||
deferred.resolve();
|
||||
yield promiseDone;
|
||||
});
|
||||
|
||||
add_task(function*() {
|
||||
Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
//
|
||||
// This file contains tests that need to leave uncaught asynchronous
|
||||
// errors. If your test catches all its asynchronous errors, please
|
||||
// put it in another file.
|
||||
//
|
||||
|
||||
Promise.Debugging.clearUncaughtErrorObservers();
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* test_phase_simple_async() {
|
||||
do_print("Testing various combinations of a phase with a single condition");
|
||||
for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
|
||||
for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
|
||||
for (let resolution of [arg, Promise.reject(arg)]) {
|
||||
for (let success of [false, true]) {
|
||||
for (let state of [[null],
|
||||
[],
|
||||
[() => "some state"],
|
||||
[function() {
|
||||
throw new Error("State BOOM"); }],
|
||||
[function() {
|
||||
return {
|
||||
toJSON: function() {
|
||||
throw new Error("State.toJSON BOOM");
|
||||
}
|
||||
};
|
||||
}]]) {
|
||||
// Asynchronous phase
|
||||
do_print("Asynchronous test with " + arg + ", " + resolution + ", " + kind);
|
||||
let lock = makeLock(kind);
|
||||
let outParam = { isFinished: false };
|
||||
lock.addBlocker(
|
||||
"Async test",
|
||||
function() {
|
||||
if (success) {
|
||||
return longRunningAsyncTask(resolution, outParam);
|
||||
} else {
|
||||
throw resolution;
|
||||
}
|
||||
},
|
||||
...state
|
||||
);
|
||||
do_check_false(outParam.isFinished);
|
||||
yield lock.wait();
|
||||
do_check_eq(outParam.isFinished, success);
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronous phase - just test that we don't throw/freeze
|
||||
do_print("Synchronous test with " + arg + ", " + resolution + ", " + kind);
|
||||
let lock = makeLock(kind);
|
||||
lock.addBlocker(
|
||||
"Sync test",
|
||||
resolution
|
||||
);
|
||||
yield lock.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* test_phase_many() {
|
||||
do_print("Testing various combinations of a phase with many conditions");
|
||||
for (let kind of ["phase", "barrier", "xpcom-barrier", "xpcom-barrier-unwrapped"]) {
|
||||
let lock = makeLock(kind);
|
||||
let outParams = [];
|
||||
for (let arg of [undefined, null, "foo", 100, new Error("BOOM")]) {
|
||||
for (let resolve of [true, false]) {
|
||||
do_print("Testing with " + kind + ", " + arg + ", " + resolve);
|
||||
let resolution = resolve ? arg : Promise.reject(arg);
|
||||
let outParam = { isFinished: false };
|
||||
lock.addBlocker(
|
||||
"Test " + Math.random(),
|
||||
() => longRunningAsyncTask(resolution, outParam)
|
||||
);
|
||||
}
|
||||
}
|
||||
do_check_true(outParams.every((x) => !x.isFinished));
|
||||
yield lock.wait();
|
||||
do_check_true(outParams.every((x) => x.isFinished));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
add_task(function*() {
|
||||
Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test conversion between nsIPropertyBag and JS values.
|
||||
*/
|
||||
|
||||
let PropertyBagConverter = asyncShutdownService.wrappedJSObject._propertyBagConverter;
|
||||
|
||||
function run_test() {
|
||||
test_conversions();
|
||||
}
|
||||
|
||||
function normalize(obj) {
|
||||
if (obj == null || typeof obj != "object") {
|
||||
return obj;
|
||||
}
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(normalize);
|
||||
}
|
||||
let result = {};
|
||||
for (let k of Object.keys(obj).sort()) {
|
||||
result[k] = normalize(obj[k]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function test_conversions() {
|
||||
const SAMPLES = [
|
||||
// Simple values
|
||||
1,
|
||||
true,
|
||||
"string",
|
||||
null,
|
||||
|
||||
// Objects
|
||||
{
|
||||
a: 1,
|
||||
b: true,
|
||||
c: "string",
|
||||
d:.5,
|
||||
e: [2, false, "another string", .3],
|
||||
f: [],
|
||||
g: {
|
||||
a2: 1,
|
||||
b2: true,
|
||||
c2: "string",
|
||||
d2:.5,
|
||||
e2: [2, false, "another string", .3],
|
||||
f2: [],
|
||||
g2: [{
|
||||
a3: 1,
|
||||
b3: true,
|
||||
c3: "string",
|
||||
d3:.5,
|
||||
e3: [2, false, "another string", .3],
|
||||
f3: [],
|
||||
g3: {}
|
||||
}]
|
||||
}
|
||||
}];
|
||||
|
||||
for (let sample of SAMPLES) {
|
||||
let stringified = JSON.stringify(normalize(sample), null, "\t");
|
||||
do_print("Testing conversions of " + stringified);
|
||||
let rewrites = [sample];
|
||||
for (let i = 1; i < 3; ++i) {
|
||||
let source = rewrites[i - 1];
|
||||
let bag = PropertyBagConverter.fromValue(source);
|
||||
do_print(" => " + bag);
|
||||
if (source == null) {
|
||||
Assert.ok(bag == null, "The bag is null");
|
||||
} else if (typeof source == "object") {
|
||||
Assert.ok(bag instanceof Ci.nsIPropertyBag, "The bag is a property bag");
|
||||
} else {
|
||||
Assert.ok(typeof bag != "object", "The bag is not an object");
|
||||
}
|
||||
let dest = PropertyBagConverter.toValue(bag);
|
||||
let restringified = JSON.stringify(normalize(dest), null, "\t");
|
||||
do_print("Comparing");
|
||||
do_print(stringified);
|
||||
do_print(restringified);
|
||||
Assert.deepEqual(sample, dest, "Testing after " + i + " conversions");
|
||||
rewrites.push(dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
head=head.js
|
||||
tail=
|
||||
|
||||
[test_AsyncShutdown.js]
|
||||
[test_AsyncShutdown_leave_uncaught.js]
|
||||
[test_converters.js]
|
||||
Reference in New Issue
Block a user