From 91466318af891bbf9bd6a6ce6775166e1fb33290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Fri, 22 Nov 2024 22:47:11 -0500 Subject: [PATCH] demo: Added USB portal support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added UI for USB - Added rusb to the crates. - Added libusb to the manifest. Signed-off-by: Hubert Figuière --- ashpd-demo/Cargo.lock | 29 ++ ashpd-demo/Cargo.toml | 1 + .../com.belmoussaoui.ashpd.demo.Devel.json | 14 + ashpd-demo/data/resources.gresource.xml | 2 + ashpd-demo/data/resources/ui/usb.ui | 63 +++ .../data/resources/ui/usb_device_row.ui | 29 ++ ashpd-demo/data/resources/ui/window.ui | 14 + ashpd-demo/src/portals/desktop/mod.rs | 2 + ashpd-demo/src/portals/desktop/usb.rs | 390 ++++++++++++++++++ ashpd-demo/src/window.rs | 5 +- 10 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 ashpd-demo/data/resources/ui/usb.ui create mode 100644 ashpd-demo/data/resources/ui/usb_device_row.ui create mode 100644 ashpd-demo/src/portals/desktop/usb.rs diff --git a/ashpd-demo/Cargo.lock b/ashpd-demo/Cargo.lock index bae07d545..df35148dd 100644 --- a/ashpd-demo/Cargo.lock +++ b/ashpd-demo/Cargo.lock @@ -78,6 +78,7 @@ dependencies = [ "gtk4", "libadwaita", "libshumate", + "rusb", "serde", "tracing", "tracing-subscriber", @@ -1590,6 +1591,18 @@ dependencies = [ "system-deps 6.2.2", ] +[[package]] +name = "libusb1-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1982,6 +1995,16 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rusb" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4" +dependencies = [ + "libc", + "libusb1-sys", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2349,6 +2372,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" diff --git a/ashpd-demo/Cargo.toml b/ashpd-demo/Cargo.toml index 4e2baa9ae..1cb3073ba 100644 --- a/ashpd-demo/Cargo.toml +++ b/ashpd-demo/Cargo.toml @@ -14,6 +14,7 @@ gettext-rs = {version = "0.7", features = ["gettext-system"]} gst = {package = "gstreamer", version = "0.23"} gst4gtk = {package = "gst-plugin-gtk4", version = "0.13", features = ["wayland", "x11egl", "x11glx", "gtk_v4_14"]} gtk = {package = "gtk4", version = "0.9", features = ["v4_14"]} +rusb = "0.9.4" serde = {version = "1.0", features = ["derive"]} shumate = {version = "0.6", package = "libshumate"} tracing = "0.1" diff --git a/ashpd-demo/build-aux/com.belmoussaoui.ashpd.demo.Devel.json b/ashpd-demo/build-aux/com.belmoussaoui.ashpd.demo.Devel.json index 1d655aa8d..644cf8b54 100644 --- a/ashpd-demo/build-aux/com.belmoussaoui.ashpd.demo.Devel.json +++ b/ashpd-demo/build-aux/com.belmoussaoui.ashpd.demo.Devel.json @@ -14,6 +14,7 @@ "--socket=wayland", "--device=dri", "--own-name=com.belmoussaoui.ashpd.demo", + "--usb=all", "--env=RUST_LOG=ashpd_demo=debug,ashpd=debug", "--env=G_MESSAGES_DEBUG=none", "--env=RUST_BACKTRACE=1" @@ -31,6 +32,19 @@ ] }, "modules": [ + { + "name": "libusb", + "config-opts": [ + "--disable-udev" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/libusb/libusb/releases/download/v1.0.23/libusb-1.0.23.tar.bz2", + "sha256": "db11c06e958a82dac52cf3c65cb4dd2c3f339c8a988665110e0d24d19312ad8d" + } + ] + }, { "name": "libshumate", "buildsystem": "meson", diff --git a/ashpd-demo/data/resources.gresource.xml b/ashpd-demo/data/resources.gresource.xml index 1202c6af2..28fc4afbe 100644 --- a/ashpd-demo/data/resources.gresource.xml +++ b/ashpd-demo/data/resources.gresource.xml @@ -26,6 +26,8 @@ resources/ui/screenshot.ui resources/ui/screencast.ui resources/ui/secret.ui + resources/ui/usb.ui + resources/ui/usb_device_row.ui resources/ui/wallpaper.ui resources/style.css diff --git a/ashpd-demo/data/resources/ui/usb.ui b/ashpd-demo/data/resources/ui/usb.ui new file mode 100644 index 000000000..a168dcefa --- /dev/null +++ b/ashpd-demo/data/resources/ui/usb.ui @@ -0,0 +1,63 @@ + + + + diff --git a/ashpd-demo/data/resources/ui/usb_device_row.ui b/ashpd-demo/data/resources/ui/usb_device_row.ui new file mode 100644 index 000000000..9e5bf5e5b --- /dev/null +++ b/ashpd-demo/data/resources/ui/usb_device_row.ui @@ -0,0 +1,29 @@ + + + + diff --git a/ashpd-demo/data/resources/ui/window.ui b/ashpd-demo/data/resources/ui/window.ui index a95869152..c6fbaeed6 100644 --- a/ashpd-demo/data/resources/ui/window.ui +++ b/ashpd-demo/data/resources/ui/window.ui @@ -155,6 +155,12 @@ secret + + + USB + usb + + Wallpaper @@ -325,6 +331,14 @@ + + + usb + + + + + wallpaper diff --git a/ashpd-demo/src/portals/desktop/mod.rs b/ashpd-demo/src/portals/desktop/mod.rs index 79a12f47d..97bedde8c 100644 --- a/ashpd-demo/src/portals/desktop/mod.rs +++ b/ashpd-demo/src/portals/desktop/mod.rs @@ -16,6 +16,7 @@ mod remote_desktop; mod screencast; mod screenshot; mod secret; +mod usb; mod wallpaper; pub use account::AccountPage; @@ -36,4 +37,5 @@ pub use remote_desktop::RemoteDesktopPage; pub use screencast::ScreenCastPage; pub use screenshot::ScreenshotPage; pub use secret::SecretPage; +pub use usb::UsbPage; pub use wallpaper::WallpaperPage; diff --git a/ashpd-demo/src/portals/desktop/usb.rs b/ashpd-demo/src/portals/desktop/usb.rs new file mode 100644 index 000000000..0db2b94cb --- /dev/null +++ b/ashpd-demo/src/portals/desktop/usb.rs @@ -0,0 +1,390 @@ +// Copyright (C) 2024 GNOME Foundation +// +// Authors: +// Hubert Figuière +// + +use std::{cell::RefCell, collections::HashMap, os::fd::AsRawFd, sync::Arc}; + +use adw::{prelude::*, subclass::prelude::*}; +use ashpd::{ + desktop::{ + usb::{Device, UsbDevice, UsbProxy}, + Session, + }, + zbus::zvariant::{Fd, ObjectPath}, + WindowIdentifier, +}; +use futures_util::{lock::Mutex, StreamExt}; +use glib::clone; +use gtk::glib; +use rusb::UsbContext; + +use crate::widgets::{PortalPage, PortalPageExt, PortalPageImpl}; + +glib::wrapper! { + pub struct UsbDeviceRow(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow; +} + +impl UsbDeviceRow { + fn with_device(page: UsbPage, device_id: String, writable: bool) -> Self { + let obj: Self = glib::Object::new(); + + let imp = obj.imp(); + imp.acquire.connect_clicked(clone!( + #[weak] + page, + #[strong] + device_id, + move |_| { + glib::spawn_future_local(clone!( + #[strong] + page, + #[strong] + device_id, + async move { page.share(&device_id, writable).await } + )); + } + )); + imp.release.connect_clicked(clone!( + #[weak] + page, + #[strong] + device_id, + move |_| { + glib::spawn_future_local(clone!( + #[strong] + page, + #[strong] + device_id, + async move { page.unshare(&device_id).await } + )); + } + )); + obj + } + + fn acquire(&self) { + self.imp().checkbox.set_active(true); + } + + fn release(&self) { + self.imp().checkbox.set_active(false); + } +} + +mod imp { + use super::*; + + #[derive(Debug, gtk::CompositeTemplate, Default)] + #[template(resource = "/com/belmoussaoui/ashpd/demo/usb_device_row.ui")] + pub struct UsbDeviceRow { + #[template_child] + pub(super) checkbox: TemplateChild, + #[template_child] + pub(super) acquire: TemplateChild, + #[template_child] + pub(super) release: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for UsbDeviceRow { + const NAME: &'static str = "UsbDeviceRow"; + type Type = super::UsbDeviceRow; + type ParentType = adw::ActionRow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for UsbDeviceRow {} + impl WidgetImpl for UsbDeviceRow {} + impl ListBoxRowImpl for UsbDeviceRow {} + impl PreferencesRowImpl for UsbDeviceRow {} + impl ActionRowImpl for UsbDeviceRow {} + + #[derive(Debug, gtk::CompositeTemplate, Default)] + #[template(resource = "/com/belmoussaoui/ashpd/demo/usb.ui")] + pub struct UsbPage { + #[template_child] + pub usb_devices: TemplateChild, + rows: RefCell>, + pub session: Arc>>>>, + pub event_source: Arc>>, + } + + impl UsbPage { + fn add(&self, uuid: String, row: super::UsbDeviceRow) { + self.usb_devices.get().add(&row); + self.rows.borrow_mut().insert(uuid, row); + } + + fn clear_devices(&self) { + for row in self.rows.borrow().values() { + self.usb_devices.get().remove(row); + } + self.rows.borrow_mut().clear(); + } + + fn acquired_device(&self, uuid: &str) { + if let Some(row) = self.rows.borrow().get(uuid) { + row.acquire(); + } + } + + pub(super) fn released_device(&self, uuid: &str) { + if let Some(row) = self.rows.borrow().get(uuid) { + row.release(); + } + } + + fn usb_describe_device(fd: &dyn AsRawFd) -> ashpd::Result { + let context = rusb::Context::new() + .map_err(|_| ashpd::PortalError::Failed("rusb Context".to_string()))?; + let handle = unsafe { context.open_device_with_fd(fd.as_raw_fd()) } + .map_err(|_| ashpd::PortalError::Failed("open USB device".to_string()))?; + let device = handle.device(); + let device_desc = device.device_descriptor().unwrap(); + Ok(format!( + "Bus {:03} Device {:03} ID {:04x}:{:04x}", + device.bus_number(), + device.address(), + device_desc.vendor_id(), + device_desc.product_id() + )) + } + + fn add_device_row(&self, page: &super::UsbPage, device: &(String, UsbDevice)) { + let row = super::UsbDeviceRow::with_device( + page.clone(), + device.0.clone(), + device.1.writable.unwrap_or(false), + ); + let vendor = device.1.vendor().unwrap_or_default(); + let dev = device.1.model().unwrap_or_default(); + row.set_title(&format!("{} {}", &vendor, &dev)); + if let Some(devnode) = &device.1.device_file { + row.set_subtitle(devnode); + } + page.imp().add(device.0.clone(), row); + } + + pub(super) async fn refresh_devices(&self) -> ashpd::Result<()> { + let page = self.obj(); + + self.clear_devices(); + + let usb = UsbProxy::new().await?; + let devices = usb.enumerate_devices().await?; + for device in devices { + self.add_device_row(&page, &device); + } + Ok(()) + } + + pub(super) async fn finish_acquire_device( + &self, + usb: &UsbProxy<'_>, + object_path: &ObjectPath<'_>, + ) { + loop { + let result = usb.finish_acquire_devices(object_path).await; + match result { + Ok(result) => { + println!("result {result:?}"); + if !result.1 { + continue; + } + for device in &result.0 { + if let Ok(fd) = &device.1 { + match Self::usb_describe_device(&Fd::from(fd)) { + Ok(describe) => self.obj().info(&describe), + Err(err) => self.obj().info(&err.to_string()), + } + } + self.acquired_device(&device.0); + } + } + Err(err) => { + tracing::error!("Finish acquire device error: {err}"); + self.obj() + .error(&format!("Finish acquire device error: {err}")); + } + } + break; + } + } + + pub(super) async fn start_session(&self) -> ashpd::Result<()> { + let usb = UsbProxy::new().await?; + let session = usb.create_session().await?; + self.session.lock().await.replace(session); + + let session = self.session.clone(); + let usb = UsbProxy::new().await?; + loop { + if session.lock().await.is_none() { + tracing::debug!("session is gone"); + break; + } + if let Some(response) = usb.receive_device_events().await?.next().await { + let events = response.events(); + for ev in events { + println!( + "Received event: {} for device {}", + ev.event_action(), + ev.event_device_id() + ); + } + } + } + tracing::debug!("Loop is gone"); + Ok(()) + } + + pub(super) async fn stop_session(&self) -> anyhow::Result<()> { + if let Some(session) = self.session.lock().await.take() { + session.close().await?; + } + Ok(()) + } + } + + #[glib::object_subclass] + impl ObjectSubclass for UsbPage { + const NAME: &'static str = "UsbPage"; + type Type = super::UsbPage; + type ParentType = PortalPage; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + + klass.install_action_async("usb.refresh", None, |page, _, _| async move { + page.refresh_devices().await + }); + klass.install_action_async("usb.start_session", None, |page, _, _| async move { + page.start_session().await + }); + klass.install_action_async("usb.stop_session", None, |page, _, _| async move { + page.stop_session().await; + }); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for UsbPage { + fn constructed(&self) { + self.parent_constructed(); + self.obj().action_set_enabled("usb.stop_session", false); + } + } + + impl WidgetImpl for UsbPage { + fn map(&self) { + glib::spawn_future_local(clone!( + #[weak(rename_to = widget)] + self, + async move { + widget.obj().refresh_devices().await; + } + )); + + self.parent_map(); + } + } + + impl BinImpl for UsbPage {} + impl PortalPageImpl for UsbPage {} +} + +glib::wrapper! { + pub struct UsbPage(ObjectSubclass) + @extends gtk::Widget, adw::Bin, PortalPage; +} + +impl UsbPage { + async fn refresh_devices(&self) { + match self.imp().refresh_devices().await { + Ok(_) => {} + Err(err) => { + tracing::error!("Failed to refresh USB devices: {err}"); + self.error("Failed to refresh USB devices."); + } + } + } + + async fn start_session(&self) { + self.action_set_enabled("usb.start_session", false); + self.action_set_enabled("usb.stop_session", true); + + match self.imp().start_session().await { + Ok(_) => self.info("USB session started"), + Err(err) => { + tracing::error!("Failed to start USB session: {err}"); + self.error("Failed to start USB session."); + self.action_set_enabled("usb.start_session", true); + self.action_set_enabled("usb.stop_session", false); + } + } + } + + async fn stop_session(&self) { + self.action_set_enabled("usb.start_session", true); + self.action_set_enabled("usb.stop_session", false); + + match self.imp().stop_session().await { + Ok(_) => self.info("USB session stopped"), + Err(err) => { + tracing::error!("Failed to stop USB session: {err}"); + self.error("Failed to stop USB session."); + } + } + } + + async fn do_share(&self, device_id: &String, device_writable: bool) -> ashpd::Result<()> { + let root = self.native().unwrap(); + let identifier = WindowIdentifier::from_native(&root).await; + let usb = UsbProxy::new().await?; + let resp = usb + .acquire_devices( + identifier.as_ref(), + &[Device(device_id.to_string(), device_writable)], + ) + .await?; + if resp.response().is_ok() { + let path = resp.path(); + self.imp().finish_acquire_device(&usb, path).await; + } + + Ok(()) + } + + async fn share(&self, device_id: &String, device_writable: bool) { + let result = self.do_share(device_id, device_writable).await; + if let Err(err) = result { + tracing::error!("Acquire device error: {err}"); + self.error(&format!("Acquire device error: {err}")); + } + } + + async fn unshare(&self, device_id: &str) { + let result = async { + let usb = UsbProxy::new().await?; + usb.release_devices(&[device_id]).await + }.await; + if let Err(err) = result { + tracing::error!("Acquire device error: {err}"); + self.error(&format!("Acquire device error: {err}")); + } + self.imp().released_device(device_id); + } +} diff --git a/ashpd-demo/src/window.rs b/ashpd-demo/src/window.rs index b80e68847..4a216c1af 100644 --- a/ashpd-demo/src/window.rs +++ b/ashpd-demo/src/window.rs @@ -14,7 +14,7 @@ use crate::{ AccountPage, BackgroundPage, CameraPage, DevicePage, DynamicLauncherPage, EmailPage, FileChooserPage, InhibitPage, LocationPage, NetworkMonitorPage, NotificationPage, OpenUriPage, PrintPage, ProxyResolverPage, RemoteDesktopPage, ScreenCastPage, - ScreenshotPage, SecretPage, WallpaperPage, + ScreenshotPage, SecretPage, UsbPage, WallpaperPage, }, DocumentsPage, }, @@ -45,6 +45,8 @@ mod imp { #[template_child] pub camera: TemplateChild, #[template_child] + pub usb: TemplateChild, + #[template_child] pub wallpaper: TemplateChild, #[template_child] pub location: TemplateChild, @@ -88,6 +90,7 @@ mod imp { split_view: TemplateChild::default(), camera: TemplateChild::default(), dynamic_launcher: TemplateChild::default(), + usb: TemplateChild::default(), wallpaper: TemplateChild::default(), location: TemplateChild::default(), notification: TemplateChild::default(),