diff --git a/crates/cardwire-cli/src/main.rs b/crates/cardwire-cli/src/main.rs index c9fd3e65..67ac0a1f 100644 --- a/crates/cardwire-cli/src/main.rs +++ b/crates/cardwire-cli/src/main.rs @@ -82,7 +82,10 @@ async fn main() -> anyhow::Result<()> { } } else { let mut map = std::collections::BTreeMap::new(); - let objects = client.get_managed_objects().await.unwrap_or_default(); + let objects = match client.get_managed_objects().await { + Ok(objects) => objects, + Err(e) => handle_error(e.into()), + }; for (path, interfaces) in objects { let path_str = path.as_str(); if let Some(id_str) = @@ -376,7 +379,7 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -fn handle_error(err: zbus::Error) { +fn handle_error(err: zbus::Error) -> ! { match err { zbus::Error::MethodError(name, description, _) => { if let Some(msg) = description { @@ -397,4 +400,5 @@ fn handle_error(err: zbus::Error) { }, _ => eprintln!("{}", err), } + std::process::exit(1) } diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index 27e9ca6a..716f43f4 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -64,6 +64,28 @@ async fn main() -> Result<()> { } }; + // Give the shim its signal emitter so it can notify clients (e.g. gnome) when the GPU list + // changes + if let Some(switcheroo_conn) = _conn.as_ref() { + match switcheroo_conn + .object_server() + .interface::<_, crate::interface::SwitcherooInterface>("/net/hadess/SwitcherooControl") + .await + { + Ok(switcheroo_ref) => { + daemon + .switcheroo_interface + .signal_emitter + .get_or_init(|| switcheroo_ref.signal_emitter().to_owned()); + } + Err(e) => { + log::warn!( + "Failed to get the Switcheroo shim interface ({e}); GPU change notifications will not be emitted" + ); + } + } + } + let object_server: &zbus::ObjectServer = conn.object_server(); spawn_dbus_api(object_server, &mut daemon).await?; // Spawn background tasks diff --git a/crates/cardwire-daemon/src/file/config.rs b/crates/cardwire-daemon/src/file/config.rs index 0b29b176..fec01661 100644 --- a/crates/cardwire-daemon/src/file/config.rs +++ b/crates/cardwire-daemon/src/file/config.rs @@ -4,9 +4,13 @@ use crate::{ file::common::{FileKind, create_default_file}, interface::Modes }; use anyhow::Context; +use log::warn; +use tokio::io::AsyncWriteExt; use serde::{Deserialize, Serialize}; -use std::{fs, io}; +use std::{ + fs, io, time::{SystemTime, UNIX_EPOCH} +}; const CONFIG_PATH: &str = "/etc/cardwire"; #[derive(Deserialize, Serialize, Debug)] @@ -49,31 +53,78 @@ impl CardwireConfig { /// Read TOML config file and return it's settings as a struct pub fn build() -> anyhow::Result { let config_file = format!("{}/cardwire.toml", CONFIG_PATH); - Self::parse_config(&config_file) - } - /// Parse the .toml file into a CardwireConfig - fn parse_config(config_file: &str) -> anyhow::Result { // create the config if it doesnt exist - if !(fs::exists(config_file)?) { + if !(fs::exists(&config_file)?) { Self::create_default_config().context("Could not create default dir for config")?; } + // remove leftover temp files from a save interrupted by a crash + Self::cleanup_stale_tmp_files(); // read the config into a string and parse it let config_content = - fs::read_to_string(config_file).context("Could not read cardwire.toml")?; - toml::from_str(&config_content).context("Failed to parse the toml config") + fs::read_to_string(&config_file).context("Could not read cardwire.toml")?; + Ok(Self::parse_or_default(&config_content)) + } + /// Remove leftover cardwire.toml.*.tmp files from a save interrupted by a crash + fn cleanup_stale_tmp_files() { + let Ok(entries) = fs::read_dir(CONFIG_PATH) else { + return; + }; + for entry in entries.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + if name.starts_with("cardwire.toml.") && name.ends_with(".tmp") { + let _ = fs::remove_file(entry.path()); + } + } + } + /// Parse the .toml content into a CardwireConfig, on parse failure fall back to + /// defaults instead of taking the daemon down, leaving the broken file untouched + fn parse_or_default(config_content: &str) -> CardwireConfig { + match toml::from_str(config_content) { + Ok(config) => config, + Err(e) => { + warn!( + "Failed to parse cardwire.toml ({e}); running with default settings, fix the file and restart the daemon" + ); + CardwireConfig::default() + } + } } /// Create a default cardwire.toml if not present fn create_default_config() -> anyhow::Result<()> { create_default_file(FileKind::Config)?; Ok(()) } - /// Save the config into cardwire.toml + /// Save the config into cardwire.toml, atomically: write to a unique temp file in the same + /// directory (exclusive create so concurrent saves never share a file), fsync, then rename + /// over the target so a crash can't truncate the config pub async fn save_config(&self) -> io::Result<()> { let path = format!("{}/cardwire.toml", CONFIG_PATH); - match toml::to_string_pretty(&self) { - Ok(config_toml) => tokio::fs::write(path, config_toml).await, - Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)), + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(io::Error::other)? + .as_nanos(); + let tmp_path = format!("{}/cardwire.toml.{}.tmp", CONFIG_PATH, unique); + let config_toml = match toml::to_string_pretty(&self) { + Ok(config_toml) => config_toml, + Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), + }; + let result = async { + let mut tmp_file = tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&tmp_path) + .await?; + tmp_file.write_all(config_toml.as_bytes()).await?; + tmp_file.sync_all().await?; + drop(tmp_file); + tokio::fs::rename(&tmp_path, &path).await + } + .await; + if result.is_err() { + let _ = tokio::fs::remove_file(&tmp_path).await; } + result } pub fn experimental_nvidia_block(&self) -> bool { self.experimental_nvidia_block @@ -174,4 +225,25 @@ external_display_auto_switch = true assert_eq!(parsed.battery_auto_switch_mode(), Modes::Smart); assert!(parsed.external_display_auto_switch()); } + + #[test] + fn test_cardwire_config_parse_or_default_on_valid_toml() { + let config = CardwireConfig::parse_or_default( + "auto_apply_gpu_state = false\nbattery_auto_switch_mode = \"smart\"\n", + ); + assert!(!config.auto_apply_gpu_state()); + assert_eq!(config.battery_auto_switch_mode(), Modes::Smart); + assert!(!config.experimental_nvidia_block()); + assert!(!config.external_display_auto_switch()); + } + + #[test] + fn test_cardwire_config_parse_or_default_on_invalid_toml_uses_defaults() { + let config = CardwireConfig::parse_or_default("this is not [[[ valid toml"); + assert!(config.auto_apply_gpu_state()); + assert!(!config.experimental_nvidia_block()); + assert!(!config.battery_auto_switch()); + assert_eq!(config.battery_auto_switch_mode(), Modes::Hybrid); + assert!(!config.external_display_auto_switch()); + } } diff --git a/crates/cardwire-daemon/src/interface/config.rs b/crates/cardwire-daemon/src/interface/config.rs index 2a5baaaf..ff5188a8 100644 --- a/crates/cardwire-daemon/src/interface/config.rs +++ b/crates/cardwire-daemon/src/interface/config.rs @@ -17,6 +17,7 @@ pub struct ConfigMemory { pub battery_auto_switch: Arc, pub battery_auto_switch_mode: Arc, pub external_display_auto_switch: Arc, + save_lock: Arc>, } impl ConfigMemory { /// build a ConfigMemory from CardwireConfig @@ -36,6 +37,7 @@ impl ConfigMemory { battery_auto_switch, battery_auto_switch_mode, external_display_auto_switch, + save_lock: Arc::new(tokio::sync::Mutex::new(())), } } } @@ -111,6 +113,8 @@ impl ConfigInterface { } #[zbus(property)] pub async fn set_battery_auto_switch_mode(&self, mode: u32) -> fdo::Result<()> { + // Validate before storing so an invalid value can't poison the in-memory state + Modes::try_from(mode).map_err(|err| fdo::Error::InvalidArgs(err.to_string()))?; self.config .battery_auto_switch_mode .store(mode, Ordering::Relaxed); @@ -119,6 +123,9 @@ impl ConfigInterface { } /// Save the daemon's configuration to cardwire.toml pub async fn save_to_file(&self) -> fdo::Result<()> { + // Serialize saves so concurrent setters can't interleave writes and the final file + // always reflects the last stored state + let _save_guard = self.config.save_lock.lock().await; // Include monitor-owned settings whenever any D-Bus property rewrites the whole file. let config = CardwireConfig::new( self.config.auto_apply_gpu_state.load(Ordering::Relaxed), diff --git a/crates/cardwire-daemon/src/interface/debug.rs b/crates/cardwire-daemon/src/interface/debug.rs index 1d8dda7b..58f61964 100644 --- a/crates/cardwire-daemon/src/interface/debug.rs +++ b/crates/cardwire-daemon/src/interface/debug.rs @@ -1,7 +1,7 @@ use crate::{ core::{ gpu::GpuEnumerator, pci::{self, DbusPciDevice, PciDevice} - }, tasks::watch_power_state + }, interface::SwitcherooInterface, tasks::watch_power_state }; use cardwire_ebpf_userspace::EbpfBlocker; use log::{info, warn}; @@ -24,6 +24,7 @@ pub struct DebugInterface { pub pci_list: Arc>>, pub object_server: Option, pub power_tasks: Arc>>>>, + pub switcheroo: SwitcherooInterface, } impl DebugInterface { #[allow(clippy::too_many_arguments)] @@ -37,6 +38,7 @@ impl DebugInterface { pci_list: Arc>>, object_server: Option, power_tasks: Arc>>>>, + switcheroo: SwitcherooInterface, ) -> anyhow::Result { Ok(DebugInterface { mode_state, @@ -48,6 +50,7 @@ impl DebugInterface { pci_list, object_server, power_tasks, + switcheroo, }) } } @@ -155,6 +158,7 @@ impl DebugInterface { warn!("failed to re-apply mode on hotplug: {e}"); return Err(e); } + self.switcheroo.emit_gpu_list_changed().await; } Ok(()) diff --git a/crates/cardwire-daemon/src/interface/gpu.rs b/crates/cardwire-daemon/src/interface/gpu.rs index 680bdd50..17d81392 100644 --- a/crates/cardwire-daemon/src/interface/gpu.rs +++ b/crates/cardwire-daemon/src/interface/gpu.rs @@ -188,8 +188,8 @@ impl GpuInterface { // now get fd directory let fd_dir: PathBuf = read_dir(&path) .map_err(|e| fdo::Error::IOError(e.to_string()))? - .filter(|r| r.is_ok()) - .map(|r| r.unwrap().path()) + .flatten() + .map(|r| r.path()) .filter(|r| r.file_name() == Some(OsStr::new("fd"))) .collect(); for entry in read_dir(fd_dir) diff --git a/crates/cardwire-daemon/src/interface/switcheroo.rs b/crates/cardwire-daemon/src/interface/switcheroo.rs index f9779dff..14cfad4b 100644 --- a/crates/cardwire-daemon/src/interface/switcheroo.rs +++ b/crates/cardwire-daemon/src/interface/switcheroo.rs @@ -1,11 +1,11 @@ use std::{ - collections::{BTreeMap, HashMap}, sync::Arc + collections::{BTreeMap, HashMap}, sync::{Arc, OnceLock} }; use log::warn; use tokio::sync::RwLock; use zbus::{ - interface, zvariant::{self, OwnedValue, Value} + interface, object_server::SignalEmitter, zvariant::{self, OwnedValue, Value} }; use crate::{core::gpu::GpuVendor, interface::GpuInterface}; @@ -13,10 +13,118 @@ use crate::{core::gpu::GpuVendor, interface::GpuInterface}; #[derive(Clone)] pub struct SwitcherooInterface { pub gpu_list: Arc>>, + pub signal_emitter: Arc>>, } impl SwitcherooInterface { pub fn build(gpu_list: Arc>>) -> Self { - Self { gpu_list } + Self { + gpu_list, + signal_emitter: Arc::new(OnceLock::new()), + } + } + + /// Emit a PropertiesChanged signal for the three read-only properties, mirroring + /// upstream switcheroo-control's change notification on GPU list updates + pub async fn emit_gpu_list_changed(&self) { + let Some(emitter) = &self.signal_emitter.get() else { + return; + }; + + let (has_dual_gpu, num_gpus, gpus) = { + let gpu_list = self.gpu_list.read().await; + ( + Self::has_dual_gpu_locked(&gpu_list), + Self::num_gpus_locked(&gpu_list), + Self::gpus_locked(&gpu_list), + ) + }; + + let mut changed: HashMap<&str, OwnedValue> = HashMap::new(); + changed.insert("HasDualGpu", OwnedValue::from(has_dual_gpu)); + changed.insert("NumGPUs", OwnedValue::from(num_gpus)); + let gpus_value = match OwnedValue::try_from(Value::from(gpus)) { + Ok(value) => value, + Err(err) => { + warn!("could not build switcheroo GPUs payload: {err}"); + return; + } + }; + changed.insert("GPUs", gpus_value); + + let body = ("net.hadess.SwitcherooControl", changed, Vec::<&str>::new()); + if let Err(err) = emitter + .emit( + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + &body, + ) + .await + { + warn!("failed to emit switcheroo PropertiesChanged: {err}"); + } + } + + /// true if exactly two GPUs are available, see has_dual_gpu() + fn has_dual_gpu_locked(gpu_list: &BTreeMap) -> bool { + Self::num_gpus_locked(gpu_list) == 2 + } + + /// number of available GPUs, see num_gpus() + fn num_gpus_locked(gpu_list: &BTreeMap) -> u32 { + gpu_list + .values() + .filter(|gpu| gpu.device.is_available()) + .count() as u32 + } + + /// Build the GPUs property payload from an already-locked gpu list, see gpus() + fn gpus_locked( + gpu_list: &BTreeMap, + ) -> Vec> { + let mut vec: Vec> = Vec::new(); + let available_gpus: Vec<&GpuInterface> = gpu_list + .values() + .filter(|gpu| gpu.device.is_available()) + .collect(); + let gpu_count = available_gpus.len(); + + for gpu in available_gpus { + let mut dict = HashMap::new(); + + // The name (s) + let name_str = zvariant::Str::from(gpu.device.name()); + dict.insert("Name", OwnedValue::from(name_str)); + // Env Vars + let env_vars = compute_switcheroo_env( + gpu_count, + gpu.device.is_default(), + gpu.device.is_discrete(), + gpu.id, + gpu.device.gpu_vendor(), + gpu.device.pci().pci_address(), + ); + + let env_val = Value::from(env_vars); + match OwnedValue::try_from(env_val) { + Ok(value) => { + dict.insert("Environment", value); + } + Err(err) => { + warn!( + "could not convert switcheroo environment for {}: {err}", + gpu.device.name() + ); + continue; + } + } + // "Default" (b) + dict.insert("Default", OwnedValue::from(gpu.device.is_default())); + // "Discrete" (b) + dict.insert("Discrete", OwnedValue::from(gpu.device.is_discrete())); + + vec.push(dict); + } + vec } } @@ -92,69 +200,19 @@ impl SwitcherooInterface { #[zbus(property, name = "HasDualGpu")] pub async fn has_dual_gpu(&self) -> bool { let gpu_list = self.gpu_list.read().await; - gpu_list - .values() - .filter(|gpu| gpu.device.is_available()) - .count() - .eq(&2) + Self::has_dual_gpu_locked(&gpu_list) } #[zbus(property, name = "NumGPUs")] pub async fn num_gpus(&self) -> u32 { let gpu_list = self.gpu_list.read().await; - gpu_list - .values() - .filter(|gpu| gpu.device.is_available()) - .count() as u32 + Self::num_gpus_locked(&gpu_list) } #[zbus(property, name = "GPUs")] pub async fn gpus(&self) -> Vec> { - let mut vec: Vec> = Vec::new(); let gpu_list = self.gpu_list.read().await; - let available_gpus: Vec<&GpuInterface> = gpu_list - .values() - .filter(|gpu| gpu.device.is_available()) - .collect(); - let gpu_count = available_gpus.len(); - - for gpu in available_gpus { - let mut dict = HashMap::new(); - - // The name (s) - let name_str = zvariant::Str::from(gpu.device.name()); - dict.insert("Name", OwnedValue::from(name_str)); - // Env Vars - let env_vars = compute_switcheroo_env( - gpu_count, - gpu.device.is_default(), - gpu.device.is_discrete(), - gpu.id, - gpu.device.gpu_vendor(), - gpu.device.pci().pci_address(), - ); - - let env_val = Value::from(env_vars); - match OwnedValue::try_from(env_val) { - Ok(value) => { - dict.insert("Environment", value); - } - Err(err) => { - warn!( - "could not convert switcheroo environment for {}: {err}", - gpu.device.name() - ); - continue; - } - } - // "Default" (b) - dict.insert("Default", OwnedValue::from(gpu.device.is_default())); - // "Discrete" (b) - dict.insert("Discrete", OwnedValue::from(gpu.device.is_discrete())); - - vec.push(dict); - } - vec + Self::gpus_locked(&gpu_list) } } diff --git a/crates/cardwire-daemon/src/models.rs b/crates/cardwire-daemon/src/models.rs index b674242c..be080040 100644 --- a/crates/cardwire-daemon/src/models.rs +++ b/crates/cardwire-daemon/src/models.rs @@ -91,6 +91,8 @@ impl DaemonManager { let logger_interface = LoggerInterface::build(); + let switcheroo_interface = SwitcherooInterface::build(Arc::clone(&gpu_interfaces)); + Ok(Self { mode_interface: mode_interface.clone(), gpu_interfaces: Arc::clone(&gpu_interfaces), @@ -108,8 +110,9 @@ impl DaemonManager { Arc::clone(&pci_list), None, Arc::clone(&power_tasks), + switcheroo_interface.clone(), )?, - switcheroo_interface: SwitcherooInterface::build(Arc::clone(&gpu_interfaces)), + switcheroo_interface, logger_interface, logger_signal: None, inner: DaemonInner { diff --git a/crates/cardwire-daemon/src/tasks/monitor_udev.rs b/crates/cardwire-daemon/src/tasks/monitor_udev.rs index 90fce9ae..10ebbf21 100644 --- a/crates/cardwire-daemon/src/tasks/monitor_udev.rs +++ b/crates/cardwire-daemon/src/tasks/monitor_udev.rs @@ -16,8 +16,11 @@ pub async fn monitor_pci_changes(debug_int: DebugInterface) -> zbus::Result<()> && (action == "bind" || action == "unbind") { info!("detected pci event, refreshing GPU interfaces"); - if let Err(e) = debug_int.refresh_gpu().await { - error!("failed to reresh gpu interface: {}", e); + match debug_int.refresh_gpu().await { + Ok(()) => {} + Err(e) => { + error!("failed to refresh gpu interface: {}", e); + } } } } diff --git a/crates/cardwire-ebpf/src/main.rs b/crates/cardwire-ebpf/src/main.rs index 61ace756..08a4ac31 100644 --- a/crates/cardwire-ebpf/src/main.rs +++ b/crates/cardwire-ebpf/src/main.rs @@ -326,11 +326,11 @@ unsafe fn try_tracepoint_enter_getdents64(ctx: TracePointContext) -> Result> 32) as u32; + let tid = bpf_get_current_pid_tgid() as u32; let dirp_ptr: u64 = unsafe { ctx.read_at(DIRP_OFFSET)? }; - CW_DIRENT.insert(pid, dirp_ptr, 0)?; + CW_DIRENT.insert(tid, dirp_ptr, 0)?; ReturnCode::SUCCESS } @@ -344,14 +344,14 @@ pub fn tracepoint_exit_getdents64(ctx: TracePointContext) -> u32 { } unsafe fn try_tracepoint_exit_getdents64(ctx: TracePointContext) -> Result { - let pid = (bpf_get_current_pid_tgid() >> 32) as u32; - let dirent_ptr = match unsafe { CW_DIRENT.get(pid) } { + let tid = bpf_get_current_pid_tgid() as u32; + let dirent_ptr = match unsafe { CW_DIRENT.get(tid) } { Some(ptr) => *ptr as *const linux_dirent64, None => return ReturnCode::SUCCESS, }; // Remove entry immediately to avoid map leak - let _ = CW_DIRENT.remove(pid); + let _ = CW_DIRENT.remove(tid); let retval = match unsafe { ctx.read_at::(16) } { Ok(ret) => ret as u64, diff --git a/crates/cardwire-gui/src/app.rs b/crates/cardwire-gui/src/app.rs index 1342e06e..a93adc35 100644 --- a/crates/cardwire-gui/src/app.rs +++ b/crates/cardwire-gui/src/app.rs @@ -83,7 +83,7 @@ impl AppState { self.error = None; } Err(err) => { - self.gpu_list.clear(); + // Keep the previous gpu list on failure, just surface the error self.error = Some(format!("Error fetching GPUs: {}", err)); } }, diff --git a/crates/cardwire-gui/src/helpers/dbus.rs b/crates/cardwire-gui/src/helpers/dbus.rs index 72e08c21..83a6b679 100644 --- a/crates/cardwire-gui/src/helpers/dbus.rs +++ b/crates/cardwire-gui/src/helpers/dbus.rs @@ -160,7 +160,6 @@ impl CardwireDbus { } DaemonSettings::BattAutoSwitchMode => { if let Some(mode_to_apply) = mode_opt { - println!("setting mode: {:?}", mode_to_apply as u32); proxy .set_property(&setting.to_string(), mode_to_apply as u32) .await diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index 49d5a874..546610eb 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -4,7 +4,7 @@ use iced::{ Subscription, futures::{SinkExt, StreamExt, channel::mpsc::Sender}, stream }; -use log::{error, warn}; +use log::{info, warn}; use tokio::select; use tokio_stream::StreamMap; @@ -163,6 +163,60 @@ fn config_sub() -> Subscription { let mut config_switch_battery = proxy.receive_battery_auto_switch_changed().await; let mut config_switch_battery_mode = proxy.receive_battery_auto_switch_mode_changed().await; + + // Fetch the initial values once so the toggles render the real daemon state on + // startup, before any change signal arrives + match proxy.experimental_nvidia_block().await { + Ok(state) => { + let _ = output + .send(Message::FetchedSetting(Ok(( + DaemonSettings::ExpNvidiaBlock, + Some(state), + None, + )))) + .await; + } + Err(e) => warn!("Failed to fetch experimental_nvidia_block: {}", e), + } + match proxy.auto_apply_gpu_state().await { + Ok(state) => { + let _ = output + .send(Message::FetchedSetting(Ok(( + DaemonSettings::AutoApplyGpuState, + Some(state), + None, + )))) + .await; + } + Err(e) => warn!("Failed to fetch auto_apply_gpu_state: {}", e), + } + match proxy.battery_auto_switch().await { + Ok(state) => { + let _ = output + .send(Message::FetchedSetting(Ok(( + DaemonSettings::BattAutoSwitch, + Some(state), + None, + )))) + .await; + } + Err(e) => warn!("Failed to fetch battery_auto_switch: {}", e), + } + match proxy.battery_auto_switch_mode().await { + Ok(mode) => { + if let Some(mode) = Mode::from_repr(mode) { + let _ = output + .send(Message::FetchedSetting(Ok(( + DaemonSettings::BattAutoSwitchMode, + None, + Some(mode), + )))) + .await; + } + } + Err(e) => warn!("Failed to fetch battery_auto_switch_mode: {}", e), + } + loop { select! { // Exp nvidia block @@ -253,75 +307,55 @@ fn gpu_sub() -> Subscription { } } - let proxy = match CardwireGpuIntProxy::new(&connection).await { + // Listen to the daemon's ObjectManager so GPU hotplug or a daemon-side refresh is + // picked up + let om_proxy = match Proxy::new( + &connection, + "org.opengamingcollective.cardwire", + "/org/opengamingcollective/cardwire", + "org.freedesktop.DBus.ObjectManager", + ) + .await + { Ok(p) => p, Err(e) => { - warn!("Failed to create D-Bus proxy: {}", e); + warn!("Failed to create ObjectManager proxy: {}", e); return; } }; - // mutable so it can be update later if list refresh - #[allow(unused_mut)] - let mut gpu_objects = match proxy.get_managed_objects().await { - Ok(list) => list, + let mut om_added = match om_proxy.receive_signal("InterfacesAdded").await { + Ok(s) => s, Err(e) => { - warn!("Failed to retrieve dbus managed objects (gpu_list): {}", e); + warn!("Failed to subscribe to InterfacesAdded: {}", e); return; } }; - - // Count the number of gpus, if the daemon didn't mess the BTree, having count = 2 means - // there is a gpu at 0 and at 1 - let mut dbus_streams = StreamMap::new(); - let mut dbus_properties = StreamMap::new(); - for (path, _) in gpu_objects { - let path_str = path.as_str(); - if let Some(id_str) = - path_str.strip_prefix("/org/opengamingcollective/cardwire/Gpu/") - && let Ok(id) = id_str.parse::() - { - let path = format!("/org/opengamingcollective/cardwire/Gpu/{}", id); - let gpu_proxy = match Proxy::new( - &connection, - "org.opengamingcollective.cardwire", - path, - "org.opengamingcollective.cardwire.Gpu", - ) - .await - { - Ok(p) => p, - Err(e) => { - error!("Couldn't create gpu {} proxy: {}", id, e); - return; - } - }; - // First time we need to populate the gpu power_state - if let Ok(power_state) = - gpu_proxy.call::<&str, (), String>("PowerState", &()).await - { - let _ = output - .send(Message::UpdateGpuPowerState(id as usize, power_state)) - .await; - } - - let power_signal = match gpu_proxy.receive_signal("PowerStateChanged").await { - Ok(s) => s, - Err(e) => { - error!("Couldn't receive gpu {} power signal: {}", id, e); - return; - } - }; - let stream_name = format!("gpu_power_{}", id); - dbus_streams.insert(stream_name, power_signal); - - let block_signal: proxy::PropertyStream<'_, bool> = - gpu_proxy.receive_property_changed("Block").await; - - let stream_name = format!("gpu_block_{}", id); - dbus_properties.insert(stream_name, block_signal); + let mut om_removed = match om_proxy.receive_signal("InterfacesRemoved").await { + Ok(s) => s, + Err(e) => { + warn!("Failed to subscribe to InterfacesRemoved: {}", e); + return; } + }; + + let mut dbus_streams: StreamMap> = + StreamMap::new(); + let mut dbus_properties: StreamMap> = + StreamMap::new(); + if let Err(e) = build_gpu_streams( + &connection, + &mut output, + &mut dbus_streams, + &mut dbus_properties, + ) + .await + { + warn!("Failed to retrieve dbus managed objects (gpu_list): {}", e); + return; } + loop { + let mut needs_refresh = false; select! { Some(msg) = dbus_streams.next() => { let msg_id = msg.0; @@ -357,12 +391,103 @@ fn gpu_sub() -> Subscription { _ => {} } }, + Some(msg) = om_added.next() => { + if let Ok((path, _)) = msg.body().deserialize::<( + OwnedObjectPath, + HashMap>, + )>() + && path.as_str().starts_with("/org/opengamingcollective/cardwire/Gpu/") + { + needs_refresh = true; + } + }, + Some(msg) = om_removed.next() => { + if let Ok((path, _)) = msg.body().deserialize::<( + OwnedObjectPath, + Vec, + )>() + && path.as_str().starts_with("/org/opengamingcollective/cardwire/Gpu/") + { + needs_refresh = true; + } + }, + } + if needs_refresh { + info!("GPU list changed on the daemon, refreshing"); + // Refetch the gpu list and rebuild the signal streams from the new GPU set + match CardwireDbus::new().get_devices_list().await { + Ok(gpu_list) => { + let _ = output.send(Message::AllDevicesFetched(Ok(gpu_list))).await; + } + Err(error) => { + let _ = output + .send(Message::AllDevicesFetched(Err(error.to_string()))) + .await; + } + } + dbus_streams.clear(); + dbus_properties.clear(); + if let Err(e) = build_gpu_streams( + &connection, + &mut output, + &mut dbus_streams, + &mut dbus_properties, + ) + .await + { + warn!("Failed to rebuild GPU signal streams: {}", e); + } } } }) }) } +/// Fetch the current GPU set from the daemon and (re)build the per-GPU signal streams +/// (PowerStateChanged and Block property changes) +async fn build_gpu_streams( + connection: &Connection, + output: &mut Sender, + dbus_streams: &mut StreamMap>, + dbus_properties: &mut StreamMap>, +) -> zbus::Result<()> { + let proxy = CardwireGpuIntProxy::new(connection).await?; + let gpu_objects = proxy.get_managed_objects().await?; + + for (path, _) in gpu_objects { + let path_str = path.as_str(); + if let Some(id_str) = path_str.strip_prefix("/org/opengamingcollective/cardwire/Gpu/") + && let Ok(id) = id_str.parse::() + { + let path = format!("/org/opengamingcollective/cardwire/Gpu/{}", id); + let gpu_proxy = Proxy::new( + connection, + "org.opengamingcollective.cardwire", + path, + "org.opengamingcollective.cardwire.Gpu", + ) + .await?; + // First time we need to populate the gpu power_state + if let Ok(power_state) = gpu_proxy.call::<&str, (), String>("PowerState", &()).await { + let _ = output + .send(Message::UpdateGpuPowerState(id as usize, power_state)) + .await; + } + + let power_signal = gpu_proxy.receive_signal("PowerStateChanged").await?; + let stream_name = format!("gpu_power_{}", id); + dbus_streams.insert(stream_name, power_signal); + + let block_signal: proxy::PropertyStream<'_, bool> = + gpu_proxy.receive_property_changed("Block").await; + + let stream_name = format!("gpu_block_{}", id); + dbus_properties.insert(stream_name, block_signal); + } + } + Ok(()) +} + #[proxy( default_service = "org.opengamingcollective.cardwire", default_path = "/org/opengamingcollective/cardwire", diff --git a/nix/ci-15gpu.nix b/nix/ci-15gpu.nix index 17560d4d..b0e30bbc 100644 --- a/nix/ci-15gpu.nix +++ b/nix/ci-15gpu.nix @@ -71,12 +71,12 @@ t.assertIn("17", machine.succeed("cardwire list | wc -l"), "Must be 17 (15 GPUs + 2 headers)") with subtest("Try to switch to integrated and hybrid"): - t.assertIn("Couldn't set mode to Integrated, the mode require exactly 2 GPUs", machine.succeed("cardwire set integrated 2>&1"), "Mode has been switched to integrated") - t.assertIn("Couldn't set mode to Hybrid, the mode require exactly 2 GPUs", machine.succeed("cardwire set hybrid 2>&1"), "Mode has been switched to hybrid") + t.assertIn("Couldn't set mode to Integrated, the mode requires exactly 2 GPUs", machine.fail("cardwire set integrated 2>&1"), "Mode has been switched to integrated") + t.assertIn("Mode has been set to Hybrid", machine.succeed("cardwire set hybrid"), "Mode has been switched to hybrid") with subtest("Set to manual, and block 14 gpus"): machine.succeed("cardwire set manual") - t.assertIn("cannot be blocked", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") + t.assertIn("cannot be blocked", machine.fail("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") for x in range(1, 15): machine.succeed(f'cardwire gpu {x} --block') diff --git a/nix/ci-2gpu.nix b/nix/ci-2gpu.nix index 21187139..97836e2b 100644 --- a/nix/ci-2gpu.nix +++ b/nix/ci-2gpu.nix @@ -71,6 +71,6 @@ t.assertIn("hybrid", machine.succeed("cat /var/lib/cardwire/mode.json"), "mode.json didnt get saved") with subtest("Try to block default gpu"): - t.assertIn("Per GPU block is only available on manual mode", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") + t.assertIn("Per GPU block is only available on manual mode", machine.fail("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") ''; } diff --git a/nix/ci-3gpu.nix b/nix/ci-3gpu.nix index a44dc932..bcd025b3 100644 --- a/nix/ci-3gpu.nix +++ b/nix/ci-3gpu.nix @@ -56,14 +56,14 @@ machine.wait_until_succeeds("su - john -c 'cardwire help'") with subtest("Try to switch to integrated and hybrid"): - t.assertIn("Couldn't set mode to Integrated, the mode require exactly 2 GPUs", machine.succeed("cardwire set integrated 2>&1"), "Mode has been switched to integrated") - t.assertIn("Couldn't set mode to Hybrid, the mode require exactly 2 GPUs", machine.succeed("cardwire set hybrid 2>&1"), "Mode has been switched to hybrid") + t.assertIn("Couldn't set mode to Integrated, the mode requires exactly 2 GPUs", machine.fail("cardwire set integrated 2>&1"), "Mode has been switched to integrated") + t.assertIn("Mode has been set to Hybrid", machine.succeed("cardwire set hybrid"), "Mode has been switched to hybrid") with subtest("Set to manual, and block two gpus"): machine.succeed("cardwire set manual") machine.succeed("cardwire gpu 1 --block") machine.succeed("cardwire gpu 2 --block") - t.assertIn("cannot be blocked", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") + t.assertIn("cannot be blocked", machine.fail("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") with subtest("Check gpu_state.json to see if two gpus got blocked"):