extensions: Refactor extension system for asynchronous loading

This commit refactors extensionSystem to load extensions
asynchronously. This will enable loading extensions using ECMAScript
modules in a future commit.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2364>
This commit is contained in:
Evan Welsh 2022-11-24 13:55:18 -06:00 committed by Marge Bot
parent 56beb6ff2b
commit d918290ac8

View File

@ -23,7 +23,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
constructor() { constructor() {
super(); super();
this._initialized = false; this._initializationPromise = null;
this._updateNotified = false; this._updateNotified = false;
this._extensions = new Map(); this._extensions = new Map();
@ -32,7 +32,9 @@ var ExtensionManager = class extends Signals.EventEmitter {
this._extensionOrder = []; this._extensionOrder = [];
this._checkVersion = false; this._checkVersion = false;
Main.sessionMode.connect('updated', this._sessionUpdated.bind(this)); Main.sessionMode.connect('updated', () => {
this._sessionUpdated();
});
} }
init() { init() {
@ -54,13 +56,15 @@ var ExtensionManager = class extends Signals.EventEmitter {
}); });
this._installExtensionUpdates(); this._installExtensionUpdates();
this._sessionUpdated(); this._sessionUpdated().then(() => {
ExtensionDownloader.checkForUpdates();
});
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => { GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => {
ExtensionDownloader.checkForUpdates(); ExtensionDownloader.checkForUpdates();
return GLib.SOURCE_CONTINUE; return GLib.SOURCE_CONTINUE;
}); });
ExtensionDownloader.checkForUpdates();
} }
get updatesSupported() { get updatesSupported() {
@ -92,12 +96,12 @@ var ExtensionManager = class extends Signals.EventEmitter {
return false; return false;
} }
_callExtensionDisable(uuid) { async _callExtensionDisable(uuid) {
let extension = this.lookup(uuid); let extension = this.lookup(uuid);
if (!extension) if (!extension)
return; return;
if (extension.state != ExtensionState.ENABLED) if (extension.state !== ExtensionState.ENABLED)
return; return;
extension.state = ExtensionState.DISABLING; extension.state = ExtensionState.DISABLING;
@ -139,7 +143,8 @@ var ExtensionManager = class extends Signals.EventEmitter {
for (let i = 0; i < order.length; i++) { for (let i = 0; i < order.length; i++) {
let otherUuid = order[i]; let otherUuid = order[i];
try { try {
this.lookup(otherUuid).stateObj.enable(); // eslint-disable-next-line no-await-in-loop
await this.lookup(otherUuid).stateObj.enable();
} catch (e) { } catch (e) {
this.logExtensionError(otherUuid, e); this.logExtensionError(otherUuid, e);
} }
@ -147,13 +152,13 @@ var ExtensionManager = class extends Signals.EventEmitter {
this._extensionOrder.splice(orderIdx, 1); this._extensionOrder.splice(orderIdx, 1);
if (extension.state != ExtensionState.ERROR) { if (extension.state !== ExtensionState.ERROR) {
extension.state = ExtensionState.DISABLED; extension.state = ExtensionState.DISABLED;
this.emit('extension-state-changed', extension); this.emit('extension-state-changed', extension);
} }
} }
_callExtensionEnable(uuid) { async _callExtensionEnable(uuid) {
if (!this._extensionSupportsSessionMode(uuid)) if (!this._extensionSupportsSessionMode(uuid))
return; return;
@ -161,10 +166,11 @@ var ExtensionManager = class extends Signals.EventEmitter {
if (!extension) if (!extension)
return; return;
if (extension.state == ExtensionState.INITIALIZED) if (extension.state === ExtensionState.INITIALIZED)
this._callExtensionInit(uuid); await this._callExtensionInit(uuid);
if (extension.state != ExtensionState.DISABLED)
if (extension.state !== ExtensionState.DISABLED)
return; return;
extension.state = ExtensionState.ENABLING; extension.state = ExtensionState.ENABLING;
@ -187,7 +193,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
} }
try { try {
extension.stateObj.enable(); await extension.stateObj.enable();
extension.state = ExtensionState.ENABLED; extension.state = ExtensionState.ENABLED;
this._extensionOrder.push(uuid); this._extensionOrder.push(uuid);
this.emit('extension-state-changed', extension); this.emit('extension-state-changed', extension);
@ -372,7 +378,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
return extension.metadata.version === version; return extension.metadata.version === version;
} }
loadExtension(extension) { async loadExtension(extension) {
// Default to error, we set success as the last step // Default to error, we set success as the last step
extension.state = ExtensionState.ERROR; extension.state = ExtensionState.ERROR;
@ -385,10 +391,11 @@ var ExtensionManager = class extends Signals.EventEmitter {
let enabled = this._enabledExtensions.includes(extension.uuid) && let enabled = this._enabledExtensions.includes(extension.uuid) &&
this._extensionSupportsSessionMode(extension.uuid); this._extensionSupportsSessionMode(extension.uuid);
if (enabled) { if (enabled) {
if (!this._callExtensionInit(extension.uuid)) if (!await this._callExtensionInit(extension.uuid))
return; return;
if (extension.state == ExtensionState.DISABLED)
this._callExtensionEnable(extension.uuid); if (extension.state === ExtensionState.DISABLED)
await this._callExtensionEnable(extension.uuid);
} else { } else {
extension.state = ExtensionState.INITIALIZED; extension.state = ExtensionState.INITIALIZED;
} }
@ -400,27 +407,26 @@ var ExtensionManager = class extends Signals.EventEmitter {
this.emit('extension-state-changed', extension); this.emit('extension-state-changed', extension);
} }
unloadExtension(extension) { async unloadExtension(extension) {
const { uuid, type } = extension; const { uuid, type } = extension;
// Try to disable it -- if it's ERROR'd, we can't guarantee that, // Try to disable it -- if it's ERROR'd, we can't guarantee that,
// but it will be removed on next reboot, and hopefully nothing // but it will be removed on next reboot, and hopefully nothing
// broke too much. // broke too much.
this._callExtensionDisable(uuid); await this._callExtensionDisable(uuid);
extension.state = ExtensionState.UNINSTALLED; extension.state = ExtensionState.UNINSTALLED;
this.emit('extension-state-changed', extension); this.emit('extension-state-changed', extension);
// If we did install an importer, it is now cached and it's // The extension is now cached and it's impossible to load a different version
// impossible to load a different version if (type === ExtensionType.PER_USER && extension.isImported)
if (type === ExtensionType.PER_USER && extension.imports)
this._unloadedExtensions.set(uuid, extension.metadata.version); this._unloadedExtensions.set(uuid, extension.metadata.version);
this._extensions.delete(uuid); this._extensions.delete(uuid);
return true; return true;
} }
reloadExtension(oldExtension) { async reloadExtension(oldExtension) {
// Grab the things we'll need to pass to createExtensionObject // Grab the things we'll need to pass to createExtensionObject
// to reload it. // to reload it.
let { uuid, dir, type } = oldExtension; let { uuid, dir, type } = oldExtension;
@ -437,10 +443,10 @@ var ExtensionManager = class extends Signals.EventEmitter {
return; return;
} }
this.loadExtension(newExtension); await this.loadExtension(newExtension);
} }
_callExtensionInit(uuid) { async _callExtensionInit(uuid) {
if (!this._extensionSupportsSessionMode(uuid)) if (!this._extensionSupportsSessionMode(uuid))
return false; return false;
@ -458,7 +464,16 @@ var ExtensionManager = class extends Signals.EventEmitter {
let extensionModule; let extensionModule;
let extensionState = null; let extensionState = null;
// TODO: This function does not have an async operation but when ESM is
// merged there will be a dynamic import here instead of `installImporter`.
await 0;
ExtensionUtils.installImporter(extension); ExtensionUtils.installImporter(extension);
// Extensions can only be imported once, so add a property to avoid
// attempting to re-import an extension.
extension.isImported = true;
try { try {
extensionModule = extension.imports.extension; extensionModule = extension.imports.extension;
} catch (e) { } catch (e) {
@ -468,7 +483,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
if (extensionModule.init) { if (extensionModule.init) {
try { try {
extensionState = extensionModule.init(extension); extensionState = await extensionModule.init(extension);
} catch (e) { } catch (e) {
this.logExtensionError(uuid, e); this.logExtensionError(uuid, e);
return false; return false;
@ -492,8 +507,8 @@ var ExtensionManager = class extends Signals.EventEmitter {
_updateCanChange(extension) { _updateCanChange(extension) {
let hasError = let hasError =
extension.state == ExtensionState.ERROR || extension.state === ExtensionState.ERROR ||
extension.state == ExtensionState.OUT_OF_DATE; extension.state === ExtensionState.OUT_OF_DATE;
let isMode = this._getModeExtensions().includes(extension.uuid); let isMode = this._getModeExtensions().includes(extension.uuid);
let modeOnly = global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY); let modeOnly = global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY);
@ -519,27 +534,36 @@ var ExtensionManager = class extends Signals.EventEmitter {
return extensions.filter(item => !disabledExtensions.includes(item)); return extensions.filter(item => !disabledExtensions.includes(item));
} }
_onUserExtensionsEnabledChanged() { async _onUserExtensionsEnabledChanged() {
this._onEnabledExtensionsChanged(); await this._onEnabledExtensionsChanged();
this._onSettingsWritableChanged(); this._onSettingsWritableChanged();
} }
_onEnabledExtensionsChanged() { async _onEnabledExtensionsChanged() {
let newEnabledExtensions = this._getEnabledExtensions(); let newEnabledExtensions = this._getEnabledExtensions();
// Find and enable all the newly enabled extensions: UUIDs found in the // Find and enable all the newly enabled extensions: UUIDs found in the
// new setting, but not in the old one. // new setting, but not in the old one.
newEnabledExtensions const extensionsToEnable = newEnabledExtensions
.filter(uuid => !this._enabledExtensions.includes(uuid) && .filter(uuid => !this._enabledExtensions.includes(uuid) &&
this._extensionSupportsSessionMode(uuid)) this._extensionSupportsSessionMode(uuid));
.forEach(uuid => this._callExtensionEnable(uuid)); for (const uuid of extensionsToEnable) {
// eslint-disable-next-line no-await-in-loop
await this._callExtensionEnable(uuid);
}
// Find and disable all the newly disabled extensions: UUIDs found in the // Find and disable all the newly disabled extensions: UUIDs found in the
// old setting, but not in the new one. // old setting, but not in the new one.
this._extensionOrder const extensionsToDisable = this._extensionOrder
.filter(uuid => !newEnabledExtensions.includes(uuid) || .filter(uuid => !newEnabledExtensions.includes(uuid) ||
!this._extensionSupportsSessionMode(uuid)) !this._extensionSupportsSessionMode(uuid));
.reverse().forEach(uuid => this._callExtensionDisable(uuid)); // Reverse mutates the original array, but .filter() creates a new array.
extensionsToDisable.reverse();
for (const uuid of extensionsToDisable) {
// eslint-disable-next-line no-await-in-loop
await this._callExtensionDisable(uuid);
}
this._enabledExtensions = newEnabledExtensions; this._enabledExtensions = newEnabledExtensions;
} }
@ -551,7 +575,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
} }
} }
_onVersionValidationChanged() { async _onVersionValidationChanged() {
const checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY); const checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY);
if (checkVersion === this._checkVersion) if (checkVersion === this._checkVersion)
return; return;
@ -561,16 +585,18 @@ var ExtensionManager = class extends Signals.EventEmitter {
// Disabling extensions modifies the order array, so use a copy // Disabling extensions modifies the order array, so use a copy
let extensionOrder = this._extensionOrder.slice(); let extensionOrder = this._extensionOrder.slice();
// Disable enabled extensions in the reverse order first to avoid // Disable enabled extensions first to avoid
// the "rebasing" done in _callExtensionDisable... // the "rebasing" done in _callExtensionDisable...
extensionOrder.slice().reverse().forEach(uuid => { this._disableAllExtensions();
this._callExtensionDisable(uuid);
});
// ...and then reload and enable extensions in the correct order again. // ...and then reload and enable extensions in the correct order again.
[...this._extensions.values()].sort((a, b) => { const extensionsToReload = [...this._extensions.values()].sort((a, b) => {
return extensionOrder.indexOf(a.uuid) - extensionOrder.indexOf(b.uuid); return extensionOrder.indexOf(a.uuid) - extensionOrder.indexOf(b.uuid);
}).forEach(extension => this.reloadExtension(extension)); });
for (const extension of extensionsToReload) {
// eslint-disable-next-line no-await-in-loop
await this.reloadExtension(extension);
}
} }
_installExtensionUpdates() { _installExtensionUpdates() {
@ -596,21 +622,25 @@ var ExtensionManager = class extends Signals.EventEmitter {
} }
} }
_loadExtensions() { async _loadExtensions() {
global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, () => {
this._onEnabledExtensionsChanged.bind(this)); this._onEnabledExtensionsChanged();
global.settings.connect(`changed::${DISABLED_EXTENSIONS_KEY}`, });
this._onEnabledExtensionsChanged.bind(this)); global.settings.connect(`changed::${DISABLED_EXTENSIONS_KEY}`, () => {
global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, this._onEnabledExtensionsChanged();
this._onUserExtensionsEnabledChanged.bind(this)); });
global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, () => {
this._onVersionValidationChanged.bind(this)); this._onUserExtensionsEnabledChanged();
global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`, });
this._onSettingsWritableChanged.bind(this)); global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, () => {
global.settings.connect(`writable-changed::${DISABLED_EXTENSIONS_KEY}`, this._onVersionValidationChanged();
this._onSettingsWritableChanged.bind(this)); });
global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`, () =>
this._onSettingsWritableChanged());
global.settings.connect(`writable-changed::${DISABLED_EXTENSIONS_KEY}`, () =>
this._onSettingsWritableChanged());
this._onVersionValidationChanged(); await this._onVersionValidationChanged();
this._enabledExtensions = this._getEnabledExtensions(); this._enabledExtensions = this._getEnabledExtensions();
@ -642,33 +672,50 @@ var ExtensionManager = class extends Signals.EventEmitter {
return extension; return extension;
}).filter(extension => extension !== null); }).filter(extension => extension !== null);
for (const extension of extensionObjects) for (const extension of extensionObjects) {
this.loadExtension(extension); // eslint-disable-next-line no-await-in-loop
} await this.loadExtension(extension);
_enableAllExtensions() {
if (!this._initialized) {
this._loadExtensions();
this._initialized = true;
} else {
this._enabledExtensions.forEach(uuid => {
this._callExtensionEnable(uuid);
});
} }
} }
_disableAllExtensions() { async _enableAllExtensions() {
if (this._initialized) { if (!this._initializationPromise)
this._extensionOrder.slice().reverse().forEach(uuid => { this._initializationPromise = this._loadExtensions();
this._callExtensionDisable(uuid);
}); await this._initializationPromise;
for (const uuid of this._enabledExtensions) {
// eslint-disable-next-line no-await-in-loop
await this._callExtensionEnable(uuid);
} }
} }
_sessionUpdated() {
/**
* Disables all currently enabled extensions.
*/
async _disableAllExtensions() {
// Wait for extensions to finish loading before starting
// to disable, otherwise some extensions may enable after
// this function.
if (this._initializationPromise)
await this._initializationPromise;
const extensionsToDisable = this._extensionOrder.slice();
// Extensions are disabled in the reverse order
// from when they were enabled.
extensionsToDisable.reverse();
for (const uuid of extensionsToDisable) {
// eslint-disable-next-line no-await-in-loop
await this._callExtensionDisable(uuid);
}
}
async _sessionUpdated() {
// Take care of added or removed sessionMode extensions // Take care of added or removed sessionMode extensions
this._onEnabledExtensionsChanged(); await this._onEnabledExtensionsChanged();
this._enableAllExtensions(); await this._enableAllExtensions();
} }
}; };