diff options
author | Aleksander Morgado <aleksander@aleksander.es> | 2016-01-11 19:46:25 +0100 |
---|---|---|
committer | Aleksander Morgado <aleksander@aleksander.es> | 2016-03-09 23:59:46 +0100 |
commit | fd4fdbf21b20983cdd46fc925389dd96ca3a99d3 (patch) | |
tree | 64e61dcfdbf201f86658ad26ead0ec133401147b /src/mm-plugin-manager.c | |
parent | be9b5e0141bbebdff0694a6a664e1074448c3618 (diff) |
plugin-manager: refactor device/port support checks and allow cancellations
Diffstat (limited to 'src/mm-plugin-manager.c')
-rw-r--r-- | src/mm-plugin-manager.c | 1732 |
1 files changed, 1179 insertions, 553 deletions
diff --git a/src/mm-plugin-manager.c b/src/mm-plugin-manager.c index 187c0159..636af09b 100644 --- a/src/mm-plugin-manager.c +++ b/src/mm-plugin-manager.c @@ -29,16 +29,6 @@ #include "mm-plugin.h" #include "mm-log.h" -/* Default time to defer probing checks */ -#define DEFER_TIMEOUT_SECS 3 - -/* 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 - static void initable_iface_init (GInitableIface *iface); G_DEFINE_TYPE_EXTENDED (MMPluginManager, mm_plugin_manager, G_TYPE_OBJECT, 0, @@ -61,286 +51,253 @@ struct _MMPluginManagerPrivate { GList *plugins; /* Last, the generic plugin. */ MMPlugin *generic; + + /* List of ongoing device support checks */ + GList *device_contexts; }; /*****************************************************************************/ -/* Look for plugin */ +/* Build plugin list for a single port */ -MMPlugin * -mm_plugin_manager_peek_plugin (MMPluginManager *self, - const gchar *plugin_name) +static GList * +plugin_manager_build_plugins_list (MMPluginManager *self, + MMDevice *device, + GUdevDevice *port) { + GList *list = NULL; GList *l; + gboolean supported_found = FALSE; - 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); + for (l = self->priv->plugins; l && !supported_found; l = g_list_next (l)) { + MMPluginSupportsHint hint; - if (g_str_equal (plugin_name, mm_plugin_get_name (plugin))) - return plugin; + 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(); + } } - return NULL; + /* 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; } /*****************************************************************************/ -/* Find device support */ +/* 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; - MMDevice *device; - GSimpleAsyncResult *result; - GTimer *timer; - guint timeout_id; - gulong grabbed_id; - gulong released_id; + DeviceContext *device_context; + PortContext *port_context; + GTask *task; +} CommonAsyncContext; - gboolean min_wait_timeout_expired; - GList *running_probes; -} FindDeviceSupportContext; +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); +} -typedef struct { - FindDeviceSupportContext *parent_ctx; +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 GUDev reported port object */ GUdevDevice *port; - guint initial_timeout_id; + /* 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; -} PortProbeContext; - -static void port_probe_context_step (PortProbeContext *port_probe_ctx); -static void suggest_port_probe_result (FindDeviceSupportContext *ctx, - PortProbeContext *origin, - MMPlugin *suggested_plugin); +}; static void -port_probe_context_free (PortProbeContext *ctx) +port_context_unref (PortContext *port_context) { - g_assert (ctx->defer_id == 0); - - if (ctx->best_plugin) - g_object_unref (ctx->best_plugin); - if (ctx->suggested_plugin) - g_object_unref (ctx->suggested_plugin); - if (ctx->plugins) - g_list_free_full (ctx->plugins, (GDestroyNotify)g_object_unref); - g_object_unref (ctx->port); - g_slice_free (PortProbeContext, ctx); + 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 void -find_device_support_context_complete_and_free (FindDeviceSupportContext *ctx) +static PortContext * +port_context_ref (PortContext *port_context) { - g_assert (ctx->timeout_id == 0); - - mm_dbg ("(Plugin Manager) [%s] device support check finished in '%lf' seconds", - mm_device_get_path (ctx->device), - g_timer_elapsed (ctx->timer, NULL)); - g_timer_destroy (ctx->timer); - - /* Set async operation result */ - if (!mm_device_peek_plugin (ctx->device)) { - g_simple_async_result_set_error (ctx->result, - MM_CORE_ERROR, - MM_CORE_ERROR_UNSUPPORTED, - "not supported by any plugin"); - } else { - g_simple_async_result_set_op_res_gboolean (ctx->result, TRUE); - } - - g_simple_async_result_complete (ctx->result); - - g_signal_handler_disconnect (ctx->device, ctx->grabbed_id); - g_signal_handler_disconnect (ctx->device, ctx->released_id); - - g_warn_if_fail (ctx->running_probes == NULL); - - g_object_unref (ctx->result); - g_object_unref (ctx->device); - g_object_unref (ctx->self); - g_slice_free (FindDeviceSupportContext, ctx); + g_atomic_int_inc (&port_context->ref_count); + return port_context; } -gboolean -mm_plugin_manager_find_device_support_finish (MMPluginManager *self, - GAsyncResult *result, - GError **error) +static MMPlugin * +port_context_run_finish (MMPluginManager *self, + GAsyncResult *res, + GError **error) { - return !g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error); + return g_task_propagate_pointer (G_TASK (res), error); } static void -port_probe_context_finished (PortProbeContext *port_probe_ctx) +port_context_complete (PortContext *port_context) { - FindDeviceSupportContext *ctx = port_probe_ctx->parent_ctx; - MMPlugin *device_plugin; - - /* Get info about the currently scheduled plugin in the device */ - device_plugin = (MMPlugin *)mm_device_peek_plugin (ctx->device); - - if (!port_probe_ctx->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_plugin) { - mm_dbg ("(Plugin Manager) [%s] assuming port can be handled by the '%s' plugin", - g_udev_device_get_name (port_probe_ctx->port), - mm_plugin_get_name (device_plugin)); - } else { - gboolean cancel_remaining; - GList *l; - - mm_dbg ("(Plugin Manager) [%s] not supported by any plugin", - g_udev_device_get_name (port_probe_ctx->port)); - - /* Tell the device to ignore this port */ - mm_device_ignore_port (ctx->device, port_probe_ctx->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. */ - cancel_remaining = TRUE; - for (l = ctx->running_probes; l; l = g_list_next (l)) { - PortProbeContext *other = l->data; - - /* Do not cancel anything if we find at least one probe which is not - * waiting for the suggested plugin */ - if (other != port_probe_ctx && !other->defer_until_suggested) { - cancel_remaining = FALSE; - break; - } - } + GTask *task; - if (cancel_remaining) - /* Set a NULL suggested plugin, will cancel the probes */ - suggest_port_probe_result (ctx, port_probe_ctx, NULL); - } - } else { - /* Notify the plugin to the device, if this is the first port probing - * 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_plugin || - (g_str_equal (mm_plugin_get_name (device_plugin), MM_PLUGIN_GENERIC_NAME) && - device_plugin != port_probe_ctx->best_plugin)) { - /* Only log best plugin if it's not the generic one */ - if (!g_str_equal (mm_plugin_get_name (port_probe_ctx->best_plugin), MM_PLUGIN_GENERIC_NAME)) - mm_dbg ("(Plugin Manager) (%s) [%s]: found best plugin for device (%s)", - mm_plugin_get_name (port_probe_ctx->best_plugin), - g_udev_device_get_name (port_probe_ctx->port), - mm_device_get_path (ctx->device)); - - mm_device_set_plugin (ctx->device, G_OBJECT (port_probe_ctx->best_plugin)); - - /* Suggest this plugin also to other port probes */ - suggest_port_probe_result (ctx, port_probe_ctx, port_probe_ctx->best_plugin); - } - /* Warn if the best plugin found for this port differs from the - * best plugin found for the the first probed port */ - else if (!g_str_equal (mm_plugin_get_name (device_plugin), - mm_plugin_get_name (port_probe_ctx->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_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 (port_probe_ctx->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) (%s): will use plugin '%s' instead of '%s', modem is Icera-capable", - g_udev_device_get_name (port_probe_ctx->port), - mm_plugin_get_name (MM_PLUGIN (mm_device_peek_plugin (ctx->device))), - mm_plugin_get_name (port_probe_ctx->best_plugin)); - } else if (new_allowed_icera && previous_forbidden_icera) { - mm_warn ("(Plugin Manager) (%s): overriding previously selected device plugin '%s' with '%s', modem is Icera-capable", - g_udev_device_get_name (port_probe_ctx->port), - mm_plugin_get_name (MM_PLUGIN (mm_device_peek_plugin (ctx->device))), - mm_plugin_get_name (port_probe_ctx->best_plugin)); - mm_device_set_plugin (ctx->device, G_OBJECT (port_probe_ctx->best_plugin)); - } else { - mm_warn ("(Plugin Manager) (%s): plugin mismatch error (expected: '%s', got: '%s')", - g_udev_device_get_name (port_probe_ctx->port), - mm_plugin_get_name (MM_PLUGIN (mm_device_peek_plugin (ctx->device))), - mm_plugin_get_name (port_probe_ctx->best_plugin)); - } - } - } + /* Steal the task from the task */ + g_assert (port_context->task); + task = port_context->task; + port_context->task = NULL; - /* Remove us from the list of running probes */ - g_assert (g_list_find (ctx->running_probes, port_probe_ctx) != NULL); - ctx->running_probes = g_list_remove (ctx->running_probes, port_probe_ctx); + /* 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 there are running probes around, wait for them to finish */ - if (ctx->running_probes != NULL) { - GList *l; - GString *s = NULL; - guint i = 0; + 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); +} - for (l = ctx->running_probes; l; l = g_list_next (l)) { - const gchar *portname = g_udev_device_get_name (((PortProbeContext *)l->data)->port); +static void port_context_next (PortContext *port_context); - if (!s) - s = g_string_new (portname); - else - g_string_append_printf (s, ", %s", portname); - i++; - } +static void +port_context_supported (PortContext *port_context, + MMPlugin *plugin) +{ + g_assert (plugin); - mm_dbg ("(Plugin Manager) '%s' port probe finished, still %u running probes in this device (%s)", - g_udev_device_get_name (port_probe_ctx->port), i, s->str); - g_string_free (s, TRUE); - } - /* If we didn't use the minimum probing time, wait for it to finish */ - else if (ctx->timeout_id > 0) { - mm_dbg ("(Plugin Manager) '%s' port probe finished, last one in device, " - "but minimum probing time not consumed yet ('%lf' seconds elapsed)", - g_udev_device_get_name (port_probe_ctx->port), - g_timer_elapsed (ctx->timer, NULL)); - } else { - mm_dbg ("(Plugin Manager) '%s' port probe finished, last one in device", - g_udev_device_get_name (port_probe_ctx->port)); - /* If we just finished the last running probe, we can now finish the device - * support check */ - find_device_support_context_complete_and_free (ctx); - } + mm_dbg ("[plugin manager] task %s: found best plugin for port (%s)", + port_context->name, mm_plugin_get_name (plugin)); - port_probe_context_free (port_probe_ctx); + /* Found a best plugin, store it to return it */ + port_context->best_plugin = g_object_ref (plugin); + port_context_complete (port_context); } static gboolean -deferred_support_check_idle (PortProbeContext *port_probe_ctx) +port_context_defer_ready (PortContext *port_context) { - port_probe_ctx->defer_id = 0; - port_probe_context_step (port_probe_ctx); + port_context->defer_id = 0; + port_context_next (port_context); return G_SOURCE_REMOVE; } static void -suggest_single_port_probe_result (PortProbeContext *target_port_probe_ctx, - MMPlugin *suggested_plugin, - gboolean reschedule_deferred) +port_context_set_suggestion (PortContext *port_context, + MMPlugin *suggested_plugin) { gboolean forbidden_icera; @@ -353,36 +310,32 @@ suggest_single_port_probe_result (PortProbeContext *target_port_probe_ctx, * the deferred until suggested probes get finished. */ - if (target_port_probe_ctx->best_plugin || target_port_probe_ctx->suggested_plugin) + /* 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 (target_port_probe_ctx->defer_until_suggested) { + if (port_context->defer_until_suggested) { /* Reset the defer until suggested flag; we consider this * cancelled probe completed now. */ - target_port_probe_ctx->defer_until_suggested = FALSE; + port_context->defer_until_suggested = FALSE; if (suggested_plugin) { - mm_dbg ("(Plugin Manager) (%s) [%s] deferred task completed, got suggested plugin", - mm_plugin_get_name (suggested_plugin), - g_udev_device_get_name (target_port_probe_ctx->port)); + 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 */ - target_port_probe_ctx->suggested_plugin = g_object_ref (suggested_plugin); - target_port_probe_ctx->current = g_list_find (target_port_probe_ctx->current, - target_port_probe_ctx->suggested_plugin); - } else { - mm_dbg ("(Plugin Manager) [%s] deferred task cancelled, no suggested plugin", - g_udev_device_get_name (target_port_probe_ctx->port)); - target_port_probe_ctx->best_plugin = NULL; - target_port_probe_ctx->current = NULL; + 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; } - /* Schedule checking support, which will end the operation */ - if (reschedule_deferred) { - g_assert (target_port_probe_ctx->defer_id == 0); - target_port_probe_ctx->defer_id = g_idle_add ((GSourceFunc)deferred_support_check_idle, - target_port_probe_ctx); - } + mm_dbg ("[plugin manager] task %s: deferred task completed, no suggested plugin", + port_context->name); + port_context_complete (port_context); return; } @@ -409,403 +362,1076 @@ suggest_single_port_probe_result (PortProbeContext *target_port_probe_ctx, * should run its probing independently, and we'll later decide * which result applies to the whole device. */ - mm_dbg ("(Plugin Manager) (%s) [%s] suggested plugin for port", - mm_plugin_get_name (suggested_plugin), - g_udev_device_get_name (target_port_probe_ctx->port)); - target_port_probe_ctx->suggested_plugin = g_object_ref (suggested_plugin); + 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 -suggest_port_probe_result (FindDeviceSupportContext *ctx, - PortProbeContext *origin, - MMPlugin *suggested_plugin) +port_context_unsupported (PortContext *port_context, + MMPlugin *plugin) { - GList *l; + g_assert (plugin); - for (l = ctx->running_probes; l; l = g_list_next (l)) { - PortProbeContext *port_probe_ctx = l->data; + /* 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 (port_probe_ctx != origin) - suggest_single_port_probe_result (port_probe_ctx, suggested_plugin, TRUE); + /* 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 -plugin_supports_port_ready (MMPlugin *plugin, - GAsyncResult *result, - PortProbeContext *port_probe_ctx) +port_context_defer_until_suggested (PortContext *port_context, + MMPlugin *plugin) { - MMPluginSupportsResult support_result; - GError *error = NULL; + 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; + } - /* Get supports check results */ - support_result = mm_plugin_supports_port_finish (plugin, result, &error); + /* 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) { - mm_warn ("(Plugin Manager) (%s) [%s] error when checking support: '%s'", - mm_plugin_get_name (plugin), - g_udev_device_get_name (port_probe_ctx->port), - error->message); + 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: - /* Found a best plugin */ - port_probe_ctx->best_plugin = g_object_ref (plugin); - - if (port_probe_ctx->suggested_plugin && - port_probe_ctx->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. */ - mm_dbg ("(Plugin Manager) (%s) [%s] found best plugin for port, " - "but not the same as the suggested one (%s)", - mm_plugin_get_name (port_probe_ctx->best_plugin), - g_udev_device_get_name (port_probe_ctx->port), - mm_plugin_get_name (port_probe_ctx->suggested_plugin)); - } else { - mm_dbg ("(Plugin Manager) (%s) [%s] found best plugin for port", - mm_plugin_get_name (port_probe_ctx->best_plugin), - g_udev_device_get_name (port_probe_ctx->port)); - } - port_probe_ctx->current = NULL; + port_context_supported (port_context, plugin); + break; + 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; + } - /* Step, which will end the port probe operation */ - port_probe_context_step (port_probe_ctx); - return; + /* 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; - case MM_PLUGIN_SUPPORTS_PORT_UNSUPPORTED: - if (port_probe_ctx->suggested_plugin) { - if (port_probe_ctx->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 ("(Plugin Manager) [%s] ignoring port unsupported by physical modem's plugin", - g_udev_device_get_name (port_probe_ctx->port)); - port_probe_ctx->best_plugin = NULL; - port_probe_ctx->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. */ - port_probe_ctx->current = g_list_find (port_probe_ctx->current, - port_probe_ctx->suggested_plugin); - } - } else { - /* If the plugin knows it doesn't support the modem, just keep on - * checking the next plugin. - */ - port_probe_ctx->current = g_list_next (port_probe_ctx->current); - } + /* If we're cancelled, done */ + if (g_cancellable_is_cancelled (port_context->cancellable)) { + port_context_complete (port_context); + return; + } - /* Step */ - port_probe_context_step (port_probe_ctx); + /* 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, + (GAsyncReadyCallback) plugin_supports_port_ready, + port_context_ref (port_context)); +} - case MM_PLUGIN_SUPPORTS_PORT_DEFER: - /* Try with the suggested one after being deferred */ - if (port_probe_ctx->suggested_plugin) { - mm_dbg ("(Plugin Manager) (%s) [%s] deferring support check, suggested: %s", - mm_plugin_get_name (MM_PLUGIN (port_probe_ctx->current->data)), - g_udev_device_get_name (port_probe_ctx->port), - mm_plugin_get_name (MM_PLUGIN (port_probe_ctx->suggested_plugin))); - port_probe_ctx->current = g_list_find (port_probe_ctx->current, - port_probe_ctx->suggested_plugin); - } else { - mm_dbg ("(Plugin Manager) (%s) [%s] deferring support check", - mm_plugin_get_name (MM_PLUGIN (port_probe_ctx->current->data)), - g_udev_device_get_name (port_probe_ctx->port)); - } +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; + } - /* Schedule checking support */ - port_probe_ctx->defer_id = g_timeout_add_seconds (DEFER_TIMEOUT_SECS, - (GSourceFunc)deferred_support_check_idle, - port_probe_ctx); - return; + /* 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; +} - case MM_PLUGIN_SUPPORTS_PORT_DEFER_UNTIL_SUGGESTED: - /* If we're deferred until suggested, but there is already a plugin - * suggested in the parent device context, grab it. This may happen if - * e.g. a wwan interface arrives *after* a port has already been probed. - */ - if (!port_probe_ctx->suggested_plugin) { - MMPlugin *device_plugin; - - /* Get info about the currently scheduled plugin in the device */ - device_plugin = (MMPlugin *)mm_device_peek_plugin (port_probe_ctx->parent_ctx->device); - if (device_plugin) { - mm_dbg ("(Plugin Manager) (%s) [%s] task deferred until result suggested and got suggested plugin", - mm_plugin_get_name (device_plugin), - g_udev_device_get_name (port_probe_ctx->port)); - /* Flag it as deferred before suggesting probe result */ - port_probe_ctx->defer_until_suggested = TRUE; - suggest_single_port_probe_result (port_probe_ctx, device_plugin, FALSE); +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)); } + } - /* If we arrived here and we already have a plugin suggested, use it */ - if (port_probe_ctx->suggested_plugin) { - if (port_probe_ctx->suggested_plugin == plugin) { - mm_dbg ("(Plugin Manager) (%s) [%s] task completed, got suggested plugin", - mm_plugin_get_name (port_probe_ctx->suggested_plugin), - g_udev_device_get_name (port_probe_ctx->port)); - port_probe_ctx->best_plugin = g_object_ref (port_probe_ctx->suggested_plugin); - port_probe_ctx->current = NULL; - } else { - mm_dbg ("(Plugin Manager) (%s) [%s] re-checking support on deferred task, got suggested plugin", - mm_plugin_get_name (port_probe_ctx->suggested_plugin), - g_udev_device_get_name (port_probe_ctx->port)); - port_probe_ctx->current = g_list_find (port_probe_ctx->current, - port_probe_ctx->suggested_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 (); - /* Schedule checking support, which will end the operation */ - port_probe_context_step (port_probe_ctx); - return; - } + /* 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); - /* 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) [%s] deferring support check until result suggested", - g_udev_device_get_name (port_probe_ctx->port)); - port_probe_ctx->defer_until_suggested = TRUE; - return; + 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, + GUdevDevice *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, g_udev_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_port_context (DeviceContext *device_context, + GUdevDevice *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 (g_udev_device_get_name (port_context->port), g_udev_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 -port_probe_context_step (PortProbeContext *port_probe_ctx) +device_context_complete (DeviceContext *device_context) { - FindDeviceSupportContext *ctx = port_probe_ctx->parent_ctx; + GTask *task; - /* Already checked all plugins? */ - if (!port_probe_ctx->current) { - port_probe_context_finished (port_probe_ctx); - return; + /* 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; } - /* Ask the current plugin to check support of this port */ - mm_plugin_supports_port (MM_PLUGIN (port_probe_ctx->current->data), - ctx->device, - port_probe_ctx->port, - (GAsyncReadyCallback)plugin_supports_port_ready, - port_probe_ctx); + /* 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 GList * -build_plugins_list (MMPluginManager *self, - MMDevice *device, - GUdevDevice *port) +static void +device_context_suggest_plugin (DeviceContext *device_context, + PortContext *port_context, + MMPlugin *suggested_plugin) { - GList *list = NULL; GList *l; - gboolean supported_found = FALSE; + 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; + } + } - for (l = self->priv->plugins; l && !supported_found; l = g_list_next (l)) { - MMPluginSupportsHint hint; + /* 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); +} - 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(); +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; } - /* 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)); + /* 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; + } - mm_dbg ("(Plugin Manager) [%s] Found '%u' plugins to try...", - g_udev_device_get_name (port), - g_list_length (list)); - for (l = list; l; l = g_list_next (l)) { - mm_dbg ("(Plugin Manager) [%s] Will try with plugin '%s'", - g_udev_device_get_name (port), - mm_plugin_get_name (MM_PLUGIN (l->data))); + /* 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; } - return list; + /* 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 -port_probe_context_start (FindDeviceSupportContext *ctx, - PortProbeContext *port_probe_ctx) +device_context_continue (DeviceContext *device_context) { - /* 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) */ - port_probe_ctx->plugins = build_plugins_list (ctx->self, ctx->device, port_probe_ctx->port); - port_probe_ctx->current = port_probe_ctx->plugins; - - /* If we got one suggested, it will be the first one, unless it is the generic plugin */ - port_probe_ctx->suggested_plugin = (!!mm_device_peek_plugin (ctx->device) ? - MM_PLUGIN (mm_device_get_plugin (ctx->device)) : - NULL); - if (port_probe_ctx->suggested_plugin) { - if (g_str_equal (mm_plugin_get_name (port_probe_ctx->suggested_plugin), - MM_PLUGIN_GENERIC_NAME)) - /* Initially ignore generic plugin suggested */ - g_clear_object (&port_probe_ctx->suggested_plugin); + 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 = g_udev_device_get_name (port_context->port); + if (!s) + s = g_string_new (portname); else - port_probe_ctx->current = g_list_find (port_probe_ctx->current, - port_probe_ctx->suggested_plugin); + g_string_append_printf (s, ", %s", portname); + + /* Active? */ + if (!port_context->defer_until_suggested) + n_active++; + n++; } - port_probe_context_step (port_probe_ctx); + 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 -device_port_grabbed_cb (MMDevice *device, - GUdevDevice *port, - FindDeviceSupportContext *ctx) +port_context_run_ready (MMPluginManager *self, + GAsyncResult *res, + CommonAsyncContext *common) { - PortProbeContext *port_probe_ctx; + 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); - /* Launch probing task on this port with the first plugin of the list */ - port_probe_ctx = g_slice_new0 (PortProbeContext); - port_probe_ctx->parent_ctx = ctx; - port_probe_ctx->port = g_object_ref (port); + /* 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; - /* Schedule in the list of probes to run */ - ctx->running_probes = g_list_prepend (ctx->running_probes, port_probe_ctx); + mm_dbg ("[plugin manager] task %s: min probing time elapsed", device_context->name); - /* If port grabbed after the min wait timeout expired, launch probing directly */ - if (ctx->min_wait_timeout_expired) - port_probe_context_start (ctx, port_probe_ctx); + /* Wakeup the device context logic */ + device_context_continue (device_context); + return G_SOURCE_REMOVE; } static void -device_port_released_cb (MMDevice *device, - GUdevDevice *port, - FindDeviceSupportContext *ctx) +device_context_run_port_context (DeviceContext *device_context, + PortContext *port_context) { - /* TODO: abort probing on that port */ + 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 -min_probing_timeout_cb (FindDeviceSupportContext *ctx) +device_context_min_wait_time_elapsed (DeviceContext *device_context) { - ctx->timeout_id = 0; + MMPluginManager *self; + GList *l; - /* If there are no running probes around, we're free to finish */ - if (ctx->running_probes == NULL) { - mm_dbg ("(Plugin Manager) [%s] Minimum probing time consumed and no more ports to probe", - mm_device_get_path (ctx->device)); - find_device_support_context_complete_and_free (ctx); - } else { - GList *l; - gboolean not_deferred = FALSE; + /* Recover plugin manager */ + self = MM_PLUGIN_MANAGER (device_context->self); - mm_dbg ("(Plugin Manager) [%s] Minimum probing time consumed", - mm_device_get_path (ctx->device)); + device_context->min_wait_time_id = 0; + mm_dbg ("[plugin manager] task %s: min wait time elapsed", device_context->name); - /* If all we got were probes with 'deferred_until_suggested', just cancel - * the probing. May happen e.g. with just 'net' ports */ - for (l = ctx->running_probes; l; l = g_list_next (l)) { - PortProbeContext *port_probe_ctx = l->data; + /* 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; - if (!port_probe_ctx->defer_until_suggested) { - not_deferred = TRUE; - break; - } - } + /* 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, + GUdevDevice *port) +{ + PortContext *port_context; + + mm_dbg ("[plugin manager] task %s: port released: %s", + device_context->name, g_udev_device_get_name (port)); + + port_context = device_context_peek_port_context (device_context, port); + + /* This is not something worth warning. If the probing task has already + * been finished, it will already be removed from the list */ + if (!port_context) + return; + + /* Request cancellation of this single port, will be completed asynchronously */ + port_context_cancel (port_context); +} + +static void +device_context_port_grabbed (DeviceContext *device_context, + GUdevDevice *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, g_udev_device_get_name (port)); + + /* Ignore if for any reason we still have it around */ + port_context = device_context_peek_port_context (device_context, port); + if (port_context) { + mm_warn ("[plugin manager] task %s: port already being processed", + device_context->name); + return; + } - if (!not_deferred) - suggest_port_probe_result (ctx, NULL, NULL); + /* 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; } - return FALSE; + /* 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 -min_wait_timeout_cb (FindDeviceSupportContext *ctx) +device_context_cancel (DeviceContext *device_context) { - GList *l; + /* 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; + } - ctx->min_wait_timeout_expired = TRUE; + /* 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); + } - /* Launch supports check for each port in the Plugin Manager */ - for (l = ctx->running_probes; l; l = g_list_next (l)) - port_probe_context_start (ctx, (PortProbeContext *)(l->data)); + /* 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); - /* Schedule additional min probing timeout */ + /* 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 timeout of 2s. We force the probing time of the device to + /* 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. */ - ctx->timeout_id = g_timeout_add (MIN_PROBING_TIME_MSECS - MIN_WAIT_TIME_MSECS, - (GSourceFunc)min_probing_timeout_cb, - ctx); - return G_SOURCE_REMOVE; + * 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_path (device_context->device), mm_device_get_path (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_find_device_support (MMPluginManager *self, - MMDevice *device, - GAsyncReadyCallback callback, - gpointer user_data) +mm_plugin_manager_device_support_check (MMPluginManager *self, + MMDevice *device, + GAsyncReadyCallback callback, + gpointer user_data) { - FindDeviceSupportContext *ctx; - - mm_dbg ("(Plugin Manager) [%s] Checking device support...", - mm_device_get_path (device)); - - ctx = g_slice_new0 (FindDeviceSupportContext); - ctx->self = g_object_ref (self); - ctx->device = g_object_ref (device); - ctx->result = g_simple_async_result_new (G_OBJECT (self), - callback, - user_data, - mm_plugin_manager_find_device_support); - - /* Connect to device port grabbed/released notifications */ - ctx->grabbed_id = g_signal_connect (device, - MM_DEVICE_PORT_GRABBED, - G_CALLBACK (device_port_grabbed_cb), - ctx); - ctx->released_id = g_signal_connect (device, - MM_DEVICE_PORT_RELEASED, - G_CALLBACK (device_port_released_cb), - ctx); - - ctx->timer = g_timer_new (); + DeviceContext *device_context; + GTask *task; - /* 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 */ - ctx->timeout_id = g_timeout_add (MIN_WAIT_TIME_MSECS, - (GSourceFunc)min_wait_timeout_cb, - ctx); + /* + * 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_path (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_path (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; } /*****************************************************************************/ |