Resolve symlinks when matching AppHost socket for --apphost (#17618)#18298
Conversation
|
@mitchdenny Rebase and squash. I'm not sure what is new. Is it possible to add tests? |
…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>
b79c722 to
fb06aba
Compare
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 18298Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 18298" |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
✅ PR Testing Report — #18298 (symlink
|
| 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:
- macOS
TMPDIRis itself/var/folders/…→/private/var/folders/…(the classic/tmp→/private/tmpcase from [bug] aspire describe --apphost fails to find running AppHost when path traverses a symlink (e.g. /tmp on macOS) #17618). - Plus an explicit
linked → actualdirectory symlink for an unambiguous second layer.
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
- The user-facing "No AppHost is currently running" message still shows the original supplied path (relative), unchanged by the fix — confirmed.
aspire stopafterward cleaned up the socket cleanly (incidental confirmation of Clean up socket file after stopping running AppHost instance (#17587) #18296 behavior).
Tested on macOS (osx-arm64).
There was a problem hiding this comment.
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
JamesNK
left a comment
There was a problem hiding this comment.
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.
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>
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
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>
|
Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt. |
|
✅ No documentation update needed. Branch taken: Triggered signals (1):
Per-signal documentation proof:
The |
Description
Fixes #17618 —
aspire 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/tmpon 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 (
getcwdcanonicalizes), soPath.GetFullPath(appHostPath)on the producer side hashes the canonical path. The consumer (AppHostConnectionResolver) computed the socket key from the raw user-supplied--apphostpath, so the twoappHostIdhashes 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/tmpgap.Fix
AppHostConnectionResolvernow computes the socket-lookup key withPathNormalizer.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
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 oldResolveToFilesystemPathbehavior and pass withResolveSymlinks./var→/private/var.Fixes #17618