ElectronNET.API: Add new runtime code (for launch, lifecycle and service orchestration)

This commit is contained in:
softworkz
2025-10-13 13:24:28 +02:00
parent b06d20450b
commit d1db928222
19 changed files with 1175 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
namespace ElectronNET.Common
{
using System;
using System.Collections.Immutable;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Services;
public static class Extensions
{
public static bool IsUnpackaged(this StartupMethod method)
{
switch (method)
{
case StartupMethod.UnpackedElectronFirst:
case StartupMethod.UnpackedDotnetFirst:
return true;
default:
return false;
}
}
public static bool IsReady(this LifetimeServiceBase service)
{
return service != null && service.State == LifetimeState.Ready;
}
public static bool IsNotStopped(this LifetimeServiceBase service)
{
return service != null && service.State != LifetimeState.Stopped;
}
public static bool IsNullOrStopped(this LifetimeServiceBase service)
{
return service == null || service.State == LifetimeState.Stopped;
}
}
}

View File

@@ -0,0 +1,55 @@
namespace ElectronNET
{
using System;
using System.Collections.Immutable;
using System.Threading.Tasks;
using ElectronNET.API;
using ElectronNET.Runtime;
using ElectronNET.Runtime.Controllers;
using ElectronNET.Runtime.Data;
public static class ElectronNetRuntime
{
internal static StartupManager StartupManager;
internal const int DefaultSocketPort = 8000;
internal const int DefaultWebPort = 8001;
internal const string ElectronPortArgumentName = "electronPort";
internal const string ElectronPidArgumentName = "electronPID";
/// <summary>Initializes the <see cref="ElectronNetRuntime"/> class.</summary>
static ElectronNetRuntime()
{
StartupManager = new StartupManager();
StartupManager.Initialize();
}
public static int? ElectronSocketPort { get; internal set; }
public static int? AspNetWebPort { get; internal set; }
public static StartupMethod StartupMethod { get; internal set; }
public static DotnetAppType DotnetAppType { get; internal set; }
public static string ElectronExecutable { get; internal set; }
public static ImmutableList<string> ProcessArguments { get; internal set; }
public static BuildInfo BuildInfo { get; internal set; }
public static IElectronNetRuntimeController RuntimeController => RuntimeControllerCore;
// The below properties are non-public
internal static RuntimeControllerBase RuntimeControllerCore { get; set; }
internal static int? ElectronProcessId { get; set; }
internal static Func<Task> OnAppReadyCallback { get; set; }
internal static SocketIoFacade GetSocket()
{
return RuntimeControllerCore?.Socket;
}
}
}

View File

