Skip to content

Commit ba6ebce

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

File tree

3 files changed

+145
-653
lines changed

3 files changed

+145
-653
lines changed

src/ElectronNET.API/API/ApiBase.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
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>();
1319

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

0 commit comments

Comments
 (0)