diff --git a/src/OmniSharp.Abstractions/Services/DotNetVersion.cs b/src/OmniSharp.Abstractions/Services/DotNetVersion.cs new file mode 100644 index 0000000000..b438cfaeb4 --- /dev/null +++ b/src/OmniSharp.Abstractions/Services/DotNetVersion.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace OmniSharp.Services +{ + public class DotNetVersion + { + public static DotNetVersion FailedToStartError { get; } = new DotNetVersion("`dotnet --version` failed to start."); + + public bool HasError { get; } + public string ErrorMessage { get; } + + public SemanticVersion Version { get; } + + private DotNetVersion(SemanticVersion version) + { + Version = version; + } + + private DotNetVersion(string errorMessage) + { + HasError = true; + ErrorMessage = errorMessage; + } + + public static DotNetVersion Parse(List lines) + { + if (lines == null || lines.Count == 0) + { + return new DotNetVersion("`dotnet --version` produced no output."); + } + + if (SemanticVersion.TryParse(lines[0], out var version)) + { + return new DotNetVersion(version); + } + + var requestedSdkVersion = string.Empty; + var globalJsonFile = string.Empty; + + foreach (var line in lines) + { + var colonIndex = line.IndexOf(':'); + if (colonIndex >= 0) + { + var name = line.Substring(0, colonIndex).Trim(); + var value = line.Substring(colonIndex + 1).Trim(); + + if (string.IsNullOrEmpty(requestedSdkVersion) && name.Equals("Requested SDK version", StringComparison.OrdinalIgnoreCase)) + { + requestedSdkVersion = value; + } + else if (string.IsNullOrEmpty(globalJsonFile) && name.Equals("global.json file", StringComparison.OrdinalIgnoreCase)) + { + globalJsonFile = value; + } + } + } + + return requestedSdkVersion.Length > 0 && globalJsonFile.Length > 0 + ? new DotNetVersion($"Install the [{requestedSdkVersion}] .NET SDK or update [{globalJsonFile}] to match an installed SDK.") + : new DotNetVersion($"Unexpected output from `dotnet --version`: {string.Join(Environment.NewLine, lines)}"); + } + } +} diff --git a/src/OmniSharp.Abstractions/Services/IDotNetCliService.cs b/src/OmniSharp.Abstractions/Services/IDotNetCliService.cs index ca1c06014b..ca353eedfb 100644 --- a/src/OmniSharp.Abstractions/Services/IDotNetCliService.cs +++ b/src/OmniSharp.Abstractions/Services/IDotNetCliService.cs @@ -19,9 +19,9 @@ public interface IDotNetCliService /// /// Launches "dotnet --version" in the given working directory and returns a - /// representing the returned version text. + /// representing the returned version text. /// - SemanticVersion GetVersion(string workingDirectory = null); + DotNetVersion GetVersion(string workingDirectory = null); /// /// Launches "dotnet --version" in the given working directory and determines @@ -36,7 +36,7 @@ public interface IDotNetCliService /// .NET CLI. If true, this .NET CLI supports project.json development; /// otherwise, it supports .csproj development. /// - bool IsLegacy(SemanticVersion version); + bool IsLegacy(DotNetVersion version); /// /// Launches "dotnet restore" in the given working directory. diff --git a/src/OmniSharp.DotNetTest/TestManager.cs b/src/OmniSharp.DotNetTest/TestManager.cs index e54e60b173..80426403ca 100644 --- a/src/OmniSharp.DotNetTest/TestManager.cs +++ b/src/OmniSharp.DotNetTest/TestManager.cs @@ -62,12 +62,18 @@ public static TestManager Create(Project project, IDotNetCliService dotNetCli, I var version = dotNetCli.GetVersion(workingDirectory); + if (version.HasError) + { + EmitTestMessage(eventEmitter, TestMessageLevel.Error, version.ErrorMessage); + throw new Exception(version.ErrorMessage); + } + if (dotNetCli.IsLegacy(version)) { throw new NotSupportedException("Legacy .NET SDK is not supported"); } - return (TestManager)new VSTestManager(project, workingDirectory, dotNetCli, version, eventEmitter, loggerFactory); + return (TestManager)new VSTestManager(project, workingDirectory, dotNetCli, version.Version, eventEmitter, loggerFactory); } protected abstract string GetCliTestArguments(int port, int parentProcessId); @@ -204,9 +210,9 @@ protected void EmitTestComletedEvent(DotNetTestResult result) EventEmitter.Emit("TestCompleted", result); } - protected void EmitTestMessage(TestMessageLevel messageLevel, string message) + private static void EmitTestMessage(IEventEmitter eventEmitter, TestMessageLevel messageLevel, string message) { - EventEmitter.Emit(TestMessageEvent.Id, + eventEmitter.Emit(TestMessageEvent.Id, new TestMessageEvent { MessageLevel = messageLevel.ToString().ToLowerInvariant(), @@ -214,6 +220,11 @@ protected void EmitTestMessage(TestMessageLevel messageLevel, string message) }); } + protected void EmitTestMessage(TestMessageLevel messageLevel, string message) + { + EmitTestMessage(EventEmitter, messageLevel, message); + } + protected void EmitTestMessage(TestMessagePayload testMessage) { EmitTestMessage(testMessage.MessageLevel, testMessage.Message); diff --git a/src/OmniSharp.Host/Services/DotNetCliService.cs b/src/OmniSharp.Host/Services/DotNetCliService.cs index f2e79cfbff..e89e899e10 100644 --- a/src/OmniSharp.Host/Services/DotNetCliService.cs +++ b/src/OmniSharp.Host/Services/DotNetCliService.cs @@ -6,7 +6,6 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using OmniSharp.Eventing; @@ -17,6 +16,8 @@ namespace OmniSharp.Services { internal class DotNetCliService : IDotNetCliService { + const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE); + private readonly ILogger _logger; private readonly IEventEmitter _eventEmitter; private readonly ConcurrentDictionary _locks; @@ -128,17 +129,62 @@ public Process Start(string arguments, string workingDirectory) return Process.Start(startInfo); } - public SemanticVersion GetVersion(string workingDirectory = null) + public DotNetVersion GetVersion(string workingDirectory = null) { - var output = ProcessHelper.RunAndCaptureOutput(DotNetPath, "--version", workingDirectory); + // Ensure that we set the DOTNET_CLI_UI_LANGUAGE environment variable to "en-US" before + // running 'dotnet --version'. Otherwise, we may get localized results. + var originalValue = Environment.GetEnvironmentVariable(DOTNET_CLI_UI_LANGUAGE); + Environment.SetEnvironmentVariable(DOTNET_CLI_UI_LANGUAGE, "en-US"); + + try + { + Process process; + try + { + process = Start("--version", workingDirectory); + } + catch + { + return DotNetVersion.FailedToStartError; + } + + if (process.HasExited) + { + return DotNetVersion.FailedToStartError; + } + + var lines = new List(); + process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrWhiteSpace(e.Data)) + { + lines.Add(e.Data); + } + }; + + process.ErrorDataReceived += (_, e) => + { + if (!string.IsNullOrWhiteSpace(e.Data)) + { + lines.Add(e.Data); + } + }; + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); - return SemanticVersion.Parse(output); + process.WaitForExit(); + + return DotNetVersion.Parse(lines); + } + finally + { + Environment.SetEnvironmentVariable(DOTNET_CLI_UI_LANGUAGE, originalValue); + } } public DotNetInfo GetInfo(string workingDirectory = null) { - const string DOTNET_CLI_UI_LANGUAGE = nameof(DOTNET_CLI_UI_LANGUAGE); - // Ensure that we set the DOTNET_CLI_UI_LANGUAGE environment variable to "en-US" before // running 'dotnet --info'. Otherwise, we may get localized results. var originalValue = Environment.GetEnvironmentVariable(DOTNET_CLI_UI_LANGUAGE); @@ -197,8 +243,15 @@ public bool IsLegacy(string workingDirectory = null) /// Determines whether the specified version is from a "legacy" .NET CLI. /// If true, this .NET CLI supports project.json development; otherwise, it supports .csproj development. /// - public bool IsLegacy(SemanticVersion version) + public bool IsLegacy(DotNetVersion dotnetVersion) { + if (dotnetVersion.HasError) + { + return false; + } + + var version = dotnetVersion.Version; + if (version.Major < 1) { return true; diff --git a/tests/OmniSharp.Tests/DotNetCliServiceFacts.cs b/tests/OmniSharp.Tests/DotNetCliServiceFacts.cs index c9503a8bb1..43bbe1011d 100644 --- a/tests/OmniSharp.Tests/DotNetCliServiceFacts.cs +++ b/tests/OmniSharp.Tests/DotNetCliServiceFacts.cs @@ -30,7 +30,11 @@ public void GetVersion() { var dotNetCli = host.GetExport(); - var version = dotNetCli.GetVersion(); + var cliVersion = dotNetCli.GetVersion(); + + Assert.False(cliVersion.HasError); + + var version = cliVersion.Version; Assert.Equal(Major, version.Major); Assert.Equal(Minor, version.Minor); diff --git a/tests/OmniSharp.Tests/DotNetVersionFacts.cs b/tests/OmniSharp.Tests/DotNetVersionFacts.cs new file mode 100644 index 0000000000..61c21aaf7b --- /dev/null +++ b/tests/OmniSharp.Tests/DotNetVersionFacts.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using OmniSharp.Services; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Tests +{ + public class DotNetVersionFacts : AbstractTestFixture + { + public DotNetVersionFacts(ITestOutputHelper output) + : base(output) + { + } + + [Theory] + [InlineData("6.0.201")] + [InlineData("7.0.100-preview.2.22153.17")] + public void ParseVersion(string versionString) + { + var cliVersion = DotNetVersion.Parse(new() { versionString }); + + Assert.False(cliVersion.HasError, $"{versionString} did not successfully parse."); + + Assert.Equal(versionString, cliVersion.Version.ToString()); + } + + [Fact] + public void ParseErrorMessage() + { + const string RequestedSdkVersion = "6.0.301-rtm.22263.15"; + const string GlobalJsonFile = "/Users/username/Source/format/global.json"; + const string ExpectedErrorMessage = $"Install the [{RequestedSdkVersion}] .NET SDK or update [{GlobalJsonFile}] to match an installed SDK."; + + var lines = new List() { + "The command could not be loaded, possibly because:", + " * You intended to execute a .NET application:", + " The application '--version' does not exist.", + " * You intended to execute a .NET SDK command:", + " A compatible .NET SDK was not found.", + "", + $"Requested SDK version: {RequestedSdkVersion}", + $"global.json file: {GlobalJsonFile}", + "", + "Installed SDKs:", + "6.0.105 [/usr/local/share/dotnet/sdk]", + "6.0.202 [/usr/local/share/dotnet/sdk]", + "6.0.300 [/usr/local/share/dotnet/sdk]", + "7.0.100-preview.4.22252.9 [/usr/local/share/dotnet/sdk]", + "", + $"Install the [{RequestedSdkVersion}] .NET SDK or update [{GlobalJsonFile}] to match an installed SDK.", + "", + "Learn about SDK resolution:", + "https://aka.ms/dotnet/sdk-not-found" + }; + + var cliVersion = DotNetVersion.Parse(lines); + + Assert.True(cliVersion.HasError); + + Assert.Equal(ExpectedErrorMessage, cliVersion.ErrorMessage); + } + } +}