73649a0d6a
We currently assign the stylesheet to an extension whenever the file exists,
regardless of whether it actually loaded successfully or not.
And thus we load an extension that ships a stylesheet even if that file can't
be used.
There is no point in trying to load an extension if its stylesheet wasn't
loaded in the first place, so make sure this happens only on success.
https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/188
(cherry picked from commit 5b3ff7184e
)
374 lines
10 KiB
JavaScript
374 lines
10 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const Lang = imports.lang;
|
|
const Signals = imports.signals;
|
|
|
|
const GLib = imports.gi.GLib;
|
|
const Gio = imports.gi.Gio;
|
|
const St = imports.gi.St;
|
|
|
|
const ExtensionUtils = imports.misc.extensionUtils;
|
|
const Main = imports.ui.main;
|
|
|
|
var ExtensionState = {
|
|
ENABLED: 1,
|
|
DISABLED: 2,
|
|
ERROR: 3,
|
|
OUT_OF_DATE: 4,
|
|
DOWNLOADING: 5,
|
|
INITIALIZED: 6,
|
|
|
|
// Used as an error state for operations on unknown extensions,
|
|
// should never be in a real extensionMeta object.
|
|
UNINSTALLED: 99
|
|
};
|
|
|
|
// Arrays of uuids
|
|
var enabledExtensions;
|
|
// Contains the order that extensions were enabled in.
|
|
var extensionOrder = [];
|
|
|
|
// We don't really have a class to add signals on. So, create
|
|
// a simple dummy object, add the signal methods, and export those
|
|
// publically.
|
|
var _signals = {};
|
|
Signals.addSignalMethods(_signals);
|
|
|
|
var connect = _signals.connect.bind(_signals);
|
|
var disconnect = _signals.disconnect.bind(_signals);
|
|
|
|
const ENABLED_EXTENSIONS_KEY = 'enabled-extensions';
|
|
const DISABLE_USER_EXTENSIONS_KEY = 'disable-user-extensions';
|
|
const EXTENSION_DISABLE_VERSION_CHECK_KEY = 'disable-extension-version-validation';
|
|
|
|
var initted = false;
|
|
var enabled;
|
|
|
|
function disableExtension(uuid) {
|
|
let extension = ExtensionUtils.extensions[uuid];
|
|
if (!extension)
|
|
return;
|
|
|
|
if (extension.state != ExtensionState.ENABLED)
|
|
return;
|
|
|
|
// "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 = extensionOrder.indexOf(uuid);
|
|
let order = extensionOrder.slice(orderIdx + 1);
|
|
let orderReversed = order.slice().reverse();
|
|
|
|
for (let i = 0; i < orderReversed.length; i++) {
|
|
let uuid = orderReversed[i];
|
|
try {
|
|
ExtensionUtils.extensions[uuid].stateObj.disable();
|
|
} catch(e) {
|
|
logExtensionError(uuid, e);
|
|
}
|
|
}
|
|
|
|
if (extension.stylesheet) {
|
|
let theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
|
|
theme.unload_stylesheet(extension.stylesheet);
|
|
}
|
|
|
|
try {
|
|
extension.stateObj.disable();
|
|
} catch(e) {
|
|
logExtensionError(uuid, e);
|
|
}
|
|
|
|
for (let i = 0; i < order.length; i++) {
|
|
let uuid = order[i];
|
|
try {
|
|
ExtensionUtils.extensions[uuid].stateObj.enable();
|
|
} catch(e) {
|
|
logExtensionError(uuid, e);
|
|
}
|
|
}
|
|
|
|
extensionOrder.splice(orderIdx, 1);
|
|
|
|
if ( extension.state != ExtensionState.ERROR ) {
|
|
extension.state = ExtensionState.DISABLED;
|
|
_signals.emit('extension-state-changed', extension);
|
|
}
|
|
}
|
|
|
|
function enableExtension(uuid) {
|
|
let extension = ExtensionUtils.extensions[uuid];
|
|
if (!extension)
|
|
return;
|
|
|
|
if (extension.state == ExtensionState.INITIALIZED)
|
|
initExtension(uuid);
|
|
|
|
if (extension.state != ExtensionState.DISABLED)
|
|
return;
|
|
|
|
extensionOrder.push(uuid);
|
|
|
|
let stylesheetNames = [global.session_mode + '.css', 'stylesheet.css'];
|
|
let theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
|
|
for (let i = 0; i < stylesheetNames.length; i++) {
|
|
try {
|
|
let stylesheetFile = extension.dir.get_child(stylesheetNames[i]);
|
|
theme.load_stylesheet(stylesheetFile);
|
|
extension.stylesheet = stylesheetFile;
|
|
break;
|
|
} catch (e) {
|
|
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
|
|
continue; // not an error
|
|
log(`Failed to load stylesheet for extension ${uuid}: ${e.message}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
extension.stateObj.enable();
|
|
extension.state = ExtensionState.ENABLED;
|
|
_signals.emit('extension-state-changed', extension);
|
|
return;
|
|
} catch(e) {
|
|
logExtensionError(uuid, e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
function logExtensionError(uuid, error) {
|
|
let extension = ExtensionUtils.extensions[uuid];
|
|
if (!extension)
|
|
return;
|
|
|
|
let message = '' + error;
|
|
|
|
extension.state = ExtensionState.ERROR;
|
|
if (!extension.errors)
|
|
extension.errors = [];
|
|
extension.errors.push(message);
|
|
|
|
log('Extension "%s" had error: %s'.format(uuid, message));
|
|
_signals.emit('extension-state-changed', { uuid: uuid,
|
|
error: message,
|
|
state: extension.state });
|
|
}
|
|
|
|
function loadExtension(extension) {
|
|
// Default to error, we set success as the last step
|
|
extension.state = ExtensionState.ERROR;
|
|
|
|
let checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY);
|
|
|
|
if (checkVersion && ExtensionUtils.isOutOfDate(extension)) {
|
|
extension.state = ExtensionState.OUT_OF_DATE;
|
|
} else {
|
|
let enabled = enabledExtensions.indexOf(extension.uuid) != -1;
|
|
if (enabled) {
|
|
if (!initExtension(extension.uuid))
|
|
return;
|
|
if (extension.state == ExtensionState.DISABLED)
|
|
enableExtension(extension.uuid);
|
|
} else {
|
|
extension.state = ExtensionState.INITIALIZED;
|
|
}
|
|
}
|
|
|
|
_signals.emit('extension-state-changed', extension);
|
|
}
|
|
|
|
function unloadExtension(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.
|
|
disableExtension(extension.uuid);
|
|
|
|
extension.state = ExtensionState.UNINSTALLED;
|
|
_signals.emit('extension-state-changed', extension);
|
|
|
|
delete ExtensionUtils.extensions[extension.uuid];
|
|
return true;
|
|
}
|
|
|
|
function reloadExtension(oldExtension) {
|
|
// Grab the things we'll need to pass to createExtensionObject
|
|
// to reload it.
|
|
let { uuid: uuid, dir: dir, type: type } = oldExtension;
|
|
|
|
// Then unload the old extension.
|
|
unloadExtension(oldExtension);
|
|
|
|
// Now, recreate the extension and load it.
|
|
let newExtension;
|
|
try {
|
|
newExtension = ExtensionUtils.createExtensionObject(uuid, dir, type);
|
|
} catch(e) {
|
|
logExtensionError(uuid, e);
|
|
return;
|
|
}
|
|
|
|
loadExtension(newExtension);
|
|
}
|
|
|
|
function initExtension(uuid) {
|
|
let extension = ExtensionUtils.extensions[uuid];
|
|
let dir = extension.dir;
|
|
|
|
if (!extension)
|
|
throw new Error("Extension was not properly created. Call loadExtension first");
|
|
|
|
let extensionJs = dir.get_child('extension.js');
|
|
if (!extensionJs.query_exists(null)) {
|
|
logExtensionError(uuid, new Error('Missing extension.js'));
|
|
return false;
|
|
}
|
|
|
|
let extensionModule;
|
|
let extensionState = null;
|
|
|
|
ExtensionUtils.installImporter(extension);
|
|
try {
|
|
extensionModule = extension.imports.extension;
|
|
} catch(e) {
|
|
logExtensionError(uuid, e);
|
|
return false;
|
|
}
|
|
|
|
if (extensionModule.init) {
|
|
try {
|
|
extensionState = extensionModule.init(extension);
|
|
} catch(e) {
|
|
logExtensionError(uuid, e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!extensionState)
|
|
extensionState = extensionModule;
|
|
extension.stateObj = extensionState;
|
|
|
|
extension.state = ExtensionState.DISABLED;
|
|
_signals.emit('extension-loaded', uuid);
|
|
return true;
|
|
}
|
|
|
|
function getEnabledExtensions() {
|
|
let extensions;
|
|
if (Array.isArray(Main.sessionMode.enabledExtensions))
|
|
extensions = Main.sessionMode.enabledExtensions;
|
|
else
|
|
extensions = [];
|
|
|
|
if (global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY))
|
|
return extensions;
|
|
|
|
return extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY));
|
|
}
|
|
|
|
function onEnabledExtensionsChanged() {
|
|
let newEnabledExtensions = getEnabledExtensions();
|
|
|
|
if (!enabled)
|
|
return;
|
|
|
|
// Find and enable all the newly enabled extensions: UUIDs found in the
|
|
// new setting, but not in the old one.
|
|
newEnabledExtensions.filter(
|
|
uuid => !enabledExtensions.includes(uuid)
|
|
).forEach(uuid => {
|
|
enableExtension(uuid);
|
|
});
|
|
|
|
// Find and disable all the newly disabled extensions: UUIDs found in the
|
|
// old setting, but not in the new one.
|
|
enabledExtensions.filter(
|
|
item => !newEnabledExtensions.includes(item)
|
|
).forEach(uuid => {
|
|
disableExtension(uuid);
|
|
});
|
|
|
|
enabledExtensions = newEnabledExtensions;
|
|
}
|
|
|
|
function _onVersionValidationChanged() {
|
|
// we want to reload all extensions, but only enable
|
|
// extensions when allowed by the sessionMode, so
|
|
// temporarily disable them all
|
|
enabledExtensions = [];
|
|
for (let uuid in ExtensionUtils.extensions)
|
|
reloadExtension(ExtensionUtils.extensions[uuid]);
|
|
enabledExtensions = getEnabledExtensions();
|
|
|
|
if (Main.sessionMode.allowExtensions) {
|
|
enabledExtensions.forEach(uuid => {
|
|
enableExtension(uuid);
|
|
});
|
|
}
|
|
}
|
|
|
|
function _loadExtensions() {
|
|
global.settings.connect('changed::' + ENABLED_EXTENSIONS_KEY, onEnabledExtensionsChanged);
|
|
global.settings.connect('changed::' + DISABLE_USER_EXTENSIONS_KEY, onEnabledExtensionsChanged);
|
|
global.settings.connect('changed::' + EXTENSION_DISABLE_VERSION_CHECK_KEY, _onVersionValidationChanged);
|
|
|
|
enabledExtensions = getEnabledExtensions();
|
|
|
|
let finder = new ExtensionUtils.ExtensionFinder();
|
|
finder.connect('extension-found', (finder, extension) => {
|
|
loadExtension(extension);
|
|
});
|
|
finder.scanExtensions();
|
|
}
|
|
|
|
function enableAllExtensions() {
|
|
if (enabled)
|
|
return;
|
|
|
|
if (!initted) {
|
|
_loadExtensions();
|
|
initted = true;
|
|
} else {
|
|
enabledExtensions.forEach(uuid => {
|
|
enableExtension(uuid);
|
|
});
|
|
}
|
|
enabled = true;
|
|
}
|
|
|
|
function disableAllExtensions() {
|
|
if (!enabled)
|
|
return;
|
|
|
|
if (initted) {
|
|
extensionOrder.slice().reverse().forEach(uuid => {
|
|
disableExtension(uuid);
|
|
});
|
|
}
|
|
|
|
enabled = false;
|
|
}
|
|
|
|
function _sessionUpdated() {
|
|
// For now sessionMode.allowExtensions controls extensions from both the
|
|
// 'enabled-extensions' preference and the sessionMode.enabledExtensions
|
|
// property; it might make sense to make enabledExtensions independent
|
|
// from allowExtensions in the future
|
|
if (Main.sessionMode.allowExtensions) {
|
|
if (initted)
|
|
enabledExtensions = getEnabledExtensions();
|
|
enableAllExtensions();
|
|
} else {
|
|
disableAllExtensions();
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
Main.sessionMode.connect('updated', _sessionUpdated);
|
|
_sessionUpdated();
|
|
}
|