From ab3a275e20c36cc21e529bb2c4328ea36024ecba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Sat, 6 Jul 2019 15:31:57 +0200 Subject: [PATCH 01/26] extensionUtils: Move ExtensionState definition here It makes sense to keep extension-related enums in the same module instead of spreading them between ExtensionSystem and ExtensionUtils. More importantly, this will make the type available to the extensions-prefs tool (which runs in a different process and therefore only has access to a limited set of modules). https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/misc/extensionUtils.js | 13 +++++++++++++ js/ui/extensionSystem.js | 13 +------------ js/ui/lookingGlass.js | 14 ++++++++------ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js index fb1e2b506..dc6e74cf8 100644 --- a/js/misc/extensionUtils.js +++ b/js/misc/extensionUtils.js @@ -17,6 +17,19 @@ var ExtensionType = { SESSION_MODE: 3 }; +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 +}; + // Maps uuid -> metadata object var extensions = {}; diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 9ffdb4f3d..3091af2ba 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -6,18 +6,7 @@ const Signals = imports.signals; 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 -}; +const { ExtensionState } = ExtensionUtils; // Arrays of uuids var enabledExtensions; diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js index 958211df0..fefb3f731 100644 --- a/js/ui/lookingGlass.js +++ b/js/ui/lookingGlass.js @@ -14,6 +14,8 @@ const Tweener = imports.ui.tweener; const Main = imports.ui.main; const JsParse = imports.misc.jsParse; +const { ExtensionState } = ExtensionUtils; + const CHEVRON = '>>> '; /* Imports...feel free to add here as needed */ @@ -684,16 +686,16 @@ var Extensions = class Extensions { _stateToString(extensionState) { switch (extensionState) { - case ExtensionSystem.ExtensionState.ENABLED: + case ExtensionState.ENABLED: return _("Enabled"); - case ExtensionSystem.ExtensionState.DISABLED: - case ExtensionSystem.ExtensionState.INITIALIZED: + case ExtensionState.DISABLED: + case ExtensionState.INITIALIZED: return _("Disabled"); - case ExtensionSystem.ExtensionState.ERROR: + case ExtensionState.ERROR: return _("Error"); - case ExtensionSystem.ExtensionState.OUT_OF_DATE: + case ExtensionState.OUT_OF_DATE: return _("Out of date"); - case ExtensionSystem.ExtensionState.DOWNLOADING: + case ExtensionState.DOWNLOADING: return _("Downloading"); } return 'Unknown'; // Not translated, shouldn't appear -- 2.29.2 From c16d1589d093dac4e0efe21c7f1aeb635afabd0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Thu, 7 Mar 2019 01:45:45 +0100 Subject: [PATCH 02/26] extensionSystem: Turn into a class The extension system started out as a set of simple functions, but gained more state later, and even some hacks to emit signals without having an object to emit them on. There is no good reason for that weirdness, so rather than imitating an object, wrap the existing system into a real ExtensionManager object. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/ui/extensionDownloader.js | 17 +- js/ui/extensionSystem.js | 569 +++++++++++++++++------------------ js/ui/lookingGlass.js | 5 +- js/ui/main.js | 3 +- js/ui/shellDBus.js | 7 +- 5 files changed, 297 insertions(+), 304 deletions(-) diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index 9aed29c69..fe37463f2 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -6,6 +6,7 @@ const Config = imports.misc.config; const ExtensionUtils = imports.misc.extensionUtils; const ExtensionSystem = imports.ui.extensionSystem; const FileUtils = imports.misc.fileUtils; +const Main = imports.ui.main; const ModalDialog = imports.ui.modalDialog; const _signals = ExtensionSystem._signals; @@ -25,7 +26,7 @@ function installExtension(uuid, invocation) { _httpSession.queue_message(message, (session, message) => { if (message.status_code != Soup.KnownStatusCode.OK) { - ExtensionSystem.logExtensionError(uuid, 'downloading info: ' + message.status_code); + Main.extensionManager.logExtensionError(uuid, 'downloading info: ' + message.status_code); invocation.return_dbus_error('org.gnome.Shell.DownloadInfoError', message.status_code.toString()); return; } @@ -34,7 +35,7 @@ function installExtension(uuid, invocation) { try { info = JSON.parse(message.response_body.data); } catch (e) { - ExtensionSystem.logExtensionError(uuid, 'parsing info: ' + e); + Main.extensionManager.logExtensionError(uuid, 'parsing info: ' + e); invocation.return_dbus_error('org.gnome.Shell.ParseInfoError', e.toString()); return; } @@ -53,7 +54,7 @@ function uninstallExtension(uuid) { if (extension.type != ExtensionUtils.ExtensionType.PER_USER) return false; - if (!ExtensionSystem.unloadExtension(extension)) + if (!Main.extensionManager.unloadExtension(extension)) return false; FileUtils.recursivelyDeleteDir(extension.dir, true); @@ -117,7 +118,7 @@ function updateExtension(uuid) { let oldExtension = ExtensionUtils.extensions[uuid]; let extensionDir = oldExtension.dir; - if (!ExtensionSystem.unloadExtension(oldExtension)) + if (!Main.extensionManager.unloadExtension(oldExtension)) return; FileUtils.recursivelyMoveDir(extensionDir, oldExtensionTmpDir); @@ -127,10 +128,10 @@ function updateExtension(uuid) { try { extension = ExtensionUtils.createExtensionObject(uuid, extensionDir, ExtensionUtils.ExtensionType.PER_USER); - ExtensionSystem.loadExtension(extension); + Main.extensionManager.loadExtension(extension); } catch(e) { if (extension) - ExtensionSystem.unloadExtension(extension); + Main.extensionManager.unloadExtension(extension); logError(e, 'Error loading extension %s'.format(uuid)); @@ -139,7 +140,7 @@ function updateExtension(uuid) { // Restore what was there before. We can't do much if we // fail here. - ExtensionSystem.loadExtension(oldExtension); + Main.extensionManager.loadExtension(oldExtension); return; } @@ -239,7 +240,7 @@ class InstallExtensionDialog extends ModalDialog.ModalDialog { try { let extension = ExtensionUtils.createExtensionObject(uuid, dir, ExtensionUtils.ExtensionType.PER_USER); - ExtensionSystem.loadExtension(extension); + Main.extensionManager.loadExtension(extension); } catch(e) { uninstallExtension(uuid); errback('LoadExtensionError', e); diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 3091af2ba..b7e908223 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -8,358 +8,351 @@ const Main = imports.ui.main; const { ExtensionState } = ExtensionUtils; -// 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; +var ExtensionManager = class { + constructor() { + this._initted = false; + this._enabled = false; -function disableExtension(uuid) { - let extension = ExtensionUtils.extensions[uuid]; - if (!extension) - return; + this._enabledExtensions = []; + this._extensionOrder = []; - if (extension.state != ExtensionState.ENABLED) - return; + Main.sessionMode.connect('updated', this._sessionUpdated.bind(this)); + this._sessionUpdated(); + } - // "Rebase" the extension order by disabling and then enabling extensions - // in order to help prevent conflicts. + disableExtension(uuid) { + let extension = ExtensionUtils.extensions[uuid]; + if (!extension) + return; - // Example: - // order = [A, B, C, D, E] - // user disables C - // this should: disable E, disable D, disable C, enable D, enable E + if (extension.state != ExtensionState.ENABLED) + return; - let orderIdx = extensionOrder.indexOf(uuid); - let order = extensionOrder.slice(orderIdx + 1); - let orderReversed = order.slice().reverse(); + // "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 uuid = orderReversed[i]; + try { + ExtensionUtils.extensions[uuid].stateObj.disable(); + } catch (e) { + this.logExtensionError(uuid, e); + } + } + + if (extension.stylesheet) { + let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); + theme.unload_stylesheet(extension.stylesheet); + delete extension.stylesheet; + } - for (let i = 0; i < orderReversed.length; i++) { - let uuid = orderReversed[i]; try { - ExtensionUtils.extensions[uuid].stateObj.disable(); + extension.stateObj.disable(); } catch(e) { - logExtensionError(uuid, e); + this.logExtensionError(uuid, e); } - } - if (extension.stylesheet) { - let theme = St.ThemeContext.get_for_stage(global.stage).get_theme(); - theme.unload_stylesheet(extension.stylesheet); - delete extension.stylesheet; - } + for (let i = 0; i < order.length; i++) { + let uuid = order[i]; + try { + ExtensionUtils.extensions[uuid].stateObj.enable(); + } catch (e) { + this.logExtensionError(uuid, e); + } + } - try { - extension.stateObj.disable(); - } catch(e) { - logExtensionError(uuid, e); - } + this._extensionOrder.splice(orderIdx, 1); - for (let i = 0; i < order.length; i++) { - let uuid = order[i]; - try { - ExtensionUtils.extensions[uuid].stateObj.enable(); - } catch(e) { - logExtensionError(uuid, e); + if (extension.state != ExtensionState.ERROR) { + extension.state = ExtensionState.DISABLED; + this.emit('extension-state-changed', extension); } } - extensionOrder.splice(orderIdx, 1); - - if ( extension.state != ExtensionState.ERROR ) { - extension.state = ExtensionState.DISABLED; - _signals.emit('extension-state-changed', extension); - } -} + enableExtension(uuid) { + let extension = ExtensionUtils.extensions[uuid]; + if (!extension) + return; -function enableExtension(uuid) { - let extension = ExtensionUtils.extensions[uuid]; - if (!extension) - return; + if (extension.state == ExtensionState.INITIALIZED) + this.initExtension(uuid); - if (extension.state == ExtensionState.INITIALIZED) - initExtension(uuid); + if (extension.state != ExtensionState.DISABLED) + return; - if (extension.state != ExtensionState.DISABLED) - return; + this._extensionOrder.push(uuid); - 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; + } + } - 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; + extension.stateObj.enable(); + extension.state = ExtensionState.ENABLED; + this.emit('extension-state-changed', extension); + return; } 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}`); + if (extension.stylesheet) { + theme.unload_stylesheet(extension.stylesheet); + delete extension.stylesheet; + } + this.logExtensionError(uuid, e); return; } } - try { - extension.stateObj.enable(); - extension.state = ExtensionState.ENABLED; - _signals.emit('extension-state-changed', extension); - return; - } catch(e) { - if (extension.stylesheet) { - theme.unload_stylesheet(extension.stylesheet); - delete extension.stylesheet; - } - logExtensionError(uuid, e); - return; - } -} - -function logExtensionError(uuid, error) { - let extension = ExtensionUtils.extensions[uuid]; - if (!extension) - return; + logExtensionError(uuid, error) { + let extension = ExtensionUtils.extensions[uuid]; + if (!extension) + return; - let message = '' + error; + let message = '' + error; - extension.state = ExtensionState.ERROR; - if (!extension.errors) - extension.errors = []; - extension.errors.push(message); + 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, + log('Extension "%s" had error: %s'.format(uuid, message)); + this.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; + 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); + 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); + if (checkVersion && ExtensionUtils.isOutOfDate(extension)) { + extension.state = ExtensionState.OUT_OF_DATE; } else { - extension.state = ExtensionState.INITIALIZED; + let enabled = this._enabledExtensions.includes(extension.uuid); + if (enabled) { + if (!this.initExtension(extension.uuid)) + return; + if (extension.state == ExtensionState.DISABLED) + this.enableExtension(extension.uuid); + } else { + extension.state = ExtensionState.INITIALIZED; + } } + + this.emit('extension-state-changed', extension); } - _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; + 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. + this.disableExtension(extension.uuid); + + extension.state = ExtensionState.UNINSTALLED; + this.emit('extension-state-changed', extension); + + delete ExtensionUtils.extensions[extension.uuid]; + return true; } - loadExtension(newExtension); -} + reloadExtension(oldExtension) { + // Grab the things we'll need to pass to createExtensionObject + // to reload it. + let { uuid: uuid, dir: dir, type: type } = oldExtension; -function initExtension(uuid) { - let extension = ExtensionUtils.extensions[uuid]; - let dir = extension.dir; + // Then unload the old extension. + this.unloadExtension(oldExtension); - if (!extension) - throw new Error("Extension was not properly created. Call loadExtension first"); + // Now, recreate the extension and load it. + let newExtension; + try { + newExtension = ExtensionUtils.createExtensionObject(uuid, dir, type); + } catch (e) { + this.logExtensionError(uuid, e); + return; + } - let extensionJs = dir.get_child('extension.js'); - if (!extensionJs.query_exists(null)) { - logExtensionError(uuid, new Error('Missing extension.js')); - return false; + this.loadExtension(newExtension); } - let extensionModule; - let extensionState = null; + initExtension(uuid) { + let extension = ExtensionUtils.extensions[uuid]; + let dir = extension.dir; - ExtensionUtils.installImporter(extension); - try { - extensionModule = extension.imports.extension; - } catch(e) { - logExtensionError(uuid, e); - return false; - } + 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)) { + this.logExtensionError(uuid, new Error('Missing extension.js')); + return false; + } + + let extensionModule; + let extensionState = null; - if (extensionModule.init) { + ExtensionUtils.installImporter(extension); try { - extensionState = extensionModule.init(extension); + extensionModule = extension.imports.extension; } catch(e) { - logExtensionError(uuid, e); + this.logExtensionError(uuid, e); return false; } + + if (extensionModule.init) { + try { + extensionState = extensionModule.init(extension); + } catch (e) { + this.logExtensionError(uuid, e); + return false; + } + } + + if (!extensionState) + extensionState = extensionModule; + extension.stateObj = extensionState; + + extension.state = ExtensionState.DISABLED; + this.emit('extension-loaded', uuid); + return true; } - 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); - }); + _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 _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); - if (Main.sessionMode.enabledExtensions.indexOf(extension.uuid) != -1) - extension.type = ExtensionUtils.ExtensionType.SESSION_MODE; - }); - finder.scanExtensions(); -} - -function enableAllExtensions() { - if (enabled) - return; - - if (!initted) { - _loadExtensions(); - initted = true; - } else { - enabledExtensions.forEach(uuid => { - enableExtension(uuid); + + _onEnabledExtensionsChanged() { + let newEnabledExtensions = this._getEnabledExtensions(); + + if (!this._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 => !this._enabledExtensions.includes(uuid) + ).forEach(uuid => { + this.enableExtension(uuid); }); + + // Find and disable all the newly disabled extensions: UUIDs found in the + // old setting, but not in the new one. + this._enabledExtensions.filter( + item => !newEnabledExtensions.includes(item) + ).forEach(uuid => { + this.disableExtension(uuid); + }); + + this._enabledExtensions = newEnabledExtensions; + } + + _onVersionValidationChanged() { + // we want to reload all extensions, but only enable + // extensions when allowed by the sessionMode, so + // temporarily disable them all + this._enabledExtensions = []; + for (let uuid in ExtensionUtils.extensions) + this.reloadExtension(ExtensionUtils.extensions[uuid]); + this._enabledExtensions = this._getEnabledExtensions(); + + if (Main.sessionMode.allowExtensions) { + this._enabledExtensions.forEach(uuid => { + this.enableExtension(uuid); + }); + } } - enabled = true; -} -function disableAllExtensions() { - if (!enabled) - return; + _loadExtensions() { + global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, + this._onEnabledExtensionsChanged.bind(this)); + global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, + this._onEnabledExtensionsChanged.bind(this)); + global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, + this._onVersionValidationChanged.bind(this)); + + this._enabledExtensions = this._getEnabledExtensions(); - if (initted) { - extensionOrder.slice().reverse().forEach(uuid => { - disableExtension(uuid); + let finder = new ExtensionUtils.ExtensionFinder(); + finder.connect('extension-found', (finder, extension) => { + this.loadExtension(extension); }); + finder.scanExtensions(); + } + + enableAllExtensions() { + if (this._enabled) + return; + + if (!this._initted) { + this._loadExtensions(); + this._initted = true; + } else { + this._enabledExtensions.forEach(uuid => { + this.enableExtension(uuid); + }); + } + this._enabled = true; } - 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(); + disableAllExtensions() { + if (!this._enabled) + return; + + if (this._initted) { + this._extensionOrder.slice().reverse().forEach(uuid => { + this.disableExtension(uuid); + }); + } + + this._enabled = false; } -} -function init() { - Main.sessionMode.connect('updated', _sessionUpdated); - _sessionUpdated(); -} + _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 (this._initted) + this._enabledExtensions = this._getEnabledExtensions(); + this.enableAllExtensions(); + } else { + this.disableAllExtensions(); + } + } +}; +Signals.addSignalMethods(ExtensionManager.prototype); diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js index fefb3f731..e947574f2 100644 --- a/js/ui/lookingGlass.js +++ b/js/ui/lookingGlass.js @@ -7,7 +7,6 @@ const Signals = imports.signals; const System = imports.system; const History = imports.misc.history; -const ExtensionSystem = imports.ui.extensionSystem; const ExtensionUtils = imports.misc.extensionUtils; const ShellEntry = imports.ui.shellEntry; const Tweener = imports.ui.tweener; @@ -624,8 +623,8 @@ var Extensions = class Extensions { for (let uuid in ExtensionUtils.extensions) this._loadExtension(null, uuid); - ExtensionSystem.connect('extension-loaded', - this._loadExtension.bind(this)); + Main.extensionManager.connect('extension-loaded', + this._loadExtension.bind(this)); } _loadExtension(o, uuid) { diff --git a/js/ui/main.js b/js/ui/main.js index 8dde95bf9..7bfbce497 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -43,6 +43,7 @@ const STICKY_KEYS_ENABLE = 'stickykeys-enable'; const GNOMESHELL_STARTED_MESSAGE_ID = 'f3ea493c22934e26811cd62abe8e203a'; var componentManager = null; +var extensionManager = null; var panel = null; var overview = null; var runDialog = null; @@ -218,7 +219,7 @@ function _initializeUI() { _startDate = new Date(); ExtensionDownloader.init(); - ExtensionSystem.init(); + extensionManager = new ExtensionSystem.ExtensionManager(); if (sessionMode.isGreeter && screenShield) { layoutManager.connect('startup-prepared', () => { diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js index 112d60feb..4b04e68ac 100644 --- a/js/ui/shellDBus.js +++ b/js/ui/shellDBus.js @@ -4,7 +4,6 @@ const { Gio, GLib, Meta, Shell } = imports.gi; const Lang = imports.lang; const Config = imports.misc.config; -const ExtensionSystem = imports.ui.extensionSystem; const ExtensionDownloader = imports.ui.extensionDownloader; const ExtensionUtils = imports.misc.extensionUtils; const Main = imports.ui.main; @@ -250,8 +249,8 @@ var GnomeShellExtensions = class { constructor() { this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellExtensionsIface, this); this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell'); - ExtensionSystem.connect('extension-state-changed', - this._extensionStateChanged.bind(this)); + Main.extensionManager.connect('extension-state-changed', + this._extensionStateChanged.bind(this)); } ListExtensions() { @@ -334,7 +333,7 @@ var GnomeShellExtensions = class { if (!extension) return; - ExtensionSystem.reloadExtension(extension); + Main.extensionManager.reloadExtension(extension); } CheckForUpdates() { -- 2.29.2 From 7419ee28b5b568dd4478db7f3c890ff01678637a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 8 Jul 2019 02:53:32 +0200 Subject: [PATCH 03/26] extensionSystem: Make methods to call extension functions private While public methods to enable/disable extensions make sense for an extension manager, the existing ones are only used internally. Make them private and rename them, so that we can re-use the current names for more useful public methods. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/ui/extensionSystem.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index b7e908223..c5fb007ae 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -24,7 +24,7 @@ var ExtensionManager = class { this._sessionUpdated(); } - disableExtension(uuid) { + _callExtensionDisable(uuid) { let extension = ExtensionUtils.extensions[uuid]; if (!extension) return; @@ -82,13 +82,13 @@ var ExtensionManager = class { } } - enableExtension(uuid) { + _callExtensionEnable(uuid) { let extension = ExtensionUtils.extensions[uuid]; if (!extension) return; if (extension.state == ExtensionState.INITIALIZED) - this.initExtension(uuid); + this._callExtensionInit(uuid); if (extension.state != ExtensionState.DISABLED) return; @@ -155,10 +155,10 @@ var ExtensionManager = class { } else { let enabled = this._enabledExtensions.includes(extension.uuid); if (enabled) { - if (!this.initExtension(extension.uuid)) + if (!this._callExtensionInit(extension.uuid)) return; if (extension.state == ExtensionState.DISABLED) - this.enableExtension(extension.uuid); + this._callExtensionEnable(extension.uuid); } else { extension.state = ExtensionState.INITIALIZED; } @@ -171,7 +171,7 @@ var ExtensionManager = class { // 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. - this.disableExtension(extension.uuid); + this._callExtensionDisable(extension.uuid); extension.state = ExtensionState.UNINSTALLED; this.emit('extension-state-changed', extension); @@ -200,7 +200,7 @@ var ExtensionManager = class { this.loadExtension(newExtension); } - initExtension(uuid) { + _callExtensionInit(uuid) { let extension = ExtensionUtils.extensions[uuid]; let dir = extension.dir; @@ -266,7 +266,7 @@ var ExtensionManager = class { newEnabledExtensions.filter( uuid => !this._enabledExtensions.includes(uuid) ).forEach(uuid => { - this.enableExtension(uuid); + this._callExtensionEnable(uuid); }); // Find and disable all the newly disabled extensions: UUIDs found in the @@ -274,7 +274,7 @@ var ExtensionManager = class { this._enabledExtensions.filter( item => !newEnabledExtensions.includes(item) ).forEach(uuid => { - this.disableExtension(uuid); + this._callExtensionDisable(uuid); }); this._enabledExtensions = newEnabledExtensions; @@ -291,7 +291,7 @@ var ExtensionManager = class { if (Main.sessionMode.allowExtensions) { this._enabledExtensions.forEach(uuid => { - this.enableExtension(uuid); + this._callExtensionEnable(uuid); }); } } @@ -313,7 +313,7 @@ var ExtensionManager = class { finder.scanExtensions(); } - enableAllExtensions() { + _enableAllExtensions() { if (this._enabled) return; @@ -322,19 +322,19 @@ var ExtensionManager = class { this._initted = true; } else { this._enabledExtensions.forEach(uuid => { - this.enableExtension(uuid); + this._callExtensionEnable(uuid); }); } this._enabled = true; } - disableAllExtensions() { + _disableAllExtensions() { if (!this._enabled) return; if (this._initted) { this._extensionOrder.slice().reverse().forEach(uuid => { - this.disableExtension(uuid); + this._callExtensionDisable(uuid); }); } @@ -349,9 +349,9 @@ var ExtensionManager = class { if (Main.sessionMode.allowExtensions) { if (this._initted) this._enabledExtensions = this._getEnabledExtensions(); - this.enableAllExtensions(); + this._enableAllExtensions(); } else { - this.disableAllExtensions(); + this._disableAllExtensions(); } } }; -- 2.29.2 From e88419278531b136984f9f05a8c056145d03edba Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Wed, 17 Jan 2018 13:43:11 +0100 Subject: [PATCH 04/26] extensionSystem: Add methods to enable/disable extensions Extensions are currently enabled or disabled by directly changing the list in the 'enabled-extensions' GSettings key. As we will soon add an overriding 'disabled-extensions' key as well, it makes sense to offer explicit API for enabling/disabling to avoid duplicating the logic. For the corresponding D-Bus API, the methods were even mentioned in the GSettings schema, albeit unimplemented until now. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- .../org.gnome.Shell.Extensions.xml | 24 +++++++++++++++++ js/ui/extensionDownloader.js | 12 ++------- js/ui/extensionSystem.js | 26 +++++++++++++++++++ js/ui/shellDBus.js | 8 ++++++ 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/data/dbus-interfaces/org.gnome.Shell.Extensions.xml b/data/dbus-interfaces/org.gnome.Shell.Extensions.xml index ce69439fc..22273f889 100644 --- a/data/dbus-interfaces/org.gnome.Shell.Extensions.xml +++ b/data/dbus-interfaces/org.gnome.Shell.Extensions.xml @@ -173,6 +173,30 @@ + + \ + \ + \ + \ + + + \ + \ + \ + \ + diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js index 43efa95e9..2b4ce5753 100644 --- a/js/extensionPrefs/main.js +++ b/js/extensionPrefs/main.js @@ -241,7 +241,7 @@ var Application = class { this._mainStack.add_named(new EmptyPlaceholder(), 'placeholder'); this._shellProxy = new GnomeShellProxy(Gio.DBus.session, 'org.gnome.Shell', '/org/gnome/Shell'); - this._shellProxy.connectSignal('ExtensionStatusChanged', (proxy, senderName, [uuid, state, error]) => { + this._shellProxy.connectSignal('ExtensionStateChanged', (proxy, senderName, [uuid, state]) => { if (ExtensionUtils.extensions[uuid] !== undefined) this._scanExtensions(); }); diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 8ff0fa56f..98eaf5259 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -159,15 +159,14 @@ var ExtensionManager = class { let message = '' + error; + extension.error = message; extension.state = ExtensionState.ERROR; if (!extension.errors) extension.errors = []; extension.errors.push(message); log('Extension "%s" had error: %s'.format(uuid, message)); - this.emit('extension-state-changed', { uuid: uuid, - error: message, - state: extension.state }); + this.emit('extension-state-changed', extension); } loadExtension(extension) { diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js index af5889789..23274c0a3 100644 --- a/js/ui/shellDBus.js +++ b/js/ui/shellDBus.js @@ -335,6 +335,10 @@ var GnomeShellExtensions = class { } _extensionStateChanged(_, newState) { + let state = ExtensionUtils.serializeExtension(newState); + this._dbusImpl.emit_signal('ExtensionStateChanged', + new GLib.Variant('(sa{sv})', [newState.uuid, state])); + this._dbusImpl.emit_signal('ExtensionStatusChanged', GLib.Variant.new('(sis)', [newState.uuid, newState.state, newState.error])); } -- 2.29.2 From 47d185f8964f9e04430f4ef97d6d712faaf32078 Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Tue, 4 Dec 2018 09:31:27 +0100 Subject: [PATCH 07/26] extensionSystem: Add canChange property to extensions Whether or not an extension can be enabled/disabled depends on various factors: Whether the extension is in error state, whether user extensions are disabled and whether the underlying GSettings keys are writable. This is complex enough to share the logic, so add it to the extension properties that are exposed over D-Bus. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/misc/extensionUtils.js | 3 ++- js/ui/extensionSystem.js | 44 +++++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js index bc9c36f4e..025cd042e 100644 --- a/js/misc/extensionUtils.js +++ b/js/misc/extensionUtils.js @@ -31,7 +31,7 @@ var ExtensionState = { UNINSTALLED: 99 }; -const SERIALIZED_PROPERTIES = ['type', 'state', 'path', 'error', 'hasPrefs']; +const SERIALIZED_PROPERTIES = ['type', 'state', 'path', 'error', 'hasPrefs', 'canChange']; // Maps uuid -> metadata object var extensions = {}; @@ -222,6 +222,7 @@ function createExtensionObject(uuid, dir, type) { extension.path = dir.get_path(); extension.error = ''; extension.hasPrefs = dir.get_child('prefs.js').query_exists(null); + extension.canChange = false; extensions[uuid] = extension; diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 98eaf5259..a83e53c83 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -189,6 +189,7 @@ var ExtensionManager = class { } } + this._updateCanChange(extension); this.emit('extension-state-changed', extension); } @@ -267,12 +268,28 @@ var ExtensionManager = class { return true; } - _getEnabledExtensions() { - let extensions; + _getModeExtensions() { if (Array.isArray(Main.sessionMode.enabledExtensions)) - extensions = Main.sessionMode.enabledExtensions; - else - extensions = []; + 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); + + extension.canChange = + !hasError && + global.settings.is_writable(ENABLED_EXTENSIONS_KEY) && + (isMode || !modeOnly); + } + + _getEnabledExtensions() { + let extensions = this._getModeExtensions(); if (global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY)) return extensions; @@ -280,6 +297,11 @@ var ExtensionManager = class { return extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY)); } + _onUserExtensionsEnabledChanged() { + this._onEnabledExtensionsChanged(); + this._onSettingsWritableChanged(); + } + _onEnabledExtensionsChanged() { let newEnabledExtensions = this._getEnabledExtensions(); @@ -305,6 +327,14 @@ var ExtensionManager = class { this._enabledExtensions = newEnabledExtensions; } + _onSettingsWritableChanged() { + for (let uuid in ExtensionUtils.extensions) { + let extension = ExtensionUtils.extensions[uuid]; + this._updateCanChange(extension); + this.emit('extension-state-changed', extension); + } + } + _onVersionValidationChanged() { // we want to reload all extensions, but only enable // extensions when allowed by the sessionMode, so @@ -325,9 +355,11 @@ var ExtensionManager = class { global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, this._onEnabledExtensionsChanged.bind(this)); global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, - this._onEnabledExtensionsChanged.bind(this)); + this._onUserExtensionsEnabledChanged.bind(this)); global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, this._onVersionValidationChanged.bind(this)); + global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`, + this._onSettingsWritableChanged.bind(this)); this._enabledExtensions = this._getEnabledExtensions(); -- 2.29.2 From ce14c00ca0707c78dc920ede3a157b7c9d55fff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Tue, 28 May 2019 23:22:37 +0200 Subject: [PATCH 08/26] extensionPrefs: Inherit from Gtk.Application Extension preferences Application class is just a container for a GtkApplication so instead of using composition we can inherit from the base GObject class. Also replace signal connections with vfunc's. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/631 --- js/extensionPrefs/main.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js index 2b4ce5753..94a5b12fe 100644 --- a/js/extensionPrefs/main.js +++ b/js/extensionPrefs/main.js @@ -17,18 +17,16 @@ function stripPrefix(string, prefix) { return string; } -var Application = class { - constructor() { +var Application = GObject.registerClass({ + GTypeName: 'ExtensionPrefs_Application' +}, class Application extends Gtk.Application { + _init() { GLib.set_prgname('gnome-shell-extension-prefs'); - this.application = new Gtk.Application({ + super._init({ application_id: 'org.gnome.shell.ExtensionPrefs', flags: Gio.ApplicationFlags.HANDLES_COMMAND_LINE }); - this.application.connect('activate', this._onActivate.bind(this)); - this.application.connect('command-line', this._onCommandLine.bind(this)); - this.application.connect('startup', this._onStartup.bind(this)); - this._extensionPrefsModules = {}; this._startupUuid = null; @@ -84,7 +82,7 @@ var Application = class { visible: true })); if (this._skipMainWindow) { - this.application.add_window(dialog); + this.add_window(dialog); if (this._window) this._window.destroy(); this._window = dialog; @@ -206,8 +204,8 @@ var Application = class { return scroll; } - _buildUI(app) { - this._window = new Gtk.ApplicationWindow({ application: app, + _buildUI() { + this._window = new Gtk.ApplicationWindow({ application: this, window_position: Gtk.WindowPosition.CENTER }); this._window.set_default_size(800, 500); @@ -295,17 +293,19 @@ var Application = class { this._loaded = true; } - _onActivate() { + vfunc_activate() { this._window.present(); } - _onStartup(app) { - this._buildUI(app); + vfunc_startup() { + super.vfunc_startup(); + + this._buildUI(); this._scanExtensions(); } - _onCommandLine(app, commandLine) { - app.activate(); + vfunc_command_line(commandLine) { + this.activate(); let args = commandLine.get_arguments(); if (args.length) { @@ -325,7 +325,7 @@ var Application = class { } return 0; } -}; +}); var Expander = GObject.registerClass({ Properties: { @@ -631,6 +631,5 @@ function main(argv) { Gettext.bindtextdomain(Config.GETTEXT_PACKAGE, Config.LOCALEDIR); Gettext.textdomain(Config.GETTEXT_PACKAGE); - let app = new Application(); - app.application.run(argv); + new Application().run(argv); } -- 2.29.2 From ea202d21b65c027db03e773a51bd8fecf4a2fb0a Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Thu, 1 Nov 2018 13:50:30 +0100 Subject: [PATCH 09/26] extensionPrefs: Attach extension object to each row Each row represents an extension, so it makes sense to associate the rows with the actual extensions instead of linking rows and extensions by looking up the UUID in the external extensions map in ExtensionUtils. This will also make it much easier to stop using the shared extension loading / map in favor of the extension D-Bus API. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/extensionPrefs/main.js | 120 +++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js index 94a5b12fe..7e7b2dcc7 100644 --- a/js/extensionPrefs/main.js +++ b/js/extensionPrefs/main.js @@ -34,52 +34,31 @@ var Application = GObject.registerClass({ this._skipMainWindow = false; } - _extensionAvailable(uuid) { - let extension = ExtensionUtils.extensions[uuid]; - - if (!extension) - return false; + _showPrefs(uuid) { + let row = this._extensionSelector.get_children().find(c => { + return c.uuid === uuid && c.hasPrefs; + }); - if (!extension.dir.get_child('prefs.js').query_exists(null)) + if (!row) return false; - return true; - } - - _getExtensionPrefsModule(extension) { - let uuid = extension.metadata.uuid; - - if (this._extensionPrefsModules.hasOwnProperty(uuid)) - return this._extensionPrefsModules[uuid]; - - ExtensionUtils.installImporter(extension); - - let prefsModule = extension.imports.prefs; - prefsModule.init(extension.metadata); - - this._extensionPrefsModules[uuid] = prefsModule; - return prefsModule; - } - - _selectExtension(uuid) { - if (!this._extensionAvailable(uuid)) - return; - - let extension = ExtensionUtils.extensions[uuid]; let widget; try { - let prefsModule = this._getExtensionPrefsModule(extension); - widget = prefsModule.buildPrefsWidget(); + widget = row.prefsModule.buildPrefsWidget(); } catch (e) { - widget = this._buildErrorUI(extension, e); + widget = this._buildErrorUI(row, e); } - let dialog = new Gtk.Window({ modal: !this._skipMainWindow, - type_hint: Gdk.WindowTypeHint.DIALOG }); - dialog.set_titlebar(new Gtk.HeaderBar({ show_close_button: true, - title: extension.metadata.name, - visible: true })); + let dialog = new Gtk.Window({ + modal: !this._skipMainWindow, + type_hint: Gdk.WindowTypeHint.DIALOG + }); + dialog.set_titlebar(new Gtk.HeaderBar({ + show_close_button: true, + title: row.name, + visible: true + })); if (this._skipMainWindow) { this.add_window(dialog); @@ -96,7 +75,7 @@ var Application = GObject.registerClass({ dialog.show(); } - _buildErrorUI(extension, exc) { + _buildErrorUI(row, exc) { let scroll = new Gtk.ScrolledWindow({ hscrollbar_policy: Gtk.PolicyType.NEVER, propagate_natural_height: true @@ -183,13 +162,13 @@ var Application = GObject.registerClass({ label: _("Homepage"), tooltip_text: _("Visit extension homepage"), no_show_all: true, - visible: extension.metadata.url != null + visible: row.url != null }); toolbar.add(urlButton); urlButton.connect('clicked', w => { let context = w.get_display().get_app_launch_context(); - Gio.AppInfo.launch_default_for_uri(extension.metadata.url, context); + Gio.AppInfo.launch_default_for_uri(row.url, context); }); let expandedBox = new Gtk.Box({ @@ -248,9 +227,7 @@ var Application = GObject.registerClass({ } _sortList(row1, row2) { - let name1 = ExtensionUtils.extensions[row1.uuid].metadata.name; - let name2 = ExtensionUtils.extensions[row2.uuid].metadata.name; - return name1.localeCompare(name2); + return row1.name.localeCompare(row2.name); } _updateHeader(row, before) { @@ -269,11 +246,10 @@ var Application = GObject.registerClass({ } _extensionFound(finder, extension) { - let row = new ExtensionRow(extension.uuid); + let row = new ExtensionRow(extension); - row.prefsButton.visible = this._extensionAvailable(row.uuid); row.prefsButton.connect('clicked', () => { - this._selectExtension(row.uuid); + this._showPrefs(row.uuid); }); row.show_all(); @@ -286,8 +262,8 @@ var Application = GObject.registerClass({ else this._mainStack.visible_child_name = 'placeholder'; - if (this._startupUuid && this._extensionAvailable(this._startupUuid)) - this._selectExtension(this._startupUuid); + if (this._startupUuid) + this._showPrefs(this._startupUuid); this._startupUuid = null; this._skipMainWindow = false; this._loaded = true; @@ -316,11 +292,9 @@ var Application = GObject.registerClass({ // Strip off "extension:///" prefix which fakes a URI, if it exists uuid = stripPrefix(uuid, "extension:///"); - if (this._extensionAvailable(uuid)) - this._selectExtension(uuid); - else if (!this._loaded) + if (!this._loaded) this._startupUuid = uuid; - else + else if (!this._showPrefs(uuid)) this._skipMainWindow = false; } return 0; @@ -504,10 +478,11 @@ class DescriptionLabel extends Gtk.Label { var ExtensionRow = GObject.registerClass( class ExtensionRow extends Gtk.ListBoxRow { - _init(uuid) { + _init(extension) { super._init(); - this.uuid = uuid; + this._extension = extension; + this._prefsModule = null; this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); this._settings.connect('changed::enabled-extensions', () => { @@ -525,9 +500,23 @@ class ExtensionRow extends Gtk.ListBoxRow { this._buildUI(); } - _buildUI() { - let extension = ExtensionUtils.extensions[this.uuid]; + get uuid() { + return this._extension.uuid; + } + + get name() { + return this._extension.metadata.name; + } + + get hasPrefs() { + return this._extension.hasPrefs; + } + get url() { + return this._extension.metadata.url; + } + + _buildUI() { let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, hexpand: true, margin_end: 24, spacing: 24, margin: 12 }); @@ -537,19 +526,20 @@ class ExtensionRow extends Gtk.ListBoxRow { spacing: 6, hexpand: true }); hbox.add(vbox); - let name = GLib.markup_escape_text(extension.metadata.name, -1); + let name = GLib.markup_escape_text(this.name, -1); let label = new Gtk.Label({ label: '' + name + '', use_markup: true, halign: Gtk.Align.START }); vbox.add(label); - let desc = extension.metadata.description.split('\n')[0]; + let desc = this._extension.metadata.description.split('\n')[0]; label = new DescriptionLabel({ label: desc, wrap: true, lines: 2, ellipsize: Pango.EllipsizeMode.END, xalign: 0, yalign: 0 }); vbox.add(label); let button = new Gtk.Button({ valign: Gtk.Align.CENTER, + visible: this.hasPrefs, no_show_all: true }); button.set_image(new Gtk.Image({ icon_name: 'emblem-system-symbolic', icon_size: Gtk.IconSize.BUTTON, @@ -573,11 +563,10 @@ class ExtensionRow extends Gtk.ListBoxRow { } _canEnable() { - let extension = ExtensionUtils.extensions[this.uuid]; let checkVersion = !this._settings.get_boolean('disable-extension-version-validation'); return !this._settings.get_boolean('disable-user-extensions') && - !(checkVersion && ExtensionUtils.isOutOfDate(extension)); + !(checkVersion && ExtensionUtils.isOutOfDate(this._extension)); } _isEnabled() { @@ -605,6 +594,17 @@ class ExtensionRow extends Gtk.ListBoxRow { } while (pos != -1); this._settings.set_strv('enabled-extensions', extensions); } + + get prefsModule() { + if (!this._prefsModule) { + ExtensionUtils.installImporter(this._extension); + + this._prefsModule = this._extension.imports.prefs; + this._prefsModule.init(this._extension.metadata); + } + + return this._prefsModule; + } }); function initEnvironment() { -- 2.29.2 From 8d80e6667ded38dac53fe245a10191b5d4a3150b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Sat, 6 Jul 2019 01:48:05 +0200 Subject: [PATCH 10/26] extensionPrefs: Override getCurrentExtension() for extensions Extensions are used to calling the getCurrentExtension() utility function, both from the extension itself and from its preferences. For the latter, that relies on the extensions map in ExtensionUtils being populated from the separated extension-prefs process just like from gnome-shell. This won't be the case anymore when we switch to the extensions D-Bus API, but as we know which extension we are showing the prefs dialog for, we can patch in a simple replacement that gives extensions the expected API. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/extensionPrefs/main.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js index 7e7b2dcc7..29de8202a 100644 --- a/js/extensionPrefs/main.js +++ b/js/extensionPrefs/main.js @@ -599,6 +599,9 @@ class ExtensionRow extends Gtk.ListBoxRow { if (!this._prefsModule) { ExtensionUtils.installImporter(this._extension); + // give extension prefs access to their own extension object + ExtensionUtils.getCurrentExtension = () => this._extension; + this._prefsModule = this._extension.imports.prefs; this._prefsModule.init(this._extension.metadata); } -- 2.29.2 From e03a21b1f768405050bbfda1eb2bbf2ffcf7b4ca Mon Sep 17 00:00:00 2001 From: Didier Roche Date: Thu, 1 Nov 2018 13:55:17 +0100 Subject: [PATCH 11/26] extensionPrefs: Switch to D-Bus API to get extension live state By direclty using the underlying GSetting, whether or not an extension appears as enabled or disabled currently depends only on whether it is included in the 'enabled-extensions' list or not. However this doesn't necessarily reflect the real extension state, as an extension may be in error state, or enabled via the session mode. Switch to the extensions D-Bus API to ensure that the list of extensions and each extension's state correctly reflects the state in gnome-shell. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/extensionPrefs/main.js | 166 +++++++++++++++++++++++++------------- 1 file changed, 110 insertions(+), 56 deletions(-) diff --git a/js/extensionPrefs/main.js b/js/extensionPrefs/main.js index 29de8202a..f1b732e85 100644 --- a/js/extensionPrefs/main.js +++ b/js/extensionPrefs/main.js @@ -8,6 +8,8 @@ const Config = imports.misc.config; const ExtensionUtils = imports.misc.extensionUtils; const { loadInterfaceXML } = imports.misc.fileUtils; +const { ExtensionState } = ExtensionUtils; + const GnomeShellIface = loadInterfaceXML('org.gnome.Shell.Extensions'); const GnomeShellProxy = Gio.DBusProxy.makeProxyWrapper(GnomeShellIface); @@ -32,6 +34,11 @@ var Application = GObject.registerClass({ this._startupUuid = null; this._loaded = false; this._skipMainWindow = false; + this._shellProxy = null; + } + + get shellProxy() { + return this._shellProxy; } _showPrefs(uuid) { @@ -218,10 +225,8 @@ var Application = GObject.registerClass({ this._mainStack.add_named(new EmptyPlaceholder(), 'placeholder'); this._shellProxy = new GnomeShellProxy(Gio.DBus.session, 'org.gnome.Shell', '/org/gnome/Shell'); - this._shellProxy.connectSignal('ExtensionStateChanged', (proxy, senderName, [uuid, state]) => { - if (ExtensionUtils.extensions[uuid] !== undefined) - this._scanExtensions(); - }); + this._shellProxy.connectSignal('ExtensionStateChanged', + this._onExtensionStateChanged.bind(this)); this._window.show_all(); } @@ -238,14 +243,51 @@ var Application = GObject.registerClass({ row.set_header(sep); } + _findExtensionRow(uuid) { + return this._extensionSelector.get_children().find(c => c.uuid === uuid); + } + + _onExtensionStateChanged(proxy, senderName, [uuid, newState]) { + let row = this._findExtensionRow(uuid); + if (row) { + let { state } = ExtensionUtils.deserializeExtension(newState); + if (state == ExtensionState.UNINSTALLED) + row.destroy(); + return; // we only deal with new and deleted extensions here + } + + this._shellProxy.GetExtensionInfoRemote(uuid, ([serialized]) => { + let extension = ExtensionUtils.deserializeExtension(serialized); + if (!extension) + return; + // check the extension wasn't added in between + if (this._findExtensionRow(uuid) != null) + return; + this._addExtensionRow(extension); + }); + } + _scanExtensions() { - let finder = new ExtensionUtils.ExtensionFinder(); - finder.connect('extension-found', this._extensionFound.bind(this)); - finder.scanExtensions(); - this._extensionsLoaded(); + this._shellProxy.ListExtensionsRemote(([extensionsMap], e) => { + if (e) { + if (e instanceof Gio.DBusError) { + log(`Failed to connect to shell proxy: ${e}`); + this._mainStack.add_named(new NoShellPlaceholder(), 'noshell'); + this._mainStack.visible_child_name = 'noshell'; + } else + throw e; + return; + } + + for (let uuid in extensionsMap) { + let extension = ExtensionUtils.deserializeExtension(extensionsMap[uuid]); + this._addExtensionRow(extension); + } + this._extensionsLoaded(); + }); } - _extensionFound(finder, extension) { + _addExtensionRow(extension) { let row = new ExtensionRow(extension); row.prefsButton.connect('clicked', () => { @@ -466,6 +508,35 @@ class EmptyPlaceholder extends Gtk.Box { } }); +var NoShellPlaceholder = GObject.registerClass( +class NoShellPlaceholder extends Gtk.Box { + _init() { + super._init({ + orientation: Gtk.Orientation.VERTICAL, + spacing: 12, + margin: 100, + margin_bottom: 60 + }); + + let label = new Gtk.Label({ + label: '%s'.format( + _("Something’s gone wrong")), + use_markup: true + }); + label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + this.add(label); + + label = new Gtk.Label({ + label: _("We’re very sorry, but it was not possible to get the list of installed extensions. Make sure you are logged into GNOME and try again."), + justify: Gtk.Justification.CENTER, + wrap: true + }); + this.add(label); + + this.show_all(); + } +}); + var DescriptionLabel = GObject.registerClass( class DescriptionLabel extends Gtk.Label { vfunc_get_preferred_height_for_width(width) { @@ -481,22 +552,23 @@ class ExtensionRow extends Gtk.ListBoxRow { _init(extension) { super._init(); + this._app = Gio.Application.get_default(); this._extension = extension; this._prefsModule = null; - this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell' }); - this._settings.connect('changed::enabled-extensions', () => { - this._switch.state = this._isEnabled(); - }); - this._settings.connect('changed::disable-extension-version-validation', - () => { - this._switch.sensitive = this._canEnable(); - }); - this._settings.connect('changed::disable-user-extensions', - () => { - this._switch.sensitive = this._canEnable(); + this._extensionStateChangedId = this._app.shellProxy.connectSignal( + 'ExtensionStateChanged', (p, sender, [uuid, newState]) => { + if (this.uuid !== uuid) + return; + + this._extension = ExtensionUtils.deserializeExtension(newState); + let state = (this._extension.state == ExtensionState.ENABLED); + this._switch.state = state; + this._switch.sensitive = this._canToggle(); }); + this.connect('destroy', this._onDestroy.bind(this)); + this._buildUI(); } @@ -516,6 +588,15 @@ class ExtensionRow extends Gtk.ListBoxRow { return this._extension.metadata.url; } + _onDestroy() { + if (!this._app.shellProxy) + return; + + if (this._extensionStateChangedId) + this._app.shellProxy.disconnectSignal(this._extensionStateChangedId); + this._extensionStateChangedId = 0; + } + _buildUI() { let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, hexpand: true, margin_end: 24, spacing: 24, @@ -549,50 +630,23 @@ class ExtensionRow extends Gtk.ListBoxRow { this.prefsButton = button; - this._switch = new Gtk.Switch({ valign: Gtk.Align.CENTER, - sensitive: this._canEnable(), - state: this._isEnabled() }); + this._switch = new Gtk.Switch({ + valign: Gtk.Align.CENTER, + sensitive: this._canToggle(), + state: this._extension.state === ExtensionState.ENABLED + }); this._switch.connect('notify::active', () => { if (this._switch.active) - this._enable(); + this._app.shellProxy.EnableExtensionRemote(this.uuid); else - this._disable(); + this._app.shellProxy.DisableExtensionRemote(this.uuid); }); this._switch.connect('state-set', () => true); hbox.add(this._switch); } - _canEnable() { - let checkVersion = !this._settings.get_boolean('disable-extension-version-validation'); - - return !this._settings.get_boolean('disable-user-extensions') && - !(checkVersion && ExtensionUtils.isOutOfDate(this._extension)); - } - - _isEnabled() { - let extensions = this._settings.get_strv('enabled-extensions'); - return extensions.indexOf(this.uuid) != -1; - } - - _enable() { - let extensions = this._settings.get_strv('enabled-extensions'); - if (extensions.indexOf(this.uuid) != -1) - return; - - extensions.push(this.uuid); - this._settings.set_strv('enabled-extensions', extensions); - } - - _disable() { - let extensions = this._settings.get_strv('enabled-extensions'); - let pos = extensions.indexOf(this.uuid); - if (pos == -1) - return; - do { - extensions.splice(pos, 1); - pos = extensions.indexOf(this.uuid); - } while (pos != -1); - this._settings.set_strv('enabled-extensions', extensions); + _canToggle() { + return this._extension.canChange; } get prefsModule() { -- 2.29.2 From 9baf77dcae765618902d958549801276156f1255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Sun, 7 Jul 2019 23:38:27 +0200 Subject: [PATCH 12/26] extensionSystem: Move extension loading into ExtensionManager Now that extension loading and the extensions map are no longer shared between the gnome-shell and gnome-shell-extension-prefs processes, we can move both into the ExtensionManager which makes much more sense conceptually. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/misc/extensionUtils.js | 95 ++---------------------------- js/ui/extensionDownloader.js | 12 ++-- js/ui/extensionSystem.js | 110 +++++++++++++++++++++++++++++------ js/ui/lookingGlass.js | 4 +- js/ui/main.js | 1 + js/ui/shellDBus.js | 8 +-- 6 files changed, 111 insertions(+), 119 deletions(-) diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js index 025cd042e..c513ebc06 100644 --- a/js/misc/extensionUtils.js +++ b/js/misc/extensionUtils.js @@ -7,10 +7,8 @@ const { Gio, GLib } = imports.gi; const Gettext = imports.gettext; const Lang = imports.lang; -const Signals = imports.signals; const Config = imports.misc.config; -const FileUtils = imports.misc.fileUtils; var ExtensionType = { SYSTEM: 1, @@ -33,9 +31,6 @@ var ExtensionState = { const SERIALIZED_PROPERTIES = ['type', 'state', 'path', 'error', 'hasPrefs', 'canChange']; -// Maps uuid -> metadata object -var extensions = {}; - /** * getCurrentExtension: * @@ -66,13 +61,17 @@ function getCurrentExtension() { if (!match) return null; + // local import, as the module is used from outside the gnome-shell process + // as well (not this function though) + let extensionManager = imports.ui.main.extensionManager; + let path = match[1]; let file = Gio.File.new_for_path(path); // Walk up the directory tree, looking for an extension with // the same UUID as a directory name. while (file != null) { - let extension = extensions[file.get_basename()]; + let extension = extensionManager.extensions[file.get_basename()]; if (extension !== undefined) return extension; file = file.get_parent(); @@ -178,57 +177,6 @@ function isOutOfDate(extension) { return false; } -function createExtensionObject(uuid, dir, type) { - let info; - - let metadataFile = dir.get_child('metadata.json'); - if (!metadataFile.query_exists(null)) { - throw new Error('Missing metadata.json'); - } - - let metadataContents, success, tag; - try { - [success, metadataContents, tag] = metadataFile.load_contents(null); - if (metadataContents instanceof Uint8Array) - metadataContents = imports.byteArray.toString(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); - } - - let requiredProperties = ['uuid', 'name', 'description', 'shell-version']; - for (let i = 0; i < requiredProperties.length; i++) { - let prop = requiredProperties[i]; - if (!meta[prop]) { - throw new Error('missing "' + prop + '" property in metadata.json'); - } - } - - if (uuid != meta.uuid) { - throw new Error('uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + uuid + '"'); - } - - let extension = {}; - - extension.metadata = meta; - extension.uuid = meta.uuid; - extension.type = type; - extension.dir = dir; - extension.path = dir.get_path(); - extension.error = ''; - extension.hasPrefs = dir.get_child('prefs.js').query_exists(null); - extension.canChange = false; - - extensions[uuid] = extension; - - return extension; -} - function serializeExtension(extension) { let obj = {}; Lang.copyProperties(extension.metadata, obj); @@ -283,36 +231,3 @@ function installImporter(extension) { extension.imports = imports[extension.uuid]; imports.searchPath = oldSearchPath; } - -var ExtensionFinder = class { - _loadExtension(extensionDir, info, perUserDir) { - let fileType = info.get_file_type(); - if (fileType != Gio.FileType.DIRECTORY) - return; - let uuid = info.get_name(); - let existing = extensions[uuid]; - if (existing) { - log('Extension %s already installed in %s. %s will not be loaded'.format(uuid, existing.path, extensionDir.get_path())); - return; - } - - let extension; - let type = extensionDir.has_prefix(perUserDir) ? ExtensionType.PER_USER - : ExtensionType.SYSTEM; - try { - extension = createExtensionObject(uuid, extensionDir, type); - } catch(e) { - logError(e, 'Could not load extension %s'.format(uuid)); - return; - } - this.emit('extension-found', extension); - } - - scanExtensions() { - let perUserDir = Gio.File.new_for_path(global.userdatadir); - FileUtils.collectFromDatadirs('extensions', true, (dir, info) => { - this._loadExtension(dir, info, perUserDir); - }); - } -}; -Signals.addSignalMethods(ExtensionFinder.prototype); diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index de52edfa6..1d92a5740 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -43,7 +43,7 @@ function installExtension(uuid, invocation) { } function uninstallExtension(uuid) { - let extension = ExtensionUtils.extensions[uuid]; + let extension = Main.extensionManager.extensions[uuid]; if (!extension) return false; @@ -112,7 +112,7 @@ function updateExtension(uuid) { _httpSession.queue_message(message, (session, message) => { gotExtensionZipFile(session, message, uuid, newExtensionTmpDir, () => { - let oldExtension = ExtensionUtils.extensions[uuid]; + let oldExtension = Main.extensionManager.extensions[uuid]; let extensionDir = oldExtension.dir; if (!Main.extensionManager.unloadExtension(oldExtension)) @@ -124,7 +124,7 @@ function updateExtension(uuid) { let extension = null; try { - extension = ExtensionUtils.createExtensionObject(uuid, extensionDir, ExtensionUtils.ExtensionType.PER_USER); + extension = Main.extensionManager.createExtensionObject(uuid, extensionDir, ExtensionUtils.ExtensionType.PER_USER); Main.extensionManager.loadExtension(extension); } catch(e) { if (extension) @@ -150,8 +150,8 @@ function updateExtension(uuid) { function checkForUpdates() { let metadatas = {}; - for (let uuid in ExtensionUtils.extensions) { - metadatas[uuid] = ExtensionUtils.extensions[uuid].metadata; + for (let uuid in Main.extensionManager.extensions) { + metadatas[uuid] = Main.extensionManager.extensions[uuid].metadata; } let params = { shell_version: Config.PACKAGE_VERSION, @@ -229,7 +229,7 @@ class InstallExtensionDialog extends ModalDialog.ModalDialog { function callback() { try { - let extension = ExtensionUtils.createExtensionObject(uuid, dir, ExtensionUtils.ExtensionType.PER_USER); + let extension = Main.extensionManager.createExtensionObject(uuid, dir, ExtensionUtils.ExtensionType.PER_USER); Main.extensionManager.loadExtension(extension); if (!Main.extensionManager.enableExtension(uuid)) throw new Error(`Cannot add ${uuid} to enabled extensions gsettings key`); diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index a83e53c83..0fd49c5ca 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -4,9 +4,10 @@ const { Gio, St } = imports.gi; const Signals = imports.signals; const ExtensionUtils = imports.misc.extensionUtils; +const FileUtils = imports.misc.fileUtils; const Main = imports.ui.main; -const { ExtensionState } = ExtensionUtils; +const { ExtensionState, ExtensionType } = ExtensionUtils; const ENABLED_EXTENSIONS_KEY = 'enabled-extensions'; const DISABLE_USER_EXTENSIONS_KEY = 'disable-user-extensions'; @@ -17,15 +18,23 @@ var ExtensionManager = class { this._initted = false; this._enabled = false; + this._extensions = {}; this._enabledExtensions = []; this._extensionOrder = []; Main.sessionMode.connect('updated', this._sessionUpdated.bind(this)); + } + + init() { this._sessionUpdated(); } + get extensions() { + return this._extensions; + } + _callExtensionDisable(uuid) { - let extension = ExtensionUtils.extensions[uuid]; + let extension = this._extensions[uuid]; if (!extension) return; @@ -47,7 +56,7 @@ var ExtensionManager = class { for (let i = 0; i < orderReversed.length; i++) { let uuid = orderReversed[i]; try { - ExtensionUtils.extensions[uuid].stateObj.disable(); + this._extensions[uuid].stateObj.disable(); } catch (e) { this.logExtensionError(uuid, e); } @@ -68,7 +77,7 @@ var ExtensionManager = class { for (let i = 0; i < order.length; i++) { let uuid = order[i]; try { - ExtensionUtils.extensions[uuid].stateObj.enable(); + this._extensions[uuid].stateObj.enable(); } catch (e) { this.logExtensionError(uuid, e); } @@ -83,7 +92,7 @@ var ExtensionManager = class { } _callExtensionEnable(uuid) { - let extension = ExtensionUtils.extensions[uuid]; + let extension = this._extensions[uuid]; if (!extension) return; @@ -127,7 +136,7 @@ var ExtensionManager = class { } enableExtension(uuid) { - if (!ExtensionUtils.extensions[uuid]) + if (!this._extensions[uuid]) return false; let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY); @@ -140,7 +149,7 @@ var ExtensionManager = class { } disableExtension(uuid) { - if (!ExtensionUtils.extensions[uuid]) + if (!this._extensions[uuid]) return false; let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY); @@ -153,7 +162,7 @@ var ExtensionManager = class { } logExtensionError(uuid, error) { - let extension = ExtensionUtils.extensions[uuid]; + let extension = this._extensions[uuid]; if (!extension) return; @@ -169,6 +178,54 @@ var ExtensionManager = class { 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); + if (metadataContents instanceof Uint8Array) + metadataContents = imports.byteArray.toString(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}`); + } + + let requiredProperties = ['uuid', 'name', 'description', 'shell-version']; + for (let i = 0; i < requiredProperties.length; i++) { + let prop = requiredProperties[i]; + if (!meta[prop]) { + throw new Error(`missing "${prop}" property in metadata.json`); + } + } + + 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), + canChange: false + }; + this._extensions[uuid] = extension; + + return extension; + } + loadExtension(extension) { // Default to error, we set success as the last step extension.state = ExtensionState.ERROR; @@ -202,7 +259,7 @@ var ExtensionManager = class { extension.state = ExtensionState.UNINSTALLED; this.emit('extension-state-changed', extension); - delete ExtensionUtils.extensions[extension.uuid]; + delete this._extensions[extension.uuid]; return true; } @@ -217,7 +274,7 @@ var ExtensionManager = class { // Now, recreate the extension and load it. let newExtension; try { - newExtension = ExtensionUtils.createExtensionObject(uuid, dir, type); + newExtension = this.createExtensionObject(uuid, dir, type); } catch (e) { this.logExtensionError(uuid, e); return; @@ -227,7 +284,7 @@ var ExtensionManager = class { } _callExtensionInit(uuid) { - let extension = ExtensionUtils.extensions[uuid]; + let extension = this._extensions[uuid]; let dir = extension.dir; if (!extension) @@ -328,7 +385,7 @@ var ExtensionManager = class { } _onSettingsWritableChanged() { - for (let uuid in ExtensionUtils.extensions) { + for (let uuid in this._extensions) { let extension = ExtensionUtils.extensions[uuid]; this._updateCanChange(extension); this.emit('extension-state-changed', extension); @@ -340,8 +397,8 @@ var ExtensionManager = class { // extensions when allowed by the sessionMode, so // temporarily disable them all this._enabledExtensions = []; - for (let uuid in ExtensionUtils.extensions) - this.reloadExtension(ExtensionUtils.extensions[uuid]); + for (let uuid in this._extensions) + this.reloadExtension(this._extensions[uuid]); this._enabledExtensions = this._getEnabledExtensions(); if (Main.sessionMode.allowExtensions) { @@ -363,11 +420,30 @@ var ExtensionManager = class { this._enabledExtensions = this._getEnabledExtensions(); - let finder = new ExtensionUtils.ExtensionFinder(); - finder.connect('extension-found', (finder, extension) => { + let perUserDir = Gio.File.new_for_path(global.userdatadir); + FileUtils.collectFromDatadirs('extensions', true, (dir, info) => { + let fileType = info.get_file_type(); + if (fileType != Gio.FileType.DIRECTORY) + return; + let uuid = info.get_name(); + let existing = this._extensions[uuid]; + if (existing) { + log(`Extension ${uuid} already installed in ${existing.path}. ${dir.get_path()} will not be loaded`); + return; + } + + let extension; + let type = dir.has_prefix(perUserDir) + ? ExtensionType.PER_USER + : ExtensionType.SYSTEM; + try { + extension = this.createExtensionObject(uuid, dir, type); + } catch (e) { + logError(e, `Could not load extension ${uuid}`); + return; + } this.loadExtension(extension); }); - finder.scanExtensions(); } _enableAllExtensions() { diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js index e947574f2..b8f8b14c9 100644 --- a/js/ui/lookingGlass.js +++ b/js/ui/lookingGlass.js @@ -620,7 +620,7 @@ var Extensions = class Extensions { this._extensionsList.add(this._noExtensions); this.actor.add(this._extensionsList); - for (let uuid in ExtensionUtils.extensions) + for (let uuid in Main.extensionManager.extensions) this._loadExtension(null, uuid); Main.extensionManager.connect('extension-loaded', @@ -628,7 +628,7 @@ var Extensions = class Extensions { } _loadExtension(o, uuid) { - let extension = ExtensionUtils.extensions[uuid]; + let extension = Main.extensionManager.extensions[uuid]; // There can be cases where we create dummy extension metadata // that's not really a proper extension. Don't bother with these. if (!extension.metadata.name) diff --git a/js/ui/main.js b/js/ui/main.js index 7bfbce497..5fa5a8077 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -220,6 +220,7 @@ function _initializeUI() { ExtensionDownloader.init(); extensionManager = new ExtensionSystem.ExtensionManager(); + extensionManager.init(); if (sessionMode.isGreeter && screenShield) { layoutManager.connect('startup-prepared', () => { diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js index 23274c0a3..dc3a61df6 100644 --- a/js/ui/shellDBus.js +++ b/js/ui/shellDBus.js @@ -254,7 +254,7 @@ var GnomeShellExtensions = class { ListExtensions() { let out = {}; - for (let uuid in ExtensionUtils.extensions) { + for (let uuid in Main.extensionManager.extensions) { let dbusObj = this.GetExtensionInfo(uuid); out[uuid] = dbusObj; } @@ -262,12 +262,12 @@ var GnomeShellExtensions = class { } GetExtensionInfo(uuid) { - let extension = ExtensionUtils.extensions[uuid] || {}; + let extension = Main.extensionManager.extensions[uuid] || {}; return ExtensionUtils.serializeExtension(extension); } GetExtensionErrors(uuid) { - let extension = ExtensionUtils.extensions[uuid]; + let extension = Main.extensionManager.extensions[uuid]; if (!extension) return []; @@ -303,7 +303,7 @@ var GnomeShellExtensions = class { } ReloadExtension(uuid) { - let extension = ExtensionUtils.extensions[uuid]; + let extension = Main.extensionManager.extensions[uuid]; if (!extension) return; -- 2.29.2 From 776b82542aa705ea527dfbdd1a6d3fb1588092e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 8 Jul 2019 00:01:11 +0200 Subject: [PATCH 13/26] extensionSystem: Store extensions in a Map After making the extensions map private to the ExtensionManager, we can switch it to a proper hash table which is more appropriate. https://bugzilla.gnome.org/show_bug.cgi?id=789852 --- js/misc/extensionUtils.js | 2 +- js/ui/extensionDownloader.js | 8 +++---- js/ui/extensionSystem.js | 42 ++++++++++++++++++++---------------- js/ui/lookingGlass.js | 5 +++-- js/ui/shellDBus.js | 10 ++++----- 5 files changed, 37 insertions(+), 30 deletions(-) diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js index c513ebc06..62b25d46c 100644 --- a/js/misc/extensionUtils.js +++ b/js/misc/extensionUtils.js @@ -71,7 +71,7 @@ function getCurrentExtension() { // Walk up the directory tree, looking for an extension with // the same UUID as a directory name. while (file != null) { - let extension = extensionManager.extensions[file.get_basename()]; + let extension = extensionManager.lookup(file.get_basename()); if (extension !== undefined) return extension; file = file.get_parent(); diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index 1d92a5740..77d013ffb 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -43,7 +43,7 @@ function installExtension(uuid, invocation) { } function uninstallExtension(uuid) { - let extension = Main.extensionManager.extensions[uuid]; + let extension = Main.extensionManager.lookup(uuid); if (!extension) return false; @@ -112,7 +112,7 @@ function updateExtension(uuid) { _httpSession.queue_message(message, (session, message) => { gotExtensionZipFile(session, message, uuid, newExtensionTmpDir, () => { - let oldExtension = Main.extensionManager.extensions[uuid]; + let oldExtension = Main.extensionManager.lookup(uuid); let extensionDir = oldExtension.dir; if (!Main.extensionManager.unloadExtension(oldExtension)) @@ -150,9 +150,9 @@ function updateExtension(uuid) { function checkForUpdates() { let metadatas = {}; - for (let uuid in Main.extensionManager.extensions) { + Main.extensionManager.getUuids().forEach(uuid => { metadatas[uuid] = Main.extensionManager.extensions[uuid].metadata; - } + }); let params = { shell_version: Config.PACKAGE_VERSION, installed: JSON.stringify(metadatas) }; diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 0fd49c5ca..cd3e78301 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -18,7 +18,7 @@ var ExtensionManager = class { this._initted = false; this._enabled = false; - this._extensions = {}; + this._extensions = new Map(); this._enabledExtensions = []; this._extensionOrder = []; @@ -29,12 +29,16 @@ var ExtensionManager = class { this._sessionUpdated(); } - get extensions() { - return this._extensions; + lookup(uuid) { + return this._extensions.get(uuid); + } + + getUuids() { + return [...this._extensions.keys()]; } _callExtensionDisable(uuid) { - let extension = this._extensions[uuid]; + let extension = this.lookup(uuid); if (!extension) return; @@ -56,7 +60,7 @@ var ExtensionManager = class { for (let i = 0; i < orderReversed.length; i++) { let uuid = orderReversed[i]; try { - this._extensions[uuid].stateObj.disable(); + this.lookup(uuid).stateObj.disable(); } catch (e) { this.logExtensionError(uuid, e); } @@ -77,7 +81,7 @@ var ExtensionManager = class { for (let i = 0; i < order.length; i++) { let uuid = order[i]; try { - this._extensions[uuid].stateObj.enable(); + this.lookup(uuid).stateObj.enable(); } catch (e) { this.logExtensionError(uuid, e); } @@ -92,7 +96,7 @@ var ExtensionManager = class { } _callExtensionEnable(uuid) { - let extension = this._extensions[uuid]; + let extension = this.lookup(uuid); if (!extension) return; @@ -136,7 +140,7 @@ var ExtensionManager = class { } enableExtension(uuid) { - if (!this._extensions[uuid]) + if (!this._extensions.has(uuid)) return false; let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY); @@ -149,7 +153,7 @@ var ExtensionManager = class { } disableExtension(uuid) { - if (!this._extensions[uuid]) + if (!this._extensions.has(uuid)) return false; let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY); @@ -162,7 +166,7 @@ var ExtensionManager = class { } logExtensionError(uuid, error) { - let extension = this._extensions[uuid]; + let extension = this.lookup(uuid); if (!extension) return; @@ -221,7 +225,7 @@ var ExtensionManager = class { hasPrefs: dir.get_child('prefs.js').query_exists(null), canChange: false }; - this._extensions[uuid] = extension; + this._extensions.set(uuid, extension); return extension; } @@ -259,7 +263,7 @@ var ExtensionManager = class { extension.state = ExtensionState.UNINSTALLED; this.emit('extension-state-changed', extension); - delete this._extensions[extension.uuid]; + this._extensions.delete(extension.uuid); return true; } @@ -284,7 +288,7 @@ var ExtensionManager = class { } _callExtensionInit(uuid) { - let extension = this._extensions[uuid]; + let extension = this.lookup(uuid); let dir = extension.dir; if (!extension) @@ -385,8 +389,7 @@ var ExtensionManager = class { } _onSettingsWritableChanged() { - for (let uuid in this._extensions) { - let extension = ExtensionUtils.extensions[uuid]; + for (let extension of this._extensions.values()) { this._updateCanChange(extension); this.emit('extension-state-changed', extension); } @@ -397,8 +400,11 @@ var ExtensionManager = class { // extensions when allowed by the sessionMode, so // temporarily disable them all this._enabledExtensions = []; - for (let uuid in this._extensions) - this.reloadExtension(this._extensions[uuid]); + + // The loop modifies the extensions map, so iterate over a copy + let extensions = [...this._extensions.values()]; + for (let extension of extensions) + this.reloadExtension(extension); this._enabledExtensions = this._getEnabledExtensions(); if (Main.sessionMode.allowExtensions) { @@ -426,7 +432,7 @@ var ExtensionManager = class { if (fileType != Gio.FileType.DIRECTORY) return; let uuid = info.get_name(); - let existing = this._extensions[uuid]; + let existing = this.lookup(uuid); if (existing) { log(`Extension ${uuid} already installed in ${existing.path}. ${dir.get_path()} will not be loaded`); return; diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js index b8f8b14c9..9196959bd 100644 --- a/js/ui/lookingGlass.js +++ b/js/ui/lookingGlass.js @@ -620,15 +620,16 @@ var Extensions = class Extensions { this._extensionsList.add(this._noExtensions); this.actor.add(this._extensionsList); - for (let uuid in Main.extensionManager.extensions) + Main.extensionManager.getUuids().forEach(uuid => { this._loadExtension(null, uuid); + }); Main.extensionManager.connect('extension-loaded', this._loadExtension.bind(this)); } _loadExtension(o, uuid) { - let extension = Main.extensionManager.extensions[uuid]; + let extension = Main.extensionManager.lookup(uuid); // There can be cases where we create dummy extension metadata // that's not really a proper extension. Don't bother with these. if (!extension.metadata.name) diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js index dc3a61df6..be9b10491 100644 --- a/js/ui/shellDBus.js +++ b/js/ui/shellDBus.js @@ -254,20 +254,20 @@ var GnomeShellExtensions = class { ListExtensions() { let out = {}; - for (let uuid in Main.extensionManager.extensions) { + Main.extensionManager.getUuids().forEach(uuid => { let dbusObj = this.GetExtensionInfo(uuid); out[uuid] = dbusObj; - } + }); return out; } GetExtensionInfo(uuid) { - let extension = Main.extensionManager.extensions[uuid] || {}; + let extension = Main.extensionManager.lookup(uuid) || {}; return ExtensionUtils.serializeExtension(extension); } GetExtensionErrors(uuid) { - let extension = Main.extensionManager.extensions[uuid]; + let extension = Main.extensionManager.lookup(uuid); if (!extension) return []; @@ -303,7 +303,7 @@ var GnomeShellExtensions = class { } ReloadExtension(uuid) { - let extension = Main.extensionManager.extensions[uuid]; + let extension = Main.extensionManager.lookup(uuid); if (!extension) return; -- 2.29.2 From 2cc95ba7c93c04bae0006b7d018928600d9cbb13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 22 Jan 2020 14:45:15 +0100 Subject: [PATCH 14/26] extensionSystem: Add hasUpdate state The current support for extension updates is half-baked at best. We are about to change that, and implement offline updates similar to gnome-software. As a first step, add a hasUpdate property to the extension state which will communicate available updates. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 --- js/misc/extensionUtils.js | 10 +++++++++- js/ui/extensionSystem.js | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js index 62b25d46c..a812acdb1 100644 --- a/js/misc/extensionUtils.js +++ b/js/misc/extensionUtils.js @@ -29,7 +29,15 @@ var ExtensionState = { UNINSTALLED: 99 }; -const SERIALIZED_PROPERTIES = ['type', 'state', 'path', 'error', 'hasPrefs', 'canChange']; +const SERIALIZED_PROPERTIES = [ + 'type', + 'state', + 'path', + 'error', + 'hasPrefs', + 'hasUpdate', + 'canChange', +]; /** * getCurrentExtension: diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index cd3e78301..93faf48d4 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -165,6 +165,15 @@ var ExtensionManager = class { return true; } + notifyExtensionUpdate(uuid) { + let extension = this.lookup(uuid); + if (!extension) + return; + + extension.hasUpdate = true; + this.emit('extension-state-changed', extension); + } + logExtensionError(uuid, error) { let extension = this.lookup(uuid); if (!extension) @@ -223,6 +232,7 @@ var ExtensionManager = class { path: dir.get_path(), error: '', hasPrefs: dir.get_child('prefs.js').query_exists(null), + hasUpdate: false, canChange: false }; this._extensions.set(uuid, extension); -- 2.29.2 From 07330eaac64fc115851ec9d5a0969bd046599e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 22 Jan 2020 15:07:45 +0100 Subject: [PATCH 15/26] extensionDownloader: Make checkForUpdates() check for updates Currently the method installs updates instead of merely checking for them (or it would do, if it actually worked). This is not just surprising considering the method name, the whole idea of live updates is problematic and will not work properly more often than not: - imports are cached, so any local modules will stay at their original version until a shell restart - GTypes cannot be unregistered So change the method to only download available updates, and set the extensions' hasUpdate state accordingly. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 --- js/ui/extensionDownloader.js | 51 +++++++----------------------------- 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index 77d013ffb..66cb13d56 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -97,53 +97,20 @@ function gotExtensionZipFile(session, message, uuid, dir, callback, errback) { }); } -function updateExtension(uuid) { - // This gets a bit tricky. We want the update to be seamless - - // if we have any error during downloading or extracting, we - // want to not unload the current version. - - let oldExtensionTmpDir = GLib.Dir.make_tmp('XXXXXX-shell-extension'); - let newExtensionTmpDir = GLib.Dir.make_tmp('XXXXXX-shell-extension'); +function downloadExtensionUpdate(uuid) { + let dir = Gio.File.new_for_path( + GLib.build_filenamev([global.userdatadir, 'extension-updates', uuid])); let params = { shell_version: Config.PACKAGE_VERSION }; let url = REPOSITORY_URL_DOWNLOAD.format(uuid); let message = Soup.form_request_new_from_hash('GET', url, params); - _httpSession.queue_message(message, (session, message) => { - gotExtensionZipFile(session, message, uuid, newExtensionTmpDir, () => { - let oldExtension = Main.extensionManager.lookup(uuid); - let extensionDir = oldExtension.dir; - - if (!Main.extensionManager.unloadExtension(oldExtension)) - return; - - FileUtils.recursivelyMoveDir(extensionDir, oldExtensionTmpDir); - FileUtils.recursivelyMoveDir(newExtensionTmpDir, extensionDir); - - let extension = null; - - try { - extension = Main.extensionManager.createExtensionObject(uuid, extensionDir, ExtensionUtils.ExtensionType.PER_USER); - Main.extensionManager.loadExtension(extension); - } catch(e) { - if (extension) - Main.extensionManager.unloadExtension(extension); - - logError(e, 'Error loading extension %s'.format(uuid)); - - FileUtils.recursivelyDeleteDir(extensionDir, false); - FileUtils.recursivelyMoveDir(oldExtensionTmpDir, extensionDir); - - // Restore what was there before. We can't do much if we - // fail here. - Main.extensionManager.loadExtension(oldExtension); - return; - } - - FileUtils.recursivelyDeleteDir(oldExtensionTmpDir, true); - }, (code, message) => { - log('Error while updating extension %s: %s (%s)'.format(uuid, code, message ? message : '')); + _httpSession.queue_message(message, session => { + gotExtensionZipFile(session, message, uuid, dir, () => { + Main.extensionManager.notifyExtensionUpdate(uuid); + }, (code, msg) => { + log(`Error while downloading update for extension ${uuid}: ${code} (${msg})`); }); }); } @@ -169,7 +136,7 @@ function checkForUpdates() { if (operation == 'blacklist') uninstallExtension(uuid); else if (operation == 'upgrade' || operation == 'downgrade') - updateExtension(uuid); + downloadExtensionUpdate(uuid); } }); } -- 2.29.2 From 49eaf28202787f0802663aa609ee9f87eb548b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 22 Jan 2020 15:42:06 +0100 Subject: [PATCH 16/26] extensionDownloader: Only check updates for user extensions System extensions cannot be updated through the website, so don't even try. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 --- js/ui/extensionDownloader.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index 66cb13d56..c8f6735c5 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -118,7 +118,10 @@ function downloadExtensionUpdate(uuid) { function checkForUpdates() { let metadatas = {}; Main.extensionManager.getUuids().forEach(uuid => { - metadatas[uuid] = Main.extensionManager.extensions[uuid].metadata; + let extension = Main.extensionManager.lookup(uuid); + if (extension.type !== ExtensionUtils.ExtensionType.PER_USER) + return; + metadatas[uuid] = extension.metadata; }); let params = { shell_version: Config.PACKAGE_VERSION, -- 2.29.2 From 67d709de14b083a013b3b1160e5cc451cf96bfde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 27 Jan 2020 01:13:49 +0100 Subject: [PATCH 17/26] extensionDownloader: Exclude extensions with pending updates from check While it is possible that an extension has a newer version available than the previously downloaded update, it's more likely that we end up downloading the same archive again. That would be a bit silly despite the usually small size, so we can either use the metadata from the update, or exclude the extension from the check. The latter is much easier, so let's go with that for now. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 --- js/ui/extensionDownloader.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index c8f6735c5..ede276c37 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -121,6 +121,8 @@ function checkForUpdates() { let extension = Main.extensionManager.lookup(uuid); if (extension.type !== ExtensionUtils.ExtensionType.PER_USER) return; + if (extension.hasUpdate) + return; metadatas[uuid] = extension.metadata; }); -- 2.29.2 From 2fa19162c71787fbb9aa9af1d35e0e9cab11c1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 22 Jan 2020 16:53:32 +0100 Subject: [PATCH 18/26] extensionDownloader: Include version validation in update check The extensions website will consider the setting to find the best suitable extension version, so we should transmit the parameter for better results. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 --- js/ui/extensionDownloader.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index ede276c37..f957c6c62 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -126,8 +126,13 @@ function checkForUpdates() { metadatas[uuid] = extension.metadata; }); - let params = { shell_version: Config.PACKAGE_VERSION, - installed: JSON.stringify(metadatas) }; + let versionCheck = global.settings.get_boolean( + 'disable-extension-version-validation'); + let params = { + shell_version: Config.PACKAGE_VERSION, + installed: JSON.stringify(metadatas), + disable_version_validation: `${versionCheck}`, + }; let url = REPOSITORY_URL_UPDATE; let message = Soup.form_request_new_from_hash('GET', url, params); -- 2.29.2 From ccb0095b1981233ca980d44c260c0d36eef910bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Wed, 22 Jan 2020 15:09:05 +0100 Subject: [PATCH 19/26] extensionSystem: Install pending updates on startup Now that we have a way to check for updates and download them, we should actually apply them as well. Do this on startup before any extensions are initialized, to make sure we don't run into any conflicts with a previously loaded version. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/945 --- js/ui/extensionSystem.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 93faf48d4..36a248dc1 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -1,6 +1,6 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -const { Gio, St } = imports.gi; +const { GLib, Gio, St } = imports.gi; const Signals = imports.signals; const ExtensionUtils = imports.misc.extensionUtils; @@ -26,6 +26,7 @@ var ExtensionManager = class { } init() { + this._installExtensionUpdates(); this._sessionUpdated(); } @@ -424,6 +425,21 @@ var ExtensionManager = class { } } + _installExtensionUpdates() { + FileUtils.collectFromDatadirs('extension-updates', true, (dir, info) => { + let fileType = info.get_file_type(); + if (fileType !== Gio.FileType.DIRECTORY) + return; + let uuid = info.get_name(); + let extensionDir = Gio.File.new_for_path( + GLib.build_filenamev([global.userdatadir, 'extensions', uuid])); + + FileUtils.recursivelyDeleteDir(extensionDir, false); + FileUtils.recursivelyMoveDir(dir, extensionDir); + FileUtils.recursivelyDeleteDir(dir, true); + }); + } + _loadExtensions() { global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, this._onEnabledExtensionsChanged.bind(this)); -- 2.29.2 From e377b16ffb667be40a850ff03e092f2f9dfe8fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Fri, 24 Jan 2020 18:09:34 +0100 Subject: [PATCH 20/26] extensionPrefs: Add application icon We are about to make the tool a user-visible application, so we need an icon. Add one (plus its symbolic variant). https://gitlab.gnome.org/GNOME/gnome-shell/issues/1968 --- data/gnome-shell-extension-prefs.desktop.in.in | 1 + data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg | 7 +++++++ .../symbolic/apps/org.gnome.Extensions-symbolic.svg | 1 + data/icons/meson.build | 1 + data/meson.build | 1 + meson.build | 1 + 6 files changed, 12 insertions(+) create mode 100644 data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg create mode 100644 data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg create mode 100644 data/icons/meson.build diff --git a/data/gnome-shell-extension-prefs.desktop.in.in b/data/gnome-shell-extension-prefs.desktop.in.in index 1b144c5bd..1b58c424e 100644 --- a/data/gnome-shell-extension-prefs.desktop.in.in +++ b/data/gnome-shell-extension-prefs.desktop.in.in @@ -1,6 +1,7 @@ [Desktop Entry] Type=Application Name=Shell Extensions +Icon=org.gnome.Extensions Comment=Configure GNOME Shell Extensions Exec=@bindir@/gnome-shell-extension-prefs %u X-GNOME-Bugzilla-Bugzilla=GNOME diff --git a/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg b/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg new file mode 100644 index 000000000..49d63888b --- /dev/null +++ b/data/icons/hicolor/scalable/apps/org.gnome.Extensions.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg b/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg new file mode 100644 index 000000000..43786ff4a --- /dev/null +++ b/data/icons/hicolor/symbolic/apps/org.gnome.Extensions-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/meson.build b/data/icons/meson.build new file mode 100644 index 000000000..eff6e4b53 --- /dev/null +++ b/data/icons/meson.build @@ -0,0 +1 @@ +install_subdir('hicolor', install_dir: icondir) diff --git a/data/meson.build b/data/meson.build index 31ac4514e..33edb58c4 100644 --- a/data/meson.build +++ b/data/meson.build @@ -42,6 +42,7 @@ endforeach subdir('dbus-interfaces') +subdir('icons') subdir('theme') data_resources = [ diff --git a/meson.build b/meson.build index 0acaba705..2dd1bbc7a 100644 --- a/meson.build +++ b/meson.build @@ -52,6 +52,7 @@ pkglibdir = join_paths(libdir, meson.project_name()) autostartdir = join_paths(sysconfdir, 'xdg', 'autostart') convertdir = join_paths(datadir, 'GConf', 'gsettings') desktopdir = join_paths(datadir, 'applications') +icondir = join_paths(datadir, 'icons') ifacedir = join_paths(datadir, 'dbus-1', 'interfaces') localedir = join_paths(datadir, 'locale') portaldir = join_paths(datadir, 'xdg-desktop-portal', 'portals') -- 2.29.2 From 6b3fa1549f9682f54f55cdd963a242cd279ff17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Sun, 26 Jan 2020 23:47:24 +0100 Subject: [PATCH 21/26] extensionSystem: Show notification when updates are available Now that the extensions app has the ability to handle updates, we can use it as source of updates notifications. https://gitlab.gnome.org/GNOME/gnome-shell/issues/1968 --- js/ui/extensionSystem.js | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 36a248dc1..805e08cae 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -1,11 +1,12 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- -const { GLib, Gio, St } = imports.gi; +const { GLib, Gio, GObject, St } = imports.gi; const Signals = imports.signals; const ExtensionUtils = imports.misc.extensionUtils; const FileUtils = imports.misc.fileUtils; const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; const { ExtensionState, ExtensionType } = ExtensionUtils; @@ -17,6 +18,7 @@ var ExtensionManager = class { constructor() { this._initted = false; this._enabled = false; + this._updateNotified = false; this._extensions = new Map(); this._enabledExtensions = []; @@ -173,6 +175,18 @@ var ExtensionManager = class { 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.')); + source.notify(notification); + } } logExtensionError(uuid, error) { @@ -521,3 +535,26 @@ var ExtensionManager = class { } }; Signals.addSignalMethods(ExtensionManager.prototype); + +class ExtensionUpdateSource extends MessageTray.Source { + constructor() { + const appSys = Shell.AppSystem.get_default(); + this._app = appSys.lookup_app('gnome-shell-extension-prefs.desktop'); + + super(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(); + } +} -- 2.29.2 From f6a5e2731f487d7a0ac088aff53ca1e76006c118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 27 Jan 2020 00:59:19 +0100 Subject: [PATCH 22/26] extensionSystem: Periodically check for extension updates Now that we can download, apply and display extension updates, it is time to actually check for updates. Schedule an update check right on startup, then every 24 hours. https://gitlab.gnome.org/GNOME/gnome-shell/issues/1968 --- js/ui/extensionSystem.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 805e08cae..914abb309 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -3,6 +3,7 @@ const { GLib, Gio, GObject, St } = imports.gi; const Signals = imports.signals; +const ExtensionDownloader = imports.ui.extensionDownloader; const ExtensionUtils = imports.misc.extensionUtils; const FileUtils = imports.misc.fileUtils; const Main = imports.ui.main; @@ -14,6 +15,8 @@ const ENABLED_EXTENSIONS_KEY = 'enabled-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 { constructor() { this._initted = false; @@ -30,6 +33,12 @@ var ExtensionManager = class { init() { this._installExtensionUpdates(); this._sessionUpdated(); + + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => { + ExtensionDownloader.checkForUpdates(); + return GLib.SOURCE_CONTINUE; + }); + ExtensionDownloader.checkForUpdates(); } lookup(uuid) { -- 2.29.2 From cb3ed33a72fea3ae6b8df031abca48b99dba75a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 9 Mar 2020 16:49:34 +0100 Subject: [PATCH 23/26] extensionDownloader: Remove pending updates with extension When an extension is uninstalled, there is no point in keeping a pending update: If the update didn't fail (which it currently does), we would end up sneakily reinstalling the extension. https://gitlab.gnome.org/GNOME/gnome-shell/issues/2343 --- js/ui/extensionDownloader.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index f957c6c62..0bd77e125 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -55,6 +55,15 @@ function uninstallExtension(uuid) { return false; FileUtils.recursivelyDeleteDir(extension.dir, true); + + try { + const updatesDir = Gio.File.new_for_path(GLib.build_filenamev( + [global.userdatadir, 'extension-updates', extension.uuid])); + FileUtils.recursivelyDeleteDir(updatesDir, true); + } catch (e) { + // not an error + } + return true; } -- 2.29.2 From 6abb5a189a7c97de8c0ed28c40f34fb625363223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 9 Mar 2020 16:45:22 +0100 Subject: [PATCH 24/26] extensionSystem: Catch errors when updating extensions Extension updates are installed at startup, so any errors that bubble up uncaught will prevent the startup to complete. While the most likely error reason was addressed in the previous commit (pending update for a no-longer exitent extension), it makes sense to catch any kind of corrupt updates to not interfere with shell startup. https://gitlab.gnome.org/GNOME/gnome-shell/issues/2343 --- js/ui/extensionSystem.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 914abb309..320af54e4 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -457,9 +457,14 @@ var ExtensionManager = class { let extensionDir = Gio.File.new_for_path( GLib.build_filenamev([global.userdatadir, 'extensions', uuid])); - FileUtils.recursivelyDeleteDir(extensionDir, false); - FileUtils.recursivelyMoveDir(dir, extensionDir); - FileUtils.recursivelyDeleteDir(dir, true); + try { + FileUtils.recursivelyDeleteDir(extensionDir, false); + FileUtils.recursivelyMoveDir(dir, extensionDir); + } catch (e) { + log('Failed to install extension updates for %s'.format(uuid)); + } finally { + FileUtils.recursivelyDeleteDir(dir, true); + } }); } -- 2.29.2 From 405897a9930362dad590eb8bd425c130dc636083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Tue, 26 Jan 2021 17:12:04 +0100 Subject: [PATCH 25/26] extensionSystem: Fix opening Extensions app from notification Launching the app is implemented by the source's open() method, but only external notifications are hooked up to call into the source when no default action was provided. --- js/ui/extensionSystem.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js index 320af54e4..81804ea5e 100644 --- a/js/ui/extensionSystem.js +++ b/js/ui/extensionSystem.js @@ -194,6 +194,8 @@ var ExtensionManager = class { let notification = new MessageTray.Notification(source, _('Extension Updates Available'), _('Extension updates are ready to be installed.')); + notification.connect('activated', + () => source.open()); source.notify(notification); } } -- 2.29.2 From 8d50b96701eefa7f9bff4af8c855087eee35739a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 1 Feb 2021 18:26:00 +0100 Subject: [PATCH 26/26] extensionDownloader: Refuse to override system extensions The website allows to "update" system extensions by installing the upstream version into the user's home directory. Prevent that by refusing to download and install extensions that are already installed system-wide. --- js/ui/extensionDownloader.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js index 0bd77e125..1e6f5340a 100644 --- a/js/ui/extensionDownloader.js +++ b/js/ui/extensionDownloader.js @@ -16,6 +16,14 @@ var REPOSITORY_URL_UPDATE = REPOSITORY_URL_BASE + '/update-info/'; let _httpSession; function installExtension(uuid, invocation) { + const oldExt = Main.extensionManager.lookup(uuid); + if (oldExt && oldExt.type === ExtensionUtils.ExtensionType.SYSTEM) { + log('extensionDownloader: Trying to replace system extension %s'.format(uuid)); + invocation.return_dbus_error('org.gnome.Shell.InstallError', + 'System extensions cannot be replaced'); + return; + } + let params = { uuid: uuid, shell_version: Config.PACKAGE_VERSION }; -- 2.29.2