Skip to content

Commit d84256c

Browse files
authored
Add new overloads for GlobalExclusiveDeviceAccess.CommunicateWithDevice (#377)
1 parent c5b0e01 commit d84256c

File tree

6 files changed

+460
-109
lines changed

6 files changed

+460
-109
lines changed

nanoFramework.Tools.DebugLibrary.Shared/NFDevice/GlobalExclusiveDeviceAccess.cs

Lines changed: 243 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)