Skip to content

Switch to async socket lib #595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
916eecf
Merge branch 'bug/578'
theolivenbaum Jun 28, 2021
d188baa
Merge remote-tracking branch 'MiniguyBrendan/master'
theolivenbaum Jun 28, 2021
1a6d50d
Set up CI with Azure Pipelines
theolivenbaum Jun 28, 2021
f08711e
Fix variables
theolivenbaum Jun 28, 2021
94b5faa
add publish to nuget
theolivenbaum Jun 28, 2021
468a4fa
fix package name
theolivenbaum Jun 28, 2021
505a5b2
fix NPM install
theolivenbaum Jun 28, 2021
bcbe762
Add option to set electron version
theolivenbaum Jun 28, 2021
3b7b592
fix name on resource path
theolivenbaum Jun 28, 2021
2f3d2c0
print all resource names
theolivenbaum Jun 28, 2021
09cb7a6
Update EmbeddedFileHelper.cs
theolivenbaum Jun 28, 2021
d8ba9d9
change assembly name
theolivenbaum Jun 28, 2021
f785734
fix strange issue with capitalization of assembly name
theolivenbaum Jun 28, 2021
8f5a785
use new Electron 12 default values
theolivenbaum Jun 28, 2021
6dee083
Merge remote-tracking branch 'upstream/master'
theolivenbaum Jul 12, 2021
83fd5a5
Add manual initialization methods
theolivenbaum Jul 12, 2021
f07f2e9
Update build-nuget.yaml
theolivenbaum Jul 12, 2021
82755e3
Add comments
theolivenbaum Jul 12, 2021
f200013
add custom property handle for electronize start command
theolivenbaum Jul 12, 2021
8880e04
remove remaining .Result calls
theolivenbaum Jul 12, 2021
126d39f
wip
theolivenbaum Jul 12, 2021
95d6147
works
theolivenbaum Jul 12, 2021
e621449
Merge pull request #2 from theolivenbaum/remove-deprecated-socket-lib
theolivenbaum Jul 12, 2021
b03bc7c
Clean-up
theolivenbaum Jul 12, 2021
ec2261f
Remove wrong ReleaseNoteInfo model
theolivenbaum Jul 13, 2021
9107b16
Add prototype from-build-output command
theolivenbaum Jul 13, 2021
c4ff481
add debug option
theolivenbaum Jul 21, 2021
7fe8f6e
fix wrong object initializer
theolivenbaum Jul 21, 2021
237638a
add command for simple build
theolivenbaum Aug 9, 2021
d6b29f7
avoid calling npm install if nothing changed
theolivenbaum Aug 9, 2021
8063f49
run all continuations asyncronously
theolivenbaum Aug 17, 2021
cbe0637
Simplify repeated code with OnResult method, fix race condition with …
theolivenbaum Aug 18, 2021
bd08938
Delete build-nuget.yaml
theolivenbaum Aug 18, 2021
336c3b9
cleanup for merge
theolivenbaum Aug 18, 2021
b708b4b
reset default values
theolivenbaum Aug 18, 2021
b6338ac
log error messages from asp-net process
theolivenbaum Aug 19, 2021
e8e3649
Merge branch 'master' into switch-to-new-socket-lib
theolivenbaum Aug 19, 2021
b42eba8
Handle non-zero exit codes from ASP.NET process
theolivenbaum Aug 20, 2021
d9a7411
Merge branch 'master' into switch-to-new-socket-lib
theolivenbaum Aug 20, 2021
133dcd6
Fix exit method
theolivenbaum Aug 20, 2021
8f71480
Merge branch 'master' into switch-to-new-socket-lib
theolivenbaum Aug 20, 2021
67b59d9
fix quotes
theolivenbaum Aug 20, 2021
9daaebc
Add timeout property for splashscreen
theolivenbaum Aug 20, 2021
a8229f2
add internal method to emit socket events synchronously, and use it f…
theolivenbaum Aug 20, 2021
0aab352
remove yield call
theolivenbaum Aug 20, 2021
ca9bee7
Merge branch 'master' into switch-to-new-socket-lib
theolivenbaum Aug 20, 2021
444c09f
Update main.js
theolivenbaum Aug 21, 2021
da9d965
Update main.js
theolivenbaum Aug 21, 2021
87d97ef
Merge branch 'master' into switch-to-new-socket-lib
theolivenbaum Aug 21, 2021
bf48bc8
replace remaining cases with OnResult, fix deserialization of json ob…
theolivenbaum Aug 23, 2021
31094f9
Remove unecessary JsonSerializer settings, fix default serializer
theolivenbaum Aug 23, 2021
8143159
Update ElectronNET.API.csproj
theolivenbaum Aug 23, 2021
0090a7c
fix wrong completed event name
theolivenbaum Aug 23, 2021
f9693ce
handle destroyed windows
theolivenbaum Aug 23, 2021
9a680a4
improve how we handle when window has been destroyed already
theolivenbaum Aug 23, 2021
fdbf718
Update browserWindows.js
theolivenbaum Aug 23, 2021
fb99dbd
add null return to avoid having to check for undefined
theolivenbaum Aug 23, 2021
953f1e2
fix event names for when there are more than one window, view or webc…
theolivenbaum Aug 24, 2021
0393b0b
Merge branch 'master' into switch-to-new-socket-lib
theolivenbaum Aug 24, 2021
ac77643
add variable to control kill behaviour
theolivenbaum Aug 25, 2021
94efaf8
Merge branch 'master' of https://github.com/theolivenbaum/Electron.NET
theolivenbaum Aug 25, 2021
bad5946
pass electron process id to child
theolivenbaum Aug 25, 2021
67c592a
kill child process when electronize cli is killed
theolivenbaum Aug 25, 2021
e39e342
Update ProcessHelper.cs
theolivenbaum Aug 25, 2021
8bbe6a9
Fix serialization of array of objects
theolivenbaum Aug 25, 2021
144a0a0
change argument name, detach process on creation
theolivenbaum Aug 25, 2021
8b03a6b
remove stdio handling for detached case
theolivenbaum Aug 25, 2021
efec886
Update main.js
theolivenbaum Aug 25, 2021
64b91fc
Update main.js
theolivenbaum Aug 25, 2021
be41cae
Update main.js
theolivenbaum Aug 25, 2021
48d5497
Revert "Update main.js"
theolivenbaum Aug 25, 2021
5e82ae4
add socket events to emit console messages
theolivenbaum Aug 25, 2021
18d425b
Merge branch 'master' into switch-to-new-socket-lib
theolivenbaum Aug 25, 2021
891870a
Merge branch 'master' into switch-to-new-socket-lib
GregorBiswanger Apr 6, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
628 changes: 119 additions & 509 deletions ElectronNET.API/App.cs

