Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 11 additions & 2 deletions src/ElectronNET.API/Bridge/SocketIOConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,29 @@
namespace ElectronNET.API;

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ElectronNET.API.Serialization;
using SocketIO.Serializer.SystemTextJson;
using SocketIO = SocketIOClient.SocketIO;
using SocketIOOptions = SocketIOClient.SocketIOOptions;

internal class SocketIOConnection : ISocketConnection
{
private readonly SocketIO _socket;
private readonly object _lockObj = new object();
private bool _isDisposed;

public SocketIOConnection(string uri)
public SocketIOConnection(string uri, string authorization)
{
_socket = new SocketIO(uri);
var opts = string.IsNullOrEmpty(authorization) ? new SocketIOOptions() : new SocketIOOptions
{
ExtraHeaders = new Dictionary<string, string>
{
["authorization"] = authorization
},
};
_socket = new SocketIO(uri, opts);
_socket.Serializer = new SystemTextJsonSerializer(ElectronJson.Options);
// Use default System.Text.Json serializer from SocketIOClient.
// Outgoing args are normalized to camelCase via SerializeArg in Emit.
Expand Down
3 changes: 3 additions & 0 deletions src/ElectronNET.API/Common/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class ProcessRunner : IDisposable
private readonly StringBuilder stdOut = new StringBuilder(4 * 1024);
private readonly StringBuilder stdErr = new StringBuilder(4 * 1024);

public event EventHandler<string> LineReceived;

private volatile ManualResetEvent stdOutEvent;
private volatile ManualResetEvent stdErrEvent;
private volatile Stopwatch stopwatch;
Expand Down Expand Up @@ -571,6 +573,7 @@ private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
if (e.Data != null)
{
Console.WriteLine("|| " + e.Data);
LineReceived?.Invoke(this, e.Data);
}
else
{
Expand Down
3 changes: 3 additions & 0 deletions src/ElectronNET.API/ElectronNetRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static class ElectronNetRuntime
internal const int DefaultWebPort = 8001;
internal const string ElectronPortArgumentName = "electronPort";
internal const string ElectronPidArgumentName = "electronPID";
internal const string ElectronAuthTokenArgumentName = "electronAuthToken";

/// <summary>Initializes the <see cref="ElectronNetRuntime"/> class.</summary>
static ElectronNetRuntime()
Expand All @@ -26,6 +27,8 @@ static ElectronNetRuntime()

public static string ElectronExtraArguments { get; set; }

public static string ElectronAuthToken { get; internal set; }

public static int? ElectronSocketPort { get; internal set; }

public static int? AspNetWebPort { get; internal set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ internal class RuntimeControllerDotNetFirst : RuntimeControllerBase
{
private ElectronProcessBase electronProcess;
private SocketBridgeService socketBridge;
private int? port;

public RuntimeControllerDotNetFirst()
{
Expand Down Expand Up @@ -41,19 +40,13 @@ protected override Task StartCore()
var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged();
var electronBinaryName = ElectronNetRuntime.ElectronExecutable;
var args = string.Format("{0} {1}", ElectronNetRuntime.ElectronExtraArguments, Environment.CommandLine).Trim();
this.port = ElectronNetRuntime.ElectronSocketPort;

if (!this.port.HasValue)
{
this.port = PortHelper.GetFreePort(ElectronNetRuntime.DefaultSocketPort);
ElectronNetRuntime.ElectronSocketPort = this.port;
}
var port = ElectronNetRuntime.ElectronSocketPort ?? 0;

Console.Error.WriteLine("[StartCore]: isUnPacked: {0}", isUnPacked);
Console.Error.WriteLine("[StartCore]: electronBinaryName: {0}", electronBinaryName);
Console.Error.WriteLine("[StartCore]: args: {0}", args);

this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, this.port.Value);
this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, port);
this.electronProcess.Ready += this.ElectronProcess_Ready;
this.electronProcess.Stopped += this.ElectronProcess_Stopped;

Expand All @@ -63,8 +56,10 @@ protected override Task StartCore()

private void ElectronProcess_Ready(object sender, EventArgs e)
{
var port = ElectronNetRuntime.ElectronSocketPort.Value;
var token = ElectronNetRuntime.ElectronAuthToken;
this.TransitionState(LifetimeState.Started);
this.socketBridge = new SocketBridgeService(this.port!.Value);
this.socketBridge = new SocketBridgeService(port, token);
this.socketBridge.Ready += this.SocketBridge_Ready;
this.socketBridge.Stopped += this.SocketBridge_Stopped;
this.socketBridge.Start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ internal class RuntimeControllerElectronFirst : RuntimeControllerBase
{
private ElectronProcessBase electronProcess;
private SocketBridgeService socketBridge;
private int? port;

public RuntimeControllerElectronFirst()
{
Expand All @@ -36,20 +35,16 @@ internal override ISocketConnection Socket

protected override Task StartCore()
{
this.port = ElectronNetRuntime.ElectronSocketPort;

if (!this.port.HasValue)
{
throw new Exception("No port has been specified by Electron!");
}
var port = ElectronNetRuntime.ElectronSocketPort.Value;
var token = ElectronNetRuntime.ElectronAuthToken;

if (!ElectronNetRuntime.ElectronProcessId.HasValue)
{
throw new Exception("No electronPID has been specified by Electron!");
}

this.TransitionState(LifetimeState.Starting);
this.socketBridge = new SocketBridgeService(this.port!.Value);
this.socketBridge = new SocketBridgeService(port, token);
this.socketBridge.Ready += this.SocketBridge_Ready;
this.socketBridge.Stopped += this.SocketBridge_Stopped;
this.socketBridge.Start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ElectronNET.Common;
using ElectronNET.Runtime.Data;
Expand All @@ -15,6 +18,7 @@
[Localizable(false)]
internal class ElectronProcessActive : ElectronProcessBase
{
private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at .* using ([a-f0-9]+)$");
private readonly bool isUnpackaged;
private readonly string electronBinaryName;
private readonly string extraArguments;
Expand Down Expand Up @@ -101,7 +105,6 @@ private void CheckRuntimeIdentifier()
}

var osPart = buildInfoRid.Split('-').First();

var mismatch = false;

switch (osPart)
Expand Down Expand Up @@ -157,18 +160,57 @@ protected override Task StopCore()

private async Task StartInternal(string startCmd, string args, string directoriy)
{
try
var tcs = new TaskCompletionSource();
using var cts = new CancellationTokenSource(2 * 60_000); // cancel after 2 minutes
using var _ = cts.Token.Register(() =>
{
// Time is over - let's kill the process and move on
this.process.Cancel();
// We don't want to raise exceptions here - just pass the barrier
tcs.SetResult();
});

void Read_SocketIO_Parameters(object sender, string line)
{
// Look for "Electron Socket: listening on port %s at ..."
Comment on lines +163 to +175
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a path completely without reading the process output.
(without auth token)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not do that. It's less secure and just more complicated. Let's keep it to the point.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not want to have that in our application that dotnet is relying on the console output of Electron.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have nothing against making the auth token mandatory, but it can be provided by the dotnet side as cli param, so it doesn't need to be read it from the console output. (that's no more and no less secure)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically possible, but that's another branch / logic that we would need to take care of. So I'd opt out of that.

Copy link
Copy Markdown
Collaborator

@softworkz softworkz Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was just a thought. My only actual concern is the console parsing. That's admittedly a rather egoistic concern. I do not mean to say that parsing CLI output is generally wrong or bad, it's about the specific context of our app: Linux is a world of 1001 surprises, often bad surprises and I don't want to employ anything which has potential for causing bad surprises - that's why I would like to retain a path where this doesn't happen.

Thanks

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah can relate to that... My suggestion would be to have it merged / out in a preview version and test it extensively. If that does not hold / has weak points then I think a different approach must be anyway found (because I would still insist that a single flow is better than multiple - but that single flow has to work in all cases).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is this: as soon as this is merged, there is no more point for me to submit any fixes and updates, because I cannot use the packages anymore.

Beyond my own usage, introducing something new in a way that it's optional, is generally advantageous:

  • Nobody's story will be hard broken or blocked
  • It's useful for differential diagnosis when issues are reported, to determine whether it's by the new code or due to other reasons
  • It would make me very happy 😉

var match = extractor.Match(line);

if (match?.Success ?? false)
{
var port = int.Parse(match.Groups[1].Value);
var token = match.Groups[2].Value;

this.process.LineReceived -= Read_SocketIO_Parameters;
ElectronNetRuntime.ElectronAuthToken = token;
ElectronNetRuntime.ElectronSocketPort = port;
tcs.SetResult();
}
}

void Monitor_SocketIO_Failure(object sender, EventArgs e)
{
await Task.Delay(10.ms()).ConfigureAwait(false);
// We don't want to raise exceptions here - just pass the barrier
if (tcs.Task.IsCompleted)
{
this.Process_Exited(sender, e);
}
else
{
tcs.SetResult();
}
}

try
{
Console.Error.WriteLine("[StartInternal]: startCmd: {0}", startCmd);
Console.Error.WriteLine("[StartInternal]: args: {0}", args);

this.process = new ProcessRunner("ElectronRunner");
this.process.ProcessExited += this.Process_Exited;
this.process.ProcessExited += Monitor_SocketIO_Failure;
this.process.LineReceived += Read_SocketIO_Parameters;
this.process.Run(startCmd, args, directoriy);

await Task.Delay(500.ms()).ConfigureAwait(false);
await tcs.Task.ConfigureAwait(false);

Console.Error.WriteLine("[StartInternal]: after run:");

Expand All @@ -178,11 +220,11 @@ private async Task StartInternal(string startCmd, string args, string directoriy
Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardOutput);

Task.Run(() => this.TransitionState(LifetimeState.Stopped));

throw new Exception("Failed to launch the Electron process.");
}

this.TransitionState(LifetimeState.Ready);
else
{
this.TransitionState(LifetimeState.Ready);
}
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
internal class SocketBridgeService : LifetimeServiceBase
{
private readonly int socketPort;
private readonly string authorization;
private readonly string socketUrl;
private SocketIOConnection socket;

public SocketBridgeService(int socketPort)
public SocketBridgeService(int socketPort, string authorization)
{
this.socketPort = socketPort;
this.authorization = authorization;
this.socketUrl = $"http://localhost:{this.socketPort}";
}

Expand All @@ -23,7 +25,7 @@ public SocketBridgeService(int socketPort)

protected override Task StartCore()
{
this.socket = new SocketIOConnection(this.socketUrl);
this.socket = new SocketIOConnection(this.socketUrl, this.authorization);
this.socket.BridgeConnected += this.Socket_BridgeConnected;
this.socket.BridgeDisconnected += this.Socket_BridgeDisconnected;
Task.Run(this.Connect);
Expand Down
14 changes: 14 additions & 0 deletions src/ElectronNET.API/Runtime/StartupManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ private void CollectProcessData()
Console.WriteLine("Electron Process ID: " + result);
}
}

var authTokenArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronAuthTokenArgumentName, StringComparison.OrdinalIgnoreCase));

if (authTokenArg != null)
{
var parts = authTokenArg.Split('=', StringSplitOptions.TrimEntries);

if (parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]))
{
var result = parts[1];
ElectronNetRuntime.ElectronAuthToken = result;
Console.WriteLine("Use Auth Token: " + result);
}
}
}

private void SetElectronExecutable()
Expand Down
15 changes: 10 additions & 5 deletions src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ElectronNET.API
{
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using ElectronNET.AspNet;
Expand All @@ -10,6 +11,7 @@
using ElectronNET.Runtime.Helpers;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

/// <summary>
/// Provides extension methods for <see cref="IWebHostBuilder"/> to enable Electron.NET
Expand Down Expand Up @@ -66,23 +68,26 @@ public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[]
// work as expected, see issue #952
Environment.SetEnvironmentVariable("ELECTRON_RUN_AS_NODE", null);

var webPort = PortHelper.GetFreePort(ElectronNetRuntime.AspNetWebPort ?? ElectronNetRuntime.DefaultWebPort);
ElectronNetRuntime.AspNetWebPort = webPort;
var webPort = ElectronNetRuntime.AspNetWebPort ?? 0;

// check for the content folder if its exists in base director otherwise no need to include
// It was used before because we are publishing the project which copies everything to bin folder and contentroot wwwroot was folder there.
// now we have implemented the live reload if app is run using /watch then we need to use the default project path.

// For port 0 (dynamic port assignment), Kestrel requires binding to specific IP (127.0.0.1) not localhost
var host = webPort == 0 ? "127.0.0.1" : "localhost";

if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot"))
{
builder = builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
.UseUrls("http://localhost:" + webPort);
.UseUrls($"http://{host}:{webPort}");
}
else
{
builder = builder.UseUrls("http://localhost:" + webPort);
builder = builder.UseUrls($"http://{host}:{webPort}");
}

builder = builder.ConfigureServices(services =>
builder = builder.ConfigureServices((context, services) =>
{
services.AddTransient<IStartupFilter, ServerReadyStartupFilter>();
services.AddSingleton<AspNetLifetimeAdapter>();
Expand Down
Loading
Loading