Skip to content

Commit 2a6d211

Browse files
committed
Mitigate race condition, introduce timeout and simplify code for property gets
1 parent 93f457d commit 2a6d211

File tree

3 files changed

+155
-653
lines changed

3 files changed

+155
-653
lines changed

src/ElectronNET.API/API/ApiBase.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
namespace ElectronNET.API
22
{
3+
using System;
34
using System.Collections.Concurrent;
5+
using System.Diagnostics;
46
using System.Runtime.CompilerServices;
7+
using System.Threading.Tasks;
58
using ElectronNET.Common;
69

710
public abstract class ApiBase
811
{
912
internal const int PropertyTimeout = 1000;
1013

1114
private readonly string objectName;
15+
private readonly ConcurrentDictionary<string, PropertyGetter> propertyGetters = new ConcurrentDictionary<string, PropertyGetter>();
16+
private readonly ConcurrentDictionary<string, string> propertyEventNames = new ConcurrentDictionary<string, string>();
17+
private readonly ConcurrentDictionary<string, string> propertyMessageNames = new ConcurrentDictionary<string, string>();
1218
private readonly ConcurrentDictionary<string, string> methodMessageNames = new ConcurrentDictionary<string, string>();
19+
private readonly object objLock = new object();
1320

1421
public virtual int Id
1522
{
@@ -82,5 +89,89 @@ protected void CallMethod3(object val1, object val2, object val3, [CallerMemberN
8289
BridgeConnector.Socket.Emit(messageName, val1, val2, val3);
8390
}
8491
}
92+
93+
protected Task<T> GetPropertyAsync<T>([CallerMemberName] string callerName = null)
94+
{
95+
Debug.Assert(callerName != null, nameof(callerName) + " != null");
96+
97+
lock (this.objLock)
98+
{
99+
return this.propertyGetters.GetOrAdd(callerName, _ =>
100+
{
101+
var getter = new PropertyGetter<T>(this, callerName, PropertyTimeout);
102+
103+
getter.Task<T>().ContinueWith(_ =>
104+
{
105+
lock (this.objLock)
106+
{
107+
return this.propertyGetters.TryRemove(callerName, out var _);
108+
}
109+
});
110+
111+
return getter;
112+
}).Task<T>();
113+
}
114+
}
115+
116+
internal abstract class PropertyGetter
117+
{
118+
public abstract Task<T> Task<T>();
119+
}
120+
121+
internal class PropertyGetter<T> : PropertyGetter
122+
{
123+
private readonly Task<T> tcsTask;
124+
private TaskCompletionSource<T> tcs;
125+
126+
public PropertyGetter(ApiBase apiBase, string callerName, int timeoutMs)
127+
{
128+
this.tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
129+
this.tcsTask = this.tcs.Task;
130+
131+
var eventName = apiBase.propertyEventNames.GetOrAdd(callerName, s => $"{apiBase.objectName}-{s.StripAsync().LowerFirst()}{apiBase.SocketEventCompleteSuffix}");
132+
var messageName = apiBase.propertyMessageNames.GetOrAdd(callerName, s => apiBase.objectName + s.StripAsync());
133+
134+
BridgeConnector.Socket.On<T>(eventName, (result) =>
135+
{
136+
BridgeConnector.Socket.Off(eventName);
137+
138+
lock (this)
139+
{
140+
this.tcs?.SetResult(result);
141+
this.tcs = null;
142+
}
143+
});
144+
145+
if (apiBase.Id >= 0)
146+
{
147+
BridgeConnector.Socket.Emit(messageName, apiBase.Id);
148+
}
149+
else
150+
{
151+
BridgeConnector.Socket.Emit(messageName);
152+
}
153+
154+
System.Threading.Tasks.Task.Delay(ApiBase.PropertyTimeout).ContinueWith(_ =>
155+
{
156+
if (this.tcs != null)
157+
{
158+
lock (this)
159+
{
160+
if (this.tcs != null)
161+
{
162+
var ex = new TimeoutException($"No response after {timeoutMs:D}ms trying to retrieve value {apiBase.objectName}.{callerName}()");
163+
this.tcs.TrySetException(ex);
164+
this.tcs = null;
165+
}
166+
}
167+
}
168+
});
169+
}
170+
171+
public override Task<T1> Task<T1>()
172+
{
173+
return this.tcsTask as Task<T1>;
174+
}
175+
}
85176
}
86177
}

0 commit comments

Comments
 (0)