Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions src/ElectronNET.API/Common/ProcessRunner.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
namespace ElectronNET.Common
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
Expand Down Expand Up @@ -112,11 +111,6 @@ public string StandardError
public int? LastExitCode { get; private set; }

public bool Run(string exeFileName, string commandLineArgs, string workingDirectory)
{
return this.Run(exeFileName, commandLineArgs, workingDirectory, null);
}

public bool Run(string exeFileName, string commandLineArgs, string workingDirectory, IDictionary<string, string> environmentVariables)
{
this.CommandLine = commandLineArgs;
this.WorkingFolder = workingDirectory;
Expand All @@ -134,14 +128,6 @@ public bool Run(string exeFileName, string commandLineArgs, string workingDirect
WorkingDirectory = workingDirectory
};

if (environmentVariables != null)
{
foreach (var kv in environmentVariables)
{
startInfo.EnvironmentVariables[kv.Key] = kv.Value;
}
}

return this.Run(startInfo);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
namespace ElectronNET.Runtime.Services.ElectronProcess
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ElectronNET.Common;
Expand All @@ -20,8 +17,7 @@
[Localizable(false)]
internal class ElectronProcessActive : ElectronProcessBase
{
private const string AuthTokenEnvVar = "ELECTRONNET_AUTH_TOKEN";
private const string StartupInfoEnvVar = "ELECTRONNET_STARTUP_INFO";
private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at .* using ([a-f0-9]+)$");

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-14 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-14 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-14 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-14 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-26 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-26 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-15-intel API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-15-intel API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / ubuntu-24.04 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / ubuntu-24.04 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / ubuntu-24.04 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / ubuntu-24.04 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / ubuntu-22.04 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / ubuntu-22.04 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-26 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-26 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / ubuntu-22.04 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / ubuntu-22.04 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / windows-2025 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / windows-2025 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / windows-2022 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / windows-2022 API-38.2.2

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-15-intel API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / macos-15-intel API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / windows-2022 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / windows-2022 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / windows-2025 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 20 in src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs

View workflow job for this annotation

GitHub Actions / 2 / windows-2025 API-30.4.0

The type or namespace name 'Regex' could not be found (are you missing a using directive or an assembly reference?)

private readonly bool isUnpackaged;
private readonly string electronBinaryName;
Expand Down Expand Up @@ -96,23 +92,8 @@
workingDir = dir.FullName;
}

// Generate the auth token on the .NET side (256 bit entropy) and pass it
// to Electron via an environment variable. Electron will report the
// OS-selected port via a temporary handshake file - this avoids any
// dependency on parsing Electron's console output.
var authToken = CreateAuthToken();
var startupInfoPath = Path.Combine(
Path.GetTempPath(),
$"electronnet-startup-{Environment.ProcessId}-{Guid.NewGuid():N}.json");

// We don't await this in order to let the state transition to "Starting"
Task.Run(async () => await this.StartInternal(startCmd, args, workingDir, authToken, startupInfoPath).ConfigureAwait(false));
}

private static string CreateAuthToken()
{
var bytes = RandomNumberGenerator.GetBytes(32);
return Convert.ToHexString(bytes).ToLowerInvariant();
Task.Run(async () => await this.StartInternal(startCmd, args, workingDir).ConfigureAwait(false));
}

private void CheckRuntimeIdentifier()
Expand Down Expand Up @@ -177,7 +158,7 @@
return Task.CompletedTask;
}

private async Task StartInternal(string startCmd, string args, string directoriy, string authToken, string startupInfoPath)
private async Task StartInternal(string startCmd, string args, string directory)
{
var tcs = new TaskCompletionSource();
using var cts = new CancellationTokenSource(2 * 60_000); // cancel after 2 minutes
Expand All @@ -189,6 +170,23 @@
tcs.TrySetResult();
});

void Read_SocketIO_Parameters(object sender, string line)
{
// Look for "Electron Socket: listening on port %s at ..."
var match = extractor.Match(line);

if (match?.Success ?? false)
{
var port = int.Parse(match.Groups[1].Value);
var token = match.Groups[2].Value;

this.process.LineReceived -= Read_SocketIO_Parameters;
ElectronNetRuntime.ElectronAuthToken = token;
ElectronNetRuntime.ElectronSocketPort = port;
tcs.SetResult();
}
}

