Skip to content

Commit

Permalink
Add Hostfxr Discovery Logic in place of Bad PATH from VS Code (#2048)
Browse files Browse the repository at this point in the history
* Move registry logic into its own place

This is so we can add hostfxr logic to find the path in the registry and dedupe logic and isolate the components.

* fix build

* move tests over

* isolate query logic to another component

* update linux test for 9.0 release

* Add support for mac and linux

* Scan registry properly

* Add logging

* Add simple linux mac test

* Improve registry logic

* Fix registry and host lookup

* fix paran

* use correct path method

* fix it again

* why are parans so hard xd

* Fix logic for path detection

* Add env variable to skip hostfxr logic

* linux tests should consider if os suppots dotnet 9 by default

* Fix test

* Since the tests can't edit /etc/ use a mock file system

* tests expect dotnet dll in path
  • Loading branch information
nagilson authored Jan 6, 2025
1 parent cd01dbc commit b884fc9
Show file tree
Hide file tree
Showing 17 changed files with 595 additions and 139 deletions.
2 changes: 1 addition & 1 deletion sample/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2207,7 +2207,7 @@ uuid@^8.3.0:
fsevents "^2.3.3"

"vscode-dotnet-runtime@file:../vscode-dotnet-runtime-extension":
version "2.2.3"
version "2.2.4"
resolved "file:../vscode-dotnet-runtime-extension"
dependencies:
"@types/chai-as-promised" "^7.1.8"
Expand Down
21 changes: 21 additions & 0 deletions vscode-dotnet-runtime-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {
DotnetFindPathLookupSetting,
DotnetFindPathMetCondition,
DotnetFindPathCommandInvoked,
DotnetFindPathNoPathMetCondition,
JsonInstaller,
} from 'vscode-dotnet-runtime-library';
import { dotnetCoreAcquisitionExtensionId } from './DotnetCoreAcquisitionId';
Expand Down Expand Up @@ -512,7 +513,27 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
return { dotnetPath: validatedRoot };
}

const dotnetOnHostfxrRecord = await finder.findHostInstallPaths(commandContext.acquireContext.architecture);
for(const dotnetPath of dotnetOnHostfxrRecord ?? [])
{
const validatedHostfxr = await getPathIfValid(dotnetPath, validator, commandContext);
if(validatedHostfxr && process.env.DOTNET_INSTALL_TOOL_SKIP_HOSTFXR !== 'true')
{
loggingObserver.dispose();
return { dotnetPath: validatedHostfxr };
}
}

loggingObserver.dispose();
globalEventStream.post(new DotnetFindPathNoPathMetCondition(`Could not find a single host path that met the conditions.
existingPath : ${existingPath?.dotnetPath}
onPath : ${JSON.stringify(dotnetsOnPATH)}
onRealPath : ${JSON.stringify(dotnetsOnRealPATH)}
onRoot : ${dotnetOnROOT}
onHostfxrRecord : ${JSON.stringify(dotnetOnHostfxrRecord)}
Requirement:
${JSON.stringify(commandContext)}`));
return undefined;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
EnvironmentVariableIsDefined,
MockEnvironmentVariableCollection,
getPathSeparator,
LinuxVersionResolver,
getMockUtilityContext,
} from 'vscode-dotnet-runtime-library';
import * as extension from '../../extension';
import { warn } from 'console';
Expand Down Expand Up @@ -208,7 +210,7 @@ suite('DotnetCoreAcquisitionExtension End to End', function()
}

