summaryrefslogtreecommitdiffstats
path: root/toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs')
-rw-r--r--toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs285
1 files changed, 285 insertions, 0 deletions
diff --git a/toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs b/toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs
new file mode 100644
index 0000000000..20b7f53ad4
--- /dev/null
+++ b/toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs
@@ -0,0 +1,285 @@
+/* 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.sys.mjs
+ */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+});
+
+/**
+ * Conversion between nsIPropertyBags and JS values.
+ * This uses a conservative approach to avoid losing data and doesn't throw.
+ * Don't use this if you need perfect serialization and deserialization.
+ */
+class PropertyBagConverter {
+ /**
+ * When the js value to convert is a primitive, it is stored in the property
+ * bag under a key with this name.
+ */
+ get primitiveProperty() {
+ return "PropertyBagConverter_primitive";
+ }
+
+ /**
+ * Converts from a PropertyBag to a JS value.
+ * @param {nsIPropertyBag} bag The PropertyBag to convert.
+ * @returns {jsval} A JS value.
+ */
+ propertyBagToJsValue(bag) {
+ if (!(bag instanceof Ci.nsIPropertyBag)) {
+ return null;
+ }
+ let result = {};
+ for (let { name, value: property } of bag.enumerator) {
+ let value = this.#toValue(property);
+ if (name == this.primitiveProperty) {
+ return value;
+ }
+ result[name] = value;
+ }
+ return result;
+ }
+
+ #toValue(property) {
+ if (property instanceof Ci.nsIPropertyBag) {
+ return this.propertyBagToJsValue(property);
+ }
+ if (["number", "boolean"].includes(typeof property)) {
+ return property;
+ }
+ try {
+ return JSON.parse(property);
+ } catch (ex) {
+ // Not JSON.
+ }
+ return property;
+ }
+
+ /**
+ * Converts from a JS value to a PropertyBag.
+ * @param {jsval} val JS value to convert.
+ * @returns {nsIPropertyBag} A PropertyBag.
+ * @note function is converted to "(function)" and undefined to null.
+ */
+ jsValueToPropertyBag(val) {
+ let bag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag
+ );
+ if (val && typeof val == "object") {
+ for (let k of Object.keys(val)) {
+ bag.setProperty(k, this.#fromValue(val[k]));
+ }
+ } else {
+ bag.setProperty(this.primitiveProperty, this.#fromValue(val));
+ }
+ return bag;
+ }
+
+ #fromValue(value) {
+ if (typeof value == "function") {
+ return "(function)";
+ }
+ if (value === undefined) {
+ value = null;
+ }
+ if (["number", "boolean", "string"].includes(typeof value)) {
+ return value;
+ }
+ return JSON.stringify(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;
+ },
+ get isClosed() {
+ return this._moduleClient.isClosed;
+ },
+ 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: () =>
+ new PropertyBagConverter().propertyBagToJsValue(xpcomBlocker.state),
+ 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(["nsIAsyncShutdownClient"]),
+};
+
+/**
+ * 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 new PropertyBagConverter().jsValueToPropertyBag(
+ 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"]),
+};
+
+export 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 = lazy.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 lazy.AsyncShutdown.Barrier(name));
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAsyncShutdownService"]),
+};