Check Architecture on .NET FindPath API (#1974)

* Fix incorrect PATH echo

* Add Architecture Check into Find PATH API

We had a discussion about how Mac users tend to need the arm64 runtime and often have the x64 host installed on their PATH. This was a big concern. With global installs at least we can still rely on the output from `dotnet --info` for now, so we decided to add this back in.

* Fix test, the runtime install does not have an arch printed so we cant quite use it to check properly

* Fix test

* Fix test

* undo comment out

* Fix a very terrible bug due to a typo

* code cleanup
This commit is contained in:
Noah Gilson 2024-10-22 11:37:31 -07:00 коммит произвёл GitHub
Родитель 13e2bf6e6c
Коммит 2ff7c2ac4d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
8 изменённых файлов: 87 добавлений и 29 удалений

Просмотреть файл

@ -18,6 +18,7 @@
"capabilities": {
"virtualWorkspaces": true
},
"connectionString": "InstrumentationKey=02dc18e0-7494-43b2-b2a3-18ada5fcb522;IngestionEndpoint=https://westus2-0.in.applicationinsights.azure.com/;LiveEndpoint=https://westus2.livediagnostics.monitor.azure.com/;ApplicationId=e8e56970-a18a-4101-b7d1-1c5dd7c29eeb",
"main": "./out/extension.js",
"contributes": {
"commands": [

Просмотреть файл

@ -73,7 +73,6 @@ import {
IDotnetConditionValidator,
DotnetFindPathSettingFound,
DotnetFindPathLookupSetting,
DotnetFindPathDidNotMeetCondition,
DotnetFindPathMetCondition,
DotnetFindPathCommandInvoked,
JsonInstaller,
@ -472,7 +471,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
if(existingPath)
{
globalEventStream.post(new DotnetFindPathSettingFound(`Found vscode setting.`));
outputChannel.show(true);
loggingObserver.dispose();
return existingPath;
}
@ -485,7 +484,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
const validatedPATH = await getPathIfValid(dotnetPath, validator, commandContext);
if(validatedPATH)
{
outputChannel.show(true);
loggingObserver.dispose();
return validatedPATH;
}
}
@ -496,7 +495,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
const validatedRealPATH = await getPathIfValid(dotnetPath, validator, commandContext);
if(validatedRealPATH)
{
outputChannel.show(true);
loggingObserver.dispose();
return validatedRealPATH;
}
}
@ -505,12 +504,11 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
const validatedRoot = await getPathIfValid(dotnetOnROOT, validator, commandContext);
if(validatedRoot)
{
outputChannel.show(true);
loggingObserver.dispose();
return validatedRoot;
}
outputChannel.show(true);
loggingObserver.dispose();
return undefined;
});
@ -524,10 +522,6 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
globalEventStream.post(new DotnetFindPathMetCondition(`${path} met the conditions.`));
return path;
}
else
{
globalEventStream.post(new DotnetFindPathDidNotMeetCondition(`${path} did NOT satisfy the conditions.`));
}
}
return undefined;

Просмотреть файл