async function findPathWithRequirementAndInstall(version : string, iMode : DotnetInstallMode, arch : string, condition : DotnetVersionSpecRequirement, shouldFind : boolean, contextToLookFor? : IDotnetAcquireContext, setPath = true,
blockNoArch = false)
blockNoArch = false, dontCheckNonPaths = true)
{
const installPath = await installRuntime(version, iMode, arch);

Expand All @@ -232,13 +234,19 @@ suite('DotnetCoreAcquisitionExtension End to End', function()
process.env.DOTNET_INSTALL_TOOL_DONT_ACCEPT_UNKNOWN_ARCH = '1';
}

if(dontCheckNonPaths)
{
process.env.DOTNET_INSTALL_TOOL_SKIP_HOSTFXR = 'true';
}

const result : IDotnetAcquireResult = await vscode.commands.executeCommand<IDotnetAcquireResult>('dotnet.findPath',
{ acquireContext : contextToLookFor ?? { version, requestingExtensionId : requestingExtensionId, mode: iMode, architecture : arch } as IDotnetAcquireContext,
versionSpecRequirement : condition} as IDotnetFindPathContext
);

extensionContext.environmentVariableCollection.replace('DOTNET_INSTALL_TOOL_DONT_ACCEPT_UNKNOWN_ARCH', '0');
process.env.DOTNET_INSTALL_TOOL_DONT_ACCEPT_UNKNOWN_ARCH = '0';
process.env.DOTNET_INSTALL_TOOL_SKIP_HOSTFXR = '0';

if(shouldFind)
{
Expand Down Expand Up @@ -574,8 +582,8 @@ suite('DotnetCoreAcquisitionExtension End to End', function()
}
else
{
assert.equal(result[0].version, '9.0.1xx', 'The SDK did not recommend the version it was supposed to, which should be N.0.1xx based on surface level distro knowledge. If a new version is available, this test may need to be updated to the newest version.');

const distroVersion = await new LinuxVersionResolver(mockAcquisitionContext, getMockUtilityContext()).getRunningDistro();
assert.equal(result[0].version, Number(distroVersion) > 22.04 ? '9.0.1xx' : '8.0.1xx', 'The SDK did not recommend the version it was supposed to, which should be N.0.1xx based on surface level distro knowledge. If a new version is available, this test may need to be updated to the newest version.');
}
}).timeout(standardTimeoutTime);

Expand Down
94 changes: 90 additions & 4 deletions vscode-dotnet-runtime-library/src/Acquisition/DotnetPathFinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,38 @@ import { IDotnetPathFinder } from './IDotnetPathFinder';

import * as os from 'os';
import * as path from 'path';
import { realpathSync, existsSync } from 'fs';
import * as lodash from 'lodash';
import { realpathSync, existsSync, readFileSync } from 'fs';
import { EnvironmentVariableIsDefined, getDotnetExecutable, getOSArch, getPathSeparator } from '../Utils/TypescriptUtilities';
import { DotnetConditionValidator } from './DotnetConditionValidator';
import {
DotnetFindPathHostFxrResolutionLookup,
DotnetFindPathLookupPATH,
DotnetFindPathLookupRealPATH,
DotnetFindPathLookupRootPATH,
DotnetFindPathNoHostOnFileSystem,
DotnetFindPathNoHostOnRegistry,
DotnetFindPathNoRuntimesOnHost,
DotnetFindPathOnFileSystem,
DotnetFindPathOnRegistry,
DotnetFindPathPATHFound,
DotnetFindPathRealPATHFound,
DotnetFindPathRootEmulationPATHFound,
DotnetFindPathRootPATHFound,
DotnetFindPathRootUnderEmulationButNoneSet
DotnetFindPathRootUnderEmulationButNoneSet,
FileDoesNotExist
} from '../EventStream/EventStreamEvents';
import { RegistryReader } from './RegistryReader';
import { FileUtilities } from '../Utils/FileUtilities';
import { IFileUtilities } from '../Utils/IFileUtilities';

