soc.2012.statscollector: c255ae66: Make the plugin finch based; add OS hand...
sanket at soc.pidgin.im
sanket at soc.pidgin.im
Mon May 21 01:56:05 EDT 2012
----------------------------------------------------------------------
Revision: c255ae66ef0cdaf227e1ff863e3e150344624b42
Parent: 3a7e0725bd9872a1fb640aec9783bbac597069e5
Author: sanket at soc.pidgin.im
Date: 05/21/12 02:46:48
Branch: im.pidgin.soc.2012.statscollector
URL: http://d.pidgin.im/viewmtn/revision/info/c255ae66ef0cdaf227e1ff863e3e150344624b42
Changelog:
Make the plugin finch based; add OS handling for APPLE
There are major changes with this commit.
* The plugin type is changed to libpurple so that testing could be done
on UI-less systems
* I have added a *LOT* of handling for major OS such as APPLE and POSIX
compliants. With APPLE we can now use Gestalt functions (using dlopen and
dlsym) and get away with having to compile with -frameowork CoreServices
* To understand the nature of OS bitness, we have decided to use ``uname''
as the best understood standard. So if uname -m returns either of ``*64''
type architectures we will assume them to be 64 bit OS and 32 otherwise.
Note that bitness of OS, application and hardware capabilities are 3
completely different things (and could fundamentally all be varying)
There's a need to test the plugin on various falvor of UNIX and APPLE platforms.
Besides also testing various hardware platforms such as PPC or AMD etc.
Changes against parent 3a7e0725bd9872a1fb640aec9783bbac597069e5
added libpurple/plugins/statscollector.c
-------------- next part --------------
============================================================
--- /dev/null
+++ libpurple/plugins/statscollector.c 0ba7b53a386db455975c38e67b0212d81b87469e
@@ -0,0 +1,725 @@
+/* Log the activity of users */
+
+#include "internal.h"
+/* #include "pidgin.h" */
+
+#include "debug.h"
+#include "log.h"
+#include "notify.h"
+#include "signals.h"
+#include "util.h"
+#include "version.h"
+#include "cipher.h"
+#include "prpl.h"
+#include "core.h"
+
+#include <glib.h>
+/* #include <gtkplugin.h> */
+
+#ifdef _WIN32
+#include<windows.h>
+
+#elif defined __APPLE__
+#include<CoreServices/CoreServices.h>
+#include<sys/types.h>
+#include<sys/sysctl.h>
+#include<sys/utsname.h>
+#include<dlfcn.h>
+
+#endif
+
+#define STATS_MAJOR_VERSION 1
+#define STATS_MINOR_VERSION 0
+
+xmlnode *root_stats, *cpuinfo_xml;
+GHashTable *stats_acc_ht, *stats_plugins_ht;
+int save_timer = 0;
+
+/* Types of Operating Systems */
+enum OS_TYPES {WINDOWS, APPLE, UNIX};
+enum BIT_32_64 {BIT_32, BIT_64};
+
+static void
+save_xml(){
+
+ /* Uses the Hash tables and other XML nodes
+ * that have been saved/modified and create
+ * final modified XML out of it to be flushed
+ */
+
+ GHashTableIter iter;
+ gpointer key, value;
+ xmlnode *nroot, *nacc, *nplugins, *node;
+ char *data;
+
+ nroot = xmlnode_new("stats");
+
+ /* Load CPUinfo strucutre */
+ xmlnode_insert_child(nroot, cpuinfo_xml);
+
+ nacc = xmlnode_new_child(nroot, "accounts");
+ nplugins = xmlnode_new_child(nroot, "plugins");
+
+ /* Use the hash tables to populate acc and plugins */
+
+ g_hash_table_iter_init(&iter, stats_acc_ht);
+
+ while(g_hash_table_iter_next(&iter, &key, &value)){
+
+ node = xmlnode_from_str(value, -1);
+ xmlnode_insert_child(nacc, node);
+
+ }
+
+
+ g_hash_table_iter_init(&iter, stats_plugins_ht);
+
+ while(g_hash_table_iter_next(&iter, &key, &value)){
+
+ node = xmlnode_from_str(value, -1);
+ xmlnode_insert_child(nplugins, node);
+
+ }
+
+ data = xmlnode_to_formatted_str(nroot, NULL);
+ purple_util_write_data_to_file("stats.xml", data, -1);
+
+}
+
+static gboolean
+save_cb(gpointer data){
+ save_xml();
+ save_timer = 0;
+ return FALSE;
+}
+
+static void
+schedule_stats_save(void){
+ if(save_timer == 0)
+ save_timer = purple_timeout_add_seconds(5, save_cb, NULL);
+}
+
+static xmlnode *
+get_app_32_64(){
+
+ /* Determines if the application is running in 32 or 64 bit mode */
+ int pt_size;
+ xmlnode *bit_size;
+
+ pt_size = sizeof(int)*8;
+ bit_size = xmlnode_new("app-bit");
+ xmlnode_insert_data(bit_size, g_strdup_printf("%d",pt_size), -1);
+ return bit_size;
+
+}
+
+static xmlnode *
+get_os_32_64(){
+
+ /* Determines whether the kernel we are running is 32 or 64 bit */
+ int bit_size=-1;
+ xmlnode *bit_size_xml;
+#ifdef _WIN32
+ BOOL bIsWow64;
+ typedef BOOL (WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL);
+ LPFN_ISWOW64PROCESS fnIsWow64Process;
+#elif (defined __USE_POSIX) || (defined __APPLE__) /* What to use for APPLE */
+ struct utsname os_utsname;
+ char *m_name;
+#endif
+
+ bit_size_xml = xmlnode_new("os-bit");
+
+#ifdef _WIN64
+ bit_size = 64;
+
+#elif defined _WIN32
+ /* Check if we are running as wow64 process */
+ bIsWow64 = FALSE;
+ fnIsWow64Process = (LPFN_ISWOW64PROCESS) GetProcAddress( \
+ GetModuleHandle(TEXT("kernel32")),"IsWow64Process");
+
+ if(NULL != fnIsWow64Process)
+ {
+ if (!fnIsWow64Process(GetCurrentProcess(),&bIsWow64))
+ {
+ bit_size = -1; /* Cannot say! */
+ }
+ }
+ if(bIsWow64) bit_size= 64;
+ else bit_size = 32;
+
+#elif (defined __USE_POSIX) || (defined __APPLE__)
+ /* Use uname to find kernel architecture and infer bit-size */
+ uname(&os_utsname);
+ m_name = os_utsname.machine;
+
+ /* Identifying 64 bit OS is tricky. We use the following identifiers
+ * on basis of experience with uname(2) call.
+ */
+
+ if(!g_strcmp0(m_name, "x86_64") || !g_strcmp0(m_name, "ia64") \
+ || !g_strcmp0(m_name, "amd64") || !g_strcmp0(m_name, "ppc64")) \
+ bit_size = 64;
+ else bit_size = 32;
+#endif
+
+ xmlnode_insert_data(bit_size_xml, g_strdup_printf("%d", bit_size), -1);
+
+ return bit_size_xml;
+
+}
+
+static xmlnode *
+get_arch(){
+
+ /* Obtains the architecture type */
+
+ xmlnode *arch_xml;
+ char *arch_str;
+#if (defined __USE_POSIX) || (defined __APPLE__)
+ struct utsname os_utsname;
+#elif defined _WIN32
+ SYSTEM_INFO sys_info;
+ typedef void (WINAPI *PGNSI)(LPSYSTEM_INFO);
+ typedef BOOL (WINAPI *PGPI)(DWORD, DWORD, DWORD, DWORD, PDWORD);
+ PGNSI pGNSI;
+#endif
+
+ arch_xml = xmlnode_new("arch");
+
+
+#if (defined __USE_POSIX) || (defined __APPLE__)
+ uname(&os_utsname);
+ arch_str = os_utsname.machine;
+
+#elif defined _WIN32
+ pGNSI = (PGNSI) GetProcAddress(
+ GetModuleHandle(TEXT("kernel32.dll")), "GetNativeSystemInfo");
+
+ if(NULL != pGNSI)
+ pGNSI(&sys_info);
+ else GetSystemInfo(&sys_info);
+
+ switch(sys_info.wProcessorArchitecture){
+
+ case 9:
+ arch_str = "AMD64";
+ break;
+ case 6:
+ arch_str = "IA64";
+ break;
+ case 0:
+ arch_str = "INTEL";
+ break;
+ default:
+ arch_str = "UNKNOWN";
+ }
+#endif
+
+ xmlnode_set_attrib(arch_xml, "id", arch_str);
+
+ return arch_xml;
+
+}
+
+static xmlnode *
+get_os_name(){
+
+ /* Determines the name of the operating system as <os-info id="...">
+ * we could specialize it's name further in this xml
+ */
+
+ xmlnode *os_name_xml;
+ char *name_attrib;
+#ifdef _WIN32
+ xmlnode *major_version, *minor_version;
+ OSVERSIONINFO osvi;
+
+#elif defined __USE_POSIX
+ xmlnode *os_release, *os_version;
+ struct utsname os_utsname;
+
+#elif defined __APPLE__
+ int mib[2];
+ size_t length;
+ SInt32 major_version,minor_version,bug_fix_version;
+
+ char sys_ver_file[] = "/System/Library/CoreServices/SystemVersion.plist";
+ char *sys_ver_contents, *key_str, *value_str;
+
+ GHashTable *sys_ver_ht;
+ GError *err;
+ xmlnode *product_version_xml;
+ xmlnode *dict_sys_ver_xml, *key_sys_ver_xml, *value_sys_ver_xml;
+ xmlnode *major_version_xml, *minor_version_xml, *bug_version_xml;
+ OSErr err1, err2, err3;
+
+ /* Get handle for Gestalt by dlopen */
+ int (*gestalt_ext)(int, int *);
+ void *sdl_library;
+
+ sdl_library = dlopen("/System/Library/Frameworks/CoreServices.framework/CoreServices", RTLD_LAZY); // we assume standard location for framework
+ gestalt_ext = dlsym(sdl_library, "Gestalt");
+
+#endif
+
+
+ os_name_xml = xmlnode_new("os-info");
+
+#ifdef _WIN32
+ name_attrib = "windows";
+#elif defined __linux__
+ name_attrib = "linux";
+#elif defined __APPLE__
+ name_attrib = "apple";
+#endif
+
+ xmlnode_set_attrib(os_name_xml, "id", name_attrib);
+
+ /* Get additional information */
+#ifdef _WIN32
+ ZeroMemory(&osvi, sizeof(OSVERSIONINFO));
+ osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
+ GetVersionEx(&osvi);
+ major_version = xmlnode_new("major-version");
+ minor_version = xmlnode_new("minor-version");
+
+ xmlnode_insert_data(major_version, \
+ g_strdup_printf("%d",osvi.dwMajorVersion),-1);
+ xmlnode_insert_data(minor_version, \
+ g_strdup_printf("%d",osvi.dwMinorVersion),-1);
+
+ xmlnode_insert_child(os_name_xml, major_version);
+ xmlnode_insert_child(os_name_xml, minor_version);
+
+#elif defined __USE_POSIX
+ uname(&os_utsname);
+ os_release = xmlnode_new("release");
+ os_version = xmlnode_new("version");
+ xmlnode_insert_data(os_release, os_utsname.release, -1);
+ xmlnode_insert_data(os_version, os_utsname.version, -1);
+
+ xmlnode_insert_child(os_name_xml, os_release);
+ xmlnode_insert_child(os_name_xml, os_version);
+
+#elif defined __APPLE__
+
+ major_version_xml = xmlnode_new("major-version");
+ minor_version_xml = xmlnode_new("minor-version");
+ bug_version_xml = xmlnode_new("bug-fix-version");
+
+ if(gestalt_ext){
+ gestalt_ext('sys1', &major_version);
+ gestalt_ext('sys2', &minor_version);
+ gestalt_ext('sys3', &bug_fix_version);
+ }
+
+ else purple_debug_info("STATS", "Could not load gestalt at run time");
+
+#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4
+ if(gestalt_ext){
+
+ xmlnode_insert_data(major_version_xml, \
+ g_strdup_printf("%d", major_version), -1);
+ xmlnode_insert_data(minor_version_xml, \
+ g_strdup_printf("%d", minor_version), -1);
+ xmlnode_insert_data(bug_version_xml, \
+ g_strdup_printf("%d", bug_fix_version), -1);
+
+ xmlnode_insert_child(os_name_xml, major_version_xml);
+ xmlnode_insert_child(os_name_xml, minor_version_xml);
+ xmlnode_insert_child(os_name_xml, bug_version_xml);
+ }
+ else
+#endif /* MAC OS X > 10.4 */
+ {
+
+ /* Process SystemVersion.plist to get information
+ * It's a key-value pair in XML. Currently pidgin
+ * does not have a ready-made solution so I am
+ * going to simply iterate through the elements,
+ * till I get NULL implying end of dictionary
+ */
+
+ g_file_get_contents(sys_ver_file, &sys_ver_contents, &length, &err);
+
+ purple_debug_info("STATS", "%s", sys_ver_contents);
+
+ /* Parse the file to XML */
+ dict_sys_ver_xml = xmlnode_get_child( \
+ xmlnode_from_str(sys_ver_contents, -1), "dict");
+
+ /* Set the initial pointers */
+ key_sys_ver_xml = xmlnode_get_child(dict_sys_ver_xml, "key");
+ value_sys_ver_xml = xmlnode_get_child(dict_sys_ver_xml, "string");
+
+ sys_ver_ht = g_hash_table_new(g_str_hash, g_str_equal);
+
+ do{ /* Iterate over the file */
+
+ key_str = xmlnode_get_data(key_sys_ver_xml);
+ value_str = xmlnode_get_data(value_sys_ver_xml);
+
+ purple_debug_info("STATS", "%s %s", key_str, value_str);
+
+ g_hash_table_insert(sys_ver_ht, (void *)key_str, (void *)value_str);
+
+ key_sys_ver_xml = xmlnode_get_next_twin(key_sys_ver_xml);
+ value_sys_ver_xml = xmlnode_get_next_twin(value_sys_ver_xml);
+
+ }while(key_sys_ver_xml && value_sys_ver_xml);
+
+ product_version_xml = xmlnode_new("product-version");
+ xmlnode_insert_data(product_version_xml, \
+ g_hash_table_lookup(sys_ver_ht, "ProductVersion"), -1);
+ xmlnode_insert_child(os_name_xml, product_version_xml);
+
+ }
+
+#endif /* APPLE block */
+
+ return os_name_xml;
+
+}
+
+
+static xmlnode *
+get_cpuinfo_xml(){
+
+ /* Obtains a XML node containing the CPU info */
+
+ xmlnode *root, *languages, *node, *ver, *cpu, *app_arch, *os_arch, *arch_type;
+ const char * const * langs;
+ int i;
+
+ root = xmlnode_new("cpuinfo");
+
+ /* CPU os/hw info */
+
+ cpu = xmlnode_new("cpu");
+ xmlnode_insert_child(root, cpu);
+
+ /* OS Name info */
+ xmlnode_insert_child(cpu, get_os_name());
+
+ /* Application 32/64 bits */
+ app_arch = get_app_32_64();
+ xmlnode_insert_child(cpu, app_arch);
+
+ /* Native 32/64 bit arch */
+ os_arch = get_os_32_64();
+ xmlnode_insert_child(cpu, os_arch);
+
+ /* Arch type */
+ arch_type = get_arch();
+ xmlnode_insert_child(cpu, arch_type);
+
+ /* Languages */
+
+ langs = g_get_language_names();
+
+ languages = xmlnode_new("languages");
+ xmlnode_insert_child(root, languages);
+
+ for(i=0;langs[i];i++){
+
+ purple_debug_info("STATS", "Langs: %s\n", langs[i]);
+ node = xmlnode_new("language");
+ xmlnode_insert_data(node, langs[i], -1);
+ xmlnode_insert_child(languages, node);
+
+ }
+
+ /* Version information */
+
+ ver = xmlnode_new("purple-version");
+
+ xmlnode_insert_data(ver, purple_core_get_version(), -1);
+
+ /* Add the version information to the root cpuinfo */
+
+ xmlnode_insert_child(root, ver);
+
+ purple_debug_info("STATS", "%s\n", xmlnode_to_formatted_str(root, NULL));
+
+ return root;
+
+}
+
+
+static const gchar *
+md5(const guchar *str)
+{
+ PurpleCipherContext *context;
+ static gchar digest[41];
+
+ context = purple_cipher_context_new_by_name("md5", NULL);
+ g_return_val_if_fail(context != NULL, NULL);
+
+ purple_cipher_context_append(context, str, strlen((char*)str));
+
+ if (!purple_cipher_context_digest_to_str(context, sizeof(digest), digest, NULL))
+ return NULL;
+
+ purple_cipher_context_destroy(context);
+
+ return digest;
+}
+
+static char *
+get_acc_id(const char *username, const char *prpl_id){
+
+ /* Generates a id from username and prpl_id */
+
+ char *id;
+
+ id = g_new0(char, 500);
+
+ strcat(id, username);
+ strcat(id, "-");
+ strcat(id, prpl_id);
+
+ return (char *)md5((const guchar *)id);
+
+}
+
+static void
+acc_sign_on_event(PurpleAccount *account){
+
+ /* Store information about logging on of an account. The following
+ * measures should be taken:
+ * 1. Check if we already have such an account in our database
+ * 1.1 If no, then add the account to the list of accounts used
+ */
+
+ const char *username, *protocol, *data;
+ char *id;
+
+ xmlnode *acc, *p_node;
+ username = purple_account_get_username(account);
+ protocol = purple_account_get_protocol_id(account);
+
+ id = get_acc_id(username, protocol);
+
+ /* check if the account already exist in our XML file */
+
+ if(g_hash_table_lookup(stats_acc_ht, id))
+ purple_debug_info("STATS", "Account already exists!");
+ else{
+
+ acc = xmlnode_new("account");
+
+ xmlnode_set_attrib(acc, "id", id);
+
+ p_node = xmlnode_new_child(acc, "protocol");
+ xmlnode_insert_data(p_node, protocol, -1);
+
+ data = xmlnode_to_str(acc, NULL);
+
+ g_hash_table_insert(stats_acc_ht, (void *)id, (void *)data);
+
+ }
+
+ schedule_stats_save();
+
+}
+
+static void
+plugin_load_event(PurplePlugin *plugin){
+
+ /* Store information about logging on of an account. The following
+ * measures should be taken:
+ * 1. Check if we already have such an account in our database
+ * 1.1 If no, then add the account to the list of accounts used
+ */
+
+ const char *plugin_id, *data;
+ xmlnode *plugin_xml;
+ PurplePluginType plugin_type;
+
+ plugin_id = purple_plugin_get_id(plugin);
+
+ plugin_type = (PurplePluginType) plugin->info->type;
+
+ /* purple_debug_info("STATS", "Plugin %s %d\\n", plugin_id, plugin_type); */
+
+
+ /* check if the account already exist in our XML file */
+
+ if(g_hash_table_lookup(stats_plugins_ht, plugin_id))
+ purple_debug_info("STATS", "Plugin %s already exists!", plugin_id);
+
+ else if(plugin_type != PURPLE_PLUGIN_STANDARD)
+ /* We don't need no prpl plugins */
+ purple_debug_info("STATS", "Plugin %s is *not* a standard plugin\n", plugin_id);
+
+ else{
+
+ plugin_xml = xmlnode_new("plugin");
+ xmlnode_set_attrib(plugin_xml, "id", plugin_id);
+ data = xmlnode_to_str(plugin_xml, NULL);
+ g_hash_table_insert(stats_plugins_ht, (void *)plugin_id, (void *)data);
+
+ }
+
+ schedule_stats_save();
+
+}
+
+static xmlnode *
+init_stats(){
+
+ /* This function loads the stats.xml into a global object.
+ * Other functionalities include:
+ * 1. Check if the file is corrupt (bad XML) or missing
+ * 2. In case plugin versions change the XML format, handle that
+ */
+
+ /* GHashTableIter *iter; */
+ GList *loaded_plugins;
+ const gchar *filename;
+ gchar *file_contents;
+ const char *id_node;
+ gsize length;
+ GError *error = NULL;
+ xmlnode *stats_acc_xml, *stats_plugins_xml, *root, *start;
+
+ /* Load the xml */
+ filename = g_build_filename(purple_user_dir(), "stats.xml", NULL);
+ g_file_get_contents(filename, &file_contents, &length, &error);
+ purple_debug_info("STATS", "%s", file_contents);
+ root = xmlnode_from_str(file_contents, -1);
+
+ stats_acc_ht = g_hash_table_new(g_str_hash, g_str_equal);
+ stats_plugins_ht = g_hash_table_new(g_str_hash, g_str_equal);
+
+
+ if(!root){
+
+ /* Reset the XML if it does not exist or is corrupt */
+
+ purple_debug_info("STATS", "failed to load stats.xml");
+
+ /* Populate CPU info only on start as this will not change often */
+ cpuinfo_xml = get_cpuinfo_xml();
+ schedule_stats_save();
+
+ }
+
+ else{
+
+ /* Populate the hash tables with account and plugin information */
+
+ stats_acc_xml = xmlnode_get_child(root, "accounts");
+ stats_plugins_xml = xmlnode_get_child(root, "plugins");
+ cpuinfo_xml = xmlnode_get_child(root, "cpuinfo");
+
+ purple_debug_info("STATS", "Accounts: %s", xmlnode_to_formatted_str(stats_acc_xml, NULL));
+
+ start = xmlnode_get_child(stats_acc_xml, "account");
+
+ for(;start;start = xmlnode_get_next_twin(start)){
+
+ id_node = xmlnode_get_attrib((const xmlnode *)start, "id");
+
+ purple_debug_info("STATS","%s %s", id_node, xmlnode_to_formatted_str(start, NULL));
+ g_hash_table_insert(stats_acc_ht, (gpointer)id_node,
+ (char *)xmlnode_to_formatted_str(start, NULL));
+
+ }
+
+
+ for(;start;start = xmlnode_get_next_twin(start)){
+
+ id_node = xmlnode_get_attrib((const xmlnode *)start, "id");
+
+ g_hash_table_insert(stats_plugins_ht, (gpointer)id_node,
+ (char *)xmlnode_to_formatted_str(start, NULL));
+
+ }
+
+
+ }
+
+ /* It's a good idea to get all loaded plugins and insert them
+ * since, we might miss a few if users are already using them
+ * and then enable our plugin
+ */
+
+ loaded_plugins = purple_plugins_get_loaded();
+
+ g_list_foreach(loaded_plugins, (GFunc)plugin_load_event, NULL);
+
+
+
+ /* Return the structure for modification */
+ return root;
+}
+
+static gboolean
+plugin_load(PurplePlugin *plugin) {
+
+ /* This function does the minimal requirements of registering on
+ * signals currently. Storing of information can hence be done
+ * on various activities */
+
+ /* Load the stats file into a global variable for any updations */
+
+ root_stats = init_stats();
+
+ /* Register the account signals for sign-on */
+
+ purple_signal_connect(purple_accounts_get_handle(), "account-signed-on",
+ plugin, PURPLE_CALLBACK(acc_sign_on_event), NULL);
+
+ /* Register the plugin signals for sign-on */
+
+ purple_signal_connect(purple_plugins_get_handle(), "plugin-load",
+ plugin, PURPLE_CALLBACK(plugin_load_event), NULL);
+
+ return TRUE;
+}
+
+static PurplePluginInfo info =
+{
+ PURPLE_PLUGIN_MAGIC,
+ PURPLE_MAJOR_VERSION, /**< major version */
+ PURPLE_MINOR_VERSION, /**< minor version */
+ PURPLE_PLUGIN_STANDARD, /**< type */
+ NULL, /**< ui_requirement */
+ 0, /**< flags */
+ NULL, /**< dependencies */
+ PURPLE_PRIORITY_DEFAULT, /**< priority */
+ "core-stats-collector", /**< id */
+ N_("Statistics collection"), /**< name */
+ DISPLAY_VERSION, /**< version */
+ N_("Automated collection of statistics for "
+ "pidgin.im "), /**< summary */
+ N_("Collects statistics about usage of pidgin at "
+ "regular intervales and sends this usage "
+ "information to pidgin.im for collection "
+ " and display"), /**< description */
+ "Sanket Agarwal <sanket at sanketagarwal.com>", /**< author */
+ PURPLE_WEBSITE, /**< homepage */
+ plugin_load, /**< load */
+ NULL, /**< unload */
+ NULL, /**< destroy */
+ NULL, /**< ui_info */
+ NULL, /**< extra_info */
+ NULL, /**< prefs_info */
+ NULL, /**< actions */
+
+ /* padding */
+ NULL,
+ NULL,
+ NULL,
+ NULL
+};
+
+static void
+init_plugin(PurplePlugin *plugin)
+{
+}
+
+PURPLE_INIT_PLUGIN(statscollector, init_plugin, info)
More information about the Commits
mailing list