diff --git a/sample/package-lock.json b/sample/package-lock.json index bdc90cec97..f8f775c0ea 100644 --- a/sample/package-lock.json +++ b/sample/package-lock.json @@ -1,12 +1,12 @@ { "name": "sample-extension", - "version": "0.0.1", + "version": "0.0.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sample-extension", - "version": "0.0.1", + "version": "0.0.9", "license": "MIT", "dependencies": { "@vscode/vsce": "^2.19.0", @@ -33,7 +33,7 @@ }, "../vscode-dotnet-runtime-extension": { "name": "vscode-dotnet-runtime", - "version": "2.1.6", + "version": "2.1.7", "license": "MIT", "dependencies": { "@types/chai-as-promised": "^7.1.8", @@ -81,6 +81,7 @@ "@types/vscode": "1.74.0", "@vscode/extension-telemetry": "^0.9.7", "@vscode/sudo-prompt": "^9.3.1", + "@vscode/test-electron": "^2.4.1", "axios": "^1.7.4", "axios-cache-interceptor": "^1.5.3", "axios-retry": "^3.4.0", @@ -96,8 +97,7 @@ "run-script-os": "^1.1.6", "semver": "^7.6.2", "shelljs": "^0.8.5", - "typescript": "^5.5.4", - "vscode-test": "^1.6.1" + "typescript": "^5.5.4" }, "devDependencies": { "@types/chai": "4.2.22", @@ -5675,6 +5675,7 @@ "@types/vscode": "1.74.0", "@vscode/extension-telemetry": "^0.9.7", "@vscode/sudo-prompt": "^9.3.1", + "@vscode/test-electron": "^2.4.1", "axios": "^1.7.4", "axios-cache-interceptor": "^1.5.3", "axios-retry": "^3.4.0", @@ -5692,8 +5693,7 @@ "run-script-os": "^1.1.6", "semver": "^7.6.2", "shelljs": "^0.8.5", - "typescript": "^5.5.4", - "vscode-test": "^1.6.1" + "typescript": "^5.5.4" } }, "vscode-dotnet-sdk": { diff --git a/sample/package.json b/sample/package.json index 3507f1e180..5845e7cb8f 100644 --- a/sample/package.json +++ b/sample/package.json @@ -7,7 +7,7 @@ "url": "https://github.com/dotnet/vscode-dotnet-runtime.git" }, "license": "MIT", - "version": "0.0.1", + "version": "0.0.9", "publisher": "ms-dotnettools", "engines": { "vscode": "^1.75.0" @@ -66,6 +66,11 @@ "title": "Install .NET SDK Globally via .NET Install Tool (Former Runtime Extension)", "category": "Sample" }, + { + "command": "sample.dotnet.findPath", + "title": "Find the .NET on the PATH", + "category": "Sample" + }, { "command": "sample.dotnet-sdk.acquire", "title": "Acquire .NET SDK", diff --git a/sample/src/extension.ts b/sample/src/extension.ts index 3b612c9fa6..1a36a0d3f0 100644 --- a/sample/src/extension.ts +++ b/sample/src/extension.ts @@ -8,12 +8,14 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { DotnetInstallMode, + DotnetVersionSpecRequirement, IDotnetAcquireContext, IDotnetAcquireResult, + IDotnetFindPathContext, IDotnetListVersionsResult, } from 'vscode-dotnet-runtime-library'; -import * as runtimeExtension from 'vscode-dotnet-runtime'; -import * as sdkExtension from 'vscode-dotnet-sdk'; +import * as runtimeExtension from 'vscode-dotnet-runtime'; // comment this out when packing the extension +import * as sdkExtension from 'vscode-dotnet-sdk'; // comment this out when packing the extension import { install } from 'source-map-support'; export function activate(context: vscode.ExtensionContext) { @@ -33,8 +35,8 @@ export function activate(context: vscode.ExtensionContext) { */ const requestingExtensionId = 'ms-dotnettools.sample-extension'; - runtimeExtension.activate(context); - sdkExtension.activate(context); + runtimeExtension.activate(context); // comment this out when packing the extension + sdkExtension.activate(context); // comment this out when packing the extension // -------------------------------------------------------------------------- @@ -167,17 +169,6 @@ ${stderr}`); } }); - context.subscriptions.push( - sampleHelloWorldRegistration, - sampleAcquireRegistration, - sampleAcquireASPNETRegistration, - sampleAcquireStatusRegistration, - sampleDotnetUninstallAllRegistration, - sampleConcurrentTest, - sampleConcurrentASPNETTest, - sampleShowAcquisitionLogRegistration, - ); - const sampleGlobalSDKFromRuntimeRegistration = vscode.commands.registerCommand('sample.dotnet.acquireGlobalSDK', async (version) => { if (!version) { version = await vscode.window.showInputBox({ @@ -199,6 +190,48 @@ ${stderr}`); } }); + const sampleFindPathRegistration = vscode.commands.registerCommand('sample.dotnet.findPath', async () => + { + const version = await vscode.window.showInputBox( + { + placeHolder: '8.0', + value: '8.0', + prompt: 'The .NET runtime version.', + }); + + const arch = await vscode.window.showInputBox({ + placeHolder: 'x64', + value: 'x64', + prompt: 'The .NET runtime architecture.', + }); + + const requirement = await vscode.window.showInputBox({ + placeHolder: 'greater_than_or_equal', + value: 'greater_than_or_equal', + prompt: 'The condition to search for a requirement.', + }); + + let commandContext : IDotnetFindPathContext = { acquireContext: {version: version, requestingExtensionId: requestingExtensionId, architecture : arch, mode : 'runtime'} as IDotnetAcquireContext, + versionSpecRequirement: requirement as DotnetVersionSpecRequirement}; + + const result = await vscode.commands.executeCommand('dotnet.findPath', commandContext); + + vscode.window.showInformationMessage(`.NET Path Discovered\n +${JSON.stringify(result) ?? 'undefined'}`); + }); + + context.subscriptions.push( + sampleHelloWorldRegistration, + sampleAcquireRegistration, + sampleAcquireASPNETRegistration, + sampleAcquireStatusRegistration, + sampleDotnetUninstallAllRegistration, + sampleConcurrentTest, + sampleConcurrentASPNETTest, + sampleShowAcquisitionLogRegistration, + sampleFindPathRegistration, + ); + // -------------------------------------------------------------------------- // ---------------------sdk extension registrations-------------------------- diff --git a/sample/yarn.lock b/sample/yarn.lock index 078fda9e89..e1b27a11a0 100644 --- a/sample/yarn.lock +++ b/sample/yarn.lock @@ -1809,6 +1809,7 @@ util-deprecate@^1.0.1: "@types/vscode" "1.74.0" "@vscode/extension-telemetry" "^0.9.7" "@vscode/sudo-prompt" "^9.3.1" + "@vscode/test-electron" "^2.4.1" axios "^1.7.4" axios-cache-interceptor "^1.5.3" axios-retry "^3.4.0" @@ -1825,12 +1826,11 @@ util-deprecate@^1.0.1: semver "^7.6.2" shelljs "^0.8.5" typescript "^5.5.4" - vscode-test "^1.6.1" optionalDependencies: fsevents "^2.3.3" "vscode-dotnet-runtime@file:../vscode-dotnet-runtime-extension": - version "2.1.6" + version "2.1.7" resolved "file:../vscode-dotnet-runtime-extension" dependencies: "@types/chai-as-promised" "^7.1.8" diff --git a/vscode-dotnet-runtime-extension/CHANGELOG.md b/vscode-dotnet-runtime-extension/CHANGELOG.md index d40676e10f..0cecd920de 100644 --- a/vscode-dotnet-runtime-extension/CHANGELOG.md +++ b/vscode-dotnet-runtime-extension/CHANGELOG.md @@ -7,7 +7,11 @@ and this project adheres to [Semantic Versioning]. ## [Unreleased] -## [2.1.6] - 2024-09-05 +## [2.1.7] - 2024-09-31 + +Adds the API dotnet.findPath() to see if there's an existing .NET installation on the PATH. + +## [2.1.6] - 2024-09-20 Fixes an issue with SDK installs on Mac M1 or Arm Mac where the non-emulation path was used. Fixes an issue where spaces in the username will cause failure for SDK resolution. diff --git a/vscode-dotnet-runtime-extension/package-lock.json b/vscode-dotnet-runtime-extension/package-lock.json index c23c3e23a4..00e16608c7 100644 --- a/vscode-dotnet-runtime-extension/package-lock.json +++ b/vscode-dotnet-runtime-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-dotnet-runtime", - "version": "2.1.6", + "version": "2.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vscode-dotnet-runtime", - "version": "2.1.6", + "version": "2.1.7", "license": "MIT", "dependencies": { "@types/chai-as-promised": "^7.1.8", diff --git a/vscode-dotnet-runtime-extension/package.json b/vscode-dotnet-runtime-extension/package.json index 91c8bbdaff..241535b273 100644 --- a/vscode-dotnet-runtime-extension/package.json +++ b/vscode-dotnet-runtime-extension/package.json @@ -11,9 +11,9 @@ "author": "Microsoft Corporation", "displayName": ".NET Install Tool", "description": "This extension installs and manages different versions of the .NET SDK and Runtime.", - "appInsightsKey": "02dc18e0-7494-43b2-b2a3-18ada5fcb522", + "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", "icon": "images/dotnetIcon.png", - "version": "2.1.6", + "version": "2.1.7", "publisher": "ms-dotnettools", "engines": { "vscode": "^1.81.1" @@ -79,7 +79,7 @@ "dotnetAcquisitionExtension.existingDotnetPath": { "type": "array", "markdownDescription": "The path to an existing .NET host executable for an extension's code to run under, not for your project to run under.\nRestart VS Code to apply changes.\n\n⚠️ This is NOT the .NET Runtime that your project will use to run. Extensions such as `C#`, `C# DevKit`, and more have components written in .NET. This .NET PATH is the `dotnet.exe` that these extensions will use to run their code, not your code.\n\nUsing a path value in which .NET does not meet the requirements of a specific extension will cause that extension to fail.\n\n🚀 The version of .NET that is used for your project is determined by the .NET host, or dotnet.exe. The .NET host picks a runtime based on your project. To use a specific version of .NET for your project, install the .NET SDK using the `.NET Install Tool - Install SDK System-Wide` command, install .NET manually using [our instructions](https://dotnet.microsoft.com/download), or edit your PATH environment variable to point to a `dotnet.exe` that has an `/sdk/` folder with only one SDK.", - "description": "The path to an existing .NET host executable for an extension's code to run under, not for your project to run under.\nRestart VS Code to apply changes.\n\n⚠️ This is NOT the .NET Runtime that your project will use to run. Extensions such as 'C#', 'C# DevKit', and more have components written in .NET. This .NET PATH is the 'dotnet.exe' that these extensions will use to run their code, not your code.\n\nUsing a path value in which .NET does not meet the requirements of a specific extension will cause that extension to fail.\n\n🚀 The version of .NET that is used for your project is determined by the .NET host, or dotnet.exe. The .NET host picks a runtime based on your project. To use a specific version of .NET for your project, install the .NET SDK using the '.NET Install Tool - Install SDK System-Wide' command, use the instructions at https://dotnet.microsoft.com/download to manually install the .NET SDK, or edit your PATH environment variable to point to a 'dotnet.exe' that has an '/sdk/' folder with only one SDK.", + "description": "The path to an existing .NET host executable for an extension's code to run under, not for your project to run under.\nRestart VS Code to apply changes.\n\n⚠️ This is NOT the .NET Runtime that your project will use to run. Extensions such as 'C#', 'C# DevKit', and more have components written in .NET. This .NET PATH is the 'dotnet.exe' that these extensions will use to run their code, not your code.\n\nUsing a path value in which .NET does not meet the requirements of a specific extension will cause that extension to fail.\n\n🚀 The version of .NET that is used for your project is determined by the .NET host, or dotnet.exe. The .NET host picks a runtime based on your project. To use a specific version of .NET for your project, install the .NET SDK using the '.NET Install Tool - Install SDK System-Wide' command, use the instructions at https://dotnet.microsoft.com/download to manually install the .NET SDK, or edit your PATH environment variable to point to a 'dotnet.exe' that has an '/sdk/' folder with only one SDK.", "examples": [ "C:\\Program Files\\dotnet\\dotnet.exe", "/usr/local/share/dotnet/dotnet", @@ -99,7 +99,7 @@ "type": "string", "description": "URL to a proxy if you use one, such as: https://proxy:port" }, - "dotnetAcquisitionExtension.allowInvalidPaths": { + "dotnetAcquisitionExtension.allowInvalidPaths": { "type": "boolean", "description": "If you'd like to continue using a .NET path that is not meant to be used for an extension and may cause instability (please read above about the existingDotnetPath setting) then set this to true and restart." } diff --git a/vscode-dotnet-runtime-extension/src/extension.ts b/vscode-dotnet-runtime-extension/src/extension.ts index c7027589d9..1134d52727 100644 --- a/vscode-dotnet-runtime-extension/src/extension.ts +++ b/vscode-dotnet-runtime-extension/src/extension.ts @@ -66,6 +66,16 @@ import { getMajorMinor, DotnetOfflineWarning, IUtilityContext, + IDotnetFindPathContext, + DotnetVersionSpecRequirement, + DotnetConditionValidator, + DotnetPathFinder, + IDotnetConditionValidator, + DotnetFindPathSettingFound, + DotnetFindPathLookupSetting, + DotnetFindPathDidNotMeetCondition, + DotnetFindPathMetCondition, + DotnetFindPathCommandInvoked, } from 'vscode-dotnet-runtime-library'; import { dotnetCoreAcquisitionExtensionId } from './DotnetCoreAcquisitionId'; import { InstallTrackerSingleton } from 'vscode-dotnet-runtime-library/dist/Acquisition/InstallTrackerSingleton'; @@ -87,6 +97,7 @@ namespace commandKeys { export const acquireGlobalSDK = 'acquireGlobalSDK'; export const acquireStatus = 'acquireStatus'; export const uninstall = 'uninstall'; + export const findPath = 'findPath'; export const uninstallPublic = 'uninstallPublic' export const uninstallAll = 'uninstallAll'; export const listVersions = 'listVersions'; @@ -432,6 +443,95 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex return uninstall(commandContext); }); + /** + * @param commandContext The context of the request to find the dotnet path. + * We wrap an AcquisitionContext which must include the version, requestingExtensionId, architecture of .NET desired, and mode. + * The architecture should be of the node format ('x64', 'x86', 'arm64', etc.) + * + * @returns the path to the dotnet executable, if one can be found. This should be the true path to the executable. undefined if none can be found. + * + * @remarks Priority Order for path lookup: + * VSCode Setting -> PATH -> Realpath of PATH -> DOTNET_ROOT (Emulation DOTNET_ROOT if set first) + * + * This accounts for pmc installs, snap installs, bash configurations, and other non-standard installations such as homebrew. + */ + const dotnetFindPathRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.findPath}`, async (commandContext : IDotnetFindPathContext) => + { + globalEventStream.post(new DotnetFindPathCommandInvoked(`The find path command was invoked.`, commandContext)); + + if(!commandContext.acquireContext.mode || !commandContext.acquireContext.requestingExtensionId || !commandContext.acquireContext.version || !commandContext.acquireContext.architecture) + { + throw new EventCancellationError('BadContextualFindPathError', `The find path request was missing required information: a mode, version, architecture, and requestingExtensionId.`); + } + + globalEventStream.post(new DotnetFindPathLookupSetting(`Looking up vscode setting.`)); + const workerContext = getAcquisitionWorkerContext(commandContext.acquireContext.mode, commandContext.acquireContext); + const existingPath = await resolveExistingPathIfExists(existingPathConfigWorker, commandContext.acquireContext, workerContext, utilContext, commandContext.versionSpecRequirement); + + if(existingPath) + { + globalEventStream.post(new DotnetFindPathSettingFound(`Found vscode setting.`)); + outputChannel.show(true); + return existingPath; + } + + const validator = new DotnetConditionValidator(workerContext, utilContext); + const finder = new DotnetPathFinder(workerContext, utilContext); + + const dotnetsOnPATH = await finder.findRawPathEnvironmentSetting(); + for(const dotnetPath of dotnetsOnPATH ?? []) + { + const validatedPATH = await getPathIfValid(dotnetPath, validator, commandContext); + if(validatedPATH) + { + outputChannel.show(true); + return validatedPATH; + } + } + + const dotnetsOnRealPATH = await finder.findRealPathEnvironmentSetting(); + for(const dotnetPath of dotnetsOnRealPATH ?? []) + { + const validatedRealPATH = await getPathIfValid(dotnetPath, validator, commandContext); + if(validatedRealPATH) + { + outputChannel.show(true); + return validatedRealPATH; + } + } + + const dotnetOnROOT = await finder.findDotnetRootPath(commandContext.acquireContext.architecture); + const validatedRoot = await getPathIfValid(dotnetOnROOT, validator, commandContext); + if(validatedRoot) + { + outputChannel.show(true); + + return validatedRoot; + } + + outputChannel.show(true); + return undefined; + }); + + async function getPathIfValid(path : string | undefined, validator : IDotnetConditionValidator, commandContext : IDotnetFindPathContext) : Promise + { + if(path) + { + const validated = await validator.dotnetMeetsRequirement(path, commandContext); + if(validated) + { + globalEventStream.post(new DotnetFindPathMetCondition(`${path} met the conditions.`)); + return path; + } + else + { + globalEventStream.post(new DotnetFindPathDidNotMeetCondition(`${path} did NOT satisfy the conditions.`)); + } + } + + return undefined; + } + async function uninstall(commandContext: IDotnetAcquireContext | undefined, force = false) : Promise { let result = '1'; @@ -515,11 +615,11 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex // Helper Functions async function resolveExistingPathIfExists(configResolver : ExtensionConfigurationWorker, commandContext : IDotnetAcquireContext, - workerContext : IAcquisitionWorkerContext, utilityContext : IUtilityContext) : Promise + workerContext : IAcquisitionWorkerContext, utilityContext : IUtilityContext, requirement? : DotnetVersionSpecRequirement) : Promise { const existingPathResolver = new ExistingPathResolver(workerContext, utilityContext); - const existingPath = await existingPathResolver.resolveExistingPath(configResolver.getAllPathConfigurationValues(), commandContext.requestingExtensionId, displayWorker); + const existingPath = await existingPathResolver.resolveExistingPath(configResolver.getAllPathConfigurationValues(), commandContext.requestingExtensionId, displayWorker, requirement); if (existingPath) { globalEventStream.post(new DotnetExistingPathResolutionCompleted(existingPath.dotnetPath)); return new Promise((resolve) => { @@ -663,6 +763,7 @@ We will try to install .NET, but are unlikely to be able to connect to the serve dotnetAcquireStatusRegistration, dotnetAcquireGlobalSDKRegistration, acquireGlobalSDKPublicRegistration, + dotnetFindPathRegistration, dotnetListVersionsRegistration, dotnetRecommendedVersionRegistration, dotnetUninstallRegistration, diff --git a/vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts b/vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts index 9be2c5e060..8fb43ce54d 100644 --- a/vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts +++ b/vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts @@ -25,7 +25,13 @@ import { getMockAcquisitionContext, DotnetInstallMode, DotnetInstallType, - MockEventStream + MockEventStream, + IDotnetFindPathContext, + getDotnetExecutable, + DotnetVersionSpecRequirement, + EnvironmentVariableIsDefined, + MockEnvironmentVariableCollection, + getPathSeparator, } from 'vscode-dotnet-runtime-library'; import * as extension from '../../extension'; import { warn } from 'console'; @@ -33,10 +39,11 @@ import { InstallTrackerSingleton } from 'vscode-dotnet-runtime-library/dist/Acqu const assert : any = chai.assert; const standardTimeoutTime = 40000; +const originalPATH = process.env.PATH; suite('DotnetCoreAcquisitionExtension End to End', function() { - this.retries(3); + this.retries(1); const storagePath = path.join(__dirname, 'tmp'); const mockState = new MockExtensionContext(); const extensionPath = path.join(__dirname, '/../../..'); @@ -44,6 +51,7 @@ suite('DotnetCoreAcquisitionExtension End to End', function() const requestingExtensionId = 'fake.extension'; const mockDisplayWorker = new MockWindowDisplayWorker(); let extensionContext: vscode.ExtensionContext; + const environmentVariableCollection = new MockEnvironmentVariableCollection(); const existingPathVersionToFake = '5.0.2~x64' const pathWithIncorrectVersionForTest = path.join(__dirname, `/.dotnet/${existingPathVersionToFake}/dotnet`); @@ -82,6 +90,7 @@ suite('DotnetCoreAcquisitionExtension End to End', function() globalState: mockState, extensionPath, logPath, + environmentVariableCollection } as any; process.env.DOTNET_INSTALL_TOOL_UNDER_TEST = 'true'; @@ -95,12 +104,14 @@ suite('DotnetCoreAcquisitionExtension End to End', function() this.afterEach(async () => { // Tear down tmp storage for fresh run + process.env.PATH = originalPATH; + await vscode.commands.executeCommand('dotnet.uninstallAll'); mockState.clear(); MockTelemetryReporter.telemetryEvents = []; rimraf.sync(storagePath); InstallTrackerSingleton.getInstance(new MockEventStream(), new MockExtensionContext()).clearPromises(); - }); + }).timeout(standardTimeoutTime); test('Activate', async () => { // Commands should now be registered @@ -108,15 +119,20 @@ suite('DotnetCoreAcquisitionExtension End to End', function() assert.isAbove(extensionContext.subscriptions.length, 0); }).timeout(standardTimeoutTime); - async function installRuntime(dotnetVersion : string, installMode : DotnetInstallMode) + async function installRuntime(dotnetVersion : string, installMode : DotnetInstallMode, arch? : string) { - const context: IDotnetAcquireContext = { version: dotnetVersion, requestingExtensionId, mode: installMode }; + let context: IDotnetAcquireContext = { version: dotnetVersion, requestingExtensionId, mode: installMode }; + if(arch) + { + context.architecture = arch; + } const result = await vscode.commands.executeCommand('dotnet.acquire', context); assert.exists(result, 'Command results a result'); assert.exists(result!.dotnetPath, 'The return type of the local runtime install command has a .dotnetPath property'); assert.isTrue(fs.existsSync(result!.dotnetPath), 'The returned path of .net does exist'); assert.include(result!.dotnetPath, '.dotnet', '.dotnet is in the path of the local runtime install'); assert.include(result!.dotnetPath, context.version, 'the path of the local runtime install includes the version of the runtime requested'); + return result.dotnetPath ?? 'runtimePathNotFound'; } @@ -185,6 +201,45 @@ suite('DotnetCoreAcquisitionExtension End to End', function() assert.isFalse(fs.existsSync(result!.dotnetPath), 'the dotnet path result does not exist after uninstalling from all owners'); } + function includesPathWithLikelyDotnet(pathToCheck : string) : boolean + { + const lowerPath = pathToCheck.toLowerCase(); + 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) + { + const installPath = await installRuntime(version, iMode, arch); + + // use path.dirname : the dotnet.exe cant be on the PATH + if(setPath) + { + process.env.PATH = `${path.dirname(installPath)}${getPathSeparator()}${process.env.PATH?.split(getPathSeparator()).filter((x : string) => !(includesPathWithLikelyDotnet(x))).join(getPathSeparator())}`; + } + else + { + // remove dotnet so the test will work on machines with dotnet installed + process.env.PATH = `${process.env.PATH?.split(getPathSeparator()).filter((x : string) => !(includesPathWithLikelyDotnet(x))).join(getPathSeparator())}`; + process.env.DOTNET_ROOT = path.dirname(installPath); + } + + extensionContext.environmentVariableCollection.replace('PATH', process.env.PATH ?? ''); + const result = await vscode.commands.executeCommand('dotnet.findPath', + { acquireContext : contextToLookFor ?? { version, requestingExtensionId : requestingExtensionId, mode: iMode, architecture : arch } as IDotnetAcquireContext, + versionSpecRequirement : condition} as IDotnetFindPathContext + ); + + if(shouldFind) + { + assert.exists(result, 'find path command returned a result'); + assert.equal(result, installPath, 'The path returned by findPath is correct'); + } + else + { + assert.equal(result, undefined, 'find path command returned no undefined if no path matches condition'); + } + } + test('Install Local Runtime Command', async () => { await installRuntime('2.2', 'runtime'); @@ -227,6 +282,59 @@ suite('DotnetCoreAcquisitionExtension End to End', function() await installMultipleVersions(['2.2', '3.0', '3.1'], 'aspnetcore'); }).timeout(standardTimeoutTime * 2); + test('Find dotnet PATH Command Met Condition', async () => { + // install 5.0 then look for 5.0 path + await findPathWithRequirementAndInstall('5.0', 'runtime', os.arch(), 'greater_than_or_equal', true); + }).timeout(standardTimeoutTime); + + test('Find dotnet PATH Command Met ROOT Condition', async () => { + // install 7.0, set dotnet_root and not path, then look for root + const oldROOT = process.env.DOTNET_ROOT; + + await findPathWithRequirementAndInstall('7.0', 'runtime', os.arch(), 'equal', true, + {version : '7.0', mode : 'runtime', architecture : os.arch(), requestingExtensionId : requestingExtensionId}, false + ); + + if(EnvironmentVariableIsDefined(oldROOT)) + { + process.env.DOTNET_ROOT = oldROOT; + } + else + { + delete process.env.DOTNET_ROOT; + } + }).timeout(standardTimeoutTime); + + test('Find dotnet PATH Command Unmet Version Condition', async () => { + // Install 3.1, look for 8.0 which is not less than or equal to 3.1 + await findPathWithRequirementAndInstall('8.0', 'runtime', os.arch(), 'less_than_or_equal', false, + {version : '3.1', mode : 'runtime', architecture : os.arch(), requestingExtensionId : requestingExtensionId} + ); + }).timeout(standardTimeoutTime); + + test('Find dotnet PATH Command Unmet Mode Condition', async () => { + // look for 3.1 runtime but install 3.1 aspnetcore + await findPathWithRequirementAndInstall('3.1', 'runtime', os.arch(), 'equal', false, + {version : '3.1', mode : 'aspnetcore', architecture : os.arch(), requestingExtensionId : requestingExtensionId} + ); + }).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. + // + // 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} + ); + } + }).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. diff --git a/vscode-dotnet-runtime-library/src/Acquisition/DotnetConditionValidator.ts b/vscode-dotnet-runtime-library/src/Acquisition/DotnetConditionValidator.ts new file mode 100644 index 0000000000..bd47e2d151 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/Acquisition/DotnetConditionValidator.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ +import { DotnetVersionSpecRequirement } from '../DotnetVersionSpecRequirement'; +import { IDotnetFindPathContext } from '../IDotnetFindPathContext'; +import { CommandExecutor } from '../Utils/CommandExecutor'; +import { ICommandExecutor } from '../Utils/ICommandExecutor'; +import { IUtilityContext } from '../Utils/IUtilityContext'; +import { IDotnetListInfo } from './IDotnetListInfo'; +import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; +import { IDotnetConditionValidator } from './IDotnetConditionValidator'; +import * as versionUtils from './VersionUtilities'; +import { FileUtilities } from '../Utils/FileUtilities'; +import { EnvironmentVariableIsDefined } from '../Utils/TypescriptUtilities'; + + +export class DotnetConditionValidator implements IDotnetConditionValidator +{ + public constructor(private readonly workerContext : IAcquisitionWorkerContext, private readonly utilityContext : IUtilityContext, private executor? : ICommandExecutor) + { + this.executor ??= new CommandExecutor(this.workerContext, this.utilityContext); + } + + public async dotnetMeetsRequirement(dotnetExecutablePath: string, requirement : IDotnetFindPathContext) : Promise + { + const availableRuntimes = await this.getRuntimes(dotnetExecutablePath); + const requestedMajorMinor = versionUtils.getMajorMinor(requirement.acquireContext.version, this.workerContext.eventStream, this.workerContext); + const hostArch = await this.getHostArchitecture(dotnetExecutablePath); + + if(availableRuntimes.some((runtime) => + { + const foundVersion = versionUtils.getMajorMinor(runtime.version, this.workerContext.eventStream, this.workerContext); + return runtime.mode === requirement.acquireContext.mode && this.stringArchitectureMeetsRequirement(hostArch, requirement.acquireContext.architecture) && + this.stringVersionMeetsRequirement(foundVersion, requestedMajorMinor, requirement.versionSpecRequirement); + })) + { + return true; + } + else + { + const availableSDKs = await this.getSDKs(dotnetExecutablePath); + if(availableSDKs.some((sdk) => + { + // 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 true; + } + } + + return false; + } + + /** + * + * @param hostPath The path to the dotnet executable + * @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. + * + * @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 + { + 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. + + 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/ */ + } + + public async getSDKs(existingPath : string) : Promise + { + const findSDKsCommand = CommandExecutor.makeCommand(`"${existingPath}"`, ['--list-sdks']); + + const sdkInfo = await (this.executor!).execute(findSDKsCommand, undefined, false).then((result) => + { + const sdks = result.stdout.split('\n').map((line) => line.trim()).filter((line) => line.length > 0); + const sdkInfos : IDotnetListInfo[] = sdks.map((sdk) => + { + const parts = sdk.split(' ', 2); + return { + mode: 'sdk', + version: parts[0], + directory: sdk.split(' ').slice(1).join(' ').slice(1, -1) // need to remove the brackets from the path [path] + } as IDotnetListInfo; + }).filter(x => x !== null) as IDotnetListInfo[]; + + return sdkInfos; + }); + + return sdkInfo; + } + + private stringVersionMeetsRequirement(foundVersion : string, requiredVersion : string, requirement : DotnetVersionSpecRequirement) : boolean + { + if(requirement === 'equal') + { + return foundVersion === requiredVersion; + } + else if(requirement === 'greater_than_or_equal') + { + return foundVersion >= requiredVersion; + } + else if(requirement === 'less_than_or_equal') + { + return foundVersion <= requiredVersion; + } + + return false; + } + + private stringArchitectureMeetsRequirement(outputArchitecture : string, requiredArchitecture : string | null | undefined) : boolean + { + return !requiredArchitecture || outputArchitecture === '' || FileUtilities.dotnetInfoArchToNodeArch(outputArchitecture, this.workerContext.eventStream) === requiredArchitecture; + } + + public async getRuntimes(existingPath : string) : Promise + { + const findRuntimesCommand = CommandExecutor.makeCommand(`"${existingPath}"`, ['--list-runtimes']); + + const windowsDesktopString = 'Microsoft.WindowsDesktop.App'; + const aspnetCoreString = 'Microsoft.AspNetCore.App'; + const runtimeString = 'Microsoft.NETCore.App'; + + const runtimeInfo = await (this.executor!).execute(findRuntimesCommand, undefined, false).then((result) => + { + const runtimes = result.stdout.split('\n').map((line) => line.trim()).filter((line) => line.length > 0); + const runtimeInfos : IDotnetListInfo[] = runtimes.map((runtime) => + { + const parts = runtime.split(' ', 3); // account for spaces in PATH, no space should appear before then and luckily path is last + return { + mode: parts[0] === aspnetCoreString ? 'aspnetcore' : parts[0] === runtimeString ? 'runtime' : 'sdk', // sdk is a placeholder for windows desktop, will never match since this is for runtime search only + version: parts[1], + directory: runtime.split(' ').slice(2).join(' ').slice(1, -1) // account for spaces in PATH, no space should appear before then and luckily path is last. + // the 2nd slice needs to remove the brackets from the path [path] + } as IDotnetListInfo; + }).filter(x => x !== null) as IDotnetListInfo[]; + + return runtimeInfos; + }); + + return runtimeInfo; + } +} \ No newline at end of file diff --git a/vscode-dotnet-runtime-library/src/Acquisition/DotnetPathFinder.ts b/vscode-dotnet-runtime-library/src/Acquisition/DotnetPathFinder.ts new file mode 100644 index 0000000000..4f9593e252 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/Acquisition/DotnetPathFinder.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +import { CommandExecutor } from '../Utils/CommandExecutor'; +import { ICommandExecutor } from '../Utils/ICommandExecutor'; +import { IUtilityContext } from '../Utils/IUtilityContext'; +import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; +import { IDotnetPathFinder } from './IDotnetPathFinder'; + +import * as os from 'os'; +import * as path from 'path'; +import { realpathSync } from 'fs'; +import { EnvironmentVariableIsDefined, getDotnetExecutable, getOSArch } from '../Utils/TypescriptUtilities'; +import { DotnetConditionValidator } from './DotnetConditionValidator'; +import { + DotnetFindPathLookupPATH, + DotnetFindPathLookupRealPATH, + DotnetFindPathLookupRootPATH, + DotnetFindPathNoRuntimesOnHost, + DotnetFindPathPATHFound, + DotnetFindPathRealPATHFound, + DotnetFindPathRootEmulationPATHFound, + DotnetFindPathRootPATHFound, + DotnetFindPathRootUnderEmulationButNoneSet +} from '../EventStream/EventStreamEvents'; + +export class DotnetPathFinder implements IDotnetPathFinder +{ + + public constructor(private readonly workerContext : IAcquisitionWorkerContext, private readonly utilityContext : IUtilityContext, private executor? : ICommandExecutor) + { + this.executor ??= new CommandExecutor(this.workerContext, this.utilityContext); + } + + /** + * + * @returns The DOTNET_ROOT environment variable, which is the root path for the dotnet installation. + * Some applications, such as `dotnet test`, prefer DOTNET_ROOT over the PATH setting. + * DOTNET_ROOT is also not the only setting. + * DOTNET_ROOT(x86) - Deprecated. Only used when running 32-bit executables. VS Code 32 bit is deprecated, so don't support this. + * DOTNET_ROOT_X86 - The non deprecated version of the above variable, still ignore. + * DOTNET_ROOT_X64 - Used when running 64-bit executables on an ARM64 OS. + * Node only runs on x64 and not ARM, but that doesn't mean the .NET Application won't run on ARM. + * + * DOTNET_HOST_PATH may be set but this is owned by the host. Don't respect any user setting here. + * + * The VS Code Workspace environment may also be different from the System environment. + */ + public async findDotnetRootPath(requestedArchitecture : string) : Promise + { + this.workerContext.eventStream.post(new DotnetFindPathLookupRootPATH(`Looking up .NET on the root.`)); + + if(requestedArchitecture === 'x64' && (this.executor !== undefined ? (await getOSArch(this.executor)).includes('arm') : false)) + { + let dotnetOnRootEmulationPath = process.env.DOTNET_ROOT_X64; + if(EnvironmentVariableIsDefined(dotnetOnRootEmulationPath)) + { + // DOTNET_ROOT should be set to the directory containing the dotnet executable, not the executable itself. + // https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-environment-variables + dotnetOnRootEmulationPath = path.join(dotnetOnRootEmulationPath!, getDotnetExecutable()); + this.workerContext.eventStream.post(new DotnetFindPathRootEmulationPATHFound(`Under emulation and emulation root is set to ${dotnetOnRootEmulationPath}.`)); + return dotnetOnRootEmulationPath; + } + else + { + this.workerContext.eventStream.post(new DotnetFindPathRootUnderEmulationButNoneSet(`Under emulation but DOTNET_ROOT_X64 is not set.`)); + } + } + + let dotnetOnRootPath = process.env.DOTNET_ROOT; + if(EnvironmentVariableIsDefined(dotnetOnRootPath)) + { + // DOTNET_ROOT should be set to the directory containing the dotnet executable, not the executable itself. + dotnetOnRootPath = path.join(dotnetOnRootPath!, getDotnetExecutable()); + this.workerContext.eventStream.post(new DotnetFindPathRootPATHFound(`Found .NET on the root: ${dotnetOnRootPath}`)); + return dotnetOnRootPath; + } + return undefined; + } + + /** + * + * @returns A set of the path environment variable(s) for which or where dotnet, which may need to be converted to the actual path if it points to a polymorphic executable. + * For example, `snap` installs dotnet to snap/bin/dotnet, which you can call --list-runtimes on. + * The 'realpath' of that is 'usr/bin/snap', which you cannot invoke --list-runtimes on, because it is snap. + * In this case, we need to use this polymorphic path to find the actual path later. + * + * In an install such as homebrew, the PATH is not indicative of all of the PATHs. So dotnet may be missing in the PATH even though it is found in an alternative shell. + * The PATH can be discovered using path_helper on mac. + */ + public async findRawPathEnvironmentSetting(tryUseTrueShell = true) : Promise + { + const oldLookup = process.env.DOTNET_MULTILEVEL_LOOKUP; + process.env.DOTNET_MULTILEVEL_LOOKUP = '0'; // make it so --list-runtimes only finds the runtimes on that path: https://learn.microsoft.com/en-us/dotnet/core/compatibility/deployment/7.0/multilevel-lookup#reason-for-change + + const searchEnvironment = process.env; // this is the default, but sometimes it does not get picked up + const options = tryUseTrueShell && os.platform() !== 'win32' ? { env : searchEnvironment, shell: process.env.SHELL === '/bin/bash' ? '/bin/bash' : '/bin/sh'} : {env : searchEnvironment}; + + 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', []), + undefined, + false))?.stdout} + +Bin Bash Path: ${os.platform() !== 'win32' ? (await this.executor?.execute(CommandExecutor.makeCommand('env', ['bash']), {shell : '/bin/bash'}, false))?.stdout : 'N/A'} +` + )); + + let pathLocatorCommand = ''; + if(os.platform() === 'win32') + { + pathLocatorCommand = (await this.executor?.tryFindWorkingCommand([ + // We have to give the command an argument to return status 0, and the only thing its guaranteed to find is itself :) + CommandExecutor.makeCommand('where', ['where']), + CommandExecutor.makeCommand('where.exe', ['where.exe']), + CommandExecutor.makeCommand('%SystemRoot%\\System32\\where.exe', ['%SystemRoot%\\System32\\where.exe']), // if PATH is corrupted + CommandExecutor.makeCommand('C:\\Windows\\System32\\where.exe', ['C:\\Windows\\System32\\where.exe']) // in case SystemRoot is corrupted, best effort guess + ], options))?.commandRoot ?? 'where'; + } + else + { + pathLocatorCommand = (await this.executor?.tryFindWorkingCommand([ + CommandExecutor.makeCommand('which', ['which']), + CommandExecutor.makeCommand('/usr/bin/which', ['/usr/bin/which']), // if PATH is corrupted + ], options))?.commandRoot ?? 'which'; + } + + const findCommand = CommandExecutor.makeCommand(pathLocatorCommand, ['dotnet']); + const dotnetsOnPATH = (await this.executor?.execute(findCommand, options))?.stdout.split('\n').map(x => x.trim()).filter(x => x !== ''); + if(dotnetsOnPATH) + { + this.workerContext.eventStream.post(new DotnetFindPathPATHFound(`Found .NET on the path: ${JSON.stringify(dotnetsOnPATH)}`)); + return this.returnWithRestoringEnvironment(await this.getTruePath(dotnetsOnPATH), 'DOTNET_MULTILEVEL_LOOKUP', oldLookup); + + } + return this.returnWithRestoringEnvironment(undefined, 'DOTNET_MULTILEVEL_LOOKUP', oldLookup); + } + + // eslint-disable-next-line @typescript-eslint/require-await + private async returnWithRestoringEnvironment(returnValue : string[] | undefined, envVarToRestore : string, envResToRestore : string | undefined) : Promise + { + if(EnvironmentVariableIsDefined(envVarToRestore)) + { + process.env[envVarToRestore] = envResToRestore; + } + else + { + delete process.env[envVarToRestore]; + } + return returnValue; + } + + /** + * @returns The 'realpath' or resolved path for dotnet from which or where dotnet. + * Some installers, such as the Ubuntu install with the PMC Feed, the PATH is set to /usr/bin/dotnet which is a symlink to /usr/share/dotnet. + * If we want to return the actual path, we need to use realpath. + * + * We can't use realpath on all paths, because some paths are polymorphic executables and the realpath is invalid. + */ + public async findRealPathEnvironmentSetting(tryUseTrueShell = true) : Promise + { + this.workerContext.eventStream.post(new DotnetFindPathLookupRealPATH(`Looking up .NET on the real path.`)); + const dotnetsOnPATH = await this.findRawPathEnvironmentSetting(tryUseTrueShell); + if(dotnetsOnPATH) + { + const realPaths = dotnetsOnPATH.map(x => realpathSync(x)); + this.workerContext.eventStream.post(new DotnetFindPathRealPATHFound(`Found .NET on the path: ${JSON.stringify(dotnetsOnPATH)}, realpath: ${realPaths}`)); + return this.getTruePath(realPaths); + } + return undefined; + } + + /** + * + * @param tentativePaths Paths that may hold a dotnet executable. + * @returns The actual physical location/path on disk where the executables lie for each of the paths. + * Some of the symlinks etc resolve to a path which works but is still not the actual path. + */ + private async getTruePath(tentativePaths : string[]) : Promise + { + const truePaths = []; + + for(const tentativePath of tentativePaths) + { + const runtimeInfo = await new DotnetConditionValidator(this.workerContext, this.utilityContext, this.executor).getRuntimes(tentativePath); + if(runtimeInfo.length > 0) + { + // q.t. from @dibarbet on the C# Extension: + // The .NET install layout is a well known structure on all platforms. + // See https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md#net-core-install-layout + // + // Therefore we know that the runtime path is always in /shared/ + // and the dotnet executable is always at /dotnet(.exe). + // + // Since dotnet --list-runtimes will always use the real assembly path to output the runtime folder (no symlinks!) + // we know the dotnet executable will be two folders up in the install root. + truePaths.push(path.join(path.dirname(path.dirname(runtimeInfo[0].directory)), getDotnetExecutable())); + } + else + { + this.workerContext.eventStream.post(new DotnetFindPathNoRuntimesOnHost(`The host: ${tentativePath} does not contain a .NET runtime installation.`)); + } + } + + return truePaths.length > 0 ? truePaths : tentativePaths; + } +} diff --git a/vscode-dotnet-runtime-library/src/Acquisition/ExistingPathResolver.ts b/vscode-dotnet-runtime-library/src/Acquisition/ExistingPathResolver.ts index 8070b36510..36fbed25d4 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/ExistingPathResolver.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/ExistingPathResolver.ts @@ -10,15 +10,15 @@ import { IAcquisitionWorkerContext } from '../Acquisition/IAcquisitionWorkerCont import { IWindowDisplayWorker } from '../EventStream/IWindowDisplayWorker'; import { IDotnetAcquireResult } from '../IDotnetAcquireResult'; import { IExistingPaths } from '../IExtensionContext'; -import { DotnetInstallMode } from './DotnetInstallMode'; -import * as versionUtils from './VersionUtilities'; import { ICommandExecutor } from '../Utils/ICommandExecutor'; +import { DotnetConditionValidator } from './DotnetConditionValidator'; +import { IDotnetFindPathContext } from '../IDotnetFindPathContext'; +import { DotnetVersionSpecRequirement } from '../DotnetVersionSpecRequirement'; const badExistingPathWarningMessage = `The 'existingDotnetPath' setting was set, but it did not meet the requirements for this extension to run properly. This setting has been ignored. If you would like to continue to use the setting anyways, set dotnetAcquisitionExtension.allowInvalidPaths to true in the .NET Install Tool Extension Settings.`; -interface IDotnetListInfo { mode: DotnetInstallMode, version: string, directory : string }; export class ExistingPathResolver { @@ -28,10 +28,10 @@ export class ExistingPathResolver this.executor ??= new CommandExecutor(this.workerContext, this.utilityContext); } - public async resolveExistingPath(existingPaths: IExistingPaths | undefined, extensionId: string | undefined, windowDisplayWorker: IWindowDisplayWorker): Promise + public async resolveExistingPath(existingPaths: IExistingPaths | undefined, extensionId: string | undefined, windowDisplayWorker: IWindowDisplayWorker, requirement? : DotnetVersionSpecRequirement): Promise { const existingPath = this.getExistingPath(existingPaths, extensionId, windowDisplayWorker); - if (existingPath && (await this.providedPathMeetsAPIRequirement(this.workerContext, existingPath, this.workerContext.acquisitionContext) || this.allowInvalidPath(this.workerContext))) + if (existingPath && (await this.providedPathMeetsAPIRequirement(this.workerContext, existingPath, this.workerContext.acquisitionContext, requirement) || this.allowInvalidPath(this.workerContext))) { return { dotnetPath: existingPath } as IDotnetAcquireResult; } @@ -92,95 +92,16 @@ export class ExistingPathResolver return workerContext.allowInvalidPathSetting ?? false; } - private async providedPathMeetsAPIRequirement(workerContext : IAcquisitionWorkerContext, existingPath : string, apiRequest : IDotnetAcquireContext) : Promise + private async providedPathMeetsAPIRequirement(workerContext : IAcquisitionWorkerContext, existingPath : string, apiRequest : IDotnetAcquireContext, requirement? : DotnetVersionSpecRequirement) : Promise { + const validator = new DotnetConditionValidator(this.workerContext, this.utilityContext, this.executor); + const validated = await validator.dotnetMeetsRequirement(existingPath, {acquireContext : apiRequest, versionSpecRequirement : requirement ?? 'equal'} as IDotnetFindPathContext); - const availableRuntimes = await this.getRuntimes(existingPath); - const requestedMajorMinor = versionUtils.getMajorMinor(apiRequest.version, this.workerContext.eventStream, this.workerContext); - - if(availableRuntimes.some((runtime) => - { - return runtime.mode === apiRequest.mode && versionUtils.getMajorMinor(runtime.version, this.workerContext.eventStream, this.workerContext) === requestedMajorMinor; - })) + if(!validated && !this.allowInvalidPath(workerContext)) { - return true; + this.utilityContext.ui.showWarningMessage(`${badExistingPathWarningMessage}\nExtension: ${workerContext.acquisitionContext.requestingExtensionId ?? 'Unspecified'}`, () => {/* No Callback */}, ); } - else - { - const availableSDKs = await this.getSDKs(existingPath); - if(availableSDKs.some((sdk) => - { - // The SDK includes the Runtime, ASP.NET Core Runtime, and Windows Desktop Runtime. So, we don't need to check the mode. - return versionUtils.getMajorMinor(sdk.version, this.workerContext.eventStream, this.workerContext) === requestedMajorMinor; - })) - { - return true; - } - - if(!this.allowInvalidPath(workerContext)) - { - this.utilityContext.ui.showWarningMessage(`${badExistingPathWarningMessage}\nExtension: ${workerContext.acquisitionContext.requestingExtensionId ?? 'Unspecified'}`, () => {/* No Callback */}, ); - } - return false; - } - } - - private async getSDKs(existingPath : string) : Promise - { - const findSDKsCommand = CommandExecutor.makeCommand(`"${existingPath}"`, ['--list-sdks']); - - const sdkInfo = await (this.executor!).execute(findSDKsCommand).then((result) => - { - const sdks = result.stdout.split('\n').map((line) => line.trim()).filter((line) => line.length > 0); - const sdkInfos : IDotnetListInfo[] = sdks.map((sdk) => - { - if(sdk === '') // new line in output that got trimmed - { - return null; - } - const parts = sdk.split(' ', 2); // account for spaces in PATH, no space should appear before then - return { - mode: 'sdk', - version: parts[0], - directory: sdk.split(' ').slice(1).join(' ').slice(1, -1) // need to remove the brackets from the path [path] - } as IDotnetListInfo; - }).filter(x => x !== null) as IDotnetListInfo[]; - - return sdkInfos; - }); - - return sdkInfo; - } - - private async getRuntimes(existingPath : string) : Promise - { - const findRuntimesCommand = CommandExecutor.makeCommand(`"${existingPath}"`, ['--list-runtimes']); - - const windowsDesktopString = 'Microsoft.WindowsDesktop.App'; - const aspnetCoreString = 'Microsoft.AspNetCore.App'; - const runtimeString = 'Microsoft.NETCore.App'; - - const runtimeInfo = await (this.executor!).execute(findRuntimesCommand).then((result) => - { - const runtimes = result.stdout.split('\n').map((line) => line.trim()).filter((line) => line.length > 0); - const runtimeInfos : IDotnetListInfo[] = runtimes.map((runtime) => - { - if(runtime === '') // new line in output that got trimmed - { - return null; - } - const parts = runtime.split(' ', 3); // account for spaces in PATH, no space should appear before then - return { - mode: parts[0] === aspnetCoreString ? 'aspnetcore' : parts[0] === runtimeString ? 'runtime' : 'sdk', // sdk is a placeholder for windows desktop, will never match since this is for runtime search only - version: parts[1], - directory: runtime.split(' ').slice(2).join(' ').slice(1, -1) // account for spaces in PATH, no space should appear before then and luckily path is last. - // the 2nd slice needs to remove the brackets from the path [path] - } as IDotnetListInfo; - }).filter(x => x !== null) as IDotnetListInfo[]; - - return runtimeInfos; - }); - return runtimeInfo; + return validated; } } diff --git a/vscode-dotnet-runtime-library/src/Acquisition/IDotnetConditionValidator.ts b/vscode-dotnet-runtime-library/src/Acquisition/IDotnetConditionValidator.ts new file mode 100644 index 0000000000..10bd83aa04 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/Acquisition/IDotnetConditionValidator.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +import { IDotnetFindPathContext } from '../IDotnetFindPathContext'; + +export interface IDotnetConditionValidator +{ + dotnetMeetsRequirement(dotnetExecutablePath: string, requirement : IDotnetFindPathContext): Promise; +} diff --git a/vscode-dotnet-runtime-library/src/Acquisition/IDotnetListInfo.ts b/vscode-dotnet-runtime-library/src/Acquisition/IDotnetListInfo.ts new file mode 100644 index 0000000000..f81a385196 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/Acquisition/IDotnetListInfo.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +import { DotnetInstallMode } from "./DotnetInstallMode"; + +export interface IDotnetListInfo { mode: DotnetInstallMode, version: string, directory : string }; diff --git a/vscode-dotnet-runtime-library/src/Acquisition/IDotnetPathFinder.ts b/vscode-dotnet-runtime-library/src/Acquisition/IDotnetPathFinder.ts new file mode 100644 index 0000000000..41602c4709 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/Acquisition/IDotnetPathFinder.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +export interface IDotnetPathFinder +{ + findDotnetRootPath(requestedArchitecture : string): Promise; + findRawPathEnvironmentSetting(tryUseTrueShell : boolean): Promise; + findRealPathEnvironmentSetting(tryUseTrueShell : boolean): Promise; +} diff --git a/vscode-dotnet-runtime-library/src/Acquisition/WinMacGlobalInstaller.ts b/vscode-dotnet-runtime-library/src/Acquisition/WinMacGlobalInstaller.ts index 6752d7cc32..e4179479c2 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/WinMacGlobalInstaller.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/WinMacGlobalInstaller.ts @@ -36,6 +36,7 @@ import { IUtilityContext } from '../Utils/IUtilityContext'; import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; import { DotnetInstall } from './DotnetInstall'; import { CommandExecutorResult } from '../Utils/CommandExecutorResult'; +import { getOSArch } from '../Utils/TypescriptUtilities'; namespace validationPromptConstants { @@ -359,8 +360,7 @@ If you were waiting for the install to succeed, please extend the timeout settin const standardHostPath = path.resolve(`/usr/local/share/dotnet/dotnet`); const arm64EmulationHostPath = path.resolve(`/usr/local/share/dotnet/x64/dotnet`); - const findTrueArchCommand = CommandExecutor.makeCommand(`uname`, [`-p`]); - if((os.arch() === 'x64' || os.arch() === 'ia32') && (await this.commandRunner.execute(findTrueArchCommand, null, false)).stdout.toLowerCase().includes('arm') && (fs.existsSync(arm64EmulationHostPath) || !macPathShouldExist)) + if((os.arch() === 'x64' || os.arch() === 'ia32') && (await getOSArch(this.commandRunner)).includes('arm') && (fs.existsSync(arm64EmulationHostPath) || !macPathShouldExist)) { // VS Code runs on an emulated version of node which will return x64 or use x86 emulation for ARM devices. // os.arch() returns the architecture of the node binary, not the system architecture, so it will not report arm on an arm device. diff --git a/vscode-dotnet-runtime-library/src/DotnetVersionSpecRequirement.ts b/vscode-dotnet-runtime-library/src/DotnetVersionSpecRequirement.ts new file mode 100644 index 0000000000..7782677f02 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/DotnetVersionSpecRequirement.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +export type DotnetVersionSpecRequirement = 'equal' | 'greater_than_or_equal' | 'less_than_or_equal'; + diff --git a/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts b/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts index 436cea0391..8ccc44d25c 100644 --- a/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts +++ b/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts @@ -11,6 +11,7 @@ import { TelemetryUtilities } from './TelemetryUtilities'; import { InstallToStrings , DotnetInstall } from '../Acquisition/DotnetInstall'; import { DotnetInstallMode } from '../Acquisition/DotnetInstallMode'; import { DotnetInstallType } from '../IDotnetAcquireContext'; +import { IDotnetFindPathContext } from '../IDotnetFindPathContext'; export class EventCancellationError extends Error { @@ -810,6 +811,67 @@ export class DotnetWSLOperationOutputEvent extends DotnetCustomMessageEvent { public readonly eventName = 'DotnetWSLOperationOutputEvent'; } +export class DotnetFindPathCommandInvoked extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathCommandInvoked'; + constructor(public readonly eventMessage: string, public readonly request : IDotnetFindPathContext) { super(eventMessage); } + + public getProperties() { + return { Message: this.eventMessage, Context : JSON.stringify(this.request)}; + }; +} + +export class DotnetFindPathLookupSetting extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathLookupSetting'; +} + +export class DotnetFindPathSettingFound extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathSettingFound'; +} + +export class DotnetFindPathLookupPATH extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathLookupPATH'; +} + +export class DotnetFindPathPATHFound extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathPATHFound'; +} + +export class DotnetFindPathLookupRealPATH extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathLookupRealPATH'; +} + +export class DotnetFindPathRealPATHFound extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathRealPATHFound'; +} + +export class DotnetFindPathNoRuntimesOnHost extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathNoRuntimesOnHost'; +} + +export class DotnetFindPathLookupRootPATH extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathLookupRealPATH'; +} + +export class DotnetFindPathRootEmulationPATHFound extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathRealPATHFound'; +} + +export class DotnetFindPathRootUnderEmulationButNoneSet extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathRealPATHFound'; +} + +export class DotnetFindPathRootPATHFound extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathRealPATHFound'; +} + +export class DotnetFindPathMetCondition extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathMetCondition'; +} + +export class DotnetFindPathDidNotMeetCondition extends DotnetCustomMessageEvent { + public readonly eventName = 'DotnetFindPathDidNotMeetCondition'; +} + export class DotnetTelemetrySettingEvent extends DotnetCustomMessageEvent { public readonly eventName = 'DotnetTelemetrySettingEvent'; } diff --git a/vscode-dotnet-runtime-library/src/EventStream/EventStreamRegistration.ts b/vscode-dotnet-runtime-library/src/EventStream/EventStreamRegistration.ts index 3f4beee66d..d278a63a8d 100644 --- a/vscode-dotnet-runtime-library/src/EventStream/EventStreamRegistration.ts +++ b/vscode-dotnet-runtime-library/src/EventStream/EventStreamRegistration.ts @@ -19,7 +19,7 @@ import { ModalEventRepublisher } from './ModalEventPublisher'; export interface IPackageJson { version: string; - appInsightsKey: string; + connectionString: string; name: string; } diff --git a/vscode-dotnet-runtime-library/src/EventStream/TelemetryObserver.ts b/vscode-dotnet-runtime-library/src/EventStream/TelemetryObserver.ts index 0129297f21..31e59f0a6e 100644 --- a/vscode-dotnet-runtime-library/src/EventStream/TelemetryObserver.ts +++ b/vscode-dotnet-runtime-library/src/EventStream/TelemetryObserver.ts @@ -31,9 +31,9 @@ export class TelemetryObserver implements IEventStreamObserver { if (telemetryReporter === undefined) { const extensionVersion = packageJson.version; - const appInsightsKey = packageJson.appInsightsKey; + const connectionString = packageJson.connectionString; const extensionId = packageJson.name; - this.telemetryReporter = new TelemetryReporter(appInsightsKey); + this.telemetryReporter = new TelemetryReporter(connectionString); } else { diff --git a/vscode-dotnet-runtime-library/src/IDotnetFindPathContext.ts b/vscode-dotnet-runtime-library/src/IDotnetFindPathContext.ts new file mode 100644 index 0000000000..f91878fbd7 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/IDotnetFindPathContext.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ + +import { DotnetVersionSpecRequirement } from './DotnetVersionSpecRequirement'; +import { IDotnetAcquireContext } from './IDotnetAcquireContext'; + +export interface IDotnetFindPathContext +{ + acquireContext: IDotnetAcquireContext; + versionSpecRequirement: DotnetVersionSpecRequirement; +} \ No newline at end of file diff --git a/vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts b/vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts index 887e58f0a1..0f1710d1f2 100644 --- a/vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts +++ b/vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts @@ -61,6 +61,12 @@ import { IEventStream } from '../EventStream/EventStream'; export class CommandExecutor extends ICommandExecutor { private pathTroubleshootingOption = 'Troubleshoot'; + private englishOutputEnvironmentVariables = { + LC_ALL: 'en_US.UTF-8', + LANG: 'en_US.UTF-8', + LANGUAGE: 'en', + DOTNET_CLI_UI_LANGUAGE: 'en-US', + }; // Not all systems have english installed -- not sure if it's safe to use this. private sudoProcessCommunicationDir = path.join(__dirname, 'install scripts'); private fileUtil : IFileUtilities; private hasEverLaunchedSudoFork = false; @@ -416,7 +422,7 @@ with options ${JSON.stringify(options)}.`)); { execElevated(fullCommandString, options, (error?: Error, execStdout?: string | Buffer, execStderr?: string | Buffer) => { - if(error && terminalFailure) + if(error && terminalFailure && !error?.message?.includes('screen size is bogus')) { return reject(this.parseVSCodeSudoExecError(error, fullCommandString)); } @@ -520,7 +526,7 @@ Please report this at https://github.com/dotnet/vscode-dotnet-runtime/issues.`), * @param matchingCommandParts Any follow up words in that command to execute, matching in the same order as commandRoots * @returns the index of the working command you provided, if no command works, -1. */ - public async tryFindWorkingCommand(commands : CommandExecutorCommand[]) : Promise + public async tryFindWorkingCommand(commands : CommandExecutorCommand[], options? : any) : Promise { let workingCommand : CommandExecutorCommand | null = null; @@ -528,7 +534,7 @@ Please report this at https://github.com/dotnet/vscode-dotnet-runtime/issues.`), { try { - const cmdFoundOutput = (await this.execute(command)).status; + const cmdFoundOutput = (await this.execute(command, options)).status; if(cmdFoundOutput === '0') { workingCommand = command; diff --git a/vscode-dotnet-runtime-library/src/Utils/FileUtilities.ts b/vscode-dotnet-runtime-library/src/Utils/FileUtilities.ts index 039a95a6a9..1eb18a899a 100644 --- a/vscode-dotnet-runtime-library/src/Utils/FileUtilities.ts +++ b/vscode-dotnet-runtime-library/src/Utils/FileUtilities.ts @@ -165,6 +165,38 @@ export class FileUtilities extends IFileUtilities } } + /** + * + * @param nodeArchitecture the architecture output of dotnet --info from the runtime + * @returns the architecture in the style that node expects + * + * @remarks Falls back to string 'auto' if a mapping does not exist which is not a valid architecture. + * So far, the outputs are actually all identical so this is not really 'needed' but good to have in the future :) + */ + public static dotnetInfoArchToNodeArch(dotnetInfoArch : string, eventStream : IEventStream) + { + switch(dotnetInfoArch) + { + case 'x64': { + return dotnetInfoArch; + } + case 'x86': { + // In case the function is called twice + return dotnetInfoArch; + } + case 'arm': { // This shouldn't be an output yet, but its possible in the future + return dotnetInfoArch; + } + case 'arm64': { + return dotnetInfoArch; + } + default: { + eventStream.post(new DotnetCommandFallbackArchitectureEvent(`The architecture ${dotnetInfoArch} of the platform is unexpected, falling back to auto-arch.`)); + return 'auto'; + } + } + } + /** * * @param nodeOS the OS in node style string of what to install diff --git a/vscode-dotnet-runtime-library/src/Utils/ICommandExecutor.ts b/vscode-dotnet-runtime-library/src/Utils/ICommandExecutor.ts index 27e63f5880..88b6d12e7d 100644 --- a/vscode-dotnet-runtime-library/src/Utils/ICommandExecutor.ts +++ b/vscode-dotnet-runtime-library/src/Utils/ICommandExecutor.ts @@ -39,7 +39,7 @@ export abstract class ICommandExecutor * @param commands The set of commands to see if one of them is available/works. * @returns the working command index if one is available, else -1. */ - public abstract tryFindWorkingCommand(commands : CommandExecutorCommand[]) : Promise; + public abstract tryFindWorkingCommand(commands : CommandExecutorCommand[], options? : any) : Promise; public static makeCommand(command : string, args : string[], isSudo = false) : CommandExecutorCommand { diff --git a/vscode-dotnet-runtime-library/src/Utils/TypescriptUtilities.ts b/vscode-dotnet-runtime-library/src/Utils/TypescriptUtilities.ts index 33fa441c33..bc61311e6d 100644 --- a/vscode-dotnet-runtime-library/src/Utils/TypescriptUtilities.ts +++ b/vscode-dotnet-runtime-library/src/Utils/TypescriptUtilities.ts @@ -8,6 +8,8 @@ import * as os from 'os'; import { IEventStream } from '../EventStream/EventStream'; import { DotnetWSLCheckEvent, DotnetWSLOperationOutputEvent } from '../EventStream/EventStreamEvents'; import { IEvent } from '../EventStream/IEvent'; +import { ICommandExecutor } from './ICommandExecutor'; +import { CommandExecutor } from './CommandExecutor'; export async function loopWithTimeoutOnCond(sampleRatePerMs : number, durationToWaitBeforeTimeoutMs : number, conditionToStop : () => boolean, doAfterStop : () => void, eventStream : IEventStream, waitEvent : IEvent) @@ -59,4 +61,31 @@ status: ${commandResult.status?.toString()}` } return commandResult.stdout.toString() !== ''; +} + +export async function getOSArch(executor : ICommandExecutor) : Promise +{ + if(os.platform() === 'darwin') + { + const findTrueArchCommand = CommandExecutor.makeCommand(`uname`, [`-p`]); + return (await executor.execute(findTrueArchCommand, null, false)).stdout.toLowerCase().trim(); + } + + return os.arch(); +} + +export function getDotnetExecutable() : string +{ + return os.platform() === 'win32' ? 'dotnet.exe' : 'dotnet'; +} + +export function EnvironmentVariableIsDefined(variable : any) : boolean +{ + // Most of the time this will be 'undefined', so this is the fastest check. + return variable !== 'undefined' && variable !== null && variable !== '' && variable !== undefined; +} + +export function getPathSeparator() : string +{ + return os.platform() === 'win32' ? ';' : ':'; } \ No newline at end of file diff --git a/vscode-dotnet-runtime-library/src/index.ts b/vscode-dotnet-runtime-library/src/index.ts index d86d7f5ee5..19b37e74d0 100644 --- a/vscode-dotnet-runtime-library/src/index.ts +++ b/vscode-dotnet-runtime-library/src/index.ts @@ -7,6 +7,8 @@ export * from './IExtensionContext'; export * from './IDotnetAcquireContext'; export * from './IDotnetListVersionsContext'; export * from './IDotnetUninstallContext'; +export * from './DotnetVersionSpecRequirement'; +export * from './IDotnetFindPathContext'; export * from './IDotnetAcquireResult'; export * from './IDotnetEnsureDependenciesContext'; export * from './IExtensionContext'; @@ -34,6 +36,11 @@ export * from './Utils/VSCodeEnvironment'; export * from './Utils/IUtilityContext'; export * from './Utils/WebRequestWorker'; export * from './Acquisition/DotnetCoreAcquisitionWorker'; +export * from './Acquisition/DotnetConditionValidator'; +export * from './Acquisition/IDotnetPathFinder'; +export * from './Acquisition/DotnetPathFinder'; +export * from './Acquisition/IDotnetConditionValidator'; +export * from './Acquisition/IDotnetListInfo'; export * from './Acquisition/DotnetInstall'; export * from './Acquisition/IAcquisitionWorkerContext'; export * from './Acquisition/DirectoryProviderFactory'; diff --git a/vscode-dotnet-runtime-library/src/test/mocks/MockEnvironmentVariableCollection.ts b/vscode-dotnet-runtime-library/src/test/mocks/MockEnvironmentVariableCollection.ts index a3f924a220..9ecab0c409 100644 --- a/vscode-dotnet-runtime-library/src/test/mocks/MockEnvironmentVariableCollection.ts +++ b/vscode-dotnet-runtime-library/src/test/mocks/MockEnvironmentVariableCollection.ts @@ -23,9 +23,10 @@ export class MockEnvironmentVariableCollection implements vscode.EnvironmentVari } public replace(variable: string, value: string): void { - throw new Error('Method not implemented.'); + this.variables[variable] = value; } + public prepend(variable: string, value: string): void { throw new Error('Method not implemented.'); } diff --git a/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts b/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts index 011d619145..bc89306d45 100644 --- a/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts +++ b/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts @@ -362,6 +362,11 @@ export class MockCommandExecutor extends ICommandExecutor this.otherCommandPatternsToMock = []; this.otherCommandsReturnValues = []; } + + public async setEnvironmentVariable(variable : string, value : string, vscodeContext : IVSCodeExtensionContext, failureWarningMessage? : string, nonWinFailureMessage? : string) + { + return this.trueExecutor.setEnvironmentVariable(variable, value, vscodeContext, failureWarningMessage, nonWinFailureMessage); + } } export class MockFileUtilities extends IFileUtilities diff --git a/vscode-dotnet-runtime-library/src/test/unit/DotnetCoreAcquisitionWorker.test.ts b/vscode-dotnet-runtime-library/src/test/unit/DotnetCoreAcquisitionWorker.test.ts index e20cc9671d..078d844f0c 100644 --- a/vscode-dotnet-runtime-library/src/test/unit/DotnetCoreAcquisitionWorker.test.ts +++ b/vscode-dotnet-runtime-library/src/test/unit/DotnetCoreAcquisitionWorker.test.ts @@ -16,10 +16,7 @@ import { DotnetInstallGraveyardEvent, DotnetUninstallAllCompleted, DotnetUninstallAllStarted, - TestAcquireCalled, - DotnetASPNetRuntimeAcquisitionTotalSuccessEvent, - DotnetGlobalSDKAcquisitionTotalSuccessEvent, - DotnetRuntimeAcquisitionTotalSuccessEvent + TestAcquireCalled } from '../../EventStream/EventStreamEvents'; import { EventType } from '../../EventStream/EventType'; import { @@ -41,6 +38,7 @@ import { IEventStream } from '../../EventStream/EventStream'; import { DotnetInstallType} from '../../IDotnetAcquireContext'; import { getInstallIdCustomArchitecture } from '../../Utils/InstallIdUtilities'; import { InstallTrackerSingleton } from '../../Acquisition/InstallTrackerSingleton'; +import { getDotnetExecutable } from '../../Utils/TypescriptUtilities'; const assert = chai.assert; chai.use(chaiAsPromised); @@ -93,11 +91,11 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { { if(mode === 'runtime' || mode === 'aspnetcore') { - return path.join(dotnetFolderName, installId, os.platform() === 'win32' ? 'dotnet.exe' : 'dotnet') + return path.join(dotnetFolderName, installId, getDotnetExecutable()) } else if(mode === 'sdk') { - return path.join(dotnetFolderName, os.platform() === 'win32' ? 'dotnet.exe' : 'dotnet'); + return path.join(dotnetFolderName, getDotnetExecutable()); } return 'There is a mode without a designated return path'; diff --git a/vscode-dotnet-runtime-library/src/test/unit/LoggingObserver.test.ts b/vscode-dotnet-runtime-library/src/test/unit/LoggingObserver.test.ts index bb3284e81b..0076b00e2f 100644 --- a/vscode-dotnet-runtime-library/src/test/unit/LoggingObserver.test.ts +++ b/vscode-dotnet-runtime-library/src/test/unit/LoggingObserver.test.ts @@ -33,5 +33,5 @@ suite('LoggingObserver Unit Tests', () => { assert.include(logContent, fakeEvent.eventName, 'The log file does not contain the expected content that should be written to it'); }); - }).timeout(10000); + }).timeout(10000 * 2); }); diff --git a/vscode-dotnet-runtime.code-workspace b/vscode-dotnet-runtime.code-workspace index 52cedfbe22..d1c9a63a3c 100644 --- a/vscode-dotnet-runtime.code-workspace +++ b/vscode-dotnet-runtime.code-workspace @@ -15,7 +15,9 @@ ], "settings": { "cSpell.words": [ + "aspnetcore", "DOTNETINSTALLMODELIST", + "dotnets", "Republisher", "unlocalized" ]