mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-02-04 05:34:51 +00:00
Compare commits
67 Commits
0.4.1-pre.
...
feature/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
931aec8cd9 | ||
|
|
bbd1065a05 | ||
|
|
0fa0abd093 | ||
|
|
bfd51e64f7 | ||
|
|
bb4337a31c | ||
|
|
57753eb632 | ||
|
|
614673605a | ||
|
|
52744a1922 | ||
|
|
03da5cd7cb | ||
|
|
805d942b11 | ||
|
|
29b1f088ce | ||
|
|
4a62103749 | ||
|
|
38ee6fabb4 | ||
|
|
0ee2bbe31c | ||
|
|
a88e10bbf2 | ||
|
|
39c7e61ae5 | ||
|
|
96c454aedb | ||
|
|
5a77284610 | ||
|
|
0a23659196 | ||
|
|
5d224568d0 | ||
|
|
1c0b9378d2 | ||
|
|
c12a706289 | ||
|
|
8cc3fe4fd7 | ||
|
|
893de1510d | ||
|
|
6f49a663ea | ||
|
|
5b9e2b8b3b | ||
|
|
dee640c526 | ||
|
|
f598fbf5ce | ||
|
|
6847520ea8 | ||
|
|
75151282ff | ||
|
|
17ef6853ab | ||
|
|
12f011bc33 | ||
|
|
217fe83334 | ||
|
|
1fc881674d | ||
|
|
6e369aabef | ||
|
|
06a332827b | ||
|
|
23f79244ae | ||
|
|
6b9187cf6e | ||
|
|
108ef19a3b | ||
|
|
8c6020e35b | ||
|
|
547e9f1196 | ||
|
|
4b971af119 | ||
|
|
da8216b292 | ||
|
|
be609a513e | ||
|
|
c4a8de6c4e | ||
|
|
5b3d5e07ee | ||
|
|
9135aff855 | ||
|
|
e9efb26dff | ||
|
|
e29a3bc27a | ||
|
|
f55abb357c | ||
|
|
0b92336de2 | ||
|
|
4c17027039 | ||
|
|
5d04ab686a | ||
|
|
de0c02c503 | ||
|
|
054f5b1c4c | ||
|
|
04ec52208a | ||
|
|
268b9c90ce | ||
|
|
cb7d721b7d | ||
|
|
c1740b53fc | ||
|
|
40aed60c7d | ||
|
|
8ee81f6abd | ||
|
|
7f2ea4839e | ||
|
|
456135a562 | ||
|
|
300f52510c | ||
|
|
7e6760a428 | ||
|
|
cb20fbad25 | ||
|
|
bdfbcd5b77 |
@@ -31,7 +31,7 @@ dotnet add package ElectronNET.Core.AspNet # For ASP.NET projects
|
||||
### Step 2: Configure Project Settings
|
||||
|
||||
**Auto-generated Configuration:**
|
||||
ElectronNET.Core automatically creates `electron-builder.json` during the first build or NuGet restore. No manual configuration is needed for basic setups.
|
||||
ElectronNET.Core automatically creates `electron-builder.json` in the `Properties` folder of your project during the first build or NuGet restore. No manual configuration is needed for basic setups.
|
||||
|
||||
**Migrate Existing Configuration:**
|
||||
If you have an existing `electron.manifest.json` file:
|
||||
@@ -63,6 +63,9 @@ You can also manually edit `electron-builder.json`:
|
||||
}
|
||||
```
|
||||
|
||||
**Modify Launch Settings:**
|
||||
ElectronNET.Core no longer needs a separate CLI tool (electronize.exe) for launching. You should update your launch settings to use either the ASP.NET-first or Electron-first approach. See [Debugging](../Using/Debugging.md) for details.
|
||||
|
||||
## 🎯 Testing Migration
|
||||
|
||||
After completing the migration steps:
|
||||
|
||||
583
docs/SignalR-Authentication-Guide.md
Normal file
583
docs/SignalR-Authentication-Guide.md
Normal 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.
|
||||
450
docs/SignalR-Implementation-Summary.md
Normal file
450
docs/SignalR-Implementation-Summary.md
Normal file
@@ -0,0 +1,450 @@
|
||||
# SignalR Implementation Summary
|
||||
|
||||
This document summarizes the completed implementation of SignalR-based bidirectional communication in Electron.NET as an alternative to Socket.IO.
|
||||
|
||||
## Overview
|
||||
|
||||
The SignalR implementation provides a modern, .NET-native alternative to Socket.IO for communication between the ASP.NET Core host and the Electron process. This new startup mode was designed specifically for **Blazor Server applications** where ASP.NET Core and Electron need tighter integration and lifecycle control.
|
||||
|
||||
**Key Innovation**: .NET-first startup with dynamic port assignment - ASP.NET Core starts first, binds to port 0 (letting Kestrel choose an available port), then launches Electron with the actual URL.
|
||||
|
||||
## Primary Use Case
|
||||
|
||||
Blazor Server applications where:
|
||||
- ASP.NET Core owns the application lifecycle
|
||||
- Dynamic port binding is needed (no fixed port configuration)
|
||||
- Modern SignalR infrastructure is preferred over Socket.IO
|
||||
- Single process debugging is desired (.NET process controls Electron)
|
||||
|
||||
## Implementation Phases (All Complete)
|
||||
|
||||
### Phase 1: Core Infrastructure ✅
|
||||
- Added new `StartupMethod` enum values:
|
||||
- `UnpackagedDotnetFirstSignalR`
|
||||
- `PackagedDotnetFirstSignalR`
|
||||
- Created `ElectronHub` SignalR hub for bidirectional communication
|
||||
- Registered hub endpoint at `/electron-hub` (separate from Blazor's `/_blazor` hub)
|
||||
|
||||
### Phase 2: Runtime Controller ✅
|
||||
- Created `RuntimeControllerAspNetDotnetFirstSignalR`
|
||||
- Implemented logic to:
|
||||
- Bind Kestrel to port 0
|
||||
- Wait for Kestrel startup and capture actual port via `IServerAddressesFeature`
|
||||
- Launch Electron with `--electronurl` parameter
|
||||
- Wait for SignalR connection from Electron
|
||||
- Transition to Ready state when connected
|
||||
|
||||
### Phase 3: Electron/Node.js Side ✅
|
||||
- Added `@microsoft/signalr` npm package dependency
|
||||
- Created SignalR connection module (`signalr-bridge.js`)
|
||||
- Updated `main.js` to detect SignalR modes and connect to `/electron-hub`
|
||||
- Implemented Socket.IO-compatible interface for API compatibility
|
||||
|
||||
### Phase 4: API Bridge Adaptation ✅
|
||||
- Created `SignalRFacade` implementing `IFacade` interface
|
||||
- Ensured existing Electron API classes work with SignalR
|
||||
- Implemented type conversion helper for SignalR's JSON deserialization
|
||||
- Event routing from both directions (.NET ↔ Electron)
|
||||
|
||||
### Phase 5: Configuration & Extensions ✅
|
||||
- Updated `WebHostBuilderExtensions` for automatic SignalR configuration
|
||||
- Added startup mode detection via command-line flags
|
||||
- Configured dynamic port binding (port 0) for SignalR modes
|
||||
- Integrated with `UseElectron()` API for seamless usage
|
||||
|
||||
### Phase 6: Testing & Fixes ✅
|
||||
- Created sample Blazor Server application
|
||||
- Fixed multiple critical issues discovered during integration testing
|
||||
- Cleaned up debug logging
|
||||
- Added comprehensive code documentation
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. SignalRFacade (`src/ElectronNET.AspNet/Bridge/SignalRFacade.cs`)
|
||||
- Implements `IFacade` interface to match Socket.IO facade API
|
||||
- Handles bidirectional event routing using `IHubContext<ElectronHub>`
|
||||
- Includes `ConvertToType<T>` helper for handling SignalR's JSON deserialization quirks
|
||||
- Critical fix: Handles `JsonElement` and numeric type conversions (long → int)
|
||||
|
||||
### 2. ElectronHub (`src/ElectronNET.AspNet/Hubs/ElectronHub.cs`)
|
||||
- SignalR hub for .NET ↔ Electron communication
|
||||
- Key methods:
|
||||
- `RegisterElectronClient()` - Called by Electron on connection
|
||||
- `ElectronEvent(string, object[])` - Receives events from Electron
|
||||
- Connection/disconnection handlers notify runtime controller
|
||||
|
||||
### 3. RuntimeControllerAspNetDotnetFirstSignalR
|
||||
- Manages SignalR mode lifecycle
|
||||
- Critical flow:
|
||||
1. Wait for ASP.NET server to start
|
||||
2. Capture dynamic port from `IServerAddressesFeature`
|
||||
3. Update `ElectronNetRuntime.AspNetWebPort` with actual port
|
||||
4. Launch Electron with `--electronurl` parameter
|
||||
5. Wait for `electron-host-ready` signal before calling app ready callback
|
||||
|
||||
### 4. SignalRBridge (`src/ElectronNET.Host/api/signalr-bridge.js`)
|
||||
- JavaScript SignalR client that mimics Socket.IO interface
|
||||
- Provides `on()` and `emit()` methods for API compatibility
|
||||
- Critical fix: Event args passed as arrays, spread when calling handlers
|
||||
- Uses `@microsoft/signalr` npm package
|
||||
|
||||
### 5. Main.js Startup (`src/ElectronNET.Host/main.js`)
|
||||
- Detects SignalR mode via `--unpackeddotnetsignalr` or `--dotnetpackedsignalr` flags
|
||||
- Creates invisible keep-alive window (destroyed when first real window is created)
|
||||
- Loads API modules then signals `electron-host-ready` to .NET
|
||||
|
||||
## Usage
|
||||
|
||||
Enable SignalR mode by passing the appropriate command-line flag:
|
||||
|
||||
```bash
|
||||
# Unpacked mode (development)
|
||||
dotnet run --unpackeddotnetsignalr
|
||||
|
||||
# Packed mode (production)
|
||||
dotnet run --dotnetpackedsignalr
|
||||
```
|
||||
|
||||
Or set environment variable (deprecated, flags preferred):
|
||||
```bash
|
||||
ELECTRON_USE_SIGNALR=true
|
||||
```
|
||||
|
||||
In your ASP.NET Core Program.cs:
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add Electron.NET services
|
||||
builder.Services.AddElectron();
|
||||
|
||||
// Configure Electron with SignalR mode
|
||||
builder.WebHost.UseElectron(args, async () =>
|
||||
{
|
||||
var window = await Electron.WindowManager.CreateWindowAsync();
|
||||
window.OnReadyToShow += () => window.Show();
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
// Map SignalR hub for Electron communication
|
||||
app.MapHub<ElectronHub>("/electron-hub");
|
||||
|
||||
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
|
||||
app.Run();
|
||||
```
|
||||
|
||||
**Note**: `UseElectron()` automatically detects SignalR mode and configures everything. The rest happens automatically:
|
||||
1. Port 0 binding (dynamic port assignment)
|
||||
2. Electron launch with actual URL
|
||||
3. SignalR connection establishment (WebSockets enabled automatically by MapHub)
|
||||
4. App ready callback execution
|
||||
2. Electron launch with actual URL
|
||||
3. SignalR connection establishment (WebSockets enabled automatically by `MapHub`)
|
||||
4. App ready callback execution
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
### Why .NET-First Startup?
|
||||
SignalR mode uses .NET-first startup (vs. Electron-first in Socket.IO mode) because:
|
||||
1. **No port scanning needed** - .NET can pass the actual URL to Electron
|
||||
2. **SignalR hub must be registered** before Electron connects
|
||||
3. **Simpler lifecycle** - ASP.NET controls when Electron launches
|
||||
4. **Better for Blazor Server** - Blazor is already running when Electron starts
|
||||
5. **Single process debugging** - Developer debugs .NET process which owns Electron
|
||||
|
||||
### Why IFacade Interface?
|
||||
Introducing `IFacade` allows `BridgeConnector.Socket` to return either `SocketIOFacade` or `SignalRFacade` based on startup mode, ensuring existing API code works with both transport mechanisms without modification.
|
||||
|
||||
### Why Keep-Alive Window?
|
||||
Electron quits immediately on macOS if no windows exist. The keep-alive window ensures Electron stays running during the connection and API initialization phase. It's automatically destroyed when the first real window is created.
|
||||
|
||||
### Why 'electron-host-ready' Signal?
|
||||
Without this signal, .NET would call the app ready callback before Electron finished loading API modules, causing API calls to fail. The signal ensures proper initialization order:
|
||||
1. Electron connects to SignalR
|
||||
2. Electron loads all API modules (browserWindows, dialog, menu, etc.)
|
||||
3. Electron signals `electron-host-ready`
|
||||
4. .NET calls app ready callback
|
||||
5. App code can safely use Electron APIs
|
||||
|
||||
## Blazor Server Considerations
|
||||
|
||||
### SignalR Hub Coexistence
|
||||
Blazor Server already uses SignalR for component communication (`/_blazor` hub). Our implementation:
|
||||
- Uses separate endpoint (`/electron-hub`) to avoid conflicts
|
||||
- Both hubs coexist on the same Kestrel server without interference
|
||||
- No impact on Blazor's reconnection logic
|
||||
- Compatible with hot reload scenarios
|
||||
|
||||
### Lifecycle Integration
|
||||
- Electron window creation happens **after** Blazor app is ready
|
||||
- `UseElectron()` callback fires when SignalR hub is connected
|
||||
- Blazor components can inject Electron services to control windows
|
||||
- Proper disposal when Electron process exits
|
||||
|
||||
### Development Experience
|
||||
- Hot reload works for both Blazor and Electron integration
|
||||
- F5 debugging works seamlessly
|
||||
- No need to manually coordinate ports
|
||||
- Single process to debug (.NET process owns the lifecycle)
|
||||
|
||||
## Critical Fixes Applied
|
||||
|
||||
### 1. Race Condition: API Module Loading
|
||||
**Problem**: .NET called app ready callback before Electron finished loading API modules.
|
||||
|
||||
**Solution**: Electron signals `electron-host-ready` after loading all API modules. .NET waits for this signal before calling the app ready callback.
|
||||
|
||||
### 2. Event Argument Mismatch
|
||||
**Problem**: SignalR sent event data as nested arrays `[[data]]` instead of `[data]`.
|
||||
|
||||
**Solution**:
|
||||
- C#: Use explicit `object[] args` parameter (not `params`)
|
||||
- JS: Always pass args as array: `invoke('ElectronEvent', eventName, args)`
|
||||
- JS: Spread args when calling handlers: `handler(...argsArray)`
|
||||
|
||||
### 3. Type Conversion Failures
|
||||
**Problem**: SignalR deserializes JSON numbers as `JsonElement` or `long`, causing `Once<int>` handlers to fail silently.
|
||||
|
||||
**Solution**: `SignalRFacade.ConvertToType<T>` handles JsonElement deserialization and numeric conversions.
|
||||
|
||||
### 4. Window Shutdown Not Triggering Exit
|
||||
**Problem**: Keep-alive window prevented `window-all-closed` event from firing.
|
||||
|
||||
**Solution**: Destroy keep-alive window when first real window is created using `app.once('browser-window-created')`.
|
||||
|
||||
### 5. Dynamic Port Not Propagated
|
||||
**Problem**: When using port 0, Kestrel assigns a dynamic port, but `ElectronNetRuntime.AspNetWebPort` was not updated.
|
||||
|
||||
**Solution**: Update `AspNetWebPort` after capturing port from `IServerAddressesFeature` in `CapturePortAndLaunchElectron()`.
|
||||
|
||||
### 7. Blazor Static Files Not Loading
|
||||
**Problem**: Blazor CSS and framework files returned 404 errors.
|
||||
|
||||
**Solution**:
|
||||
- Added `app.UseStaticFiles()` to serve wwwroot content
|
||||
- Fixed middleware order: `UseAntiforgery()` must be between `UseRouting()` and `UseEndpoints()`
|
||||
- Updated scoped CSS asset reference to use lowercase name matching .NET 9+ convention
|
||||
|
||||
## Authentication & Security
|
||||
|
||||
### Token-Based Authentication (Multi-User Protection)
|
||||
|
||||
SignalR mode includes built-in authentication to prevent unauthorized connections in multi-user scenarios (e.g., Windows Server with Terminal Services/RDP).
|
||||
|
||||
**Threat Model**: On shared servers, multiple users can run the same application simultaneously. Without authentication, User A's Electron process could potentially connect to User B's ASP.NET backend.
|
||||
|
||||
**Solution**: Token-based authentication with secure cookies.
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. **.NET generates token**: When launching Electron, `RuntimeControllerAspNetDotnetFirstSignalR` generates a cryptographically secure GUID (128-bit entropy)
|
||||
2. **Token passed via command-line**: Electron receives `--authtoken=<guid>` parameter
|
||||
3. **Token appended to URLs**:
|
||||
- Initial page load: `http://localhost:PORT/?token=<guid>`
|
||||
- SignalR connection: `http://localhost:PORT/electron-hub?token=<guid>`
|
||||
4. **Middleware validates token**: `ElectronAuthenticationMiddleware` checks every HTTP request
|
||||
5. **Cookie set on first request**: After successful token validation, secure HttpOnly cookie is set
|
||||
6. **Subsequent requests use cookie**: No token in URLs after initial authentication
|
||||
|
||||
### Security Properties
|
||||
|
||||
- **Cookie Settings**:
|
||||
- `HttpOnly`: true (prevents JavaScript access, XSS protection)
|
||||
- `SameSite`: Strict (prevents CSRF)
|
||||
- `Path`: / (applies to all routes)
|
||||
- `Secure`: false (localhost is HTTP, not HTTPS)
|
||||
- `IsEssential`: true (required for app to function)
|
||||
- **Lifetime**: Session scope (expires when Electron closes)
|
||||
|
||||
- **Token Validation**:
|
||||
- Constant-time string comparison (prevents timing attacks)
|
||||
- Token stored in singleton service (one per .NET instance)
|
||||
- Never logged in full (only first 8 characters for debugging)
|
||||
|
||||
- **Protection Scope**:
|
||||
- All HTTP endpoints (Blazor pages, static files, API calls)
|
||||
- SignalR hub connection (negotiate and all hub traffic)
|
||||
- Both initial request and cookie-based requests validated
|
||||
|
||||
### What This Protects Against
|
||||
|
||||
✅ **Protected**:
|
||||
- Cross-user connections (User A → User B's backend)
|
||||
- Port scanning attacks from other users
|
||||
- Accidental connections from misconfigured processes
|
||||
|
||||
❌ **NOT Protected Against** (By Design):
|
||||
- Malicious same-user processes with debugger access
|
||||
- Process memory inspection tools (same privilege level)
|
||||
- Command-line parameter visibility (same user can see all processes)
|
||||
|
||||
**Rationale**: Same-user attacks already have full access to process memory, files, and cookies. Token-based authentication focuses on cross-user isolation, which is the primary threat in multi-user environments.
|
||||
|
||||
### Implementation Components
|
||||
|
||||
1. **IElectronAuthenticationService** (`src/ElectronNET.AspNet/Services/`)
|
||||
- Singleton service storing expected token
|
||||
- Thread-safe with lock-based validation
|
||||
- Constant-time comparison to prevent timing attacks
|
||||
|
||||
2. **ElectronAuthenticationMiddleware** (`src/ElectronNET.AspNet/Middleware/`)
|
||||
- Validates every HTTP request before routing
|
||||
- Checks cookie first, then token query parameter
|
||||
- Sets cookie on first valid token
|
||||
- Returns 401 for invalid/missing authentication
|
||||
- Structured logging for security monitoring
|
||||
|
||||
3. **Token Generation** (`RuntimeControllerAspNetDotnetFirstSignalR.cs`)
|
||||
- `Guid.NewGuid().ToString("N")` = 32 hex characters
|
||||
- Called in `LaunchElectron()` method
|
||||
- Registered with authentication service immediately
|
||||
|
||||
4. **Electron Integration** (`main.js`, `signalr-bridge.js`)
|
||||
- Extracts token from `--authtoken` parameter
|
||||
- Stores in `global.authToken` for module access
|
||||
- Appends to browser window URL and SignalR connection URL
|
||||
|
||||
### Usage in Custom Applications
|
||||
|
||||
Authentication is **enabled by default** in SignalR mode. No additional configuration required beyond service registration:
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register authentication service (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.MapRazorComponents<App>().AddInteractiveServerRenderMode();
|
||||
app.Run();
|
||||
```
|
||||
|
||||
The rest is automatic:
|
||||
- Token generation happens when Electron launches
|
||||
- Token validation happens on every request
|
||||
- Cookie management is handled by the middleware
|
||||
|
||||
### Logging & Monitoring
|
||||
|
||||
Authentication events are logged with structured logging:
|
||||
|
||||
**Successful authentication**:
|
||||
```
|
||||
[Information] Authentication successful: Setting cookie for path /
|
||||
```
|
||||
|
||||
**Failed authentication**:
|
||||
```
|
||||
[Warning] Authentication failed: Invalid token (prefix: a3f8b2c1...) for path / from 127.0.0.1
|
||||
[Warning] Authentication failed: No cookie or token provided for path /api/data from 127.0.0.1
|
||||
[Warning] Authentication failed: Invalid cookie for path /_blazor from 127.0.0.1
|
||||
```
|
||||
|
||||
**SignalR connection failures**:
|
||||
```
|
||||
[SignalRBridge] Authentication failed: The authentication token is invalid or missing.
|
||||
[SignalRBridge] Please ensure the --authtoken parameter is correctly passed to Electron.
|
||||
```
|
||||
|
||||
Log failed authentication attempts for security monitoring and troubleshooting.
|
||||
|
||||
### Testing Multi-User Scenarios
|
||||
|
||||
To test authentication in multi-user environments:
|
||||
|
||||
1. **Run as different Windows users**:
|
||||
```powershell
|
||||
# User A session
|
||||
dotnet run
|
||||
|
||||
# User B session (different RDP/Terminal Services session)
|
||||
dotnet run
|
||||
```
|
||||
|
||||
2. **Verify isolation**: User A's Electron cannot access User B's backend
|
||||
3. **Check logs**: Failed auth attempts should be logged
|
||||
4. **Monitor tokens**: Each instance generates unique token
|
||||
|
||||
For development testing on single-user machines, simulate by running multiple instances and attempting to connect with wrong/missing tokens.
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
This is a **new optional startup mode** - all existing modes continue to work unchanged:
|
||||
- `PackagedElectronFirst` - unchanged
|
||||
- `PackagedDotnetFirst` - unchanged
|
||||
- `UnpackedElectronFirst` - unchanged
|
||||
- `UnpackedDotnetFirst` - unchanged
|
||||
|
||||
Existing applications do not need to change. SignalR mode is opt-in via command-line flags.
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
**New Files**:
|
||||
- `src/ElectronNET.AspNet/Bridge/SignalRFacade.cs` (225 lines)
|
||||
- `src/ElectronNET.AspNet/Hubs/ElectronHub.cs` (108 lines)
|
||||
- `src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs` (163 lines)
|
||||
- `src/ElectronNET.AspNet/Services/IElectronAuthenticationService.cs` (20 lines)
|
||||
- `src/ElectronNET.AspNet/Services/ElectronAuthenticationService.cs` (65 lines)
|
||||
- `src/ElectronNET.AspNet/Middleware/ElectronAuthenticationMiddleware.cs` (105 lines)
|
||||
- `src/ElectronNET.Host/api/signalr-bridge.js` (125 lines)
|
||||
|
||||
**Modified Files**:
|
||||
- `src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs` - Added SignalR service registration
|
||||
- `src/ElectronNET.Host/main.js` - Added SignalR startup flow and token extraction
|
||||
- `src/ElectronNET.Host/api/browserWindows.js` - Token appended to window URLs
|
||||
- `src/ElectronNET.Host/package.json` - Added `@microsoft/signalr` dependency
|
||||
- `src/ElectronNET.Samples.BlazorSignalR/Program.cs` - Sample with authentication
|
||||
|
||||
**Total Changes**: ~1,220 lines added
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. Test with dynamic port (port 0) to ensure URL propagation works
|
||||
2. Verify window-all-closed triggers app exit
|
||||
3. Test rapid window creation/destruction
|
||||
4. Verify reconnection behavior if SignalR connection drops
|
||||
5. Test with both packed and unpacked modes
|
||||
6. Verify API calls work correctly (especially those returning data)
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Request-response pattern not yet implemented** - `InvokeElectronApi` is a placeholder. Current API calls use event-based pattern.
|
||||
2. **TouchBar API not yet supported** on macOS SignalR mode
|
||||
3. **SignalR automatic reconnection** may cause issues with pending API calls (needs circuit breaker pattern)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Request-response pattern** - Implement proper async/await pattern for API calls that return values
|
||||
2. **Metrics/diagnostics** - Add SignalR connection health monitoring
|
||||
3. **Circuit breaker** - Handle reconnection scenarios gracefully
|
||||
4. **Integration tests** - Comprehensive test suite for SignalR mode
|
||||
5. **Performance benchmarks** - Compare SignalR vs Socket.IO performance
|
||||
|
||||
## Success Metrics
|
||||
|
||||
The implementation is considered complete and functional:
|
||||
- ✅ .NET starts first with dynamic port (port 0)
|
||||
- ✅ Electron launches with actual URL
|
||||
- ✅ SignalR connection establishes successfully
|
||||
- ✅ API modules load before app ready callback
|
||||
- ✅ Window creation works from .NET
|
||||
- ✅ Window shutdown triggers app exit
|
||||
- ✅ Blazor Server pages load with correct styling
|
||||
- ✅ Both SignalR hubs coexist (Electron + Blazor)
|
||||
- ✅ Clean codebase with minimal debug logging
|
||||
- ✅ Comprehensive inline documentation
|
||||
- ✅ Token-based authentication for multi-user scenarios
|
||||
- ✅ Secure cookie-based session management
|
||||
- ✅ Structured logging for security monitoring
|
||||
- ✅ Protection against cross-user connection attempts
|
||||
236
docs/SignalR-Startup-Mode.md
Normal file
236
docs/SignalR-Startup-Mode.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# SignalR-Based Startup Mode for Electron.NET
|
||||
|
||||
## Overview
|
||||
|
||||
This feature adds a new startup mode for Electron.NET where:
|
||||
- **.NET/ASP.NET Core starts first** and binds to port 0 (dynamic port)
|
||||
- **Kestrel picks an available port** automatically
|
||||
- **Electron process is launched** with the actual URL
|
||||
- **SignalR is used for communication** instead of socket.io
|
||||
- **Blazor Server apps** can coexist with Electron control
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Phases 1-5 Complete** - Infrastructure ready, basic functionality implemented
|
||||
⏸️ **Phase 6 Pending** - Full API integration, testing, and documentation
|
||||
|
||||
## How It Works
|
||||
|
||||
### Startup Sequence
|
||||
|
||||
1. ASP.NET Core application starts
|
||||
2. Kestrel binds to `http://localhost:0` (random available port)
|
||||
3. `RuntimeControllerAspNetDotnetFirstSignalR` captures the actual port via `IServerAddressesFeature`
|
||||
4. Electron process is launched with `--electronUrl=http://localhost:XXXXX`
|
||||
5. Electron's main.js detects SignalR mode (via `--dotnetpackedsignalr` or `--unpackeddotnetsignalr` flag)
|
||||
6. Electron connects to SignalR hub at `/electron-hub`
|
||||
7. Hub notifies runtime controller of successful connection
|
||||
8. Application transitions to "Ready" state
|
||||
9. `ElectronAppReady` callback is invoked
|
||||
|
||||
### Communication Flow
|
||||
|
||||
```
|
||||
.NET/Kestrel (Port 0) ←→ SignalR Hub (/electron-hub) ←→ Electron Process
|
||||
↓ ↓ ↓
|
||||
Blazor Server ElectronHub class SignalR Client
|
||||
(/_blazor hub) (API commands) (main.js + signalr-bridge.js)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Enable SignalR Mode
|
||||
|
||||
Set the environment variable:
|
||||
```bash
|
||||
ELECTRON_USE_SIGNALR=true
|
||||
```
|
||||
|
||||
Or in launchSettings.json:
|
||||
```json
|
||||
{
|
||||
"environmentVariables": {
|
||||
"ELECTRON_USE_SIGNALR": "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Configure ASP.NET Core
|
||||
|
||||
In your `Program.cs`:
|
||||
|
||||
```csharp
|
||||
using ElectronNET.API;
|
||||
using ElectronNET.API.Entities;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddElectron();
|
||||
|
||||
builder.UseElectron(args, async () =>
|
||||
{
|
||||
var window = await Electron.WindowManager.CreateWindowAsync(
|
||||
new BrowserWindowOptions { Show = false });
|
||||
|
||||
window.OnReadyToShow += () => window.Show();
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure middleware
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
|
||||
// Map the Electron SignalR hub
|
||||
app.MapElectronHub(); // ← Required for SignalR mode
|
||||
app.MapRazorPages();
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
### 3. Run Your Application
|
||||
|
||||
Just press F5 in Visual Studio or run:
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
The application will:
|
||||
- Automatically detect SignalR mode via environment variable
|
||||
- Bind Kestrel to port 0
|
||||
- Launch Electron with the correct URL
|
||||
- Establish SignalR connection
|
||||
|
||||
## Components
|
||||
|
||||
### .NET Side
|
||||
|
||||
- **`ElectronHub`** - SignalR hub at `/electron-hub`
|
||||
- **`SignalRFacade`** - Mimics `SocketIoFacade` interface for compatibility
|
||||
- **`RuntimeControllerAspNetDotnetFirstSignalR`** - Lifecycle management
|
||||
- **`StartupMethod.PackagedDotnetFirstSignalR`** - For packaged apps
|
||||
- **`StartupMethod.UnpackedDotnetFirstSignalR`** - For debugging
|
||||
|
||||
### Electron Side
|
||||
|
||||
- **`signalr-bridge.js`** - SignalR client wrapper
|
||||
- **`main.js`** - Detects SignalR mode and connects to hub
|
||||
- **`@microsoft/signalr`** npm package
|
||||
|
||||
## Key Features
|
||||
|
||||
✅ **Dynamic Port Assignment** - No hardcoded ports, no conflicts
|
||||
✅ **Blazor Server Compatible** - Separate hub endpoints (`/electron-hub` vs `/_blazor`)
|
||||
✅ **Bidirectional Communication** - Both .NET→Electron and Electron→.NET
|
||||
✅ **Hot Reload Support** - SignalR automatic reconnection
|
||||
✅ **Multiple Instances** - Each instance gets its own port
|
||||
|
||||
## Current Limitations (Phase 6 Work Needed)
|
||||
|
||||
⚠️ **Electron API Integration** - Existing Electron APIs (WindowManager, Dialog, etc.) still use SocketIoFacade. Full integration requires:
|
||||
- Refactoring APIs to work with both facades, or
|
||||
- Creating an adapter pattern
|
||||
|
||||
⚠️ **Request-Response Pattern** - Current hub methods are one-way. Need to implement proper async request-response for API calls.
|
||||
|
||||
⚠️ **Event Routing** - Electron events need to be routed through SignalR back to .NET.
|
||||
|
||||
⚠️ **Testing** - Integration tests needed to validate end-to-end functionality.
|
||||
|
||||
## What's Implemented
|
||||
|
||||
### Phase 1: Core Infrastructure ✅
|
||||
- New `StartupMethod` enum values
|
||||
- `ElectronHub` SignalR hub
|
||||
- Hub endpoint registration
|
||||
|
||||
### Phase 2: Runtime Controller ✅
|
||||
- `RuntimeControllerAspNetDotnetFirstSignalR`
|
||||
- Port 0 binding logic
|
||||
- Electron launch with URL parameter
|
||||
- SignalR connection tracking
|
||||
|
||||
### Phase 3: Electron/Node.js Side ✅
|
||||
- `@microsoft/signalr` package integration
|
||||
- SignalR connection module
|
||||
- Startup mode detection
|
||||
- URL parameter handling
|
||||
|
||||
### Phase 4: API Bridge ✅ (Basic Structure)
|
||||
- `SignalRFacade` class
|
||||
- Event handler system
|
||||
- Hub connection integration
|
||||
|
||||
### Phase 5: Configuration ✅
|
||||
- Environment variable detection
|
||||
- Port 0 configuration
|
||||
- Automatic service registration
|
||||
|
||||
## Next Steps (Phase 6)
|
||||
|
||||
To fully utilize this feature, the following work is recommended:
|
||||
|
||||
1. **API Integration** - Make existing Electron APIs work with SignalR
|
||||
2. **Sample Application** - Create a Blazor Server demo
|
||||
3. **Integration Tests** - Validate end-to-end scenarios
|
||||
4. **Documentation** - Complete user guides and examples
|
||||
5. **Performance Testing** - Compare with socket.io mode
|
||||
|
||||
## Files Changed
|
||||
|
||||
### .NET
|
||||
- `src/ElectronNET.API/Runtime/Data/StartupMethod.cs`
|
||||
- `src/ElectronNET.AspNet/Hubs/ElectronHub.cs`
|
||||
- `src/ElectronNET.AspNet/Bridge/SignalRFacade.cs`
|
||||
- `src/ElectronNET.AspNet/Runtime/Controllers/RuntimeControllerAspNetDotnetFirstSignalR.cs`
|
||||
- `src/ElectronNET.AspNet/API/ElectronEndpointRouteBuilderExtensions.cs`
|
||||
- `src/ElectronNET.AspNet/API/WebHostBuilderExtensions.cs`
|
||||
- `src/ElectronNET.API/Runtime/StartupManager.cs`
|
||||
|
||||
### Electron/Node.js
|
||||
- `src/ElectronNET.Host/package.json`
|
||||
- `src/ElectronNET.Host/main.js`
|
||||
- `src/ElectronNET.Host/api/signalr-bridge.js` (new file)
|
||||
|
||||
## Commits
|
||||
|
||||
```
|
||||
7f2ea48 - Add PackagedDotnetFirstSignalR and UnpackedDotnetFirstSignalR startup methods
|
||||
8ee81f6 - Add ElectronHub and SignalR infrastructure for new startup modes
|
||||
40aed60 - Add RuntimeControllerAspNetDotnetFirstSignalR for SignalR-based startup
|
||||
c1740b5 - Add SignalR client support to Electron Host for new startup modes
|
||||
cb7d721 - Add SignalRFacade for SignalR-based API communication
|
||||
268b9c9 - Update RuntimeControllerAspNetDotnetFirstSignalR to use SignalRFacade
|
||||
04ec522 - Fix compilation errors - Phase 4 complete (basic structure)
|
||||
054f5b1 - Complete Phase 5: Add SignalR startup detection and port 0 configuration
|
||||
```
|
||||
|
||||
## Benefits Over Socket.io Mode
|
||||
|
||||
- **Better Integration** - Native SignalR is part of ASP.NET Core stack
|
||||
- **Type Safety** - SignalR has better TypeScript support
|
||||
- **Performance** - SignalR is optimized for ASP.NET Core
|
||||
- **Reliability** - Built-in reconnection and error handling
|
||||
- **Scalability** - Can leverage SignalR's scale-out features
|
||||
- **Consistency** - Blazor Server already uses SignalR
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to Phase 6 (full API integration):
|
||||
|
||||
1. Focus on adapting existing Electron API classes to work with SignalRFacade
|
||||
2. Implement request-response pattern in ElectronHub
|
||||
3. Add integration tests
|
||||
4. Create sample applications
|
||||
5. Update documentation
|
||||
|
||||
## License
|
||||
|
||||
MIT - Same as Electron.NET
|
||||
|
||||
---
|
||||
|
||||
**Created**: January 30, 2026
|
||||
**Status**: Infrastructure Complete, API Integration Pending
|
||||
**Contact**: See Electron.NET maintainers
|
||||
@@ -2,9 +2,11 @@
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
using ElectronNET.API.Bridge;
|
||||
|
||||
internal static class BridgeConnector
|
||||
{
|
||||
public static SocketIoFacade Socket
|
||||
public static IFacade Socket
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
62
src/ElectronNET.API/Bridge/IFacade.cs
Normal file
62
src/ElectronNET.API/Bridge/IFacade.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace ElectronNET.API.Bridge
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Common interface for communication facades (SocketIO and SignalR).
|
||||
/// Provides methods for bidirectional communication between .NET and Electron.
|
||||
/// </summary>
|
||||
internal interface IFacade
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised when the bridge connection is established.
|
||||
/// </summary>
|
||||
event EventHandler BridgeConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the bridge connection is lost.
|
||||
/// </summary>
|
||||
event EventHandler BridgeDisconnected;
|
||||
|
||||
/// <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 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 with a typed parameter.
|
||||
/// </summary>
|
||||
void Once<T>(string eventName, Action<T> action);
|
||||
|
||||
/// <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>
|
||||
/// Disposes the connection.
|
||||
/// </summary>
|
||||
void DisposeSocket();
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ namespace ElectronNET.API;
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API.Bridge;
|
||||
using ElectronNET.API.Serialization;
|
||||
using SocketIO.Serializer.SystemTextJson;
|
||||
using SocketIO = SocketIOClient.SocketIO;
|
||||
|
||||
internal class SocketIoFacade
|
||||
internal class SocketIoFacade : IFacade
|
||||
{
|
||||
private readonly SocketIO _socket;
|
||||
private readonly object _lockObj = new object();
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace ElectronNET.Common
|
||||
{
|
||||
case StartupMethod.UnpackedElectronFirst:
|
||||
case StartupMethod.UnpackedDotnetFirst:
|
||||
case StartupMethod.UnpackedDotnetFirstSignalR:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -536,7 +536,7 @@
|
||||
|
||||
if (e.Data != null)
|
||||
{
|
||||
Console.WriteLine("|| " + e.Data);
|
||||
System.Diagnostics.Debug.WriteLine(e.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -570,7 +570,7 @@
|
||||
|
||||
if (e.Data != null)
|
||||
{
|
||||
Console.WriteLine("|| " + e.Data);
|
||||
System.Diagnostics.Debug.WriteLine(e.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
namespace ElectronNET
|
||||
{
|
||||
using ElectronNET.API;
|
||||
using ElectronNET.API.Bridge;
|
||||
using ElectronNET.Runtime;
|
||||
using ElectronNET.Runtime.Controllers;
|
||||
using ElectronNET.Runtime.Data;
|
||||
@@ -49,7 +50,7 @@
|
||||
|
||||
internal static Func<Task> OnAppReadyCallback { get; set; }
|
||||
|
||||
internal static SocketIoFacade GetSocket()
|
||||
internal static IFacade GetSocket()
|
||||
{
|
||||
return RuntimeControllerCore?.Socket;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -12,7 +13,7 @@
|
||||
{
|
||||
}
|
||||
|
||||
internal abstract SocketIoFacade Socket { get; }
|
||||
internal abstract IFacade Socket { get; }
|
||||
|
||||
internal abstract ElectronProcessBase ElectronProcess { get; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -19,7 +20,7 @@
|
||||
{
|
||||
}
|
||||
|
||||
internal override SocketIoFacade Socket
|
||||
internal override IFacade Socket
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@@ -33,5 +33,23 @@
|
||||
/// On the command lines, this is "unpackeddotnet"
|
||||
/// </remarks>
|
||||
UnpackedDotnetFirst,
|
||||
|
||||
/// <summary>Packaged Electron app where DotNet launches Electron and uses SignalR for communication.</summary>
|
||||
/// <remarks>
|
||||
/// DotNet starts first on port 0 (dynamic), launches Electron with the actual URL,
|
||||
/// and uses SignalR instead of socket.io for bidirectional communication.
|
||||
/// Optimized for Blazor Server scenarios. ASP.NET Core only.
|
||||
/// On the command lines, this is "dotnetpackedsignalr"
|
||||
/// </remarks>
|
||||
PackagedDotnetFirstSignalR,
|
||||
|
||||
/// <summary>Unpackaged execution where DotNet launches Electron and uses SignalR for communication.</summary>
|
||||
/// <remarks>
|
||||
/// Similar to PackagedDotnetFirstSignalR but for debugging scenarios.
|
||||
/// DotNet starts first on port 0 (dynamic), launches Electron with the actual URL,
|
||||
/// and uses SignalR instead of socket.io for bidirectional communication.
|
||||
/// On the command lines, this is "unpackeddotnetsignalr"
|
||||
/// </remarks>
|
||||
UnpackedDotnetFirstSignalR,
|
||||
}
|
||||
}
|
||||
18
src/ElectronNET.API/Runtime/Helpers/DebuggerHelper.cs
Normal file
18
src/ElectronNET.API/Runtime/Helpers/DebuggerHelper.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace ElectronNET.Runtime.Helpers
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for debugger detection with lazy initialization.
|
||||
/// </summary>
|
||||
internal static class DebuggerHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether a debugger is attached. This value is cached for performance.
|
||||
/// </summary>
|
||||
public static bool IsAttached => _isAttached.Value;
|
||||
|
||||
private static readonly Lazy<bool> _isAttached = new Lazy<bool>(() => Debugger.IsAttached);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Probe scored for launch origin: DotNet {0} vs. {1} Electron", scoreDotNet, scoreElectron);
|
||||
// Debug trace - useful for diagnostics
|
||||
System.Diagnostics.Debug.WriteLine($"Probe scored for launch origin: DotNet {scoreDotNet} vs. {scoreElectron} Electron");
|
||||
return scoreDotNet > scoreElectron;
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@
|
||||
|
||||
private static bool? CheckIsDotNetStartup3()
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
if (DebuggerHelper.IsAttached)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Probe scored for package mode: Unpackaged {0} vs. {1} Packaged", scoreUnpackaged, scorePackaged);
|
||||
// Debug trace - useful for diagnostics
|
||||
System.Diagnostics.Debug.WriteLine($"Probe scored for package mode: Unpackaged {scoreUnpackaged} vs. {scorePackaged} Packaged");
|
||||
return scoreUnpackaged > scorePackaged;
|
||||
}
|
||||
|
||||
@@ -63,13 +64,16 @@
|
||||
|
||||
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"))
|
||||
// Faster: Direct path check instead of directory enumeration
|
||||
if (Directory.Exists(Path.Combine(baseDir, ".electron")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -79,12 +83,11 @@
|
||||
|
||||
private static bool? CheckUnpackaged3()
|
||||
{
|
||||
if (Debugger.IsAttached)
|
||||
if (DebuggerHelper.IsAttached)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,8 +161,8 @@
|
||||
{
|
||||
await Task.Delay(10.ms()).ConfigureAwait(false);
|
||||
|
||||
Console.Error.WriteLine("[StartInternal]: startCmd: {0}", startCmd);
|
||||
Console.Error.WriteLine("[StartInternal]: args: {0}", args);
|
||||
System.Diagnostics.Debug.WriteLine($"[StartInternal]: startCmd: {startCmd}");
|
||||
System.Diagnostics.Debug.WriteLine($"[StartInternal]: args: {args}");
|
||||
|
||||
this.process = new ProcessRunner("ElectronRunner");
|
||||
this.process.ProcessExited += this.Process_Exited;
|
||||
@@ -170,12 +170,10 @@
|
||||
|
||||
await Task.Delay(500.ms()).ConfigureAwait(false);
|
||||
|
||||
Console.Error.WriteLine("[StartInternal]: after run:");
|
||||
|
||||
if (!this.process.IsRunning)
|
||||
{
|
||||
Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardError);
|
||||
Console.Error.WriteLine("[StartInternal]: Process is not running: " + this.process.StandardOutput);
|
||||
System.Diagnostics.Debug.WriteLine($"[StartInternal]: Process is not running: {this.process.StandardError}");
|
||||
System.Diagnostics.Debug.WriteLine($"[StartInternal]: Process is not running: {this.process.StandardOutput}");
|
||||
|
||||
Task.Run(() => this.TransitionState(LifetimeState.Stopped));
|
||||
|
||||
@@ -186,9 +184,9 @@
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("[StartInternal]: Exception: " + this.process?.StandardError);
|
||||
Console.Error.WriteLine("[StartInternal]: Exception: " + this.process?.StandardOutput);
|
||||
Console.Error.WriteLine("[StartInternal]: Exception: " + ex);
|
||||
System.Diagnostics.Debug.WriteLine($"[StartInternal]: Exception: {this.process?.StandardError}");
|
||||
System.Diagnostics.Debug.WriteLine($"[StartInternal]: Exception: {this.process?.StandardOutput}");
|
||||
System.Diagnostics.Debug.WriteLine($"[StartInternal]: Exception: {ex}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,12 @@
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
var startTime = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
ElectronNetRuntime.BuildInfo = this.GatherBuildInfo();
|
||||
System.Diagnostics.Debug.WriteLine($"[Startup] GatherBuildInfo: {startTime.ElapsedMilliseconds}ms");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -24,15 +27,18 @@
|
||||
|
||||
this.CollectProcessData();
|
||||
this.SetElectronExecutable();
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[Startup] CollectProcessData+SetElectronExecutable: {startTime.ElapsedMilliseconds}ms");
|
||||
|
||||
ElectronNetRuntime.StartupMethod = this.DetectAppTypeAndStartup();
|
||||
Console.WriteLine((string)("Evaluated StartupMethod: " + ElectronNetRuntime.StartupMethod));
|
||||
System.Diagnostics.Debug.WriteLine($"Evaluated StartupMethod: {ElectronNetRuntime.StartupMethod}");
|
||||
System.Diagnostics.Debug.WriteLine($"[Startup] DetectAppTypeAndStartup: {startTime.ElapsedMilliseconds}ms");
|
||||
|
||||
if (ElectronNetRuntime.DotnetAppType != DotnetAppType.AspNetCoreApp)
|
||||
{
|
||||
ElectronNetRuntime.RuntimeControllerCore = this.CreateRuntimeController();
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[Startup] Total StartupManager.Initialize: {startTime.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
private RuntimeControllerBase CreateRuntimeController()
|
||||
@@ -54,15 +60,19 @@
|
||||
{
|
||||
var isLaunchedByDotNet = LaunchOrderDetector.CheckIsLaunchedByDotNet();
|
||||
var isUnPackaged = UnpackagedDetector.CheckIsUnpackaged();
|
||||
|
||||
// Check for SignalR mode via environment variable
|
||||
var useSignalR = Environment.GetEnvironmentVariable("ELECTRON_USE_SIGNALR");
|
||||
var isSignalRMode = !string.IsNullOrEmpty(useSignalR) && useSignalR.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isLaunchedByDotNet)
|
||||
{
|
||||
if (isUnPackaged)
|
||||
{
|
||||
return StartupMethod.UnpackedDotnetFirst;
|
||||
return isSignalRMode ? StartupMethod.UnpackedDotnetFirstSignalR : StartupMethod.UnpackedDotnetFirst;
|
||||
}
|
||||
|
||||
return StartupMethod.PackagedDotnetFirst;
|
||||
return isSignalRMode ? StartupMethod.PackagedDotnetFirstSignalR : StartupMethod.PackagedDotnetFirst;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -132,18 +142,18 @@
|
||||
|
||||
if (electronAssembly == null)
|
||||
{
|
||||
Console.WriteLine("GatherBuildInfo: Early exit");
|
||||
System.Diagnostics.Debug.WriteLine("GatherBuildInfo: Early exit");
|
||||
return buildInfo;
|
||||
}
|
||||
|
||||
if (electronAssembly.GetName().Name == "testhost" || electronAssembly.GetName().Name == "ReSharperTestRunner")
|
||||
{
|
||||
Console.WriteLine("GatherBuildInfo: Detected testhost");
|
||||
System.Diagnostics.Debug.WriteLine("GatherBuildInfo: Detected testhost");
|
||||
electronAssembly = AppDomain.CurrentDomain.GetData("ElectronTestAssembly") as Assembly ?? electronAssembly;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("GatherBuildInfo: No testhost detected: " + electronAssembly.GetName().Name);
|
||||
System.Diagnostics.Debug.WriteLine("GatherBuildInfo: No testhost detected: " + electronAssembly.GetName().Name);
|
||||
}
|
||||
|
||||
var attributes = electronAssembly.GetCustomAttributes<AssemblyMetadataAttribute>().ToList();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
using ElectronNET.AspNet.Hubs;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for mapping the Electron SignalR hub.
|
||||
/// </summary>
|
||||
public static class ElectronEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps the Electron SignalR hub to the /electron-hub endpoint.
|
||||
/// This is required when using SignalR-based startup modes.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The endpoint route builder.</param>
|
||||
/// <returns>The endpoint route builder for chaining.</returns>
|
||||
public static IEndpointRouteBuilder MapElectronHub(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
endpoints.MapHub<ElectronHub>("/electron-hub");
|
||||
return endpoints;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
using ElectronNET.Runtime.Helpers;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="IWebHostBuilder"/> to enable Electron.NET
|
||||
@@ -66,23 +67,31 @@
|
||||
// work as expected, see issue #952
|
||||
Environment.SetEnvironmentVariable("ELECTRON_RUN_AS_NODE", null);
|
||||
|
||||
var webPort = PortHelper.GetFreePort(ElectronNetRuntime.AspNetWebPort ?? ElectronNetRuntime.DefaultWebPort);
|
||||
// For SignalR modes, use port 0 for dynamic port assignment
|
||||
var usePort0 = ElectronNetRuntime.StartupMethod == StartupMethod.PackagedDotnetFirstSignalR ||
|
||||
ElectronNetRuntime.StartupMethod == StartupMethod.UnpackedDotnetFirstSignalR;
|
||||
|
||||
var webPort = usePort0 ? 0 : PortHelper.GetFreePort(ElectronNetRuntime.AspNetWebPort ?? ElectronNetRuntime.DefaultWebPort);
|
||||
ElectronNetRuntime.AspNetWebPort = webPort;
|
||||
|
||||
// check for the content folder if its exists in base director otherwise no need to include
|
||||
// It was used before because we are publishing the project which copies everything to bin folder and contentroot wwwroot was folder there.
|
||||
// now we have implemented the live reload if app is run using /watch then we need to use the default project path.
|
||||
|
||||
// For port 0 (dynamic port assignment), Kestrel requires binding to specific IP (127.0.0.1) not localhost
|
||||
var host = usePort0 ? "127.0.0.1" : "localhost";
|
||||
|
||||
if (Directory.Exists($"{AppDomain.CurrentDomain.BaseDirectory}\\wwwroot"))
|
||||
{
|
||||
builder = builder.UseContentRoot(AppDomain.CurrentDomain.BaseDirectory)
|
||||
.UseUrls("http://localhost:" + webPort);
|
||||
.UseUrls($"http://{host}:{webPort}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder = builder.UseUrls("http://localhost:" + webPort);
|
||||
builder = builder.UseUrls($"http://{host}:{webPort}");
|
||||
}
|
||||
|
||||
builder = builder.ConfigureServices(services =>
|
||||
builder = builder.ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddTransient<IStartupFilter, ServerReadyStartupFilter>();
|
||||
services.AddSingleton<AspNetLifetimeAdapter>();
|
||||
@@ -97,6 +106,17 @@
|
||||
case StartupMethod.UnpackedDotnetFirst:
|
||||
services.AddSingleton<IElectronNetRuntimeController, RuntimeControllerAspNetDotnetFirst>();
|
||||
break;
|
||||
case StartupMethod.PackagedDotnetFirstSignalR:
|
||||
case StartupMethod.UnpackedDotnetFirstSignalR:
|
||||
services.AddSignalR(options =>
|
||||
{
|
||||
// Enable detailed errors only in development for security
|
||||
options.EnableDetailedErrors =
|
||||
DebuggerHelper.IsAttached ||
|
||||
context.HostingEnvironment.IsDevelopment();
|
||||
});
|
||||
services.AddSingleton<IElectronNetRuntimeController, RuntimeControllerAspNetDotnetFirstSignalR>();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
232
src/ElectronNET.AspNet/Bridge/SignalRFacade.cs
Normal file
232
src/ElectronNET.AspNet/Bridge/SignalRFacade.cs
Normal file
@@ -0,0 +1,232 @@
|
||||
namespace ElectronNET.API
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using ElectronNET.API.Bridge;
|
||||
using ElectronNET.AspNet.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR-based facade that mimics the SocketIoFacade interface
|
||||
/// for compatibility with existing Electron API code.
|
||||
///
|
||||
/// Key implementation details:
|
||||
/// - Uses IHubContext to send events to Electron via 'event' hub method
|
||||
/// - Receives events from Electron via ElectronHub.ElectronEvent() method
|
||||
/// - Includes ConvertToType<T> helper to handle JsonElement and numeric type conversions
|
||||
/// - Event args are passed as arrays to match SignalR serialization behavior
|
||||
/// - Connection ID is set by ElectronHub when Electron client connects
|
||||
/// </summary>
|
||||
internal class SignalRFacade : IFacade
|
||||
{
|
||||
private readonly IHubContext<ElectronHub> _hubContext;
|
||||
private string _connectionId;
|
||||
private readonly ConcurrentDictionary<string, Action<object>> _eventHandlers;
|
||||
private readonly object _lockObj = new object();
|
||||
|
||||
public SignalRFacade(IHubContext<ElectronHub> hubContext)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_eventHandlers = new ConcurrentDictionary<string, Action<object>>();
|
||||
}
|
||||
|
||||
public event EventHandler BridgeDisconnected;
|
||||
public event EventHandler BridgeConnected;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR connections are managed by ASP.NET Core, so this is a no-op.
|
||||
/// Connection establishment happens via the ElectronHub.
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
// No-op: SignalR connection is managed by ASP.NET Core
|
||||
}
|
||||
|
||||
public void SetConnectionId(string connectionId)
|
||||
{
|
||||
_connectionId = connectionId;
|
||||
this.BridgeConnected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void OnDisconnected()
|
||||
{
|
||||
this.BridgeDisconnected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void On(string eventName, Action action)
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
_eventHandlers[eventName] = _ => Task.Run(action);
|
||||
}
|
||||
}
|
||||
|
||||
public void On<T>(string eventName, Action<T> action)
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
_eventHandlers[eventName] = obj =>
|
||||
{
|
||||
var converted = ConvertToType<T>(obj);
|
||||
if (converted != null)
|
||||
{
|
||||
Task.Run(() => action(converted));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"[SignalRFacade] Failed to convert event data to type {typeof(T).Name}");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void Once(string eventName, Action action)
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
_eventHandlers[eventName] = _ =>
|
||||
{
|
||||
this.Off(eventName);
|
||||
Task.Run(action);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void Once<T>(string eventName, Action<T> action)
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
_eventHandlers[eventName] = obj =>
|
||||
{
|
||||
this.Off(eventName);
|
||||
var converted = ConvertToType<T>(obj);
|
||||
if (converted != null)
|
||||
{
|
||||
Task.Run(() => action(converted));
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"[SignalRFacade] Failed to convert event data to type {typeof(T).Name} for event '{eventName}'");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void Off(string eventName)
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
_eventHandlers.TryRemove(eventName, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Emit(string eventName, params object[] args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_connectionId))
|
||||
{
|
||||
Console.Error.WriteLine($"[SignalRFacade] Cannot emit '{eventName}' - no connection ID");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Send message to specific Electron client via the 'event' hub method
|
||||
// This will be received by signalr-bridge.js's connection.on('event', ...)
|
||||
await _hubContext.Clients.Client(_connectionId).SendAsync("event", eventName, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[SignalRFacade] Error emitting '{eventName}': {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void TriggerEvent(string eventName, params object[] args)
|
||||
{
|
||||
if (_eventHandlers.TryGetValue(eventName, out var handler))
|
||||
{
|
||||
// If single arg, pass it directly; otherwise pass the array
|
||||
var data = args.Length == 1 ? args[0] : args;
|
||||
handler(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an object to the specified type, handling JsonElement and numeric conversions.
|
||||
/// </summary>
|
||||
private static T ConvertToType<T>(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return default;
|
||||
|
||||
// Direct type match
|
||||
if (obj is T typedValue)
|
||||
return typedValue;
|
||||
|
||||
var targetType = typeof(T);
|
||||
|
||||
// Handle JsonElement (common from SignalR deserialization)
|
||||
if (obj is JsonElement jsonElement)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(jsonElement.GetRawText());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[SignalRFacade] JsonElement deserialization failed: {ex.Message}");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle numeric conversions (SignalR often sends numbers as long/double)
|
||||
try
|
||||
{
|
||||
if (targetType == typeof(int) || targetType == typeof(int?))
|
||||
{
|
||||
return (T)(object)Convert.ToInt32(obj);
|
||||
}
|
||||
if (targetType == typeof(long) || targetType == typeof(long?))
|
||||
{
|
||||
return (T)(object)Convert.ToInt64(obj);
|
||||
}
|
||||
if (targetType == typeof(double) || targetType == typeof(double?))
|
||||
{
|
||||
return (T)(object)Convert.ToDouble(obj);
|
||||
}
|
||||
if (targetType == typeof(bool) || targetType == typeof(bool?))
|
||||
{
|
||||
return (T)(object)Convert.ToBoolean(obj);
|
||||
}
|
||||
if (targetType == typeof(string))
|
||||
{
|
||||
return (T)(object)obj.ToString();
|
||||
}
|
||||
|
||||
// For arrays, try JSON serialization roundtrip
|
||||
if (targetType.IsArray && obj is object[] arr)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(arr);
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
|
||||
// Last resort: try to serialize and deserialize
|
||||
var serialized = JsonSerializer.Serialize(obj);
|
||||
return JsonSerializer.Deserialize<T>(serialized);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[SignalRFacade] Type conversion failed from {obj.GetType().Name} to {targetType.Name}: {ex.Message}");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public void DisposeSocket()
|
||||
{
|
||||
// SignalR connections are managed by ASP.NET Core
|
||||
_eventHandlers.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/ElectronNET.AspNet/Hubs/ElectronHub.cs
Normal file
108
src/ElectronNET.AspNet/Hubs/ElectronHub.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
namespace ElectronNET.AspNet.Hubs
|
||||
{
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET;
|
||||
using ElectronNET.API;
|
||||
using ElectronNET.AspNet.Runtime;
|
||||
using ElectronNET.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR hub for bidirectional communication between ASP.NET Core and Electron.
|
||||
/// Replaces socket.io for SignalR-based startup modes.
|
||||
/// </summary>
|
||||
public class ElectronHub : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when Electron client connects to the hub.
|
||||
/// </summary>
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
// Notify the runtime controller about the connection
|
||||
var runtimeController = ElectronNetRuntime.RuntimeController as RuntimeControllerAspNetDotnetFirstSignalR;
|
||||
if (runtimeController != null)
|
||||
{
|
||||
runtimeController.OnSignalRConnected(Context.ConnectionId);
|
||||
}
|
||||
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when Electron client disconnects from the hub.
|
||||
/// </summary>
|
||||
public override async Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
if (exception != null)
|
||||
{
|
||||
Console.Error.WriteLine($"[ElectronHub] Disconnect error: {exception.Message}");
|
||||
}
|
||||
|
||||
// Notify the runtime controller about the disconnection
|
||||
var runtimeController = ElectronNetRuntime.RuntimeController as RuntimeControllerAspNetDotnetFirstSignalR;
|
||||
if (runtimeController != null)
|
||||
{
|
||||
runtimeController.OnSignalRDisconnected();
|
||||
}
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the Electron client. Called by Electron on connection.
|
||||
/// </summary>
|
||||
public async Task RegisterElectronClient()
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives events from Electron (e.g., "BrowserWindowCreated", "dialogResult").
|
||||
/// Called by Electron to send data back to .NET.
|
||||
/// </summary>
|
||||
/// <param name="eventName">The event name</param>
|
||||
/// <param name="args">The event arguments as an array</param>
|
||||
public async Task ElectronEvent(string eventName, object[] args)
|
||||
{
|
||||
// Get the SignalRFacade and trigger the event handlers
|
||||
var runtimeController = ElectronNetRuntime.RuntimeController as RuntimeControllerAspNetDotnetFirstSignalR;
|
||||
if (runtimeController?.SignalRSocket is SignalRFacade signalRFacade)
|
||||
{
|
||||
// Invoke the event handlers registered via On/Once
|
||||
signalRFacade.TriggerEvent(eventName, args ?? Array.Empty<object>());
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes an Electron API method. Called by .NET to control Electron.
|
||||
/// This is a placeholder for future API invocation patterns.
|
||||
/// </summary>
|
||||
/// <param name="method">The API method name</param>
|
||||
/// <param name="data">The method parameters as JSON</param>
|
||||
/// <returns>The result of the API call</returns>
|
||||
public async Task<string> InvokeElectronApi(string method, string data)
|
||||
{
|
||||
// Forward to Electron client
|
||||
await Clients.Caller.SendAsync("electronApiCall", method, data);
|
||||
|
||||
// TODO: Implement proper request-response pattern
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles responses from Electron API calls.
|
||||
/// This is a placeholder for future API invocation patterns.
|
||||
/// </summary>
|
||||
/// <param name="callId">The unique identifier for this API call</param>
|
||||
/// <param name="result">The result data as JSON</param>
|
||||
public async Task ElectronApiResponse(string callId, string result)
|
||||
{
|
||||
// This will be handled by the SignalR facade to complete pending tasks
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
namespace ElectronNET.AspNet.Middleware
|
||||
{
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ElectronNET.AspNet.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Middleware that validates authentication for all Electron requests.
|
||||
/// Checks for authentication cookie or token query parameter on first request.
|
||||
/// Sets HttpOnly cookie for subsequent requests.
|
||||
///
|
||||
/// Security Model:
|
||||
/// - First request includes token as query parameter (?token=guid)
|
||||
/// - Middleware validates token and sets secure HttpOnly cookie
|
||||
/// - Subsequent requests use cookie (no token in URL)
|
||||
/// - Both HTTP endpoints and SignalR hub protected
|
||||
/// </summary>
|
||||
public class ElectronAuthenticationMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly IElectronAuthenticationService _authService;
|
||||
private readonly ILogger<ElectronAuthenticationMiddleware> _logger;
|
||||
private const string AuthCookieName = "ElectronAuth";
|
||||
|
||||
public ElectronAuthenticationMiddleware(
|
||||
RequestDelegate next,
|
||||
IElectronAuthenticationService authService,
|
||||
ILogger<ElectronAuthenticationMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_authService = authService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value;
|
||||
|
||||
// Check if authentication cookie exists
|
||||
var authCookie = context.Request.Cookies[AuthCookieName];
|
||||
|
||||
if (!string.IsNullOrEmpty(authCookie))
|
||||
{
|
||||
// Cookie present - validate it
|
||||
if (_authService.ValidateToken(authCookie))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid cookie - reject
|
||||
_logger.LogWarning("Authentication failed: Invalid cookie for path {Path} from {RemoteIp}",
|
||||
path, context.Connection.RemoteIpAddress);
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Unauthorized: Invalid authentication");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No cookie - check for token in query string (first-time authentication)
|
||||
var token = context.Request.Query["token"].ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
if (_authService.ValidateToken(token))
|
||||
{
|
||||
// Valid token - set cookie for future requests
|
||||
_logger.LogInformation("Authentication successful: Setting cookie for path {Path}", path);
|
||||
|
||||
context.Response.Cookies.Append(AuthCookieName, token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true, // Prevent JavaScript access (XSS protection)
|
||||
SameSite = SameSiteMode.Strict, // CSRF protection
|
||||
Path = "/", // Valid for all routes
|
||||
Secure = false, // False because localhost is HTTP
|
||||
IsEssential = true // Required for app to function
|
||||
});
|
||||
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid token - reject
|
||||
_logger.LogWarning("Authentication failed: Invalid token (prefix: {TokenPrefix}...) for path {Path} from {RemoteIp}",
|
||||
token.Length > 8 ? token.Substring(0, 8) : token, path, context.Connection.RemoteIpAddress);
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Unauthorized: Invalid authentication");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Neither cookie nor valid token present - reject
|
||||
_logger.LogWarning("Authentication failed: No cookie or token provided for path {Path} from {RemoteIp}",
|
||||
path, context.Connection.RemoteIpAddress);
|
||||
context.Response.StatusCode = 401;
|
||||
await context.Response.WriteAsync("Unauthorized: Authentication required");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API;
|
||||
using ElectronNET.API.Bridge;
|
||||
using ElectronNET.Common;
|
||||
using ElectronNET.Runtime.Controllers;
|
||||
using ElectronNET.Runtime.Data;
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
internal override SocketBridgeService SocketBridge => this.socketBridge;
|
||||
|
||||
internal override SocketIoFacade Socket
|
||||
internal override IFacade Socket
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
namespace ElectronNET.AspNet.Runtime
|
||||
{
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.API;
|
||||
using ElectronNET.API.Bridge;
|
||||
using ElectronNET.Common;
|
||||
using ElectronNET.Runtime.Data;
|
||||
using ElectronNET.Runtime.Services.ElectronProcess;
|
||||
using ElectronNET.Runtime.Services.SocketBridge;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using ElectronNET.AspNet.Hubs;
|
||||
using ElectronNET.AspNet.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime controller for SignalR-based .NET-first startup mode.
|
||||
/// Key differences from Socket.IO mode:
|
||||
/// - Waits for ASP.NET server to start, then captures the dynamic port
|
||||
/// - Launches Electron with the actual URL (no port scanning needed)
|
||||
/// - Uses SignalRFacade instead of SocketIOFacade for bidirectional communication
|
||||
/// - Waits for 'electron-host-ready' signal to ensure API modules are loaded before calling app callback
|
||||
/// </summary>
|
||||
internal class RuntimeControllerAspNetDotnetFirstSignalR : RuntimeControllerAspNetBase
|
||||
{
|
||||
private ElectronProcessBase electronProcess;
|
||||
private readonly IServer server;
|
||||
private readonly IHubContext<ElectronHub> hubContext;
|
||||
private readonly IElectronAuthenticationService authenticationService;
|
||||
private SignalRFacade signalRFacade;
|
||||
private int? port;
|
||||
private string actualUrl;
|
||||
private bool electronLaunched;
|
||||
private string authenticationToken;
|
||||
|
||||
public RuntimeControllerAspNetDotnetFirstSignalR(
|
||||
AspNetLifetimeAdapter aspNetLifetimeAdapter,
|
||||
IServer server,
|
||||
IHubContext<ElectronHub> hubContext,
|
||||
IElectronAuthenticationService authenticationService)
|
||||
: base(aspNetLifetimeAdapter)
|
||||
{
|
||||
this.server = server;
|
||||
this.hubContext = hubContext;
|
||||
this.authenticationService = authenticationService;
|
||||
this.signalRFacade = new SignalRFacade(hubContext);
|
||||
this.electronLaunched = false;
|
||||
|
||||
this.signalRFacade.BridgeConnected += this.SignalRFacade_Connected;
|
||||
this.signalRFacade.BridgeDisconnected += this.SignalRFacade_Disconnected;
|
||||
|
||||
// Subscribe to ASP.NET ready event to launch Electron
|
||||
aspNetLifetimeAdapter.Ready += this.OnAspNetReady;
|
||||
}
|
||||
|
||||
internal override ElectronProcessBase ElectronProcess => this.electronProcess;
|
||||
internal override SocketBridgeService SocketBridge => null;
|
||||
|
||||
internal override IFacade Socket
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.State == LifetimeState.Ready)
|
||||
{
|
||||
return this.signalRFacade;
|
||||
}
|
||||
|
||||
throw new Exception("Cannot access SignalR facade. Runtime is not in 'Ready' state");
|
||||
}
|
||||
}
|
||||
|
||||
internal SignalRFacade SignalRSocket => this.signalRFacade;
|
||||
|
||||
protected override Task StartCore()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override Task StopCore()
|
||||
{
|
||||
this.electronProcess?.Stop();
|
||||
this.signalRFacade?.DisposeSocket();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnAspNetReady(object sender, EventArgs e)
|
||||
{
|
||||
if (!this.electronLaunched)
|
||||
{
|
||||
this.CapturePortAndLaunchElectron();
|
||||
}
|
||||
}
|
||||
|
||||
private void CapturePortAndLaunchElectron()
|
||||
{
|
||||
var addresses = this.server.Features.Get<IServerAddressesFeature>();
|
||||
if (addresses == null || !addresses.Addresses.Any())
|
||||
{
|
||||
throw new Exception("Could not retrieve server addresses");
|
||||
}
|
||||
|
||||
this.actualUrl = addresses.Addresses.First();
|
||||
this.port = new Uri(this.actualUrl).Port;
|
||||
|
||||
// Update the runtime port so WindowManager uses the correct URL
|
||||
ElectronNetRuntime.AspNetWebPort = this.port;
|
||||
|
||||
this.LaunchElectron();
|
||||
this.electronLaunched = true;
|
||||
}
|
||||
|
||||
private void LaunchElectron()
|
||||
{
|
||||
// Generate secure authentication token (128-bit cryptographic random GUID)
|
||||
// This token protects against unauthorized connections from other users on the same machine
|
||||
this.authenticationToken = Guid.NewGuid().ToString("N"); // 32 hex chars, no hyphens
|
||||
|
||||
// Register token with authentication service for validation
|
||||
// The middleware will validate this token on all HTTP and SignalR requests
|
||||
this.authenticationService.SetExpectedToken(this.authenticationToken);
|
||||
|
||||
var isUnPacked = ElectronNetRuntime.StartupMethod.IsUnpackaged();
|
||||
var flag = isUnPacked ? "--unpackeddotnetsignalr" : "--dotnetpackedsignalr";
|
||||
var args = $"{flag} --electronurl={this.actualUrl} --authtoken={this.authenticationToken}";
|
||||
|
||||
this.electronProcess = new ElectronProcessActive(isUnPacked, ElectronNetRuntime.ElectronExecutable, args, this.port.Value);
|
||||
// Note: We do NOT subscribe to electronProcess.Ready in SignalR mode.
|
||||
// The "ready" signal comes from the SignalR connection, not stdout.
|
||||
this.electronProcess.Stopped += this.ElectronProcess_Stopped;
|
||||
_ = this.electronProcess.Start();
|
||||
}
|
||||
|
||||
private async void SignalRFacade_Connected(object sender, EventArgs e)
|
||||
{
|
||||
// Register handler for 'electron-host-ready' signal from Electron.
|
||||
// This ensures API modules are fully loaded before calling the app ready callback.
|
||||
this.signalRFacade.Once("electron-host-ready", () =>
|
||||
{
|
||||
this.OnElectronHostReady();
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnElectronHostReady()
|
||||
{
|
||||
this.TransitionState(LifetimeState.Ready);
|
||||
|
||||
// Execute the app ready callback
|
||||
if (ElectronNetRuntime.OnAppReadyCallback != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ElectronNetRuntime.OnAppReadyCallback().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Exception in app ready callback: {ex}");
|
||||
this.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SignalRFacade_Disconnected(object sender, EventArgs e)
|
||||
{
|
||||
// IMPORTANT: Do NOT call HandleStopped synchronously here!
|
||||
// This event fires from within SignalR's OnDisconnectedAsync, and calling
|
||||
// StopApplication() synchronously causes a deadlock: the host waits for
|
||||
// OnDisconnectedAsync to complete, but we're waiting for the host to stop.
|
||||
// Fire and forget to break the deadlock.
|
||||
_ = Task.Run(() => this.HandleStopped());
|
||||
}
|
||||
|
||||
private void ElectronProcess_Stopped(object sender, EventArgs e)
|
||||
{
|
||||
this.HandleStopped();
|
||||
}
|
||||
|
||||
public void OnSignalRConnected(string connectionId)
|
||||
{
|
||||
this.signalRFacade.SetConnectionId(connectionId);
|
||||
}
|
||||
|
||||
public void OnSignalRDisconnected()
|
||||
{
|
||||
this.signalRFacade.OnDisconnected();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
namespace ElectronNET.AspNet.Runtime
|
||||
using ElectronNET.Runtime.Data;
|
||||
using ElectronNET.Runtime.Services;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ElectronNET.AspNet.Runtime;
|
||||
|
||||
internal class AspNetLifetimeAdapter : LifetimeServiceBase
|
||||
{
|
||||
using System.Threading.Tasks;
|
||||
using ElectronNET.Runtime.Data;
|
||||
using ElectronNET.Runtime.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
private readonly IHostApplicationLifetime lifetimeService;
|
||||
|
||||
internal class AspNetLifetimeAdapter : LifetimeServiceBase
|
||||
public AspNetLifetimeAdapter(IHostApplicationLifetime lifetimeService)
|
||||
{
|
||||
private readonly IHostApplicationLifetime lifetimeService;
|
||||
this.lifetimeService = lifetimeService;
|
||||
|
||||
public AspNetLifetimeAdapter(IHostApplicationLifetime lifetimeService)
|
||||
{
|
||||
this.lifetimeService = lifetimeService;
|
||||
|
||||
this.lifetimeService.ApplicationStarted.Register(() => this.TransitionState(LifetimeState.Ready));
|
||||
this.lifetimeService.ApplicationStopping.Register(() => this.TransitionState(LifetimeState.Stopping));
|
||||
this.lifetimeService.ApplicationStopped.Register(() => this.TransitionState(LifetimeState.Stopped));
|
||||
}
|
||||
|
||||
protected override Task StopCore()
|
||||
{
|
||||
this.lifetimeService.StopApplication();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
this.lifetimeService.ApplicationStarted.Register(() => this.TransitionState(LifetimeState.Ready));
|
||||
this.lifetimeService.ApplicationStopping.Register(() => this.TransitionState(LifetimeState.Stopping));
|
||||
this.lifetimeService.ApplicationStopped.Register(() => this.TransitionState(LifetimeState.Stopped));
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task StopCore()
|
||||
{
|
||||
this.lifetimeService.StopApplication();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace ElectronNET.AspNet.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of authentication service for Electron clients.
|
||||
/// Stores and validates the authentication token to ensure only the spawned Electron process can connect.
|
||||
/// </summary>
|
||||
public class ElectronAuthenticationService : IElectronAuthenticationService
|
||||
{
|
||||
private string _expectedToken;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetExpectedToken(string token)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_expectedToken = token;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ValidateToken(string token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
return false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_expectedToken))
|
||||
return false;
|
||||
|
||||
// Constant-time comparison to prevent timing attacks
|
||||
return ConstantTimeEquals(token, _expectedToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs constant-time string comparison to prevent timing attacks.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace ElectronNET.AspNet.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for validating authentication tokens from Electron clients.
|
||||
/// Used to ensure only the Electron process spawned by this .NET instance can connect.
|
||||
/// </summary>
|
||||
public interface IElectronAuthenticationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the expected authentication token for this instance.
|
||||
/// Should be called when launching Electron with the generated token.
|
||||
/// </summary>
|
||||
/// <param name="token">The authentication token</param>
|
||||
void SetExpectedToken(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Validates an incoming token against the expected token.
|
||||
/// Uses constant-time comparison to prevent timing attacks.
|
||||
/// </summary>
|
||||
/// <param name="token">The token to validate</param>
|
||||
/// <returns>True if token is valid, false otherwise</returns>
|
||||
bool ValidateToken(string token);
|
||||
}
|
||||
}
|
||||
@@ -1,313 +1,338 @@
|
||||
import { RelaunchOptions, LoginItemSettingsOptions, Settings, AboutPanelOptionsOptions } from "electron";
|
||||
import {
|
||||
RelaunchOptions,
|
||||
LoginItemSettingsOptions,
|
||||
Settings,
|
||||
AboutPanelOptionsOptions,
|
||||
} from "electron";
|
||||
import { Socket } from "net";
|
||||
|
||||
let isQuitWindowAllClosed = true, electronSocket;
|
||||
let isQuitWindowAllClosed = true;
|
||||
let appWindowAllClosedEventId;
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket, app: Electron.App) => {
|
||||
electronSocket = socket;
|
||||
electronSocket = socket;
|
||||
|
||||
// By default, quit when all windows are closed
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin' && isQuitWindowAllClosed) {
|
||||
app.quit();
|
||||
} else if (appWindowAllClosedEventId) {
|
||||
// If the user is on macOS
|
||||
// - OR -
|
||||
// If the user has indicated NOT to quit when all windows are closed,
|
||||
// emit the event.
|
||||
electronSocket.emit('app-window-all-closed' + appWindowAllClosedEventId);
|
||||
}
|
||||
// By default, quit when all windows are closed
|
||||
app.on("window-all-closed", () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== "darwin" && isQuitWindowAllClosed) {
|
||||
app.quit();
|
||||
} else if (appWindowAllClosedEventId) {
|
||||
// If the user is on macOS
|
||||
// - OR -
|
||||
// If the user has indicated NOT to quit when all windows are closed,
|
||||
// emit the event.
|
||||
electronSocket.emit("app-window-all-closed" + appWindowAllClosedEventId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("quit-app-window-all-closed", (quit) => {
|
||||
isQuitWindowAllClosed = quit;
|
||||
});
|
||||
|
||||
socket.on("register-app-window-all-closed", (id) => {
|
||||
appWindowAllClosedEventId = id;
|
||||
});
|
||||
|
||||
socket.on("register-app-before-quit", (id) => {
|
||||
app.on("before-quit", (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
electronSocket.emit("app-before-quit" + id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('quit-app-window-all-closed', (quit) => {
|
||||
isQuitWindowAllClosed = quit;
|
||||
socket.on("register-app-will-quit", (id) => {
|
||||
app.on("will-quit", (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
electronSocket.emit("app-will-quit" + id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-window-all-closed', (id) => {
|
||||
appWindowAllClosedEventId = id;
|
||||
socket.on("register-app-browser-window-blur", (id) => {
|
||||
app.on("browser-window-blur", () => {
|
||||
electronSocket.emit("app-browser-window-blur" + id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-before-quit', (id) => {
|
||||
app.on('before-quit', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
electronSocket.emit('app-before-quit' + id);
|
||||
});
|
||||
socket.on("register-app-browser-window-focus", (id) => {
|
||||
app.on("browser-window-focus", () => {
|
||||
electronSocket.emit("app-browser-window-focus" + id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-will-quit', (id) => {
|
||||
app.on('will-quit', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
electronSocket.emit('app-will-quit' + id);
|
||||
});
|
||||
socket.on("register-app-browser-window-created", (id) => {
|
||||
app.on("browser-window-created", () => {
|
||||
electronSocket.emit("app-browser-window-created" + id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-browser-window-blur', (id) => {
|
||||
app.on('browser-window-blur', () => {
|
||||
electronSocket.emit('app-browser-window-blur' + id);
|
||||
});
|
||||
socket.on("register-app-web-contents-created", (id) => {
|
||||
app.on("web-contents-created", () => {
|
||||
electronSocket.emit("app-web-contents-created" + id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-browser-window-focus', (id) => {
|
||||
app.on('browser-window-focus', () => {
|
||||
electronSocket.emit('app-browser-window-focus' + id);
|
||||
});
|
||||
socket.on("register-app-accessibility-support-changed", (id) => {
|
||||
app.on(
|
||||
"accessibility-support-changed",
|
||||
(event, accessibilitySupportEnabled) => {
|
||||
electronSocket.emit(
|
||||
"app-accessibility-support-changed" + id,
|
||||
accessibilitySupportEnabled,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("appQuit", () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
socket.on("appExit", (exitCode = 0) => {
|
||||
app.exit(exitCode);
|
||||
});
|
||||
|
||||
socket.on("appRelaunch", (options) => {
|
||||
app.relaunch(options as RelaunchOptions);
|
||||
});
|
||||
|
||||
socket.on("appFocus", (options) => {
|
||||
app.focus(options);
|
||||
});
|
||||
|
||||
socket.on("appHide", () => {
|
||||
app.hide();
|
||||
});
|
||||
|
||||
socket.on("appShow", () => {
|
||||
app.show();
|
||||
});
|
||||
|
||||
socket.on("appGetAppPath", () => {
|
||||
const path = app.getAppPath();
|
||||
electronSocket.emit("appGetAppPathCompleted", path);
|
||||
});
|
||||
|
||||
socket.on("appSetAppLogsPath", (path) => {
|
||||
app.setAppLogsPath(path);
|
||||
});
|
||||
|
||||
socket.on("appGetPath", (name) => {
|
||||
const path = app.getPath(name);
|
||||
electronSocket.emit("appGetPathCompleted", path);
|
||||
});
|
||||
|
||||
socket.on("appGetFileIcon", async (path, options) => {
|
||||
let error = {};
|
||||
|
||||
if (options) {
|
||||
const nativeImage = await app
|
||||
.getFileIcon(path, options)
|
||||
.catch((errorFileIcon) => (error = errorFileIcon));
|
||||
|
||||
electronSocket.emit("appGetFileIconCompleted", [error, nativeImage]);
|
||||
} else {
|
||||
const nativeImage = await app
|
||||
.getFileIcon(path)
|
||||
.catch((errorFileIcon) => (error = errorFileIcon));
|
||||
|
||||
electronSocket.emit("appGetFileIconCompleted", [error, nativeImage]);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("appSetPath", (name, path) => {
|
||||
app.setPath(name, path);
|
||||
});
|
||||
|
||||
socket.on("appGetVersion", () => {
|
||||
const version = app.getVersion();
|
||||
electronSocket.emit("appGetVersionCompleted", version);
|
||||
});
|
||||
|
||||
socket.on("appGetName", () => {
|
||||
electronSocket.emit("appGetNameCompleted", app.name);
|
||||
});
|
||||
|
||||
socket.on("appSetName", (name) => {
|
||||
app.name = name;
|
||||
});
|
||||
|
||||
socket.on("appGetLocale", () => {
|
||||
const locale = app.getLocale();
|
||||
electronSocket.emit("appGetLocaleCompleted", locale);
|
||||
});
|
||||
|
||||
socket.on("appAddRecentDocument", (path) => {
|
||||
app.addRecentDocument(path);
|
||||
});
|
||||
|
||||
socket.on("appClearRecentDocuments", () => {
|
||||
app.clearRecentDocuments();
|
||||
});
|
||||
|
||||
socket.on("appSetAsDefaultProtocolClient", (protocol, path, args) => {
|
||||
const success = app.setAsDefaultProtocolClient(protocol, path, args);
|
||||
electronSocket.emit("appSetAsDefaultProtocolClientCompleted", success);
|
||||
});
|
||||
|
||||
socket.on("appRemoveAsDefaultProtocolClient", (protocol, path, args) => {
|
||||
const success = app.removeAsDefaultProtocolClient(protocol, path, args);
|
||||
electronSocket.emit("appRemoveAsDefaultProtocolClientCompleted", success);
|
||||
});
|
||||
|
||||
socket.on("appIsDefaultProtocolClient", (protocol, path, args) => {
|
||||
const success = app.isDefaultProtocolClient(protocol, path, args);
|
||||
electronSocket.emit("appIsDefaultProtocolClientCompleted", success);
|
||||
});
|
||||
|
||||
socket.on("appSetUserTasks", (tasks) => {
|
||||
const success = app.setUserTasks(tasks);
|
||||
electronSocket.emit("appSetUserTasksCompleted", success);
|
||||
});
|
||||
|
||||
socket.on("appGetJumpListSettings", () => {
|
||||
const jumpListSettings = app.getJumpListSettings();
|
||||
electronSocket.emit("appGetJumpListSettingsCompleted", jumpListSettings);
|
||||
});
|
||||
|
||||
socket.on("appSetJumpList", (categories) => {
|
||||
app.setJumpList(categories);
|
||||
});
|
||||
|
||||
socket.on("appRequestSingleInstanceLock", () => {
|
||||
const success = app.requestSingleInstanceLock();
|
||||
electronSocket.emit("appRequestSingleInstanceLockCompleted", success);
|
||||
|
||||
app.on("second-instance", (event, args = [], workingDirectory = "") => {
|
||||
electronSocket.emit("secondInstance", [args, workingDirectory]);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-browser-window-created', (id) => {
|
||||
app.on('browser-window-created', () => {
|
||||
electronSocket.emit('app-browser-window-created' + id);
|
||||
});
|
||||
socket.on("appHasSingleInstanceLock", () => {
|
||||
const hasLock = app.hasSingleInstanceLock();
|
||||
|
||||
electronSocket.emit("appHasSingleInstanceLockCompleted", hasLock);
|
||||
});
|
||||
|
||||
socket.on("appReleaseSingleInstanceLock", () => {
|
||||
app.releaseSingleInstanceLock();
|
||||
});
|
||||
|
||||
socket.on("appSetUserActivity", (type, userInfo, webpageUrl) => {
|
||||
app.setUserActivity(type, userInfo, webpageUrl);
|
||||
});
|
||||
|
||||
socket.on("appGetCurrentActivityType", () => {
|
||||
const activityType = app.getCurrentActivityType();
|
||||
electronSocket.emit("appGetCurrentActivityTypeCompleted", activityType);
|
||||
});
|
||||
|
||||
socket.on("appInvalidateCurrentActivity", () => {
|
||||
app.invalidateCurrentActivity();
|
||||
});
|
||||
|
||||
socket.on("appResignCurrentActivity", () => {
|
||||
app.resignCurrentActivity();
|
||||
});
|
||||
|
||||
socket.on("appSetAppUserModelId", (id) => {
|
||||
app.setAppUserModelId(id);
|
||||
});
|
||||
|
||||
socket.on("appImportCertificate", (options) => {
|
||||
app.importCertificate(options, (result) => {
|
||||
electronSocket.emit("appImportCertificateCompleted", result);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-web-contents-created', (id) => {
|
||||
app.on('web-contents-created', () => {
|
||||
electronSocket.emit('app-web-contents-created' + id);
|
||||
});
|
||||
socket.on("appGetAppMetrics", () => {
|
||||
const processMetrics = app.getAppMetrics();
|
||||
electronSocket.emit("appGetAppMetricsCompleted", processMetrics);
|
||||
});
|
||||
|
||||
socket.on("appGetGpuFeatureStatus", () => {
|
||||
const gpuFeatureStatus = app.getGPUFeatureStatus();
|
||||
electronSocket.emit("appGetGpuFeatureStatusCompleted", gpuFeatureStatus);
|
||||
});
|
||||
|
||||
socket.on("appSetBadgeCount", (count) => {
|
||||
const success = app.setBadgeCount(count);
|
||||
electronSocket.emit("appSetBadgeCountCompleted", success);
|
||||
});
|
||||
|
||||
socket.on("appGetBadgeCount", () => {
|
||||
const count = app.getBadgeCount();
|
||||
electronSocket.emit("appGetBadgeCountCompleted", count);
|
||||
});
|
||||
|
||||
socket.on("appIsUnityRunning", () => {
|
||||
const isUnityRunning = app.isUnityRunning();
|
||||
electronSocket.emit("appIsUnityRunningCompleted", isUnityRunning);
|
||||
});
|
||||
|
||||
socket.on("appGetLoginItemSettings", (options) => {
|
||||
const loginItemSettings = app.getLoginItemSettings(
|
||||
options as LoginItemSettingsOptions,
|
||||
);
|
||||
electronSocket.emit("appGetLoginItemSettingsCompleted", loginItemSettings);
|
||||
});
|
||||
|
||||
socket.on("appSetLoginItemSettings", (settings) => {
|
||||
app.setLoginItemSettings(settings as Settings);
|
||||
});
|
||||
|
||||
socket.on("appIsAccessibilitySupportEnabled", () => {
|
||||
const isAccessibilitySupportEnabled = app.isAccessibilitySupportEnabled();
|
||||
electronSocket.emit(
|
||||
"appIsAccessibilitySupportEnabledCompleted",
|
||||
isAccessibilitySupportEnabled,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("appSetAccessibilitySupportEnabled", (enabled) => {
|
||||
app.setAccessibilitySupportEnabled(enabled);
|
||||
});
|
||||
|
||||
socket.on("appShowAboutPanel", () => {
|
||||
app.showAboutPanel();
|
||||
});
|
||||
|
||||
socket.on("appSetAboutPanelOptions", (options) => {
|
||||
app.setAboutPanelOptions(options as AboutPanelOptionsOptions);
|
||||
});
|
||||
|
||||
socket.on("appGetUserAgentFallback", () => {
|
||||
electronSocket.emit(
|
||||
"appGetUserAgentFallbackCompleted",
|
||||
app.userAgentFallback,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("appSetUserAgentFallback", (userAgent) => {
|
||||
app.userAgentFallback = userAgent;
|
||||
});
|
||||
|
||||
socket.on("register-app-on-event", (eventName, listenerName) => {
|
||||
app.on(eventName, (...args) => {
|
||||
if (args.length > 1) {
|
||||
electronSocket.emit(listenerName, args[1]);
|
||||
} else {
|
||||
electronSocket.emit(listenerName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-accessibility-support-changed', (id) => {
|
||||
app.on('accessibility-support-changed', (event, accessibilitySupportEnabled) => {
|
||||
electronSocket.emit('app-accessibility-support-changed' + id, accessibilitySupportEnabled);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('appQuit', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
socket.on('appExit', (exitCode = 0) => {
|
||||
app.exit(exitCode);
|
||||
});
|
||||
|
||||
socket.on('appRelaunch', (options) => {
|
||||
app.relaunch(options as RelaunchOptions);
|
||||
});
|
||||
|
||||
socket.on('appFocus', (options) => {
|
||||
app.focus(options);
|
||||
});
|
||||
|
||||
socket.on('appHide', () => {
|
||||
app.hide();
|
||||
});
|
||||
|
||||
socket.on('appShow', () => {
|
||||
app.show();
|
||||
});
|
||||
|
||||
socket.on('appGetAppPath', () => {
|
||||
const path = app.getAppPath();
|
||||
electronSocket.emit('appGetAppPathCompleted', path);
|
||||
});
|
||||
|
||||
socket.on('appSetAppLogsPath', (path) => {
|
||||
app.setAppLogsPath(path);
|
||||
});
|
||||
|
||||
socket.on('appGetPath', (name) => {
|
||||
const path = app.getPath(name);
|
||||
electronSocket.emit('appGetPathCompleted', path);
|
||||
});
|
||||
|
||||
socket.on('appGetFileIcon', async (path, options) => {
|
||||
let error = {};
|
||||
|
||||
if (options) {
|
||||
const nativeImage = await app.getFileIcon(path, options).catch((errorFileIcon) => error = errorFileIcon);
|
||||
|
||||
electronSocket.emit('appGetFileIconCompleted', [error, nativeImage]);
|
||||
} else {
|
||||
const nativeImage = await app.getFileIcon(path).catch((errorFileIcon) => error = errorFileIcon);
|
||||
|
||||
electronSocket.emit('appGetFileIconCompleted', [error, nativeImage]);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('appSetPath', (name, path) => {
|
||||
app.setPath(name, path);
|
||||
});
|
||||
|
||||
socket.on('appGetVersion', () => {
|
||||
const version = app.getVersion();
|
||||
electronSocket.emit('appGetVersionCompleted', version);
|
||||
});
|
||||
|
||||
socket.on('appGetName', () => {
|
||||
electronSocket.emit('appGetNameCompleted', app.name);
|
||||
});
|
||||
|
||||
socket.on('appSetName', (name) => {
|
||||
app.name = name;
|
||||
});
|
||||
|
||||
socket.on('appGetLocale', () => {
|
||||
const locale = app.getLocale();
|
||||
electronSocket.emit('appGetLocaleCompleted', locale);
|
||||
});
|
||||
|
||||
socket.on('appAddRecentDocument', (path) => {
|
||||
app.addRecentDocument(path);
|
||||
});
|
||||
|
||||
socket.on('appClearRecentDocuments', () => {
|
||||
app.clearRecentDocuments();
|
||||
});
|
||||
|
||||
socket.on('appSetAsDefaultProtocolClient', (protocol, path, args) => {
|
||||
const success = app.setAsDefaultProtocolClient(protocol, path, args);
|
||||
electronSocket.emit('appSetAsDefaultProtocolClientCompleted', success);
|
||||
});
|
||||
|
||||
socket.on('appRemoveAsDefaultProtocolClient', (protocol, path, args) => {
|
||||
const success = app.removeAsDefaultProtocolClient(protocol, path, args);
|
||||
electronSocket.emit('appRemoveAsDefaultProtocolClientCompleted', success);
|
||||
});
|
||||
|
||||
socket.on('appIsDefaultProtocolClient', (protocol, path, args) => {
|
||||
const success = app.isDefaultProtocolClient(protocol, path, args);
|
||||
electronSocket.emit('appIsDefaultProtocolClientCompleted', success);
|
||||
});
|
||||
|
||||
socket.on('appSetUserTasks', (tasks) => {
|
||||
const success = app.setUserTasks(tasks);
|
||||
electronSocket.emit('appSetUserTasksCompleted', success);
|
||||
});
|
||||
|
||||
socket.on('appGetJumpListSettings', () => {
|
||||
const jumpListSettings = app.getJumpListSettings();
|
||||
electronSocket.emit('appGetJumpListSettingsCompleted', jumpListSettings);
|
||||
});
|
||||
|
||||
socket.on('appSetJumpList', (categories) => {
|
||||
app.setJumpList(categories);
|
||||
});
|
||||
|
||||
socket.on('appRequestSingleInstanceLock', () => {
|
||||
const success = app.requestSingleInstanceLock();
|
||||
electronSocket.emit('appRequestSingleInstanceLockCompleted', success);
|
||||
|
||||
app.on('second-instance', (event, args = [], workingDirectory = '') => {
|
||||
electronSocket.emit('secondInstance', [args, workingDirectory]);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('appHasSingleInstanceLock', () => {
|
||||
const hasLock = app.hasSingleInstanceLock();
|
||||
|
||||
electronSocket.emit('appHasSingleInstanceLockCompleted', hasLock);
|
||||
});
|
||||
|
||||
socket.on('appReleaseSingleInstanceLock', () => {
|
||||
app.releaseSingleInstanceLock();
|
||||
});
|
||||
|
||||
socket.on('appSetUserActivity', (type, userInfo, webpageUrl) => {
|
||||
app.setUserActivity(type, userInfo, webpageUrl);
|
||||
});
|
||||
|
||||
socket.on('appGetCurrentActivityType', () => {
|
||||
const activityType = app.getCurrentActivityType();
|
||||
electronSocket.emit('appGetCurrentActivityTypeCompleted', activityType);
|
||||
});
|
||||
|
||||
socket.on('appInvalidateCurrentActivity', () => {
|
||||
app.invalidateCurrentActivity();
|
||||
});
|
||||
|
||||
socket.on('appResignCurrentActivity', () => {
|
||||
app.resignCurrentActivity();
|
||||
});
|
||||
|
||||
socket.on('appSetAppUserModelId', (id) => {
|
||||
app.setAppUserModelId(id);
|
||||
});
|
||||
|
||||
socket.on('appImportCertificate', (options) => {
|
||||
app.importCertificate(options, (result) => {
|
||||
electronSocket.emit('appImportCertificateCompleted', result);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('appGetAppMetrics', () => {
|
||||
const processMetrics = app.getAppMetrics();
|
||||
electronSocket.emit('appGetAppMetricsCompleted', processMetrics);
|
||||
});
|
||||
|
||||
socket.on('appGetGpuFeatureStatus', () => {
|
||||
const gpuFeatureStatus = app.getGPUFeatureStatus();
|
||||
electronSocket.emit('appGetGpuFeatureStatusCompleted', gpuFeatureStatus);
|
||||
});
|
||||
|
||||
socket.on('appSetBadgeCount', (count) => {
|
||||
const success = app.setBadgeCount(count);
|
||||
electronSocket.emit('appSetBadgeCountCompleted', success);
|
||||
});
|
||||
|
||||
socket.on('appGetBadgeCount', () => {
|
||||
const count = app.getBadgeCount();
|
||||
electronSocket.emit('appGetBadgeCountCompleted', count);
|
||||
});
|
||||
|
||||
socket.on('appIsUnityRunning', () => {
|
||||
const isUnityRunning = app.isUnityRunning();
|
||||
electronSocket.emit('appIsUnityRunningCompleted', isUnityRunning);
|
||||
});
|
||||
|
||||
socket.on('appGetLoginItemSettings', (options) => {
|
||||
const loginItemSettings = app.getLoginItemSettings(options as LoginItemSettingsOptions);
|
||||
electronSocket.emit('appGetLoginItemSettingsCompleted', loginItemSettings);
|
||||
});
|
||||
|
||||
socket.on('appSetLoginItemSettings', (settings) => {
|
||||
app.setLoginItemSettings(settings as Settings);
|
||||
});
|
||||
|
||||
socket.on('appIsAccessibilitySupportEnabled', () => {
|
||||
const isAccessibilitySupportEnabled = app.isAccessibilitySupportEnabled();
|
||||
electronSocket.emit('appIsAccessibilitySupportEnabledCompleted', isAccessibilitySupportEnabled);
|
||||
});
|
||||
|
||||
socket.on('appSetAccessibilitySupportEnabled', (enabled) => {
|
||||
app.setAccessibilitySupportEnabled(enabled);
|
||||
});
|
||||
|
||||
socket.on('appShowAboutPanel', () => {
|
||||
app.showAboutPanel();
|
||||
});
|
||||
|
||||
socket.on('appSetAboutPanelOptions', (options) => {
|
||||
app.setAboutPanelOptions(options as AboutPanelOptionsOptions);
|
||||
});
|
||||
|
||||
socket.on('appGetUserAgentFallback', () => {
|
||||
electronSocket.emit('appGetUserAgentFallbackCompleted', app.userAgentFallback);
|
||||
});
|
||||
|
||||
socket.on('appSetUserAgentFallback', (userAgent) => {
|
||||
app.userAgentFallback = userAgent;
|
||||
});
|
||||
|
||||
socket.on('register-app-on-event', (eventName, listenerName) => {
|
||||
app.on(eventName, (...args) => {
|
||||
if (args.length > 1) {
|
||||
electronSocket.emit(listenerName, args[1]);
|
||||
} else {
|
||||
electronSocket.emit(listenerName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-app-once-event', (eventName, listenerName) => {
|
||||
app.once(eventName, (...args) => {
|
||||
if (args.length > 1) {
|
||||
electronSocket.emit(listenerName, args[1]);
|
||||
} else {
|
||||
electronSocket.emit(listenerName);
|
||||
}
|
||||
});
|
||||
socket.on("register-app-once-event", (eventName, listenerName) => {
|
||||
app.once(eventName, (...args) => {
|
||||
if (args.length > 1) {
|
||||
electronSocket.emit(listenerName, args[1]);
|
||||
} else {
|
||||
electronSocket.emit(listenerName);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,143 +1,192 @@
|
||||
import { Socket } from 'net';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { Socket } from "net";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
electronSocket = socket;
|
||||
|
||||
socket.on('register-autoUpdater-error', (id) => {
|
||||
autoUpdater.on('error', (error) => {
|
||||
electronSocket.emit('autoUpdater-error' + id, error.message);
|
||||
});
|
||||
socket.on("register-autoUpdater-error", (id) => {
|
||||
autoUpdater.on("error", (error) => {
|
||||
electronSocket.emit("autoUpdater-error" + id, error.message);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-autoUpdater-checking-for-update', (id) => {
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
electronSocket.emit('autoUpdater-checking-for-update' + id);
|
||||
});
|
||||
socket.on("register-autoUpdater-checking-for-update", (id) => {
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
electronSocket.emit("autoUpdater-checking-for-update" + id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-autoUpdater-update-available', (id) => {
|
||||
autoUpdater.on('update-available', (updateInfo) => {
|
||||
electronSocket.emit('autoUpdater-update-available' + id, updateInfo);
|
||||
});
|
||||
socket.on("register-autoUpdater-update-available", (id) => {
|
||||
autoUpdater.on("update-available", (updateInfo) => {
|
||||
electronSocket.emit("autoUpdater-update-available" + id, updateInfo);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-autoUpdater-update-not-available', (id) => {
|
||||
autoUpdater.on('update-not-available', (updateInfo) => {
|
||||
electronSocket.emit('autoUpdater-update-not-available' + id, updateInfo);
|
||||
});
|
||||
socket.on("register-autoUpdater-update-not-available", (id) => {
|
||||
autoUpdater.on("update-not-available", (updateInfo) => {
|
||||
electronSocket.emit("autoUpdater-update-not-available" + id, updateInfo);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-autoUpdater-download-progress', (id) => {
|
||||
autoUpdater.on('download-progress', (progressInfo) => {
|
||||
electronSocket.emit('autoUpdater-download-progress' + id, progressInfo);
|
||||
});
|
||||
socket.on("register-autoUpdater-download-progress", (id) => {
|
||||
autoUpdater.on("download-progress", (progressInfo) => {
|
||||
electronSocket.emit("autoUpdater-download-progress" + id, progressInfo);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-autoUpdater-update-downloaded', (id) => {
|
||||
autoUpdater.on('update-downloaded', (updateInfo) => {
|
||||
electronSocket.emit('autoUpdater-update-downloaded' + id, updateInfo);
|
||||
});
|
||||
socket.on("register-autoUpdater-update-downloaded", (id) => {
|
||||
autoUpdater.on("update-downloaded", (updateInfo) => {
|
||||
electronSocket.emit("autoUpdater-update-downloaded" + id, updateInfo);
|
||||
});
|
||||
});
|
||||
|
||||
// Properties *****
|
||||
// Properties *****
|
||||
|
||||
socket.on('autoUpdater-autoDownload', () => {
|
||||
electronSocket.emit('autoUpdater-autoDownload-completed', autoUpdater.autoDownload);
|
||||
});
|
||||
socket.on("autoUpdater-autoDownload", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-autoDownload-completed",
|
||||
autoUpdater.autoDownload,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-autoDownload-set', (value) => {
|
||||
autoUpdater.autoDownload = value;
|
||||
});
|
||||
socket.on("autoUpdater-autoDownload-set", (value) => {
|
||||
autoUpdater.autoDownload = value;
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-autoInstallOnAppQuit', () => {
|
||||
electronSocket.emit('autoUpdater-autoInstallOnAppQuit-completed', autoUpdater.autoInstallOnAppQuit);
|
||||
});
|
||||
socket.on("autoUpdater-autoInstallOnAppQuit", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-autoInstallOnAppQuit-completed",
|
||||
autoUpdater.autoInstallOnAppQuit,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-autoInstallOnAppQuit-set', (value) => {
|
||||
autoUpdater.autoInstallOnAppQuit = value;
|
||||
});
|
||||
socket.on("autoUpdater-autoInstallOnAppQuit-set", (value) => {
|
||||
autoUpdater.autoInstallOnAppQuit = value;
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-allowPrerelease', () => {
|
||||
electronSocket.emit('autoUpdater-allowPrerelease-completed', autoUpdater.allowPrerelease);
|
||||
});
|
||||
socket.on("autoUpdater-allowPrerelease", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-allowPrerelease-completed",
|
||||
autoUpdater.allowPrerelease,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-allowPrerelease-set', (value) => {
|
||||
autoUpdater.allowPrerelease = value;
|
||||
});
|
||||
socket.on("autoUpdater-allowPrerelease-set", (value) => {
|
||||
autoUpdater.allowPrerelease = value;
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-fullChangelog', () => {
|
||||
electronSocket.emit('autoUpdater-fullChangelog-completed', autoUpdater.fullChangelog);
|
||||
});
|
||||
socket.on("autoUpdater-fullChangelog", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-fullChangelog-completed",
|
||||
autoUpdater.fullChangelog,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-fullChangelog-set', (value) => {
|
||||
autoUpdater.fullChangelog = value;
|
||||
});
|
||||
socket.on("autoUpdater-fullChangelog-set", (value) => {
|
||||
autoUpdater.fullChangelog = value;
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-allowDowngrade', () => {
|
||||
electronSocket.emit('autoUpdater-allowDowngrade-completed', autoUpdater.allowDowngrade);
|
||||
});
|
||||
socket.on("autoUpdater-allowDowngrade", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-allowDowngrade-completed",
|
||||
autoUpdater.allowDowngrade,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-allowDowngrade-set', (value) => {
|
||||
autoUpdater.allowDowngrade = value;
|
||||
});
|
||||
socket.on("autoUpdater-allowDowngrade-set", (value) => {
|
||||
autoUpdater.allowDowngrade = value;
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-updateConfigPath', () => {
|
||||
electronSocket.emit('autoUpdater-updateConfigPath-completed', autoUpdater.updateConfigPath || '');
|
||||
});
|
||||
socket.on("autoUpdater-updateConfigPath", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-updateConfigPath-completed",
|
||||
autoUpdater.updateConfigPath || "",
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-updateConfigPath-set', (value) => {
|
||||
autoUpdater.updateConfigPath = value;
|
||||
});
|
||||
socket.on("autoUpdater-updateConfigPath-set", (value) => {
|
||||
autoUpdater.updateConfigPath = value;
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-currentVersion', () => {
|
||||
electronSocket.emit('autoUpdater-currentVersion-completed', autoUpdater.currentVersion);
|
||||
});
|
||||
socket.on("autoUpdater-currentVersion", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-currentVersion-completed",
|
||||
autoUpdater.currentVersion,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-channel', () => {
|
||||
electronSocket.emit('autoUpdater-channel-completed', autoUpdater.channel || '');
|
||||
});
|
||||
socket.on("autoUpdater-channel", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-channel-completed",
|
||||
autoUpdater.channel || "",
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-channel-set', (value) => {
|
||||
autoUpdater.channel = value;
|
||||
});
|
||||
socket.on("autoUpdater-channel-set", (value) => {
|
||||
autoUpdater.channel = value;
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-requestHeaders', () => {
|
||||
electronSocket.emit('autoUpdater-requestHeaders-completed', autoUpdater.requestHeaders);
|
||||
});
|
||||
socket.on("autoUpdater-requestHeaders", () => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-requestHeaders-completed",
|
||||
autoUpdater.requestHeaders,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-requestHeaders-set', (value) => {
|
||||
autoUpdater.requestHeaders = value;
|
||||
});
|
||||
socket.on("autoUpdater-requestHeaders-set", (value) => {
|
||||
autoUpdater.requestHeaders = value;
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-checkForUpdatesAndNotify', async (guid) => {
|
||||
autoUpdater.checkForUpdatesAndNotify().then((updateCheckResult) => {
|
||||
electronSocket.emit('autoUpdater-checkForUpdatesAndNotify-completed' + guid, updateCheckResult);
|
||||
}).catch((error) => {
|
||||
electronSocket.emit('autoUpdater-checkForUpdatesAndNotifyError' + guid, error);
|
||||
});
|
||||
});
|
||||
socket.on("autoUpdater-checkForUpdatesAndNotify", async (guid) => {
|
||||
autoUpdater
|
||||
.checkForUpdatesAndNotify()
|
||||
.then((updateCheckResult) => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-checkForUpdatesAndNotify-completed" + guid,
|
||||
updateCheckResult,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-checkForUpdatesAndNotifyError" + guid,
|
||||
error,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-checkForUpdates', async (guid) => {
|
||||
autoUpdater.checkForUpdates().then((updateCheckResult) => {
|
||||
electronSocket.emit('autoUpdater-checkForUpdates-completed' + guid, updateCheckResult);
|
||||
}).catch((error) => {
|
||||
electronSocket.emit('autoUpdater-checkForUpdatesError' + guid, error);
|
||||
});
|
||||
});
|
||||
socket.on("autoUpdater-checkForUpdates", async (guid) => {
|
||||
autoUpdater
|
||||
.checkForUpdates()
|
||||
.then((updateCheckResult) => {
|
||||
electronSocket.emit(
|
||||
"autoUpdater-checkForUpdates-completed" + guid,
|
||||
updateCheckResult,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
electronSocket.emit("autoUpdater-checkForUpdatesError" + guid, error);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-quitAndInstall', async (isSilent, isForceRunAfter) => {
|
||||
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
|
||||
});
|
||||
socket.on("autoUpdater-quitAndInstall", async (isSilent, isForceRunAfter) => {
|
||||
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-downloadUpdate', async (guid) => {
|
||||
const downloadedPath = await autoUpdater.downloadUpdate();
|
||||
electronSocket.emit('autoUpdater-downloadUpdate-completed' + guid, downloadedPath);
|
||||
});
|
||||
socket.on("autoUpdater-downloadUpdate", async (guid) => {
|
||||
const downloadedPath = await autoUpdater.downloadUpdate();
|
||||
electronSocket.emit(
|
||||
"autoUpdater-downloadUpdate-completed" + guid,
|
||||
downloadedPath,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('autoUpdater-getFeedURL', async (guid) => {
|
||||
const feedUrl = await autoUpdater.getFeedURL();
|
||||
electronSocket.emit('autoUpdater-getFeedURL-completed' + guid, feedUrl || '');
|
||||
});
|
||||
socket.on("autoUpdater-getFeedURL", async (guid) => {
|
||||
const feedUrl = await autoUpdater.getFeedURL();
|
||||
electronSocket.emit(
|
||||
"autoUpdater-getFeedURL-completed" + guid,
|
||||
feedUrl || "",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,74 +1,82 @@
|
||||
import { Socket } from 'net';
|
||||
import { BrowserView } from 'electron';
|
||||
const browserViews: BrowserView[] = (global['browserViews'] = global['browserViews'] || []) as BrowserView[];
|
||||
import { Socket } from "net";
|
||||
import { BrowserView } from "electron";
|
||||
|
||||
const browserViews: BrowserView[] = (global["browserViews"] =
|
||||
global["browserViews"] || []) as BrowserView[];
|
||||
const proxyToCredentialsMap: { [proxy: string]: string } = (global[
|
||||
"proxyToCredentialsMap"
|
||||
] = global["proxyToCredentialsMap"] || []) as { [proxy: string]: string };
|
||||
|
||||
let browserView: BrowserView, electronSocket;
|
||||
const proxyToCredentialsMap: { [proxy: string]: string } = (global['proxyToCredentialsMap'] = global['proxyToCredentialsMap'] || []) as { [proxy: string]: string };
|
||||
|
||||
const browserViewApi = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
electronSocket = socket;
|
||||
|
||||
socket.on('createBrowserView', (options) => {
|
||||
if (!hasOwnChildreen(options, 'webPreferences', 'nodeIntegration')) {
|
||||
options = { ...options, webPreferences: { nodeIntegration: true, contextIsolation: false } };
|
||||
}
|
||||
|
||||
browserView = new BrowserView(options);
|
||||
browserView['id'] = browserViews.length + 1;
|
||||
|
||||
if (options.proxy) {
|
||||
browserView.webContents.session.setProxy({proxyRules: options.proxy});
|
||||
}
|
||||
|
||||
if (options.proxy && options.proxyCredentials) {
|
||||
proxyToCredentialsMap[options.proxy] = options.proxyCredentials;
|
||||
}
|
||||
|
||||
browserViews.push(browserView);
|
||||
|
||||
electronSocket.emit('BrowserViewCreated', browserView['id']);
|
||||
});
|
||||
|
||||
socket.on('browserView-bounds', (id) => {
|
||||
const bounds = getBrowserViewById(id).getBounds();
|
||||
|
||||
electronSocket.emit('browserView-bounds-completed', bounds);
|
||||
});
|
||||
|
||||
socket.on('browserView-bounds-set', (id, bounds) => {
|
||||
getBrowserViewById(id).setBounds(bounds);
|
||||
});
|
||||
|
||||
socket.on('browserView-setAutoResize', (id, options) => {
|
||||
getBrowserViewById(id).setAutoResize(options);
|
||||
});
|
||||
|
||||
socket.on('browserView-setBackgroundColor', (id, color) => {
|
||||
getBrowserViewById(id).setBackgroundColor(color);
|
||||
});
|
||||
|
||||
function hasOwnChildreen(obj, ...childNames) {
|
||||
for (let i = 0; i < childNames.length; i++) {
|
||||
if (!obj || !obj.hasOwnProperty(childNames[i])) {
|
||||
return false;
|
||||
}
|
||||
obj = obj[childNames[i]];
|
||||
}
|
||||
|
||||
return true;
|
||||
socket.on("createBrowserView", (options) => {
|
||||
if (!hasOwnChildreen(options, "webPreferences", "nodeIntegration")) {
|
||||
options = {
|
||||
...options,
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false },
|
||||
};
|
||||
}
|
||||
|
||||
browserView = new BrowserView(options);
|
||||
browserView["id"] = browserViews.length + 1;
|
||||
|
||||
if (options.proxy) {
|
||||
browserView.webContents.session.setProxy({ proxyRules: options.proxy });
|
||||
}
|
||||
|
||||
if (options.proxy && options.proxyCredentials) {
|
||||
proxyToCredentialsMap[options.proxy] = options.proxyCredentials;
|
||||
}
|
||||
|
||||
browserViews.push(browserView);
|
||||
|
||||
electronSocket.emit("BrowserViewCreated", browserView["id"]);
|
||||
});
|
||||
|
||||
socket.on("browserView-bounds", (id) => {
|
||||
const bounds = getBrowserViewById(id).getBounds();
|
||||
|
||||
electronSocket.emit("browserView-bounds-completed", bounds);
|
||||
});
|
||||
|
||||
socket.on("browserView-bounds-set", (id, bounds) => {
|
||||
getBrowserViewById(id).setBounds(bounds);
|
||||
});
|
||||
|
||||
socket.on("browserView-setAutoResize", (id, options) => {
|
||||
getBrowserViewById(id).setAutoResize(options);
|
||||
});
|
||||
|
||||
socket.on("browserView-setBackgroundColor", (id, color) => {
|
||||
getBrowserViewById(id).setBackgroundColor(color);
|
||||
});
|
||||
|
||||
function hasOwnChildreen(obj, ...childNames) {
|
||||
for (let i = 0; i < childNames.length; i++) {
|
||||
if (!obj || !obj.hasOwnProperty(childNames[i])) {
|
||||
return false;
|
||||
}
|
||||
obj = obj[childNames[i]];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const browserViewMediateService = (browserViewId: number): BrowserView => {
|
||||
return getBrowserViewById(browserViewId);
|
||||
return getBrowserViewById(browserViewId);
|
||||
};
|
||||
|
||||
function getBrowserViewById(id: number) {
|
||||
for (let index = 0; index < browserViews.length; index++) {
|
||||
const browserViewItem = browserViews[index];
|
||||
if (browserViewItem['id'] === id) {
|
||||
return browserViewItem;
|
||||
}
|
||||
for (let index = 0; index < browserViews.length; index++) {
|
||||
const browserViewItem = browserViews[index];
|
||||
if (browserViewItem["id"] === id) {
|
||||
return browserViewItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { browserViewApi, browserViewMediateService };
|
||||
|
||||
@@ -243,7 +243,13 @@ module.exports = (socket, app) => {
|
||||
}
|
||||
});
|
||||
if (loadUrl) {
|
||||
window.loadURL(loadUrl);
|
||||
// Append authentication token to initial URL if available
|
||||
let urlToLoad = loadUrl;
|
||||
if (global.authToken) {
|
||||
const separator = loadUrl.includes('?') ? '&' : '?';
|
||||
urlToLoad = `${loadUrl}${separator}token=${global.authToken}`;
|
||||
}
|
||||
window.loadURL(urlToLoad);
|
||||
}
|
||||
if (app.commandLine.hasSwitch("clear-cache") &&
|
||||
app.commandLine.getSwitchValue("clear-cache")) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,12 +1,15 @@
|
||||
import * as path from "path";
|
||||
import { Socket } from "net";
|
||||
import { BrowserWindow, Menu, nativeImage } from "electron";
|
||||
import { BrowserWindow, Menu } from "electron";
|
||||
import { browserViewMediateService } from "./browserView";
|
||||
const path = require("path");
|
||||
|
||||
const windows: Electron.BrowserWindow[] = (global["browserWindows"] =
|
||||
global["browserWindows"] || []) as Electron.BrowserWindow[];
|
||||
|
||||
let readyToShowWindowsIds: number[] = [];
|
||||
let window, lastOptions, electronSocket;
|
||||
let mainWindowURL;
|
||||
|
||||
const proxyToCredentialsMap: { [proxy: string]: string } = (global[
|
||||
"proxyToCredentialsMap"
|
||||
] = global["proxyToCredentialsMap"] || []) as { [proxy: string]: string };
|
||||
@@ -32,7 +35,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
socket.on("register-browserWindow-ready-to-show", (id) => {
|
||||
if (readyToShowWindowsIds.includes(id)) {
|
||||
readyToShowWindowsIds = readyToShowWindowsIds.filter(
|
||||
(value) => value !== id
|
||||
(value) => value !== id,
|
||||
);
|
||||
electronSocket.emit("browserWindow-ready-to-show" + id);
|
||||
}
|
||||
@@ -141,7 +144,11 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
|
||||
socket.on("register-browserWindow-bounds-changed", (id) => {
|
||||
const window = getWindowById(id);
|
||||
const cb = () => electronSocket.emit("browserWindow-bounds-changed" + id, window.getBounds());
|
||||
const cb = () =>
|
||||
electronSocket.emit(
|
||||
"browserWindow-bounds-changed" + id,
|
||||
window.getBounds(),
|
||||
);
|
||||
window.on("resize", cb);
|
||||
window.on("move", cb);
|
||||
});
|
||||
@@ -231,7 +238,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
__dirname,
|
||||
"..",
|
||||
"scripts",
|
||||
"blazor-preload.js"
|
||||
"blazor-preload.js",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,7 +271,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
window.on("ready-to-show", () => {
|
||||
if (readyToShowWindowsIds.includes(window.id)) {
|
||||
readyToShowWindowsIds = readyToShowWindowsIds.filter(
|
||||
(value) => value !== window.id
|
||||
(value) => value !== window.id,
|
||||
);
|
||||
} else {
|
||||
readyToShowWindowsIds.push(window.id);
|
||||
@@ -299,7 +306,13 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
});
|
||||
|
||||
if (loadUrl) {
|
||||
window.loadURL(loadUrl);
|
||||
// Append authentication token to initial URL if available
|
||||
let urlToLoad = loadUrl;
|
||||
if ((global as any).authToken) {
|
||||
const separator = loadUrl.includes("?") ? "&" : "?";
|
||||
urlToLoad = `${loadUrl}${separator}token=${(global as any).authToken}`;
|
||||
}
|
||||
window.loadURL(urlToLoad);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -531,7 +544,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"browserWindow-isFullScreenable-completed",
|
||||
fullscreenable
|
||||
fullscreenable,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -616,7 +629,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
.toString(16);
|
||||
electronSocket.emit(
|
||||
"browserWindow-getNativeWindowHandle-completed",
|
||||
nativeWindowHandle
|
||||
nativeWindowHandle,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -629,7 +642,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"setRepresentedFilename failed (likely unsupported platform):",
|
||||
e
|
||||
e,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -644,12 +657,12 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"getRepresentedFilename failed (likely unsupported platform):",
|
||||
e
|
||||
e,
|
||||
);
|
||||
}
|
||||
electronSocket.emit(
|
||||
"browserWindow-getRepresentedFilename-completed",
|
||||
pathname
|
||||
pathname,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -741,7 +754,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
imagePath = path.join(
|
||||
__dirname.replace("api", ""),
|
||||
"bin",
|
||||
originalIconPath
|
||||
originalIconPath,
|
||||
);
|
||||
}
|
||||
const { nativeImage } = require("electron");
|
||||
@@ -758,7 +771,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
|
||||
const success = getWindowById(id).setThumbarButtons(thumbarButtons);
|
||||
electronSocket.emit("browserWindowSetThumbarButtons-completed", success);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on("browserWindowSetThumbnailClip", (id, rectangle) => {
|
||||
@@ -786,7 +799,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"browserWindow-isMenuBarAutoHide-completed",
|
||||
isMenuBarAutoHide
|
||||
isMenuBarAutoHide,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -799,7 +812,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"browserWindow-isMenuBarVisible-completed",
|
||||
isMenuBarVisible
|
||||
isMenuBarVisible,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -813,7 +826,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"browserWindow-isVisibleOnAllWorkspaces-completed",
|
||||
isVisibleOnAllWorkspaces
|
||||
isVisibleOnAllWorkspaces,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -845,7 +858,7 @@ export = (socket: Socket, app: Electron.App) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"browserWindow-getParentWindow-completed",
|
||||
browserWindow.id
|
||||
browserWindow.id,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,84 +1,87 @@
|
||||
import { Socket } from 'net';
|
||||
import { clipboard, nativeImage } from 'electron';
|
||||
import { Socket } from "net";
|
||||
import { clipboard, nativeImage } from "electron";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
socket.on('clipboard-readText', (type) => {
|
||||
const text = clipboard.readText(type);
|
||||
electronSocket.emit('clipboard-readText-completed', text);
|
||||
electronSocket = socket;
|
||||
socket.on("clipboard-readText", (type) => {
|
||||
const text = clipboard.readText(type);
|
||||
electronSocket.emit("clipboard-readText-completed", text);
|
||||
});
|
||||
|
||||
socket.on("clipboard-writeText", (text, type) => {
|
||||
clipboard.writeText(text, type);
|
||||
});
|
||||
|
||||
socket.on("clipboard-readHTML", (type) => {
|
||||
const content = clipboard.readHTML(type);
|
||||
electronSocket.emit("clipboard-readHTML-completed", content);
|
||||
});
|
||||
|
||||
socket.on("clipboard-writeHTML", (markup, type) => {
|
||||
clipboard.writeHTML(markup, type);
|
||||
});
|
||||
|
||||
socket.on("clipboard-readRTF", (type) => {
|
||||
const content = clipboard.readRTF(type);
|
||||
electronSocket.emit("clipboard-readRTF-completed", content);
|
||||
});
|
||||
|
||||
socket.on("clipboard-writeRTF", (text, type) => {
|
||||
clipboard.writeHTML(text, type);
|
||||
});
|
||||
|
||||
socket.on("clipboard-readBookmark", () => {
|
||||
const bookmark = clipboard.readBookmark();
|
||||
electronSocket.emit("clipboard-readBookmark-completed", bookmark);
|
||||
});
|
||||
|
||||
socket.on("clipboard-writeBookmark", (title, url, type) => {
|
||||
clipboard.writeBookmark(title, url, type);
|
||||
});
|
||||
|
||||
socket.on("clipboard-readFindText", () => {
|
||||
const content = clipboard.readFindText();
|
||||
electronSocket.emit("clipboard-readFindText-completed", content);
|
||||
});
|
||||
|
||||
socket.on("clipboard-writeFindText", (text) => {
|
||||
clipboard.writeFindText(text);
|
||||
});
|
||||
|
||||
socket.on("clipboard-clear", (type) => {
|
||||
clipboard.clear(type);
|
||||
});
|
||||
|
||||
socket.on("clipboard-availableFormats", (type) => {
|
||||
const formats = clipboard.availableFormats(type);
|
||||
electronSocket.emit("clipboard-availableFormats-completed", formats);
|
||||
});
|
||||
|
||||
socket.on("clipboard-write", (data, type) => {
|
||||
clipboard.write(data, type);
|
||||
});
|
||||
|
||||
socket.on("clipboard-readImage", (type) => {
|
||||
const image = clipboard.readImage(type);
|
||||
electronSocket.emit("clipboard-readImage-completed", {
|
||||
1: image.toPNG().toString("base64"),
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('clipboard-writeText', (text, type) => {
|
||||
clipboard.writeText(text, type);
|
||||
});
|
||||
socket.on("clipboard-writeImage", (data, type) => {
|
||||
const dataContent = JSON.parse(data);
|
||||
const image = nativeImage.createEmpty();
|
||||
|
||||
socket.on('clipboard-readHTML', (type) => {
|
||||
const content = clipboard.readHTML(type);
|
||||
electronSocket.emit('clipboard-readHTML-completed', content);
|
||||
});
|
||||
// tslint:disable-next-line: forin
|
||||
for (const key in dataContent) {
|
||||
const scaleFactor = key;
|
||||
const bytes = data[key];
|
||||
const buffer = Buffer.from(bytes, "base64");
|
||||
image.addRepresentation({ scaleFactor: +scaleFactor, buffer: buffer });
|
||||
}
|
||||
|
||||
socket.on('clipboard-writeHTML', (markup, type) => {
|
||||
clipboard.writeHTML(markup, type);
|
||||
});
|
||||
|
||||
socket.on('clipboard-readRTF', (type) => {
|
||||
const content = clipboard.readRTF(type);
|
||||
electronSocket.emit('clipboard-readRTF-completed', content);
|
||||
});
|
||||
|
||||
socket.on('clipboard-writeRTF', (text, type) => {
|
||||
clipboard.writeHTML(text, type);
|
||||
});
|
||||
|
||||
socket.on('clipboard-readBookmark', () => {
|
||||
const bookmark = clipboard.readBookmark();
|
||||
electronSocket.emit('clipboard-readBookmark-completed', bookmark);
|
||||
});
|
||||
|
||||
socket.on('clipboard-writeBookmark', (title, url, type) => {
|
||||
clipboard.writeBookmark(title, url, type);
|
||||
});
|
||||
|
||||
socket.on('clipboard-readFindText', () => {
|
||||
const content = clipboard.readFindText();
|
||||
electronSocket.emit('clipboard-readFindText-completed', content);
|
||||
});
|
||||
|
||||
socket.on('clipboard-writeFindText', (text) => {
|
||||
clipboard.writeFindText(text);
|
||||
});
|
||||
|
||||
socket.on('clipboard-clear', (type) => {
|
||||
clipboard.clear(type);
|
||||
});
|
||||
|
||||
socket.on('clipboard-availableFormats', (type) => {
|
||||
const formats = clipboard.availableFormats(type);
|
||||
electronSocket.emit('clipboard-availableFormats-completed', formats);
|
||||
});
|
||||
|
||||
socket.on('clipboard-write', (data, type) => {
|
||||
clipboard.write(data, type);
|
||||
});
|
||||
|
||||
socket.on('clipboard-readImage', (type) => {
|
||||
const image = clipboard.readImage(type);
|
||||
electronSocket.emit('clipboard-readImage-completed', { 1: image.toPNG().toString('base64') });
|
||||
});
|
||||
|
||||
socket.on('clipboard-writeImage', (data, type) => {
|
||||
const dataContent = JSON.parse(data);
|
||||
const image = nativeImage.createEmpty();
|
||||
|
||||
// tslint:disable-next-line: forin
|
||||
for (const key in dataContent) {
|
||||
const scaleFactor = key;
|
||||
const bytes = data[key];
|
||||
const buffer = Buffer.from(bytes, 'base64');
|
||||
image.addRepresentation({ scaleFactor: +scaleFactor, buffer: buffer });
|
||||
}
|
||||
|
||||
clipboard.writeImage(image, type);
|
||||
});
|
||||
clipboard.writeImage(image, type);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { Socket } from 'net';
|
||||
import { Socket } from "net";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket, app: Electron.App) => {
|
||||
electronSocket = socket;
|
||||
electronSocket = socket;
|
||||
|
||||
socket.on('appCommandLineAppendSwitch', (the_switch: string, value: string) => {
|
||||
app.commandLine.appendSwitch(the_switch, value);
|
||||
});
|
||||
socket.on(
|
||||
"appCommandLineAppendSwitch",
|
||||
(the_switch: string, value: string) => {
|
||||
app.commandLine.appendSwitch(the_switch, value);
|
||||
},
|
||||
);
|
||||
|
||||
socket.on('appCommandLineAppendArgument', (value: string) => {
|
||||
app.commandLine.appendArgument(value);
|
||||
});
|
||||
socket.on("appCommandLineAppendArgument", (value: string) => {
|
||||
app.commandLine.appendArgument(value);
|
||||
});
|
||||
|
||||
socket.on('appCommandLineHasSwitch', (value: string) => {
|
||||
const hasSwitch = app.commandLine.hasSwitch(value);
|
||||
electronSocket.emit('appCommandLineHasSwitchCompleted', hasSwitch);
|
||||
});
|
||||
socket.on("appCommandLineHasSwitch", (value: string) => {
|
||||
const hasSwitch = app.commandLine.hasSwitch(value);
|
||||
electronSocket.emit("appCommandLineHasSwitchCompleted", hasSwitch);
|
||||
});
|
||||
|
||||
socket.on('appCommandLineGetSwitchValue', (the_switch: string) => {
|
||||
const value = app.commandLine.getSwitchValue(the_switch);
|
||||
electronSocket.emit('appCommandLineGetSwitchValueCompleted', value);
|
||||
});
|
||||
socket.on("appCommandLineGetSwitchValue", (the_switch: string) => {
|
||||
const value = app.commandLine.getSwitchValue(the_switch);
|
||||
electronSocket.emit("appCommandLineGetSwitchValueCompleted", value);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,45 +1,64 @@
|
||||
import { Socket } from 'net';
|
||||
import { BrowserWindow, dialog } from 'electron';
|
||||
import { Socket } from "net";
|
||||
import { BrowserWindow, dialog } from "electron";
|
||||
|
||||
let electronSocket: Socket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
socket.on('showMessageBox', async (browserWindow, options, guid) => {
|
||||
if ('id' in browserWindow) {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
electronSocket = socket;
|
||||
socket.on("showMessageBox", async (browserWindow, options, guid) => {
|
||||
if ("id" in browserWindow) {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
|
||||
const messageBoxReturnValue = await dialog.showMessageBox(window, options);
|
||||
electronSocket.emit('showMessageBoxComplete' + guid, [messageBoxReturnValue.response, messageBoxReturnValue.checkboxChecked]);
|
||||
} else {
|
||||
const id = guid || options;
|
||||
const messageBoxReturnValue = await dialog.showMessageBox(browserWindow);
|
||||
const messageBoxReturnValue = await dialog.showMessageBox(
|
||||
window,
|
||||
options,
|
||||
);
|
||||
electronSocket.emit("showMessageBoxComplete" + guid, [
|
||||
messageBoxReturnValue.response,
|
||||
messageBoxReturnValue.checkboxChecked,
|
||||
]);
|
||||
} else {
|
||||
const id = guid || options;
|
||||
const messageBoxReturnValue = await dialog.showMessageBox(browserWindow);
|
||||
|
||||
electronSocket.emit('showMessageBoxComplete' + id, [messageBoxReturnValue.response, messageBoxReturnValue.checkboxChecked]);
|
||||
}
|
||||
});
|
||||
electronSocket.emit("showMessageBoxComplete" + id, [
|
||||
messageBoxReturnValue.response,
|
||||
messageBoxReturnValue.checkboxChecked,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('showOpenDialog', async (browserWindow, options, guid) => {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
const openDialogReturnValue = await dialog.showOpenDialog(window, options);
|
||||
socket.on("showOpenDialog", async (browserWindow, options, guid) => {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
const openDialogReturnValue = await dialog.showOpenDialog(window, options);
|
||||
|
||||
electronSocket.emit('showOpenDialogComplete' + guid, openDialogReturnValue.filePaths || []);
|
||||
});
|
||||
electronSocket.emit(
|
||||
"showOpenDialogComplete" + guid,
|
||||
openDialogReturnValue.filePaths || [],
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('showSaveDialog', async (browserWindow, options, guid) => {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
const saveDialogReturnValue = await dialog.showSaveDialog(window, options);
|
||||
socket.on("showSaveDialog", async (browserWindow, options, guid) => {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
const saveDialogReturnValue = await dialog.showSaveDialog(window, options);
|
||||
|
||||
electronSocket.emit('showSaveDialogComplete' + guid, saveDialogReturnValue.filePath || '');
|
||||
});
|
||||
electronSocket.emit(
|
||||
"showSaveDialogComplete" + guid,
|
||||
saveDialogReturnValue.filePath || "",
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('showErrorBox', (title, content) => {
|
||||
dialog.showErrorBox(title, content);
|
||||
});
|
||||
socket.on("showErrorBox", (title, content) => {
|
||||
dialog.showErrorBox(title, content);
|
||||
});
|
||||
|
||||
socket.on('showCertificateTrustDialog', async (browserWindow, options, guid) => {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
await dialog.showCertificateTrustDialog(window, options);
|
||||
socket.on(
|
||||
"showCertificateTrustDialog",
|
||||
async (browserWindow, options, guid) => {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
await dialog.showCertificateTrustDialog(window, options);
|
||||
|
||||
electronSocket.emit('showCertificateTrustDialogComplete' + guid);
|
||||
});
|
||||
electronSocket.emit("showCertificateTrustDialogComplete" + guid);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,78 +1,81 @@
|
||||
import { Socket } from 'net';
|
||||
import { app, Menu } from 'electron';
|
||||
import { Socket } from "net";
|
||||
import { app, Menu } from "electron";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
electronSocket = socket;
|
||||
|
||||
socket.on('dock-bounce', (type) => {
|
||||
const id = app.dock.bounce(type);
|
||||
electronSocket.emit('dock-bounce-completed', id);
|
||||
});
|
||||
socket.on("dock-bounce", (type) => {
|
||||
const id = app.dock.bounce(type);
|
||||
electronSocket.emit("dock-bounce-completed", id);
|
||||
});
|
||||
|
||||
socket.on('dock-cancelBounce', (id) => {
|
||||
app.dock.cancelBounce(id);
|
||||
});
|
||||
socket.on("dock-cancelBounce", (id) => {
|
||||
app.dock.cancelBounce(id);
|
||||
});
|
||||
|
||||
socket.on('dock-downloadFinished', (filePath) => {
|
||||
app.dock.downloadFinished(filePath);
|
||||
});
|
||||
socket.on("dock-downloadFinished", (filePath) => {
|
||||
app.dock.downloadFinished(filePath);
|
||||
});
|
||||
|
||||
socket.on('dock-setBadge', (text) => {
|
||||
app.dock.setBadge(text);
|
||||
});
|
||||
socket.on("dock-setBadge", (text) => {
|
||||
app.dock.setBadge(text);
|
||||
});
|
||||
|
||||
socket.on('dock-getBadge', () => {
|
||||
const text = app.dock.getBadge();
|
||||
electronSocket.emit('dock-getBadge-completed', text);
|
||||
});
|
||||
socket.on("dock-getBadge", () => {
|
||||
const text = app.dock.getBadge();
|
||||
electronSocket.emit("dock-getBadge-completed", text);
|
||||
});
|
||||
|
||||
socket.on('dock-hide', () => {
|
||||
app.dock.hide();
|
||||
});
|
||||
socket.on("dock-hide", () => {
|
||||
app.dock.hide();
|
||||
});
|
||||
|
||||
socket.on('dock-show', () => {
|
||||
app.dock.show();
|
||||
});
|
||||
socket.on("dock-show", () => {
|
||||
app.dock.show();
|
||||
});
|
||||
|
||||
socket.on('dock-isVisible', () => {
|
||||
const isVisible = app.dock.isVisible();
|
||||
electronSocket.emit('dock-isVisible-completed', isVisible);
|
||||
});
|
||||
socket.on("dock-isVisible", () => {
|
||||
const isVisible = app.dock.isVisible();
|
||||
electronSocket.emit("dock-isVisible-completed", isVisible);
|
||||
});
|
||||
|
||||
socket.on('dock-setMenu', (menuItems) => {
|
||||
let menu = null;
|
||||
socket.on("dock-setMenu", (menuItems) => {
|
||||
let menu = null;
|
||||
|
||||
if (menuItems) {
|
||||
menu = Menu.buildFromTemplate(menuItems);
|
||||
if (menuItems) {
|
||||
menu = Menu.buildFromTemplate(menuItems);
|
||||
|
||||
addMenuItemClickConnector(menu.items, (id) => {
|
||||
electronSocket.emit('dockMenuItemClicked', id);
|
||||
});
|
||||
}
|
||||
|
||||
app.dock.setMenu(menu);
|
||||
});
|
||||
|
||||
// TODO: Menu (macOS) still to be implemented
|
||||
socket.on('dock-getMenu', () => {
|
||||
const menu = app.dock.getMenu();
|
||||
electronSocket.emit('dock-getMenu-completed', menu);
|
||||
});
|
||||
|
||||
socket.on('dock-setIcon', (image) => {
|
||||
app.dock.setIcon(image);
|
||||
});
|
||||
|
||||
function addMenuItemClickConnector(menuItems, callback) {
|
||||
menuItems.forEach((item) => {
|
||||
if (item.submenu && item.submenu.items.length > 0) {
|
||||
addMenuItemClickConnector(item.submenu.items, callback);
|
||||
}
|
||||
|
||||
if ('id' in item && item.id) {
|
||||
item.click = () => { callback(item.id); };
|
||||
}
|
||||
});
|
||||
addMenuItemClickConnector(menu.items, (id) => {
|
||||
electronSocket.emit("dockMenuItemClicked", id);
|
||||
});
|
||||
}
|
||||
|
||||
app.dock.setMenu(menu);
|
||||
});
|
||||
|
||||
// TODO: Menu (macOS) still to be implemented
|
||||
socket.on("dock-getMenu", () => {
|
||||
const menu = app.dock.getMenu();
|
||||
electronSocket.emit("dock-getMenu-completed", menu);
|
||||
});
|
||||
|
||||
socket.on("dock-setIcon", (image) => {
|
||||
app.dock.setIcon(image);
|
||||
});
|
||||
|
||||
function addMenuItemClickConnector(menuItems, callback) {
|
||||
menuItems.forEach((item) => {
|
||||
if (item.submenu && item.submenu.items.length > 0) {
|
||||
addMenuItemClickConnector(item.submenu.items, callback);
|
||||
}
|
||||
|
||||
if ("id" in item && item.id) {
|
||||
item.click = () => {
|
||||
callback(item.id);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import { globalShortcut } from 'electron';
|
||||
import { Socket } from 'net';
|
||||
import { globalShortcut } from "electron";
|
||||
import { Socket } from "net";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
socket.on('globalShortcut-register', (accelerator) => {
|
||||
globalShortcut.register(accelerator, () => {
|
||||
electronSocket.emit('globalShortcut-pressed', accelerator);
|
||||
});
|
||||
electronSocket = socket;
|
||||
socket.on("globalShortcut-register", (accelerator) => {
|
||||
globalShortcut.register(accelerator, () => {
|
||||
electronSocket.emit("globalShortcut-pressed", accelerator);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('globalShortcut-isRegistered', (accelerator) => {
|
||||
const isRegistered = globalShortcut.isRegistered(accelerator);
|
||||
socket.on("globalShortcut-isRegistered", (accelerator) => {
|
||||
const isRegistered = globalShortcut.isRegistered(accelerator);
|
||||
|
||||
electronSocket.emit('globalShortcut-isRegisteredCompleted', isRegistered);
|
||||
});
|
||||
electronSocket.emit("globalShortcut-isRegisteredCompleted", isRegistered);
|
||||
});
|
||||
|
||||
socket.on('globalShortcut-unregister', (accelerator) => {
|
||||
globalShortcut.unregister(accelerator);
|
||||
});
|
||||
socket.on("globalShortcut-unregister", (accelerator) => {
|
||||
globalShortcut.unregister(accelerator);
|
||||
});
|
||||
|
||||
socket.on('globalShortcut-unregisterAll', () => {
|
||||
try {
|
||||
globalShortcut.unregisterAll();
|
||||
} catch (error) { }
|
||||
});
|
||||
socket.on("globalShortcut-unregisterAll", () => {
|
||||
try {
|
||||
globalShortcut.unregisterAll();
|
||||
} catch (error) {}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,81 +1,90 @@
|
||||
import { ipcMain, BrowserWindow, BrowserView, Menu } from 'electron';
|
||||
import { Socket } from 'net';
|
||||
import { ipcMain, BrowserWindow, BrowserView, Menu } from "electron";
|
||||
import { Socket } from "net";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
socket.on('registerIpcMainChannel', (channel) => {
|
||||
ipcMain.on(channel, (event, args) => {
|
||||
electronSocket.emit(channel, [event.preventDefault(), args]);
|
||||
});
|
||||
electronSocket = socket;
|
||||
socket.on("registerIpcMainChannel", (channel) => {
|
||||
ipcMain.on(channel, (event, args) => {
|
||||
electronSocket.emit(channel, [event.preventDefault(), args]);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('registerSyncIpcMainChannel', (channel) => {
|
||||
ipcMain.on(channel, (event, args) => {
|
||||
const x = <any>socket;
|
||||
x.removeAllListeners(channel + 'Sync');
|
||||
socket.on(channel + 'Sync', (result) => {
|
||||
event.returnValue = result;
|
||||
});
|
||||
socket.on("registerSyncIpcMainChannel", (channel) => {
|
||||
ipcMain.on(channel, (event, args) => {
|
||||
const x = <any>socket;
|
||||
x.removeAllListeners(channel + "Sync");
|
||||
socket.on(channel + "Sync", (result) => {
|
||||
event.returnValue = result;
|
||||
});
|
||||
|
||||
electronSocket.emit(channel, [event.preventDefault(), args]);
|
||||
});
|
||||
electronSocket.emit(channel, [event.preventDefault(), args]);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('registerOnceIpcMainChannel', (channel) => {
|
||||
ipcMain.once(channel, (event, args) => {
|
||||
electronSocket.emit(channel, [event.preventDefault(), args]);
|
||||
});
|
||||
socket.on("registerOnceIpcMainChannel", (channel) => {
|
||||
ipcMain.once(channel, (event, args) => {
|
||||
electronSocket.emit(channel, [event.preventDefault(), args]);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('removeAllListenersIpcMainChannel', (channel) => {
|
||||
ipcMain.removeAllListeners(channel);
|
||||
});
|
||||
socket.on("removeAllListenersIpcMainChannel", (channel) => {
|
||||
ipcMain.removeAllListeners(channel);
|
||||
});
|
||||
|
||||
socket.on('sendToIpcRenderer', (browserWindow, channel, data) => {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
socket.on("sendToIpcRenderer", (browserWindow, channel, data) => {
|
||||
const window = BrowserWindow.fromId(browserWindow.id);
|
||||
|
||||
if (window) {
|
||||
window.webContents.send(channel, ...data);
|
||||
if (window) {
|
||||
window.webContents.send(channel, ...data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("sendToIpcRendererBrowserView", (id, channel, data) => {
|
||||
const browserViews: BrowserView[] = (global["browserViews"] =
|
||||
global["browserViews"] || []) as BrowserView[];
|
||||
let view: BrowserView = null;
|
||||
for (let i = 0; i < browserViews.length; i++) {
|
||||
if (browserViews[i]["id"] === id) {
|
||||
view = browserViews[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (view) {
|
||||
view.webContents.send(channel, ...data);
|
||||
}
|
||||
});
|
||||
|
||||
// Integration helpers: programmatically click menu items from renderer tests
|
||||
ipcMain.on("integration-click-application-menu", (event, id: string) => {
|
||||
try {
|
||||
const menu = Menu.getApplicationMenu();
|
||||
const mi = menu ? menu.getMenuItemById(id) : null;
|
||||
if (mi && typeof (mi as any).click === "function") {
|
||||
const bw = BrowserWindow.fromWebContents(event.sender);
|
||||
(mi as any).click(undefined, bw, undefined);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
"integration-click-context-menu",
|
||||
(event, windowId: number, id: string) => {
|
||||
try {
|
||||
const entries = (global as any)["contextMenuItems"] || [];
|
||||
const entry = entries.find((x: any) => x.browserWindowId === windowId);
|
||||
const mi = entry?.menu?.items?.find((i: any) => i.id === id);
|
||||
if (mi && typeof (mi as any).click === "function") {
|
||||
const bw = BrowserWindow.fromId(windowId);
|
||||
(mi as any).click(undefined, bw, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('sendToIpcRendererBrowserView', (id, channel, data) => {
|
||||
const browserViews: BrowserView[] = (global['browserViews'] = global['browserViews'] || []) as BrowserView[];
|
||||
let view: BrowserView = null;
|
||||
for (let i = 0; i < browserViews.length; i++) {
|
||||
if (browserViews[i]['id'] === id) {
|
||||
view = browserViews[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (view) {
|
||||
view.webContents.send(channel, ...data);
|
||||
}
|
||||
});
|
||||
|
||||
// Integration helpers: programmatically click menu items from renderer tests
|
||||
ipcMain.on('integration-click-application-menu', (event, id: string) => {
|
||||
try {
|
||||
const menu = Menu.getApplicationMenu();
|
||||
const mi = menu ? menu.getMenuItemById(id) : null;
|
||||
if (mi && typeof (mi as any).click === 'function') {
|
||||
const bw = BrowserWindow.fromWebContents(event.sender);
|
||||
(mi as any).click(undefined, bw, undefined);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
});
|
||||
|
||||
ipcMain.on('integration-click-context-menu', (event, windowId: number, id: string) => {
|
||||
try {
|
||||
const entries = (global as any)['contextMenuItems'] || [];
|
||||
const entry = entries.find((x: any) => x.browserWindowId === windowId);
|
||||
const mi = entry?.menu?.items?.find((i: any) => i.id === id);
|
||||
if (mi && typeof (mi as any).click === 'function') {
|
||||
const bw = BrowserWindow.fromId(windowId);
|
||||
(mi as any).click(undefined, bw, undefined);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,71 +1,92 @@
|
||||
import { Socket } from 'net';
|
||||
import { Menu, BrowserWindow } from 'electron';
|
||||
const contextMenuItems = (global['contextMenuItems'] = global['contextMenuItems'] || []);
|
||||
import { Socket } from "net";
|
||||
import { Menu, BrowserWindow } from "electron";
|
||||
|
||||
const contextMenuItems = (global["contextMenuItems"] =
|
||||
global["contextMenuItems"] || []);
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
socket.on('menu-setContextMenu', (browserWindowId, menuItems) => {
|
||||
const menu = Menu.buildFromTemplate(menuItems);
|
||||
electronSocket = socket;
|
||||
socket.on("menu-setContextMenu", (browserWindowId, menuItems) => {
|
||||
const menu = Menu.buildFromTemplate(menuItems);
|
||||
|
||||
addContextMenuItemClickConnector(menu.items, browserWindowId, (id, windowId) => {
|
||||
electronSocket.emit('contextMenuItemClicked', [id, windowId]);
|
||||
});
|
||||
addContextMenuItemClickConnector(
|
||||
menu.items,
|
||||
browserWindowId,
|
||||
(id, windowId) => {
|
||||
electronSocket.emit("contextMenuItemClicked", [id, windowId]);
|
||||
},
|
||||
);
|
||||
|
||||
const index = contextMenuItems.findIndex(contextMenu => contextMenu.browserWindowId === browserWindowId);
|
||||
const index = contextMenuItems.findIndex(
|
||||
(contextMenu) => contextMenu.browserWindowId === browserWindowId,
|
||||
);
|
||||
|
||||
const contextMenuItem = {
|
||||
menu: menu,
|
||||
browserWindowId: browserWindowId
|
||||
const contextMenuItem = {
|
||||
menu: menu,
|
||||
browserWindowId: browserWindowId,
|
||||
};
|
||||
|
||||
if (index === -1) {
|
||||
contextMenuItems.push(contextMenuItem);
|
||||
} else {
|
||||
contextMenuItems[index] = contextMenuItem;
|
||||
}
|
||||
});
|
||||
|
||||
function addContextMenuItemClickConnector(
|
||||
menuItems,
|
||||
browserWindowId,
|
||||
callback,
|
||||
) {
|
||||
menuItems.forEach((item) => {
|
||||
if (item.submenu && item.submenu.items.length > 0) {
|
||||
addContextMenuItemClickConnector(
|
||||
item.submenu.items,
|
||||
browserWindowId,
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
if ("id" in item && item.id) {
|
||||
item.click = () => {
|
||||
callback(item.id, browserWindowId);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (index === -1) {
|
||||
contextMenuItems.push(contextMenuItem);
|
||||
} else {
|
||||
contextMenuItems[index] = contextMenuItem;
|
||||
}
|
||||
socket.on("menu-contextMenuPopup", (browserWindowId) => {
|
||||
contextMenuItems.forEach((x) => {
|
||||
if (x.browserWindowId === browserWindowId) {
|
||||
const browserWindow = BrowserWindow.fromId(browserWindowId);
|
||||
x.menu.popup(browserWindow);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("menu-setApplicationMenu", (menuItems) => {
|
||||
const menu = Menu.buildFromTemplate(menuItems);
|
||||
|
||||
addMenuItemClickConnector(menu.items, (id) => {
|
||||
electronSocket.emit("menuItemClicked", id);
|
||||
});
|
||||
|
||||
function addContextMenuItemClickConnector(menuItems, browserWindowId, callback) {
|
||||
menuItems.forEach((item) => {
|
||||
if (item.submenu && item.submenu.items.length > 0) {
|
||||
addContextMenuItemClickConnector(item.submenu.items, browserWindowId, callback);
|
||||
}
|
||||
Menu.setApplicationMenu(menu);
|
||||
});
|
||||
|
||||
if ('id' in item && item.id) {
|
||||
item.click = () => { callback(item.id, browserWindowId); };
|
||||
}
|
||||
});
|
||||
}
|
||||
function addMenuItemClickConnector(menuItems, callback) {
|
||||
menuItems.forEach((item) => {
|
||||
if (item.submenu && item.submenu.items.length > 0) {
|
||||
addMenuItemClickConnector(item.submenu.items, callback);
|
||||
}
|
||||
|
||||
socket.on('menu-contextMenuPopup', (browserWindowId) => {
|
||||
contextMenuItems.forEach(x => {
|
||||
if (x.browserWindowId === browserWindowId) {
|
||||
const browserWindow = BrowserWindow.fromId(browserWindowId);
|
||||
x.menu.popup(browserWindow);
|
||||
}
|
||||
});
|
||||
if ("id" in item && item.id) {
|
||||
item.click = () => {
|
||||
callback(item.id);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('menu-setApplicationMenu', (menuItems) => {
|
||||
const menu = Menu.buildFromTemplate(menuItems);
|
||||
|
||||
addMenuItemClickConnector(menu.items, (id) => {
|
||||
electronSocket.emit('menuItemClicked', id);
|
||||
});
|
||||
|
||||
Menu.setApplicationMenu(menu);
|
||||
});
|
||||
|
||||
function addMenuItemClickConnector(menuItems, callback) {
|
||||
menuItems.forEach((item) => {
|
||||
if (item.submenu && item.submenu.items.length > 0) {
|
||||
addMenuItemClickConnector(item.submenu.items, callback);
|
||||
}
|
||||
|
||||
if ('id' in item && item.id) {
|
||||
item.click = () => { callback(item.id); };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,41 +1,52 @@
|
||||
import { Socket } from 'net';
|
||||
import { nativeTheme } from 'electron';
|
||||
import { Socket } from "net";
|
||||
import { nativeTheme } from "electron";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
electronSocket = socket;
|
||||
|
||||
socket.on('nativeTheme-shouldUseDarkColors', () => {
|
||||
const shouldUseDarkColors = nativeTheme.shouldUseDarkColors;
|
||||
socket.on("nativeTheme-shouldUseDarkColors", () => {
|
||||
const shouldUseDarkColors = nativeTheme.shouldUseDarkColors;
|
||||
|
||||
electronSocket.emit('nativeTheme-shouldUseDarkColors-completed', shouldUseDarkColors);
|
||||
});
|
||||
|
||||
socket.on('nativeTheme-shouldUseHighContrastColors', () => {
|
||||
const shouldUseHighContrastColors = nativeTheme.shouldUseHighContrastColors;
|
||||
|
||||
electronSocket.emit('nativeTheme-shouldUseHighContrastColors-completed', shouldUseHighContrastColors);
|
||||
});
|
||||
|
||||
socket.on('nativeTheme-shouldUseInvertedColorScheme', () => {
|
||||
const shouldUseInvertedColorScheme = nativeTheme.shouldUseInvertedColorScheme;
|
||||
|
||||
electronSocket.emit('nativeTheme-shouldUseInvertedColorScheme-completed', shouldUseInvertedColorScheme);
|
||||
});
|
||||
|
||||
socket.on('nativeTheme-getThemeSource', () => {
|
||||
const themeSource = nativeTheme.themeSource;
|
||||
|
||||
electronSocket.emit('nativeTheme-getThemeSource-completed', themeSource);
|
||||
});
|
||||
|
||||
socket.on('nativeTheme-themeSource', (themeSource) => {
|
||||
nativeTheme.themeSource = themeSource;
|
||||
});
|
||||
|
||||
socket.on('register-nativeTheme-updated', (id) => {
|
||||
nativeTheme.on('updated', () => {
|
||||
electronSocket.emit('nativeTheme-updated' + id);
|
||||
});
|
||||
electronSocket.emit(
|
||||
"nativeTheme-shouldUseDarkColors-completed",
|
||||
shouldUseDarkColors,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("nativeTheme-shouldUseHighContrastColors", () => {
|
||||
const shouldUseHighContrastColors = nativeTheme.shouldUseHighContrastColors;
|
||||
|
||||
electronSocket.emit(
|
||||
"nativeTheme-shouldUseHighContrastColors-completed",
|
||||
shouldUseHighContrastColors,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("nativeTheme-shouldUseInvertedColorScheme", () => {
|
||||
const shouldUseInvertedColorScheme =
|
||||
nativeTheme.shouldUseInvertedColorScheme;
|
||||
|
||||
electronSocket.emit(
|
||||
"nativeTheme-shouldUseInvertedColorScheme-completed",
|
||||
shouldUseInvertedColorScheme,
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("nativeTheme-getThemeSource", () => {
|
||||
const themeSource = nativeTheme.themeSource;
|
||||
|
||||
electronSocket.emit("nativeTheme-getThemeSource-completed", themeSource);
|
||||
});
|
||||
|
||||
socket.on("nativeTheme-themeSource", (themeSource) => {
|
||||
nativeTheme.themeSource = themeSource;
|
||||
});
|
||||
|
||||
socket.on("register-nativeTheme-updated", (id) => {
|
||||
nativeTheme.on("updated", () => {
|
||||
electronSocket.emit("nativeTheme-updated" + id);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,58 +1,64 @@
|
||||
import { Socket } from 'net';
|
||||
import { Notification } from 'electron';
|
||||
const notifications: Electron.Notification[] = (global['notifications'] = global['notifications'] || []) as Electron.Notification[];
|
||||
import { Socket } from "net";
|
||||
import { Notification } from "electron";
|
||||
|
||||
const notifications: Electron.Notification[] = (global["notifications"] =
|
||||
global["notifications"] || []) as Electron.Notification[];
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
socket.on('createNotification', (options) => {
|
||||
const notification = new Notification(options);
|
||||
let haveEvent = false;
|
||||
electronSocket = socket;
|
||||
socket.on("createNotification", (options) => {
|
||||
const notification = new Notification(options);
|
||||
let haveEvent = false;
|
||||
|
||||
if (options.showID) {
|
||||
haveEvent = true;
|
||||
notification.on('show', () => {
|
||||
electronSocket.emit('NotificationEventShow', options.showID);
|
||||
});
|
||||
}
|
||||
if (options.showID) {
|
||||
haveEvent = true;
|
||||
notification.on("show", () => {
|
||||
electronSocket.emit("NotificationEventShow", options.showID);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.clickID) {
|
||||
haveEvent = true;
|
||||
notification.on('click', () => {
|
||||
electronSocket.emit('NotificationEventClick', options.clickID);
|
||||
});
|
||||
}
|
||||
if (options.clickID) {
|
||||
haveEvent = true;
|
||||
notification.on("click", () => {
|
||||
electronSocket.emit("NotificationEventClick", options.clickID);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.closeID) {
|
||||
haveEvent = true;
|
||||
notification.on('close', () => {
|
||||
electronSocket.emit('NotificationEventClose', options.closeID);
|
||||
});
|
||||
}
|
||||
if (options.closeID) {
|
||||
haveEvent = true;
|
||||
notification.on("close", () => {
|
||||
electronSocket.emit("NotificationEventClose", options.closeID);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.replyID) {
|
||||
haveEvent = true;
|
||||
notification.on('reply', (event, value) => {
|
||||
electronSocket.emit('NotificationEventReply', [options.replyID, value]);
|
||||
});
|
||||
}
|
||||
if (options.replyID) {
|
||||
haveEvent = true;
|
||||
notification.on("reply", (event, value) => {
|
||||
electronSocket.emit("NotificationEventReply", [options.replyID, value]);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.actionID) {
|
||||
haveEvent = true;
|
||||
notification.on('action', (event, value) => {
|
||||
electronSocket.emit('NotificationEventAction', [options.actionID, value]);
|
||||
});
|
||||
}
|
||||
if (options.actionID) {
|
||||
haveEvent = true;
|
||||
notification.on("action", (event, value) => {
|
||||
electronSocket.emit("NotificationEventAction", [
|
||||
options.actionID,
|
||||
value,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
if (haveEvent) {
|
||||
notifications.push(notification);
|
||||
}
|
||||
if (haveEvent) {
|
||||
notifications.push(notification);
|
||||
}
|
||||
|
||||
notification.show();
|
||||
});
|
||||
notification.show();
|
||||
});
|
||||
|
||||
socket.on('notificationIsSupported', () => {
|
||||
const isSupported = Notification.isSupported();
|
||||
electronSocket.emit('notificationIsSupportedCompleted', isSupported);
|
||||
});
|
||||
socket.on("notificationIsSupported", () => {
|
||||
const isSupported = Notification.isSupported();
|
||||
electronSocket.emit("notificationIsSupportedCompleted", isSupported);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,42 +1,43 @@
|
||||
import { Socket } from 'net';
|
||||
import { powerMonitor } from 'electron';
|
||||
import { Socket } from "net";
|
||||
import { powerMonitor } from "electron";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
socket.on('register-powerMonitor-lock-screen', () => {
|
||||
powerMonitor.on('lock-screen', () => {
|
||||
electronSocket.emit('powerMonitor-lock-screen');
|
||||
});
|
||||
electronSocket = socket;
|
||||
socket.on("register-powerMonitor-lock-screen", () => {
|
||||
powerMonitor.on("lock-screen", () => {
|
||||
electronSocket.emit("powerMonitor-lock-screen");
|
||||
});
|
||||
socket.on('register-powerMonitor-unlock-screen', () => {
|
||||
powerMonitor.on('unlock-screen', () => {
|
||||
electronSocket.emit('powerMonitor-unlock-screen');
|
||||
});
|
||||
});
|
||||
socket.on("register-powerMonitor-unlock-screen", () => {
|
||||
powerMonitor.on("unlock-screen", () => {
|
||||
electronSocket.emit("powerMonitor-unlock-screen");
|
||||
});
|
||||
socket.on('register-powerMonitor-suspend', () => {
|
||||
powerMonitor.on('suspend', () => {
|
||||
electronSocket.emit('powerMonitor-suspend');
|
||||
});
|
||||
});
|
||||
socket.on("register-powerMonitor-suspend", () => {
|
||||
powerMonitor.on("suspend", () => {
|
||||
electronSocket.emit("powerMonitor-suspend");
|
||||
});
|
||||
socket.on('register-powerMonitor-resume', () => {
|
||||
powerMonitor.on('resume', () => {
|
||||
electronSocket.emit('powerMonitor-resume');
|
||||
});
|
||||
});
|
||||
socket.on("register-powerMonitor-resume", () => {
|
||||
powerMonitor.on("resume", () => {
|
||||
electronSocket.emit("powerMonitor-resume");
|
||||
});
|
||||
socket.on('register-powerMonitor-ac', () => {
|
||||
powerMonitor.on('on-ac', () => {
|
||||
electronSocket.emit('powerMonitor-ac');
|
||||
});
|
||||
});
|
||||
socket.on("register-powerMonitor-ac", () => {
|
||||
powerMonitor.on("on-ac", () => {
|
||||
electronSocket.emit("powerMonitor-ac");
|
||||
});
|
||||
socket.on('register-powerMonitor-battery', () => {
|
||||
powerMonitor.on('on-battery', () => {
|
||||
electronSocket.emit('powerMonitor-battery');
|
||||
});
|
||||
});
|
||||
socket.on("register-powerMonitor-battery", () => {
|
||||
powerMonitor.on("on-battery", () => {
|
||||
electronSocket.emit("powerMonitor-battery");
|
||||
});
|
||||
socket.on('register-powerMonitor-shutdown', () => {
|
||||
powerMonitor.on('shutdown', () => {
|
||||
electronSocket.emit('powerMonitor-shutdown');
|
||||
});
|
||||
});
|
||||
socket.on("register-powerMonitor-shutdown", () => {
|
||||
powerMonitor.on("shutdown", () => {
|
||||
electronSocket.emit("powerMonitor-shutdown");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,73 +1,74 @@
|
||||
import { Socket } from 'net';
|
||||
import { Socket } from "net";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
electronSocket = socket;
|
||||
|
||||
socket.on('process-execPath', () => {
|
||||
const value = process.execPath;
|
||||
electronSocket.emit('process-execPath-completed', value);
|
||||
});
|
||||
socket.on("process-execPath", () => {
|
||||
const value = process.execPath;
|
||||
electronSocket.emit("process-execPath-completed", value);
|
||||
});
|
||||
|
||||
socket.on('process-argv', () => {
|
||||
const value = process.argv;
|
||||
electronSocket.emit('process-argv-completed', value);
|
||||
});
|
||||
socket.on("process-argv", () => {
|
||||
const value = process.argv;
|
||||
electronSocket.emit("process-argv-completed", value);
|
||||
});
|
||||
|
||||
socket.on('process-type', () => {
|
||||
const value = process.type;
|
||||
electronSocket.emit('process-type-completed', value);
|
||||
});
|
||||
socket.on("process-type", () => {
|
||||
const value = process.type;
|
||||
electronSocket.emit("process-type-completed", value);
|
||||
});
|
||||
|
||||
socket.on('process-versions', () => {
|
||||
const value = process.versions;
|
||||
electronSocket.emit('process-versions-completed', value);
|
||||
});
|
||||
socket.on("process-versions", () => {
|
||||
const value = process.versions;
|
||||
electronSocket.emit("process-versions-completed", value);
|
||||
});
|
||||
|
||||
socket.on('process-defaultApp', () => {
|
||||
if (process.defaultApp === undefined) {
|
||||
electronSocket.emit('process-defaultApp-completed', false);
|
||||
return;
|
||||
}
|
||||
electronSocket.emit('process-defaultApp-completed', process.defaultApp);
|
||||
});
|
||||
socket.on("process-defaultApp", () => {
|
||||
if (process.defaultApp === undefined) {
|
||||
electronSocket.emit("process-defaultApp-completed", false);
|
||||
return;
|
||||
}
|
||||
electronSocket.emit("process-defaultApp-completed", process.defaultApp);
|
||||
});
|
||||
|
||||
socket.on('process-isMainFrame', () => {
|
||||
if (process.isMainFrame === undefined) {
|
||||
electronSocket.emit('process-isMainFrame-completed', false);
|
||||
return;
|
||||
}
|
||||
electronSocket.emit('process-isMainFrame-completed', process.isMainFrame);
|
||||
});
|
||||
socket.on("process-isMainFrame", () => {
|
||||
if (process.isMainFrame === undefined) {
|
||||
electronSocket.emit("process-isMainFrame-completed", false);
|
||||
return;
|
||||
}
|
||||
electronSocket.emit("process-isMainFrame-completed", process.isMainFrame);
|
||||
});
|
||||
|
||||
socket.on('process-resourcesPath', () => {
|
||||
const value = process.resourcesPath;
|
||||
electronSocket.emit('process-resourcesPath-completed', value);
|
||||
});
|
||||
socket.on("process-resourcesPath", () => {
|
||||
const value = process.resourcesPath;
|
||||
electronSocket.emit("process-resourcesPath-completed", value);
|
||||
});
|
||||
|
||||
socket.on('process-upTime', () => {
|
||||
let value = process.uptime();
|
||||
if (value === undefined) {
|
||||
value = -1;
|
||||
}
|
||||
electronSocket.emit('process-upTime-completed', value);
|
||||
});
|
||||
socket.on("process-upTime", () => {
|
||||
let value = process.uptime();
|
||||
if (value === undefined) {
|
||||
value = -1;
|
||||
}
|
||||
electronSocket.emit("process-upTime-completed", value);
|
||||
});
|
||||
|
||||
socket.on('process-pid', () => {
|
||||
if (process.pid === undefined) {
|
||||
electronSocket.emit('process-pid-completed', -1);
|
||||
return;
|
||||
}
|
||||
electronSocket.emit('process-pid-completed', process.pid);
|
||||
});
|
||||
socket.on("process-pid", () => {
|
||||
if (process.pid === undefined) {
|
||||
electronSocket.emit("process-pid-completed", -1);
|
||||
return;
|
||||
}
|
||||
electronSocket.emit("process-pid-completed", process.pid);
|
||||
});
|
||||
|
||||
socket.on('process-arch', () => {
|
||||
const value = process.arch;
|
||||
electronSocket.emit('process-arch-completed', value);
|
||||
});
|
||||
socket.on("process-arch", () => {
|
||||
const value = process.arch;
|
||||
electronSocket.emit("process-arch-completed", value);
|
||||
});
|
||||
|
||||
socket.on('process-platform', () => {
|
||||
const value = process.platform;
|
||||
electronSocket.emit('process-platform-completed', value);
|
||||
})
|
||||
socket.on("process-platform", () => {
|
||||
const value = process.platform;
|
||||
electronSocket.emit("process-platform-completed", value);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,55 +1,59 @@
|
||||
import { Socket } from 'net';
|
||||
import { screen } from 'electron';
|
||||
import { Socket } from "net";
|
||||
import { screen } from "electron";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
electronSocket = socket;
|
||||
|
||||
socket.on('register-screen-display-added', (id) => {
|
||||
screen.on('display-added', (event, display) => {
|
||||
electronSocket.emit('screen-display-added' + id, display);
|
||||
});
|
||||
socket.on("register-screen-display-added", (id) => {
|
||||
screen.on("display-added", (event, display) => {
|
||||
electronSocket.emit("screen-display-added" + id, display);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-screen-display-removed', (id) => {
|
||||
screen.on('display-removed', (event, display) => {
|
||||
electronSocket.emit('screen-display-removed' + id, display);
|
||||
});
|
||||
socket.on("register-screen-display-removed", (id) => {
|
||||
screen.on("display-removed", (event, display) => {
|
||||
electronSocket.emit("screen-display-removed" + id, display);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('register-screen-display-metrics-changed', (id) => {
|
||||
screen.on('display-metrics-changed', (event, display, changedMetrics) => {
|
||||
electronSocket.emit('screen-display-metrics-changed' + id, [display, changedMetrics]);
|
||||
});
|
||||
socket.on("register-screen-display-metrics-changed", (id) => {
|
||||
screen.on("display-metrics-changed", (event, display, changedMetrics) => {
|
||||
electronSocket.emit("screen-display-metrics-changed" + id, [
|
||||
display,
|
||||
changedMetrics,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('screen-getCursorScreenPoint', () => {
|
||||
const point = screen.getCursorScreenPoint();
|
||||
electronSocket.emit('screen-getCursorScreenPoint-completed', point);
|
||||
});
|
||||
socket.on("screen-getCursorScreenPoint", () => {
|
||||
const point = screen.getCursorScreenPoint();
|
||||
electronSocket.emit("screen-getCursorScreenPoint-completed", point);
|
||||
});
|
||||
|
||||
socket.on('screen-getMenuBarWorkArea', () => {
|
||||
const height = screen.getPrimaryDisplay().workArea;
|
||||
electronSocket.emit('screen-getMenuBarWorkArea-completed', height);
|
||||
});
|
||||
socket.on("screen-getMenuBarWorkArea", () => {
|
||||
const height = screen.getPrimaryDisplay().workArea;
|
||||
electronSocket.emit("screen-getMenuBarWorkArea-completed", height);
|
||||
});
|
||||
|
||||
socket.on('screen-getPrimaryDisplay', () => {
|
||||
const display = screen.getPrimaryDisplay();
|
||||
electronSocket.emit('screen-getPrimaryDisplay-completed', display);
|
||||
});
|
||||
socket.on("screen-getPrimaryDisplay", () => {
|
||||
const display = screen.getPrimaryDisplay();
|
||||
electronSocket.emit("screen-getPrimaryDisplay-completed", display);
|
||||
});
|
||||
|
||||
socket.on('screen-getAllDisplays', () => {
|
||||
const display = screen.getAllDisplays();
|
||||
electronSocket.emit('screen-getAllDisplays-completed', display);
|
||||
});
|
||||
socket.on("screen-getAllDisplays", () => {
|
||||
const display = screen.getAllDisplays();
|
||||
electronSocket.emit("screen-getAllDisplays-completed", display);
|
||||
});
|
||||
|
||||
socket.on('screen-getDisplayNearestPoint', (point) => {
|
||||
const display = screen.getDisplayNearestPoint(point);
|
||||
electronSocket.emit('screen-getDisplayNearestPoint-completed', display);
|
||||
});
|
||||
socket.on("screen-getDisplayNearestPoint", (point) => {
|
||||
const display = screen.getDisplayNearestPoint(point);
|
||||
electronSocket.emit("screen-getDisplayNearestPoint-completed", display);
|
||||
});
|
||||
|
||||
socket.on('screen-getDisplayMatching', (rectangle) => {
|
||||
const display = screen.getDisplayMatching(rectangle);
|
||||
electronSocket.emit('screen-getDisplayMatching-completed', display);
|
||||
});
|
||||
socket.on("screen-getDisplayMatching", (rectangle) => {
|
||||
const display = screen.getDisplayMatching(rectangle);
|
||||
electronSocket.emit("screen-getDisplayMatching-completed", display);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,63 +1,64 @@
|
||||
import { Socket } from 'net';
|
||||
import { shell } from 'electron';
|
||||
import { Socket } from "net";
|
||||
import { shell } from "electron";
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
electronSocket = socket;
|
||||
socket.on('shell-showItemInFolder', (fullPath) => {
|
||||
shell.showItemInFolder(fullPath);
|
||||
electronSocket = socket;
|
||||
socket.on("shell-showItemInFolder", (fullPath) => {
|
||||
shell.showItemInFolder(fullPath);
|
||||
|
||||
electronSocket.emit('shell-showItemInFolderCompleted');
|
||||
});
|
||||
electronSocket.emit("shell-showItemInFolderCompleted");
|
||||
});
|
||||
|
||||
socket.on('shell-openPath', async (path) => {
|
||||
const errorMessage = await shell.openPath(path);
|
||||
socket.on("shell-openPath", async (path) => {
|
||||
const errorMessage = await shell.openPath(path);
|
||||
|
||||
electronSocket.emit('shell-openPathCompleted', errorMessage);
|
||||
});
|
||||
electronSocket.emit("shell-openPathCompleted", errorMessage);
|
||||
});
|
||||
|
||||
socket.on('shell-openExternal', async (url, options) => {
|
||||
let result = '';
|
||||
socket.on("shell-openExternal", async (url, options) => {
|
||||
let result = "";
|
||||
|
||||
if (options) {
|
||||
await shell.openExternal(url, options).catch(e => {
|
||||
result = e.message;
|
||||
});
|
||||
} else {
|
||||
await shell.openExternal(url).catch((e) => {
|
||||
result = e.message;
|
||||
});
|
||||
}
|
||||
if (options) {
|
||||
await shell.openExternal(url, options).catch((e) => {
|
||||
result = e.message;
|
||||
});
|
||||
} else {
|
||||
await shell.openExternal(url).catch((e) => {
|
||||
result = e.message;
|
||||
});
|
||||
}
|
||||
|
||||
electronSocket.emit('shell-openExternalCompleted', result);
|
||||
});
|
||||
electronSocket.emit("shell-openExternalCompleted", result);
|
||||
});
|
||||
|
||||
socket.on('shell-trashItem', async (fullPath, deleteOnFail) => {
|
||||
let success = false;
|
||||
socket.on("shell-trashItem", async (fullPath, deleteOnFail) => {
|
||||
let success = false;
|
||||
|
||||
try {
|
||||
await shell.trashItem(fullPath);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
success = false;
|
||||
}
|
||||
try {
|
||||
await shell.trashItem(fullPath);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
success = false;
|
||||
}
|
||||
|
||||
electronSocket.emit('shell-trashItem-completed', success);
|
||||
});
|
||||
electronSocket.emit("shell-trashItem-completed", success);
|
||||
});
|
||||
|
||||
socket.on('shell-beep', () => {
|
||||
shell.beep();
|
||||
});
|
||||
socket.on("shell-beep", () => {
|
||||
shell.beep();
|
||||
});
|
||||
|
||||
socket.on('shell-writeShortcutLink', (shortcutPath, operation, options) => {
|
||||
const success = shell.writeShortcutLink(shortcutPath, operation, options);
|
||||
socket.on("shell-writeShortcutLink", (shortcutPath, operation, options) => {
|
||||
const success = shell.writeShortcutLink(shortcutPath, operation, options);
|
||||
|
||||
electronSocket.emit('shell-writeShortcutLinkCompleted', success);
|
||||
});
|
||||
electronSocket.emit("shell-writeShortcutLinkCompleted", success);
|
||||
});
|
||||
|
||||
socket.on('shell-readShortcutLink', (shortcutPath) => {
|
||||
const shortcutDetails = shell.readShortcutLink(shortcutPath);
|
||||
socket.on("shell-readShortcutLink", (shortcutPath) => {
|
||||
const shortcutDetails = shell.readShortcutLink(shortcutPath);
|
||||
|
||||
electronSocket.emit('shell-readShortcutLinkCompleted', shortcutDetails);
|
||||
});
|
||||
electronSocket.emit("shell-readShortcutLinkCompleted", shortcutDetails);
|
||||
});
|
||||
};
|
||||
|
||||
214
src/ElectronNET.Host/api/signalr-bridge.js
Normal file
214
src/ElectronNET.Host/api/signalr-bridge.js
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* SignalR connection module for Electron.NET
|
||||
*
|
||||
* This module provides a Socket.IO-compatible interface for SignalR communication.
|
||||
* Key features:
|
||||
* - Mimics Socket.IO's on() and emit() methods for compatibility with existing API modules
|
||||
* - Handles event registration and propagation between Electron and .NET
|
||||
* - Event args are always passed as arrays to match C# ElectronEvent(string, object[]) signature
|
||||
* - Spreads args when calling handlers to match Socket.IO behavior
|
||||
* - Supports automatic reconnection with configurable logging level
|
||||
*/
|
||||
const signalR = require("@microsoft/signalr");
|
||||
const { app } = require("electron");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
// Flag to track if we've already initiated shutdown due to EPIPE
|
||||
let isShuttingDownFromEPIPE = false;
|
||||
|
||||
// Handle EPIPE errors at the process stdout/stderr level
|
||||
// When the pipe breaks (e.g., .NET process terminates), quit Electron gracefully
|
||||
const handlePipeError = (err) => {
|
||||
if (err.code === "EPIPE" || err.code === "ERR_STREAM_WRITE_AFTER_END") {
|
||||
// Pipe is broken - the .NET process has terminated
|
||||
if (!isShuttingDownFromEPIPE) {
|
||||
isShuttingDownFromEPIPE = true;
|
||||
// Give a brief moment for any pending operations, then quit
|
||||
setImmediate(() => {
|
||||
if (app && app.quit) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Re-throw other errors
|
||||
throw err;
|
||||
};
|
||||
|
||||
// Suppress EPIPE errors at the process stdout/stderr level
|
||||
if (process.stdout && !process.stdout.listenerCount("error")) {
|
||||
process.stdout.on("error", handlePipeError);
|
||||
}
|
||||
|
||||
if (process.stderr && !process.stderr.listenerCount("error")) {
|
||||
process.stderr.on("error", handlePipeError);
|
||||
}
|
||||
|
||||
// Custom logger for SignalR that uses environment-aware logging
|
||||
class SafeLogger {
|
||||
constructor(minLevel) {
|
||||
this.minLevel = minLevel || signalR.LogLevel.Warning;
|
||||
}
|
||||
|
||||
log(logLevel, message) {
|
||||
// Skip if below minimum level
|
||||
if (logLevel < this.minLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (logLevel) {
|
||||
case signalR.LogLevel.Critical:
|
||||
case signalR.LogLevel.Error:
|
||||
logger.error(`[SignalR] ${message}`);
|
||||
break;
|
||||
case signalR.LogLevel.Warning:
|
||||
logger.warn(`[SignalR] ${message}`);
|
||||
break;
|
||||
case signalR.LogLevel.Information:
|
||||
logger.info(`[SignalR] ${message}`);
|
||||
break;
|
||||
case signalR.LogLevel.Debug:
|
||||
case signalR.LogLevel.Trace:
|
||||
logger.debug(`[SignalR] ${message}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SignalRBridge {
|
||||
constructor(hubUrl, authToken) {
|
||||
this.hubUrl = hubUrl;
|
||||
this.authToken = authToken;
|
||||
this.connection = null;
|
||||
this.isConnected = false;
|
||||
this.eventHandlers = new Map(); // For socket.io-style .on() handlers
|
||||
this.callIdCounter = 0;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
// Append authentication token to the SignalR connection URL
|
||||
const connectionUrl = this.authToken
|
||||
? `${this.hubUrl}?token=${this.authToken}`
|
||||
: this.hubUrl;
|
||||
|
||||
// Determine SignalR log level based on environment
|
||||
// Warning level suppresses verbose packet-level logging
|
||||
const { getLogLevel, LogLevel: AppLogLevel } = require("../logger");
|
||||
let signalRLogLevel;
|
||||
|
||||
if (getLogLevel() <= AppLogLevel.DEBUG) {
|
||||
// Debug mode: show Info level (connection events without packet details)
|
||||
signalRLogLevel = signalR.LogLevel.Information;
|
||||
} else {
|
||||
// Development/Production: only warnings and errors
|
||||
signalRLogLevel = signalR.LogLevel.Warning;
|
||||
}
|
||||
|
||||
this.connection = new signalR.HubConnectionBuilder()
|
||||
.withUrl(connectionUrl)
|
||||
.withAutomaticReconnect()
|
||||
.configureLogging(new SafeLogger(signalRLogLevel))
|
||||
.build();
|
||||
|
||||
// Handle reconnection
|
||||
this.connection.onreconnecting((error) => {
|
||||
logger.error(`[SignalRBridge] Connection lost. Reconnecting...`, error);
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
this.connection.onreconnected((connectionId) => {
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
this.connection.onclose((error) => {
|
||||
if (error) {
|
||||
logger.error(`[SignalRBridge] Connection closed:`, error);
|
||||
}
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
// Set up handlers for messages from .NET
|
||||
this.setupMessageHandlers();
|
||||
|
||||
try {
|
||||
await this.connection.start();
|
||||
this.isConnected = true;
|
||||
|
||||
// Register with the hub
|
||||
await this.connection.invoke("RegisterElectronClient");
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Check if this is an authentication error
|
||||
if (err.message && err.message.includes("401")) {
|
||||
logger.error(
|
||||
`[SignalRBridge] Authentication failed: The authentication token is invalid or missing.`,
|
||||
);
|
||||
logger.error(
|
||||
`[SignalRBridge] Please ensure the --authtoken parameter is correctly passed to Electron.`,
|
||||
);
|
||||
} else {
|
||||
logger.error(`[SignalRBridge] Connection failed:`, err);
|
||||
}
|
||||
this.isConnected = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setupMessageHandlers() {
|
||||
// Handle generic events from .NET - this is where .NET's Emit() calls arrive
|
||||
this.connection.on("event", (eventName, args) => {
|
||||
// args is an array from .NET - spread it when calling handlers
|
||||
const argsArray = Array.isArray(args) ? args : [args];
|
||||
|
||||
// Check if we have handlers registered for this event
|
||||
if (this.eventHandlers.has(eventName)) {
|
||||
const handlers = this.eventHandlers.get(eventName);
|
||||
handlers.forEach((handler) => {
|
||||
try {
|
||||
handler(...argsArray);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[SignalRBridge] Error in event handler for ${eventName}:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Socket.io compatibility: register event handler
|
||||
on(eventName, callback) {
|
||||
if (!this.eventHandlers.has(eventName)) {
|
||||
this.eventHandlers.set(eventName, []);
|
||||
}
|
||||
this.eventHandlers.get(eventName).push(callback);
|
||||
}
|
||||
|
||||
// Socket.io compatibility: emit event (send to .NET)
|
||||
async emit(eventName, ...args) {
|
||||
if (!this.isConnected) {
|
||||
logger.warn(`[SignalRBridge] Cannot emit ${eventName} - not connected`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Always pass args as an array to match C# method signature
|
||||
await this.connection.invoke("ElectronEvent", eventName, args);
|
||||
} catch (err) {
|
||||
logger.error(`[SignalRBridge] Error emitting ${eventName}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this.connection) {
|
||||
await this.connection.stop();
|
||||
this.isConnected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SignalRBridge };
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Socket } from 'net';
|
||||
import { Menu, Tray, nativeImage } from 'electron';
|
||||
|
||||
let tray: { value: Electron.Tray } = (global['$tray'] = global['tray'] || { value: null });
|
||||
let electronSocket;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use strict";
|
||||
const electron_1 = require("electron");
|
||||
const browserView_1 = require("./browserView");
|
||||
const fs = require("fs");
|
||||
const browserView_1 = require("./browserView");
|
||||
const { logger } = require("../logger");
|
||||
let electronSocket;
|
||||
module.exports = (socket) => {
|
||||
electronSocket = socket;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as fs from "fs";
|
||||
import { Socket } from "net";
|
||||
import {BrowserWindow, BrowserView} from "electron";
|
||||
import { BrowserWindow, BrowserView } from "electron";
|
||||
import { browserViewMediateService } from "./browserView";
|
||||
const fs = require("fs");
|
||||
|
||||
let electronSocket;
|
||||
|
||||
export = (socket: Socket) => {
|
||||
@@ -68,7 +69,7 @@ export = (socket: Socket) => {
|
||||
errorCode,
|
||||
validatedUrl,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -136,17 +137,17 @@ export = (socket: Socket) => {
|
||||
async (id, code, userGesture = false) => {
|
||||
const result = await getWindowById(id).webContents.executeJavaScript(
|
||||
code,
|
||||
userGesture
|
||||
userGesture,
|
||||
);
|
||||
electronSocket.emit("webContents-executeJavaScript-completed", result);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on("webContents-getUrl", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit(
|
||||
"webContents-getUrl" + id,
|
||||
browserWindow.webContents.getURL()
|
||||
browserWindow.webContents.getURL(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -155,7 +156,7 @@ export = (socket: Socket) => {
|
||||
(id, domains) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
browserWindow.webContents.session.allowNTLMCredentialsForDomains(domains);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on("webContents-session-clearAuthCache", async (...args) => {
|
||||
@@ -194,7 +195,7 @@ export = (socket: Socket) => {
|
||||
await browserWindow.webContents.session.clearHostResolverCache();
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-clearHostResolverCache-completed" + guid
|
||||
"webContents-session-clearHostResolverCache-completed" + guid,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -203,7 +204,7 @@ export = (socket: Socket) => {
|
||||
await browserWindow.webContents.session.clearStorageData({});
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-clearStorageData-completed" + guid
|
||||
"webContents-session-clearStorageData-completed" + guid,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -214,9 +215,9 @@ export = (socket: Socket) => {
|
||||
await browserWindow.webContents.session.clearStorageData(options);
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-clearStorageData-options-completed" + guid
|
||||
"webContents-session-clearStorageData-options-completed" + guid,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on("webContents-session-createInterruptedDownload", (id, options) => {
|
||||
@@ -241,13 +242,12 @@ export = (socket: Socket) => {
|
||||
|
||||
socket.on("webContents-session-getBlobData", async (id, identifier, guid) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
const buffer = await browserWindow.webContents.session.getBlobData(
|
||||
identifier
|
||||
);
|
||||
const buffer =
|
||||
await browserWindow.webContents.session.getBlobData(identifier);
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-getBlobData-completed" + guid,
|
||||
buffer.buffer
|
||||
buffer.buffer,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -257,7 +257,7 @@ export = (socket: Socket) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-getCacheSize-completed" + guid,
|
||||
size
|
||||
size,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -267,7 +267,7 @@ export = (socket: Socket) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-getPreloads-completed" + guid,
|
||||
preloads
|
||||
preloads,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -277,7 +277,7 @@ export = (socket: Socket) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-getUserAgent-completed" + guid,
|
||||
userAgent
|
||||
userAgent,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -287,7 +287,7 @@ export = (socket: Socket) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-resolveProxy-completed" + guid,
|
||||
proxy
|
||||
proxy,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -314,9 +314,9 @@ export = (socket: Socket) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
browserWindow.webContents.session.setUserAgent(
|
||||
userAgent,
|
||||
acceptLanguages
|
||||
acceptLanguages,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on(
|
||||
@@ -328,17 +328,17 @@ export = (socket: Socket) => {
|
||||
session.webRequest.onBeforeRequest(filter, (details, callback) => {
|
||||
socket.emit(
|
||||
`webContents-session-webRequest-onBeforeRequest${id}`,
|
||||
details
|
||||
details,
|
||||
);
|
||||
// Listen for a response from C# to continue the request
|
||||
electronSocket.once(
|
||||
`webContents-session-webRequest-onBeforeRequest-response${id}`,
|
||||
(response) => {
|
||||
callback(response);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on("register-webContents-session-cookies-changed", (id) => {
|
||||
@@ -353,7 +353,7 @@ export = (socket: Socket) => {
|
||||
cause,
|
||||
removed,
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -363,7 +363,7 @@ export = (socket: Socket) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-cookies-get-completed" + guid,
|
||||
cookies
|
||||
cookies,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -381,9 +381,9 @@ export = (socket: Socket) => {
|
||||
await browserWindow.webContents.session.cookies.remove(url, name);
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-cookies-remove-completed" + guid
|
||||
"webContents-session-cookies-remove-completed" + guid,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on("webContents-session-cookies-flushStore", async (id, guid) => {
|
||||
@@ -391,7 +391,7 @@ export = (socket: Socket) => {
|
||||
await browserWindow.webContents.session.cookies.flushStore();
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-cookies-flushStore-completed" + guid
|
||||
"webContents-session-cookies-flushStore-completed" + guid,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -441,7 +441,7 @@ export = (socket: Socket) => {
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-getAllExtensions-completed",
|
||||
chromeExtensionInfo
|
||||
chromeExtensionInfo,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -456,87 +456,108 @@ export = (socket: Socket) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
const extension = await browserWindow.webContents.session.loadExtension(
|
||||
path,
|
||||
{ allowFileAccess: allowFileAccess }
|
||||
{ allowFileAccess: allowFileAccess },
|
||||
);
|
||||
|
||||
electronSocket.emit(
|
||||
"webContents-session-loadExtension-completed",
|
||||
extension
|
||||
extension,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
socket.on('webContents-getZoomFactor', (id) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
const text = browserWindow.webContents.getZoomFactor();
|
||||
electronSocket.emit('webContents-getZoomFactor-completed', text);
|
||||
});
|
||||
socket.on("webContents-getZoomFactor", (id) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
const text = browserWindow.webContents.getZoomFactor();
|
||||
electronSocket.emit("webContents-getZoomFactor-completed", text);
|
||||
});
|
||||
|
||||
socket.on('webContents-setZoomFactor', (id, factor) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
browserWindow.webContents.setZoomFactor(factor);
|
||||
});
|
||||
socket.on("webContents-setZoomFactor", (id, factor) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
browserWindow.webContents.setZoomFactor(factor);
|
||||
});
|
||||
|
||||
socket.on('webContents-getZoomLevel', (id) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
const content = browserWindow.webContents.getZoomLevel();
|
||||
electronSocket.emit('webContents-getZoomLevel-completed', content);
|
||||
});
|
||||
socket.on("webContents-getZoomLevel", (id) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
const content = browserWindow.webContents.getZoomLevel();
|
||||
electronSocket.emit("webContents-getZoomLevel-completed", content);
|
||||
});
|
||||
|
||||
socket.on('webContents-setZoomLevel', (id, level) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
browserWindow.webContents.setZoomLevel(level);
|
||||
});
|
||||
socket.on("webContents-setZoomLevel", (id, level) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
browserWindow.webContents.setZoomLevel(level);
|
||||
});
|
||||
|
||||
socket.on('webContents-setVisualZoomLevelLimits', async (id, minimumLevel, maximumLevel) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
await browserWindow.webContents.setVisualZoomLevelLimits(minimumLevel, maximumLevel);
|
||||
electronSocket.emit('webContents-setVisualZoomLevelLimits-completed');
|
||||
});
|
||||
socket.on(
|
||||
"webContents-setVisualZoomLevelLimits",
|
||||
async (id, minimumLevel, maximumLevel) => {
|
||||
const browserWindow = getWindowById(id);
|
||||
await browserWindow.webContents.setVisualZoomLevelLimits(
|
||||
minimumLevel,
|
||||
maximumLevel,
|
||||
);
|
||||
electronSocket.emit("webContents-setVisualZoomLevelLimits-completed");
|
||||
},
|
||||
);
|
||||
|
||||
socket.on("webContents-toggleDevTools", (id) => {
|
||||
getWindowById(id).webContents.toggleDevTools();
|
||||
});
|
||||
socket.on("webContents-toggleDevTools", (id) => {
|
||||
getWindowById(id).webContents.toggleDevTools();
|
||||
});
|
||||
|
||||
socket.on("webContents-closeDevTools", (id) => {
|
||||
getWindowById(id).webContents.closeDevTools();
|
||||
});
|
||||
socket.on("webContents-closeDevTools", (id) => {
|
||||
getWindowById(id).webContents.closeDevTools();
|
||||
});
|
||||
|
||||
socket.on("webContents-isDevToolsOpened", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit('webContents-isDevToolsOpened-completed', browserWindow.webContents.isDevToolsOpened());
|
||||
});
|
||||
socket.on("webContents-isDevToolsOpened", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit(
|
||||
"webContents-isDevToolsOpened-completed",
|
||||
browserWindow.webContents.isDevToolsOpened(),
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("webContents-isDevToolsFocused", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit('webContents-isDevToolsFocused-completed', browserWindow.webContents.isDevToolsFocused());
|
||||
});
|
||||
socket.on("webContents-isDevToolsFocused", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit(
|
||||
"webContents-isDevToolsFocused-completed",
|
||||
browserWindow.webContents.isDevToolsFocused(),
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("webContents-setAudioMuted", (id, muted) => {
|
||||
getWindowById(id).webContents.setAudioMuted(muted);
|
||||
});
|
||||
socket.on("webContents-setAudioMuted", (id, muted) => {
|
||||
getWindowById(id).webContents.setAudioMuted(muted);
|
||||
});
|
||||
|
||||
socket.on("webContents-isAudioMuted", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit('webContents-isAudioMuted-completed', browserWindow.webContents.isAudioMuted());
|
||||
});
|
||||
socket.on("webContents-isAudioMuted", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit(
|
||||
"webContents-isAudioMuted-completed",
|
||||
browserWindow.webContents.isAudioMuted(),
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("webContents-isCurrentlyAudible", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit('webContents-isCurrentlyAudible-completed', browserWindow.webContents.isCurrentlyAudible());
|
||||
});
|
||||
socket.on("webContents-isCurrentlyAudible", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit(
|
||||
"webContents-isCurrentlyAudible-completed",
|
||||
browserWindow.webContents.isCurrentlyAudible(),
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("webContents-getUserAgent", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit('webContents-getUserAgent-completed', browserWindow.webContents.getUserAgent());
|
||||
});
|
||||
socket.on("webContents-getUserAgent", function (id) {
|
||||
const browserWindow = getWindowById(id);
|
||||
electronSocket.emit(
|
||||
"webContents-getUserAgent-completed",
|
||||
browserWindow.webContents.getUserAgent(),
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("webContents-setUserAgent", (id, userAgent) => {
|
||||
getWindowById(id).webContents.setUserAgent(userAgent);
|
||||
});
|
||||
socket.on("webContents-setUserAgent", (id, userAgent) => {
|
||||
getWindowById(id).webContents.setUserAgent(userAgent);
|
||||
});
|
||||
|
||||
function getWindowById(
|
||||
id: number
|
||||
function getWindowById(
|
||||
id: number,
|
||||
): Electron.BrowserWindow | Electron.BrowserView {
|
||||
if (id >= 1000) {
|
||||
return browserViewMediateService(id - 1000);
|
||||
|
||||
199
src/ElectronNET.Host/logger.js
Normal file
199
src/ElectronNET.Host/logger.js
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Environment-aware logging utility for Electron.NET
|
||||
*
|
||||
* Provides structured logging with log levels that respect the environment.
|
||||
* Preserves console.time/timeEnd for performance measurements.
|
||||
*/
|
||||
|
||||
// Log levels
|
||||
const LogLevel = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
SILENT: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect the current environment based on various indicators
|
||||
* @returns {'development'|'production'|'debug'} The detected environment
|
||||
*/
|
||||
function detectEnvironment() {
|
||||
// Check for unpacked/development mode flags
|
||||
const args = process.argv.join(" ").toLowerCase();
|
||||
if (
|
||||
args.includes("--unpackeddotnet") ||
|
||||
args.includes("--unpackedelectron") ||
|
||||
args.includes("--unpackeddotnetsignalr")
|
||||
) {
|
||||
return "development";
|
||||
}
|
||||
|
||||
// Check NODE_ENV
|
||||
const nodeEnv = process.env.NODE_ENV?.toLowerCase();
|
||||
if (nodeEnv === "development" || nodeEnv === "dev") {
|
||||
return "development";
|
||||
}
|
||||
|
||||
// Check for debugger
|
||||
if (
|
||||
process.execArgv.some(
|
||||
(arg) => arg.includes("inspect") || arg.includes("debug"),
|
||||
)
|
||||
) {
|
||||
return "debug";
|
||||
}
|
||||
|
||||
// Default to production for packaged apps
|
||||
return "production";
|
||||
}
|
||||
|
||||
// Determine current environment and default log level
|
||||
const environment = detectEnvironment();
|
||||
const defaultLogLevels = {
|
||||
debug: LogLevel.DEBUG,
|
||||
development: LogLevel.INFO,
|
||||
production: LogLevel.WARN,
|
||||
};
|
||||
|
||||
let currentLogLevel = defaultLogLevels[environment];
|
||||
|
||||
/**
|
||||
* Set the current log level
|
||||
* @param {number} level - LogLevel enum value
|
||||
*/
|
||||
function setLogLevel(level) {
|
||||
currentLogLevel = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current log level
|
||||
* @returns {number} Current LogLevel
|
||||
*/
|
||||
function getLogLevel() {
|
||||
return currentLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current environment
|
||||
* @returns {string} Current environment name
|
||||
*/
|
||||
function getEnvironment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a log level should be output
|
||||
* @param {number} level - LogLevel to check
|
||||
* @returns {boolean} True if the level should be logged
|
||||
*/
|
||||
function shouldLog(level) {
|
||||
return level >= currentLogLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe wrapper for console methods that catches EPIPE errors
|
||||
*/
|
||||
const safeConsole = {
|
||||
log: (...args) => {
|
||||
try {
|
||||
console.log(...args);
|
||||
} catch (e) {
|
||||
// Ignore EPIPE errors when console is detached
|
||||
}
|
||||
},
|
||||
warn: (...args) => {
|
||||
try {
|
||||
console.warn(...args);
|
||||
} catch (e) {
|
||||
// Ignore EPIPE errors when console is detached
|
||||
}
|
||||
},
|
||||
error: (...args) => {
|
||||
try {
|
||||
console.error(...args);
|
||||
} catch (e) {
|
||||
// Ignore EPIPE errors when console is detached
|
||||
}
|
||||
},
|
||||
// Preserve timing functions as-is
|
||||
time: (label) => {
|
||||
try {
|
||||
console.time(label);
|
||||
} catch (e) {
|
||||
// Ignore EPIPE errors
|
||||
}
|
||||
},
|
||||
timeEnd: (label) => {
|
||||
try {
|
||||
console.timeEnd(label);
|
||||
} catch (e) {
|
||||
// Ignore EPIPE errors
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Logger with environment-aware log levels
|
||||
*/
|
||||
const logger = {
|
||||
/**
|
||||
* Log a debug message (only in debug mode)
|
||||
*/
|
||||
debug: (...args) => {
|
||||
if (shouldLog(LogLevel.DEBUG)) {
|
||||
safeConsole.log("[DEBUG]", ...args);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Log an info message
|
||||
*/
|
||||
info: (...args) => {
|
||||
if (shouldLog(LogLevel.INFO)) {
|
||||
safeConsole.log("[INFO]", ...args);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Log a warning message
|
||||
*/
|
||||
warn: (...args) => {
|
||||
if (shouldLog(LogLevel.WARN)) {
|
||||
safeConsole.warn("[WARN]", ...args);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Log an error message
|
||||
*/
|
||||
error: (...args) => {
|
||||
if (shouldLog(LogLevel.ERROR)) {
|
||||
safeConsole.error("[ERROR]", ...args);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Start a timing measurement (always logged)
|
||||
*/
|
||||
time: (label) => {
|
||||
safeConsole.time(label);
|
||||
},
|
||||
|
||||
/**
|
||||
* End a timing measurement (always logged)
|
||||
*/
|
||||
timeEnd: (label) => {
|
||||
safeConsole.timeEnd(label);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
logger,
|
||||
safeConsole,
|
||||
LogLevel,
|
||||
setLogLevel,
|
||||
getLogLevel,
|
||||
getEnvironment,
|
||||
shouldLog,
|
||||
};
|
||||
@@ -5,6 +5,8 @@ const path = require('path');
|
||||
const cProcess = require('child_process').spawn;
|
||||
const portscanner = require('portscanner');
|
||||
const { imageSize } = require('image-size');
|
||||
const { logger } = require('./logger');
|
||||
|
||||
let io, server, browserWindows, ipc, apiProcess, loadURL;
|
||||
let appApi, menu, dialogApi, notification, tray, webContents;
|
||||
let globalShortcut, shellApi, screen, clipboard, autoUpdater;
|
||||
@@ -14,6 +16,9 @@ let processInfo;
|
||||
let splashScreen;
|
||||
let nativeTheme;
|
||||
let dock;
|
||||
let desktopCapturer;
|
||||
let electronHostHook;
|
||||
let touchBar;
|
||||
let launchFile;
|
||||
let launchUrl;
|
||||
let processApi;
|
||||
@@ -22,15 +27,24 @@ let manifestJsonFileName = 'package.json';
|
||||
let unpackedelectron = false;
|
||||
let unpackeddotnet = false;
|
||||
let dotnetpacked = false;
|
||||
let unpackeddotnetsignalr = false;
|
||||
let dotnetpackedsignalr = false;
|
||||
let electronforcedport;
|
||||
let electronUrl;
|
||||
|
||||
if (app.commandLine.hasSwitch('manifest')) {
|
||||
manifestJsonFileName = app.commandLine.getSwitchValue('manifest');
|
||||
}
|
||||
|
||||
console.log('Entry!!!: ');
|
||||
|
||||
if (app.commandLine.hasSwitch('unpackedelectron')) {
|
||||
// Check for SignalR modes first (these take precedence)
|
||||
if (app.commandLine.hasSwitch('unpackeddotnetsignalr')) {
|
||||
unpackeddotnetsignalr = true;
|
||||
}
|
||||
else if (app.commandLine.hasSwitch('dotnetpackedsignalr')) {
|
||||
dotnetpackedsignalr = true;
|
||||
}
|
||||
// Then check legacy modes
|
||||
else if (app.commandLine.hasSwitch('unpackedelectron')) {
|
||||
unpackedelectron = true;
|
||||
}
|
||||
else if (app.commandLine.hasSwitch('unpackeddotnet')) {
|
||||
@@ -44,6 +58,17 @@ if (app.commandLine.hasSwitch('electronforcedport')) {
|
||||
electronforcedport = app.commandLine.getSwitchValue('electronforcedport');
|
||||
}
|
||||
|
||||
let authToken;
|
||||
if (app.commandLine.hasSwitch('authtoken')) {
|
||||
authToken = app.commandLine.getSwitchValue('authtoken');
|
||||
// Store in global for access by browser windows
|
||||
global.authToken = authToken;
|
||||
}
|
||||
|
||||
if (app.commandLine.hasSwitch('electronurl')) {
|
||||
electronUrl = app.commandLine.getSwitchValue('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 {
|
||||
@@ -60,11 +85,11 @@ try {
|
||||
try { app.exit(0); } catch (err) { process.exit(0); }
|
||||
}
|
||||
} else {
|
||||
console.warn('custom_main.js found but no onStartup function exported.');
|
||||
logger.warn('custom_main.js found but no onStartup function exported.');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error while executing custom_main.js:', err);
|
||||
logger.error('Error while executing custom_main.js:', err);
|
||||
}
|
||||
|
||||
const currentPath = __dirname;
|
||||
@@ -73,7 +98,7 @@ let manifestJsonFilePath = path.join(currentPath, manifestJsonFileName);
|
||||
|
||||
// if running unpackedelectron, lets change the path
|
||||
if (unpackedelectron || unpackeddotnet) {
|
||||
console.log('unpackedelectron! dir: ' + currentPath);
|
||||
logger.debug('Running in unpacked mode, dir: ' + currentPath);
|
||||
|
||||
manifestJsonFilePath = path.join(currentPath, manifestJsonFileName);
|
||||
currentBinPath = path.join(currentPath, '../'); // go to project directory
|
||||
@@ -140,7 +165,10 @@ function getForwardedArgs() {
|
||||
|
||||
const forwardedArgs = getForwardedArgs();
|
||||
|
||||
app.on('ready', () => {
|
||||
app.on('ready', async () => {
|
||||
// Start overall startup timer
|
||||
logger.time('[Startup] Total Electron Startup');
|
||||
|
||||
// Fix ERR_UNKNOWN_URL_SCHEME using file protocol
|
||||
// https://github.com/electron/electron/issues/23757
|
||||
////protocol.registerFileProtocol('file', (request, callback) => {
|
||||
@@ -152,8 +180,41 @@ app.on('ready', () => {
|
||||
startSplashScreen();
|
||||
}
|
||||
|
||||
// Check if we're using SignalR-based startup
|
||||
// SignalR mode is activated by --unpackeddotnetsignalr or --dotnetpackedsignalr flags
|
||||
// .NET passes the actual server URL via --electronurl parameter (no port scanning needed)
|
||||
if (unpackeddotnetsignalr || dotnetpackedsignalr) {
|
||||
if (!electronUrl) {
|
||||
logger.error('[Electron] ERROR: SignalR mode requires --electronUrl parameter');
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a temporary invisible window to keep Electron alive during startup.
|
||||
// Without any windows, Electron would quit immediately on macOS.
|
||||
// This will be destroyed once the first real window is created.
|
||||
const { BrowserWindow } = require('electron');
|
||||
const keepAliveWindow = new BrowserWindow({
|
||||
show: false,
|
||||
width: 1,
|
||||
height: 1
|
||||
});
|
||||
|
||||
// Destroy the keep-alive window when the first real window is created
|
||||
app.once('browser-window-created', (event, window) => {
|
||||
if (keepAliveWindow && !keepAliveWindow.isDestroyed()) {
|
||||
keepAliveWindow.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
await startSignalRApiBridge(electronUrl);
|
||||
logger.timeEnd('[Startup] Total Electron Startup');
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy socket.io startup
|
||||
if (electronforcedport) {
|
||||
console.log('Electron Socket IO (forced) Port: ' + electronforcedport);
|
||||
logger.info('Electron Socket IO (forced) Port: ' + electronforcedport);
|
||||
startSocketApiBridge(electronforcedport);
|
||||
return;
|
||||
}
|
||||
@@ -166,31 +227,47 @@ app.on('ready', () => {
|
||||
|
||||
// hostname needs to be localhost, otherwise Windows Firewall will be triggered.
|
||||
portscanner.findAPortNotInUse(defaultElectronPort, 65535, 'localhost', function (error, port) {
|
||||
console.log('Electron Socket IO Port: ' + port);
|
||||
logger.info('Electron Socket IO Port: ' + port);
|
||||
startSocketApiBridge(port);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('quit', async (event, exitCode) => {
|
||||
try {
|
||||
server.close();
|
||||
server.closeAllConnections();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
apiProcess?.kill();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
try {
|
||||
if (io && typeof io.close === 'function') {
|
||||
io.close();
|
||||
// Clean up Socket.IO resources (legacy mode only)
|
||||
if (typeof server !== 'undefined' && server) {
|
||||
try {
|
||||
server.close();
|
||||
server.closeAllConnections();
|
||||
} catch (e) {
|
||||
logger.error('Error closing Socket.IO server:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up API process (Socket.IO mode only)
|
||||
if (typeof apiProcess !== 'undefined' && apiProcess) {
|
||||
try {
|
||||
apiProcess.kill();
|
||||
} catch (e) {
|
||||
logger.error('Error killing API process:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up Socket.IO connection (legacy mode only)
|
||||
if (typeof io !== 'undefined' && io && typeof io.close === 'function') {
|
||||
try {
|
||||
io.close();
|
||||
} catch (e) {
|
||||
logger.error('Error closing Socket.IO connection:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up SignalR connection (SignalR mode only)
|
||||
if (global['electronsignalr'] && typeof global['electronsignalr'].connection !== 'undefined') {
|
||||
try {
|
||||
await global['electronsignalr'].connection.stop();
|
||||
} catch (e) {
|
||||
logger.error('Error closing SignalR connection:', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -246,9 +323,7 @@ function startSplashScreen() {
|
||||
// it's an image, so we can compute the desired splash screen size
|
||||
imageSize(imageFile, (error, dimensions) => {
|
||||
if (error) {
|
||||
console.log(`load splashscreen error:`);
|
||||
console.error(error);
|
||||
|
||||
logger.error(`load splashscreen error:`, error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
@@ -259,7 +334,7 @@ function startSplashScreen() {
|
||||
function startSocketApiBridge(port) {
|
||||
// instead of 'require('socket.io')(port);' we need to use this workaround
|
||||
// otherwise the Windows Firewall will be triggered
|
||||
console.log('Electron Socket: starting...');
|
||||
logger.debug('Electron Socket: starting...');
|
||||
server = require('http').createServer();
|
||||
const { Server } = require('socket.io');
|
||||
let hostHook;
|
||||
@@ -271,7 +346,7 @@ function startSocketApiBridge(port) {
|
||||
|
||||
server.listen(port, 'localhost');
|
||||
server.on('listening', function () {
|
||||
console.log('Electron Socket: listening on port %s at %s', server.address().port, server.address().address);
|
||||
logger.info('Electron Socket: listening on port %s at %s', server.address().port, server.address().address);
|
||||
// Now that socket connection is established, we can guarantee port will not be open for portscanner
|
||||
if (unpackedelectron) {
|
||||
startAspCoreBackendUnpackaged(port);
|
||||
@@ -286,9 +361,9 @@ function startSocketApiBridge(port) {
|
||||
|
||||
// @ts-ignore
|
||||
io.on('connection', (socket) => {
|
||||
console.log('Electron Socket: connected!');
|
||||
logger.info('Electron Socket: connected!');
|
||||
socket.on('disconnect', function (reason) {
|
||||
console.log('Got disconnect! Reason: ' + reason);
|
||||
logger.debug('Got disconnect! Reason: ' + reason);
|
||||
try {
|
||||
////console.log('requireCache');
|
||||
////console.log(require.cache['electron-host-hook']);
|
||||
@@ -299,7 +374,7 @@ function startSocketApiBridge(port) {
|
||||
hostHook = undefined;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
logger.error(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -308,7 +383,7 @@ function startSocketApiBridge(port) {
|
||||
global['electronsocket'].setMaxListeners(0);
|
||||
}
|
||||
|
||||
console.log('Electron Socket: loading components...');
|
||||
logger.debug('Electron Socket: loading components...');
|
||||
|
||||
if (appApi === undefined) appApi = require('./api/app')(socket, app);
|
||||
if (browserWindows === undefined) browserWindows = require('./api/browserWindows')(socket, app);
|
||||
@@ -366,13 +441,123 @@ function startSocketApiBridge(port) {
|
||||
hostHook.onHostReady();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
logger.error(error.message);
|
||||
}
|
||||
|
||||
console.log('Electron Socket: startup complete.');
|
||||
logger.info('Electron Socket: startup complete.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the SignalR API bridge for .NET-first SignalR mode.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Connect to SignalR hub at /electron-hub endpoint
|
||||
* 2. Register as Electron client
|
||||
* 3. Load all API modules (same modules as Socket.IO mode)
|
||||
* 4. Signal 'electron-host-ready' to .NET to trigger app ready callback
|
||||
*
|
||||
* This ensures .NET doesn't call the app ready callback until all API modules
|
||||
* are loaded and ready to handle requests from .NET code.
|
||||
*/
|
||||
async function startSignalRApiBridge(baseUrl) {
|
||||
const { SignalRBridge } = require('./api/signalr-bridge');
|
||||
const hubUrl = `${baseUrl}/electron-hub`;
|
||||
|
||||
// Pass the authentication token to the SignalR bridge
|
||||
const signalRBridge = new SignalRBridge(hubUrl, global.authToken);
|
||||
|
||||
try {
|
||||
logger.time('[Startup] SignalR Connection');
|
||||
const connected = await signalRBridge.connect();
|
||||
logger.timeEnd('[Startup] SignalR Connection');
|
||||
|
||||
if (!connected) {
|
||||
logger.error('[SignalRBridge] Failed to connect to SignalR hub');
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the bridge globally for API access
|
||||
global['electronsignalr'] = signalRBridge;
|
||||
|
||||
// Load API modules in parallel for faster startup
|
||||
logger.time('[Startup] Module Loading');
|
||||
|
||||
// Define module loaders - each returns the initialized module
|
||||
const loadModules = () => {
|
||||
const modules = {};
|
||||
|
||||
// Load all modules in parallel using Promise.all
|
||||
return Promise.all([
|
||||
// Critical modules (always needed)
|
||||
Promise.resolve().then(() => modules.appApi = require('./api/app')(signalRBridge, app)),
|
||||
Promise.resolve().then(() => modules.browserWindows = require('./api/browserWindows')(signalRBridge, app)),
|
||||
Promise.resolve().then(() => modules.commandLine = require('./api/commandLine')(signalRBridge, app)),
|
||||
Promise.resolve().then(() => modules.webContents = require('./api/webContents')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.ipc = require('./api/ipc')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.menu = require('./api/menu')(signalRBridge)),
|
||||
|
||||
// Secondary modules (commonly used)
|
||||
Promise.resolve().then(() => modules.dialogApi = require('./api/dialog')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.notification = require('./api/notification')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.shellApi = require('./api/shell')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.clipboard = require('./api/clipboard')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.screen = require('./api/screen')(signalRBridge)),
|
||||
|
||||
// Utility modules (less frequently used)
|
||||
Promise.resolve().then(() => modules.autoUpdater = require('./api/autoUpdater')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.tray = require('./api/tray')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.globalShortcut = require('./api/globalShortcut')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.nativeTheme = require('./api/nativeTheme')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.powerMonitor = require('./api/powerMonitor')(signalRBridge)),
|
||||
Promise.resolve().then(() => modules.processApi = require('./api/process')(signalRBridge)),
|
||||
|
||||
// Platform-specific modules
|
||||
Promise.resolve().then(() => {
|
||||
if (process.platform === 'darwin') {
|
||||
modules.dock = require('./api/dock')(signalRBridge, app);
|
||||
}
|
||||
})
|
||||
]).then(() => modules);
|
||||
};
|
||||
|
||||
const modules = await loadModules();
|
||||
|
||||
// Assign to global variables (for backward compatibility)
|
||||
if (appApi === undefined) appApi = modules.appApi;
|
||||
if (browserWindows === undefined) browserWindows = modules.browserWindows;
|
||||
if (commandLine === undefined) commandLine = modules.commandLine;
|
||||
if (autoUpdater === undefined) autoUpdater = modules.autoUpdater;
|
||||
if (ipc === undefined) ipc = modules.ipc;
|
||||
if (menu === undefined) menu = modules.menu;
|
||||
if (dialogApi === undefined) dialogApi = modules.dialogApi;
|
||||
if (notification === undefined) notification = modules.notification;
|
||||
if (tray === undefined) tray = modules.tray;
|
||||
if (webContents === undefined) webContents = modules.webContents;
|
||||
if (globalShortcut === undefined) globalShortcut = modules.globalShortcut;
|
||||
if (clipboard === undefined) clipboard = modules.clipboard;
|
||||
if (screen === undefined) screen = modules.screen;
|
||||
if (shellApi === undefined) shellApi = modules.shellApi;
|
||||
if (nativeTheme === undefined) nativeTheme = modules.nativeTheme;
|
||||
if (powerMonitor === undefined) powerMonitor = modules.powerMonitor;
|
||||
if (dock === undefined && modules.dock) dock = modules.dock;
|
||||
if (processApi === undefined) processApi = modules.processApi;
|
||||
|
||||
logger.timeEnd('[Startup] Module Loading');
|
||||
|
||||
// Signal to .NET that Electron is fully ready (API modules loaded)
|
||||
logger.time('[Startup] Host Ready Signal');
|
||||
await signalRBridge.emit('electron-host-ready');
|
||||
logger.timeEnd('[Startup] Host Ready Signal');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[SignalRBridge] Error during startup:', error);
|
||||
logger.error('[SignalRBridge] Stack:', error.stack);
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
function startAspCoreBackend(electronPort) {
|
||||
startBackend();
|
||||
|
||||
@@ -395,11 +580,11 @@ function startAspCoreBackend(electronPort) {
|
||||
|
||||
let binFilePath = path.join(currentBinPath, binaryFile);
|
||||
var options = { cwd: currentBinPath };
|
||||
console.log('Starting backend with parameters:', parameters.join(' '));
|
||||
logger.debug('Starting backend with parameters:', parameters.join(' '));
|
||||
apiProcess = cProcess(binFilePath, parameters, options);
|
||||
|
||||
apiProcess.stdout.on('data', (data) => {
|
||||
console.log(`stdout: ${data.toString()}`);
|
||||
logger.debug(`stdout: ${data.toString()}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -425,11 +610,11 @@ function startAspCoreBackendUnpackaged(electronPort) {
|
||||
|
||||
let binFilePath = path.join(currentBinPath, binaryFile);
|
||||
var options = { cwd: currentBinPath };
|
||||
console.log('Starting backend (unpackaged) with parameters:', parameters.join(' '));
|
||||
logger.debug('Starting backend (unpackaged) with parameters:', parameters.join(' '));
|
||||
apiProcess = cProcess(binFilePath, parameters, options);
|
||||
|
||||
apiProcess.stdout.on('data', (data) => {
|
||||
console.log(`stdout: ${data.toString()}`);
|
||||
logger.debug(`stdout: ${data.toString()}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
188
src/ElectronNET.Host/package-lock.json
generated
188
src/ElectronNET.Host/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
@@ -252,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",
|
||||
@@ -367,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",
|
||||
@@ -1117,6 +1164,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",
|
||||
@@ -1169,6 +1234,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",
|
||||
@@ -1614,9 +1689,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"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": {
|
||||
@@ -1735,6 +1810,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",
|
||||
@@ -1912,6 +2007,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",
|
||||
@@ -1927,12 +2034,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",
|
||||
@@ -1955,6 +2067,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",
|
||||
@@ -2045,6 +2163,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",
|
||||
@@ -2213,6 +2337,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",
|
||||
@@ -2280,6 +2434,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",
|
||||
@@ -2289,6 +2453,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",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"author": "Gregor Biswanger, Florian Rappl",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"dasherize": "^2.0.0",
|
||||
"electron-host-hook": "file:./ElectronHostHook",
|
||||
"image-size": "^1.2.1",
|
||||
|
||||
23
src/ElectronNET.Samples.BlazorSignalR/Components/App.razor
Normal file
23
src/ElectronNET.Samples.BlazorSignalR/Components/App.razor
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<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-blazorsignalr.styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">ElectronNET.Samples.BlazorSignalR</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Rejoining the server...
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Failed to rejoin.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Retry
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
The session has been paused by the server.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible">
|
||||
Resume
|
||||
</button>
|
||||
<p class="components-resume-failed-visible">
|
||||
Failed to resume the session.<br />Please reload the page.
|
||||
</p>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -0,0 +1,157 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: white;
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: #6b9ed2;
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@page "/counter"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
@@ -0,0 +1,64 @@
|
||||
@page "/weather"
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates showing data.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th aria-label="Temperature in Celsius">Temp. (C)</th>
|
||||
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Simulate asynchronous loading to demonstrate streaming rendering
|
||||
await Task.Delay(500);
|
||||
|
||||
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = startDate.AddDays(index),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public int TemperatureC { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,11 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@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
|
||||
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||
<!-- Enable Electron.NET Dev Mode to use local packages -->
|
||||
<ElectronNetDevMode>true</ElectronNetDevMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\ElectronNET\build\ElectronNET.Core.props" Condition="$(ElectronNetDevMode)" />
|
||||
|
||||
<PropertyGroup Label="ElectronNetCommon">
|
||||
<Title>Electron.NET Blazor SignalR 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>
|
||||
<Company>Electron.NET</Company>
|
||||
<Copyright>Copyright © 2026, Electron.NET</Copyright>
|
||||
<ElectronVersion>30.4.0</ElectronVersion>
|
||||
<License>MIT</License>
|
||||
<ElectronSingleInstance>true</ElectronSingleInstance>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ElectronNET.API\ElectronNET.API.csproj" Condition="$(ElectronNetDevMode)" />
|
||||
<ProjectReference Include="..\ElectronNET.AspNet\ElectronNET.AspNet.csproj" Condition="$(ElectronNetDevMode)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ElectronNET.Core" Version="0.4.0" Condition="'$(ElectronNetDevMode)' != 'true'" />
|
||||
<PackageReference Include="ElectronNET.Core.AspNet" Version="0.4.0" Condition="'$(ElectronNetDevMode)' != 'true'" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\ElectronNET\build\ElectronNET.Core.targets" Condition="$(ElectronNetDevMode)" />
|
||||
|
||||
</Project>
|
||||
21
src/ElectronNET.Samples.BlazorSignalR/Program.Minimal.cs.txt
Normal file
21
src/ElectronNET.Samples.BlazorSignalR/Program.Minimal.cs.txt
Normal 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();
|
||||
95
src/ElectronNET.Samples.BlazorSignalR/Program.cs
Normal file
95
src/ElectronNET.Samples.BlazorSignalR/Program.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using ElectronNET.API;
|
||||
using ElectronNET.API.Entities;
|
||||
using ElectronNET.AspNet.Middleware;
|
||||
using ElectronNET.AspNet.Services;
|
||||
|
||||
var watch = new System.Diagnostics.Stopwatch();
|
||||
watch.Start();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
// Add CORS for SignalR
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("ElectronPolicy", policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
// Register Electron authentication service as singleton
|
||||
builder.Services.AddSingleton<IElectronAuthenticationService, ElectronAuthenticationService>();
|
||||
|
||||
builder.Services.AddElectron();
|
||||
|
||||
// Configure Electron.NET with SignalR mode
|
||||
// Note: Callback is registered now but executes after app starts
|
||||
IServiceProvider? serviceProvider = null;
|
||||
|
||||
builder.WebHost.UseElectron(args, async () =>
|
||||
{
|
||||
if (serviceProvider is null)
|
||||
{
|
||||
throw new InvalidOperationException("ServiceProvider not initialized. This callback should only execute after app.Build().");
|
||||
}
|
||||
|
||||
var options = new BrowserWindowOptions
|
||||
{
|
||||
Show = false,
|
||||
Width = 1200,
|
||||
Height = 800,
|
||||
IsRunningBlazor = true,
|
||||
};
|
||||
|
||||
// Log startup time using ILogger - serviceProvider is captured after app.Build()
|
||||
var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("Electron.Startup");
|
||||
logger.LogInformation("App startup time until Electron launch: {ElapsedMilliseconds} ms", watch.ElapsedMilliseconds);
|
||||
|
||||
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux())
|
||||
options.AutoHideMenuBar = true;
|
||||
|
||||
var browserWindow = await Electron.WindowManager.CreateWindowAsync(options);
|
||||
|
||||
browserWindow.OnReadyToShow += () => browserWindow.Show();
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
serviceProvider = app.Services; // Capture for use in Electron callback above
|
||||
|
||||
// Register authentication middleware FIRST (before routing, static files, etc.)
|
||||
app.UseMiddleware<ElectronAuthenticationMiddleware>();
|
||||
|
||||
// Enable routing
|
||||
app.UseRouting();
|
||||
|
||||
// Enable CORS
|
||||
app.UseCors("ElectronPolicy");
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
}
|
||||
|
||||
// Serve static files (CSS, JS, images, etc.)
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||
|
||||
// UseAntiforgery must be after UseRouting
|
||||
app.UseAntiforgery();
|
||||
|
||||
// Map SignalR hub for Electron communication
|
||||
app.MapHub<ElectronNET.AspNet.Hubs.ElectronHub>("/electron-hub");
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<ElectronNET.Samples.BlazorSignalR.Components.App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/refs/heads/master/packages/app-builder-lib/scheme.json",
|
||||
"compression": "maximum",
|
||||
"linux": {
|
||||
"target": [
|
||||
"tar.xz"
|
||||
],
|
||||
"executableArgs": [ "--no-sandbox" ],
|
||||
"artifactName": "${name}-${arch}-${version}.${ext}"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": "x64"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:0",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ELECTRON_USE_SIGNALR": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/ElectronNET.Samples.BlazorSignalR/README.md
Normal file
153
src/ElectronNET.Samples.BlazorSignalR/README.md
Normal 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
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.AspNetCore.SignalR": "Warning",
|
||||
"Microsoft.AspNetCore.Http.Connections": "Warning",
|
||||
"Microsoft.AspNetCore.Watch": "Warning",
|
||||
"Microsoft.AspNetCore.Watch.BrowserRefresh": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/ElectronNET.Samples.BlazorSignalR/appsettings.json
Normal file
9
src/ElectronNET.Samples.BlazorSignalR/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
60
src/ElectronNET.Samples.BlazorSignalR/wwwroot/app.css
Normal file
60
src/ElectronNET.Samples.BlazorSignalR/wwwroot/app.css
Normal file
@@ -0,0 +1,60 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #006bb7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
BIN
src/ElectronNET.Samples.BlazorSignalR/wwwroot/favicon.png
Normal file
BIN
src/ElectronNET.Samples.BlazorSignalR/wwwroot/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
4085
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
4085
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
1
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
6
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4084
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
4084
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
597
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
597
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
594
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
594
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
@@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2024 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5402
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
5402
src/ElectronNET.Samples.BlazorSignalR/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user