diff --git a/src/Nethermind/Nethermind.Config/ConfigExtensions.cs b/src/Nethermind/Nethermind.Config/ConfigExtensions.cs index f061fbfe37c..07d87963a74 100644 --- a/src/Nethermind/Nethermind.Config/ConfigExtensions.cs +++ b/src/Nethermind/Nethermind.Config/ConfigExtensions.cs @@ -2,12 +2,35 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections.Concurrent; using System.ComponentModel; +using System.Linq; using System.Reflection; namespace Nethermind.Config; public static class ConfigExtensions { + private static readonly ConcurrentDictionary PortOptions = new(); + + public static string GetCategoryName(Type type) + { + if (type.IsAssignableTo(typeof(INoCategoryConfig))) + return null; + + string categoryName = type.Name.RemoveEnd("Config"); + if (type.IsInterface) categoryName = categoryName.RemoveStart('I'); + return categoryName; + } + + public static void AddPortOptionName(Type categoryType, string optionName) => + PortOptions.TryAdd( + GetCategoryName(categoryType) is { } categoryName ? $"{categoryName}.{optionName}" : optionName, + true + ); + + public static string[] GetPortOptionNames() => + PortOptions.Keys.OrderByDescending(x => x).ToArray(); + public static T GetDefaultValue(this IConfig config, string propertyName) { Type type = config.GetType(); diff --git a/src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs b/src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs index 0a768581a47..e37ece79cad 100644 --- a/src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs +++ b/src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs @@ -16,5 +16,7 @@ public class ConfigItemAttribute : Attribute public bool DisabledForCli { get; set; } public string EnvironmentVariable { get; set; } + + public bool IsPortOption { get; set; } } } diff --git a/src/Nethermind/Nethermind.Config/StringExtensions.cs b/src/Nethermind/Nethermind.Config/StringExtensions.cs index d4a2390af1a..2764354e803 100644 --- a/src/Nethermind/Nethermind.Config/StringExtensions.cs +++ b/src/Nethermind/Nethermind.Config/StringExtensions.cs @@ -15,6 +15,9 @@ public static string RemoveStart(this string thisString, char removeChar) => public static string RemoveEnd(this string thisString, char removeChar) => thisString.EndsWith(removeChar) ? thisString[..^1] : thisString; + public static string RemoveEnd(this string thisString, string removeString) => + thisString.EndsWith(removeString) ? thisString[..^removeString.Length] : thisString; + public static bool Contains(this IEnumerable collection, string value, StringComparison comparison) => collection.Any(i => string.Equals(i, value, comparison)); } diff --git a/src/Nethermind/Nethermind.Grpc/IGrpcConfig.cs b/src/Nethermind/Nethermind.Grpc/IGrpcConfig.cs index e14953bbc3f..f8d8e0be53a 100644 --- a/src/Nethermind/Nethermind.Grpc/IGrpcConfig.cs +++ b/src/Nethermind/Nethermind.Grpc/IGrpcConfig.cs @@ -14,7 +14,7 @@ public interface IGrpcConfig : IConfig [ConfigItem(Description = "An address of the host under which gRPC will be running", DefaultValue = "localhost")] string Host { get; } - [ConfigItem(Description = "Port of the host under which gRPC will be exposed", DefaultValue = "50000")] + [ConfigItem(Description = "Port of the host under which gRPC will be exposed", DefaultValue = "50000", IsPortOption = true)] int Port { get; } } } diff --git a/src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs b/src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs index e52d9d1e265..5acc461118f 100644 --- a/src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs +++ b/src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs @@ -43,10 +43,10 @@ public interface IJsonRpcConfig : IConfig [ConfigItem(Description = "The diagnostic recording mode.", DefaultValue = "None")] RpcRecorderState RpcRecorderState { get; set; } - [ConfigItem(Description = "The JSON-RPC service HTTP port.", DefaultValue = "8545")] + [ConfigItem(Description = "The JSON-RPC service HTTP port.", DefaultValue = "8545", IsPortOption = true)] int Port { get; set; } - [ConfigItem(Description = "The JSON-RPC service WebSockets port.", DefaultValue = "8545")] + [ConfigItem(Description = "The JSON-RPC service WebSockets port.", DefaultValue = "8545", IsPortOption = true)] int WebSocketsPort { get; set; } [ConfigItem(Description = "The path to connect a UNIX domain socket over.")] @@ -147,7 +147,7 @@ public interface IJsonRpcConfig : IConfig [ConfigItem(Description = "The Engine API host.", DefaultValue = "127.0.0.1")] string EngineHost { get; set; } - [ConfigItem(Description = "The Engine API port.", DefaultValue = "null")] + [ConfigItem(Description = "The Engine API port.", DefaultValue = "null", IsPortOption = true)] int? EnginePort { get; set; } [ConfigItem( diff --git a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConnectionsPool.cs b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConnectionsPool.cs index 723998d3b8c..6d7536b4da8 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConnectionsPool.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/DiscoveryConnectionsPool.cs @@ -32,7 +32,9 @@ public DiscoveryConnectionsPool(ILogger logger, INetworkConfig networkConfig, ID public async Task BindAsync(Bootstrap bootstrap, int port) { if (_byPort.TryGetValue(port, out Task? task)) return await task; - _byPort.Add(port, task = bootstrap.BindAsync(_ip, port)); + + task = NetworkHelper.HandlePortTakenError(() => bootstrap.BindAsync(_ip, port), port); + _byPort.Add(port, task); return await task.ContinueWith(t => { diff --git a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs index c0522d66425..b56d04c664c 100644 --- a/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs +++ b/src/Nethermind/Nethermind.Network.Discovery/Discv5/DiscoveryV5App.cs @@ -98,7 +98,7 @@ .. _discoveryDb.GetAllValues().Select(enr => enrFactory.CreateFromBytes(enr, ide NettyDiscoveryV5Handler.Register(s); }); - _discv5Protocol = discv5Builder.Build(); + _discv5Protocol = NetworkHelper.HandlePortTakenError(discv5Builder.Build, networkConfig.DiscoveryPort); _discv5Protocol.NodeAdded += (e) => NodeAddedByDiscovery(e.Record); _discv5Protocol.NodeRemoved += NodeRemovedByDiscovery; diff --git a/src/Nethermind/Nethermind.Network/Config/INetworkConfig.cs b/src/Nethermind/Nethermind.Network/Config/INetworkConfig.cs index 9f66a70d2e6..83bfdf6924b 100644 --- a/src/Nethermind/Nethermind.Network/Config/INetworkConfig.cs +++ b/src/Nethermind/Nethermind.Network/Config/INetworkConfig.cs @@ -47,10 +47,10 @@ public interface INetworkConfig : IConfig [ConfigItem(DisabledForCli = true, HiddenFromDocs = true, DefaultValue = "10000")] int P2PPingInterval { get; } - [ConfigItem(Description = $"The UDP port number for incoming discovery connections. It's recommended to keep it the same as the TCP port (`{nameof(P2PPort)}`) because other values have not been tested yet.", DefaultValue = "30303")] + [ConfigItem(Description = $"The UDP port number for incoming discovery connections. It's recommended to keep it the same as the TCP port (`{nameof(P2PPort)}`) because other values have not been tested yet.", DefaultValue = "30303", IsPortOption = true)] int DiscoveryPort { get; set; } - [ConfigItem(Description = "The TCP port for incoming P2P connections.", DefaultValue = "30303")] + [ConfigItem(Description = "The TCP port for incoming P2P connections.", DefaultValue = "30303", IsPortOption = true)] int P2PPort { get; set; } [ConfigItem(DisabledForCli = true, HiddenFromDocs = true, DefaultValue = "2000")] diff --git a/src/Nethermind/Nethermind.Network/NetworkHelper.cs b/src/Nethermind/Nethermind.Network/NetworkHelper.cs new file mode 100644 index 00000000000..535cc3401c5 --- /dev/null +++ b/src/Nethermind/Nethermind.Network/NetworkHelper.cs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.IO; +using System.Net.Sockets; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + +namespace Nethermind.Network; + +public static class NetworkHelper +{ + private static PortInUseException MapOrRethrow(Exception exception, int[]? ports = null, string[]? urls = null) + { + if (exception is AggregateException) + exception = exception.InnerException!; + + switch (exception) + { + case SocketException { SocketErrorCode: SocketError.AddressAlreadyInUse or SocketError.AccessDenied }: + return ports != null ? new(exception, ports) : new(exception, urls!); + case IOException { Source: "Grpc.Core" } when exception.Message.Contains("Failed to bind port"): + return ports != null ? new(exception, ports) : new(exception, urls!); + default: + ExceptionDispatchInfo.Throw(exception); + throw exception; // Make compiler happy, should never execute + } + } + + public static void HandlePortTakenError(Action action, params int[] ports) + { + try + { + action(); + } + catch (Exception exception) + { + throw MapOrRethrow(exception, ports: ports); + } + } + + public static T HandlePortTakenError(Func action, params int[] ports) + { + try + { + return action(); + } + catch (Exception exception) + { + throw MapOrRethrow(exception, ports: ports); + } + } + + public static async Task HandlePortTakenError(Func action, params string[] urls) + { + try + { + await action(); + } + catch (Exception exception) + { + throw MapOrRethrow(exception, urls: urls); + } + } + + public static async Task HandlePortTakenError(Func> action, params int[] ports) + { + try + { + return await action(); + } + catch (Exception exception) + { + throw MapOrRethrow(exception, ports: ports); + } + } +} diff --git a/src/Nethermind/Nethermind.Network/PortInUseException.cs b/src/Nethermind/Nethermind.Network/PortInUseException.cs new file mode 100644 index 00000000000..3ae3ddfa54d --- /dev/null +++ b/src/Nethermind/Nethermind.Network/PortInUseException.cs @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.IO; +using System.Linq; +using Nethermind.Config; + +namespace Nethermind.Network; + +public class PortInUseException : IOException +{ + public PortInUseException(Exception exception, params int[] ports) : base( + $"{GetReason(ports)} " + + "If you intend to run 2 or more nodes on one machine, ensure you have changed all configured ports under: " + + $"{"\n\t" + string.Join("\n\t", ConfigExtensions.GetPortOptionNames())}", + exception + ) + { } + + public PortInUseException(Exception exception, params string[] urls) : this(exception, GetPorts(urls)) { } + + private static int[] GetPorts(string[] urls) => urls.Select(u => new Uri(u).Port).ToArray(); + + private static string GetReason(params int[] ports) + { + return ports.Length switch + { + 0 => "One of the configured ports is in use.", + 1 => $"Port {ports[0]} is in use.", + _ => $"One or more of the ports {string.Join(',', ports)} are in use." + }; + } +} diff --git a/src/Nethermind/Nethermind.Network/Rlpx/RlpxHost.cs b/src/Nethermind/Nethermind.Network/Rlpx/RlpxHost.cs index 32bafe6001a..7917c84cd84 100644 --- a/src/Nethermind/Nethermind.Network/Rlpx/RlpxHost.cs +++ b/src/Nethermind/Nethermind.Network/Rlpx/RlpxHost.cs @@ -131,25 +131,16 @@ public async Task Init() InitializeChannel(ch, session); })); - Task openTask = LocalIp is null - ? bootstrap.BindAsync(LocalPort) - : bootstrap.BindAsync(IPAddress.Parse(LocalIp), LocalPort); + Task openTask = NetworkHelper.HandlePortTakenError(() => LocalIp is null + ? bootstrap.BindAsync(LocalPort) + : bootstrap.BindAsync(IPAddress.Parse(LocalIp), LocalPort), + LocalPort); _bootstrapChannel = await openTask.ContinueWith(t => { if (t.IsFaulted) { - AggregateException aggregateException = t.Exception; - if (aggregateException?.InnerException is SocketException socketException - && socketException.ErrorCode == 10048) - { - if (_logger.IsError) _logger.Error($"Port {LocalPort} is in use. You can change the port used by adding: --{nameof(NetworkConfig).Replace("Config", string.Empty)}.{nameof(NetworkConfig.P2PPort)} 30303"); - } - else - { - if (_logger.IsError) _logger.Error($"{nameof(Init)} failed", t.Exception); - } - + if (_logger.IsError) _logger.Error($"{nameof(Init)} failed", t.Exception); return null; } diff --git a/src/Nethermind/Nethermind.Runner/Ethereum/GrpcRunner.cs b/src/Nethermind/Nethermind.Runner/Ethereum/GrpcRunner.cs index 3dd5be11dfd..6fefb7084bc 100644 --- a/src/Nethermind/Nethermind.Runner/Ethereum/GrpcRunner.cs +++ b/src/Nethermind/Nethermind.Runner/Ethereum/GrpcRunner.cs @@ -6,6 +6,7 @@ using Grpc.Core; using Nethermind.Grpc; using Nethermind.Logging; +using Nethermind.Network; namespace Nethermind.Runner.Ethereum { @@ -30,7 +31,9 @@ public Task Start(CancellationToken cancellationToken) Services = { NethermindService.BindService(_service) }, Ports = { new ServerPort(_config.Host, _config.Port, ServerCredentials.Insecure) } }; - _server.Start(); + NetworkHelper.HandlePortTakenError( + _server.Start, _config.Port + ); if (_logger.IsInfo) _logger.Info($"Started GRPC server on {_config.Host}:{_config.Port}."); return Task.CompletedTask; diff --git a/src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs b/src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs index d282a25c994..f606bc934b7 100644 --- a/src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs +++ b/src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs @@ -16,6 +16,7 @@ using Nethermind.Core.Authentication; using Nethermind.JsonRpc; using Nethermind.Logging; +using Nethermind.Network; using Nethermind.Runner.JsonRpc; using Nethermind.Runner.Logging; using Nethermind.Sockets; @@ -59,7 +60,7 @@ public JsonRpcRunner( _api = api; } - public Task Start(CancellationToken cancellationToken) + public async Task Start(CancellationToken cancellationToken) { if (_logger.IsDebug) _logger.Debug("Initializing JSON RPC"); string[] urls = _jsonRpcUrlCollection.Urls; @@ -95,11 +96,11 @@ public Task Start(CancellationToken cancellationToken) if (!cancellationToken.IsCancellationRequested) { - _webHost.Start(); + await NetworkHelper.HandlePortTakenError( + () => _webHost.StartAsync(cancellationToken), urls + ); if (_logger.IsDebug) _logger.Debug($"JSON RPC : {urlsString}"); } - - return Task.CompletedTask; } public async Task StopAsync() diff --git a/src/Nethermind/Nethermind.Runner/Program.cs b/src/Nethermind/Nethermind.Runner/Program.cs index 163d31df96b..e400cb17ce8 100644 --- a/src/Nethermind/Nethermind.Runner/Program.cs +++ b/src/Nethermind/Nethermind.Runner/Program.cs @@ -288,8 +288,11 @@ private static void BuildOptionsFromConfigFiles(CommandLineApplication app) ConfigItemAttribute? configItemAttribute = propertyInfo.GetCustomAttribute(); if (!(configItemAttribute?.DisabledForCli ?? false)) { - _ = app.Option($"--{configType.Name[1..].Replace("Config", string.Empty)}.{propertyInfo.Name}", $"{(configItemAttribute is null ? "" : configItemAttribute.Description + $" (DEFAULT: {configItemAttribute.DefaultValue})" ?? "")}", CommandOptionType.SingleValue); - + _ = app.Option($"--{ConfigExtensions.GetCategoryName(configType)}.{propertyInfo.Name}", $"{(configItemAttribute is null ? "" : configItemAttribute.Description + $" (DEFAULT: {configItemAttribute.DefaultValue})" ?? "")}", CommandOptionType.SingleValue); + } + if (configItemAttribute?.IsPortOption == true) + { + ConfigExtensions.AddPortOptionName(configType, propertyInfo.Name); } } }