From cebe6b17cf733f215b92093fa32985acdc9c6b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Fri, 13 Sep 2024 19:03:33 -0400 Subject: [PATCH 01/10] Introduce USB portal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The USB portal is the middleman between sandboxed apps, and the devices connected and available to the host system. This is the first version of the portal. Device filtering ================ Sandboxed apps must declare which USB devices they support ahead of time. This information is read by the XDG Desktop Portal and used to determine which USB devices will be exposed to requesting apps. On Flatpak, these enumerable and hidden devices are set by the "--usb" and "--nousb" arguments against "flatpak build-finish" and "flatpak run". Neither "--devices=all" nor "--device=usb" do influence the portal. Hidding a device always take precedence over making them enumerable, even when a blanket permission ("--usb=all") is set. Individual devices are assigned a unique identifier by the portal, which is used for all further interactions. This unique identifier is completely random and independent of the device. Permission checks are in place to not allow apps to try and guess device ids without having permission to access then. Permissions =========== There are 2 dynamic permissions managed by the USB portal in the permission store: 1. Blanket USB permission: per-app permission to use any methods of the USB portal. Without this permission, apps must not be able to do anything - enumerate, monitor, or acquire - with the USB portal. [1] 2. Specific device permission: per-app permission to acquire a specific USB device, down to the serial number. Enumerating devices =================== There are 2 ways for apps to learn about devices: - Apps can call the EnumerateDevices() method, which gives a snapshot of the current devices to the app. - Apps can create a device monitoring session with CreateSession() which sends the list of available devices on creation, and also notifies the app about connected and disconnected devices. Only devices that the app is allowed to see are reported in both cases. The udev properties exposed by device enumeration is limited to a well known subset of properties. [2] Device acquisition & release ============================ Once an app has determined which devices it wants to access, the app can call the AcquireDevices() method. This method may prompt a dialog for the user to allow or deny the app from accessing specific devices. If permission is granted, XDG Desktop Portal tries to open the device file on the behalf of the requesting app, and pass down the file descriptor to that file. [3] --- [1] Exceptionally, apps can release previously acquired devices, even when this permission is disabled. This is so because we don't yet have kernel-sided USB revoking. With USB revoking in place, it would be possible to hard-cut app access right when the app permission changes. [2] This patch uses a hardcoded list. There is no mechanism for apps to influence which other udev properties are fetched. This approach is open to suggestions - it may be necessary to expose more information more liberally through the portal. [3] This is clearly not ideal. The ideal approach is to go through logind's TakeDevice() method. However, that will add significant complexity to the portal, since this logind method can only be called by the session controller (i.e. the only executable capable of calling TakeControl() in the session - usually the compositor). This can and probably should be implemented in a subsequent round of improvements to the USB portal. Co-Authored By: Georges Basile Stavracas Neto Co-Authored-By: Ryan Gonzalez Signed-off-by: Hubert Figuière --- .github/workflows/Containerfile | 1 + data/meson.build | 2 + data/org.freedesktop.impl.portal.Usb.xml | 81 ++ data/org.freedesktop.portal.Usb.xml | 236 ++++ doc/api-reference.rst | 1 + doc/impl-dbus-interfaces.rst | 1 + meson.build | 7 + meson_options.txt | 4 + po/POTFILES.in | 1 + src/meson.build | 11 + src/usb.c | 1579 ++++++++++++++++++++++ src/usb.h | 28 + src/xdg-desktop-portal.c | 8 + src/xdp-app-info-flatpak.c | 61 + src/xdp-app-info-private.h | 2 + src/xdp-app-info.c | 19 + src/xdp-app-info.h | 6 + src/xdp-usb-query.c | 213 +++ src/xdp-usb-query.h | 89 ++ src/xdp-utils.c | 2 +- src/xdp-utils.h | 2 +- 21 files changed, 2352 insertions(+), 2 deletions(-) create mode 100644 data/org.freedesktop.impl.portal.Usb.xml create mode 100644 data/org.freedesktop.portal.Usb.xml create mode 100644 src/usb.c create mode 100644 src/usb.h create mode 100644 src/xdp-usb-query.c create mode 100644 src/xdp-usb-query.h diff --git a/.github/workflows/Containerfile b/.github/workflows/Containerfile index 5d34ebb80..7ca313c53 100644 --- a/.github/workflows/Containerfile +++ b/.github/workflows/Containerfile @@ -29,6 +29,7 @@ RUN apt install -y --no-install-recommends \ libgstreamer-plugins-good1.0-dev \ libgeoclue-2-dev \ libglib2.0-dev \ + libgudev-1.0-dev \ libjson-glib-dev \ libpipewire-0.3-dev \ libsystemd-dev \ diff --git a/data/meson.build b/data/meson.build index 4e601077e..66ad68d16 100644 --- a/data/meson.build +++ b/data/meson.build @@ -35,6 +35,7 @@ portal_sources = files( 'org.freedesktop.portal.Session.xml', 'org.freedesktop.portal.Settings.xml', 'org.freedesktop.portal.Trash.xml', + 'org.freedesktop.portal.Usb.xml', 'org.freedesktop.portal.Wallpaper.xml', ) @@ -61,6 +62,7 @@ portal_impl_sources = files( 'org.freedesktop.impl.portal.Secret.xml', 'org.freedesktop.impl.portal.Session.xml', 'org.freedesktop.impl.portal.Settings.xml', + 'org.freedesktop.impl.portal.Usb.xml', 'org.freedesktop.impl.portal.Wallpaper.xml', ) diff --git a/data/org.freedesktop.impl.portal.Usb.xml b/data/org.freedesktop.impl.portal.Usb.xml new file mode 100644 index 000000000..d72b6c3d8 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Usb.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Usb.xml b/data/org.freedesktop.portal.Usb.xml new file mode 100644 index 000000000..05b09b36e --- /dev/null +++ b/data/org.freedesktop.portal.Usb.xml @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/api-reference.rst b/doc/api-reference.rst index 7057851b5..7a0c85f2e 100644 --- a/doc/api-reference.rst +++ b/doc/api-reference.rst @@ -41,4 +41,5 @@ and the object path ``/org/freedesktop/portal/desktop`` on the session bus. doc-org.freedesktop.portal.Session.rst doc-org.freedesktop.portal.Settings.rst doc-org.freedesktop.portal.Trash.rst + doc-org.freedesktop.portal.Usb.rst doc-org.freedesktop.portal.Wallpaper.rst diff --git a/doc/impl-dbus-interfaces.rst b/doc/impl-dbus-interfaces.rst index a7e9425f3..937d2d196 100644 --- a/doc/impl-dbus-interfaces.rst +++ b/doc/impl-dbus-interfaces.rst @@ -30,4 +30,5 @@ accessible to sandboxed applications. doc-org.freedesktop.impl.portal.Secret.rst doc-org.freedesktop.impl.portal.Session.rst doc-org.freedesktop.impl.portal.Settings.rst + doc-org.freedesktop.impl.portal.Usb.rst doc-org.freedesktop.impl.portal.Wallpaper.rst diff --git a/meson.build b/meson.build index 6a571978f..75a264dd7 100644 --- a/meson.build +++ b/meson.build @@ -132,6 +132,7 @@ libportal_dep = dependency( ) pipewire_dep = dependency('libpipewire-0.3', version: '>= 0.2.90') libsystemd_dep = dependency('libsystemd', required: get_option('systemd')) +gudev_dep = dependency('gudev-1.0', required: get_option('gudev')) bwrap = find_program('bwrap', required: get_option('sandboxed-image-validation').allowed() or get_option('sandboxed-sound-validation').allowed()) @@ -160,6 +161,11 @@ if have_libsystemd config_h.set('HAVE_LIBSYSTEMD', 1) endif +have_gudev = gudev_dep.found() +if have_gudev + config_h.set('HAVE_GUDEV', 1) +endif + add_project_arguments(['-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_72'], language: 'c') build_documentation = false @@ -218,6 +224,7 @@ summary({ 'Enable libsystemd support': have_libsystemd, 'Enable geoclue support': have_geoclue, 'Enable libportal support': have_libportal, + 'Enable gudev support': have_gudev, 'Enable installed tests:': enable_installed_tests, 'Enable python test suite': enable_pytest, 'Build man pages': rst2man.found(), diff --git a/meson_options.txt b/meson_options.txt index d64071f42..64d9bd3d3 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -22,6 +22,10 @@ option('geoclue', type: 'feature', value: 'auto', description: 'Enable Geoclue support. Needed for location portal') +option('gudev', + type: 'feature', + value: 'auto', + description: 'Enable udev support. Needed for the USB portal.') option('systemd', type: 'feature', value: 'auto', diff --git a/po/POTFILES.in b/po/POTFILES.in index 3b7adcc80..d514b2167 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -5,4 +5,5 @@ src/dynamic-launcher.c src/location.c src/screenshot.c src/settings.c +src/usb.c src/wallpaper.c diff --git a/src/meson.build b/src/meson.build index 9be185c8e..0da816488 100644 --- a/src/meson.build +++ b/src/meson.build @@ -59,6 +59,7 @@ xdp_utils_sources = files( 'xdp-app-info-flatpak.c', 'xdp-app-info-snap.c', 'xdp-app-info-test.c', + 'xdp-usb-query.c', ) if have_libsystemd @@ -123,6 +124,12 @@ if have_geoclue ) endif +if have_gudev + xdg_desktop_portal_sources += files( + 'usb.c', + ) +endif + common_deps = [ glib_dep, gio_dep, @@ -130,6 +137,10 @@ common_deps = [ json_glib_dep, ] +if gudev_dep.found() + common_deps += gudev_dep +endif + xdg_desktop_portal_deps = common_deps + [ geoclue_dep, pipewire_dep, diff --git a/src/usb.c b/src/usb.c new file mode 100644 index 000000000..a83ab923c --- /dev/null +++ b/src/usb.c @@ -0,0 +1,1579 @@ +/* + * Copyright © 2023-2024 GNOME Foundation Inc. + * 2020 Endless OS Foundation LLC + * + * This program 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 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, see . + * + * Authors: + * Georges Basile Stavracas Neto + * Hubert Figuière + * Ryan Gonzalez + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "usb.h" +#include "request.h" +#include "permissions.h" +#include "session.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" +#include "xdp-usb-query.h" + +#define PERMISSION_TABLE "usb" +#define PERMISSION_ID "usb" +#define MAX_DEVICES 8 + +/* TODO: + * + * AccessDevices() + * - Check if backend is returning appropriate device ids + * - Check if backend is not increasing permissions + * - Save allowed devices in the permission store + */ + +struct _XdpUsb +{ + XdpDbusUsbSkeleton parent_instance; + + GHashTable *ids_to_devices; + GHashTable *syspaths_to_ids; + + GHashTable *sessions; + GHashTable *sender_infos; + + GUdevClient *gudev_client; +}; + +#define XDP_TYPE_USB (xdp_usb_get_type ()) +G_DECLARE_FINAL_TYPE (XdpUsb, xdp_usb, XDP, USB, XdpDbusUsbSkeleton) + +static void xdp_usb_iface_init (XdpDbusUsbIface *iface); + +G_DEFINE_FINAL_TYPE_WITH_CODE (XdpUsb, xdp_usb, XDP_DBUS_TYPE_USB_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_USB, xdp_usb_iface_init)); + +struct _XdpUsbSession +{ + Session parent; + + GHashTable *available_devices; +}; + +#define XDP_TYPE_USB_SESSION (xdp_usb_session_get_type ()) +G_DECLARE_FINAL_TYPE (XdpUsbSession, + xdp_usb_session, + XDP, USB_SESSION, + Session) + +G_DEFINE_TYPE (XdpUsbSession, xdp_usb_session, session_get_type ()) + +typedef struct +{ + char *device_id; + gboolean writable; +} UsbDeviceAcquireData; + +typedef struct _UsbOwnedDevice +{ + gatomicrefcount ref_count; + + char *sender_name; + char *device_id; + int fd; +} UsbOwnedDevice; + +typedef enum +{ + USB_SENDER_STATE_DEFAULT, + USB_SENDER_STATE_ACQUIRING_DEVICES, +} UsbSenderState; + +typedef struct _UsbSenderInfo +{ + gatomicrefcount ref_count; + + char *sender_name; + XdpAppInfo *app_info; + + UsbSenderState sender_state; + GPtrArray *acquiring_devices; + + GHashTable *owned_devices; /* device id → UsbOwnedDevices */ +} UsbSenderInfo; + +static XdpDbusImplUsb *usb_impl; +static XdpUsb *usb; + +static void usb_device_acquire_data_free (UsbDeviceAcquireData *acquire_data); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbDeviceAcquireData, usb_device_acquire_data_free) + +static void usb_owned_device_unref (UsbOwnedDevice *owned_device); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbOwnedDevice, usb_owned_device_unref) + +static void usb_sender_info_unref (UsbSenderInfo *sender_info); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbSenderInfo, usb_sender_info_unref) + +static gboolean +is_gudev_device_suitable (GUdevDevice *device) +{ + const char *device_file = NULL; + const char *devtype = NULL; + + g_assert (G_UDEV_IS_DEVICE (device)); + g_return_val_if_fail (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0, + FALSE); + + device_file = g_udev_device_get_device_file (device); + if (!device_file) + return FALSE; + + devtype = g_udev_device_get_property (device, "DEVTYPE"); + if (!devtype || g_strcmp0 (devtype, "usb_device") != 0) + return FALSE; + + /* We check access to the device */ + if (access (device_file, R_OK) == -1) + return FALSE; + + return TRUE; +} + +static char * +unique_permission_id_for_device (GUdevDevice *device) +{ + g_autoptr(GString) permission_id = NULL; + const char *property; + + permission_id = g_string_new ("usb:"); + + property = g_udev_device_get_property (device, "ID_VENDOR_ID"); + if (property) + g_string_append (permission_id, property); + + property = g_udev_device_get_property (device, "ID_MODEL_ID"); + g_string_append_printf (permission_id, "%s%s", "/", property ? property : ""); + + property = g_udev_device_get_property (device, "ID_SERIAL"); + g_string_append_printf (permission_id, "%s%s", "/", property ? property : ""); + + return g_string_free (g_steal_pointer (&permission_id), FALSE); +} + +static void +usb_device_acquire_data_free (UsbDeviceAcquireData *acquire_data) +{ + g_return_if_fail (acquire_data != NULL); + + g_clear_pointer (&acquire_data->device_id, g_free); + g_clear_pointer (&acquire_data, g_free); +} + +static UsbOwnedDevice * +usb_owned_device_ref (UsbOwnedDevice *owned_device) +{ + g_return_val_if_fail (owned_device != NULL, NULL); + + g_atomic_ref_count_inc (&owned_device->ref_count); + + return owned_device; +} + +static void +usb_owned_device_unref (UsbOwnedDevice *owned_device) +{ + g_return_if_fail (owned_device != NULL); + + if (g_atomic_ref_count_dec (&owned_device->ref_count)) + { + g_clear_fd (&owned_device->fd, NULL); + g_clear_pointer (&owned_device->device_id, g_free); + g_clear_pointer (&owned_device, g_free); + } +} + +static void +usb_sender_info_unref (UsbSenderInfo *sender_info) +{ + g_return_if_fail (sender_info != NULL); + + if (g_atomic_ref_count_dec (&sender_info->ref_count)) + { + g_clear_object (&sender_info->app_info); + g_clear_pointer (&sender_info->sender_name, g_free); + g_clear_pointer (&sender_info->owned_devices, g_hash_table_destroy); + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + g_clear_pointer (&sender_info, g_free); + } +} + +static UsbSenderInfo * +usb_sender_info_new (const char *sender_name, + XdpAppInfo *app_info) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + + sender_info = g_new0 (UsbSenderInfo, 1); + g_atomic_ref_count_init (&sender_info->ref_count); + sender_info->sender_name = g_strdup (sender_name); + sender_info->app_info = g_object_ref (app_info); + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + sender_info->owned_devices = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) usb_owned_device_unref); + + return g_steal_pointer (&sender_info); +} + +static UsbSenderInfo * +usb_sender_info_from_sender (XdpUsb *self, + const char *sender, + XdpAppInfo *app_info) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + + + sender_info = g_hash_table_lookup (self->sender_infos, sender); + + if (!sender_info) + { + sender_info = usb_sender_info_new (sender, app_info); + g_hash_table_insert (self->sender_infos, g_strdup (sender), sender_info); + } + + g_assert (sender_info != NULL); + g_atomic_ref_count_inc (&sender_info->ref_count); + + return g_steal_pointer (&sender_info); +} + +static UsbSenderInfo * +usb_sender_info_from_call (XdpUsb *self, + Call *call) +{ + g_return_val_if_fail (call != NULL, NULL); + + return usb_sender_info_from_sender (self, call->sender, call->app_info); +} + +static UsbSenderInfo * +usb_sender_info_from_request (XdpUsb *self, + Request *request) +{ + g_return_val_if_fail (request != NULL, NULL); + + return usb_sender_info_from_sender (self, request->sender, request->app_info); +} + +static void +usb_sender_info_acquire_device (UsbSenderInfo *sender_info, + const char *device_id, + int fd) +{ + g_autoptr(UsbOwnedDevice) owned_device = NULL; + + g_assert (sender_info != NULL); + g_assert (!g_hash_table_contains (sender_info->owned_devices, device_id)); + + owned_device = g_new0 (UsbOwnedDevice, 1); + g_atomic_ref_count_init (&owned_device->ref_count); + owned_device->device_id = g_strdup (device_id); + owned_device->fd = g_steal_fd (&fd); + + g_hash_table_insert (sender_info->owned_devices, + g_strdup (device_id), + g_steal_pointer (&owned_device)); +} + +static void +usb_sender_info_release_device (UsbSenderInfo *sender_info, + const char *device_id) +{ + g_assert (sender_info != NULL); + + if (!g_hash_table_remove (sender_info->owned_devices, device_id)) + g_warning ("Device %s not owned by %s", device_id, sender_info->sender_name); + +} + +static Permission +usb_sender_info_get_device_permission (UsbSenderInfo *sender_info, + GUdevDevice *device) +{ + g_autofree char *permission_id = NULL; + + g_assert (G_UDEV_IS_DEVICE (device)); + + permission_id = unique_permission_id_for_device (device); + return get_permission_sync (xdp_app_info_get_id (sender_info->app_info), + PERMISSION_TABLE, permission_id); +} + +static void +usb_sender_info_set_device_permission (UsbSenderInfo *sender_info, + GUdevDevice *device, + Permission permission) +{ + g_autofree char *permission_id = NULL; + + g_assert (G_UDEV_IS_DEVICE (device)); + + permission_id = unique_permission_id_for_device (device); + set_permission_sync (xdp_app_info_get_id (sender_info->app_info), + PERMISSION_TABLE, permission_id, permission); +} + +static gboolean +usb_sender_info_match_device (UsbSenderInfo *sender_info, + GUdevDevice *device) +{ + const char *device_subclass_str = NULL; + const char *device_class_str = NULL; + const char *product_id_str = NULL; + const char *vendor_id_str = NULL; + gboolean device_has_product_id = FALSE; + gboolean device_has_vendor_id = FALSE; + gboolean device_has_subclass = FALSE; + gboolean device_has_class = FALSE; + Permission permission; + uint16_t device_product_id; + uint16_t device_vendor_id; + uint16_t device_subclass; + uint16_t device_class; + gboolean match = FALSE; + const GPtrArray *queries = NULL; + + permission = usb_sender_info_get_device_permission (sender_info, device); + if (permission == PERMISSION_NO) + return FALSE; + + vendor_id_str = g_udev_device_get_property (device, "ID_VENDOR_ID"); + if (vendor_id_str != NULL && validate_hex_uint16 (vendor_id_str, 4, &device_vendor_id)) + device_has_vendor_id = TRUE; + + product_id_str = g_udev_device_get_property (device, "ID_MODEL_ID"); + if (product_id_str != NULL && validate_hex_uint16 (product_id_str, 4, &device_product_id)) + device_has_product_id = TRUE; + + device_class_str = g_udev_device_get_sysfs_attr (device, "bDeviceClass"); + if (device_class_str != NULL && validate_hex_uint16 (device_class_str, 2, &device_class)) + device_has_class = TRUE; + + device_subclass_str = g_udev_device_get_sysfs_attr (device, "bDeviceSubclass"); + if (device_subclass_str != NULL && validate_hex_uint16 (device_subclass_str, 2, &device_subclass)) + device_has_subclass = TRUE; + + queries = xdp_app_info_get_usb_queries (sender_info->app_info); + for (size_t i = 0; queries && i < queries->len; i++) + { + XdpUsbQuery *query = g_ptr_array_index (queries, i); + gboolean query_matches = TRUE; + + if (!query) + { + g_debug ("query %ld is null", i); + continue; + } + + for (size_t j = 0; j < query->rules->len; j++) + { + XdpUsbRule *rule = g_ptr_array_index (query->rules, j); + + switch (rule->rule_type) + { + case XDP_USB_RULE_TYPE_ALL: + query_matches = TRUE; + break; + + case XDP_USB_RULE_TYPE_CLASS: + query_matches &= device_has_class && + device_class == rule->d.device_class.class; + + if (rule->d.device_class.type == XDP_USB_RULE_CLASS_TYPE_CLASS_SUBCLASS) + query_matches &= device_has_subclass && + device_subclass == rule->d.device_class.subclass; + + break; + + case XDP_USB_RULE_TYPE_DEVICE: + query_matches &= device_has_product_id && + device_product_id == rule->d.product.id; + break; + + case XDP_USB_RULE_TYPE_VENDOR: + query_matches &= device_has_vendor_id && + device_vendor_id == rule->d.vendor.id; + break; + + default: + g_assert_not_reached (); + } + } + + switch (query->query_type) + { + case XDP_USB_QUERY_TYPE_ENUMERABLE: + if (query_matches) + match = TRUE; + break; + + case XDP_USB_QUERY_TYPE_HIDDEN: + if (query_matches) + return FALSE; + break; + } + } + + return match; +} + +static void +xdp_usb_session_close (Session *session) +{ + g_debug ("USB session '%s' closed", session->id); + + g_assert (g_hash_table_contains (usb->sessions, session)); + g_hash_table_remove (usb->sessions, session); +} + +static void +xdp_usb_session_dispose (GObject *object) +{ + XdpUsbSession *usb_session = XDP_USB_SESSION (object); + + g_clear_pointer (&usb_session->available_devices, g_hash_table_destroy); +} + +static void +xdp_usb_session_class_init (XdpUsbSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + SessionClass *session_class = (SessionClass*) klass; + + object_class->dispose = xdp_usb_session_dispose; + + session_class->close = xdp_usb_session_close; +} + +static void +xdp_usb_session_init (XdpUsbSession *session) +{ + session->available_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); +} + +static XdpUsbSession * +xdp_usb_session_new (GDBusConnection *connection, + Call *call, + GVariant *options, + GError **error) +{ + Session *session = NULL; + + session = g_initable_new (XDP_TYPE_USB_SESSION, + NULL, error, + "connection", connection, + "sender", call->sender, + "app-id", xdp_app_info_get_id (call->app_info), + "token", lookup_session_token (options), + NULL); + if (!session) + return NULL; + + g_debug ("[usb] USB session '%s' created", session->id); + + return XDP_USB_SESSION (session); +} + +static GVariant * +gudev_device_to_variant (XdpUsb *self, + UsbSenderInfo *sender_info, + GUdevDevice *device) +{ + g_auto(GVariantDict) udev_properties_dict = G_VARIANT_DICT_INIT (NULL); + g_auto(GVariantDict) device_variant_dict = G_VARIANT_DICT_INIT (NULL); + g_autoptr(GUdevDevice) parent = NULL; + const char *device_file = NULL; + size_t n_added_properties = 0; + + static const char * const allowed_udev_properties[] = { + "ID_INPUT_JOYSTICK", + "ID_MODEL_ID", + "ID_MODEL_ENC", + "ID_MODEL_FROM_DATABASE", + "ID_REVISION", + "ID_SERIAL", + "ID_SERIAL_SHORT", + "ID_TYPE", + "ID_VENDOR_ENC", + "ID_VENDOR_ID", + "ID_VENDOR_FROM_DATABASE", + NULL, + }; + + g_assert (is_gudev_device_suitable (device)); + + parent = g_udev_device_get_parent (device); + if (parent != NULL && usb_sender_info_match_device (sender_info, parent)) + { + const char *parent_syspath = NULL; + const char *parent_id = NULL; + + parent_syspath = g_udev_device_get_sysfs_path (parent); + if (parent_syspath != NULL) + { + parent_id = g_hash_table_lookup (self->syspaths_to_ids, parent_syspath); + if (parent_id != NULL) + g_variant_dict_insert (&device_variant_dict, "parent", "s", parent_id); + } + } + + device_file = g_udev_device_get_device_file (device); + g_variant_dict_insert (&device_variant_dict, "device-file", "s", device_file); + + if (access (device_file, R_OK) != -1) + g_variant_dict_insert (&device_variant_dict, "readable", "b", TRUE); + if (access (device_file, W_OK) != -1) + g_variant_dict_insert (&device_variant_dict, "writable", "b", TRUE); + + for (size_t i = 0; allowed_udev_properties[i] != NULL; i++) + { + const char *property = g_udev_device_get_property (device, allowed_udev_properties[i]); + + if (!property) + continue; + + g_variant_dict_insert (&udev_properties_dict, allowed_udev_properties[i], "s", property); + n_added_properties++; + } + + if (n_added_properties > 0) + { + g_variant_dict_insert (&device_variant_dict, + "properties", + "@a{sv}", + g_variant_dict_end (&udev_properties_dict)); + } + + return g_variant_ref_sink (g_variant_dict_end (&device_variant_dict)); +} + +/* Register the device and create a unique ID for it */ +static char * +register_with_unique_usb_id (XdpUsb *self, + GUdevDevice *device) +{ + g_autofree char *id = NULL; + const char *syspath; + + g_assert (is_gudev_device_suitable (device)); + + syspath = g_udev_device_get_sysfs_path (device); + g_assert (syspath != NULL); + + do + { + g_clear_pointer (&id, g_free); + id = g_uuid_string_random (); + } + while (g_hash_table_contains (self->ids_to_devices, id)); + + g_debug ("Assigned unique ID %s to USB device %s", id, syspath); + + g_hash_table_insert (self->ids_to_devices, g_strdup (id), g_object_ref (device)); + g_hash_table_insert (self->syspaths_to_ids, g_strdup (syspath), g_strdup (id)); + + return g_steal_pointer (&id); +} + +static void +handle_session_event (XdpUsb *self, + XdpUsbSession *usb_session, + GUdevDevice *device, + const char *id, + const char *action, + gboolean removing) +{ + g_autoptr(GVariant) device_variant = NULL; + GVariantBuilder devices_builder; + UsbSenderInfo *sender_info; + Session *session; + + g_assert (G_UDEV_IS_DEVICE (device)); + g_assert (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0); + + session = SESSION (usb_session); + sender_info = g_hash_table_lookup (self->sender_infos, session->sender); + g_assert (sender_info != NULL); + + /* We can't use usb_sender_info_match_device() when a device is being removed because, + * on removal, the only property the GUdevDevice has is its sysfs path. + * Check if this device was previously available to the USB session + * instead. */ + if ((removing && !g_hash_table_contains (usb_session->available_devices, id)) || + (!removing && !usb_sender_info_match_device (sender_info, device))) + return; + + g_variant_builder_init (&devices_builder, G_VARIANT_TYPE ("a(ssa{sv})")); + + device_variant = gudev_device_to_variant (self, sender_info, device); + g_variant_builder_add (&devices_builder, "(ss@a{sv})", action, id, device_variant); + + g_dbus_connection_emit_signal (session->connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Usb", + "DeviceEvents", + g_variant_new ("(o@a(ssa{sv}))", + session->id, + g_variant_builder_end (&devices_builder)), + NULL); + + if (removing) + g_hash_table_remove (usb_session->available_devices, id); + else + g_hash_table_add (usb_session->available_devices, g_strdup (id)); +} + +static void +gudev_client_uevent_cb (GUdevClient *client, + const char *action, + GUdevDevice *device, + XdpUsb *self) +{ + static const char *supported_actions[] = { + "add", + "change", + "remove", + NULL, + }; + + g_autofree char *id = NULL; + GHashTableIter iter; + XdpUsbSession *usb_session; + const char *syspath = NULL; + gboolean removing; + + if (!g_strv_contains (supported_actions, action)) + return; + + if (!is_gudev_device_suitable (device)) + return; + + removing = g_str_equal (action, "remove"); + + if (g_str_equal (action, "add")) + { + id = register_with_unique_usb_id (self, device); + } + else + { + syspath = g_udev_device_get_sysfs_path (device); + + g_assert (syspath != NULL); + id = g_strdup (g_hash_table_lookup (self->syspaths_to_ids, syspath)); + } + + g_assert (id != NULL); + + /* Send event to all sessions that are allowed to handle it */ + g_hash_table_iter_init (&iter, self->sessions); + while (g_hash_table_iter_next (&iter, (gpointer *) &usb_session, NULL)) + handle_session_event (self, usb_session, device, id, action, removing); + + if (removing) + { + g_assert (syspath != NULL); + + g_debug ("Removing %s -> %s", id, syspath); + + /* The value of id is owned by syspaths_to_ids, so that must be removed *after* + the id is used for removal from ids_to_devices. */ + if (!g_hash_table_remove (self->ids_to_devices, id)) + g_critical ("Error removing USB device from ids_to_devices table"); + + if (!g_hash_table_remove (self->syspaths_to_ids, syspath)) + g_critical ("Error removing USB device from syspaths_to_ids table"); + } +} + +static void +send_initial_device_list (XdpUsb *self, + XdpUsbSession *usb_session, + Call *call) +{ + /* Send initial list of devices the app has permission to see */ + g_autoptr(UsbSenderInfo) sender_info = NULL; + Session *session = SESSION (usb_session); + GVariantBuilder devices_builder; + g_autoptr(GVariant) events = NULL; + GHashTableIter iter; + GUdevDevice *device; + const char *id; + gboolean has_devices = FALSE; + + g_debug ("[usb] Appending devices to CreateSession response"); + + g_variant_builder_init (&devices_builder, G_VARIANT_TYPE ("(oa(ssa{sv}))")); + g_variant_builder_add (&devices_builder, "o", session->id); + g_variant_builder_open (&devices_builder, G_VARIANT_TYPE ("a(ssa{sv})")); + + g_assert (self != NULL); + + sender_info = usb_sender_info_from_call (self, call); + + g_hash_table_iter_init (&iter, self->ids_to_devices); + while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &device)) + { + g_autoptr(GVariant) device_variant = NULL; + + g_assert (G_UDEV_IS_DEVICE (device)); + g_assert (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0); + + if (!usb_sender_info_match_device (sender_info, device)) + continue; + + device_variant = gudev_device_to_variant (self, sender_info, device); + g_variant_builder_add (&devices_builder, "(ss@a{sv})", "add", id, device_variant); + + g_hash_table_add (usb_session->available_devices, g_strdup (id)); + + has_devices = TRUE; + } + + g_variant_builder_close (&devices_builder); + events = g_variant_ref_sink (g_variant_builder_end (&devices_builder)); + + if (!has_devices) + return; + + g_dbus_connection_emit_signal (session->connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Usb", + "DeviceEvents", + events, + NULL); +} + +static gboolean +handle_create_session (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + GDBusConnection *connection; + GVariantBuilder options_builder; + XdpUsbSession *usb_session; + Permission permission; + Session *session; + Call *call; + XdpUsb *self; + + static const XdpOptionKey usb_create_session_options[] = { + { "session_handle_token", G_VARIANT_TYPE_STRING, NULL }, + }; + + self = XDP_USB (object); + call = call_from_invocation (invocation); + + g_debug ("[usb] Handling CreateSession"); + + permission = get_permission_sync (xdp_app_info_get_id (call->app_info), + PERMISSION_TABLE, + PERMISSION_ID); + if (permission == PERMISSION_NO) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed to create USB sessions"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, + &options_builder, + usb_create_session_options, + G_N_ELEMENTS (usb_create_session_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_ref_sink (g_variant_builder_end (&options_builder)); + + connection = g_dbus_method_invocation_get_connection (invocation); + usb_session = xdp_usb_session_new (connection, call, options, &error); + if (!usb_session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + session = SESSION (usb_session); + if (!session_export (session, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + session_close (session, FALSE); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + session_register (session); + + g_debug ("New USB session registered: %s", session->id); + g_hash_table_add (self->sessions, usb_session); + + xdp_dbus_usb_complete_create_session (object, invocation, session->id); + + send_initial_device_list (self, usb_session, call); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static GVariant * +list_permitted_devices (XdpUsb *self, + Call *call) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + GVariantBuilder builder; + GHashTableIter iter; + GUdevDevice *device; + const char *id; + + sender_info = usb_sender_info_from_call (self, call); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sa{sv})")); + + g_hash_table_iter_init (&iter, self->ids_to_devices); + while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &device)) + { + g_assert (G_UDEV_IS_DEVICE (device)); + g_assert (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0); + + if (usb_sender_info_match_device (sender_info, device)) + { + g_autoptr(GVariant) device_variant = gudev_device_to_variant (self, sender_info, device); + g_variant_builder_add (&builder, "(s@a{sv})", id, device_variant); + } + } + + return g_variant_ref_sink (g_variant_builder_end (&builder)); +} + +/* List devices the app has permission */ +static gboolean +handle_enumerate_devices (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_autoptr(GVariant) options = NULL; + g_autoptr(GVariant) devices = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + Permission permission; + Call *call; + XdpUsb *self; + + static const XdpOptionKey usb_enumerate_devices_options[] = { + }; + + self = XDP_USB (object); + call = call_from_invocation (invocation); + + permission = get_permission_sync (xdp_app_info_get_id (call->app_info), + PERMISSION_TABLE, + PERMISSION_ID); + + if (permission == PERMISSION_NO) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed to enumerate devices"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + usb_enumerate_devices_options, + G_N_ELEMENTS (usb_enumerate_devices_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_ref_sink (g_variant_builder_end (&options_builder)); + + devices = list_permitted_devices(self, call); + + xdp_dbus_usb_complete_enumerate_devices (object, invocation, devices); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +usb_acquire_devices_cb (GObject *source_object, + GAsyncResult *result, + gpointer data) +{ + XdgDesktopPortalResponseEnum response; + g_autoptr(UsbSenderInfo) sender_info = NULL; + g_autoptr(GVariantIter) devices_iter = NULL; + g_auto(GVariantBuilder) results_builder; + g_autoptr (GVariant) results = NULL; + g_autoptr(Request) request = data; + g_autoptr(GError) error = NULL; + GVariant *options; + const char *device_id; + + REQUEST_AUTOLOCK (request); + + response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER; + sender_info = usb_sender_info_from_request (usb, request); + + g_assert (sender_info != NULL); + g_assert (sender_info->sender_state == USB_SENDER_STATE_ACQUIRING_DEVICES); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_usb_call_acquire_devices_finish (usb_impl, &response, &results, result, &error)) + { + response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER; + g_dbus_error_strip_remote_error (error); + goto out; + } + + /* TODO: check if the list of devices that the backend reported is strictly + * equal or a subset of the devices the app requested. */ + + /* TODO: check if we're strictly equal or downgrading the "writable" option */ + + if (!g_variant_lookup (results, "devices", "a(sa{sv})", &devices_iter)) + goto out; + + if (response == XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS) + { + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + sender_info->acquiring_devices = + g_ptr_array_new_full (g_variant_iter_n_children (devices_iter), + (GDestroyNotify) usb_device_acquire_data_free); + while (g_variant_iter_next (devices_iter, "(&s@a{sv})", &device_id, &options)) + { + g_autoptr(UsbDeviceAcquireData) access_data = NULL; + GUdevDevice *device; + gboolean writable; + + device = g_hash_table_lookup (usb->ids_to_devices, device_id); + if (!device) + continue; + + if (!g_variant_lookup (options, "writable", "b", &writable)) + writable = FALSE; + + access_data = g_new0 (UsbDeviceAcquireData, 1); + access_data->device_id = g_strdup (device_id); + access_data->writable = writable; + + g_ptr_array_add (sender_info->acquiring_devices, g_steal_pointer (&access_data)); + + usb_sender_info_set_device_permission (sender_info, device, PERMISSION_YES); + + g_clear_pointer (&options, g_variant_unref); + } + } + else if (response == XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED) + { + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + } + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + request_unexport (request); + } +} + +static gboolean +filter_access_devices (XdpUsb *self, + UsbSenderInfo *sender_info, + GVariant *devices, + GVariant **out_filtered_devices, + GError **out_error) +{ + GVariantBuilder filtered_devices_builder; + GVariantIter *device_options_iter; + GVariantIter devices_iter; + const char *device_id; + size_t n_devices; + + static const XdpOptionKey usb_device_options[] = { + { "writable", G_VARIANT_TYPE_BOOLEAN, NULL }, + }; + + g_assert (self != NULL); + g_assert (sender_info != NULL); + g_assert (devices != NULL); + g_assert (out_filtered_devices != NULL && *out_filtered_devices == NULL); + g_assert (out_error != NULL && *out_error == NULL); + + n_devices = g_variant_iter_init (&devices_iter, devices); + + if (n_devices == 0) + { + g_set_error (out_error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "No devices in the devices array"); + return FALSE; + } + + g_variant_builder_init (&filtered_devices_builder, G_VARIANT_TYPE ("a(sa{sv}a{sv})")); + + while (g_variant_iter_next (&devices_iter, + "(&sa{sv})", + &device_id, + &device_options_iter)) + { + g_autoptr(GVariantIter) owned_deviced_options_iter = device_options_iter; + g_autoptr(GVariant) device_variant = NULL; + GVariantDict device_options_dict; + GUdevDevice *device; + GVariant *device_option_value; + const char *device_option; + + device = g_hash_table_lookup (self->ids_to_devices, device_id); + + if (!device) + { + g_set_error (out_error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Device %s not available", + device_id); + return FALSE; + } + + g_assert (G_UDEV_IS_DEVICE (device)); + g_assert (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0); + + /* Can the app even request this device? */ + if (!usb_sender_info_match_device (sender_info, device)) + { + g_set_error (out_error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Access to device %s is not allowed", + device_id); + return FALSE; + } + + g_variant_dict_init (&device_options_dict, NULL); + + while (g_variant_iter_next (device_options_iter, + "{&sv}", + &device_option, + &device_option_value)) + { + for (size_t i = 0; i < G_N_ELEMENTS (usb_device_options); i++) + { + if (g_strcmp0 (device_option, usb_device_options[i].key) != 0) + continue; + + if (!g_variant_is_of_type (device_option_value, usb_device_options[i].type)) + { + g_set_error (out_error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Invalid type for option '%s'", + device_option); + g_variant_builder_clear (&filtered_devices_builder); + g_variant_dict_clear (&device_options_dict); + g_clear_pointer (&device_option_value, g_variant_unref); + return FALSE; + } + + g_variant_dict_insert_value (&device_options_dict, device_option, device_option_value); + + g_clear_pointer (&device_option_value, g_variant_unref); + } + } + + device_variant = gudev_device_to_variant (self, sender_info, device); + + g_variant_builder_add (&filtered_devices_builder, + "(s@a{sv}@a{sv})", + device_id, + device_variant, + g_variant_dict_end (&device_options_dict)); + } + + *out_filtered_devices = + g_variant_ref_sink (g_variant_builder_end (&filtered_devices_builder)); + return TRUE; +} + +static gboolean +handle_acquire_devices (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + const char *arg_parent_window, + GVariant *arg_devices, + GVariant *arg_options) +{ + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autoptr(UsbSenderInfo) sender_info = NULL; + g_autoptr(GVariant) filtered_devices = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + Permission permission; + Request *request; + XdpUsb *self; + + static const XdpOptionKey usb_acquire_devices_options[] = { + }; + + self = XDP_USB (object); + request = request_from_invocation (invocation); + + g_debug ("[usb] Handling AccessDevices"); + + REQUEST_AUTOLOCK (request); + + permission = get_permission_sync (xdp_app_info_get_id (request->app_info), + PERMISSION_TABLE, + PERMISSION_ID); + if (permission == PERMISSION_NO) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed to create USB sessions"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (usb_impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (usb_impl)), + request->id, + NULL, + &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, + &options_builder, + usb_acquire_devices_options, + G_N_ELEMENTS (usb_acquire_devices_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_ref_sink (g_variant_builder_end (&options_builder)); + + sender_info = usb_sender_info_from_request (self, request); + g_assert (sender_info != NULL); + + if (sender_info->sender_state != USB_SENDER_STATE_DEFAULT) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Cannot call AcquireDevices() with an unfinished " + "call to AcquireDevices()"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* Validate devices */ + if (!filter_access_devices (self, sender_info, arg_devices, &filtered_devices, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + sender_info->sender_state = USB_SENDER_STATE_ACQUIRING_DEVICES; + + xdp_dbus_impl_usb_call_acquire_devices (usb_impl, + request->id, + arg_parent_window, + xdp_app_info_get_id (request->app_info), + filtered_devices, + options, + NULL, + usb_acquire_devices_cb, + g_object_ref (request)); + + xdp_dbus_usb_complete_acquire_devices (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_finish_acquire_devices (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + g_autoptr(GUnixFDList) fds = NULL; + GVariantBuilder results_builder; + Permission permission; + uint32_t accessed_devices; + gboolean finished; + Call *call; + XdpUsb *self; + + self = XDP_USB (object); + call = call_from_invocation (invocation); + + g_debug ("[usb] Handling FinishAccessDevices"); + + sender_info = usb_sender_info_from_call (self, call); + + permission = get_permission_sync (xdp_app_info_get_id (call->app_info), + PERMISSION_TABLE, + PERMISSION_ID); + if (permission == PERMISSION_NO) + { + /* If permission was revoked in between D-Bus calls, reset state */ + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed to access USB devices"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (sender_info->sender_state != USB_SENDER_STATE_ACQUIRING_DEVICES || sender_info->acquiring_devices == NULL) + { + /* If the request was cancelled in some way. This would happen by calling + * FinishAcquireDevices after the user denied the permission. + */ + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "There is no device being acquired"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* We should never trigger these asserts. */ + g_assert (sender_info->sender_state == USB_SENDER_STATE_ACQUIRING_DEVICES); + g_assert (sender_info->acquiring_devices != NULL); + + fds = g_unix_fd_list_new (); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE ("a(sa{sv})")); + + accessed_devices = 0; + while (accessed_devices < MAX_DEVICES && + sender_info->acquiring_devices->len > 0) + { + g_autoptr(UsbDeviceAcquireData) access_data = NULL; + g_autoptr(GError) error = NULL; + GVariantDict dict; + UsbOwnedDevice *owned_device = NULL; + g_autofd int fd = -1; + int fd_index; + + g_variant_dict_init (&dict, NULL); + + access_data = g_ptr_array_steal_index (sender_info->acquiring_devices, 0); + + /* Check we haven't already acquired the device */ + owned_device = g_hash_table_lookup (sender_info->owned_devices, access_data->device_id); + if (!owned_device) + { + const char *device_file; + GUdevDevice *device; + + device = g_hash_table_lookup (self->ids_to_devices, access_data->device_id); + + if (!device) + { + g_variant_dict_insert (&dict, "success", "b", FALSE); + g_variant_dict_insert (&dict, "error", "s", _("Device not available")); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + continue; + } + + device_file = g_udev_device_get_device_file (device); + g_assert (device_file != NULL); + + /* Can the app even request this device? */ + if (!usb_sender_info_match_device (sender_info, device)) + { + g_variant_dict_insert (&dict, "success", "b", FALSE); + g_variant_dict_insert (&dict, "error", "s", _("Not allowed")); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + continue; + } + + fd = open (device_file, access_data->writable ? O_RDWR : O_RDONLY); + if (fd == -1) + { + g_variant_dict_insert (&dict, "success", "b", FALSE); + g_variant_dict_insert (&dict, "error", "s", g_strerror (errno)); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + continue; + } + fd_index = g_unix_fd_list_append (fds, fd, &error); + } + else + { + /* If we have already acquired the device, just return the fd again */ + fd_index = g_unix_fd_list_append (fds, owned_device->fd, &error); + } + + if (error) + { + g_variant_dict_insert (&dict, "success", "b", FALSE); + g_variant_dict_insert (&dict, "error", "s", error->message); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + continue; + } + + /* This sender now owns this device. Either create a new one + * or ref the existing one. + */ + if (!owned_device) + { + usb_sender_info_acquire_device (sender_info, + access_data->device_id, + g_steal_fd (&fd)); + } + else + { + usb_owned_device_ref (owned_device); + } + + g_variant_dict_insert (&dict, "success", "b", TRUE); + g_variant_dict_insert (&dict, "fd", "h", fd_index); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + + accessed_devices++; + } + + finished = sender_info->acquiring_devices->len == 0; + + if (finished) + { + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + } + + g_dbus_method_invocation_return_value_with_unix_fd_list (invocation, + g_variant_new ("(@a(sa{sv})b)", + g_variant_builder_end (&results_builder), + finished), + fds); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static gboolean +handle_release_devices (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + const char * const *arg_devices, + GVariant *arg_options) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + Call *call; + XdpUsb *self; + + static const XdpOptionKey usb_release_devices_options[] = { + }; + + self = XDP_USB (object); + call = call_from_invocation (invocation); + + g_debug ("[usb] Handling ReleaseDevices"); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, + &options_builder, + usb_release_devices_options, + G_N_ELEMENTS (usb_release_devices_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_ref_sink (g_variant_builder_end (&options_builder)); + + sender_info = usb_sender_info_from_call (self, call); + g_assert (sender_info != NULL); + + for (size_t i = 0; arg_devices && arg_devices[i]; i++) + usb_sender_info_release_device (sender_info, arg_devices[i]); + + xdp_dbus_usb_complete_release_devices (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +xdp_usb_iface_init (XdpDbusUsbIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_enumerate_devices = handle_enumerate_devices; + iface->handle_acquire_devices = handle_acquire_devices; + iface->handle_finish_acquire_devices = handle_finish_acquire_devices; + iface->handle_release_devices = handle_release_devices; +} + +static void +xdp_usb_dispose (GObject *object) +{ + XdpUsb *self = XDP_USB (object); + + g_clear_pointer (&self->ids_to_devices, g_hash_table_unref); + g_clear_pointer (&self->syspaths_to_ids, g_hash_table_unref); + g_clear_pointer (&self->sessions, g_hash_table_unref); + + g_clear_object (&self->gudev_client); +} + +static void +xdp_usb_class_init (XdpUsbClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = xdp_usb_dispose; +} + +static void +xdp_usb_init (XdpUsb *self) +{ + g_autolist(GUdevDevice) devices = NULL; + static const char * const subsystems[] = { + "usb", + NULL, + }; + + g_debug ("[usb] Initializing USB portal"); + + xdp_dbus_usb_set_version (XDP_DBUS_USB (self), 1); + + self->ids_to_devices = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + self->syspaths_to_ids = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_free); + self->sessions = g_hash_table_new (g_direct_hash, g_direct_equal); + self->sender_infos = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) usb_sender_info_unref); + + self->gudev_client = g_udev_client_new (subsystems); + g_signal_connect (self->gudev_client, + "uevent", + G_CALLBACK (gudev_client_uevent_cb), + self); + + /* Initialize devices */ + devices = g_udev_client_query_by_subsystem (self->gudev_client, "usb"); + for (GList *l = devices; l; l = l->next) + { + g_autofree char *id = NULL; + GUdevDevice *device = l->data; + + if (!is_gudev_device_suitable (device)) + continue; + + id = register_with_unique_usb_id (self, device); + } +} + +static void +peer_died_cb (const char *sender) +{ + if (usb && g_hash_table_remove (usb->sender_infos, sender)) + g_debug ("Removed sender %s", sender); +} + +GDBusInterfaceSkeleton * +xdp_usb_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + usb_impl = xdp_dbus_impl_usb_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (usb_impl == NULL) + { + g_warning ("Failed to create USB proxy: %s", error->message); + return NULL; + } + + xdp_connection_track_name_owners (connection, peer_died_cb); + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (usb_impl), G_MAXINT); + + g_assert (usb_impl != NULL); + g_assert (usb == NULL); + + usb = g_object_new (xdp_usb_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (usb); +} diff --git a/src/usb.h b/src/usb.h new file mode 100644 index 000000000..636382895 --- /dev/null +++ b/src/usb.h @@ -0,0 +1,28 @@ +/* + * Copyright © 2023 GNOME Foundation Inc. + * 2020 Endless OS Foundation LLC + * + * This program 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 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, see . + * + * Authors: + * Georges Basile Stavracas Neto + * Ryan Gonzalez + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * xdp_usb_create (GDBusConnection *connection, + const char *dbus_name); diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c index eab3d6d5e..f71637fff 100644 --- a/src/xdg-desktop-portal.c +++ b/src/xdg-desktop-portal.c @@ -67,6 +67,7 @@ #include "secret.h" #include "settings.h" #include "trash.h" +#include "usb.h" #include "wallpaper.h" static int global_exit_status = 0; @@ -362,6 +363,13 @@ on_bus_acquired (GDBusConnection *connection, if (implementation != NULL) export_portal_implementation (connection, input_capture_create (connection, implementation->dbus_name)); + +#ifdef HAVE_GUDEV + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Usb"); + if (implementation != NULL) + export_portal_implementation (connection, + xdp_usb_create (connection, implementation->dbus_name)); +#endif } static void diff --git a/src/xdp-app-info-flatpak.c b/src/xdp-app-info-flatpak.c index 41594524f..771887a4a 100644 --- a/src/xdp-app-info-flatpak.c +++ b/src/xdp-app-info-flatpak.c @@ -1,5 +1,6 @@ /* * Copyright © 2024 Red Hat, Inc + * Copyright © 2024 GNOME Foundation Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later * @@ -15,6 +16,9 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . + * + * Authors: + * Hubert Figuière */ #include "config.h" @@ -28,6 +32,7 @@ #include #include "xdp-app-info-flatpak-private.h" +#include "xdp-usb-query.h" #define FLATPAK_ENGINE_ID "org.flatpak" @@ -48,6 +53,7 @@ struct _XdpAppInfoFlatpak XdpAppInfo parent; GKeyFile *flatpak_info; + GPtrArray *queries; }; G_DEFINE_FINAL_TYPE (XdpAppInfoFlatpak, xdp_app_info_flatpak, XDP_TYPE_APP_INFO) @@ -364,6 +370,58 @@ xdp_app_info_flatpak_validate_autostart (XdpAppInfo *app_info, return TRUE; } +static const GPtrArray * +xdp_app_info_flaptak_get_usb_queries (XdpAppInfo *app_info) +{ + XdpAppInfoFlatpak *app_info_flatpak = XDP_APP_INFO_FLATPAK (app_info); + + if (!app_info_flatpak->queries) + { + g_autoptr(GPtrArray) usb_queries = NULL; + + usb_queries = g_ptr_array_new_with_free_func ((GDestroyNotify) xdp_usb_query_free); + + g_auto(GStrv) enumerable_devices = NULL; + g_auto(GStrv) hidden_devices = NULL; + + enumerable_devices = g_key_file_get_string_list (app_info_flatpak->flatpak_info, + "USB Devices", + "enumerable-devices", + NULL, NULL); + + for (size_t i = 0; enumerable_devices && enumerable_devices[i] != NULL; i++) + { + g_autoptr(XdpUsbQuery) query = + xdp_usb_query_from_string (XDP_USB_QUERY_TYPE_ENUMERABLE, enumerable_devices[i]); + + if (query) + g_ptr_array_add (usb_queries, g_steal_pointer (&query)); + } + + hidden_devices = g_key_file_get_string_list (app_info_flatpak->flatpak_info, + "USB Devices", + "hidden-devices", + NULL, NULL); + + for (size_t i = 0; hidden_devices && hidden_devices[i] != NULL; i++) + { + g_autoptr(XdpUsbQuery) query = + xdp_usb_query_from_string (XDP_USB_QUERY_TYPE_HIDDEN, hidden_devices[i]); + + if (query) + g_ptr_array_add (usb_queries, g_steal_pointer (&query)); + } + + g_message ("Found %d enumerable and %d hidden for app %s", + enumerable_devices ? g_strv_length (enumerable_devices) : 0, + hidden_devices ? g_strv_length (hidden_devices) : 0, + xdp_app_info_get_id (app_info)); + app_info_flatpak->queries = g_steal_pointer (&usb_queries); + } + + return app_info_flatpak->queries; +} + static gboolean xdp_app_info_flatpak_validate_dynamic_launcher (XdpAppInfo *app_info, GKeyFile *key_file, @@ -430,6 +488,7 @@ xdp_app_info_flatpak_dispose (GObject *object) XdpAppInfoFlatpak *app_info = XDP_APP_INFO_FLATPAK (object); g_clear_pointer (&app_info->flatpak_info, g_key_file_free); + g_clear_pointer (&app_info->queries, g_ptr_array_unref); G_OBJECT_CLASS (xdp_app_info_flatpak_parent_class)->dispose (object); } @@ -444,6 +503,8 @@ xdp_app_info_flatpak_class_init (XdpAppInfoFlatpakClass *klass) app_info_class->remap_path = xdp_app_info_flatpak_remap_path; + app_info_class->get_usb_queries = + xdp_app_info_flaptak_get_usb_queries; app_info_class->validate_autostart = xdp_app_info_flatpak_validate_autostart; app_info_class->validate_dynamic_launcher = diff --git a/src/xdp-app-info-private.h b/src/xdp-app-info-private.h index fb68dbd06..0b5b06081 100644 --- a/src/xdp-app-info-private.h +++ b/src/xdp-app-info-private.h @@ -33,6 +33,8 @@ struct _XdpAppInfoClass char * (*remap_path) (XdpAppInfo *app_info, const char *path); + const GPtrArray * (*get_usb_queries) (XdpAppInfo *app_info); + gboolean (*validate_autostart) (XdpAppInfo *app_info, GKeyFile *keyfile, const char * const *autostart_exec, diff --git a/src/xdp-app-info.c b/src/xdp-app-info.c index e6799ec48..c755811f9 100644 --- a/src/xdp-app-info.c +++ b/src/xdp-app-info.c @@ -1,5 +1,6 @@ /* * Copyright © 2024 Red Hat, Inc + * Copyright © 2024 GNOME Foundation Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later * @@ -15,6 +16,9 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . + * + * Authors: + * Hubert Figuière */ #include "config.h" @@ -43,6 +47,7 @@ #include "xdp-app-info-snap-private.h" #include "xdp-app-info-host-private.h" #include "xdp-app-info-test-private.h" +#include "xdp-utils.h" #define DBUS_NAME_DBUS "org.freedesktop.DBus" #define DBUS_INTERFACE_DBUS DBUS_NAME_DBUS @@ -547,6 +552,20 @@ xdp_app_info_validate_dynamic_launcher (XdpAppInfo *app_info, error); } +const GPtrArray * +xdp_app_info_get_usb_queries (XdpAppInfo *app_info) +{ + XdpAppInfoPrivate *priv = xdp_app_info_get_instance_private (app_info); + + if (!priv->id || + !XDP_APP_INFO_GET_CLASS (app_info)->get_usb_queries) + { + return NULL; + } + + return XDP_APP_INFO_GET_CLASS (app_info)->get_usb_queries (app_info); +} + static gboolean xdp_connection_get_pid_legacy (GDBusConnection *connection, const char *sender, diff --git a/src/xdp-app-info.h b/src/xdp-app-info.h index 1b0172b19..35f44e733 100644 --- a/src/xdp-app-info.h +++ b/src/xdp-app-info.h @@ -1,5 +1,6 @@ /* * Copyright © 2024 Red Hat, Inc + * Copyright © 2024 GNOME Foundation Inc. * * SPDX-License-Identifier: LGPL-2.1-or-later * @@ -15,6 +16,9 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . + * + * Authors: + * Hubert Figuière */ #pragma once @@ -75,6 +79,8 @@ gboolean xdp_app_info_validate_dynamic_launcher (XdpAppInfo *app_info, GKeyFile *key_file, GError **error); +const GPtrArray * xdp_app_info_get_usb_queries (XdpAppInfo *app_info); + XdpAppInfo * xdp_invocation_lookup_app_info_sync (GDBusMethodInvocation *invocation, GCancellable *cancellable, GError **error); diff --git a/src/xdp-usb-query.c b/src/xdp-usb-query.c new file mode 100644 index 000000000..7483a1294 --- /dev/null +++ b/src/xdp-usb-query.c @@ -0,0 +1,213 @@ +/* + * Copyright © 2023-2024 GNOME Foundation Inc. + * + * This program 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 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, see . + * + * Authors: + * Georges Basile Stavracas Neto + * Hubert Figuière + */ + +#include +#include + +#include "xdp-usb-query.h" + +static void +xdp_usb_rule_free (XdpUsbRule *rule) +{ + g_free (rule); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpUsbRule, xdp_usb_rule_free); + +gboolean +validate_hex_uint16 (const char *value, + size_t expected_length, + uint16_t *out_value) +{ + size_t len; + char *end; + long n; + + g_assert (value != NULL); + g_assert (expected_length > 0 && expected_length <= 4); + + len = strlen (value); + if (len != expected_length) + return FALSE; + + n = strtol (value, &end, 16); + + if (end - value != len) + return FALSE; + + if (n <= 0 || n > UINT16_MAX) + return FALSE; + + if (out_value) + *out_value = n; + + return TRUE; +} + +static gboolean +parse_all_usb_rule (XdpUsbRule *dest, + GStrv data) +{ + if (g_strv_length (data) != 1) + return FALSE; + + dest->rule_type = XDP_USB_RULE_TYPE_ALL; + return TRUE; +} + +static gboolean +parse_cls_usb_rule (XdpUsbRule *dest, + GStrv data) +{ + const char *subclass; + const char *class; + + if (g_strv_length (data) < 3) + return FALSE; + + class = data[1]; + subclass = data[2]; + + if (!validate_hex_uint16 (class, 2, &dest->d.device_class.class)) + return FALSE; + + if (g_strcmp0 (subclass, "*") == 0) + dest->d.device_class.type = XDP_USB_RULE_CLASS_TYPE_CLASS_ONLY; + else if (validate_hex_uint16 (subclass, 2, &dest->d.device_class.subclass)) + dest->d.device_class.type = XDP_USB_RULE_CLASS_TYPE_CLASS_SUBCLASS; + else + return FALSE; + + dest->rule_type = XDP_USB_RULE_TYPE_CLASS; + return TRUE; +} + +static gboolean +parse_dev_usb_rule (XdpUsbRule *dest, + GStrv data) +{ + if (g_strv_length (data) != 2) + return FALSE; + + if (!validate_hex_uint16 (data[1], 4, &dest->d.product.id)) + return FALSE; + + dest->rule_type = XDP_USB_RULE_TYPE_DEVICE; + return TRUE; +} + +static gboolean +parse_vnd_usb_rule (XdpUsbRule *dest, + GStrv data) +{ + if (g_strv_length (data) != 2) + return FALSE; + + if (!validate_hex_uint16 (data[1], 4, &dest->d.product.id)) + return FALSE; + + dest->rule_type = XDP_USB_RULE_TYPE_VENDOR; + return TRUE; +} + +static const struct { + const char *name; + gboolean (*parse) (XdpUsbRule *dest, + GStrv data); +} rule_parsers[] = { + { "all", parse_all_usb_rule }, + { "cls", parse_cls_usb_rule }, + { "dev", parse_dev_usb_rule }, + { "vnd", parse_vnd_usb_rule }, +}; + +static XdpUsbRule * +xdp_usb_rule_from_string (const char *string) +{ + g_autoptr(XdpUsbRule) usb_rule = NULL; + g_auto(GStrv) split = NULL; + gboolean parsed = FALSE; + + split = g_strsplit (string, ":", 0); + + if (!split || g_strv_length (split) > 3) + return NULL; + + usb_rule = g_new0 (XdpUsbRule, 1); + + for (size_t i = 0; i < G_N_ELEMENTS (rule_parsers); i++) + { + if (g_strcmp0 (rule_parsers[i].name, split[0]) == 0) + { + if (!rule_parsers[i].parse (usb_rule, split)) + return FALSE; + + parsed = TRUE; + break; + } + } + + if (!parsed) + return NULL; + + return g_steal_pointer (&usb_rule); +} + +void +xdp_usb_query_free (XdpUsbQuery *query) +{ + g_return_if_fail (query != NULL); + + g_clear_pointer (&query->rules, g_ptr_array_unref); + g_free (query); +} + +XdpUsbQuery * +xdp_usb_query_from_string (XdpUsbQueryType query_type, + const char *string) +{ + g_autoptr(XdpUsbQuery) usb_query = NULL; + g_auto(GStrv) split = NULL; + + split = g_strsplit (string, "+", 0); + if (!split) + return NULL; + + usb_query = g_new0 (XdpUsbQuery, 1); + usb_query->query_type = query_type; + usb_query->rules = g_ptr_array_new_with_free_func ((GDestroyNotify) xdp_usb_rule_free); + + for (size_t i = 0; split[i] != NULL; i++) + { + g_autoptr(XdpUsbRule) usb_rule = NULL; + const char *rule = split[i]; + + usb_rule = xdp_usb_rule_from_string (rule); + if (!usb_rule) + return NULL; + + g_ptr_array_add (usb_query->rules, g_steal_pointer (&usb_rule)); + } + + g_return_val_if_fail (usb_query->rules->len > 0, NULL); + + return g_steal_pointer (&usb_query); +} diff --git a/src/xdp-usb-query.h b/src/xdp-usb-query.h new file mode 100644 index 000000000..250cb7724 --- /dev/null +++ b/src/xdp-usb-query.h @@ -0,0 +1,89 @@ +/* + * Copyright © 2023-2024 GNOME Foundation Inc. + * + * This program 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 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, see . + * + * Authors: + * Georges Basile Stavracas Neto + * Hubert Figuière + */ + +#include + +#pragma once + +typedef enum +{ + XDP_USB_RULE_TYPE_ALL, + XDP_USB_RULE_TYPE_CLASS, + XDP_USB_RULE_TYPE_DEVICE, + XDP_USB_RULE_TYPE_VENDOR, +} UsbRuleType; + +typedef enum +{ + XDP_USB_RULE_CLASS_TYPE_CLASS_ONLY, + XDP_USB_RULE_CLASS_TYPE_CLASS_SUBCLASS, +} UsbDeviceClassType; + +typedef struct +{ + UsbDeviceClassType type; + uint16_t class; + uint16_t subclass; +} UsbDeviceClass; + +typedef struct +{ + uint16_t id; +} UsbProduct; + +typedef struct +{ + uint16_t id; +} UsbVendor; + +typedef struct +{ + UsbRuleType rule_type; + + union { + UsbDeviceClass device_class; + UsbProduct product; + UsbVendor vendor; + } d; +} XdpUsbRule; + +typedef enum +{ + XDP_USB_QUERY_TYPE_HIDDEN, + XDP_USB_QUERY_TYPE_ENUMERABLE, +} XdpUsbQueryType; + +typedef struct +{ + XdpUsbQueryType query_type; + GPtrArray *rules; +} XdpUsbQuery; + +void xdp_usb_query_free (XdpUsbQuery *query); +XdpUsbQuery *xdp_usb_query_from_string (XdpUsbQueryType query_type, + const char *string); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpUsbQuery, xdp_usb_query_free); + +gboolean +validate_hex_uint16 (const char *value, + size_t expected_length, + uint16_t *out_value); diff --git a/src/xdp-utils.c b/src/xdp-utils.c index ba09fbeb5..bd183e09f 100644 --- a/src/xdp-utils.c +++ b/src/xdp-utils.c @@ -162,7 +162,7 @@ xdp_connection_track_name_owners (GDBusConnection *connection, gboolean xdp_filter_options (GVariant *options, GVariantBuilder *filtered, - XdpOptionKey *supported_options, + const XdpOptionKey *supported_options, int n_supported_options, GError **error) { diff --git a/src/xdp-utils.h b/src/xdp-utils.h index b0ef28200..59a18d67c 100644 --- a/src/xdp-utils.h +++ b/src/xdp-utils.h @@ -81,7 +81,7 @@ typedef struct { gboolean xdp_filter_options (GVariant *options_in, GVariantBuilder *options_out, - XdpOptionKey *supported_options, + const XdpOptionKey *supported_options, int n_supported_options, GError **error); From 7944bb5aa3d3a602f5328d80df887bd70e4207b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Fri, 13 Sep 2024 19:11:45 -0400 Subject: [PATCH 02/10] usb: If not sandboxed, assume all devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hubert Figuière --- src/xdp-app-info-host.c | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/xdp-app-info-host.c b/src/xdp-app-info-host.c index 78f688f40..9b7a1f80d 100644 --- a/src/xdp-app-info-host.c +++ b/src/xdp-app-info-host.c @@ -25,14 +25,25 @@ #endif #include "xdp-app-info-host-private.h" +#include "xdp-usb-query.h" struct _XdpAppInfoHost { XdpAppInfo parent; + + GPtrArray *usb_queries; }; G_DEFINE_FINAL_TYPE (XdpAppInfoHost, xdp_app_info_host, XDP_TYPE_APP_INFO) +static const GPtrArray * +xdp_app_info_host_get_usb_queries (XdpAppInfo *app_info) +{ + XdpAppInfoHost *app_info_host = XDP_APP_INFO_HOST (app_info); + + return app_info_host->usb_queries; +} + gboolean xdp_app_info_host_is_valid_sub_app_id (XdpAppInfo *app_info, const char *sub_app_id) @@ -58,12 +69,26 @@ xdp_app_info_host_validate_dynamic_launcher (XdpAppInfo *app_info, return TRUE; } +static void +xdp_app_info_host_dispose (GObject *object) +{ + XdpAppInfoHost *app_info = XDP_APP_INFO_HOST (object); + + g_clear_pointer (&app_info->usb_queries, g_ptr_array_unref); + + G_OBJECT_CLASS (xdp_app_info_host_parent_class)->dispose (object); +} static void xdp_app_info_host_class_init (XdpAppInfoHostClass *klass) { XdpAppInfoClass *app_info_class = XDP_APP_INFO_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = xdp_app_info_host_dispose; + + app_info_class->get_usb_queries = + xdp_app_info_host_get_usb_queries; app_info_class->validate_autostart = xdp_app_info_host_validate_autostart; app_info_class->validate_dynamic_launcher = @@ -73,6 +98,14 @@ xdp_app_info_host_class_init (XdpAppInfoHostClass *klass) static void xdp_app_info_host_init (XdpAppInfoHost *app_info_host) { + g_autoptr(XdpUsbQuery) query = NULL; + + app_info_host->usb_queries = + g_ptr_array_new_with_free_func ((GDestroyNotify) xdp_usb_query_free); + + query = xdp_usb_query_from_string (XDP_USB_QUERY_TYPE_ENUMERABLE, "all"); + if (query) + g_ptr_array_add (app_info_host->usb_queries, g_steal_pointer (&query)); } #ifdef HAVE_LIBSYSTEMD From 4a76eea6d6688a684ff818172ff6731ad0f0e669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Sun, 20 Oct 2024 21:25:21 -0400 Subject: [PATCH 03/10] usb: Allow multiple pending devices acquisitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hubert Figuière --- data/org.freedesktop.portal.Usb.xml | 11 +++++++-- src/request.c | 6 +++++ src/request.h | 1 + src/usb.c | 38 +++++++++++++++++++---------- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/data/org.freedesktop.portal.Usb.xml b/data/org.freedesktop.portal.Usb.xml index 05b09b36e..74cb3005e 100644 --- a/data/org.freedesktop.portal.Usb.xml +++ b/data/org.freedesktop.portal.Usb.xml @@ -142,6 +142,7 @@ + diff --git a/src/request.c b/src/request.c index 70353c636..4a6ffeb5e 100644 --- a/src/request.c +++ b/src/request.c @@ -254,6 +254,12 @@ request_from_invocation (GDBusMethodInvocation *invocation) return g_object_get_data (G_OBJECT (invocation), "request"); } +const char * +request_get_object_path (Request *request) +{ + return request->id; +} + void request_export (Request *request, GDBusConnection *connection) diff --git a/src/request.h b/src/request.h index 38d6cc25c..2c43fe5d6 100644 --- a/src/request.h +++ b/src/request.h @@ -73,6 +73,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (Request, g_object_unref) void request_init_invocation (GDBusMethodInvocation *invocation, XdpAppInfo *app_info); Request *request_from_invocation (GDBusMethodInvocation *invocation); +const char *request_get_object_path (Request *request); void request_export (Request *request, GDBusConnection *connection); void request_unexport (Request *request); diff --git a/src/usb.c b/src/usb.c index a83ab923c..1758c6cbf 100644 --- a/src/usb.c +++ b/src/usb.c @@ -127,7 +127,7 @@ typedef struct _UsbSenderInfo XdpAppInfo *app_info; UsbSenderState sender_state; - GPtrArray *acquiring_devices; + GHashTable *pending_devices; /* object_path → GPtrArray */ GHashTable *owned_devices; /* device id → UsbOwnedDevices */ } UsbSenderInfo; @@ -232,7 +232,7 @@ usb_sender_info_unref (UsbSenderInfo *sender_info) g_clear_object (&sender_info->app_info); g_clear_pointer (&sender_info->sender_name, g_free); g_clear_pointer (&sender_info->owned_devices, g_hash_table_destroy); - g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + g_clear_pointer (&sender_info->pending_devices, g_hash_table_destroy); g_clear_pointer (&sender_info, g_free); } } @@ -251,6 +251,9 @@ usb_sender_info_new (const char *sender_name, sender_info->owned_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) usb_owned_device_unref); + sender_info->pending_devices = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_ptr_array_unref); return g_steal_pointer (&sender_info); } @@ -959,6 +962,8 @@ usb_acquire_devices_cb (GObject *source_object, REQUEST_AUTOLOCK (request); + g_print ("got request back %s\n", request_get_object_path (request)); + response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER; sender_info = usb_sender_info_from_request (usb, request); @@ -984,8 +989,7 @@ usb_acquire_devices_cb (GObject *source_object, if (response == XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS) { - g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); - sender_info->acquiring_devices = + g_autoptr(GPtrArray) pending_devices = g_ptr_array_new_full (g_variant_iter_n_children (devices_iter), (GDestroyNotify) usb_device_acquire_data_free); while (g_variant_iter_next (devices_iter, "(&s@a{sv})", &device_id, &options)) @@ -1005,12 +1009,15 @@ usb_acquire_devices_cb (GObject *source_object, access_data->device_id = g_strdup (device_id); access_data->writable = writable; - g_ptr_array_add (sender_info->acquiring_devices, g_steal_pointer (&access_data)); + g_ptr_array_add (pending_devices, g_steal_pointer (&access_data)); usb_sender_info_set_device_permission (sender_info, device, PERMISSION_YES); g_clear_pointer (&options, g_variant_unref); } + g_hash_table_insert (sender_info->pending_devices, + g_strdup (request_get_object_path (request)), + g_steal_pointer (&pending_devices)); } else if (response == XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED) { @@ -1252,6 +1259,7 @@ handle_acquire_devices (XdpDbusUsb *object, static gboolean handle_finish_acquire_devices (XdpDbusUsb *object, GDBusMethodInvocation *invocation, + const char *object_path, GVariant *arg_options) { g_autoptr(UsbSenderInfo) sender_info = NULL; @@ -1260,9 +1268,12 @@ handle_finish_acquire_devices (XdpDbusUsb *object, Permission permission; uint32_t accessed_devices; gboolean finished; + GPtrArray *pending_devices = NULL; Call *call; XdpUsb *self; + g_print ("object path: %s\n", object_path); + self = XDP_USB (object); call = call_from_invocation (invocation); @@ -1277,7 +1288,7 @@ handle_finish_acquire_devices (XdpDbusUsb *object, { /* If permission was revoked in between D-Bus calls, reset state */ sender_info->sender_state = USB_SENDER_STATE_DEFAULT; - g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + g_hash_table_remove (sender_info->pending_devices, object_path); g_dbus_method_invocation_return_error (invocation, XDG_DESKTOP_PORTAL_ERROR, @@ -1286,13 +1297,14 @@ handle_finish_acquire_devices (XdpDbusUsb *object, return G_DBUS_METHOD_INVOCATION_HANDLED; } - if (sender_info->sender_state != USB_SENDER_STATE_ACQUIRING_DEVICES || sender_info->acquiring_devices == NULL) + pending_devices = g_hash_table_lookup (sender_info->pending_devices, object_path); + if (sender_info->sender_state != USB_SENDER_STATE_ACQUIRING_DEVICES || pending_devices == NULL) { /* If the request was cancelled in some way. This would happen by calling * FinishAcquireDevices after the user denied the permission. */ sender_info->sender_state = USB_SENDER_STATE_DEFAULT; - g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + g_hash_table_remove (sender_info->pending_devices, object_path); g_dbus_method_invocation_return_error (invocation, XDG_DESKTOP_PORTAL_ERROR, @@ -1303,7 +1315,7 @@ handle_finish_acquire_devices (XdpDbusUsb *object, /* We should never trigger these asserts. */ g_assert (sender_info->sender_state == USB_SENDER_STATE_ACQUIRING_DEVICES); - g_assert (sender_info->acquiring_devices != NULL); + g_assert (pending_devices != NULL); fds = g_unix_fd_list_new (); @@ -1311,7 +1323,7 @@ handle_finish_acquire_devices (XdpDbusUsb *object, accessed_devices = 0; while (accessed_devices < MAX_DEVICES && - sender_info->acquiring_devices->len > 0) + pending_devices->len > 0) { g_autoptr(UsbDeviceAcquireData) access_data = NULL; g_autoptr(GError) error = NULL; @@ -1322,7 +1334,7 @@ handle_finish_acquire_devices (XdpDbusUsb *object, g_variant_dict_init (&dict, NULL); - access_data = g_ptr_array_steal_index (sender_info->acquiring_devices, 0); + access_data = g_ptr_array_steal_index (pending_devices, 0); /* Check we haven't already acquired the device */ owned_device = g_hash_table_lookup (sender_info->owned_devices, access_data->device_id); @@ -1408,12 +1420,12 @@ handle_finish_acquire_devices (XdpDbusUsb *object, accessed_devices++; } - finished = sender_info->acquiring_devices->len == 0; + finished = pending_devices->len == 0; if (finished) { sender_info->sender_state = USB_SENDER_STATE_DEFAULT; - g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + g_hash_table_remove (sender_info->pending_devices, object_path); } g_dbus_method_invocation_return_value_with_unix_fd_list (invocation, From 407cdb65199947e37a82437b6d923e7ed5d48a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Sun, 20 Oct 2024 22:36:34 -0400 Subject: [PATCH 04/10] usb: Remove the sender_state for device acquisition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hubert Figuière --- src/usb.c | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/src/usb.c b/src/usb.c index 1758c6cbf..ab2897807 100644 --- a/src/usb.c +++ b/src/usb.c @@ -113,12 +113,6 @@ typedef struct _UsbOwnedDevice int fd; } UsbOwnedDevice; -typedef enum -{ - USB_SENDER_STATE_DEFAULT, - USB_SENDER_STATE_ACQUIRING_DEVICES, -} UsbSenderState; - typedef struct _UsbSenderInfo { gatomicrefcount ref_count; @@ -126,7 +120,6 @@ typedef struct _UsbSenderInfo char *sender_name; XdpAppInfo *app_info; - UsbSenderState sender_state; GHashTable *pending_devices; /* object_path → GPtrArray */ GHashTable *owned_devices; /* device id → UsbOwnedDevices */ @@ -247,7 +240,6 @@ usb_sender_info_new (const char *sender_name, g_atomic_ref_count_init (&sender_info->ref_count); sender_info->sender_name = g_strdup (sender_name); sender_info->app_info = g_object_ref (app_info); - sender_info->sender_state = USB_SENDER_STATE_DEFAULT; sender_info->owned_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) usb_owned_device_unref); @@ -968,7 +960,6 @@ usb_acquire_devices_cb (GObject *source_object, sender_info = usb_sender_info_from_request (usb, request); g_assert (sender_info != NULL); - g_assert (sender_info->sender_state == USB_SENDER_STATE_ACQUIRING_DEVICES); g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); @@ -1021,7 +1012,8 @@ usb_acquire_devices_cb (GObject *source_object, } else if (response == XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED) { - sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + g_hash_table_remove (sender_info->pending_devices, + request_get_object_path (request)); } out: @@ -1219,16 +1211,6 @@ handle_acquire_devices (XdpDbusUsb *object, sender_info = usb_sender_info_from_request (self, request); g_assert (sender_info != NULL); - if (sender_info->sender_state != USB_SENDER_STATE_DEFAULT) - { - g_dbus_method_invocation_return_error (invocation, - XDG_DESKTOP_PORTAL_ERROR, - XDG_DESKTOP_PORTAL_ERROR_FAILED, - "Cannot call AcquireDevices() with an unfinished " - "call to AcquireDevices()"); - return G_DBUS_METHOD_INVOCATION_HANDLED; - } - /* Validate devices */ if (!filter_access_devices (self, sender_info, arg_devices, &filtered_devices, &error)) { @@ -1239,8 +1221,6 @@ handle_acquire_devices (XdpDbusUsb *object, request_set_impl_request (request, impl_request); request_export (request, g_dbus_method_invocation_get_connection (invocation)); - sender_info->sender_state = USB_SENDER_STATE_ACQUIRING_DEVICES; - xdp_dbus_impl_usb_call_acquire_devices (usb_impl, request->id, arg_parent_window, @@ -1287,7 +1267,6 @@ handle_finish_acquire_devices (XdpDbusUsb *object, if (permission == PERMISSION_NO) { /* If permission was revoked in between D-Bus calls, reset state */ - sender_info->sender_state = USB_SENDER_STATE_DEFAULT; g_hash_table_remove (sender_info->pending_devices, object_path); g_dbus_method_invocation_return_error (invocation, @@ -1298,12 +1277,11 @@ handle_finish_acquire_devices (XdpDbusUsb *object, } pending_devices = g_hash_table_lookup (sender_info->pending_devices, object_path); - if (sender_info->sender_state != USB_SENDER_STATE_ACQUIRING_DEVICES || pending_devices == NULL) + if (pending_devices == NULL) { /* If the request was cancelled in some way. This would happen by calling * FinishAcquireDevices after the user denied the permission. */ - sender_info->sender_state = USB_SENDER_STATE_DEFAULT; g_hash_table_remove (sender_info->pending_devices, object_path); g_dbus_method_invocation_return_error (invocation, @@ -1314,7 +1292,6 @@ handle_finish_acquire_devices (XdpDbusUsb *object, } /* We should never trigger these asserts. */ - g_assert (sender_info->sender_state == USB_SENDER_STATE_ACQUIRING_DEVICES); g_assert (pending_devices != NULL); fds = g_unix_fd_list_new (); @@ -1424,7 +1401,6 @@ handle_finish_acquire_devices (XdpDbusUsb *object, if (finished) { - sender_info->sender_state = USB_SENDER_STATE_DEFAULT; g_hash_table_remove (sender_info->pending_devices, object_path); } From 22545f248fb14de5d222216d2ee45cc295575651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Fri, 25 Oct 2024 23:09:01 -0400 Subject: [PATCH 05/10] usb: Fix device insertion / removal handled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't assert on is_udev_device_suitable() Signed-off-by: Hubert Figuière --- src/usb.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/usb.c b/src/usb.c index ab2897807..a00749a7b 100644 --- a/src/usb.c +++ b/src/usb.c @@ -155,10 +155,6 @@ is_gudev_device_suitable (GUdevDevice *device) if (!devtype || g_strcmp0 (devtype, "usb_device") != 0) return FALSE; - /* We check access to the device */ - if (access (device_file, R_OK) == -1) - return FALSE; - return TRUE; } @@ -535,7 +531,8 @@ gudev_device_to_variant (XdpUsb *self, NULL, }; - g_assert (is_gudev_device_suitable (device)); + if (!is_gudev_device_suitable (device)) + return NULL; parent = g_udev_device_get_parent (device); if (parent != NULL && usb_sender_info_match_device (sender_info, parent)) @@ -758,6 +755,10 @@ send_initial_device_list (XdpUsb *self, continue; device_variant = gudev_device_to_variant (self, sender_info, device); + /* NULL mean the device isn't suitable */ + if (device_variant == NULL) + continue; + g_variant_builder_add (&devices_builder, "(ss@a{sv})", "add", id, device_variant); g_hash_table_add (usb_session->available_devices, g_strdup (id)); @@ -879,6 +880,9 @@ list_permitted_devices (XdpUsb *self, if (usb_sender_info_match_device (sender_info, device)) { g_autoptr(GVariant) device_variant = gudev_device_to_variant (self, sender_info, device); + if (device_variant == NULL) + continue; + g_variant_builder_add (&builder, "(s@a{sv})", id, device_variant); } } From 352aa67349803540cd84958506c51a691d530291 Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Thu, 17 Oct 2024 13:34:56 +0200 Subject: [PATCH 06/10] app-info/test: Allow passing USB queries via env We want to test the USB portal which requires USB queries to determine which USB devices should be enumerable and could potentially be acquired. This adds an environment variable similar to the one for the app id that the test harness can set. --- src/xdp-app-info-test-private.h | 3 +- src/xdp-app-info-test.c | 61 ++++++++++++++++++++++++++++++++- src/xdp-app-info.c | 7 +++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/xdp-app-info-test-private.h b/src/xdp-app-info-test-private.h index 1cc11e008..d323323ec 100644 --- a/src/xdp-app-info-test-private.h +++ b/src/xdp-app-info-test-private.h @@ -32,4 +32,5 @@ G_DECLARE_FINAL_TYPE (XdpAppInfoTest, XDP, APP_INFO_TEST, XdpAppInfo) -XdpAppInfo * xdp_app_info_test_new (const char *app_id); +XdpAppInfo * xdp_app_info_test_new (const char *app_id, + const char *usb_queries_str); diff --git a/src/xdp-app-info-test.c b/src/xdp-app-info-test.c index 1b27e3b52..f7e1242e1 100644 --- a/src/xdp-app-info-test.c +++ b/src/xdp-app-info-test.c @@ -21,9 +21,13 @@ #include "xdp-app-info-test-private.h" +#include "xdp-usb-query.h" + struct _XdpAppInfoTest { XdpAppInfo parent; + + GPtrArray *usb_queries; }; G_DEFINE_FINAL_TYPE (XdpAppInfoTest, xdp_app_info_test, XDP_TYPE_APP_INFO) @@ -46,15 +50,38 @@ xdp_app_info_test_validate_dynamic_launcher (XdpAppInfo *app_info, return TRUE; } +static const GPtrArray * +xdp_app_info_test_get_usb_queries (XdpAppInfo *app_info) +{ + XdpAppInfoTest *app_info_test = XDP_APP_INFO_TEST (app_info); + + return app_info_test->usb_queries; +} + +static void +xdp_app_info_test_dispose (GObject *object) +{ + XdpAppInfoTest *app_info = XDP_APP_INFO_TEST (object); + + g_clear_pointer (&app_info->usb_queries, g_ptr_array_unref); + + G_OBJECT_CLASS (xdp_app_info_test_parent_class)->dispose (object); +} + static void xdp_app_info_test_class_init (XdpAppInfoTestClass *klass) { XdpAppInfoClass *app_info_class = XDP_APP_INFO_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = xdp_app_info_test_dispose; app_info_class->validate_autostart = xdp_app_info_test_validate_autostart; app_info_class->validate_dynamic_launcher = xdp_app_info_test_validate_dynamic_launcher; + app_info_class->get_usb_queries = + xdp_app_info_test_get_usb_queries; } static void @@ -62,8 +89,38 @@ xdp_app_info_test_init (XdpAppInfoTest *app_info_test) { } +static GPtrArray * +parse_usb_queries_string (const char *usb_queries_str) +{ + g_autoptr(GPtrArray) usb_queries = NULL; + g_auto(GStrv) queries_strs = NULL; + + if (!usb_queries_str) + return NULL; + + usb_queries = + g_ptr_array_new_with_free_func ((GDestroyNotify) xdp_usb_query_free); + + queries_strs = g_strsplit (usb_queries_str, ";", 0); + for (size_t i = 0; queries_strs[i] != NULL; i++) + { + g_autoptr(XdpUsbQuery) query = + xdp_usb_query_from_string (XDP_USB_QUERY_TYPE_ENUMERABLE, + queries_strs[i]); + + if (query) + g_ptr_array_add (usb_queries, g_steal_pointer (&query)); + } + + if (usb_queries->len == 0) + return NULL; + + return g_steal_pointer (&usb_queries); +} + XdpAppInfo * -xdp_app_info_test_new (const char *app_id) +xdp_app_info_test_new (const char *app_id, + const char *usb_queries_str) { g_autoptr (XdpAppInfoTest) app_info_test = NULL; @@ -73,5 +130,7 @@ xdp_app_info_test_new (const char *app_id) -1, NULL, TRUE, TRUE, TRUE); + app_info_test->usb_queries = parse_usb_queries_string (usb_queries_str); + return XDP_APP_INFO (g_steal_pointer (&app_info_test)); } diff --git a/src/xdp-app-info.c b/src/xdp-app-info.c index c755811f9..76b803b8c 100644 --- a/src/xdp-app-info.c +++ b/src/xdp-app-info.c @@ -751,6 +751,7 @@ xdp_connection_lookup_app_info_sync (GDBusConnection *connection, g_autofd int pidfd = -1; uint32_t pid; const char *test_override_app_id; + const char *test_override_usb_queries; g_autoptr(GError) local_error = NULL; app_info = cache_lookup_app_info_by_sender (sender); @@ -761,8 +762,12 @@ xdp_connection_lookup_app_info_sync (GDBusConnection *connection, return NULL; test_override_app_id = g_getenv ("XDG_DESKTOP_PORTAL_TEST_APP_ID"); + test_override_usb_queries = g_getenv ("XDG_DESKTOP_PORTAL_TEST_USB_QUERIES"); if (test_override_app_id) - app_info = xdp_app_info_test_new (test_override_app_id); + { + app_info = xdp_app_info_test_new (test_override_app_id, + test_override_usb_queries); + } if (app_info == NULL) app_info = xdp_app_info_flatpak_new (pid, pidfd, &local_error); From 9d93a9cb6ad132b78fc3d9c263c7f5211c71d6ff Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Thu, 17 Oct 2024 14:31:29 +0200 Subject: [PATCH 07/10] tests/py: Allow setting usb_queries for AppInfoTest via fixture This lets us control which USB devices are enumerable by setting the fixture to valid xdp USB query. --- tests/__init__.py | 5 +++++ tests/conftest.py | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index cb306a886..fc8d0c047 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -338,6 +338,7 @@ def __init__( dbus_test_case, portal_name: str, app_id: str = "org.example.App", + usb_queries: Optional[str] = None, umockdev=None, ): self.dbus_test_case = dbus_test_case @@ -347,6 +348,7 @@ def __init__( self.dbus_monitor = None self.portal_interfaces: Dict[str, dbus.Interface] = {} self.app_id = app_id + self.usb_queries = usb_queries self.busses = {dbusmock.BusType.SYSTEM: {}, dbusmock.BusType.SESSION: {}} self.umockdev = umockdev @@ -458,6 +460,9 @@ def start_xdp(self): env["XDG_CURRENT_DESKTOP"] = "test" env["XDG_DESKTOP_PORTAL_TEST_APP_ID"] = self.app_id + if self.usb_queries: + env["XDG_DESKTOP_PORTAL_TEST_USB_QUERIES"] = self.usb_queries + if self.umockdev: env["UMOCKDEV_DIR"] = self.umockdev.get_root_dir() diff --git a/tests/conftest.py b/tests/conftest.py index 76af3ce70..283f44426 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -133,6 +133,15 @@ def app_id(): return "org.example.App" +@pytest.fixture +def usb_queries(): + """ + Default fixture providing the usb queries the connecting process can + enumerate + """ + return None + + @pytest.fixture def umockdev(): """ @@ -143,12 +152,18 @@ def umockdev(): @pytest.fixture def portal_mock( - dbus_test_case, portal_name, required_templates, template_params, app_id, umockdev + dbus_test_case, + portal_name, + required_templates, + template_params, + app_id, + usb_queries, + umockdev, ) -> PortalMock: """ Fixture yielding a PortalMock object with the impl started, if applicable. """ - pmock = PortalMock(dbus_test_case, portal_name, app_id, umockdev) + pmock = PortalMock(dbus_test_case, portal_name, app_id, usb_queries, umockdev) for template, params in required_templates.items(): params = template_params.get(template, params) From f315921b632712beab1a39d318d7a25f3e94fb4b Mon Sep 17 00:00:00 2001 From: Sebastian Wick Date: Thu, 17 Oct 2024 14:37:39 +0200 Subject: [PATCH 08/10] tests/usb: Test the USB portal via pytest, dbusmock and umockdev --- .github/workflows/Containerfile | 6 +- .github/workflows/container.yml | 2 +- meson.build | 1 + tests/meson.build | 7 +- tests/portals/meson.build | 1 + tests/templates/meson.build | 1 + tests/templates/usb.py | 129 +++++++++++ tests/test_usb.py | 380 ++++++++++++++++++++++++++++++++ 8 files changed, 524 insertions(+), 3 deletions(-) create mode 100644 tests/templates/usb.py create mode 100644 tests/test_usb.py diff --git a/.github/workflows/Containerfile b/.github/workflows/Containerfile index 7ca313c53..699f48b42 100644 --- a/.github/workflows/Containerfile +++ b/.github/workflows/Containerfile @@ -47,7 +47,11 @@ RUN apt install -y --no-install-recommends \ python3-pytest \ python3-pytest-xdist \ python3-dbusmock \ - python3-dbus + python3-dbus \ + libumockdev0 \ + libumockdev-dev \ + umockdev \ + gir1.2-umockdev-1.0 # Install pip RUN apt install -y --no-install-recommends python3-pip diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index b1f66e95e..206801c1f 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -1,5 +1,5 @@ env: - IMAGE_TAG: 20241024-1 + IMAGE_TAG: 20241024-3 on: workflow_call: diff --git a/meson.build b/meson.build index 75a264dd7..adac2c7a5 100644 --- a/meson.build +++ b/meson.build @@ -133,6 +133,7 @@ libportal_dep = dependency( pipewire_dep = dependency('libpipewire-0.3', version: '>= 0.2.90') libsystemd_dep = dependency('libsystemd', required: get_option('systemd')) gudev_dep = dependency('gudev-1.0', required: get_option('gudev')) +umockdev_dep = dependency('umockdev-1.0') bwrap = find_program('bwrap', required: get_option('sandboxed-image-validation').allowed() or get_option('sandboxed-sound-validation').allowed()) diff --git a/tests/meson.build b/tests/meson.build index ed214a124..8c6888a5e 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -277,7 +277,11 @@ python = pymod.find_installation( required: get_option('pytest'), ) -enable_pytest = pytest.found() and python.found() and python.language_version().version_compare('>=3.9') +enable_pytest = \ + pytest.found() and \ + python.found() and \ + python.language_version().version_compare('>=3.9') and \ + umockdev_dep.found() if enable_pytest subdir('templates') @@ -304,6 +308,7 @@ if enable_pytest 'test_location.py', 'test_remotedesktop.py', 'test_trash.py', + 'test_usb.py', ] foreach pytest_file : pytest_files configure_file( diff --git a/tests/portals/meson.build b/tests/portals/meson.build index 96b932680..7c31aa8e3 100644 --- a/tests/portals/meson.build +++ b/tests/portals/meson.build @@ -15,6 +15,7 @@ test_portals = [ 'org.freedesktop.impl.portal.RemoteDesktop', 'org.freedesktop.impl.portal.Screenshot', 'org.freedesktop.impl.portal.Settings', + 'org.freedesktop.impl.portal.Usb', 'org.freedesktop.impl.portal.Wallpaper', ] diff --git a/tests/templates/meson.build b/tests/templates/meson.build index 315832011..24e74dba7 100644 --- a/tests/templates/meson.build +++ b/tests/templates/meson.build @@ -6,6 +6,7 @@ template_files = [ 'globalshortcuts.py', 'inputcapture.py', 'remotedesktop.py', + 'usb.py', ] foreach template_file : template_files configure_file( diff --git a/tests/templates/usb.py b/tests/templates/usb.py new file mode 100644 index 000000000..3b8cb889b --- /dev/null +++ b/tests/templates/usb.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests.templates import Response, init_template_logger, ImplRequest +import dbus +import dbus.service +from dbusmock import MOCK_IFACE + +from gi.repository import GLib + + +BUS_NAME = "org.freedesktop.impl.portal.Test" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.impl.portal.Usb" +VERSION = 1 + + +logger = init_template_logger(__name__) + + +def load(mock, parameters={}): + logger.debug(f"Loading parameters: {parameters}") + + mock.delay: int = parameters.get("delay", 200) + mock.response: int = parameters.get("response", 0) + mock.filters = parameters.get("filters", {}) + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary( + { + "version": dbus.UInt32(parameters.get("version", VERSION)), + } + ), + ) + + +@dbus.service.method( + MAIN_IFACE, + in_signature="ossa(sa{sv}a{sv})a{sv}", + out_signature="ua{sv}", + async_callbacks=("cb_success", "cb_error"), +) +def AcquireDevices( + self, + handle, + parent_window, + app_id, + devices, + options, + cb_success, + cb_error, +): + try: + logger.debug( + f"AcquireDevices({handle}, {parent_window}, {app_id}, {devices}, {options})" + ) + + # no options supported + assert not options + devices_out = [] + + for device in devices: + (id, info, access_options) = device + props = info["properties"] + + allows_writable = self.filters.get("writable", True) + needs_writable = access_options.get("writable", False) + if needs_writable and not allows_writable: + logger.debug(f"Skipping device {id} because it requires writable") + continue + + needs_vendor = self.filters.get("vendor", None) + needs_vendor = int(needs_vendor, 16) if needs_vendor else None + + vendor = props.get("ID_VENDOR_ID", None) + vendor = int(vendor, 16) if vendor else None + + if needs_vendor is not None and needs_vendor != vendor: + logger.debug( + f"Skipping device {id} because it does not belong to vendor {needs_vendor:02x}" + ) + continue + + needs_model = self.filters.get("model", None) + needs_model = int(needs_model, 16) if needs_model else None + + model = props.get("ID_MODEL_ID", None) + model = int(model, 16) if model else None + + if needs_model is not None and needs_model != model: + logger.debug( + f"Skipping device {id} because it is not a model {needs_model:02x}" + ) + continue + + devices_out.append( + dbus.Struct([id, access_options], signature="sa{sv}", variant_level=1) + ) + + response = Response( + self.response, + {"devices": dbus.Array(devices_out, signature="(sa{sv})", variant_level=1)}, + ) + request = ImplRequest(self, BUS_NAME, handle) + request.export() + + def reply(): + logger.debug(f"AcquireDevices with response {response}") + cb_success(response.response, response.results) + + logger.debug(f"scheduling delay of {self.delay}") + GLib.timeout_add(self.delay, reply) + + except Exception as e: + logger.critical(e) + cb_error(e) + + +@dbus.service.method( + MOCK_IFACE, + in_signature="a{sv}", + out_signature="", +) +def SetSelectionFilters(self, filters): + logger.debug(f"SetSelectionFilters({filters})") + + self.filters = filters diff --git a/tests/test_usb.py b/tests/test_usb.py new file mode 100644 index 000000000..1619cc9be --- /dev/null +++ b/tests/test_usb.py @@ -0,0 +1,380 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is formatted with Python Black + +from tests import Session +from gi.repository import GLib + +import pytest +import os +import gi + +gi.require_version("UMockdev", "1.0") +from gi.repository import UMockdev # noqa E402 + + +@pytest.fixture +def portal_name(): + return "Usb" + + +@pytest.fixture +def umockdev(): + return UMockdev.Testbed.new() + + +class TestUsb: + _num_devices = 0 + + def generate_device( + self, testbed, vendor, vendor_name, product, product_name, serial + ): + n = self._num_devices + self._num_devices += 1 + + testbed.add_from_string(f"""P: /devices/usb{n} +N: bus/usb/001/{n:03d} +E: BUSNUM=001 +E: DEVNUM={n:03d} +E: DEVNAME=/dev/bus/usb/001/{n:03d} +E: DEVTYPE=usb_device +E: DRIVER=usb +E: ID_BUS=usb +E: ID_MODEL={product_name} +E: ID_MODEL_ID={product} +E: ID_REVISION=0002 +E: ID_SERIAL={vendor_name}_{product_name}_{serial} +E: ID_SERIAL_SHORT={serial} +E: ID_VENDOR={vendor_name} +E: ID_VENDOR_ID={vendor} +E: SUBSYSTEM=usb +A: idProduct={product} +A: idVendor={vendor} +""") + + return f"/sys/devices/usb{n}" + + def test_version(self, portal_mock): + portal_mock.check_version(1) + + def test_create_close_session(self, portal_mock, app_id): + usb_intf = portal_mock.get_dbus_interface() + + session = Session( + portal_mock.dbus_con, + usb_intf.CreateSession({"session_handle_token": "session_token0"}), + ) + + session.close() + + def test_empty_initial_devices(self, portal_mock, app_id): + device_events_signal_received = False + + usb_intf = portal_mock.get_dbus_interface() + + Session( + portal_mock.dbus_con, + usb_intf.CreateSession({"session_handle_token": "session_token0"}), + ) + + def cb_device_events(session_handle, events): + nonlocal device_events_signal_received + device_events_signal_received = True + + usb_intf.connect_to_signal("DeviceEvents", cb_device_events) + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + assert not device_events_signal_received + + @pytest.mark.parametrize("usb_queries", ["vnd:04a9", None]) + def test_initial_devices(self, portal_mock, app_id, usb_queries): + device_events_signal_received = False + devices_received = 0 + + self.generate_device( + portal_mock.umockdev, + "04a9", + "Canon_Inc.", + "31c0", + "Canon_Digital_Camera", + "C767F1C714174C309255F70E4A7B2EE2", + ) + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + usb_intf = portal_mock.get_dbus_interface() + + session = Session( + portal_mock.dbus_con, + usb_intf.CreateSession({"session_handle_token": "session_token0"}), + ) + + def cb_device_events(session_handle, events): + nonlocal device_events_signal_received + nonlocal devices_received + assert session.handle == session_handle + + for action, id, device in events: + assert action == "add" + devices_received += 1 + + device_events_signal_received = True + + usb_intf.connect_to_signal("DeviceEvents", cb_device_events) + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + if usb_queries is None: + assert not device_events_signal_received + assert devices_received == 0 + else: + assert device_events_signal_received + assert devices_received == 1 + + @pytest.mark.parametrize("usb_queries", ["vnd:04a9", None]) + def test_device_add(self, portal_mock, app_id, usb_queries): + device_events_signal_received = False + devices_received = 0 + device = None + + usb_intf = portal_mock.get_dbus_interface() + + session = Session( + portal_mock.dbus_con, + usb_intf.CreateSession({"session_handle_token": "session_token0"}), + ) + + def cb_device_events(session_handle, events): + nonlocal device_events_signal_received + nonlocal devices_received + nonlocal device + assert session.handle == session_handle + + for action, _, dev in events: + assert action == "add" + device = dev + devices_received += 1 + + device_events_signal_received = True + + usb_intf.connect_to_signal("DeviceEvents", cb_device_events) + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + assert not device_events_signal_received + + self.generate_device( + portal_mock.umockdev, + "04a9", + "Canon_Inc.", + "31c0", + "Canon_Digital_Camera", + "C767F1C714174C309255F70E4A7B2EE2", + ) + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + if usb_queries is None: + assert not device_events_signal_received + assert devices_received == 0 + else: + assert device_events_signal_received + assert devices_received == 1 + + assert device + assert device["readable"] + assert device["writable"] + assert device["device-file"] == "/dev/bus/usb/001/000" + assert device["properties"]["ID_VENDOR_ID"] == "04a9" + assert device["properties"]["ID_MODEL_ID"] == "31c0" + assert ( + device["properties"]["ID_SERIAL"] + == "Canon_Inc._Canon_Digital_Camera_C767F1C714174C309255F70E4A7B2EE2" + ) + + @pytest.mark.parametrize("usb_queries", ["vnd:04a9", None]) + def test_device_remove(self, portal_mock, app_id, usb_queries): + device_events_signal_count = 0 + devices_received = 0 + devices_removed = 0 + + dev_path = self.generate_device( + portal_mock.umockdev, + "04a9", + "Canon_Inc.", + "31c0", + "Canon_Digital_Camera", + "C767F1C714174C309255F70E4A7B2EE2", + ) + + usb_intf = portal_mock.get_dbus_interface() + + session = Session( + portal_mock.dbus_con, + usb_intf.CreateSession({"session_handle_token": "session_token0"}), + ) + + def cb_device_events(session_handle, events): + nonlocal device_events_signal_count + nonlocal devices_received + nonlocal devices_removed + + assert session.handle == session_handle + + for action, id, device in events: + if action == "add": + devices_received += 1 + elif action == "remove": + devices_removed += 1 + else: + assert False + + device_events_signal_count += 1 + + usb_intf.connect_to_signal("DeviceEvents", cb_device_events) + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + if usb_queries is None: + assert device_events_signal_count == 0 + assert devices_received == 0 + assert devices_removed == 0 + else: + assert device_events_signal_count == 1 + assert devices_received == 1 + assert devices_removed == 0 + + portal_mock.umockdev.remove_device(dev_path) + + mainloop = GLib.MainLoop() + GLib.timeout_add(300, mainloop.quit) + mainloop.run() + + if usb_queries is None: + assert device_events_signal_count == 0 + assert devices_received == 0 + assert devices_removed == 0 + else: + assert device_events_signal_count == 2 + assert devices_received == 1 + assert devices_removed == 1 + + @pytest.mark.parametrize("usb_queries", ["vnd:04a9;vnd:04aa"]) + @pytest.mark.parametrize("params", [{"filters": {"vendor": "04a9"}}]) + def test_acquire(self, portal_mock, app_id): + self.generate_device( + portal_mock.umockdev, + "04a9", + "Canon_Inc.", + "31c0", + "Canon_Digital_Camera", + "C767F1C714174C309255F70E4A7B2EE2", + ) + + self.generate_device( + portal_mock.umockdev, + "04aa", + "Someone Else.", + "31c0", + "SomeProduct", + "00001", + ) + + possible_vendors = ["04a9", "04aa"] + + usb_intf = portal_mock.get_dbus_interface() + devices = usb_intf.EnumerateDevices({}) + assert len(devices) == 2 + (id1, dev_info1) = devices[0] + assert id1 + assert dev_info1 + vendor_id = dev_info1["properties"]["ID_VENDOR_ID"] + assert vendor_id in possible_vendors + possible_vendors.remove(vendor_id) + (id2, dev_info2) = devices[1] + assert id2 + assert dev_info2 + vendor_id = dev_info2["properties"]["ID_VENDOR_ID"] + assert vendor_id in possible_vendors + possible_vendors.remove(vendor_id) + + request = portal_mock.create_request() + response = request.call( + "AcquireDevices", + parent_window="", + devices=[ + (id1, {"writable": True}), + (id2, {"writable": True}), + ], + options={}, + ) + assert response.response == 0 + + (results, finished) = usb_intf.FinishAcquireDevices(request.handle, {}) + assert finished + assert len(results) == 1 + (res_id, device) = results[0] + assert res_id == id1 or res_id == id2 + assert device["success"] + fd = device["fd"].take() + assert fd > 0 + with os.fdopen(fd, "r") as f: + assert f + assert "error" not in device + + usb_intf.ReleaseDevices([res_id], {}) + + @pytest.mark.parametrize("usb_queries", ["vnd:0001"]) + @pytest.mark.parametrize( + "expected,params", + [ + (1, {"filters": {"model": "0000"}}), + (1, {"filters": {"model": "0001"}}), + (0, {"filters": {"model": "0002"}}), + (2, {"filters": {"vendor": "0001"}}), + (0, {"filters": {"vendor": "0002"}}), + (1, {"filters": {"vendor": "0001", "model": "0000"}}), + (0, {"filters": {"vendor": "0002", "model": "0000"}}), + ], + ) + def test_queries(self, expected, portal_mock, app_id, usb_queries): + for i in range(2): + self.generate_device( + portal_mock.umockdev, + "0001", + "example_org", + f"000{i}", + f"model{i}", + "0001", + ) + + usb_intf = portal_mock.get_dbus_interface() + devices = usb_intf.EnumerateDevices({}) + assert len(devices) == 2 + acquire_devices = [(id, {"writable": True}) for (id, _) in devices] + + request = portal_mock.create_request() + response = request.call( + "AcquireDevices", + parent_window="", + devices=acquire_devices, + options={}, + ) + assert response.response == 0 + + (results, finished) = usb_intf.FinishAcquireDevices(request.handle, {}) + assert finished + assert len(results) == expected From 3651c74ae952e2b439a016faabe9d60cafe305b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Tue, 29 Oct 2024 22:46:52 -0400 Subject: [PATCH 09/10] test/usb: Increase timemout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hubert Figuière --- tests/test_usb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index 1619cc9be..b65a2cfee 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -245,7 +245,7 @@ def cb_device_events(session_handle, events): usb_intf.connect_to_signal("DeviceEvents", cb_device_events) mainloop = GLib.MainLoop() - GLib.timeout_add(300, mainloop.quit) + GLib.timeout_add(1000, mainloop.quit) mainloop.run() if usb_queries is None: @@ -260,7 +260,7 @@ def cb_device_events(session_handle, events): portal_mock.umockdev.remove_device(dev_path) mainloop = GLib.MainLoop() - GLib.timeout_add(300, mainloop.quit) + GLib.timeout_add(1000, mainloop.quit) mainloop.run() if usb_queries is None: From a48dc30e80de725f6391959cfca308060bce2e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Thu, 31 Oct 2024 21:43:21 -0400 Subject: [PATCH 10/10] more test debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hubert Figuière --- tests/test_usb.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/test_usb.py b/tests/test_usb.py index b65a2cfee..6bfa67deb 100644 --- a/tests/test_usb.py +++ b/tests/test_usb.py @@ -203,12 +203,21 @@ def cb_device_events(session_handle, events): == "Canon_Inc._Canon_Digital_Camera_C767F1C714174C309255F70E4A7B2EE2" ) - @pytest.mark.parametrize("usb_queries", ["vnd:04a9", None]) + @pytest.mark.parametrize("usb_queries", ["vnd:04a9;vnd:04aa", None]) def test_device_remove(self, portal_mock, app_id, usb_queries): device_events_signal_count = 0 devices_received = 0 devices_removed = 0 + dev_path = self.generate_device( + portal_mock.umockdev, + "04aa", + "Someone Else.", + "31c0", + "SomeProduct", + "00001", + ) + dev_path = self.generate_device( portal_mock.umockdev, "04a9", @@ -229,18 +238,20 @@ def cb_device_events(session_handle, events): nonlocal device_events_signal_count nonlocal devices_received nonlocal devices_removed - assert session.handle == session_handle + print("events ", events) for action, id, device in events: + print("device event ", action) if action == "add": devices_received += 1 elif action == "remove": devices_removed += 1 else: assert False - device_events_signal_count += 1 + print("count ", device_events_signal_count) + print("count recv", devices_received) usb_intf.connect_to_signal("DeviceEvents", cb_device_events) @@ -254,22 +265,26 @@ def cb_device_events(session_handle, events): assert devices_removed == 0 else: assert device_events_signal_count == 1 - assert devices_received == 1 + assert devices_received == 2 assert devices_removed == 0 + print("Removong device ", dev_path) portal_mock.umockdev.remove_device(dev_path) + print("Removed") mainloop = GLib.MainLoop() GLib.timeout_add(1000, mainloop.quit) mainloop.run() + print("End of loop") + if usb_queries is None: assert device_events_signal_count == 0 assert devices_received == 0 assert devices_removed == 0 else: assert device_events_signal_count == 2 - assert devices_received == 1 + assert devices_received == 2 assert devices_removed == 1 @pytest.mark.parametrize("usb_queries", ["vnd:04a9;vnd:04aa"])