From 151fedfdb00daaca9b75a089362d3f3b052043d5 Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Mon, 21 Aug 2023 16:42:51 -0700 Subject: [PATCH] Launch the build host process and run design-time builds in it 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. --- .../HostWorkspace/BuildHostProcessManager.cs | 111 ++++++++++++++++++ .../LanguageServerProjectSystem.cs | 27 +++-- .../Core/MSBuild.BuildHost/BuildHost.cs | 45 +++++++ .../Core/MSBuild.BuildHost/IBuildHost.cs | 23 ++++ .../MSBuild.BuildHost/IRemoteProjectFile.cs | 23 ++++ .../MSBuild/Logging/DiagnosticLogItem.cs | 7 ++ .../MSBuild/ProjectFile/DocumentFileInfo.cs | 8 ++ .../MSBuild/ProjectFile/ProjectFileInfo.cs | 28 ++++- .../ProjectFile/ProjectFileReference.cs | 4 + ...alysis.Workspaces.MSBuild.BuildHost.csproj | 4 + .../Core/MSBuild.BuildHost/Program.cs | 59 +++++++++- .../MSBuild.BuildHost/RemoteProjectFile.cs | 32 +++++ 12 files changed, 347 insertions(+), 24 deletions(-) create mode 100644 src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/BuildHostProcessManager.cs create mode 100644 src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs create mode 100644 src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs create mode 100644 src/Workspaces/Core/MSBuild.BuildHost/IRemoteProjectFile.cs create mode 100644 src/Workspaces/Core/MSBuild.BuildHost/RemoteProjectFile.cs diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/BuildHostProcessManager.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/BuildHostProcessManager.cs new file mode 100644 index 0000000000000..9c3bf922beaca --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/BuildHostProcessManager.cs @@ -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 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(); + } + + 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(); + } + } +} + diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs index c759e4a928260..0bc1914a253cc 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs @@ -157,7 +157,7 @@ private async Task 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; @@ -178,9 +178,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList var stopwatch = Stopwatch.StartNew(); // TODO: support configuration switching - var projectBuildManager = new ProjectBuildManager(additionalGlobalProperties: ImmutableDictionary.Empty, msbuildLogger: CreateMSBuildLogger()); - - projectBuildManager.StartBatchBuild(); + using var buildHostProcessManager = new BuildHostProcessManager(); var displayedToast = 0; @@ -192,7 +190,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList { 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. @@ -210,12 +208,11 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList } finally { - projectBuildManager.EndBatchBuild(); - _logger.LogInformation($"Completed (re)load of all projects in {stopwatch.Elapsed}"); } } + // TODO: reconnect this to the out-of-proc build host private Build.Framework.ILogger? CreateMSBuildLogger() { if (_globalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.BinaryLogPath) is not string binaryLogDirectory) @@ -229,13 +226,15 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList return new BinaryLogger { Parameters = binaryLogPath, Verbosity = Build.Framework.LoggerVerbosity.Diagnostic }; } - private async Task LoadOrReloadProjectAsync(string projectPath, ProjectBuildManager projectBuildManager, CancellationToken cancellationToken) + private async Task 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()); @@ -268,15 +267,17 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList } } - 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 { diff --git a/src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs b/src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs new file mode 100644 index 0000000000000..0b9b6973dd572 --- /dev/null +++ b/src/Workspaces/Core/MSBuild.BuildHost/BuildHost.cs @@ -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.Empty); + _buildManager.StartBatchBuild(); + } + + public Task IsProjectFileSupportedAsync(string projectFilePath, CancellationToken cancellationToken) + { + return Task.FromResult(_projectFileLoaderRegistry.TryGetLoaderFromProjectPath(projectFilePath, DiagnosticReportingMode.Ignore, out var _)); + } + + public async Task 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(); + } +} diff --git a/src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs b/src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs new file mode 100644 index 0000000000000..afb75f43cd066 --- /dev/null +++ b/src/Workspaces/Core/MSBuild.BuildHost/IBuildHost.cs @@ -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; + +/// +/// The RPC interface implemented by this host; called via JSON-RPC. +/// +internal interface IBuildHost +{ + /// + /// Returns whether this project's language is supported. + /// + Task IsProjectFileSupportedAsync(string path, CancellationToken cancellationToken); + + Task LoadProjectFileAsync(string path, CancellationToken cancellationToken); + + void Shutdown(); +} diff --git a/src/Workspaces/Core/MSBuild.BuildHost/IRemoteProjectFile.cs b/src/Workspaces/Core/MSBuild.BuildHost/IRemoteProjectFile.cs new file mode 100644 index 0000000000000..c58d075582056 --- /dev/null +++ b/src/Workspaces/Core/MSBuild.BuildHost/IRemoteProjectFile.cs @@ -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; + +/// +/// A trimmed down interface of that is usable for RPC to the build host process and meets all the requirements of being an interface. +/// +[RpcMarshalable] +internal interface IRemoteProjectFile : IDisposable +{ + Task> GetProjectFileInfosAsync(CancellationToken cancellationToken); + Task> GetDiagnosticLogItemsAsync(CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/Logging/DiagnosticLogItem.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/Logging/DiagnosticLogItem.cs index a31f5a2829c70..6cd90a50eccfd 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/Logging/DiagnosticLogItem.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/Logging/DiagnosticLogItem.cs @@ -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) diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/DocumentFileInfo.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/DocumentFileInfo.cs index 8d614f81e7684..b411cdd5a873a 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/DocumentFileInfo.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/DocumentFileInfo.cs @@ -3,17 +3,20 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; +using System.Runtime.Serialization; namespace Microsoft.CodeAnalysis.MSBuild { /// /// Represents a source file that is part of a project file. /// + [DataContract] internal sealed class DocumentFileInfo(string filePath, string logicalPath, bool isLinked, bool isGenerated, SourceCodeKind sourceCodeKind, ImmutableArray folders) { /// /// The absolute path to the document file on disk. /// + [DataMember(Order = 0)] public string FilePath { get; } = filePath; /// @@ -21,27 +24,32 @@ internal sealed class DocumentFileInfo(string filePath, string logicalPath, bool /// The document may not actually exist at this location, and is used /// to represent linked documents. This includes the file name. /// + [DataMember(Order = 1)] public string LogicalPath { get; } = logicalPath; /// /// True if the document has a logical path that differs from its /// absolute file path. /// + [DataMember(Order = 2)] public bool IsLinked { get; } = isLinked; /// /// True if the file was generated during build. /// + [DataMember(Order = 3)] public bool IsGenerated { get; } = isGenerated; /// /// The of this document. /// + [DataMember(Order = 4)] public SourceCodeKind SourceCodeKind { get; } = sourceCodeKind; /// /// Containing folders of the document relative to the containing project root path. /// + [DataMember(Order = 5)] public ImmutableArray Folders { get; } = folders; } } diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileInfo.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileInfo.cs index dadcf9fc74c79..7cd34c0f2dc27 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileInfo.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Runtime.Serialization; using Microsoft.CodeAnalysis.MSBuild.Logging; using Roslyn.Utilities; @@ -14,35 +15,42 @@ namespace Microsoft.CodeAnalysis.MSBuild /// built with MSBuild. If the project is multi-targeting, this represents /// the information from a single target framework. /// + [DataContract] internal sealed class ProjectFileInfo { + [DataMember(Order = 0)] public bool IsEmpty { get; } /// /// The language of this project. /// + [DataMember(Order = 1)] public string Language { get; } /// /// The path to the project file for this project. /// + [DataMember(Order = 2)] public string? FilePath { get; } - /// - /// The path to the intermediate output file this project generates. - /// - public string? IntermediateOutputFilePath { get; } - /// /// The path to the output file this project generates. /// + [DataMember(Order = 3)] public string? OutputFilePath { get; } /// /// The path to the reference assembly output file this project generates. /// + [DataMember(Order = 4)] public string? OutputRefFilePath { get; } + /// + /// The path to the intermediate output file this project generates. + /// + [DataMember(Order = 5)] + public string? IntermediateOutputFilePath { get; } + /// /// The default namespace of the project ("" if not defined, which means global namespace), /// or null if it is unknown or not applicable. @@ -54,43 +62,51 @@ internal sealed class ProjectFileInfo /// In the future, we might consider officially exposing "default namespace" for VB project /// (e.g. through a "defaultnamespace" msbuild property) /// + [DataMember(Order = 6)] public string? DefaultNamespace { get; } /// /// The target framework of this project. /// This takes the form of the 'short name' form used by NuGet (e.g. net46, netcoreapp2.0, etc.) /// + [DataMember(Order = 7)] public string? TargetFramework { get; } /// /// The target framework identifier of this project. /// Used to determine if a project is targeting .net core. /// + [DataMember(Order = 8)] public string? TargetFrameworkIdentifier { get; } /// /// The command line args used to compile the project. /// + [DataMember(Order = 9)] public ImmutableArray CommandLineArgs { get; } /// /// The source documents. /// + [DataMember(Order = 10)] public ImmutableArray Documents { get; } /// /// The additional documents. /// + [DataMember(Order = 11)] public ImmutableArray AdditionalDocuments { get; } /// /// The analyzer config documents. /// + [DataMember(Order = 12)] public ImmutableArray AnalyzerConfigDocuments { get; } /// /// References to other projects. /// + [DataMember(Order = 13)] public ImmutableArray ProjectReferences { get; } public override string ToString() @@ -98,7 +114,7 @@ public override string ToString() ? FilePath ?? string.Empty : $"{FilePath} ({TargetFramework})"; - private ProjectFileInfo( + public ProjectFileInfo( bool isEmpty, string language, string? filePath, diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileReference.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileReference.cs index b3b75fe3bf606..6ed6024473d6a 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileReference.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileReference.cs @@ -4,23 +4,27 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Runtime.Serialization; namespace Microsoft.CodeAnalysis.MSBuild { /// /// Represents a reference to another project file. /// + [DataContract] internal sealed class ProjectFileReference { /// /// The path on disk to the other project file. /// This path may be relative to the referencing project's file or an absolute path. /// + [DataMember(Order = 0)] public string Path { get; } /// /// The aliases assigned to this reference, if any. /// + [DataMember(Order = 1)] public ImmutableArray Aliases { get; } public ProjectFileReference(string path, ImmutableArray aliases) diff --git a/src/Workspaces/Core/MSBuild.BuildHost/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj b/src/Workspaces/Core/MSBuild.BuildHost/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj index 3d64bafb6c927..de48cd2cc70ad 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj +++ b/src/Workspaces/Core/MSBuild.BuildHost/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj @@ -6,6 +6,8 @@ Microsoft.CodeAnalysis true $(SourceBuildTargetFrameworks);net472 + + false @@ -16,7 +18,9 @@ + + diff --git a/src/Workspaces/Core/MSBuild.BuildHost/Program.cs b/src/Workspaces/Core/MSBuild.BuildHost/Program.cs index 05731380cf077..b16f7fa80df28 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/Program.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/Program.cs @@ -3,14 +3,63 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis.Host.Mef; +using StreamJsonRpc; -namespace Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost +#if NETCOREAPP +using System.Runtime.Loader; +#endif + +namespace Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost; + +internal static class Program { - internal class Program + internal static async Task Main() { - public static void Main(string[] args) + // We'll locate MSBuild right away before any other methods try to load any MSBuild types and fail in the process. + // TODO: we should pick the appropriate instance via a command line switch. + MSBuildLocator.RegisterInstance(MSBuildLocator.QueryVisualStudioInstances().First()); + + // Create a MEF container so we can create our SolutionServices to use for discovering language services; we'll include our own assembly in since that contains the services used + // for actually loading MSBuild projects. + var thisAssembly = typeof(Program).Assembly; + +#if NETCOREAPP + + // In the .NET Core case, the dependencies we want to dynamically load are not in our deps.json file, so we won't find them when MefHostServices tries to load them + // with Assembly.Load. To work around this, we'll use LoadFrom instead by hooking our AssemblyLoadContext Resolving. + AssemblyLoadContext.Default.Resolving += (context, assemblyName) => + { + try + { + return Assembly.LoadFrom(Path.Combine(Path.GetDirectoryName(thisAssembly.Location)!, assemblyName.Name! + ".dll")); + } + catch (Exception) + { + return null; + } + }; + +#endif + + var hostServices = MefHostServices.Create(MefHostServices.DefaultAssemblies.Append(thisAssembly)); + var solutionServices = new AdhocWorkspace(hostServices).Services.SolutionServices; + + var messageHandler = new HeaderDelimitedMessageHandler(sendingStream: Console.OpenStandardOutput(), receivingStream: Console.OpenStandardInput(), new JsonMessageFormatter()); + + var jsonRpc = new JsonRpc(messageHandler) { - throw new NotImplementedException(); - } + ExceptionStrategy = ExceptionProcessing.CommonErrorData, + }; + + jsonRpc.AddLocalRpcTarget(new BuildHost(jsonRpc, solutionServices)); + jsonRpc.StartListening(); + + await jsonRpc.Completion.ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/MSBuild.BuildHost/RemoteProjectFile.cs b/src/Workspaces/Core/MSBuild.BuildHost/RemoteProjectFile.cs new file mode 100644 index 0000000000000..63586459646ff --- /dev/null +++ b/src/Workspaces/Core/MSBuild.BuildHost/RemoteProjectFile.cs @@ -0,0 +1,32 @@ +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.MSBuild.Logging; + +namespace Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost; + +internal class RemoteProjectFile : IRemoteProjectFile +{ + private readonly IProjectFile _projectFile; + + public RemoteProjectFile(IProjectFile projectFile) + { + _projectFile = projectFile; + } + + public void Dispose() + { + } + + public Task> GetProjectFileInfosAsync(CancellationToken cancellationToken) + => _projectFile.GetProjectFileInfosAsync(cancellationToken); + + public Task> GetDiagnosticLogItemsAsync(CancellationToken cancellationToken) + => Task.FromResult(_projectFile.Log.ToImmutableArray()); + +}