mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-07-09 02:07:47 +00:00
@@ -1,7 +1,6 @@
|
||||
namespace ElectronNET.Common
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
@@ -112,11 +111,6 @@
|
||||
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;
|
||||
@@ -134,14 +128,6 @@
|
||||
WorkingDirectory = workingDirectory
|
||||
};
|
||||
|
||||
if (environmentVariables != null)
|
||||
{
|
||||
foreach (var kv in environmentVariables)
|
||||
{
|
||||
startInfo.EnvironmentVariables[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return this.Run(startInfo);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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]+)$");
|
||||
|
||||
private readonly bool isUnpackaged;
|
||||
private readonly string electronBinaryName;
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:");
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
@@ -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) {
|
||||
@@ -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) => {
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user