Compare commits

..

84 Commits

Author SHA1 Message Date
Florian Rappl
b107244eb5 Review corrections 2026-03-05 01:52:29 +01:00
Florian Rappl
14cba046a7 Start with correct port 2026-03-04 09:52:15 +01:00
Florian Rappl
633f4ae224 Respect auth token from Node.js start 2026-03-04 00:02:18 +01:00
Florian Rappl
14ecbb65a3 Compute auth token in Node 2026-03-03 23:54:14 +01:00
Florian Rappl
cf5938450f Use dynamic port by default 2026-03-03 11:11:54 +01:00
Florian Rappl
087b0e22f0 Avoid setting authorization if not present 2026-03-03 01:09:45 +01:00
Florian Rappl
29498b0017 Wait for the port to be read 2026-03-02 22:33:48 +01:00
Florian Rappl
49a24d3fc9 Fixed trailing whitespace 2026-03-02 22:01:33 +01:00
Florian Rappl
4450d629f1 Merge branch 'feature/secure-connection' of https://github.com/ElectronNET/Electron.NET into feature/secure-connection 2026-03-02 21:59:19 +01:00
Florian Rappl
248c64d6dd Auto-determine used port 2026-03-02 21:21:19 +01:00
Florian Rappl
d1e858465c Removed SignalR 2026-03-02 18:03:03 +01:00
Florian Rappl
7837b69999 Always set to info 2026-03-02 16:42:58 +01:00
Florian Rappl
834c22aaa1 Merge branch 'main' of https://github.com/ElectronNET/Electron.NET into feature/secure-connection 2026-02-27 00:37:39 +01:00
Florian Rappl
92e5a6f862 Removed logger 2026-02-23 00:05:33 +01:00
Florian Rappl
16925f401b Renamed facades 2026-02-22 23:56:06 +01:00
Florian Rappl
7e7bc21f2d Merge branch 'develop' of https://github.com/ElectronNET/Electron.NET into feature/secure-connection 2026-02-22 21:51:05 +01:00
Florian Rappl
5eb49b0e54 Removed unnecessary debugger helper 2026-02-05 15:54:23 +01:00
Florian Rappl
2390ba7b2f Removed misleading comment 2026-02-05 15:53:50 +01:00
Florian Rappl
564ec7b191 Added forgotten debug helper 2026-02-05 15:51:48 +01:00
Florian Rappl
32bcfa95ab Restore logging 2026-02-04 11:44:27 +01:00
Florian Rappl
cf735fab4c Original formatting 2026-02-04 11:33:15 +01:00
Florian Rappl
cf1cb18d7a Merge branch 'develop' of https://github.com/ElectronNET/Electron.NET into feature/secure-connection 2026-02-04 11:29:01 +01:00
Florian Rappl
931aec8cd9 Updated lodash 2026-02-02 16:15:19 +01:00
Florian Rappl
bbd1065a05 Merge branch 'epsitec-improvements' into feature/secure-connection 2026-02-02 16:13:18 +01:00
Florian Rappl
0fa0abd093 Merge branch 'develop' of https://github.com/ElectronNET/Electron.NET into feature/secure-connection 2026-02-02 16:01:30 +01:00
Pierre Arnaud
bfd51e64f7 Fix dead-lock on shutdown 2026-01-31 15:37:52 +01:00
Pierre Arnaud
bb4337a31c Cosmetic 2026-01-31 15:37:35 +01:00
Pierre Arnaud
57753eb632 Replace Console.WriteLine with ILogger for startup time
Changed startup time measurement to use structured logging via ILogger
instead of Console.WriteLine. This provides:
- Consistent logging format with other app messages
- Proper log level (Information)
- Structured data (ElapsedMilliseconds as named parameter)
- Better integration with logging infrastructure

Output now appears as:
info: Electron.Startup[0]
      App startup time until Electron launch: 1284 ms
2026-01-31 14:37:32 +01:00
Pierre Arnaud
614673605a Add explicit BrowserRefresh logging filter
Added 'Microsoft.AspNetCore.Watch.BrowserRefresh' to Warning level
to suppress the 'Middleware loaded' and 'Script injected' debug messages
that appear when running with dotnet watch or Ctrl+F5 from Visual Studio.
2026-01-31 14:30:07 +01:00
Pierre Arnaud
52744a1922 Final cleanup: Suppress remaining debug output
- Removed '|| ' prefix from ProcessRunner stdout/stderr redirection
  * Changed to Debug.WriteLine (only visible when debugger attached)
- Suppressed [Startup] timing messages in StartupManager
  * Moved to Debug.WriteLine for diagnostic purposes
- Suppressed 'No testhost detected' messages
- Added logging configuration to suppress ASP.NET Watch debug messages
  * Microsoft.AspNetCore.Watch set to Warning level

Result: Clean console output in development with only meaningful messages.
Timing and debug traces still available when debugger is attached.
2026-01-31 14:27:34 +01:00
Pierre Arnaud
03da5cd7cb Phase 4: Replace Console output with Debug.WriteLine in diagnostic code
- Replaced Console.WriteLine with System.Diagnostics.Debug.WriteLine in:
  * LaunchOrderDetector (probe scoring)
  * UnpackagedDetector (probe scoring)
  * ElectronProcessActive (StartInternal traces)
- Removed '[StartInternal]: after run:' trace
- Debug.WriteLine only outputs when debugger is attached

These were the noisiest debugging traces. Other Console.WriteLine usage
in StartupManager, ProcessRunner, etc. would require ILogger injection
which is a larger architectural change deferred for now.
2026-01-31 11:19:28 +01:00
Pierre Arnaud
805d942b11 Phase 3: Configure SignalR logging to reduce verbosity
- Updated SafeLogger to support minimum log level threshold
- Configure SignalR log level based on environment:
  * Debug mode: Info level (connection events only)
  * Development/Production: Warning level (errors/warnings only)
- Eliminates verbose packet-level logging
- Connection lifecycle events still visible in debug mode

This dramatically reduces console noise while preserving important events.
2026-01-31 11:17:26 +01:00
Pierre Arnaud
29b1f088ce Phase 2: Clean up debugging traces and use environment-aware logging
- Removed 'Entry!!!:' debugging leftover from main.js
- Replaced all console.log/warn/error with logger methods
- Preserved console.time/timeEnd for performance measurements
- Updated browserWindows.js and webContents.js to use logger
- Changed debug traces to DEBUG level (won't show in production)
- Changed info messages to INFO level (show in development)
- All errors use ERROR level (always shown)

Socket.IO connection messages now respect log levels based on environment.
2026-01-31 11:16:11 +01:00
Pierre Arnaud
4a62103749 Phase 1: Add environment-aware logging infrastructure
- Created logger.js with log levels (DEBUG, INFO, WARN, ERROR)
- Implemented environment detection (debug/development/production)
- Auto-detect based on flags, NODE_ENV, and debugger
- Preserve console.time/timeEnd for performance measurements
- Updated SignalR bridge to use new logger system
- Replaced safeConsole calls with environment-aware logger

Default log levels:
- Debug mode: DEBUG and above
- Development: INFO and above
- Production: WARN and above
2026-01-31 11:09:25 +01:00
Pierre Arnaud
38ee6fabb4 Only enable SignalR detailed errors in development
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.
2026-01-31 10:04:29 +01:00
Pierre Arnaud
0ee2bbe31c Refactor debugger detection to use shared DebuggerHelper
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>
2026-01-31 09:38:26 +01:00
Pierre Arnaud
a88e10bbf2 Measure total startup time 2026-01-31 09:18:24 +01:00
Pierre Arnaud
39c7e61ae5 Auto-quit Electron when pipe to .NET process breaks
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.
2026-01-31 09:17:09 +01:00
Pierre Arnaud
96c454aedb Add process-level EPIPE error handlers
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.
2026-01-31 09:10:48 +01:00
Pierre Arnaud
5a77284610 Fix EPIPE error when .NET process terminates before Electron
- 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.
2026-01-31 09:04:58 +01:00
Pierre Arnaud
0a23659196 Phase 1b: Optimize startup detection for faster .NET initialization
Performance optimizations to startup detection:
- Reduced GatherBuildInfo: >1000ms → 7ms (99% faster!)
- Reduced CollectProcessData: →  86ms
- Reduced DetectAppTypeAndStartup: → 91ms
- Total StartupManager.Initialize: → 91ms (down from >1 second)

Electron startup also improved significantly:
- Module loading: 719ms → 108ms (85% faster!)
- SignalR connection: 884ms → 403ms (54% faster!)
- Total Electron startup: 2.3s → 700ms (70% faster!)

Optimizations applied:

1. Directory checks (UnpackagedDetector):
   - Changed GetDirectories().Any() to direct Directory.Exists()
   - Avoids expensive directory enumeration
   - Saves ~200-500ms on slow disks

2. Debugger state caching:
   - Cache Debugger.IsAttached result in static field
   - Avoids multiple expensive debugger checks
   - Used by both UnpackagedDetector and LaunchOrderDetector
   - Saves ~50-100ms per check

3. Added timing measurements:
   - Added Stopwatch to StartupManager.Initialize()
   - Detailed timing for each initialization phase
   - Visibility into startup performance

Results:
- .NET startup detection: >1000ms → 91ms
- Electron module loading: 719ms → 108ms
- SignalR connection: 884ms → 403ms
- **Total improvement: ~70% faster startup**

The combination of faster directory checks, cached debugger state,
and parallel module loading (from previous commit) results in
sub-second startup times in development mode.
2026-01-30 23:02:13 +01:00
Pierre Arnaud
5d224568d0 Phase 1: Implement parallel module loading for faster startup
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.
2026-01-30 22:56:37 +01:00
Pierre Arnaud
1c0b9378d2 Phase 6: Add comprehensive authentication documentation
Created new authentication guide:
- docs/SignalR-Authentication-Guide.md (500+ lines)
- Complete threat model and security architecture
- Flow diagrams and implementation details
- Troubleshooting guide with common issues
- FAQ covering design decisions and usage

Updated SignalR implementation summary:
- Added authentication & security section
- Documented token flow and cookie management
- Security properties and protection scope
- Logging & monitoring guidelines
- Multi-user testing procedures
- Updated file changes summary and success metrics

Documentation includes:
- Architecture diagrams (ASCII art)
- Code examples for all components
- Step-by-step authentication flow
- Security considerations and rationale
- Common troubleshooting scenarios
- Testing recommendations
2026-01-30 22:39:45 +01:00
Pierre Arnaud
c12a706289 Phase 5.2: Add comprehensive logging and error handling for authentication
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
2026-01-30 22:37:37 +01:00
Pierre Arnaud
8cc3fe4fd7 Phase 5.1: Fix quit handler to support both Socket.IO and SignalR modes
- 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
2026-01-30 22:36:18 +01:00
Pierre Arnaud
893de1510d Phase 3: Pass authentication token to SignalR connection URL
- 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.
2026-01-30 22:35:39 +01:00
Pierre Arnaud
6f49a663ea Phase 4: Inject authentication service into RuntimeController and set token 2026-01-30 19:20:03 +01:00
Pierre Arnaud
5b9e2b8b3b Phase 2.1: Register authentication services and middleware in Program.cs 2026-01-30 19:19:23 +01:00
Pierre Arnaud
dee640c526 Phase 1.2: Extract token in Electron and append to URL
- 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
2026-01-30 19:14:09 +01:00
Pierre Arnaud
f598fbf5ce Phase 1.1: Generate authentication token in RuntimeController
- 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
2026-01-30 19:13:12 +01:00
Pierre Arnaud
6847520ea8 Remove HSTS and HTTPS redirection for Electron apps
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.
2026-01-30 17:30:07 +01:00
Pierre Arnaud
75151282ff Remove unnecessary UseWebSockets() call
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().
2026-01-30 17:28:40 +01:00
Pierre Arnaud
17ef6853ab Fix ASP0014 warning: Use top-level route registration for ElectronHub
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.
2026-01-30 17:14:14 +01:00
Pierre Arnaud
12f011bc33 Add comprehensive SignalR implementation documentation
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.
2026-01-30 17:11:57 +01:00
Pierre Arnaud
217fe83334 Add documentation comments to SignalR implementation
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.
2026-01-30 17:08:37 +01:00
Pierre Arnaud
1fc881674d Clean up debug logging from SignalR implementation
Remove excessive console logging that was added during debugging:
- Removed verbose logging from Program.cs (app ready callback steps)
- Removed HTTP request logging middleware
- Cleaned up RuntimeControllerAspNetDotnetFirstSignalR lifecycle logging
- Streamlined ElectronHub connection/event logging
- Simplified SignalRFacade event handling logging
- Reduced JavaScript logging in main.js and signalr-bridge.js
- Reset log levels to Warning for SignalR components in appsettings

Kept only essential error logging and critical state transitions.
Production-ready logging levels maintained.
2026-01-30 17:07:24 +01:00
Pierre Arnaud
6e369aabef Fix static files and CSS asset reference for SignalR sample
- 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
2026-01-30 16:58:29 +01:00
Pierre Arnaud
06a332827b Fix window shutdown and URL port for SignalR mode
- 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)
2026-01-30 16:43:53 +01:00
Pierre Arnaud
23f79244ae Fix SignalR event args spreading for Electron handlers
- Changed event handler to receive args as single array parameter
- Spread array elements as individual arguments to match Socket.IO behavior
2026-01-30 16:40:20 +01:00
Pierre Arnaud
6b9187cf6e Fix SignalR bridge communication issues
- 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
2026-01-30 16:36:27 +01:00
Pierre Arnaud
108ef19a3b 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.
2026-01-30 15:29:48 +01:00
Pierre Arnaud
8c6020e35b WIP: Debugging SignalR connection timeout
- 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
2026-01-30 13:27:03 +01:00
Pierre Arnaud
547e9f1196 Fix: Add safe console wrapper to prevent EPIPE errors
- 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.
2026-01-30 13:22:35 +01:00
Pierre Arnaud
4b971af119 Fix: Execute app ready callback in SignalR mode
- 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)
2026-01-30 13:19:49 +01:00
Pierre Arnaud
da8216b292 Implement bidirectional event routing for SignalR mode
- 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
2026-01-30 13:09:55 +01:00
Pierre Arnaud
be609a513e Refactor: Introduce IFacade interface for SocketIO and SignalR facades
- 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.
2026-01-30 13:03:19 +01:00
Pierre Arnaud
c4a8de6c4e Fix psl dist folder issue by copying from source after npm install 2026-01-30 12:52:40 +01:00
Pierre Arnaud
5b3d5e07ee Add @microsoft/signalr to package.template.json for npm install during build 2026-01-30 12:46:57 +01:00
Pierre Arnaud
9135aff855 Fix electronurl parameter case sensitivity for Electron command line 2026-01-30 12:39:32 +01:00
Pierre Arnaud
e9efb26dff Fix main.js to check SignalR flags first before legacy mode flags 2026-01-30 12:37:12 +01:00
Pierre Arnaud
e29a3bc27a Fix IsUnpackaged extension to include UnpackedDotnetFirstSignalR 2026-01-30 12:34:42 +01:00
Pierre Arnaud
f55abb357c Fix Electron launch: subscribe to ASP.NET Ready event in SignalR controller 2026-01-30 12:26:17 +01:00
Pierre Arnaud
0b92336de2 Fix dynamic port binding: use 127.0.0.1 instead of localhost for port 0 2026-01-30 12:22:07 +01:00
Pierre Arnaud
4c17027039 Add BlazorSignalR sample to solution file 2026-01-30 12:20:08 +01:00
Pierre Arnaud
5d04ab686a Add ElectronNET.Samples.BlazorSignalR - complete sample app for SignalR mode 2026-01-30 12:17:20 +01:00
Pierre Arnaud
de0c02c503 Add comprehensive documentation for SignalR-based startup mode 2026-01-30 12:11:48 +01:00
Pierre Arnaud
054f5b1c4c Complete Phase 5: Add SignalR startup detection and port 0 configuration 2026-01-30 12:10:49 +01:00
Pierre Arnaud
04ec52208a Fix compilation errors - Phase 4 complete (basic structure) 2026-01-30 12:08:43 +01:00
Pierre Arnaud
268b9c90ce Update RuntimeControllerAspNetDotnetFirstSignalR to use SignalRFacade 2026-01-30 12:06:29 +01:00
Pierre Arnaud
cb7d721b7d Add SignalRFacade for SignalR-based API communication 2026-01-30 12:04:33 +01:00
Pierre Arnaud
c1740b53fc Add SignalR client support to Electron Host for new startup modes 2026-01-30 12:02:46 +01:00
Pierre Arnaud
40aed60c7d Add RuntimeControllerAspNetDotnetFirstSignalR for SignalR-based startup 2026-01-30 12:00:27 +01:00
Pierre Arnaud
8ee81f6abd Add ElectronHub and SignalR infrastructure for new startup modes 2026-01-30 11:58:31 +01:00
Pierre Arnaud
7f2ea4839e Add PackagedDotnetFirstSignalR and UnpackedDotnetFirstSignalR startup methods 2026-01-30 11:57:40 +01:00
127 changed files with 1407 additions and 1712 deletions

