Skip to content

Commit

Permalink
Load solutions out-of-proc
Browse files Browse the repository at this point in the history
This brings a few benefits:

1. It's possible that the user might have an 8.0 SDK but only a 7.0
   runtime installed; in that case, our language server will run with
   the 7.0 runtime, but will fail to find an MSBuild since the only
   one on the machine might be the 8.0 version which is not runnable.
   This enures we can still function in that scenario. We'll also fall
   back to using a .NET Framework MSBuild if somehow that's all you
   have.
2. When we loaded an MSBuild, that sets environment variables, which
   might not be the same variables set if we're registering a
   diffrent version in our child processes. But the environment
   variables might get inherited, which could result in surprises.

Fixes https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1886993
  • Loading branch information
jasonmalinowski committed Oct 4, 2023
1 parent 0f99a0d commit ebfbec7
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task<IBuildHost> 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.");
Expand All @@ -52,7 +52,7 @@ public async Task<IBuildHost> GetBuildHostAsync(string projectFilePath, Cancella
return buildHost;
}

private async Task<IBuildHost> GetBuildHostAsync(BuildHostProcessKind buildHostKind, CancellationToken cancellationToken)
public async Task<IBuildHost> GetBuildHostAsync(BuildHostProcessKind buildHostKind, CancellationToken cancellationToken)
{
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
Expand Down Expand Up @@ -211,7 +211,7 @@ private static BuildHostProcessKind GetKindForProject(string projectFilePath)
return BuildHostProcessKind.NetFramework;
}

private enum BuildHostProcessKind
public enum BuildHostProcessKind
{
NetCore,
NetFramework
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,11 +26,9 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
internal sealed class LanguageServerProjectSystem
{
/// <summary>
/// A single gate for code that is adding work to <see cref="_projectsToLoadAndReload" /> and modifying <see cref="_msbuildLoaded" />.
/// 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 <see cref="_projectsToLoadAndReload" />. This is just we don't have code simultaneously trying to load and unload solutions at once.
/// </summary>
private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1);
private bool _msbuildLoaded = false;

/// <summary>
/// The suffix to use for the binary log name; incremented each time we have a new build. Should be incremented with <see cref="Interlocked.Increment(ref int)"/>.
Expand Down Expand Up @@ -87,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.
Expand All @@ -132,39 +124,6 @@ public async Task OpenProjectsAsync(ImmutableArray<string> projectFilePaths)
}
}

private async Task<bool> 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<ProjectToLoad> projectPathsToLoadOrReload, CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="$(RefOnlyMicrosoftBuildVersion)" ExcludeAssets="Runtime" PrivateAssets="All" />
<PackageReference Include="Microsoft.Build.Locator" Version="$(MicrosoftBuildLocatorVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftExtensionsLoggingVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsoleVersion)" />
Expand Down
36 changes: 30 additions & 6 deletions src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,7 +33,7 @@ public BuildHost(ILoggerFactory loggerFactory, string? binaryLogPath)
_binaryLogPath = binaryLogPath;
}

private bool TryEnsureMSBuildLoaded(string projectFilePath)
private bool TryEnsureMSBuildLoaded(string projectOrSolutionFilePath)
{
lock (_gate)
{
Expand All @@ -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
Expand Down Expand Up @@ -97,19 +98,42 @@ private void CreateBuildManager()
}
}

public Task<bool> IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken)
public Task<bool> HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken)
{
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<ImmutableArray<(string ProjectPath, string ProjectGuid)>> GetProjectsInSolutionAsync(string solutionFilePath, CancellationToken cancellationToken)
{
if (!TryEnsureMSBuildLoaded(projectFilePath))
return Task.FromResult(false);
EnsureMSBuildLoaded(solutionFilePath);
return Task.FromResult(GetProjectsInSolution(solutionFilePath));
}

[MemberNotNull(nameof(_buildManager))]
[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<bool> IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken)
{
EnsureMSBuildLoaded(projectFilePath);
CreateBuildManager();

return Task.FromResult(TryGetLoaderForPath(projectFilePath) is not null);
}

public async Task<IRemoteProjectFile> 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);
Expand Down
10 changes: 9 additions & 1 deletion src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -13,7 +14,14 @@ namespace Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost;
internal interface IBuildHost
{
/// <summary>
/// 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.
/// </summary>
Task<bool> HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken);

Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>> GetProjectsInSolutionAsync(string solutionFilePath, CancellationToken cancellationToken);

/// <summary>
/// Returns whether this project's is supported by this host.
/// </summary>
Task<bool> IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken);

Expand Down

0 comments on commit ebfbec7

Please sign in to comment.