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:
Jasper St. Pierre 2012-01-18 19:55:20 -05:00
parent 2f27b94757
commit 80ff6ff797
3 changed files with 176 additions and 136 deletions

View File

@ -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
View 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);
}

View File

@ -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({