From 108ef19a3b1d09ee6a6a1d828d207cee62e0c166 Mon Sep 17 00:00:00 2001 From: Pierre Arnaud Date: Fri, 30 Jan 2026 15:29:48 +0100 Subject: [PATCH] CRITICAL FIX: Prevent Electron from quitting in SignalR mode ROOT CAUSE: Electron quits when app.on('ready') completes with 0 windows. In SignalR mode, no windows are created immediately, so Electron exits, triggering ElectronProcess_Stopped and shutting down ASP.NET. SOLUTION: Create an invisible 1x1 keep-alive window in SignalR mode to prevent Electron from quitting while waiting for SignalR connection. Also: - Make app.on('ready') async and await startSignalRApiBridge() - Add window-all-closed handler for SignalR mode - Add extensive debug logging to track lifecycle - Don't subscribe to electronProcess.Ready in SignalR controller This fixes the premature shutdown that prevented SignalR connection. --- .../API/WebHostBuilderExtensions.cs | 6 ++- ...ntimeControllerAspNetDotnetFirstSignalR.cs | 11 +++-- src/ElectronNET.Host/main.js | 42 +++++++++++++++++-- .../Program.Minimal.cs.txt | 21 ++++++++++ .../Program.cs | 26 ++++++++++-- .../appsettings.Development.json | 4 +- 6 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 src/ElectronNET.Samples.BlazorSignalR/Program.Minimal.cs.txt diff --git a/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs b/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs index 1a9b82b..83daafe 100644 --- a/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs +++ b/src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs @@ -107,7 +107,11 @@ break; case StartupMethod.PackagedDotnetFirstSignalR: case StartupMethod.UnpackedDotnetFirstSignalR: - services.AddSignalR(); + services.AddSignalR(options => + { + // Enable detailed errors for debugging + options.EnableDetailedErrors = true; + }); services.AddSingleton(); break; default: diff --git a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs index 653fbcb..c81be1a 100644 --- a/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs +++ b/src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs @@ -68,6 +68,8 @@ namespace ElectronNET.AspNet.Runtime protected override Task StopCore() { + Console.WriteLine("[RuntimeControllerAspNetDotnetFirstSignalR] StopCore called!"); + Console.WriteLine($"[RuntimeControllerAspNetDotnetFirstSignalR] Stack trace: {Environment.StackTrace}"); this.electronProcess?.Stop(); this.signalRFacade?.DisposeSocket(); return Task.CompletedTask; @@ -107,15 +109,17 @@ namespace ElectronNET.AspNet.Runtime Console.WriteLine($"[RuntimeControllerAspNetDotnetFirstSignalR] Launching: {args}"); this.electronProcess = new ElectronProcessActive(isUnPacked, ElectronNetRuntime.ElectronExecutable, args, this.port.Value); - this.electronProcess.Ready += this.ElectronProcess_Ready; + // Note: We do NOT subscribe to electronProcess.Ready in SignalR mode + // The "ready" signal comes from SignalR connection, not stdout this.electronProcess.Stopped += this.ElectronProcess_Stopped; _ = this.electronProcess.Start(); } + // Keep this method but it won't be called in SignalR mode private void ElectronProcess_Ready(object sender, EventArgs e) { - Console.WriteLine("[RuntimeControllerAspNetDotnetFirstSignalR] Electron ready"); - this.TransitionState(LifetimeState.Started); + // Not used in SignalR mode - ready state comes from SignalR connection + Console.WriteLine("[RuntimeControllerAspNetDotnetFirstSignalR] Electron process started (stdout ready)"); } private async void SignalRFacade_Connected(object sender, EventArgs e) @@ -146,6 +150,7 @@ namespace ElectronNET.AspNet.Runtime private void ElectronProcess_Stopped(object sender, EventArgs e) { + Console.WriteLine("[RuntimeControllerAspNetDotnetFirstSignalR] ElectronProcess_Stopped event fired!"); this.HandleStopped(); } diff --git a/src/ElectronNET.Host/main.js b/src/ElectronNET.Host/main.js index d8d8334..490182d 100644 --- a/src/ElectronNET.Host/main.js +++ b/src/ElectronNET.Host/main.js @@ -159,7 +159,7 @@ function getForwardedArgs() { const forwardedArgs = getForwardedArgs(); -app.on('ready', () => { +app.on('ready', async () => { // Fix ERR_UNKNOWN_URL_SCHEME using file protocol // https://github.com/electron/electron/issues/23757 ////protocol.registerFileProtocol('file', (request, callback) => { @@ -179,7 +179,24 @@ app.on('ready', () => { return; } console.log('[Electron] Starting in SignalR mode'); - startSignalRApiBridge(electronUrl); + + // Create an invisible window to keep Electron alive + // (otherwise Electron quits when the ready handler completes with 0 windows) + const { BrowserWindow } = require('electron'); + const keepAliveWindow = new BrowserWindow({ + show: false, + width: 1, + height: 1 + }); + console.log('[Electron] Created keep-alive window'); + + // Prevent Electron from quitting when all windows are closed + app.on('window-all-closed', () => { + // Don't quit - we're in SignalR mode and windows will be created via API + console.log('[Electron] All windows closed, but staying alive (SignalR mode)'); + }); + + await startSignalRApiBridge(electronUrl); return; } @@ -406,8 +423,25 @@ function startSocketApiBridge(port) { } async function startSignalRApiBridge(baseUrl) { - console.log('[SignalRBridge] Starting SignalR API bridge...'); - console.log(`[SignalRBridge] Base URL: ${baseUrl}`); + try { + console.log('[SignalRBridge] Starting SignalR API bridge...'); + console.log(`[SignalRBridge] Base URL: ${baseUrl}`); + + const { SignalRBridge } = require('./api/signalr-bridge'); + const hubUrl = `${baseUrl}/electron-hub`; + + console.log(`[SignalRBridge] Connecting to hub: ${hubUrl}`); + + const signalRBridge = new SignalRBridge(hubUrl); + const connected = await signalRBridge.connect(); + + console.log(`[SignalRBridge] connect() returned: ${connected}`); + } catch (error) { + console.error('[SignalRBridge] FATAL ERROR in startSignalRApiBridge:', error); + console.error('[SignalRBridge] Stack:', error.stack); + app.quit(); + return; + } const { SignalRBridge } = require('./api/signalr-bridge'); const hubUrl = `${baseUrl}/electron-hub`; diff --git a/src/ElectronNET.Samples.BlazorSignalR/Program.Minimal.cs.txt b/src/ElectronNET.Samples.BlazorSignalR/Program.Minimal.cs.txt new file mode 100644 index 0000000..d45983b --- /dev/null +++ b/src/ElectronNET.Samples.BlazorSignalR/Program.Minimal.cs.txt @@ -0,0 +1,21 @@ +using ElectronNET.API; +using ElectronNET.AspNet.Hubs; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddElectron(); + +// Configure Electron.NET with SignalR mode +builder.WebHost.UseElectron(args, async () => +{ + Console.WriteLine("[TEST] App ready callback started"); +}); + +var app = builder.Build(); + +// Absolute minimal setup +app.MapHub("/electron-hub"); + +Console.WriteLine("[TEST] Application starting..."); + +app.Run(); diff --git a/src/ElectronNET.Samples.BlazorSignalR/Program.cs b/src/ElectronNET.Samples.BlazorSignalR/Program.cs index d821131..84265a5 100644 --- a/src/ElectronNET.Samples.BlazorSignalR/Program.cs +++ b/src/ElectronNET.Samples.BlazorSignalR/Program.cs @@ -8,6 +8,17 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); +// Add CORS for SignalR +builder.Services.AddCors(options => +{ + options.AddPolicy("ElectronPolicy", policy => + { + policy.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + builder.Services.AddElectron(); // Configure Electron.NET with SignalR mode @@ -44,6 +55,12 @@ var app = builder.Build(); // Enable WebSockets (required for SignalR) app.UseWebSockets(); +// Enable routing +app.UseRouting(); + +// Enable CORS +app.UseCors("ElectronPolicy"); + // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { @@ -55,10 +72,13 @@ if (!app.Environment.IsDevelopment()) app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); -app.UseAntiforgery(); +// Use endpoints for SignalR hub +app.UseEndpoints(endpoints => +{ + endpoints.MapHub("/electron-hub"); +}); -// Map the Electron SignalR hub (required for SignalR mode) -app.MapElectronHub(); +app.UseAntiforgery(); app.MapStaticAssets(); app.MapRazorComponents() diff --git a/src/ElectronNET.Samples.BlazorSignalR/appsettings.Development.json b/src/ElectronNET.Samples.BlazorSignalR/appsettings.Development.json index 0c208ae..8153d14 100644 --- a/src/ElectronNET.Samples.BlazorSignalR/appsettings.Development.json +++ b/src/ElectronNET.Samples.BlazorSignalR/appsettings.Development.json @@ -2,7 +2,9 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.SignalR": "Debug", + "Microsoft.AspNetCore.Http.Connections": "Debug" } } }