fune/toolkit/components/asyncshutdown/nsAsyncShutdown.sys.mjs
Marco Bonardo f74b84ea35 Bug 1805529 - The Sqlite.sys.mjs module may open connections too late on shutdown. r=Gijs
Asynchronously opening a connection through the Sqlite module may intersect a
shutdown phase so that the request for the connection happens when the phase is
still open, but the callback is invoked after the phase has been closed.
In this case adding a shutdown blocker to close the connection will fail and
async shutdown is likely to crash after waiting for the Sqlite module.
In these edge cases it's safer to just close the connection and reject to the
consumer.

Differential Revision: https://phabricator.services.mozilla.com/D164597
2023-01-05 13:33:23 +00:00

285 lines
7.7 KiB
JavaScript

/* 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"]),
};