diff --git a/package.json b/package.json index 49b4fa565..992f898d3 100644 --- a/package.json +++ b/package.json @@ -988,7 +988,7 @@ "omnisharp.dotnetPath": { "type": "string", "scope": "window", - "description": "Specified the path to a dotnet installation to use when \"useModernNet\" is set to true, instead of the default system one. Example: \"/home/username/mycustomdotnetdirectory\"." + "description": "Specified the path to a dotnet installation to use when \"useModernNet\" is set to true, instead of the default system one. This only influences the dotnet installation to use for hosting Omnisharp itself. Example: \"/home/username/mycustomdotnetdirectory\"." }, "omnisharp.waitForDebugger": { "type": "boolean", @@ -1092,6 +1092,14 @@ "type": "string", "description": "Path to the .runsettings file which should be used when running unit tests." }, + "omnisharp.dotNetCliPaths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Paths to a local download of the .NET CLI to use for running any user code.", + "uniqueItems": true + }, "razor.plugin.path": { "type": "string", "scope": "machine", diff --git a/src/common.ts b/src/common.ts index 94b541480..5460f47c0 100644 --- a/src/common.ts +++ b/src/common.ts @@ -56,9 +56,9 @@ export function safeLength(arr: T[] | undefined) { return arr ? arr.length : 0; } -export async function execChildProcess(command: string, workingDirectory: string = getExtensionPath()): Promise { +export async function execChildProcess(command: string, workingDirectory: string = getExtensionPath(), env: NodeJS.ProcessEnv = {}): Promise { return new Promise((resolve, reject) => { - cp.exec(command, { cwd: workingDirectory, maxBuffer: 500 * 1024 }, (error, stdout, stderr) => { + cp.exec(command, { cwd: workingDirectory, maxBuffer: 500 * 1024, env: env }, (error, stdout, stderr) => { if (error) { reject(new Error(`${error} ${stdout} diff --git a/src/constants/IGetDotnetInfo.ts b/src/constants/IGetDotnetInfo.ts index 15c22a766..ad2f346b1 100644 --- a/src/constants/IGetDotnetInfo.ts +++ b/src/constants/IGetDotnetInfo.ts @@ -6,5 +6,5 @@ import { DotnetInfo } from "../utils/getDotnetInfo"; export interface IGetDotnetInfo { - (): Promise; + (dotNetCliPaths: string[]): Promise; } \ No newline at end of file diff --git a/src/coreclr-debug/activate.ts b/src/coreclr-debug/activate.ts index cca5a6d53..714946499 100644 --- a/src/coreclr-debug/activate.ts +++ b/src/coreclr-debug/activate.ts @@ -14,10 +14,11 @@ import CSharpExtensionExports from '../CSharpExtensionExports'; import { getRuntimeDependencyPackageWithId } from '../tools/RuntimeDependencyPackageUtils'; import { getDotnetInfo, DotnetInfo } from '../utils/getDotnetInfo'; import { DotnetDebugConfigurationProvider } from './debugConfigurationProvider'; +import { Options } from '../omnisharp/options'; let _debugUtil: CoreClrDebugUtil = null; -export async function activate(thisExtension: vscode.Extension, context: vscode.ExtensionContext, platformInformation: PlatformInformation, eventStream: EventStream) { +export async function activate(thisExtension: vscode.Extension, context: vscode.ExtensionContext, platformInformation: PlatformInformation, eventStream: EventStream, options: Options) { _debugUtil = new CoreClrDebugUtil(context.extensionPath); if (!CoreClrDebugUtil.existsSync(_debugUtil.debugAdapterDir())) { @@ -29,10 +30,10 @@ export async function activate(thisExtension: vscode.Extension { - return _debugUtil.checkDotNetCli() +async function completeDebuggerInstall(platformInformation: PlatformInformation, eventStream: EventStream, options: Options): Promise { + return _debugUtil.checkDotNetCli(options.dotNetCliPaths) .then(async (dotnetInfo: DotnetInfo) => { let isValidArchitecture: boolean = await checkIsValidArchitecture(platformInformation, eventStream); @@ -132,7 +133,7 @@ function showDotnetToolsWarning(message: string): void { // Else it will launch the debug adapter export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescriptorFactory { - constructor(private readonly platformInfo: PlatformInformation, private readonly eventStream: EventStream, private readonly packageJSON: any, private readonly extensionPath: string) { + constructor(private readonly platformInfo: PlatformInformation, private readonly eventStream: EventStream, private readonly packageJSON: any, private readonly extensionPath: string, private readonly options: Options) { } async createDebugAdapterDescriptor(_session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined): Promise { @@ -160,7 +161,7 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip } // install.complete does not exist, check dotnetCLI to see if we can complete. else if (!CoreClrDebugUtil.existsSync(util.installCompleteFilePath())) { - let success: boolean = await completeDebuggerInstall(this.platformInfo, this.eventStream); + let success: boolean = await completeDebuggerInstall(this.platformInfo, this.eventStream, this.options); if (!success) { this.eventStream.post(new DebuggerNotInstalledFailure()); @@ -172,12 +173,13 @@ export class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescrip // debugger has finished installation, kick off our debugger process // Check for targetArchitecture - const targetArchitecture: string = getTargetArchitecture(this.platformInfo, _session.configuration.targetArchitecture, await getDotnetInfo()); + let dotNetInfo = await getDotnetInfo(this.options.dotNetCliPaths); + const targetArchitecture: string = getTargetArchitecture(this.platformInfo, _session.configuration.targetArchitecture, dotNetInfo); // use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session) if (!executable) { const command = path.join(common.getExtensionPath(), ".debugger", targetArchitecture, "vsdbg-ui" + CoreClrDebugUtil.getPlatformExeExtension()); - executable = new vscode.DebugAdapterExecutable(command); + executable = new vscode.DebugAdapterExecutable(command, [], { env: { 'DOTNET_ROOT' : dotNetInfo.CliPath ? path.dirname(dotNetInfo.CliPath) : '' } }); } // make VS Code launch the DA executable diff --git a/src/coreclr-debug/util.ts b/src/coreclr-debug/util.ts index 4ffc78b48..962816c2b 100644 --- a/src/coreclr-debug/util.ts +++ b/src/coreclr-debug/util.ts @@ -69,8 +69,8 @@ export class CoreClrDebugUtil { // is new enough for us. // Returns: a promise that returns a DotnetInfo class // Throws: An DotNetCliError() from the return promise if either dotnet does not exist or is too old. - public async checkDotNetCli(): Promise { - let dotnetInfo = await getDotnetInfo(); + public async checkDotNetCli(dotNetCliPaths: string[]): Promise { + let dotnetInfo = await getDotnetInfo(dotNetCliPaths); if (dotnetInfo.FullInfo === DOTNET_MISSING_MESSAGE) { // something went wrong with spawning 'dotnet --info' diff --git a/src/features/commands.ts b/src/features/commands.ts index 166e1c466..70c2c0fcc 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -23,7 +23,7 @@ import { IHostExecutableResolver } from '../constants/IHostExecutableResolver'; import { getDotnetInfo } from '../utils/getDotnetInfo'; import { getDecompilationAuthorization, resetDecompilationAuthorization } from '../omnisharp/decompilationPrompt'; -export default function registerCommands(context: vscode.ExtensionContext, server: OmniSharpServer, platformInfo: PlatformInformation, eventStream: EventStream, optionProvider: OptionProvider, monoResolver: IHostExecutableResolver, packageJSON: any, extensionPath: string): CompositeDisposable { +export default function registerCommands(context: vscode.ExtensionContext, server: OmniSharpServer, platformInfo: PlatformInformation, eventStream: EventStream, optionProvider: OptionProvider, monoResolver: IHostExecutableResolver, dotnetResolver: IHostExecutableResolver, packageJSON: any, extensionPath: string): CompositeDisposable { let disposable = new CompositeDisposable(); disposable.add(vscode.commands.registerCommand('o.restart', async () => restartOmniSharp(context, server, optionProvider))); disposable.add(vscode.commands.registerCommand('o.pickProjectAndStart', async () => pickProjectAndStart(server, optionProvider))); @@ -53,7 +53,7 @@ export default function registerCommands(context: vscode.ExtensionContext, serve // Register command for generating tasks.json and launch.json assets. disposable.add(vscode.commands.registerCommand('dotnet.generateAssets', async (selectedIndex) => generateAssets(server, selectedIndex))); - disposable.add(vscode.commands.registerCommand('csharp.reportIssue', async () => reportIssue(vscode, eventStream, getDotnetInfo, platformInfo.isValidPlatformForMono(), optionProvider.GetLatestOptions(), monoResolver))); + disposable.add(vscode.commands.registerCommand('csharp.reportIssue', async () => reportIssue(vscode, eventStream, getDotnetInfo, platformInfo.isValidPlatformForMono(), optionProvider.GetLatestOptions(), monoResolver, dotnetResolver))); disposable.add(vscode.commands.registerCommand('csharp.showDecompilationTerms', async () => showDecompilationTerms(context, server, optionProvider))); diff --git a/src/features/reportIssue.ts b/src/features/reportIssue.ts index be8bcf373..75bd97c73 100644 --- a/src/features/reportIssue.ts +++ b/src/features/reportIssue.ts @@ -11,11 +11,13 @@ import { OpenURL } from "../omnisharp/loggingEvents"; import { Options } from "../omnisharp/options"; import { IHostExecutableResolver } from "../constants/IHostExecutableResolver"; import { IGetDotnetInfo } from "../constants/IGetDotnetInfo"; +import { dirname } from "path"; const issuesUrl = "https://github.com/OmniSharp/omnisharp-vscode/issues/new"; -export default async function reportIssue(vscode: vscode, eventStream: EventStream, getDotnetInfo: IGetDotnetInfo, isValidPlatformForMono: boolean, options: Options, monoResolver: IHostExecutableResolver) { - const dotnetInfo = await getDotnetInfo(); +export default async function reportIssue(vscode: vscode, eventStream: EventStream, getDotnetInfo: IGetDotnetInfo, isValidPlatformForMono: boolean, options: Options, monoResolver: IHostExecutableResolver, dotnetResolver: IHostExecutableResolver) { + // Get info for the dotnet that the Omnisharp executable is run on, not the dotnet Omnisharp will execute user code on. + const dotnetInfo = await getDotnetInfo([ dirname((await dotnetResolver.getHostExecutableInfo(options)).path) ]); const monoInfo = await getMonoIfPlatformValid(isValidPlatformForMono, options, monoResolver); let extensions = getInstalledExtensions(vscode); let csharpExtVersion = getCsharpExtensionVersion(vscode); diff --git a/src/main.ts b/src/main.ts index 61fb6779a..4448194f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -173,7 +173,7 @@ export async function activate(context: vscode.ExtensionContext): Promise('assetPromptDisabled')) { disposables.add(server.onServerStart(async () => { diff --git a/src/omnisharp/loggingEvents.ts b/src/omnisharp/loggingEvents.ts index 89d843831..b67b40ffb 100644 --- a/src/omnisharp/loggingEvents.ts +++ b/src/omnisharp/loggingEvents.ts @@ -41,7 +41,7 @@ export class OmnisharpStart extends TelemetryEventWithMeasures { export class OmnisharpInitialisation implements BaseEvent { type = EventType.OmnisharpInitialisation; - constructor(public timeStamp: Date, public solutionPath: string) { } + constructor(public dotNetCliPaths: string[], public timeStamp: Date, public solutionPath: string) { } } export class OmnisharpLaunch implements BaseEvent { diff --git a/src/omnisharp/options.ts b/src/omnisharp/options.ts index 6204896c9..9ca8bb532 100644 --- a/src/omnisharp/options.ts +++ b/src/omnisharp/options.ts @@ -56,7 +56,8 @@ export class Options { public dotnetPath: string, public excludePaths: string[], public maxProjectFileCountForDiagnosticAnalysis: number, - public testRunSettings: string) { + public testRunSettings: string, + public dotNetCliPaths: string[]) { } public static Read(vscode: vscode): Options { @@ -148,6 +149,8 @@ export class Options { const excludePaths = this.getExcludedPaths(vscode); + const dotNetCliPaths = omnisharpConfig.get('dotNetCliPaths', []); + return new Options( path, useModernNet, @@ -198,7 +201,8 @@ export class Options { dotnetPath, excludePaths, maxProjectFileCountForDiagnosticAnalysis, - testRunSettings + testRunSettings, + dotNetCliPaths ); } diff --git a/src/omnisharp/server.ts b/src/omnisharp/server.ts index 8c54289d4..a0bdda575 100644 --- a/src/omnisharp/server.ts +++ b/src/omnisharp/server.ts @@ -433,6 +433,10 @@ export class OmniSharpServer { args.push('RoslynExtensionsOptions:AnalyzeOpenDocumentsOnly=true'); } + for (let i = 0; i < options.dotNetCliPaths.length; i++) { + args.push(`DotNetCliOptions:LocationPaths:${i}=${options.dotNetCliPaths[i]}`); + } + let launchInfo: LaunchInfo; try { launchInfo = await this._omnisharpManager.GetOmniSharpLaunchInfo(this.packageJSON.defaults.omniSharp, options.path, /* useFramework */ !options.useModernNet, serverUrl, latestVersionFileServerPath, installPath, this.extensionPath); @@ -443,7 +447,7 @@ export class OmniSharpServer { return; } - this.eventStream.post(new ObservableEvents.OmnisharpInitialisation(new Date(), solutionPath)); + this.eventStream.post(new ObservableEvents.OmnisharpInitialisation(options.dotNetCliPaths, new Date(), solutionPath)); this._fireEvent(Events.BeforeServerStart, solutionPath); try { diff --git a/src/utils/getDotnetInfo.ts b/src/utils/getDotnetInfo.ts index d4e5dfdcb..ec23b27b3 100644 --- a/src/utils/getDotnetInfo.ts +++ b/src/utils/getDotnetInfo.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { join } from "path"; import { execChildProcess } from "../common"; +import { CoreClrDebugUtil } from "../coreclr-debug/util"; export const DOTNET_MISSING_MESSAGE = "A valid dotnet installation could not be found."; @@ -13,15 +15,28 @@ let _dotnetInfo: DotnetInfo; // is new enough for us. // Returns: a promise that returns a DotnetInfo class // Throws: An DotNetCliError() from the return promise if either dotnet does not exist or is too old. -export async function getDotnetInfo(): Promise { +export async function getDotnetInfo(dotNetCliPaths: string[]): Promise { if (_dotnetInfo !== undefined) { return _dotnetInfo; } + let dotnetExeName = CoreClrDebugUtil.getPlatformExeExtension(); + let dotnetExecutablePath = undefined; + + for (const dotnetPath of dotNetCliPaths) { + let dotnetFullPath = join(dotnetPath, dotnetExeName); + if (CoreClrDebugUtil.existsSync(dotnetFullPath)) { + dotnetExecutablePath = dotnetFullPath; + break; + } + } + let dotnetInfo = new DotnetInfo(); try { - let data = await execChildProcess('dotnet --info', process.cwd()); + let data = await execChildProcess(`${dotnetExecutablePath || 'dotnet'} --info`, process.cwd()); + + dotnetInfo.CliPath = dotnetExecutablePath; dotnetInfo.FullInfo = data; @@ -49,6 +64,7 @@ export async function getDotnetInfo(): Promise { } export class DotnetInfo { + public CliPath?: string; public FullInfo: string; public Version: string; public OsVersion: string; diff --git a/test/unitTests/Fakes/FakeDotnetResolver.ts b/test/unitTests/Fakes/FakeDotnetResolver.ts new file mode 100644 index 000000000..0b1b8dbff --- /dev/null +++ b/test/unitTests/Fakes/FakeDotnetResolver.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IHostExecutableResolver } from "../../../src/constants/IHostExecutableResolver"; +import { HostExecutableInformation } from "../../../src/constants/HostExecutableInformation"; + +export const fakeMonoInfo: HostExecutableInformation = { + version: "someDotNetVersion", + path: "someDotNetPath", + env: undefined +}; + +export class FakeDotnetResolver implements IHostExecutableResolver { + public getDotnetCalled: boolean; + + constructor(public willReturnDotnetInfo = true) { + this.getDotnetCalled = false; + } + + async getHostExecutableInfo(): Promise { + this.getDotnetCalled = true; + if (this.willReturnDotnetInfo) { + return Promise.resolve(fakeMonoInfo); + } + + return Promise.resolve(undefined); + } +} diff --git a/test/unitTests/Fakes/FakeOptions.ts b/test/unitTests/Fakes/FakeOptions.ts index 2c3c1ba92..0a3d55ee1 100644 --- a/test/unitTests/Fakes/FakeOptions.ts +++ b/test/unitTests/Fakes/FakeOptions.ts @@ -56,5 +56,6 @@ export function getEmptyOptions(): Options { /* dotnetPath */"", /* excludePaths */null, /* maxProjectFileCountForDiagnosticAnalysis */null, - /* testRunSettings */""); + /* testRunSettings */"", + /* dotNetCliPaths */[]); } diff --git a/test/unitTests/features/reportIssue.test.ts b/test/unitTests/features/reportIssue.test.ts index c87b7bea3..9d4dcb4bc 100644 --- a/test/unitTests/features/reportIssue.test.ts +++ b/test/unitTests/features/reportIssue.test.ts @@ -13,6 +13,7 @@ import { vscode } from "../../../src/vscodeAdapter"; import { Options } from "../../../src/omnisharp/options"; import { FakeGetDotnetInfo, fakeDotnetInfo } from "../Fakes/FakeGetDotnetInfo"; import { FakeMonoResolver, fakeMonoInfo } from "../Fakes/FakeMonoResolver"; +import { FakeDotnetResolver } from "../Fakes/FakeDotnetResolver"; suite(`${reportIssue.name}`, () => { const vscodeVersion = "myVersion"; @@ -40,6 +41,7 @@ suite(`${reportIssue.name}`, () => { }; let fakeMonoResolver: FakeMonoResolver; + let fakeDotnetResolver : FakeDotnetResolver; let eventStream: EventStream; let eventBus: TestEventBus; let getDotnetInfo = FakeGetDotnetInfo; @@ -67,55 +69,56 @@ suite(`${reportIssue.name}`, () => { eventStream = new EventStream(); eventBus = new TestEventBus(eventStream); fakeMonoResolver = new FakeMonoResolver(); + fakeDotnetResolver = new FakeDotnetResolver(); }); test(`${OpenURL.name} event is created`, async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver, fakeDotnetResolver); let events = eventBus.getEvents(); expect(events).to.have.length(1); expect(events[0].constructor.name).to.be.equal(`${OpenURL.name}`); }); test(`${OpenURL.name} event is created with the omnisharp-vscode github repo issues url`, async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, false, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, false, options, fakeMonoResolver, fakeDotnetResolver); let url = (eventBus.getEvents()[0]).url; expect(url).to.include("https://github.com/OmniSharp/omnisharp-vscode/issues/new?body=Please paste the output from your clipboard"); }); suite("The body is passed to the vscode clipboard and", () => { test("it contains the vscode version", async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver, fakeDotnetResolver); expect(issueBody).to.include(`**VSCode version**: ${vscodeVersion}`); }); test("it contains the csharp extension version", async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver, fakeDotnetResolver); expect(issueBody).to.include(`**C# Extension**: ${csharpExtVersion}`); }); test("it contains dotnet info", async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver, fakeDotnetResolver); expect(issueBody).to.contain(fakeDotnetInfo.FullInfo); }); test("mono information is obtained when it is a valid mono platform", async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver, fakeDotnetResolver); expect(fakeMonoResolver.getMonoCalled).to.be.equal(true); }); test("mono version is put in the body when it is a valid mono platform", async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver, fakeDotnetResolver); expect(fakeMonoResolver.getMonoCalled).to.be.equal(true); expect(issueBody).to.contain(fakeMonoInfo.version); }); test("mono information is not obtained when it is not a valid mono platform", async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, false, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, false, options, fakeMonoResolver, fakeDotnetResolver); expect(fakeMonoResolver.getMonoCalled).to.be.equal(false); }); test("The url contains the name, publisher and version for all the extensions that are not builtin", async () => { - await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver); + await reportIssue(vscode, eventStream, getDotnetInfo, isValidForMono, options, fakeMonoResolver, fakeDotnetResolver); expect(issueBody).to.contain(extension2.packageJSON.name); expect(issueBody).to.contain(extension2.packageJSON.publisher); expect(issueBody).to.contain(extension2.packageJSON.version); diff --git a/test/unitTests/logging/OmnisharpLoggerObserver.test.ts b/test/unitTests/logging/OmnisharpLoggerObserver.test.ts index 3ec769cd3..94c566bff 100644 --- a/test/unitTests/logging/OmnisharpLoggerObserver.test.ts +++ b/test/unitTests/logging/OmnisharpLoggerObserver.test.ts @@ -95,7 +95,7 @@ suite("OmnisharpLoggerObserver", () => { [ - new OmnisharpInitialisation(new Date(5), "somePath"), + new OmnisharpInitialisation([], new Date(5), "somePath"), ].forEach((event: OmnisharpInitialisation) => { test(`${event.constructor.name}: TimeStamp and SolutionPath are logged`, () => { observer.post(event);