diff --git a/src/ElectronNET.API/Common/Extensions.cs b/src/ElectronNET.API/Common/Extensions.cs
new file mode 100644
index 0000000..8d9a90a
--- /dev/null
+++ b/src/ElectronNET.API/Common/Extensions.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/ElectronNET.API/ElectronNetRuntime.cs b/src/ElectronNET.API/ElectronNetRuntime.cs
new file mode 100644
index 0000000..bf3ce4e
--- /dev/null
+++ b/src/ElectronNET.API/ElectronNetRuntime.cs
@@ -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";
+
+ /// Initializes the class.
+ 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 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 OnAppReadyCallback { get; set; }
+
+ internal static SocketIoFacade GetSocket()
+ {
+ return RuntimeControllerCore?.Socket;
+ }
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerBase.cs b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerBase.cs
new file mode 100644
index 0000000..c80c0d6
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerBase.cs
@@ -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;
+ }
+
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs
new file mode 100644
index 0000000..7b8e888
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerDotNetFirst.cs
@@ -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;
+ }
+
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs
new file mode 100644
index 0000000..c8b9031
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Controllers/RuntimeControllerElectronFirst.cs
@@ -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;
+ }
+
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Data/BuildInfo.cs b/src/ElectronNET.API/Runtime/Data/BuildInfo.cs
new file mode 100644
index 0000000..cb71aa0
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Data/BuildInfo.cs
@@ -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; }
+ }
+}
\ No newline at end of file
diff --git a/src/ElectronNET.API/Runtime/Data/DotnetAppType.cs b/src/ElectronNET.API/Runtime/Data/DotnetAppType.cs
new file mode 100644
index 0000000..d52328a
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Data/DotnetAppType.cs
@@ -0,0 +1,11 @@
+namespace ElectronNET.Runtime.Data
+{
+ public enum DotnetAppType
+ {
+ /// A plain DotNet cross-platform console app.
+ ConsoleApp,
+
+ /// ASP.NET Core cross-platform app.
+ AspNetCoreApp,
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Data/LifetimeState.cs b/src/ElectronNET.API/Runtime/Data/LifetimeState.cs
new file mode 100644
index 0000000..1785887
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Data/LifetimeState.cs
@@ -0,0 +1,12 @@
+namespace ElectronNET.Runtime.Data
+{
+ public enum LifetimeState
+ {
+ Uninitialized,
+ Starting,
+ Started,
+ Ready,
+ Stopping,
+ Stopped,
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Data/StartupMethod.cs b/src/ElectronNET.API/Runtime/Data/StartupMethod.cs
new file mode 100644
index 0000000..9d3e380
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Data/StartupMethod.cs
@@ -0,0 +1,37 @@
+namespace ElectronNET.Runtime.Data
+{
+ public enum StartupMethod
+ {
+ /// Packaged Electron app where Electron launches the DotNet app.
+ ///
+ /// This is the classic way of ElectrronNET startup.
+ ///
+ PackagedElectronFirst,
+
+ /// Packaged Electron app where DotNet launches the Electron prozess.
+ ///
+ /// Provides better ways for managing the overall app lifecycle.
+ /// On the command lines, this is "dotnetpacked"
+ ///
+ PackagedDotnetFirst,
+
+ /// Unpackacked execution for debugging the Electron process and NodeJS.
+ ///
+ /// 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"
+ ///
+ UnpackedElectronFirst,
+
+
+ /// Unpackacked execution for debugging the DotNet process.
+ ///
+ /// 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"
+ ///
+ UnpackedDotnetFirst,
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Helpers/LaunchOrderDetector.cs b/src/ElectronNET.API/Runtime/Helpers/LaunchOrderDetector.cs
new file mode 100644
index 0000000..8320cfe
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Helpers/LaunchOrderDetector.cs
@@ -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>();
+
+ 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;
+ }
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Helpers/PortHelper.cs b/src/ElectronNET.API/Runtime/Helpers/PortHelper.cs
new file mode 100644
index 0000000..3a14108
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Helpers/PortHelper.cs
@@ -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;
+ }
+ }
+
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs b/src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs
new file mode 100644
index 0000000..d4fbd9d
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs
@@ -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>();
+
+ 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;
+ }
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/IElectronNetRuntimeController.cs b/src/ElectronNET.API/Runtime/IElectronNetRuntimeController.cs
new file mode 100644
index 0000000..4b69c3c
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/IElectronNetRuntimeController.cs
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs
new file mode 100644
index 0000000..57056fe
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs
@@ -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;
+
+ ///
+ /// Launches and manages the Electron app process.
+ ///
+ [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;
+
+ /// Initializes a new instance of the class.
+ /// The is debug.
+ /// Name of the electron.
+ /// The extraArguments.
+ /// The socket port.
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessBase.cs b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessBase.cs
new file mode 100644
index 0000000..969c807
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessBase.cs
@@ -0,0 +1,15 @@
+namespace ElectronNET.Runtime.Services.ElectronProcess
+{
+ using System.ComponentModel;
+
+ ///
+ /// Manages the Electron app process.
+ ///
+ [Localizable(false)]
+ internal abstract class ElectronProcessBase : LifetimeServiceBase
+ {
+ protected ElectronProcessBase()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessPassive.cs b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessPassive.cs
new file mode 100644
index 0000000..7d13b3f
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessPassive.cs
@@ -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;
+
+ ///
+ /// Launches and manages the Electron app process.
+ ///
+ [Localizable(false)]
+ internal class ElectronProcessPassive : ElectronProcessBase
+ {
+ private readonly int pid;
+ private Process process;
+
+ /// Initializes a new instance of the class.
+ ///
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ElectronNET.API/Runtime/Services/LifetimeServiceBase.cs b/src/ElectronNET.API/Runtime/Services/LifetimeServiceBase.cs
new file mode 100644
index 0000000..c2bfc6f
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Services/LifetimeServiceBase.cs
@@ -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();
+ }
+ }
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs b/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs
new file mode 100644
index 0000000..1253051
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/ElectronNET.API/Runtime/StartupManager.cs b/src/ElectronNET.API/Runtime/StartupManager.cs
new file mode 100644
index 0000000..c0e98ab
--- /dev/null
+++ b/src/ElectronNET.API/Runtime/StartupManager.cs
@@ -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().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;
+ }
+ }
+}