If the DeviceContext is cancelled, we remove it from the list of
tracked DeviceContexts in the MMPluginManager, so that new port
additions happening at the same time don't end up being added to
it.
This should allow an early cancellation of the DeviceContext without
asserting on g_assert (!device_context->extra_probing_time_id) as
there is no chance that a new port addition has reseted the extra
probing time timeout.
0x00007c1154f16abf (libc.so.6 - pthread_kill.c: 44) __pthread_kill_implementation
0x00007c1154ecbf0c (libc.so.6 - raise.c: 26) raise
0x00007c1154eb74aa (libc.so.6 - abort.c: 79) abort
0x00007c11556c08a9 (libglib-2.0.so.0 - gtestutils.c: 3450) g_assertion_message
0x00007c11556c091d (libglib-2.0.so.0 - gtestutils.c: 3476) g_assertion_message_expr
0x00005897d1ed6e7b (ModemManager - mm-plugin-manager.c: 805) device_context_unref
0x00005897d1ed6ab9 (ModemManager - mm-plugin-manager.c: 163) common_async_context_free
0x00005897d1ed5dd6 (ModemManager - mm-plugin-manager.c: 1136) port_context_run_ready
0x00007c1155550e86 (libgio-2.0.so.0 - gtask.c: 1309) g_task_return_now
0x00007c115554fe50 (libgio-2.0.so.0 - gtask.c: 1378) g_task_return
0x00007c115555047b (libgio-2.0.so.0 - gtask.c: 2022) g_task_return_new_error
0x00005897d1ed581a (ModemManager - mm-plugin-manager.c: 296) port_context_complete
0x00005897d1ed6259 (ModemManager - mm-plugin-manager.c) plugin_supports_port_ready
0x00007c1155550e86 (libgio-2.0.so.0 - gtask.c: 1309) g_task_return_now
0x00007c1155550eb9 (libgio-2.0.so.0 - gtask.c: 1323) complete_in_idle_cb
0x00007c115569c91b (libglib-2.0.so.0 - gmain.c: 3460) g_main_dispatch
0x00007c115569c91b (libglib-2.0.so.0 - gmain.c: 4200) g_main_context_dispatch
0x00007c115569cc37 (libglib-2.0.so.0 - gmain.c: 4276) g_main_context_iterate
0x00007c115569ceb5 (libglib-2.0.so.0 - gmain.c: 4479) g_main_loop_run
0x00005897d1e715bf (ModemManager - main.c: 236) main
0x00007c1154eb7705 (libc.so.6 - libc_start_call_main.h: 58) __libc_start_call_main
0x00007c1154eb77c1 (libc.so.6 - libc-start.c: 360) __libc_start_main_impl
0x00005897d1e70f10 (ModemManager + 0x000b3f10) _start
0x00007ffd5acc5057
Manually reproducing this issue was challenging, so adding here the
steps taken, for reference. This is with ModemManager running with
--no-udev (i.e. port additions reported via mmcli). The key to
reproduce the problem is the different timing between the port
additions and removals of the three ttyACM ports.
COUNT=1
while ((COUNT < 10000)); do
echo "$COUNT..."
COUNT=$((COUNT + 1))
(
sudo mmcli --report-kernel-event="name=ttyACM0,subsystem=tty,action=add"
TIMEOUT="0.0$(((RANDOM % 10) + 1))"
sleep $TIMEOUT
sudo mmcli --report-kernel-event="name=ttyACM0,subsystem=tty,action=remove"
) &
(
sudo mmcli --report-kernel-event="name=ttyACM1,subsystem=tty,action=add"
TIMEOUT="0.0$(((RANDOM % 10) + 1))"
sleep $TIMEOUT
sudo mmcli --report-kernel-event="name=ttyACM1,subsystem=tty,action=remove"
) &
(
sudo mmcli --report-kernel-event="name=ttyACM2,subsystem=tty,action=add"
TIMEOUT="0.0$(((RANDOM % 10) + 1))"
sleep $TIMEOUT
sudo mmcli --report-kernel-event="name=ttyACM2,subsystem=tty,action=remove"
) &
TIMEOUT="0.0$(((RANDOM % 10) + 1))"
sleep $TIMEOUT
COUNT=$((COUNT + 1))
done
2182 lines
79 KiB
C
2182 lines
79 KiB
C
/* -*- 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) 2012 Google, Inc.
|
|
* Copyright (C) 2011 - 2019 Aleksander Morgado <aleksander@gnu.org>
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
#include <gmodule.h>
|
|
#include <gio/gio.h>
|
|
|
|
#include <config.h>
|
|
|
|
#include <ModemManager.h>
|
|
#include <mm-errors-types.h>
|
|
|
|
#include "mm-plugin-manager.h"
|
|
#include "mm-plugin.h"
|
|
#include "mm-shared.h"
|
|
#include "mm-utils.h"
|
|
#include "mm-log-object.h"
|
|
|
|
#if defined WITH_BUILTIN_PLUGINS
|
|
# include "mm-builtin-plugins.h"
|
|
#else
|
|
# define SHARED_PREFIX "libmm-shared"
|
|
# define PLUGIN_PREFIX "libmm-plugin"
|
|
#endif
|
|
|
|
static void initable_iface_init (GInitableIface *iface);
|
|
static void log_object_iface_init (MMLogObjectInterface *iface);
|
|
|
|
G_DEFINE_TYPE_EXTENDED (MMPluginManager, mm_plugin_manager, G_TYPE_OBJECT, 0,
|
|
G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
|
|
G_IMPLEMENT_INTERFACE (MM_TYPE_LOG_OBJECT, log_object_iface_init))
|
|
|
|
enum {
|
|
PROP_0,
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
PROP_PLUGIN_DIR,
|
|
#endif
|
|
PROP_FILTER,
|
|
LAST_PROP
|
|
};
|
|
|
|
struct _MMPluginManagerPrivate {
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
/* Path to look for plugins */
|
|
gchar *plugin_dir;
|
|
#endif
|
|
|
|
/* Device filter */
|
|
MMFilter *filter;
|
|
|
|
/* 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;
|
|
|
|
/* Full list of subsystems requested by the registered plugins */
|
|
gchar **subsystems;
|
|
};
|
|
|
|
/*****************************************************************************/
|
|
/* 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, 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);
|
|
|
|
G_DEFINE_AUTOPTR_CLEANUP_FUNC (DeviceContext, device_context_unref)
|
|
G_DEFINE_AUTOPTR_CLEANUP_FUNC (PortContext, port_context_unref)
|
|
|
|
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 holds 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, 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)
|
|
{
|
|
MMPluginManager *self;
|
|
GTask *task;
|
|
|
|
/* If already completed, do nothing */
|
|
if (!port_context->task)
|
|
return;
|
|
|
|
/* Steal the task from the context, we only will complete once */
|
|
task = port_context->task;
|
|
port_context->task = NULL;
|
|
|
|
/* Log about the time required to complete the checks */
|
|
self = g_task_get_source_object (task);
|
|
mm_obj_dbg (self, "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)
|
|
{
|
|
MMPluginManager *self;
|
|
|
|
g_assert (plugin);
|
|
self = g_task_get_source_object (port_context->task);
|
|
|
|
mm_obj_dbg (self, "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)
|
|
{
|
|
MMPluginManager *self;
|
|
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;
|
|
|
|
/* There may not be a task at this point, so be gentle */
|
|
self = port_context->task ? g_task_get_source_object (port_context->task) : NULL;
|
|
|
|
/* 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_obj_dbg (self, "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_obj_dbg (self, "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 (mm_plugin_is_generic (suggested_plugin))
|
|
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_obj_dbg (self, "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)
|
|
{
|
|
MMPluginManager *self;
|
|
|
|
g_assert (plugin);
|
|
self = g_task_get_source_object (port_context->task);
|
|
|
|
/* 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_obj_dbg (self, "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)
|
|
{
|
|
MMPluginManager *self;
|
|
|
|
self = g_task_get_source_object (port_context->task);
|
|
|
|
/* Try with the suggested one after being deferred */
|
|
if (port_context->suggested_plugin) {
|
|
mm_obj_dbg (self, "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_obj_dbg (self, "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)
|
|
{
|
|
MMPluginManager *self;
|
|
|
|
g_assert (plugin);
|
|
self = g_task_get_source_object (port_context->task);
|
|
|
|
/* 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_obj_dbg (self, "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_obj_dbg (self, "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_obj_dbg (self, "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) /* full reference */
|
|
{
|
|
g_autoptr(PortContext) port_context = _port_context;
|
|
g_autoptr(GError) error = NULL;
|
|
MMPluginManager *self;
|
|
MMPluginSupportsResult support_result;
|
|
|
|
self = g_task_get_source_object (port_context->task);
|
|
|
|
/* 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_obj_warn (self, "task %s: error when checking support with plugin '%s': %s",
|
|
port_context->name, mm_plugin_get_name (plugin), error->message);
|
|
}
|
|
|
|
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;
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
}
|
|
|
|
static void
|
|
port_context_next (PortContext *port_context)
|
|
{
|
|
MMPluginManager *self;
|
|
MMPlugin *plugin;
|
|
|
|
self = g_task_get_source_object (port_context->task);
|
|
|
|
/* 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_obj_dbg (self, "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)
|
|
{
|
|
g_autoptr(PortContext) port_context = NULL;
|
|
MMPluginManager *self;
|
|
|
|
/* Make sure we hold a port context reference while cancelling, as the
|
|
* cancellable signal handlers may end up unref-ing our last reference
|
|
* otherwise. */
|
|
port_context = port_context_ref (_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;
|
|
|
|
self = g_task_get_source_object (port_context->task);
|
|
mm_obj_dbg (self, "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);
|
|
}
|
|
/* If the task was deferred until a result is suggested, we can also
|
|
* complete it right away */
|
|
else if (port_context->defer_until_suggested)
|
|
port_context_complete (port_context);
|
|
/* else, 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_obj_warn (self, "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_obj_dbg (self, "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_obj_dbg (self, "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_obj_dbg (self, "task %s: will try with plugin '%s' (suggested)",
|
|
port_context->name, mm_plugin_get_name (plugin));
|
|
continue;
|
|
}
|
|
if (suggested && !suggested_found) {
|
|
mm_obj_dbg (self, "task %s: won't try with plugin '%s' (skipped)",
|
|
port_context->name, mm_plugin_get_name (plugin));
|
|
continue;
|
|
}
|
|
mm_obj_dbg (self, "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 an 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_obj_dbg (self, "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 2000
|
|
|
|
/* 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 4000
|
|
|
|
/* Additional time to wait for other ports to appear after the last port is
|
|
* exposed in the system. Longer time when not using udev, as we rely on
|
|
* mmcli --report-kernel-event events to report new port additions, e.g.
|
|
* via openwrt hotplug scripts. */
|
|
#if defined WITH_UDEV
|
|
# define EXTRA_PROBING_TIME_MSECS 2000
|
|
#else
|
|
# define EXTRA_PROBING_TIME_MSECS 4000
|
|
#endif
|
|
|
|
/* 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 cancellable */
|
|
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, which is a timeout initialized as soon as the first
|
|
* port is added to the device context. 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;
|
|
|
|
/* Extra probing time, which is a timeout refreshed every time a new port
|
|
* is added to the device context. The device support check task cannot be
|
|
* finished before this timeout expires. Once the timeout is expired, the id
|
|
* is reset to 0. */
|
|
guint extra_probing_time_id;
|
|
|
|
/* Signal connection ids for the released signal from the device. This is a
|
|
* signal that will give us notifications of ports suddenly unavailable in
|
|
* the device. */
|
|
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->released_id);
|
|
g_assert (!device_context->min_wait_time_id);
|
|
g_assert (!device_context->min_probing_time_id);
|
|
g_assert (!device_context->extra_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)
|
|
{
|
|
MMPluginManager *self;
|
|
GTask *task;
|
|
|
|
self = g_task_get_source_object (device_context->task);
|
|
|
|
/* If the context is completed before the 2500ms minimum probing time, we need to wait
|
|
* until that happens, so that we give enough time to udev/hotplug to report the
|
|
* new port additions. */
|
|
if (device_context->min_probing_time_id) {
|
|
mm_obj_dbg (self, "task %s: all port probings completed, but not reached min probing time yet",
|
|
device_context->name);
|
|
return;
|
|
}
|
|
|
|
/* If the context is completed less than 1500ms before the last port was exposed,
|
|
* wait some more. */
|
|
if (device_context->extra_probing_time_id) {
|
|
mm_obj_dbg (self, "task %s: all port probings completed, but not reached extra probing time yet",
|
|
device_context->name);
|
|
return;
|
|
}
|
|
|
|
/* Steal the task from the context */
|
|
g_assert (device_context->task);
|
|
task = g_steal_pointer (&device_context->task);
|
|
|
|
/* Log about the time required to complete the checks */
|
|
mm_obj_dbg (self, "task %s: finished in '%lf' seconds",
|
|
device_context->name, g_timer_elapsed (device_context->timer, NULL));
|
|
|
|
/* Remove signal handlers */
|
|
if (device_context->released_id) {
|
|
g_signal_handler_disconnect (device_context->device, device_context->released_id);
|
|
device_context->released_id = 0;
|
|
}
|
|
|
|
/* On completion, the minimum wait time must have been already elapsed */
|
|
g_assert (!device_context->min_wait_time_id);
|
|
|
|
/* 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), 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)
|
|
{
|
|
MMPluginManager *self;
|
|
|
|
self = g_task_get_source_object (device_context->task);
|
|
|
|
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_obj_dbg (self, "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_obj_dbg (self, "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 ||
|
|
(mm_plugin_is_generic (device_context->best_plugin) &&
|
|
device_context->best_plugin != best_plugin)) {
|
|
/* Only log best plugin if it's not the generic one */
|
|
if (!mm_plugin_is_generic (best_plugin))
|
|
mm_obj_dbg (self, "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_obj_warn (self, "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_obj_warn (self, "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_obj_warn (self, "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_obj_dbg (self, "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)
|
|
{
|
|
MMPluginManager *self;
|
|
GList *l;
|
|
GString *s = NULL;
|
|
guint n = 0;
|
|
guint n_active = 0;
|
|
|
|
self = g_task_get_source_object (device_context->task);
|
|
|
|
/* If there are no running port contexts around, we're free to finish */
|
|
if (!device_context->port_contexts) {
|
|
mm_obj_dbg (self, "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_obj_dbg (self, "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_obj_dbg (self, "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_obj_warn (self, "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)
|
|
{
|
|
MMPluginManager *self;
|
|
|
|
device_context->min_probing_time_id = 0;
|
|
|
|
self = g_task_get_source_object (device_context->task);
|
|
mm_obj_dbg (self, "task %s: min probing time elapsed", device_context->name);
|
|
|
|
/* Wakeup the device context logic */
|
|
device_context_continue (device_context);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static gboolean
|
|
device_context_extra_probing_time_elapsed (DeviceContext *device_context)
|
|
{
|
|
MMPluginManager *self;
|
|
|
|
device_context->extra_probing_time_id = 0;
|
|
|
|
self = g_task_get_source_object (device_context->task);
|
|
mm_obj_dbg (self, "task %s: extra 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 && !mm_plugin_is_generic (device_context->best_plugin))
|
|
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;
|
|
GList *tmp;
|
|
|
|
self = device_context->self;
|
|
|
|
device_context->min_wait_time_id = 0;
|
|
mm_obj_dbg (self, "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);
|
|
tmp = device_context->wait_port_contexts;
|
|
device_context->wait_port_contexts = NULL;
|
|
|
|
/* Launch supports check for each port in the Plugin Manager */
|
|
for (l = tmp; l; l = g_list_next (l)) {
|
|
PortContext *port_context = (PortContext *)(l->data);
|
|
|
|
if (!mm_filter_device_and_port (self->priv->filter, port_context->device, port_context->port)) {
|
|
/* If port is filtered, unref it right away */
|
|
port_context_unref (port_context);
|
|
} else {
|
|
/* If port not filtered, store and run it */
|
|
device_context->port_contexts = g_list_append (device_context->port_contexts, port_context);
|
|
device_context_run_port_context (device_context, port_context);
|
|
}
|
|
}
|
|
g_list_free (tmp);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
device_context_port_released (DeviceContext *device_context,
|
|
MMKernelDevice *port)
|
|
{
|
|
MMPluginManager *self;
|
|
PortContext *port_context;
|
|
|
|
self = g_task_get_source_object (device_context->task);
|
|
mm_obj_dbg (self, "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_obj_dbg (self, "task %s: port wasn't found: %s",
|
|
device_context->name, mm_kernel_device_get_name (port));
|
|
}
|
|
|
|
static void
|
|
device_context_port_added (MMPluginManager *self,
|
|
DeviceContext *device_context,
|
|
MMKernelDevice *port)
|
|
{
|
|
PortContext *port_context;
|
|
|
|
mm_obj_dbg (self, "task %s: port added: %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_obj_warn (self, "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_obj_warn (self, "task %s: port context already scheduled",
|
|
device_context->name);
|
|
return;
|
|
}
|
|
|
|
/* Refresh the extra probing timeout. */
|
|
if (device_context->extra_probing_time_id)
|
|
g_source_remove (device_context->extra_probing_time_id);
|
|
device_context->extra_probing_time_id = g_timeout_add (EXTRA_PROBING_TIME_MSECS,
|
|
(GSourceFunc) device_context_extra_probing_time_elapsed,
|
|
device_context);
|
|
|
|
/* Setup a new port context for the newly added port */
|
|
port_context = port_context_new (self,
|
|
device_context->name,
|
|
device_context->device,
|
|
port);
|
|
|
|
mm_obj_dbg (self, "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_obj_dbg (self, "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 added 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)
|
|
{
|
|
MMPluginManager *self;
|
|
|
|
g_assert (device_context->task);
|
|
self = g_task_get_source_object (device_context->task);
|
|
mm_obj_dbg (self, "task %s: cancellation requested", device_context->name);
|
|
|
|
/* The device context is cancelled now */
|
|
g_assert (!g_cancellable_is_cancelled (device_context->cancellable));
|
|
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);
|
|
g_list_foreach (device_context->port_contexts, (GFunc) port_context_cancel, NULL);
|
|
}
|
|
|
|
/* Cancel all timeouts */
|
|
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;
|
|
}
|
|
if (device_context->extra_probing_time_id) {
|
|
g_source_remove (device_context->extra_probing_time_id);
|
|
device_context->extra_probing_time_id = 0;
|
|
}
|
|
|
|
/* If the device context task is not yet completed, 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->released_id);
|
|
g_assert (!device_context->min_wait_time_id);
|
|
g_assert (!device_context->min_probing_time_id);
|
|
g_assert (!device_context->extra_probing_time_id);
|
|
|
|
/* Connect to device port released notifications from the device */
|
|
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 an 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;
|
|
}
|
|
|
|
static void
|
|
plugin_manager_untrack_device_context (MMPluginManager *self,
|
|
DeviceContext *device_context)
|
|
{
|
|
GList *found;
|
|
|
|
found = g_list_find (self->priv->device_contexts, device_context);
|
|
if (found) {
|
|
self->priv->device_contexts = g_list_remove_link (self->priv->device_contexts, found);
|
|
g_list_free_full (found, (GDestroyNotify)device_context_unref);
|
|
}
|
|
}
|
|
|
|
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. If the device support task was early cancelled, e.g. if the last
|
|
* port of an existing device is removed, the device context may not exist
|
|
* in the list tracked by the plugin manager.
|
|
*/
|
|
plugin_manager_untrack_device_context (self, 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, 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 (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_obj_dbg (self, "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);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Cancel the ongoing device support check, if any */
|
|
|
|
gboolean
|
|
mm_plugin_manager_device_support_check_cancel (MMPluginManager *self,
|
|
MMDevice *device)
|
|
{
|
|
g_autoptr(DeviceContext) device_context = NULL;
|
|
|
|
/* 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;
|
|
|
|
/* The device context cancellation operation will also independently request cancellation
|
|
* of each port context. Depending on the probing task of the port, this may be completed
|
|
* asynchronously or in-place. Therefore we need to consider the case where the last port
|
|
* context cancellation also completes the device context operation in-place, so we must
|
|
* ensure the DeviceContext is valid throughout the whole cancellation. */
|
|
device_context = device_context_ref (device_context);
|
|
|
|
/* Remove from the tracked list of device contexts, so that we never add
|
|
* new ports to a device context being cancelled */
|
|
plugin_manager_untrack_device_context (self, device_context);
|
|
|
|
/* Request cancellation, will be completed asynchronously. */
|
|
return device_context_cancel (device_context);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Ask if there is an ongoing device support check */
|
|
|
|
gboolean
|
|
mm_plugin_manager_device_support_check_ongoing (MMPluginManager *self,
|
|
MMDevice *device)
|
|
{
|
|
return !!plugin_manager_peek_device_context (self, device);
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* Report a new port to the ongoing device support check */
|
|
|
|
gboolean
|
|
mm_plugin_manager_device_support_check_add_port (MMPluginManager *self,
|
|
MMDevice *device,
|
|
MMKernelDevice *port)
|
|
{
|
|
DeviceContext *device_context;
|
|
|
|
device_context = plugin_manager_peek_device_context (self, device);
|
|
if (device_context) {
|
|
device_context_port_added (self, device_context, port);
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
/* 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;
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
|
|
const gchar **
|
|
mm_plugin_manager_get_subsystems (MMPluginManager *self)
|
|
{
|
|
return (const gchar **) self->priv->subsystems;
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
|
|
static void
|
|
register_plugin_allowlist_tags (MMPluginManager *self,
|
|
MMPlugin *plugin)
|
|
{
|
|
const gchar **tags;
|
|
guint i;
|
|
|
|
if (!mm_filter_check_rule_enabled (self->priv->filter, MM_FILTER_RULE_PLUGIN_ALLOWLIST))
|
|
return;
|
|
|
|
tags = mm_plugin_get_allowed_udev_tags (plugin);
|
|
for (i = 0; tags && tags[i]; i++)
|
|
mm_filter_register_plugin_allowlist_tag (self->priv->filter, tags[i]);
|
|
}
|
|
|
|
static void
|
|
register_plugin_allowlist_vendor_ids (MMPluginManager *self,
|
|
MMPlugin *plugin)
|
|
{
|
|
const guint16 *vendor_ids;
|
|
guint i;
|
|
|
|
if (!mm_filter_check_rule_enabled (self->priv->filter, MM_FILTER_RULE_PLUGIN_ALLOWLIST))
|
|
return;
|
|
|
|
vendor_ids = mm_plugin_get_allowed_vendor_ids (plugin);
|
|
for (i = 0; vendor_ids && vendor_ids[i]; i++)
|
|
mm_filter_register_plugin_allowlist_vendor_id (self->priv->filter, vendor_ids[i]);
|
|
}
|
|
|
|
static void
|
|
register_plugin_allowlist_product_ids (MMPluginManager *self,
|
|
MMPlugin *plugin)
|
|
{
|
|
const mm_uint16_pair *product_ids;
|
|
guint i;
|
|
|
|
if (!mm_filter_check_rule_enabled (self->priv->filter, MM_FILTER_RULE_PLUGIN_ALLOWLIST))
|
|
return;
|
|
|
|
product_ids = mm_plugin_get_allowed_product_ids (plugin);
|
|
for (i = 0; product_ids && product_ids[i].l; i++)
|
|
mm_filter_register_plugin_allowlist_product_id (self->priv->filter, product_ids[i].l, product_ids[i].r);
|
|
}
|
|
|
|
static void
|
|
register_plugin_allowlist_subsystem_vendor_ids (MMPluginManager *self,
|
|
MMPlugin *plugin)
|
|
{
|
|
const mm_uint16_pair *subsystem_vendor_ids;
|
|
guint i;
|
|
|
|
if (!mm_filter_check_rule_enabled (self->priv->filter, MM_FILTER_RULE_PLUGIN_ALLOWLIST))
|
|
return;
|
|
|
|
subsystem_vendor_ids = mm_plugin_get_allowed_subsystem_vendor_ids (plugin);
|
|
for (i = 0; subsystem_vendor_ids && subsystem_vendor_ids[i].l; i++)
|
|
mm_filter_register_plugin_allowlist_subsystem_vendor_id (self->priv->filter, subsystem_vendor_ids[i].l, subsystem_vendor_ids[i].r);
|
|
}
|
|
|
|
static gboolean
|
|
track_plugin (MMPluginManager *self,
|
|
MMPlugin *plugin,
|
|
GPtrArray **subsystems,
|
|
GError **error)
|
|
{
|
|
const gchar **plugin_subsystems;
|
|
guint i;
|
|
|
|
/* Ignore plugins that don't specify subsystems */
|
|
plugin_subsystems = mm_plugin_get_allowed_subsystems (plugin);
|
|
if (!plugin_subsystems) {
|
|
g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
|
|
"Allowed subsystems not specified");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Process generic plugin */
|
|
if (mm_plugin_is_generic (plugin)) {
|
|
if (self->priv->generic) {
|
|
g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED,
|
|
"Another generic plugin already registered");
|
|
return FALSE;
|
|
}
|
|
self->priv->generic = g_object_ref (plugin);
|
|
} else
|
|
self->priv->plugins = g_list_append (self->priv->plugins, g_object_ref (plugin));
|
|
|
|
/* Track required subsystems, avoiding duplicates in the list */
|
|
for (i = 0; plugin_subsystems[i]; i++) {
|
|
if (!g_ptr_array_find_with_equal_func (*subsystems, plugin_subsystems[i], g_str_equal, NULL))
|
|
g_ptr_array_add (*subsystems, g_strdup (plugin_subsystems[i]));
|
|
}
|
|
|
|
/* Register plugin allowlist rules in filter, if any */
|
|
register_plugin_allowlist_tags (self, plugin);
|
|
register_plugin_allowlist_vendor_ids (self, plugin);
|
|
register_plugin_allowlist_product_ids (self, plugin);
|
|
register_plugin_allowlist_subsystem_vendor_ids (self, plugin);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
validate_tracked_plugins (MMPluginManager *self,
|
|
GPtrArray *subsystems_take,
|
|
GError **error)
|
|
{
|
|
g_autofree gchar *subsystems_str = NULL;
|
|
|
|
/* Check the generic plugin once all looped */
|
|
if (!self->priv->generic)
|
|
mm_obj_dbg (self, "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");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Validate required subsystems */
|
|
if (!subsystems_take->len) {
|
|
g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_NO_PLUGINS,
|
|
"empty list of subsystems required by plugins");
|
|
return FALSE;
|
|
}
|
|
|
|
/* Add trailing NULL and store as GStrv */
|
|
g_ptr_array_add (subsystems_take, NULL);
|
|
self->priv->subsystems = (gchar **) g_ptr_array_free (subsystems_take, FALSE);
|
|
subsystems_str = g_strjoinv (", ", self->priv->subsystems);
|
|
|
|
mm_obj_dbg (self, "successfully loaded %u plugins registering %u subsystems: %s",
|
|
g_list_length (self->priv->plugins) + !!self->priv->generic,
|
|
g_strv_length (self->priv->subsystems), subsystems_str);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
|
|
static MMPlugin *
|
|
load_external_plugin (MMPluginManager *self,
|
|
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, 0);
|
|
if (!module) {
|
|
mm_obj_warn (self, "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_obj_warn (self, "could not load plugin '%s': Missing major version info", path_display);
|
|
goto out;
|
|
}
|
|
|
|
if (*major_plugin_version != MM_PLUGIN_MAJOR_VERSION) {
|
|
mm_obj_warn (self, "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_obj_warn (self, "could not load plugin '%s': Missing minor version info", path_display);
|
|
goto out;
|
|
}
|
|
|
|
if (*minor_plugin_version != MM_PLUGIN_MINOR_VERSION) {
|
|
mm_obj_warn (self, "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_obj_warn (self, "could not load plugin '%s': %s", path_display, g_module_error ());
|
|
goto out;
|
|
}
|
|
|
|
plugin = (*plugin_create_func) ();
|
|
if (plugin)
|
|
mm_obj_dbg (self, "loaded plugin '%s' from '%s'", mm_plugin_get_name (plugin), path_display);
|
|
else
|
|
mm_obj_warn (self, "could not load plugin '%s': initialization failed", path_display);
|
|
|
|
out:
|
|
if (module && !plugin)
|
|
g_module_close (module);
|
|
|
|
g_free (path_display);
|
|
|
|
return plugin;
|
|
}
|
|
|
|
static void
|
|
load_external_shared (MMPluginManager *self,
|
|
const gchar *path)
|
|
{
|
|
GModule *module;
|
|
gchar *path_display;
|
|
const gchar **shared_name = NULL;
|
|
gint *major_shared_version;
|
|
gint *minor_shared_version;
|
|
gboolean success = FALSE;
|
|
|
|
/* Get printable UTF-8 string of the path */
|
|
path_display = g_filename_display_name (path);
|
|
|
|
module = g_module_open (path, 0);
|
|
if (!module) {
|
|
mm_obj_warn (self, "could not load shared '%s': %s", path_display, g_module_error ());
|
|
goto out;
|
|
}
|
|
|
|
if (!g_module_symbol (module, "mm_shared_major_version", (gpointer *) &major_shared_version)) {
|
|
mm_obj_warn (self, "could not load shared '%s': Missing major version info", path_display);
|
|
goto out;
|
|
}
|
|
|
|
if (*major_shared_version != MM_SHARED_MAJOR_VERSION) {
|
|
mm_obj_warn (self, "could not load shared '%s': Shared major version %d, %d is required",
|
|
path_display, *major_shared_version, MM_SHARED_MAJOR_VERSION);
|
|
goto out;
|
|
}
|
|
|
|
if (!g_module_symbol (module, "mm_shared_minor_version", (gpointer *) &minor_shared_version)) {
|
|
mm_obj_warn (self, "could not load shared '%s': Missing minor version info", path_display);
|
|
goto out;
|
|
}
|
|
|
|
if (*minor_shared_version != MM_SHARED_MINOR_VERSION) {
|
|
mm_obj_warn (self, "could not load shared '%s': Shared minor version %d, %d is required",
|
|
path_display, *minor_shared_version, MM_SHARED_MINOR_VERSION);
|
|
goto out;
|
|
}
|
|
|
|
if (!g_module_symbol (module, "mm_shared_name", (gpointer *) &shared_name)) {
|
|
mm_obj_warn (self, "could not load shared '%s': Missing name", path_display);
|
|
goto out;
|
|
}
|
|
|
|
mm_obj_dbg (self, "loaded shared '%s' utils from '%s'", *shared_name, path_display);
|
|
success = TRUE;
|
|
|
|
out:
|
|
if (module && !success)
|
|
g_module_close (module);
|
|
|
|
g_free (path_display);
|
|
}
|
|
|
|
static gboolean
|
|
load_external_plugins (MMPluginManager *self,
|
|
GError **error)
|
|
{
|
|
GDir *dir = NULL;
|
|
const gchar *fname;
|
|
GList *shared_paths = NULL;
|
|
GList *plugin_paths = NULL;
|
|
GList *l;
|
|
GPtrArray *subsystems = NULL;
|
|
g_autofree gchar *plugindir_display = NULL;
|
|
gboolean valid_plugins = FALSE;
|
|
|
|
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_obj_dbg (self, "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) {
|
|
if (!g_str_has_suffix (fname, G_MODULE_SUFFIX))
|
|
continue;
|
|
if (g_str_has_prefix (fname, SHARED_PREFIX))
|
|
shared_paths = g_list_prepend (shared_paths, g_module_build_path (self->priv->plugin_dir, fname));
|
|
else if (g_str_has_prefix (fname, PLUGIN_PREFIX))
|
|
plugin_paths = g_list_prepend (plugin_paths, g_module_build_path (self->priv->plugin_dir, fname));
|
|
}
|
|
|
|
/* Load all shared utils */
|
|
for (l = shared_paths; l; l = g_list_next (l))
|
|
load_external_shared (self, (const gchar *)(l->data));
|
|
|
|
/* Load all plugins */
|
|
subsystems = g_ptr_array_new ();
|
|
for (l = plugin_paths; l; l = g_list_next (l)) {
|
|
g_autoptr(MMPlugin) plugin = NULL;
|
|
g_autoptr(GError) inner_error = NULL;
|
|
|
|
plugin = load_external_plugin (self, (const gchar *)(l->data));
|
|
if (!plugin)
|
|
continue;
|
|
|
|
if (!track_plugin (self, plugin, &subsystems, &inner_error))
|
|
mm_obj_warn (self, "ignored plugin '%s': %s", mm_plugin_get_name (plugin), inner_error->message);
|
|
}
|
|
|
|
valid_plugins = validate_tracked_plugins (self, subsystems, error);
|
|
|
|
out:
|
|
g_list_free_full (shared_paths, g_free);
|
|
g_list_free_full (plugin_paths, g_free);
|
|
if (dir)
|
|
g_dir_close (dir);
|
|
|
|
return valid_plugins;
|
|
}
|
|
|
|
#else
|
|
|
|
static gboolean
|
|
load_builtin_plugins (MMPluginManager *self,
|
|
GError **error)
|
|
{
|
|
GList *builtin_plugins;
|
|
GList *l;
|
|
GPtrArray *subsystems = NULL;
|
|
|
|
subsystems = g_ptr_array_new ();
|
|
|
|
builtin_plugins = mm_builtin_plugins_load ();
|
|
for (l = builtin_plugins; l; l = g_list_next (l)) {
|
|
gboolean tracked;
|
|
MMPlugin *plugin;
|
|
|
|
plugin = MM_PLUGIN (l->data);
|
|
mm_obj_dbg (self, "loaded builtin plugin '%s'", mm_plugin_get_name (plugin));
|
|
tracked = track_plugin (self, plugin, &subsystems, NULL);
|
|
g_assert (tracked);
|
|
}
|
|
g_list_free_full (builtin_plugins, (GDestroyNotify)g_object_unref);
|
|
|
|
return validate_tracked_plugins (self, subsystems, error);
|
|
}
|
|
|
|
#endif /* !WITH_BUILTIN_PLUGINS */
|
|
|
|
/*****************************************************************************/
|
|
|
|
static gchar *
|
|
log_object_build_id (MMLogObject *_self)
|
|
{
|
|
return g_strdup ("plugin-manager");
|
|
}
|
|
|
|
/*****************************************************************************/
|
|
|
|
MMPluginManager *
|
|
mm_plugin_manager_new (MMFilter *filter,
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
const gchar *plugin_dir,
|
|
#endif
|
|
GError **error)
|
|
{
|
|
return g_initable_new (MM_TYPE_PLUGIN_MANAGER,
|
|
NULL,
|
|
error,
|
|
MM_PLUGIN_MANAGER_FILTER, filter,
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
MM_PLUGIN_MANAGER_PLUGIN_DIR, plugin_dir,
|
|
#endif
|
|
NULL);
|
|
}
|
|
|
|
static void
|
|
mm_plugin_manager_init (MMPluginManager *self)
|
|
{
|
|
/* Initialize opaque pointer to private data */
|
|
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
|
|
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) {
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
case PROP_PLUGIN_DIR:
|
|
g_free (priv->plugin_dir);
|
|
priv->plugin_dir = g_value_dup_string (value);
|
|
break;
|
|
#endif
|
|
case PROP_FILTER:
|
|
priv->filter = g_value_dup_object (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) {
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
case PROP_PLUGIN_DIR:
|
|
g_value_set_string (value, priv->plugin_dir);
|
|
break;
|
|
#endif
|
|
case PROP_FILTER:
|
|
g_value_set_object (value, priv->filter);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
initable_init (GInitable *initable,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
#if defined WITH_BUILTIN_PLUGINS
|
|
return load_builtin_plugins (MM_PLUGIN_MANAGER (initable), error);
|
|
#else
|
|
/* Load the list of plugins from the filesystem*/
|
|
return load_external_plugins (MM_PLUGIN_MANAGER (initable), error);
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
dispose (GObject *object)
|
|
{
|
|
MMPluginManager *self = MM_PLUGIN_MANAGER (object);
|
|
|
|
g_list_free_full (g_steal_pointer (&self->priv->plugins), g_object_unref);
|
|
g_clear_object (&self->priv->generic);
|
|
g_clear_object (&self->priv->filter);
|
|
g_clear_pointer (&self->priv->subsystems, g_strfreev);
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
g_clear_pointer (&self->priv->plugin_dir, g_free);
|
|
#endif
|
|
|
|
G_OBJECT_CLASS (mm_plugin_manager_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
initable_iface_init (GInitableIface *iface)
|
|
{
|
|
iface->init = initable_init;
|
|
}
|
|
|
|
static void
|
|
log_object_iface_init (MMLogObjectInterface *iface)
|
|
{
|
|
iface->build_id = log_object_build_id;
|
|
}
|
|
|
|
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 */
|
|
#if !defined WITH_BUILTIN_PLUGINS
|
|
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));
|
|
#endif
|
|
|
|
g_object_class_install_property
|
|
(object_class, PROP_FILTER,
|
|
g_param_spec_object (MM_PLUGIN_MANAGER_FILTER,
|
|
"Filter",
|
|
"Device filter",
|
|
MM_TYPE_FILTER,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
|
|
}
|