diff --git a/src/Tye.Hosting/TyeHost.cs b/src/Tye.Hosting/TyeHost.cs index 7127e8e7..ca805822 100644 --- a/src/Tye.Hosting/TyeHost.cs +++ b/src/Tye.Hosting/TyeHost.cs @@ -3,7 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; using System.Linq; +using System.Net; +using System.Net.Sockets; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; @@ -22,6 +25,9 @@ namespace Tye.Hosting { public class TyeHost : IDisposable { + private const int DefaultPort = 8000; + private const int AutodetectPort = 0; + private Microsoft.Extensions.Logging.ILogger? _logger; private IHostApplicationLifetime? _lifetime; private AggregateApplicationProcessor? _processor; @@ -56,12 +62,12 @@ namespace Tye.Hosting var app = BuildWebApplication(_application, _args, Sink); DashboardWebApplication = app; - ConfigureApplication(app); - _logger = app.Logger; _lifetime = app.ApplicationLifetime; - _logger.LogInformation("Executing application from {Source}", _application.Source); + _logger.LogInformation("Executing application from {Source}", _application.Source); + + ConfigureApplication(app); var configuration = app.Configuration; @@ -144,9 +150,9 @@ namespace Tye.Hosting return app; } - private static void ConfigureApplication(WebApplication app) + private void ConfigureApplication(WebApplication app) { - var port = app.Configuration["port"] ?? "0"; + var port = ComputePort(app); app.Listen($"http://127.0.0.1:{port}"); @@ -164,6 +170,70 @@ namespace Tye.Hosting app.MapFallbackToPage("/_Host"); } + private int ComputePort(WebApplication app) + { + // logic for computing the port: + // - we allow the user to specify the port... if they don't + // - we want to use a predictable port so that it's easy to remember how to + // get to the dashboard ... and + // - we don't want to cause conflicts with any of the users known bindings + // or something else running. + + var port = app.Configuration["port"]; + if (!string.IsNullOrEmpty(port)) + { + // Port was passed in at the command-line, use it! + return int.Parse(port, NumberStyles.Number, CultureInfo.InvariantCulture); + } + else if (IsPortInUseByBinding(_application, DefaultPort)) + { + // Port has been reserved for the app. + app.Logger.LogInformation($"Default dashboard port {DefaultPort} has been reserved by the application, choosing random port."); + return AutodetectPort; + } + else if (IsPortAlreadyInUse(DefaultPort)) + { + // Port is in use by something already running. + app.Logger.LogInformation($"Default dashboard port {DefaultPort} is in use, choosing random port."); + return AutodetectPort; + } + else + { + return DefaultPort; + } + } + + private static bool IsPortAlreadyInUse(int port) + { + var endpoint = new IPEndPoint(IPAddress.Loopback, port); + try + { + using var socket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(endpoint); + return false; + } + catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse) + { + return true; + } + } + + private static bool IsPortInUseByBinding(Model.Application application, int port) + { + foreach (var service in application.Services) + { + foreach (var binding in service.Value.Description.Bindings) + { + if (binding.Port == port) + { + return true; + } + } + } + + return false; + } + private static AggregateApplicationProcessor CreateApplicationProcessor(string[] args, Microsoft.Extensions.Logging.ILogger logger, IConfiguration configuration) { var diagnosticOptions = DiagnosticOptions.FromConfiguration(configuration);