@@ -11,98 +11,294 @@ namespace nanoFramework.Tools.Debugger.NFDevice
1111 /// <summary>
1212 /// Code that wants to access a device should use this system-wide exclusive access while
1313 /// communicating to a device to prevent that another nanoFramework tool also wants to
14- /// communicate with the device.
14+ /// communicate with the device. The public methods return an instance of <see cref="GlobalExclusiveDeviceAccess"/>
15+ /// if the access has been granted; that instance has to be disposed of when exclusive
16+ /// access is no longer needed.
1517 /// </summary>
16- public static class GlobalExclusiveDeviceAccess
18+ public sealed class GlobalExclusiveDeviceAccess : IDisposable
1719 {
1820 #region Fields
21+
1922 /// <summary>
2023 /// Base name for the system-wide mutex that controls access to a device connected to a COM port.
2124 /// </summary>
22- private const string MutexBaseName = "276545121198496AADD346A60F14EF8D_" ;
25+ private const string MutexBaseName = @"276545121198496AADD346A60F14EF8D_" ;
26+ private static readonly Dictionary < string , ( AsyncLocal < GlobalExclusiveDeviceAccess > instance , Semaphore mutex ) > s_locks = [ ] ;
27+ private readonly Semaphore _mutex ;
28+ private int _lockCount ;
29+ private readonly string _portInstanceId ;
30+
2331 #endregion
2432
2533 #region Methods
34+
35+ /// <summary>
36+ /// Get exclusive access to a connected device to communicate with that device.
37+ /// </summary>
38+ /// <param name="device">The connected device.</param>
39+ /// <param name="millisecondsTimeout">Maximum time in milliseconds to wait for exclusive access</param>
40+ /// <param name="cancellationToken">Cancellation token that can be cancelled to stop/abort waiting for the exclusive access.</param>
41+ /// <returns>Returns an instance of <see cref="GlobalExclusiveDeviceAccess"/> if exclusive access has been granted.
42+ /// Returns <c>null</c> if exclusive access cannot be obtained within <paramref name="millisecondsTimeout"/>,
43+ /// or if <paramref name="cancellationToken"/> was cancelled.</returns>
44+ public static GlobalExclusiveDeviceAccess TryGet (
45+ NanoDeviceBase device ,
46+ int millisecondsTimeout = Timeout . Infinite ,
47+ CancellationToken ? cancellationToken = null )
48+ {
49+ return GetOrCreate (
50+ device . ConnectionPort . InstanceId ,
51+ millisecondsTimeout ,
52+ cancellationToken ) ;
53+ }
54+
2655 /// <summary>
27- /// Communicate with a serial device and ensure the code to be executed as exclusive access to the device.
56+ /// Get exclusive access to a device connected to the specified port, to communicate with that device.
57+ /// </summary>
58+ /// <param name="port">The port the device is connected to.</param>
59+ /// <param name="millisecondsTimeout">Maximum time in milliseconds to wait for exclusive access</param>
60+ /// <param name="cancellationToken">Cancellation token that can be cancelled to stop/abort waiting for the exclusive access.</param>
61+ /// <returns>Returns an instance of <see cref="GlobalExclusiveDeviceAccess"/> if exclusive access has been granted.
62+ /// Returns <c>null</c> if exclusive access cannot be obtained within <paramref name="millisecondsTimeout"/>,
63+ /// or if <paramref name="cancellationToken"/> was cancelled.</returns>
64+ public static GlobalExclusiveDeviceAccess TryGet (
65+ IPort port ,
66+ int millisecondsTimeout = Timeout . Infinite ,
67+ CancellationToken ? cancellationToken = null )
68+ {
69+ return GetOrCreate (
70+ port . InstanceId ,
71+ millisecondsTimeout ,
72+ cancellationToken ) ;
73+ }
74+
75+ /// <summary>
76+ /// Get exclusive access to a device connected to the specified serial port, to communicate with that device.
2877 /// </summary>
2978 /// <param name="serialPort">The serial port the device is connected to.</param>
30- /// <param name="communication">Code to execute while having exclusive access to the device</param>
3179 /// <param name="millisecondsTimeout">Maximum time in milliseconds to wait for exclusive access</param>
32- /// <param name="cancellationToken">Cancellation token that can be cancelled to stop/abort running the <paramref name="communication"/>.
33- /// This method does not stop/abort execution of <paramref name="communication"/> after it has been started.</param>
34- /// <returns>Indicates whether the <paramref name="communication"/> has been executed. Returns <c>false</c> if exclusive access
35- /// cannot be obtained within <paramref name="millisecondsTimeout"/>, or if <paramref name="cancellationToken"/> was cancelled
36- /// before the <paramref name="communication"/> has been started.</returns>
37- public static bool CommunicateWithDevice ( string serialPort , Action communication , int millisecondsTimeout = Timeout . Infinite , CancellationToken ? cancellationToken = null )
80+ /// <param name="cancellationToken">Cancellation token that can be cancelled to stop/abort waiting for the exclusive access.</param>
81+ /// <returns>Returns an instance of <see cref="GlobalExclusiveDeviceAccess"/> if exclusive access has been granted.
82+ /// Returns <c>null</c> if exclusive access cannot be obtained within <paramref name="millisecondsTimeout"/>,
83+ /// or if <paramref name="cancellationToken"/> was cancelled.</returns>
84+ public static GlobalExclusiveDeviceAccess TryGet (
85+ string serialPort ,
86+ int millisecondsTimeout = Timeout . Infinite ,
87+ CancellationToken ? cancellationToken = null )
3888 {
39- return DoCommunicateWithDevice ( serialPort , communication , millisecondsTimeout , cancellationToken ) ;
89+ return GetOrCreate (
90+ serialPort ,
91+ millisecondsTimeout ,
92+ cancellationToken ) ;
4093 }
4194
4295 /// <summary>
43- /// Communicate with a device accessible via the network and ensure the code to be executed as exclusive access to the device.
96+ /// Get exclusive access to a device at the specified network address, to communicate with that device.
4497 /// </summary>
4598 /// <param name="address">The network address the device is connected to.</param>
46- /// <param name="communication">Code to execute while having exclusive access to the device</param>
4799 /// <param name="millisecondsTimeout">Maximum time in milliseconds to wait for exclusive access</param>
48- /// <param name="cancellationToken">Cancellation token that can be cancelled to stop/abort running the <paramref name="communication"/>.
49- /// This method does not stop/abort execution of <paramref name="communication"/> after it has been started.</param>
50- /// <returns>Indicates whether the <paramref name="communication"/> has been executed. Returns <c>false</c> if exclusive access
51- /// cannot be obtained within <paramref name="millisecondsTimeout"/>, or if <paramref name="cancellationToken"/> was cancelled
52- /// before the <paramref name="communication"/> has been started.</returns>
53- public static bool CommunicateWithDevice ( NetworkDeviceInformation address , Action communication , int millisecondsTimeout = Timeout . Infinite , CancellationToken ? cancellationToken = null )
100+ /// <param name="cancellationToken">Cancellation token that can be cancelled to stop/abort waiting for the exclusive access.</param>
101+ /// <returns>Returns an instance of <see cref="GlobalExclusiveDeviceAccess"/> if exclusive access has been granted.
102+ /// Returns <c>null</c> if exclusive access cannot be obtained within <paramref name="millisecondsTimeout"/>,
103+ /// or if <paramref name="cancellationToken"/> was cancelled.</returns>
104+ public static GlobalExclusiveDeviceAccess TryGet (
105+ NetworkDeviceInformation address ,
106+ int millisecondsTimeout = Timeout . Infinite ,
107+ CancellationToken ? cancellationToken = null )
54108 {
55- return DoCommunicateWithDevice ( $ "{ address . Host } :{ address . Port } ", communication , millisecondsTimeout , cancellationToken ) ;
109+ return GetOrCreate (
110+ $ "{ address . Host } :{ address . Port } ",
111+ millisecondsTimeout ,
112+ cancellationToken ) ;
56113 }
114+
57115 #endregion
58116
59117 #region Implementation
60- private static bool DoCommunicateWithDevice ( string connectionKey , Action communication , int millisecondsTimeout , CancellationToken ? cancellationToken )
118+
119+ private static GlobalExclusiveDeviceAccess GetOrCreate (
120+ string portInstanceId ,
121+ int millisecondsTimeout ,
122+ CancellationToken ? cancellationToken )
61123 {
62- for ( bool retry = true ; retry ; )
124+ if ( cancellationToken ? . IsCancellationRequested == true )
63125 {
64- retry = false ;
126+ return null ;
127+ }
65128
66- var waitHandles = new List < WaitHandle > ( ) ;
67- var mutex = new Mutex ( false , $ " { MutexBaseName } { connectionKey } " ) ;
68- waitHandles . Add ( mutex ) ;
129+ // If the access lock has been created earlier (in previous statements) and has not yet been disposed,
130+ // use that lock.
131+ GlobalExclusiveDeviceAccess result = null ;
69132
70- CancellationTokenSource timeOutToken = null ;
71- if ( millisecondsTimeout > 0 && millisecondsTimeout != Timeout . Infinite )
133+ lock ( s_locks )
134+ {
135+ if ( s_locks . TryGetValue (
136+ portInstanceId ,
137+ out ( AsyncLocal < GlobalExclusiveDeviceAccess > , Semaphore ) instance ) )
72138 {
73- timeOutToken = new CancellationTokenSource ( millisecondsTimeout ) ;
74- waitHandles . Add ( timeOutToken . Token . WaitHandle ) ;
139+ // Note that the result can still be null, in case the exclusive access was obtained by
140+ // code that is not a statement previously executed in the context of the current async-thread.
141+ result = instance . Item1 ? . Value ;
75142 }
143+ }
76144
77- if ( cancellationToken . HasValue )
145+ if ( result is not null )
146+ {
147+ // Use the same lock as in Dispose
148+ lock ( result . _mutex )
78149 {
79- waitHandles . Add ( cancellationToken . Value . WaitHandle ) ;
150+ if ( result . _lockCount == 0 )
151+ {
152+ // It should already have been disposed of. Fix that for good measure.
153+ lock ( s_locks )
154+ {
155+ s_locks . Remove ( portInstanceId ) ;
156+ }
157+
158+ result = null ;
159+ }
160+ else
161+ {
162+ result . _lockCount ++ ;
163+ return result ;
164+ }
80165 }
166+ }
167+
168+ CancellationTokenSource timeOutToken = null ;
169+
170+ if ( millisecondsTimeout != Timeout . Infinite )
171+ {
172+ timeOutToken = new CancellationTokenSource ( millisecondsTimeout ) ;
173+ }
81174
82- try
175+ try
176+ {
177+ while ( result is null )
83178 {
84- if ( WaitHandle . WaitAny ( waitHandles . ToArray ( ) ) == 0 )
179+ // Cannot use Mutex as the Mutex must be released on the same thread as it is created.
180+ // That may not be the case if this is used in async code.
181+ var mutex = new Semaphore (
182+ 0 ,
183+ 1 ,
184+ $ "{ MutexBaseName } { portInstanceId } ",
185+ out bool createdNew ) ;
186+
187+ if ( createdNew )
85188 {
86- communication ( ) ;
87- return true ;
189+ // This code has created the semaphore, so it has exclusive access
190+ result = new GlobalExclusiveDeviceAccess ( portInstanceId , mutex ) ;
191+ }
192+ else
193+ {
194+ // Wait for the semaphore created elsewhere
195+ var waitHandles = new List < WaitHandle > ( )
196+ {
197+ // Mutex must be added first
198+ mutex
199+ } ;
200+
201+ // The problem with a semaphore it that, while waiting, it does not detect if the application
202+ // with exclusive access is aborted without releasing the semaphore. The semaphore
203+ // has to be re-created for that. If the other application just releases the semaphore,
204+ // the wait is ended.
205+ var iterationToken = new CancellationTokenSource ( 1000 ) ;
206+ waitHandles . Add ( iterationToken . Token . WaitHandle ) ;
207+
208+ // Add the other tokens as well
209+ if ( timeOutToken is not null )
210+ {
211+ waitHandles . Add ( timeOutToken . Token . WaitHandle ) ;
212+ }
213+
214+ if ( cancellationToken . HasValue )
215+ {
216+ waitHandles . Add ( cancellationToken . Value . WaitHandle ) ;
217+ }
218+
219+ try
220+ {
221+ // Try to get exclusive access to the device.
222+ int handleIndex = WaitHandle . WaitAny ( [ .. waitHandles ] ) ;
223+
224+ if ( handleIndex == 0 )
225+ {
226+ // Exclusive access granted as the wait ended because of the mutex
227+ result = new GlobalExclusiveDeviceAccess ( portInstanceId , mutex ) ;
228+ }
229+ else if ( handleIndex != 1 )
230+ {
231+ // timeOutToken or cancellationToken are cancelled
232+ break ;
233+ }
234+ }
235+ finally
236+ {
237+ iterationToken . Dispose ( ) ;
238+
239+ if ( result is null )
240+ {
241+ mutex . Dispose ( ) ;
242+ }
243+ }
88244 }
89245 }
90- catch ( AbandonedMutexException )
246+ }
247+ finally
248+ {
249+ timeOutToken ? . Dispose ( ) ;
250+ }
251+
252+ return result ;
253+ }
254+
255+ private GlobalExclusiveDeviceAccess (
256+ string portInstanceId ,
257+ Semaphore mutex )
258+ {
259+ _mutex = mutex ;
260+ _lockCount = 1 ;
261+ _portInstanceId = portInstanceId ;
262+
263+ var instance = new AsyncLocal < GlobalExclusiveDeviceAccess >
264+ {
265+ Value = this
266+ } ;
267+
268+ lock ( s_locks )
269+ {
270+ s_locks [ portInstanceId ] = ( instance , mutex ) ;
271+ }
272+ }
273+
274+ /// <summary>
275+ /// Dispose of the exclusive access.
276+ /// </summary>
277+ public void Dispose ( )
278+ {
279+ bool removeFromLocks = false ;
280+
281+ lock ( _mutex )
282+ {
283+ _lockCount -- ;
284+
285+ if ( _lockCount == 0 )
91286 {
92- // While this process is waiting on a mutex, the process that owned the mutex has been terminated
93- // without properly releasing the mutex.
94- // Try again, if this is the only remaining process it will re-create the mutex and get exclusive access.
95- retry = true ;
287+ _mutex . Release ( ) ;
288+ _mutex . Dispose ( ) ;
289+ removeFromLocks = true ;
96290 }
97- finally
291+ }
292+
293+ if ( removeFromLocks )
294+ {
295+ lock ( s_locks )
98296 {
99- mutex . ReleaseMutex ( ) ;
100- timeOutToken ? . Dispose ( ) ;
297+ s_locks . Remove ( _portInstanceId ) ;
101298 }
102299 }
103-
104- return false ;
105300 }
301+
106302 #endregion
107303 }
108304}
0 commit comments