Skip to content

Commit

Permalink
Launch the build host process and run design-time builds in it
Browse files Browse the repository at this point in the history
This implements the basic launching of the build hsot process, setting
up the RPC channel, and running the design-time builds in that process.
Right now this only works for .NET Core projects.
  • Loading branch information
jasonmalinowski committed Aug 22, 2023
1 parent bf6fcbb commit 69e1b40
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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.Threading;
using Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost;
using Roslyn.Utilities;
using StreamJsonRpc;

namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;

internal sealed class BuildHostProcessManager : IDisposable
{
private readonly SemaphoreSlim _gate = new(initialCount: 1);
private BuildHostProcess? _process;

public async Task<IBuildHost> GetBuildHostAsync(CancellationToken cancellationToken)
{
using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
if (_process == null)
{
_process = new BuildHostProcess(LaunchDotNetCoreBuildHost());
_process.Disconnected += BuildHostProcess_Disconnected;
}

return _process.BuildHost;
}
}

#pragma warning disable VSTHRD100 // Avoid async void methods: We're responding to Process.Exited, so an async void event handler is all we can do
private async void BuildHostProcess_Disconnected(object? sender, EventArgs e)
#pragma warning restore VSTHRD100 // Avoid async void methods
{
Contract.ThrowIfNull(sender, $"{nameof(BuildHostProcess)}.{nameof(BuildHostProcess.Disconnected)} was raised with a null sender.");

using (await _gate.DisposableWaitAsync().ConfigureAwait(false))
{
if (_process == sender)
{
_process.Dispose();
_process = null;
}
}
}

private static Process LaunchDotNetCoreBuildHost()
{
var processStartInfo = new ProcessStartInfo()
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
};

// 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.ArgumentList.Add("--roll-forward");
processStartInfo.ArgumentList.Add("LatestMajor");
processStartInfo.Environment["DOTNET_ROLL_FORWARD_TO_PRERELEASE"] = "1";

processStartInfo.ArgumentList.Add(typeof(IBuildHost).Assembly.Location);
var process = Process.Start(processStartInfo);
Contract.ThrowIfNull(process, "Process.Start failed to launch a process.");
return process;
}

public void Dispose()
{
_process?.Dispose();
}

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