export class DotnetPathFinder implements IDotnetPathFinder
{

public constructor(private readonly workerContext : IAcquisitionWorkerContext, private readonly utilityContext : IUtilityContext, private executor? : ICommandExecutor)
public constructor(private readonly workerContext : IAcquisitionWorkerContext, private readonly utilityContext : IUtilityContext, private executor? : ICommandExecutor, private file? : IFileUtilities)
{
this.executor ??= new CommandExecutor(this.workerContext, this.utilityContext);
this.file ??= new FileUtilities();
}

/**
Expand Down Expand Up @@ -144,7 +155,7 @@ Bin Bash Path: ${os.platform() !== 'win32' ? (await this.executor?.execute(Comma
const dotnetExecutable = getDotnetExecutable();
for (const pathOnPATH of pathsOnPATH)
{
const resolvedDotnetPath = path.resolve(pathOnPATH, dotnetExecutable);
const resolvedDotnetPath = path.join(pathOnPATH, dotnetExecutable);
if (existsSync(resolvedDotnetPath))
{
this.workerContext.eventStream.post(new DotnetFindPathLookupPATH(`Looking up .NET on the path by processing PATH string. resolved: ${resolvedDotnetPath}.`));
Expand Down Expand Up @@ -196,6 +207,81 @@ Bin Bash Path: ${os.platform() !== 'win32' ? (await this.executor?.execute(Comma
return undefined;
}

public async findHostInstallPaths(requestedArchitecture : string) : Promise<string[] | undefined>
{
this.workerContext.eventStream.post(new DotnetFindPathHostFxrResolutionLookup(`Looking up .NET without checking the PATH.`));

const oldLookup = process.env.DOTNET_MULTILEVEL_LOOKUP;
process.env.DOTNET_MULTILEVEL_LOOKUP = '0';

if(os.platform() === 'win32')
{
const registryReader = new RegistryReader(this.workerContext, this.utilityContext, this.executor);
const hostPathWin = await registryReader.getHostLocation(requestedArchitecture);
const paths = hostPathWin ? [path.join(hostPathWin, getDotnetExecutable()), path.join(realpathSync(hostPathWin), getDotnetExecutable())] : [];
if(paths.length > 0)
{
this.workerContext.eventStream.post(new DotnetFindPathOnRegistry(`The host could be found in the registry. ${JSON.stringify(paths)}`));
}
else
{
this.workerContext.eventStream.post(new DotnetFindPathNoHostOnRegistry(`The host could not be found in the registry`));
}
return this.returnWithRestoringEnvironment(await this.getTruePath(lodash.uniq(paths)), 'DOTNET_MULTILEVEL_LOOKUP', oldLookup);
}
else
{
// Possible values for arch are: x86, x64, arm32, arm64
// x86 and arm32 are not a concern since 32 bit vscode is out of support and not needed by other extensions

// https://github.com/dotnet/designs/blob/main/accepted/2021/install-location-per-architecture.md#new-format

const paths : string[] = [];
const netSixAndAboveHostInstallSaveLocation = `/etc/dotnet/install_location_${requestedArchitecture}`;
const netFiveAndNetSixAboveFallBackInstallSaveLocation = `/etc/dotnet/install_location`;

paths.push(...this.getPathsFromEtc(netSixAndAboveHostInstallSaveLocation));
paths.push(...this.getPathsFromEtc(netFiveAndNetSixAboveFallBackInstallSaveLocation));

if(paths.length > 0)
{
this.workerContext.eventStream.post(new DotnetFindPathOnFileSystem(`The host could be found in the file system. ${JSON.stringify(paths)}`));
}
else
{
this.workerContext.eventStream.post(new DotnetFindPathNoHostOnFileSystem(`The host could not be found in the file system.`));
}

return this.returnWithRestoringEnvironment(await this.getTruePath(lodash.uniq(paths)), 'DOTNET_MULTILEVEL_LOOKUP', oldLookup);
}
}

private getPathsFromEtc(etcLoc : string) : Array<string>
{
const paths : string[] = [];
if(this.file!.existsSync(etcLoc))
{
try
{
const installPath = this.file!.readSync(etcLoc).toString().trim();
paths.push(path.join(installPath, getDotnetExecutable()));
paths.push(path.join(realpathSync(installPath), getDotnetExecutable()));
}
catch(error : any) // eslint-disable-line @typescript-eslint/no-explicit-any
{
// readfile throws if the file gets deleted in between the existing check and now
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.workerContext.eventStream.post(new FileDoesNotExist(`The host was deleted after checking in the file system. ${error?.message} ${etcLoc}`));
}
}
else
{
this.workerContext.eventStream.post(new FileDoesNotExist(`The host save location never existed at ${etcLoc}`));
}

return paths;
}

/**
*
* @param tentativePaths Paths that may hold a dotnet executable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface IDotnetPathFinder
findDotnetRootPath(requestedArchitecture : string): Promise<string | undefined>;
findRawPathEnvironmentSetting(tryUseTrueShell : boolean): Promise<string[] | undefined>;
findRealPathEnvironmentSetting(tryUseTrueShell : boolean): Promise<string[] | undefined>;
findHostInstallPaths(requestedArchitecture : string): Promise<string[] | undefined>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ export abstract class IGlobalInstaller {

public abstract getExpectedGlobalSDKPath(specificSDKVersionInstalled : string, installedArch : string, macPathShouldExist? : boolean) : Promise<string>

public abstract getGlobalSdkVersionsInstalledOnMachine() : Promise<Array<string>>;

/**
*
* @returns The folder where global sdk installers will be downloaded onto the disk.
Expand Down
14 changes: 14 additions & 0 deletions vscode-dotnet-runtime-library/src/Acquisition/IRegistryReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Licensed to the .NET Foundation under one or more agreements.
* The .NET Foundation licenses this file to you under the MIT license.
*--------------------------------------------------------------------------------------------*/

import { IEventStream } from '../EventStream/EventStream';
import { IVSCodeExtensionContext } from '../IVSCodeExtensionContext';

export abstract class IRegistryReader
{
constructor() {}

public abstract getGlobalSdkVersionsInstalledOnMachine() : Promise<Array<string>>;
}
Loading

0 comments on commit b884fc9

Please sign in to comment.