Skip to content

Resolve symlinks when matching AppHost socket for --apphost (#17618)#18298

Merged
mitchdenny merged 4 commits into
mainfrom
fix/17618-symlink-apphost
Jun 18, 2026
Merged

Resolve symlinks when matching AppHost socket for --apphost (#17618)#18298
mitchdenny merged 4 commits into
mainfrom
fix/17618-symlink-apphost

Conversation

@mitchdenny

Copy link
Copy Markdown
Member

Description

Fixes #17618aspire describe --apphost <path> (and other --apphost-accepting commands) reports "No AppHost is currently running" whenever the supplied path traverses a symlink (e.g. /tmp/private/tmp on macOS, or symlinked $HOME/mounted workspaces on Linux).

Root cause

A running AppHost keys its backchannel socket off the symlink-resolved path: the AppHost process's working directory is reported physically by the OS (getcwd canonicalizes), so Path.GetFullPath(appHostPath) on the producer side hashes the canonical path. The consumer (AppHostConnectionResolver) computed the socket key from the raw user-supplied --apphost path, so the two appHostId hashes never matched when a symlink was involved.

The original attempt used PathNormalizer.ResolveToFilesystemPath, but that helper only normalizes Windows casing and is a no-op on Linux/macOS — so it could not bridge the /tmp/private/tmp gap.

Fix

AppHostConnectionResolver now computes the socket-lookup key with PathNormalizer.ResolveSymlinks(projectFile.FullName), so the consumer hashes the same canonical path as the running AppHost. The user-facing error message still displays the original supplied path (relative to the working directory), so output is unchanged.

Tests

  • Adds ResolveConnectionAsync_WithSymlinkedProjectPath_ResolvesToCanonicalSocketKey (Linux/macOS-gated): creates a socket keyed off the canonical path, addresses the project through a symlinked directory, and asserts the socket is located. Verified to fail on the old ResolveToFilesystemPath behavior and pass with ResolveSymlinks.
  • Updates the existing dead-PID pruning test to key its socket off the resolved path (matching a real AppHost), since the macOS temp workspace itself lives under symlinked /var/private/var.

Stacked on #17619 (fix/17619-noninteractive-describe): the new resolver test depends on the ICliHostEnvironment constructor parameter introduced there. The diff against that base shows only the symlink changes. Retarget to main once #17619 merges.

Split out of #18261 so each fix can be reviewed in isolation.

Fixes #17618

Base automatically changed from fix/17619-noninteractive-describe to main June 18, 2026 00:54
@JamesNK

JamesNK commented Jun 18, 2026

Copy link
Copy Markdown
Member

@mitchdenny Rebase and squash. I'm not sure what is new.

Is it possible to add tests?

mitchdenny and others added 2 commits June 18, 2026 11:36
…phost

Fixes issue where aspire describe --apphost fails when path contains symlinks
(e.g., /tmp on macOS which resolves to /private/tmp).

Resolves #17618: aspire describe --apphost fails to find running AppHost when path traverses a symlink

The socket lookup in AppHostConnectionResolver now canonicalizes the project file path
before computing the backchannel socket key, matching the path canonicalization that
occurs when the AppHost is started via ProjectLocator.

This ensures both the running AppHost and the describe command use the same canonical
path when looking up sockets, regardless of whether the user provided a symlinked path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The explicit --apphost socket lookup in AppHostConnectionResolver used
PathNormalizer.ResolveToFilesystemPath, which only normalizes Windows
casing and is a no-op on Linux/macOS. A running AppHost keys its
backchannel socket off the symlink-resolved path (its process working
directory is reported physically by the OS, e.g. /tmp -> /private/tmp on
macOS), so the consumer never matched when the supplied path traversed a
symlink and reported 'No AppHost is currently running'.

Switch the socket-key computation to PathNormalizer.ResolveSymlinks so
the consumer hashes the same canonical path as the producer. The
user-facing error path still displays the original supplied path. Adds a
regression test that fails on the previous behavior, and updates the
dead-PID pruning test to key its socket off the resolved path (matching a
real AppHost) since the macOS temp workspace lives under the symlinked
/var -> /private/var.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny force-pushed the fix/17618-symlink-apphost branch from b79c722 to fb06aba Compare June 18, 2026 01:36
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🚀 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/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 18298

Or

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

@mitchdenny mitchdenny marked this pull request as draft June 18, 2026 01:46
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@mitchdenny

Copy link
Copy Markdown
Member Author

✅ PR Testing Report — #18298 (symlink --apphost socket resolution, fixes #17618)

Result: PASS — the fix resolves #17618 and a same-version no-fix build reproduces the bug.

CLI verification

Installed 13.5.0-pr.18298.gfb06aba0
PR head fb06aba0 ✅ matches
Install get-aspire-cli-pr.sh … 18298 (isolated --install-path/--skip-path)

Scenario

Empty single-file AppHost (aspire-empty, C#) started detached so it keys its backchannel socket off the canonical path. Queried it via aspire describe --apphost <path> through two layers of symlink:

Running AppHost socket: ~/.aspire/cli/bch/n4cIkWXBQM8KWeWYt7n.76997.

Result matrix

To prove it's this fix (not a version artifact), I toggled only AppHostConnectionResolver's key computation (ResolveSymlinks ↔ the old ResolveToFilesystemPath) and ran the CLI from source against the same running AppHost:

--apphost path No fix (ResolveToFilesystemPath) With fix (ResolveSymlinks)
/private/var/…/actual/apphost.cs (fully resolved) ✅ connects ✅ connects
/var/…/actual/apphost.cs (via /var symlink) "No AppHost is currently running" ✅ connects
/var/…/linked/apphost.cs (via /var + linked) "No AppHost is currently running" ✅ connects

The no-fix build only works when the user types the fully symlink-resolved path — exactly the #17618 symptom. The PR build connects through every symlinked form. The resolver is otherwise sound (fully-resolved path works in both).

ℹ️ The current stable 13.4.5 CLI is not a usable baseline here: it can't read the 13.5.0 compact socket format at all (fails even on the resolved path), so the contrast was done against a same-version (13.5.0) no-fix build.

Notes

Tested on macOS (osx-arm64).

@mitchdenny mitchdenny marked this pull request as ready for review June 18, 2026 03:23
Copilot AI review requested due to automatic review settings June 18, 2026 03:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes issue #17618 where aspire describe --apphost <path> (and other --apphost-accepting commands) failed to find a running AppHost when the supplied path traversed a symlink (e.g., /tmp/private/tmp on macOS). The root cause was that the running AppHost keyed its backchannel socket off the OS-physical path (since getcwd canonicalizes), but the consumer (AppHostConnectionResolver) hashed the raw user-supplied path. The fix applies PathNormalizer.ResolveSymlinks on the consumer side before computing the socket lookup key.

Changes:

  • In AppHostConnectionResolver, resolve symlinks on the project path before computing the socket-lookup key, while keeping the user-supplied path for display in error messages.
  • Add a new regression test (ResolveConnectionAsync_WithSymlinkedProjectPath_ResolvesToCanonicalSocketKey) gated to Linux/macOS that verifies the resolver matches a socket keyed off the canonical path when given a symlinked project path.
  • Update the existing dead-PID pruning test to key its socket off the resolved path, matching real AppHost behavior (needed on macOS where the temp workspace is under symlinked /var).
Show a summary per file
File Description
src/Aspire.Cli/Backchannel/AppHostConnectionResolver.cs Resolve symlinks via PathNormalizer.ResolveSymlinks before socket key computation; preserve original path for display
tests/Aspire.Cli.Tests/Backchannel/AppHostConnectionResolverTests.cs Add symlink regression test; update dead-PID test to use resolved path for socket creation

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment thread src/Aspire.Cli/Backchannel/AppHostConnectionResolver.cs

@JamesNK JamesNK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

1 correctness concern: the symlink fix is applied only to AppHostConnectionResolver but the same unresolved-path bug exists at two other FindMatchingNonOrphanedSockets call sites (AppHostLauncher, DotNetAppHostProject). Centralizing the resolution inside FindMatchingNonOrphanedSockets would fix all callers at once.

Comment thread src/Aspire.Cli/Backchannel/AppHostConnectionResolver.cs
Move symlink canonicalization into AppHostHelper.FindMatchingNonOrphanedSockets so all callers consistently match AppHost backchannel sockets keyed on canonical paths, and update tests accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

The test seeded socket files from the raw drive-less path "/path/to/MyApp.AppHost.csproj"
but FindMatchingNonOrphanedSockets now resolves symlinks (via Path.GetFullPath) before
hashing. On Windows, Path.GetFullPath roots that path to "C:\path\to\...", producing a
different hash and zero matches. Key the seeded sockets off the same resolved path so both
sides agree on all platforms.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 18, 2026 09:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new

@mitchdenny mitchdenny enabled auto-merge (squash) June 18, 2026 10:10
@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@mitchdenny mitchdenny merged commit dc1777a into main Jun 18, 2026
675 of 678 checks passed
@mitchdenny mitchdenny deleted the fix/17618-symlink-apphost branch June 18, 2026 10:28
@microsoft-github-policy-service microsoft-github-policy-service Bot added this to the 13.5 milestone Jun 18, 2026
@aspire-repo-bot

Copy link
Copy Markdown
Contributor

✅ No documentation update needed.

Branch taken: docs_required → already documented by name

Triggered signals (1): pr_body_has_cli_flag_mention

  • Evidence: PR body mentions `aspire describe --apphost <path>` (and other --apphost-accepting commands).

Per-signal documentation proof:

Signal Docs file Quoted text
pr_body_has_cli_flag_mention (--apphost) src/frontend/src/content/docs/reference/cli/commands/aspire-describe.mdx - ** --apphost <path> ** — The path to the Aspire AppHost project file. When specified, the command connects to the AppHost running from that project file without prompting for selection.`

The --apphost flag is already documented by name in the existing aspire.dev docs. This PR is a pure bug fix: AppHostHelper.FindMatchingNonOrphanedSockets now calls PathNormalizer.ResolveSymlinks before computing the socket lookup key, so paths traversing symlinks (e.g., /tmp/private/tmp on macOS) correctly match the running AppHost's socket. No new flag, option, API, or user-facing behavior was introduced — the change restores the documented behavior for all valid paths.

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.

[bug] aspire describe --apphost fails to find running AppHost when path traverses a symlink (e.g. /tmp on macOS)

3 participants