Skip to content

Add port availability check to aspire doctor#14378

Open
davidfowl wants to merge 6 commits intomainfrom
davidfowl/doctor-port-availability-check
Open

Add port availability check to aspire doctor#14378
davidfowl wants to merge 6 commits intomainfrom
davidfowl/doctor-port-availability-check

Conversation

@davidfowl
Copy link
Member

@davidfowl davidfowl commented Feb 6, 2026

Description

Add a new environment check to aspire doctor that detects when configured ports in apphost.run.json or launchSettings.json are unavailable. On Windows, this checks against Hyper-V excluded port ranges (via netsh). On all platforms, it verifies ports can actually be bound via a TCP socket test.

This addresses a confusing failure mode where the dashboard link displayed by aspire run doesn't work because the configured HTTPS port (e.g. 56279) silently falls within a Windows Hyper-V excluded port range (56224-56323). DCP can't bind the proxy, the CLI falls back to displaying the configured URL, and the user gets connection refused with no indication of why.

Example output when a port is blocked:

Environment
  ⚠  Configured ports unavailable: 56279 (applicationUrl)
        These ports fall within a Windows excluded port range (often reserved by Hyper-V)
        or are otherwise unavailable. The dashboard link displayed by 'aspire run' will not work.
        Delete apphost.run.json (or launchSettings.json) and run 'aspire run' to auto-assign
        available ports, or update the file with ports outside excluded ranges.
        Run 'netsh interface ipv4 show excludedportrange protocol=tcp' to see reserved ranges.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?
    • Yes
    • No

Add a new environment check that detects when configured ports in
apphost.run.json or launchSettings.json are unavailable. On Windows,
this checks against Hyper-V excluded port ranges (via netsh). On all
platforms, it verifies ports can actually be bound.

This addresses a confusing failure mode where the dashboard link
displayed by 'aspire run' doesn't work because the configured port
falls within a Windows excluded port range.
Copilot AI review requested due to automatic review settings February 6, 2026 20:30
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14378

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14378"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new aspire doctor environment check that detects when ports configured in apphost.run.json or launchSettings.json are unavailable, with special handling on Windows to detect Hyper-V excluded port ranges.

Changes:

  • Introduces PortAvailabilityCheck to read configured ports, detect Windows excluded port ranges, and attempt TCP binds.
  • Registers the new environment check with the CLI host.
  • Adds unit tests for configured-port extraction from apphost.run.json and launchSettings.json.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
tests/Aspire.Cli.Tests/Utils/PortAvailabilityCheckTests.cs Adds tests validating parsing of configured ports from config files.
src/Aspire.Cli/Utils/EnvironmentChecker/PortAvailabilityCheck.cs Implements the new environment check logic (config parsing, netsh excluded-port query, bind test, doctor output).
src/Aspire.Cli/Program.cs Registers PortAvailabilityCheck in DI so it runs as part of aspire doctor.

