diff --git a/src/docfx/Models/BuildCommand.cs b/src/docfx/Models/BuildCommand.cs index a62257acc78..1bf2a05e0aa 100644 --- a/src/docfx/Models/BuildCommand.cs +++ b/src/docfx/Models/BuildCommand.cs @@ -14,6 +14,12 @@ public override int Execute(CommandContext context, BuildCommandOptions settings { return CommandHelper.Run(settings, () => { + if (settings.Serve && CommandHelper.IsTcpPortAlreadyUsed(settings.Host, settings.Port)) + { + Logger.LogError($"Serve option specified. But TCP port {settings.Port ?? 8080} is already being in use."); + return; + } + var (config, baseDirectory) = Docset.GetConfig(settings.ConfigFile); MergeOptionsToConfig(settings, config.build, baseDirectory); var serveDirectory = RunBuild.Exec(config.build, new(), baseDirectory, settings.OutputFolder); diff --git a/src/docfx/Models/CommandHelper.cs b/src/docfx/Models/CommandHelper.cs index 798e23aa52b..dc8b45717f0 100644 --- a/src/docfx/Models/CommandHelper.cs +++ b/src/docfx/Models/CommandHelper.cs @@ -1,8 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net; +using System.Net.NetworkInformation; using Docfx.Common; +#nullable enable + namespace Docfx; internal class CommandHelper @@ -49,4 +53,33 @@ public static int Run(LogOptions options, Action run) return Logger.HasError ? -1 : 0; } + + public static bool IsTcpPortAlreadyUsed(string? host, int? port) + { + port ??= 8080; + + var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); + var ipEndpoints = ipGlobalProperties.GetActiveTcpListeners() + .Where(x => x.Port == port); + + if (!ipEndpoints.Any()) + return false; // Specified port is not used by any endpoint. + + switch (host) + { + case null: + case "localhost": + return ipEndpoints.Any(x => IPAddress.IsLoopback(x.Address)); // Check both IPv4/IPv6 loopback address. + default: + if (IPAddress.TryParse(host, out var address)) + { + return ipEndpoints.Any(x => x.Address == address); + } + else + { + // Anything not recognized as a valid IP address (e.g. `*`) binds to all IPv4 and IPv6 IPAddresses. + return true; + } + } + } } diff --git a/src/docfx/Models/DefaultCommand.cs b/src/docfx/Models/DefaultCommand.cs index e6c6b1e6d0c..fd21157e4be 100644 --- a/src/docfx/Models/DefaultCommand.cs +++ b/src/docfx/Models/DefaultCommand.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Reflection; +using Docfx.Common; using Docfx.Dotnet; using Docfx.Pdf; using Spectre.Console.Cli; @@ -29,6 +30,12 @@ public override int Execute(CommandContext context, Options options) return CommandHelper.Run(options, () => { + if (options.Serve && CommandHelper.IsTcpPortAlreadyUsed(options.Host, options.Port)) + { + Logger.LogError($"Serve option specified. But TCP port {options.Port ?? 8080} is already being in use."); + return; + } + var (config, configDirectory) = Docset.GetConfig(options.ConfigFile); var outputFolder = options.OutputFolder; string serveDirectory = null;