From d008675b1bb1360f32360c98f54175783c5f0f2f Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 26 Aug 2015 23:58:09 +0200 Subject: [PATCH 01/12] plugins: introduce load and unload functions for plugins Currently they encapsulate loading and unloading of standard plugins. In the future plugins can provide such functions to load their types of plugins. Such a dummy proxy plugin is implemented now to load standard plugins so that these aren't going to be specially handled. --- src/plugindata.h | 9 ++ src/pluginprivate.h | 9 +- src/plugins.c | 237 ++++++++++++++++++++++++++++++-------------- 3 files changed, 177 insertions(+), 78 deletions(-) diff --git a/src/plugindata.h b/src/plugindata.h index 5e5a5b1fbe..989a8d6cae 100644 --- a/src/plugindata.h +++ b/src/plugindata.h @@ -347,6 +347,15 @@ void geany_plugin_set_data(GeanyPlugin *plugin, gpointer data, GDestroyNotify fr geany_plugin_register_full((plugin), GEANY_API_VERSION, \ (min_api_version), GEANY_ABI_VERSION, (pdata), (free_func)) +/* Hooks that need to be implemented for every proxy */ +typedef struct _GeanyProxyFuncs +{ + void (*load) (GeanyPlugin *proxy, GeanyPlugin *subplugins, const gchar *filename, gpointer pdata); + void (*unload) (GeanyPlugin *proxy, GeanyPlugin *subplugins, gpointer pdata); +} +GeanyProxyFuncs; + + /* Deprecated aliases */ #ifndef GEANY_DISABLE_DEPRECATED diff --git a/src/pluginprivate.h b/src/pluginprivate.h index 74a6a0615d..74509fa9ca 100644 --- a/src/pluginprivate.h +++ b/src/pluginprivate.h @@ -46,6 +46,8 @@ typedef enum _LoadedFlags { } LoadedFlags; +typedef struct GeanyPluginPrivate Plugin; /* shorter alias */ + typedef struct GeanyPluginPrivate { GModule *module; @@ -66,6 +68,10 @@ typedef struct GeanyPluginPrivate gpointer cb_data; /* user data passed back to functions in GeanyPluginFuncs */ GDestroyNotify cb_data_destroy; /* called when the plugin is unloaded, for cb_data */ LoadedFlags flags; /* bit-or of LoadedFlags */ + + /* proxy plugin support */ + GeanyProxyFuncs proxy_cbs; + Plugin *proxy; /* The proxy that handles this plugin */ } GeanyPluginPrivate; @@ -73,9 +79,6 @@ GeanyPluginPrivate; #define PLUGIN_IS_LEGACY(p) (((p)->flags & IS_LEGACY) != 0) #define PLUGIN_HAS_LOAD_DATA(p) (((p)->flags & LOAD_DATA) != 0) -typedef GeanyPluginPrivate Plugin; /* shorter alias */ - - void plugin_watch_object(Plugin *plugin, gpointer object); G_END_DECLS diff --git a/src/plugins.c b/src/plugins.c index bcc42e9b90..04cf86d91e 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -75,8 +75,31 @@ static GtkWidget *menu_separator = NULL; static gchar *get_plugin_path(void); static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data); -static GeanyData geany_data; +typedef struct { + gchar extension[8]; + Plugin *plugin; /* &builtin_so_proxy_plugin for native plugins */ +} PluginProxy; + + +static void plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, const gchar *filename, gpointer pdata); +static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer pdata); + +static Plugin builtin_so_proxy_plugin = { + .proxy_cbs = { + .load = plugin_load_gmodule, + .unload = plugin_unload_gmodule, + }, + /* rest of Plugin can be NULL/0 */ +}; + +static PluginProxy builtin_so_proxy = { + .extension = G_MODULE_SUFFIX, + .plugin = &builtin_so_proxy_plugin, +}; +static GPtrArray *active_proxies = NULL; + +static GeanyData geany_data; static void geany_data_init(void) @@ -105,16 +128,15 @@ geany_data_init(void) /* Prevent the same plugin filename being loaded more than once. * Note: g_module_name always returns the .so name, even when Plugin::filename is a .la file. */ static gboolean -plugin_loaded(GModule *module) +plugin_loaded(Plugin *plugin) { gchar *basename_module, *basename_loaded; GList *item; - basename_module = g_path_get_basename(g_module_name(module)); + basename_module = g_path_get_basename(plugin->filename); for (item = plugin_list; item != NULL; item = g_list_next(item)) { - basename_loaded = g_path_get_basename( - g_module_name(((Plugin*)item->data)->module)); + basename_loaded = g_path_get_basename(((Plugin*)item->data)->filename); if (utils_str_equal(basename_module, basename_loaded)) { @@ -131,7 +153,7 @@ plugin_loaded(GModule *module) * would cause a crash. */ for (item = active_plugin_list; item != NULL; item = g_list_next(item)) { - basename_loaded = g_path_get_basename(g_module_name(((Plugin*)item->data)->module)); + basename_loaded = g_path_get_basename(((Plugin*)item->data)->filename); if (utils_str_equal(basename_module, basename_loaded)) { @@ -168,19 +190,19 @@ static Plugin *find_active_plugin_by_name(const gchar *filename) static gboolean plugin_check_version(Plugin *plugin, int plugin_version_code) { - GModule *module = plugin->module; + const gchar *name = g_module_name(plugin->module); if (plugin_version_code < 0) { msgwin_status_add(_("The plugin \"%s\" is not binary compatible with this " - "release of Geany - please recompile it."), g_module_name(module)); + "release of Geany - please recompile it."), name); geany_debug("Plugin \"%s\" is not binary compatible with this " - "release of Geany - recompile it.", g_module_name(module)); + "release of Geany - recompile it.", name); return FALSE; } - if (plugin_version_code > GEANY_API_VERSION) + else if (plugin_version_code > GEANY_API_VERSION) { geany_debug("Plugin \"%s\" requires a newer version of Geany (API >= v%d).", - g_module_name(module), plugin_version_code); + name, plugin_version_code); return FALSE; } return TRUE; @@ -425,7 +447,7 @@ static void register_legacy_plugin(Plugin *plugin, GModule *module) if (! g_module_symbol(module, "plugin_" #__x, (void *) (&p_##__x))) \ { \ geany_debug("Plugin \"%s\" has no plugin_" #__x "() function - ignoring plugin!", \ - g_module_name(plugin->module)); \ + g_module_name(module)); \ return; \ } CHECK_FUNC(version_check); @@ -481,8 +503,9 @@ static gboolean plugin_load(Plugin *plugin) { gboolean init_ok = TRUE; + /* Start the plugin. Legacy plugins require additional cruft. */ - if (PLUGIN_IS_LEGACY(plugin)) + if (PLUGIN_IS_LEGACY(plugin) && plugin->proxy == &builtin_so_proxy_plugin) { GeanyPlugin **p_geany_plugin; PluginInfo **p_info; @@ -531,20 +554,70 @@ plugin_load(Plugin *plugin) } +static void plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *fname, gpointer pdata) +{ + GModule *module; + void (*p_geany_load_module)(GeanyPlugin *); + + g_return_val_if_fail(g_module_supported(), NULL); + /* Don't use G_MODULE_BIND_LAZY otherwise we can get unresolved symbols at runtime, + * causing a segfault. Without that flag the module will safely fail to load. + * G_MODULE_BIND_LOCAL also helps find undefined symbols e.g. app when it would + * otherwise not be detected due to the shadowing of Geany's app variable. + * Also without G_MODULE_BIND_LOCAL calling public functions e.g. the old info() + * function from a plugin will be shadowed. */ + module = g_module_open(fname, G_MODULE_BIND_LOCAL); + if (!module) + { + geany_debug("Can't load plugin: %s", g_module_error()); + return; + } + + subplugin->priv->module = module; + /*geany_debug("Initializing plugin '%s'", plugin->info.name);*/ + g_module_symbol(module, "geany_load_module", (void *) &p_geany_load_module); + if (p_geany_load_module) + { + /* This is a new style plugin. It should fill in plugin->info and then call + * geany_plugin_register() in its geany_load_module() to successfully load. + * The ABI and API checks are performed by geany_plugin_register() (i.e. by us). + * We check the LOADED_OK flag separately to protect us against buggy plugins + * who ignore the result of geany_plugin_register() and register anyway */ + p_geany_load_module(subplugin); + } + else + { + /* This is the legacy / deprecated code path. It does roughly the same as + * geany_load_module() and geany_plugin_register() together for the new ones */ + register_legacy_plugin(subplugin->priv, module); + } + /* We actually check the LOADED_OK flag later */ +} + + +static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer pdata) +{ + GModule *module = subplugin->priv->module; + + g_return_if_fail(module); + + if (! g_module_close(module)) + g_warning("%s: %s", subplugin->priv->filename, g_module_error()); +} + + /* Load and optionally init a plugin. * load_plugin decides whether the plugin's plugin_init() function should be called or not. If it is * called, the plugin will be started, if not the plugin will be read only (for the list of * available plugins in the plugin manager). * When add_to_list is set, the plugin will be added to the plugin manager's plugin_list. */ static Plugin* -plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list) +plugin_new(Plugin *proxy, const gchar *fname, gboolean load_plugin, gboolean add_to_list) { Plugin *plugin; - GModule *module; - void (*p_geany_load_module)(GeanyPlugin *); g_return_val_if_fail(fname, NULL); - g_return_val_if_fail(g_module_supported(), NULL); + g_return_val_if_fail(proxy, NULL); /* find the plugin in the list of already loaded, active plugins and use it, otherwise * load the module */ @@ -563,64 +636,37 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list) return plugin; } - /* Don't use G_MODULE_BIND_LAZY otherwise we can get unresolved symbols at runtime, - * causing a segfault. Without that flag the module will safely fail to load. - * G_MODULE_BIND_LOCAL also helps find undefined symbols e.g. app when it would - * otherwise not be detected due to the shadowing of Geany's app variable. - * Also without G_MODULE_BIND_LOCAL calling public functions e.g. the old info() - * function from a plugin will be shadowed. */ - module = g_module_open(fname, G_MODULE_BIND_LOCAL); - if (! module) - { - geany_debug("Can't load plugin: %s", g_module_error()); - return NULL; - } - - if (plugin_loaded(module)) - { - geany_debug("Plugin \"%s\" already loaded.", fname); - - if (! g_module_close(module)) - g_warning("%s: %s", fname, g_module_error()); - return NULL; - } - plugin = g_new0(Plugin, 1); - plugin->module = module; plugin->filename = g_strdup(fname); + plugin->proxy = proxy; plugin->public.geany_data = &geany_data; plugin->public.priv = plugin; /* Fields of plugin->info/funcs must to be initialized by the plugin */ plugin->public.info = &plugin->info; plugin->public.funcs = &plugin->cbs; - g_module_symbol(module, "geany_load_module", (void *) &p_geany_load_module); - if (p_geany_load_module) - { - /* This is a new style plugin. It should fill in plugin->info and then call - * geany_plugin_register() in its geany_load_module() to successfully load. - * The ABI and API checks are performed by geany_plugin_register() (i.e. by us). - * We check the LOADED_OK flag separately to protect us against buggy plugins - * who ignore the result of geany_plugin_register() and register anyway */ - p_geany_load_module(&plugin->public); - } - else + if (plugin_loaded(plugin)) { - /* This is the legacy / deprecated code path. It does roughly the same as - * geany_load_module() and geany_plugin_register() together for the new ones */ - register_legacy_plugin(plugin, module); + geany_debug("Plugin \"%s\" already loaded.", fname); + goto err; } + /* Load plugin, this should read its name etc. It must also call + * geany_plugin_register() for the following PLUGIN_LOADED_OK condition */ + proxy->proxy_cbs.load(&proxy->public, &plugin->public, fname, proxy->cb_data); + if (! PLUGIN_LOADED_OK(plugin)) { geany_debug("Failed to load \"%s\" - ignoring plugin!", fname); goto err; } + /* The proxy assumes success, therefore we have to call unload from here + * on in case of errors */ if (EMPTY(plugin->info.name)) { geany_debug("No plugin name set for \"%s\" - ignoring plugin!", fname); - goto err; + goto err_unload; } if (load_plugin && !plugin_load(plugin)) @@ -628,7 +674,7 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list) /* Handle failing init same as failing to load for now. In future we * could present a informational UI or something */ geany_debug("Plugin failed to initialize \"%s\" - ignoring plugin!", fname); - goto err; + goto err_unload; } if (add_to_list) @@ -636,11 +682,11 @@ plugin_new(const gchar *fname, gboolean load_plugin, gboolean add_to_list) return plugin; -err: +err_unload: if (plugin->cb_data_destroy) plugin->cb_data_destroy(plugin->cb_data); - if (! g_module_close(module)) - g_warning("%s: %s", fname, g_module_error()); + proxy->proxy_cbs.unload(&proxy->public, &plugin->public, proxy->cb_data); +err: g_free(plugin->filename); g_free(plugin); return NULL; @@ -754,21 +800,19 @@ plugin_cleanup(Plugin *plugin) static void plugin_free(Plugin *plugin) { + Plugin *proxy; + g_return_if_fail(plugin); - g_return_if_fail(plugin->module); + g_return_if_fail(plugin->proxy); + proxy = plugin->proxy; if (is_active_plugin(plugin)) plugin_cleanup(plugin); active_plugin_list = g_list_remove(active_plugin_list, plugin); plugin_list = g_list_remove(plugin_list, plugin); - /* cb_data_destroy might be plugin code and must be called before unloading the module */ - if (plugin->cb_data_destroy) - plugin->cb_data_destroy(plugin->cb_data); - - if (! g_module_close(plugin->module)) - g_warning("%s: %s", plugin->filename, g_module_error()); + proxy->proxy_cbs.unload(&proxy->public, &plugin->public, proxy->cb_data); g_free(plugin->filename); g_free(plugin); @@ -830,6 +874,36 @@ static gboolean check_plugin_path(const gchar *fname) } +/* Retuns NULL if this ain't a plugin, + * otherwise it returns the appropriate PluginProxy instance to load it */ +static PluginProxy* is_plugin(const gchar *file) +{ + PluginProxy *proxy; + const gchar *ext; + guint i; + + /* extract file extension to avoid g_str_has_suffix() in the loop */ + ext = (const gchar *)strrchr(file, '.'); + if (ext == NULL) + return FALSE; + /* ensure the dot is really part of the filename */ + else if (strchr(ext, G_DIR_SEPARATOR) != NULL) + return FALSE; + + ext += 1; + /* O(n*m), (m being extensions per proxy) doesn't scale very well in theory + * but not a problem in practice yet */ + foreach_ptr_array(proxy, i, active_proxies) + { + if (utils_str_casecmp(ext, proxy->extension) == 0) + { + return proxy; + } + } + return NULL; +} + + /* load active plugins at startup */ static void load_active_plugins(void) @@ -843,9 +917,19 @@ load_active_plugins(void) { const gchar *fname = active_plugins_pref[i]; +#ifdef G_OS_WIN32 + /* ensure we have canonical paths */ + gchar *p = fname; + while ((p = strchr(p, '/')) != NULL) + *p = G_DIR_SEPARATOR; +#endif + if (!EMPTY(fname) && g_file_test(fname, G_FILE_TEST_EXISTS)) { - if (!check_plugin_path(fname) || plugin_new(fname, TRUE, FALSE) == NULL) + PluginProxy *proxy = NULL; + if (check_plugin_path(fname)) + proxy = is_plugin(fname); + if (proxy == NULL || plugin_new(proxy->plugin, fname, TRUE, FALSE) == NULL) failed_plugins_list = g_list_prepend(failed_plugins_list, g_strdup(fname)); } } @@ -856,20 +940,18 @@ static void load_plugins_from_path(const gchar *path) { GSList *list, *item; - gchar *fname, *tmp; gint count = 0; list = utils_get_file_list(path, NULL, NULL); for (item = list; item != NULL; item = g_slist_next(item)) { - tmp = strrchr(item->data, '.'); - if (tmp == NULL || utils_str_casecmp(tmp, "." G_MODULE_SUFFIX) != 0) - continue; + gchar *fname = g_build_filename(path, item->data, NULL); + PluginProxy *proxy = is_plugin(fname); - fname = g_build_filename(path, item->data, NULL); - if (plugin_new(fname, FALSE, TRUE)) + if (proxy != NULL && plugin_new(proxy->plugin, fname, FALSE, TRUE)) count++; + g_free(fname); } @@ -1028,6 +1110,9 @@ void plugins_init(void) g_signal_connect(geany_object, "save-settings", G_CALLBACK(update_active_plugins_pref), NULL); stash_group_add_string_vector(group, &active_plugins_pref, "active_plugins", NULL); + + active_proxies = g_ptr_array_sized_new(1); + g_ptr_array_add(active_proxies, &builtin_so_proxy); } @@ -1146,6 +1231,7 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer GtkTreePath *path = gtk_tree_path_new_from_string(pth); GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(pm_widgets.tree)); Plugin *p; + Plugin *proxy; gtk_tree_model_get_iter(model, &iter, path); gtk_tree_path_free(path); @@ -1163,8 +1249,9 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer state = ! old_state; /* toggle the state */ - /* save the filename of the plugin */ + /* save the filename and proxy of the plugin */ file_name = g_strdup(p->filename); + proxy = p->proxy; /* unload plugin module */ if (!state) @@ -1174,7 +1261,7 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer plugin_free(p); /* reload plugin module and initialize it if item is checked */ - p = plugin_new(file_name, state, TRUE); + p = plugin_new(proxy, file_name, state, TRUE); if (!p) { /* plugin file may no longer be on disk, or is now incompatible */ From bdaab9c8378913016d8bc6c76d2100e6d6a9baab Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 26 Aug 2015 23:58:12 +0200 Subject: [PATCH 02/12] plugins: generic load_data instead of module pointer in Plugin struct Being a GModule is actually a detail of standard plugins. Future proxy plugins might need different handles. Therefore replace the module field with a more generic pointer and encapsulate the GModule detail further. This pointer shall be returned from GeanyProxyFuncs::load and is passed back to GeanyProxyFuncs::unload, and isn't interpreted by Geany. --- src/plugindata.h | 4 +- src/pluginprivate.h | 5 ++- src/plugins.c | 100 ++++++++++++++++++++++++++++++++++---------- src/pluginutils.c | 11 +---- 4 files changed, 86 insertions(+), 34 deletions(-) diff --git a/src/plugindata.h b/src/plugindata.h index 989a8d6cae..49bd45d899 100644 --- a/src/plugindata.h +++ b/src/plugindata.h @@ -350,8 +350,8 @@ void geany_plugin_set_data(GeanyPlugin *plugin, gpointer data, GDestroyNotify fr /* Hooks that need to be implemented for every proxy */ typedef struct _GeanyProxyFuncs { - void (*load) (GeanyPlugin *proxy, GeanyPlugin *subplugins, const gchar *filename, gpointer pdata); - void (*unload) (GeanyPlugin *proxy, GeanyPlugin *subplugins, gpointer pdata); + gpointer (*load) (GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *filename, gpointer pdata); + void (*unload) (GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata); } GeanyProxyFuncs; diff --git a/src/pluginprivate.h b/src/pluginprivate.h index 74509fa9ca..5368c4770c 100644 --- a/src/pluginprivate.h +++ b/src/pluginprivate.h @@ -50,7 +50,6 @@ typedef struct GeanyPluginPrivate Plugin; /* shorter alias */ typedef struct GeanyPluginPrivate { - GModule *module; gchar *filename; /* plugin filename (/path/libname.so) */ PluginInfo info; /* plugin name, description, etc */ GeanyPlugin public; /* fields the plugin can read */ @@ -72,6 +71,8 @@ typedef struct GeanyPluginPrivate /* proxy plugin support */ GeanyProxyFuncs proxy_cbs; Plugin *proxy; /* The proxy that handles this plugin */ + gpointer proxy_data; /* Data passed to the proxy hooks of above proxy, so + * this gives the proxy a pointer to each plugin */ } GeanyPluginPrivate; @@ -80,6 +81,8 @@ GeanyPluginPrivate; #define PLUGIN_HAS_LOAD_DATA(p) (((p)->flags & LOAD_DATA) != 0) void plugin_watch_object(Plugin *plugin, gpointer object); +void plugin_make_resident(Plugin *plugin); +gpointer plugin_get_module_symbol(Plugin *plugin, const gchar *sym); G_END_DECLS diff --git a/src/plugins.c b/src/plugins.c index 04cf86d91e..eed36454d2 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -81,8 +81,8 @@ typedef struct { } PluginProxy; -static void plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, const gchar *filename, gpointer pdata); -static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer pdata); +static gpointer plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, const gchar *filename, gpointer pdata); +static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata); static Plugin builtin_so_proxy_plugin = { .proxy_cbs = { @@ -190,22 +190,27 @@ static Plugin *find_active_plugin_by_name(const gchar *filename) static gboolean plugin_check_version(Plugin *plugin, int plugin_version_code) { - const gchar *name = g_module_name(plugin->module); + gboolean ret = TRUE; if (plugin_version_code < 0) { + gchar *name = g_path_get_basename(plugin->filename); msgwin_status_add(_("The plugin \"%s\" is not binary compatible with this " "release of Geany - please recompile it."), name); geany_debug("Plugin \"%s\" is not binary compatible with this " "release of Geany - recompile it.", name); - return FALSE; + ret = FALSE; + g_free(name); } else if (plugin_version_code > GEANY_API_VERSION) { + gchar *name = g_path_get_basename(plugin->filename); geany_debug("Plugin \"%s\" requires a newer version of Geany (API >= v%d).", name, plugin_version_code); - return FALSE; + ret = FALSE; + g_free(name); } - return TRUE; + + return ret; } @@ -239,9 +244,10 @@ static void read_key_group(Plugin *plugin) { GeanyKeyGroupInfo *p_key_info; GeanyKeyGroup **p_key_group; + GModule *module = plugin->proxy_data; - g_module_symbol(plugin->module, "plugin_key_group_info", (void *) &p_key_info); - g_module_symbol(plugin->module, "plugin_key_group", (void *) &p_key_group); + g_module_symbol(module, "plugin_key_group_info", (void *) &p_key_info); + g_module_symbol(module, "plugin_key_group", (void *) &p_key_group); if (p_key_info && p_key_group) { GeanyKeyGroupInfo *key_info = p_key_info; @@ -329,8 +335,10 @@ gboolean geany_plugin_register(GeanyPlugin *plugin, gint api_version, gint min_a /* Only init and cleanup callbacks are truly mandatory. */ if (! cbs->init || ! cbs->cleanup) { - geany_debug("Plugin '%s' has no %s function - ignoring plugin!", - g_module_name(p->module), cbs->init ? "cleanup" : "init"); + gchar *name = g_path_get_basename(p->filename); + geany_debug("Plugin '%s' has no %s function - ignoring plugin!", name, + cbs->init ? "cleanup" : "init"); + g_free(name); } else { @@ -510,15 +518,16 @@ plugin_load(Plugin *plugin) GeanyPlugin **p_geany_plugin; PluginInfo **p_info; PluginFields **plugin_fields; + GModule *module = plugin->proxy_data; /* set these symbols before plugin_init() is called * we don't set geany_data since it is set directly by plugin_new() */ - g_module_symbol(plugin->module, "geany_plugin", (void *) &p_geany_plugin); + g_module_symbol(module, "geany_plugin", (void *) &p_geany_plugin); if (p_geany_plugin) *p_geany_plugin = &plugin->public; - g_module_symbol(plugin->module, "plugin_info", (void *) &p_info); + g_module_symbol(module, "plugin_info", (void *) &p_info); if (p_info) *p_info = &plugin->info; - g_module_symbol(plugin->module, "plugin_fields", (void *) &plugin_fields); + g_module_symbol(module, "plugin_fields", (void *) &plugin_fields); if (plugin_fields) *plugin_fields = &plugin->fields; read_key_group(plugin); @@ -554,7 +563,7 @@ plugin_load(Plugin *plugin) } -static void plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *fname, gpointer pdata) +static gpointer plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *fname, gpointer pdata) { GModule *module; void (*p_geany_load_module)(GeanyPlugin *); @@ -570,10 +579,9 @@ static void plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, cons if (!module) { geany_debug("Can't load plugin: %s", g_module_error()); - return; + return NULL; } - subplugin->priv->module = module; /*geany_debug("Initializing plugin '%s'", plugin->info.name);*/ g_module_symbol(module, "geany_load_module", (void *) &p_geany_load_module); if (p_geany_load_module) @@ -592,14 +600,15 @@ static void plugin_load_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, cons register_legacy_plugin(subplugin->priv, module); } /* We actually check the LOADED_OK flag later */ + return module; } -static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer pdata) +static void plugin_unload_gmodule(GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata) { - GModule *module = subplugin->priv->module; + GModule *module = (GModule *) load_data; - g_return_if_fail(module); + g_return_if_fail(module != NULL); if (! g_module_close(module)) g_warning("%s: %s", subplugin->priv->filename, g_module_error()); @@ -653,7 +662,7 @@ plugin_new(Plugin *proxy, const gchar *fname, gboolean load_plugin, gboolean add /* Load plugin, this should read its name etc. It must also call * geany_plugin_register() for the following PLUGIN_LOADED_OK condition */ - proxy->proxy_cbs.load(&proxy->public, &plugin->public, fname, proxy->cb_data); + plugin->proxy_data = proxy->proxy_cbs.load(&proxy->public, &plugin->public, fname, proxy->cb_data); if (! PLUGIN_LOADED_OK(plugin)) { @@ -669,6 +678,16 @@ plugin_new(Plugin *proxy, const gchar *fname, gboolean load_plugin, gboolean add goto err_unload; } + /* cb_data_destroy() frees plugin->cb_data. If that pointer also passed to unload() afterwards + * then that would become a use-after-free. Disallow this combination. If a proxy + * needs the same pointer it must not use a destroy func but free manually in its unload(). */ + if (plugin->proxy_data == proxy->cb_data && plugin->cb_data_destroy) + { + geany_debug("Proxy of plugin \"%s\" specified invalid data - ignoring plugin!", fname); + plugin->proxy_data = NULL; + goto err_unload; + } + if (load_plugin && !plugin_load(plugin)) { /* Handle failing init same as failing to load for now. In future we @@ -685,7 +704,7 @@ plugin_new(Plugin *proxy, const gchar *fname, gboolean load_plugin, gboolean add err_unload: if (plugin->cb_data_destroy) plugin->cb_data_destroy(plugin->cb_data); - proxy->proxy_cbs.unload(&proxy->public, &plugin->public, proxy->cb_data); + proxy->proxy_cbs.unload(&proxy->public, &plugin->public, plugin->proxy_data, proxy->cb_data); err: g_free(plugin->filename); g_free(plugin); @@ -758,6 +777,40 @@ static void remove_sources(Plugin *plugin) } +/* Make the GModule backing plugin resident (if it's GModule-backed at all) */ +void plugin_make_resident(Plugin *plugin) +{ + if (plugin->proxy == &builtin_so_proxy_plugin) + { + g_return_if_fail(plugin->proxy_data != NULL); + g_module_make_resident(plugin->proxy_data); + } + else + g_warning("Skipping g_module_make_resident() for non-native plugin"); +} + + +/* Retrieve the address of a symbol sym located in plugin, if it's GModule-backed */ +gpointer plugin_get_module_symbol(Plugin *plugin, const gchar *sym) +{ + gpointer symbol; + + if (plugin->proxy == &builtin_so_proxy_plugin) + { + g_return_val_if_fail(plugin->proxy_data != NULL, NULL); + if (g_module_symbol(plugin->proxy_data, sym, &symbol)) + return symbol; + else + g_warning("Failed to locate signal handler for '%s': %s", + sym, g_module_error()); + } + else /* TODO: Could possibly support this via a new proxy hook */ + g_warning("Failed to locate signal handler for '%s': Not supported for non-native plugins", + sym); + return NULL; +} + + static gboolean is_active_plugin(Plugin *plugin) { return (g_list_find(active_plugin_list, plugin) != NULL); @@ -812,7 +865,10 @@ plugin_free(Plugin *plugin) active_plugin_list = g_list_remove(active_plugin_list, plugin); plugin_list = g_list_remove(plugin_list, plugin); - proxy->proxy_cbs.unload(&proxy->public, &plugin->public, proxy->cb_data); + /* cb_data_destroy might be plugin code and must be called before unloading the module. */ + if (plugin->cb_data_destroy) + plugin->cb_data_destroy(plugin->cb_data); + proxy->proxy_cbs.unload(&proxy->public, &plugin->public, plugin->proxy_data, proxy->cb_data); g_free(plugin->filename); g_free(plugin); diff --git a/src/pluginutils.c b/src/pluginutils.c index 782498c0b4..36856572e9 100644 --- a/src/pluginutils.c +++ b/src/pluginutils.c @@ -96,8 +96,7 @@ GEANY_API_SYMBOL void plugin_module_make_resident(GeanyPlugin *plugin) { g_return_if_fail(plugin); - - g_module_make_resident(plugin->priv->module); + plugin_make_resident(plugin->priv); } @@ -444,12 +443,7 @@ static void connect_plugin_signals(GtkBuilder *builder, GObject *object, gpointer symbol = NULL; struct BuilderConnectData *data = user_data; - if (!g_module_symbol(data->plugin->priv->module, handler_name, &symbol)) - { - g_warning("Failed to locate signal handler for '%s': %s", - signal_name, g_module_error()); - return; - } + symbol = plugin_get_module_symbol(data->plugin->priv, handler_name); plugin_signal_connect(data->plugin, object, signal_name, FALSE, G_CALLBACK(symbol) /*ub?*/, data->user_data); @@ -503,7 +497,6 @@ void plugin_builder_connect_signals(GeanyPlugin *plugin, struct BuilderConnectData data = { NULL }; g_return_if_fail(plugin != NULL && plugin->priv != NULL); - g_return_if_fail(plugin->priv->module != NULL); g_return_if_fail(GTK_IS_BUILDER(builder)); data.user_data = user_data; From 203644a23397a0f4bca56eff820bac547208561b Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 26 Aug 2015 23:58:15 +0200 Subject: [PATCH 03/12] plugins: refactor GtkListStore population code into separate function --- src/plugins.c | 57 +++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/plugins.c b/src/plugins.c index eed36454d2..6640e15a74 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -1278,6 +1278,9 @@ static void pm_selection_changed(GtkTreeSelection *selection, gpointer user_data } +static void pm_populate(GtkListStore *store); + + static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer data) { gboolean old_state, state; @@ -1339,6 +1342,33 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer g_free(file_name); } +static void pm_populate(GtkListStore *store) +{ + GtkTreeIter iter; + GList *list; + + gtk_list_store_clear(store); + list = g_list_first(plugin_list); + if (list == NULL) + { + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, PLUGIN_COLUMN_CHECK, FALSE, + PLUGIN_COLUMN_PLUGIN, NULL, -1); + } + else + { + for (; list != NULL; list = list->next) + { + Plugin *p = list->data; + + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, + PLUGIN_COLUMN_CHECK, is_active_plugin(p), + PLUGIN_COLUMN_PLUGIN, p, + -1); + } + } +} static gboolean pm_treeview_query_tooltip(GtkWidget *widget, gint x, gint y, gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data) @@ -1504,8 +1534,6 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store) GtkCellRenderer *text_renderer, *checkbox_renderer; GtkTreeViewColumn *column; GtkTreeModel *filter_model; - GtkTreeIter iter; - GList *list; GtkTreeSelection *sel; g_signal_connect(tree, "query-tooltip", G_CALLBACK(pm_treeview_query_tooltip), NULL); @@ -1539,27 +1567,6 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store) g_signal_connect(tree, "button-press-event", G_CALLBACK(pm_treeview_button_press_cb), NULL); - list = g_list_first(plugin_list); - if (list == NULL) - { - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, PLUGIN_COLUMN_CHECK, FALSE, - PLUGIN_COLUMN_PLUGIN, NULL, -1); - } - else - { - Plugin *p; - for (; list != NULL; list = list->next) - { - p = list->data; - - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, - PLUGIN_COLUMN_CHECK, is_active_plugin(p), - PLUGIN_COLUMN_PLUGIN, p, - -1); - } - } /* filter */ filter_model = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL); gtk_tree_model_filter_set_visible_func( @@ -1567,8 +1574,9 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store) /* set model to tree view */ gtk_tree_view_set_model(GTK_TREE_VIEW(tree), filter_model); - g_object_unref(store); g_object_unref(filter_model); + + pm_populate(store); } @@ -1679,6 +1687,7 @@ static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data) pm_widgets.store = gtk_list_store_new( PLUGIN_N_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_POINTER); pm_prepare_treeview(pm_widgets.tree, pm_widgets.store); + g_object_unref(pm_widgets.store); swin = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(swin), From e5bb6571c60be2169c3d66ae38c72d72709bf10e Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 26 Aug 2015 23:58:18 +0200 Subject: [PATCH 04/12] plugins: when loading active ones, loop until no more proxy plugins are added During the loading of the active plugins they are also initialized (done at startup). As a result, these plugins could be pluxys and make more plugins available, some of which may be active as well. Because of this the loop has to be restarted if pluxies become available to also load active plugins that depend on the pluxy. The loop is only restarted at the end so only nested pluxys could possibly cause the loop to be run more than twice. --- src/plugins.c | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/plugins.c b/src/plugins.c index 6640e15a74..43e72ed793 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -964,31 +964,38 @@ static PluginProxy* is_plugin(const gchar *file) static void load_active_plugins(void) { - guint i, len; + guint i, len, proxies; if (active_plugins_pref == NULL || (len = g_strv_length(active_plugins_pref)) == 0) return; - for (i = 0; i < len; i++) + /* If proxys are loaded we have to restart to load plugins that sort before their proxy */ + do { - const gchar *fname = active_plugins_pref[i]; + proxies = active_proxies->len; + g_list_free_full(failed_plugins_list, (GDestroyNotify) g_free); + failed_plugins_list = NULL; + for (i = 0; i < len; i++) + { + gchar *fname = active_plugins_pref[i]; #ifdef G_OS_WIN32 - /* ensure we have canonical paths */ - gchar *p = fname; - while ((p = strchr(p, '/')) != NULL) - *p = G_DIR_SEPARATOR; + /* ensure we have canonical paths */ + gchar *p = fname; + while ((p = strchr(p, '/')) != NULL) + *p = G_DIR_SEPARATOR; #endif - if (!EMPTY(fname) && g_file_test(fname, G_FILE_TEST_EXISTS)) - { - PluginProxy *proxy = NULL; - if (check_plugin_path(fname)) - proxy = is_plugin(fname); - if (proxy == NULL || plugin_new(proxy->plugin, fname, TRUE, FALSE) == NULL) - failed_plugins_list = g_list_prepend(failed_plugins_list, g_strdup(fname)); + if (!EMPTY(fname) && g_file_test(fname, G_FILE_TEST_EXISTS)) + { + PluginProxy *proxy = NULL; + if (check_plugin_path(fname)) + proxy = is_plugin(fname); + if (proxy == NULL || plugin_new(proxy->plugin, fname, TRUE, FALSE) == NULL) + failed_plugins_list = g_list_prepend(failed_plugins_list, g_strdup(fname)); + } } - } + } while (proxies != active_proxies->len); } From 3ccf959013edb6744ab58f6953a2b44dfb1abef3 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 26 Aug 2015 23:58:21 +0200 Subject: [PATCH 05/12] plugins: introduce probe() for proxy plugins When a file extension alone is ambigious as to whether a potential plugin is really handled then the proxy should use the probe hook to find out. This can be especially helpful when two pluxies work on the same file extension. The proxy's probe() should return PROXY_IGNORED or PROXY_MATCHED accordingly. A special flag value, PROXY_NOLOAD, can be or'ed into PROXY_MATCHED to say that the file belongs to the proxy, but isn't directly loaded and should not be handled by any other proxy or geany itself. Example for PROXY_IGNORED: geanypy only supports python2 at the moment. So, scripts written for python3 aren't handled by it and should be skipped for the PM dialog. Or perhaps they are handled by another proxy that supports python3. Example for PROXY_NOLOAD: A pluxy registers for the metadata file extension (.plugin) where author etc is in. The actual implmentation is in a python script (.py). The .py file is tied to the .plugin and should not be processed by other pluxies. Thus, the pluxy also registers for the .py extension but returns PROXY_MATCHED|PROXY_NOLOAD for it (if it would return only PROXY_MATCHED the sub-plugin would show up twice in the PM dialog). --- src/plugindata.h | 10 ++++++++++ src/plugins.c | 17 ++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/plugindata.h b/src/plugindata.h index 49bd45d899..a889751333 100644 --- a/src/plugindata.h +++ b/src/plugindata.h @@ -347,9 +347,19 @@ void geany_plugin_set_data(GeanyPlugin *plugin, gpointer data, GDestroyNotify fr geany_plugin_register_full((plugin), GEANY_API_VERSION, \ (min_api_version), GEANY_ABI_VERSION, (pdata), (free_func)) +typedef enum +{ + PROXY_IGNORED, + PROXY_MATCHED, + + PROXY_NOLOAD = 0x100, +} +GeanyProxyProbeResults; + /* Hooks that need to be implemented for every proxy */ typedef struct _GeanyProxyFuncs { + gint (*probe) (GeanyPlugin *proxy, const gchar *filename, gpointer pdata); gpointer (*load) (GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *filename, gpointer pdata); void (*unload) (GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata); } diff --git a/src/plugins.c b/src/plugins.c index 43e72ed793..793107425c 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -953,7 +953,22 @@ static PluginProxy* is_plugin(const gchar *file) { if (utils_str_casecmp(ext, proxy->extension) == 0) { - return proxy; + Plugin *p = proxy->plugin; + gint ret = PROXY_MATCHED; + + if (p->proxy_cbs.probe) + ret = p->proxy_cbs.probe(&p->public, file, p->cb_data); + switch (ret) + { + case PROXY_MATCHED: + return proxy; + case PROXY_MATCHED|PROXY_NOLOAD: + return NULL; + default: + if (ret != PROXY_IGNORED) + g_warning("Ignoring bogus return from proxy probe!\n"); + continue; + } } } return NULL; From 6e5ca69e2e9ddbfc2cc44527a1aa5fca8db09167 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 26 Aug 2015 23:58:25 +0200 Subject: [PATCH 06/12] plugins: add geany_plugin_register_proxy() to the plugin API This function finally allows plugins to register themselves as a proxy for one or more file extensions. Lots of documentation is added to doc/plugins.dox, please refer to that for more details. --- doc/plugins.dox | 397 +++++++++++++++++++++++++++++++++++++++++++++++ src/plugindata.h | 43 ++++- src/plugins.c | 52 +++++++ 3 files changed, 487 insertions(+), 5 deletions(-) diff --git a/doc/plugins.dox b/doc/plugins.dox index c58081dcb6..bdf70d0c80 100644 --- a/doc/plugins.dox +++ b/doc/plugins.dox @@ -43,6 +43,7 @@ GeanyFuncs::cleanup functions). @section pluginsupport Plugin Support - @link howto Plugin HowTo @endlink - get started +- @ref proxy - @ref legacy - @link plugindata.h Plugin Datatypes and Macros @endlink - @link pluginsignals.c Plugin Signals @endlink @@ -725,4 +726,400 @@ void geany_load_module(GeanyPlugin *plugin) @endcode +@page proxy Proxy Plugin HowTo + +@section proxy_intro Introduction + +Geany has built-in support for plugins. These plugins can alter the way Geany operates in many +imaginable ways which leaves little to be desired. + +However, there is one significant short-coming. Due to the infrastructure, Geany's built-in support +only covers plugins written in C, perhaps C++ and Vala. Basically all languages which can be +compiled into native shared libraries and can link GTK libraries. This excludes dynamic languages +such as Python. + +Geany provides a mechanism to enable support for those languages. Native plugins can register as +proxy plugins by being a normal plugin to the Geany-side and by providing a bridge to write plugins +in another language on the other side. + +These plugins are also called sub-plugins. This refers to the relation to their proxy. +To Geany they are first-class citizens. + +@section proxy_protocol Writing a Proxy Plugin + +The basic idea is that a proxy plugin provides methods to match, load and unload one or more +sub-plugin plugins in an abstract manner: + + - Matching consists of providing a list of supported file extensions for the sub-plugins and + a mechanism to resolve file extension uncertainty or ambiguity. The matching makes the plugin + visible to the user within the Plugin Manager. + - Loading consists of loading the sub-plugin's file, passing the file to some form of interpreter + and calling GEANY_PLUGIN_REGISTER() or GEANY_PLUGIN_REGISTER_FULL() on behalf of the sub-plugin + at some point. + - Unloading simply reverses the effect of loading. + +For providing these methods, GeanyPlugin has a field GeanyProxyFuncs which contains three function +pointers which must be initialized proir to calling geany_plugin_register_proxy(). This should be +done in the GeanyPluginFuncs::init function of the proxy plugin. + + - In the call to geany_plugin_register_proxy() the proxy plugin passes a list of file extensions. + When Geany scans through its plugin directories as usual it will also look for files with + that extensions and consider found files as plugin candidate. + - GeanyProxyFuncs::probe may be implemented to probe if a plugin candidate (that has one of the + provided file extensions) is actually a plugin. This may depend on the plugin file itself in + case of ambiguity or availability of runtime dependencies or even configuration. + @ref PROXY_IGNORED or @ref PROXY_MATCHED should be returned, possibly in combination + with the @ref PROXY_NOLOAD flag. Not implementing GeanyProxyFuncs::probe at all is eqivalent to + always returning @ref PROXY_MATCHED. + - GeanyProxyFuncs::load must be implemented to actually load the plugin. It is called by Geany + when the user enables the sub-plugin. What "loading" means is entirely up to the proxy plugin and + probably depends on the interpreter of the dynamic language that shall be supported. After + setting everything up as necessary GEANY_PLUGIN_REGISTER() or GEANY_PLUGIN_REGISTER_FULL() must + be called to register the sub-plugin. + - GeanyProxyFuncs::unload must be implemented and is called when the user unchecks the sub-plugin + or when Geany exits. Here, the proxy should release any references or memory associated to the + sub-plugin. Note that if GeanyProxyFuncs::load didn't succeed, i.e. didn't successfully register + the sub-plugin, then this function won't be called. + +GeanyProxyFuncs::load and GeanyProxyFuncs::unload receive two GeanyPlugin pointers: One that +corresponds to the proxy itself and another that corresponds to the sub-plugin. The sub-plugin's +one may be used to call various API functions on behalf of the sub-plugin, including +GEANY_PLUGIN_REGISTER() and GEANY_PLUGIN_REGISTER_FULL(). + +GeanyProxyFuncs::load may return a pointer that is passed back to GeanyProxyFuncs::unload. This can +be used to store proxy-defined but sub-plugin-specific data required for unloading. However, this +pointer is not passed to the sub-plugin's GeanyPluginFuncs. To arrange for that, you want to call +GEANY_PLUGIN_REGISTER_FULL(). This method is the key to enable proxy plugins to wrap the +GeanyPluginFuncs of all sub-plugins and yet multiplex between multiple sub-plugin, for example by +storing a per-sub-plugin interpreter context. + +@note If the pointer returned from GeanyProxyFuncs::load is the same that is passed to +GEANY_PLUGIN_REGISTER_FULL() then you must pass NULL as free_func, because that would be invoked +prior to unloading. Insert the corresponding code into GeanyProxyFuncs::unload. + +@section proxy_compat_guideline Guideline for Checking Compatiblity + +Determining if a plugin candidate is compatible is not a single test. There are multiple levels and +each should be handled differently in order to give the user a consistent feedback. + +Consider the 5 basic cases: + +1) A candidate comes with a suitable file extension but is not a workable plugin file at all. For +example, your proxy supports plugins written in a shell script (.sh) but the shebang of that script +points to an incompatible shell (or even lacks a shebang). You should check for this in +GeanyProxyFuncs::probe() and return @ref PROXY_IGNORED which hides that script from the Plugin +Manager and allows other enabled proxy plugins to pick it up. GeanyProxyFuncs::probe() returning +@ref PROXY_IGNORED is an indication that the candidate is meant for another proxy, or the user +placed the file by accident in one of Geany's plugin directories. In other words the candidate +simply doesn't correspond to your proxy. Thus any noise by debug messages for this case is +undesirable. + +2) A proxy plugin provides its own, versioned API to sub-plugin. The API version of the sub-plugin +is not compatible with the API exposed by the proxy. GeanyProxyFuncs::probe() should never perform +a version check because its sole purpose is to indicate a proxy's correspondence to a given +candidate. It should return @ref PROXY_MATCHED instead. Later, Geany will invoke the +GeanyProxyFuncs::load(), and this function is the right place for a version check. If it fails then +you simply do not call GEANY_PLUGIN_REGISTER(), but rather print a debug message. The result is +that the sub-plugin is not shown in the Plugin Manager at all. This is consistent with the +treatment of native plugins by Geany. + +3) The sub-plugin is also depending on Geany's API version (whether it is or not depends on the +design of the proxy). In this case do not do anything special but forward the API version the +sub-plugin is written/compiled against to GEANY_PLUGIN_REGISTER(). Here, Geany will perform its own +compatiblity check, allowing for a consistent user feedback. The result is again that the +sub-plugin is hidden from the Plugin Manager, like in case 2. But Geany will print a debug message +so you can skip that. + + +If you have even more cases try to fit it into case 1 or 2, depending on whether other proxy +plugins should get a chance to load the candidate or not. + +@section proxy_dep_guideline Guideline for Runtime Errors + +A sub-plugin might not be able to run even if it's perfectly compatible with its proxy. This +includes the case when it lacks certain runtime dependencies such as programs or modules but also +syntactic problems or other errors. + +There are two basic classes: + +1) Runtime errors that can be determined at load time. For example, the shebang of a script +indicates a specific interpeter version but that version is not installed on the system. Your proxy +should respond the same way as for version-incompatible plugins: don't register the plugin at +all, but leave a message the user suggesting what has to be installed in order to work. Handle +syntax errors in the scripts of sub-plugins the same way if possible. + +2) Runtime errors that cannot be determined without actually running the plugin. An example would +be missing modules in Python scripts. If your proxy has no way of foreseeing the problem the plugin +will be registered normally. However, you can catch runtime errors by implementing +GeanyPluginFuncs::init() on the plugin's behalf. This is called after user activation and allows to +indicate errors by returning @c FALSE. However, allowing the user to enable a plugin and then +disabling anyway is a poor user experience. + +Therefore, if possible, try to fail fast and disallow registration. + +@section Proxy Plugin Example + +In this section a dumb example proxy plugin is shown in order to give a practical starting point. +The sub-plugin are not actually code but rather a ini-style description of one or more menu items +that are added to Geany's tools menu and a help dialog. Real world sub-plugins would contain actual +code, usually written in a scripting language. + +A sub-plugin file looks like this: + +@code{.ini} +#!!PROXY_MAGIC!! + +[Init] +item0 = Bam +item1 = Foo +item2 = Bar + +[Help] +text = I'm a simple test. Nothing to see! + +[Info] +name = Demo Proxy Tester +description = I'm a simple test. Nothing to see! +version = 0.1 +author = The Geany developer team +@endcode + +The first line acts as a verification that this file is truly a sub-plugin. Within the [Init] section +there is the menu items for Geany's tools menu. The [Help] section declares the sub-plugins help +text which is shown in its help dialog (via GeanyPluginFuncs::help). The [Info] section is +used as-is for filling the sub-plugins PluginInfo fields. + +That's it, this dumb format is purely declarative and contains no logic. Yet we will create plugins +from it. + +We start by registering the proxy plugin to Geany. There is nothing special to it compared to +normal plugins. A proxy plugin must also fill its own @ref PluginInfo and @ref GeanyPluginFuncs, +followed by registering through GEANY_PLUGIN_REGISTER(). + + +@code{.c} + +/* Called by Geany to initialize the plugin. */ +static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + // ... +} + + +/* Called by Geany before unloading the plugin. */ +static void demoproxy_cleanup(GeanyPlugin *plugin, gpointer data) +{ + // ... +} + + +G_MODULE_EXPORT +void geany_load_module(GeanyPlugin *plugin) +{ + plugin->info->name = _("Demo Proxy"); + plugin->info->description = _("Example Proxy."); + plugin->info->version = "0.1"; + plugin->info->author = _("The Geany developer team"); + + plugin->funcs->init = demoproxy_init; + plugin->funcs->cleanup = demoproxy_cleanup; + + GEANY_PLUGIN_REGISTER(plugin, 225); +} + +@endcode + +The next step is to actually register as a proxy plugin. This is done in demoproxy_init(). +As previously mentioned, it needs a list of accepted file extensions and a set of callback +functions. + +@code{.c} +static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + const gchar *extensions[] = { "ini", "px", NULL }; + + plugin->proxy_funcs->probe = demoproxy_probe; + plugin->proxy_funcs->load = demoproxy_load; + plugin->proxy_funcs->unload = demoproxy_unload; + + return geany_plugin_register_proxy(plugin, extensions); +} + +@endcode + +The callback functions deserve a closer look. + +As already mentioned the file format includes a magic first line which must be present. +GeanyProxyFuncs::probe() verifies that it's present and avoids showing the sub-plugin in the +Plugin Manager if not. + +@code{.c} +static gint demoproxy_probe(GeanyPlugin *proxy, const gchar *filename, gpointer pdata) +{ + /* We know the extension is right (Geany checks that). For demo purposes we perform an + * additional check. This is not necessary when the extension is unique enough. */ + gboolean match = FALSE; + gchar linebuf[128]; + FILE *f = fopen(filename, "r"); + if (f != NULL) + { + if (fgets(linebuf, sizeof(linebuf), f) != NULL) + match = utils_str_equal(linebuf, "#!!PROXY_MAGIC!!\n"); + fclose(f); + } + return match ? PROXY_MATCHED : PROXY_IGNORED; +} +@endcode + +GeanyProxyFuncs::load is a bit more complex. It reads the file, fills the sub-plugin's PluginInfo +fields and calls GEANY_PLUGIN_REGISTER_FULL(). Additionally, it creates a per-plugin context that +holds GKeyFile instance (a poor man's interpeter context). You can also see that it does not call +GEANY_PLUGIN_REGISTER_FULL() if g_key_file_load_from_file() found an error (probably a syntax +problem) which means the sub-plugin cannot be enabled. + +It also installs wrapper functions for the sub-plugin's GeanyPluginFuncs as ini files aren't code. +It's very likely that your proxy needs something similar because you can only install function +pointers to native code. + +@code{.c} +typedef struct { + GKeyFile *file; + gchar *help_text; + GSList *menu_items; +} +PluginContext; + + +static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata); +static void proxy_help(GeanyPlugin *plugin, gpointer pdata); +static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata); + + +static gpointer demoproxy_load(GeanyPlugin *proxy, GeanyPlugin *plugin, + const gchar *filename, gpointer pdata) +{ + GKeyFile *file; + gboolean result; + + file = g_key_file_new(); + result = g_key_file_load_from_file(file, filename, 0, NULL); + + if (result) + { + PluginContext *data = g_new0(PluginContext, 1); + data->file = file; + + plugin->info->name = g_key_file_get_locale_string(data->file, "Info", "name", NULL, NULL); + plugin->info->description = g_key_file_get_locale_string(data->file, "Info", "description", NULL, NULL); + plugin->info->version = g_key_file_get_locale_string(data->file, "Info", "version", NULL, NULL); + plugin->info->author = g_key_file_get_locale_string(data->file, "Info", "author", NULL, NULL); + + plugin->funcs->init = proxy_init; + plugin->funcs->help = proxy_help; + plugin->funcs->cleanup = proxy_cleanup; + + /* Cannot pass g_free as free_func be Geany calls it before unloading, and since + * demoproxy_unload() accesses the data this would be catastrophic */ + GEANY_PLUGIN_REGISTER_FULL(plugin, 225, data, NULL); + return data; + } + + g_key_file_free(file); + return NULL; +} +@endcode + +demoproxy_unload() simply releases all resources aquired in demoproxy_load(). It does not have to +do anything else in for unloading. + +@code{.c} +static void demoproxy_unload(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata) +{ + PluginContext *data = load_data; + + g_free((gchar *)plugin->info->name); + g_free((gchar *)plugin->info->description); + g_free((gchar *)plugin->info->version); + g_free((gchar *)plugin->info->author); + + g_key_file_free(data->file); + g_free(data); +} +@endcode + +Finally the demo_proxy's wrapper GeanyPluginFuncs. They are called for each possible sub-plugin and +therefore have to multiplex between each using the plugin-defined data pointer. Each is called by +Geany as if it were an ordinary, native plugin. + +proxy_init() actually reads the sub-plugin's file using GKeyFile APIs. It prepares for the help +dialog and installs the menu items. proxy_help() is called when the user clicks the help button in +the Plugin Manager. Consequently, this fires up a suitable dialog, although with a dummy message. +proxy_cleanup() frees all memory allocated in proxy_init(). + +@code{.c} +static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data; + gint i = 0; + gchar *text; + + data = (PluginContext *) pdata; + + /* Normally, you would instruct the VM/interpreter to call into the actual plugin. The + * plugin would be identified by pdata. Because there is no interpreter for + * .ini files we do it inline, as this is just a demo */ + data->help_text = g_key_file_get_locale_string(data->file, "Help", "text", NULL, NULL); + while (TRUE) + { + GtkWidget *item; + gchar *key = g_strdup_printf("item%d", i++); + text = g_key_file_get_locale_string(data->file, "Init", key, NULL, NULL); + g_free(key); + + if (!text) + break; + + item = gtk_menu_item_new_with_label(text); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(plugin->geany_data->main_widgets->tools_menu), item); + gtk_widget_set_sensitive(item, FALSE); + data->menu_items = g_slist_prepend(data->menu_items, (gpointer) item); + g_free(text); + } + + return TRUE; +} + + +static void proxy_help(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data; + GtkWidget *dialog; + + data = (PluginContext *) pdata; + + dialog = gtk_message_dialog_new( + GTK_WINDOW(plugin->geany_data->main_widgets->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "%s", data->help_text); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), + _("(From the %s plugin)"), plugin->info->name); + + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + + +static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data = (PluginContext *) pdata; + + g_slist_free_full(data->menu_items, (GDestroyNotify) gtk_widget_destroy); + g_free(data->help_text); +} +@endcode + + */ diff --git a/src/plugindata.h b/src/plugindata.h index a889751333..66da862d80 100644 --- a/src/plugindata.h +++ b/src/plugindata.h @@ -240,6 +240,7 @@ GeanyData; #define geany geany_data /**< Simple macro for @c geany_data that reduces typing. */ typedef struct GeanyPluginFuncs GeanyPluginFuncs; +typedef struct GeanyProxyFuncs GeanyProxyFuncs; /** Basic information for the plugin and identification. * @see geany_plugin. */ @@ -248,7 +249,8 @@ typedef struct GeanyPlugin PluginInfo *info; /**< Fields set in plugin_set_info(). */ GeanyData *geany_data; /**< Pointer to global GeanyData intance */ GeanyPluginFuncs *funcs; /**< Functions implemented by the plugin, set in geany_load_module() */ - + GeanyProxyFuncs *proxy_funcs; /**< Hooks implemented by the plugin if it wants to act as a proxy + Must be set prior to calling geany_plugin_register_proxy() */ struct GeanyPluginPrivate *priv; /* private */ } GeanyPlugin; @@ -347,24 +349,55 @@ void geany_plugin_set_data(GeanyPlugin *plugin, gpointer data, GDestroyNotify fr geany_plugin_register_full((plugin), GEANY_API_VERSION, \ (min_api_version), GEANY_ABI_VERSION, (pdata), (free_func)) +/** Return values for GeanyProxyHooks::probe() + * + * Only @c PROXY_IGNORED, @c PROXY_MATCHED or @c PROXY_MATCHED|PROXY_NOLOAD + * are valid return values. + * + * @see geany_plugin_register_proxy() for a full description of the proxy plugin mechanisms. + * + * @since 1.26 (API 226) + */ typedef enum { + /** The proxy is not responsible at all, and Geany or other plugins are free + * to probe it. + **/ PROXY_IGNORED, + /** The proxy is responsible for this file, and creates a plugin for it */ PROXY_MATCHED, + /** The proxy is does not directly load it, but it's still tied to the proxy + * + * This is for plugins that come in multiple files where only one of these + * files is relevant for the plugin creation (for the PM dialog). The other + * files should be ignored by Geany and other proxies. Example: libpeas has + * a .plugin and a .so per plugin. Geany should not process the .so file + * if there is a corresponding .plugin. + */ PROXY_NOLOAD = 0x100, } GeanyProxyProbeResults; -/* Hooks that need to be implemented for every proxy */ -typedef struct _GeanyProxyFuncs + +/** Hooks that need to be implemented by every proxy + * + * @see geany_plugin_register_proxy() for a full description of the proxy mechanism. + * + * @since 1.26 (API 226) + **/ +struct GeanyProxyFuncs { + /** Called to determine whether the proxy is truly responsible for the requested plugin. + * A NULL pointer assumes the probe() function would always return @ref PROXY_MATCHED */ gint (*probe) (GeanyPlugin *proxy, const gchar *filename, gpointer pdata); + /** Called after probe(), to perform the actual job of loading the plugin */ gpointer (*load) (GeanyPlugin *proxy, GeanyPlugin *subplugin, const gchar *filename, gpointer pdata); + /** Called when the user initiates unloading of a plugin, e.g. on Geany exit */ void (*unload) (GeanyPlugin *proxy, GeanyPlugin *subplugin, gpointer load_data, gpointer pdata); -} -GeanyProxyFuncs; +}; +gint geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensions); /* Deprecated aliases */ #ifndef GEANY_DISABLE_DEPRECATED diff --git a/src/plugins.c b/src/plugins.c index 793107425c..b4c5274fe7 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -653,6 +653,7 @@ plugin_new(Plugin *proxy, const gchar *fname, gboolean load_plugin, gboolean add /* Fields of plugin->info/funcs must to be initialized by the plugin */ plugin->public.info = &plugin->info; plugin->public.funcs = &plugin->cbs; + plugin->public.proxy_funcs = &plugin->proxy_cbs; if (plugin_loaded(plugin)) { @@ -1758,4 +1759,55 @@ static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data) } +/** Register the plugin as a proxy for other plugins + * + * Proxy plugins register a list of file extensions and a set of callbacks that are called + * appropriately. A plugin can be a proxy for multiple types of sub-plugins by handling + * separate file extensions, however they must share the same set of hooks, because this + * function can only be called at most once per plugin. + * + * Each callback receives the plugin-defined data as parameter (see geany_plugin_register()). The + * callbacks must be set prior to calling this, by assigning to @a plugin->proxy_funcs. + * GeanyProxyFuncs::load and GeanyProxyFuncs::unload must be implemented. + * + * Nested proxies are unsupported at this point (TODO). + * + * @note It is entirely up to the proxy to provide access to Geany's plugin API. Native code + * can naturally call Geany's API directly, for interpreted languages the proxy has to + * implement some kind of bindings that the plugin can use. + * + * @see proxy for detailed documentation and an example. + * + * @param plugin The pointer to the plugin's GeanyPlugin instance + * @param extensions A @c NULL-terminated string array of file extensions, excluding the dot. + * @return @c TRUE if the proxy was successfully registered, otherwise @c FALSE + * + * @since 1.26 (API 226) + */ +GEANY_API_SYMBOL +gboolean geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensions) +{ + Plugin *p; + const gchar **ext; + + g_return_val_if_fail(plugin != NULL, FALSE); + g_return_val_if_fail(extensions != NULL, FALSE); + g_return_val_if_fail(*extensions != NULL, FALSE); + g_return_val_if_fail(plugin->proxy_funcs->load != NULL, FALSE); + g_return_val_if_fail(plugin->proxy_funcs->unload != NULL, FALSE); + + p = plugin->priv; + + foreach_strv(ext, extensions) + { + PluginProxy *proxy = g_new(PluginProxy, 1); + g_strlcpy(proxy->extension, *ext, sizeof(proxy->extension)); + proxy->plugin = p; + /* prepend, so that plugins automatically override core providers for a given extension */ + g_ptr_array_insert(active_proxies, 0, proxy); + } + + return TRUE; +} + #endif From 8ac9d56fff28a6a8b296bfde3f7c047bc8762e61 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 26 Aug 2015 23:58:28 +0200 Subject: [PATCH 07/12] plugins: reselect when toggling the current plugin When enabling/disabling pluxys in the PM dialog the list of available plugins might change. If plugins before the pluxy go/come then the wrong plugin becomes selected (the selected row number stays the same). Re-apply the selection to the current one in the toggle callback to overcome this issue. --- src/plugins.c | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/plugins.c b/src/plugins.c index b4c5274fe7..869f04081f 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -1301,6 +1301,41 @@ static void pm_selection_changed(GtkTreeSelection *selection, gpointer user_data } +static gboolean find_iter_for_plugin(Plugin *p, GtkTreeModel *model, GtkTreeIter *iter) +{ + Plugin *pp; + gboolean valid; + + for (valid = gtk_tree_model_get_iter_first(model, iter); + valid; + valid = gtk_tree_model_iter_next(model, iter)) + { + gtk_tree_model_get(model, iter, PLUGIN_COLUMN_PLUGIN, &pp, -1); + if (p == pp) + return TRUE; + } + + return FALSE; +} + + +static gboolean select_plugin(gpointer data) +{ + GtkTreeIter iter; + Plugin *p = data; + GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(pm_widgets.tree)); + + /* restore selection */ + if (find_iter_for_plugin(p, model, &iter)) + { + GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(pm_widgets.tree)); + gtk_tree_selection_select_iter(sel, &iter); + } + + return G_SOURCE_REMOVE; +} + + static void pm_populate(GtkListStore *store); @@ -1314,6 +1349,7 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(pm_widgets.tree)); Plugin *p; Plugin *proxy; + guint prev_num_proxies; gtk_tree_model_get_iter(model, &iter, path); gtk_tree_path_free(path); @@ -1334,6 +1370,7 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer /* save the filename and proxy of the plugin */ file_name = g_strdup(p->filename); proxy = p->proxy; + prev_num_proxies = active_proxies->len; /* unload plugin module */ if (!state) @@ -1362,6 +1399,19 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer /* set again the sensitiveness of the configure and help buttons */ pm_update_buttons(p); } + /* We need to find out if a proxy was added or removed because that affects the plugin list + * presented by the plugin manager. The current solution counts active_proxies twice, + * this suboptimal from an algorithmic POV, however most efficient for the extremely small + * number (at most 3) of pluxies we expect users to load */ + if (prev_num_proxies != active_proxies->len) + { + /* Rescan the plugin list as we now support more */ + if (prev_num_proxies < active_proxies->len) + load_all_plugins(); + pm_populate(pm_widgets.store); + /* restore selection. doesn't work if it's done immediately (same row keeps selected) */ + g_idle_add(select_plugin, p); + } g_free(file_name); } @@ -1524,6 +1574,9 @@ static gboolean pm_tree_filter_func(GtkTreeModel *model, GtkTreeIter *iter, gpoi gchar *haystack, *filename; gtk_tree_model_get(model, iter, PLUGIN_COLUMN_PLUGIN, &plugin, -1); + + if (!plugin) + return FALSE; key = gtk_entry_get_text(GTK_ENTRY(pm_widgets.filter_entry)); filename = g_path_get_basename(plugin->filename); From bbf8e882c2ea76c2d400decfbd6d5b44978eb3fa Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Wed, 26 Aug 2015 23:58:31 +0200 Subject: [PATCH 08/12] demoproxy: add a demo proxy showcasing how to create a proxy plugin This demo proxy does not actually do anything useful. It simply loads pseudo-plugins from an ini-style file. The point is that there will be a plugin in the PM dialog for each ini. Each ini-plugin also causes a menu item to be generated. --- plugins/Makefile.am | 10 +- plugins/demoproxy.c | 202 +++++++++++++++++++++++++++++++++++++++ plugins/demoproxytest.px | 15 +++ po/POTFILES.skip | 1 + 4 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 plugins/demoproxy.c create mode 100644 plugins/demoproxytest.px diff --git a/plugins/Makefile.am b/plugins/Makefile.am index 510beb6818..36847000ee 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -1,7 +1,8 @@ # Adapted from Pidgin's plugins/Makefile.am, thanks EXTRA_DIST = \ - makefile.win32 + makefile.win32 \ + demoproxytest.px plugindir = $(libdir)/geany @@ -11,6 +12,7 @@ plugins_include_HEADERS = \ geanyplugin.h demoplugin_la_LDFLAGS = -module -avoid-version -no-undefined +demoproxy_la_LDFLAGS = -module -avoid-version -no-undefined classbuilder_la_LDFLAGS = -module -avoid-version -no-undefined htmlchars_la_LDFLAGS = -module -avoid-version -no-undefined export_la_LDFLAGS = -module -avoid-version -no-undefined @@ -30,9 +32,11 @@ plugin_LTLIBRARIES = \ # Plugins not to be installed noinst_LTLIBRARIES = \ - demoplugin.la + demoplugin.la \ + demoproxy.la demoplugin_la_SOURCES = demoplugin.c +demoproxy_la_SOURCES = demoproxy.c classbuilder_la_SOURCES = classbuilder.c htmlchars_la_SOURCES = htmlchars.c export_la_SOURCES = export.c @@ -41,6 +45,7 @@ filebrowser_la_SOURCES = filebrowser.c splitwindow_la_SOURCES = splitwindow.c demoplugin_la_CFLAGS = -DG_LOG_DOMAIN=\""Demoplugin"\" -DLOCALEDIR=\""$(LOCALEDIR)"\" +demoproxy_la_CFLAGS = -DG_LOG_DOMAIN=\""Demoproxy"\" classbuilder_la_CFLAGS = -DG_LOG_DOMAIN=\""Classbuilder"\" htmlchars_la_CFLAGS = -DG_LOG_DOMAIN=\""HTMLChars"\" export_la_CFLAGS = -DG_LOG_DOMAIN=\""Export"\" @@ -49,6 +54,7 @@ filebrowser_la_CFLAGS = -DG_LOG_DOMAIN=\""FileBrowser"\" splitwindow_la_CFLAGS = -DG_LOG_DOMAIN=\""SplitWindow"\" demoplugin_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) +demoproxy_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) classbuilder_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) htmlchars_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) export_la_LIBADD = $(top_builddir)/src/libgeany.la $(GTK_LIBS) -lm diff --git a/plugins/demoproxy.c b/plugins/demoproxy.c new file mode 100644 index 0000000000..9e4eecdf53 --- /dev/null +++ b/plugins/demoproxy.c @@ -0,0 +1,202 @@ +/* + * demoproxy.c - this file is part of Geany, a fast and lightweight IDE + * + * Copyright 2015 Thomas Martitz + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/** + * Demo proxy - example of a basic proxy plugin for Geany. Sub-plugins add menu items to the + * Tools menu and have a help dialog. + * + * Note: This is compiled but not installed by default. On Unix, you can install it by compiling + * Geany and then copying (or symlinking) to the plugins/demoproxy.so and + * plugins/demoproxytest.px files to ~/.config/geany/plugins + * - it will be loaded at next startup. + */ + +/* plugin API, always comes first */ +#include "geanyplugin.h" + +typedef struct { + GKeyFile *file; + gchar *help_text; + GSList *menu_items; +} +PluginContext; + + +static gboolean proxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data; + gint i = 0; + gchar *text; + + data = (PluginContext *) pdata; + + /* Normally, you would instruct the VM/interpreter to call into the actual plugin. The + * plugin would be identified by pdata. Because there is no interpreter for + * .ini files we do it inline, as this is just a demo */ + data->help_text = g_key_file_get_locale_string(data->file, "Help", "text", NULL, NULL); + while (TRUE) + { + GtkWidget *item; + gchar *key = g_strdup_printf("item%d", i++); + text = g_key_file_get_locale_string(data->file, "Init", key, NULL, NULL); + g_free(key); + + if (!text) + break; + + item = gtk_menu_item_new_with_label(text); + gtk_widget_show(item); + gtk_container_add(GTK_CONTAINER(plugin->geany_data->main_widgets->tools_menu), item); + gtk_widget_set_sensitive(item, FALSE); + data->menu_items = g_slist_prepend(data->menu_items, (gpointer) item); + g_free(text); + } + + return TRUE; +} + + +static void proxy_help(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data; + GtkWidget *dialog; + + data = (PluginContext *) pdata; + + dialog = gtk_message_dialog_new( + GTK_WINDOW(plugin->geany_data->main_widgets->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "%s", data->help_text); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), + _("(From the %s plugin)"), plugin->info->name); + + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + + +static void proxy_cleanup(GeanyPlugin *plugin, gpointer pdata) +{ + PluginContext *data = (PluginContext *) pdata; + + g_slist_free_full(data->menu_items, (GDestroyNotify) gtk_widget_destroy); + g_free(data->help_text); +} + + +static gint demoproxy_probe(GeanyPlugin *proxy, const gchar *filename, gpointer pdata) +{ + /* We know the extension is right (Geany checks that). For demo purposes we perform an + * additional check. This is not necessary when the extension is unique enough. */ + gboolean match = FALSE; + gchar linebuf[128]; + FILE *f = fopen(filename, "r"); + if (f != NULL) + { + if (fgets(linebuf, sizeof(linebuf), f) != NULL) + match = utils_str_equal(linebuf, "#!!PROXY_MAGIC!!\n"); + fclose(f); + } + return match ? PROXY_MATCHED : PROXY_IGNORED; +} + + +static gpointer demoproxy_load(GeanyPlugin *proxy, GeanyPlugin *plugin, + const gchar *filename, gpointer pdata) +{ + GKeyFile *file; + gboolean result; + + file = g_key_file_new(); + result = g_key_file_load_from_file(file, filename, 0, NULL); + + if (result) + { + PluginContext *data = g_new0(PluginContext, 1); + data->file = file; + + plugin->info->name = g_key_file_get_locale_string(data->file, "Info", "name", NULL, NULL); + plugin->info->description = g_key_file_get_locale_string(data->file, "Info", "description", NULL, NULL); + plugin->info->version = g_key_file_get_locale_string(data->file, "Info", "version", NULL, NULL); + plugin->info->author = g_key_file_get_locale_string(data->file, "Info", "author", NULL, NULL); + + plugin->funcs->init = proxy_init; + plugin->funcs->help = proxy_help; + plugin->funcs->cleanup = proxy_cleanup; + + /* Cannot pass g_free as free_func be Geany calls it before unloading, and since + * demoproxy_unload() accesses the data this would be catastrophic */ + GEANY_PLUGIN_REGISTER_FULL(plugin, 225, data, NULL); + return data; + } + + g_key_file_free(file); + return NULL; +} + + +static void demoproxy_unload(GeanyPlugin *proxy, GeanyPlugin *plugin, gpointer load_data, gpointer pdata) +{ + PluginContext *data = load_data; + + g_free((gchar *)plugin->info->name); + g_free((gchar *)plugin->info->description); + g_free((gchar *)plugin->info->version); + g_free((gchar *)plugin->info->author); + + g_key_file_free(data->file); + g_free(data); +} + + +/* Called by Geany to initialize the plugin. */ +static gboolean demoproxy_init(GeanyPlugin *plugin, gpointer pdata) +{ + const gchar *extensions[] = { "ini", "px", NULL }; + + plugin->proxy_funcs->probe = demoproxy_probe; + plugin->proxy_funcs->load = demoproxy_load; + plugin->proxy_funcs->unload = demoproxy_unload; + + return geany_plugin_register_proxy(plugin, extensions); +} + + +/* Called by Geany before unloading the plugin. */ +static void demoproxy_cleanup(GeanyPlugin *plugin, gpointer data) +{ +} + + +G_MODULE_EXPORT +void geany_load_module(GeanyPlugin *plugin) +{ + plugin->info->name = _("Demo Proxy"); + plugin->info->description = _("Example Proxy."); + plugin->info->version = "0.1"; + plugin->info->author = _("The Geany developer team"); + + plugin->funcs->init = demoproxy_init; + plugin->funcs->cleanup = demoproxy_cleanup; + + GEANY_PLUGIN_REGISTER(plugin, 225); +} diff --git a/plugins/demoproxytest.px b/plugins/demoproxytest.px new file mode 100644 index 0000000000..ee10fc2b11 --- /dev/null +++ b/plugins/demoproxytest.px @@ -0,0 +1,15 @@ +#!!PLUXY_MAGIC!! + +[Init] +item0 = Bam +item1 = Foo +item2 = Bar + +[Help] +text = I'm a simple test. Nothing to see! + +[Info] +name = Demo Pluxy Tester +description = I'm a simple test. Nothing to see! +version = 0.1 +author = The Geany developer team diff --git a/po/POTFILES.skip b/po/POTFILES.skip index 4b6a8895c5..c535edcdbb 100644 --- a/po/POTFILES.skip +++ b/po/POTFILES.skip @@ -6,5 +6,6 @@ geany.desktop.in geany.glade # no need to translate these files plugins/demoplugin.c +plugins/demoproxy.c doc/stash-example.c doc/stash-gui-example.c From 7ac89deebdaf4052ed795495cfbbb8c35043a29f Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Mon, 19 May 2014 06:55:11 +0200 Subject: [PATCH 09/12] plugins: improve PM dialog for proxy and sub-plugins Geany now remembers how many plugins depend on a pluxy. It uses this information to disable the "Active" checkbox in the PM dialog. Additionally, the PM dialog displays plugins in a hierarchical manner, so that sub-plugins are shown next to pluxy. This is espcially handy since it makes the sub-plugin <-> pluxy relationship really obvious, and it's easier to spot which plugins need to be disabled before the pluxy can be disabled. This allows to remove code to re-select the plugin because the row (respective to the hierarchy level) does not change anymore. --- src/pluginprivate.h | 2 + src/plugins.c | 243 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 196 insertions(+), 49 deletions(-) diff --git a/src/pluginprivate.h b/src/pluginprivate.h index 5368c4770c..abaa44e06d 100644 --- a/src/pluginprivate.h +++ b/src/pluginprivate.h @@ -73,6 +73,8 @@ typedef struct GeanyPluginPrivate Plugin *proxy; /* The proxy that handles this plugin */ gpointer proxy_data; /* Data passed to the proxy hooks of above proxy, so * this gives the proxy a pointer to each plugin */ + gint proxied_count; /* count of active plugins this provides a proxy for + * (a count because of possibly nested proxies) */ } GeanyPluginPrivate; diff --git a/src/plugins.c b/src/plugins.c index 869f04081f..4b86ed6e95 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -99,6 +99,8 @@ static PluginProxy builtin_so_proxy = { static GPtrArray *active_proxies = NULL; +static void plugin_free(Plugin *plugin); + static GeanyData geany_data; static void @@ -125,6 +127,30 @@ geany_data_init(void) } +/* In order to have nested proxies work the count of dependent plugins must propagate up. + * This prevents that any plugin in the tree is unloaded while a leaf plugin is active. */ +static void proxied_count_inc(Plugin *proxy) +{ + do + { + proxy->proxied_count += 1; + proxy = proxy->proxy; + } while (proxy != NULL); +} + + +static void proxied_count_dec(Plugin *proxy) +{ + g_warn_if_fail(proxy->proxied_count > 0); + + do + { + proxy->proxied_count -= 1; + proxy = proxy->proxy; + } while (proxy != NULL); +} + + /* Prevent the same plugin filename being loaded more than once. * Note: g_module_name always returns the .so name, even when Plugin::filename is a .la file. */ static gboolean @@ -557,6 +583,7 @@ plugin_load(Plugin *plugin) * keep list sorted so tools menu items and plugin preference tabs are * sorted by plugin name */ active_plugin_list = g_list_insert_sorted(active_plugin_list, plugin, cmp_plugin_names); + proxied_count_inc(plugin->proxy); geany_debug("Loaded: %s (%s)", plugin->filename, plugin->info.name); return TRUE; @@ -847,10 +874,56 @@ plugin_cleanup(Plugin *plugin) plugin->cb_data_destroy = NULL; } + proxied_count_dec(plugin->proxy); geany_debug("Unloaded: %s", plugin->filename); } +/* Remove all plugins that proxy is a proxy for from plugin_list (and free) */ +static void free_subplugins(Plugin *proxy) +{ + GList *item; + + item = plugin_list; + while (item) + { + GList *next = g_list_next(item); + if (proxy == ((Plugin *) item->data)->proxy) + { + /* plugin_free modifies plugin_list */ + plugin_free((Plugin *) item->data); + } + item = next; + } +} + + +/* Returns true if the removal was successful (=> never for non-proxies) */ +static gboolean unregister_proxy(Plugin *proxy) +{ + PluginProxy *p; + guint i; + gboolean is_proxy = FALSE; + + /* Remove the proxy from the proxy list first. It might appear more than once (once + * for each extension), but if it doesn't appear at all it's not actually a proxy */ + foreach_ptr_array(p, i, active_proxies) + { + if (p->plugin == proxy) + { + is_proxy = TRUE; + g_ptr_array_remove(active_proxies, p); + i -= 1; + } + } + return is_proxy; +} + + +/* Cleanup a plugin and free all resources allocated on behalf of it. + * + * If the plugin is a proxy then this also takes special care to unload all + * subplugin loaded through it (make sure none of them is active!) */ static void plugin_free(Plugin *plugin) { @@ -858,10 +931,17 @@ plugin_free(Plugin *plugin) g_return_if_fail(plugin); g_return_if_fail(plugin->proxy); + g_return_if_fail(plugin->proxied_count == 0); proxy = plugin->proxy; + /* If this a proxy remove all depending subplugins. We can assume none of them is *activated* + * (but potentially loaded). Note that free_subplugins() might call us through recursion */ if (is_active_plugin(plugin)) + { + if (unregister_proxy(plugin)) + free_subplugins(plugin); plugin_cleanup(plugin); + } active_plugin_list = g_list_remove(active_plugin_list, plugin); plugin_list = g_list_remove(plugin_list, plugin); @@ -873,7 +953,6 @@ plugin_free(Plugin *plugin) g_free(plugin->filename); g_free(plugin); - plugin = NULL; } @@ -1048,6 +1127,28 @@ static gchar *get_plugin_path(void) } +/* See load_all_plugins(), this simply sorts items with lower hierarchy level first + * (where hierarchy level == number of intermediate proxies before the builtin so loader) */ +static gint cmp_plugin_by_proxy(gconstpointer a, gconstpointer b) +{ + const Plugin *pa = a; + const Plugin *pb = b; + + while (TRUE) + { + if (pa->proxy == pb->proxy) + return 0; + else if (pa->proxy == &builtin_so_proxy_plugin) + return -1; + else if (pb->proxy == &builtin_so_proxy_plugin) + return 1; + + pa = pa->proxy; + pb = pb->proxy; + } +} + + /* Load (but don't initialize) all plugins for the Plugin Manager dialog */ static void load_all_plugins(void) { @@ -1072,6 +1173,13 @@ static void load_all_plugins(void) /* finally load plugins from $prefix/lib/geany */ load_plugins_from_path(plugin_path_system); + /* It is important to sort any plugins that are proxied after their proxy because + * pm_populate() needs the proxy to be loaded and active (if selected by user) in order + * to properly set the value for the PLUGIN_COLUMN_CAN_UNCHECK column. The order between + * sub-plugins does not matter, only between sub-plugins and their proxy, thus + * sorting by hierarchy level is perfectly sufficient */ + plugin_list = g_list_sort(plugin_list, cmp_plugin_by_proxy); + g_free(plugin_path_config); g_free(plugin_path_system); } @@ -1195,6 +1303,15 @@ void plugins_init(void) } +/* Same as plugin_free(), except it does nothing for proxies-in-use, to be called on + * finalize in a loop */ +static void plugin_free_leaf(Plugin *p) +{ + if (p->proxied_count == 0) + plugin_free(p); +} + + /* called even if plugin support is disabled */ void plugins_finalize(void) { @@ -1203,11 +1320,11 @@ void plugins_finalize(void) g_list_foreach(failed_plugins_list, (GFunc) g_free, NULL); g_list_free(failed_plugins_list); } - if (active_plugin_list != NULL) - { - g_list_foreach(active_plugin_list, (GFunc) plugin_free, NULL); - g_list_free(active_plugin_list); - } + /* Have to loop because proxys cannot be unloaded until after all their + * plugins are unloaded as well (the second loop should should catch all the remaining ones) */ + while (active_plugin_list != NULL) + g_list_foreach(active_plugin_list, (GFunc) plugin_free_leaf, NULL); + g_strfreev(active_plugins_pref); } @@ -1236,6 +1353,7 @@ gboolean plugins_have_preferences(void) enum { PLUGIN_COLUMN_CHECK = 0, + PLUGIN_COLUMN_CAN_UNCHECK, PLUGIN_COLUMN_PLUGIN, PLUGIN_N_COLUMNS, PM_BUTTON_KEYBINDINGS, @@ -1247,7 +1365,7 @@ typedef struct { GtkWidget *dialog; GtkWidget *tree; - GtkListStore *store; + GtkTreeStore *store; GtkWidget *filter_entry; GtkWidget *configure_button; GtkWidget *keybindings_button; @@ -1319,24 +1437,7 @@ static gboolean find_iter_for_plugin(Plugin *p, GtkTreeModel *model, GtkTreeIter } -static gboolean select_plugin(gpointer data) -{ - GtkTreeIter iter; - Plugin *p = data; - GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(pm_widgets.tree)); - - /* restore selection */ - if (find_iter_for_plugin(p, model, &iter)) - { - GtkTreeSelection *sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(pm_widgets.tree)); - gtk_tree_selection_select_iter(sel, &iter); - } - - return G_SOURCE_REMOVE; -} - - -static void pm_populate(GtkListStore *store); +static void pm_populate(GtkTreeStore *store); static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer data) @@ -1352,7 +1453,6 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer guint prev_num_proxies; gtk_tree_model_get_iter(model, &iter, path); - gtk_tree_path_free(path); gtk_tree_model_get(model, &iter, PLUGIN_COLUMN_CHECK, &old_state, @@ -1360,7 +1460,10 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer /* no plugins item */ if (p == NULL) + { + gtk_tree_path_free(path); return; + } gtk_tree_model_filter_convert_iter_to_child_iter( GTK_TREE_MODEL_FILTER(model), &store_iter, &iter); @@ -1384,7 +1487,7 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer if (!p) { /* plugin file may no longer be on disk, or is now incompatible */ - gtk_list_store_remove(pm_widgets.store, &store_iter); + gtk_tree_store_remove(pm_widgets.store, &store_iter); } else { @@ -1392,40 +1495,66 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer keybindings_load_keyfile(); /* load shortcuts */ /* update model */ - gtk_list_store_set(pm_widgets.store, &store_iter, + gtk_tree_store_set(pm_widgets.store, &store_iter, PLUGIN_COLUMN_CHECK, state, PLUGIN_COLUMN_PLUGIN, p, -1); /* set again the sensitiveness of the configure and help buttons */ pm_update_buttons(p); + + /* Depending on the state disable the checkbox for the proxy of this plugin, and + * only re-enable if the proxy is not used by any other plugin */ + if (p->proxy != &builtin_so_proxy_plugin) + { + GtkTreeIter parent; + gboolean can_uncheck; + GtkTreePath *store_path = gtk_tree_model_filter_convert_path_to_child_path( + GTK_TREE_MODEL_FILTER(model), path); + + g_warn_if_fail(store_path != NULL); + if (gtk_tree_path_up(store_path)) + { + gtk_tree_model_get_iter(GTK_TREE_MODEL(pm_widgets.store), &parent, store_path); + + if (state) + can_uncheck = FALSE; + else + can_uncheck = p->proxy->proxied_count == 0; + + gtk_tree_store_set(pm_widgets.store, &parent, + PLUGIN_COLUMN_CAN_UNCHECK, can_uncheck, -1); + } + gtk_tree_path_free(store_path); + } } /* We need to find out if a proxy was added or removed because that affects the plugin list - * presented by the plugin manager. The current solution counts active_proxies twice, - * this suboptimal from an algorithmic POV, however most efficient for the extremely small - * number (at most 3) of pluxies we expect users to load */ + * presented by the plugin manager */ if (prev_num_proxies != active_proxies->len) { - /* Rescan the plugin list as we now support more */ + /* Rescan the plugin list as we now support more. Gives some "already loaded" warnings + * they are unproblematic */ if (prev_num_proxies < active_proxies->len) load_all_plugins(); + pm_populate(pm_widgets.store); - /* restore selection. doesn't work if it's done immediately (same row keeps selected) */ - g_idle_add(select_plugin, p); + gtk_tree_view_expand_row(GTK_TREE_VIEW(pm_widgets.tree), path, FALSE); } + + gtk_tree_path_free(path); g_free(file_name); } -static void pm_populate(GtkListStore *store) +static void pm_populate(GtkTreeStore *store) { GtkTreeIter iter; GList *list; - gtk_list_store_clear(store); + gtk_tree_store_clear(store); list = g_list_first(plugin_list); if (list == NULL) { - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, PLUGIN_COLUMN_CHECK, FALSE, + gtk_tree_store_append(store, &iter, NULL); + gtk_tree_store_set(store, &iter, PLUGIN_COLUMN_CHECK, FALSE, PLUGIN_COLUMN_PLUGIN, NULL, -1); } else @@ -1433,11 +1562,18 @@ static void pm_populate(GtkListStore *store) for (; list != NULL; list = list->next) { Plugin *p = list->data; + GtkTreeIter parent; + + if (p->proxy != &builtin_so_proxy_plugin + && find_iter_for_plugin(p->proxy, GTK_TREE_MODEL(pm_widgets.store), &parent)) + gtk_tree_store_append(store, &iter, &parent); + else + gtk_tree_store_append(store, &iter, NULL); - gtk_list_store_append(store, &iter); - gtk_list_store_set(store, &iter, + gtk_tree_store_set(store, &iter, PLUGIN_COLUMN_CHECK, is_active_plugin(p), PLUGIN_COLUMN_PLUGIN, p, + PLUGIN_COLUMN_CAN_UNCHECK, (p->proxied_count == 0), -1); } } @@ -1450,26 +1586,33 @@ static gboolean pm_treeview_query_tooltip(GtkWidget *widget, gint x, gint y, GtkTreeIter iter; GtkTreePath *path; Plugin *p = NULL; + gboolean can_uncheck = TRUE; if (! gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(widget), &x, &y, keyboard_mode, &model, &path, &iter)) return FALSE; - gtk_tree_model_get(model, &iter, PLUGIN_COLUMN_PLUGIN, &p, -1); + gtk_tree_model_get(model, &iter, PLUGIN_COLUMN_PLUGIN, &p, PLUGIN_COLUMN_CAN_UNCHECK, &can_uncheck, -1); if (p != NULL) { - gchar *markup; - gchar *details; + gchar *prefix, *suffix, *details, *markup; + const gchar *uchk; + uchk = can_uncheck ? + "" : _("\nOther plugins depend on this. Disable them first to allow deactivation.\n"); + /* Four allocations is less than ideal but meh */ details = g_strdup_printf(_("Version:\t%s\nAuthor(s):\t%s\nFilename:\t%s"), p->info.version, p->info.author, p->filename); - markup = g_markup_printf_escaped("%s\n%s\n\n%s", - p->info.name, p->info.description, details); + prefix = g_markup_printf_escaped("%s\n%s\n", p->info.name, p->info.description); + suffix = g_markup_printf_escaped("\n%s", details); + markup = g_strconcat(prefix, uchk, suffix, NULL); gtk_tooltip_set_markup(tooltip, markup); gtk_tree_view_set_tooltip_row(GTK_TREE_VIEW(widget), tooltip, path); g_free(details); + g_free(suffix); + g_free(prefix); g_free(markup); } gtk_tree_path_free(path); @@ -1605,7 +1748,7 @@ static void on_pm_tree_filter_entry_icon_release_cb(GtkEntry *entry, GtkEntryIco } -static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store) +static void pm_prepare_treeview(GtkWidget *tree, GtkTreeStore *store) { GtkCellRenderer *text_renderer, *checkbox_renderer; GtkTreeViewColumn *column; @@ -1618,7 +1761,8 @@ static void pm_prepare_treeview(GtkWidget *tree, GtkListStore *store) checkbox_renderer = gtk_cell_renderer_toggle_new(); column = gtk_tree_view_column_new_with_attributes( - _("Active"), checkbox_renderer, "active", PLUGIN_COLUMN_CHECK, NULL); + _("Active"), checkbox_renderer, + "active", PLUGIN_COLUMN_CHECK, "activatable", PLUGIN_COLUMN_CAN_UNCHECK, NULL); gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); g_signal_connect(checkbox_renderer, "toggled", G_CALLBACK(pm_plugin_toggled), NULL); @@ -1760,9 +1904,10 @@ static void pm_show_dialog(GtkMenuItem *menuitem, gpointer user_data) /* prepare treeview */ pm_widgets.tree = gtk_tree_view_new(); - pm_widgets.store = gtk_list_store_new( - PLUGIN_N_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_POINTER); + pm_widgets.store = gtk_tree_store_new( + PLUGIN_N_COLUMNS, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_POINTER); pm_prepare_treeview(pm_widgets.tree, pm_widgets.store); + gtk_tree_view_expand_all(GTK_TREE_VIEW(pm_widgets.tree)); g_object_unref(pm_widgets.store); swin = gtk_scrolled_window_new(NULL, NULL); From 6dfe5ce9420c3674e2e779339e72ce5d79ae70b9 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Mon, 21 Sep 2015 23:51:54 +0200 Subject: [PATCH 10/12] plugins: use GQueue to restore GLib compatibility g_ptr_array_insert() is too recent (2.40), but prepending is required. GQueue is a fine replacement with better old-glib support, at the expense of working with a doubly-linked list instead of plain array. --- src/plugins.c | 32 +++++++++++++++----------------- src/utils.h | 8 ++++++++ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/plugins.c b/src/plugins.c index 4b86ed6e95..d023950c5f 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -97,7 +97,7 @@ static PluginProxy builtin_so_proxy = { .plugin = &builtin_so_proxy_plugin, }; -static GPtrArray *active_proxies = NULL; +static GQueue active_proxies = G_QUEUE_INIT; static void plugin_free(Plugin *plugin); @@ -901,19 +901,18 @@ static void free_subplugins(Plugin *proxy) /* Returns true if the removal was successful (=> never for non-proxies) */ static gboolean unregister_proxy(Plugin *proxy) { - PluginProxy *p; - guint i; gboolean is_proxy = FALSE; + GList *node; /* Remove the proxy from the proxy list first. It might appear more than once (once * for each extension), but if it doesn't appear at all it's not actually a proxy */ - foreach_ptr_array(p, i, active_proxies) + foreach_list_safe(node, active_proxies.head) { + PluginProxy *p = node->data; if (p->plugin == proxy) { is_proxy = TRUE; - g_ptr_array_remove(active_proxies, p); - i -= 1; + g_queue_delete_link(&active_proxies, node); } } return is_proxy; @@ -1014,9 +1013,8 @@ static gboolean check_plugin_path(const gchar *fname) * otherwise it returns the appropriate PluginProxy instance to load it */ static PluginProxy* is_plugin(const gchar *file) { - PluginProxy *proxy; + GList *node; const gchar *ext; - guint i; /* extract file extension to avoid g_str_has_suffix() in the loop */ ext = (const gchar *)strrchr(file, '.'); @@ -1029,8 +1027,9 @@ static PluginProxy* is_plugin(const gchar *file) ext += 1; /* O(n*m), (m being extensions per proxy) doesn't scale very well in theory * but not a problem in practice yet */ - foreach_ptr_array(proxy, i, active_proxies) + foreach_list(node, active_proxies.head) { + PluginProxy *proxy = node->data; if (utils_str_casecmp(ext, proxy->extension) == 0) { Plugin *p = proxy->plugin; @@ -1067,7 +1066,7 @@ load_active_plugins(void) /* If proxys are loaded we have to restart to load plugins that sort before their proxy */ do { - proxies = active_proxies->len; + proxies = active_proxies.length; g_list_free_full(failed_plugins_list, (GDestroyNotify) g_free); failed_plugins_list = NULL; for (i = 0; i < len; i++) @@ -1090,7 +1089,7 @@ load_active_plugins(void) failed_plugins_list = g_list_prepend(failed_plugins_list, g_strdup(fname)); } } - } while (proxies != active_proxies->len); + } while (proxies != active_proxies.length); } @@ -1298,8 +1297,7 @@ void plugins_init(void) g_signal_connect(geany_object, "save-settings", G_CALLBACK(update_active_plugins_pref), NULL); stash_group_add_string_vector(group, &active_plugins_pref, "active_plugins", NULL); - active_proxies = g_ptr_array_sized_new(1); - g_ptr_array_add(active_proxies, &builtin_so_proxy); + g_queue_push_head(&active_proxies, &builtin_so_proxy); } @@ -1473,7 +1471,7 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer /* save the filename and proxy of the plugin */ file_name = g_strdup(p->filename); proxy = p->proxy; - prev_num_proxies = active_proxies->len; + prev_num_proxies = active_proxies.length; /* unload plugin module */ if (!state) @@ -1529,11 +1527,11 @@ static void pm_plugin_toggled(GtkCellRendererToggle *cell, gchar *pth, gpointer } /* We need to find out if a proxy was added or removed because that affects the plugin list * presented by the plugin manager */ - if (prev_num_proxies != active_proxies->len) + if (prev_num_proxies != active_proxies.length) { /* Rescan the plugin list as we now support more. Gives some "already loaded" warnings * they are unproblematic */ - if (prev_num_proxies < active_proxies->len) + if (prev_num_proxies < active_proxies.length) load_all_plugins(); pm_populate(pm_widgets.store); @@ -2002,7 +2000,7 @@ gboolean geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensio g_strlcpy(proxy->extension, *ext, sizeof(proxy->extension)); proxy->plugin = p; /* prepend, so that plugins automatically override core providers for a given extension */ - g_ptr_array_insert(active_proxies, 0, proxy); + g_queue_push_head(&active_proxies, proxy); } return TRUE; diff --git a/src/utils.h b/src/utils.h index 85d830036c..fab6b69282 100644 --- a/src/utils.h +++ b/src/utils.h @@ -116,6 +116,14 @@ G_BEGIN_DECLS #define foreach_slist(node, list) \ foreach_list(node, list) +/* Iterates all the nodes in @a list. Safe against removal during iteration + * @param node should be a (@c GList*). + * @param list @c GList to traverse. */ +#define foreach_list_safe(node, list) \ + for (GList *_node = (list), *_next = (list) ? (list)->next : NULL; \ + (node = _node) != NULL; \ + _node = _next, _next = _next ? _next->next : NULL) + /** Iterates through each unsorted filename in a @c GDir. * @param filename (@c const @c gchar*) locale-encoded filename, without path. Do not modify or free. * @param dir @c GDir created with @c g_dir_open(). Call @c g_dir_close() afterwards. From 6cb443e8631681c7b74ccef25c7656bdbc9f9305 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sun, 4 Oct 2015 00:04:57 +0200 Subject: [PATCH 11/12] plugins: enforce geany_plugin_register_proxy() can be called once In the future we might want to enable calling it again to set new supported plugin types/extensions. This is not implemented yet, but in order to allow this in the future we have to prevent it now, otherwise we'd need to break the API. --- src/plugins.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/plugins.c b/src/plugins.c index d023950c5f..bf3bf0a2cf 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -1985,6 +1985,8 @@ gboolean geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensio { Plugin *p; const gchar **ext; + PluginProxy *proxy; + GList *node; g_return_val_if_fail(plugin != NULL, FALSE); g_return_val_if_fail(extensions != NULL, FALSE); @@ -1993,10 +1995,17 @@ gboolean geany_plugin_register_proxy(GeanyPlugin *plugin, const gchar **extensio g_return_val_if_fail(plugin->proxy_funcs->unload != NULL, FALSE); p = plugin->priv; + /* Check if this was called aready. We want to reserve for the use case of calling + * this again to set new supported extensions (for example, based on proxy configuration). */ + foreach_list(node, active_proxies.head) + { + proxy = node->data; + g_return_if_fail(p != proxy->plugin); + } foreach_strv(ext, extensions) { - PluginProxy *proxy = g_new(PluginProxy, 1); + proxy = g_new(PluginProxy, 1); g_strlcpy(proxy->extension, *ext, sizeof(proxy->extension)); proxy->plugin = p; /* prepend, so that plugins automatically override core providers for a given extension */ From d0f94460eacfc555683187e7055ef5dec7f279b8 Mon Sep 17 00:00:00 2001 From: Colomban Wendling Date: Tue, 6 Oct 2015 15:42:54 +0200 Subject: [PATCH 12/12] Bump plugin API version for proxy plugins support --- src/plugindata.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugindata.h b/src/plugindata.h index 66da862d80..da94300437 100644 --- a/src/plugindata.h +++ b/src/plugindata.h @@ -58,7 +58,7 @@ G_BEGIN_DECLS * @warning You should not test for values below 200 as previously * @c GEANY_API_VERSION was defined as an enum value, not a macro. */ -#define GEANY_API_VERSION 225 +#define GEANY_API_VERSION 226 /* hack to have a different ABI when built with GTK3 because loading GTK2-linked plugins * with GTK3-linked Geany leads to crash */