mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-07-09 18:27:36 +00:00
Compare commits
38 Commits
feature/se
...
0.5.1-pre.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5858a697a | ||
|
|
58b9ad0125 | ||
|
|
d9f3da87a8 | ||
|
|
a33d76ae2f | ||
|
|
15d6a23016 | ||
|
|
c76ce9702d | ||
|
|
279a823eb7 | ||
|
|
fdab81a3a6 | ||
|
|
558e582789 | ||
|
|
c4251955d9 | ||
|
|
af5e70c000 | ||
|
|
fee3806953 | ||
|
|
979b25ffe7 | ||
|
|
07be82cca6 | ||
|
|
fe939e0fda | ||
|
|
50c9076af6 | ||
|
|
d7e1a09880 | ||
|
|
30b7b1d251 | ||
|
|
e9b3ba0add | ||
|
|
89f721d1b3 | ||
|
|
833d288891 | ||
|
|
e529d092c3 | ||
|
|
11f71feeb8 | ||
|
|
1e8b02648a | ||
|
|
2a14840c6b | ||
|
|
005feddcc5 | ||
|
|
3f14f9ae7e | ||
|
|
ce271fd30a | ||
|
|
1d1b9537b5 | ||
|
|
4c48588ce2 | ||
|
|
c9945a87a9 | ||
|
|
8fc197b32a | ||
|
|
7930831b6d | ||
|
|
201046164c | ||
|
|
b8151a2fad | ||
|
|
c1bf6d9423 | ||
|
|
f4da9588c9 | ||
|
|
04c1415f6d |
14
.github/CONTRIBUTING.md
vendored
14
.github/CONTRIBUTING.md
vendored
@@ -134,4 +134,16 @@ 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 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.
|
||||
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.
|
||||
|
||||
4
.github/workflows/Build and Publish.yml
vendored
4
.github/workflows/Build and Publish.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: Build and Publish
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
19
.github/workflows/integration-tests.yml
vendored
19
.github/workflows/integration-tests.yml
vendored
@@ -4,13 +4,14 @@ on:
|
||||
workflow_call:
|
||||
|
||||
concurrency:
|
||||
group: integration-tests-${{ github.ref }}
|
||||
group: integration-tests-${{ github.workflow }}-${{ 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:
|
||||
@@ -42,13 +43,6 @@ 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:
|
||||
@@ -79,27 +73,31 @@ 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" --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" --blame-hang --blame-hang-timeout 5min --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" --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" --blame-hang --blame-hang-timeout 5min --results-directory test-results
|
||||
|
||||
- name: Upload raw test results
|
||||
if: always()
|
||||
@@ -114,6 +112,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
if: always()
|
||||
needs: [tests]
|
||||
timeout-minutes: 10
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
2
.github/workflows/retry-test-jobs.yml
vendored
2
.github/workflows/retry-test-jobs.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
run: |
|
||||
echo "Inspecting jobs of workflow run $RUN_ID in $REPO"
|
||||
|
||||
jobs_json="$(gh api -R $REPO repos/$REPO/actions/runs/$RUN_ID/jobs)"
|
||||
jobs_json="$(gh api repos/$REPO/actions/runs/$RUN_ID/jobs)"
|
||||
|
||||
echo "Jobs and conclusions:"
|
||||
echo "$jobs_json" | jq '.jobs[] | {name: .name, conclusion: .conclusion}'
|
||||
|
||||
17
Changelog.md
17
Changelog.md
@@ -1,3 +1,20 @@
|
||||
# 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
|
||||
|
||||
@@ -1,583 +0,0 @@
|
||||
# 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.
|
||||
@@ -54,7 +54,7 @@ Add the Electron.NET configuration to your `.csproj` file:
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ElectronNET.Core" Version="0.4.1" />
|
||||
<PackageReference Include="ElectronNET.Core" Version="0.5.0" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
|
||||
32
docs/Using/Secure-Communication.md
Normal file
32
docs/Using/Secure-Communication.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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.
|
||||
@@ -24,6 +24,7 @@
|
||||
- [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
|
||||
|
||||
@@ -215,7 +215,8 @@ namespace ElectronNET.API.Entities
|
||||
/// <summary>
|
||||
/// Whether window should have a shadow. Default is true.
|
||||
/// </summary>
|
||||
public bool HasShadow { get; set; }
|
||||
[DefaultValue(true)]
|
||||
public bool HasShadow { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Forces using dark theme for the window, only works on some GTK+3 desktop environments. Default is false.
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
namespace ElectronNET.API.Entities
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using ElectronNET.API.Converter;
|
||||
|
||||
namespace ElectronNET.API.Entities
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
@@ -24,7 +28,8 @@
|
||||
/// <summary>
|
||||
/// Gets or sets the release notes.
|
||||
/// </summary>
|
||||
public ReleaseNoteInfo[] ReleaseNotes { get; set; } = new ReleaseNoteInfo[0];
|
||||
[JsonConverter(typeof(ReleaseNotesConverter))]
|
||||
public ReleaseNoteInfo[] ReleaseNotes { get; set; } = Array.Empty<ReleaseNoteInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the release date.
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
using ElectronNET.API.Bridge;
|
||||
|
||||
internal static class BridgeConnector
|
||||
{
|
||||
public static ISocketConnection Socket
|
||||
|
||||
@@ -1,57 +1,56 @@
|
||||
namespace ElectronNET.API.Bridge
|
||||
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
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
/// <summary>
|
||||
/// Raised when the bridge connection is established.
|
||||
/// </summary>
|
||||
event EventHandler BridgeConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Common interface for communication facades.
|
||||
/// Provides methods for bidirectional communication between .NET and Electron.
|
||||
/// Raised when the bridge connection is lost.
|
||||
/// </summary>
|
||||
internal interface ISocketConnection : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when the bridge connection is established.
|
||||
/// </summary>
|
||||
event EventHandler BridgeConnected;
|
||||
event EventHandler BridgeDisconnected;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the bridge connection is lost.
|
||||
/// </summary>
|
||||
event EventHandler BridgeDisconnected;
|
||||
/// <summary>
|
||||
/// Establishes the connection to Electron.
|
||||
/// </summary>
|
||||
void Connect();
|
||||
|
||||
/// <summary>
|
||||
/// Establishes the connection to Electron.
|
||||
/// </summary>
|
||||
void Connect();
|
||||
/// <summary>
|
||||
/// Registers a persistent event handler.
|
||||
/// </summary>
|
||||
void On(string eventName, Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a persistent event handler.
|
||||
/// </summary>
|
||||
void On(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 persistent event handler with a typed parameter.
|
||||
/// </summary>
|
||||
void On<T>(string eventName, Action<T> action);
|
||||
/// <summary>
|
||||
/// Registers a one-time event handler.
|
||||
/// </summary>
|
||||
void Once(string eventName, Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a one-time event handler.
|
||||
/// </summary>
|
||||
void Once(string eventName, Action 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 with a typed parameter.
|
||||
/// </summary>
|
||||
void Once<T>(string eventName, Action<T> action);
|
||||
/// <summary>
|
||||
/// Removes an event handler.
|
||||
/// </summary>
|
||||
void Off(string eventName);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an event handler.
|
||||
/// </summary>
|
||||
void Off(string eventName);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to Electron.
|
||||
/// </summary>
|
||||
Task Emit(string eventName, params object[] args);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Sends a message to Electron.
|
||||
/// </summary>
|
||||
Task Emit(string eventName, params object[] args);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
|
||||
91
src/ElectronNET.API/Converter/ReleaseNotesConverter.cs
Normal file
91
src/ElectronNET.API/Converter/ReleaseNotesConverter.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
namespace ElectronNET
|
||||
{
|
||||
using ElectronNET.API;
|
||||
using ElectronNET.API.Bridge;
|
||||
using ElectronNET.Runtime;
|
||||
using ElectronNET.Runtime.Controllers;
|
||||
using ElectronNET.Runtime.Data;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
namespace ElectronNET.Runtime.Controllers
|
||||
{
|
||||
using ElectronNET.API;
|
||||
using ElectronNET.API.Bridge;
|
||||
using ElectronNET.Common;
|
||||
using ElectronNET.Runtime.Data;
|
||||
using ElectronNET.Runtime.Helpers;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{
|
||||
}
|
||||
|
||||
internal override SocketIOConnection Socket
|
||||
internal override ISocketConnection Socket
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@@ -63,15 +63,13 @@
|
||||
|
||||
private static bool? CheckUnpackaged2()
|
||||
{
|
||||
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var dir = new DirectoryInfo(baseDir);
|
||||
|
||||
var dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
|
||||
if (dir.Name == "bin" && dir.Parent?.Name == "resources")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Directory.Exists(Path.Combine(baseDir, ".electron")))
|
||||
if (dir.GetDirectories().Any(e => e.Name == ".electron"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -86,6 +84,7 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
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;
|
||||
@@ -18,6 +19,7 @@
|
||||
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;
|
||||
@@ -104,7 +106,6 @@
|
||||
}
|
||||
|
||||
var osPart = buildInfoRid.Split('-').First();
|
||||
|
||||
var mismatch = false;
|
||||
|
||||
switch (osPart)
|
||||
@@ -158,9 +159,17 @@
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StartInternal(string startCmd, string args, string directoriy)
|
||||
private async Task StartInternal(string startCmd, string args, string directory)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -179,15 +188,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
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 += this.Process_Exited;
|
||||
this.process.ProcessExited += Monitor_SocketIO_Failure;
|
||||
this.process.LineReceived += Read_SocketIO_Parameters;
|
||||
this.process.Run(startCmd, args, directoriy);
|
||||
this.process.Run(startCmd, args, directory);
|
||||
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
|
||||
@@ -199,11 +221,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.");
|
||||
}
|
||||
|
||||
this.TransitionState(LifetimeState.Ready);
|
||||
else
|
||||
{
|
||||
this.TransitionState(LifetimeState.Ready);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
this.CollectProcessData();
|
||||
this.SetElectronExecutable();
|
||||
|
||||
|
||||
ElectronNetRuntime.StartupMethod = this.DetectAppTypeAndStartup();
|
||||
Console.WriteLine((string)("Evaluated StartupMethod: " + ElectronNetRuntime.StartupMethod));
|
||||
|
||||
@@ -56,11 +57,21 @@
|
||||
|
||||
if (isLaunchedByDotNet)
|
||||
{
|
||||
return isUnPackaged ? StartupMethod.UnpackedDotnetFirst: StartupMethod.PackagedDotnetFirst;
|
||||
if (isUnPackaged)
|
||||
{
|
||||
return StartupMethod.UnpackedDotnetFirst;
|
||||
}
|
||||
|
||||
return StartupMethod.PackagedDotnetFirst;
|
||||
}
|
||||
else
|
||||
{
|
||||
return isUnPackaged ? StartupMethod.UnpackedElectronFirst: StartupMethod.PackagedElectronFirst;
|
||||
if (isUnPackaged)
|
||||
{
|
||||
return StartupMethod.UnpackedElectronFirst;
|
||||
}
|
||||
|
||||
return StartupMethod.PackagedElectronFirst;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,10 +86,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);
|
||||
}
|
||||
}
|
||||
@@ -88,10 +99,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);
|
||||
}
|
||||
}
|
||||
@@ -113,8 +124,7 @@
|
||||
|
||||
private void SetElectronExecutable()
|
||||
{
|
||||
var executable = ElectronNetRuntime.BuildInfo.ElectronExecutable;
|
||||
|
||||
string executable = ElectronNetRuntime.BuildInfo.ElectronExecutable;
|
||||
if (string.IsNullOrEmpty(executable))
|
||||
{
|
||||
throw new Exception("AssemblyMetadataAttribute 'ElectronExecutable' could not be found!");
|
||||
|
||||
@@ -75,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"))
|
||||
{
|
||||
|
||||
@@ -97,4 +97,4 @@ namespace ElectronNET.AspNet.Middleware
|
||||
await context.Response.WriteAsync("Unauthorized: Authentication required");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
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;
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -24,8 +24,8 @@
|
||||
{
|
||||
var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged();
|
||||
var electronBinaryName = ElectronNetRuntime.ElectronExecutable;
|
||||
var port = ElectronNetRuntime.ElectronSocketPort ?? 0;
|
||||
var args = Environment.CommandLine;
|
||||
var port = ElectronNetRuntime.ElectronSocketPort ?? 0;
|
||||
|
||||
this.electronProcess = new ElectronProcessActive(isUnPacked, electronBinaryName, args, port);
|
||||
this.electronProcess.Ready += this.ElectronProcess_Ready;
|
||||
|
||||
@@ -50,4 +50,4 @@ namespace ElectronNET.AspNet.Services
|
||||
return result == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,4 @@ namespace ElectronNET.AspNet.Services
|
||||
/// <returns>True if token is valid, false otherwise</returns>
|
||||
bool ValidateToken(string token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@
|
||||
<ProjectReference Include="..\ElectronNET.API\ElectronNET.API.csproj" Condition="$(ElectronNetDevMode)" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ElectronNET.Core" Version="0.4.1" Condition="'$(ElectronNetDevMode)' != 'true'" />
|
||||
<PackageReference Include="ElectronNET.Core" Version="0.5.0" Condition="'$(ElectronNetDevMode)' != 'true'" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\ElectronNET\build\ElectronNET.Core.targets" Condition="$(ElectronNetDevMode)" />
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
"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",
|
||||
@@ -101,20 +110,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||
"version": "6.6.8",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz",
|
||||
"integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==",
|
||||
"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.3.1",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1"
|
||||
"ws": "~8.20.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
@@ -129,6 +139,23 @@
|
||||
"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",
|
||||
@@ -193,28 +220,62 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
"debug": "~4.4.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",
|
||||
@@ -245,9 +306,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
"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) => {
|
||||
@@ -15,11 +23,13 @@ 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);
|
||||
});
|
||||
});
|
||||
@@ -30,6 +40,7 @@ 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);
|
||||
});
|
||||
});
|
||||
@@ -89,6 +100,7 @@ module.exports = (socket) => {
|
||||
electron_updater_1.autoUpdater
|
||||
.checkForUpdatesAndNotify()
|
||||
.then((updateCheckResult) => {
|
||||
normalize(updateCheckResult?.updateInfo);
|
||||
electronSocket.emit("autoUpdater-checkForUpdatesAndNotify-completed" + guid, updateCheckResult);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -99,6 +111,7 @@ 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
@@ -3,6 +3,16 @@ 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;
|
||||
|
||||
@@ -20,12 +30,14 @@ 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);
|
||||
});
|
||||
});
|
||||
@@ -38,6 +50,7 @@ export = (socket: Socket) => {
|
||||
|
||||
socket.on("register-autoUpdater-update-downloaded", (id) => {
|
||||
autoUpdater.on("update-downloaded", (updateInfo) => {
|
||||
normalize(updateInfo);
|
||||
electronSocket.emit("autoUpdater-update-downloaded" + id, updateInfo);
|
||||
});
|
||||
});
|
||||
@@ -143,6 +156,7 @@ export = (socket: Socket) => {
|
||||
autoUpdater
|
||||
.checkForUpdatesAndNotify()
|
||||
.then((updateCheckResult) => {
|
||||
normalize(updateCheckResult?.updateInfo);
|
||||
electronSocket.emit(
|
||||
"autoUpdater-checkForUpdatesAndNotify-completed" + guid,
|
||||
updateCheckResult,
|
||||
@@ -160,6 +174,7 @@ export = (socket: Socket) => {
|
||||
autoUpdater
|
||||
.checkForUpdates()
|
||||
.then((updateCheckResult) => {
|
||||
normalize(updateCheckResult?.updateInfo);
|
||||
electronSocket.emit(
|
||||
"autoUpdater-checkForUpdates-completed" + guid,
|
||||
updateCheckResult,
|
||||
|
||||
@@ -57,6 +57,7 @@ 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);
|
||||
@@ -82,7 +83,7 @@ let manifestJsonFilePath = path.join(currentPath, manifestJsonFileName);
|
||||
|
||||
// if running unpackedelectron, lets change the path
|
||||
if (unpackedelectron || unpackeddotnet) {
|
||||
console.debug('Running in unpacked mode, dir: ' + currentPath);
|
||||
console.debug('Running in unpackaged mode, dir: ' + currentPath);
|
||||
|
||||
manifestJsonFilePath = path.join(currentPath, manifestJsonFileName);
|
||||
currentBinPath = path.join(currentPath, '../'); // go to project directory
|
||||
@@ -132,7 +133,8 @@ 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']);
|
||||
return process.argv.slice(2).filter(arg => {
|
||||
const sliceIndex = app.isPackaged ? 1 : 2;
|
||||
return process.argv.slice(sliceIndex).filter(arg => {
|
||||
if (!arg) return false;
|
||||
// Node/Electron internal or we already process them
|
||||
if (arg.startsWith('--manifest')) return false;
|
||||
@@ -149,7 +151,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) => {
|
||||
@@ -171,31 +173,28 @@ 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('Error closing Socket.IO server:', e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up API process (Socket.IO mode only)
|
||||
if (typeof apiProcess !== 'undefined' && apiProcess) {
|
||||
if (apiProcess) {
|
||||
try {
|
||||
apiProcess.kill();
|
||||
} catch (e) {
|
||||
console.error('Error killing API process:', e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up Socket.IO connection
|
||||
if (typeof io !== 'undefined' && io && typeof io.close === 'function') {
|
||||
if (io && io.close) {
|
||||
try {
|
||||
io.close();
|
||||
} catch (e) {
|
||||
console.error('Error closing Socket.IO connection:', e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
194
src/ElectronNET.Host/package-lock.json
generated
194
src/ElectronNET.Host/package-lock.json
generated
@@ -9,7 +9,6 @@
|
||||
"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",
|
||||
@@ -253,40 +252,6 @@
|
||||
"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",
|
||||
@@ -402,18 +367,6 @@
|
||||
"@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",
|
||||
@@ -1151,24 +1104,6 @@
|
||||
"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",
|
||||
@@ -1221,16 +1156,6 @@
|
||||
"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",
|
||||
@@ -1276,9 +1201,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -1676,9 +1601,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
@@ -1797,26 +1722,6 @@
|
||||
"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",
|
||||
@@ -1994,18 +1899,6 @@
|
||||
"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",
|
||||
@@ -2021,17 +1914,12 @@
|
||||
"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",
|
||||
@@ -2054,12 +1942,6 @@
|
||||
"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",
|
||||
@@ -2153,12 +2035,6 @@
|
||||
"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",
|
||||
@@ -2276,36 +2152,6 @@
|
||||
"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",
|
||||
@@ -2373,16 +2219,6 @@
|
||||
"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",
|
||||
@@ -2392,22 +2228,6 @@
|
||||
"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",
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
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("'", "'");
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,6 @@
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using ElectronNET.Samples.BlazorSignalR
|
||||
@using ElectronNET.Samples.BlazorSignalR.Components
|
||||
@using ElectronNET.Samples.BlazorSignalR.Components.Layout
|
||||
@using ElectronNET.Samples.AuthMiddleware
|
||||
@using ElectronNET.Samples.AuthMiddleware.Components
|
||||
@using ElectronNET.Samples.AuthMiddleware.Components.Layout
|
||||
@@ -12,10 +12,10 @@
|
||||
<Import Project="..\ElectronNET\build\ElectronNET.Core.props" Condition="$(ElectronNetDevMode)" />
|
||||
|
||||
<PropertyGroup Label="ElectronNetCommon">
|
||||
<Title>Electron.NET Blazor SignalR Sample</Title>
|
||||
<Title>Electron.NET Auth Middleware Sample</Title>
|
||||
<Version>1.0.0</Version>
|
||||
<Product>com.electronnet.blazor-signalr-sample</Product>
|
||||
<Description>Sample Blazor Server application using Electron.NET with SignalR mode</Description>
|
||||
<Product>com.electronnet.auth-middleware-sample</Product>
|
||||
<Description>Sample Blazor Server application using Electron.NET with Auth Middleware</Description>
|
||||
<Company>Electron.NET</Company>
|
||||
<Copyright>Copyright © 2026, Electron.NET</Copyright>
|
||||
<ElectronVersion>30.4.0</ElectronVersion>
|
||||
@@ -0,0 +1,24 @@
|
||||
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
|
||||
@@ -86,7 +86,7 @@ app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages:
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<ElectronNET.Samples.BlazorSignalR.Components.App>()
|
||||
app.MapRazorComponents<ElectronNET.Samples.AuthMiddleware.Components.App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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>
|
||||
|
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
Reference in New Issue
Block a user