11namespace ElectronNET . Runtime . Services . ElectronProcess
22{
33 using System ;
4+ using System . Collections . Generic ;
45 using System . ComponentModel ;
56 using System . Diagnostics ;
67 using System . IO ;
78 using System . Linq ;
89 using System . Runtime . InteropServices ;
9- using System . Text . RegularExpressions ;
10+ using System . Security . Cryptography ;
11+ using System . Text . Json ;
1012 using System . Threading ;
1113 using System . Threading . Tasks ;
1214 using ElectronNET . Common ;
1820 [ Localizable ( false ) ]
1921 internal class ElectronProcessActive : ElectronProcessBase
2022 {
21- private readonly Regex extractor = new Regex ( "^Electron Socket: listening on port (\\ d+) at .* using ([a-f0-9]+)$" ) ;
23+ private const string AuthTokenEnvVar = "ELECTRONNET_AUTH_TOKEN" ;
24+ private const string StartupInfoEnvVar = "ELECTRONNET_STARTUP_INFO" ;
25+
2226 private readonly bool isUnpackaged ;
2327 private readonly string electronBinaryName ;
2428 private readonly string extraArguments ;
@@ -92,8 +96,23 @@ protected override async Task StartCore()
9296 workingDir = dir . FullName ;
9397 }
9498
99+ // Generate the auth token on the .NET side (256 bit entropy) and pass it
100+ // to Electron via an environment variable. Electron will report the
101+ // OS-selected port via a temporary handshake file - this avoids any
102+ // dependency on parsing Electron's console output.
103+ var authToken = CreateAuthToken ( ) ;
104+ var startupInfoPath = Path . Combine (
105+ Path . GetTempPath ( ) ,
106+ $ "electronnet-startup-{ Environment . ProcessId } -{ Guid . NewGuid ( ) : N} .json") ;
107+
95108 // We don't await this in order to let the state transition to "Starting"
96- Task . Run ( async ( ) => await this . StartInternal ( startCmd , args , workingDir ) . ConfigureAwait ( false ) ) ;
109+ Task . Run ( async ( ) => await this . StartInternal ( startCmd , args , workingDir , authToken , startupInfoPath ) . ConfigureAwait ( false ) ) ;
110+ }
111+
112+ private static string CreateAuthToken ( )
113+ {
114+ var bytes = RandomNumberGenerator . GetBytes ( 32 ) ;
115+ return Convert . ToHexString ( bytes ) . ToLowerInvariant ( ) ;
97116 }
98117
99118 private void CheckRuntimeIdentifier ( )
@@ -158,7 +177,7 @@ protected override Task StopCore()
158177 return Task . CompletedTask ;
159178 }
160179
161- private async Task StartInternal ( string startCmd , string args , string directoriy )
180+ private async Task StartInternal ( string startCmd , string args , string directoriy , string authToken , string startupInfoPath )
162181 {
163182 var tcs = new TaskCompletionSource ( ) ;
164183 using var cts = new CancellationTokenSource ( 2 * 60_000 ) ; // cancel after 2 minutes
@@ -167,26 +186,9 @@ private async Task StartInternal(string startCmd, string args, string directoriy
167186 // Time is over - let's kill the process and move on
168187 this . process . Cancel ( ) ;
169188 // We don't want to raise exceptions here - just pass the barrier
170- tcs . SetResult ( ) ;
189+ tcs . TrySetResult ( ) ;
171190 } ) ;
172191
173- void Read_SocketIO_Parameters ( object sender , string line )
174- {
175- // Look for "Electron Socket: listening on port %s at ..."
176- var match = extractor . Match ( line ) ;
177-
178- if ( match ? . Success ?? false )
179- {
180- var port = int . Parse ( match . Groups [ 1 ] . Value ) ;
181- var token = match . Groups [ 2 ] . Value ;
182-
183- this . process . LineReceived -= Read_SocketIO_Parameters ;
184- ElectronNetRuntime . ElectronAuthToken = token ;
185- ElectronNetRuntime . ElectronSocketPort = port ;
186- tcs . SetResult ( ) ;
187- }
188- }
189-
190192 void Monitor_SocketIO_Failure ( object sender , EventArgs e )
191193 {
192194 // We don't want to raise exceptions here - just pass the barrier
@@ -196,7 +198,7 @@ void Monitor_SocketIO_Failure(object sender, EventArgs e)
196198 }
197199 else
198200 {
199- tcs . SetResult ( ) ;
201+ tcs . TrySetResult ( ) ;
200202 }
201203 }
202204
@@ -207,10 +209,24 @@ void Monitor_SocketIO_Failure(object sender, EventArgs e)
207209
208210 this . process = new ProcessRunner ( "ElectronRunner" ) ;
209211 this . process . ProcessExited += Monitor_SocketIO_Failure ;
210- this . process . LineReceived += Read_SocketIO_Parameters ;
211- this . process . Run ( startCmd , args , directoriy ) ;
212212
213- await tcs . Task . ConfigureAwait ( false ) ;
213+ var env = new Dictionary < string , string >
214+ {
215+ [ AuthTokenEnvVar ] = authToken ,
216+ [ StartupInfoEnvVar ] = startupInfoPath ,
217+ } ;
218+
219+ this . process . Run ( startCmd , args , directoriy , env ) ;
220+
221+ // Wait for Electron to write the startup-info file (or for the process to die / timeout).
222+ var waitTask = WaitForStartupInfoAsync ( startupInfoPath , cts . Token ) ;
223+ var completed = await Task . WhenAny ( waitTask , tcs . Task ) . ConfigureAwait ( false ) ;
224+
225+ int port = 0 ;
226+ if ( completed == waitTask && waitTask . Status == TaskStatus . RanToCompletion )
227+ {
228+ port = waitTask . Result ;
229+ }
214230
215231 Console . Error . WriteLine ( "[StartInternal]: after run:" ) ;
216232
@@ -221,10 +237,17 @@ void Monitor_SocketIO_Failure(object sender, EventArgs e)
221237
222238 Task . Run ( ( ) => this . TransitionState ( LifetimeState . Stopped ) ) ;
223239 }
224- else
240+ else if ( port > 0 )
225241 {
242+ ElectronNetRuntime . ElectronAuthToken = authToken ;
243+ ElectronNetRuntime . ElectronSocketPort = port ;
226244 this . TransitionState ( LifetimeState . Ready ) ;
227245 }
246+ else
247+ {
248+ Console . Error . WriteLine ( "[StartInternal]: Did not receive Electron startup info before process exit/timeout." ) ;
249+ Task . Run ( ( ) => this . TransitionState ( LifetimeState . Stopped ) ) ;
250+ }
228251 }
229252 catch ( Exception ex )
230253 {
@@ -233,6 +256,63 @@ void Monitor_SocketIO_Failure(object sender, EventArgs e)
233256 Console . Error . WriteLine ( "[StartInternal]: Exception: " + ex ) ;
234257 throw ;
235258 }
259+ finally
260+ {
261+ try
262+ {
263+ if ( File . Exists ( startupInfoPath ) )
264+ {
265+ File . Delete ( startupInfoPath ) ;
266+ }
267+ }
268+ catch
269+ {
270+ // best effort cleanup
271+ }
272+ }
273+ }
274+
275+ private static async Task < int > WaitForStartupInfoAsync ( string startupInfoPath , CancellationToken cancellationToken )
276+ {
277+ while ( ! cancellationToken . IsCancellationRequested )
278+ {
279+ try
280+ {
281+ if ( File . Exists ( startupInfoPath ) )
282+ {
283+ var json = await File . ReadAllTextAsync ( startupInfoPath , cancellationToken ) . ConfigureAwait ( false ) ;
284+ if ( ! string . IsNullOrWhiteSpace ( json ) )
285+ {
286+ using var doc = JsonDocument . Parse ( json ) ;
287+ if ( doc . RootElement . TryGetProperty ( "port" , out var portElement ) &&
288+ portElement . TryGetInt32 ( out var port ) &&
289+ port > 0 )
290+ {
291+ return port ;
292+ }
293+ }
294+ }
295+ }
296+ catch ( JsonException )
297+ {
298+ // File may be partially written / racing with the writer - retry.
299+ }
300+ catch ( IOException )
301+ {
302+ // Same - transient races on file access; retry.
303+ }
304+
305+ try
306+ {
307+ await Task . Delay ( 50 , cancellationToken ) . ConfigureAwait ( false ) ;
308+ }
309+ catch ( TaskCanceledException )
310+ {
311+ break ;
312+ }
313+ }
314+
315+ return 0 ;
236316 }
237317
238318 private void Process_Exited ( object sender , EventArgs e )
0 commit comments