| /* -*- 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 - 2012 Red Hat, Inc. |
| * Copyright (C) 2011 - 2012 Aleksander Morgado <aleksander@gnu.org> |
| * Copyright (C) 2012 Google, Inc. |
| */ |
| |
| #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" |
| |
| 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)) |
| |
| enum { |
| PROP_0, |
| PROP_PLUGIN_DIR, |
| LAST_PROP |
| }; |
| |
| struct _MMPluginManagerPrivate { |
| /* Path to look for plugins */ |
| gchar *plugin_dir; |
| |
| /* This list contains all plugins except for the generic one, order is not |
| * important. It is loaded once when the program starts, and the list is NOT |
| * expected to change after that.*/ |
| GList *plugins; |
| /* Last, the generic plugin. */ |
| MMPlugin *generic; |
| |
| /* List of ongoing device support checks */ |
| GList *device_contexts; |
| }; |
| |
| /*****************************************************************************/ |
| /* Build plugin list for a single port */ |
| |
| static GList * |
| plugin_manager_build_plugins_list (MMPluginManager *self, |
| MMDevice *device, |
| MMKernelDevice *port) |
| { |
| GList *list = NULL; |
| GList *l; |
| gboolean supported_found = FALSE; |
| |
| for (l = self->priv->plugins; l && !supported_found; l = g_list_next (l)) { |
| MMPluginSupportsHint hint; |
| |
| hint = mm_plugin_discard_port_early (MM_PLUGIN (l->data), device, port); |
| switch (hint) { |
| case MM_PLUGIN_SUPPORTS_HINT_UNSUPPORTED: |
| /* Fully discard */ |
| break; |
| case MM_PLUGIN_SUPPORTS_HINT_MAYBE: |
| /* Maybe supported, add to tail of list */ |
| list = g_list_append (list, g_object_ref (l->data)); |
| break; |
| case MM_PLUGIN_SUPPORTS_HINT_LIKELY: |
| /* Likely supported, add to head of list */ |
| list = g_list_prepend (list, g_object_ref (l->data)); |
| break; |
| case MM_PLUGIN_SUPPORTS_HINT_SUPPORTED: |
| /* Really supported, clean existing list and add it alone */ |
| if (list) { |
| g_list_free_full (list, (GDestroyNotify) g_object_unref); |
| list = NULL; |
| } |
| list = g_list_prepend (list, g_object_ref (l->data)); |
| /* This will end the loop as well */ |
| supported_found = TRUE; |
| break; |
| default: |
| g_assert_not_reached(); |
| } |
| } |
| |
| /* Add the generic plugin at the end of the list */ |
| if (self->priv->generic) |
| list = g_list_append (list, g_object_ref (self->priv->generic)); |
| |
| return list; |
| } |
| |
| /*****************************************************************************/ |
| /* Common context for async operations |
| * |
| * The DeviceContext and PortContext structs are not proper objects, and that |
| * means that they cannot be given as core parameter of GIO async results. |
| * Instead, we'll use the MMPluginManager as that core parameter always, and |
| * we'll pass along a common context with all the remaining details as user |
| * data. |
| */ |
| |
| typedef struct _DeviceContext DeviceContext; |
| typedef struct _PortContext PortContext; |
| |
| static DeviceContext *device_context_ref (DeviceContext *device_context); |
| static void device_context_unref (DeviceContext *device_context); |
| static PortContext *port_context_ref (PortContext *port_context); |
| static void port_context_unref (PortContext *port_context); |
| |
| typedef struct { |
| MMPluginManager *self; |
| DeviceContext *device_context; |
| PortContext *port_context; |
| GTask *task; |
| } CommonAsyncContext; |
| |
| static void |
| common_async_context_free (CommonAsyncContext *common) |
| { |
| if (common->port_context) |
| port_context_unref (common->port_context); |
| if (common->device_context) |
| device_context_unref (common->device_context); |
| if (common->self) |
| g_object_unref (common->self); |
| if (common->task) |
| g_object_unref (common->task); |
| g_slice_free (CommonAsyncContext, common); |
| } |
| |
| static CommonAsyncContext * |
| common_async_context_new (MMPluginManager *self, |
| DeviceContext *device_context, |
| PortContext *port_context, |
| GTask *task) |
| { |
| CommonAsyncContext *common; |
| |
| common = g_slice_new0 (CommonAsyncContext); |
| common->self = (self ? g_object_ref (self) : NULL); |
| common->device_context = (device_context ? device_context_ref (device_context) : NULL); |
| common->port_context = (port_context ? port_context_ref (port_context) : NULL); |
| common->task = (task ? g_object_ref (task) : NULL); |
| return common; |
| } |
| |
| /*****************************************************************************/ |
| /* Port context */ |
| |
| /* Default time to defer probing checks */ |
| #define DEFER_TIMEOUT_SECS 3 |
| |
| /* |
| * Port context |
| * |
| * This structure hold all the probing information related to a single port. |
| */ |
| struct _PortContext { |
| /* Reference counting */ |
| volatile gint ref_count; |
| /* The name of the context */ |
| gchar *name; |
| /* The device where the port is*/ |
| MMDevice *device; |
| /* The reported kernel port object */ |
| MMKernelDevice *port; |
| |
| /* The operation task */ |
| GTask *task; |
| /* Internal ancellable */ |
| GCancellable *cancellable; |
| |
| /* Timer tracking how much time is required for the port support check */ |
| GTimer *timer; |
| |
| /* This list contains all the plugins that have to be tested with a given |
| * port. The list is created once when the task is started, and is never |
| * modified afterwards. */ |
| GList *plugins; |
| /* This is the current plugin being tested. If NULL, there are no more |
| * plugins to try. */ |
| GList *current; |
| /* A best plugin has been found for this port. */ |
| MMPlugin *best_plugin; |
| /* A plugin was suggested for this port. */ |
| MMPlugin *suggested_plugin; |
| |
| /* The probe has been deferred */ |
| guint defer_id; |
| /* The probe must be deferred until a result is suggested by other |
| * port probe results (e.g. for WWAN ports). */ |
| gboolean defer_until_suggested; |
| }; |
| |
| static void |
| port_context_unref (PortContext *port_context) |
| { |
| if (g_atomic_int_dec_and_test (&port_context->ref_count)) { |
| /* There must never be a deferred task scheduled for this port */ |
| g_assert (port_context->defer_id == 0); |
| |
| /* The port support check task must have been completed previously */ |
| g_assert (!port_context->task); |
| |
| if (port_context->best_plugin) |
| g_object_unref (port_context->best_plugin); |
| if (port_context->suggested_plugin) |
| g_object_unref (port_context->suggested_plugin); |
| if (port_context->plugins) |
| g_list_free_full (port_context->plugins, (GDestroyNotify) g_object_unref); |
| if (port_context->cancellable) |
| g_object_unref (port_context->cancellable); |
| g_free (port_context->name); |
| g_timer_destroy (port_context->timer); |
| g_object_unref (port_context->port); |
| g_object_unref (port_context->device); |
| g_slice_free (PortContext, port_context); |
| } |
| } |
| |
| static PortContext * |
| port_context_ref (PortContext *port_context) |
| { |
| g_atomic_int_inc (&port_context->ref_count); |
| return port_context; |
| } |
| |
| static MMPlugin * |
| port_context_run_finish (MMPluginManager *self, |
| GAsyncResult *res, |
| GError **error) |
| { |
| return g_task_propagate_pointer (G_TASK (res), error); |
| } |
| |
| static void |
| port_context_complete (PortContext *port_context) |
| { |
| GTask *task; |
| |
| /* Steal the task from the task */ |
| g_assert (port_context->task); |
| task = port_context->task; |
| port_context->task = NULL; |
| |
| /* Log about the time required to complete the checks */ |
| mm_dbg ("[plugin manager] task %s: finished in '%lf' seconds", |
| port_context->name, g_timer_elapsed (port_context->timer, NULL)); |
| |
| if (!port_context->best_plugin) |
| g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED, "Unsupported"); |
| else |
| g_task_return_pointer (task, g_object_ref (port_context->best_plugin), g_object_unref); |
| g_object_unref (task); |
| } |
| |
| static void port_context_next (PortContext *port_context); |
| |
| static void |
| port_context_supported (PortContext *port_context, |
| MMPlugin *plugin) |
| { |
| g_assert (plugin); |
| |
| mm_dbg ("[plugin manager] task %s: found best plugin for port (%s)", |
| port_context->name, mm_plugin_get_name (plugin)); |
| |
| /* Found a best plugin, store it to return it */ |
| port_context->best_plugin = g_object_ref (plugin); |
| port_context_complete (port_context); |
| } |
| |
| static gboolean |
| port_context_defer_ready (PortContext *port_context) |
| { |
| port_context->defer_id = 0; |
| port_context_next (port_context); |
| return G_SOURCE_REMOVE; |
| } |
| |
| static void |
| port_context_set_suggestion (PortContext *port_context, |
| MMPlugin *suggested_plugin) |
| { |
| gboolean forbidden_icera; |
| |
| /* Plugin suggestions serve two different purposes here: |
| * 1) Finish all the probes which were deferred until suggested. |
| * 2) Suggest to other probes which plugin to test next. |
| * |
| * The exception here is when we suggest the GENERIC plugin. |
| * In this case, only purpose (1) is applied, this is, only |
| * the deferred until suggested probes get finished. |
| */ |
| |
| /* Do nothing if already best plugin found, or if a plugin has already been |
| * suggested before */ |
| if (port_context->best_plugin || port_context->suggested_plugin) |
| return; |
| |
| /* Complete tasks which were deferred until suggested */ |
| if (port_context->defer_until_suggested) { |
| /* Reset the defer until suggested flag; we consider this |
| * cancelled probe completed now. */ |
| port_context->defer_until_suggested = FALSE; |
| |
| if (suggested_plugin) { |
| mm_dbg ("[plugin manager] task %s: deferred task completed, got suggested plugin (%s)", |
| port_context->name, mm_plugin_get_name (suggested_plugin)); |
| /* Advance to the suggested plugin and re-check support there */ |
| port_context->suggested_plugin = g_object_ref (suggested_plugin); |
| port_context->current = g_list_find (port_context->current, port_context->suggested_plugin); |
| /* Schedule checking support */ |
| g_assert (port_context->defer_id == 0); |
| port_context->defer_id = g_idle_add ((GSourceFunc) port_context_defer_ready, port_context); |
| return; |
| } |
| |
| mm_dbg ("[plugin manager] task %s: deferred task completed, no suggested plugin", |
| port_context->name); |
| port_context_complete (port_context); |
| return; |
| } |
| |
| /* If no plugin being suggested, done */ |
| if (!suggested_plugin) |
| return; |
| |
| /* The GENERIC plugin is NEVER suggested to others */ |
| if (g_str_equal (mm_plugin_get_name (suggested_plugin), MM_PLUGIN_GENERIC_NAME)) |
| return; |
| |
| /* If the plugin has MM_PLUGIN_FORBIDDEN_ICERA set, we do *not* suggest |
| * the plugin to others. Icera devices may not reply to the icera probing |
| * in all ports, so if other ports need to be tested for icera support, |
| * they should all go on. */ |
| g_object_get (suggested_plugin, |
| MM_PLUGIN_FORBIDDEN_ICERA, &forbidden_icera, |
| NULL); |
| if (forbidden_icera) |
| return; |
| |
| /* We should *not* cancel probing in the port if the plugin being |
| * checked right now is not the one being suggested. Each port |
| * should run its probing independently, and we'll later decide |
| * which result applies to the whole device. |
| */ |
| mm_dbg ("[plugin manager] task %s: got suggested plugin (%s)", |
| port_context->name, mm_plugin_get_name (suggested_plugin)); |
| port_context->suggested_plugin = g_object_ref (suggested_plugin); |
| } |
| |
| static void |
| port_context_unsupported (PortContext *port_context, |
| MMPlugin *plugin) |
| { |
| g_assert (plugin); |
| |
| /* If there is no suggested plugin, go on to the next one */ |
| if (!port_context->suggested_plugin) { |
| port_context->current = g_list_next (port_context->current); |
| port_context_next (port_context); |
| return; |
| } |
| |
| /* 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 cancel the port probing and avoid more tests. |
| */ |
| if (port_context->suggested_plugin == plugin) { |
| mm_dbg ("[plugin manager] task %s: ignoring port unsupported by physical modem's plugin", |
| port_context->name); |
| port_context_complete (port_context); |
| return; |
| } |
| |
| /* 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. */ |
| port_context->current = g_list_find (port_context->current, port_context->suggested_plugin); |
| port_context_next (port_context); |
| } |
| |
| static void |
| port_context_defer (PortContext *port_context) |
| { |
| /* Try with the suggested one after being deferred */ |
| if (port_context->suggested_plugin) { |
| mm_dbg ("[plugin manager] task %s: deferring support check (%s suggested)", |
| port_context->name, mm_plugin_get_name (MM_PLUGIN (port_context->suggested_plugin))); |
| port_context->current = g_list_find (port_context->current, port_context->suggested_plugin); |
| } else |
| mm_dbg ("[plugin manager] task %s: deferring support check", |
| port_context->name); |
| |
| /* Schedule checking support. |
| * |
| * In this case we don't pass a port context reference because we're able |
| * to fully cancel the timeout ourselves. */ |
| port_context->defer_id = g_timeout_add_seconds (DEFER_TIMEOUT_SECS, |
| (GSourceFunc) port_context_defer_ready, |
| port_context); |
| } |
| |
| static void |
| port_context_defer_until_suggested (PortContext *port_context, |
| MMPlugin *plugin) |
| { |
| g_assert (plugin); |
| |
| /* If we arrived here and we already have a plugin suggested, use it */ |
| if (port_context->suggested_plugin) { |
| /* We can finish this context */ |
| if (port_context->suggested_plugin == plugin) { |
| mm_dbg ("[plugin manager] task %s: completed, got suggested plugin (%s)", |
| port_context->name, mm_plugin_get_name (port_context->suggested_plugin)); |
| /* Store best plugin and end operation */ |
| port_context->best_plugin = g_object_ref (port_context->suggested_plugin); |
| port_context_complete (port_context); |
| return; |
| } |
| |
| /* Recheck support in deferred task */ |
| mm_dbg ("[plugin manager] task %s: re-checking support on deferred task, got suggested plugin (%s)", |
| port_context->name, mm_plugin_get_name (port_context->suggested_plugin)); |
| port_context->current = g_list_find (port_context->current, port_context->suggested_plugin); |
| port_context_next (port_context); |
| return; |
| } |
| |
| /* 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 ("[plugin manager] task %s: deferring support check until result suggested", |
| port_context->name); |
| port_context->defer_until_suggested = TRUE; |
| } |
| |
| static void |
| plugin_supports_port_ready (MMPlugin *plugin, |
| GAsyncResult *res, |
| PortContext *port_context) |
| { |
| MMPluginSupportsResult support_result; |
| GError *error = NULL; |
| |
| /* Get supports check results */ |
| support_result = mm_plugin_supports_port_finish (plugin, res, &error); |
| if (error) { |
| g_assert_cmpuint (support_result, ==, MM_PLUGIN_SUPPORTS_PORT_UNKNOWN); |
| mm_warn ("[plugin manager] task %s: error when checking support with plugin '%s': '%s'", |
| port_context->name, mm_plugin_get_name (plugin), error->message); |
| g_error_free (error); |
| } |
| |
| switch (support_result) { |
| case MM_PLUGIN_SUPPORTS_PORT_SUPPORTED: |
| port_context_supported (port_context, plugin); |
| break; |
| case MM_PLUGIN_SUPPORTS_PORT_UNKNOWN: |
| case MM_PLUGIN_SUPPORTS_PORT_UNSUPPORTED: |
| port_context_unsupported (port_context, plugin); |
| break; |
| case MM_PLUGIN_SUPPORTS_PORT_DEFER: |
| port_context_defer (port_context); |
| break; |
| case MM_PLUGIN_SUPPORTS_PORT_DEFER_UNTIL_SUGGESTED: |
| port_context_defer_until_suggested (port_context, plugin); |
| break; |
| } |
| |
| /* We received a full reference, to make sure the context was always |
| * valid during the async call */ |
| port_context_unref (port_context); |
| } |
| |
| static void |
| port_context_next (PortContext *port_context) |
| { |
| MMPlugin *plugin; |
| |
| /* If we're cancelled, done */ |
| if (g_cancellable_is_cancelled (port_context->cancellable)) { |
| port_context_complete (port_context); |
| return; |
| } |
| |
| /* Already checked all plugins? */ |
| if (!port_context->current) { |
| port_context_complete (port_context); |
| return; |
| } |
| |
| /* Ask the current plugin to check support of this port. |
| * |
| * A full new reference to the port context is given as user data to the |
| * async method because we want to make sure the context is still valid |
| * once the method finishes. */ |
| plugin = MM_PLUGIN (port_context->current->data); |
| mm_dbg ("[plugin manager] task %s: checking with plugin '%s'", |
| port_context->name, mm_plugin_get_name (plugin)); |
| mm_plugin_supports_port (plugin, |
| port_context->device, |
| port_context->port, |
| port_context->cancellable, |
| (GAsyncReadyCallback) plugin_supports_port_ready, |
| port_context_ref (port_context)); |
| } |
| |
| static gboolean |
| port_context_cancel (PortContext *port_context) |
| { |
| /* Port context cancellation, which only makes sense if the context is |
| * actually being run, so just exit if it isn't. */ |
| if (!port_context->task) |
| return FALSE; |
| |
| /* If cancelled already, do nothing */ |
| if (g_cancellable_is_cancelled (port_context->cancellable)) |
| return FALSE; |
| |
| mm_dbg ("[plugin manager) task %s: cancellation requested", |
| port_context->name); |
| |
| /* The port context is cancelled now */ |
| g_cancellable_cancel (port_context->cancellable); |
| |
| /* If the task was deferred, we can cancel and complete it right away */ |
| if (port_context->defer_id) { |
| g_source_remove (port_context->defer_id); |
| port_context->defer_id = 0; |
| port_context_complete (port_context); |
| return TRUE; |
| } |
| |
| /* If the task was deferred until a result is suggested, we can also |
| * complete it right away */ |
| if (port_context->defer_until_suggested) { |
| port_context_complete (port_context); |
| return TRUE; |
| } |
| |
| /* The task may be currently checking support with a given plugin */ |
| return TRUE; |
| } |
| |
| static void |
| port_context_run (MMPluginManager *self, |
| PortContext *port_context, |
| GList *plugins, |
| MMPlugin *suggested, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| g_assert (!port_context->task); |
| g_assert (!port_context->defer_id); |
| g_assert (!port_context->plugins); |
| g_assert (!port_context->current); |
| g_assert (!port_context->suggested_plugin); |
| |
| /* Setup plugins to probe and first one to check. */ |
| port_context->plugins = g_list_copy_deep (plugins, (GCopyFunc) g_object_ref, NULL); |
| port_context->current = port_context->plugins; |
| |
| /* If we got one suggested, it will be the first one */ |
| if (suggested) { |
| port_context->suggested_plugin = g_object_ref (suggested); |
| port_context->current = g_list_find (port_context->current, port_context->suggested_plugin); |
| if (!port_context->current) |
| mm_warn ("[plugin manager] task %s: suggested plugin (%s) not among the ones to test", |
| port_context->name, mm_plugin_get_name (suggested)); |
| } |
| |
| /* Log the list of plugins found and specify which are the ones that are going |
| * to be run */ |
| { |
| gboolean suggested_found = FALSE; |
| GList *l; |
| |
| mm_dbg ("[plugin manager] task %s: found '%u' plugins to try", |
| port_context->name, g_list_length (port_context->plugins)); |
| |
| for (l = port_context->plugins; l; l = g_list_next (l)) { |
| MMPlugin *plugin; |
| |
| plugin = MM_PLUGIN (l->data); |
| if (suggested_found) { |
| mm_dbg ("[plugin manager] task %s: may try with plugin '%s'", |
| port_context->name, mm_plugin_get_name (plugin)); |
| continue; |
| } |
| if (suggested && l == port_context->current) { |
| suggested_found = TRUE; |
| mm_dbg ("[plugin manager] task %s: will try with plugin '%s' (suggested)", |
| port_context->name, mm_plugin_get_name (plugin)); |
| continue; |
| } |
| if (suggested && !suggested_found) { |
| mm_dbg ("[plugin manager] task %s: won't try with plugin '%s' (skipped)", |
| port_context->name, mm_plugin_get_name (plugin)); |
| continue; |
| } |
| mm_dbg ("[plugin manager] task %s: will try with plugin '%s'", |
| port_context->name, mm_plugin_get_name (plugin)); |
| } |
| } |
| |
| /* The full port context is now cancellable. We pass this cancellable also |
| * to the inner GTask, so that if we're cancelled we always return a |
| * cancellation error, regardless of what the standard logic does. */ |
| port_context->cancellable = g_cancellable_new (); |
| |
| /* Create a inner task for the port context. The result we expect is the |
| * best plugin found for the port. */ |
| port_context->task = g_task_new (self, port_context->cancellable, callback, user_data); |
| |
| mm_dbg ("[plugin manager) task %s: started", port_context->name); |
| |
| /* Go probe with the first plugin */ |
| port_context_next (port_context); |
| } |
| |
| static PortContext * |
| port_context_new (MMPluginManager *self, |
| const gchar *parent_name, |
| MMDevice *device, |
| MMKernelDevice *port) |
| { |
| PortContext *port_context; |
| |
| port_context = g_slice_new0 (PortContext); |
| port_context->ref_count = 1; |
| port_context->device = g_object_ref (device); |
| port_context->port = g_object_ref (port); |
| port_context->timer = g_timer_new (); |
| |
| /* Set context name */ |
| port_context->name = g_strdup_printf ("%s,%s", parent_name, mm_kernel_device_get_name (port)); |
| |
| return port_context; |
| } |
| |
| /*****************************************************************************/ |
| /* Device context */ |
| |
| /* Time to wait for ports to appear before starting to probe the first one */ |
| #define MIN_WAIT_TIME_MSECS 1500 |
| |
| /* Time to wait for other ports to appear once the first port is exposed |
| * (needs to be > MIN_WAIT_TIME_MSECS!!) */ |
| #define MIN_PROBING_TIME_MSECS 2500 |
| |
| /* The wait time we define must always be less than the probing time */ |
| G_STATIC_ASSERT (MIN_WAIT_TIME_MSECS < MIN_PROBING_TIME_MSECS); |
| |
| /* |
| * Device context |
| * |
| * This structure holds all the information related to a single device. This |
| * information includes references to all port contexts generated in the device, |
| * as well as a reference to the parent plugin manager object and the async |
| * task to complete when finished. |
| */ |
| struct _DeviceContext { |
| /* Reference counting */ |
| volatile gint ref_count; |
| /* The name of the context */ |
| gchar *name; |
| /* The plugin manager */ |
| MMPluginManager *self; |
| /* The device for which we're looking support */ |
| MMDevice *device; |
| |
| /* The operation task */ |
| GTask *task; |
| /* Internal ancellable */ |
| GCancellable *cancellable; |
| |
| /* Timer tracking how much time is required for the device support check */ |
| GTimer *timer; |
| |
| /* The best plugin at a given moment. Once the last port task finishes, this |
| * will be the one being returned in the async result */ |
| MMPlugin *best_plugin; |
| |
| /* Minimum wait time. No port probing can start before this timeout expires. |
| * Once the timeout is expired, the id is reset to 0. */ |
| guint min_wait_time_id; |
| /* Port support check contexts waiting to be run after min wait time */ |
| GList *wait_port_contexts; |
| |
| /* Minimum probing_time. The device support check task cannot be finished |
| * before this timeout expires. Once the timeout is expired, the id is reset |
| * to 0. */ |
| guint min_probing_time_id; |
| |
| /* Signal connection ids for the grabbed/released signals from the device. |
| * These are the signals that will give us notifications of what ports are |
| * available (or suddenly unavailable) in the device. */ |
| gulong grabbed_id; |
| gulong released_id; |
| |
| /* Port support check contexts being run */ |
| GList *port_contexts; |
| }; |
| |
| static void |
| device_context_unref (DeviceContext *device_context) |
| { |
| if (g_atomic_int_dec_and_test (&device_context->ref_count)) { |
| /* When the last reference is gone there must be no source scheduled and no |
| * pending port tasks. */ |
| g_assert (!device_context->grabbed_id); |
| g_assert (!device_context->released_id); |
| g_assert (!device_context->min_wait_time_id); |
| g_assert (!device_context->min_probing_time_id); |
| g_assert (!device_context->port_contexts); |
| |
| /* The device support check task must have been completed previously */ |
| g_assert (!device_context->task); |
| |
| g_free (device_context->name); |
| g_timer_destroy (device_context->timer); |
| if (device_context->cancellable) |
| g_object_unref (device_context->cancellable); |
| if (device_context->best_plugin) |
| g_object_unref (device_context->best_plugin); |
| g_object_unref (device_context->device); |
| g_object_unref (device_context->self); |
| g_slice_free (DeviceContext, device_context); |
| } |
| } |
| |
| static DeviceContext * |
| device_context_ref (DeviceContext *device_context) |
| { |
| g_atomic_int_inc (&device_context->ref_count); |
| return device_context; |
| } |
| |
| static PortContext * |
| device_context_peek_running_port_context (DeviceContext *device_context, |
| MMKernelDevice *port) |
| { |
| GList *l; |
| |
| for (l = device_context->port_contexts; l; l = g_list_next (l)) { |
| PortContext *port_context; |
| |
| port_context = (PortContext *)(l->data); |
| if ((port_context->port == port) || |
| (!g_strcmp0 (mm_kernel_device_get_name (port_context->port), mm_kernel_device_get_name (port)))) |
| return port_context; |
| } |
| return NULL; |
| } |
| |
| static PortContext * |
| device_context_peek_waiting_port_context (DeviceContext *device_context, |
| MMKernelDevice *port) |
| { |
| GList *l; |
| |
| for (l = device_context->wait_port_contexts; l; l = g_list_next (l)) { |
| PortContext *port_context; |
| |
| port_context = (PortContext *)(l->data); |
| if ((port_context->port == port) || |
| (!g_strcmp0 (mm_kernel_device_get_name (port_context->port), mm_kernel_device_get_name (port)))) |
| return port_context; |
| } |
| return NULL; |
| } |
| |
| static MMPlugin * |
| device_context_run_finish (MMPluginManager *self, |
| GAsyncResult *res, |
| GError **error) |
| { |
| return MM_PLUGIN (g_task_propagate_pointer (G_TASK (res), error)); |
| } |
| |
| static void |
| device_context_complete (DeviceContext *device_context) |
| { |
| GTask *task; |
| |
| /* Steal the task from the context */ |
| g_assert (device_context->task); |
| task = device_context->task; |
| device_context->task = NULL; |
| |
| /* Log about the time required to complete the checks */ |
| mm_dbg ("[plugin manager] task %s: finished in '%lf' seconds", |
| device_context->name, g_timer_elapsed (device_context->timer, NULL)); |
| |
| /* Remove signal handlers */ |
| if (device_context->grabbed_id) { |
| g_signal_handler_disconnect (device_context->device, device_context->grabbed_id); |
| device_context->grabbed_id = 0; |
| } |
| if (device_context->released_id) { |
| g_signal_handler_disconnect (device_context->device, device_context->released_id); |
| device_context->released_id = 0; |
| } |
| |
| /* Remove timeouts, if still around */ |
| if (device_context->min_wait_time_id) { |
| g_source_remove (device_context->min_wait_time_id); |
| device_context->min_wait_time_id = 0; |
| } |
| if (device_context->min_probing_time_id) { |
| g_source_remove (device_context->min_probing_time_id); |
| device_context->min_probing_time_id = 0; |
| } |
| |
| /* Task completion */ |
| if (!device_context->best_plugin) |
| g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED, |
| "not supported by any plugin"); |
| else |
| g_task_return_pointer (task, g_object_ref (device_context->best_plugin), (GDestroyNotify) g_object_unref); |
| g_object_unref (task); |
| } |
| |
| static void |
| device_context_suggest_plugin (DeviceContext *device_context, |
| PortContext *port_context, |
| MMPlugin *suggested_plugin) |
| { |
| GList *l; |
| GList *listdup; |
| |
| /* If the suggested plugin is NULL, we'll propagate the suggestion only if all |
| * the port contexts are deferred until suggested. */ |
| if (!suggested_plugin) { |
| for (l = device_context->port_contexts; l; l = g_list_next (l)) { |
| PortContext *other_port_context = (PortContext *)(l->data); |
| |
| /* Do not propagate NULL if we find at least one probe which is not |
| * waiting for the suggested plugin */ |
| if (other_port_context != port_context && !other_port_context->defer_until_suggested) |
| return; |
| } |
| } |
| |
| /* Do the suggestion propagation. |
| * Shallow copy, just so that we can iterate safely without worrying about the |
| * original list being modified while hte completions happen */ |
| listdup = g_list_copy (device_context->port_contexts); |
| for (l = listdup; l; l = g_list_next (l)) |
| port_context_set_suggestion ((PortContext *)(l->data), suggested_plugin); |
| g_list_free (listdup); |
| } |
| |
| static void |
| device_context_set_best_plugin (DeviceContext *device_context, |
| PortContext *port_context, |
| MMPlugin *best_plugin) |
| { |
| if (!best_plugin) { |
| /* If the port appeared after an already probed port, which decided that |
| * the Generic plugin was the best one (which is by default not initially |
| * suggested), we'll end up arriving here. Don't ignore it, it may well |
| * be a wwan port that we do need to grab. */ |
| if (device_context->best_plugin) { |
| mm_dbg ("[plugin manager] task %s: assuming port can be handled by the '%s' plugin", |
| port_context->name, mm_plugin_get_name (device_context->best_plugin)); |
| return; |
| } |
| |
| /* Unsupported error, this is generic when we cannot find a plugin */ |
| mm_dbg ("[plugin manager] task %s: not supported by any plugin" , |
| port_context->name); |
| |
| /* Tell the device to ignore this port */ |
| mm_device_ignore_port (device_context->device, port_context->port); |
| |
| /* If this is the last valid probe which was running (i.e. the last one |
| * not being deferred-until-suggested), cancel all remaining ones. */ |
| device_context_suggest_plugin (device_context, port_context, NULL); |
| return; |
| } |
| |
| /* Store the plugin as the best one in the device if this is the first |
| * result we got. Also, if the previously suggested plugin was the GENERIC |
| * one and now we're reporting a more specific one, use the new one. |
| */ |
| if (!device_context->best_plugin || |
| (g_str_equal (mm_plugin_get_name (device_context->best_plugin), MM_PLUGIN_GENERIC_NAME) && |
| device_context->best_plugin != best_plugin)) { |
| /* Only log best plugin if it's not the generic one */ |
| if (!g_str_equal (mm_plugin_get_name (best_plugin), MM_PLUGIN_GENERIC_NAME)) |
| mm_dbg ("[plugin manager] task %s: found best plugin: %s", |
| port_context->name, mm_plugin_get_name (best_plugin)); |
| /* Store and suggest this plugin also to other port probes */ |
| device_context->best_plugin = g_object_ref (best_plugin); |
| device_context_suggest_plugin (device_context, port_context, best_plugin); |
| return; |
| } |
| |
| /* Warn if the best plugin found for this port differs from the |
| * best plugin found for the the first probed port */ |
| if (!g_str_equal (mm_plugin_get_name (device_context->best_plugin), mm_plugin_get_name (best_plugin))) { |
| /* Icera modems may not reply to the icera probing in all ports. We handle this by |
| * checking the forbidden/allowed icera flags in both the current and the expected |
| * plugins. If either of these plugins requires icera and the other doesn't, we |
| * pick the Icera one as best plugin. */ |
| gboolean previous_forbidden_icera; |
| gboolean previous_allowed_icera; |
| gboolean new_forbidden_icera; |
| gboolean new_allowed_icera; |
| |
| g_object_get (device_context->best_plugin, |
| MM_PLUGIN_ALLOWED_ICERA, &previous_allowed_icera, |
| MM_PLUGIN_FORBIDDEN_ICERA, &previous_forbidden_icera, |
| NULL); |
| g_assert (previous_allowed_icera == FALSE || previous_forbidden_icera == FALSE); |
| |
| g_object_get (best_plugin, |
| MM_PLUGIN_ALLOWED_ICERA, &new_allowed_icera, |
| MM_PLUGIN_FORBIDDEN_ICERA, &new_forbidden_icera, |
| NULL); |
| g_assert (new_allowed_icera == FALSE || new_forbidden_icera == FALSE); |
| |
| if (previous_allowed_icera && new_forbidden_icera) |
| mm_warn ("[plugin manager] task %s: will use plugin '%s' instead of '%s', modem is icera-capable", |
| port_context->name, |
| mm_plugin_get_name (device_context->best_plugin), |
| mm_plugin_get_name (best_plugin)); |
| else if (new_allowed_icera && previous_forbidden_icera) { |
| mm_warn ("[plugin manager] task %s: overriding previously selected device plugin '%s' with '%s', modem is icera-capable", |
| port_context->name, |
| mm_plugin_get_name (device_context->best_plugin), |
| mm_plugin_get_name (best_plugin)); |
| g_object_unref (device_context->best_plugin); |
| device_context->best_plugin = g_object_ref (best_plugin); |
| } else |
| mm_warn ("[plugin manager] task %s: plugin mismatch error (device reports '%s', port reports '%s')", |
| port_context->name, |
| mm_plugin_get_name (device_context->best_plugin), |
| mm_plugin_get_name (best_plugin)); |
| return; |
| } |
| |
| /* Device plugin equal to best plugin */ |
| mm_dbg ("[plugin manager] task %s: best plugin matches device reported one: %s", |
| port_context->name, mm_plugin_get_name (best_plugin)); |
| } |
| |
| static void |
| device_context_continue (DeviceContext *device_context) |
| { |
| GList *l; |
| GString *s = NULL; |
| guint n = 0; |
| guint n_active = 0; |
| |
| /* If there are no running port contexts around, we're free to finish */ |
| if (!device_context->port_contexts) { |
| mm_dbg ("[plugin manager] task %s: no more ports to probe", device_context->name); |
| device_context_complete (device_context); |
| return; |
| } |
| |
| /* We'll count how many port contexts are 'active' (i.e. not deferred |
| * until a result is suggested). Also, prepare to log about the pending |
| * ports */ |
| for (l = device_context->port_contexts; l; l = g_list_next (l)) { |
| PortContext *port_context = (PortContext *) (l->data); |
| const gchar *portname; |
| |
| portname = mm_kernel_device_get_name (port_context->port); |
| if (!s) |
| s = g_string_new (portname); |
| else |
| g_string_append_printf (s, ", %s", portname); |
| |
| /* Active? */ |
| if (!port_context->defer_until_suggested) |
| n_active++; |
| n++; |
| } |
| |
| g_assert (n > 0 && s); |
| mm_dbg ("[plugin Manager] task %s: still %u running probes (%u active): %s", |
| device_context->name, n, n_active, s->str); |
| g_string_free (s, TRUE); |
| |
| if (n_active == 0) { |
| mm_dbg ("[plugin manager] task %s: no active tasks to probe", device_context->name); |
| device_context_suggest_plugin (device_context, NULL, NULL); |
| } |
| } |
| |
| static void |
| port_context_run_ready (MMPluginManager *self, |
| GAsyncResult *res, |
| CommonAsyncContext *common) |
| { |
| GError *error = NULL; |
| MMPlugin *best_plugin; |
| |
| /* Returns a full reference to the best plugin */ |
| best_plugin = port_context_run_finish (self, res, &error); |
| if (!best_plugin) { |
| /* The only error we can ignore is UNSUPPORTED */ |
| if (g_error_matches (error, MM_CORE_ERROR, MM_CORE_ERROR_UNSUPPORTED)) { |
| /* This error is not critical */ |
| device_context_set_best_plugin (common->device_context, common->port_context, NULL); |
| } else |
| mm_warn ("[plugin manager] task %s: failed: %s", common->port_context->name, error->message); |
| g_error_free (error); |
| } else { |
| /* Set the plugin as the best one in the device context */ |
| device_context_set_best_plugin (common->device_context, common->port_context, best_plugin); |
| g_object_unref (best_plugin); |
| } |
| |
| /* We MUST have the port context in the list at this point, because we're |
| * going to remove the reference, so assert if this is not true. The caller |
| * must always make sure that the port_context is available in the list */ |
| g_assert (g_list_find (common->device_context->port_contexts, common->port_context)); |
| common->device_context->port_contexts = g_list_remove (common->device_context->port_contexts, |
| common->port_context); |
| port_context_unref (common->port_context); |
| |
| /* Continue the device context logic */ |
| device_context_continue (common->device_context); |
| |
| /* Cleanup the context of the async operation */ |
| common_async_context_free (common); |
| } |
| |
| static gboolean |
| device_context_min_probing_time_elapsed (DeviceContext *device_context) |
| { |
| device_context->min_probing_time_id = 0; |
| |
| mm_dbg ("[plugin manager] task %s: min probing time elapsed", device_context->name); |
| |
| /* Wakeup the device context logic */ |
| device_context_continue (device_context); |
| return G_SOURCE_REMOVE; |
| } |
| |
| static void |
| device_context_run_port_context (DeviceContext *device_context, |
| PortContext *port_context) |
| { |
| GList *plugins; |
| MMPlugin *suggested = NULL; |
| MMPluginManager *self; |
| |
| /* Recover plugin manager */ |
| self = MM_PLUGIN_MANAGER (device_context->self); |
| |
| /* Setup plugins to probe and first one to check. |
| * Make sure this plugins list is built after the MIN WAIT TIME has been expired |
| * (so that per-driver filters work correctly) */ |
| plugins = plugin_manager_build_plugins_list (self, device_context->device, port_context->port); |
| |
| /* If we got one already set in the device context, it will be the first one, |
| * unless it is the generic plugin */ |
| if (device_context->best_plugin && |
| !g_str_equal (mm_plugin_get_name (device_context->best_plugin), MM_PLUGIN_GENERIC_NAME)) { |
| suggested = device_context->best_plugin; |
| } |
| |
| port_context_run (self, |
| port_context, |
| plugins, |
| suggested, |
| (GAsyncReadyCallback) port_context_run_ready, |
| common_async_context_new (self, |
| device_context, |
| port_context, |
| NULL)); |
| g_list_free_full (plugins, g_object_unref); |
| } |
| |
| static gboolean |
| device_context_min_wait_time_elapsed (DeviceContext *device_context) |
| { |
| MMPluginManager *self; |
| GList *l; |
| |
| /* Recover plugin manager */ |
| self = MM_PLUGIN_MANAGER (device_context->self); |
| |
| device_context->min_wait_time_id = 0; |
| mm_dbg ("[plugin manager] task %s: min wait time elapsed", device_context->name); |
| |
| /* Move list of port contexts out of the wait list */ |
| g_assert (!device_context->port_contexts); |
| device_context->port_contexts = device_context->wait_port_contexts; |
| device_context->wait_port_contexts = NULL; |
| |
| /* Launch supports check for each port in the Plugin Manager */ |
| for (l = device_context->port_contexts; l; l = g_list_next (l)) |
| device_context_run_port_context (device_context, (PortContext *)(l->data)); |
| |
| return G_SOURCE_REMOVE; |
| } |
| |
| static void |
| device_context_port_released (DeviceContext *device_context, |
| MMKernelDevice *port) |
| { |
| PortContext *port_context; |
| |
| mm_dbg ("[plugin manager] task %s: port released: %s", |
| device_context->name, mm_kernel_device_get_name (port)); |
| |
| /* Check if there's a waiting port context */ |
| port_context = device_context_peek_waiting_port_context (device_context, port); |
| if (port_context) { |
| /* We didn't run the port context yet, we can remove it right away */ |
| device_context->wait_port_contexts = g_list_remove (device_context->wait_port_contexts, port_context); |
| port_context_unref (port_context); |
| return; |
| } |
| |
| /* Now, check running port contexts, which will need cancellation if found */ |
| port_context = device_context_peek_running_port_context (device_context, port); |
| if (port_context) { |
| /* Request cancellation of this single port, will be completed asynchronously */ |
| port_context_cancel (port_context); |
| return; |
| } |
| |
| /* This is not something worth warning. If the probing task has already |
| * been finished, it will already be removed from the list */ |
| mm_dbg ("[plugin manager] task %s: port wasn't found: %s", |
| device_context->name, mm_kernel_device_get_name (port)); |
| } |
| |
| static void |
| device_context_port_grabbed (DeviceContext *device_context, |
| MMKernelDevice *port) |
| { |
| MMPluginManager *self; |
| PortContext *port_context; |
| |
| /* Recover plugin manager */ |
| self = MM_PLUGIN_MANAGER (device_context->self); |
| |
| mm_dbg ("[plugin manager] task %s: port grabbed: %s", |
| device_context->name, mm_kernel_device_get_name (port)); |
| |
| /* Ignore if for any reason we still have it in the running list */ |
| port_context = device_context_peek_running_port_context (device_context, port); |
| if (port_context) { |
| mm_warn ("[plugin manager] task %s: port context already being processed", |
| device_context->name); |
| return; |
| } |
| |
| /* Ignore if for any reason we still have it in the waiting list */ |
| port_context = device_context_peek_waiting_port_context (device_context, port); |
| if (port_context) { |
| mm_warn ("[plugin manager] task %s: port context already scheduled", |
| device_context->name); |
| return; |
| } |
| |
| /* Setup a new port context for the newly grabbed port */ |
| port_context = port_context_new (self, |
| device_context->name, |
| device_context->device, |
| port); |
| |
| mm_dbg ("[plugin manager] task %s: new support task for port", |
| port_context->name); |
| |
| /* ÃŽf still waiting the min wait time, store it in the waiting list */ |
| if (device_context->min_wait_time_id) { |
| mm_dbg ("[plugin manager) task %s: deferred until min wait time elapsed", |
| port_context->name); |
| /* Store the port reference in the list within the device */ |
| device_context->wait_port_contexts = g_list_prepend (device_context->wait_port_contexts, port_context); |
| return; |
| } |
| |
| /* Store the port reference in the list within the device */ |
| device_context->port_contexts = g_list_prepend (device_context->port_contexts, port_context) ; |
| |
| /* If the port has been grabbed after the min wait timeout expired, launch |
| * probing directly */ |
| device_context_run_port_context (device_context, port_context); |
| } |
| |
| static gboolean |
| device_context_cancel (DeviceContext *device_context) |
| { |
| /* If cancelled already, do nothing */ |
| if (g_cancellable_is_cancelled (device_context->cancellable)) |
| return FALSE; |
| |
| mm_dbg ("[plugin manager) task %s: cancellation requested", |
| device_context->name); |
| |
| /* The device context is cancelled now */ |
| g_cancellable_cancel (device_context->cancellable); |
| |
| /* Remove all port contexts in the waiting list. This will allow early cancellation |
| * if it arrives before the min wait time has elapsed */ |
| if (device_context->wait_port_contexts) { |
| g_assert (!device_context->port_contexts); |
| g_list_free_full (device_context->wait_port_contexts, (GDestroyNotify) port_context_unref); |
| device_context->wait_port_contexts = NULL; |
| } |
| |
| /* Cancel all ongoing port contexts, if they're not already cancelled */ |
| if (device_context->port_contexts) { |
| g_assert (!device_context->wait_port_contexts); |
| /* Request cancellation, will be completed asynchronously */ |
| g_list_foreach (device_context->port_contexts, (GFunc) port_context_cancel, NULL); |
| } |
| |
| /* Wakeup the device context logic. If we were still waiting for the |
| * min probing time, this will complete the device context. */ |
| device_context_continue (device_context); |
| return TRUE; |
| } |
| |
| static void |
| device_context_run (MMPluginManager *self, |
| DeviceContext *device_context, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| g_assert (!device_context->task); |
| g_assert (!device_context->grabbed_id); |
| g_assert (!device_context->released_id); |
| g_assert (!device_context->min_wait_time_id); |
| g_assert (!device_context->min_probing_time_id); |
| |
| /* Connect to device port grabbed/released notifications from the device */ |
| device_context->grabbed_id = g_signal_connect_swapped (device_context->device, |
| MM_DEVICE_PORT_GRABBED, |
| G_CALLBACK (device_context_port_grabbed), |
| device_context); |
| device_context->released_id = g_signal_connect_swapped (device_context->device, |
| MM_DEVICE_PORT_RELEASED, |
| G_CALLBACK (device_context_port_released), |
| device_context); |
| |
| /* Set the initial waiting timeout. We don't want to probe any port before |
| * this timeout expires, so that we get as many ports added in the device |
| * as possible. If we don't do this, some plugin filters won't work properly, |
| * like the 'forbidden-drivers' one. |
| */ |
| device_context->min_wait_time_id = g_timeout_add (MIN_WAIT_TIME_MSECS, |
| (GSourceFunc) device_context_min_wait_time_elapsed, |
| device_context); |
| |
| /* Set the initial probing timeout. We force the probing time of the device to |
| * be at least this amount of time, so that the kernel has enough time to |
| * bring up ports. Given that we launch this only when the first port of the |
| * device has been exposed in udev, this timeout effectively means that we |
| * leave up to 2s to the remaining ports to appear. |
| */ |
| device_context->min_probing_time_id = g_timeout_add (MIN_PROBING_TIME_MSECS, |
| (GSourceFunc) device_context_min_probing_time_elapsed, |
| device_context); |
| |
| /* The full device context is now cancellable. We pass this cancellable also |
| * to the inner GTask, so that if we're cancelled we always return a |
| * cancellation error, regardless of what the standard logic does. */ |
| device_context->cancellable = g_cancellable_new (); |
| |
| /* Create a inner task for the device context. We'll complete this task when |
| * the last port has been probed. */ |
| device_context->task = g_task_new (self, device_context->cancellable, callback, user_data); |
| } |
| |
| static DeviceContext * |
| device_context_new (MMPluginManager *self, |
| MMDevice *device) |
| { |
| static gulong unique_task_id = 0; |
| DeviceContext *device_context; |
| |
| /* Create new device context and store the task */ |
| device_context = g_slice_new0 (DeviceContext); |
| device_context->ref_count = 1; |
| device_context->self = g_object_ref (self); |
| device_context->device = g_object_ref (device); |
| device_context->timer = g_timer_new (); |
| |
| /* Set context name (just for logging) */ |
| device_context->name = g_strdup_printf ("%lu", unique_task_id++); |
| |
| return device_context; |
| } |
| |
| /*****************************************************************************/ |
| /* Look for plugin to support the given device |
| * |
| * This operation is initiated when the new MMDevice is detected. Once that |
| * happens, a new support check for the whole device will arrive at the plugin |
| * manager. |
| * |
| * Ports in the device, though, are added dynamically and automatically |
| * afterwards once the device support check has been created. It is the device |
| * support check context itself adding the newly added ports. |
| * |
| * The device support check task is finished once all port support check tasks |
| * have also been finished. |
| * |
| * Given that the ports are added dynamically, there is some minimum duration |
| * for the device support check task, otherwise we may end up not detecting |
| * any port. |
| * |
| * The device support check tasks are stored also in the plugin manager, so |
| * that the cancellation API doesn't require anything more specific than the |
| * device for which the support check task should be cancelled. |
| */ |
| |
| MMPlugin * |
| mm_plugin_manager_device_support_check_finish (MMPluginManager *self, |
| GAsyncResult *res, |
| GError **error) |
| { |
| return MM_PLUGIN (g_task_propagate_pointer (G_TASK (res), error)); |
| } |
| |
| static DeviceContext * |
| plugin_manager_peek_device_context (MMPluginManager *self, |
| MMDevice *device) |
| { |
| GList *l; |
| |
| for (l = self->priv->device_contexts; l; l = g_list_next (l)) { |
| DeviceContext *device_context; |
| |
| device_context = (DeviceContext *)(l->data); |
| if ((device == device_context->device) || |
| (!g_strcmp0 (mm_device_get_uid (device_context->device), mm_device_get_uid (device)))) |
| return device_context; |
| } |
| return NULL; |
| } |
| |
| gboolean |
| mm_plugin_manager_device_support_check_cancel (MMPluginManager *self, |
| MMDevice *device) |
| { |
| DeviceContext *device_context; |
| |
| /* If the device context isn't found, ignore the cancellation request. */ |
| device_context = plugin_manager_peek_device_context (self, device); |
| if (!device_context) |
| return FALSE; |
| |
| /* Request cancellation, will be completed asynchronously */ |
| return device_context_cancel (device_context); |
| } |
| |
| static void |
| device_context_run_ready (MMPluginManager *self, |
| GAsyncResult *res, |
| CommonAsyncContext *common) |
| { |
| GError *error = NULL; |
| MMPlugin *best_plugin; |
| |
| /* We get a full reference back */ |
| best_plugin = device_context_run_finish (self, res, &error); |
| |
| /* |
| * Once the task is finished, we can also remove it from the plugin manager |
| * list. We MUST have the port context in the list at this point, because |
| * we're going to dispose the reference, so assert if this is not true. |
| */ |
| g_assert (g_list_find (common->self->priv->device_contexts, common->device_context)); |
| common->self->priv->device_contexts = g_list_remove (common->self->priv->device_contexts, |
| common->device_context); |
| device_context_unref (common->device_context); |
| |
| /* Report result or error once removed from our internal list */ |
| if (!best_plugin) |
| g_task_return_error (common->task, error); |
| else |
| g_task_return_pointer (common->task, best_plugin, (GDestroyNotify) g_object_unref); |
| common_async_context_free (common); |
| } |
| |
| void |
| mm_plugin_manager_device_support_check (MMPluginManager *self, |
| MMDevice *device, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| DeviceContext *device_context; |
| GTask *task; |
| |
| /* |
| * Create a new task for the device support check request. |
| * |
| * Note that we handle cancellations ourselves, as we don't want the caller |
| * to be required to keep track of a GCancellable for each of these tasks. |
| */ |
| task = g_task_new (G_OBJECT (self), NULL, callback, user_data); |
| |
| /* Fail if there is already a task for the same device */ |
| device_context = plugin_manager_peek_device_context (self, device); |
| if (device_context) { |
| g_task_return_new_error (task, MM_CORE_ERROR, MM_CORE_ERROR_IN_PROGRESS, |
| "Device support check task already available for device '%s'", |
| mm_device_get_uid (device)); |
| g_object_unref (task); |
| return; |
| } |
| |
| /* Create new device context */ |
| device_context = device_context_new (self, device); |
| |
| /* Track the device context in the list within the plugin manager. */ |
| self->priv->device_contexts = g_list_prepend (self->priv->device_contexts, device_context); |
| |
| mm_dbg ("[plugin manager] task %s: new support task for device: %s", |
| device_context->name, mm_device_get_uid (device_context->device)); |
| |
| /* Run device context */ |
| device_context_run (self, |
| device_context, |
| (GAsyncReadyCallback) device_context_run_ready, |
| common_async_context_new (self, |
| device_context, |
| NULL, |
| task)); |
| g_object_unref (task); |
| } |
| |
| /*****************************************************************************/ |
| /* Look for plugin */ |
| |
| MMPlugin * |
| mm_plugin_manager_peek_plugin (MMPluginManager *self, |
| const gchar *plugin_name) |
| { |
| GList *l; |
| |
| if (self->priv->generic && g_str_equal (plugin_name, mm_plugin_get_name (self->priv->generic))) |
| return self->priv->generic; |
| |
| for (l = self->priv->plugins; l; l = g_list_next (l)) { |
| MMPlugin *plugin = MM_PLUGIN (l->data); |
| |
| if (g_str_equal (plugin_name, mm_plugin_get_name (plugin))) |
| return plugin; |
| } |
| |
| return NULL; |
| } |
| |
| /*****************************************************************************/ |
| |
| 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) { |
| mm_warn ("[plugin manager] 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)) { |
| mm_warn ("[plugin manager] could not load plugin '%s': Missing major version info", path_display); |
| goto out; |
| } |
| |
| if (*major_plugin_version != MM_PLUGIN_MAJOR_VERSION) { |
| mm_warn ("[plugin manager] 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)) { |
| mm_warn ("[plugin manager] could not load plugin '%s': Missing minor version info", path_display); |
| goto out; |
| } |
| |
| if (*minor_plugin_version != MM_PLUGIN_MINOR_VERSION) { |
| mm_warn ("[plugin manager] 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)) { |
| mm_warn ("[plugin manager] 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 ("[plugin manager] could not load plugin '%s': initialization failed", path_display); |
| |
| out: |
| if (module && !plugin) |
| g_module_close (module); |
| |
| g_free (path_display); |
| |
| return plugin; |
| } |
| |
| static gboolean |
| load_plugins (MMPluginManager *self, |
| GError **error) |
| { |
| GDir *dir = NULL; |
| const gchar *fname; |
| gchar *plugindir_display = NULL; |
| |
| if (!g_module_supported ()) { |
| g_set_error (error, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_UNSUPPORTED, |
| "modules are not supported on your platform!"); |
| goto out; |
| } |
| |
| /* Get printable UTF-8 string of the path */ |
| plugindir_display = g_filename_display_name (self->priv->plugin_dir); |
| |
| mm_dbg ("[plugin manager] looking for plugins in '%s'", plugindir_display); |
| dir = g_dir_open (self->priv->plugin_dir, 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 (self->priv->plugin_dir, fname); |
| plugin = load_plugin (path); |
| g_free (path); |
| |
| if (!plugin) |
| continue; |
| |
| mm_dbg ("[plugin manager] loaded plugin '%s'", mm_plugin_get_name (plugin)); |
| |
| if (g_str_equal (mm_plugin_get_name (plugin), MM_PLUGIN_GENERIC_NAME)) |
| /* Generic plugin */ |
| self->priv->generic = plugin; |
| else |
| /* Vendor specific plugin */ |
| self->priv->plugins = g_list_append (self->priv->plugins, plugin); |
| } |
| |
| /* Check the generic plugin once all looped */ |
| if (!self->priv->generic) |
| mm_warn ("[plugin manager] generic plugin not loaded"); |
| |
| /* Treat as error if we don't find any plugin */ |
| if (!self->priv->plugins && !self->priv->generic) { |
| g_set_error (error, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_NO_PLUGINS, |
| "no plugins found in plugin directory '%s'", |
| plugindir_display); |
| goto out; |
| } |
| |
| mm_dbg ("[plugin manager] successfully loaded %u plugins", |
| g_list_length (self->priv->plugins) + !!self->priv->generic); |
| |
| out: |
| if (dir) |
| g_dir_close (dir); |
| g_free (plugindir_display); |
| |
| /* Return TRUE if at least one plugin found */ |
| return (self->priv->plugins || self->priv->generic); |
| } |
| |
| MMPluginManager * |
| mm_plugin_manager_new (const gchar *plugin_dir, |
| GError **error) |
| { |
| return g_initable_new (MM_TYPE_PLUGIN_MANAGER, |
| NULL, |
| error, |
| MM_PLUGIN_MANAGER_PLUGIN_DIR, plugin_dir, |
| 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); |
| } |
| |
| static void |
| set_property (GObject *object, |
| guint prop_id, |
| const GValue *value, |
| GParamSpec *pspec) |
| { |
| MMPluginManagerPrivate *priv = MM_PLUGIN_MANAGER (object)->priv; |
| |
| switch (prop_id) { |
| case PROP_PLUGIN_DIR: |
| g_free (priv->plugin_dir); |
| priv->plugin_dir = g_value_dup_string (value); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static void |
| get_property (GObject *object, |
| guint prop_id, |
| GValue *value, |
| GParamSpec *pspec) |
| { |
| MMPluginManagerPrivate *priv = MM_PLUGIN_MANAGER (object)->priv; |
| |
| switch (prop_id) { |
| case PROP_PLUGIN_DIR: |
| g_value_set_string (value, priv->plugin_dir); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| 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 |
| dispose (GObject *object) |
| { |
| MMPluginManager *self = MM_PLUGIN_MANAGER (object); |
| |
| /* Cleanup list of plugins */ |
| if (self->priv->plugins) { |
| g_list_free_full (self->priv->plugins, (GDestroyNotify)g_object_unref); |
| self->priv->plugins = NULL; |
| } |
| g_clear_object (&self->priv->generic); |
| |
| g_free (self->priv->plugin_dir); |
| self->priv->plugin_dir = NULL; |
| |
| G_OBJECT_CLASS (mm_plugin_manager_parent_class)->dispose (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->dispose = dispose; |
| object_class->set_property = set_property; |
| object_class->get_property = get_property; |
| |
| /* Properties */ |
| g_object_class_install_property |
| (object_class, PROP_PLUGIN_DIR, |
| g_param_spec_string (MM_PLUGIN_MANAGER_PLUGIN_DIR, |
| "Plugin directory", |
| "Where to look for plugins", |
| NULL, |
| G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); |
| } |