@@ -0,0 +1,33 @@
namespace ElectronNET.Runtime.Controllers
{
using System.Threading.Tasks;
using ElectronNET.API;
using ElectronNET.Runtime.Services;
using ElectronNET.Runtime.Services.ElectronProcess;
using ElectronNET.Runtime.Services.SocketBridge;
internal abstract class RuntimeControllerBase : LifetimeServiceBase, IElectronNetRuntimeController
{
protected RuntimeControllerBase()
{
}
internal abstract SocketIoFacade Socket { get; }
internal abstract ElectronProcessBase ElectronProcess { get; }
internal abstract SocketBridgeService SocketBridge { get; }
protected override Task StartCore()
{
return Task.CompletedTask;
}
protected override Task StopCore()
{
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,107 @@
namespace ElectronNET.Runtime.Controllers
{
using System;
using System.Threading.Tasks;
using ElectronNET.API;
using ElectronNET.Common;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Helpers;
using ElectronNET.Runtime.Services.ElectronProcess;
using ElectronNET.Runtime.Services.SocketBridge;
internal class RuntimeControllerDotNetFirst : RuntimeControllerBase
{
private ElectronProcessBase electronProcess;
private SocketBridgeService socketBridge;
private int? port;
public RuntimeControllerDotNetFirst()
{
}
internal override SocketIoFacade Socket
{
get
{
if (this.State == LifetimeState.Ready)
{
return this.socketBridge.Socket;
}
throw new Exception("Cannot access socket bridge. Runtime is not in 'Ready' state");
}
}
internal override ElectronProcessBase ElectronProcess => this.electronProcess;
internal override SocketBridgeService SocketBridge => this.socketBridge;
protected override Task StartCore()
{
var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged();
var electronBinaryName = ElectronNetRuntime.ElectronExecutable;
var args = Environment.CommandLine;
this.port = ElectronNetRuntime.ElectronSocketPort;
if (!this.port.HasValue)
{
this.port = PortHelper.GetFreePort(ElectronNetRuntime.DefaultSocketPort);
ElectronNetRuntime.ElectronSocketPort = this.port;
}
this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, this.port.Value);
this.electronProcess.Ready += this.ElectronProcess_Ready;
this.electronProcess.Stopped += this.ElectronProcess_Stopped;
return this.electronProcess.Start();
}
private void ElectronProcess_Ready(object sender, EventArgs e)
{
this.TransitionState(LifetimeState.Started);
this.socketBridge = new SocketBridgeService(this.port!.Value);
this.socketBridge.Ready += this.SocketBridge_Ready;
this.socketBridge.Stopped += this.SocketBridge_Stopped;
this.socketBridge.Start();
}
private void SocketBridge_Ready(object sender, EventArgs e)
{
this.TransitionState(LifetimeState.Ready);
}
private void SocketBridge_Stopped(object sender, EventArgs e)
{
this.HandleStopped();
}
private void ElectronProcess_Stopped(object sender, EventArgs e)
{
this.HandleStopped();
}
private void HandleStopped()
{
if (this.socketBridge.State != LifetimeState.Stopped)
{
this.socketBridge.Stop();
}
else if (this.electronProcess.State != LifetimeState.Stopped)
{
this.electronProcess.Stop();
}
else
{
this.TransitionState(LifetimeState.Stopped);
}
}
protected override Task StopCore()
{
this.electronProcess.Stop();
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,108 @@
namespace ElectronNET.Runtime.Controllers
{
using System;
using System.Threading.Tasks;
using ElectronNET.API;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Services.ElectronProcess;
using ElectronNET.Runtime.Services.SocketBridge;
internal class RuntimeControllerElectronFirst : RuntimeControllerBase
{
private ElectronProcessBase electronProcess;
private SocketBridgeService socketBridge;
private int? port;
public RuntimeControllerElectronFirst()
{
}
internal override SocketIoFacade Socket
{
get
{
if (this.State == LifetimeState.Ready)
{
return this.socketBridge.Socket;
}
throw new Exception("Cannot access socket bridge. Runtime is not in 'Ready' state");
}
}
internal override ElectronProcessBase ElectronProcess => this.electronProcess;
internal override SocketBridgeService SocketBridge => this.socketBridge;
protected override Task StartCore()
{
this.port = ElectronNetRuntime.ElectronSocketPort;
if (!this.port.HasValue)
{
throw new Exception("No port has been specified by Electron!");
}
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.Ready += this.SocketBridge_Ready;
this.socketBridge.Stopped += this.SocketBridge_Stopped;
this.socketBridge.Start();
this.electronProcess = new ElectronProcessPassive(ElectronNetRuntime.ElectronProcessId.Value);
this.electronProcess.Ready += this.ElectronProcess_Ready;
this.electronProcess.Stopped += this.ElectronProcess_Stopped;
this.electronProcess.Start();
return Task.CompletedTask;
}
private void ElectronProcess_Ready(object sender, EventArgs e)
{
}
private void SocketBridge_Ready(object sender, EventArgs e)
{
this.TransitionState(LifetimeState.Ready);
}
private void SocketBridge_Stopped(object sender, EventArgs e)
{
this.HandleStopped();
}
private void ElectronProcess_Stopped(object sender, EventArgs e)
{
this.HandleStopped();
}
private void HandleStopped()
{
if (this.socketBridge.State != LifetimeState.Stopped)
{
this.socketBridge.Stop();
}
else if (this.electronProcess.State != LifetimeState.Stopped)
{
this.electronProcess.Stop();
}
else
{
this.TransitionState(LifetimeState.Stopped);
}
}
protected override Task StopCore()
{
this.socketBridge.Stop();
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,19 @@
namespace ElectronNET.Runtime.Data
{
public class BuildInfo
{
public string ElectronExecutable { get; internal set; }
public string ElectronVersion { get; internal set; }
public string RuntimeIdentifier { get; internal set; }
public string ElectronSingleInstance { get; internal set; }
public string Title { get; internal set; }
public string Version { get; internal set; }
public string BuildConfiguration { get; internal set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace ElectronNET.Runtime.Data
{
public enum DotnetAppType
{
/// <summary>A plain DotNet cross-platform console app.</summary>
ConsoleApp,
/// <summary>ASP.NET Core cross-platform app.</summary>
AspNetCoreApp,
}
}

View File

@@ -0,0 +1,12 @@
namespace ElectronNET.Runtime.Data
{
public enum LifetimeState
{
Uninitialized,
Starting,
Started,
Ready,
Stopping,
Stopped,
}
}

View File

@@ -0,0 +1,37 @@
namespace ElectronNET.Runtime.Data
{
public enum StartupMethod
{
/// <summary>Packaged Electron app where Electron launches the DotNet app.</summary>
/// <remarks>
/// This is the classic way of ElectrronNET startup.
/// </remarks>
PackagedElectronFirst,
/// <summary>Packaged Electron app where DotNet launches the Electron prozess.</summary>
/// <remarks>
/// Provides better ways for managing the overall app lifecycle.
/// On the command lines, this is "dotnetpacked"
/// </remarks>
PackagedDotnetFirst,
/// <summary>Unpackacked execution for debugging the Electron process and NodeJS.</summary>
/// <remarks>
/// Similar to the legacy ElectronNET debugging but without packaging (=fast) and allows selection of
/// the debug adapter. It's rarely useful, unless it's about debugging NodeJS.
/// Note: 'Unpackaged' means that it's run directly from the compilation output folders (./bin/*).
/// On the command lines, this is "unpackedelectron"
/// </remarks>
UnpackedElectronFirst,
/// <summary>Unpackacked execution for debugging the DotNet process.</summary>
/// <remarks>
/// This is the new way of super-fast startup for debugging in-place with Hot Reload
/// (edit and continue), even on WSL - all from within Visual Studio.
/// Note: 'Unpackaged' means that it's run directly from the compilation output folders (./bin/*).
/// On the command lines, this is "unpackeddotnet"
/// </remarks>
UnpackedDotnetFirst,
}
}

View File

@@ -0,0 +1,72 @@
namespace ElectronNET.Runtime.Helpers
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
internal class LaunchOrderDetector
{
public static bool CheckIsLaunchedByDotNet()
{
var tests = new List<Func<bool?>>();
tests.Add(CheckIsDotNetStartup1);
tests.Add(CheckIsDotNetStartup2);
tests.Add(CheckIsDotNetStartup3);
int scoreDotNet = 0, scoreElectron = 0;
foreach (var test in tests)
{
var res = test();
if (res == true)
{
scoreDotNet++;
}
if (res == false)
{
scoreElectron++;
}
}
Console.WriteLine("Probe scored for launch origin: DotNet {0} vs. {1} Electron", scoreDotNet, scoreElectron);
return scoreDotNet > scoreElectron;
}
private static bool? CheckIsDotNetStartup1()
{
var hasPortArg = ElectronNetRuntime.ProcessArguments.Any(e => e.Contains(ElectronNetRuntime.ElectronPortArgumentName, StringComparison.OrdinalIgnoreCase));
if (hasPortArg)
{
return false;
}
return true;
}
private static bool? CheckIsDotNetStartup2()
{
var hasPidArg = ElectronNetRuntime.ProcessArguments.Any(e => e.Contains(ElectronNetRuntime.ElectronPidArgumentName, StringComparison.OrdinalIgnoreCase));
if (hasPidArg)
{
return false;
}
return true;
}
private static bool? CheckIsDotNetStartup3()
{
if (Debugger.IsAttached)
{
return true;
}
return null;
}
}
}

View File

@@ -0,0 +1,26 @@
namespace ElectronNET.Runtime.Helpers
{
using System.Linq;
using System.Net.NetworkInformation;
internal static class PortHelper
{
public static int GetFreePort(int? defaultPost)
{
var listeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners().Select(e => e.Port).ToList();
int port = defaultPost ?? 8000;
while (true)
{
if (!listeners.Contains(port))
{
return port;
}
port += 2;
}
}
}
}

View File

@@ -0,0 +1,109 @@
namespace ElectronNET.Runtime.Helpers
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
internal class UnpackagedDetector
{
public static bool CheckIsUnpackaged()
{
var tests = new List<Func<bool?>>();
tests.Add(CheckUnpackaged1);
// We let this one account twice
tests.Add(CheckUnpackaged2);
tests.Add(CheckUnpackaged2);
tests.Add(CheckUnpackaged3);
tests.Add(CheckUnpackaged4);
int scoreUnpackaged = 0, scorePackaged = 0;
foreach (var test in tests)
{
var res = test();
if (res == true)
{
scoreUnpackaged++;
}
if (res == false)
{
scorePackaged++;
}
}
Console.WriteLine("Probe scored for package mode: Unpackaged {0} vs. {1} Packaged", scoreUnpackaged, scorePackaged);
return scoreUnpackaged > scorePackaged;
}
private static bool? CheckUnpackaged1()
{
var cfg = ElectronNetRuntime.BuildInfo.BuildConfiguration;
if (cfg != null)
{
if (cfg.Equals("Debug", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (cfg.Equals("Release", StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return null;
}
private static bool? CheckUnpackaged2()
{
var dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
if (dir.Name == "bin" && dir.Parent?.Name == "resources")
{
return false;
}
if (dir.GetDirectories().Any(e => e.Name == ".electron"))
{
return true;
}
return null;
}
private static bool? CheckUnpackaged3()
{
if (Debugger.IsAttached)
{
return true;
}
return null;
}
private static bool? CheckUnpackaged4()
{
var isUnpackaged = ElectronNetRuntime.ProcessArguments.Any(e => e.Contains("unpacked", StringComparison.OrdinalIgnoreCase));
if (isUnpackaged)
{
return true;
}
var isPackaged = ElectronNetRuntime.ProcessArguments.Any(e => e.Contains("dotnetpacked", StringComparison.OrdinalIgnoreCase));
if (isPackaged)
{
return false;
}
return null;
}
}
}

View File

@@ -0,0 +1,33 @@
namespace ElectronNET.Runtime
{
using System;
using System.Threading.Tasks;
using ElectronNET.Runtime.Data;
public interface IElectronNetRuntimeController
{
LifetimeState State { get; }
Task WaitStartedTask { get; }
Task WaitReadyTask { get; }
Task WaitStoppingTask { get; }
Task WaitStoppedTask { get; }
event EventHandler Starting;
event EventHandler Started;
event EventHandler Ready;
event EventHandler Stopping;
event EventHandler Stopped;
Task Start();
Task Stop();
}
}

View File

@@ -0,0 +1,94 @@
namespace ElectronNET.Runtime.Services.ElectronProcess
{
using System;
using System.ComponentModel;
using System.IO;
using System.Threading.Tasks;
using ElectronNET.Common;
using ElectronNET.Runtime.Data;
/// <summary>
/// Launches and manages the Electron app process.
/// </summary>
[Localizable(false)]
internal class ElectronProcessActive : ElectronProcessBase
{
private readonly bool isUnpackaged;
private readonly string electronBinaryName;
private readonly string extraArguments;
private readonly int socketPort;
private ProcessRunner process;
/// <summary>Initializes a new instance of the <see cref="ElectronProcessActive"/> class.</summary>
/// <param name="isUnpackaged">The is debug.</param>
/// <param name="electronBinaryName">Name of the electron.</param>
/// <param name="extraArguments">The extraArguments.</param>
/// <param name="socketPort">The socket port.</param>
public ElectronProcessActive(bool isUnpackaged, string electronBinaryName, string extraArguments, int socketPort)
{
this.isUnpackaged = isUnpackaged;
this.electronBinaryName = electronBinaryName;
this.extraArguments = extraArguments;
this.socketPort = socketPort;
}
protected override Task StartCore()
{
var dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
string startCmd, args, workingDir;
if (this.isUnpackaged)
{
var electrondir = Path.Combine(dir.FullName, ".electron");
startCmd = Path.Combine(electrondir, "node_modules", "electron", "dist", "electron");
args = $"main.js -unpackeddotnet --trace-warnings -electronforcedport={this.socketPort:D} " + this.extraArguments;
workingDir = electrondir;
}
else
{
dir = dir.Parent?.Parent;
startCmd = Path.Combine(dir.FullName, this.electronBinaryName);
args = $"-dotnetpacked -electronforcedport={this.socketPort:D} " + this.extraArguments;
workingDir = dir.FullName;
}
// We don't await this in order to let the state transition to "Starting"
Task.Run(async () => await this.StartInternal(startCmd, args, workingDir).ConfigureAwait(false));
return Task.CompletedTask;
}
protected override Task StopCore()
{
this.process.Cancel();
return Task.CompletedTask;
}
private async Task StartInternal(string startCmd, string args, string directoriy)
{
await Task.Delay(10).ConfigureAwait(false);
this.process = new ProcessRunner("ElectronRunner");
this.process.ProcessExited += this.Process_Exited;
this.process.Run(startCmd, args, directoriy);
await Task.Delay(500).ConfigureAwait(false);
if (!this.process.IsRunning)
{
Task.Run(() => this.TransitionState(LifetimeState.Stopped));
throw new Exception("Failed to launch the Electron process.");
}
this.TransitionState(LifetimeState.Ready);
}
private void Process_Exited(object sender, EventArgs e)
{
this.TransitionState(LifetimeState.Stopped);
}
}
}

View File

@@ -0,0 +1,15 @@
namespace ElectronNET.Runtime.Services.ElectronProcess
{
using System.ComponentModel;
/// <summary>
/// Manages the Electron app process.
/// </summary>
[Localizable(false)]
internal abstract class ElectronProcessBase : LifetimeServiceBase
{
protected ElectronProcessBase()
{
}
}
}

View File

@@ -0,0 +1,53 @@
namespace ElectronNET.Runtime.Services.ElectronProcess
{
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;
using ElectronNET.Runtime.Data;
/// <summary>
/// Launches and manages the Electron app process.
/// </summary>
[Localizable(false)]
internal class ElectronProcessPassive : ElectronProcessBase
{
private readonly int pid;
private Process process;
/// <summary>Initializes a new instance of the <see cref="ElectronProcessPassive"/> class.</summary>
/// <param name="pid"></param>
public ElectronProcessPassive(int pid)
{
this.pid = pid;
}
protected override Task StartCore()
{
this.process = Process.GetProcessById(this.pid);
if (this.process == null)
{
throw new ArgumentException($"Unable to find process with ID {this.pid}");
}
this.process.Exited += this.Process_Exited1;
Task.Run(() => this.TransitionState(LifetimeState.Ready));
return Task.CompletedTask;
}
private void Process_Exited1(object sender, EventArgs e)
{
this.TransitionState(LifetimeState.Stopped);
}
protected override Task StopCore()
{
// Not sure about this:
////this.process.Kill(true);
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,135 @@
namespace ElectronNET.Runtime.Services
{
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using ElectronNET.Runtime.Data;
public abstract class LifetimeServiceBase
{
private readonly TaskCompletionSource tcsStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource tcsReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource tcsStopping = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource tcsStopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
private LifetimeState state = LifetimeState.Uninitialized;
protected LifetimeServiceBase() { }
public event EventHandler Starting;
public event EventHandler Started;
public event EventHandler Ready;
public event EventHandler Stopping;
public event EventHandler Stopped;
public LifetimeState State => this.state;
public Task WaitStartedTask => this.tcsStarted.Task;
public Task WaitReadyTask => this.tcsReady.Task;
public Task WaitStoppingTask => this.tcsStopping.Task;
public Task WaitStoppedTask => this.tcsStopped.Task;
public virtual async Task Start()
{
this.ValidateMaxState(LifetimeState.Uninitialized);
await this.StartCore().ConfigureAwait(false);
this.TransitionState(LifetimeState.Starting);
}
public virtual async Task Stop()
{
this.ValidateMaxState(LifetimeState.Ready);
await this.StopCore().ConfigureAwait(false);
this.TransitionState(LifetimeState.Stopping);
}
protected virtual Task StopCore()
{
return Task.CompletedTask;
}
protected virtual Task StartCore()
{
return Task.CompletedTask;
}
private void ValidateMaxState(LifetimeState evalState, [CallerMemberName] string callerMemberName = null)
{
if (this.state > evalState)
{
throw new Exception($"Invalid state! Cannot execute {callerMemberName} in state {this.state}");
}
}
protected void TransitionState(LifetimeState newState)
{
if (newState == this.state)
{
return;
}
if (newState < this.state)
{
throw new Exception($"Invalid state transision from {this.state} to {newState}: " + this.GetType().Name);
}
var oldState = this.state;
this.state = newState;
switch (this.state)
{
case LifetimeState.Starting:
this.Starting?.Invoke(this, EventArgs.Empty);
break;
case LifetimeState.Started:
this.Started?.Invoke(this, EventArgs.Empty);
break;
case LifetimeState.Ready:
this.Ready?.Invoke(this, EventArgs.Empty);
break;
case LifetimeState.Stopping:
this.Stopping?.Invoke(this, EventArgs.Empty);
break;
case LifetimeState.Stopped:
this.Stopped?.Invoke(this, EventArgs.Empty);
break;
default:
throw new ArgumentOutOfRangeException();
}
if (oldState < LifetimeState.Started && newState >= LifetimeState.Started)
{
this.tcsStarted.TrySetResult();
}
if (oldState < LifetimeState.Ready && newState >= LifetimeState.Ready)
{
this.tcsReady.TrySetResult();
}
if (oldState < LifetimeState.Stopping && newState >= LifetimeState.Stopping)
{
this.tcsStopping.TrySetResult();
}
if (oldState < LifetimeState.Stopped && newState >= LifetimeState.Stopped)
{
this.tcsStopped.TrySetResult();
}
}
}
}

View File

@@ -0,0 +1,56 @@
namespace ElectronNET.Runtime.Services.SocketBridge
{
using System;
using System.Threading.Tasks;
using ElectronNET.API;
using ElectronNET.Runtime.Data;
internal class SocketBridgeService : LifetimeServiceBase
{
private readonly int socketPort;
private readonly string socketUrl;
private SocketIoFacade socket;
public SocketBridgeService(int socketPort)
{
this.socketPort = socketPort;
this.socketUrl = $"http://localhost:{this.socketPort}";
}
public int SocketPort => this.socketPort;
internal SocketIoFacade Socket => this.socket;
protected override Task StartCore()
{
this.socket = new SocketIoFacade(this.socketUrl);
this.socket.BridgeConnected += this.Socket_BridgeConnected;
this.socket.BridgeDisconnected += this.Socket_BridgeDisconnected;
Task.Run(this.Connect);
return Task.CompletedTask;
}
protected override Task StopCore()
{
this.socket.DisposeSocket();
return Task.CompletedTask;
}
private void Connect()
{
this.socket.Connect();
this.TransitionState(LifetimeState.Started);
}
private void Socket_BridgeDisconnected(object sender, EventArgs e)
{
this.TransitionState(LifetimeState.Stopped);
}
private void Socket_BridgeConnected(object sender, EventArgs e)
{
this.TransitionState(LifetimeState.Ready);
}
}
}

View File

@@ -0,0 +1,163 @@
namespace ElectronNET.Runtime
{
using System;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Reflection;
using ElectronNET.Runtime.Controllers;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Helpers;
internal class StartupManager
{
public void Initialize()
{
try
{
ElectronNetRuntime.BuildInfo = this.GatherBuildInfo();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
this.CollectProcessData();
this.SetElectronExecutable();
ElectronNetRuntime.StartupMethod = this.DetectAppTypeAndStartup();
Console.WriteLine((string)("Evaluated StartupMethod: " + ElectronNetRuntime.StartupMethod));
if (ElectronNetRuntime.DotnetAppType != DotnetAppType.AspNetCoreApp)
{
ElectronNetRuntime.RuntimeControllerCore = this.CreateRuntimeController();
}
}
private RuntimeControllerBase CreateRuntimeController()
{
switch (ElectronNetRuntime.StartupMethod)
{
case StartupMethod.PackagedDotnetFirst:
case StartupMethod.UnpackedDotnetFirst:
return new RuntimeControllerDotNetFirst();
case StartupMethod.PackagedElectronFirst:
case StartupMethod.UnpackedElectronFirst:
return new RuntimeControllerElectronFirst();
default:
throw new ArgumentOutOfRangeException();
}
return null;
}
private StartupMethod DetectAppTypeAndStartup()
{
var isLaunchedByDotNet = LaunchOrderDetector.CheckIsLaunchedByDotNet();
var isUnPackaged = UnpackagedDetector.CheckIsUnpackaged();
if (isLaunchedByDotNet)
{
if (isUnPackaged)
{
return StartupMethod.UnpackedDotnetFirst;
}
return StartupMethod.PackagedDotnetFirst;
}
else
{
if (isUnPackaged)
{
return StartupMethod.UnpackedElectronFirst;
}
return StartupMethod.PackagedElectronFirst;
}
}
private void CollectProcessData()
{
var argsList = Environment.GetCommandLineArgs().ToImmutableList();
ElectronNetRuntime.ProcessArguments = argsList;
var portArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronPortArgumentName, StringComparison.OrdinalIgnoreCase));
if (portArg != null)
{
var parts = portArg.Split('=', StringSplitOptions.TrimEntries);
if (parts.Length > 1 && int.TryParse(parts[1], NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var result))
{
ElectronNetRuntime.ElectronSocketPort = result;
Console.WriteLine("Use Electron Port: " + result);
}
}
var pidArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronPidArgumentName, StringComparison.OrdinalIgnoreCase));
if (pidArg != null)
{
var parts = pidArg.Split('=', StringSplitOptions.TrimEntries);
if (parts.Length > 1 && int.TryParse(parts[1], NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var result))
{
ElectronNetRuntime.ElectronProcessId = result;
Console.WriteLine("Electron Process ID: " + result);
}
}
}
private void SetElectronExecutable()
{
string executable = ElectronNetRuntime.BuildInfo.ElectronExecutable;
if (string.IsNullOrEmpty(executable))
{
throw new Exception("AssemblyMetadataAttribute 'ElectronExecutable' could not be found!");
}
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
executable += ".exe";
}
ElectronNetRuntime.ElectronExecutable = executable;
}
private BuildInfo GatherBuildInfo()
{
var buildInfo = new BuildInfo();
var attributes = Assembly.GetEntryAssembly()?.GetCustomAttributes<AssemblyMetadataAttribute>().ToList();
if (attributes?.Count > 0)
{
buildInfo.ElectronExecutable = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronExecutable))?.Value;
buildInfo.ElectronVersion = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronVersion))?.Value;
buildInfo.RuntimeIdentifier = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.RuntimeIdentifier))?.Value;
buildInfo.ElectronSingleInstance = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronSingleInstance))?.Value;
buildInfo.Title = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.Title))?.Value;
buildInfo.Version = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.Version))?.Value;
buildInfo.BuildConfiguration = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.BuildConfiguration))?.Value;
var isAspNet = attributes.FirstOrDefault(e => e.Key == "IsAspNet")?.Value;
if (isAspNet?.Length > 0 && bool.TryParse(isAspNet, out var res) && res)
{
ElectronNetRuntime.DotnetAppType = DotnetAppType.AspNetCoreApp;
}
var httpPort = attributes.FirstOrDefault(e => e.Key == "AspNetHttpPort")?.Value;
if (httpPort?.Length > 0 && int.TryParse(httpPort, out var port))
{
ElectronNetRuntime.AspNetWebPort = port;
}
}
return buildInfo;
}
}
}