Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run design time builds out-of-process #69616

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a827ef9
Split Microsoft.CodeAnalysis.Workspaces.MSBuild into two
jasonmalinowski Aug 18, 2023
53ee4ec
Fix some spelling errors
jasonmalinowski Aug 18, 2023
a6e3cfb
Remove ProjectFileInfo.Log
jasonmalinowski Aug 22, 2023
64d345b
Launch the build host process and run design-time builds in it
jasonmalinowski Aug 21, 2023
ac72bba
Fix indenting in Microsoft.CodeAnalysis.LanguageServer.csproj
jasonmalinowski Aug 22, 2023
71b655c
Deploy a net472 version of the BuildHost into the LanguageServer
jasonmalinowski Aug 22, 2023
5c7a3db
Delete blank line
jasonmalinowski Aug 23, 2023
e93ef80
Support loading projects in a .NET Framework build host
jasonmalinowski Aug 23, 2023
0fba304
Reconnect support for logging binary logs in design time builds
jasonmalinowski Aug 23, 2023
abd9885
Unset MSBUILD_EXE_PATH when launching build hosts
jasonmalinowski Aug 23, 2023
79f8561
Switch away from async void
jasonmalinowski Aug 24, 2023
1a419e1
Don't load MSBuild until we actually have a project path
jasonmalinowski Aug 25, 2023
f9621a1
Use the .NET Core build host on Mac/Linux until we support mono
jasonmalinowski Aug 26, 2023
22bbff7
Include MSBuild.BuildHost in our VS.ExternalAPIs.Roslyn package
jasonmalinowski Aug 29, 2023
2435e4f
Add Roslyn.VisualStudio.Setup as a dependency
jasonmalinowski Aug 29, 2023
b4b64d1
Include the BuildHost binaries in the Workspaces.MSBuild package
jasonmalinowski Aug 29, 2023
df13de5
Include executables in the BuildBoss checks for VS.ExternalAPIs.Roslyn
jasonmalinowski Aug 29, 2023
e4ec102
Allow the BuildHost to directly log via stderr
jasonmalinowski Aug 30, 2023
51e6e45
Put a space after the category so it looks nicer
jasonmalinowski Aug 30, 2023
1c53ab7
Don't dispose the RPC channel during a method call
jasonmalinowski Aug 30, 2023
4a152e8
Merge branch 'dotnet/main' into out-of-proc-design-time-builds
jasonmalinowski Aug 30, 2023
e919bf4
Stop packaging C# binaries in the net472 build host
jasonmalinowski Aug 30, 2023
88f5c21
Don't deploy XML documentation comment files for the net472 BuildHost
jasonmalinowski Aug 30, 2023
66ac5d9
Update parameter names on IBuildHost to match the implementation
jasonmalinowski Aug 30, 2023
75f2235
Use MSBuildLocator.IsRegistered to see if we're already registered
jasonmalinowski Aug 30, 2023
022c233
Add a fallback flag to keep in-process build support available
jasonmalinowski Aug 30, 2023
b2cc3b8
We don't have to hook assembly load now that we're not using MEF
jasonmalinowski Aug 30, 2023
5e24433
Merge remote-tracking branch 'dotnet/main' into out-of-proc-design-ti…
jasonmalinowski Sep 1, 2023
df96112
Disable ngen for the BuildHost
jasonmalinowski Sep 5, 2023
ce7780c
Disable color output for the build host process
jasonmalinowski Sep 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Roslyn.sln
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Roslyn.VisualStudio.Service
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.ExternalAccess.CompilerDeveloperSDK", "src\Tools\ExternalAccess\CompilerDeveloperSDK\Microsoft.CodeAnalysis.ExternalAccess.CompilerDeveloperSDK.csproj", "{A833B11C-5072-4A1F-A32B-2700433B0D3D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost", "src\Workspaces\Core\MSBuild.BuildHost\Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj", "{B1481D94-682E-46EC-ADBE-A16EB46FEEE9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1349,6 +1351,10 @@ Global
{A833B11C-5072-4A1F-A32B-2700433B0D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A833B11C-5072-4A1F-A32B-2700433B0D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A833B11C-5072-4A1F-A32B-2700433B0D3D}.Release|Any CPU.Build.0 = Release|Any CPU
{B1481D94-682E-46EC-ADBE-A16EB46FEEE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1481D94-682E-46EC-ADBE-A16EB46FEEE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1481D94-682E-46EC-ADBE-A16EB46FEEE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1481D94-682E-46EC-ADBE-A16EB46FEEE9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1598,6 +1604,7 @@ Global
{172F3A04-644B-492C-9632-B07B52A5C0C4} = {55A62CFA-1155-46F1-ADF3-BEEE51B58AB5}
{09E88382-0D7B-4A15-B1AF-0B89A5B67227} = {8DBA5174-B0AA-4561-82B1-A46607697753}
{A833B11C-5072-4A1F-A32B-2700433B0D3D} = {8977A560-45C2-4EC2-A849-97335B382C74}
{B1481D94-682E-46EC-ADBE-A16EB46FEEE9} = {55A62CFA-1155-46F1-ADF3-BEEE51B58AB5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {604E6B91-7BC0-4126-AE07-D4D2FEFC3D29}
Expand Down
1 change: 1 addition & 0 deletions SpellingExclusions.dic
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
awaitable
Refactorings
Infos
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.Diagnostics;
using System.Runtime.InteropServices;
using System.Xml;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost;
using Microsoft.Extensions.Logging;
using Roslyn.Utilities;
using StreamJsonRpc;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal sealed class BuildHostProcessManager : IAsyncDisposable
{
private readonly ILoggerFactory? _loggerFactory;
private readonly ILogger? _logger;
private readonly string? _binaryLogPath;

private readonly SemaphoreSlim _gate = new(initialCount: 1);
private readonly Dictionary<BuildHostProcessKind, BuildHostProcess> _processes = new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah interesting - so eventually we could even have different .net core build hosts at the same time if we needed (for different projects)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! DevKit doesn't support it, and it's not clear we should either from a user-scenario standpoint, but once we have MSBuildLocator behavior changed it's easier to write it right than maintain the current behavior. This type will eventually move to MSBuildWorkspace too where people might be doing programmatic things across multiple repositories too, so since we're an API it's harder to say "we don't support it".


public BuildHostProcessManager(ILoggerFactory? loggerFactory = null, string? binaryLogPath = null)
{
_loggerFactory = loggerFactory;
_logger = loggerFactory?.CreateLogger<BuildHostProcessManager>();
_binaryLogPath = binaryLogPath;
}

public async Task<IBuildHost> GetBuildHostAsync(string projectFilePath, CancellationToken cancellationToken)
{
var neededBuildHostKind = GetKindForProject(projectFilePath);

_logger?.LogTrace($"Choosing a build host of type {neededBuildHostKind} for {projectFilePath}");

using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
if (!_processes.TryGetValue(neededBuildHostKind, out var buildHostProcess))
{
var process = neededBuildHostKind switch
{
BuildHostProcessKind.NetCore => LaunchDotNetCoreBuildHost(),
BuildHostProcessKind.NetFramework => LaunchDotNetFrameworkBuildHost(),
_ => throw ExceptionUtilities.UnexpectedValue(neededBuildHostKind)
};

buildHostProcess = new BuildHostProcess(process, _loggerFactory);
buildHostProcess.Disconnected += BuildHostProcess_Disconnected;
_processes.Add(neededBuildHostKind, buildHostProcess);
}

return buildHostProcess.BuildHost;
}
}

private void BuildHostProcess_Disconnected(object? sender, EventArgs e)
{
Contract.ThrowIfNull(sender, $"{nameof(BuildHostProcess)}.{nameof(BuildHostProcess.Disconnected)} was raised with a null sender.");
jasonmalinowski marked this conversation as resolved.
Show resolved Hide resolved

Task.Run(async () =>
{
BuildHostProcess? processToDispose = null;

using (await _gate.DisposableWaitAsync().ConfigureAwait(false))
{
// Remove it from our map; it's possible it might have already been removed if we had more than one way we observed a disconnect.
var existingProcess = _processes.SingleOrNull(p => p.Value == sender);
if (existingProcess.HasValue)
{
processToDispose = existingProcess.Value.Value;
_processes.Remove(existingProcess.Value.Key);
}
}

// Dispose outside of the lock (even though we don't expect much to happen at this point)
if (processToDispose != null)
{
processToDispose.LoggerForProcessMessages?.LogTrace("Process exited.");
await processToDispose.DisposeAsync();
}
});
}

public async ValueTask DisposeAsync()
{
foreach (var process in _processes.Values)
await process.DisposeAsync();
}

private Process LaunchDotNetCoreBuildHost()
{
var processStartInfo = new ProcessStartInfo()
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet",
};

// We need to roll forward to the latest runtime, since the project may be using an SDK (or an SDK required runtime) newer than we ourselves built with.
// We set the environment variable since --roll-forward LatestMajor doesn't roll forward to prerelease SDKs otherwise.
processStartInfo.Environment["DOTNET_ROLL_FORWARD_TO_PRERELEASE"] = "1";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what we're doing here is launching the build host with whatever the latest major version they have installed is right?

What directory are we launching this from - I think that could affect which dotnet we use if they for example have a global.json? If it does matter, we should potentially be explicit about what the working directory should be

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions!

So what we're doing here is launching the build host with whatever the latest major version they have installed is right?

At the moment, yes, since MSBuildLocator currently requires that you have to be running the same or higher major version to even discover the applicable SDK in the first place; it's an annoying catch-22. And once that process is launched, we will find the global.json for the first project we then ask it to process when we invoke MSBuildLocator there.

Once this is in, I plan on making a change to MSBuildLocator that would allow us to discover (but not register) the applicable SDK version directly. In that case:

  1. We'll know the required runtime version of the build process, so we can avoid launching a prerelease runtime for a released SDK.
  2. We'll also be able to have different build host processes for different SDK versions, for the more esoteric cases of a huge monorepo with different global.jsons. DevKit doesn't technically support that scenario (and we don't either today), but it's something that will fall out in the wash automatically.

What directory are we launching this from

This is still a reminder for me though that was brought up that DevKit might explicitly set a working directory so there's no risk this locks a user folder underneath us.

processStartInfo.ArgumentList.Add("--roll-forward");
processStartInfo.ArgumentList.Add("LatestMajor");

processStartInfo.ArgumentList.Add(typeof(IBuildHost).Assembly.Location);

AppendBuildHostCommandLineArgumentsConfigureProcess(processStartInfo);

var process = Process.Start(processStartInfo);
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");
return process;
}

private Process LaunchDotNetFrameworkBuildHost()
{
var netFrameworkBuildHost = Path.Combine(Path.GetDirectoryName(typeof(BuildHostProcessManager).Assembly.Location)!, "BuildHost-net472", "Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe");
Contract.ThrowIfFalse(File.Exists(netFrameworkBuildHost), $"Unable to locate the .NET Framework build host at {netFrameworkBuildHost}");

var processStartInfo = new ProcessStartInfo()
{
FileName = netFrameworkBuildHost,
};

AppendBuildHostCommandLineArgumentsConfigureProcess(processStartInfo);

var process = Process.Start(processStartInfo);
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");
return process;
}

private void AppendBuildHostCommandLineArgumentsConfigureProcess(ProcessStartInfo processStartInfo)
{
if (_binaryLogPath is not null)
{
processStartInfo.ArgumentList.Add("--binlog");
processStartInfo.ArgumentList.Add(_binaryLogPath);
}

// MSBUILD_EXE_PATH is read by MSBuild to find related tasks and targets. We don't want this to be inherited by our build process, or otherwise
// it might try to load targets that aren't appropriate for the build host.
processStartInfo.Environment.Remove("MSBUILD_EXE_PATH");

processStartInfo.CreateNoWindow = true;
processStartInfo.UseShellExecute = false;
processStartInfo.RedirectStandardInput = true;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
}

private static readonly XmlReaderSettings s_xmlSettings = new()
{
DtdProcessing = DtdProcessing.Prohibit,
};

private static BuildHostProcessKind GetKindForProject(string projectFilePath)
{
// At the moment we don't have mono support here, so if we're not on Windows, we'll always force to .NET Core.
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return BuildHostProcessKind.NetCore;

// This implements the algorithm as stated in https://github.com/dotnet/project-system/blob/9a761848e0f330a45e349685a266fea00ac3d9c5/docs/opening-with-new-project-system.md;
// we'll load the XML of the project directly, and inspect for certain elements.
XDocument document;

// Read the XML, prohibiting DTD processing due the the usual concerns there.
using (var fileStream = new FileStream(projectFilePath, FileMode.Open, FileAccess.Read))
using (var xmlReader = XmlReader.Create(fileStream, s_xmlSettings))
document = XDocument.Load(xmlReader);

// If we don't have a root, doesn't really matter which. This project is just malformed.
if (document.Root == null)
return BuildHostProcessKind.NetCore;

// Look for SDK attribute on the root
if (document.Root.Attribute("Sdk") != null)
return BuildHostProcessKind.NetCore;

// Look for <Import Sdk=... />
if (document.Root.Elements("Import").Attributes("Sdk").Any())
return BuildHostProcessKind.NetCore;

// Look for <Sdk ... />
if (document.Root.Elements("Sdk").Any())
return BuildHostProcessKind.NetCore;

// Looking for PropertyGroups that contain TargetFramework or TargetFrameworks nodes
var propertyGroups = document.Descendants("PropertyGroup");
if (propertyGroups.Elements("TargetFramework").Any() || propertyGroups.Elements("TargetFrameworks").Any())
return BuildHostProcessKind.NetCore;

// Nothing that indicates it's an SDK-style project, so use our .NET framework host
return BuildHostProcessKind.NetFramework;
jasonmalinowski marked this conversation as resolved.
Show resolved Hide resolved
}

private enum BuildHostProcessKind
{
NetCore,
NetFramework
}

private sealed class BuildHostProcess : IAsyncDisposable
{
private readonly Process _process;
private readonly JsonRpc _jsonRpc;

private int _disposed = 0;

public BuildHostProcess(Process process, ILoggerFactory? loggerFactory)
{
LoggerForProcessMessages = loggerFactory?.CreateLogger($"BuildHost PID {process.Id}");

_process = process;

_process.EnableRaisingEvents = true;
_process.Exited += Process_Exited;

_process.ErrorDataReceived += Process_ErrorDataReceived;

var messageHandler = new HeaderDelimitedMessageHandler(sendingStream: _process.StandardInput.BaseStream, receivingStream: _process.StandardOutput.BaseStream, new JsonMessageFormatter());

_jsonRpc = new JsonRpc(messageHandler);
_jsonRpc.StartListening();
BuildHost = _jsonRpc.Attach<IBuildHost>();

// Call this last so our type is fully constructed before we start firing events
_process.BeginErrorReadLine();
}

private void Process_Exited(object? sender, EventArgs e)
{
Disconnected?.Invoke(this, EventArgs.Empty);
jasonmalinowski marked this conversation as resolved.
Show resolved Hide resolved
}

private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data is not null)
LoggerForProcessMessages?.LogTrace($"Message from Process: {e.Data}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to consider for later - similar to LSP trace logging, sometimes the json itself is useful to log at the most verbose levels. In case there are weird serialization issues.

}

public IBuildHost BuildHost { get; }
public ILogger? LoggerForProcessMessages { get; }

public event EventHandler? Disconnected;

public async ValueTask DisposeAsync()
{
// Ensure only one thing disposes; while we disconnect the process will go away, which will call us to do this again
if (Interlocked.CompareExchange(ref _disposed, value: 1, comparand: 0) != 0)
return;

// We will call Shutdown in a try/catch; if the process has gone bad it's possible the connection is no longer functioning.
try
{
await BuildHost.ShutdownAsync();
_jsonRpc.Dispose();

LoggerForProcessMessages?.LogTrace("Process gracefully shut down.");
}
catch (Exception e)
{
LoggerForProcessMessages?.LogError(e, "Exception while shutting down the BuildHost process.");

// OK, process may have gone bad.
_process.Kill();
}
}
}
}

Loading