a3f568db3d
Previously, we checked hidraw devices against drivers by using the HIDIOCGRAWINFO ioctl. While this works, it's not ideal for doing unit tests since umockdev would have to implement hidraw ioctls. The new approach uses the HID_ID property on the parent hid device, which contains the VID/PID pair.
589 lines
18 KiB
C
589 lines
18 KiB
C
/*
|
|
* FpContext - A FPrint context
|
|
* Copyright (C) 2019 Benjamin Berg <bberg@redhat.com>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#define FP_COMPONENT "context"
|
|
#include <fpi-log.h>
|
|
|
|
#include "fpi-context.h"
|
|
#include "fpi-device.h"
|
|
#include <gusb.h>
|
|
#include <stdio.h>
|
|
|
|
#include <config.h>
|
|
|
|
#ifdef HAVE_UDEV
|
|
#include <gudev/gudev.h>
|
|
#endif
|
|
|
|
/**
|
|
* SECTION: fp-context
|
|
* @title: FpContext
|
|
* @short_description: Discover fingerprint devices
|
|
*
|
|
* The #FpContext allows you to discover fingerprint scanning hardware. This
|
|
* is the starting point when integrating libfprint into your software.
|
|
*
|
|
* The <link linkend="device-added">device-added</link> and device-removed signals allow you to handle devices
|
|
* that may be hotplugged at runtime.
|
|
*/
|
|
|
|
typedef struct
|
|
{
|
|
GUsbContext *usb_ctx;
|
|
GCancellable *cancellable;
|
|
|
|
GSList *sources;
|
|
|
|
gint pending_devices;
|
|
gboolean enumerated;
|
|
|
|
GArray *drivers;
|
|
GPtrArray *devices;
|
|
} FpContextPrivate;
|
|
|
|
G_DEFINE_TYPE_WITH_PRIVATE (FpContext, fp_context, G_TYPE_OBJECT)
|
|
|
|
enum {
|
|
DEVICE_ADDED_SIGNAL,
|
|
DEVICE_REMOVED_SIGNAL,
|
|
LAST_SIGNAL
|
|
};
|
|
static guint signals[LAST_SIGNAL] = { 0 };
|
|
|
|
static const char *
|
|
get_drivers_whitelist_env (void)
|
|
{
|
|
return g_getenv ("FP_DRIVERS_WHITELIST");
|
|
}
|
|
|
|
static gboolean
|
|
is_driver_allowed (const gchar *driver)
|
|
{
|
|
g_auto(GStrv) whitelisted_drivers = NULL;
|
|
const char *fp_drivers_whitelist_env;
|
|
int i;
|
|
|
|
g_return_val_if_fail (driver, TRUE);
|
|
|
|
fp_drivers_whitelist_env = get_drivers_whitelist_env ();
|
|
|
|
if (!fp_drivers_whitelist_env)
|
|
return TRUE;
|
|
|
|
whitelisted_drivers = g_strsplit (fp_drivers_whitelist_env, ":", -1);
|
|
|
|
for (i = 0; whitelisted_drivers[i]; ++i)
|
|
if (g_strcmp0 (driver, whitelisted_drivers[i]) == 0)
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
FpContext *context;
|
|
FpDevice *device;
|
|
GSource *source;
|
|
} RemoveDeviceData;
|
|
|
|
static gboolean
|
|
remove_device_idle_cb (RemoveDeviceData *data)
|
|
{
|
|
FpContextPrivate *priv = fp_context_get_instance_private (data->context);
|
|
guint idx = 0;
|
|
|
|
g_return_val_if_fail (g_ptr_array_find (priv->devices, data->device, &idx), G_SOURCE_REMOVE);
|
|
|
|
g_signal_emit (data->context, signals[DEVICE_REMOVED_SIGNAL], 0, data->device);
|
|
g_ptr_array_remove_index_fast (priv->devices, idx);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
remove_device_data_free (RemoveDeviceData *data)
|
|
{
|
|
FpContextPrivate *priv = fp_context_get_instance_private (data->context);
|
|
|
|
priv->sources = g_slist_remove (priv->sources, data->source);
|
|
g_free (data);
|
|
}
|
|
|
|
static void
|
|
remove_device (FpContext *context, FpDevice *device)
|
|
{
|
|
g_autoptr(GSource) source = NULL;
|
|
FpContextPrivate *priv = fp_context_get_instance_private (context);
|
|
RemoveDeviceData *data;
|
|
|
|
data = g_new (RemoveDeviceData, 1);
|
|
data->context = context;
|
|
data->device = device;
|
|
|
|
source = data->source = g_idle_source_new ();
|
|
g_source_set_callback (source,
|
|
G_SOURCE_FUNC (remove_device_idle_cb), data,
|
|
(GDestroyNotify) remove_device_data_free);
|
|
g_source_attach (source, g_main_context_get_thread_default ());
|
|
|
|
priv->sources = g_slist_prepend (priv->sources, source);
|
|
}
|
|
|
|
static void
|
|
device_remove_on_notify_open_cb (FpContext *context, GParamSpec *pspec, FpDevice *device)
|
|
{
|
|
remove_device (context, device);
|
|
}
|
|
|
|
static void
|
|
device_removed_cb (FpContext *context, FpDevice *device)
|
|
{
|
|
gboolean open = FALSE;
|
|
|
|
g_object_get (device, "open", &open, NULL);
|
|
|
|
/* Wait for device close if the device is currently still open. */
|
|
if (open)
|
|
{
|
|
g_signal_connect_object (device, "notify::open",
|
|
(GCallback) device_remove_on_notify_open_cb,
|
|
context,
|
|
G_CONNECT_SWAPPED);
|
|
}
|
|
else
|
|
{
|
|
remove_device (context, device);
|
|
}
|
|
}
|
|
|
|
static void
|
|
async_device_init_done_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
FpDevice *device;
|
|
FpContext *context;
|
|
FpContextPrivate *priv;
|
|
|
|
device = FP_DEVICE (g_async_initable_new_finish (G_ASYNC_INITABLE (source_object),
|
|
res, &error));
|
|
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
|
|
return;
|
|
|
|
context = FP_CONTEXT (user_data);
|
|
priv = fp_context_get_instance_private (context);
|
|
priv->pending_devices--;
|
|
|
|
if (error)
|
|
{
|
|
g_message ("Ignoring device due to initialization error: %s", error->message);
|
|
return;
|
|
}
|
|
|
|
g_ptr_array_add (priv->devices, device);
|
|
|
|
g_signal_connect_object (device, "removed",
|
|
(GCallback) device_removed_cb,
|
|
context,
|
|
G_CONNECT_SWAPPED);
|
|
|
|
g_signal_emit (context, signals[DEVICE_ADDED_SIGNAL], 0, device);
|
|
}
|
|
|
|
static void
|
|
usb_device_added_cb (FpContext *self, GUsbDevice *device, GUsbContext *usb_ctx)
|
|
{
|
|
FpContextPrivate *priv = fp_context_get_instance_private (self);
|
|
GType found_driver = G_TYPE_NONE;
|
|
const FpIdEntry *found_entry = NULL;
|
|
gint found_score = 0;
|
|
gint i;
|
|
guint16 pid, vid;
|
|
|
|
pid = g_usb_device_get_pid (device);
|
|
vid = g_usb_device_get_vid (device);
|
|
|
|
/* Find the best driver to handle this USB device. */
|
|
for (i = 0; i < priv->drivers->len; i++)
|
|
{
|
|
GType driver = g_array_index (priv->drivers, GType, i);
|
|
g_autoptr(FpDeviceClass) cls = g_type_class_ref (driver);
|
|
const FpIdEntry *entry;
|
|
|
|
if (cls->type != FP_DEVICE_TYPE_USB)
|
|
continue;
|
|
|
|
for (entry = cls->id_table; entry->pid; entry++)
|
|
{
|
|
gint driver_score = 50;
|
|
|
|
if (entry->pid != pid || entry->vid != vid)
|
|
continue;
|
|
|
|
if (cls->usb_discover)
|
|
driver_score = cls->usb_discover (device);
|
|
|
|
/* Is this driver better than the one we had? */
|
|
if (driver_score <= found_score)
|
|
continue;
|
|
|
|
found_score = driver_score;
|
|
found_driver = driver;
|
|
found_entry = entry;
|
|
}
|
|
}
|
|
|
|
if (found_driver == G_TYPE_NONE)
|
|
{
|
|
g_debug ("No driver found for USB device %04X:%04X", vid, pid);
|
|
return;
|
|
}
|
|
|
|
priv->pending_devices++;
|
|
g_async_initable_new_async (found_driver,
|
|
G_PRIORITY_LOW,
|
|
priv->cancellable,
|
|
async_device_init_done_cb,
|
|
self,
|
|
"fpi-usb-device", device,
|
|
"fpi-driver-data", found_entry->driver_data,
|
|
NULL);
|
|
}
|
|
|
|
static void
|
|
usb_device_removed_cb (FpContext *self, GUsbDevice *device, GUsbContext *usb_ctx)
|
|
{
|
|
FpContextPrivate *priv = fp_context_get_instance_private (self);
|
|
gint i;
|
|
|
|
/* Do the lazy way and just look at each device. */
|
|
for (i = 0; i < priv->devices->len; i++)
|
|
{
|
|
FpDevice *dev = g_ptr_array_index (priv->devices, i);
|
|
FpDeviceClass *cls = FP_DEVICE_GET_CLASS (dev);
|
|
|
|
if (cls->type != FP_DEVICE_TYPE_USB)
|
|
continue;
|
|
|
|
if (fpi_device_get_usb_device (dev) == device)
|
|
fpi_device_remove (dev);
|
|
}
|
|
}
|
|
|
|
static void
|
|
fp_context_finalize (GObject *object)
|
|
{
|
|
FpContext *self = (FpContext *) object;
|
|
FpContextPrivate *priv = fp_context_get_instance_private (self);
|
|
|
|
g_clear_pointer (&priv->devices, g_ptr_array_unref);
|
|
|
|
g_cancellable_cancel (priv->cancellable);
|
|
g_clear_object (&priv->cancellable);
|
|
g_clear_pointer (&priv->drivers, g_array_unref);
|
|
|
|
g_slist_free_full (g_steal_pointer (&priv->sources), (GDestroyNotify) g_source_destroy);
|
|
|
|
if (priv->usb_ctx)
|
|
g_object_run_dispose (G_OBJECT (priv->usb_ctx));
|
|
g_clear_object (&priv->usb_ctx);
|
|
|
|
G_OBJECT_CLASS (fp_context_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
fp_context_class_init (FpContextClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->finalize = fp_context_finalize;
|
|
|
|
/**
|
|
* FpContext::device-added:
|
|
* @context: the #FpContext instance that emitted the signal
|
|
* @device: A #FpDevice
|
|
*
|
|
* This signal is emitted when a fingerprint reader is added.
|
|
**/
|
|
signals[DEVICE_ADDED_SIGNAL] = g_signal_new ("device-added",
|
|
G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET (FpContextClass, device_added),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__OBJECT,
|
|
G_TYPE_NONE,
|
|
1,
|
|
FP_TYPE_DEVICE);
|
|
|
|
/**
|
|
* FpContext::device-removed:
|
|
* @context: the #FpContext instance that emitted the signal
|
|
* @device: A #FpDevice
|
|
*
|
|
* This signal is emitted when a fingerprint reader is removed.
|
|
*
|
|
* It is guaranteed that the device has been closed before this signal
|
|
* is emitted. See the #FpDevice removed signal documentation for more
|
|
* information.
|
|
**/
|
|
signals[DEVICE_REMOVED_SIGNAL] = g_signal_new ("device-removed",
|
|
G_TYPE_FROM_CLASS (klass),
|
|
G_SIGNAL_RUN_LAST,
|
|
G_STRUCT_OFFSET (FpContextClass, device_removed),
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__OBJECT,
|
|
G_TYPE_NONE,
|
|
1,
|
|
FP_TYPE_DEVICE);
|
|
}
|
|
|
|
static void
|
|
fp_context_init (FpContext *self)
|
|
{
|
|
g_autoptr(GError) error = NULL;
|
|
FpContextPrivate *priv = fp_context_get_instance_private (self);
|
|
guint i;
|
|
|
|
priv->drivers = fpi_get_driver_types ();
|
|
|
|
if (get_drivers_whitelist_env ())
|
|
{
|
|
for (i = 0; i < priv->drivers->len;)
|
|
{
|
|
GType driver = g_array_index (priv->drivers, GType, i);
|
|
g_autoptr(FpDeviceClass) cls = g_type_class_ref (driver);
|
|
|
|
if (!is_driver_allowed (cls->id))
|
|
g_array_remove_index (priv->drivers, i);
|
|
else
|
|
++i;
|
|
}
|
|
}
|
|
|
|
priv->devices = g_ptr_array_new_with_free_func (g_object_unref);
|
|
|
|
priv->cancellable = g_cancellable_new ();
|
|
priv->usb_ctx = g_usb_context_new (&error);
|
|
if (!priv->usb_ctx)
|
|
{
|
|
g_message ("Could not initialise USB Subsystem: %s", error->message);
|
|
}
|
|
else
|
|
{
|
|
g_usb_context_set_debug (priv->usb_ctx, G_LOG_LEVEL_INFO);
|
|
g_signal_connect_object (priv->usb_ctx,
|
|
"device-added",
|
|
G_CALLBACK (usb_device_added_cb),
|
|
self,
|
|
G_CONNECT_SWAPPED);
|
|
g_signal_connect_object (priv->usb_ctx,
|
|
"device-removed",
|
|
G_CALLBACK (usb_device_removed_cb),
|
|
self,
|
|
G_CONNECT_SWAPPED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* fp_context_new:
|
|
*
|
|
* Create a new #FpContext.
|
|
*
|
|
* Returns: (transfer full): a newly created #FpContext
|
|
*/
|
|
FpContext *
|
|
fp_context_new (void)
|
|
{
|
|
return g_object_new (FP_TYPE_CONTEXT, NULL);
|
|
}
|
|
|
|
/**
|
|
* fp_context_enumerate:
|
|
* @context: a #FpContext
|
|
*
|
|
* Enumerate all devices. You should call this function exactly once
|
|
* at startup. Please note that it iterates the mainloop until all
|
|
* devices are enumerated.
|
|
*/
|
|
void
|
|
fp_context_enumerate (FpContext *context)
|
|
{
|
|
FpContextPrivate *priv = fp_context_get_instance_private (context);
|
|
gint i;
|
|
|
|
g_return_if_fail (FP_IS_CONTEXT (context));
|
|
|
|
if (priv->enumerated)
|
|
return;
|
|
|
|
priv->enumerated = TRUE;
|
|
|
|
/* USB devices are handled from callbacks */
|
|
if (priv->usb_ctx)
|
|
g_usb_context_enumerate (priv->usb_ctx);
|
|
|
|
/* Handle Virtual devices based on environment variables */
|
|
for (i = 0; i < priv->drivers->len; i++)
|
|
{
|
|
GType driver = g_array_index (priv->drivers, GType, i);
|
|
g_autoptr(FpDeviceClass) cls = g_type_class_ref (driver);
|
|
const FpIdEntry *entry;
|
|
|
|
if (cls->type != FP_DEVICE_TYPE_VIRTUAL)
|
|
continue;
|
|
|
|
for (entry = cls->id_table; entry->pid; entry++)
|
|
{
|
|
const gchar *val;
|
|
|
|
val = g_getenv (entry->virtual_envvar);
|
|
if (!val || val[0] == '\0')
|
|
continue;
|
|
|
|
g_debug ("Found virtual environment device: %s, %s", entry->virtual_envvar, val);
|
|
priv->pending_devices++;
|
|
g_async_initable_new_async (driver,
|
|
G_PRIORITY_LOW,
|
|
priv->cancellable,
|
|
async_device_init_done_cb,
|
|
context,
|
|
"fpi-environ", val,
|
|
"fpi-driver-data", entry->driver_data,
|
|
NULL);
|
|
g_debug ("created");
|
|
}
|
|
}
|
|
|
|
|
|
#ifdef HAVE_UDEV
|
|
{
|
|
g_autoptr(GUdevClient) udev_client = g_udev_client_new (NULL);
|
|
|
|
/* This uses a very simple algorithm to allocate devices to drivers and assumes that no two drivers will want the same device. Future improvements
|
|
* could add a usb_discover style udev_discover that returns a score, however for internal devices the potential overlap should be very low between
|
|
* separate drivers.
|
|
*/
|
|
|
|
g_autoptr(GList) spidev_devices = g_udev_client_query_by_subsystem (udev_client, "spidev");
|
|
g_autoptr(GList) hidraw_devices = g_udev_client_query_by_subsystem (udev_client, "hidraw");
|
|
|
|
/* for each potential driver, try to match all requested resources. */
|
|
for (i = 0; i < priv->drivers->len; i++)
|
|
{
|
|
GType driver = g_array_index (priv->drivers, GType, i);
|
|
g_autoptr(FpDeviceClass) cls = g_type_class_ref (driver);
|
|
const FpIdEntry *entry;
|
|
|
|
if (cls->type != FP_DEVICE_TYPE_UDEV)
|
|
continue;
|
|
|
|
for (entry = cls->id_table; entry->udev_types; entry++)
|
|
{
|
|
GList *matched_spidev = NULL, *matched_hidraw = NULL;
|
|
|
|
if (entry->udev_types & FPI_DEVICE_UDEV_SUBTYPE_SPIDEV)
|
|
{
|
|
for (matched_spidev = spidev_devices; matched_spidev; matched_spidev = matched_spidev->next)
|
|
{
|
|
const gchar * sysfs = g_udev_device_get_sysfs_path (matched_spidev->data);
|
|
if (!sysfs)
|
|
continue;
|
|
if (strstr (sysfs, entry->spi_acpi_id))
|
|
break;
|
|
}
|
|
/* If match was not found exit */
|
|
if (matched_spidev == NULL)
|
|
continue;
|
|
}
|
|
if (entry->udev_types & FPI_DEVICE_UDEV_SUBTYPE_HIDRAW)
|
|
{
|
|
for (matched_hidraw = hidraw_devices; matched_hidraw; matched_hidraw = matched_hidraw->next)
|
|
{
|
|
/* Find the parent HID node, and check the vid/pid from its HID_ID property */
|
|
g_autoptr(GUdevDevice) parent = g_udev_device_get_parent_with_subsystem (matched_hidraw->data, "hid", NULL);
|
|
const gchar * hid_id = g_udev_device_get_property (parent, "HID_ID");
|
|
guint32 vendor, product;
|
|
|
|
if (!parent || !hid_id)
|
|
continue;
|
|
|
|
if (sscanf (hid_id, "%*X:%X:%X", &vendor, &product) != 2)
|
|
continue;
|
|
|
|
if (vendor == entry->hid_id.vid && product == entry->hid_id.pid)
|
|
break;
|
|
}
|
|
/* If match was not found exit */
|
|
if (matched_hidraw == NULL)
|
|
continue;
|
|
}
|
|
priv->pending_devices++;
|
|
g_async_initable_new_async (driver,
|
|
G_PRIORITY_LOW,
|
|
priv->cancellable,
|
|
async_device_init_done_cb,
|
|
context,
|
|
"fpi-driver-data", entry->driver_data,
|
|
"fpi-udev-data-spidev", (matched_spidev ? g_udev_device_get_device_file (matched_spidev->data) : NULL),
|
|
"fpi-udev-data-hidraw", (matched_hidraw ? g_udev_device_get_device_file (matched_hidraw->data) : NULL),
|
|
NULL);
|
|
/* remove entries from list to avoid conflicts */
|
|
if (matched_spidev)
|
|
{
|
|
g_object_unref (matched_spidev->data);
|
|
spidev_devices = g_list_delete_link (spidev_devices, matched_spidev);
|
|
}
|
|
if (matched_hidraw)
|
|
{
|
|
g_object_unref (matched_hidraw->data);
|
|
hidraw_devices = g_list_delete_link (hidraw_devices, matched_hidraw);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* free all unused elemnts in both lists */
|
|
g_list_foreach (spidev_devices, (GFunc) g_object_unref, NULL);
|
|
g_list_foreach (hidraw_devices, (GFunc) g_object_unref, NULL);
|
|
}
|
|
#endif
|
|
|
|
while (priv->pending_devices)
|
|
g_main_context_iteration (NULL, TRUE);
|
|
}
|
|
|
|
/**
|
|
* fp_context_get_devices:
|
|
* @context: a #FpContext
|
|
*
|
|
* Get all devices. fp_context_enumerate() will be called as needed.
|
|
*
|
|
* Returns: (transfer none) (element-type FpDevice): a new #GPtrArray of #GUsbDevice's.
|
|
*/
|
|
GPtrArray *
|
|
fp_context_get_devices (FpContext *context)
|
|
{
|
|
FpContextPrivate *priv = fp_context_get_instance_private (context);
|
|
|
|
g_return_val_if_fail (FP_IS_CONTEXT (context), NULL);
|
|
|
|
fp_context_enumerate (context);
|
|
|
|
return priv->devices;
|
|
}
|