@ -207,7 +207,8 @@ suite('DotnetCoreAcquisitionExtension End to End', function()
return lowerPath.includes('dotnet') || lowerPath.includes('program') || lowerPath.includes('share') || lowerPath.includes('bin') || lowerPath.includes('snap') || lowerPath.includes('homebrew');
}
async function findPathWithRequirementAndInstall(version : string, iMode : DotnetInstallMode, arch : string, condition : DotnetVersionSpecRequirement, shouldFind : boolean, contextToLookFor? : IDotnetAcquireContext, setPath = true)
async function findPathWithRequirementAndInstall(version : string, iMode : DotnetInstallMode, arch : string, condition : DotnetVersionSpecRequirement, shouldFind : boolean, contextToLookFor? : IDotnetAcquireContext, setPath = true,
blockNoArch = false)
{
const installPath = await installRuntime(version, iMode, arch);
@ -224,11 +225,21 @@ suite('DotnetCoreAcquisitionExtension End to End', function()
}
extensionContext.environmentVariableCollection.replace('PATH', process.env.PATH ?? '');
if(blockNoArch)
{
extensionContext.environmentVariableCollection.replace('DOTNET_INSTALL_TOOL_DONT_ACCEPT_UNKNOWN_ARCH', '1');
process.env.DOTNET_INSTALL_TOOL_DONT_ACCEPT_UNKNOWN_ARCH = '1';
}
const result = 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';
if(shouldFind)
{
assert.exists(result, 'find path command returned a result');
@ -319,22 +330,39 @@ suite('DotnetCoreAcquisitionExtension End to End', function()
);
}).timeout(standardTimeoutTime);
/*
test('Find dotnet PATH Command Unmet Arch Condition', async () => {
// look for a different architecture of 3.1
if(os.platform() !== 'darwin')
{
// The CI Machines are running on ARM64 for OS X.
// They also have an x64 HOST. We can't set DOTNET_MULTILEVEL_LOOKUP to 0 because it will break the ability to find the host on --info
// As our runtime installs have no host. So the architecture will read as x64 even though it's not.
// As a 3.1 runtime host does not provide the architecture, but we try to use 3.1 because CI machines won't have it.
//
// This is not fixable until the runtime team releases a better way to get the architecture of a particular dotnet installation.
await findPathWithRequirementAndInstall('3.1', 'runtime', os.arch() == 'arm64' ? 'x64' : os.arch(), 'greater_than_or_equal', false,
{version : '3.1', mode : 'runtime', architecture : 'arm64', requestingExtensionId : requestingExtensionId}, true, true
);
}
}).timeout(standardTimeoutTime);
test('Find dotnet PATH Command Unmet Arch Condition With Host that prints Arch', async () => {
if(os.platform() !== 'darwin')
{
await findPathWithRequirementAndInstall('9.0', 'runtime', os.arch() == 'arm64' ? 'x64' : os.arch(), 'greater_than_or_equal', false,
{version : '9.0', mode : 'runtime', architecture : 'arm64', requestingExtensionId : requestingExtensionId}
);
}
}).timeout(standardTimeoutTime);
test('Find dotnet PATH Command No Arch Available But Accept By Default', async () => {
// look for a different architecture of 3.1
if(os.platform() !== 'darwin')
{
await findPathWithRequirementAndInstall('3.1', 'runtime', os.arch() == 'arm64' ? 'x64' : os.arch(), 'greater_than_or_equal', true,
{version : '3.1', mode : 'runtime', architecture : 'arm64', requestingExtensionId : requestingExtensionId}
);
}
}).timeout(standardTimeoutTime);
*/
test('Install SDK Globally E2E (Requires Admin)', async () => {
// We only test if the process is running under ADMIN because non-admin requires user-intervention.

Просмотреть файл

@ -13,6 +13,8 @@ import { IDotnetConditionValidator } from './IDotnetConditionValidator';
import * as versionUtils from './VersionUtilities';
import * as os from 'os';
import { FileUtilities } from '../Utils/FileUtilities';
import { DotnetFindPathDidNotMeetCondition, DotnetUnableToCheckPATHArchitecture } from '../EventStream/EventStreamEvents';
export class DotnetConditionValidator implements IDotnetConditionValidator
{
@ -25,7 +27,7 @@ export class DotnetConditionValidator implements IDotnetConditionValidator
{
const availableRuntimes = await this.getRuntimes(dotnetExecutablePath);
const requestedMajorMinor = versionUtils.getMajorMinor(requirement.acquireContext.version, this.workerContext.eventStream, this.workerContext);
const hostArch = await this.getHostArchitecture(dotnetExecutablePath);
const hostArch = await this.getHostArchitecture(dotnetExecutablePath, requirement);
if(availableRuntimes.some((runtime) =>
{
@ -43,11 +45,16 @@ export class DotnetConditionValidator implements IDotnetConditionValidator
{
// The SDK includes the Runtime, ASP.NET Core Runtime, and Windows Desktop Runtime. So, we don't need to check the mode.
const foundVersion = versionUtils.getMajorMinor(sdk.version, this.workerContext.eventStream, this.workerContext);
return this.stringArchitectureMeetsRequirement(hostArch, requirement.acquireContext.architecture), this.stringVersionMeetsRequirement(foundVersion, requestedMajorMinor, requirement.versionSpecRequirement);
return this.stringArchitectureMeetsRequirement(hostArch, requirement.acquireContext.architecture) && this.stringVersionMeetsRequirement(foundVersion, requestedMajorMinor, requirement.versionSpecRequirement);
}))
{
return true;
}
else
{
this.workerContext.eventStream.post(new DotnetFindPathDidNotMeetCondition(`${dotnetExecutablePath} did NOT satisfy the conditions: hostArch: ${hostArch}, requiredArch: ${requirement.acquireContext.architecture},
required version: ${requestedMajorMinor}`));
}
}
return false;
@ -59,18 +66,43 @@ export class DotnetConditionValidator implements IDotnetConditionValidator
* @returns The architecture of the dotnet host from the PATH, in dotnet info string format
* The .NET Host will only list versions of the runtime and sdk that match its architecture.
* Thus, any runtime or sdk that it prints out will be the same architecture as the host.
* This information is not always accurate as dotnet info is subject to change.
*
* @remarks Will return '' if the architecture cannot be determined for some peculiar reason (e.g. dotnet --info is broken or changed).
*/
// eslint-disable-next-line @typescript-eslint/require-await
private async getHostArchitecture(hostPath : string) : Promise<string>
private async getHostArchitecture(hostPath : string, requirement : IDotnetFindPathContext) : Promise<string>
{
return '';
/* The host architecture can be inaccurate. Imagine a local runtime install. There is no way to tell the architecture of that runtime,
... as the Host will not print its architecture in dotnet info.
Return '' for now to pass all arch checks.
// dotnet --info is not machine-readable and subject to breaking changes. See https://github.com/dotnet/sdk/issues/33697 and https://github.com/dotnet/runtime/issues/98735/
// Unfortunately even with a new API, that might not go in until .NET 10 and beyond, so we have to rely on dotnet --info for now.*/
Need to get an issue from the runtime team. See https://github.com/dotnet/sdk/issues/33697 and https://github.com/dotnet/runtime/issues/98735/ */
const infoCommand = CommandExecutor.makeCommand(`"${hostPath}"`, ['--info']);
const envWithForceEnglish = process.env;
envWithForceEnglish.DOTNET_CLI_UI_LANGUAGE = 'en-US';
// System may not have english installed, but CDK already calls this without issue -- the .NET SDK language invocation is also wrapped by a runtime library and natively includes english assets
const hostArch = await (this.executor!).execute(infoCommand, { env: envWithForceEnglish }, false).then((result) =>
{
const lines = result.stdout.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
// This is subject to change but there is no good alternative to do this
const archLine = lines.find((line) => line.startsWith('Architecture:'));
if(archLine === undefined)
{
this.workerContext.eventStream.post(new DotnetUnableToCheckPATHArchitecture(`Could not find the architecture of the dotnet host ${hostPath}. If this host does not match the architecture ${requirement.acquireContext.architecture}:
Please set the PATH to a dotnet host that matches the architecture ${requirement.acquireContext.architecture}. An incorrect architecture will cause instability for the extension ${requirement.acquireContext.requestingExtensionId}.`));
if(process.env.DOTNET_INSTALL_TOOL_DONT_ACCEPT_UNKNOWN_ARCH === '1')
{
return 'unknown'; // Bad value to cause failure mismatch, which will become 'auto'
}
else
{
return '';
}
}
const arch = archLine.split(' ')[1];
return arch;
});
return hostArch;
}
public async getSDKs(existingPath : string) : Promise<IDotnetListInfo[]>

Просмотреть файл

@ -100,7 +100,7 @@ export class DotnetPathFinder implements IDotnetPathFinder
this.workerContext.eventStream.post(new DotnetFindPathLookupPATH(`Looking up .NET on the path. Process.env.path: ${process.env.PATH}.
Executor Path: ${(await this.executor?.execute(
os.platform() === 'win32' ? CommandExecutor.makeCommand('echo', ['%PATH']) : CommandExecutor.makeCommand('env', []),
os.platform() === 'win32' ? CommandExecutor.makeCommand('echo', ['%PATH%']) : CommandExecutor.makeCommand('env', []),
undefined,
false))?.stdout}

Просмотреть файл

@ -746,6 +746,10 @@ export class DotnetFileIntegrityFailureEvent extends DotnetVisibleWarningEvent {
public readonly eventName = 'DotnetFileIntegrityFailureEvent';
}
export class DotnetUnableToCheckPATHArchitecture extends DotnetVisibleWarningEvent {
public readonly eventName = 'DotnetUnableToCheckPATHArchitecture';
}
export class DotnetVersionCategorizedEvent extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetVersionCategorizedEvent';

Просмотреть файл

@ -30,10 +30,8 @@ export class TelemetryObserver implements IEventStreamObserver {
{
if (telemetryReporter === undefined)
{
const extensionVersion = packageJson.version;
const connectionString = packageJson.connectionString;
const extensionId = packageJson.name;
this.telemetryReporter = new TelemetryReporter(connectionString);
const connectionString : string = packageJson.connectionString;
this.telemetryReporter = new TelemetryReporter(connectionString ?? '');
}
else
{

Просмотреть файл

@ -14,6 +14,7 @@
"appInsightsKey": "02dc18e0-7494-43b2-b2a3-18ada5fcb522",
"icon": "images/dotnetIcon.png",
"version": "2.0.1",
"connectionString": "InstrumentationKey=02dc18e0-7494-43b2-b2a3-18ada5fcb522;IngestionEndpoint=https://westus2-0.in.applicationinsights.azure.com/;LiveEndpoint=https://westus2.livediagnostics.monitor.azure.com/;ApplicationId=e8e56970-a18a-4101-b7d1-1c5dd7c29eeb",
"publisher": "ms-dotnettools",
"engines": {
"vscode": "^1.74.0"