diff options
Diffstat (limited to 'toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs')
-rw-r--r-- | toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs | 285 |
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"]), +}; |