mirror of
https://github.com/Gnucash/gnucash.git
synced 2024-11-25 10:20:18 -06:00
651 lines
19 KiB
C
651 lines
19 KiB
C
/*************************************************************
|
|
* gnc-module.c -- loadable plugin/module system for gnucash
|
|
* Copyright 2001 Linux Developers Group, Inc.
|
|
*************************************************************/
|
|
/********************************************************************\
|
|
* 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, contact: *
|
|
* *
|
|
* Free Software Foundation Voice: +1-617-542-5942 *
|
|
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
|
|
* Boston, MA 02110-1301, USA gnu@gnu.org *
|
|
* *
|
|
\********************************************************************/
|
|
|
|
|
|
#include <config.h>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <gmodule.h>
|
|
#include <sys/types.h>
|
|
#ifdef HAVE_DIRENT_H
|
|
# include <dirent.h>
|
|
#endif
|
|
|
|
#include "gnc-module.h"
|
|
|
|
static GHashTable * loaded_modules = NULL;
|
|
static GList * module_info = NULL;
|
|
|
|
typedef struct
|
|
{
|
|
char * module_path;
|
|
char * module_description;
|
|
char * module_filepath;
|
|
int module_interface;
|
|
int module_age;
|
|
int module_revision;
|
|
} GNCModuleInfo;
|
|
|
|
typedef struct
|
|
{
|
|
GModule * gmodule;
|
|
gchar * filename;
|
|
int load_count;
|
|
GNCModuleInfo * info;
|
|
int (* init_func)(int refcount);
|
|
} GNCLoadedModule;
|
|
|
|
static GNCModuleInfo * gnc_module_get_info(const char * lib_path);
|
|
|
|
/*************************************************************
|
|
* gnc_module_system_search_dirs
|
|
* return a list of dirs to look in for gnc_module libraries
|
|
*************************************************************/
|
|
|
|
static GList *
|
|
gnc_module_system_search_dirs(void)
|
|
{
|
|
const char *spath = g_getenv("GNC_MODULE_PATH");
|
|
GList * list = NULL;
|
|
GString * token = g_string_new(NULL);
|
|
int escchar = 0;
|
|
const char *cpos;
|
|
|
|
if (!spath)
|
|
{
|
|
spath = DEFAULT_MODULE_PATH;
|
|
}
|
|
|
|
for (cpos = spath; *cpos; cpos++)
|
|
{
|
|
switch (*cpos)
|
|
{
|
|
#ifndef G_OS_WIN32
|
|
/* On windows, with '\' as the directory separator character,
|
|
this additional de-quoting will make every path processing
|
|
fail miserably. Anyway this should probably be thrown out
|
|
altogether, because this additional level of de-quoting
|
|
(after shell quoting) is completely unexpected and
|
|
uncommon. */
|
|
case '\\':
|
|
if (!escchar)
|
|
{
|
|
escchar = TRUE;
|
|
}
|
|
else
|
|
{
|
|
g_string_append_c(token, *cpos);
|
|
escchar = FALSE;
|
|
}
|
|
break;
|
|
#endif
|
|
|
|
/* This is ':' on UNIX machines and ';' under Windows. */
|
|
case G_SEARCHPATH_SEPARATOR:
|
|
if (!escchar)
|
|
{
|
|
char *token_str = g_string_free (token, FALSE);
|
|
list = g_list_append (list, token_str);
|
|
token = g_string_new(NULL);
|
|
}
|
|
else
|
|
{
|
|
g_string_append_c(token, *cpos);
|
|
escchar = FALSE;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
g_string_append_c(token, *cpos);
|
|
escchar = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
if (token->len)
|
|
{
|
|
char *token_str = g_string_free (token, FALSE);
|
|
list = g_list_append(list, token_str);
|
|
}
|
|
else
|
|
{
|
|
g_string_free(token, TRUE);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/*************************************************************
|
|
* gnc_module_system_init
|
|
* initialize the module system
|
|
*************************************************************/
|
|
void
|
|
gnc_module_system_init(void)
|
|
{
|
|
if (loaded_modules)
|
|
return;
|
|
|
|
loaded_modules = g_hash_table_new(g_direct_hash, g_direct_equal);
|
|
|
|
/* now crawl the GNC_MODULE_PATH to find likely libraries */
|
|
gnc_module_system_refresh();
|
|
}
|
|
|
|
static inline gboolean
|
|
exclude_module (const char* module)
|
|
{
|
|
/* These modules were converted to shared libraries and removed
|
|
* from GnuCash 4. Simply dlopening them will introduce duplicate
|
|
* symbols and cause unwanted initializations that may lead to a
|
|
* crash. See https://bugs.gnucash.org/show_bug.cgi?id=798229.
|
|
*/
|
|
static const char* excluded_modules[] =
|
|
{
|
|
"libgncmod-app-utils",
|
|
"libgncmod-bi-import",
|
|
"libgncmod-csv-export",
|
|
"libgncmod-csv-import",
|
|
"libgncmod-customer-import",
|
|
"libgncmod-engine",
|
|
"libgncmod-generic-import",
|
|
"libgncmod-gnome-search",
|
|
"libgncmod-gnome-utils",
|
|
"libgncmod-html",
|
|
"libgncmod-ledger-core",
|
|
"libgncmod-locale-reports-us",
|
|
"libgncmod-log-replay",
|
|
"libgncmod-qif-import",
|
|
"libgncmod-register-core",
|
|
"libgncmod-register-gnome",
|
|
"libgncmod-report-gnome",
|
|
"libgncmod-report-system",
|
|
"libgncmod-stylesheets",
|
|
"libgncmod-tax-us"
|
|
};
|
|
static const unsigned len = G_N_ELEMENTS (excluded_modules);
|
|
unsigned namelen = strchr(module, '.') ?
|
|
strchr(module, '.') - module : strlen (module);
|
|
for (unsigned i = 0; i < len; ++i)
|
|
if (strncmp(excluded_modules[i], module, MIN(namelen, strlen(excluded_modules[i]))) == 0)
|
|
return TRUE;
|
|
return FALSE;
|
|
}
|
|
|
|
/*************************************************************
|
|
* gnc_module_system_refresh
|
|
* build the database of modules by looking through the
|
|
* GNC_MODULE_PATH
|
|
*************************************************************/
|
|
|
|
void
|
|
gnc_module_system_refresh(void)
|
|
{
|
|
GList * search_dirs;
|
|
GList * current;
|
|
|
|
if (!loaded_modules)
|
|
{
|
|
gnc_module_system_init();
|
|
}
|
|
|
|
/* get the GNC_MODULE_PATH and split it into directories */
|
|
search_dirs = gnc_module_system_search_dirs();
|
|
|
|
/* look in each search directory */
|
|
for (current = search_dirs; current; current = current->next)
|
|
{
|
|
GDir *d = g_dir_open(current->data, 0, NULL);
|
|
const gchar *dent = NULL;
|
|
char * fullpath = NULL;
|
|
GNCModuleInfo * info;
|
|
|
|
if (!d) continue;
|
|
|
|
while ((dent = g_dir_read_name(d)) != NULL)
|
|
{
|
|
/* is the file a loadable module? */
|
|
|
|
/* Gotcha: On MacOS, G_MODULE_SUFFIX is defined as "so",
|
|
* but if we do not build clean libtool modules with
|
|
* "-module", we get dynamic libraries ending in .dylib
|
|
* On Windows, all modules will move to bin/, so they will
|
|
* be mixed with other libraries, such as gtk+. Adding a
|
|
* prefix "libgncmod" filter will prevent the module loader
|
|
* from loading other libraries. The filter should work on
|
|
* other platforms.
|
|
*/
|
|
if ((g_str_has_suffix(dent, "." G_MODULE_SUFFIX)
|
|
|| g_str_has_suffix(dent, ".dylib"))
|
|
&& g_str_has_prefix(dent, GNC_MODULE_PREFIX)
|
|
&& !exclude_module(dent))
|
|
{
|
|
/* get the full path name, then dlopen the library and see
|
|
* if it has the appropriate symbols to be a gnc_module */
|
|
fullpath = g_build_filename((const gchar *)(current->data),
|
|
dent, (char*)NULL);
|
|
info = gnc_module_get_info(fullpath);
|
|
|
|
if (info)
|
|
{
|
|
module_info = g_list_prepend(module_info, info);
|
|
}
|
|
g_free(fullpath);
|
|
}
|
|
}
|
|
g_dir_close(d);
|
|
|
|
}
|
|
/* free the search dir strings */
|
|
g_list_free_full (search_dirs, g_free);
|
|
}
|
|
|
|
|
|
/*************************************************************
|
|
* gnc_module_system_modinfo
|
|
* return the list of module information
|
|
*************************************************************/
|
|
|
|
GList *
|
|
gnc_module_system_modinfo(void)
|
|
{
|
|
if (!loaded_modules)
|
|
{
|
|
gnc_module_system_init();
|
|
}
|
|
|
|
return module_info;
|
|
}
|
|
|
|
|
|
/*
|
|
* gnc_module_get_symbol
|
|
* gets the munged symbol from the file
|
|
*/
|
|
static gboolean
|
|
gnc_module_get_symbol(GModule* gmodule, const char* symbol, gpointer res)
|
|
{
|
|
gchar** strs;
|
|
gchar* munged_symbol;
|
|
gchar *basename;
|
|
gboolean ret;
|
|
|
|
g_return_val_if_fail(gmodule, FALSE);
|
|
g_return_val_if_fail(symbol, FALSE);
|
|
|
|
/* Separate the file from its extension */
|
|
/* Note: This currently does not work with versioned libtool dlls,
|
|
* as they are named like libgncmodbaz-0.dll */
|
|
basename = g_path_get_basename(g_module_name(gmodule));
|
|
strs = g_strsplit(basename, ".", 2);
|
|
g_free(basename);
|
|
|
|
/* Translate any dashes to underscores */
|
|
g_strdelimit(strs[0], "-", '_');
|
|
|
|
/* Create the symbol <filename>_<symbol> and retrieve that symbol */
|
|
munged_symbol = g_strdup_printf("%s_%s", strs[0], symbol);
|
|
ret = g_module_symbol(gmodule, munged_symbol, res);
|
|
|
|
/* printf("(%d) Looking for symbol %s\n", ret, munged_symbol); */
|
|
|
|
/* Free everything */
|
|
g_strfreev(strs);
|
|
g_free(munged_symbol);
|
|
return ret;
|
|
}
|
|
|
|
/*************************************************************
|
|
* gnc_module_get_info
|
|
* check a proposed gnc_module by looking for specific symbols in it;
|
|
* if it's a gnc_module, return a struct describing it.
|
|
*************************************************************/
|
|
|
|
static GNCModuleInfo *
|
|
gnc_module_get_info(const char * fullpath)
|
|
{
|
|
GModule *gmodule;
|
|
gpointer modsysver;
|
|
GNCModuleInfo *info = NULL;
|
|
gpointer initfunc, pathfunc, descripfunc, iface, revision, age;
|
|
gchar * (* f_path)(void);
|
|
gchar * (* f_descrip)(void);
|
|
|
|
/* g_debug("(init) dlopening '%s'\n", fullpath); */
|
|
gmodule = g_module_open(fullpath, G_MODULE_BIND_LAZY);
|
|
if (gmodule == NULL)
|
|
{
|
|
g_debug("Failed to dlopen() '%s': %s\n", fullpath, g_module_error());
|
|
return NULL;
|
|
}
|
|
|
|
/* the modsysver tells us what the expected symbols and their
|
|
* types are */
|
|
if (!gnc_module_get_symbol(gmodule, "gnc_module_system_interface", &modsysver))
|
|
{
|
|
/* g_debug("Module '%s' does not contain 'gnc_module_system_interface'\n", */
|
|
/* fullpath); */
|
|
goto get_info_close;
|
|
}
|
|
|
|
if (*(int *)modsysver != 0)
|
|
{
|
|
g_warning("Module '%s' requires newer module system\n", fullpath);
|
|
goto get_info_close;
|
|
}
|
|
|
|
if (!gnc_module_get_symbol(gmodule, "gnc_module_init", &initfunc) ||
|
|
!gnc_module_get_symbol(gmodule, "gnc_module_path", &pathfunc) ||
|
|
!gnc_module_get_symbol(gmodule, "gnc_module_description", &descripfunc) ||
|
|
!gnc_module_get_symbol(gmodule, "gnc_module_current", &iface) ||
|
|
!gnc_module_get_symbol(gmodule, "gnc_module_revision", &revision) ||
|
|
!gnc_module_get_symbol(gmodule, "gnc_module_age", &age))
|
|
{
|
|
g_warning("Module '%s' does not match module signature\n", fullpath);
|
|
goto get_info_close;
|
|
}
|
|
|
|
/* we have found a gnc_module. */
|
|
info = g_new0(GNCModuleInfo, 1);
|
|
f_path = pathfunc;
|
|
f_descrip = descripfunc;
|
|
info->module_path = f_path();
|
|
info->module_description = f_descrip();
|
|
info->module_filepath = g_strdup(fullpath);
|
|
info->module_interface = *(int *)iface;
|
|
info->module_age = *(int *)age;
|
|
info->module_revision = *(int *)revision;
|
|
|
|
g_module_make_resident(gmodule);
|
|
get_info_close:
|
|
/* g_debug("(init) closing '%s'\n", fullpath); */
|
|
g_module_close(gmodule);
|
|
|
|
return info;
|
|
}
|
|
|
|
|
|
/*************************************************************
|
|
* gnc_module_locate
|
|
* find the best matching module for the name, interface pair
|
|
*************************************************************/
|
|
|
|
static GNCModuleInfo *
|
|
gnc_module_locate(const gchar * module_name, int iface)
|
|
{
|
|
GNCModuleInfo * best = NULL;
|
|
GNCModuleInfo * current = NULL;
|
|
GList * lptr;
|
|
|
|
if (!loaded_modules)
|
|
{
|
|
gnc_module_system_init();
|
|
}
|
|
|
|
for (lptr = module_info; lptr; lptr = lptr->next)
|
|
{
|
|
current = lptr->data;
|
|
if (!strcmp(module_name, current->module_path) &&
|
|
(iface >= (current->module_interface - current->module_age)) &&
|
|
(iface <= current->module_interface))
|
|
{
|
|
if (best)
|
|
{
|
|
if ((current->module_interface > best->module_interface) ||
|
|
((current->module_interface == best->module_interface) &&
|
|
(current->module_age > best->module_age)) ||
|
|
((current->module_interface == best->module_interface) &&
|
|
(current->module_age == best->module_age) &&
|
|
(current->module_revision > best->module_revision)))
|
|
{
|
|
best = current;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
best = current;
|
|
}
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
static void
|
|
list_loaded (gpointer k, gpointer v, gpointer data)
|
|
{
|
|
GList ** l = data;
|
|
*l = g_list_prepend(*l, v);
|
|
}
|
|
|
|
static GNCLoadedModule *
|
|
gnc_module_check_loaded(const char * module_name, gint iface)
|
|
{
|
|
GNCModuleInfo * modinfo = gnc_module_locate(module_name, iface);
|
|
GList * modules = NULL;
|
|
GList * p = NULL;
|
|
GNCLoadedModule * rv = NULL;
|
|
|
|
if (modinfo == NULL)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
if (!loaded_modules)
|
|
{
|
|
gnc_module_system_init();
|
|
}
|
|
|
|
/* turn the loaded-modules table into a list */
|
|
g_hash_table_foreach(loaded_modules, list_loaded, &modules);
|
|
|
|
/* walk the list to see if the file we want is already open */
|
|
for (p = modules; p; p = p->next)
|
|
{
|
|
GNCLoadedModule * lm = p->data;
|
|
if (!strcmp(lm->filename, modinfo->module_filepath))
|
|
{
|
|
rv = lm;
|
|
break;
|
|
}
|
|
}
|
|
g_list_free(modules);
|
|
return rv;
|
|
}
|
|
|
|
|
|
/*************************************************************
|
|
* gnc_module_load
|
|
* Ensure that the module named by "module_name" is loaded.
|
|
*************************************************************/
|
|
|
|
static GNCModule
|
|
gnc_module_load_common(const char * module_name, gint iface, gboolean optional)
|
|
{
|
|
|
|
GNCLoadedModule * info;
|
|
GModule * gmodule;
|
|
GNCModuleInfo * modinfo;
|
|
|
|
g_debug ("module_name: %s", module_name);
|
|
|
|
if (!loaded_modules)
|
|
{
|
|
gnc_module_system_init();
|
|
}
|
|
|
|
info = gnc_module_check_loaded(module_name, iface);
|
|
|
|
/* if the module's already loaded, just increment its use count.
|
|
* otherwise, load it and check for the initializer
|
|
* "gnc_module_init". if we find that, assume it's a gnucash module
|
|
* and run the function. */
|
|
|
|
if (info)
|
|
{
|
|
/* module already loaded ... call the init thunk */
|
|
if (info->init_func)
|
|
{
|
|
if (info->init_func(info->load_count))
|
|
{
|
|
info->load_count++;
|
|
g_debug ("module %s already loaded", module_name);
|
|
return info;
|
|
}
|
|
else
|
|
{
|
|
g_warning ("module init failed: %s", module_name);
|
|
return NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
g_warning ("module has no init func: %s", module_name);
|
|
return NULL;
|
|
}
|
|
/* NOTREACHED */
|
|
g_error("internal error");
|
|
return NULL;
|
|
}
|
|
|
|
modinfo = gnc_module_locate(module_name, iface);
|
|
if (!modinfo)
|
|
{
|
|
if (optional)
|
|
{
|
|
g_message ("Could not locate optional module %s interface v.%d",
|
|
module_name, iface);
|
|
}
|
|
else
|
|
{
|
|
g_warning ("Could not locate module %s interface v.%d",
|
|
module_name, iface);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* if (modinfo) */
|
|
/* g_debug("(init) loading '%s' from '%s'\n", module_name, */
|
|
/* modinfo->module_filepath); */
|
|
|
|
if ((gmodule = g_module_open(modinfo->module_filepath, 0)) != NULL)
|
|
{
|
|
gpointer initfunc;
|
|
|
|
if (gnc_module_get_symbol(gmodule, "gnc_module_init", &initfunc))
|
|
{
|
|
/* stick it in the hash table */
|
|
info = g_new0(GNCLoadedModule, 1);
|
|
info->gmodule = gmodule;
|
|
info->filename = g_strdup(modinfo->module_filepath);
|
|
info->load_count = 1;
|
|
info->init_func = initfunc;
|
|
g_hash_table_insert(loaded_modules, info, info);
|
|
|
|
/* now call its init function. this should load any dependent
|
|
* modules, too. If it doesn't return TRUE unload the module. */
|
|
if (!info->init_func(0))
|
|
{
|
|
/* init failed. unload the module. */
|
|
g_warning ("Initialization failed for module %s", module_name);
|
|
g_hash_table_remove(loaded_modules, info);
|
|
g_free(info->filename);
|
|
g_free(info);
|
|
/* g_module_close(module); */
|
|
return NULL;
|
|
}
|
|
|
|
return info;
|
|
}
|
|
else
|
|
{
|
|
g_warning ("Module %s (%s) is not a gnc-module.\n", module_name,
|
|
modinfo->module_filepath);
|
|
//lt_dlclose(handle);
|
|
}
|
|
return info;
|
|
}
|
|
|
|
g_warning ("Failed to open module %s: %s\n", module_name, g_module_error());
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
GNCModule
|
|
gnc_module_load(const char * module_name, gint iface)
|
|
{
|
|
return gnc_module_load_common(module_name, iface, FALSE);
|
|
}
|
|
|
|
GNCModule
|
|
gnc_module_load_optional(const char * module_name, gint iface)
|
|
{
|
|
return gnc_module_load_common(module_name, iface, TRUE);
|
|
}
|
|
|
|
/*************************************************************
|
|
* gnc_module_unload
|
|
* unload a module (only actually unload it if the use count goes to 0)
|
|
*************************************************************/
|
|
|
|
int
|
|
gnc_module_unload(GNCModule module)
|
|
{
|
|
GNCLoadedModule * info;
|
|
|
|
if (!loaded_modules)
|
|
{
|
|
gnc_module_system_init();
|
|
}
|
|
|
|
if ((info = g_hash_table_lookup(loaded_modules, module)) != NULL)
|
|
{
|
|
gpointer unload_thunk;
|
|
int unload_val = TRUE;
|
|
|
|
info->load_count--;
|
|
if (gnc_module_get_symbol(info->gmodule, "gnc_module_end", &unload_thunk))
|
|
{
|
|
int (* thunk)(int) = unload_thunk;
|
|
unload_val = thunk(info->load_count);
|
|
}
|
|
|
|
/* actually unload the module if necessary */
|
|
if (info->load_count == 0)
|
|
{
|
|
/* now close the module and free the struct */
|
|
/* g_debug("(unload) closing %s\n", info->filename); */
|
|
/* g_module_close(info->gmodule); */
|
|
g_hash_table_remove(loaded_modules, module);
|
|
g_free(info);
|
|
}
|
|
return unload_val;
|
|
}
|
|
else
|
|
{
|
|
g_warning ("Failed to unload module %p (it is not loaded)\n", module);
|
|
return 0;
|
|
}
|
|
}
|
|
|