b0d9d00762
This is primarily useful for SPI devices. These devices sometimes needs a combination of an SPI and HID device, so discovery is a bit more complicated.
568 lines
17 KiB
C
568 lines
17 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 <config.h>
|
|
|
|
#ifdef HAVE_UDEV
|
|
#include <sys/ioctl.h>
|
|
#include <sys/unistd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <linux/hidraw.h>
|
|
#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;
|
|
|
|
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;
|
|
} 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);
|
|
|
|
g_free (data);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
remove_device (FpContext *context, FpDevice *device)
|
|
{
|
|
RemoveDeviceData *data;
|
|
|
|
data = g_new (RemoveDeviceData, 1);
|
|
data->context = context;
|
|
data->device = device;
|
|
|
|
g_idle_add ((GSourceFunc) remove_device_idle_cb, data);
|
|
}
|
|
|
|
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_swapped (device, "notify::open", (GCallback) device_remove_on_notify_open_cb, context);
|
|
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_swapped (device, "removed", (GCallback) device_removed_cb, context);
|
|
|
|
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);
|
|
|
|
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)
|
|
{
|
|
const gchar * devnode = g_udev_device_get_device_file (matched_hidraw->data);
|
|
int temp_hid = -1, res;
|
|
struct hidraw_devinfo info;
|
|
|
|
if (!devnode)
|
|
continue;
|
|
|
|
temp_hid = open (devnode, O_RDWR);
|
|
if (temp_hid < 0)
|
|
continue;
|
|
|
|
res = ioctl (temp_hid, HIDIOCGRAWINFO, &info);
|
|
close (temp_hid);
|
|
if (res < 0)
|
|
continue;
|
|
if (info.vendor == entry->hid_id.vid && info.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;
|
|
}
|