Changed EnableDetailedErrors to be conditional based on:
- Debugger attached (DebuggerHelper.IsAttached)
- context.HostingEnvironment.IsDevelopment()
Updated ConfigureServices to accept WebHostBuilderContext to properly
access the hosting environment instead of directly reading environment
variables. This prevents detailed error messages from being exposed in
production builds, which is important for security.
Created a new DebuggerHelper class with Lazy<bool> initialization that is
shared by both LaunchOrderDetector and UnpackagedDetector. This eliminates
code duplication and provides a cleaner, more maintainable solution.
- Added DebuggerHelper.cs with lazy-initialized IsAttached property
- Refactored LaunchOrderDetector to use DebuggerHelper
- Refactored UnpackagedDetector to use DebuggerHelper
- Removed nullable bool pattern in favor of Lazy<bool>
When an EPIPE error is detected (indicating the .NET process has terminated),
Electron now automatically quits gracefully instead of just suppressing the error.
This ensures the Electron window doesn't remain open after the backend dies.
Uses setImmediate to allow any pending operations to complete before quitting,
and includes a flag to prevent multiple quit attempts.
The previous fix using try-catch wasn't sufficient because EPIPE errors
are thrown asynchronously at the socket level when writing to stdout/stderr.
Added error event handlers to process.stdout and process.stderr to catch
and suppress EPIPE errors at the source, preventing the error dialog entirely.
- Added SafeLogger class to wrap SignalR logging and prevent EPIPE errors
- Enhanced safeConsole wrapper to include warn method
- Replaced all console calls in SignalRBridge with safeConsole
- Configured SignalR to use custom SafeLogger instead of default ConsoleLogger
This prevents the 'broken pipe, write' error dialog when the .NET process
is killed before the Electron process can cleanly shut down.
Performance optimization:
- Refactored 18 sequential require() calls to load in parallel
- Use Promise.all() to load all API modules simultaneously
- Added comprehensive startup timing measurements
Timing breakdown (before → after):
- Module loading: ~900-1200ms → 719ms (20-40% faster)
- Total Electron startup: Measured at 2.3 seconds
- SignalR connection: 884ms
- Host ready signal: 33ms
Implementation:
- Load all modules in parallel using Promise.resolve().then()
- Organize modules by priority (critical, secondary, utility)
- Maintain backward compatibility with global variable assignments
- Add console.time() measurements for each phase
Benefits:
- Faster perceived startup time
- Non-blocking module initialization
- Clear visibility into startup bottlenecks
- Foundation for future lazy loading optimizations
This is the first phase of startup optimization, targeting the
biggest bottleneck (sequential module loading) with the least effort.
Middleware logging:
- Log successful authentication with cookie setting
- Log failed authentication attempts with path and remote IP
- Log token prefix (first 8 chars) for invalid tokens, never full token
- Added structured logging with ILogger<T>
Electron error handling:
- Detect 401 authentication errors in SignalR connection
- Provide helpful error message about --authtoken parameter
- Differentiate auth errors from other connection failures
Documentation:
- Added XML comments explaining security model
- Documented token generation rationale (128-bit entropy)
- Clarified middleware validation flow in comments
Security:
- Never log full token values
- Generic error messages to prevent information leakage
- Failed auth attempts logged for security monitoring
- Added checks for undefined variables before cleanup
- Socket.IO resources (server, apiProcess, io) only cleaned up in legacy mode
- SignalR connection properly stopped in SignalR mode
- Improved error messages to identify which cleanup failed
- Prevents 'Cannot read properties of undefined' error on app quit
- Modified main.js to pass authToken to SignalRBridge constructor
- Updated signalr-bridge.js to append token to hub URL
- Removed skip logic for negotiate endpoint in middleware
- SignalR negotiate request now includes token for authentication
- Cookie is set after successful negotiate for subsequent requests
This enables SignalR connections to authenticate using the same
token-based mechanism as HTTP requests, completing the authentication
flow for both Blazor pages and SignalR hub communication.
- Extract authtoken from command-line in main.js
- Store token in global.authToken for access by API modules
- Append token to initial window URL as query parameter
- Update both JS and TS versions of browserWindows
- First request will be: http://localhost:PORT/?token=GUID
- Add authenticationToken field to store GUID
- Generate secure token using Guid.NewGuid().ToString('N')
- Pass token to Electron via --authtoken command-line parameter
- Token is 32 hex characters with 128 bits of entropy
HSTS and HTTPS redirection are designed for public web servers, not
desktop applications:
- ASP.NET Core only listens on http://localhost (local-only)
- No man-in-the-middle risk for same-machine communication
- HTTPS would require certificate setup with no security benefit
- HTTPS overhead slows down local IPC unnecessarily
Electron apps should use plain HTTP for localhost communication.
SignalR's MapHub<T>() automatically enables WebSocket support, making
explicit UseWebSockets() redundant. SignalR also supports fallback
transports (Server-Sent Events, Long Polling) if WebSockets are unavailable.
Updated documentation to reflect this and clarify that WebSockets are
enabled automatically by MapHub().
Changed from app.UseEndpoints() pattern to direct app.MapHub() call,
following modern ASP.NET Core conventions. This removes the analyzer
warning while maintaining the same functionality.
Created detailed summary document incorporating:
- All 6 implementation phases (complete)
- Usage examples with code snippets
- Architecture decisions and rationale
- Critical fixes and their solutions
- Blazor Server integration considerations
- Backward compatibility guarantees
- Known limitations and future enhancements
- Success metrics and validation
Document serves as both implementation reference and user guide.
Added comprehensive code comments explaining:
- RuntimeControllerAspNetDotnetFirstSignalR: .NET-first startup flow and key differences from Socket.IO
- SignalRFacade: Type conversion handling and event propagation details
- signalr-bridge.js: Socket.IO compatibility layer and arg handling
- main.js: Keep-alive window pattern and SignalR startup sequence
Comments focus on explaining WHY decisions were made, not just WHAT the code does.
- Fix middleware order: UseAntiforgery must be between UseRouting and UseEndpoints
- Add UseStaticFiles() to serve wwwroot content
- Fix scoped CSS bundle reference: use lowercase 'electronnet-samples-blazorsignalr.styles.css' to match generated asset name
- Add HTTP request logging for debugging
- Enable detailed logging for routing and static files in development
- Destroy keep-alive window when first real window is created (enables proper window-all-closed behavior)
- Update ElectronNetRuntime.AspNetWebPort with actual port after Kestrel starts (fixes http://localhost:0 issue)
- Fix duplicate SignalR connection in main.js (removed redundant connect block)
- Fix race condition: Electron now signals 'electron-host-ready' after loading API modules
- Fix type conversion in SignalRFacade for event handlers (handle JsonElement and numeric types)
- Fix SignalR argument mismatch: pass args as array instead of spread, use object[] instead of params
- .NET now waits for electron-host-ready before calling app ready callback
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.
- Add WebSockets middleware to ASP.NET pipeline
- Move HTTPS redirect to production only
- Add extensive debug logging in startSignalRApiBridge
- Enable Warning level logging in SignalR client
Issue: signalRBridge.connect() hangs and never resolves
- Connection starts but never completes
- No error messages from SignalR client
- ASP.NET shuts down after timeout
- ElectronHub.OnConnectedAsync never called
Next: Investigate why WebSocket connection doesn't establish
- Wrap all console.log/error calls in try-catch to handle EPIPE
- Disable SignalR client logging (LogLevel.None)
- Prevents Electron crash when console pipes are closed
This fixes the 'EPIPE: broken pipe, write' error that was preventing
the Electron window from displaying.
- Add RunReadyCallback execution when SignalR connects
- This triggers window creation and other app initialization
- Fixes missing variables in main.js (desktopCapturer, electronHostHook, touchBar, shellApi)
- Window now opens successfully in SignalR mode
Known issue: EPIPE console error when logging to closed pipe (to be fixed)
- Update signalr-bridge.js to handle .NET→Electron events via 'event' channel
- Add socket.io-compatible .on() and .emit() methods to SignalRBridge
- Update main.js to load all Electron API modules with SignalR bridge
- Update SignalRFacade.Emit() to send events via 'event' channel
- Add ElectronHub.ElectronEvent() to receive Electron→.NET events
- Add SignalRFacade.TriggerEvent() to invoke .NET event handlers
- Remove duplicate ElectronEvent method from hub
This enables full bidirectional communication:
- .NET can call Electron APIs via Emit (e.g., createBrowserWindow)
- Electron can send events back to .NET (e.g., BrowserWindowCreated)
- Event handlers registered via On/Once now work with SignalR
- Create IFacade interface defining common API for SocketIO and SignalR
- Update SocketIoFacade to implement IFacade
- Update SignalRFacade to implement IFacade (add Connect no-op)
- Update RuntimeControllerBase.Socket to return IFacade
- Update RuntimeControllerAspNetBase.Socket to return IFacade
- Update RuntimeControllerDotNetFirst.Socket to return IFacade
- Update ElectronNetRuntime.GetSocket() to return IFacade
- Update BridgeConnector.Socket to return IFacade
This enables polymorphic usage of both facades throughout the codebase
and prepares for full Electron API integration with SignalR mode.