public BuildHostProcess(Process process)
{
_process = process;

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

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

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

private void Process_Exited(object? sender, EventArgs e)
{
Disconnected?.Invoke(this, EventArgs.Empty);
}

public IBuildHost BuildHost { get; }

public event EventHandler? Disconnected;

public void Dispose()
{
_jsonRpc.Dispose();
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private async Task<bool> TryEnsureMSBuildLoadedAsync(string workingDirectory)
if (msbuildInstance != null)
{
MSBuildLocator.RegisterInstance(msbuildInstance);
_logger.LogInformation($"Loaded MSBuild at {msbuildInstance.MSBuildPath}");
_logger.LogInformation($"Loaded MSBuild in-process from {msbuildInstance.MSBuildPath}");
_msbuildLoaded = true;

return true;
Expand All @@ -164,9 +164,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<string>
var stopwatch = Stopwatch.StartNew();

// TODO: support configuration switching
var projectBuildManager = new ProjectBuildManager(additionalGlobalProperties: ImmutableDictionary<string, string>.Empty);

projectBuildManager.StartBatchBuild();
using var buildHostProcessManager = new BuildHostProcessManager();

var displayedToast = 0;

Expand All @@ -178,7 +176,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<string>
{
tasks.Add(Task.Run(async () =>
{
var errorKind = await LoadOrReloadProjectAsync(projectPathToLoadOrReload, projectBuildManager, cancellationToken);
var errorKind = await LoadOrReloadProjectAsync(projectPathToLoadOrReload, 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.
Expand All @@ -196,19 +194,19 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<string>
}
finally
{
projectBuildManager.EndBatchBuild();

_logger.LogInformation($"Completed (re)load of all projects in {stopwatch.Elapsed}");
}
}

private async Task<LSP.MessageType?> LoadOrReloadProjectAsync(string projectPath, ProjectBuildManager projectBuildManager, CancellationToken cancellationToken)
private async Task<LSP.MessageType?> LoadOrReloadProjectAsync(string projectPath, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken)
{
try
{
if (_projectFileLoaderRegistry.TryGetLoaderFromProjectPath(projectPath, out var loader))
var buildHost = await buildHostProcessManager.GetBuildHostAsync(cancellationToken);

if (await buildHost.IsProjectFileSupportedAsync(projectPath, cancellationToken))
{
var loadedFile = await loader.LoadProjectFileAsync(projectPath, projectBuildManager, cancellationToken);
var loadedFile = await buildHost.LoadProjectFileAsync(projectPath, cancellationToken);
var loadedProjectInfos = await loadedFile.GetProjectFileInfosAsync(cancellationToken);

var existingProjects = _loadedProjects.GetOrAdd(projectPath, static _ => new List<LoadedProject>());
Expand Down Expand Up @@ -241,15 +239,17 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList<string>
}
}

if (loadedFile.Log.Any())
var diagnosticLogItems = await loadedFile.GetDiagnosticLogItemsAsync(cancellationToken);

if (diagnosticLogItems.Any())
{
foreach (var logItem in loadedFile.Log)
foreach (var logItem in diagnosticLogItems)
{
var projectName = Path.GetFileName(projectPath);
_logger.Log(logItem.Kind is WorkspaceDiagnosticKind.Failure ? LogLevel.Error : LogLevel.Warning, $"{logItem.Kind} while loading {logItem.ProjectFilePath}: {logItem.Message}");
}

return loadedFile.Log.Any(logItem => logItem.Kind is WorkspaceDiagnosticKind.Failure) ? LSP.MessageType.Error : LSP.MessageType.Warning;
return diagnosticLogItems.Any(logItem => logItem.Kind is WorkspaceDiagnosticKind.Failure) ? LSP.MessageType.Error : LSP.MessageType.Warning;
}
else
{
Expand Down
45 changes: 45 additions & 0 deletions src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.MSBuild.Build;
using Roslyn.Utilities;
using StreamJsonRpc;

namespace Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost;

internal sealed class BuildHost : IBuildHost
{
private readonly JsonRpc _jsonRpc;
private readonly ProjectFileLoaderRegistry _projectFileLoaderRegistry;
private readonly ProjectBuildManager _buildManager;

public BuildHost(JsonRpc jsonRpc, SolutionServices solutionServices)
{
_jsonRpc = jsonRpc;
_projectFileLoaderRegistry = new ProjectFileLoaderRegistry(solutionServices, new DiagnosticReporter(new AdhocWorkspace()));
_buildManager = new ProjectBuildManager(System.Collections.Immutable.ImmutableDictionary<string, string>.Empty);
_buildManager.StartBatchBuild();
}

public Task<bool> IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken)
{
return Task.FromResult(_projectFileLoaderRegistry.TryGetLoaderFromProjectPath(projectFilePath, DiagnosticReportingMode.Ignore, out var _));
}

public async Task<IRemoteProjectFile> LoadProjectFileAsync(string projectFilePath, CancellationToken cancellationToken)
{
Contract.ThrowIfFalse(_projectFileLoaderRegistry.TryGetLoaderFromProjectPath(projectFilePath, out var projectLoader));
return new RemoteProjectFile(await projectLoader.LoadProjectFileAsync(projectFilePath, _buildManager, cancellationToken).ConfigureAwait(false));
}

public void Shutdown()
{
_buildManager.EndBatchBuild();
_jsonRpc.Dispose();
}
}
23 changes: 23 additions & 0 deletions src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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.Threading;
using System.Threading.Tasks;

namespace Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost;

/// <summary>
/// The RPC interface implemented by this host; called via JSON-RPC.
/// </summary>
internal interface IBuildHost
{
/// <summary>
/// Returns whether this project's language is supported.
/// </summary>
Task<bool> IsProjectFileSupportedAsync(string path, CancellationToken cancellationToken);

Task<IRemoteProjectFile> LoadProjectFileAsync(string path, CancellationToken cancellationToken);

void Shutdown();
}
23 changes: 23 additions & 0 deletions src/Workspaces/Core/MSBuild.BuildHost/IRemoteProjectFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.MSBuild.Logging;
using StreamJsonRpc;

namespace Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost;

/// <summary>
/// A trimmed down interface of <see cref="IProjectFile"/> that is usable for RPC to the build host process and meets all the requirements of being an <see cref="RpcMarshalableAttribute"/> interface.
/// </summary>
[RpcMarshalable]
internal interface IRemoteProjectFile : IDisposable
{
Task<ImmutableArray<ProjectFileInfo>> GetProjectFileInfosAsync(CancellationToken cancellationToken);
Task<ImmutableArray<DiagnosticLogItem>> GetDiagnosticLogItemsAsync(CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Runtime.Serialization;

namespace Microsoft.CodeAnalysis.MSBuild.Logging
{
[DataContract]
internal class DiagnosticLogItem
{
[DataMember(Order = 0)]
public WorkspaceDiagnosticKind Kind { get; }

[DataMember(Order = 1)]
public string Message { get; }

[DataMember(Order = 2)]
public string ProjectFilePath { get; }

public DiagnosticLogItem(WorkspaceDiagnosticKind kind, string message, string projectFilePath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,47 @@
// 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.Runtime.Serialization;

namespace Microsoft.CodeAnalysis.MSBuild
{
/// <summary>
/// Represents a source file that is part of a project file.
/// </summary>
[DataContract]
internal sealed class DocumentFileInfo
{
/// <summary>
/// The absolute path to the document file on disk.
/// </summary>
[DataMember(Order = 0)]
public string FilePath { get; }

/// <summary>
/// A fictional path to the document, relative to the project.
/// The document may not actually exist at this location, and is used
/// to represent linked documents. This includes the file name.
/// </summary>
[DataMember(Order = 1)]
public string LogicalPath { get; }

/// <summary>
/// True if the document has a logical path that differs from its
/// absolute file path.
/// </summary>
[DataMember(Order = 2)]
public bool IsLinked { get; }

/// <summary>
/// True if the file was generated during build.
/// </summary>
[DataMember(Order = 3)]
public bool IsGenerated { get; }

/// <summary>
/// The <see cref="SourceCodeKind"/> of this document.
/// </summary>
[DataMember(Order = 4)]
public SourceCodeKind SourceCodeKind { get; }

public DocumentFileInfo(string filePath, string logicalPath, bool isLinked, bool isGenerated, SourceCodeKind sourceCodeKind)
Expand Down
Loading

0 comments on commit 69e1b40

Please sign in to comment.