Skip to content

Commit

Permalink
Add new API dotnet.findPath to find the .NET host to use if one alr…
Browse files Browse the repository at this point in the history
…eady exists that meets a requirement. (#1954)

* update PATH setting description to make it more clear

* add very prototype code

* add tests and get runtime check working

* Fix some bugs

* Consider that the .NET SDK can also satisfy the Runtime

* fix typo

* fix the tests

* fix

* fix another test

* Fix tests even more

* undo yarn changes

* fix the final test hopefully

* undo yarn changes

* Dont use dotnet.exe as its not platform agnostic

* use "markdownDescription" for nicer rendering in VSCode

* Tweak call to action to use VSCode mechanisms first, then our standard installation docs, and only in the last resort mention PATH munging.

* Fix URLs

* Move to version 2.1.7

* add basic command'

* Ignore existingPath setting for SDK installs.

C# DevKit never uses the path returned by our installation. This means users would think this path would change the sdk that this extension uses but that is not the case. This path to dotnet.exe is meant to be the path for the runtime for extensions to run on, and not the SDK path. It's confusing that the setting was used for both and a misstep in a way. DevKit is the main caller of this API so we think we can change this with minimal breakage.

* Remove warning setting and fix invalid path setting

The setting must be accessed earlier. This means vscode will need to be restarted. We also update the readme and messaging a bit so its more publicly clear in all places what the setting is for.

* Fix test

* Refactor code out into a Validator for Conditions

dotnet --list-runtimes and more need to be called in more places. This is a separate task so it should be done. I did not change the code in any way except for adding the requirement clause type.

* Prepare code to validate the path

* add a lot of prototypey code

* add comment for future work

* merge with main

* look up the architecture

* Improve the code

* Go 2 Directories Up to find the True Path on PATH

* Final initial loop of API code

* Fix bug parsing list runtimes

* Add tests

* Fix test and search for where if its not installed

* Consider where may return multiple values

* Fix test

* tests mostly working

* code cleanup - get rid of extra api to set env

* Restore the env var so we dont edit it for other processes

* Uncomment the remaining tests

* Respond to lint

* fix callback

* Fix path to be os-gnostic

* Only search for where on windows and also search for which

* provide env to the find command so /usr/bin/whcih can be used

* Call which which instead of which so the correct command can be found

* Install 3.1 instead of 7.0 because the DTL CI machines seem to have a 7.0 SDK on them :zany:

* give up on arch check for now because it is inaccurate, see comment

* Add github issue in comment for context

* make linter happy

* Respond to PR feedback

* Migrate to connection strings

Resolves #1958

The old application insights key was created by @LakshanF, when we migrated to the new vscode-extension-telemetry service, their API had a breaking change to require a connection string instead of an insights key. #1948

The connection key can be public and is hard coded. Our existing key has been in our open source, source code for many years. Here is what their guidance says: https://www.npmjs.com/package/@vscode/extension-telemetry

> Follow guide to set up Application Insights in Azure and get your connection string. Don't worry about hardcoding it, it is not sensitive.

* Respond to PR Feedback

---------

Co-authored-by: Chet Husk <[email protected]>
  • Loading branch information
nagilson and baronfel authored Sep 30, 2024
1 parent d13a8db commit 40d1a8b
Show file tree
Hide file tree
Showing 31 changed files with 869 additions and 146 deletions.
14 changes: 7 additions & 7 deletions sample/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
63 changes: 48 additions & 15 deletions sample/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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


// --------------------------------------------------------------------------
Expand Down Expand Up @@ -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({
Expand All @@ -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--------------------------
Expand Down
4 changes: 2 additions & 2 deletions sample/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion vscode-dotnet-runtime-extension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions vscode-dotnet-runtime-extension/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions vscode-dotnet-runtime-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand All @@ -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."
}
Expand Down
105 changes: 103 additions & 2 deletions vscode-dotnet-runtime-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<string | undefined>
{
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<string>
{
let result = '1';
Expand Down Expand Up @@ -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<IDotnetAcquireResult | null>
workerContext : IAcquisitionWorkerContext, utilityContext : IUtilityContext, requirement? : DotnetVersionSpecRequirement) : Promise<IDotnetAcquireResult | null>
{
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) => {
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 40d1a8b

Please sign in to comment.