Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detailed port-in-use error #7316

Merged
merged 9 commits into from
Aug 24, 2024
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);
alexb5dh marked this conversation as resolved.
Show resolved Hide resolved
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)
LukaszRozmej marked this conversation as resolved.
Show resolved Hide resolved
{
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