-
Notifications
You must be signed in to change notification settings - Fork 866
Fix apphost path case sensitivity mismatch in VS Code extension #15866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
adamint
wants to merge
2
commits into
microsoft:main
from
adamint:fix/apphost-path-case-sensitivity-15588
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import * as assert from 'assert'; | ||
| import * as fs from 'fs'; | ||
| import * as os from 'os'; | ||
| import * as path from 'path'; | ||
| import { resolveCanonicalPath } from '../utils/io'; | ||
|
|
||
| suite('utils/io tests', () => { | ||
|
|
||
| test('resolveCanonicalPath returns canonical casing for existing file', () => { | ||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aspire-test-')); | ||
|
|
||
| // Resolve tmpDir itself first so symlinks (e.g., /var -> /private/var on macOS) | ||
| // don't interfere with comparisons | ||
| const canonicalTmpDir = fs.realpathSync.native(tmpDir); | ||
| const filePath = path.join(canonicalTmpDir, 'MyAppHost.csproj'); | ||
| fs.writeFileSync(filePath, ''); | ||
|
|
||
| try { | ||
| const resolved = resolveCanonicalPath(filePath); | ||
|
|
||
| assert.ok(fs.existsSync(resolved), 'Resolved path should exist'); | ||
| assert.ok(path.isAbsolute(resolved), 'Resolved path should be absolute'); | ||
| assert.strictEqual(resolved, filePath, 'Canonical path should match when input casing is already correct'); | ||
| } finally { | ||
| fs.unlinkSync(filePath); | ||
| fs.rmdirSync(canonicalTmpDir); | ||
| } | ||
| }); | ||
|
|
||
| test('resolveCanonicalPath resolves wrong-cased path on case-insensitive filesystem', function () { | ||
| // This test is meaningful only on case-insensitive file systems (macOS, Windows) | ||
| if (process.platform === 'linux') { | ||
| this.skip(); | ||
| return; | ||
| } | ||
|
|
||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aspire-test-')); | ||
| const canonicalTmpDir = fs.realpathSync.native(tmpDir); | ||
| const filePath = path.join(canonicalTmpDir, 'MyAppHost.csproj'); | ||
| fs.writeFileSync(filePath, ''); | ||
|
|
||
| try { | ||
| // Pass a wrong-cased version of the path | ||
| const wrongCased = path.join(canonicalTmpDir, 'myapphost.csproj'); | ||
| const resolved = resolveCanonicalPath(wrongCased); | ||
|
|
||
| // On case-insensitive FS, realpathSync.native returns the actual on-disk casing | ||
| assert.strictEqual(path.basename(resolved), 'MyAppHost.csproj', | ||
| `Expected resolved path to have on-disk casing "MyAppHost.csproj", got "${path.basename(resolved)}"`); | ||
| assert.strictEqual(resolved, filePath, 'Full resolved path should match the on-disk canonical path'); | ||
| } finally { | ||
| fs.unlinkSync(filePath); | ||
| fs.rmdirSync(canonicalTmpDir); | ||
| } | ||
| }); | ||
|
|
||
| test('resolveCanonicalPath returns original path for non-existent file', () => { | ||
| const fakePath = path.join(os.tmpdir(), 'non-existent-dir-15588', 'DoesNotExist.csproj'); | ||
| const resolved = resolveCanonicalPath(fakePath); | ||
|
|
||
| assert.strictEqual(resolved, fakePath, 'Should return original path when file does not exist'); | ||
| }); | ||
|
|
||
| test('resolveCanonicalPath handles symlinks', function () { | ||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aspire-test-')); | ||
| const canonicalTmpDir = fs.realpathSync.native(tmpDir); | ||
| const realFile = path.join(canonicalTmpDir, 'RealFile.csproj'); | ||
| const symlink = path.join(canonicalTmpDir, 'SymLink.csproj'); | ||
|
|
||
| fs.writeFileSync(realFile, ''); | ||
|
|
||
| try { | ||
| fs.symlinkSync(realFile, symlink); | ||
| } catch { | ||
| // Symlinks may not be available (e.g., some Windows configs) | ||
| this.skip(); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const resolved = resolveCanonicalPath(symlink); | ||
| assert.ok(resolved.endsWith('RealFile.csproj'), | ||
| `Expected symlink to resolve to "RealFile.csproj", got "${path.basename(resolved)}"`); | ||
| } finally { | ||
| fs.unlinkSync(symlink); | ||
| fs.unlinkSync(realFile); | ||
| fs.rmdirSync(canonicalTmpDir); | ||
| } | ||
| }); | ||
|
|
||
| test('resolveCanonicalPath handles directory paths', () => { | ||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aspire-test-')); | ||
| const canonicalTmpDir = fs.realpathSync.native(tmpDir); | ||
|
|
||
| try { | ||
| const resolved = resolveCanonicalPath(canonicalTmpDir); | ||
| assert.ok(fs.existsSync(resolved), 'Resolved directory path should exist'); | ||
| assert.ok(path.isAbsolute(resolved), 'Resolved directory path should be absolute'); | ||
| } finally { | ||
| fs.rmdirSync(canonicalTmpDir); | ||
| } | ||
| }); | ||
|
|
||
| test('resolveCanonicalPath normalizes parent directory casing on case-insensitive filesystem', function () { | ||
| // Verifies that case normalization works for parent directories too, | ||
| // not just the filename component | ||
| if (process.platform === 'linux') { | ||
| this.skip(); | ||
| return; | ||
| } | ||
|
|
||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'AspireTestDir-')); | ||
| const canonicalTmpDir = fs.realpathSync.native(tmpDir); | ||
| const subDir = path.join(canonicalTmpDir, 'SubFolder'); | ||
| fs.mkdirSync(subDir); | ||
| const filePath = path.join(subDir, 'AppHost.csproj'); | ||
| fs.writeFileSync(filePath, ''); | ||
|
|
||
| try { | ||
| // Construct a path with wrong casing in both directory and file name | ||
| const wrongCased = path.join(canonicalTmpDir, 'subfolder', 'apphost.csproj'); | ||
| const resolved = resolveCanonicalPath(wrongCased); | ||
|
|
||
| assert.ok(resolved.includes('SubFolder'), `Expected "SubFolder" in resolved path, got "${resolved}"`); | ||
| assert.ok(resolved.endsWith('AppHost.csproj'), `Expected "AppHost.csproj" at end, got "${path.basename(resolved)}"`); | ||
| } finally { | ||
| fs.unlinkSync(filePath); | ||
| fs.rmdirSync(subDir); | ||
| fs.rmdirSync(canonicalTmpDir); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the AppHost was started from a symlinked path (for example a symlinked repo root, or a config entry that points through a symlink)?
realpathSync.native()does more than fix casing here: it dereferences the symlink and changes the literal path we send back via--apphost.That breaks the current CLI contract because the server side still keys AppHost state off
Path.GetFullPath(appPath)rather than a realpath/canonical target (DotNetBasedAppHostServerProjectandPrebuiltAppHostServerboth hash the full path string, andAuxiliaryBackchannelMonitorscope checks also usePath.GetFullPath).Path.GetFullPathpreserves the symlink text, so a running AppHost started as/repo-link/AppHost.csprojis distinct from/actual/repo/AppHost.csproj. After this change, restart/stop/logs can fail to find an already-running AppHost again, just under a different path variant.This is a regression from the old behavior, which forwarded the configured/opened path unchanged. The fix direction seems to be: normalize casing without resolving symlinks, or make the CLI canonicalize AppHost identity the same way on both sides.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fair. The change will need to be made in the CLI. Moving area path and closing this PR