void Monitor_SocketIO_Failure(object sender, EventArgs e)
{
// We don't want to raise exceptions here - just pass the barrier
Expand All @@ -209,24 +207,10 @@

this.process = new ProcessRunner("ElectronRunner");
this.process.ProcessExited += Monitor_SocketIO_Failure;
this.process.LineReceived += Read_SocketIO_Parameters;
this.process.Run(startCmd, args, directoriy);

var env = new Dictionary<string, string>
{
[AuthTokenEnvVar] = authToken,
[StartupInfoEnvVar] = startupInfoPath,
};

this.process.Run(startCmd, args, directoriy, env);

// Wait for Electron to write the startup-info file (or for the process to die / timeout).
var waitTask = WaitForStartupInfoAsync(startupInfoPath, cts.Token);
var completed = await Task.WhenAny(waitTask, tcs.Task).ConfigureAwait(false);

int port = 0;
if (completed == waitTask && waitTask.Status == TaskStatus.RanToCompletion)
{
port = waitTask.Result;
}
await tcs.Task.ConfigureAwait(false);

Console.Error.WriteLine("[StartInternal]: after run:");

Expand All @@ -237,16 +221,9 @@

Task.Run(() => this.TransitionState(LifetimeState.Stopped));
}
else if (port > 0)
{
ElectronNetRuntime.ElectronAuthToken = authToken;
ElectronNetRuntime.ElectronSocketPort = port;
this.TransitionState(LifetimeState.Ready);
}
else
{
Console.Error.WriteLine("[StartInternal]: Did not receive Electron startup info before process exit/timeout.");
Task.Run(() => this.TransitionState(LifetimeState.Stopped));
this.TransitionState(LifetimeState.Ready);
}
}
catch (Exception ex)
Expand All @@ -256,63 +233,6 @@
Console.Error.WriteLine("[StartInternal]: Exception: " + ex);
throw;
}
finally
{
try
{
if (File.Exists(startupInfoPath))
{
File.Delete(startupInfoPath);
}
}
catch
{
// best effort cleanup
}
}
}

private static async Task<int> WaitForStartupInfoAsync(string startupInfoPath, CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (File.Exists(startupInfoPath))
{
var json = await File.ReadAllTextAsync(startupInfoPath, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(json))
{
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("port", out var portElement) &&
portElement.TryGetInt32(out var port) &&
port > 0)
{
return port;
}
}
}
}
catch (JsonException)
{
// File may be partially written / racing with the writer - retry.
}
catch (IOException)
{
// Same - transient races on file access; retry.
}

try
{
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
break;
}
}

return 0;
}

private void Process_Exited(object sender, EventArgs e)
Expand Down
35 changes: 4 additions & 31 deletions src/ElectronNET.Host/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,7 @@ let unpackeddotnet = false;
let dotnetpacked = false;
let electronforcedport;
let electronUrl;
// Auth token: prefer the value provided by the .NET host via environment variable
// (dotnet-first startup). Fall back to a freshly generated token so Electron can
// still be launched stand-alone (e.g. for debugging).
let authToken = process.env.ELECTRONNET_AUTH_TOKEN || randomUUID().split('-').join('');
// Path to a temporary handshake file. When set by the .NET host, Electron writes
// the OS-selected socket port into this file so .NET does not have to parse the
// console output.
const startupInfoPath = process.env.ELECTRONNET_STARTUP_INFO;
let authToken = randomUUID().split('-').join('');

if (app.commandLine.hasSwitch('manifest')) {
manifestJsonFileName = app.commandLine.getSwitchValue('manifest');
Expand Down Expand Up @@ -281,25 +274,7 @@ function startSocketApiBridge(port) {
server.listen(port, host);
server.on('listening', function () {
const addr = server.address();
console.info(`Electron Socket: listening on port ${addr.port} at ${addr.address}`);

// If the .NET host requested a startup-info handshake, write the selected
// port atomically (tmp + rename) so .NET can pick it up without parsing
// our console output. The auth token is intentionally NOT written to disk
// - the .NET host already knows it (it generated it).
if (startupInfoPath) {
try {
const payload = JSON.stringify({ port: addr.port, pid: process.pid });
const tmp = `${startupInfoPath}.tmp`;
const writeOptions = platform() === 'win32'
? { encoding: 'utf8' }
: { encoding: 'utf8', mode: 0o600 };
fs.writeFileSync(tmp, payload, writeOptions);
fs.renameSync(tmp, startupInfoPath);
} catch (err) {
console.error('Failed to write Electron startup info file:', err);
}
}
console.info(`Electron Socket: listening on port ${addr.port} at ${addr.address} using ${authToken}`);

// Now that socket connection is established, we can guarantee port will not be open for portscanner
if (unpackedelectron) {
Expand Down Expand Up @@ -431,8 +406,7 @@ function startAspCoreBackend(electronPort) {

let binFilePath = path.join(currentBinPath, binaryFile);
var options = { cwd: currentBinPath };
// Do not log the parameters: they include the auth token.
console.debug('Starting backend.');
console.debug('Starting backend with parameters:', parameters.join(' '));
apiProcess = cProcess(binFilePath, parameters, options);

apiProcess.stdout.on('data', (data) => {
Expand Down Expand Up @@ -462,8 +436,7 @@ function startAspCoreBackendUnpackaged(electronPort) {

let binFilePath = path.join(currentBinPath, binaryFile);
var options = { cwd: currentBinPath };
// Do not log the parameters: they include the auth token.
console.debug('Starting backend (unpackaged).');
console.debug('Starting backend (unpackaged) with parameters:', parameters.join(' '));
apiProcess = cProcess(binFilePath, parameters, options);

apiProcess.stdout.on('data', (data) => {
Expand Down
Loading