Comment on lines 54 to 61
results.Add(new EnvironmentCheckResult
{
Category = "environment",
Name = "port-availability",
Status = EnvironmentCheckStatus.Warning,
Message = $"Configured ports unavailable: {portList}",
Details = "These ports fall within a Windows excluded port range (often reserved by Hyper-V) or are otherwise unavailable. The dashboard link displayed by 'aspire run' will not work.",
Fix = "Delete apphost.run.json (or launchSettings.json) and run 'aspire run' to auto-assign available ports, or update the file with ports outside excluded ranges.\nRun 'netsh interface ipv4 show excludedportrange protocol=tcp' to see reserved ranges."
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning Details/Fix text is Windows-specific (Hyper-V excluded port ranges + netsh command) but it’s emitted on all platforms whenever a port can’t be bound. On macOS/Linux this guidance is incorrect/misleading. Consider tailoring Details/Fix based on OS and/or whether the port was blocked due to an excluded range vs a bind failure (e.g., omit the netsh instruction on non-Windows and adjust the wording when excludedRanges is empty).

Suggested change
results.Add(new EnvironmentCheckResult
{
Category = "environment",
Name = "port-availability",
Status = EnvironmentCheckStatus.Warning,
Message = $"Configured ports unavailable: {portList}",
Details = "These ports fall within a Windows excluded port range (often reserved by Hyper-V) or are otherwise unavailable. The dashboard link displayed by 'aspire run' will not work.",
Fix = "Delete apphost.run.json (or launchSettings.json) and run 'aspire run' to auto-assign available ports, or update the file with ports outside excluded ranges.\nRun 'netsh interface ipv4 show excludedportrange protocol=tcp' to see reserved ranges."
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var hasExcludedPort = blockedPorts.Any(p => IsPortExcluded(p.Port, excludedRanges));
var hasExcludedRanges = excludedRanges.Count > 0;
var details = (isWindows && hasExcludedPort)
? "Some configured ports fall within a Windows excluded port range (often reserved by Hyper-V) or are otherwise unavailable. The dashboard link displayed by 'aspire run' might not work."
: "Some configured ports are unavailable on this machine. The dashboard link displayed by 'aspire run' might not work.";
var fix = "Delete apphost.run.json (or launchSettings.json) and run 'aspire run' to auto-assign available ports, or update the file with ports that are currently available.";
if (isWindows && hasExcludedRanges)
{
fix += "\nOn Windows, run 'netsh interface ipv4 show excludedportrange protocol=tcp' to see reserved port ranges and choose ports outside them.";
}
results.Add(new EnvironmentCheckResult
{
Category = "environment",
Name = "port-availability",
Status = EnvironmentCheckStatus.Warning,
Message = $"Configured ports unavailable: {portList}",
Details = details,
Fix = fix

Copilot uses AI. Check for mistakes.
}
catch (Exception ex)
{
logger.LogDebug(ex, "Failed to check port availability");
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The top-level try/catch in CheckAsync swallows all exceptions and returns whatever has been accumulated so far (often an empty list), which can make the check silently disappear from aspire doctor. Other environment checks return a Warning/Fail result when an exception occurs. Consider returning a Warning result like "Unable to check port availability" with Details = ex.Message so users know the check couldn’t be performed.

Suggested change
logger.LogDebug(ex, "Failed to check port availability");
logger.LogDebug(ex, "Failed to check port availability");
results.Add(new EnvironmentCheckResult
{
Category = "environment",
Name = "port-availability",
Status = EnvironmentCheckStatus.Warning,
Message = "Unable to check port availability",
Details = ex.Message
});

Copilot uses AI. Check for mistakes.
Comment on lines 176 to 188
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = "netsh",
Arguments = "interface ipv4 show excludedportrange protocol=tcp",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(5000);

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetExcludedPortRanges() attempts to enforce a 5s timeout with WaitForExit(5000), but it calls StandardOutput.ReadToEnd() first. If netsh hangs or produces output slowly, ReadToEnd() can block indefinitely and the timeout won’t help, potentially hanging aspire doctor. Consider reading output asynchronously with a timeout/cancellation and killing the process if it exceeds the limit (similar to ContainerRuntimeCheck’s process handling).

Copilot uses AI. Check for mistakes.
Comment on lines 167 to 200
/// <summary>
/// Gets the Windows excluded port ranges using netsh.
/// </summary>
internal static List<(int Start, int End)> GetExcludedPortRanges()
{
var ranges = new List<(int Start, int End)>();

try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = "netsh",
Arguments = "interface ipv4 show excludedportrange protocol=tcp",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(5000);

foreach (var line in output.Split('\n'))
{
var trimmed = line.Trim();
var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2 &&
int.TryParse(parts[0], out var start) &&
int.TryParse(parts[1], out var end))
{
ranges.Add((start, end));
}
}
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new Windows-specific behavior (netsh excluded port range parsing + exclusion detection) isn’t unit-tested. Given the repo already tests parsing helpers for other environment checks, consider extracting the netsh-output parsing into a separate method that accepts the command output string and adding unit tests for typical netsh output (including headers and "* - Administered" lines). This will keep coverage stable without requiring netsh to run during tests.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit 5fd685d:

Test Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
ResourcesCommandShowsRunningResources ▶️ View Recording

📹 Recordings uploaded automatically from CI run #21787301944

@danegsta
Copy link
Member

danegsta commented Feb 7, 2026

We should check if the port is in the user's ephemeral port range as well. Ephemeral ports aren't necessarily reserved (although the hyper-v reserved block is usually in the ephemeral range), but there's a much higher chance of random conflicts as it's the range all random and outgoing ports are assigned from.

- Extract shared LaunchProfileHelper for apphost.run.json / launchSettings.json parsing
- Reuse shared helper in PortAvailabilityCheck and GuestAppHostProject
- OS-specific warning text (Windows mentions Hyper-V/netsh, others are generic)
- Return Warning result on exception instead of silently swallowing
- Add async read with timeout for netsh to prevent hanging
- Extract ParseExcludedPortRanges as testable static method
- Add tests for netsh output parsing and port extraction from env vars
Warn when configured ports fall within the OS ephemeral/dynamic port range,
which has a higher chance of random conflicts from outgoing connections.

Cross-platform: Windows (netsh), Linux (/proc), macOS (sysctl), with IANA
default fallback. Includes ParseDynamicPortRange tests.
@davidfowl davidfowl force-pushed the davidfowl/doctor-port-availability-check branch 2 times, most recently from 965727f to b6b9751 Compare February 7, 2026 02:03
Link LaunchSettings.cs and LaunchProfile.cs from Aspire.Hosting and the
LaunchSettingsSerializerContext from Shared/LaunchProfiles into Aspire.Cli.
LaunchProfileHelper now deserializes into typed LaunchSettings/LaunchProfile
models instead of using raw JsonDocument parsing.
@davidfowl davidfowl force-pushed the davidfowl/doctor-port-availability-check branch from b6b9751 to 062e8c1 Compare February 7, 2026 04:26
The TcpListener bind test would report configured ports as 'unavailable' if
an Aspire app was already running and bound to those ports — exactly when a
user would run 'aspire doctor'. Removed the bind test entirely; the remaining
checks (Windows excluded port ranges, ephemeral range) are static analysis
that don't depend on runtime state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants