From 4b0dd4e7020a9d2938dbb36520018f5491ec0b38 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Thu, 14 May 2026 13:48:39 -0500 Subject: [PATCH 1/6] wip: add impl.portal.Request interface --- credentialsd-ui/src/dbus.rs | 48 +++++++++++++++++---------- credentialsd/src/dbus/flow_control.rs | 8 +++++ credentialsd/src/dbus/ui_control.rs | 32 ++++++++++-------- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index d050a2a8..4464c898 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -48,6 +48,7 @@ impl CredentialPortalBackend { &self, #[zbus(header)] header: Header<'_>, #[zbus(object_server)] object_server: &ObjectServer, + handle: ObjectPath<'_>, parent_window: Optional, origin: String, r#type: Operation, @@ -62,10 +63,6 @@ impl CredentialPortalBackend { let Some(sender) = header.sender() else { return Err(fdo::Error::BadAddress("Sender not found".to_string())); }; - let object_path = ObjectPath::from_string_unchecked(format!( - "/org/freedesktop/portal/Credential/{}", - request_id - )); let ui_context = UiContext { parent_window: parent_window.into(), origin, @@ -78,16 +75,22 @@ impl CredentialPortalBackend { app_path, options, }; + let ui_events_forwarder_task = Arc::new(AsyncMutex::new(None)); let ceremony = CeremonyObject { ui_context, request_tx: self.request_tx.clone(), return_address: sender.to_owned().into(), - ui_events_forwarder_task: None, + ui_events_forwarder_task: ui_events_forwarder_task.clone(), bg_events_tx: None, }; - object_server.at(object_path.clone(), ceremony).await?; + + let request = CeremonyRequest { + ui_events_forwarder_task, + }; + object_server.at(handle.clone(), ceremony).await?; + object_server.at(handle.clone(), request).await?; tracing::debug!("Received UI launch request"); - Ok(object_path) + Ok(handle.into_owned()) } } @@ -95,7 +98,7 @@ pub struct CeremonyObject { ui_context: UiContext, pub request_tx: Sender<(ViewRequest, Arc>)>, pub return_address: OwnedUniqueName, - ui_events_forwarder_task: Option>, + ui_events_forwarder_task: Arc>>>, bg_events_tx: Option>, } @@ -129,7 +132,10 @@ impl CeremonyObject { } } }); - self.ui_events_forwarder_task = Some(ui_events_task); + self.ui_events_forwarder_task + .lock() + .await + .insert(ui_events_task); // Assuming this is a PublicKey request, require the rp_id let rp_id = self @@ -180,24 +186,32 @@ impl CeremonyObject { return Err(fdo::Error::Failed("Failed to handle event".to_string())); } - async fn cancel( + #[zbus(signal)] + async fn user_interacted( + emitter: SignalEmitter<'_>, + event: &UserInteractedEvent, + ) -> zbus::Result<()>; +} + +struct CeremonyRequest { + ui_events_forwarder_task: Arc>>>, +} + +#[interface(name = "org.freedesktop.impl.portal.Request")] +impl CeremonyRequest { + async fn close( &mut self, #[zbus(header)] header: Header<'_>, #[zbus(object_server)] object_server: &ObjectServer, ) -> fdo::Result<()> { - if let Some(task) = self.ui_events_forwarder_task.take() { + if let Some(task) = self.ui_events_forwarder_task.lock().await.take() { task.cancel().await; } if let Some(path) = header.path() { // TODO: Send clean up task to GUI thread. object_server.remove::(path).await?; + object_server.remove::(path).await?; } Ok(()) } - - #[zbus(signal)] - async fn user_interacted( - emitter: SignalEmitter<'_>, - event: &UserInteractedEvent, - ) -> zbus::Result<()>; } diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index b42b51d8..acc063c6 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -22,6 +22,7 @@ use tokio::sync::oneshot; use tokio::sync::{mpsc::Sender, Mutex as AsyncMutex}; use tokio::task::AbortHandle; use zbus::connection::Connection; +use zbus::zvariant::OwnedObjectPath; use crate::credential_service::{nfc::NfcState, DeviceStateUpdate, ManageDevice}; use crate::dbus::ui_control::Ceremony; @@ -123,8 +124,15 @@ async fn handle() + ) + .try_into() + .expect("valid object path"); let flow = match ui_control_client .initialize( + handle, window_handle, origin, operation, diff --git a/credentialsd/src/dbus/ui_control.rs b/credentialsd/src/dbus/ui_control.rs index 678505e4..d7dec2f6 100644 --- a/credentialsd/src/dbus/ui_control.rs +++ b/credentialsd/src/dbus/ui_control.rs @@ -9,7 +9,7 @@ use tokio::sync::{ }; use zbus::{ fdo, proxy, - zvariant::{Optional, OwnedObjectPath}, + zvariant::{ObjectPath, Optional, OwnedObjectPath}, Connection, }; @@ -22,6 +22,7 @@ use credentialsd_common::{ pub trait UiController { fn initialize( &self, + handle: OwnedObjectPath, parent_window: Option, origin: String, r#type: Operation, @@ -44,6 +45,7 @@ pub trait UiController { trait UiControlService2 { fn initialize( &self, + handle: ObjectPath<'_>, parent_window: Optional, origin: String, r#type: Operation, @@ -54,7 +56,7 @@ trait UiControlService2 { app_pid: u32, app_path: String, options: PortalBackendOptions, - ) -> fdo::Result; + ) -> fdo::Result<()>; } #[derive(Clone, Debug)] @@ -114,6 +116,7 @@ impl UiControlServiceClient { impl UiController for UiControlServiceClient { async fn initialize( &self, + handle: OwnedObjectPath, parent_window: Option, origin: String, r#type: Operation, @@ -125,10 +128,17 @@ impl UiController for UiControlServiceClient { app_path: String, options: PortalBackendOptions, ) -> Result> { - let path = self - .proxy2() + let ceremony = CeremonyObjectProxy::new(&self.conn, handle.clone()).await?; + let (from_ui_tx, from_ui_rx) = mpsc::channel(32); + let from_ui_tx2 = from_ui_tx.clone(); + let ui_event_stream = ceremony.receive_user_interacted().await?; + tokio::task::spawn(async move { + _ = forward_ui_events(ui_event_stream, from_ui_tx2).await; + }); + self.proxy2() .await? .initialize( + handle.as_ref(), parent_window.into(), origin, r#type, @@ -141,17 +151,11 @@ impl UiController for UiControlServiceClient { options, ) .await?; - tracing::debug!(?path, "Path initialized"); - let flow_object = CeremonyObjectProxy::new(&self.conn, path).await?; - let (from_ui_tx, from_ui_rx) = mpsc::channel(32); - let ui_event_stream = flow_object.receive_user_interacted().await?; - tokio::task::spawn(async move { - _ = forward_ui_events(ui_event_stream, from_ui_tx).await; - }); + tracing::debug!(path = ?handle, "Path initialized"); // Mark as ready to receive messages. - flow_object.start().await?; + ceremony.start().await?; Ok(Ceremony { - proxy: Arc::new(flow_object), + proxy: Arc::new(ceremony), ui_events_rx: Arc::new(AsyncMutex::new(from_ui_rx)), }) } @@ -195,6 +199,7 @@ pub mod test { mpsc::{self, Receiver, Sender}, Mutex as AsyncMutex, Notify, }; + use zbus::zvariant::OwnedObjectPath; use crate::dbus::ui_control::Ceremony; @@ -208,6 +213,7 @@ pub mod test { impl UiController for DummyUiClient { async fn initialize( &self, + _handle: OwnedObjectPath, _parent_window: Option, _origin: String, _type: Operation, From 98f88c1eb78713b7f43641f9c41dcc903b0c3fd4 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 19 Jun 2026 08:25:54 -0500 Subject: [PATCH 2/6] common: Make PortalBackendOptions a D-Bus dict --- credentialsd-common/src/model.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index 9fb76f12..a012bd5b 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -1,7 +1,7 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; -use zvariant::{Optional, OwnedFd, SerializeDict, Type}; +use zvariant::{DeserializeDict, Optional, OwnedFd, SerializeDict, Type}; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct Credential { @@ -44,7 +44,8 @@ pub enum Operation { PublicKeyGet, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[derive(Clone, Debug, PartialEq, SerializeDict, DeserializeDict, Type)] +#[zvariant(signature = "dict")] pub struct PortalBackendOptions { /// A token that can be used to activate the UI window. pub activation_token: Optional, From 93f157ea1e2eb3fd59f71c014e7ba89cbf85b98b Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 19 Jun 2026 09:40:19 -0500 Subject: [PATCH 3/6] ui: Make Start() method idempotent --- credentialsd-ui/src/dbus.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index 4464c898..daf7f5a1 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -110,6 +110,12 @@ impl CeremonyObject { &mut self, #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, ) -> fdo::Result<()> { + let mut ui_events_task = self.ui_events_forwarder_task.lock().await; + if ui_events_task.is_some() { + tracing::warn!("Start() method called more than once. Ignoring."); + return Ok(()); + } + let (ui_events_tx, ui_events_rx) = channel::bounded(32); let (bg_events_tx, bg_events_rx) = channel::bounded(32); let flow_control_client = FlowControlClient { @@ -121,7 +127,7 @@ impl CeremonyObject { let emitter = emitter .set_destination(BusName::Unique((&self.return_address).into())) .to_owned(); - let ui_events_task = async_std::task::spawn(async move { + *ui_events_task = Some(async_std::task::spawn(async move { while let Ok(ui_event) = ui_events_rx.recv().await { tracing::trace!(?ui_event, "Sending UI event signal to portal"); if emitter.user_interacted(&ui_event).await.is_err() { @@ -131,11 +137,7 @@ impl CeremonyObject { break; } } - }); - self.ui_events_forwarder_task - .lock() - .await - .insert(ui_events_task); + })); // Assuming this is a PublicKey request, require the rp_id let rp_id = self From 2f26acf41623e77a58940bbf25e896753d654067 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Fri, 19 Jun 2026 12:51:59 -0500 Subject: [PATCH 4/6] ui: Clean up D-Bus objects on cancel or disconnection, remove Start() method --- credentialsd-ui/Cargo.toml | 2 +- credentialsd-ui/src/dbus.rs | 140 ++++++++++++++++++---- credentialsd-ui/src/gui/view_model/mod.rs | 4 +- credentialsd/src/dbus/ui_control.rs | 3 - 4 files changed, 120 insertions(+), 29 deletions(-) diff --git a/credentialsd-ui/Cargo.toml b/credentialsd-ui/Cargo.toml index 57e44fff..b8a691c3 100644 --- a/credentialsd-ui/Cargo.toml +++ b/credentialsd-ui/Cargo.toml @@ -12,7 +12,7 @@ wayland = ["gdk-wayland"] [dependencies] async-std = { version = "1.13.1", features = ["unstable"] } credentialsd-common = { path = "../credentialsd-common" } -futures-lite = "2.6.0" +futures-lite.workspace = true gettext-rs = { version = "0.7", features = ["gettext-system"] } gtk = { version = "0.10.3", package = "gtk4", features = ["v4_14"] } gdk-wayland = { version = "0.10.3", package = "gdk4-wayland", optional = true } diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index daf7f5a1..ec9701b4 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -1,16 +1,19 @@ use std::sync::Arc; use async_std::{ - channel::{self, Sender}, + channel::{self, Receiver, Sender}, sync::Mutex as AsyncMutex, task::JoinHandle, }; +use futures_lite::{FutureExt, StreamExt}; use zbus::{ - ObjectServer, fdo, interface, + Connection, ObjectServer, + fdo::{self, DBusProxy}, + interface, message::Header, names::{BusName, OwnedUniqueName}, object_server::SignalEmitter, - zvariant::{ObjectPath, Optional}, + zvariant::{ObjectPath, Optional, OwnedObjectPath}, }; use credentialsd_common::{ @@ -46,9 +49,10 @@ pub(crate) struct UiContext { impl CredentialPortalBackend { async fn initialize( &self, + #[zbus(connection)] connection: &Connection, #[zbus(header)] header: Header<'_>, #[zbus(object_server)] object_server: &ObjectServer, - handle: ObjectPath<'_>, + handle: OwnedObjectPath, parent_window: Optional, origin: String, r#type: Operation, @@ -59,10 +63,54 @@ impl CredentialPortalBackend { app_pid: u32, app_path: String, options: PortalBackendOptions, - ) -> fdo::Result> { - let Some(sender) = header.sender() else { + ) -> fdo::Result<()> { + let Some(sender) = header.sender().map(|h| h.to_owned()) else { return Err(fdo::Error::BadAddress("Sender not found".to_string())); }; + + // Set up cancellation background task. + let (cancel_task, client_cancelled_tx, gui_stopped_tx) = { + let sender = sender.clone(); + let object_path = handle.clone(); + let (client_cancelled_tx, client_cancelled_rx) = channel::bounded(1); + let (gui_stopped_tx, gui_stopped_rx) = channel::bounded(1); + let client_disconnected_rx = + notify_on_disconnected(connection, sender.clone().into()).await?; + let object_server = object_server.clone(); + let cancel_task = async_std::task::spawn(async move { + let disconnect_fut = client_disconnected_rx.recv(); + let cancel_fut = client_cancelled_rx.recv(); + let gui_stopped_fut = gui_stopped_rx.recv(); + + match disconnect_fut.race(cancel_fut).race(gui_stopped_fut).await { + Ok(Ok(())) => { + tracing::debug!(%sender, "Client cancelled or disconnected, dropping request") + } + Ok(Err(err)) => { + tracing::error!(%sender, %err, "Failed to watch for client disconnection") + } + Err(_) => { + tracing::error!(%sender, "Client disconnection task dropped prematurely") + } + } + + // TODO: Signal GUI thread of cancellation + if let Err(err) = object_server + .remove::(&object_path) + .await + { + tracing::warn!(%object_path, %err, "Failed to remove Ceremony request"); + } + if let Err(err) = object_server + .remove::(&object_path) + .await + { + tracing::warn!(%object_path, %err, "Failed to remove org.freedesktop.impl.portal.Request"); + } + }); + (cancel_task, client_cancelled_tx, gui_stopped_tx) + }; + let ui_context = UiContext { parent_window: parent_window.into(), origin, @@ -76,22 +124,59 @@ impl CredentialPortalBackend { options, }; let ui_events_forwarder_task = Arc::new(AsyncMutex::new(None)); - let ceremony = CeremonyObject { + let mut ceremony = CeremonyObject { ui_context, request_tx: self.request_tx.clone(), return_address: sender.to_owned().into(), ui_events_forwarder_task: ui_events_forwarder_task.clone(), bg_events_tx: None, }; - let request = CeremonyRequest { ui_events_forwarder_task, + cancel_task: Arc::new(AsyncMutex::new(Some(cancel_task))), + client_cancelled_tx, }; - object_server.at(handle.clone(), ceremony).await?; object_server.at(handle.clone(), request).await?; + + let emitter = SignalEmitter::new(connection, handle.clone())?; + ceremony.start(gui_stopped_tx, emitter.to_owned()).await?; + object_server.at(handle, ceremony).await?; + tracing::debug!("Received UI launch request"); - Ok(handle.into_owned()) + Ok(()) + } +} + +async fn notify_on_disconnected( + conn: &Connection, + bus_name: BusName<'static>, +) -> Result>, fdo::Error> { + let (tx, rx) = channel::bounded(1); + let dbus = DBusProxy::new(conn).await?; + + if !dbus.name_has_owner((&bus_name).into()).await? { + _ = tx.send(Ok(())).await; + tracing::trace!(%bus_name, "Name not connected."); + return Ok(rx); } + async_std::task::spawn(async move { + async fn watch(dbus: DBusProxy<'_>, bus_name: BusName<'_>) -> fdo::Result<()> { + let mut stream = dbus.receive_name_owner_changed().await?; + while let Some(signal) = stream.next().await { + let args = signal.args()?; + if args.name == bus_name && args.new_owner.is_none() { + tracing::trace!(%bus_name, "Name owner disconnected."); + return Ok(()); + } + } + Err(fdo::Error::Disconnected(format!( + "Disconnected from bus while waiting for name owner change on {bus_name}" + ))) + } + let res = watch(dbus, bus_name).await; + _ = tx.send(res).await; + }); + Ok(rx) } pub struct CeremonyObject { @@ -102,13 +187,13 @@ pub struct CeremonyObject { bg_events_tx: Option>, } -#[interface(name = "org.freedesktop.impl.portal.experimental.Credential.Ceremony")] impl CeremonyObject { /// Start the UI ceremony with an initial set of available credential interfaces. /// Call this method after subscribing to the signals. async fn start( &mut self, - #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + stopped_tx: Sender>, + emitter: SignalEmitter<'static>, ) -> fdo::Result<()> { let mut ui_events_task = self.ui_events_forwarder_task.lock().await; if ui_events_task.is_some() { @@ -131,12 +216,16 @@ impl CeremonyObject { while let Ok(ui_event) = ui_events_rx.recv().await { tracing::trace!(?ui_event, "Sending UI event signal to portal"); if emitter.user_interacted(&ui_event).await.is_err() { - tracing::error!("Failed to send UI event signal."); - // TODO: we need to cancel the request here, so we need a - // channel back to the ceremony object to send the cancellation. + tracing::trace!("Failed to send UI event signal."); break; } } + tracing::trace!("ui_events_task ending"); + if stopped_tx.send(Ok(())).await.is_err() { + tracing::error!( + "Failed to notify CredentialPortalBackend that request is ready for cleanup" + ); + }; })); // Assuming this is a PublicKey request, require the rp_id @@ -174,7 +263,10 @@ impl CeremonyObject { } Ok(()) } +} +#[interface(name = "org.freedesktop.impl.portal.experimental.Credential.Ceremony")] +impl CeremonyObject { async fn notify_state_changed(&self, event: BackgroundEvent) -> fdo::Result<()> { tracing::trace!(?event, "Received background event"); if let Some(tx) = &self.bg_events_tx { @@ -197,22 +289,22 @@ impl CeremonyObject { struct CeremonyRequest { ui_events_forwarder_task: Arc>>>, + cancel_task: Arc>>>, + client_cancelled_tx: Sender>, } #[interface(name = "org.freedesktop.impl.portal.Request")] impl CeremonyRequest { - async fn close( - &mut self, - #[zbus(header)] header: Header<'_>, - #[zbus(object_server)] object_server: &ObjectServer, - ) -> fdo::Result<()> { + async fn close(&mut self) -> fdo::Result<()> { + tracing::debug!("Client requested cancellation"); if let Some(task) = self.ui_events_forwarder_task.lock().await.take() { task.cancel().await; } - if let Some(path) = header.path() { - // TODO: Send clean up task to GUI thread. - object_server.remove::(path).await?; - object_server.remove::(path).await?; + if let Some(task) = self.cancel_task.lock().await.take() { + task.cancel().await; + } + if self.client_cancelled_tx.send(Ok(())).await.is_err() { + tracing::warn!("Request already cancelled"); } Ok(()) } diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index 4fdb22b5..8f4f75bc 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -149,6 +149,7 @@ impl ViewModel { }; let mut all_events = view_events.merge(bg_events.map(Event::Background)); while let Some(event) = all_events.next().await { + tracing::trace!("View event received: {event:?}"); match event { Event::View(ViewEvent::Initiated) => { self.update_title().await; @@ -336,7 +337,7 @@ impl ViewModel { } } -#[derive(Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub enum ViewEvent { Initiated, CredentialSelected(String), @@ -344,6 +345,7 @@ pub enum ViewEvent { UserCancelled, } +#[derive(Debug)] pub enum Event { Background(BackgroundEvent), View(ViewEvent), diff --git a/credentialsd/src/dbus/ui_control.rs b/credentialsd/src/dbus/ui_control.rs index d7dec2f6..702df901 100644 --- a/credentialsd/src/dbus/ui_control.rs +++ b/credentialsd/src/dbus/ui_control.rs @@ -89,7 +89,6 @@ impl Ceremony { default_service = "xyz.iinuwa.credentialsd.UiControl" )] trait CeremonyObject { - async fn start(&self) -> fdo::Result<()>; async fn notify_state_changed(&self, event: BackgroundEvent) -> fdo::Result<()>; async fn cancel(&self) -> fdo::Result<()>; @@ -152,8 +151,6 @@ impl UiController for UiControlServiceClient { ) .await?; tracing::debug!(path = ?handle, "Path initialized"); - // Mark as ready to receive messages. - ceremony.start().await?; Ok(Ceremony { proxy: Arc::new(ceremony), ui_events_rx: Arc::new(AsyncMutex::new(from_ui_rx)), From 6dec56aad873c0a5673170aad5f332298a7d8930 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 20 Jun 2026 08:25:07 -0500 Subject: [PATCH 5/6] ui: Allow Esc key to close window --- credentialsd-ui/src/dbus.rs | 2 +- credentialsd-ui/src/gui/view_model/gtk/application.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index ec9701b4..088b3f19 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -13,7 +13,7 @@ use zbus::{ message::Header, names::{BusName, OwnedUniqueName}, object_server::SignalEmitter, - zvariant::{ObjectPath, Optional, OwnedObjectPath}, + zvariant::{Optional, OwnedObjectPath}, }; use credentialsd_common::{ diff --git a/credentialsd-ui/src/gui/view_model/gtk/application.rs b/credentialsd-ui/src/gui/view_model/gtk/application.rs index b1f0255b..aa0a97b5 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/application.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/application.rs @@ -187,7 +187,7 @@ impl CredentialsUi { // Sets up keyboard shortcuts fn setup_accels(&self) { - self.set_accels_for_action("app.quit", &["q"]); + self.set_accels_for_action("app.quit", &["q", "Escape"]); self.set_accels_for_action("window.close", &["w"]); } From 0b225cf463496c3cedf0719a2abd8d47ad795c5a Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 20 Jun 2026 22:33:39 -0500 Subject: [PATCH 6/6] ui: Notify GUI thread of cancellation --- credentialsd-ui/src/dbus.rs | 32 ++++++++++++++++++----- credentialsd-ui/src/gui/mod.rs | 26 ++++++++++++++---- credentialsd-ui/src/gui/view_model/mod.rs | 1 + 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index 088b3f19..f2151afb 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -27,7 +27,11 @@ use credentialsd_common::{ use crate::client::FlowControlClient; pub struct CredentialPortalBackend { - pub request_tx: Sender<(ViewRequest, Arc>)>, + pub request_tx: Sender<( + ViewRequest, + Arc>, + Receiver<()>, + )>, } #[derive(Debug, Clone)] @@ -69,11 +73,12 @@ impl CredentialPortalBackend { }; // Set up cancellation background task. - let (cancel_task, client_cancelled_tx, gui_stopped_tx) = { + let (cancel_task, client_cancelled_tx, gui_stopped_tx, cancel_gui_rx) = { let sender = sender.clone(); let object_path = handle.clone(); let (client_cancelled_tx, client_cancelled_rx) = channel::bounded(1); let (gui_stopped_tx, gui_stopped_rx) = channel::bounded(1); + let (cancel_gui_tx, cancel_gui_rx) = channel::bounded(1); let client_disconnected_rx = notify_on_disconnected(connection, sender.clone().into()).await?; let object_server = object_server.clone(); @@ -94,7 +99,9 @@ impl CredentialPortalBackend { } } - // TODO: Signal GUI thread of cancellation + if cancel_gui_tx.send(()).await.is_err() { + tracing::error!("Failed to send cancellation request to GUI"); + }; if let Err(err) = object_server .remove::(&object_path) .await @@ -108,7 +115,12 @@ impl CredentialPortalBackend { tracing::warn!(%object_path, %err, "Failed to remove org.freedesktop.impl.portal.Request"); } }); - (cancel_task, client_cancelled_tx, gui_stopped_tx) + ( + cancel_task, + client_cancelled_tx, + gui_stopped_tx, + cancel_gui_rx, + ) }; let ui_context = UiContext { @@ -139,7 +151,9 @@ impl CredentialPortalBackend { object_server.at(handle.clone(), request).await?; let emitter = SignalEmitter::new(connection, handle.clone())?; - ceremony.start(gui_stopped_tx, emitter.to_owned()).await?; + ceremony + .start(gui_stopped_tx, cancel_gui_rx, emitter.to_owned()) + .await?; object_server.at(handle, ceremony).await?; tracing::debug!("Received UI launch request"); @@ -181,7 +195,11 @@ async fn notify_on_disconnected( pub struct CeremonyObject { ui_context: UiContext, - pub request_tx: Sender<(ViewRequest, Arc>)>, + pub request_tx: Sender<( + ViewRequest, + Arc>, + Receiver<()>, + )>, pub return_address: OwnedUniqueName, ui_events_forwarder_task: Arc>>>, bg_events_tx: Option>, @@ -193,6 +211,7 @@ impl CeremonyObject { async fn start( &mut self, stopped_tx: Sender>, + cancel_rx: Receiver<()>, emitter: SignalEmitter<'static>, ) -> fdo::Result<()> { let mut ui_events_task = self.ui_events_forwarder_task.lock().await; @@ -256,6 +275,7 @@ impl CeremonyObject { window_handle: self.ui_context.parent_window.clone().into(), }, Arc::new(AsyncMutex::new(flow_control_client)), + cancel_rx, ); if self.request_tx.send(req).await.is_err() { tracing::error!("Received message to start flow, but GUI thread is not listening."); diff --git a/credentialsd-ui/src/gui/mod.rs b/credentialsd-ui/src/gui/mod.rs index e7e3e104..9525d7b6 100644 --- a/credentialsd-ui/src/gui/mod.rs +++ b/credentialsd-ui/src/gui/mod.rs @@ -15,17 +15,25 @@ use crate::client::FlowControlClient; use view_model::ViewEvent; pub(super) fn start_gui_thread( - rx: Receiver<(ViewRequest, Arc>)>, + rx: Receiver<( + ViewRequest, + Arc>, + Receiver<()>, + )>, ) -> Result, std::io::Error> { thread::Builder::new().name("gui".into()).spawn(move || { // D-Bus received a request and needs a window open - while let Ok((view_request, flow_controller)) = rx.recv_blocking() { - run_gui(flow_controller, view_request); + while let Ok((view_request, flow_controller, cancel_rx)) = rx.recv_blocking() { + run_gui(flow_controller, view_request, cancel_rx); } }) } -fn run_gui(flow_controller: Arc>, request: ViewRequest) { +fn run_gui( + flow_controller: Arc>, + request: ViewRequest, + cancel_rx: Receiver<()>, +) { let parent_window: Option = request.window_handle.as_ref().and_then(|h| { h.to_string() .try_into() @@ -35,13 +43,21 @@ fn run_gui(flow_controller: Arc>, request: ViewReq let (tx_update, rx_update) = async_std::channel::unbounded::(); let (tx_event, rx_event) = async_std::channel::unbounded::(); + let tx_event2 = tx_event.clone(); + let cancel_task = async_std::task::spawn(async move { + if let Ok(_) = cancel_rx.recv().await { + if tx_event2.send(ViewEvent::UserCancelled).await.is_err() { + tracing::error!("Failed to send cancellation to view model"); + } + } + }); let event_loop = async_std::task::spawn(async move { - let request_id = request.id; let mut vm = view_model::ViewModel::new(request, flow_controller.clone(), rx_event, tx_update); vm.start_event_loop().await; tracing::debug!("Finishing user request."); // If cancellation fails, that's fine. + cancel_task.cancel().await; let _ = flow_controller.lock().await.cancel_request().await; // TODO: Clean up flow_object when request completes }); diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index 8f4f75bc..4b4454a5 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -186,6 +186,7 @@ impl ViewModel { } } Event::View(ViewEvent::UserCancelled) => { + self.tx_update.send(ViewUpdate::Cancelled).await.unwrap(); break; }