From b5a9c87973bcef1ce34b06770b76297e74562f27 Mon Sep 17 00:00:00 2001 From: Damian Suess Date: Mon, 11 May 2026 08:44:51 -0400 Subject: [PATCH 1/3] Rename WaitForPropertyValueInternalAsync to WaitForPropertyValueAsync and make public --- .../Extensions/WatchableExtensions.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Linux.Bluetooth/Extensions/WatchableExtensions.cs b/src/Linux.Bluetooth/Extensions/WatchableExtensions.cs index 8dd2a81..5887802 100644 --- a/src/Linux.Bluetooth/Extensions/WatchableExtensions.cs +++ b/src/Linux.Bluetooth/Extensions/WatchableExtensions.cs @@ -17,7 +17,7 @@ public static class WatchableExtensions /// Task or exception. /// On timeout a is thrown. public static Task WaitForPropertyValueAsync(this IAdapter1 obj, string propertyName, T value, TimeSpan timeout) - => WaitForPropertyValueInternalAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); + => WaitForPropertyValueAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); /// Wait for AdvertisingManager's Property and specified value to resolve. /// Type of value. @@ -28,7 +28,7 @@ public static Task WaitForPropertyValueAsync(this IAdapter1 obj, string prope /// Task or exception. /// On timeout a is thrown. public static Task WaitForPropertyValueAsync(this ILEAdvertisingManager1 obj, string propertyName, T value, TimeSpan timeout) - => WaitForPropertyValueInternalAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); + => WaitForPropertyValueAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); /// Wait for Device's Property and specified value to resolve. /// Type of value. @@ -39,7 +39,7 @@ public static Task WaitForPropertyValueAsync(this ILEAdvertisingManager1 obj, /// Task or exception. /// On timeout a is thrown. public static Task WaitForPropertyValueAsync(this IDevice1 obj, string propertyName, T value, TimeSpan timeout) - => WaitForPropertyValueInternalAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); + => WaitForPropertyValueAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); /// Wait for Battery's Property and specified value to resolve. /// Type of value. @@ -50,7 +50,7 @@ public static Task WaitForPropertyValueAsync(this IDevice1 obj, string proper /// Task or exception. /// On timeout a is thrown. public static Task WaitForPropertyValueAsync(this IBattery1 obj, string propertyName, T value, TimeSpan timeout) - => WaitForPropertyValueInternalAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); + => WaitForPropertyValueAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); /* /// Wait for GattService's Property and specified value to resolve. @@ -62,7 +62,7 @@ public static Task WaitForPropertyValueAsync(this IBattery1 obj, string prope /// Task or exception. /// On timeout a is thrown. public static Task WaitForPropertyValueAsync(this IGattService1 obj, string propertyName, T value, TimeSpan timeout) - => WaitForPropertyValueInternalAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); + => WaitForPropertyValueAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); /// Wait for GattCharacteristic's Property and specified value to resolve. /// Type of value. @@ -73,7 +73,7 @@ public static Task WaitForPropertyValueAsync(this IGattService1 obj, string p /// Task or exception. /// On timeout a is thrown. public static Task WaitForPropertyValueAsync(this IGattCharacteristic1 obj, string propertyName, T value, TimeSpan timeout) - => WaitForPropertyValueInternalAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); + => WaitForPropertyValueAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); /// Wait for GattDescriptor's Property and specified value to resolve. /// Type of value. @@ -84,7 +84,7 @@ public static Task WaitForPropertyValueAsync(this IGattCharacteristic1 obj, s /// Task or exception. /// On timeout a is thrown. public static Task WaitForPropertyValueAsync(this IGattDescriptor1 obj, string propertyName, T value, TimeSpan timeout) - => WaitForPropertyValueInternalAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); + => WaitForPropertyValueAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); */ /// Wait for MediaControl's Property and specified value to resolve. @@ -96,7 +96,7 @@ public static Task WaitForPropertyValueAsync(this IGattDescriptor1 obj, strin /// Task or exception. /// On timeout a is thrown. public static Task WaitForPropertyValueAsync(this IMediaControl1 obj, string propertyName, T value, TimeSpan timeout) - => WaitForPropertyValueInternalAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); + => WaitForPropertyValueAsync(obj.GetAsync, obj.WatchPropertiesAsync, propertyName, value, timeout); /// /// Wait for watchable objects property and specified value to resolve. @@ -110,10 +110,13 @@ public static Task WaitForPropertyValueAsync(this IMediaControl1 obj, string /// TimeSpan to wait for. /// Task or exception. /// On timeout a is thrown. - private static async Task WaitForPropertyValueInternalAsync( + public static async Task WaitForPropertyValueAsync( Func> getAsync, - Func, Task> watchPropertiesAsync, - string propertyName, T value, TimeSpan timeout) + Func, + Task> watchPropertiesAsync, + string propertyName, + T value, + TimeSpan timeout) { // TODO: Change to Task versus throwing an error. var (watchTask, watcher) = WaitForPropertyValueInternal(watchPropertiesAsync, propertyName, value); From 7fdd157625a2ad5d7c9d0f469a2deea7f90af55e Mon Sep 17 00:00:00 2001 From: Valentin Kindschi Date: Wed, 5 Aug 2026 13:20:13 +0200 Subject: [PATCH 2/3] fix(AdvertisementMonitor): Dispose the monitor's own D-Bus connection --- src/Linux.Bluetooth/AdvertisementMonitor.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Linux.Bluetooth/AdvertisementMonitor.cs b/src/Linux.Bluetooth/AdvertisementMonitor.cs index aa9f2c2..201c3db 100644 --- a/src/Linux.Bluetooth/AdvertisementMonitor.cs +++ b/src/Linux.Bluetooth/AdvertisementMonitor.cs @@ -9,7 +9,7 @@ namespace Linux.Bluetooth /// Advertisement Monitor class. /// Requires 'Experimental = true' and 'KernelExperimental = true' in BlueZ main.conf /// - public class AdvertisementMonitor : IAdvertisementMonitor1, IObjectManager + public class AdvertisementMonitor : IAdvertisementMonitor1, IObjectManager, IDisposable { public ObjectPath ObjectPath { get; } @@ -48,6 +48,16 @@ public async Task StopAsync() _conn.UnregisterObject(this); } + /// + /// Closes the D-Bus connection opened by the constructor. Each monitor owns its own connection, so a + /// monitor dropped without disposing leaks its socket until finalization. + /// + public void Dispose() + { + _conn.Dispose(); + GC.SuppressFinalize(this); + } + public Task ActivateAsync() { Console.WriteLine("Advertisement monitoring activated"); From 05e08b2c3a991c138ee70eab9fc88b2e724bb140 Mon Sep 17 00:00:00 2001 From: Valentin Kindschi Date: Thu, 6 Aug 2026 17:48:58 +0200 Subject: [PATCH 3/3] feat(AdvertisementMonitor): Report the loss of the monitor's DBus connection The monitor exports a DBus object, which Tmds.DBus only allows on a manual connection, and a manual connection never reconnects: after a bus restart BlueZ has dropped the monitor and no DeviceFound or DeviceLost is ever raised again. The connection is private, so a consumer had no way to notice. ConnectionLost reports it so the monitor can be disposed and re-created; recovery stays the consumer's decision, as the object cannot be revived in place. --- src/Linux.Bluetooth/AdvertisementMonitor.cs | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Linux.Bluetooth/AdvertisementMonitor.cs b/src/Linux.Bluetooth/AdvertisementMonitor.cs index 201c3db..4db39c2 100644 --- a/src/Linux.Bluetooth/AdvertisementMonitor.cs +++ b/src/Linux.Bluetooth/AdvertisementMonitor.cs @@ -16,6 +16,16 @@ public class AdvertisementMonitor : IAdvertisementMonitor1, IObjectManager, IDis public event EventHandler? DeviceFoundEvent; public event EventHandler? DeviceLostEvent; + /// + /// Raised when the D-Bus connection backing this monitor drops. + /// + /// + /// The connection is created by this monitor and never reconnects, so BlueZ loses the monitor + /// registration for good: no further DeviceFoundEvent or DeviceLostEvent is ever raised. Dispose this + /// monitor and create a new one to resume monitoring. + /// + public event EventHandler? ConnectionLost; + private readonly Connection _conn; private readonly AdvertisementMonitor1Properties _properties; private readonly IAdvertisementMonitorManager1 _manager; @@ -40,20 +50,33 @@ public async Task StartAsync() await _conn.ConnectAsync(); await _conn.RegisterObjectAsync(this); await _manager.RegisterMonitorAsync(ObjectPath); + + _conn.StateChanged += OnConnectionStateChanged; } public async Task StopAsync() { + _conn.StateChanged -= OnConnectionStateChanged; + await _manager.UnregisterMonitorAsync(ObjectPath); _conn.UnregisterObject(this); } + private void OnConnectionStateChanged(object sender, ConnectionStateChangedEventArgs e) + { + if (e.State == ConnectionState.Disconnected) + { + ConnectionLost?.Invoke(this, EventArgs.Empty); + } + } + /// /// Closes the D-Bus connection opened by the constructor. Each monitor owns its own connection, so a /// monitor dropped without disposing leaks its socket until finalization. /// public void Dispose() { + _conn.StateChanged -= OnConnectionStateChanged; _conn.Dispose(); GC.SuppressFinalize(this); }