From ac59130b67f11d3fc53a9fdba34df8e4870da551 Mon Sep 17 00:00:00 2001 From: Uzair Nawaz Date: Thu, 20 Aug 2026 18:08:30 -0500 Subject: [PATCH] fix(cardwire-gui): prevent multiple instances and focus the running one on relaunch Co-Authored-By: Claude Sonnet 5 --- crates/cardwire-gui/src/app.rs | 2 + crates/cardwire-gui/src/main.rs | 13 ++++++ crates/cardwire-gui/src/message.rs | 1 + crates/cardwire-gui/src/single_instance.rs | 43 +++++++++++++++++++ crates/cardwire-gui/src/subscription.rs | 50 +++++++++++++++++++++- 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 crates/cardwire-gui/src/single_instance.rs diff --git a/crates/cardwire-gui/src/app.rs b/crates/cardwire-gui/src/app.rs index 2545d107..9a10b1e4 100644 --- a/crates/cardwire-gui/src/app.rs +++ b/crates/cardwire-gui/src/app.rs @@ -273,6 +273,7 @@ impl AppState { } } Message::TrayShutdownComplete => return iced::exit(), + Message::ShowWindow => return self.open_or_focus_window(), Message::WindowClosed(id) => { if self.window_id == Some(id) { self.window_id = None; @@ -536,6 +537,7 @@ impl AppState { Subscription::batch([ crate::subscription::dbus_sub(), crate::subscription::tray_sub(), + crate::subscription::show_window_sub(), window::close_events().map(Message::WindowClosed), ]) } diff --git a/crates/cardwire-gui/src/main.rs b/crates/cardwire-gui/src/main.rs index 80daf4b8..2165490c 100644 --- a/crates/cardwire-gui/src/main.rs +++ b/crates/cardwire-gui/src/main.rs @@ -4,12 +4,14 @@ mod gui_config; mod helpers; mod message; mod models; +mod single_instance; mod subscription; mod tray; mod ui; use app::AppState; use env_logger::Env; +use log::info; fn main() -> iced::Result { env_logger::Builder::from_env(Env::default().default_filter_or("info")) @@ -17,6 +19,17 @@ fn main() -> iced::Result { .format_timestamp(None) .init(); + // Keep alive for the process's lifetime: dropping it releases the D-Bus name and a + // subsequent launch would no longer detect this instance as running. + let _single_instance_guard = match single_instance::acquire() { + single_instance::Acquisition::Acquired(connection) => Some(connection), + single_instance::Acquisition::AlreadyRunning => { + info!("cardwire-gui is already running; exiting"); + return Ok(()); + } + single_instance::Acquisition::Unchecked => None, + }; + unsafe { // Vulkan wakes the dGPU std::env::set_var("WGPU_BACKEND", "gl"); diff --git a/crates/cardwire-gui/src/message.rs b/crates/cardwire-gui/src/message.rs index ede0f41a..77ba2a99 100644 --- a/crates/cardwire-gui/src/message.rs +++ b/crates/cardwire-gui/src/message.rs @@ -20,6 +20,7 @@ pub enum Message { TrayAction(TrayAction), TrayUnavailable(String), TrayShutdownComplete, + ShowWindow, WindowClosed(iced::window::Id), UpdateGpuPowerState(usize, String), UpdateBlockState(usize, bool), diff --git a/crates/cardwire-gui/src/single_instance.rs b/crates/cardwire-gui/src/single_instance.rs new file mode 100644 index 00000000..f83c72ae --- /dev/null +++ b/crates/cardwire-gui/src/single_instance.rs @@ -0,0 +1,43 @@ +use log::warn; +use zbus::{Error, blocking::Connection, fdo::RequestNameFlags}; + +pub(crate) const BUS_NAME: &str = "org.opengamingcollective.cardwire.Gui"; +pub(crate) const OBJECT_PATH: &str = "/org/opengamingcollective/cardwire/Gui"; +/// Signal a later launch broadcasts to ask the running instance to raise its window. +pub(crate) const SHOW_SIGNAL: &str = "Show"; + +pub enum Acquisition { + Acquired(Connection), + AlreadyRunning, + /// Could not be verified (e.g. no session bus available). Callers should fail open rather + /// than block startup on an inconclusive check. + Unchecked, +} + +pub fn acquire() -> Acquisition { + let connection = match Connection::session() { + Ok(connection) => connection, + Err(error) => { + warn!( + "could not connect to session bus to check for another running instance: {error}" + ); + return Acquisition::Unchecked; + } + }; + + match connection.request_name_with_flags(BUS_NAME, RequestNameFlags::DoNotQueue.into()) { + Ok(_) => Acquisition::Acquired(connection), + Err(Error::NameTaken) => { + if let Err(error) = + connection.emit_signal(None::<&str>, OBJECT_PATH, BUS_NAME, SHOW_SIGNAL, &()) + { + warn!("could not ask the running instance to show its window: {error}"); + } + Acquisition::AlreadyRunning + } + Err(error) => { + warn!("could not request D-Bus name to check for another running instance: {error}"); + Acquisition::Unchecked + } + } +} diff --git a/crates/cardwire-gui/src/subscription.rs b/crates/cardwire-gui/src/subscription.rs index 4d346c32..72276859 100644 --- a/crates/cardwire-gui/src/subscription.rs +++ b/crates/cardwire-gui/src/subscription.rs @@ -12,7 +12,7 @@ use crate::{ helpers::CardwireDbus, message::Message, models::{DaemonSettings, LogEntry, Mode, PciDevice}, tray }; use zbus::{ - Connection, Proxy, names::OwnedInterfaceName, proxy, zvariant::{OwnedObjectPath, OwnedValue} + Connection, MatchRule, MessageStream, Proxy, message::Type as MessageType, names::OwnedInterfaceName, proxy, zvariant::{OwnedObjectPath, OwnedValue} }; pub fn tray_sub() -> Subscription { @@ -53,6 +53,54 @@ pub fn tray_sub() -> Subscription { }) } +pub fn show_window_sub() -> Subscription { + Subscription::run_with("cardwire_show_window_subscription", |_id| { + stream::channel(1, |mut output: Sender| async move { + let connection = match Connection::session().await { + Ok(conn) => conn, + Err(error) => { + warn!( + "Failed to connect to D-Bus for show-window requests: {}", + error + ); + return; + } + }; + + let rule = match show_window_match_rule() { + Ok(rule) => rule, + Err(error) => { + warn!("Failed to build show-window match rule: {}", error); + return; + } + }; + + let mut signals = match MessageStream::for_match_rule(rule, &connection, None).await { + Ok(stream) => stream, + Err(error) => { + warn!("Failed to listen for show-window requests: {}", error); + return; + } + }; + + while signals.next().await.is_some() { + if output.send(Message::ShowWindow).await.is_err() { + return; + } + } + }) + }) +} + +fn show_window_match_rule() -> zbus::Result> { + Ok(MatchRule::builder() + .msg_type(MessageType::Signal) + .path(crate::single_instance::OBJECT_PATH)? + .interface(crate::single_instance::BUS_NAME)? + .member(crate::single_instance::SHOW_SIGNAL)? + .build()) +} + // CardwireMode is used to listen to mode change signals #[proxy(