diff --git a/Changelog.md b/Changelog.md index 7d0789d..3df8cdc 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,7 @@ - Updated dependencies - Fixed single instance handling on macOS (#1040) +- Fixed slow socket bridge startup by binding to an explicit loopback address (#1103) - Fixed socket bridge connection when a system proxy is configured (#1105) - Fixed electron-builder using the host RID instead of the target RID (#1097) - Fixed cross-compilation behavior on same platform (#1098) @epsnm diff --git a/src/ElectronNET.API/ElectronNetRuntime.cs b/src/ElectronNET.API/ElectronNetRuntime.cs index 97fa2ed..e823052 100644 --- a/src/ElectronNET.API/ElectronNetRuntime.cs +++ b/src/ElectronNET.API/ElectronNetRuntime.cs @@ -15,6 +15,7 @@ internal const int DefaultSocketPort = 8000; internal const int DefaultWebPort = 8001; internal const string ElectronPortArgumentName = "electronPort"; + internal const string ElectronHostArgumentName = "electronHost"; internal const string ElectronPidArgumentName = "electronPID"; internal const string ElectronAuthTokenArgumentName = "electronAuthToken"; @@ -31,6 +32,11 @@ public static int? ElectronSocketPort { get; internal set; } + /// + /// The loopback address the Electron socket bridge is listening on. + /// + public static string ElectronSocketHost { get; internal set; } + public static int? AspNetWebPort { get; internal set; } public static StartupMethod StartupMethod { get; internal set; } diff --git a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs index b7c65ed..51c409e 100644 --- a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs +++ b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs @@ -18,7 +18,7 @@ [Localizable(false)] internal class ElectronProcessActive : ElectronProcessBase { - private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at .* using ([a-f0-9]+)$"); + private readonly Regex extractor = new Regex("^Electron Socket: listening on port (\\d+) at (\\S+) using ([a-f0-9]+)$"); private readonly bool isUnpackaged; private readonly string electronBinaryName; @@ -179,11 +179,13 @@ if (match?.Success ?? false) { var port = int.Parse(match.Groups[1].Value); - var token = match.Groups[2].Value; + var host = match.Groups[2].Value; + var token = match.Groups[3].Value; this.process.LineReceived -= Read_SocketIO_Parameters; ElectronNetRuntime.ElectronAuthToken = token; ElectronNetRuntime.ElectronSocketPort = port; + ElectronNetRuntime.ElectronSocketHost = host; tcs.SetResult(); } } diff --git a/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs b/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs index 4530a65..dd40033 100644 --- a/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs +++ b/src/ElectronNET.API/Runtime/Services/SocketBridge/SocketBridgeService.cs @@ -16,7 +16,19 @@ { this.socketPort = socketPort; this.authorization = authorization; - this.socketUrl = $"http://localhost:{this.socketPort}"; + this.socketUrl = $"http://{FormatHost(ElectronNetRuntime.ElectronSocketHost)}:{this.socketPort}"; + } + + // The Electron host reports the loopback address it is actually listening on; only + // when it is unknown we have to fall back to the ambiguous hostname. + private static string FormatHost(string socketHost) + { + if (string.IsNullOrWhiteSpace(socketHost)) + { + return "localhost"; + } + + return socketHost.Contains(':') ? $"[{socketHost}]" : socketHost; } public int SocketPort => this.socketPort; diff --git a/src/ElectronNET.API/Runtime/StartupManager.cs b/src/ElectronNET.API/Runtime/StartupManager.cs index e7ceff1..7817894 100644 --- a/src/ElectronNET.API/Runtime/StartupManager.cs +++ b/src/ElectronNET.API/Runtime/StartupManager.cs @@ -94,8 +94,20 @@ } } - var pidArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronPidArgumentName, StringComparison.OrdinalIgnoreCase)); + var hostArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronHostArgumentName, StringComparison.OrdinalIgnoreCase)); + if (hostArg != null) + { + var parts = hostArg.Split('=', StringSplitOptions.TrimEntries); + + if (parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1])) + { + ElectronNetRuntime.ElectronSocketHost = parts[1]; + + Console.WriteLine("Use Electron Host: " + parts[1]); + } + } + var pidArg = argsList.FirstOrDefault(e => e.Contains(ElectronNetRuntime.ElectronPidArgumentName, StringComparison.OrdinalIgnoreCase)); if (pidArg != null) { var parts = pidArg.Split('=', StringSplitOptions.TrimEntries); diff --git a/src/ElectronNET.Host/main.js b/src/ElectronNET.Host/main.js index 1366494..d610da8 100644 --- a/src/ElectronNET.Host/main.js +++ b/src/ElectronNET.Host/main.js @@ -175,6 +175,7 @@ function getForwardedArgs() { if (cleaned.startsWith('remote-debugging-port')) return false; // We add /electronPort ourselves later if (cleaned.startsWith('electronPort=')) return false; + if (cleaned.startsWith('electronHost=')) return false; if (cleaned.startsWith('electronWebPort=')) return false; return true; }); @@ -295,7 +296,6 @@ function startSocketApiBridge(port) { // otherwise the Windows Firewall will be triggered console.debug('Electron Socket: starting...'); server = createServer(); - const host = !port ? '127.0.0.1' : 'localhost'; let hostHook; io = new Server({ pingTimeout: 60000, // in ms, default is 5000 @@ -303,16 +303,35 @@ function startSocketApiBridge(port) { }); io.attach(server); - server.listen(port, host); + // Never bind to the 'localhost' hostname: it may resolve to ::1 and 127.0.0.1 in any + // order, so server and client can end up on different stacks - which costs a failed + // connection attempt (or a DNS lookup) on every startup. + const hostCandidates = ['127.0.0.1', '::1']; + let hostIndex = 0; + + server.on('error', (error) => { + const isUnavailable = error.code === 'EADDRNOTAVAIL' || error.code === 'EAFNOSUPPORT' || error.code === 'EINVAL'; + + if (isUnavailable && hostIndex + 1 < hostCandidates.length) { + console.warn(`Electron Socket: cannot bind to ${hostCandidates[hostIndex]} (${error.code}), falling back to ${hostCandidates[hostIndex + 1]}.`); + hostIndex++; + server.listen(port, hostCandidates[hostIndex]); + return; + } + + console.error('Electron Socket: ' + error.message); + }); + + server.listen(port, hostCandidates[hostIndex]); server.on('listening', function () { const addr = server.address(); 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) { - startAspCoreBackendUnpackaged(addr.port); + startAspCoreBackendUnpackaged(addr.port, addr.address); } else if (!unpackeddotnet && !dotnetpacked) { - startAspCoreBackend(addr.port); + startAspCoreBackend(addr.port, addr.address); } }); @@ -416,7 +435,7 @@ function startSocketApiBridge(port) { }); } -function startAspCoreBackend(electronPort) { +function startAspCoreBackend(electronPort, electronHost) { startBackend(); function startBackend() { @@ -425,6 +444,7 @@ function startAspCoreBackend(electronPort) { const parameters = [ envParam, `/electronPort=${electronPort}`, + `/electronHost=${electronHost}`, `/electronPID=${process.pid}`, `/electronAuthToken=${authToken}`, // forward user supplied args (avoid duplicate environment) @@ -447,7 +467,7 @@ function startAspCoreBackend(electronPort) { } } -function startAspCoreBackendUnpackaged(electronPort) { +function startAspCoreBackendUnpackaged(electronPort, electronHost) { startBackend(); function startBackend() { @@ -456,6 +476,7 @@ function startAspCoreBackendUnpackaged(electronPort) { const parameters = [ envParam, `/electronPort=${electronPort}`, + `/electronHost=${electronHost}`, `/electronPID=${process.pid}`, `/electronAuthToken=${authToken}`, ...forwardedArgs.filter(a => !(envParam && a.startsWith('--environment=')))