2021-07-12 19:50:39 +02:00
using System ;
2021-08-18 10:24:12 +02:00
using System.Collections.Concurrent ;
using System.Collections.Generic ;
2021-08-23 11:22:37 +02:00
using System.Linq ;
2021-08-18 10:24:12 +02:00
using System.Threading ;
2021-07-12 21:33:35 +02:00
using System.Threading.Tasks ;
2021-08-23 11:22:37 +02:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Serialization ;
2021-07-12 19:50:39 +02:00
using SocketIOClient ;
2021-08-23 11:22:37 +02:00
using SocketIOClient.JsonSerializer ;
2021-07-12 19:50:39 +02:00
using SocketIOClient.Newtonsoft.Json ;
2017-10-14 17:58:16 +02:00
namespace ElectronNET.API
{
internal static class BridgeConnector
{
2021-08-18 10:24:12 +02:00
internal static class EventTasks < T >
{
//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 < string , TaskCompletionSource < T > > _taskCompletionSources = new ( ) ;
private static readonly Dictionary < string , string > _eventKeys = new ( ) ;
private static readonly object _lock = new ( ) ;
/// <summary>
/// Get or add a new TaskCompletionSource<typeparamref name="T"/> for a given event key
/// </summary>
/// <param name="key"></param>
/// <param name="eventKey"></param>
/// <param name="taskCompletionSource"></param>
/// <param name="waitThisFirstAndThenTryAgain"></param>
/// <returns>Returns true if a new TaskCompletionSource<typeparamref name="T"/> was added to the dictionary</returns>
internal static bool TryGetOrAdd ( string key , string eventKey , out TaskCompletionSource < T > 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
}
}
/// <summary>
/// Clean up the TaskCompletionSource<typeparamref name="T"/> from the dictionary if and only if it is the same as the passed argument
/// </summary>
/// <param name="key"></param>
/// <param name="eventKey"></param>
/// <param name="taskCompletionSource"></param>
internal static void DoneWith ( string key , string eventKey , TaskCompletionSource < T > 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 ) ;
}
}
}
}
2021-07-12 19:50:39 +02:00
private static SocketIO _socket ;
2021-08-18 10:24:12 +02:00
2019-05-18 02:00:56 +02:00
private static object _syncRoot = new object ( ) ;
2017-10-14 17:58:16 +02:00
2021-07-12 21:33:35 +02:00
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
2021-07-21 10:42:04 +02:00
Task . Run ( async ( ) = >
{
if ( App . SocketDebug )
{
Console . WriteLine ( $"Sending event {eventString}" ) ;
}
2021-08-18 10:24:12 +02:00
2021-07-21 10:42:04 +02:00
await Socket . EmitAsync ( eventString , args ) ;
if ( App . SocketDebug )
{
Console . WriteLine ( $"Sent event {eventString}" ) ;
}
} ) ;
2021-07-12 21:33:35 +02:00
}
2021-08-20 15:06:58 +02:00
/// <summary>
/// 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
/// </summary>
/// <param name="eventString"></param>
/// <param name="args"></param>
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}" ) ;
}
}
2021-08-18 10:24:12 +02:00
public static void Off ( string eventString )
2021-07-12 19:50:39 +02:00
{
Socket . Off ( eventString ) ;
2021-08-18 10:24:12 +02:00
}
public static void On ( string eventString , Action fn )
{
Socket . On ( eventString , _ = > fn ( ) ) ;
}
public static void On < T > ( string eventString , Action < T > fn )
{
Socket . On ( eventString , ( o ) = > fn ( o . GetValue < T > ( 0 ) ) ) ;
}
public static void Once < T > ( string eventString , Action < T > fn )
{
On < T > ( eventString , ( o ) = >
{
Off ( eventString ) ;
fn ( o ) ;
} ) ;
}
public static async Task < T > OnResult < T > ( 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 < T > . 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 < T > ( triggerEvent , completedEvent , args ) ;
}
else
{
//A new TaskCompletionSource was added, so we need to register the completed event here
On < T > ( completedEvent , ( result ) = >
{
Off ( completedEvent ) ;
taskCompletionSource . SetResult ( result ) ;
EventTasks < T > . DoneWith ( triggerEvent , eventKey , taskCompletionSource ) ;
} ) ;
Emit ( triggerEvent , args ) ;
}
}
return await taskCompletionSource . Task ;
}
2021-07-12 19:50:39 +02:00
2021-08-18 10:24:12 +02:00
public static async Task < T > OnResult < T > ( string triggerEvent , string completedEvent , CancellationToken cancellationToken , params object [ ] args )
{
var taskCompletionSource = new TaskCompletionSource < T > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
using ( cancellationToken . Register ( ( ) = > taskCompletionSource . TrySetCanceled ( ) ) )
{
On < T > ( completedEvent , ( result ) = >
{
Off ( completedEvent ) ;
taskCompletionSource . SetResult ( result ) ;
} ) ;
Emit ( triggerEvent , args ) ;
return await taskCompletionSource . Task . ConfigureAwait ( false ) ;
}
}
2021-07-12 19:50:39 +02:00
private static SocketIO Socket
2017-10-14 17:58:16 +02:00
{
2017-10-23 21:24:05 +02:00
get
2017-10-14 17:58:16 +02:00
{
2021-07-12 19:50:39 +02:00
if ( _socket is null )
2017-10-23 21:24:05 +02:00
{
2021-07-12 19:50:39 +02:00
if ( HybridSupport . IsElectronActive )
2017-10-23 21:24:05 +02:00
{
2021-07-12 19:50:39 +02:00
lock ( _syncRoot )
2017-11-04 00:16:14 +01:00
{
2021-07-12 19:50:39 +02:00
if ( _socket is null & & HybridSupport . IsElectronActive )
2017-11-04 00:16:14 +01:00
{
2021-07-12 19:50:39 +02:00
var socket = new SocketIO ( $"http://localhost:{BridgeSettings.SocketPort}" , new SocketIOOptions ( )
{
EIO = 3
} ) ;
2021-08-23 11:22:37 +02:00
socket . JsonSerializer = new CamelCaseNewtonsoftJsonSerializer ( socket . Options . EIO ) ;
2021-07-12 19:50:39 +02:00
socket . OnConnected + = ( _ , __ ) = >
{
Console . WriteLine ( "BridgeConnector connected!" ) ;
} ;
socket . ConnectAsync ( ) . Wait ( ) ;
_socket = socket ;
}
2017-11-04 00:16:14 +01:00
}
}
2021-07-12 19:50:39 +02:00
else
2017-11-04 00:16:14 +01:00
{
2021-07-12 19:50:39 +02:00
throw new Exception ( "Missing Socket Port" ) ;
2017-11-04 00:16:14 +01:00
}
2017-10-23 21:24:05 +02:00
}
return _socket ;
}
2017-10-14 17:58:16 +02:00
}
2021-08-23 11:22:37 +02:00
private class CamelCaseNewtonsoftJsonSerializer : NewtonsoftJsonSerializer
{
public CamelCaseNewtonsoftJsonSerializer ( int eio ) : base ( eio )
{
}
public override JsonSerializerSettings CreateOptions ( )
{
return new JsonSerializerSettings ( )
{
2021-08-23 11:57:42 +02:00
ContractResolver = new CamelCasePropertyNamesContractResolver ( ) ,
NullValueHandling = NullValueHandling . Ignore ,
DefaultValueHandling = DefaultValueHandling . Ignore
2021-08-23 11:22:37 +02:00
} ;
}
}
2017-10-14 17:58:16 +02:00
}
}