Skip to content

Commit

Permalink
Detailed port-in-use error (#7316)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexb5dh authored Aug 24, 2024
1 parent c60b3c1 commit ad86f41
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 29 deletions.
23 changes: 23 additions & 0 deletions src/Nethermind/Nethermind.Config/ConfigExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, bool> 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<T>(this IConfig config, string propertyName)
{
Type type = config.GetType();
Expand Down
2 changes: 2 additions & 0 deletions src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ public class ConfigItemAttribute : Attribute
public bool DisabledForCli { get; set; }

public string EnvironmentVariable { get; set; }

public bool IsPortOption { get; set; }
}
}
3 changes: 3 additions & 0 deletions src/Nethermind/Nethermind.Config/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> collection, string value, StringComparison comparison) =>
collection.Any(i => string.Equals(i, value, comparison));
}
Expand Down
2 changes: 1 addition & 1 deletion src/Nethermind/Nethermind.Grpc/IGrpcConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
}
6 changes: 3 additions & 3 deletions src/Nethermind/Nethermind.JsonRpc/IJsonRpcConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.")]
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public DiscoveryConnectionsPool(ILogger logger, INetworkConfig networkConfig, ID
public async Task<IChannel> BindAsync(Bootstrap bootstrap, int port)
{
if (_byPort.TryGetValue(port, out Task<IChannel>? 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 =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 2 additions & 2 deletions src/Nethermind/Nethermind.Network/Config/INetworkConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
78 changes: 78 additions & 0 deletions src/Nethermind/Nethermind.Network/NetworkHelper.cs
Original file line number Diff line number Diff line change
@@ -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<T>(Func<T> action, params int[] ports)
{
try
{
return action();
}
catch (Exception exception)
{
throw MapOrRethrow(exception, ports: ports);
}
}

public static async Task HandlePortTakenError(Func<Task> action, params string[] urls)
{
try
{
await action();
}
catch (Exception exception)
{
throw MapOrRethrow(exception, urls: urls);
}
}

public static async Task<T> HandlePortTakenError<T>(Func<Task<T>> action, params int[] ports)
{
try
{
return await action();
}
catch (Exception exception)
{
throw MapOrRethrow(exception, ports: ports);
}
}
}
34 changes: 34 additions & 0 deletions src/Nethermind/Nethermind.Network/PortInUseException.cs
Original file line number Diff line number Diff line change
@@ -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."
};
}
}
19 changes: 5 additions & 14 deletions src/Nethermind/Nethermind.Network/Rlpx/RlpxHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,25 +131,16 @@ public async Task Init()
InitializeChannel(ch, session);
}));

Task<IChannel> openTask = LocalIp is null
? bootstrap.BindAsync(LocalPort)
: bootstrap.BindAsync(IPAddress.Parse(LocalIp), LocalPort);
Task<IChannel> 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;
}
Expand Down
5 changes: 4 additions & 1 deletion src/Nethermind/Nethermind.Runner/Ethereum/GrpcRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Grpc.Core;
using Nethermind.Grpc;
using Nethermind.Logging;
using Nethermind.Network;

namespace Nethermind.Runner.Ethereum
{
Expand All @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions src/Nethermind/Nethermind.Runner/Ethereum/JsonRpcRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
7 changes: 5 additions & 2 deletions src/Nethermind/Nethermind.Runner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,11 @@ private static void BuildOptionsFromConfigFiles(CommandLineApplication app)
ConfigItemAttribute? configItemAttribute = propertyInfo.GetCustomAttribute<ConfigItemAttribute>();
if (!(configItemAttribute?.DisabledForCli ?? false))
{
_ = app.Option($"--{configType.Name[1..].Replace("Config", string.Empty)}.{propertyInfo.Name}", $"{(configItemAttribute is null ? "<missing documentation>" : configItemAttribute.Description + $" (DEFAULT: {configItemAttribute.DefaultValue})" ?? "<missing documentation>")}", CommandOptionType.SingleValue);

_ = app.Option($"--{ConfigExtensions.GetCategoryName(configType)}.{propertyInfo.Name}", $"{(configItemAttribute is null ? "<missing documentation>" : configItemAttribute.Description + $" (DEFAULT: {configItemAttribute.DefaultValue})" ?? "<missing documentation>")}", CommandOptionType.SingleValue);
}
if (configItemAttribute?.IsPortOption == true)
{
ConfigExtensions.AddPortOptionName(configType, propertyInfo.Name);
}
}
}
Expand Down

0 comments on commit ad86f41

Please sign in to comment.