From 55e3ce30534c3c4fe6958da75b128d6873856ec8 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 15:15:52 +0200 Subject: [PATCH 1/5] feat(cardwired): add new Dbus signal for new apps --- crates/cardwire-daemon/src/analyzer/models.rs | 15 ++++++++++++++- crates/cardwire-daemon/src/daemon.rs | 16 ++++++++++++++++ crates/cardwire-daemon/src/file/sql.rs | 11 +++++++++++ crates/cardwire-daemon/src/interface/smart.rs | 14 ++++++++++++-- crates/cardwire-daemon/src/manager.rs | 15 +++++++++------ 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/crates/cardwire-daemon/src/analyzer/models.rs b/crates/cardwire-daemon/src/analyzer/models.rs index 35d60549..32db6109 100644 --- a/crates/cardwire-daemon/src/analyzer/models.rs +++ b/crates/cardwire-daemon/src/analyzer/models.rs @@ -13,7 +13,7 @@ use zbus::object_server::SignalEmitter; use crate::{ analyzer::{ dynamic_analysis::{check_env, get_app_id_wayland_with_retry, get_steam_app_id}, helpers::{comm_to_string, get_real_process_name, is_proc_still_alive}, static_analysis::{self, AppMetadata, watch_fdo_folders} - }, file::GpuPolicy, interface::{LogEntry, LoggerInterfaceSignals} + }, file::{DbusAppMetadata, GpuPolicy}, interface::{LogEntry, LoggerInterfaceSignals, SmartPolicyInterface} }; #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -51,6 +51,7 @@ pub struct CardwireAnalyzer { reported_pids: Arc>>, report_semaphore: Arc, signal: Arc>>, + new_app_signal: Arc>>, } // Bound the number of concurrent report tasks @@ -65,6 +66,7 @@ impl CardwireAnalyzer { signal: Arc>>, db_cache: Arc>>, db_tx: mpsc::Sender<(String, AppMetadata, oneshot::Sender)>, + new_app_signal: Arc>>, ) -> anyhow::Result { let mut blocker = blocker.write().await; let exec_ring = blocker.get_exec_ring()?; @@ -103,6 +105,7 @@ impl CardwireAnalyzer { reported_pids: Arc::new(RwLock::new(HashSet::new())), report_semaphore: Arc::new(Semaphore::new(REPORT_SEMAPHORE_PERMITS)), signal, + new_app_signal, }) } pub async fn run(self) -> anyhow::Result<()> { @@ -350,6 +353,7 @@ impl CardwireAnalyzer { } } + let dbus_meta = DbusAppMetadata::from_app_metadata(&meta, GpuPolicy::Blocked as u32); let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); let res = self .db_tx @@ -363,6 +367,15 @@ impl CardwireAnalyzer { .write() .await .insert(lookup_name.to_string(), GpuPolicy::Blocked); + if let Some(emitter) = self.new_app_signal.get() + && let Err(e) = SmartPolicyInterface::new_app_added( + emitter, + (lookup_name.to_string(), dbus_meta), + ) + .await + { + error!("failed to emit process_blocked_changed: {}", e); + } info!( "Discovered a new app: {}, adding to the database", lookup_name diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index 49c57e3a..3a1cf221 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -181,6 +181,22 @@ async fn spawn_dbus_api( object_server .at(path, daemon.smart_policy_interface.clone()) .await?; + match object_server + .interface::<_, crate::interface::SmartPolicyInterface>(path) + .await + { + Ok(smart_ref) => { + daemon + .smart_policy_interface + .new_app_signal + .get_or_init(|| smart_ref.signal_emitter().to_owned()); + } + Err(e) => { + log::warn!( + "Failed to get the Smart Policy interface ({e}); New App notifications will not be emitted" + ); + } + } drop(power_tasks); // drop gpu list to prevent deadlock diff --git a/crates/cardwire-daemon/src/file/sql.rs b/crates/cardwire-daemon/src/file/sql.rs index 7bc1388b..d86fb391 100644 --- a/crates/cardwire-daemon/src/file/sql.rs +++ b/crates/cardwire-daemon/src/file/sql.rs @@ -45,6 +45,17 @@ pub struct DbusAppMetadata { pub gpu_policy: u32, } +impl DbusAppMetadata { + pub fn from_app_metadata(meta: &AppMetadata, gpu_policy: u32) -> Self { + Self { + display_name: meta.display_name.clone(), + desktop_file_id: meta.desktop_file_id.clone(), + icon_name: meta.icon_name.clone(), + gpu_policy, + } + } +} + #[derive(Debug, Clone)] pub struct CardwireDatabase { pub cache: Arc>>, diff --git a/crates/cardwire-daemon/src/interface/smart.rs b/crates/cardwire-daemon/src/interface/smart.rs index 6b4b5edc..d97bc514 100644 --- a/crates/cardwire-daemon/src/interface/smart.rs +++ b/crates/cardwire-daemon/src/interface/smart.rs @@ -1,10 +1,12 @@ use aya::maps::{HashMap as AyaHashMap, MapError as AyaMapError}; use cardwire_ebpf_userspace::EbpfBlocker; -use std::{collections::HashMap, path::Path, sync::Arc}; +use std::{ + collections::HashMap, path::Path, sync::{Arc, OnceLock} +}; use tokio::sync::{Mutex, RwLock}; use zbus::{ - fdo::{self, Error::Failed}, interface + fdo::{self, Error::Failed}, interface, object_server::SignalEmitter }; use crate::file::{CardwireDatabase, DbusAppMetadata, GpuPolicy}; @@ -15,6 +17,7 @@ pub struct SmartPolicyInterface { forced_map: Arc>>, pub database: CardwireDatabase, policy_lock: Arc>, + pub new_app_signal: Arc>>, } impl SmartPolicyInterface { @@ -27,6 +30,7 @@ impl SmartPolicyInterface { forced_map, database: db, policy_lock: Arc::new(Mutex::new(())), + new_app_signal: Arc::new(OnceLock::new()), } } } @@ -178,4 +182,10 @@ impl SmartPolicyInterface { Ok(()) } + + #[zbus(signal)] + pub async fn new_app_added( + emitter: &SignalEmitter<'_>, + new_app: (String, DbusAppMetadata), + ) -> zbus::Result<()>; } diff --git a/crates/cardwire-daemon/src/manager.rs b/crates/cardwire-daemon/src/manager.rs index 7fa962ea..a4e2c720 100644 --- a/crates/cardwire-daemon/src/manager.rs +++ b/crates/cardwire-daemon/src/manager.rs @@ -274,13 +274,16 @@ impl DaemonManager { let db_cache = self.smart_policy_interface.database.cache.clone(); let tx = self.smart_policy_interface.database.tx.clone(); + let new_app_signal = Arc::clone(&self.smart_policy_interface.new_app_signal); + async move { - let cardwire_analyzer = CardwireAnalyzer::build(blocker, logger, signal, db_cache, tx) - .await - .map_err(|err| { - error!("Failed to build CardwireAnalyzer: {}", err); - err - })?; + let cardwire_analyzer = + CardwireAnalyzer::build(blocker, logger, signal, db_cache, tx, new_app_signal) + .await + .map_err(|err| { + error!("Failed to build CardwireAnalyzer: {}", err); + err + })?; let res = cardwire_analyzer.run().await; if let Err(ref e) = res { error!("CardwireAnalyzer task failed: {}", e); From 883de10fbb99fc098850195cfc6903b171613f81 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 15:17:15 +0200 Subject: [PATCH 2/5] feat(cardwire-gui): listen to new_app events --- crates/cardwire-gui/src/app.rs | 4 ++ crates/cardwire-gui/src/message.rs | 1 + crates/cardwire-gui/src/subscription.rs | 64 +++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/cardwire-gui/src/app.rs b/crates/cardwire-gui/src/app.rs index caba719a..df060136 100644 --- a/crates/cardwire-gui/src/app.rs +++ b/crates/cardwire-gui/src/app.rs @@ -393,6 +393,10 @@ impl AppState { self.error = Some(format!("Error fetching app policies: {}", err)); } }, + Message::NewAppDiscovered((app_id, meta)) => { + let resolved = crate::helpers::resolve_app_metadata(&app_id, &meta); + self.smart_state.app_policies.insert(app_id, resolved); + } Message::SetAppPolicy(app_id, policy) => { let conn = self.zbus_conn.clone(); let app_id_clone = app_id.clone(); diff --git a/crates/cardwire-gui/src/message.rs b/crates/cardwire-gui/src/message.rs index 815d5d79..1e8ae929 100644 --- a/crates/cardwire-gui/src/message.rs +++ b/crates/cardwire-gui/src/message.rs @@ -37,6 +37,7 @@ pub enum Message { FetchedAppPolicies( Result, String>, ), + NewAppDiscovered((String, crate::models::DbusAppMetadata)), SetAppPolicy(String, i32), AppPolicyResult(Result<(String, i32), String>), UpdateSmartSearch(String), diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index 1fc9610b..360e7e6c 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -597,11 +597,43 @@ fn logger_sub() -> Subscription { }) } +#[proxy( + default_service = "org.opengamingcollective.cardwire", + default_path = "/org/opengamingcollective/cardwire", + interface = "org.opengamingcollective.cardwire.SmartPolicy" +)] +trait CardwireSmartPolicy { + fn get_app_policies(&self) -> zbus::Result>; + #[zbus(signal)] + fn new_app_added(&self, new_app: (String, crate::models::DbusAppMetadata)) -> zbus::Result<()>; +} + fn smart_sub() -> Subscription { - Subscription::run_with("cardwire_smart_subscription", |_| { - stream::channel(10, |mut output: Sender| async move { - let conn = CardwireDbus::new(); - match conn.get_app_policies().await { + Subscription::run_with("cardwire_smart_subscription", |_id| { + stream::channel(100, |mut output: Sender| async move { + let connection = match Connection::system().await { + Ok(conn) => conn, + Err(e) => { + warn!("Failed to connect to D-Bus: {}", e); + let _ = output + .send(Message::FetchedAppPolicies(Err(e.to_string()))) + .await; + return; + } + }; + + let proxy = match CardwireSmartPolicyProxy::new(&connection).await { + Ok(p) => p, + Err(e) => { + warn!("Failed to create D-Bus SmartPolicy proxy: {}", e); + let _ = output + .send(Message::FetchedAppPolicies(Err(e.to_string()))) + .await; + return; + } + }; + + match proxy.get_app_policies().await { Ok(policies) => { let _ = output.send(Message::FetchedAppPolicies(Ok(policies))).await; } @@ -611,6 +643,30 @@ fn smart_sub() -> Subscription { .await; } } + + let mut new_app_stream = match proxy.receive_new_app_added().await { + Ok(stream) => stream, + Err(err) => { + warn!("Failed to subscribe to D-Bus new_app_added signal: {}", err); + let _ = output + .send(Message::FetchedAppPolicies(Err(err.to_string()))) + .await; + return; + } + }; + + while let Some(signal) = new_app_stream.next().await { + if let Ok(args) = signal.args() { + let new_app = args.new_app().clone(); + let _ = output.send(Message::NewAppDiscovered(new_app)).await; + } + } + + let _ = output + .send(Message::FetchedAppPolicies(Err( + "Cardwire daemon disconnected".to_string(), + ))) + .await; }) }) } From 8ede781be001da960f09d0f45ab88ad6987c300a Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 15:23:41 +0200 Subject: [PATCH 3/5] fix(cardwire-gui): register new_app_added before fetching policies --- crates/cardwire-gui/src/subscription.rs | 202 +++++++++++++++++++----- 1 file changed, 161 insertions(+), 41 deletions(-) diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index 360e7e6c..6f5f2f97 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -608,6 +608,54 @@ trait CardwireSmartPolicy { fn new_app_added(&self, new_app: (String, crate::models::DbusAppMetadata)) -> zbus::Result<()>; } +async fn run_smart_sub(connection: &Connection, output: &mut Sender) { + let proxy = match CardwireSmartPolicyProxy::new(connection).await { + Ok(p) => p, + Err(e) => { + warn!("Failed to create D-Bus SmartPolicy proxy: {}", e); + let _ = output + .send(Message::FetchedAppPolicies(Err(e.to_string()))) + .await; + return; + } + }; + + let mut new_app_stream = match proxy.receive_new_app_added().await { + Ok(stream) => stream, + Err(err) => { + warn!("Failed to subscribe to D-Bus new_app_added signal: {}", err); + let _ = output + .send(Message::FetchedAppPolicies(Err(err.to_string()))) + .await; + return; + } + }; + + match proxy.get_app_policies().await { + Ok(policies) => { + let _ = output.send(Message::FetchedAppPolicies(Ok(policies))).await; + } + Err(err) => { + let _ = output + .send(Message::FetchedAppPolicies(Err(err.to_string()))) + .await; + } + } + + while let Some(signal) = new_app_stream.next().await { + if let Ok(args) = signal.args() { + let new_app = args.new_app().clone(); + let _ = output.send(Message::NewAppDiscovered(new_app)).await; + } + } + + let _ = output + .send(Message::FetchedAppPolicies(Err( + "Cardwire daemon disconnected".to_string(), + ))) + .await; +} + fn smart_sub() -> Subscription { Subscription::run_with("cardwire_smart_subscription", |_id| { stream::channel(100, |mut output: Sender| async move { @@ -622,53 +670,125 @@ fn smart_sub() -> Subscription { } }; - let proxy = match CardwireSmartPolicyProxy::new(&connection).await { - Ok(p) => p, - Err(e) => { - warn!("Failed to create D-Bus SmartPolicy proxy: {}", e); - let _ = output - .send(Message::FetchedAppPolicies(Err(e.to_string()))) - .await; - return; - } - }; + run_smart_sub(&connection, &mut output).await; + }) + }) +} - match proxy.get_app_policies().await { - Ok(policies) => { - let _ = output.send(Message::FetchedAppPolicies(Ok(policies))).await; - } - Err(err) => { - let _ = output - .send(Message::FetchedAppPolicies(Err(err.to_string()))) - .await; - } +#[cfg(test)] +mod tests { + use super::*; + use tokio::sync::mpsc; + use zbus::interface; + + struct TestSmartPolicyServer { + snapshot_policies: HashMap, + on_get_policies_called: Option>, + } + + #[interface(name = "org.opengamingcollective.cardwire.SmartPolicy")] + impl TestSmartPolicyServer { + async fn get_app_policies( + &mut self, + ) -> zbus::fdo::Result> { + if let Some(tx) = self.on_get_policies_called.take() { + let _ = tx.send(()).await; } + Ok(self.snapshot_policies.clone()) + } - let mut new_app_stream = match proxy.receive_new_app_added().await { - Ok(stream) => stream, - Err(err) => { - warn!("Failed to subscribe to D-Bus new_app_added signal: {}", err); - let _ = output - .send(Message::FetchedAppPolicies(Err(err.to_string()))) - .await; - return; - } - }; + #[zbus(signal)] + async fn new_app_added( + signal_emitter: &zbus::object_server::SignalEmitter<'_>, + new_app: (String, crate::models::DbusAppMetadata), + ) -> zbus::Result<()>; + } - while let Some(signal) = new_app_stream.next().await { - if let Ok(args) = signal.args() { - let new_app = args.new_app().clone(); - let _ = output.send(Message::NewAppDiscovered(new_app)).await; - } + #[tokio::test] + async fn test_smart_sub_handles_app_persisted_between_stream_registration_and_snapshot() { + let (p0, p1) = tokio::net::UnixStream::pair().expect("failed to create unix stream pair"); + + let (notify_tx, mut notify_rx) = mpsc::channel::<()>(1); + + let server_obj = TestSmartPolicyServer { + snapshot_policies: HashMap::from([( + "app1".to_string(), + crate::models::DbusAppMetadata { + display_name: "App One".to_string(), + desktop_file_id: None, + icon_name: None, + gpu_policy: 0, + }, + )]), + on_get_policies_called: Some(notify_tx), + }; + + let server_conn = zbus::connection::Builder::unix_stream(p0) + .serve_at("/org/opengamingcollective/cardwire", server_obj) + .unwrap() + .build() + .await + .unwrap(); + + let client_conn = zbus::connection::Builder::unix_stream(p1) + .build() + .await + .unwrap(); + + let (mut sender, mut receiver) = iced::futures::channel::mpsc::channel(100); + + let server_conn_clone = server_conn.clone(); + let sub_handle = tokio::spawn(async move { + run_smart_sub(&client_conn, &mut sender).await; + }); + + // Wait until get_app_policies is invoked (which proves stream registration has completed) + notify_rx + .recv() + .await + .expect("get_app_policies was not called"); + + // Emit signal during snapshot handling (simulating app persisted right after stream setup) + let iface_ref = server_conn_clone + .object_server() + .interface::<_, TestSmartPolicyServer>("/org/opengamingcollective/cardwire") + .await + .unwrap(); + + let emitter = iface_ref.signal_emitter().to_owned(); + let new_app_meta = crate::models::DbusAppMetadata { + display_name: "App Two (Interleaved)".to_string(), + desktop_file_id: Some("app2.desktop".to_string()), + icon_name: Some("app2-icon".to_string()), + gpu_policy: 0, + }; + + TestSmartPolicyServer::new_app_added(&emitter, ("app2".to_string(), new_app_meta.clone())) + .await + .unwrap(); + + // Check received messages in order + let first_msg = receiver.next().await.expect("expected snapshot message"); + match first_msg { + Message::FetchedAppPolicies(Ok(policies)) => { + assert!(policies.contains_key("app1")); + assert!(!policies.contains_key("app2")); } + other => panic!("expected FetchedAppPolicies(Ok), got {:?}", other), + } - let _ = output - .send(Message::FetchedAppPolicies(Err( - "Cardwire daemon disconnected".to_string(), - ))) - .await; - }) - }) + let second_msg = receiver.next().await.expect("expected signal message"); + match second_msg { + Message::NewAppDiscovered((id, meta)) => { + assert_eq!(id, "app2"); + assert_eq!(meta.display_name, "App Two (Interleaved)"); + } + other => panic!("expected NewAppDiscovered, got {:?}", other), + } + + drop(server_conn); + sub_handle.abort(); + } } pub fn dbus_sub() -> Subscription { From 40ebf6b641b383c244e76c245b6d7d9f1fca0340 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 15:30:07 +0200 Subject: [PATCH 4/5] fix(cardwire-gui): icon lookup and dbus sub --- .../cardwire-gui/src/helpers/app_resolver.rs | 149 +++++++++++++++++- crates/cardwire-gui/src/subscription.rs | 22 +-- 2 files changed, 158 insertions(+), 13 deletions(-) diff --git a/crates/cardwire-gui/src/helpers/app_resolver.rs b/crates/cardwire-gui/src/helpers/app_resolver.rs index d5b4669c..b387e8e1 100644 --- a/crates/cardwire-gui/src/helpers/app_resolver.rs +++ b/crates/cardwire-gui/src/helpers/app_resolver.rs @@ -66,6 +66,14 @@ pub fn resolve_app_metadata(app_id: &str, raw: &DbusAppMetadata) -> ResolvedApp } } candidate_filenames.push(format!("{}.desktop", app_id)); + candidate_filenames.push(format!("{}.desktop", app_id.to_lowercase())); + let mut chars = app_id.chars(); + if let Some(first) = chars.next() { + let capitalized = format!("{}{}.desktop", first.to_uppercase(), chars.as_str()); + if !candidate_filenames.contains(&capitalized) { + candidate_filenames.push(capitalized); + } + } 'search_desktop: for data_dir in &data_dirs { let apps_dir = data_dir.join("applications"); @@ -137,9 +145,27 @@ fn resolve_icon_path( if p.is_absolute() && p.exists() { return Some(p.to_path_buf()); } - names_to_check.push(name); + names_to_check.push(name.to_string()); + names_to_check.push(name.to_lowercase()); + let mut chars = name.chars(); + if let Some(first) = chars.next() { + let capitalized = format!("{}{}", first.to_uppercase(), chars.as_str()); + if !names_to_check.contains(&capitalized) { + names_to_check.push(capitalized); + } + } + } + if !names_to_check.contains(&app_id.to_string()) { + names_to_check.push(app_id.to_string()); + names_to_check.push(app_id.to_lowercase()); + let mut chars = app_id.chars(); + if let Some(first) = chars.next() { + let capitalized = format!("{}{}", first.to_uppercase(), chars.as_str()); + if !names_to_check.contains(&capitalized) { + names_to_check.push(capitalized); + } + } } - names_to_check.push(app_id); let extensions = ["png", "svg", "xpm"]; let icon_subdirs = [ @@ -174,3 +200,122 @@ fn resolve_icon_path( None } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_simulate_alacritty_lookup() { + let raw = DbusAppMetadata { + display_name: "Alacritty".to_string(), + desktop_file_id: Some("Alacritty".to_string()), + icon_name: Some("Alacritty".to_string()), + gpu_policy: 0, + }; + let resolved = resolve_app_metadata("alacritty", &raw); + println!("\n=== Alacritty (from Daemon metadata) ==="); + println!("App ID: {}", resolved.app_id); + println!("Display Name: {}", resolved.display_name); + println!("Desktop ID: {:?}", resolved.desktop_file_id); + println!("Icon Name: {:?}", resolved.icon_name); + println!("Icon Path: {:?}", resolved.icon_path); + assert_eq!(resolved.display_name, "Alacritty"); + assert!(resolved.icon_path.is_some()); + } + + #[test] + fn test_simulate_alacritty_fallback_lookup() { + let raw = DbusAppMetadata { + display_name: "".to_string(), + desktop_file_id: None, + icon_name: None, + gpu_policy: 0, + }; + let resolved = resolve_app_metadata("alacritty", &raw); + println!("\n=== Alacritty (from binary app_id fallback) ==="); + println!("App ID: {}", resolved.app_id); + println!("Display Name: {}", resolved.display_name); + println!("Desktop ID: {:?}", resolved.desktop_file_id); + println!("Icon Name: {:?}", resolved.icon_name); + println!("Icon Path: {:?}", resolved.icon_path); + assert_eq!(resolved.display_name, "Alacritty"); + assert!(resolved.icon_path.is_some()); + } + + #[test] + fn test_simulate_mission_center_lookup() { + let raw = DbusAppMetadata { + display_name: "Mission Center".to_string(), + desktop_file_id: Some("io.missioncenter.MissionCenter".to_string()), + icon_name: Some("io.missioncenter.MissionCenter".to_string()), + gpu_policy: 0, + }; + let resolved = resolve_app_metadata("missioncenter", &raw); + println!("\n=== Mission Center (from Daemon metadata) ==="); + println!("App ID: {}", resolved.app_id); + println!("Display Name: {}", resolved.display_name); + println!("Desktop ID: {:?}", resolved.desktop_file_id); + println!("Icon Name: {:?}", resolved.icon_name); + println!("Icon Path: {:?}", resolved.icon_path); + assert_eq!(resolved.display_name, "Mission Center"); + assert!(resolved.icon_path.is_some()); + } + + #[test] + fn test_simulate_mission_center_flatpak_or_app_id_lookup() { + let raw = DbusAppMetadata { + display_name: "".to_string(), + desktop_file_id: None, + icon_name: None, + gpu_policy: 0, + }; + let resolved = resolve_app_metadata("io.missioncenter.MissionCenter", &raw); + println!("\n=== Mission Center (from Flatpak/App ID fallback) ==="); + println!("App ID: {}", resolved.app_id); + println!("Display Name: {}", resolved.display_name); + println!("Desktop ID: {:?}", resolved.desktop_file_id); + println!("Icon Name: {:?}", resolved.icon_name); + println!("Icon Path: {:?}", resolved.icon_path); + assert_eq!(resolved.display_name, "Mission Center"); + assert!(resolved.icon_path.is_some()); + } + + #[test] + fn test_simulate_blender_lookup() { + let raw = DbusAppMetadata { + display_name: "Blender".to_string(), + desktop_file_id: Some("org.blender.Blender".to_string()), + icon_name: Some("org.blender.Blender".to_string()), + gpu_policy: 0, + }; + let resolved = resolve_app_metadata("blender", &raw); + println!("\n=== Blender (from Daemon metadata) ==="); + println!("App ID: {}", resolved.app_id); + println!("Display Name: {}", resolved.display_name); + println!("Desktop ID: {:?}", resolved.desktop_file_id); + println!("Icon Name: {:?}", resolved.icon_name); + println!("Icon Path: {:?}", resolved.icon_path); + assert_eq!(resolved.display_name, "Blender"); + assert!(resolved.icon_path.is_some()); + } + + #[test] + fn test_simulate_blender_flatpak_app_id_lookup() { + let raw = DbusAppMetadata { + display_name: "".to_string(), + desktop_file_id: None, + icon_name: None, + gpu_policy: 0, + }; + let resolved = resolve_app_metadata("org.blender.Blender", &raw); + println!("\n=== Blender (from Flatpak App ID) ==="); + println!("App ID: {}", resolved.app_id); + println!("Display Name: {}", resolved.display_name); + println!("Desktop ID: {:?}", resolved.desktop_file_id); + println!("Icon Name: {:?}", resolved.icon_name); + println!("Icon Path: {:?}", resolved.icon_path); + assert_eq!(resolved.display_name, "Blender"); + assert!(resolved.icon_path.is_some()); + } +} diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index 6f5f2f97..d2af4acc 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -675,6 +675,17 @@ fn smart_sub() -> Subscription { }) } +pub fn dbus_sub() -> Subscription { + Subscription::batch([ + config_sub(), + mode_sub(), + gpu_sub(), + pci_sub(), + logger_sub(), + smart_sub(), + ]) +} + #[cfg(test)] mod tests { use super::*; @@ -790,14 +801,3 @@ mod tests { sub_handle.abort(); } } - -pub fn dbus_sub() -> Subscription { - Subscription::batch([ - config_sub(), - mode_sub(), - gpu_sub(), - pci_sub(), - logger_sub(), - smart_sub(), - ]) -} From 1bdc11a47d56a2e638074fa376fd310be03a5627 Mon Sep 17 00:00:00 2001 From: luytan Date: Sat, 8 Aug 2026 15:48:49 +0200 Subject: [PATCH 5/5] fix(cardwire-gui): remove test that make the cli hangs --- .../cardwire-gui/src/helpers/app_resolver.rs | 119 ------------------ crates/cardwire-gui/src/subscription.rs | 116 ----------------- 2 files changed, 235 deletions(-) diff --git a/crates/cardwire-gui/src/helpers/app_resolver.rs b/crates/cardwire-gui/src/helpers/app_resolver.rs index b387e8e1..f8a2cfff 100644 --- a/crates/cardwire-gui/src/helpers/app_resolver.rs +++ b/crates/cardwire-gui/src/helpers/app_resolver.rs @@ -200,122 +200,3 @@ fn resolve_icon_path( None } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_simulate_alacritty_lookup() { - let raw = DbusAppMetadata { - display_name: "Alacritty".to_string(), - desktop_file_id: Some("Alacritty".to_string()), - icon_name: Some("Alacritty".to_string()), - gpu_policy: 0, - }; - let resolved = resolve_app_metadata("alacritty", &raw); - println!("\n=== Alacritty (from Daemon metadata) ==="); - println!("App ID: {}", resolved.app_id); - println!("Display Name: {}", resolved.display_name); - println!("Desktop ID: {:?}", resolved.desktop_file_id); - println!("Icon Name: {:?}", resolved.icon_name); - println!("Icon Path: {:?}", resolved.icon_path); - assert_eq!(resolved.display_name, "Alacritty"); - assert!(resolved.icon_path.is_some()); - } - - #[test] - fn test_simulate_alacritty_fallback_lookup() { - let raw = DbusAppMetadata { - display_name: "".to_string(), - desktop_file_id: None, - icon_name: None, - gpu_policy: 0, - }; - let resolved = resolve_app_metadata("alacritty", &raw); - println!("\n=== Alacritty (from binary app_id fallback) ==="); - println!("App ID: {}", resolved.app_id); - println!("Display Name: {}", resolved.display_name); - println!("Desktop ID: {:?}", resolved.desktop_file_id); - println!("Icon Name: {:?}", resolved.icon_name); - println!("Icon Path: {:?}", resolved.icon_path); - assert_eq!(resolved.display_name, "Alacritty"); - assert!(resolved.icon_path.is_some()); - } - - #[test] - fn test_simulate_mission_center_lookup() { - let raw = DbusAppMetadata { - display_name: "Mission Center".to_string(), - desktop_file_id: Some("io.missioncenter.MissionCenter".to_string()), - icon_name: Some("io.missioncenter.MissionCenter".to_string()), - gpu_policy: 0, - }; - let resolved = resolve_app_metadata("missioncenter", &raw); - println!("\n=== Mission Center (from Daemon metadata) ==="); - println!("App ID: {}", resolved.app_id); - println!("Display Name: {}", resolved.display_name); - println!("Desktop ID: {:?}", resolved.desktop_file_id); - println!("Icon Name: {:?}", resolved.icon_name); - println!("Icon Path: {:?}", resolved.icon_path); - assert_eq!(resolved.display_name, "Mission Center"); - assert!(resolved.icon_path.is_some()); - } - - #[test] - fn test_simulate_mission_center_flatpak_or_app_id_lookup() { - let raw = DbusAppMetadata { - display_name: "".to_string(), - desktop_file_id: None, - icon_name: None, - gpu_policy: 0, - }; - let resolved = resolve_app_metadata("io.missioncenter.MissionCenter", &raw); - println!("\n=== Mission Center (from Flatpak/App ID fallback) ==="); - println!("App ID: {}", resolved.app_id); - println!("Display Name: {}", resolved.display_name); - println!("Desktop ID: {:?}", resolved.desktop_file_id); - println!("Icon Name: {:?}", resolved.icon_name); - println!("Icon Path: {:?}", resolved.icon_path); - assert_eq!(resolved.display_name, "Mission Center"); - assert!(resolved.icon_path.is_some()); - } - - #[test] - fn test_simulate_blender_lookup() { - let raw = DbusAppMetadata { - display_name: "Blender".to_string(), - desktop_file_id: Some("org.blender.Blender".to_string()), - icon_name: Some("org.blender.Blender".to_string()), - gpu_policy: 0, - }; - let resolved = resolve_app_metadata("blender", &raw); - println!("\n=== Blender (from Daemon metadata) ==="); - println!("App ID: {}", resolved.app_id); - println!("Display Name: {}", resolved.display_name); - println!("Desktop ID: {:?}", resolved.desktop_file_id); - println!("Icon Name: {:?}", resolved.icon_name); - println!("Icon Path: {:?}", resolved.icon_path); - assert_eq!(resolved.display_name, "Blender"); - assert!(resolved.icon_path.is_some()); - } - - #[test] - fn test_simulate_blender_flatpak_app_id_lookup() { - let raw = DbusAppMetadata { - display_name: "".to_string(), - desktop_file_id: None, - icon_name: None, - gpu_policy: 0, - }; - let resolved = resolve_app_metadata("org.blender.Blender", &raw); - println!("\n=== Blender (from Flatpak App ID) ==="); - println!("App ID: {}", resolved.app_id); - println!("Display Name: {}", resolved.display_name); - println!("Desktop ID: {:?}", resolved.desktop_file_id); - println!("Icon Name: {:?}", resolved.icon_name); - println!("Icon Path: {:?}", resolved.icon_path); - assert_eq!(resolved.display_name, "Blender"); - assert!(resolved.icon_path.is_some()); - } -} diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index d2af4acc..b8965ea3 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -685,119 +685,3 @@ pub fn dbus_sub() -> Subscription { smart_sub(), ]) } - -#[cfg(test)] -mod tests { - use super::*; - use tokio::sync::mpsc; - use zbus::interface; - - struct TestSmartPolicyServer { - snapshot_policies: HashMap, - on_get_policies_called: Option>, - } - - #[interface(name = "org.opengamingcollective.cardwire.SmartPolicy")] - impl TestSmartPolicyServer { - async fn get_app_policies( - &mut self, - ) -> zbus::fdo::Result> { - if let Some(tx) = self.on_get_policies_called.take() { - let _ = tx.send(()).await; - } - Ok(self.snapshot_policies.clone()) - } - - #[zbus(signal)] - async fn new_app_added( - signal_emitter: &zbus::object_server::SignalEmitter<'_>, - new_app: (String, crate::models::DbusAppMetadata), - ) -> zbus::Result<()>; - } - - #[tokio::test] - async fn test_smart_sub_handles_app_persisted_between_stream_registration_and_snapshot() { - let (p0, p1) = tokio::net::UnixStream::pair().expect("failed to create unix stream pair"); - - let (notify_tx, mut notify_rx) = mpsc::channel::<()>(1); - - let server_obj = TestSmartPolicyServer { - snapshot_policies: HashMap::from([( - "app1".to_string(), - crate::models::DbusAppMetadata { - display_name: "App One".to_string(), - desktop_file_id: None, - icon_name: None, - gpu_policy: 0, - }, - )]), - on_get_policies_called: Some(notify_tx), - }; - - let server_conn = zbus::connection::Builder::unix_stream(p0) - .serve_at("/org/opengamingcollective/cardwire", server_obj) - .unwrap() - .build() - .await - .unwrap(); - - let client_conn = zbus::connection::Builder::unix_stream(p1) - .build() - .await - .unwrap(); - - let (mut sender, mut receiver) = iced::futures::channel::mpsc::channel(100); - - let server_conn_clone = server_conn.clone(); - let sub_handle = tokio::spawn(async move { - run_smart_sub(&client_conn, &mut sender).await; - }); - - // Wait until get_app_policies is invoked (which proves stream registration has completed) - notify_rx - .recv() - .await - .expect("get_app_policies was not called"); - - // Emit signal during snapshot handling (simulating app persisted right after stream setup) - let iface_ref = server_conn_clone - .object_server() - .interface::<_, TestSmartPolicyServer>("/org/opengamingcollective/cardwire") - .await - .unwrap(); - - let emitter = iface_ref.signal_emitter().to_owned(); - let new_app_meta = crate::models::DbusAppMetadata { - display_name: "App Two (Interleaved)".to_string(), - desktop_file_id: Some("app2.desktop".to_string()), - icon_name: Some("app2-icon".to_string()), - gpu_policy: 0, - }; - - TestSmartPolicyServer::new_app_added(&emitter, ("app2".to_string(), new_app_meta.clone())) - .await - .unwrap(); - - // Check received messages in order - let first_msg = receiver.next().await.expect("expected snapshot message"); - match first_msg { - Message::FetchedAppPolicies(Ok(policies)) => { - assert!(policies.contains_key("app1")); - assert!(!policies.contains_key("app2")); - } - other => panic!("expected FetchedAppPolicies(Ok), got {:?}", other), - } - - let second_msg = receiver.next().await.expect("expected signal message"); - match second_msg { - Message::NewAppDiscovered((id, meta)) => { - assert_eq!(id, "app2"); - assert_eq!(meta.display_name, "App Two (Interleaved)"); - } - other => panic!("expected NewAppDiscovered, got {:?}", other), - } - - drop(server_conn); - sub_handle.abort(); - } -}