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); 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/helpers/app_resolver.rs b/crates/cardwire-gui/src/helpers/app_resolver.rs index d5b4669c..f8a2cfff 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 = [ 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..b8965ea3 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -597,20 +597,80 @@ 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<()>; +} + +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", |_| { - stream::channel(10, |mut output: Sender| async move { - let conn = CardwireDbus::new(); - match conn.get_app_policies().await { - Ok(policies) => { - let _ = output.send(Message::FetchedAppPolicies(Ok(policies))).await; - } - Err(err) => { + 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(err.to_string()))) + .send(Message::FetchedAppPolicies(Err(e.to_string()))) .await; + return; } - } + }; + + run_smart_sub(&connection, &mut output).await; }) }) }