From e7a5362f06522748bd06ceb00c83df00c6fb3fa6 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 17:57:28 +0200 Subject: [PATCH 01/17] fix(cardwired): atomic save to config and prevent loop if config is wrong --- crates/cardwire-daemon/src/file/config.rs | 68 +++++++++++++++++++---- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/crates/cardwire-daemon/src/file/config.rs b/crates/cardwire-daemon/src/file/config.rs index 0b29b176..9c773581 100644 --- a/crates/cardwire-daemon/src/file/config.rs +++ b/crates/cardwire-daemon/src/file/config.rs @@ -4,6 +4,8 @@ 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}; @@ -49,31 +51,54 @@ 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")?; } // 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)) + } + /// 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 temp file in the same + /// directory, 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 tmp_path = format!("{}/cardwire.toml.tmp", CONFIG_PATH); + 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::File::create(&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 +199,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()); + } } From 237fc411d458db4f1e2c8f90704694cf6108b623 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 18:03:08 +0200 Subject: [PATCH 02/17] fix(cardwire-ebpf): use tid instead of pid for getdents to prevent corruption --- crates/cardwire-ebpf/src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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, From ad32db06b2478c0da269263466cb6a863763ee3f Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 18:18:47 +0200 Subject: [PATCH 03/17] feat(cardwired): implement switcheroo signals --- crates/cardwire-daemon/src/daemon.rs | 12 ++++++ .../src/interface/switcheroo.rs | 40 ++++++++++++++++++- crates/cardwire-daemon/src/models.rs | 3 +- .../cardwire-daemon/src/tasks/monitor_udev.rs | 14 +++++-- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index 27e9ca6a..dfa894ad 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -64,6 +64,18 @@ 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() + && let Ok(switcheroo_ref) = switcheroo_conn + .object_server() + .interface::<_, crate::interface::SwitcherooInterface>("/net/hadess/SwitcherooControl") + .await + { + daemon.switcheroo_interface.signal_emitter = + Some(switcheroo_ref.signal_emitter().to_owned()); + } + 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/interface/switcheroo.rs b/crates/cardwire-daemon/src/interface/switcheroo.rs index f9779dff..67b8e9b0 100644 --- a/crates/cardwire-daemon/src/interface/switcheroo.rs +++ b/crates/cardwire-daemon/src/interface/switcheroo.rs @@ -5,7 +5,7 @@ use std::{ 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,46 @@ use crate::{core::gpu::GpuVendor, interface::GpuInterface}; #[derive(Clone)] pub struct SwitcherooInterface { pub gpu_list: Arc>>, + pub signal_emitter: Option>, } impl SwitcherooInterface { pub fn build(gpu_list: Arc>>) -> Self { - Self { gpu_list } + Self { + gpu_list, + signal_emitter: None, + } + } + + /// 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 else { + return; + }; + + let mut changed: HashMap<&str, OwnedValue> = HashMap::new(); + changed.insert("HasDualGpu", OwnedValue::from(self.has_dual_gpu().await)); + changed.insert("NumGPUs", OwnedValue::from(self.num_gpus().await)); + let gpus_value = match OwnedValue::try_from(Value::from(self.gpus().await)) { + 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}"); + } } } diff --git a/crates/cardwire-daemon/src/models.rs b/crates/cardwire-daemon/src/models.rs index b674242c..53eb0b05 100644 --- a/crates/cardwire-daemon/src/models.rs +++ b/crates/cardwire-daemon/src/models.rs @@ -266,8 +266,9 @@ impl DaemonManager { } pub fn monitor_udev_future(&self) -> impl Future> + 'static { let debug_int = self.debug_interface.clone(); + let switcheroo = self.switcheroo_interface.clone(); async move { - let res = tasks::monitor_pci_changes(debug_int).await; + let res = tasks::monitor_pci_changes(debug_int, switcheroo).await; if let Err(ref e) = res { error!("monitor_udev task failed: {}", e); } diff --git a/crates/cardwire-daemon/src/tasks/monitor_udev.rs b/crates/cardwire-daemon/src/tasks/monitor_udev.rs index 90fce9ae..51d6ff8f 100644 --- a/crates/cardwire-daemon/src/tasks/monitor_udev.rs +++ b/crates/cardwire-daemon/src/tasks/monitor_udev.rs @@ -3,9 +3,12 @@ use log::{error, info}; use tokio::io::{Interest, unix::AsyncFd}; -use crate::interface::DebugInterface; +use crate::interface::{DebugInterface, SwitcherooInterface}; -pub async fn monitor_pci_changes(debug_int: DebugInterface) -> zbus::Result<()> { +pub async fn monitor_pci_changes( + debug_int: DebugInterface, + switcheroo: SwitcherooInterface, +) -> zbus::Result<()> { let udev_monitor = udev::MonitorBuilder::new()?.match_subsystem("pci")?; let udev_fd = AsyncFd::new(udev_monitor.listen()?)?; loop { @@ -16,8 +19,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(()) => switcheroo.emit_gpu_list_changed().await, + Err(e) => { + error!("failed to reresh gpu interface: {}", e); + } } } } From 88a96c1c52a4f7cd070fe6afcf8fc1c2b7501d81 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 18:32:54 +0200 Subject: [PATCH 04/17] fix(cardwired): handle error correctly --- crates/cardwire-cli/src/main.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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) } From 2cd6a18d5325142337bbdf028ede5537ba0b0be0 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 18:37:21 +0200 Subject: [PATCH 05/17] fix(cardwired): valide atuo switch mode config before storing it --- crates/cardwire-daemon/src/interface/config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/cardwire-daemon/src/interface/config.rs b/crates/cardwire-daemon/src/interface/config.rs index 2a5baaaf..810b8923 100644 --- a/crates/cardwire-daemon/src/interface/config.rs +++ b/crates/cardwire-daemon/src/interface/config.rs @@ -111,6 +111,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); From 69002825bda80c84e20e5cb76aeccbf339924096 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 18:53:20 +0200 Subject: [PATCH 06/17] fix(cardwire-gui): listen to GPU object-manager on GPU Refresh --- crates/cardwire-gui/src/subscription.rs | 187 ++++++++++++++++-------- 1 file changed, 129 insertions(+), 58 deletions(-) diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index 49d5a874..f38ceac7 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; @@ -253,75 +253,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 +337,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", From 0187706b76e5530541577d29ad6a6c8817921ce3 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 18:56:30 +0200 Subject: [PATCH 07/17] fix(cardwire-gui): fetch settings at start --- crates/cardwire-gui/src/subscription.rs | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index f38ceac7..546610eb 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -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 From c1b419c665a284d0bf3857b62e28801d91d87138 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 19:04:53 +0200 Subject: [PATCH 08/17] chore(cardwire-gui): remove old println! --- crates/cardwire-gui/src/helpers/dbus.rs | 1 - 1 file changed, 1 deletion(-) 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 From 1e8379e4d5e8fd4d76330708f85438f9748acafa Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 19:16:25 +0200 Subject: [PATCH 09/17] fix(cardwired): replace unwrap with flatten --- crates/cardwire-daemon/src/interface/gpu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From 85488dcbd6784af098272f7acc6d6a12779e118e Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 19:16:51 +0200 Subject: [PATCH 10/17] fix(cardwired): warn on switcheroo interface fail --- crates/cardwire-daemon/src/daemon.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index dfa894ad..ed160741 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -66,14 +66,22 @@ 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() - && let Ok(switcheroo_ref) = switcheroo_conn + if let Some(switcheroo_conn) = _conn.as_ref() { + match switcheroo_conn .object_server() .interface::<_, crate::interface::SwitcherooInterface>("/net/hadess/SwitcherooControl") .await - { - daemon.switcheroo_interface.signal_emitter = - Some(switcheroo_ref.signal_emitter().to_owned()); + { + Ok(switcheroo_ref) => { + daemon.switcheroo_interface.signal_emitter = + Some(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(); From 475dfeb61a48310a9b281979192821b664aa6cb7 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 19:21:20 +0200 Subject: [PATCH 11/17] fix(cardwired): add a save_lock, unique save tmp and auto clean --- crates/cardwire-daemon/src/file/config.rs | 36 ++++++++++++++++--- .../cardwire-daemon/src/interface/config.rs | 5 +++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/crates/cardwire-daemon/src/file/config.rs b/crates/cardwire-daemon/src/file/config.rs index 9c773581..fec01661 100644 --- a/crates/cardwire-daemon/src/file/config.rs +++ b/crates/cardwire-daemon/src/file/config.rs @@ -8,7 +8,9 @@ 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)] @@ -55,11 +57,26 @@ impl CardwireConfig { 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")?; 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 { @@ -78,17 +95,26 @@ impl CardwireConfig { create_default_file(FileKind::Config)?; Ok(()) } - /// Save the config into cardwire.toml, atomically: write to a temp file in the same - /// directory, fsync, then rename over the target so a crash can't truncate the config + /// 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); - let tmp_path = format!("{}/cardwire.toml.tmp", CONFIG_PATH); + 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::File::create(&tmp_path).await?; + 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); diff --git a/crates/cardwire-daemon/src/interface/config.rs b/crates/cardwire-daemon/src/interface/config.rs index 810b8923..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(())), } } } @@ -121,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), From 7059b86ada912c0a40a8e2cd604dc65871303ef9 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 19:23:45 +0200 Subject: [PATCH 12/17] fix(cardwired): use one guard to emit switcheroo update signal --- .../src/interface/switcheroo.rs | 134 ++++++++++-------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/crates/cardwire-daemon/src/interface/switcheroo.rs b/crates/cardwire-daemon/src/interface/switcheroo.rs index 67b8e9b0..9ed7a693 100644 --- a/crates/cardwire-daemon/src/interface/switcheroo.rs +++ b/crates/cardwire-daemon/src/interface/switcheroo.rs @@ -30,10 +30,19 @@ impl SwitcherooInterface { 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(self.has_dual_gpu().await)); - changed.insert("NumGPUs", OwnedValue::from(self.num_gpus().await)); - let gpus_value = match OwnedValue::try_from(Value::from(self.gpus().await)) { + 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}"); @@ -54,6 +63,69 @@ impl SwitcherooInterface { 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 + } } pub fn compute_switcheroo_env( @@ -128,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) } } From 49f720277ec3cad0a794cccf84cf3f77e6f0d0e6 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 19:37:24 +0200 Subject: [PATCH 13/17] fix(cardwired): directly emit signal inside debug refresh_gpu --- crates/cardwire-daemon/src/interface/debug.rs | 6 +++++- crates/cardwire-daemon/src/models.rs | 8 +++++--- crates/cardwire-daemon/src/tasks/monitor_udev.rs | 9 +++------ 3 files changed, 13 insertions(+), 10 deletions(-) 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/models.rs b/crates/cardwire-daemon/src/models.rs index 53eb0b05..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 { @@ -266,9 +269,8 @@ impl DaemonManager { } pub fn monitor_udev_future(&self) -> impl Future> + 'static { let debug_int = self.debug_interface.clone(); - let switcheroo = self.switcheroo_interface.clone(); async move { - let res = tasks::monitor_pci_changes(debug_int, switcheroo).await; + let res = tasks::monitor_pci_changes(debug_int).await; if let Err(ref e) = res { error!("monitor_udev task failed: {}", e); } diff --git a/crates/cardwire-daemon/src/tasks/monitor_udev.rs b/crates/cardwire-daemon/src/tasks/monitor_udev.rs index 51d6ff8f..c80060d7 100644 --- a/crates/cardwire-daemon/src/tasks/monitor_udev.rs +++ b/crates/cardwire-daemon/src/tasks/monitor_udev.rs @@ -3,12 +3,9 @@ use log::{error, info}; use tokio::io::{Interest, unix::AsyncFd}; -use crate::interface::{DebugInterface, SwitcherooInterface}; +use crate::interface::DebugInterface; -pub async fn monitor_pci_changes( - debug_int: DebugInterface, - switcheroo: SwitcherooInterface, -) -> zbus::Result<()> { +pub async fn monitor_pci_changes(debug_int: DebugInterface) -> zbus::Result<()> { let udev_monitor = udev::MonitorBuilder::new()?.match_subsystem("pci")?; let udev_fd = AsyncFd::new(udev_monitor.listen()?)?; loop { @@ -20,7 +17,7 @@ pub async fn monitor_pci_changes( { info!("detected pci event, refreshing GPU interfaces"); match debug_int.refresh_gpu().await { - Ok(()) => switcheroo.emit_gpu_list_changed().await, + Ok(()) => {} Err(e) => { error!("failed to reresh gpu interface: {}", e); } From aa7c93f0fcfd3538d1b9821224055c2fdda427bd Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 19:46:36 +0200 Subject: [PATCH 14/17] chore(cardwired): fix typo in log --- crates/cardwire-daemon/src/tasks/monitor_udev.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/tasks/monitor_udev.rs b/crates/cardwire-daemon/src/tasks/monitor_udev.rs index c80060d7..10ebbf21 100644 --- a/crates/cardwire-daemon/src/tasks/monitor_udev.rs +++ b/crates/cardwire-daemon/src/tasks/monitor_udev.rs @@ -19,7 +19,7 @@ pub async fn monitor_pci_changes(debug_int: DebugInterface) -> zbus::Result<()> match debug_int.refresh_gpu().await { Ok(()) => {} Err(e) => { - error!("failed to reresh gpu interface: {}", e); + error!("failed to refresh gpu interface: {}", e); } } } From f4a60834725fdee921a712e255c8dd8ff570b3a2 Mon Sep 17 00:00:00 2001 From: luytan Date: Thu, 6 Aug 2026 19:47:20 +0200 Subject: [PATCH 15/17] fix(cardwire-gui): keep gpu list on error --- crates/cardwire-gui/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)); } }, From 98cd768a996d305936bdd48ccd18cb42a49ef5a4 Mon Sep 17 00:00:00 2001 From: luytan Date: Fri, 7 Aug 2026 09:41:23 +0200 Subject: [PATCH 16/17] fix(cardwired): turn signal emitter into a OnceLock --- crates/cardwire-daemon/src/daemon.rs | 6 ++++-- crates/cardwire-daemon/src/interface/switcheroo.rs | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index ed160741..716f43f4 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -73,8 +73,10 @@ async fn main() -> Result<()> { .await { Ok(switcheroo_ref) => { - daemon.switcheroo_interface.signal_emitter = - Some(switcheroo_ref.signal_emitter().to_owned()); + daemon + .switcheroo_interface + .signal_emitter + .get_or_init(|| switcheroo_ref.signal_emitter().to_owned()); } Err(e) => { log::warn!( diff --git a/crates/cardwire-daemon/src/interface/switcheroo.rs b/crates/cardwire-daemon/src/interface/switcheroo.rs index 9ed7a693..14cfad4b 100644 --- a/crates/cardwire-daemon/src/interface/switcheroo.rs +++ b/crates/cardwire-daemon/src/interface/switcheroo.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeMap, HashMap}, sync::Arc + collections::{BTreeMap, HashMap}, sync::{Arc, OnceLock} }; use log::warn; @@ -13,20 +13,20 @@ use crate::{core::gpu::GpuVendor, interface::GpuInterface}; #[derive(Clone)] pub struct SwitcherooInterface { pub gpu_list: Arc>>, - pub signal_emitter: Option>, + pub signal_emitter: Arc>>, } impl SwitcherooInterface { pub fn build(gpu_list: Arc>>) -> Self { Self { gpu_list, - signal_emitter: None, + 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 else { + let Some(emitter) = &self.signal_emitter.get() else { return; }; From da75d3390ac65118a488144d80730c2cf8e60b18 Mon Sep 17 00:00:00 2001 From: luytan Date: Fri, 7 Aug 2026 11:14:55 +0200 Subject: [PATCH 17/17] fix: fix nix ci --- nix/ci-15gpu.nix | 6 +++--- nix/ci-2gpu.nix | 2 +- nix/ci-3gpu.nix | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) 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"):