Large diffs are not rendered by default.

323 changes: 87 additions & 236 deletions ElectronNET.API/AutoUpdater.cs

Large diffs are not rendered by default.

310 changes: 292 additions & 18 deletions ElectronNET.API/BridgeConnector.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,318 @@
using Quobject.SocketIoClientDotNet.Client;
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using SocketIOClient;
using SocketIOClient.JsonSerializer;
using SocketIOClient.Newtonsoft.Json;

namespace ElectronNET.API
{
internal static class BridgeConnector
{
private static Socket _socket;
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);
}
}
}
}

private static SocketIO _socket;

private static object _syncRoot = new object();

public static Socket Socket
public static void Emit(string eventString, params object[] args)
{
get
//We don't care about waiting for the event to be emitted, so this doesn't need to be async

Task.Run(async () =>
{
if (App.SocketDebug)
{
Console.WriteLine($"Sending event {eventString}");
}

await Socket.EmitAsync(eventString, args);

if (App.SocketDebug)
{
Console.WriteLine($"Sent event {eventString}");
}
});
}

/// <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}");
}
}

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<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 = completedEvent;

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(completedEvent, 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(completedEvent, eventKey, taskCompletionSource);
});

Emit(triggerEvent, args);
}
}

return await taskCompletionSource.Task;
}


public static async Task<T> OnResult<T>(string triggerEvent, string completedEvent, CancellationToken cancellationToken, params object[] args)
{
string eventKey = completedEvent;

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
{
if(_socket == null && HybridSupport.IsElectronActive)
var hash = new HashCode();
foreach (var obj in args)
{
hash.Add(obj);
}
eventKey = $"{eventKey}-{(uint)hash.ToHashCode()}";
}

if (EventTasks<T>.TryGetOrAdd(completedEvent, 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 Task.Run(() => waitThisFirstAndThenTryAgain, cancellationToken);
}
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, cancellationToken, args);
}
else
{
lock (_syncRoot)
using (cancellationToken.Register(() => taskCompletionSource.TrySetCanceled()))
{
if (_socket == null && HybridSupport.IsElectronActive)
//A new TaskCompletionSource was added, so we need to register the completed event here

On<T>(completedEvent, (result) =>
{
_socket = IO.Socket("http://localhost:" + BridgeSettings.SocketPort);
_socket.On(Socket.EVENT_CONNECT, () =>
{
Console.WriteLine("BridgeConnector connected!");
});
}
Off(completedEvent);
taskCompletionSource.SetResult(result);
EventTasks<T>.DoneWith(completedEvent, eventKey, taskCompletionSource);
});

Emit(triggerEvent, args);
}
}
else if(_socket == null && !HybridSupport.IsElectronActive)
}

return await taskCompletionSource.Task;
}
private static SocketIO Socket
{
get
{
if (_socket is null)
{
lock (_syncRoot)
if (HybridSupport.IsElectronActive)
{
if (_socket == null && !HybridSupport.IsElectronActive)

lock (_syncRoot)
{
_socket = IO.Socket(new Uri("http://localhost"), new IO.Options { AutoConnect = false });
if (_socket is null && HybridSupport.IsElectronActive)
{
var socket = new SocketIO($"http://localhost:{BridgeSettings.SocketPort}", new SocketIOOptions()
{
EIO = 3
});

socket.JsonSerializer = new CamelCaseNewtonsoftJsonSerializer(socket.Options.EIO);


socket.OnConnected += (_, __) =>
{
Console.WriteLine("BridgeConnector connected!");
};

socket.ConnectAsync().Wait();

_socket = socket;
}
}
}
else
{
throw new Exception("Missing Socket Port");
}
}

return _socket;
}
}

private class CamelCaseNewtonsoftJsonSerializer : NewtonsoftJsonSerializer
{
public CamelCaseNewtonsoftJsonSerializer(int eio) : base(eio)
{
}

public override JsonSerializerSettings CreateOptions()
{
return new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
};
}
}
}
}
11 changes: 11 additions & 0 deletions ElectronNET.API/BridgeSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,16 @@ public static class BridgeSettings
/// The web port.
/// </value>
public static string WebPort { get; internal set; }

/// <summary>
/// Manually set the port values instead of using the UseElectron extension method
/// </summary>
/// <param name="socketPort"></param>
/// <param name="webPort"></param>
public static void InitializePorts(int socketPort, int webPort)
{
SocketPort = socketPort.ToString();
WebPort = webPort.ToString();
}
}
}
Loading