/* -*- 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"

#define SHARED_PREFIX "libmm-shared"
#define PLUGIN_PREFIX "libmm-plugin"

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,
    PROP_PLUGIN_DIR,
    PROP_FILTER,
    LAST_PROP
};

struct _MMPluginManagerPrivate {
    /* Path to look for plugins */
    gchar *plugin_dir;
    /* 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);

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)
{
    MMPluginManager        *self;
    MMPluginSupportsResult  support_result;
    GError                 *error = NULL;

    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);
        g_error_free (error);
    }

    switch (support_result) {
    case MM_PLUGIN_SUPPORTS_PORT_SUPPORTED:
        port_context_supported (port_context, plugin);
        break;
    case MM_PLUGIN_SUPPORTS_PORT_UNKNOWN:
    case MM_PLUGIN_SUPPORTS_PORT_UNSUPPORTED:
        port_context_unsupported (port_context, plugin);
        break;
    case MM_PLUGIN_SUPPORTS_PORT_DEFER:
        port_context_defer (port_context);
        break;
    case MM_PLUGIN_SUPPORTS_PORT_DEFER_UNTIL_SUGGESTED:
        port_context_defer_until_suggested (port_context, plugin);
        break;
    default:
        g_assert_not_reached ();
    }

    /* 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)
{
    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)
{
    MMPluginManager *self;

    /* 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);

    /* 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_ref (port_context);
    {
        /* 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 */
    }
    port_context_unref (port_context);

    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 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

/* 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 1500
#else
# define EXTRA_PROBING_TIME_MSECS 3000
#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 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->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 = device_context->task;
    device_context->task = NULL;

    /* 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->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;
    }

    /* 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_grabbed (DeviceContext  *device_context,
                             MMKernelDevice *port)
{
    MMPluginManager *self;
    PortContext     *port_context;

    /* Recover plugin manager */
    self = MM_PLUGIN_MANAGER (device_context->self);

    mm_obj_dbg (self, "task %s: port grabbed: %s",
                device_context->name, mm_kernel_device_get_name (port));

    /* Ignore if for any reason we still have it in the running list */
    port_context = device_context_peek_running_port_context (device_context, port);
    if (port_context) {
        mm_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 grabbed 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 grabbed after the min wait timeout expired, launch
     * probing directly */
    device_context_run_port_context (device_context, port_context);
}

