mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-02-04 05:34:51 +00:00
ElectronNET.API: Add new runtime code (for launch, lifecycle and service orchestration)
This commit is contained in:
37
src/ElectronNET.API/Common/Extensions.cs
Normal file
37
src/ElectronNET.API/Common/Extensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/ElectronNET.API/ElectronNetRuntime.cs
Normal file
55
src/ElectronNET.API/ElectronNetRuntime.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
19
src/ElectronNET.API/Runtime/Data/BuildInfo.cs
Normal file
19
src/ElectronNET.API/Runtime/Data/BuildInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
11
src/ElectronNET.API/Runtime/Data/DotnetAppType.cs
Normal file
11
src/ElectronNET.API/Runtime/Data/DotnetAppType.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
12
src/ElectronNET.API/Runtime/Data/LifetimeState.cs
Normal file
12
src/ElectronNET.API/Runtime/Data/LifetimeState.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace ElectronNET.Runtime.Data
|
||||
{
|
||||
public enum LifetimeState
|
||||
{
|
||||
Uninitialized,
|
||||
Starting,
|
||||
Started,
|
||||
Ready,
|
||||
Stopping,
|
||||
Stopped,
|
||||
}
|
||||
}
|
||||
37
src/ElectronNET.API/Runtime/Data/StartupMethod.cs
Normal file
37
src/ElectronNET.API/Runtime/Data/StartupMethod.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
72
src/ElectronNET.API/Runtime/Helpers/LaunchOrderDetector.cs
Normal file
72
src/ElectronNET.API/Runtime/Helpers/LaunchOrderDetector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/ElectronNET.API/Runtime/Helpers/PortHelper.cs
Normal file
26
src/ElectronNET.API/Runtime/Helpers/PortHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
109
src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs
Normal file
109
src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/ElectronNET.API/Runtime/IElectronNetRuntimeController.cs
Normal file
33
src/ElectronNET.API/Runtime/IElectronNetRuntimeController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/ElectronNET.API/Runtime/Services/LifetimeServiceBase.cs
Normal file
135
src/ElectronNET.API/Runtime/Services/LifetimeServiceBase.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/ElectronNET.API/Runtime/StartupManager.cs
Normal file
163
src/ElectronNET.API/Runtime/StartupManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user