diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/BuildHostProcessManager.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/BuildHostProcessManager.cs index d252ab86aafd3..25ce59ab9279a 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/BuildHostProcessManager.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/BuildHostProcessManager.cs @@ -41,7 +41,7 @@ public async Task GetBuildHostAsync(string projectFilePath, Cancella // Check if this is the case. if (neededBuildHostKind == BuildHostProcessKind.NetFramework) { - if (!await buildHost.IsProjectFileSupportedAsync(projectFilePath, cancellationToken)) + if (!await buildHost.HasUsableMSBuildAsync(projectFilePath, cancellationToken)) { // It's not usable, so we'll fall back to the .NET Core one. _logger?.LogWarning($"An installation of Visual Studio or the Build Tools for Visual Studio could not be found; {projectFilePath} will be loaded with the .NET Core SDK and may encounter errors."); @@ -52,7 +52,7 @@ public async Task GetBuildHostAsync(string projectFilePath, Cancella return buildHost; } - private async Task GetBuildHostAsync(BuildHostProcessKind buildHostKind, CancellationToken cancellationToken) + public async Task GetBuildHostAsync(BuildHostProcessKind buildHostKind, CancellationToken cancellationToken) { using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) { @@ -211,7 +211,7 @@ private static BuildHostProcessKind GetKindForProject(string projectFilePath) return BuildHostProcessKind.NetFramework; } - private enum BuildHostProcessKind + public enum BuildHostProcessKind { NetCore, NetFramework diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs index 0baab5879c9c7..82a1090a5474f 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs @@ -6,8 +6,6 @@ using System.Collections.Immutable; using System.Composition; using System.Diagnostics; -using System.Runtime.CompilerServices; -using Microsoft.Build.Locator; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; @@ -16,7 +14,6 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.ProjectSystem; using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; @@ -29,11 +26,9 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; internal sealed class LanguageServerProjectSystem { /// - /// A single gate for code that is adding work to and modifying . - /// This is just we don't have code simultaneously trying to load and unload solutions at once. + /// A single gate for code that is adding work to . This is just we don't have code simultaneously trying to load and unload solutions at once. /// private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1); - private bool _msbuildLoaded = false; /// /// The suffix to use for the binary log name; incremented each time we have a new build. Should be incremented with . @@ -88,28 +83,24 @@ public LanguageServerProjectSystem( } public async Task OpenSolutionAsync(string solutionFilePath) - { - if (await TryEnsureMSBuildLoadedAsync(Path.GetDirectoryName(solutionFilePath)!)) - await OpenSolutionCoreAsync(solutionFilePath); - } - - [MethodImpl(MethodImplOptions.NoInlining)] // Don't inline; the caller needs to ensure MSBuild is loaded before we can use MSBuild types here - private async Task OpenSolutionCoreAsync(string solutionFilePath) { using (await _gate.DisposableWaitAsync()) { _logger.LogInformation($"Loading {solutionFilePath}..."); - var solutionFile = Microsoft.Build.Construction.SolutionFile.Parse(solutionFilePath); _workspaceFactory.ProjectSystemProjectFactory.SolutionPath = solutionFilePath; - foreach (var project in solutionFile.ProjectsInOrder) - { - if (project.ProjectType == Microsoft.Build.Construction.SolutionProjectType.SolutionFolder) - { - continue; - } + // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed, + // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes. + await using var buildHostProcessManager = new BuildHostProcessManager(_loggerFactory); + var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessManager.BuildHostProcessKind.NetCore, CancellationToken.None); + + // If we don't have a .NET Core SDK on this machine at all, try .NET Framework + if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None)) + buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessManager.BuildHostProcessKind.NetFramework, CancellationToken.None); - _projectsToLoadAndReload.AddWork(new ProjectToLoad(project.AbsolutePath, project.ProjectGuid)); + foreach (var project in await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None)) + { + _projectsToLoadAndReload.AddWork(new ProjectToLoad(project.ProjectPath, project.ProjectGuid)); } // Wait for the in progress batch to complete and send a project initialized notification to the client. @@ -133,39 +124,6 @@ public async Task OpenProjectsAsync(ImmutableArray projectFilePaths) } } - private async Task TryEnsureMSBuildLoadedAsync(string workingDirectory) - { - using (await _gate.DisposableWaitAsync()) - { - if (_msbuildLoaded) - { - return true; - } - else - { - var msbuildDiscoveryOptions = new VisualStudioInstanceQueryOptions { DiscoveryTypes = DiscoveryType.DotNetSdk, WorkingDirectory = workingDirectory }; - var msbuildInstances = MSBuildLocator.QueryVisualStudioInstances(msbuildDiscoveryOptions); - var msbuildInstance = msbuildInstances.FirstOrDefault(); - - if (msbuildInstance != null) - { - MSBuildLocator.RegisterInstance(msbuildInstance); - _logger.LogInformation($"Loaded MSBuild in-process from {msbuildInstance.MSBuildPath}"); - _msbuildLoaded = true; - - return true; - } - else - { - _logger.LogError($"Unable to find a MSBuild to use to load {workingDirectory}."); - await ShowToastNotification.ShowToastNotificationAsync(LSP.MessageType.Error, LanguageServerResources.There_were_problems_loading_your_projects_See_log_for_details, CancellationToken.None, ShowToastNotification.ShowCSharpLogsCommand); - - return false; - } - } - } - } - private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList projectPathsToLoadOrReload, CancellationToken cancellationToken) { var stopwatch = Stopwatch.StartNew(); @@ -173,13 +131,8 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList { - var errorKind = await LoadOrReloadProjectAsync(projectToLoad, buildHostProcessManager, inProcessBuildHost, cancellationToken); + var errorKind = await LoadOrReloadProjectAsync(projectToLoad, buildHostProcessManager, cancellationToken); if (errorKind is LSP.MessageType.Error) { // We should display a toast when the value of displayedToast is 0. This will also update the value to 1 meaning we won't send any more toasts. @@ -210,9 +163,6 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, BuildHostProcessManager? buildHostProcessManager, BuildHost? inProcessBuildHost, CancellationToken cancellationToken) + private async Task LoadOrReloadProjectAsync(ProjectToLoad projectToLoad, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken) { try { var projectPath = projectToLoad.Path; - // If we have a process manager, then get an OOP process; otherwise we're still using in-proc builds so just fetch one in-process - var buildHost = inProcessBuildHost ?? await buildHostProcessManager!.GetBuildHostAsync(projectPath, cancellationToken); + var buildHost = await buildHostProcessManager!.GetBuildHostAsync(projectPath, cancellationToken); if (await buildHost.IsProjectFileSupportedAsync(projectPath, cancellationToken)) { diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj index 439a3ddc512c5..5c28c18ada715 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj @@ -64,8 +64,6 @@ - - diff --git a/src/Features/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs b/src/Features/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs index 95275ec147db0..1782271dced5d 100644 --- a/src/Features/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs +++ b/src/Features/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs @@ -14,11 +14,5 @@ internal static class LanguageServerProjectSystemOptionsStorage /// A folder to log binlogs to when running design-time builds. /// public static readonly Option2 BinaryLogPath = new Option2("dotnet_binary_log_path", defaultValue: null, s_optionGroup); - - /// - /// Whether we are doing design-time builds in-process; this is only to offer a fallback if the OOP builds are broken, and should be removed once - /// we don't have folks using this. - /// - public static readonly Option2 LoadInProcess = new("dotnet_load_in_process", defaultValue: false, s_optionGroup); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs b/src/Features/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs index ea13956a387b7..fbdd30b6b8c6d 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs @@ -61,7 +61,6 @@ internal partial class DidChangeConfigurationNotificationHandler LspOptionsStorage.LspEnableReferencesCodeLens, LspOptionsStorage.LspEnableTestsCodeLens, // Project system - LanguageServerProjectSystemOptionsStorage.BinaryLogPath, - LanguageServerProjectSystemOptionsStorage.LoadInProcess); + LanguageServerProjectSystemOptionsStorage.BinaryLogPath); } } diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Configuration/DidChangeConfigurationNotificationHandlerTest.cs b/src/Features/LanguageServer/ProtocolUnitTests/Configuration/DidChangeConfigurationNotificationHandlerTest.cs index d385486a0e9c5..c7561f8283cf9 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Configuration/DidChangeConfigurationNotificationHandlerTest.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Configuration/DidChangeConfigurationNotificationHandlerTest.cs @@ -145,7 +145,6 @@ public void VerifyLspClientOptionNames() "code_lens.dotnet_enable_references_code_lens", "code_lens.dotnet_enable_tests_code_lens", "projects.dotnet_binary_log_path", - "projects.dotnet_load_in_process", }.OrderBy(name => name); Assert.Equal(expectedNames, actualNames); diff --git a/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs b/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs index bcb3ed3e7193a..37046b4eb9713 100644 --- a/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs +++ b/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs @@ -226,7 +226,6 @@ public void OptionHasStorageIfNecessary(string configName) "dotnet_style_operator_placement_when_wrapping", // Doesn't have VS UI. TODO: https://github.com/dotnet/roslyn/issues/66062 "dotnet_style_prefer_foreach_explicit_cast_in_source", // For a small customer segment, doesn't warrant VS UI. "dotnet_binary_log_path", // VSCode only option for the VS Code project system; does not apply to VS - "dotnet_load_in_process", // VSCode only option for the VS Code project system; does not apply to VS "dotnet_lsp_using_devkit", // VSCode internal only option. Does not need any UI. "dotnet_enable_references_code_lens", // VSCode only option. Does not apply to VS. "dotnet_enable_tests_code_lens", // VSCode only option. Does not apply to VS. diff --git a/src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs b/src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs index 9860cd8758fa2..652c07e88c768 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.Build.Construction; using Microsoft.Build.Locator; using Microsoft.Build.Logging; using Microsoft.CodeAnalysis.MSBuild; @@ -32,7 +33,7 @@ public BuildHost(ILoggerFactory loggerFactory, string? binaryLogPath) _binaryLogPath = binaryLogPath; } - private bool TryEnsureMSBuildLoaded(string projectFilePath) + private bool TryEnsureMSBuildLoaded(string projectOrSolutionFilePath) { lock (_gate) { @@ -56,7 +57,7 @@ private bool TryEnsureMSBuildLoaded(string projectFilePath) // Locate the right SDK for this particular project; MSBuildLocator ensures in this case the first one is the preferred one. // TODO: we should pick the appropriate instance back in the main process and just use the one chosen here. - var options = new VisualStudioInstanceQueryOptions { DiscoveryTypes = DiscoveryType.DotNetSdk, WorkingDirectory = Path.GetDirectoryName(projectFilePath) }; + var options = new VisualStudioInstanceQueryOptions { DiscoveryTypes = DiscoveryType.DotNetSdk, WorkingDirectory = Path.GetDirectoryName(projectOrSolutionFilePath) }; instance = MSBuildLocator.QueryVisualStudioInstances(options).FirstOrDefault(); #endif @@ -97,11 +98,33 @@ private void CreateBuildManager() } } - public Task IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken) + public Task HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken) { - if (!TryEnsureMSBuildLoaded(projectFilePath)) - return Task.FromResult(false); + return Task.FromResult(TryEnsureMSBuildLoaded(projectOrSolutionFilePath)); + } + private void EnsureMSBuildLoaded(string projectFilePath) + { + Contract.ThrowIfFalse(TryEnsureMSBuildLoaded(projectFilePath), $"We don't have an MSBuild to use; {nameof(HasUsableMSBuildAsync)} should have been called first to check."); + } + + public Task> GetProjectsInSolutionAsync(string solutionFilePath, CancellationToken cancellationToken) + { + EnsureMSBuildLoaded(solutionFilePath); + return Task.FromResult(GetProjectsInSolution(solutionFilePath)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] // Do not inline this, since this uses MSBuild types which are being loaded by the caller + private static ImmutableArray<(string ProjectPath, string ProjectGuid)> GetProjectsInSolution(string solutionFilePath) + { + return SolutionFile.Parse(solutionFilePath).ProjectsInOrder + .Where(static p => p.ProjectType != SolutionProjectType.SolutionFolder) + .SelectAsArray(static p => (p.AbsolutePath, p.ProjectGuid)); + } + + public Task IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken) + { + EnsureMSBuildLoaded(projectFilePath); CreateBuildManager(); return Task.FromResult(TryGetLoaderForPath(projectFilePath) is not null); @@ -109,7 +132,7 @@ public Task IsProjectFileSupportedAsync(string projectFilePath, Cancellati public async Task LoadProjectFileAsync(string projectFilePath, CancellationToken cancellationToken) { - Contract.ThrowIfFalse(TryEnsureMSBuildLoaded(projectFilePath), $"We don't have an MSBuild to use; {nameof(IsProjectFileSupportedAsync)} should have been called first."); + EnsureMSBuildLoaded(projectFilePath); CreateBuildManager(); var projectLoader = TryGetLoaderForPath(projectFilePath); diff --git a/src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs b/src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs index a14160f221bbe..9be89aa72f20f 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,14 @@ namespace Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost; internal interface IBuildHost { /// - /// Returns whether this project's is supported by this host, considering both the project language and also MSBuild availability. + /// Returns true if this build host was able to discover a usable MSBuild instance. This should be called before calling other methods. + /// + Task HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken); + + Task> GetProjectsInSolutionAsync(string solutionFilePath, CancellationToken cancellationToken); + + /// + /// Returns whether this project's is supported by this host. /// Task IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken);