From be495282b8b671a4a832cafbe2dbd76db6c1c8d8 Mon Sep 17 00:00:00 2001 From: Raphii Date: Sat, 19 Sep 2026 22:54:26 +0200 Subject: [PATCH] fix(winrtble): release radio state handlers on final adapter drop Keep radio event registration ownership shared across adapter clones. Use a weak manager reference in the callback and unregister when the last adapter drops. Add Windows hardware regression tests for handler release, adapter clones, and scan cleanup. --- src/winrtble/adapter.rs | 124 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 11 deletions(-) diff --git a/src/winrtble/adapter.rs b/src/winrtble/adapter.rs index 022c57ee..36d8a2a4 100644 --- a/src/winrtble/adapter.rs +++ b/src/winrtble/adapter.rs @@ -45,9 +45,22 @@ pub struct Adapter { manager: Arc>, radio: Radio, bluetooth_adapter: BluetoothAdapter, + _state_handler: Option>, +} + +struct RadioStateHandler { + radio: Radio, + token: i64, +} + +impl Drop for RadioStateHandler { + fn drop(&mut self) { + if let Err(err) = self.radio.RemoveStateChanged(self.token) { + log::warn!("Failed to remove Bluetooth radio state handler: {err}"); + } + } } -// https://github.com/microsoft/windows-rs/blob/master/crates/libs/windows/src/Windows/Devices/Radios/mod.rs fn winrt_error(error: E) -> Error { Error::Other(format!("{error:?}").into()) } @@ -73,22 +86,31 @@ impl Adapter { let watcher = Arc::new(Mutex::new(BLEWatcher::new(coded_phy_supported)?)); let manager = Arc::new(AdapterManager::default()); - let radio_clone = radio.clone(); - let manager_clone = manager.clone(); - let handler = TypedEventHandler::new(move |_sender, _args| { - let state = get_central_state(&radio_clone); - manager_clone.emit(CentralEvent::StateUpdate(state)); - Ok(()) - }); - if let Err(err) = radio.StateChanged(&handler) { - eprintln!("radio.StateChanged error: {}", err); - } + let manager_weak = Arc::downgrade(&manager); + let handler = + TypedEventHandler::::new(move |sender, _args| { + if let Some(manager) = manager_weak.upgrade() { + manager.emit(CentralEvent::StateUpdate(get_central_state(sender.ok()?))); + } + Ok(()) + }); + let state_handler = match radio.StateChanged(&handler) { + Ok(token) => Some(Arc::new(RadioStateHandler { + radio: radio.clone(), + token, + })), + Err(err) => { + log::warn!("Failed to register Bluetooth radio state handler: {err}"); + None + } + }; Ok(Adapter { watcher, manager, radio, bluetooth_adapter, + _state_handler: state_handler, }) } } @@ -335,3 +357,83 @@ impl Central for Adapter { Ok(get_central_state(&self.radio)) } } + +#[cfg(test)] +mod cleanup_tests { + use super::*; + use crate::api::Manager as _; + + #[tokio::test] + #[ignore = "requires a Windows Bluetooth radio"] + async fn dropping_last_radio_handler_unregisters_callback() { + let manager = crate::platform::Manager::new().await.unwrap(); + let adapter = manager + .adapters() + .await + .unwrap() + .into_iter() + .next() + .unwrap(); + let owner = Arc::new(()); + let captured = owner.clone(); + let handler = TypedEventHandler::new(move |_sender, _args| { + let _ = &captured; + Ok(()) + }); + let token = adapter.radio.StateChanged(&handler).unwrap(); + let registration = Arc::new(RadioStateHandler { + radio: adapter.radio.clone(), + token, + }); + drop(handler); + let clone = registration.clone(); + drop(registration); + assert_eq!(Arc::strong_count(&owner), 2); + drop(clone); + assert_eq!(Arc::strong_count(&owner), 1); + } + + #[tokio::test] + #[ignore = "requires a Windows Bluetooth radio"] + async fn dropping_last_adapter_releases_manager() { + let manager = crate::platform::Manager::new().await.unwrap(); + let adapter = manager + .adapters() + .await + .unwrap() + .into_iter() + .next() + .unwrap(); + let weak = Arc::downgrade(&adapter.manager); + let clone = adapter.clone(); + let events = adapter.events().await.unwrap(); + drop(adapter); + assert!(weak.upgrade().is_some()); + drop(clone); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + assert!(weak.upgrade().is_none()); + drop(events); + } + + #[tokio::test] + #[ignore = "requires a Windows Bluetooth radio"] + async fn stopped_scan_releases_manager_after_adapter_drop() { + let manager = crate::platform::Manager::new().await.unwrap(); + for _ in 0..8 { + let adapter = manager + .adapters() + .await + .unwrap() + .into_iter() + .next() + .unwrap(); + let weak = Arc::downgrade(&adapter.manager); + adapter.start_scan(ScanFilter::default()).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + adapter.stop_scan().await.unwrap(); + drop(adapter); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + assert!(weak.upgrade().is_none()); + } + } +}