diff --git a/src/ElectronNET.AspNet/API/ServiceCollectionExtensions.cs b/src/ElectronNET.AspNet/API/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..b12f636 --- /dev/null +++ b/src/ElectronNET.AspNet/API/ServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +namespace ElectronNET.API +{ + using Microsoft.Extensions.DependencyInjection; + + /// + /// + /// + public static class ServiceCollectionExtensions + { + /// + /// Adds the Members to the Service Collection + /// + public static IServiceCollection AddElectron(this IServiceCollection services) + => services + // adding in this manner to ensure late binding. + .AddSingleton(_ => IpcMain.Instance) + .AddSingleton(_ => App.Instance) + .AddSingleton(_ => AutoUpdater.Instance) + .AddSingleton(_ => WindowManager.Instance) + .AddSingleton(_ => Menu.Instance) + .AddSingleton(_ => Dialog.Instance) + .AddSingleton(_ => Notification.Instance) + .AddSingleton(_ => Tray.Instance) + .AddSingleton(_ => GlobalShortcut.Instance) + .AddSingleton(_ => Shell.Instance) + .AddSingleton(_ => Screen.Instance) + .AddSingleton(_ => Clipboard.Instance) + .AddSingleton(_ => HostHook.Instance) + .AddSingleton(_ => PowerMonitor.Instance) + .AddSingleton(_ => NativeTheme.Instance) + .AddSingleton(_ => Dock.Instance); + } +} diff --git a/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs b/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs new file mode 100644 index 0000000..c607836 --- /dev/null +++ b/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs @@ -0,0 +1,103 @@ +namespace ElectronNET.API +{ + using System; + using System.IO; + using System.Threading.Tasks; + using ElectronNET.AspNet; + using ElectronNET.AspNet.Runtime; + using ElectronNET.Runtime; + using ElectronNET.Runtime.Data; + using ElectronNET.Runtime.Helpers; + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Provides extension methods for to enable Electron.NET + /// integration in ASP.NET Core applications (including Razor Pages) using the WebHost-based hosting model. + /// + /// + /// Call this extension during web host configuration (for example, inside ConfigureWebHostDefaults in Program.cs) + /// to wire up Electron with any command-line arguments and an optional application-ready callback. + /// + public static class WebHostBuilderExtensions + { + /// + /// Adds Electron.NET support to the current ASP.NET Core web host and registers an application-ready callback. + /// + /// The to extend. + /// The command-line arguments passed to the process. + /// + /// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization. + /// + /// + /// The same instance to enable fluent configuration. + /// + /// + /// + /// using Microsoft.AspNetCore.Hosting; + /// using Microsoft.Extensions.Hosting; + /// using ElectronNET.API; + /// + /// public class Program + /// { + /// public static void Main(string[] args) + /// { + /// Host.CreateDefaultBuilder(args) + /// .ConfigureWebHostDefaults(webBuilder => + /// { + /// webBuilder.UseStartup<Startup>(); + /// webBuilder.UseElectron(args, async () => + /// { + /// // Create the main browser window or perform other startup tasks. + /// }); + /// }) + /// .Build() + /// .Run(); + /// } + /// } + /// + /// + public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func onAppReadyCallback) + { + ElectronNetRuntime.OnAppReadyCallback = onAppReadyCallback; + + var webPort = PortHelper.GetFreePort(ElectronNetRuntime.AspNetWebPort ?? ElectronNetRuntime.DefaultWebPort); + ElectronNetRuntime.AspNetWebPort = webPort; + + // check for the content folder if its exists in base director otherwise no need to include + // It was used before because we are publishing the project which copies everything to bin folder and contentroot wwwroot was folder there. + // now we have implemented the live reload if app is run using /watch then we need to use the default project path. + if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot")) + { + builder = builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory) + .UseUrls("http://localhost:" + webPort); + } + else + { + builder = builder.UseUrls("http://localhost:" + webPort); + } + + builder = builder.ConfigureServices(services => + { + services.AddTransient(); + services.AddSingleton(); + + switch (ElectronNetRuntime.StartupMethod) + { + case StartupMethod.PackagedElectronFirst: + case StartupMethod.UnpackedElectronFirst: + services.AddSingleton(); + break; + case StartupMethod.PackagedDotnetFirst: + case StartupMethod.UnpackedDotnetFirst: + services.AddSingleton(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + }); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/ElectronNET.AspNet/ElectronNET.AspNet.csproj b/src/ElectronNET.AspNet/ElectronNET.AspNet.csproj new file mode 100644 index 0000000..d3a95fe --- /dev/null +++ b/src/ElectronNET.AspNet/ElectronNET.AspNet.csproj @@ -0,0 +1,44 @@ + + + + + + net6.0;net8.0 + ..\..\artifacts + $(PackageNamePrefix).AspNet + $(PackageId) + $(DescriptionFirstPart) This package contains the ASP.Net Core integration. + true + True + snupkg + disable + ElectronNET + + + 1701;1702;4014;CS4014;CA1416;CS1591 + + + 1701;1702;4014;CS4014;CA1416;CS1591 + + + 1701;1702;4014;CS4014;CA1416;CS1591 + + + 1701;1702;4014;CS4014;CA1416;CS1591 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + \ No newline at end of file diff --git a/src/ElectronNET.AspNet/PackageIcon.png b/src/ElectronNET.AspNet/PackageIcon.png new file mode 100644 index 0000000..10d55eb Binary files /dev/null and b/src/ElectronNET.AspNet/PackageIcon.png differ diff --git a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetBase.cs b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetBase.cs new file mode 100644 index 0000000..65487df --- /dev/null +++ b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetBase.cs @@ -0,0 +1,132 @@ +namespace ElectronNET.AspNet.Runtime +{ + using System; + using System.Threading.Tasks; + using ElectronNET.API; + using ElectronNET.Common; + using ElectronNET.Runtime.Controllers; + using ElectronNET.Runtime.Data; + using ElectronNET.Runtime.Services.SocketBridge; + + internal abstract class RuntimeControllerAspNetBase : RuntimeControllerBase + { + private readonly AspNetLifetimeAdapter aspNetLifetimeAdapter; + private SocketBridgeService socketBridge; + + protected RuntimeControllerAspNetBase(AspNetLifetimeAdapter aspNetLifetimeAdapter) + { + this.aspNetLifetimeAdapter = aspNetLifetimeAdapter; + this.aspNetLifetimeAdapter.Ready += this.AspNetLifetimeAdapter_Ready; + this.aspNetLifetimeAdapter.Stopping += this.AspNetLifetimeAdapter_Stopping; + this.aspNetLifetimeAdapter.Stopped += this.AspNetLifetimeAdapter_Stopped; + + ElectronNetRuntime.RuntimeControllerCore = this; + } + + internal override SocketBridgeService SocketBridge => this.socketBridge; + + 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"); + } + } + + protected void CreateSocketBridge(int port) + { + this.socketBridge = new SocketBridgeService(port); + this.socketBridge.Ready += this.SocketBridge_Ready; + this.socketBridge.Stopped += this.SocketBridge_Stopped; + this.socketBridge.Start(); + } + + protected void HandleReady() + { + if (this.SocketBridge.IsReady() && + this.ElectronProcess.IsReady() && + this.aspNetLifetimeAdapter.IsReady()) + { + this.TransitionState(LifetimeState.Ready); + Task.Run(this.RunReadyCallback); + } + } + + protected void HandleStopped() + { + this.TransitionState(LifetimeState.Stopping); + + if (this.SocketBridge.IsNotStopped()) + { + this.SocketBridge.Stop(); + } + + if (this.ElectronProcess.IsNotStopped()) + { + this.ElectronProcess.Stop(); + } + + if (this.aspNetLifetimeAdapter.IsNotStopped()) + { + this.aspNetLifetimeAdapter.Stop(); + } + + if ((this.SocketBridge.IsNullOrStopped()) && + (this.ElectronProcess.IsNullOrStopped()) && + (this.aspNetLifetimeAdapter.IsNullOrStopped())) + { + this.TransitionState(LifetimeState.Stopped); + } + } + + protected abstract override Task StopCore(); + + private void SocketBridge_Ready(object sender, EventArgs e) + { + this.HandleReady(); + } + + private void AspNetLifetimeAdapter_Ready(object sender, EventArgs e) + { + this.HandleReady(); + } + + private void SocketBridge_Stopped(object sender, EventArgs e) + { + this.HandleStopped(); + } + + private void AspNetLifetimeAdapter_Stopped(object sender, EventArgs e) + { + this.HandleStopped(); + } + + private void AspNetLifetimeAdapter_Stopping(object sender, EventArgs e) + { + } + + private async Task RunReadyCallback() + { + if (ElectronNetRuntime.OnAppReadyCallback == null) + { + Console.WriteLine("Warning: Non OnReadyCallback provided in UseElectron() setup."); + return; + } + + try + { + await ElectronNetRuntime.OnAppReadyCallback().ConfigureAwait(false); + } + catch (Exception ex) + { + Console.WriteLine("Exception while executing OnAppReadyCallback. Stopping...\n" + ex); + this.Stop(); + } + } + } +} diff --git a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirst.cs b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirst.cs new file mode 100644 index 0000000..4c76291 --- /dev/null +++ b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirst.cs @@ -0,0 +1,58 @@ +namespace ElectronNET.AspNet.Runtime +{ + using System; + using System.Threading.Tasks; + using ElectronNET.Common; + using ElectronNET.Runtime.Data; + using ElectronNET.Runtime.Helpers; + using ElectronNET.Runtime.Services.ElectronProcess; + + internal class RuntimeControllerAspNetDotnetFirst : RuntimeControllerAspNetBase + { + private ElectronProcessBase electronProcess; + private int? port; + + public RuntimeControllerAspNetDotnetFirst(AspNetLifetimeAdapter aspNetLifetimeAdapter) : base(aspNetLifetimeAdapter) + { + } + + internal override ElectronProcessBase ElectronProcess => this.electronProcess; + + 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(); + } + + protected override Task StopCore() + { + this.electronProcess.Stop(); + return Task.CompletedTask; + } + + private void ElectronProcess_Ready(object sender, EventArgs e) + { + this.TransitionState(LifetimeState.Started); + this.CreateSocketBridge(this.port!.Value); + } + + private void ElectronProcess_Stopped(object sender, EventArgs e) + { + this.HandleStopped(); + } + } +} diff --git a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetElectronFirst.cs b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetElectronFirst.cs new file mode 100644 index 0000000..c9eb069 --- /dev/null +++ b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetElectronFirst.cs @@ -0,0 +1,56 @@ +namespace ElectronNET.AspNet.Runtime +{ + using System; + using System.Threading.Tasks; + using ElectronNET.Runtime.Data; + using ElectronNET.Runtime.Services.ElectronProcess; + + internal class RuntimeControllerAspNetElectronFirst : RuntimeControllerAspNetBase + { + private ElectronProcessBase electronProcess; + private int? port; + + public RuntimeControllerAspNetElectronFirst(AspNetLifetimeAdapter aspNetLifetimeAdapter) : base(aspNetLifetimeAdapter) + { + } + + internal override ElectronProcessBase ElectronProcess => this.electronProcess; + + 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.CreateSocketBridge(this.port!.Value); + + this.electronProcess = new ElectronProcessPassive(ElectronNetRuntime.ElectronProcessId.Value); + this.electronProcess.Stopped += this.ElectronProcess_Stopped; + + this.electronProcess.Start(); + + Task.Run(() => this.TransitionState(LifetimeState.Started)); + + return Task.CompletedTask; + } + + protected override Task StopCore() + { + this.electronProcess.Stop(); + return Task.CompletedTask; + } + + private void ElectronProcess_Stopped(object sender, EventArgs e) + { + this.HandleStopped(); + } + } +} diff --git a/src/ElectronNET.AspNet/Runtime/Helpers/ServerReadyStartupFilter.cs b/src/ElectronNET.AspNet/Runtime/Helpers/ServerReadyStartupFilter.cs new file mode 100644 index 0000000..115a96f --- /dev/null +++ b/src/ElectronNET.AspNet/Runtime/Helpers/ServerReadyStartupFilter.cs @@ -0,0 +1,30 @@ +namespace ElectronNET.AspNet +{ + using System; + using ElectronNET.AspNet.Runtime; + using ElectronNET.Runtime; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.DependencyInjection; + + internal sealed class ServerReadyStartupFilter : IStartupFilter + { + /// + /// Extends the provided and returns an of the same type. + /// + /// The Configure method to extend. + /// A modified . + public Action Configure(Action next) + { + return app => + { + _ = app.ApplicationServices.GetService(); + var runtimeController = app.ApplicationServices.GetService(); + + runtimeController.Start(); + + next(app); + }; + } + } +} diff --git a/src/ElectronNET.AspNet/Runtime/Services/AspNetLifetimeAdapter.cs b/src/ElectronNET.AspNet/Runtime/Services/AspNetLifetimeAdapter.cs new file mode 100644 index 0000000..9c108a0 --- /dev/null +++ b/src/ElectronNET.AspNet/Runtime/Services/AspNetLifetimeAdapter.cs @@ -0,0 +1,27 @@ +namespace ElectronNET.AspNet.Runtime +{ + using System.Threading.Tasks; + using ElectronNET.Runtime.Data; + using ElectronNET.Runtime.Services; + using Microsoft.Extensions.Hosting; + + internal class AspNetLifetimeAdapter : LifetimeServiceBase + { + private readonly IHostApplicationLifetime lifetimeSercice; + + public AspNetLifetimeAdapter(IHostApplicationLifetime lifetimeSercice) + { + this.lifetimeSercice = lifetimeSercice; + + this.lifetimeSercice.ApplicationStarted.Register(() => this.TransitionState(LifetimeState.Ready)); + this.lifetimeSercice.ApplicationStopping.Register(() => this.TransitionState(LifetimeState.Stopping)); + this.lifetimeSercice.ApplicationStopped.Register(() => this.TransitionState(LifetimeState.Stopped)); + } + + protected override Task StopCore() + { + this.lifetimeSercice.StopApplication(); + return Task.CompletedTask; + } + } +}