summaryrefslogtreecommitdiff
path: root/src/plugins/Plugins.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/Plugins.vala')
-rw-r--r--src/plugins/Plugins.vala436
1 files changed, 436 insertions, 0 deletions
diff --git a/src/plugins/Plugins.vala b/src/plugins/Plugins.vala
new file mode 100644
index 0000000..d0f9185
--- /dev/null
+++ b/src/plugins/Plugins.vala
@@ -0,0 +1,436 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+namespace Plugins {
+
+// GModule doesn't have a truly generic way to determine if a file is a shared library by extension,
+// so these are hard-coded
+private const string[] SHARED_LIB_EXTS = { "so", "la" };
+
+// Although not expecting this system to last very long, these ranges declare what versions of this
+// interface are supported by the current implementation.
+private const int MIN_SPIT_INTERFACE = 0;
+private const int MAX_SPIT_INTERFACE = 0;
+
+public class ExtensionPoint {
+ public GLib.Type pluggable_type { get; private set; }
+ // name is user-visible
+ public string name { get; private set; }
+ public string? icon_name { get; private set; }
+ public string[]? core_ids { get; private set; }
+
+ public ExtensionPoint(Type pluggable_type, string name, string? icon_name, string[]? core_ids) {
+ this.pluggable_type = pluggable_type;
+ this.name = name;
+ this.icon_name = icon_name;
+ this.core_ids = core_ids;
+ }
+}
+
+private class ModuleRep {
+ public File file;
+ public Module? module;
+ public Spit.Module? spit_module = null;
+ public int spit_interface = Spit.UNSUPPORTED_INTERFACE;
+ public string? id = null;
+
+ private ModuleRep(File file) {
+ this.file = file;
+
+ module = Module.open(file.get_path(), ModuleFlags.BIND_LAZY);
+ }
+
+ ~ModuleRep() {
+ // ensure that the Spit.Module is destroyed before the GLib.Module
+ spit_module = null;
+ }
+
+ // Have to use this funky static factory because GModule is a compact class and has no copy
+ // constructor. The handle must be kept open for the lifetime of the application (or until
+ // the module is ready to be discarded), as dropping the reference will unload the binary.
+ public static ModuleRep? open(File file) {
+ ModuleRep module_rep = new ModuleRep(file);
+
+ return (module_rep.module != null) ? module_rep : null;
+ }
+}
+
+private class PluggableRep {
+ public Spit.Pluggable pluggable { get; private set; }
+ public string id { get; private set; }
+ public bool is_core { get; private set; default = false; }
+ public bool activated { get; private set; default = false; }
+
+ private bool enabled = false;
+
+ // Note that creating a PluggableRep does not activate it.
+ public PluggableRep(Spit.Pluggable pluggable) {
+ this.pluggable = pluggable;
+ id = pluggable.get_id();
+ }
+
+ public void activate() {
+ // determine if a core pluggable (which is only known after all the extension points
+ // register themselves)
+ is_core = is_core_pluggable(pluggable);
+
+ FuzzyPropertyState saved_state = Config.Facade.get_instance().is_plugin_enabled(id);
+ enabled = ((is_core && (saved_state != FuzzyPropertyState.DISABLED)) ||
+ (!is_core && (saved_state == FuzzyPropertyState.ENABLED)));
+
+ // inform the plugin of its activation state
+ pluggable.activation(enabled);
+
+ activated = true;
+ }
+
+ public bool is_enabled() {
+ return enabled;
+ }
+
+ // Returns true if value changed, false otherwise
+ public bool set_enabled(bool enabled) {
+ if (enabled == this.enabled)
+ return false;
+
+ this.enabled = enabled;
+ Config.Facade.get_instance().set_plugin_enabled(id, enabled);
+ pluggable.activation(enabled);
+
+ return true;
+ }
+}
+
+private File[] search_dirs;
+private Gee.HashMap<string, ModuleRep> module_table;
+private Gee.HashMap<string, PluggableRep> pluggable_table;
+private Gee.HashMap<Type, ExtensionPoint> extension_points;
+private Gee.HashSet<string> core_ids;
+
+public void init() throws Error {
+ search_dirs = new File[0];
+ search_dirs += AppDirs.get_user_plugins_dir();
+ search_dirs += AppDirs.get_system_plugins_dir();
+
+ module_table = new Gee.HashMap<string, ModuleRep>();
+ pluggable_table = new Gee.HashMap<string, PluggableRep>();
+ extension_points = new Gee.HashMap<Type, ExtensionPoint>();
+ core_ids = new Gee.HashSet<string>();
+
+ // do this after constructing member variables so accessors don't blow up if GModule isn't
+ // supported
+ if (!Module.supported()) {
+ warning("Plugins not support: GModule not supported on this platform.");
+
+ return;
+ }
+
+ foreach (File dir in search_dirs) {
+ try {
+ search_for_plugins(dir);
+ } catch (Error err) {
+ debug("Unable to search directory %s for plugins: %s", dir.get_path(), err.message);
+ }
+ }
+}
+
+public void terminate() {
+ search_dirs = null;
+ pluggable_table = null;
+ module_table = null;
+ extension_points = null;
+ core_ids = null;
+}
+
+public class Notifier {
+ private static Notifier? instance = null;
+
+ public signal void pluggable_activation(Spit.Pluggable pluggable, bool enabled);
+
+ private Notifier() {
+ }
+
+ public static Notifier get_instance() {
+ if (instance == null)
+ instance = new Notifier();
+
+ return instance;
+ }
+}
+
+public void register_extension_point(Type type, string name, string? icon_name, string[]? core_ids) {
+ // if this assertion triggers, it means this extension point has already registered
+ assert(!extension_points.has_key(type));
+
+ extension_points.set(type, new ExtensionPoint(type, name, icon_name, core_ids));
+
+ // add core IDs to master list
+ if (core_ids != null) {
+ foreach (string core_id in core_ids)
+ Plugins.core_ids.add(core_id);
+ }
+
+ // activate all the pluggables for this extension point
+ foreach (PluggableRep pluggable_rep in pluggable_table.values) {
+ if (!pluggable_rep.pluggable.get_type().is_a(type))
+ continue;
+
+ pluggable_rep.activate();
+ Notifier.get_instance().pluggable_activation(pluggable_rep.pluggable, pluggable_rep.is_enabled());
+ }
+}
+
+public Gee.Collection<Spit.Pluggable> get_pluggables(bool include_disabled = false) {
+ Gee.Collection<Spit.Pluggable> all = new Gee.HashSet<Spit.Pluggable>();
+ foreach (PluggableRep pluggable_rep in pluggable_table.values) {
+ if (pluggable_rep.activated && (include_disabled || pluggable_rep.is_enabled()))
+ all.add(pluggable_rep.pluggable);
+ }
+
+ return all;
+}
+
+public bool is_core_pluggable(Spit.Pluggable pluggable) {
+ return core_ids.contains(pluggable.get_id());
+}
+
+private ModuleRep? get_module_for_pluggable(Spit.Pluggable needle) {
+ foreach (ModuleRep module_rep in module_table.values) {
+ Spit.Pluggable[]? pluggables = module_rep.spit_module.get_pluggables();
+ if (pluggables != null) {
+ foreach (Spit.Pluggable pluggable in pluggables) {
+ if (pluggable == needle)
+ return module_rep;
+ }
+ }
+ }
+
+ return null;
+}
+
+public string? get_pluggable_module_id(Spit.Pluggable needle) {
+ ModuleRep? module_rep = get_module_for_pluggable(needle);
+
+ return (module_rep != null) ? module_rep.spit_module.get_id() : null;
+}
+
+public Gee.Collection<ExtensionPoint> get_extension_points(owned CompareDataFunc? compare_func = null) {
+ Gee.Collection<ExtensionPoint> sorted = new Gee.TreeSet<ExtensionPoint>((owned) compare_func);
+ sorted.add_all(extension_points.values);
+
+ return sorted;
+}
+
+public Gee.Collection<Spit.Pluggable> get_pluggables_for_type(Type type,
+ owned CompareDataFunc? compare_func = null, bool include_disabled = false) {
+ // if this triggers it means the extension point didn't register itself at init() time
+ assert(extension_points.has_key(type));
+
+ Gee.Collection<Spit.Pluggable> for_type = new Gee.TreeSet<Spit.Pluggable>((owned) compare_func);
+ foreach (PluggableRep pluggable_rep in pluggable_table.values) {
+ if (pluggable_rep.activated
+ && pluggable_rep.pluggable.get_type().is_a(type)
+ && (include_disabled || pluggable_rep.is_enabled())) {
+ for_type.add(pluggable_rep.pluggable);
+ }
+ }
+
+ return for_type;
+}
+
+public string? get_pluggable_name(string id) {
+ PluggableRep? pluggable_rep = pluggable_table.get(id);
+
+ return (pluggable_rep != null && pluggable_rep.activated)
+ ? pluggable_rep.pluggable.get_pluggable_name() : null;
+}
+
+public bool get_pluggable_info(string id, ref Spit.PluggableInfo info) {
+ PluggableRep? pluggable_rep = pluggable_table.get(id);
+ if (pluggable_rep == null || !pluggable_rep.activated)
+ return false;
+
+ pluggable_rep.pluggable.get_info(ref info);
+
+ return true;
+}
+
+public bool get_pluggable_enabled(string id, out bool enabled) {
+ PluggableRep? pluggable_rep = pluggable_table.get(id);
+ if (pluggable_rep == null || !pluggable_rep.activated) {
+ enabled = false;
+
+ return false;
+ }
+
+ enabled = pluggable_rep.is_enabled();
+
+ return true;
+}
+
+public void set_pluggable_enabled(string id, bool enabled) {
+ PluggableRep? pluggable_rep = pluggable_table.get(id);
+ if (pluggable_rep == null || !pluggable_rep.activated)
+ return;
+
+ if (pluggable_rep.set_enabled(enabled))
+ Notifier.get_instance().pluggable_activation(pluggable_rep.pluggable, enabled);
+}
+
+public File get_pluggable_module_file(Spit.Pluggable pluggable) {
+ ModuleRep? module_rep = get_module_for_pluggable(pluggable);
+
+ return (module_rep != null) ? module_rep.file : null;
+}
+
+public int compare_pluggable_names(void *a, void *b) {
+ Spit.Pluggable *apluggable = (Spit.Pluggable *) a;
+ Spit.Pluggable *bpluggable = (Spit.Pluggable *) b;
+
+ return apluggable->get_pluggable_name().collate(bpluggable->get_pluggable_name());
+}
+
+public int compare_extension_point_names(void *a, void *b) {
+ ExtensionPoint *apoint = (ExtensionPoint *) a;
+ ExtensionPoint *bpoint = (ExtensionPoint *) b;
+
+ return apoint->name.collate(bpoint->name);
+}
+
+private bool is_shared_library(File file) {
+ string name, ext;
+ disassemble_filename(file.get_basename(), out name, out ext);
+
+ foreach (string shared_ext in SHARED_LIB_EXTS) {
+ if (ext == shared_ext)
+ return true;
+ }
+
+ return false;
+}
+
+private void search_for_plugins(File dir) throws Error {
+ debug("Searching %s for plugins ...", dir.get_path());
+
+ // build a set of module names sans file extension ... this is to deal with the question of
+ // .so vs. .la existing in the same directory (and letting GModule deal with the problem)
+ FileEnumerator enumerator = dir.enumerate_children(Util.FILE_ATTRIBUTES,
+ FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
+ for (;;) {
+ FileInfo? info = enumerator.next_file(null);
+ if (info == null)
+ break;
+
+ if (info.get_is_hidden())
+ continue;
+
+ File file = dir.get_child(info.get_name());
+
+ switch (info.get_file_type()) {
+ case FileType.DIRECTORY:
+ try {
+ search_for_plugins(file);
+ } catch (Error err) {
+ warning("Unable to search directory %s for plugins: %s", file.get_path(), err.message);
+ }
+ break;
+
+ case FileType.REGULAR:
+ if (is_shared_library(file))
+ load_module(file);
+ break;
+
+ default:
+ // ignored
+ break;
+ }
+ }
+}
+
+private void load_module(File file) {
+ ModuleRep? module_rep = ModuleRep.open(file);
+ if (module_rep == null) {
+ critical("Unable to load module %s: %s", file.get_path(), Module.error());
+
+ return;
+ }
+
+ // look for the well-known entry point
+ void *entry;
+ if (!module_rep.module.symbol(Spit.ENTRY_POINT_NAME, out entry)) {
+ critical("Unable to load module %s: well-known entry point %s not found", file.get_path(),
+ Spit.ENTRY_POINT_NAME);
+
+ return;
+ }
+
+ Spit.EntryPoint spit_entry_point = (Spit.EntryPoint) entry;
+
+ assert(MIN_SPIT_INTERFACE <= Spit.CURRENT_INTERFACE && Spit.CURRENT_INTERFACE <= MAX_SPIT_INTERFACE);
+ Spit.EntryPointParams params = Spit.EntryPointParams();
+ params.host_min_spit_interface = MIN_SPIT_INTERFACE;
+ params.host_max_spit_interface = MAX_SPIT_INTERFACE;
+ params.module_spit_interface = Spit.UNSUPPORTED_INTERFACE;
+ params.module_file = file;
+
+ module_rep.spit_module = spit_entry_point(&params);
+ if (params.module_spit_interface == Spit.UNSUPPORTED_INTERFACE) {
+ critical("Unable to load module %s: module reports no support for SPIT interfaces %d to %d",
+ file.get_path(), MIN_SPIT_INTERFACE, MAX_SPIT_INTERFACE);
+
+ return;
+ }
+
+ if (params.module_spit_interface < MIN_SPIT_INTERFACE || params.module_spit_interface > MAX_SPIT_INTERFACE) {
+ critical("Unable to load module %s: module reports unsupported SPIT version %d (out of range %d to %d)",
+ file.get_path(), module_rep.spit_interface, MIN_SPIT_INTERFACE, MAX_SPIT_INTERFACE);
+
+ return;
+ }
+
+ module_rep.spit_interface = params.module_spit_interface;
+
+ // verify type (as best as possible; still potential to segfault inside GType here)
+ if (!(module_rep.spit_module is Spit.Module))
+ module_rep.spit_module = null;
+
+ if (module_rep.spit_module == null) {
+ critical("Unable to load module %s (SPIT %d): no spit module returned", file.get_path(),
+ module_rep.spit_interface);
+
+ return;
+ }
+
+ // if module has already been loaded, drop this one (search path is set up to load user-installed
+ // binaries prior to system binaries)
+ module_rep.id = prepare_input_text(module_rep.spit_module.get_id(), PrepareInputTextOptions.DEFAULT, -1);
+ if (module_rep.id == null) {
+ critical("Unable to load module %s (SPIT %d): invalid or empty module name",
+ file.get_path(), module_rep.spit_interface);
+
+ return;
+ }
+
+ if (module_table.has_key(module_rep.id)) {
+ critical("Not loading module %s (SPIT %d): module with name \"%s\" already loaded",
+ file.get_path(), module_rep.spit_interface, module_rep.id);
+
+ return;
+ }
+
+ debug("Loaded SPIT module \"%s %s\" (%s) [%s]", module_rep.spit_module.get_module_name(),
+ module_rep.spit_module.get_version(), module_rep.id, file.get_path());
+
+ // stash in module table by their ID
+ module_table.set(module_rep.id, module_rep);
+
+ // stash pluggables in pluggable table by their ID
+ foreach (Spit.Pluggable pluggable in module_rep.spit_module.get_pluggables())
+ pluggable_table.set(pluggable.get_id(), new PluggableRep(pluggable));
+}
+
+}
+