Skip to content

Commit 6f5fb16

Browse files
Merge pull request #595 from theolivenbaum/switch-to-new-socket-lib
Switch to async socket lib
2 parents ab162d4 + 891870a commit 6f5fb16

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1847
-2826
lines changed

ElectronNET.API/App.cs

Lines changed: 119 additions & 509 deletions
Large diffs are not rendered by default.

ElectronNET.API/AutoUpdater.cs

Lines changed: 87 additions & 236 deletions
Large diffs are not rendered by default.

ElectronNET.API/BridgeConnector.cs

Lines changed: 292 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,318 @@
1-
using Quobject.SocketIoClientDotNet.Client;
2-
using System;
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Newtonsoft.Json;
8+
using Newtonsoft.Json.Serialization;
9+
using SocketIOClient;
10+
using SocketIOClient.JsonSerializer;
11+
using SocketIOClient.Newtonsoft.Json;
312

413
namespace ElectronNET.API
514
{
615
internal static class BridgeConnector
716
{
8-
private static Socket _socket;
17+
internal static class EventTasks<T>
18+
{
19+
//Although SocketIO already manage event handlers, we need to manage this here as well for the OnResult calls,
20+
//because SocketIO will simply replace the existing event handler on every call to On(key, ...) , which means there is
21+
//a race condition between On / Off calls that can lead to tasks deadlocking forever without ever triggering their On handler
22+
23+
private static readonly Dictionary<string, TaskCompletionSource<T>> _taskCompletionSources = new();
24+
private static readonly Dictionary<string, string> _eventKeys = new();
25+
private static readonly object _lock = new();
26+
27+
/// <summary>
28+
/// Get or add a new TaskCompletionSource<typeparamref name="T"/> for a given event key
29+
/// </summary>
30+
/// <param name="key"></param>
31+
/// <param name="eventKey"></param>
32+
/// <param name="taskCompletionSource"></param>
33+
/// <param name="waitThisFirstAndThenTryAgain"></param>
34+
/// <returns>Returns true if a new TaskCompletionSource<typeparamref name="T"/> was added to the dictionary</returns>
35+
internal static bool TryGetOrAdd(string key, string eventKey, out TaskCompletionSource<T> taskCompletionSource, out Task waitThisFirstAndThenTryAgain)
36+
{
37+
lock (_lock)
38+
{
39+
if (!_taskCompletionSources.TryGetValue(key, out taskCompletionSource))
40+
{
41+
taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
42+
_taskCompletionSources[key] = taskCompletionSource;
43+
_eventKeys[key] = eventKey;
44+
waitThisFirstAndThenTryAgain = null;
45+
return true; //Was added, so we need to also register the socket events
46+
}
47+
48+
if(_eventKeys.TryGetValue(key, out var existingEventKey) && existingEventKey == eventKey)
49+
{
50+
waitThisFirstAndThenTryAgain = null;
51+
return false; //No need to register the socket events twice
52+
}
53+
54+
waitThisFirstAndThenTryAgain = taskCompletionSource.Task; //Will need to try again after the previous existing one is done
55+
56+
taskCompletionSource = null;
57+
58+
return true; //Need to register the socket events, but must first await the previous task to complete
59+
}
60+
}
61+
62+
/// <summary>
63+
/// Clean up the TaskCompletionSource<typeparamref name="T"/> from the dictionary if and only if it is the same as the passed argument
64+
/// </summary>
65+
/// <param name="key"></param>
66+
/// <param name="eventKey"></param>
67+
/// <param name="taskCompletionSource"></param>
68+
internal static void DoneWith(string key, string eventKey, TaskCompletionSource<T> taskCompletionSource)
69+
{
70+
lock (_lock)
71+
{
72+
if (_taskCompletionSources.TryGetValue(key, out var existingTaskCompletionSource)
73+
&& ReferenceEquals(existingTaskCompletionSource, taskCompletionSource))
74+
{
75+
_taskCompletionSources.Remove(key);
76+
}
77+
78+
if (_eventKeys.TryGetValue(key, out var existingEventKey) && existingEventKey == eventKey)
79+
{
80+
_eventKeys.Remove(key);
81+
}
82+
}
83+
}
84+
}
85+
86+
private static SocketIO _socket;
87+
988
private static object _syncRoot = new object();
1089

11-
public static Socket Socket
90+
public static void Emit(string eventString, params object[] args)
1291
{
13-
get
92+
//We don't care about waiting for the event to be emitted, so this doesn't need to be async
93+
94+
Task.Run(async () =>
95+
{
96+
if (App.SocketDebug)
97+
{
98+
Console.WriteLine($"Sending event {eventString}");
99+
}
100+
101+
await Socket.EmitAsync(eventString, args);
102+
103+
if (App.SocketDebug)
104+
{
105+
Console.WriteLine($"Sent event {eventString}");
106+
}
107+
});
108+
}
109+
110+
/// <summary>
111+
/// This method is only used on places where we need to be sure the event was sent on the socket, such as Quit, Exit, Relaunch and QuitAndInstall methods
112+
/// </summary>
113+
/// <param name="eventString"></param>
114+
/// <param name="args"></param>
115+
internal static void EmitSync(string eventString, params object[] args)
116+
{
117+
if (App.SocketDebug)
118+
{
119+
Console.WriteLine($"Sending event {eventString}");
120+
}
121+
122+
Socket.EmitAsync(eventString, args).Wait();
123+
124+
if (App.SocketDebug)
125+
{
126+
Console.WriteLine($"Sent event {eventString}");
127+
}
128+
}
129+
130+
public static void Off(string eventString)
131+
{
132+
Socket.Off(eventString);
133+
}
134+
135+
public static void On(string eventString, Action fn)
136+
{
137+
Socket.On(eventString, _ => fn());
138+
}
139+
140+
public static void On<T>(string eventString, Action<T> fn)
141+
{
142+
Socket.On(eventString, (o) => fn(o.GetValue<T>(0)));
143+
}
144+
145+
public static void Once<T>(string eventString, Action<T> fn)
146+
{
147+
On<T>(eventString, (o) =>
148+
{
149+
Off(eventString);
150+
fn(o);
151+
});
152+
}
153+
154+
public static async Task<T> OnResult<T>(string triggerEvent, string completedEvent, params object[] args)
155+
{
156+
string eventKey = completedEvent;
157+
158+
if (args is object && args.Length > 0) // If there are arguments passed, we generate a unique event key with the arguments
159+
// this allow us to wait for previous events first before registering new ones
160+
{
161+
var hash = new HashCode();
162+
foreach(var obj in args)
163+
{
164+
hash.Add(obj);
165+
}
166+
eventKey = $"{eventKey}-{(uint)hash.ToHashCode()}";
167+
}
168+
169+
if (EventTasks<T>.TryGetOrAdd(completedEvent, eventKey, out var taskCompletionSource, out var waitThisFirstAndThenTryAgain))
170+
{
171+
if (waitThisFirstAndThenTryAgain is object)
172+
{
173+
//There was a pending call with different parameters, so we need to wait that first and then call here again
174+
try
175+
{
176+
await waitThisFirstAndThenTryAgain;
177+
}
178+
catch
179+
{
180+
//Ignore any exceptions here so we can set a new event below
181+
//The exception will also be visible to the original first caller due to taskCompletionSource.Task
182+
}
183+
184+
//Try again to set the event
185+
return await OnResult<T>(triggerEvent, completedEvent, args);
186+
}
187+
else
188+
{
189+
//A new TaskCompletionSource was added, so we need to register the completed event here
190+
191+
On<T>(completedEvent, (result) =>
192+
{
193+
Off(completedEvent);
194+
taskCompletionSource.SetResult(result);
195+
EventTasks<T>.DoneWith(completedEvent, eventKey, taskCompletionSource);
196+
});
197+
198+
Emit(triggerEvent, args);
199+
}
200+
}
201+
202+
return await taskCompletionSource.Task;
203+
}
204+
205+
206+
public static async Task<T> OnResult<T>(string triggerEvent, string completedEvent, CancellationToken cancellationToken, params object[] args)
207+
{
208+
string eventKey = completedEvent;
209+
210+
if (args is object && args.Length > 0) // If there are arguments passed, we generate a unique event key with the arguments
211+
// this allow us to wait for previous events first before registering new ones
14212
{
15-
if(_socket == null && HybridSupport.IsElectronActive)
213+
var hash = new HashCode();
214+
foreach (var obj in args)
215+
{
216+
hash.Add(obj);
217+
}
218+
eventKey = $"{eventKey}-{(uint)hash.ToHashCode()}";
219+
}
220+
221+
if (EventTasks<T>.TryGetOrAdd(completedEvent, eventKey, out var taskCompletionSource, out var waitThisFirstAndThenTryAgain))
222+
{
223+
if (waitThisFirstAndThenTryAgain is object)
224+
{
225+
//There was a pending call with different parameters, so we need to wait that first and then call here again
226+
try
227+
{
228+
await Task.Run(() => waitThisFirstAndThenTryAgain, cancellationToken);
229+
}
230+
catch
231+
{
232+
//Ignore any exceptions here so we can set a new event below
233+
//The exception will also be visible to the original first caller due to taskCompletionSource.Task
234+
}
235+
236+
//Try again to set the event
237+
return await OnResult<T>(triggerEvent, completedEvent, cancellationToken, args);
238+
}
239+
else
16240
{
17-
lock (_syncRoot)
241+
using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled()))
18242
{
19-
if (_socket == null && HybridSupport.IsElectronActive)
243+
//A new TaskCompletionSource was added, so we need to register the completed event here
244+
245+
On<T>(completedEvent, (result) =>
20246
{
21-
_socket = IO.Socket("http://localhost:" + BridgeSettings.SocketPort);
22-
_socket.On(Socket.EVENT_CONNECT, () =>
23-
{
24-
Console.WriteLine("BridgeConnector connected!");
25-
});
26-
}
247+
Off(completedEvent);
248+
taskCompletionSource.SetResult(result);
249+
EventTasks<T>.DoneWith(completedEvent, eventKey, taskCompletionSource);
250+
});
251+
252+
Emit(triggerEvent, args);
27253
}
28254
}
29-
else if(_socket == null && !HybridSupport.IsElectronActive)
255+
}
256+
257+
return await taskCompletionSource.Task;
258+
}
259+
private static SocketIO Socket
260+
{
261+
get
262+
{
263+
if (_socket is null)
30264
{
31-
lock (_syncRoot)
265+
if (HybridSupport.IsElectronActive)
32266
{
33-
if (_socket == null && !HybridSupport.IsElectronActive)
267+
268+
lock (_syncRoot)
34269
{
35-
_socket = IO.Socket(new Uri("http://localhost"), new IO.Options { AutoConnect = false });
270+
if (_socket is null && HybridSupport.IsElectronActive)
271+
{
272+
var socket = new SocketIO($"http://localhost:{BridgeSettings.SocketPort}", new SocketIOOptions()
273+
{
274+
EIO = 3
275+
});
276+
277+
socket.JsonSerializer = new CamelCaseNewtonsoftJsonSerializer(socket.Options.EIO);
278+
279+
280+
socket.OnConnected += (_, __) =>
281+
{
282+
Console.WriteLine("BridgeConnector connected!");
283+
};
284+
285+
socket.ConnectAsync().Wait();
286+
287+
_socket = socket;
288+
}
36289
}
37290
}
291+
else
292+
{
293+
throw new Exception("Missing Socket Port");
294+
}
38295
}
39296

40297
return _socket;
41298
}
42299
}
300+
301+
private class CamelCaseNewtonsoftJsonSerializer : NewtonsoftJsonSerializer
302+
{
303+
public CamelCaseNewtonsoftJsonSerializer(int eio) : base(eio)
304+
{
305+
}
306+
307+
public override JsonSerializerSettings CreateOptions()
308+
{
309+
return new JsonSerializerSettings()
310+
{
311+
ContractResolver = new CamelCasePropertyNamesContractResolver(),
312+
NullValueHandling = NullValueHandling.Ignore,
313+
DefaultValueHandling = DefaultValueHandling.Ignore
314+
};
315+
}
316+
}
43317
}
44318
}

ElectronNET.API/BridgeSettings.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,16 @@ public static class BridgeSettings
2020
/// The web port.
2121
/// </value>
2222
public static string WebPort { get; internal set; }
23+
24+
/// <summary>
25+
/// Manually set the port values instead of using the UseElectron extension method
26+
/// </summary>
27+
/// <param name="socketPort"></param>
28+
/// <param name="webPort"></param>
29+
public static void InitializePorts(int socketPort, int webPort)
30+
{
31+
SocketPort = socketPort.ToString();
32+
WebPort = webPort.ToString();
33+
}
2334
}
2435
}

0 commit comments

Comments
 (0)