1c98a95974
Extensions often need to set up additional resources from their directory, like settings, translations or image assets. So far extensions have used getCurrentExtension() to access the shell's internal extension object which contains path and dir properties. That's far from ideal, first because it requires generating a stack to figure out the current extension, and second because the internal object also contains state that extensions shouldn't meddle with. Just include those properties in the metadata we pass to the extension constructor. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2838>
798 lines
27 KiB
JavaScript
798 lines
27 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported init connect disconnect ExtensionManager */
|
|
|
|
const GLib = imports.gi.GLib;
|
|
const Gio = imports.gi.Gio;
|
|
const GObject = imports.gi.GObject;
|
|
const Shell = imports.gi.Shell;
|
|
const St = imports.gi.St;
|
|
const Signals = imports.misc.signals;
|
|
|
|
const ExtensionDownloader = imports.ui.extensionDownloader;
|
|
const ExtensionUtils = imports.misc.extensionUtils;
|
|
const FileUtils = imports.misc.fileUtils;
|
|
const Main = imports.ui.main;
|
|
const MessageTray = imports.ui.messageTray;
|
|
|
|
const Config = imports.misc.config;
|
|
|
|
const { ExtensionState, ExtensionType } = ExtensionUtils;
|
|
|
|
const ENABLED_EXTENSIONS_KEY = 'enabled-extensions';
|
|
const DISABLED_EXTENSIONS_KEY = 'disabled-extensions';
|
|
const DISABLE_USER_EXTENSIONS_KEY = 'disable-user-extensions';
|
|
const EXTENSION_DISABLE_VERSION_CHECK_KEY = 'disable-extension-version-validation';
|
|
|
|
const UPDATE_CHECK_TIMEOUT = 24 * 60 * 60; // 1 day in seconds
|
|
|
|
var ExtensionManager = class extends Signals.EventEmitter {
|
|
constructor() {
|
|
super();
|
|
|
|
this._initializationPromise = null;
|
|
this._updateNotified = false;
|
|
|
|
this._extensions = new Map();
|
|
this._unloadedExtensions = new Map();
|
|
this._enabledExtensions = [];
|
|
this._extensionOrder = [];
|
|
this._checkVersion = false;
|
|
|
|
St.Settings.get().connect('notify::color-scheme',
|
|
() => this._reloadExtensionStylesheets());
|
|
|
|
Main.sessionMode.connect('updated', () => {
|
|
this._sessionUpdated();
|
|
});
|
|
}
|
|
|
|
init() {
|
|
// The following file should exist for a period of time when extensions
|
|
// are enabled after start. If it exists, then the systemd unit will
|
|
// disable extensions should gnome-shell crash.
|
|
// Should the file already exist from a previous login, then this is OK.
|
|
let disableFilename = GLib.build_filenamev([GLib.get_user_runtime_dir(), 'gnome-shell-disable-extensions']);
|
|
let disableFile = Gio.File.new_for_path(disableFilename);
|
|
try {
|
|
disableFile.create(Gio.FileCreateFlags.REPLACE_DESTINATION, null);
|
|
} catch (e) {
|
|
log(`Failed to create file ${disableFilename}: ${e.message}`);
|
|
}
|
|
|
|
const shutdownId = global.connect('shutdown',
|
|
() => disableFile.delete(null));
|
|
|
|
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 60, () => {
|
|
global.disconnect(shutdownId);
|
|
|
|
disableFile.delete(null);
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
|
|
this._installExtensionUpdates();
|
|
this._sessionUpdated().then(() => {
|
|
ExtensionDownloader.checkForUpdates();
|
|
});
|
|
|
|
GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => {
|
|
ExtensionDownloader.checkForUpdates();
|
|
|
|
return GLib.SOURCE_CONTINUE;
|
|
});
|
|
}
|
|
|
|
get updatesSupported() {
|
|
const appSys = Shell.AppSystem.get_default();
|
|
return (appSys.lookup_app('org.gnome.Extensions.desktop') !== null) ||
|
|
(appSys.lookup_app('com.mattjakeman.ExtensionManager.desktop') !== null);
|
|
}
|
|
|
|
lookup(uuid) {
|
|
return this._extensions.get(uuid);
|
|
}
|
|
|
|
getUuids() {
|
|
return [...this._extensions.keys()];
|
|
}
|
|
|
|
_reloadExtensionStylesheets() {
|
|
for (const ext of this._extensions.values()) {
|
|
// No stylesheet, nothing to reload
|
|
if (!ext.stylesheet)
|
|
continue;
|
|
|
|
// No variants, so skip reloading
|
|
const path = ext.stylesheet.get_path();
|
|
if (!path.endsWith('-dark.css') && !path.endsWith('-light.css'))
|
|
continue;
|
|
|
|
try {
|
|
this._unloadExtensionStylesheet(ext);
|
|
this._loadExtensionStylesheet(ext);
|
|
} catch (e) {
|
|
this._callExtensionDisable(ext.uuid);
|
|
this.logExtensionError(ext.uuid, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
_loadExtensionStylesheet(extension) {
|
|
if (extension.state !== ExtensionState.ENABLED &&
|
|
extension.state !== ExtensionState.ENABLING)
|
|
return;
|
|
|
|
const variant = Main.getStyleVariant();
|
|
const stylesheetNames = [
|
|
`${global.sessionMode}-${variant}.css`,
|
|
`stylesheet-${variant}.css`,
|
|
`${global.sessionMode}.css`,
|
|
'stylesheet.css',
|
|
];
|
|
const theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
|
|
for (const name of stylesheetNames) {
|
|
try {
|
|
const stylesheetFile = extension.dir.get_child(name);
|
|
theme.load_stylesheet(stylesheetFile);
|
|
extension.stylesheet = stylesheetFile;
|
|
break;
|
|
} catch (e) {
|
|
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
|
|
continue; // not an error
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
_unloadExtensionStylesheet(extension) {
|
|
if (!extension.stylesheet)
|
|
return;
|
|
|
|
const theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
|
|
theme.unload_stylesheet(extension.stylesheet);
|
|
delete extension.stylesheet;
|
|
}
|
|
|
|
_extensionSupportsSessionMode(uuid) {
|
|
const extension = this.lookup(uuid);
|
|
|
|
if (!extension)
|
|
return false;
|
|
|
|
if (extension.sessionModes.includes(Main.sessionMode.currentMode))
|
|
return true;
|
|
|
|
if (extension.sessionModes.includes(Main.sessionMode.parentMode))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
async _callExtensionDisable(uuid) {
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
return;
|
|
|
|
if (extension.state !== ExtensionState.ENABLED)
|
|
return;
|
|
|
|
extension.state = ExtensionState.DISABLING;
|
|
this.emit('extension-state-changed', extension);
|
|
|
|
// "Rebase" the extension order by disabling and then enabling extensions
|
|
// in order to help prevent conflicts.
|
|
|
|
// Example:
|
|
// order = [A, B, C, D, E]
|
|
// user disables C
|
|
// this should: disable E, disable D, disable C, enable D, enable E
|
|
|
|
let orderIdx = this._extensionOrder.indexOf(uuid);
|
|
let order = this._extensionOrder.slice(orderIdx + 1);
|
|
let orderReversed = order.slice().reverse();
|
|
|
|
for (let i = 0; i < orderReversed.length; i++) {
|
|
let otherUuid = orderReversed[i];
|
|
try {
|
|
this.lookup(otherUuid).stateObj.disable();
|
|
} catch (e) {
|
|
this.logExtensionError(otherUuid, e);
|
|
}
|
|
}
|
|
|
|
try {
|
|
extension.stateObj.disable();
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
}
|
|
|
|
this._unloadExtensionStylesheet(extension);
|
|
|
|
for (let i = 0; i < order.length; i++) {
|
|
let otherUuid = order[i];
|
|
try {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.lookup(otherUuid).stateObj.enable();
|
|
} catch (e) {
|
|
this.logExtensionError(otherUuid, e);
|
|
}
|
|
}
|
|
|
|
this._extensionOrder.splice(orderIdx, 1);
|
|
|
|
if (extension.state !== ExtensionState.ERROR) {
|
|
extension.state = ExtensionState.DISABLED;
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
}
|
|
|
|
async _callExtensionEnable(uuid) {
|
|
if (!this._extensionSupportsSessionMode(uuid))
|
|
return;
|
|
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
return;
|
|
|
|
if (extension.state === ExtensionState.INITIALIZED)
|
|
await this._callExtensionInit(uuid);
|
|
|
|
|
|
if (extension.state !== ExtensionState.DISABLED)
|
|
return;
|
|
|
|
extension.state = ExtensionState.ENABLING;
|
|
this.emit('extension-state-changed', extension);
|
|
|
|
try {
|
|
this._loadExtensionStylesheet(extension);
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await extension.stateObj.enable();
|
|
extension.state = ExtensionState.ENABLED;
|
|
this._extensionOrder.push(uuid);
|
|
this.emit('extension-state-changed', extension);
|
|
} catch (e) {
|
|
this._unloadExtensionStylesheet(extension);
|
|
this.logExtensionError(uuid, e);
|
|
}
|
|
}
|
|
|
|
enableExtension(uuid) {
|
|
if (!this._extensions.has(uuid))
|
|
return false;
|
|
|
|
let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
|
|
let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);
|
|
|
|
if (disabledExtensions.includes(uuid)) {
|
|
disabledExtensions = disabledExtensions.filter(item => item !== uuid);
|
|
global.settings.set_strv(DISABLED_EXTENSIONS_KEY, disabledExtensions);
|
|
}
|
|
|
|
if (!enabledExtensions.includes(uuid)) {
|
|
enabledExtensions.push(uuid);
|
|
global.settings.set_strv(ENABLED_EXTENSIONS_KEY, enabledExtensions);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
disableExtension(uuid) {
|
|
if (!this._extensions.has(uuid))
|
|
return false;
|
|
|
|
let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
|
|
let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);
|
|
|
|
if (enabledExtensions.includes(uuid)) {
|
|
enabledExtensions = enabledExtensions.filter(item => item !== uuid);
|
|
global.settings.set_strv(ENABLED_EXTENSIONS_KEY, enabledExtensions);
|
|
}
|
|
|
|
if (!disabledExtensions.includes(uuid)) {
|
|
disabledExtensions.push(uuid);
|
|
global.settings.set_strv(DISABLED_EXTENSIONS_KEY, disabledExtensions);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
openExtensionPrefs(uuid, parentWindow, options) {
|
|
const extension = this.lookup(uuid);
|
|
if (!extension || !extension.hasPrefs)
|
|
return false;
|
|
|
|
Gio.DBus.session.call(
|
|
'org.gnome.Shell.Extensions',
|
|
'/org/gnome/Shell/Extensions',
|
|
'org.gnome.Shell.Extensions',
|
|
'OpenExtensionPrefs',
|
|
new GLib.Variant('(ssa{sv})', [uuid, parentWindow, options]),
|
|
null,
|
|
Gio.DBusCallFlags.NONE,
|
|
-1,
|
|
null);
|
|
return true;
|
|
}
|
|
|
|
notifyExtensionUpdate(uuid) {
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
return;
|
|
|
|
extension.hasUpdate = true;
|
|
this.emit('extension-state-changed', extension);
|
|
|
|
if (!this._updateNotified) {
|
|
this._updateNotified = true;
|
|
|
|
let source = new ExtensionUpdateSource();
|
|
Main.messageTray.add(source);
|
|
|
|
let notification = new MessageTray.Notification(source,
|
|
_('Extension Updates Available'),
|
|
_('Extension updates are ready to be installed.'));
|
|
notification.connect('activated',
|
|
() => source.open());
|
|
source.showNotification(notification);
|
|
}
|
|
}
|
|
|
|
logExtensionError(uuid, error) {
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
return;
|
|
|
|
const message = error instanceof Error
|
|
? error.message : error.toString();
|
|
|
|
extension.error = message;
|
|
extension.state = ExtensionState.ERROR;
|
|
if (!extension.errors)
|
|
extension.errors = [];
|
|
extension.errors.push(message);
|
|
|
|
logError(error, `Extension ${uuid}`);
|
|
this._updateCanChange(extension);
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
|
|
createExtensionObject(uuid, dir, type) {
|
|
let metadataFile = dir.get_child('metadata.json');
|
|
if (!metadataFile.query_exists(null))
|
|
throw new Error('Missing metadata.json');
|
|
|
|
let metadataContents, success_;
|
|
try {
|
|
[success_, metadataContents] = metadataFile.load_contents(null);
|
|
metadataContents = new TextDecoder().decode(metadataContents);
|
|
} catch (e) {
|
|
throw new Error(`Failed to load metadata.json: ${e}`);
|
|
}
|
|
let meta;
|
|
try {
|
|
meta = JSON.parse(metadataContents);
|
|
} catch (e) {
|
|
throw new Error(`Failed to parse metadata.json: ${e}`);
|
|
}
|
|
|
|
const requiredProperties = [{
|
|
prop: 'uuid',
|
|
typeName: 'string',
|
|
}, {
|
|
prop: 'name',
|
|
typeName: 'string',
|
|
}, {
|
|
prop: 'description',
|
|
typeName: 'string',
|
|
}, {
|
|
prop: 'shell-version',
|
|
typeName: 'string array',
|
|
typeCheck: v => Array.isArray(v) && v.length > 0 && v.every(e => typeof e === 'string'),
|
|
}];
|
|
for (let i = 0; i < requiredProperties.length; i++) {
|
|
const {
|
|
prop, typeName, typeCheck = v => typeof v === typeName,
|
|
} = requiredProperties[i];
|
|
|
|
if (!meta[prop])
|
|
throw new Error(`missing "${prop}" property in metadata.json`);
|
|
if (!typeCheck(meta[prop]))
|
|
throw new Error(`property "${prop}" is not of type ${typeName}`);
|
|
}
|
|
|
|
if (uuid != meta.uuid)
|
|
throw new Error(`uuid "${meta.uuid}" from metadata.json does not match directory name "${uuid}"`);
|
|
|
|
let extension = {
|
|
metadata: meta,
|
|
uuid: meta.uuid,
|
|
type,
|
|
dir,
|
|
path: dir.get_path(),
|
|
error: '',
|
|
hasPrefs: dir.get_child('prefs.js').query_exists(null),
|
|
hasUpdate: false,
|
|
canChange: false,
|
|
sessionModes: meta['session-modes'] ? meta['session-modes'] : ['user'],
|
|
};
|
|
this._extensions.set(uuid, extension);
|
|
|
|
return extension;
|
|
}
|
|
|
|
_canLoad(extension) {
|
|
if (!this._unloadedExtensions.has(extension.uuid))
|
|
return true;
|
|
|
|
const version = this._unloadedExtensions.get(extension.uuid);
|
|
return extension.metadata.version === version;
|
|
}
|
|
|
|
_isOutOfDate(extension) {
|
|
const [major] = Config.PACKAGE_VERSION.split('.');
|
|
return !extension.metadata['shell-version'].some(v => v.startsWith(major));
|
|
}
|
|
|
|
async loadExtension(extension) {
|
|
// Default to error, we set success as the last step
|
|
extension.state = ExtensionState.ERROR;
|
|
|
|
if (this._checkVersion && this._isOutOfDate(extension)) {
|
|
extension.state = ExtensionState.OUT_OF_DATE;
|
|
} else if (!this._canLoad(extension)) {
|
|
this.logExtensionError(extension.uuid, new Error(
|
|
'A different version was loaded previously. You need to log out for changes to take effect.'));
|
|
} else {
|
|
let enabled = this._enabledExtensions.includes(extension.uuid) &&
|
|
this._extensionSupportsSessionMode(extension.uuid);
|
|
if (enabled) {
|
|
if (!await this._callExtensionInit(extension.uuid))
|
|
return;
|
|
|
|
if (extension.state === ExtensionState.DISABLED)
|
|
await this._callExtensionEnable(extension.uuid);
|
|
} else {
|
|
extension.state = ExtensionState.INITIALIZED;
|
|
}
|
|
|
|
this._unloadedExtensions.delete(extension.uuid);
|
|
}
|
|
|
|
this._updateCanChange(extension);
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
|
|
async unloadExtension(extension) {
|
|
const { uuid, type } = extension;
|
|
|
|
// 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
|
|
// broke too much.
|
|
await this._callExtensionDisable(uuid);
|
|
|
|
extension.state = ExtensionState.UNINSTALLED;
|
|
this.emit('extension-state-changed', extension);
|
|
|
|
// The extension is now cached and it's impossible to load a different version
|
|
if (type === ExtensionType.PER_USER && extension.isImported)
|
|
this._unloadedExtensions.set(uuid, extension.metadata.version);
|
|
|
|
this._extensions.delete(uuid);
|
|
return true;
|
|
}
|
|
|
|
async reloadExtension(oldExtension) {
|
|
// Grab the things we'll need to pass to createExtensionObject
|
|
// to reload it.
|
|
let { uuid, dir, type } = oldExtension;
|
|
|
|
// Then unload the old extension.
|
|
await this.unloadExtension(oldExtension);
|
|
|
|
// Now, recreate the extension and load it.
|
|
let newExtension;
|
|
try {
|
|
newExtension = this.createExtensionObject(uuid, dir, type);
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
return;
|
|
}
|
|
|
|
await this.loadExtension(newExtension);
|
|
}
|
|
|
|
async _callExtensionInit(uuid) {
|
|
if (!this._extensionSupportsSessionMode(uuid))
|
|
return false;
|
|
|
|
let extension = this.lookup(uuid);
|
|
if (!extension)
|
|
throw new Error("Extension was not properly created. Call createExtensionObject first");
|
|
|
|
let dir = extension.dir;
|
|
let extensionJs = dir.get_child('extension.js');
|
|
if (!extensionJs.query_exists(null)) {
|
|
this.logExtensionError(uuid, new Error('Missing extension.js'));
|
|
return false;
|
|
}
|
|
|
|
let extensionModule;
|
|
let extensionState = null;
|
|
|
|
try {
|
|
extensionModule = await import(extensionJs.get_uri());
|
|
|
|
// Extensions can only be imported once, so add a property to avoid
|
|
// attempting to re-import an extension.
|
|
extension.isImported = true;
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const {metadata, path} = extension;
|
|
extensionState =
|
|
new extensionModule.default({...metadata, dir, path});
|
|
} catch (e) {
|
|
this.logExtensionError(uuid, e);
|
|
return false;
|
|
}
|
|
|
|
extension.stateObj = extensionState;
|
|
extension.state = ExtensionState.DISABLED;
|
|
this.emit('extension-loaded', uuid);
|
|
return true;
|
|
}
|
|
|
|
_getModeExtensions() {
|
|
if (Array.isArray(Main.sessionMode.enabledExtensions))
|
|
return Main.sessionMode.enabledExtensions;
|
|
return [];
|
|
}
|
|
|
|
_updateCanChange(extension) {
|
|
let hasError =
|
|
extension.state === ExtensionState.ERROR ||
|
|
extension.state === ExtensionState.OUT_OF_DATE;
|
|
|
|
let isMode = this._getModeExtensions().includes(extension.uuid);
|
|
let modeOnly = global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY);
|
|
|
|
let changeKey = isMode
|
|
? DISABLE_USER_EXTENSIONS_KEY
|
|
: ENABLED_EXTENSIONS_KEY;
|
|
|
|
extension.canChange =
|
|
!hasError &&
|
|
global.settings.is_writable(changeKey) &&
|
|
(isMode || !modeOnly);
|
|
}
|
|
|
|
_getEnabledExtensions() {
|
|
let extensions = this._getModeExtensions();
|
|
|
|
if (!global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY))
|
|
extensions = extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY));
|
|
|
|
// filter out 'disabled-extensions' which takes precedence
|
|
let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);
|
|
return extensions.filter(item => !disabledExtensions.includes(item));
|
|
}
|
|
|
|
async _onUserExtensionsEnabledChanged() {
|
|
await this._onEnabledExtensionsChanged();
|
|
this._onSettingsWritableChanged();
|
|
}
|
|
|
|
async _onEnabledExtensionsChanged() {
|
|
let newEnabledExtensions = this._getEnabledExtensions();
|
|
|
|
// Find and enable all the newly enabled extensions: UUIDs found in the
|
|
// new setting, but not in the old one.
|
|
const extensionsToEnable = newEnabledExtensions
|
|
.filter(uuid => !this._enabledExtensions.includes(uuid) &&
|
|
this._extensionSupportsSessionMode(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
|
|
// old setting, but not in the new one.
|
|
const extensionsToDisable = this._extensionOrder
|
|
.filter(uuid => !newEnabledExtensions.includes(uuid) ||
|
|
!this._extensionSupportsSessionMode(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;
|
|
}
|
|
|
|
_onSettingsWritableChanged() {
|
|
for (let extension of this._extensions.values()) {
|
|
this._updateCanChange(extension);
|
|
this.emit('extension-state-changed', extension);
|
|
}
|
|
}
|
|
|
|
async _onVersionValidationChanged() {
|
|
const checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY);
|
|
if (checkVersion === this._checkVersion)
|
|
return;
|
|
|
|
this._checkVersion = checkVersion;
|
|
|
|
// Disabling extensions modifies the order array, so use a copy
|
|
let extensionOrder = this._extensionOrder.slice();
|
|
|
|
// Disable enabled extensions first to avoid
|
|
// the "rebasing" done in _callExtensionDisable...
|
|
this._disableAllExtensions();
|
|
|
|
// ...and then reload and enable extensions in the correct order again.
|
|
const extensionsToReload = [...this._extensions.values()].sort((a, b) => {
|
|
return extensionOrder.indexOf(a.uuid) - extensionOrder.indexOf(b.uuid);
|
|
});
|
|
for (const extension of extensionsToReload) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.reloadExtension(extension);
|
|
}
|
|
}
|
|
|
|
_installExtensionUpdates() {
|
|
if (!this.updatesSupported)
|
|
return;
|
|
|
|
for (const {dir, info} of FileUtils.collectFromDatadirs('extension-updates', true)) {
|
|
let fileType = info.get_file_type();
|
|
if (fileType !== Gio.FileType.DIRECTORY)
|
|
continue;
|
|
let uuid = info.get_name();
|
|
let extensionDir = Gio.File.new_for_path(
|
|
GLib.build_filenamev([global.userdatadir, 'extensions', uuid]));
|
|
|
|
try {
|
|
FileUtils.recursivelyDeleteDir(extensionDir, false);
|
|
FileUtils.recursivelyMoveDir(dir, extensionDir);
|
|
} catch (e) {
|
|
log(`Failed to install extension updates for ${uuid}`);
|
|
} finally {
|
|
FileUtils.recursivelyDeleteDir(dir, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
async _loadExtensions() {
|
|
global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, () => {
|
|
this._onEnabledExtensionsChanged();
|
|
});
|
|
global.settings.connect(`changed::${DISABLED_EXTENSIONS_KEY}`, () => {
|
|
this._onEnabledExtensionsChanged();
|
|
});
|
|
global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, () => {
|
|
this._onUserExtensionsEnabledChanged();
|
|
});
|
|
global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, () => {
|
|
this._onVersionValidationChanged();
|
|
});
|
|
global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`, () =>
|
|
this._onSettingsWritableChanged());
|
|
global.settings.connect(`writable-changed::${DISABLED_EXTENSIONS_KEY}`, () =>
|
|
this._onSettingsWritableChanged());
|
|
|
|
await this._onVersionValidationChanged();
|
|
|
|
this._enabledExtensions = this._getEnabledExtensions();
|
|
|
|
let perUserDir = Gio.File.new_for_path(global.userdatadir);
|
|
|
|
const extensionFiles = [...FileUtils.collectFromDatadirs('extensions', true)];
|
|
const extensionObjects = extensionFiles.map(({dir, info}) => {
|
|
let fileType = info.get_file_type();
|
|
if (fileType !== Gio.FileType.DIRECTORY)
|
|
return null;
|
|
let uuid = info.get_name();
|
|
let existing = this.lookup(uuid);
|
|
if (existing) {
|
|
log(`Extension ${uuid} already installed in ${existing.path}. ${dir.get_path()} will not be loaded`);
|
|
return null;
|
|
}
|
|
|
|
let extension;
|
|
let type = dir.has_prefix(perUserDir)
|
|
? ExtensionType.PER_USER
|
|
: ExtensionType.SYSTEM;
|
|
try {
|
|
extension = this.createExtensionObject(uuid, dir, type);
|
|
} catch (error) {
|
|
logError(error, `Could not load extension ${uuid}`);
|
|
return null;
|
|
}
|
|
|
|
return extension;
|
|
}).filter(extension => extension !== null);
|
|
|
|
for (const extension of extensionObjects) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.loadExtension(extension);
|
|
}
|
|
}
|
|
|
|
async _enableAllExtensions() {
|
|
if (!this._initializationPromise)
|
|
this._initializationPromise = this._loadExtensions();
|
|
|
|
await this._initializationPromise;
|
|
|
|
for (const uuid of this._enabledExtensions) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this._callExtensionEnable(uuid);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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
|
|
await this._onEnabledExtensionsChanged();
|
|
await this._enableAllExtensions();
|
|
}
|
|
};
|
|
|
|
const ExtensionUpdateSource = GObject.registerClass(
|
|
class ExtensionUpdateSource extends MessageTray.Source {
|
|
_init() {
|
|
let appSys = Shell.AppSystem.get_default();
|
|
this._app = appSys.lookup_app('org.gnome.Extensions.desktop');
|
|
if (!this._app)
|
|
this._app = appSys.lookup_app('com.mattjakeman.ExtensionManager.desktop');
|
|
|
|
super._init(this._app.get_name());
|
|
}
|
|
|
|
getIcon() {
|
|
return this._app.app_info.get_icon();
|
|
}
|
|
|
|
_createPolicy() {
|
|
return new MessageTray.NotificationApplicationPolicy(this._app.id);
|
|
}
|
|
|
|
open() {
|
|
this._app.activate();
|
|
Main.overview.hide();
|
|
Main.panel.closeCalendar();
|
|
}
|
|
});
|