1- using System . Diagnostics ;
21using System . Reactive . Disposables ;
32using Darp . Ble . Data ;
43using Darp . Ble . Exceptions ;
54using Darp . Ble . Gap ;
5+ using Darp . Ble . Utils ;
66using Microsoft . Extensions . Logging ;
77#if ! NET9_0_OR_GREATER
88using Lock = System . Object ;
@@ -22,13 +22,14 @@ internal enum ObserverState
2222/// <summary> The ble observer </summary>
2323/// <param name="device"> The ble device </param>
2424/// <param name="logger"> The logger </param>
25- public abstract class BleObserver ( BleDevice device , ILogger < BleObserver > logger ) : IAsyncDisposable , IBleObserver
25+ public abstract class BleObserver ( BleDevice device , ILogger < BleObserver > logger ) : IBleObserver , IAsyncDisposable
2626{
2727 private readonly BleDevice _bleDevice = device ;
28- private readonly List < Action < IGapAdvertisement > > _actions = [ ] ;
29- private readonly Lock _lock = new ( ) ;
30- private readonly SemaphoreSlim _observationStartSemaphore = new ( 1 , 1 ) ;
31- private ObserverState _observerState = ObserverState . Stopped ;
28+ private readonly SemaphoreSlim _startStopSemaphore = new ( 1 , 1 ) ;
29+ private readonly Lock _handlersLock = new ( ) ;
30+ private Action < IGapAdvertisement > [ ] _handlers = [ ] ;
31+
32+ private volatile ObserverState _observerState = ObserverState . Stopped ;
3233
3334 /// <summary> The logger </summary>
3435 protected ILogger < BleObserver > Logger { get ; } = logger ;
@@ -54,28 +55,40 @@ public abstract class BleObserver(BleDevice device, ILogger<BleObserver> logger)
5455 /// <inheritdoc />
5556 public bool Configure ( BleObservationParameters parameters )
5657 {
57- if ( _observerState is not ObserverState . Stopped )
58+ ObjectDisposedException . ThrowIf ( _bleDevice . IsDisposing , nameof ( BleObserver ) ) ;
59+
60+ if ( ! _startStopSemaphore . Wait ( 0 ) )
5861 return false ;
59- Parameters = parameters ;
60- return true ;
62+ try
63+ {
64+ if ( _observerState is not ObserverState . Stopped )
65+ return false ;
66+ Parameters = parameters ;
67+ return true ;
68+ }
69+ finally
70+ {
71+ _startStopSemaphore . Release ( ) ;
72+ }
6173 }
6274
6375 /// <inheritdoc />
6476 public async Task StartObservingAsync ( CancellationToken cancellationToken = default )
6577 {
6678 ObjectDisposedException . ThrowIf ( _bleDevice . IsDisposing , nameof ( BleObserver ) ) ;
6779
68- await _observationStartSemaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
80+ await _startStopSemaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
6981 try
7082 {
7183 if ( _observerState is ObserverState . Observing )
7284 return ;
85+ if ( _observerState is not ObserverState . Stopped )
86+ throw new InvalidOperationException ( $ "Observer is in invalid state { _observerState } ") ;
7387
74- Debug . Assert ( _observerState == ObserverState . Stopped ) ;
7588 _observerState = ObserverState . Starting ;
7689 await StartObservingAsyncCore ( cancellationToken ) . ConfigureAwait ( false ) ;
7790 _observerState = ObserverState . Observing ;
78- Logger . LogTrace ( "Started advertising observation" ) ;
91+ Logger . LogObserverStarted ( ) ;
7992 }
8093 catch ( Exception e ) when ( e is not BleObservationStartException )
8194 {
@@ -85,28 +98,34 @@ public async Task StartObservingAsync(CancellationToken cancellationToken = defa
8598 }
8699 finally
87100 {
88- _observationStartSemaphore . Release ( ) ;
101+ _startStopSemaphore . Release ( ) ;
89102 }
90103 }
91104
92105 /// <inheritdoc />
93- public IDisposable OnAdvertisement < T > ( T state , Action < T , IGapAdvertisement > onAdvertisement )
106+ public IDisposable OnAdvertisement ( Action < IGapAdvertisement > onAdvertisement )
94107 {
95108 ObjectDisposedException . ThrowIf ( _bleDevice . IsDisposing , nameof ( BleObserver ) ) ;
96- Action < IGapAdvertisement > action = advertisement => onAdvertisement ( state , advertisement ) ;
97- lock ( _lock )
109+
110+ // Extend handlers list
111+ lock ( _handlersLock )
98112 {
99- _actions . Add ( action ) ;
113+ Action < IGapAdvertisement > [ ] oldHandlers = _handlers ;
114+ var newArr = new Action < IGapAdvertisement > [ oldHandlers . Length + 1 ] ;
115+ Array . Copy ( oldHandlers , newArr , oldHandlers . Length ) ;
116+ newArr [ ^ 1 ] = onAdvertisement ;
117+ Volatile . Write ( ref _handlers , newArr ) ;
100118 }
101119
102120 return Disposable . Create (
103- ( this , action ) ,
121+ ( this , onAdvertisement ) ,
104122 static tuple =>
105123 {
106- ( BleObserver bleObserver , Action < IGapAdvertisement > action ) = tuple ;
107- lock ( bleObserver . _lock )
124+ ( BleObserver self , Action < IGapAdvertisement > handler ) = tuple ;
125+ lock ( self . _handlersLock )
108126 {
109- bleObserver . _actions . Remove ( action ) ;
127+ if ( Helpers . TryRemoveImmutable ( self . _handlers , handler , out var newHandlers ) )
128+ Volatile . Write ( ref self . _handlers , newHandlers ) ;
110129 }
111130 }
112131 ) ;
@@ -116,19 +135,25 @@ public IDisposable OnAdvertisement<T>(T state, Action<T, IGapAdvertisement> onAd
116135 /// <param name="advertisement"> The advertisement </param>
117136 protected void OnNext ( IGapAdvertisement advertisement )
118137 {
119- lock ( _lock )
138+ // Try to suppress receival of advertisements after stopping/disposal
139+ // Best-effort only. No thread safety guarantees
140+ if ( _bleDevice . IsDisposing || _observerState is ObserverState . Stopping or ObserverState . Stopped )
141+ return ;
142+
143+ // Taking the current snapshot of the handlers.
144+ // In case of an unsubscription of a handler we might have taken the reference here already and call it afterward.
145+ // This is a known tradeoff
146+ Action < IGapAdvertisement > [ ] handlers = Volatile . Read ( ref _handlers ) ;
147+ foreach ( Action < IGapAdvertisement > handler in handlers )
120148 {
121- for ( int i = _actions . Count - 1 ; i >= 0 ; i -- )
149+ try
122150 {
123- try
124- {
125- var onAdvertisement = _actions [ i ] ;
126- onAdvertisement ( advertisement ) ;
127- }
128- catch ( Exception e )
129- {
130- Logger . LogWarning ( e , "Exception while handling advertisement event: {Message}" , e . Message ) ;
131- }
151+ handler ( advertisement ) ;
152+ }
153+ catch ( Exception e )
154+ {
155+ // An exception inside the handler should not crash all observers. Logging and ignoring ...
156+ Logger . LogObservationErrorDuringAdvertisementHandling ( e ) ;
132157 }
133158 }
134159 }
@@ -141,24 +166,37 @@ protected void OnNext(IGapAdvertisement advertisement)
141166 /// <inheritdoc />
142167 public async Task StopObservingAsync ( )
143168 {
144- await _observationStartSemaphore . WaitAsync ( ) . ConfigureAwait ( false ) ;
169+ await _startStopSemaphore . WaitAsync ( ) . ConfigureAwait ( false ) ;
145170 try
146171 {
147172 // Return early if
148173 // Stopped -> Nothing to do
149174 // Stopping -> Some recursive call has lead to us being here
150175 if ( _observerState is ObserverState . Stopping or ObserverState . Stopped )
151176 return ;
177+ if ( _observerState is not ObserverState . Observing )
178+ throw new InvalidOperationException ( $ "Observer is in invalid state { _observerState } ") ;
152179
153- Debug . Assert ( _observerState == ObserverState . Observing ) ;
154180 _observerState = ObserverState . Stopping ;
155- await StopObservingAsyncCore ( ) . ConfigureAwait ( false ) ;
156- Logger . LogTrace ( "Stopped advertising observation" ) ;
181+
182+ try
183+ {
184+ await StopObservingAsyncCore ( ) . ConfigureAwait ( false ) ;
185+ }
186+ catch ( Exception e )
187+ {
188+ Logger . LogObserverErrorDuringStopping ( e ) ;
189+ // In case of an error when stopping we assume we are still observing
190+ // Not ideal, but better than to wait for ever
191+ _observerState = ObserverState . Observing ;
192+ throw ;
193+ }
194+ Logger . LogObserverStopped ( ) ;
157195 _observerState = ObserverState . Stopped ;
158196 }
159197 finally
160198 {
161- _observationStartSemaphore . Release ( ) ;
199+ _startStopSemaphore . Release ( ) ;
162200 }
163201 }
164202
@@ -171,11 +209,11 @@ public async ValueTask DisposeAsync()
171209 {
172210 GC . SuppressFinalize ( this ) ;
173211 await StopObservingAsync ( ) . ConfigureAwait ( false ) ;
174- lock ( _lock )
212+ lock ( _handlersLock )
175213 {
176- _actions . Clear ( ) ;
214+ _handlers = [ ] ;
177215 }
178- _observationStartSemaphore . Dispose ( ) ;
216+ _startStopSemaphore . Dispose ( ) ;
179217 await DisposeAsyncCore ( ) . ConfigureAwait ( false ) ;
180218 }
181219
0 commit comments