View File

@@ -134,16 +134,4 @@ TL;DR: Unless there is a technical reason (e.g., a crucial new API not being ava
We pretty much release whenever we have something new (i.e., do fixes such as a 0.1.1, or add new features, such as a 0.2.0) quite quickly.
We will go for a 1.0.0 release of this as early as ~mid of June 2026 (unless we find some critical things or want to extend the beta phase for ElectronNET.Core). This should be sufficient time to get some user input and have enough experience to call it stable.
## Updating Electron Versions
The releases of Electron are found on the [releases.electronjs.org](https://releases.electronjs.org/release?page=1) website.
You can update the `src\ElectronNET\build\ElectronNETRules.Project.xaml` file with new entries using the following script (run it in the browser's console when being on the Electron Releases website):
```js
[...new Set([...document.querySelectorAll('tbody tr a[href^="/release/v"]')].map(m => m.getAttribute('href').replace('/release/v', '')))].map(m => `<EnumValue Name="${m}" DisplayName="${m}" />`).sort().join('\n')
```
Alternatively, use the website's information to feed into an AI agent of your choice.
We will go for a 1.0.0 release of this as early as ~mid of January 2026 (unless we find some critical things or want to extend the beta phase for ElectronNET.Core). This should be sufficient time to get some user input and have enough experience to call it stable.

View File

@@ -1,8 +1,6 @@
name: Build and Publish
on:
push:
branches: [main, develop]
on: [push]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -4,14 +4,13 @@ on:
workflow_call:
concurrency:
group: integration-tests-${{ github.workflow }}-${{ github.ref }}
group: integration-tests-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: ${{ matrix.os }} API-${{ matrix.electronVersion }}
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
@@ -43,6 +42,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Random delay (0-20 seconds)
shell: bash
run: |
DELAY=$((RANDOM % 21))
echo "Waiting for $DELAY seconds..."
sleep $DELAY
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
@@ -73,31 +79,27 @@ jobs:
- name: Run tests (Linux)
if: runner.os == 'Linux'
continue-on-error: true
timeout-minutes: 15
run: |
mkdir -p test-results
xvfb-run -a dotnet test src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj \
-c Release --no-build -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} -p:ElectronVersion=${{ matrix.electronVersion }} \
--logger "trx;LogFileName=${{ matrix.os }}-electron-${{ matrix.electronVersion }}.trx" \
--logger "console;verbosity=detailed" \
--blame-hang --blame-hang-timeout 5min \
--results-directory test-results
- name: Run tests (Windows)
if: runner.os == 'Windows'
continue-on-error: true
timeout-minutes: 15
run: |
New-Item -ItemType Directory -Force -Path test-results | Out-Null
dotnet test src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj -c Release --no-build -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} -p:ElectronVersion=${{ matrix.electronVersion }} --logger "trx;LogFileName=${{ matrix.os }}-electron-${{ matrix.electronVersion }}.trx" --logger "console;verbosity=detailed" --blame-hang --blame-hang-timeout 5min --results-directory test-results
dotnet test src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj -c Release --no-build -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} -p:ElectronVersion=${{ matrix.electronVersion }} --logger "trx;LogFileName=${{ matrix.os }}-electron-${{ matrix.electronVersion }}.trx" --logger "console;verbosity=detailed" --results-directory test-results
- name: Run tests (macOS)
if: runner.os == 'macOS'
continue-on-error: true
timeout-minutes: 15
run: |
mkdir -p test-results
dotnet test src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj -c Release --no-build -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} -p:ElectronVersion=${{ matrix.electronVersion }} --logger "trx;LogFileName=${{ matrix.os }}-electron-${{ matrix.electronVersion }}.trx" --logger "console;verbosity=detailed" --blame-hang --blame-hang-timeout 5min --results-directory test-results
dotnet test src/ElectronNET.IntegrationTests/ElectronNET.IntegrationTests.csproj -c Release --no-build -r ${{ matrix.rid }} -p:RuntimeIdentifier=${{ matrix.rid }} -p:ElectronVersion=${{ matrix.electronVersion }} --logger "trx;LogFileName=${{ matrix.os }}-electron-${{ matrix.electronVersion }}.trx" --logger "console;verbosity=detailed" --results-directory test-results
- name: Upload raw test results
if: always()
@@ -112,7 +114,6 @@ jobs:
runs-on: ubuntu-24.04
if: always()
needs: [tests]
timeout-minutes: 10
permissions:
actions: read

View File

@@ -27,7 +27,7 @@ jobs:
run: |
echo "Inspecting jobs of workflow run $RUN_ID in $REPO"
jobs_json="$(gh api repos/$REPO/actions/runs/$RUN_ID/jobs)"
jobs_json="$(gh api -R $REPO repos/$REPO/actions/runs/$RUN_ID/jobs)"
echo "Jobs and conclusions:"
echo "$jobs_json" | jq '.jobs[] | {name: .name, conclusion: .conclusion}'

View File

