Move a lot of miscellaneous code related to extensions into a new module
ExtensionUtils is a new module that has a lot of miscellaneous things related to loading extensions and the extension system put into a place that does not depend on Shell or St. Note that this will break extensions that have with multiple files by replacing the old uuid-based importer with an object directly on the meta object. https://bugzilla.gnome.org/show_bug.cgi?id=668429
This commit is contained in:
parent
2f27b94757
commit
80ff6ff797
@ -9,6 +9,7 @@ nobase_dist_js_DATA = \
|
|||||||
gdm/powerMenu.js \
|
gdm/powerMenu.js \
|
||||||
misc/config.js \
|
misc/config.js \
|
||||||
misc/docInfo.js \
|
misc/docInfo.js \
|
||||||
|
misc/extensionUtils.js \
|
||||||
misc/fileUtils.js \
|
misc/fileUtils.js \
|
||||||
misc/format.js \
|
misc/format.js \
|
||||||
misc/gnomeSession.js \
|
misc/gnomeSession.js \
|
||||||
|
155
js/misc/extensionUtils.js
Normal file
155
js/misc/extensionUtils.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||||||
|
|
||||||
|
// Common utils for the extension system and the extension
|
||||||
|
// preferences tool
|
||||||
|
|
||||||
|
const GLib = imports.gi.GLib;
|
||||||
|
const Gio = imports.gi.Gio;
|
||||||
|
const ShellJS = imports.gi.ShellJS;
|
||||||
|
|
||||||
|
const Config = imports.misc.config;
|
||||||
|
|
||||||
|
const ExtensionType = {
|
||||||
|
SYSTEM: 1,
|
||||||
|
PER_USER: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// GFile for user extensions
|
||||||
|
var userExtensionsDir = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* versionCheck:
|
||||||
|
* @required: an array of versions we're compatible with
|
||||||
|
* @current: the version we have
|
||||||
|
*
|
||||||
|
* Check if a component is compatible for an extension.
|
||||||
|
* @required is an array, and at least one version must match.
|
||||||
|
* @current must be in the format <major>.<minor>.<point>.<micro>
|
||||||
|
* <micro> is always ignored
|
||||||
|
* <point> is ignored if <minor> is even (so you can target the
|
||||||
|
* whole stable release)
|
||||||
|
* <minor> and <major> must match
|
||||||
|
* Each target version must be at least <major> and <minor>
|
||||||
|
*/
|
||||||
|
function versionCheck(required, current) {
|
||||||
|
let currentArray = current.split('.');
|
||||||
|
let major = currentArray[0];
|
||||||
|
let minor = currentArray[1];
|
||||||
|
let point = currentArray[2];
|
||||||
|
for (let i = 0; i < required.length; i++) {
|
||||||
|
let requiredArray = required[i].split('.');
|
||||||
|
if (requiredArray[0] == major &&
|
||||||
|
requiredArray[1] == minor &&
|
||||||
|
(requiredArray[2] == point ||
|
||||||
|
(requiredArray[2] == undefined && parseInt(minor) % 2 == 0)))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOutOfDate(meta) {
|
||||||
|
if (!versionCheck(meta['shell-version'], Config.PACKAGE_VERSION))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (meta['js-version'] && !versionCheck(meta['js-version'], Config.GJS_VERSION))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMetadata(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);
|
||||||
|
} 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encourage people to add this
|
||||||
|
if (!meta.url) {
|
||||||
|
global.log('Warning: Missing "url" property in metadata.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uuid != meta.uuid) {
|
||||||
|
throw new Error('uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + uuid + '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.type = type;
|
||||||
|
meta.dir = dir;
|
||||||
|
meta.path = dir.get_path();
|
||||||
|
meta.error = '';
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
var _meta = null;
|
||||||
|
|
||||||
|
function installImporter(meta) {
|
||||||
|
_meta = meta;
|
||||||
|
ShellJS.add_extension_importer('imports.misc.extensionUtils._meta', 'importer', meta.path);
|
||||||
|
_meta = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
let userExtensionsPath = GLib.build_filenamev([global.userdatadir, 'extensions']);
|
||||||
|
userExtensionsDir = Gio.file_new_for_path(userExtensionsPath);
|
||||||
|
try {
|
||||||
|
if (!userExtensionsDir.query_exists(null))
|
||||||
|
userExtensionsDir.make_directory_with_parents(null);
|
||||||
|
} catch (e) {
|
||||||
|
global.logError('' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanExtensionsInDirectory(callback, dir, type) {
|
||||||
|
let fileEnum;
|
||||||
|
let file, info;
|
||||||
|
try {
|
||||||
|
fileEnum = dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
|
||||||
|
} catch(e) {
|
||||||
|
global.logError('' + e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((info = fileEnum.next_file(null)) != null) {
|
||||||
|
let fileType = info.get_file_type();
|
||||||
|
if (fileType != Gio.FileType.DIRECTORY)
|
||||||
|
continue;
|
||||||
|
let uuid = info.get_name();
|
||||||
|
let extensionDir = dir.get_child(uuid);
|
||||||
|
callback(uuid, extensionDir, type);
|
||||||
|
}
|
||||||
|
fileEnum.close(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanExtensions(callback) {
|
||||||
|
let systemDataDirs = GLib.get_system_data_dirs();
|
||||||
|
for (let i = 0; i < systemDataDirs.length; i++) {
|
||||||
|
let dirPath = GLib.build_filenamev([systemDataDirs[i], 'gnome-shell', 'extensions']);
|
||||||
|
let dir = Gio.file_new_for_path(dirPath);
|
||||||
|
if (dir.query_exists(null))
|
||||||
|
scanExtensionsInDirectory(callback, dir, ExtensionType.SYSTEM);
|
||||||
|
}
|
||||||
|
scanExtensionsInDirectory(callback, userExtensionsDir, ExtensionType.PER_USER);
|
||||||
|
}
|
@ -8,10 +8,10 @@ const GLib = imports.gi.GLib;
|
|||||||
const Gio = imports.gi.Gio;
|
const Gio = imports.gi.Gio;
|
||||||
const St = imports.gi.St;
|
const St = imports.gi.St;
|
||||||
const Shell = imports.gi.Shell;
|
const Shell = imports.gi.Shell;
|
||||||
const ShellJS = imports.gi.ShellJS;
|
|
||||||
const Soup = imports.gi.Soup;
|
const Soup = imports.gi.Soup;
|
||||||
|
|
||||||
const Config = imports.misc.config;
|
const Config = imports.misc.config;
|
||||||
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||||||
const FileUtils = imports.misc.fileUtils;
|
const FileUtils = imports.misc.fileUtils;
|
||||||
const ModalDialog = imports.ui.modalDialog;
|
const ModalDialog = imports.ui.modalDialog;
|
||||||
|
|
||||||
@ -30,11 +30,6 @@ const ExtensionState = {
|
|||||||
UNINSTALLED: 99
|
UNINSTALLED: 99
|
||||||
};
|
};
|
||||||
|
|
||||||
const ExtensionType = {
|
|
||||||
SYSTEM: 1,
|
|
||||||
PER_USER: 2
|
|
||||||
};
|
|
||||||
|
|
||||||
const REPOSITORY_URL_BASE = 'https://extensions.gnome.org';
|
const REPOSITORY_URL_BASE = 'https://extensions.gnome.org';
|
||||||
const REPOSITORY_URL_DOWNLOAD = REPOSITORY_URL_BASE + '/download-extension/%s.shell-extension.zip';
|
const REPOSITORY_URL_DOWNLOAD = REPOSITORY_URL_BASE + '/download-extension/%s.shell-extension.zip';
|
||||||
const REPOSITORY_URL_INFO = REPOSITORY_URL_BASE + '/extension-info/';
|
const REPOSITORY_URL_INFO = REPOSITORY_URL_BASE + '/extension-info/';
|
||||||
@ -60,8 +55,6 @@ _httpSession.ssl_ca_file = _getCertFile();
|
|||||||
|
|
||||||
// Maps uuid -> metadata object
|
// Maps uuid -> metadata object
|
||||||
const extensionMeta = {};
|
const extensionMeta = {};
|
||||||
// Maps uuid -> importer object (extension directory tree)
|
|
||||||
const extensions = {};
|
|
||||||
// Maps uuid -> extension state object (returned from init())
|
// Maps uuid -> extension state object (returned from init())
|
||||||
const extensionStateObjs = {};
|
const extensionStateObjs = {};
|
||||||
// Contains the order that extensions were enabled in.
|
// Contains the order that extensions were enabled in.
|
||||||
@ -69,8 +62,6 @@ const extensionOrder = [];
|
|||||||
|
|
||||||
// Arrays of uuids
|
// Arrays of uuids
|
||||||
var enabledExtensions;
|
var enabledExtensions;
|
||||||
// GFile for user extensions
|
|
||||||
var userExtensionsDir = null;
|
|
||||||
|
|
||||||
// We don't really have a class to add signals on. So, create
|
// We don't really have a class to add signals on. So, create
|
||||||
// a simple dummy object, add the signal methods, and export those
|
// a simple dummy object, add the signal methods, and export those
|
||||||
@ -86,36 +77,6 @@ var errors = {};
|
|||||||
|
|
||||||
const ENABLED_EXTENSIONS_KEY = 'enabled-extensions';
|
const ENABLED_EXTENSIONS_KEY = 'enabled-extensions';
|
||||||
|
|
||||||
/**
|
|
||||||
* versionCheck:
|
|
||||||
* @required: an array of versions we're compatible with
|
|
||||||
* @current: the version we have
|
|
||||||
*
|
|
||||||
* Check if a component is compatible for an extension.
|
|
||||||
* @required is an array, and at least one version must match.
|
|
||||||
* @current must be in the format <major>.<minor>.<point>.<micro>
|
|
||||||
* <micro> is always ignored
|
|
||||||
* <point> is ignored if <minor> is even (so you can target the
|
|
||||||
* whole stable release)
|
|
||||||
* <minor> and <major> must match
|
|
||||||
* Each target version must be at least <major> and <minor>
|
|
||||||
*/
|
|
||||||
function versionCheck(required, current) {
|
|
||||||
let currentArray = current.split('.');
|
|
||||||
let major = currentArray[0];
|
|
||||||
let minor = currentArray[1];
|
|
||||||
let point = currentArray[2];
|
|
||||||
for (let i = 0; i < required.length; i++) {
|
|
||||||
let requiredArray = required[i].split('.');
|
|
||||||
if (requiredArray[0] == major &&
|
|
||||||
requiredArray[1] == minor &&
|
|
||||||
(requiredArray[2] == point ||
|
|
||||||
(requiredArray[2] == undefined && parseInt(minor) % 2 == 0)))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function installExtensionFromUUID(uuid, version_tag) {
|
function installExtensionFromUUID(uuid, version_tag) {
|
||||||
let params = { uuid: uuid,
|
let params = { uuid: uuid,
|
||||||
version_tag: version_tag,
|
version_tag: version_tag,
|
||||||
@ -143,18 +104,13 @@ function uninstallExtensionFromUUID(uuid) {
|
|||||||
disableExtension(uuid);
|
disableExtension(uuid);
|
||||||
|
|
||||||
// Don't try to uninstall system extensions
|
// Don't try to uninstall system extensions
|
||||||
if (meta.type != ExtensionType.PER_USER)
|
if (meta.type != ExtensionUtils.ExtensionType.PER_USER)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
meta.state = ExtensionState.UNINSTALLED;
|
meta.state = ExtensionState.UNINSTALLED;
|
||||||
_signals.emit('extension-state-changed', meta);
|
_signals.emit('extension-state-changed', meta);
|
||||||
|
|
||||||
delete extensionMeta[uuid];
|
delete extensionMeta[uuid];
|
||||||
|
|
||||||
// Importers are marked as PERMANENT, so we can't do this.
|
|
||||||
// delete extensions[uuid];
|
|
||||||
extensions[uuid] = undefined;
|
|
||||||
|
|
||||||
delete extensionStateObjs[uuid];
|
delete extensionStateObjs[uuid];
|
||||||
delete errors[uuid];
|
delete errors[uuid];
|
||||||
|
|
||||||
@ -179,7 +135,7 @@ function gotExtensionZipFile(session, message, uuid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let stream = new Gio.UnixOutputStream({ fd: fd });
|
let stream = new Gio.UnixOutputStream({ fd: fd });
|
||||||
let dir = userExtensionsDir.get_child(uuid);
|
let dir = ExtensionUtils.userExtensionsDir.get_child(uuid);
|
||||||
Shell.write_soup_message_to_stream(stream, message);
|
Shell.write_soup_message_to_stream(stream, message);
|
||||||
stream.close(null);
|
stream.close(null);
|
||||||
let [success, pid] = GLib.spawn_async(null,
|
let [success, pid] = GLib.spawn_async(null,
|
||||||
@ -203,7 +159,7 @@ function gotExtensionZipFile(session, message, uuid) {
|
|||||||
global.settings.set_strv(ENABLED_EXTENSIONS_KEY, enabledExtensions);
|
global.settings.set_strv(ENABLED_EXTENSIONS_KEY, enabledExtensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadExtension(dir, ExtensionType.PER_USER, true);
|
loadExtension(dir, ExtensionUtils.ExtensionType.PER_USER, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,65 +255,26 @@ function logExtensionError(uuid, message, state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadExtension(dir, type, enabled) {
|
function loadExtension(dir, type, enabled) {
|
||||||
let info;
|
|
||||||
let uuid = dir.get_basename();
|
let uuid = dir.get_basename();
|
||||||
|
|
||||||
let metadataFile = dir.get_child('metadata.json');
|
|
||||||
if (!metadataFile.query_exists(null)) {
|
|
||||||
logExtensionError(uuid, 'Missing metadata.json');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadataContents;
|
|
||||||
try {
|
|
||||||
metadataContents = Shell.get_file_contents_utf8_sync(metadataFile.get_path());
|
|
||||||
} catch (e) {
|
|
||||||
logExtensionError(uuid, 'Failed to load metadata.json: ' + e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let meta;
|
let meta;
|
||||||
|
|
||||||
|
if (extensionMeta[uuid] != undefined) {
|
||||||
|
throw new Error('extension already loaded');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
meta = JSON.parse(metadataContents);
|
meta = ExtensionUtils.loadMetadata(uuid, dir, type);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
logExtensionError(uuid, 'Failed to parse metadata.json: ' + e);
|
logExtensionError(uuid, e.message);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let requiredProperties = ['uuid', 'name', 'description', 'shell-version'];
|
|
||||||
for (let i = 0; i < requiredProperties.length; i++) {
|
|
||||||
let prop = requiredProperties[i];
|
|
||||||
if (!meta[prop]) {
|
|
||||||
logExtensionError(uuid, 'missing "' + prop + '" property in metadata.json');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extensions[uuid] != undefined) {
|
|
||||||
logExtensionError(uuid, 'extension already loaded');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encourage people to add this
|
|
||||||
if (!meta['url']) {
|
|
||||||
global.log('Warning: Missing "url" property in metadata.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uuid != meta.uuid) {
|
|
||||||
logExtensionError(uuid, 'uuid "' + meta.uuid + '" from metadata.json does not match directory name "' + uuid + '"');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionMeta[uuid] = meta;
|
extensionMeta[uuid] = meta;
|
||||||
meta.type = type;
|
|
||||||
meta.dir = dir;
|
|
||||||
meta.path = dir.get_path();
|
|
||||||
meta.error = '';
|
|
||||||
|
|
||||||
// Default to error, we set success as the last step
|
// Default to error, we set success as the last step
|
||||||
meta.state = ExtensionState.ERROR;
|
meta.state = ExtensionState.ERROR;
|
||||||
|
|
||||||
if (!versionCheck(meta['shell-version'], Config.PACKAGE_VERSION) ||
|
if (ExtensionUtils.isOutOfDate(meta)) {
|
||||||
(meta['js-version'] && !versionCheck(meta['js-version'], Config.GJS_VERSION))) {
|
|
||||||
logExtensionError(uuid, 'extension is not compatible with current GNOME Shell and/or GJS version', ExtensionState.OUT_OF_DATE);
|
logExtensionError(uuid, 'extension is not compatible with current GNOME Shell and/or GJS version', ExtensionState.OUT_OF_DATE);
|
||||||
meta.state = ExtensionState.OUT_OF_DATE;
|
meta.state = ExtensionState.OUT_OF_DATE;
|
||||||
return;
|
return;
|
||||||
@ -389,8 +306,8 @@ function loadExtension(dir, type, enabled) {
|
|||||||
let extensionModule;
|
let extensionModule;
|
||||||
let extensionState = null;
|
let extensionState = null;
|
||||||
try {
|
try {
|
||||||
ShellJS.add_extension_importer('imports.ui.extensionSystem.extensions', meta.uuid, dir.get_path());
|
ExtensionUtils.installImporter(meta);
|
||||||
extensionModule = extensions[meta.uuid].extension;
|
extensionModule = meta.importer.extension;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (stylesheetPath != null)
|
if (stylesheetPath != null)
|
||||||
theme.unload_stylesheet(stylesheetPath);
|
theme.unload_stylesheet(stylesheetPath);
|
||||||
@ -457,50 +374,17 @@ function onEnabledExtensionsChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
let userExtensionsPath = GLib.build_filenamev([global.userdatadir, 'extensions']);
|
ExtensionUtils.init();
|
||||||
userExtensionsDir = Gio.file_new_for_path(userExtensionsPath);
|
|
||||||
try {
|
|
||||||
if (!userExtensionsDir.query_exists(null))
|
|
||||||
userExtensionsDir.make_directory_with_parents(null);
|
|
||||||
} catch (e) {
|
|
||||||
global.logError('' + e);
|
|
||||||
}
|
|
||||||
|
|
||||||
global.settings.connect('changed::' + ENABLED_EXTENSIONS_KEY, onEnabledExtensionsChanged);
|
global.settings.connect('changed::' + ENABLED_EXTENSIONS_KEY, onEnabledExtensionsChanged);
|
||||||
enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
|
enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _loadExtensionsIn(dir, type) {
|
|
||||||
let fileEnum;
|
|
||||||
let file, info;
|
|
||||||
try {
|
|
||||||
fileEnum = dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
|
|
||||||
} catch (e) {
|
|
||||||
global.logError('' + e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while ((info = fileEnum.next_file(null)) != null) {
|
|
||||||
let fileType = info.get_file_type();
|
|
||||||
if (fileType != Gio.FileType.DIRECTORY)
|
|
||||||
continue;
|
|
||||||
let name = info.get_name();
|
|
||||||
let child = dir.get_child(name);
|
|
||||||
let enabled = enabledExtensions.indexOf(name) != -1;
|
|
||||||
loadExtension(child, type, enabled);
|
|
||||||
}
|
|
||||||
fileEnum.close(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadExtensions() {
|
function loadExtensions() {
|
||||||
let systemDataDirs = GLib.get_system_data_dirs();
|
ExtensionUtils.scanExtensions(function(uuid, dir, type) {
|
||||||
for (let i = 0; i < systemDataDirs.length; i++) {
|
let enabled = enabledExtensions.indexOf(uuid) != -1;
|
||||||
let dirPath = systemDataDirs[i] + '/gnome-shell/extensions';
|
loadExtension(dir, type, enabled);
|
||||||
let dir = Gio.file_new_for_path(dirPath);
|
});
|
||||||
if (dir.query_exists(null))
|
|
||||||
_loadExtensionsIn(dir, ExtensionType.SYSTEM);
|
|
||||||
}
|
|
||||||
_loadExtensionsIn(userExtensionsDir, ExtensionType.PER_USER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const InstallExtensionDialog = new Lang.Class({
|
const InstallExtensionDialog = new Lang.Class({
|
||||||
|
Loading…
Reference in New Issue
Block a user