diff --git a/src/ElectronNET.API/Common/ProcessRunner.cs b/src/ElectronNET.API/Common/ProcessRunner.cs index a8b98067..d561020e 100644 --- a/src/ElectronNET.API/Common/ProcessRunner.cs +++ b/src/ElectronNET.API/Common/ProcessRunner.cs @@ -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 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 environmentVariables) { this.CommandLine = commandLineArgs; this.WorkingFolder = workingDirectory; @@ -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); } diff --git a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs index c74dad80..4d778572 100644 --- a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs +++ b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs @@ -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 @@ protected override async Task StartCore() 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 @@ protected override Task StopCore() 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 @@ private async Task StartInternal(string startCmd, string args, string directoriy 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 @@ void Monitor_SocketIO_Failure(object sender, EventArgs e) 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 - { - [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 @@ void Monitor_SocketIO_Failure(object sender, EventArgs e) 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 @@ void Monitor_SocketIO_Failure(object sender, EventArgs e) Console.Error.WriteLine("[StartInternal]: Exception: " + ex); throw; } - finally - { - try - { - if (File.Exists(startupInfoPath)) - { - File.Delete(startupInfoPath); - } - } - catch - { - // best effort cleanup - } - } - } - - private static async Task 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) diff --git a/src/ElectronNET.Host/main.js b/src/ElectronNET.Host/main.js index ebf5fbef..09b3b015 100644 --- a/src/ElectronNET.Host/main.js +++ b/src/ElectronNET.Host/main.js @@ -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) => {