blob: 175762e7ce2dc353b5ca78965bc0220b0e50f70c [file] [log] [blame]
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* 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:
*
* Copyright (C) 2008 - 2009 Novell, Inc.
* Copyright (C) 2009 - 2011 Red Hat, Inc.
* Copyright (C) 2011 Aleksander Morgado <aleksander@gnu.org>
*/
#include <string.h>
#include <ctype.h>
#include <gmodule.h>
#include <gio/gio.h>
#include <ModemManager.h>
#include <mm-errors-types.h>
#include "mm-plugin-manager.h"
#include "mm-plugin.h"
#include "mm-log.h"
/* Default time to defer probing checks */
#define SUPPORTS_DEFER_TIMEOUT_SECS 3
static void initable_iface_init (GInitableIface *iface);
G_DEFINE_TYPE_EXTENDED (MMPluginManager, mm_plugin_manager, G_TYPE_OBJECT, 0,
G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
initable_iface_init));
struct _MMPluginManagerPrivate {
/* The list of plugins. It is loaded once when the program starts, and the
* list is NOT expected to change after that. */
GSList *plugins;
/* Hash table to keep track of support tasks, using physical path of the
* device as key (which means that more than one tasks may be associated
* to the same key if the modem happens to show more than one port).
* The data in each HT item will be SupportsInfoList (not a GSList directly,
* as we want to be able to modify the list without replacing items with
* the HT API, which also replaces keys). */
GHashTable *supports;
};
/* List of support infos associated to the same physical device */
typedef struct {
GSList *info_list;
} SupportsInfoList;
/* Context of the task looking for best port support */
typedef struct {
MMPluginManager *self;
GSimpleAsyncResult *result;
/* Input context */
gchar *subsys;
gchar *name;
gchar *physdev_path;
MMBaseModem *existing;
/* Current context */
MMPlugin *suggested_plugin;
GSList *current;
guint source_id;
gboolean defer_until_suggested;
/* Output context */
MMPlugin *best_plugin;
} SupportsInfo;
static gboolean find_port_support_idle (SupportsInfo *info);
static void
supports_info_free (SupportsInfo *info)
{
if (!info)
return;
/* There shouldn't be any ongoing supports operation */
g_assert (info->current == NULL);
/* There shouldn't be any scheduled source */
g_assert (info->source_id == 0);
if (info->existing)
g_object_unref (info->existing);
g_object_unref (info->result);
g_free (info->subsys);
g_free (info->name);
g_free (info->physdev_path);
g_free (info);
}
static void
supports_info_list_free (SupportsInfoList *list)
{
g_slist_foreach (list->info_list,
(GFunc)supports_info_free,
NULL);
g_slist_free (list->info_list);
g_free (list);
}
static void
add_supports_info (MMPluginManager *self,
SupportsInfo *info)
{
SupportsInfoList *list;
list = g_hash_table_lookup (self->priv->supports,
info->physdev_path);
if (!list) {
list = g_malloc0 (sizeof (SupportsInfoList));
g_hash_table_insert (self->priv->supports,
g_strdup (info->physdev_path),
list);
}
list->info_list = g_slist_append (list->info_list, info);
}
static void
remove_supports_info (MMPluginManager *self,
SupportsInfo *info)
{
SupportsInfoList *list;
list = g_hash_table_lookup (self->priv->supports,
info->physdev_path);
g_assert (list != NULL);
g_assert (list->info_list != NULL);
list->info_list = g_slist_remove (list->info_list, info);
/* If it was the last info for the given physical path,
* also remove it */
if (!list->info_list)
g_hash_table_remove (self->priv->supports,
info->physdev_path);
/* Note that we just remove it from the list, we don't free it */
}
static void
suggest_supports_info_result (MMPluginManager *self,
const gchar *physdev_path,
MMPlugin *suggested_plugin)
{
SupportsInfoList *list;
GSList *l;
list = g_hash_table_lookup (self->priv->supports,
physdev_path);
if (!list)
return;
/* Look for support infos on the same physical path */
for (l = list->info_list;
l;
l = g_slist_next (l)) {
SupportsInfo *info = l->data;
if (!info->best_plugin &&
!info->suggested_plugin) {
/* TODO: Cancel probing in the port if the plugin being
* checked right now is not the one being suggested.
*/
mm_dbg ("(%s): (%s) suggested plugin for port",
mm_plugin_get_name (suggested_plugin),
info->name);
info->suggested_plugin = suggested_plugin;
/* If we got a task deferred until a suggestion comes,
* complete it */
if (info->defer_until_suggested) {
mm_dbg ("(%s): (%s) deferred task completed, got suggested plugin",
mm_plugin_get_name (suggested_plugin),
info->name);
/* Schedule checking support, which will end the operation */
info->best_plugin = info->suggested_plugin;
info->current = NULL;
info->source_id = g_idle_add ((GSourceFunc)find_port_support_idle,
info);
}
}
}
}
static void
cancel_all_deferred_supports_info (MMPluginManager *self,
const gchar *physdev_path)
{
gboolean abort_cancel = FALSE;
SupportsInfoList *list;
GSList *l;
list = g_hash_table_lookup (self->priv->supports,
physdev_path);
if (!list)
return;
/* Look for support infos on the same physical path.
* We need to look for tasks being deferred until suggested and count
* them. */
for (l = list->info_list;
l && !abort_cancel;
l = g_slist_next (l)) {
SupportsInfo *info = l->data;
if (!info->defer_until_suggested)
abort_cancel = TRUE;
}
if (abort_cancel)
return;
/* If all remaining tasks were deferred until suggested, we need to
* cancel them completely */
for (l = list->info_list; l; l = g_slist_next (l)) {
SupportsInfo *info = l->data;
mm_dbg ("(%s) deferred task aborted, no suggested plugin set",
info->name);
/* Schedule checking support, which will end the operation */
info->current = NULL;
info->best_plugin = NULL;
info->source_id = g_idle_add ((GSourceFunc)find_port_support_idle, info);
}
}
static void
supports_port_ready_cb (MMPlugin *plugin,
GAsyncResult *result,
SupportsInfo *info)
{
MMPluginSupportsResult support_result;
GError *error = NULL;
/* Get supports check results */
support_result = mm_plugin_supports_port_finish (plugin,
result,
&error);
if (error) {
mm_warn ("(%s): (%s) error when checking support: '%s'",
mm_plugin_get_name (plugin),
info->name,
error->message);
g_error_free (error);
}
switch (support_result) {
case MM_PLUGIN_SUPPORTS_PORT_SUPPORTED:
/* Found a best plugin */
info->best_plugin = plugin;
if (info->suggested_plugin &&
info->suggested_plugin != plugin) {
/* The last plugin we tried said it supported this port, but it
* doesn't correspond with the one we're being suggested. */
g_warn_if_reached ();
}
mm_dbg ("(%s): (%s) found best plugin for port",
mm_plugin_get_name (info->best_plugin),
info->name);
info->current = NULL;
/* Schedule checking support, which will end the operation */
info->source_id = g_idle_add ((GSourceFunc)find_port_support_idle,
info);
break;
case MM_PLUGIN_SUPPORTS_PORT_UNSUPPORTED:
if (info->suggested_plugin) {
if (info->suggested_plugin == plugin) {
/* If the plugin that just completed the support check claims
* not to support this port, but this plugin is clearly the
* right plugin since it claimed this port's physical modem,
* just drop the port.
*/
mm_dbg ("(%s/%s): ignoring port unsupported by physical modem's plugin",
info->subsys,
info->name);
info->best_plugin = NULL;
info->current = NULL;
} else {
/* The last plugin we tried is NOT the one we got suggested, so
* directly check support with the suggested plugin. If we
* already checked its support, it won't be checked again. */
info->current = g_slist_find (info->current,
info->suggested_plugin);
}
} else {
/* If the plugin knows it doesn't support the modem, just keep on
* checking the next plugin.
*/
info->current = g_slist_next (info->current);
}
info->source_id = g_idle_add ((GSourceFunc)find_port_support_idle,
info);
break;
case MM_PLUGIN_SUPPORTS_PORT_DEFER:
/* Try with the suggested one after being deferred */
if (info->suggested_plugin) {
mm_dbg ("(%s): (%s) deferring support check, suggested: %s",
mm_plugin_get_name (MM_PLUGIN (info->current->data)),
info->name,
mm_plugin_get_name (MM_PLUGIN (info->suggested_plugin)));
info->current = g_slist_find (info->current,
info->suggested_plugin);
} else {
mm_dbg ("(%s): (%s) deferring support check",
mm_plugin_get_name (MM_PLUGIN (info->current->data)),
info->name);
}
/* Schedule checking support */
info->source_id = g_timeout_add_seconds (SUPPORTS_DEFER_TIMEOUT_SECS,
(GSourceFunc)find_port_support_idle,
info);
break;
case MM_PLUGIN_SUPPORTS_PORT_DEFER_UNTIL_SUGGESTED:
/* If we arrived here and we already have a plugin suggested, use it */
if (info->suggested_plugin) {
mm_dbg ("(%s): (%s) task completed, got suggested plugin",
mm_plugin_get_name (info->suggested_plugin),
info->name);
/* Schedule checking support, which will end the operation */
info->best_plugin = info->suggested_plugin;
info->current = NULL;
info->source_id = g_idle_add ((GSourceFunc)find_port_support_idle,
info);
} else {
/* We are deferred until a suggested plugin is given. If last supports task
* of a given device is finished without finding a best plugin, this task
* will get finished reporting unsupported. */
mm_dbg ("(%s) deferring support check until result suggested",
info->name);
info->defer_until_suggested = TRUE;
}
break;
}
}
static gboolean
find_port_support_idle (SupportsInfo *info)
{
info->source_id = 0;
/* Already checked all plugins? */
if (!info->current) {
/* Report best plugin in asynchronous result (could be none) */
if (info->best_plugin)
g_simple_async_result_set_op_res_gpointer (
info->result,
g_object_ref (info->best_plugin),
(GDestroyNotify)g_object_unref);
else
g_simple_async_result_set_op_res_gpointer (
info->result,
NULL,
NULL);
/* We are only giving the plugin as result, so we can now safely remove
* the supports info from the manager. Always untrack the supports info
* before completing the operation. */
remove_supports_info (info->self, info);
/* We are reporting a best plugin found to a port. We can now
* 'suggest' this same plugin to other ports of the same device. */
if (info->best_plugin)
suggest_supports_info_result (info->self,
info->physdev_path,
info->best_plugin);
/* If ending without a best plugin, we need to cancel all probing tasks
* that got deferred until suggested. */
else
cancel_all_deferred_supports_info (info->self,
info->physdev_path);
g_simple_async_result_complete (info->result);
supports_info_free (info);
return FALSE;
}
/* Ask the current plugin to check support of this port */
mm_plugin_supports_port (MM_PLUGIN (info->current->data),
info->subsys,
info->name,
info->physdev_path,
info->existing,
(GAsyncReadyCallback)supports_port_ready_cb,
info);
return FALSE;
}
MMPlugin *
mm_plugin_manager_find_port_support_finish (MMPluginManager *self,
GAsyncResult *result,
GError **error)
{
g_return_val_if_fail (MM_IS_PLUGIN_MANAGER (self), NULL);
g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
/* Propagate error, if any */
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error))
return NULL;
/* Return the plugin found, if any */
return g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
}
void
mm_plugin_manager_find_port_support (MMPluginManager *self,
const gchar *subsys,
const gchar *name,
const gchar *physdev_path,
MMPlugin *suggested_plugin,
MMBaseModem *existing,
GAsyncReadyCallback callback,
gpointer user_data)
{
SupportsInfo *info;
g_return_if_fail (MM_IS_PLUGIN_MANAGER (self));
/* Setup supports info */
info = g_malloc0 (sizeof (SupportsInfo));
info->self = self; /* SupportsInfo lives as long as self lives */
info->subsys = g_strdup (subsys);
info->name = g_strdup (name);
info->physdev_path = g_strdup (physdev_path);
info->suggested_plugin = suggested_plugin;
if (existing)
info->existing = g_object_ref (existing);
info->result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
NULL);
/* Set first plugin to check */
info->current = self->priv->plugins;
/* If we got one suggested, it will be the first one */
if (info->suggested_plugin) {
info->current = g_slist_find (info->current,
info->suggested_plugin);
}
/* We will keep track of the supports info internally.
* Ownership of the supports info will belong to the manager now. */
add_supports_info (self, info);
/* Schedule the processing of the supports task in an idle */
info->source_id = g_idle_add ((GSourceFunc)find_port_support_idle,
info);
}
gboolean
mm_plugin_manager_is_finding_device_support (MMPluginManager *self,
const gchar *physdev_path,
const gchar **subsys,
const gchar **name)
{
SupportsInfoList *list;
list = g_hash_table_lookup (self->priv->supports,
physdev_path);
if (list) {
if (subsys)
*subsys = ((SupportsInfo *)list->info_list->data)->subsys;
if (name)
*name = ((SupportsInfo *)list->info_list->data)->name;
return TRUE;
}
return FALSE;
}
gboolean
mm_plugin_manager_is_finding_port_support (MMPluginManager *self,
const gchar *subsys,
const gchar *name,
const gchar *physdev_path)
{
SupportsInfoList *list;
list = g_hash_table_lookup (self->priv->supports,
physdev_path);
if (list) {
GSList *l;
for (l = list->info_list;
l;
l = g_slist_next (l)) {
SupportsInfo *info = l->data;
if (g_str_equal (subsys, info->subsys) &&
g_str_equal (name, info->name)) {
/* Support check task already exists */
return TRUE;
}
}
}
return FALSE;
}
static MMPlugin *
load_plugin (const gchar *path)
{
MMPlugin *plugin = NULL;
GModule *module;
MMPluginCreateFunc plugin_create_func;
gint *major_plugin_version;
gint *minor_plugin_version;
gchar *path_display;
/* Get printable UTF-8 string of the path */
path_display = g_filename_display_name (path);
module = g_module_open (path, G_MODULE_BIND_LAZY);
if (!module) {
g_warning ("Could not load plugin '%s': %s", path_display, g_module_error ());
goto out;
}
if (!g_module_symbol (module, "mm_plugin_major_version", (gpointer *) &major_plugin_version)) {
g_warning ("Could not load plugin '%s': Missing major version info", path_display);
goto out;
}
if (*major_plugin_version != MM_PLUGIN_MAJOR_VERSION) {
g_warning ("Could not load plugin '%s': Plugin major version %d, %d is required",
path_display, *major_plugin_version, MM_PLUGIN_MAJOR_VERSION);
goto out;
}
if (!g_module_symbol (module, "mm_plugin_minor_version", (gpointer *) &minor_plugin_version)) {
g_warning ("Could not load plugin '%s': Missing minor version info", path_display);
goto out;
}
if (*minor_plugin_version != MM_PLUGIN_MINOR_VERSION) {
g_warning ("Could not load plugin '%s': Plugin minor version %d, %d is required",
path_display, *minor_plugin_version, MM_PLUGIN_MINOR_VERSION);
goto out;
}
if (!g_module_symbol (module, "mm_plugin_create", (gpointer *) &plugin_create_func)) {
g_warning ("Could not load plugin '%s': %s", path_display, g_module_error ());
goto out;
}
plugin = (*plugin_create_func) ();
if (plugin) {
g_object_weak_ref (G_OBJECT (plugin), (GWeakNotify) g_module_close, module);
} else
mm_warn ("Could not load plugin '%s': initialization failed", path_display);
out:
if (module && !plugin)
g_module_close (module);
g_free (path_display);
return plugin;
}
static gint
compare_plugins (const MMPlugin *plugin_a,
const MMPlugin *plugin_b)
{
/* The order of the plugins in the list is the same order used to check
* whether the plugin can manage a given modem:
* - First, modems that will check vendor ID from udev.
* - Then, modems that report to be sorted last (those which will check
* vendor ID also from the probed ones..
*/
if (mm_plugin_get_sort_last (plugin_a) &&
!mm_plugin_get_sort_last (plugin_b))
return 1;
if (!mm_plugin_get_sort_last (plugin_a) &&
mm_plugin_get_sort_last (plugin_b))
return -1;
return 0;
}
static void
found_plugin (MMPlugin *plugin)
{
mm_info ("Loaded plugin '%s'", mm_plugin_get_name (plugin));
}
static gboolean
load_plugins (MMPluginManager *self,
GError **error)
{
GDir *dir = NULL;
const gchar *fname;
MMPlugin *generic_plugin = NULL;
gchar *plugindir_display = NULL;
if (!g_module_supported ()) {
g_set_error (error,
MM_CORE_ERROR,
MM_CORE_ERROR_UNSUPPORTED,
"GModules are not supported on your platform!");
goto out;
}
/* Get printable UTF-8 string of the path */
plugindir_display = g_filename_display_name (PLUGINDIR);
mm_dbg ("Looking for plugins in '%s'", plugindir_display);
dir = g_dir_open (PLUGINDIR, 0, NULL);
if (!dir) {
g_set_error (error,
MM_CORE_ERROR,
MM_CORE_ERROR_NO_PLUGINS,
"Plugin directory '%s' not found",
plugindir_display);
goto out;
}
while ((fname = g_dir_read_name (dir)) != NULL) {
gchar *path;
MMPlugin *plugin;
if (!g_str_has_suffix (fname, G_MODULE_SUFFIX))
continue;
path = g_module_build_path (PLUGINDIR, fname);
plugin = load_plugin (path);
g_free (path);
if (plugin) {
if (g_str_equal (mm_plugin_get_name (plugin),
MM_PLUGIN_GENERIC_NAME))
generic_plugin = plugin;
else
self->priv->plugins = g_slist_append (self->priv->plugins,
plugin);
}
}
/* Sort last plugins that request it */
self->priv->plugins = g_slist_sort (self->priv->plugins,
(GCompareFunc)compare_plugins);
/* Make sure the generic plugin is last */
if (generic_plugin)
self->priv->plugins = g_slist_append (self->priv->plugins,
generic_plugin);
/* Treat as error if we don't find any plugin */
if (!self->priv->plugins) {
g_set_error (error,
MM_CORE_ERROR,
MM_CORE_ERROR_NO_PLUGINS,
"No plugins found in plugin directory '%s'",
plugindir_display);
goto out;
}
/* Now report about all the found plugins, in the same order they will be
* used while checking support */
g_slist_foreach (self->priv->plugins, (GFunc)found_plugin, NULL);
mm_info ("Successfully loaded %u plugins",
g_slist_length (self->priv->plugins));
out:
if (dir)
g_dir_close (dir);
g_free (plugindir_display);
return !!self->priv->plugins;
}
MMPluginManager *
mm_plugin_manager_new (GError **error)
{
return g_initable_new (MM_TYPE_PLUGIN_MANAGER,
NULL,
error,
NULL);
}
static void
mm_plugin_manager_init (MMPluginManager *manager)
{
/* Initialize opaque pointer to private data */
manager->priv = G_TYPE_INSTANCE_GET_PRIVATE (manager,
MM_TYPE_PLUGIN_MANAGER,
MMPluginManagerPrivate);
manager->priv->supports = g_hash_table_new_full (
g_str_hash,
g_str_equal,
g_free,
(GDestroyNotify)supports_info_list_free);
}
static gboolean
initable_init (GInitable *initable,
GCancellable *cancellable,
GError **error)
{
/* Load the list of plugins */
return load_plugins (MM_PLUGIN_MANAGER (initable), error);
}
static void
finalize (GObject *object)
{
MMPluginManager *self = MM_PLUGIN_MANAGER (object);
/* The Plugin Manager will only be finalized when all support tasks have
* been finished (as the GSimpleAsyncResult takes a reference to the object.
* Therefore, the hash table of support tasks should always be empty.
*/
g_assert (g_hash_table_size (self->priv->supports) == 0);
g_hash_table_destroy (self->priv->supports);
/* Cleanup list of plugins */
g_slist_foreach (self->priv->plugins, (GFunc)g_object_unref, NULL);
g_slist_free (self->priv->plugins);
G_OBJECT_CLASS (mm_plugin_manager_parent_class)->finalize (object);
}
static void
initable_iface_init (GInitableIface *iface)
{
iface->init = initable_init;
}
static void
mm_plugin_manager_class_init (MMPluginManagerClass *manager_class)
{
GObjectClass *object_class = G_OBJECT_CLASS (manager_class);
g_type_class_add_private (object_class, sizeof (MMPluginManagerPrivate));
/* Virtual methods */
object_class->finalize = finalize;
}