summaryrefslogtreecommitdiffstats
path: root/toolkit/components/asyncshutdown/nsAsyncShutdown.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/asyncshutdown/nsAsyncShutdown.jsm')
-rw-r--r--toolkit/components/asyncshutdown/nsAsyncShutdown.jsm263
1 files changed, 263 insertions, 0 deletions
diff --git a/toolkit/components/asyncshutdown/nsAsyncShutdown.jsm b/toolkit/components/asyncshutdown/nsAsyncShutdown.jsm
new file mode 100644
index 0000000000..0483824ccb
--- /dev/null
+++ b/toolkit/components/asyncshutdown/nsAsyncShutdown.jsm
@@ -0,0 +1,263 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm"
+);
+
+/**
+ * Conversion between nsIPropertyBag and JS object
+ */
+var PropertyBagConverter = {
+ // From nsIPropertyBag to JS
+ toObject(bag) {
+ if (!(bag instanceof Ci.nsIPropertyBag)) {
+ throw new TypeError("Not a property bag");
+ }
+ let result = {};
+ for (let { name, value: property } of bag.enumerator) {
+ let value = this.toValue(property);
+ result[name] = value;
+ }
+ return result;
+ },
+ toValue(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(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(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(xpcomBlocker) {
+ let candidate = this._byName.get(xpcomBlocker.name);
+ if (!candidate) {
+ return null;
+ }
+ if (candidate.xpcom === xpcomBlocker) {
+ return candidate.jsm;
+ }
+ return null;
+ },
+ _setPromisified(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(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(
+ /* 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,
+ stack,
+ });
+ },
+
+ removeBlocker(xpcomBlocker) {
+ let moduleBlocker = this._getPromisified(xpcomBlocker);
+ if (!moduleBlocker) {
+ return false;
+ }
+ this._deletePromisified(xpcomBlocker);
+ return this._moduleClient.removeBlocker(moduleBlocker);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAsyncShutdownBarrier"]),
+};
+
+/**
+ * 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(onReady) {
+ this._moduleBarrier.wait().then(() => {
+ onReady.done();
+ });
+ // By specification, _moduleBarrier.wait() cannot reject.
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAsyncShutdownBarrier"]),
+};
+
+function nsAsyncShutdownService() {
+ // Cache for the getters
+
+ for (let _k of [
+ // Parent process
+ "profileBeforeChange",
+ "profileChangeTeardown",
+ "quitApplicationGranted",
+ "sendTelemetry",
+
+ // Child processes
+ "contentChildShutdown",
+
+ // All processes
+ "webWorkersShutdown",
+ "xpcomWillShutdown",
+ ]) {
+ let k = _k;
+ Object.defineProperty(this, k, {
+ configurable: true,
+ get() {
+ delete this[k];
+ let wrapped = AsyncShutdown[k]; // May be undefined, if we're on the wrong process.
+ let result = wrapped ? new nsAsyncShutdownClient(wrapped) : undefined;
+ Object.defineProperty(this, k, {
+ value: result,
+ });
+ return result;
+ },
+ });
+ }
+
+ // Hooks for testing purpose
+ this.wrappedJSObject = {
+ _propertyBagConverter: PropertyBagConverter,
+ };
+}
+nsAsyncShutdownService.prototype = {
+ makeBarrier(name) {
+ return new nsAsyncShutdownBarrier(new AsyncShutdown.Barrier(name));
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAsyncShutdownService"]),
+};
+
+var EXPORTED_SYMBOLS = ["nsAsyncShutdownService"];