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; + } + } +}