using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using SocketIOClient; using SocketIOClient.Newtonsoft.Json; namespace ElectronNET.API { internal static class BridgeConnector { internal static class EventTasks { //Although SocketIO already manage event handlers, we need to manage this here as well for the OnResult calls, //because SocketIO will simply replace the existing event handler on every call to On(key, ...) , which means there is //a race condition between On / Off calls that can lead to tasks deadlocking forever without ever triggering their On handler private static readonly Dictionary> _taskCompletionSources = new(); private static readonly Dictionary _eventKeys = new(); private static readonly object _lock = new(); /// /// Get or add a new TaskCompletionSource for a given event key /// /// /// /// /// /// Returns true if a new TaskCompletionSource was added to the dictionary internal static bool TryGetOrAdd(string key, string eventKey, out TaskCompletionSource taskCompletionSource, out Task waitThisFirstAndThenTryAgain) { lock (_lock) { if (!_taskCompletionSources.TryGetValue(key, out taskCompletionSource)) { taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); _taskCompletionSources[key] = taskCompletionSource; _eventKeys[key] = eventKey; waitThisFirstAndThenTryAgain = null; return true; //Was added, so we need to also register the socket events } if(_eventKeys.TryGetValue(key, out var existingEventKey) && existingEventKey == eventKey) { waitThisFirstAndThenTryAgain = null; return false; //No need to register the socket events twice } waitThisFirstAndThenTryAgain = taskCompletionSource.Task; //Will need to try again after the previous existing one is done taskCompletionSource = null; return true; //Need to register the socket events, but must first await the previous task to complete } } /// /// Clean up the TaskCompletionSource from the dictionary if and only if it is the same as the passed argument /// /// /// /// internal static void DoneWith(string key, string eventKey, TaskCompletionSource taskCompletionSource) { lock (_lock) { if (_taskCompletionSources.TryGetValue(key, out var existingTaskCompletionSource) && ReferenceEquals(existingTaskCompletionSource, taskCompletionSource)) { _taskCompletionSources.Remove(key); } if (_eventKeys.TryGetValue(key, out var existingEventKey) && existingEventKey == eventKey) { _eventKeys.Remove(key); } } } } private static SocketIO _socket; private static object _syncRoot = new object(); public static void Emit(string eventString, params object[] args) { //We don't care about waiting for the event to be emitted, so this doesn't need to be async Task.Run(async () => { await Task.Yield(); if (App.SocketDebug) { Console.WriteLine($"Sending event {eventString}"); } await Socket.EmitAsync(eventString, args); if (App.SocketDebug) { Console.WriteLine($"Sent event {eventString}"); } }); } /// /// 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 /// /// /// internal static void EmitSync(string eventString, params object[] args) { if (App.SocketDebug) { Console.WriteLine($"Sending event {eventString}"); } Socket.EmitAsync(eventString, args).Wait(); if (App.SocketDebug) { Console.WriteLine($"Sent event {eventString}"); } } public static void Off(string eventString) { Socket.Off(eventString); } public static void On(string eventString, Action fn) { Socket.On(eventString, _ => fn()); } public static void On(string eventString, Action fn) { Socket.On(eventString, (o) => fn(o.GetValue(0))); } public static void Once(string eventString, Action fn) { On(eventString, (o) => { Off(eventString); fn(o); }); } public static async Task OnResult(string triggerEvent, string completedEvent, params object[] args) { string eventKey = triggerEvent; if (args is object && args.Length > 0) // If there are arguments passed, we generate a unique event key with the arguments // this allow us to wait for previous events first before registering new ones { var hash = new HashCode(); foreach(var obj in args) { hash.Add(obj); } eventKey = $"{eventKey}-{(uint)hash.ToHashCode()}"; } if (EventTasks.TryGetOrAdd(triggerEvent, eventKey, out var taskCompletionSource, out var waitThisFirstAndThenTryAgain)) { if (waitThisFirstAndThenTryAgain is object) { //There was a pending call with different parameters, so we need to wait that first and then call here again try { await waitThisFirstAndThenTryAgain; } catch { //Ignore any exceptions here so we can set a new event below //The exception will also be visible to the original first caller due to taskCompletionSource.Task } //Try again to set the event return await OnResult(triggerEvent, completedEvent, args); } else { //A new TaskCompletionSource was added, so we need to register the completed event here On(completedEvent, (result) => { Off(completedEvent); taskCompletionSource.SetResult(result); EventTasks.DoneWith(triggerEvent, eventKey, taskCompletionSource); }); Emit(triggerEvent, args); } } return await taskCompletionSource.Task; } public static async Task OnResult(string triggerEvent, string completedEvent, CancellationToken cancellationToken, params object[] args) { var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled())) { On(completedEvent, (result) => { Off(completedEvent); taskCompletionSource.SetResult(result); }); Emit(triggerEvent, args); return await taskCompletionSource.Task.ConfigureAwait(false); } } private static SocketIO Socket { get { if (_socket is null) { if (HybridSupport.IsElectronActive) { lock (_syncRoot) { if (_socket is null && HybridSupport.IsElectronActive) { var socket = new SocketIO($"http://localhost:{BridgeSettings.SocketPort}", new SocketIOOptions() { EIO = 3 }); socket.JsonSerializer = new NewtonsoftJsonSerializer(socket.Options.EIO); socket.OnConnected += (_, __) => { Console.WriteLine("BridgeConnector connected!"); }; socket.ConnectAsync().Wait(); _socket = socket; } } } else { throw new Exception("Missing Socket Port"); } } return _socket; } } } }