static gboolean
device_context_cancel (DeviceContext *device_context)
{
    MMPluginManager *self;

    /* If cancelled already, do nothing */
    if (g_cancellable_is_cancelled (device_context->cancellable))
        return FALSE;

    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_cancellable_cancel (device_context->cancellable);

    /* Remove all port contexts in the waiting list. This will allow early cancellation
     * if it arrives before the min wait time has elapsed */
    if (device_context->wait_port_contexts) {
        g_assert (!device_context->port_contexts);
        g_list_free_full (device_context->wait_port_contexts, (GDestroyNotify) port_context_unref);
        device_context->wait_port_contexts = NULL;
    }

    /* Cancel all ongoing port contexts, if they're not already cancelled */
    if (device_context->port_contexts) {
        g_assert (!device_context->wait_port_contexts);
        /* Request cancellation, will be completed asynchronously */
        g_list_foreach (device_context->port_contexts, (GFunc) port_context_cancel, NULL);
    }

    /* 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;
    }

    /* 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);
    g_assert (!device_context->extra_probing_time_id);

    /* Connect to device port grabbed/released notifications from the device */
    device_context->grabbed_id = g_signal_connect_swapped (device_context->device,
                                                           MM_DEVICE_PORT_GRABBED,
                                                           G_CALLBACK (device_context_port_grabbed),
                                                           device_context);
    device_context->released_id = g_signal_connect_swapped (device_context->device,
                                                            MM_DEVICE_PORT_RELEASED,
                                                            G_CALLBACK (device_context_port_released),
                                                            device_context);

    /* Set the initial waiting timeout. We don't want to probe any port before
     * this timeout expires, so that we get as many ports added in the device
     * as possible. If we don't do this, some plugin filters won't work properly,
     * like the 'forbidden-drivers' one.
     */
    device_context->min_wait_time_id = g_timeout_add (MIN_WAIT_TIME_MSECS,
                                                      (GSourceFunc) device_context_min_wait_time_elapsed,
                                                      device_context);

    /* Set the initial probing timeout. We force the probing time of the device to
     * be at least this amount of time, so that the kernel has enough time to
     * bring up ports. Given that we launch this only when the first port of the
     * device has been exposed in udev, this timeout effectively means that we
     * leave up to 2s to the remaining ports to appear.
     */
    device_context->min_probing_time_id = g_timeout_add (MIN_PROBING_TIME_MSECS,
                                                         (GSourceFunc) device_context_min_probing_time_elapsed,
                                                         device_context);

    /* The full device context is now cancellable. We pass this cancellable also
     * to the inner GTask, so that if we're cancelled we always return a
     * cancellation error, regardless of what the standard logic does. */
    device_context->cancellable = g_cancellable_new ();

    /* Create 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;
}

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, 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);
}

/*****************************************************************************/
/* 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 MMPlugin *
load_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);
        g_object_weak_ref (G_OBJECT (plugin), (GWeakNotify) g_module_close, module);
    } 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_shared (MMPluginManager *self,
             const gchar     *path)
{
    GModule      *module;
    gchar        *path_display;
    const gchar **shared_name = NULL;
    gint         *major_shared_version;
    gint         *minor_shared_version;

    /* 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);

out:
    if (module && !(*shared_name))
        g_module_close (module);

    g_free (path_display);
}

static gboolean
load_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 *subsystems_str = NULL;
    g_autofree gchar *plugindir_display = NULL;

    if (!g_module_supported ()) {
        g_set_error (error,
                     MM_CORE_ERROR,
                     MM_CORE_ERROR_UNSUPPORTED,
                     "modules are not supported on your platform!");
        goto out;
    }

    /* Get printable UTF-8 string of the path */
    plugindir_display = g_filename_display_name (self->priv->plugin_dir);

    mm_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_shared (self, (const gchar *)(l->data));

    /* Load all plugins */
    subsystems = g_ptr_array_new ();
    for (l = plugin_paths; l; l = g_list_next (l)) {
        MMPlugin     *plugin;
        const gchar **plugin_subsystems;
        guint         i;

        plugin = load_plugin (self, (const gchar *)(l->data));
        if (!plugin)
            continue;

        /* Ignore plugins that don't specify subsystems */
        plugin_subsystems = mm_plugin_get_allowed_subsystems (plugin);
        if (!plugin_subsystems) {
            mm_obj_warn (self, "plugin '%s' doesn't specify allowed subsystems: ignored",
                         mm_plugin_get_name (plugin));
            continue;
        }

        /* Process generic plugin */
        if (mm_plugin_is_generic (plugin)) {
            if (self->priv->generic) {
                mm_obj_warn (self, "plugin '%s' is generic and another one is already registered: ignored",
                             mm_plugin_get_name (plugin));
                continue;
            }
            self->priv->generic = plugin;
        } else
            self->priv->plugins = g_list_append (self->priv->plugins, 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);
    }

    /* 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 in plugin directory '%s'",
                     plugindir_display);
        goto out;
    }

    /* Validate required subsystems */
    if (!subsystems->len) {
        g_set_error (error,
                     MM_CORE_ERROR,
                     MM_CORE_ERROR_NO_PLUGINS,
                     "empty list of subsystems required by plugins");
        goto out;
    }
    /* Add trailing NULL and store as GStrv */
    g_ptr_array_add (subsystems, NULL);
    self->priv->subsystems = (gchar **) g_ptr_array_free (subsystems, 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);

out:
    g_list_free_full (shared_paths, g_free);
    g_list_free_full (plugin_paths, g_free);
    if (dir)
        g_dir_close (dir);

    /* Return TRUE if at least one plugin found */
    return (self->priv->plugins || self->priv->generic);
}

/*****************************************************************************/

static gchar *
log_object_build_id (MMLogObject *_self)
{
    return g_strdup ("plugin-manager");
}

/*****************************************************************************/

MMPluginManager *
mm_plugin_manager_new (const gchar  *plugin_dir,
                       MMFilter     *filter,
                       GError      **error)
{
    return g_initable_new (MM_TYPE_PLUGIN_MANAGER,
                           NULL,
                           error,
                           MM_PLUGIN_MANAGER_PLUGIN_DIR, plugin_dir,
                           MM_PLUGIN_MANAGER_FILTER,     filter,
                           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) {
    case PROP_PLUGIN_DIR:
        g_free (priv->plugin_dir);
        priv->plugin_dir = g_value_dup_string (value);
        break;
    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) {
    case PROP_PLUGIN_DIR:
        g_value_set_string (value, priv->plugin_dir);
        break;
    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)
{
    /* Load the list of plugins */
    return load_plugins (MM_PLUGIN_MANAGER (initable), error);
}

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_pointer (&self->priv->plugin_dir, g_free);
    g_clear_object (&self->priv->filter);
    g_clear_pointer (&self->priv->subsystems, g_strfreev);

    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 */
    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));
    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));
}
