Skip to content

Commit

Permalink
Check Architecture on .NET FindPath API (#1974)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nagilson authored Oct 22, 2024
1 parent 13e2bf6 commit 2ff7c2a
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 29 deletions.
1 change: 1 addition & 0 deletions sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
16 changes: 5 additions & 11 deletions vscode-dotnet-runtime-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ import {
IDotnetConditionValidator,
DotnetFindPathSettingFound,
DotnetFindPathLookupSetting,
DotnetFindPathDidNotMeetCondition,
DotnetFindPathMetCondition,
DotnetFindPathCommandInvoked,
JsonInstaller,
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}
}
Expand All @@ -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;
}
}
Expand All @@ -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;
});

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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');
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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) =>
{
Expand All @@ -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;
Expand All @@ -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.*/

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;
});

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/ */
return hostArch;
}

public async getSDKs(existingPath : string) : Promise<IDotnetListInfo[]>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
1 change: 1 addition & 0 deletions vscode-dotnet-sdk-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 2ff7c2a

Please sign in to comment.