@@ -1,27 +1,3 @@
# 0.5.2
## ElectronNET.Core
- Fixed token param being appended to external URLs (#1075)
- Added more variants for `UseElectron` (#1076) @AeonSake
# 0.5.1
## ElectronNET.Core
- Fixed slicing of arguments for packaged applications (#1072)
- Added support for Electron 42+ (#1073)
# 0.5.0
## ElectronNET.Core
- Updated internal facade from `SocketIoFacade` to `ISocketConnection` (#1037)
- Fixed `HasShadow` is not `true` by default (#1049) @AeonSake
- Fixed missing `ReadAllLines` in MSBuild execution (#1035)
- Fixed serialization of release notes for the updater (#1041)
- Added automatic port selection and secured IPC communication (#1038)
# 0.4.1
## ElectronNET.Core

583
docs/Dev/Authentication.md Normal file
View File

@@ -0,0 +1,583 @@
# Electron.NET SignalR Authentication Guide
This guide explains the token-based authentication system implemented for SignalR mode in Electron.NET, designed to protect applications in multi-user environments.
## Table of Contents
1. [Overview](#overview)
2. [Threat Model](#threat-model)
3. [Authentication Architecture](#authentication-architecture)
4. [Implementation Details](#implementation-details)
5. [Security Properties](#security-properties)
6. [Troubleshooting](#troubleshooting)
7. [FAQ](#faq)
## Overview
Electron.NET's SignalR mode includes built-in authentication to ensure that only the Electron process spawned by a specific .NET instance can connect to that instance's HTTP and SignalR endpoints.
**When is this important?**
- Multi-user Windows Server environments (Terminal Services, RDP)
- Shared development machines with multiple users
- Any scenario where multiple users run the same application simultaneously
**What does it protect against?**
- User A's Electron process connecting to User B's .NET backend
- Unauthorized port scanning and connection attempts from other users
- Accidental misconfigurations causing cross-user connections
## Threat Model
### The Problem
When multiple users run the same Electron.NET application on a shared server:
1. Each .NET process binds to a TCP port (e.g., `http://localhost:58971`)
2. TCP ports are visible to **all users** on the machine
3. Without authentication, User A's Electron could connect to User B's backend
```
┌─────────────────────────────────────────────────┐
│ Windows Server (Terminal Services) │
├─────────────────────────────────────────────────┤
│ User A Session │
│ ├─ .NET Process (localhost:58971) │
│ └─ Electron Process │
│ │
│ User B Session │
│ ├─ .NET Process (localhost:61234) │
│ └─ Electron Process (could connect to 58971) │ ❌ Prevent this!
└─────────────────────────────────────────────────┘
```
### The Solution
Each .NET process generates a unique authentication token when launching Electron. Only requests with the correct token are allowed to connect.
```
┌─────────────────────────────────────────────────┐
│ User A Session │
│ ├─ .NET (token: abc123...) │
│ └─ Electron (has token abc123...) │ ✅ Authenticated
│ │
│ User B Session │
│ ├─ .NET (token: xyz789...) │
│ └─ Electron (has token xyz789...) │ ✅ Authenticated
│ │
│ ❌ User B's Electron → User A's .NET │
│ (lacks token abc123...) │ ❌ Rejected (401)
└─────────────────────────────────────────────────┘
```
## Authentication Architecture
### Flow Diagram
```
┌──────────────┐ ┌─────────────────┐
│ .NET Process │ │ Electron Process│
└──────┬───────┘ └────────┬────────┘
│ │
│ 1. Generate Token (GUID) │
│ Token: a3f8b2c1d4e5... │
│ │
│ 2. Launch Electron │
│ --authtoken=a3f8b2c1d4e5... │
│────────────────────────────────────────────────────>│
│ │
│ │ 3. Extract Token
│ │ global.authToken = ...
│ │
│ │ 4. Initial Page Load
│<────────────────────────────────────────────────────│
│ GET /?token=a3f8b2c1d4e5... │
│ │
│ 5. Validate Token │
│ ✓ Valid → Set Cookie │
│────────────────────────────────────────────────────>│
│ HTTP 200 + Set-Cookie: ElectronAuth=... │
│ │
│ │ 6. SignalR Connection
│<────────────────────────────────────────────────────│
│ GET /electron-hub?token=a3f8b2c1d4e5... │
│ │
│ 7. Validate Token, Set Cookie │
│────────────────────────────────────────────────────>│
│ HTTP 200 + Set-Cookie │
│ │
│ │ 8. Subsequent Requests
│<────────────────────────────────────────────────────│
│ GET /api/data │
│ Cookie: ElectronAuth=a3f8b2c1d4e5... │
│ │
│ 9. Validate Cookie │
│────────────────────────────────────────────────────>│
│ HTTP 200 (authenticated) │
│ │
```
### Key Steps
1. **Token Generation**: .NET generates 128-bit cryptographic random GUID
2. **Command-Line Passing**: Token passed to Electron via `--authtoken` parameter
3. **Token Extraction**: Electron stores token in `global.authToken`
4. **URL Appending**: Token appended to initial page and SignalR connection URLs
5. **Middleware Validation**: Every HTTP request validated by middleware
6. **Cookie Setting**: Valid token results in secure HttpOnly cookie
7. **Cookie-Based Requests**: Subsequent requests use cookie (no token in URL)
## Implementation Details
### 1. Token Generation (.NET)
**File**: `src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs`
```csharp
private void LaunchElectron()
{
// Generate secure authentication token (128-bit cryptographic random GUID)
this.authenticationToken = Guid.NewGuid().ToString("N"); // 32 hex chars
// Register token with authentication service
this.authenticationService.SetExpectedToken(this.authenticationToken);
// Launch Electron with token
var args = $"--unpackeddotnetsignalr --electronurl={this.actualUrl} --authtoken={this.authenticationToken}";
this.electronProcess = new ElectronProcessActive(isUnPacked, ElectronNetRuntime.ElectronExecutable, args, this.port.Value);
_ = this.electronProcess.Start();
}
```
**Token Format**: 32-character hexadecimal string (e.g., `a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5`)
### 2. Authentication Service (.NET)
**File**: `src/ElectronNET.AspNet/Services/ElectronAuthenticationService.cs`
```csharp
public class ElectronAuthenticationService : IElectronAuthenticationService
{
private string _expectedToken;
private readonly object _lock = new object();
public void SetExpectedToken(string token)
{
lock (_lock)
{
_expectedToken = token;
}
}
public bool ValidateToken(string token)
{
if (string.IsNullOrEmpty(token))
return false;
lock (_lock)
{
if (string.IsNullOrEmpty(_expectedToken))
return false;
// Constant-time comparison prevents timing attacks
return ConstantTimeEquals(token, _expectedToken);
}
}
private static bool ConstantTimeEquals(string a, string b)
{
if (a == null || b == null || a.Length != b.Length)
return false;
var result = 0;
for (int i = 0; i < a.Length; i++)
{
result |= a[i] ^ b[i];
}
return result == 0;
}
}
```
**Key Features**:
- Thread-safe with lock
- Constant-time comparison (prevents timing attacks)
- Singleton lifetime (one per .NET instance)
### 3. Authentication Middleware (.NET)
**File**: `src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs`
```csharp
public async Task InvokeAsync(HttpContext context)
{
// Check if authentication cookie exists
var authCookie = context.Request.Cookies["ElectronAuth"];
if (!string.IsNullOrEmpty(authCookie))
{
if (_authService.ValidateToken(authCookie))
{
await _next(context);
return;
}
else
{
_logger.LogWarning("Invalid cookie for path {Path}", context.Request.Path);
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Invalid authentication");
return;
}
}
// No cookie - check for token in query string
var token = context.Request.Query["token"].ToString();
if (!string.IsNullOrEmpty(token) && _authService.ValidateToken(token))
{
// Valid token - set cookie for future requests
context.Response.Cookies.Append("ElectronAuth", token, new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Strict,
Path = "/",
Secure = false, // localhost is HTTP
IsEssential = true
});
await _next(context);
return;
}
// Reject - no valid cookie or token
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Authentication required");
}
```
**Middleware Order** (IMPORTANT):
```csharp
app.UseMiddleware<ElectronAuthenticationMiddleware>(); // ← FIRST
app.UseRouting();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapHub<ElectronHub>("/electron-hub");
app.MapRazorComponents<App>();
```
### 4. Token Extraction (Electron)
**File**: `src/ElectronNET.Host/main.js`
```javascript
// Extract authentication token from command-line
if (app.commandLine.hasSwitch('authtoken')) {
global.authToken = app.commandLine.getSwitchValue('authtoken');
}
```
### 5. Token in URLs (Electron)
**Initial Page Load** (`src/ElectronNET.Host/api/browserWindows.js`):
```javascript
// Append token to window URL
if (global.authToken) {
const separator = electronUrl.includes('?') ? '&' : '?';
electronUrl = `${electronUrl}${separator}token=${global.authToken}`;
}
window.loadURL(electronUrl);
```
**SignalR Connection** (`src/ElectronNET.Host/api/signalr-bridge.js`):
```javascript
async connect() {
// Append token to SignalR hub URL
const connectionUrl = this.authToken ?
`${this.hubUrl}?token=${this.authToken}` :
this.hubUrl;
this.connection = new signalR.HubConnectionBuilder()
.withUrl(connectionUrl)
.build();
await this.connection.start();
}
```
### 6. Service Registration (Application)
**File**: Your application's `Program.cs`
```csharp
var builder = WebApplication.CreateBuilder(args);
// Register authentication service as singleton
builder.Services.AddSingleton<IElectronAuthenticationService, ElectronAuthenticationService>();
builder.Services.AddElectron();
var app = builder.Build();
// Register middleware BEFORE UseRouting()
app.UseMiddleware<ElectronAuthenticationMiddleware>();
app.UseRouting();
app.MapHub<ElectronHub>("/electron-hub");
app.Run();
```
## Security Properties
### Cookie Configuration
| Property | Value | Purpose |
|----------|-------|---------|
| `HttpOnly` | `true` | Prevents JavaScript access (XSS protection) |
| `SameSite` | `Strict` | Prevents CSRF attacks |
| `Path` | `/` | Cookie sent with all requests |
| `Secure` | `false` | Cannot use on localhost HTTP |
| `IsEssential` | `true` | Required for app to function |
| **Lifetime** | Session | Expires when Electron closes |
### Token Properties
- **Entropy**: 128 bits (2^128 possible values)
- **Format**: 32 hexadecimal characters (GUID without hyphens)
- **Generation**: `Guid.NewGuid()` uses cryptographically secure RNG
- **Lifetime**: Entire application session
- **Uniqueness**: Each .NET instance generates unique token
### What's Protected
**All HTTP endpoints**:
- Blazor Server pages (`/`, `/counter`, etc.)
- Static files (`/css/app.css`, `/js/script.js`)
- API endpoints (custom controllers)
- SignalR hub (`/electron-hub`)
**Both transport modes**:
- Initial token-based authentication (query parameter)
- Cookie-based subsequent requests
**Cross-user isolation**:
- Different users = different tokens
- Invalid token = 401 Unauthorized
### What's NOT Protected Against
**Same-user attacks** (by design):
- Process memory inspection
- Debugger attachment
- Command-line parameter visibility
**Rationale**: A malicious process running as the same user already has full access to:
- Process memory (can read token from RAM)
- Cookies (stored in Electron's data directory)
- All files owned by the user
Token-based authentication focuses on **cross-user isolation**, not same-user security.
## Troubleshooting
### Problem: 401 Unauthorized on Initial Load
**Symptoms**:
```
GET / HTTP/1.1
Response: 401 Unauthorized
```
**Possible Causes**:
1. Token not passed to Electron
2. Token not appended to URL
3. Middleware rejecting valid token
**Debugging Steps**:
1. Check Electron command-line:
```javascript
console.log('Auth Token:', global.authToken);
```
Should print 32-character hex string.
2. Check URL in browser window:
```javascript
console.log('Loading URL:', electronUrl);
```
Should include `?token=<guid>` parameter.
3. Check .NET logs:
```
[Warning] Authentication failed: Invalid token (prefix: a3f8b2c1...)
```
4. Verify middleware registration:
```csharp
app.UseMiddleware<ElectronAuthenticationMiddleware>(); // Before UseRouting()
```
### Problem: SignalR Connection Fails
**Symptoms**:
```
[SignalRBridge] Authentication failed: The authentication token is invalid or missing.
```
**Possible Causes**:
1. Token not passed to `SignalRBridge` constructor
2. Token not appended to hub URL
3. Cookie not being sent
**Debugging Steps**:
1. Check token passed to SignalRBridge:
```javascript
console.log('SignalRBridge token:', this.authToken);
```
2. Check connection URL:
```javascript
console.log('Hub URL:', connectionUrl);
```
Should be `http://localhost:PORT/electron-hub?token=<guid>`.
3. Enable verbose logging:
```javascript
.configureLogging(signalR.LogLevel.Debug)
```
### Problem: Cookie Not Persisting
**Symptoms**: Every request includes token in URL, cookie never set.
**Possible Causes**:
1. Cookie settings incompatible with browser
2. Middleware not setting cookie
3. Response headers not sent
**Debugging Steps**:
1. Check response headers:
```
Set-Cookie: ElectronAuth=a3f8b2c1...; Path=/; HttpOnly; SameSite=Strict
```
2. Check subsequent requests:
```
Cookie: ElectronAuth=a3f8b2c1...
```
3. Enable middleware logging:
```csharp
_logger.LogInformation("Setting cookie for path {Path}", path);
```
### Problem: Token Visible in Process List
**Observation**:
```powershell
wmic process where "name='electron.exe'" get commandline
...--authtoken=a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5...
```
**Is this a problem?** No, by design.
**Explanation**:
- Command-line parameters are visible to same-user processes
- This is acceptable because:
- Same user already has access to process memory
- Same user can read cookies from Electron's data directory
- Token-based auth protects against **other users**, not same-user processes
**If you need same-user protection**: Use OS-level access controls (file permissions, process isolation) or consider named pipes with ACLs.
## FAQ
### Q: Why use tokens instead of Process ID validation?
**A**: PIDs can be recycled and reused, making validation unreliable. Additionally:
- PIDs don't provide cryptographic security
- Parent-child validation is platform-specific
- Adds complexity without meaningful security benefit
Token-based authentication provides:
- Cryptographic randomness (128-bit entropy)
- Simple cross-platform implementation
- No race conditions or PID recycling issues
### Q: Why not use HTTPS with certificates?
**A**: Localhost doesn't support HTTPS certificates easily:
- Self-signed certificates trigger browser warnings
- Certificate management adds complexity
- Token-based auth provides equivalent security for localhost IPC
### Q: Can I disable authentication?
**A**: Not recommended, but possible by removing middleware registration:
```csharp
// Remove this line:
// app.UseMiddleware<ElectronAuthenticationMiddleware>();
```
**Warning**: Only do this if:
- Application runs on single-user machines only
- No Terminal Services / RDP access
- You understand the security implications
### Q: Does this work with hot reload?
**A**: Yes, cookie persists across hot reload as long as Electron process keeps running.
### Q: What about multiple Electron windows?
**A**: All windows in the same Electron process share cookies automatically. Authentication works seamlessly across multiple windows.
### Q: How do I test authentication?
**Test 1 - Happy Path**:
1. Run application normally
2. Check logs for "Authentication successful"
3. Verify cookie is set (DevTools → Application → Cookies)
4. Subsequent requests should not include token in URL
**Test 2 - Invalid Token**:
1. Modify token in browser URL: `?token=invalid`
2. Should receive 401 Unauthorized
3. Check logs for "Authentication failed: Invalid token"
**Test 3 - No Token**:
1. Open browser manually to `http://localhost:PORT/`
2. Should receive 401 Unauthorized
3. Check logs for "Authentication failed: No cookie or token"
**Test 4 - Multi-User** (Windows Server/Terminal Services):
1. Launch app as User A
2. In User B session, try to connect to User A's port
3. Should receive 401 Unauthorized
### Q: What about packaged applications?
**A**: Authentication works identically in packaged mode. The `--authtoken` parameter is included in the packaged Electron executable.
### Q: Can I customize the cookie name?
**A**: Yes, modify `AuthCookieName` constant in `ElectronAuthenticationMiddleware.cs`:
```csharp
private const string AuthCookieName = "MyCustomCookieName";
```
## Summary
Electron.NET's token-based authentication provides:
**Security**: 128-bit entropy, constant-time comparison, secure cookies
**Simplicity**: Automatic token generation and validation
**Compatibility**: Works with Blazor, SignalR, and static files
**Monitoring**: Structured logging for security events
**Multi-User**: Cross-user isolation on shared servers
The authentication system is **enabled by default** in SignalR mode and requires minimal configuration. For most applications, simply register the services and middleware - everything else happens automatically.
For additional help or questions, see the [SignalR Implementation Summary](SignalR-Implementation-Summary.md) or open an issue on GitHub.

View File

@@ -54,7 +54,7 @@ Add the Electron.NET configuration to your `.csproj` file:
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ElectronNET.Core" Version="0.5.1" />
<PackageReference Include="ElectronNET.Core" Version="0.4.1" />
</ItemGroup>
```

View File

@@ -1,32 +0,0 @@
# Secure Communication
By default, the IPC communication between .NET and Node.js is secured on startup. Consequently, multiple instances running on different user accounts (but shared on the same machine) can safely co-exist. However, this protection is not enough to secure the web application behind - or make any security statement w.r.t. a malicious root user.
## Securing the Web Application
You can opt-in to also guard your ASP.NET Core application using the same mechanism that is already used to protected the IPC broker that deals with the .NET to Node.js communication.
The key to opt-in is to provide another service *before* calling `AddElectron` on the service collection.
The following two namespaces are used in the next instructions:
```cs
using ElectronNET.AspNet.Middleware;
using ElectronNET.AspNet.Services;
```
You'll need the following line:
```cs
builder.Services.AddSingleton<IElectronAuthenticationService, ElectronAuthenticationService>();
```
This way, Electron.NET is notified that you want to store and re-use the authentication token that has been negotiated between the .NET and Node.js processes at startup.
With this being set up you can register a middleware to actually deny requests that have originated outside of your Electron.NET application:
```cs
app.UseMiddleware<ElectronAuthenticationMiddleware>();
```
This must be placed above any routing (e.g., before calling `UseRouting` on the web application) in order to properly take effect.

View File

@@ -24,7 +24,6 @@
- [Startup-Methods](Using/Startup-Methods.md)
- [Debugging](Using/Debugging.md)
- [Package Building](Using/Package-Building.md)
- [Secure Communication](Using/Secure-Communication.md)
- [Adding a `custom_main.js`](Using/Custom_main.md)
# API Reference

View File

@@ -215,8 +215,7 @@ namespace ElectronNET.API.Entities
/// <summary>
/// Whether window should have a shadow. Default is true.
/// </summary>
[DefaultValue(true)]
public bool HasShadow { get; set; } = true;
public bool HasShadow { get; set; }
/// <summary>
/// Forces using dark theme for the window, only works on some GTK+3 desktop environments. Default is false.

View File

@@ -1,8 +1,4 @@
using System;
using System.Text.Json.Serialization;
using ElectronNET.API.Converter;
namespace ElectronNET.API.Entities
namespace ElectronNET.API.Entities
{
/// <summary>
///
@@ -28,8 +24,7 @@ namespace ElectronNET.API.Entities
/// <summary>
/// Gets or sets the release notes.
/// </summary>
[JsonConverter(typeof(ReleaseNotesConverter))]
public ReleaseNoteInfo[] ReleaseNotes { get; set; } = Array.Empty<ReleaseNoteInfo>();
public ReleaseNoteInfo[] ReleaseNotes { get; set; } = new ReleaseNoteInfo[0];
/// <summary>
/// Gets or sets the release date.

View File

@@ -2,6 +2,8 @@
// ReSharper disable once CheckNamespace
namespace ElectronNET.API
{
using ElectronNET.API.Bridge;
internal static class BridgeConnector
{
public static ISocketConnection Socket

View File

@@ -1,56 +1,57 @@
namespace ElectronNET.API;
using System;
using System.Threading.Tasks;
/// <summary>
/// Common interface for communication facades.
/// Provides methods for bidirectional communication between .NET and Electron.
/// </summary>
internal interface ISocketConnection : IDisposable
namespace ElectronNET.API.Bridge
{
/// <summary>
/// Raised when the bridge connection is established.
/// </summary>
event EventHandler BridgeConnected;
using System;
using System.Threading.Tasks;
/// <summary>
/// Raised when the bridge connection is lost.
/// Common interface for communication facades.
/// Provides methods for bidirectional communication between .NET and Electron.
/// </summary>
event EventHandler BridgeDisconnected;
internal interface ISocketConnection : IDisposable
{
/// <summary>
/// Raised when the bridge connection is established.
/// </summary>
event EventHandler BridgeConnected;
/// <summary>
/// Establishes the connection to Electron.
/// </summary>
void Connect();
/// <summary>
/// Raised when the bridge connection is lost.
/// </summary>
event EventHandler BridgeDisconnected;
/// <summary>
/// Registers a persistent event handler.
/// </summary>
void On(string eventName, Action action);
/// <summary>
/// Establishes the connection to Electron.
/// </summary>
void Connect();
/// <summary>
/// Registers a persistent event handler with a typed parameter.
/// </summary>
void On<T>(string eventName, Action<T> action);
/// <summary>
/// Registers a persistent event handler.
/// </summary>
void On(string eventName, Action action);
/// <summary>
/// Registers a one-time event handler.
/// </summary>
void Once(string eventName, Action action);
/// <summary>
/// Registers a persistent event handler with a typed parameter.
/// </summary>
void On<T>(string eventName, Action<T> action);
/// <summary>
/// Registers a one-time event handler with a typed parameter.
/// </summary>
void Once<T>(string eventName, Action<T> action);
/// <summary>
/// Registers a one-time event handler.
/// </summary>
void Once(string eventName, Action action);
/// <summary>
/// Removes an event handler.
/// </summary>
void Off(string eventName);
/// <summary>
/// Registers a one-time event handler with a typed parameter.
/// </summary>
void Once<T>(string eventName, Action<T> action);
/// <summary>
/// Sends a message to Electron.
/// </summary>
Task Emit(string eventName, params object[] args);
}
/// <summary>
/// Removes an event handler.
/// </summary>
void Off(string eventName);
/// <summary>
/// Sends a message to Electron.
/// </summary>
Task Emit(string eventName, params object[] args);
}
}

View File

@@ -5,6 +5,7 @@ namespace ElectronNET.API;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ElectronNET.API.Bridge;
using ElectronNET.API.Serialization;
using SocketIO.Serializer.SystemTextJson;
using SocketIO = SocketIOClient.SocketIO;

View File

@@ -1,91 +0,0 @@
using ElectronNET.API.Entities;
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ElectronNET.API.Converter;
/// <summary>
/// Handles the polymorphic shape of releaseNotes coming from electron-builder.
/// Depending on the updater.fullChangelog setting, electron-builder sends:
/// - null → when there are no notes
/// - "some string" → plain string (FullChangelog = false, default)
/// - ["note A", "note B"] → array of strings (after broken normalize in older TS)
/// - [{ version, note }, ...] → array of objects (FullChangelog = true)
/// All forms are normalised to ReleaseNoteInfo[] so the C# model stays clean.
/// See: https://github.com/ElectronNET/Electron.NET/issues/1039
/// </summary>
public class ReleaseNotesConverter : JsonConverter<ReleaseNoteInfo[]>
{
// Ensure the converter is called even when the JSON token is null,
// so we can return an empty array instead of null.
public override bool HandleNull => true;
public override ReleaseNoteInfo[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return Array.Empty<ReleaseNoteInfo>();
case JsonTokenType.String:
// Plain string: "Some release notes"
return new[] { new ReleaseNoteInfo { Note = reader.GetString() } };
case JsonTokenType.StartArray:
var list = new List<ReleaseNoteInfo>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
if (reader.TokenType == JsonTokenType.String)
{
// Array of strings: ["Note A", "Note B"]
list.Add(new ReleaseNoteInfo { Note = reader.GetString() });
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
// Array of objects: [{ "version": "1.0", "note": "..." }]
var entry = JsonSerializer.Deserialize<ReleaseNoteInfo>(ref reader, options) ?? new ReleaseNoteInfo();
list.Add(entry);
}
else
{
reader.Skip();
}
}
return list.ToArray();
default:
throw new JsonException($"Unexpected token {reader.TokenType} when reading releaseNotes.");
}
}
public override void Write(Utf8JsonWriter writer, ReleaseNoteInfo[] value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}
if (value.Length == 0)
{
writer.WriteStartArray();
writer.WriteEndArray();
return;
}
writer.WriteStartArray();
foreach (var item in value)
{
writer.WriteStartObject();
if (item.Version is not null)
{
writer.WriteString("version", item.Version);
}
writer.WriteString("note", item.Note);
writer.WriteEndObject();
}
writer.WriteEndArray();
}
}

View File

@@ -1,6 +1,7 @@
namespace ElectronNET
{
using ElectronNET.API;
using ElectronNET.API.Bridge;
using ElectronNET.Runtime;
using ElectronNET.Runtime.Controllers;
using ElectronNET.Runtime.Data;
@@ -50,6 +51,8 @@
internal static int? ElectronProcessId { get; set; }
internal static Func<Task> OnAppReadyCallback { get; set; }
internal static ISocketConnection GetSocket()
{
return RuntimeControllerCore?.Socket;

View File

@@ -1,6 +1,7 @@
namespace ElectronNET.Runtime.Controllers
{
using ElectronNET.API;
using ElectronNET.API.Bridge;
using ElectronNET.Runtime.Services;
using ElectronNET.Runtime.Services.ElectronProcess;
using ElectronNET.Runtime.Services.SocketBridge;

View File

@@ -1,6 +1,7 @@
namespace ElectronNET.Runtime.Controllers
{
using ElectronNET.API;
using ElectronNET.API.Bridge;
using ElectronNET.Common;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Helpers;

View File

@@ -16,7 +16,7 @@
{
}
internal override ISocketConnection Socket
internal override SocketIOConnection Socket
{
get
{

View File

@@ -63,13 +63,15 @@
private static bool? CheckUnpackaged2()
{
var dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
var dir = new DirectoryInfo(baseDir);
if (dir.Name == "bin" && dir.Parent?.Name == "resources")
{
return false;
}
if (dir.GetDirectories().Any(e => e.Name == ".electron"))
if (Directory.Exists(Path.Combine(baseDir, ".electron")))
{
return true;
}
@@ -84,7 +86,6 @@
return true;
}
return null;
}

View File

@@ -7,7 +7,6 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ElectronNET.Common;
using ElectronNET.Runtime.Data;
@@ -19,7 +18,6 @@
internal class ElectronProcessActive : ElectronProcessBase
{
private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at .* using ([a-f0-9]+)$");
private readonly bool isUnpackaged;
private readonly string electronBinaryName;
private readonly string extraArguments;
@@ -106,6 +104,7 @@
}
var osPart = buildInfoRid.Split('-').First();
var mismatch = false;
switch (osPart)
@@ -159,17 +158,9 @@
return Task.CompletedTask;
}
private async Task StartInternal(string startCmd, string args, string directory)
private async Task StartInternal(string startCmd, string args, string directoriy)
{
var tcs = new TaskCompletionSource();
using var cts = new CancellationTokenSource(2 * 60_000); // cancel after 2 minutes
using var _ = cts.Token.Register(() =>
{
// Time is over - let's kill the process and move on
this.process.Cancel();
// We don't want to raise exceptions here - just pass the barrier
tcs.TrySetResult();
});
void Read_SocketIO_Parameters(object sender, string line)
{
@@ -188,28 +179,15 @@
}
}
void Monitor_SocketIO_Failure(object sender, EventArgs e)
{
// We don't want to raise exceptions here - just pass the barrier
if (tcs.Task.IsCompleted)
{
this.Process_Exited(sender, e);
}
else
{
tcs.TrySetResult();
}
}
try
{
Console.Error.WriteLine("[StartInternal]: startCmd: {0}", startCmd);
Console.Error.WriteLine("[StartInternal]: args: {0}", args);
this.process = new ProcessRunner("ElectronRunner");
this.process.ProcessExited += Monitor_SocketIO_Failure;
this.process.ProcessExited += this.Process_Exited;
this.process.LineReceived += Read_SocketIO_Parameters;
this.process.Run(startCmd, args, directory);
this.process.Run(startCmd, args, directoriy);
await tcs.Task.ConfigureAwait(false);
@@ -221,11 +199,11 @@
Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardOutput);
Task.Run(() => this.TransitionState(LifetimeState.Stopped));
throw new Exception("Failed to launch the Electron process.");
}
else
{
this.TransitionState(LifetimeState.Ready);
}
this.TransitionState(LifetimeState.Ready);
}
catch (Exception ex)
{

View File

@@ -25,7 +25,6 @@
this.CollectProcessData();
this.SetElectronExecutable();
ElectronNetRuntime.StartupMethod = this.DetectAppTypeAndStartup();
Console.WriteLine((string)("Evaluated StartupMethod: " + ElectronNetRuntime.StartupMethod));
@@ -57,21 +56,11 @@
if (isLaunchedByDotNet)
{
if (isUnPackaged)
{
return StartupMethod.UnpackedDotnetFirst;
}
return StartupMethod.PackagedDotnetFirst;
return isUnPackaged ? StartupMethod.UnpackedDotnetFirst: StartupMethod.PackagedDotnetFirst;
}
else
{
if (isUnPackaged)
{
return StartupMethod.UnpackedElectronFirst;
}
return StartupMethod.PackagedElectronFirst;
return isUnPackaged ? StartupMethod.UnpackedElectronFirst: StartupMethod.PackagedElectronFirst;
}
}
@@ -86,10 +75,10 @@
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);
}
}
@@ -99,10 +88,10 @@
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);
}
}
@@ -124,7 +113,8 @@
private void SetElectronExecutable()
{
string executable = ElectronNetRuntime.BuildInfo.ElectronExecutable;
var executable = ElectronNetRuntime.BuildInfo.ElectronExecutable;
if (string.IsNullOrEmpty(executable))
{
throw new Exception("AssemblyMetadataAttribute 'ElectronExecutable' could not be found!");

View File

@@ -44,98 +44,5 @@
return builder;
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core application and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process, forwarded to Electron.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="WebApplicationBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// var builder = WebApplication.CreateBuilder(args)
/// .UseElectron(args, async (processArgs) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
///
/// var app = builder.Build();
/// app.MapRazorPages();
/// app.Run();
/// </code>
/// </example>
public static WebApplicationBuilder UseElectron(this WebApplicationBuilder builder, string[] args, Func<string[], Task> onAppReadyCallback)
{
builder.WebHost.UseElectron(args, onAppReadyCallback);
return builder;
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core application and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process, forwarded to Electron.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="WebApplicationBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// var builder = WebApplication.CreateBuilder(args)
/// .UseElectron(args, async (serviceProvider) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
///
/// var app = builder.Build();
/// app.MapRazorPages();
/// app.Run();
/// </code>
/// </example>
public static WebApplicationBuilder UseElectron(this WebApplicationBuilder builder, string[] args, Func<IServiceProvider, Task> onAppReadyCallback)
{
builder.WebHost.UseElectron(args, onAppReadyCallback);
return builder;
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core application and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process, forwarded to Electron.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="WebApplicationBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// var builder = WebApplication.CreateBuilder(args)
/// .UseElectron(args, async (serviceProvider, processArgs) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
///
/// var app = builder.Build();
/// app.MapRazorPages();
/// app.Run();
/// </code>
/// </example>
public static WebApplicationBuilder UseElectron(this WebApplicationBuilder builder, string[] args, Func<IServiceProvider, string[], Task> onAppReadyCallback)
{
builder.WebHost.UseElectron(args, onAppReadyCallback);
return builder;
}
}
}

View File

@@ -61,154 +61,8 @@
/// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<Task> onAppReadyCallback)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IAppReadyCallbackResolver>(_ => new AppReadyCallbackResolver(onAppReadyCallback));
});
ElectronNetRuntime.OnAppReadyCallback = onAppReadyCallback;
return UseElectronCore(builder, args);
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core web host and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="IWebHostBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// 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&lt;Startup&gt;();
/// webBuilder.UseElectron(args, async (processArgs) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
/// })
/// .Build()
/// .Run();
/// }
/// }
/// </code>
/// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<string[], Task> onAppReadyCallback)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IAppReadyCallbackResolver>(_ => new AppReadyCallbackResolver(args, onAppReadyCallback));
});
return UseElectronCore(builder, args);
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core web host and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="IWebHostBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// 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&lt;Startup&gt;();
/// webBuilder.UseElectron(args, async (serviceProvider) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
/// })
/// .Build()
/// .Run();
/// }
/// }
/// </code>
/// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<IServiceProvider, Task> onAppReadyCallback)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IAppReadyCallbackResolver>(provider => new AppReadyCallbackResolver(provider, onAppReadyCallback));
});
return UseElectronCore(builder, args);
}
/// <summary>
/// Adds Electron.NET support to the current ASP.NET Core web host and registers an application-ready callback.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/> to extend.</param>
/// <param name="args">The command-line arguments passed to the process.</param>
/// <param name="onAppReadyCallback">
/// An asynchronous callback invoked when the Electron app is ready. Use this to create windows or perform initialization.
/// </param>
/// <returns>
/// The same <see cref="IWebHostBuilder"/> instance to enable fluent configuration.
/// </returns>
/// <example>
/// <code language="csharp">
/// 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&lt;Startup&gt;();
/// webBuilder.UseElectron(args, async (serviceProvider, processArgs) =>
/// {
/// // Create the main browser window or perform other startup tasks.
/// });
/// })
/// .Build()
/// .Run();
/// }
/// }
/// </code>
/// </example>
public static IWebHostBuilder UseElectron(this IWebHostBuilder builder, string[] args, Func<IServiceProvider, string[], Task> onAppReadyCallback)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IAppReadyCallbackResolver>(provider => new AppReadyCallbackResolver(provider, args, onAppReadyCallback));
});
return UseElectronCore(builder, args);
}
private static IWebHostBuilder UseElectronCore(IWebHostBuilder builder, string[] args)
{
// no matter how this is set - let's unset to prevent Electron not starting as expected
// e.g., VS Code sets this env variable, but this will cause `require("electron")` to not
// work as expected, see issue #952
@@ -221,7 +75,7 @@
// now we have implemented the live reload if app is run using /watch then we need to use the default project path.
// For port 0 (dynamic port assignment), Kestrel requires binding to specific IP (127.0.0.1) not localhost
var host = webPort == 0 ? "127.0.0.1" : "localhost";
var host = webPort == 0? "127.0.0.1" : "localhost";
if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot"))
{

View File

@@ -97,4 +97,4 @@ namespace ElectronNET.AspNet.Middleware
await context.Response.WriteAsync("Unauthorized: Authentication required");
}
}
}
}

View File

@@ -7,6 +7,7 @@
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection;
using ElectronNET.API;
using ElectronNET.API.Bridge;
using ElectronNET.AspNet.Services;
using ElectronNET.Common;
using ElectronNET.Runtime.Controllers;
@@ -17,15 +18,13 @@
{
private readonly IServer server;
private readonly AspNetLifetimeAdapter aspNetLifetimeAdapter;
private readonly IAppReadyCallbackResolver callbackResolver;
private readonly IElectronAuthenticationService authenticationService;
private SocketBridgeService socketBridge;
protected RuntimeControllerAspNetBase(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IAppReadyCallbackResolver callbackResolver, IElectronAuthenticationService authenticationService = null)
protected RuntimeControllerAspNetBase(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IElectronAuthenticationService authenticationService = null)
{
this.server = server;
this.aspNetLifetimeAdapter = aspNetLifetimeAdapter;
this.callbackResolver = callbackResolver;
this.authenticationService = authenticationService;
this.aspNetLifetimeAdapter.Ready += this.AspNetLifetimeAdapter_Ready;
this.aspNetLifetimeAdapter.Stopping += this.AspNetLifetimeAdapter_Stopping;
@@ -132,15 +131,15 @@
private async Task RunReadyCallback()
{
if (!callbackResolver.HasCallback)
if (ElectronNetRuntime.OnAppReadyCallback == null)
{
Console.WriteLine("Warning: No OnReadyCallback provided in UseElectron() setup.");
Console.WriteLine("Warning: Non OnReadyCallback provided in UseElectron() setup.");
return;
}
try
{
await callbackResolver.Invoke().ConfigureAwait(false);
await ElectronNetRuntime.OnAppReadyCallback().ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -2,19 +2,19 @@
{
using System;
using System.Threading.Tasks;
using System.Security.Principal;
using Microsoft.AspNetCore.Hosting.Server;
using ElectronNET.AspNet.Services;
using ElectronNET.Common;
using ElectronNET.Runtime.Data;
using ElectronNET.Runtime.Helpers;
using ElectronNET.Runtime.Services.ElectronProcess;
using System.Security.Principal;
internal class RuntimeControllerAspNetDotnetFirst : RuntimeControllerAspNetBase
{
private ElectronProcessBase electronProcess;
public RuntimeControllerAspNetDotnetFirst(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IAppReadyCallbackResolver callbackResolver, IElectronAuthenticationService authenticationService = null) : base(server, aspNetLifetimeAdapter, callbackResolver, authenticationService)
public RuntimeControllerAspNetDotnetFirst(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IElectronAuthenticationService authenticationService = null) : base(server, aspNetLifetimeAdapter, authenticationService)
{
}
@@ -24,8 +24,8 @@
{
var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged();
var electronBinaryName = ElectronNetRuntime.ElectronExecutable;
var args = Environment.CommandLine;
var port = ElectronNetRuntime.ElectronSocketPort ?? 0;
var args = Environment.CommandLine;
this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, port);
this.electronProcess.Ready += this.ElectronProcess_Ready;

View File

@@ -11,7 +11,7 @@
{
private ElectronProcessBase electronProcess;
public RuntimeControllerAspNetElectronFirst(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IAppReadyCallbackResolver callbackResolver, IElectronAuthenticationService authenticationService = null) : base(server, aspNetLifetimeAdapter, callbackResolver, authenticationService)
public RuntimeControllerAspNetElectronFirst(IServer server, AspNetLifetimeAdapter aspNetLifetimeAdapter, IElectronAuthenticationService authenticationService = null) : base(server, aspNetLifetimeAdapter, authenticationService)
{
}

View File

@@ -1,46 +0,0 @@
using System;
using System.Threading.Tasks;
namespace ElectronNET.AspNet.Runtime
{
internal class AppReadyCallbackResolver : IAppReadyCallbackResolver
{
private readonly Func<Task> _callback;
public AppReadyCallbackResolver()
{ }
public AppReadyCallbackResolver(Func<Task> callback)
{
_callback = callback;
}
public AppReadyCallbackResolver(string[] args, Func<string[], Task> callback)
{
if (callback != null)
{
_callback = () => callback.Invoke(args);
}
}
public AppReadyCallbackResolver(IServiceProvider serviceProvider, Func<IServiceProvider, Task> callback)
{
if (callback != null)
{
_callback = () => callback.Invoke(serviceProvider);
}
}
public AppReadyCallbackResolver(IServiceProvider serviceProvider, string[] args, Func<IServiceProvider, string[], Task> callback)
{
if (callback != null)
{
_callback = () => callback.Invoke(serviceProvider, args);
}
}
public bool HasCallback => _callback != null;
public Task Invoke() => _callback?.Invoke() ?? Task.CompletedTask;
}
}

View File

@@ -1,11 +0,0 @@
using System.Threading.Tasks;
namespace ElectronNET.AspNet.Runtime
{
internal interface IAppReadyCallbackResolver
{
public bool HasCallback { get; }
public Task Invoke();
}
}

View File

@@ -50,4 +50,4 @@ namespace ElectronNET.AspNet.Services
return result == 0;
}
}
}
}

View File

@@ -21,4 +21,4 @@ namespace ElectronNET.AspNet.Services
/// <returns>True if token is valid, false otherwise</returns>
bool ValidateToken(string token);
}
}
}

View File

@@ -70,7 +70,7 @@
<ProjectReference Include="..\ElectronNET.API\ElectronNET.API.csproj" Condition="$(ElectronNetDevMode)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ElectronNET.Core" Version="0.5.1" Condition="'$(ElectronNetDevMode)' != 'true'" />
<PackageReference Include="ElectronNET.Core" Version="0.4.1" Condition="'$(ElectronNetDevMode)' != 'true'" />
</ItemGroup>
<Import Project="..\ElectronNET\build\ElectronNET.Core.targets" Condition="$(ElectronNetDevMode)" />

View File

@@ -39,15 +39,6 @@
"undici-types": "~7.14.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -110,21 +101,20 @@
}
},
"node_modules/engine.io": {
"version": "6.6.8",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz",
"integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==",
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"@types/ws": "^8.5.12",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.4.1",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.20.1"
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
@@ -139,23 +129,6 @@
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -220,62 +193,28 @@
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz",
"integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==",
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.4.1",
"ws": "~8.20.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.4.1"
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -306,9 +245,9 @@
}
},
"node_modules/ws": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"

View File

@@ -1,14 +1,6 @@
"use strict";
const electron_updater_1 = require("electron-updater");
let electronSocket;
function normalize(updateInfo) {
if (typeof updateInfo?.releaseNotes === "string") {
updateInfo.releaseNotes = [{ note: updateInfo.releaseNotes }];
}
else if (Array.isArray(updateInfo?.releaseNotes)) {
updateInfo.releaseNotes = updateInfo.releaseNotes.map((entry) => typeof entry === "string" ? { note: entry } : entry);
}
}
module.exports = (socket) => {
electronSocket = socket;
socket.on("register-autoUpdater-error", (id) => {
@@ -23,13 +15,11 @@ module.exports = (socket) => {
});
socket.on("register-autoUpdater-update-available", (id) => {
electron_updater_1.autoUpdater.on("update-available", (updateInfo) => {
normalize(updateInfo);
electronSocket.emit("autoUpdater-update-available" + id, updateInfo);
});
});
socket.on("register-autoUpdater-update-not-available", (id) => {
electron_updater_1.autoUpdater.on("update-not-available", (updateInfo) => {
normalize(updateInfo);
electronSocket.emit("autoUpdater-update-not-available" + id, updateInfo);
});
});
@@ -40,7 +30,6 @@ module.exports = (socket) => {
});
socket.on("register-autoUpdater-update-downloaded", (id) => {
electron_updater_1.autoUpdater.on("update-downloaded", (updateInfo) => {
normalize(updateInfo);
electronSocket.emit("autoUpdater-update-downloaded" + id, updateInfo);
});
});
@@ -100,7 +89,6 @@ module.exports = (socket) => {
electron_updater_1.autoUpdater
.checkForUpdatesAndNotify()
.then((updateCheckResult) => {
normalize(updateCheckResult?.updateInfo);
electronSocket.emit("autoUpdater-checkForUpdatesAndNotify-completed" + guid, updateCheckResult);
})
.catch((error) => {
@@ -111,7 +99,6 @@ module.exports = (socket) => {
electron_updater_1.autoUpdater
.checkForUpdates()
.then((updateCheckResult) => {
normalize(updateCheckResult?.updateInfo);
electronSocket.emit("autoUpdater-checkForUpdates-completed" + guid, updateCheckResult);
})
.catch((error) => {

File diff suppressed because one or more lines are too long

View File

@@ -3,16 +3,6 @@ import { autoUpdater } from "electron-updater";
let electronSocket: Socket;
function normalize(updateInfo) {
if (typeof updateInfo?.releaseNotes === "string") {
updateInfo.releaseNotes = [{ note: updateInfo.releaseNotes }];
} else if (Array.isArray(updateInfo?.releaseNotes)) {
updateInfo.releaseNotes = updateInfo.releaseNotes.map((entry) =>
typeof entry === "string" ? { note: entry } : entry,
);
}
}
export = (socket: Socket) => {
electronSocket = socket;
@@ -30,14 +20,12 @@ export = (socket: Socket) => {
socket.on("register-autoUpdater-update-available", (id) => {
autoUpdater.on("update-available", (updateInfo) => {
normalize(updateInfo);
electronSocket.emit("autoUpdater-update-available" + id, updateInfo);
});
});
socket.on("register-autoUpdater-update-not-available", (id) => {
autoUpdater.on("update-not-available", (updateInfo) => {
normalize(updateInfo);
electronSocket.emit("autoUpdater-update-not-available" + id, updateInfo);
});
});
@@ -50,7 +38,6 @@ export = (socket: Socket) => {
socket.on("register-autoUpdater-update-downloaded", (id) => {
autoUpdater.on("update-downloaded", (updateInfo) => {
normalize(updateInfo);
electronSocket.emit("autoUpdater-update-downloaded" + id, updateInfo);
});
});
@@ -156,7 +143,6 @@ export = (socket: Socket) => {
autoUpdater
.checkForUpdatesAndNotify()
.then((updateCheckResult) => {
normalize(updateCheckResult?.updateInfo);
electronSocket.emit(
"autoUpdater-checkForUpdatesAndNotify-completed" + guid,
updateCheckResult,
@@ -174,7 +160,6 @@ export = (socket: Socket) => {
autoUpdater
.checkForUpdates()
.then((updateCheckResult) => {
normalize(updateCheckResult?.updateInfo);
electronSocket.emit(
"autoUpdater-checkForUpdates-completed" + guid,
updateCheckResult,

View File

@@ -280,20 +280,8 @@ module.exports = (socket, app) => {
// Append authentication token to initial URL if available
const token = global["authToken"];
if (token) {
try {
const url = new URL(loadUrl);
const isLocal = url.hostname === "localhost" ||
url.hostname === "127.0.0.1" ||
url.hostname === "::1";
if (isLocal) {
url.searchParams.set("token", token);
}
window.loadURL(url.toString());
}
catch {
// Handle invalid URLs or file:// URLs if needed
window.loadURL(loadUrl);
}
const separator = loadUrl.includes("?") ? "&" : "?";
window.loadURL(`${loadUrl}${separator}token=${token}`);
}
else {
window.loadURL(loadUrl);

File diff suppressed because one or more lines are too long

View File

@@ -313,23 +313,8 @@ export = (socket: Socket, app: Electron.App) => {
const token = global["authToken"];
if (token) {
try {
const url = new URL(loadUrl);
const isLocal =
url.hostname === "localhost" ||
url.hostname === "127.0.0.1" ||
url.hostname === "::1";
if (isLocal) {
url.searchParams.set("token", token);
}
window.loadURL(url.toString());
} catch {
// Handle invalid URLs or file:// URLs if needed
window.loadURL(loadUrl);
}
const separator = loadUrl.includes("?") ? "&" : "?";
window.loadURL(`${loadUrl}${separator}token=${token}`);
} else {
window.loadURL(loadUrl);
}

View File

@@ -57,7 +57,6 @@ if (app.commandLine.hasSwitch('electronurl')) {
// Custom startup hook: look for custom_main.js and invoke its onStartup(host) if present.
// If the hook returns false, abort Electron startup.
try {
const fs = require('fs');
const customMainPath = path.join(__dirname, 'custom_main.js');
if (fs.existsSync(customMainPath)) {
const customMain = require(customMainPath);
@@ -83,7 +82,7 @@ let manifestJsonFilePath = path.join(currentPath, manifestJsonFileName);
// if running unpackedelectron, lets change the path
if (unpackedelectron || unpackeddotnet) {
console.debug('Running in unpackaged mode, dir: ' + currentPath);
console.debug('Running in unpacked mode, dir: ' + currentPath);
manifestJsonFilePath = path.join(currentPath, manifestJsonFileName);
currentBinPath = path.join(currentPath, '../'); // go to project directory
@@ -133,8 +132,7 @@ if (manifestJsonFile.singleInstance) {
// Collect user supplied command line args (excluding those handled by Electron host itself)
function getForwardedArgs() {
const skipSwitches = new Set(['unpackedelectron', 'unpackeddotnet', 'dotnetpacked']);
const sliceIndex = app.isPackaged ? 1 : 2;
return process.argv.slice(sliceIndex).filter(arg => {
return process.argv.slice(2).filter(arg => {
if (!arg) return false;
// Node/Electron internal or we already process them
if (arg.startsWith('--manifest')) return false;
@@ -151,7 +149,7 @@ function getForwardedArgs() {
const forwardedArgs = getForwardedArgs();
app.on('ready', () => {
app.on('ready', () => {
// Fix ERR_UNKNOWN_URL_SCHEME using file protocol
// https://github.com/electron/electron/issues/23757
////protocol.registerFileProtocol('file', (request, callback) => {
@@ -173,28 +171,31 @@ app.on('ready', () => {
});
app.on('quit', async (event, exitCode) => {
// Clean up Socket.IO resources (legacy mode only)
if (server) {
try {
server.close();
server.closeAllConnections();
} catch (e) {
console.error(e);
console.error('Error closing Socket.IO server:', e);
}
}
if (apiProcess) {
// Clean up API process (Socket.IO mode only)
if (typeof apiProcess !== 'undefined' && apiProcess) {
try {
apiProcess.kill();
} catch (e) {
console.error(e);
console.error('Error killing API process:', e);
}
}
if (io && io.close) {
// Clean up Socket.IO connection
if (typeof io !== 'undefined' && io && typeof io.close === 'function') {
try {
io.close();
} catch (e) {
console.error(e);
console.error('Error closing Socket.IO connection:', e);
}
}
});

View File

@@ -9,10 +9,12 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@microsoft/signalr": "^8.0.7",
"dasherize": "^2.0.0",
"electron-host-hook": "file:./ElectronHostHook",
"electron-updater": "^6.6.2",
"image-size": "^1.2.1",
"portscanner": "^2.2.0",
"socket.io": "^4.8.1"
},
"devDependencies": {
@@ -251,6 +253,40 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@microsoft/signalr": {
"version": "8.0.17",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.17.tgz",
"integrity": "sha512-5pM6xPtKZNJLO0Tq5nQasVyPFwi/WBY3QB5uc/v3dIPTpS1JXQbaXAQAPxFoQ5rTBFE094w8bbqkp17F9ReQvA==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
"fetch-cookie": "^2.0.3",
"node-fetch": "^2.6.7",
"ws": "^7.5.10"
}
},
"node_modules/@microsoft/signalr/node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -355,15 +391,6 @@
"@types/node": "*"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -375,6 +402,18 @@
"@types/node": "*"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -450,6 +489,15 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -868,21 +916,20 @@
}
},
"node_modules/engine.io": {
"version": "6.6.9",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz",
"integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==",
"version": "6.6.5",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
"integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"@types/ws": "^8.5.12",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.4.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.21.0"
"ws": "~8.18.3"
},
"engines": {
"node": ">=10.2.0"
@@ -1104,6 +1151,24 @@
"node": ">=0.10.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -1156,6 +1221,16 @@
"pend": "~1.2.0"
}
},
"node_modules/fetch-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
"integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==",
"license": "Unlicense",
"dependencies": {
"set-cookie-parser": "^2.4.8",
"tough-cookie": "^4.0.0"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -1201,9 +1276,9 @@
}
},
"node_modules/flatted": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC"
},
@@ -1487,6 +1562,15 @@
"node": ">=0.10.0"
}
},
"node_modules/is-number-like": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz",
"integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==",
"license": "ISC",
"dependencies": {
"lodash.isfinite": "^3.3.2"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -1591,6 +1675,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
@@ -1604,6 +1694,12 @@
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.isfinite": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz",
"integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -1701,6 +1797,26 @@
"node": ">= 0.6"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
@@ -1844,6 +1960,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/portscanner": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz",
"integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==",
"license": "MIT",
"dependencies": {
"async": "^2.6.0",
"is-number-like": "^1.0.3"
},
"engines": {
"node": ">=0.4",
"npm": ">=1.0.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -1864,6 +1994,18 @@
"node": ">=0.4.0"
}
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -1879,12 +2021,17 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
@@ -1907,6 +2054,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
@@ -2000,6 +2153,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2042,13 +2201,13 @@
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.8",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz",
"integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==",
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
"integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
"license": "MIT",
"dependencies": {
"debug": "~4.4.1",
"ws": "~8.21.0"
"ws": "~8.18.3"
}
},
"node_modules/socket.io-parser": {
@@ -2117,6 +2276,36 @@
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"license": "MIT"
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tough-cookie/node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -2184,6 +2373,16 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -2193,6 +2392,22 @@
"node": ">= 0.8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -2227,9 +2442,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"

View File

@@ -3,7 +3,6 @@
<PropertyGroup>
<!-- When this is enabled, the project will be switched from nuget packages to consuming the ElectronNet orchestration directly -->
<ElectronNetDevMode>true</ElectronNetDevMode>
<IsTest>True</IsTest>
</PropertyGroup>
<Import Project="..\ElectronNET\build\ElectronNET.Core.props" Condition="$(ElectronNetDevMode)" />

View File

@@ -1,193 +0,0 @@
using System.Diagnostics;
namespace ElectronNET.IntegrationTests.Tests;
/// <summary>
/// Unit tests for ElectronNET.MigrationChecks.targets - no Electron runtime required.
/// Covers GitHub issue #1035: System.IO.File.ReadAllLines is not available as an MSBuild
/// property function on all platforms (e.g. macOS GitHub Actions), causing MSB4185.
/// </summary>
public class MigrationChecksTargetsTests
{
private static readonly string TargetsFilePath = FindTargetsFile();
/// <summary>
/// Walks up the directory tree from <see cref="AppContext.BaseDirectory"/> until it finds
/// the migration checks targets file. This is robust against varying output paths
/// (with or without RID subfolder, debug/release, etc.).
/// </summary>
private static string FindTargetsFile()
{
const string RelativeFromRepoRoot =
"src/ElectronNET/build/ElectronNET.MigrationChecks.targets";
const string RelativeFromSrc =
"ElectronNET/build/ElectronNET.MigrationChecks.targets";
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
var fromRepoRoot = Path.Combine(dir.FullName, RelativeFromRepoRoot);
if (File.Exists(fromRepoRoot))
{
return Path.GetFullPath(fromRepoRoot);
}
var fromSrc = Path.Combine(dir.FullName, RelativeFromSrc);
if (File.Exists(fromSrc))
{
return Path.GetFullPath(fromSrc);
}
dir = dir.Parent;
}
throw new FileNotFoundException(
"Could not locate ElectronNET.MigrationChecks.targets by walking up from " +
$"'{AppContext.BaseDirectory}'.");
}
// -----------------------------------------------------------------------
// Content-level test (RED before fix, GREEN after fix on ALL platforms)
// -----------------------------------------------------------------------
[Fact]
public void MigrationChecksTargets_ShouldNotUseReadAllLines()
{
// The file must exist - if this fails the path constant above is wrong.
File.Exists(TargetsFilePath).Should().BeTrue(
$"targets file must exist at '{TargetsFilePath}'");
var content = File.ReadAllText(TargetsFilePath);
// System.IO.File::ReadAllLines is not in the MSBuild property-function
// whitelist on all platforms (MSB4185 on macOS GitHub Actions, see #1035).
// ReadAllText must be used instead.
content.Should().NotContain(
"::ReadAllLines(",
"because ReadAllLines is not available as an MSBuild property function on all " +
"platforms. Use ReadAllText instead (GitHub issue #1035).");
}
// -----------------------------------------------------------------------
// Functional build test - verifies no MSB4185 at runtime
// (RED on platforms where ReadAllLines is restricted, GREEN after fix)
// -----------------------------------------------------------------------
[Fact]
public async Task MigrationChecksTargets_BuildWithCleanPackageJson_ShouldSucceedWithoutMSB4185()
{
// Positive case: a package.json that does NOT mention electron.
// The migration check must successfully read the file via ReadAllText
// (the code path fixed by issue #1035) without producing MSB4185.
var tempDir = CreateTempProjectDirectory();
try
{
await File.WriteAllTextAsync(
Path.Combine(tempDir, "package.json"),
"""{ "devDependencies": { "vite": "^5.0.0" } }""");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetBuildAsync(tempDir);
exitCode.Should().Be(0,
$"the build must succeed when the package.json contains no electron references. " +
$"Full build output:\n{output}");
output.Should().NotContain(
"MSB4185",
$"ReadAllLines must not be used as an MSBuild property function. " +
$"Full build output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task MigrationChecksTargets_BuildWithPackageJsonContainingElectron_ShouldEmitELECTRON008WarningWithoutMSB4185()
{
// Negative case: a package.json that DOES contain "electron".
// The migration check must still read the file successfully (no MSB4185)
// and must emit the expected ELECTRON008 warning. ELECTRON008 is a
// <Warning>, not an <Error>, so the build itself still succeeds.
var tempDir = CreateTempProjectDirectory();
try
{
await File.WriteAllTextAsync(
Path.Combine(tempDir, "package.json"),
"""{ "devDependencies": { "electron": "^30.0.0" } }""");
await WriteMinimalCsprojAsync(tempDir);
var (exitCode, output) = await RunDotnetBuildAsync(tempDir);
exitCode.Should().Be(0,
$"ELECTRON008 is a Warning (not an Error) so the build itself must still " +
$"succeed. Full build output:\n{output}");
output.Should().NotContain(
"MSB4185",
$"ReadAllLines must not be used as an MSBuild property function. " +
$"Full build output:\n{output}");
output.Should().Contain(
"ELECTRON008",
$"the migration check must still detect electron references in package.json " +
$"after the ReadAllText migration. Full build output:\n{output}");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
private static string CreateTempProjectDirectory()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"electron-net-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
return tempDir;
}
private static Task WriteMinimalCsprojAsync(string tempDir)
{
// A minimal csproj that only imports the migration checks targets to keep the
// build fast. Note: MSBuildProjectDirectory is a reserved MSBuild property and
// must not be redefined manually; MSBuild sets it automatically to the folder
// of the csproj (which is tempDir here).
var targetsPathEscaped = TargetsFilePath.Replace("'", "&apos;");
return File.WriteAllTextAsync(
Path.Combine(tempDir, "TestApp.csproj"),
$"""
<Project>
<Import Project="{targetsPathEscaped}" />
<Target Name="Build" DependsOnTargets="ElectronMigrationChecks" />
</Project>
""");
}
private static async Task<(int ExitCode, string Output)> RunDotnetBuildAsync(string workingDirectory)
{
var psi = new ProcessStartInfo("dotnet", "build --nologo -v:minimal")
{
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
using var process = Process.Start(psi)!;
var stdOut = await process.StandardOutput.ReadToEndAsync();
var stdErr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode, stdOut + stdErr);
}
}

View File

@@ -1,115 +0,0 @@
namespace ElectronNET.IntegrationTests.Tests
{
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using ElectronNET.API.Entities;
/// <summary>
/// Unit tests for UpdateInfo JSON deserialization.
/// Tests the fix for issue #1039: releaseNotes arrives as string or string[] from electron-builder
/// when FullChangelog is false (default), but the C# model expects ReleaseNoteInfo[].
/// No Electron runtime is required for these tests.
/// </summary>
public class UpdateInfoSerializationTests
{
// camelCase + ignore null — mirrors ElectronJson.Options used in production
private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
// electron-builder sends a plain string when FullChangelog = false (default)
[Fact]
public void Deserialize_WithStringReleaseNotes_ShouldConvertToSingleEntry()
{
var json = """{"version":"1.2.3","releaseNotes":"Some release notes"}""";
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
result.Should().NotBeNull();
result.ReleaseNotes.Should().HaveCount(1);
result.ReleaseNotes[0].Note.Should().Be("Some release notes");
}
// After the (incorrect) TypeScript normalize: string → ["string"] which is an array of strings,
// not an array of ReleaseNoteInfo objects. The C# model must handle this too.
[Fact]
public void Deserialize_WithArrayOfStringReleaseNotes_ShouldConvertToEntries()
{
var json = """{"version":"1.2.3","releaseNotes":["Note A","Note B"]}""";
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
result.Should().NotBeNull();
result.ReleaseNotes.Should().HaveCount(2);
result.ReleaseNotes[0].Note.Should().Be("Note A");
result.ReleaseNotes[1].Note.Should().Be("Note B");
}
// When FullChangelog = true, electron-builder sends proper objects; this must keep working.
[Fact]
public void Deserialize_WithProperReleaseNoteObjects_ShouldDeserializeNormally()
{
var json = """{"version":"1.2.3","releaseNotes":[{"version":"1.2.3","note":"Proper note"}]}""";
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
result.Should().NotBeNull();
result.ReleaseNotes.Should().HaveCount(1);
result.ReleaseNotes[0].Version.Should().Be("1.2.3");
result.ReleaseNotes[0].Note.Should().Be("Proper note");
}
// Null releaseNotes should result in an empty array (matching the default value).
[Fact]
public void Deserialize_WithNullReleaseNotes_ShouldReturnEmptyArray()
{
var json = """{"version":"1.2.3","releaseNotes":null}""";
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
result.Should().NotBeNull();
result.ReleaseNotes.Should().NotBeNull();
result.ReleaseNotes.Should().BeEmpty();
}
// Absent releaseNotes field should keep the default empty array.
[Fact]
public void Deserialize_WithMissingReleaseNotes_ShouldReturnEmptyArray()
{
var json = """{"version":"1.2.3"}""";
var result = JsonSerializer.Deserialize<UpdateInfo>(json, Options);
result.Should().NotBeNull();
result.ReleaseNotes.Should().NotBeNull();
result.ReleaseNotes.Should().BeEmpty();
}
// Empty array must serialize as [] not null, so round-trips and downstream
// consumers don't receive unexpected null for a non-null array value.
[Fact]
public void Serialize_WithEmptyReleaseNotes_ShouldProduceEmptyArray()
{
var updateInfo = new UpdateInfo { Version = "1.2.3", ReleaseNotes = Array.Empty<ReleaseNoteInfo>() };
var json = JsonSerializer.Serialize(updateInfo, Options);
json.Should().Contain("\"releaseNotes\":[]");
}
// Null value: with DefaultIgnoreCondition.WhenWritingNull the property is
// omitted entirely at the serializer level (before Write() is called).
[Fact]
public void Serialize_WithNullReleaseNotes_ShouldOmitProperty()
{
var updateInfo = new UpdateInfo { Version = "1.2.3", ReleaseNotes = null };
var json = JsonSerializer.Serialize(updateInfo, Options);
json.Should().NotContain("releaseNotes");
}
}
}

View File

@@ -1,24 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronNET.Samples.AuthMiddleware", "ElectronNET.Samples.AuthMiddleware.csproj", "{1A87C5B5-16EE-5DA0-33B4-17DA71675A87}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1A87C5B5-16EE-5DA0-33B4-17DA71675A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A87C5B5-16EE-5DA0-33B4-17DA71675A87}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A87C5B5-16EE-5DA0-33B4-17DA71675A87}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A87C5B5-16EE-5DA0-33B4-17DA71675A87}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5892D89B-BD0D-43B9-9064-0CBAC9A0FF60}
EndGlobalSection
EndGlobal

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<DeleteExistingFiles>true</DeleteExistingFiles>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>publish\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<ProjectGuid>48eff821-2f4d-60cc-aa44-be0f1d6e5f35</ProjectGuid>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>

View File

@@ -8,7 +8,7 @@
<ResourcePreloader />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["electronnet-samples-authmiddleware.styles.css"]" />
<link rel="stylesheet" href="@Assets["electronnet-samples-blazorsignalr.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />

View File

@@ -1,6 +1,6 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">ElectronNET.Samples.AuthMiddleware</a>
<a class="navbar-brand" href="">ElectronNET.Samples.BlazorSignalR</a>
</div>
</div>

View File

@@ -6,6 +6,6 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using ElectronNET.Samples.AuthMiddleware
@using ElectronNET.Samples.AuthMiddleware.Components
@using ElectronNET.Samples.AuthMiddleware.Components.Layout
@using ElectronNET.Samples.BlazorSignalR
@using ElectronNET.Samples.BlazorSignalR.Components
@using ElectronNET.Samples.BlazorSignalR.Components.Layout

View File

@@ -12,10 +12,10 @@
<Import Project="..\ElectronNET\build\ElectronNET.Core.props" Condition="$(ElectronNetDevMode)" />
<PropertyGroup Label="ElectronNetCommon">
<Title>Electron.NET Auth Middleware Sample</Title>
<Title>Electron.NET Blazor SignalR Sample</Title>
<Version>1.0.0</Version>
<Product>com.electronnet.auth-middleware-sample</Product>
<Description>Sample Blazor Server application using Electron.NET with Auth Middleware</Description>
<Product>com.electronnet.blazor-signalr-sample</Product>
<Description>Sample Blazor Server application using Electron.NET with SignalR mode</Description>
<Company>Electron.NET</Company>
<Copyright>Copyright © 2026, Electron.NET</Copyright>
<ElectronVersion>30.4.0</ElectronVersion>

View File

@@ -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<ElectronHub>("/electron-hub");
Console.WriteLine("[TEST] Application starting...");
app.Run();

View File

@@ -86,7 +86,7 @@ app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages:
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<ElectronNET.Samples.AuthMiddleware.Components.App>()
app.MapRazorComponents<ElectronNET.Samples.BlazorSignalR.Components.App>()
.AddInteractiveServerRenderMode();
app.Run();

View File

@@ -0,0 +1,153 @@
# Electron.NET Blazor SignalR Sample
This sample demonstrates how to use Electron.NET with **SignalR-based startup mode** in a Blazor Server application.
## Features
**SignalR Communication** - Uses SignalR instead of socket.io for .NET ↔ Electron communication
**Dynamic Port Assignment** - Kestrel binds to port 0, avoiding conflicts
**Blazor Server** - Full Blazor Server support with interactive components
**Electron Integration** - Native desktop window with auto-hide menu bar
## Prerequisites
- .NET 10.0 SDK or later
- Node.js 22.x or later
## How to Run
### Method 1: Visual Studio
1. Open the solution in Visual Studio
2. Set `ElectronNET.Samples.BlazorSignalR` as the startup project
3. Press **F5** to run
The environment variable `ELECTRON_USE_SIGNALR=true` is already configured in `launchSettings.json`.
### Method 2: Command Line
```bash
cd src/ElectronNET.Samples.BlazorSignalR
set ELECTRON_USE_SIGNALR=true # Windows
# or
export ELECTRON_USE_SIGNALR=true # Linux/Mac
dotnet run
```
## What Happens
1. ASP.NET Core starts with Kestrel on port 0 (random available port)
2. Electron.NET detects `ELECTRON_USE_SIGNALR=true` environment variable
3. Runtime controller captures the actual port (e.g., `http://localhost:54321`)
4. Electron process is launched with `--electronUrl=http://localhost:54321`
5. Electron connects to SignalR hub at `/electron-hub`
6. Once connected, the `ElectronAppReady` callback fires
7. A browser window is created showing the Blazor Server application
8. Both Blazor's SignalR hub (`/_blazor`) and Electron's hub (`/electron-hub`) run side-by-side
## Key Files
- **Program.cs** - Application startup with Electron and SignalR configuration
- **launchSettings.json** - Sets `ELECTRON_USE_SIGNALR=true` environment variable
- **ElectronNET.Samples.BlazorSignalR.csproj** - Project configuration with Electron.NET references
## Code Highlights
### Program.cs
```csharp
// Configure Electron.NET with SignalR mode
builder.WebHost.UseElectron(args, async () =>
{
var options = new BrowserWindowOptions
{
Show = false,
Width = 1200,
Height = 800,
IsRunningBlazor = true, // Crucial for Blazor support
};
var browserWindow = await Electron.WindowManager.CreateWindowAsync(options);
browserWindow.OnReadyToShow += () => browserWindow.Show();
});
// ... configure services ...
// Map the Electron SignalR hub (required for SignalR mode)
app.MapElectronHub();
```
### launchSettings.json
```json
{
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ELECTRON_USE_SIGNALR": "true"
},
"applicationUrl": "http://localhost:0"
}
```
## Differences from Socket.io Mode
| Aspect | Socket.io Mode | SignalR Mode |
|--------|---------------|--------------|
| Communication | socket.io | SignalR |
| Port Selection | Fixed port found by portscanner | Dynamic (port 0) |
| Startup Order | Electron → .NET or .NET → Electron | .NET → Electron |
| Hub Endpoint | N/A (socket.io on separate port) | `/electron-hub` |
| Environment Variable | None | `ELECTRON_USE_SIGNALR=true` |
## Console Output
When running successfully, you should see:
```
[SignalR Sample] Application configured and starting...
[RuntimeControllerAspNetDotnetFirstSignalR] StartCore
[RuntimeControllerAspNetDotnetFirstSignalR] URL: http://localhost:54321
[RuntimeControllerAspNetDotnetFirstSignalR] Launching: --dotnetpackedsignalr --electronUrl=http://localhost:54321
[RuntimeControllerAspNetDotnetFirstSignalR] Electron ready
[SignalRBridge] Connecting to http://localhost:54321/electron-hub
[ElectronHub] Client connected: abc123xyz
[SignalRBridge] Connected successfully
[RuntimeControllerAspNetDotnetFirstSignalR] SignalR connected!
[SignalR Sample] Electron app ready callback executed!
[SignalR Sample] Window ready and visible!
```
## Troubleshooting
### Window doesn't appear
- Check that `ELECTRON_USE_SIGNALR` environment variable is set
- Look for errors in the console output
- Verify `app.MapElectronHub()` is called in Program.cs
### Connection fails
- Ensure SignalR hub is properly mapped
- Check firewall settings
- Verify Node.js and npm packages are installed (`npm install` in ElectronNET.Host)
### Hot Reload issues
- SignalR supports automatic reconnection
- If issues persist, restart the application
## Next Steps
- Modify the Blazor components in `Components/Pages/`
- Add more Electron API calls in the `ElectronAppReady` callback
- Explore bidirectional communication between Blazor and Electron
## Learn More
- [SignalR Startup Mode Documentation](../../docs/SignalR-Startup-Mode.md)
- [Electron.NET Documentation](https://github.com/ElectronNET/Electron.NET/wiki)
- [ASP.NET Core SignalR](https://docs.microsoft.com/aspnet/core/signalr)
## License
MIT - Same as Electron.NET

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More