From 8ae3b7b920f72a65a0a36f595b3cc7622be292ca Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Fri, 11 Apr 2025 11:45:46 -0700 Subject: [PATCH 01/20] Cache MEF catalog in servicehub process --- eng/targets/Services.props | 1 + .../ExportProviderBuilderTests.cs | 1 + .../LanguageServerTestComposition.cs | 4 +- .../LanguageServerExportProviderBuilder.cs | 57 +++++++++ .../Program.cs | 4 +- .../Services/ExtensionAssemblyManager.cs | 1 + .../VisualStudioRemoteHostClientProvider.cs | 14 ++- .../Services/ServiceHubServicesTests.cs | 12 +- .../IRemoteProcessTelemetryService.cs | 10 +- .../SolutionWithSourceGeneratorTests.cs | 4 +- .../MEF/FeaturesTestCompositions.cs | 2 +- .../Remote/InProcRemostHostClient.cs | 1 + .../Remote/Core}/ExportProviderBuilder.cs | 113 ++++++++---------- .../Core/IRemoteInitializationService.cs | 19 +++ ...soft.CodeAnalysis.Remote.Workspaces.csproj | 1 + .../Core/RemoteWorkspacesResources.resx | 3 + .../Remote/Core/ServiceDescriptors.cs | 1 + .../Remote/Core/ServiceHubRemoteHostClient.cs | 5 +- .../Core/xlf/RemoteWorkspacesResources.cs.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.de.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.es.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.fr.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.it.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.ja.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.ko.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.pl.xlf | 5 + .../xlf/RemoteWorkspacesResources.pt-BR.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.ru.xlf | 5 + .../Core/xlf/RemoteWorkspacesResources.tr.xlf | 5 + .../xlf/RemoteWorkspacesResources.zh-Hans.xlf | 5 + .../xlf/RemoteWorkspacesResources.zh-Hant.xlf | 5 + .../ServiceHub/Host/RemoteExportProvider.cs | 69 +++++++++++ .../ServiceHub/Host/RemoteWorkspaceManager.cs | 55 +-------- .../Services/BrokeredServiceBase.cs | 4 +- .../RemoteInitializationService.cs | 38 ++++++ .../RemoteProcessTelemetryService.cs | 16 --- 36 files changed, 342 insertions(+), 158 deletions(-) create mode 100644 src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs rename src/{LanguageServer/Microsoft.CodeAnalysis.LanguageServer => Workspaces/Remote/Core}/ExportProviderBuilder.cs (64%) create mode 100644 src/Workspaces/Remote/Core/IRemoteInitializationService.cs create mode 100644 src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs create mode 100644 src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 409f1b282f9a1..c9b59f0c095bf 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -33,6 +33,7 @@ + diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs index 7148ec5e9a274..63dd89636e2ca 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs @@ -4,6 +4,7 @@ using Basic.Reference.Assemblies; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs index 9667713fc60bc..6ec172539eb66 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis.LanguageServer.Services; +using Microsoft.CodeAnalysis.Remote; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; @@ -33,6 +34,7 @@ public static Task CreateExportProviderAsync( UseStdIo: false); var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory); assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory); - return ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory, loggerFactory); + + return LanguageServerExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory, loggerFactory, CancellationToken.None); } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs new file mode 100644 index 0000000000000..d0787093bbf23 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -0,0 +1,57 @@ +// 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 Microsoft.CodeAnalysis.LanguageServer.Logging; +using Microsoft.CodeAnalysis.LanguageServer.Services; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer; + +internal class LanguageServerExportProviderBuilder +{ + public static async Task CreateExportProviderAsync( + ExtensionAssemblyManager extensionManager, + IAssemblyLoader assemblyLoader, + string? devKitDependencyPath, + string cacheDirectory, + ILoggerFactory loggerFactory, + CancellationToken cancellationToken) + { + var baseDirectory = AppContext.BaseDirectory; + + // Load any Roslyn assemblies from the extension directory + var assemblyPaths = Directory.EnumerateFiles(baseDirectory, "Microsoft.CodeAnalysis*.dll"); + assemblyPaths = assemblyPaths.Concat(Directory.EnumerateFiles(baseDirectory, "Microsoft.ServiceHub*.dll")); + + // DevKit assemblies are not shipped in the main language server folder + // and not included in ExtensionAssemblyPaths (they get loaded into the default ALC). + // So manually add them to the MEF catalog here. + if (devKitDependencyPath != null) + { + assemblyPaths = assemblyPaths.Concat(devKitDependencyPath); + } + + // Add the extension assemblies to the MEF catalog. + assemblyPaths = assemblyPaths.Concat(extensionManager.ExtensionAssemblyPaths); + + // Create a MEF resolver that can resolve assemblies in the extension contexts. + var resolver = new Resolver(assemblyLoader); + var catalogPrefix = "c#-languageserver"; + var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(assemblyPaths.ToImmutableArray(), resolver, cacheDirectory, catalogPrefix, performCleanup: false, loggerFactory, cancellationToken); + + // Also add the ExtensionAssemblyManager so it will be available for the rest of the composition. + exportProvider.GetExportedValue().SetMefExtensionAssemblyManager(extensionManager); + + var exportProviderBuilderLogger = loggerFactory.CreateLogger(); + + // Immediately set the logger factory, so that way it'll be available for the rest of the composition + exportProvider.GetExportedValue().SetFactory(loggerFactory); + + return exportProvider; + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs index 05745fb0f0544..19b216a622d82 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs @@ -2,7 +2,6 @@ // 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.CommandLine; using System.Diagnostics; using System.IO.Pipes; @@ -20,7 +19,6 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; -using Roslyn.Utilities; using RoslynLog = Microsoft.CodeAnalysis.Internal.Log; // Setting the title can fail if the process is run without a window, such @@ -99,7 +97,7 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, Cancellation var cacheDirectory = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location)!, "cache"); - using var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, cacheDirectory, loggerFactory); + using var exportProvider = await LanguageServerExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, cacheDirectory, loggerFactory, cancellationToken); // LSP server doesn't have the pieces yet to support 'balanced' mode for source-generators. Hardcode us to // 'automatic' for now. diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/ExtensionAssemblyManager.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/ExtensionAssemblyManager.cs index d1ca799deec39..7c4b98c63a987 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/ExtensionAssemblyManager.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/ExtensionAssemblyManager.cs @@ -7,6 +7,7 @@ using System.Runtime.Loader; using Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Remote; using Microsoft.Extensions.Logging; namespace Microsoft.CodeAnalysis.LanguageServer.Services; diff --git a/src/VisualStudio/Core/Def/Remote/VisualStudioRemoteHostClientProvider.cs b/src/VisualStudio/Core/Def/Remote/VisualStudioRemoteHostClientProvider.cs index 1f133e65019a9..9ad95766f4178 100644 --- a/src/VisualStudio/Core/Def/Remote/VisualStudioRemoteHostClientProvider.cs +++ b/src/VisualStudio/Core/Def/Remote/VisualStudioRemoteHostClientProvider.cs @@ -17,7 +17,10 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.Settings; +using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.ServiceBroker; +using Microsoft.VisualStudio.Shell.Settings; using Microsoft.VisualStudio.Threading; using Roslyn.Utilities; using VSThreading = Microsoft.VisualStudio.Threading; @@ -31,6 +34,7 @@ internal sealed class Factory : IWorkspaceServiceFactory { private readonly VisualStudioWorkspace _vsWorkspace; private readonly IVsService _brokeredServiceContainer; + private readonly IServiceProvider _serviceProvider; private readonly AsynchronousOperationListenerProvider _listenerProvider; private readonly RemoteServiceCallbackDispatcherRegistry _callbackDispatchers; private readonly IGlobalOptionService _globalOptions; @@ -44,6 +48,7 @@ internal sealed class Factory : IWorkspaceServiceFactory public Factory( VisualStudioWorkspace vsWorkspace, IVsService brokeredServiceContainer, + SVsServiceProvider serviceProvider, AsynchronousOperationListenerProvider listenerProvider, IGlobalOptionService globalOptions, IThreadingContext threadingContext, @@ -52,6 +57,7 @@ public Factory( _globalOptions = globalOptions; _vsWorkspace = vsWorkspace; _brokeredServiceContainer = brokeredServiceContainer; + _serviceProvider = serviceProvider; _listenerProvider = listenerProvider; _threadingContext = threadingContext; _callbackDispatchers = new RemoteServiceCallbackDispatcherRegistry(callbackDispatchers); @@ -79,7 +85,7 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) // If we have a cached vs instance, then we can return that instance since we know they have the same host services. // Otherwise, create and cache an instance based on vs workspace for future callers with same services. if (_cachedVSInstance is null) - _cachedVSInstance = new VisualStudioRemoteHostClientProvider(_vsWorkspace.Services.SolutionServices, _globalOptions, _brokeredServiceContainer, _threadingContext, _listenerProvider, _callbackDispatchers); + _cachedVSInstance = new VisualStudioRemoteHostClientProvider(_vsWorkspace.Services.SolutionServices, _globalOptions, _brokeredServiceContainer, _serviceProvider, _threadingContext, _listenerProvider, _callbackDispatchers); return _cachedVSInstance; } @@ -90,6 +96,7 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) private readonly IGlobalOptionService _globalOptions; private readonly VSThreading.AsyncLazy _lazyClient; private readonly IVsService _brokeredServiceContainer; + private readonly IServiceProvider _serviceProvider; private readonly AsynchronousOperationListenerProvider _listenerProvider; private readonly RemoteServiceCallbackDispatcherRegistry _callbackDispatchers; private readonly TaskCompletionSource _clientCreationSource = new(TaskCreationOptions.RunContinuationsAsynchronously); @@ -98,6 +105,7 @@ private VisualStudioRemoteHostClientProvider( SolutionServices services, IGlobalOptionService globalOptions, IVsService brokeredServiceContainer, + IServiceProvider serviceProvider, IThreadingContext threadingContext, AsynchronousOperationListenerProvider listenerProvider, RemoteServiceCallbackDispatcherRegistry callbackDispatchers) @@ -105,6 +113,7 @@ private VisualStudioRemoteHostClientProvider( Services = services; _globalOptions = globalOptions; _brokeredServiceContainer = brokeredServiceContainer; + _serviceProvider = serviceProvider; _listenerProvider = listenerProvider; _callbackDispatchers = callbackDispatchers; @@ -122,9 +131,10 @@ private VisualStudioRemoteHostClientProvider( var configuration = _globalOptions.GetOption(RemoteHostOptionsStorage.OOPServerGCFeatureFlag) ? RemoteProcessConfiguration.ServerGC : 0; + var localSettingsDirectory = new ShellSettingsManager(_serviceProvider).GetApplicationDataFolder(ApplicationDataFolder.LocalSettings); // VS AsyncLazy does not currently support cancellation: - var client = await ServiceHubRemoteHostClient.CreateAsync(Services, configuration, _listenerProvider, serviceBroker, _callbackDispatchers, CancellationToken.None).ConfigureAwait(false); + var client = await ServiceHubRemoteHostClient.CreateAsync(Services, configuration, localSettingsDirectory, _listenerProvider, serviceBroker, _callbackDispatchers, CancellationToken.None).ConfigureAwait(false); // proffer in-proc brokered services: _ = brokeredServiceContainer.Proffer(SolutionAssetProvider.ServiceDescriptor, (_, _, _, _) => ValueTaskFactory.FromResult(new SolutionAssetProvider(Services))); diff --git a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs index e120228f1ce92..68590264929c9 100644 --- a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs @@ -738,8 +738,8 @@ internal async Task TestSourceGenerationExecution_RegenerateOnEdit( var workspaceConfigurationService = workspace.Services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( - (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, cancellationToken), + var remoteProcessId = await client.TryInvokeAsync( + (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; @@ -822,8 +822,8 @@ internal async Task TestSourceGenerationExecution_MinorVersionChange_NoActualCha var workspaceConfigurationService = workspace.Services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( - (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, cancellationToken), + var remoteProcessId = await client.TryInvokeAsync( + (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; @@ -877,8 +877,8 @@ internal async Task TestSourceGenerationExecution_MajorVersionChange_NoActualCha var workspaceConfigurationService = workspace.Services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( - (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, cancellationToken), + var remoteProcessId = await client.TryInvokeAsync( + (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; diff --git a/src/Workspaces/Core/Portable/Telemetry/IRemoteProcessTelemetryService.cs b/src/Workspaces/Core/Portable/Telemetry/IRemoteProcessTelemetryService.cs index 96df52c92932f..33d7cbf0a34cc 100644 --- a/src/Workspaces/Core/Portable/Telemetry/IRemoteProcessTelemetryService.cs +++ b/src/Workspaces/Core/Portable/Telemetry/IRemoteProcessTelemetryService.cs @@ -2,10 +2,9 @@ // 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 System.Collections.Immutable; -using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; namespace Microsoft.CodeAnalysis.Remote; @@ -21,11 +20,4 @@ internal interface IRemoteProcessTelemetryService /// Initializes telemetry session. /// ValueTask InitializeTelemetrySessionAsync(int hostProcessId, string serializedSession, bool logDelta, CancellationToken cancellationToken); - - /// - /// Sets for the process. - /// Called as soon as the remote process is created but can't guarantee that solution entities (projects, documents, syntax trees) have not been created beforehand. - /// - /// Process ID of the remote process. - ValueTask InitializeAsync(WorkspaceConfigurationOptions options, CancellationToken cancellationToken); } diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs index 095adae521835..2340be874e3f0 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs @@ -1387,8 +1387,8 @@ internal async Task UpdatingAnalyzerReferenceReloadsGenerators( var workspaceConfigurationService = workspace.Services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( - (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options with { SourceGeneratorExecution = executionPreference }, cancellationToken), + var remoteProcessId = await client.TryInvokeAsync( + (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options with { SourceGeneratorExecution = executionPreference }, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; diff --git a/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs b/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs index e9ae4216a379e..ed7653976f276 100644 --- a/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs +++ b/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs @@ -21,7 +21,7 @@ public static class FeaturesTestCompositions // We need to update tests to handle the options correctly before enabling the default provider. public static readonly TestComposition RemoteHost = TestComposition.Empty - .AddAssemblies(RemoteWorkspaceManager.RemoteHostAssemblies) + .AddAssemblies(RemoteExportProvider.RemoteHostAssemblies) .AddParts(typeof(TestSerializerService.Factory)); public static TestComposition WithTestHostParts(this TestComposition composition, TestHost host) diff --git a/src/Workspaces/CoreTestUtilities/Remote/InProcRemostHostClient.cs b/src/Workspaces/CoreTestUtilities/Remote/InProcRemostHostClient.cs index 6800cd6da60b2..60abbcc98f941 100644 --- a/src/Workspaces/CoreTestUtilities/Remote/InProcRemostHostClient.cs +++ b/src/Workspaces/CoreTestUtilities/Remote/InProcRemostHostClient.cs @@ -193,6 +193,7 @@ public InProcRemoteServices(SolutionServices workspaceServices, TraceListener? t RegisterRemoteBrokeredService(new RemoteFindUsagesService.Factory()); RegisterRemoteBrokeredService(new RemoteFullyQualifyService.Factory()); RegisterRemoteBrokeredService(new RemoteInheritanceMarginService.Factory()); + RegisterRemoteBrokeredService(new RemoteInitializationService.Factory()); RegisterRemoteBrokeredService(new RemoteKeepAliveService.Factory()); RegisterRemoteBrokeredService(new RemoteLegacySolutionEventsAggregationService.Factory()); RegisterRemoteBrokeredService(new RemoteMissingImportDiscoveryService.Factory()); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs similarity index 64% rename from src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs rename to src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 63cebc020eaff..83ee2d1fef1db 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -2,89 +2,73 @@ // 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.Generic; using System.Collections.Immutable; +using System.IO; using System.IO.Hashing; +using System.Linq; using System.Text; -using Microsoft.CodeAnalysis.LanguageServer.Logging; -using Microsoft.CodeAnalysis.LanguageServer.Services; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.LanguageServer; +namespace Microsoft.CodeAnalysis.Remote; internal sealed class ExportProviderBuilder { // For testing purposes, track the last cache write task. private static Task? _cacheWriteTask; + private const string CatalogSuffix = ".mef-composition"; public static async Task CreateExportProviderAsync( - ExtensionAssemblyManager extensionManager, - IAssemblyLoader assemblyLoader, - string? devKitDependencyPath, + ImmutableArray assemblyPaths, + Resolver resolver, string cacheDirectory, - ILoggerFactory loggerFactory) + string catalogPrefix, + bool performCleanup, + ILoggerFactory? loggerFactory, + CancellationToken cancellationToken) { // Clear any previous cache write task, so that it is easy to discern whether // a cache write was attempted. _cacheWriteTask = null; - var logger = loggerFactory.CreateLogger(); - var baseDirectory = AppContext.BaseDirectory; - - // Load any Roslyn assemblies from the extension directory - var assemblyPaths = Directory.EnumerateFiles(baseDirectory, "Microsoft.CodeAnalysis*.dll"); - assemblyPaths = assemblyPaths.Concat(Directory.EnumerateFiles(baseDirectory, "Microsoft.ServiceHub*.dll")); - - // DevKit assemblies are not shipped in the main language server folder - // and not included in ExtensionAssemblyPaths (they get loaded into the default ALC). - // So manually add them to the MEF catalog here. - if (devKitDependencyPath != null) - { - assemblyPaths = assemblyPaths.Concat(devKitDependencyPath); - } - - // Add the extension assemblies to the MEF catalog. - assemblyPaths = assemblyPaths.Concat(extensionManager.ExtensionAssemblyPaths); - // Get the cached MEF composition or create a new one. - var exportProviderFactory = await GetCompositionConfigurationAsync([.. assemblyPaths], assemblyLoader, cacheDirectory, logger); + var exportProviderFactory = await GetCompositionConfigurationAsync(assemblyPaths, resolver, cacheDirectory, catalogPrefix, performCleanup, loggerFactory, cancellationToken).ConfigureAwait(false); // Create an export provider, which represents a unique container of values. // You can create as many of these as you want, but typically an app needs just one. var exportProvider = exportProviderFactory.CreateExportProvider(); - // Immediately set the logger factory, so that way it'll be available for the rest of the composition - exportProvider.GetExportedValue().SetFactory(loggerFactory); - - // Also add the ExtensionAssemblyManager so it will be available for the rest of the composition. - exportProvider.GetExportedValue().SetMefExtensionAssemblyManager(extensionManager); - return exportProvider; } private static async Task GetCompositionConfigurationAsync( ImmutableArray assemblyPaths, - IAssemblyLoader assemblyLoader, + Resolver resolver, string cacheDirectory, - ILogger logger) + string catalogPrefix, + bool performCleanup, + ILoggerFactory? loggerFactory, + CancellationToken cancellationToken) { - // Create a MEF resolver that can resolve assemblies in the extension contexts. - var resolver = new Resolver(assemblyLoader); - - var compositionCacheFile = GetCompositionCacheFilePath(cacheDirectory, assemblyPaths); + var logger = loggerFactory?.CreateLogger(); + var compositionCacheFile = GetCompositionCacheFilePath(cacheDirectory, catalogPrefix, assemblyPaths); // Try to load a cached composition. try { if (File.Exists(compositionCacheFile)) { - logger.LogTrace($"Loading cached MEF catalog: {compositionCacheFile}"); + logger?.LogTrace($"Loading cached MEF catalog: {compositionCacheFile}"); CachedComposition cachedComposition = new(); using FileStream cacheStream = new(compositionCacheFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true); - var exportProviderFactory = await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, resolver); + var exportProviderFactory = await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, resolver, cancellationToken).ConfigureAwait(false); return exportProviderFactory; } @@ -92,18 +76,19 @@ private static async Task GetCompositionConfigurationAsy catch (Exception ex) { // Log the error, and move on to recover by recreating the MEF composition. - logger.LogError($"Loading cached MEF composition failed: {ex}"); + logger?.LogError($"Loading cached MEF composition failed: {ex}"); } - logger.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", assemblyPaths)}."); + logger?.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", assemblyPaths)}."); var discovery = PartDiscovery.Combine( resolver, new AttributedPartDiscovery(resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) new AttributedPartDiscoveryV1(resolver)); + var parts = await discovery.CreatePartsAsync(assemblyPaths, progress: null, cancellationToken).ConfigureAwait(false); var catalog = ComposableCatalog.Create(resolver) - .AddParts(await discovery.CreatePartsAsync(assemblyPaths)) + .AddParts(parts) .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import // Assemble the parts into a valid graph. @@ -113,13 +98,13 @@ private static async Task GetCompositionConfigurationAsy ThrowOnUnexpectedErrors(config, catalog, logger); // Try to cache the composition. - _cacheWriteTask = WriteCompositionCacheAsync(compositionCacheFile, config, logger).ReportNonFatalErrorAsync(); + _cacheWriteTask = WriteCompositionCacheAsync(compositionCacheFile, config, performCleanup, logger, cancellationToken).ReportNonFatalErrorAsync(); // Prepare an ExportProvider factory based on this graph. return config.CreateExportProviderFactory(); } - private static string GetCompositionCacheFilePath(string cacheDirectory, ImmutableArray assemblyPaths) + private static string GetCompositionCacheFilePath(string cacheDirectory, string catalogPrefix, ImmutableArray assemblyPaths) { // This should vary based on .NET runtime major version so that as some of our processes switch between our target // .NET version and the user's selected SDK runtime version (which may be newer), the MEF cache is kept isolated. @@ -127,7 +112,7 @@ private static string GetCompositionCacheFilePath(string cacheDirectory, Immutab // we might be running on .NET 7 or .NET 8, depending on the particular session and user settings. var cacheSubdirectory = $".NET {Environment.Version.Major}"; - return Path.Combine(cacheDirectory, cacheSubdirectory, $"c#-languageserver.{ComputeAssemblyHash(assemblyPaths)}.mef-composition"); + return Path.Combine(cacheDirectory, cacheSubdirectory, $"{catalogPrefix}.{ComputeAssemblyHash(assemblyPaths)}{CatalogSuffix}"); static string ComputeAssemblyHash(ImmutableArray assemblyPaths) { @@ -151,7 +136,7 @@ static string ComputeAssemblyHash(ImmutableArray assemblyPaths) } } - private static async Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, ILogger logger) + private static async Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, bool performCleanup, ILogger? logger, CancellationToken cancellationToken) { try { @@ -159,36 +144,44 @@ private static async Task WriteCompositionCacheAsync(string compositionCacheFile if (Path.GetDirectoryName(compositionCacheFile) is string directory) { - Directory.CreateDirectory(directory); + var directoryInfo = Directory.CreateDirectory(directory); + + if (performCleanup) + { + // Delete any existing cached files. + foreach (var fileInfo in directoryInfo.EnumerateFiles($"*{CatalogSuffix}")) + fileInfo.Delete(); + } } CachedComposition cachedComposition = new(); var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); using (FileStream cacheStream = new(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true)) { - await cachedComposition.SaveAsync(config, cacheStream); + await cachedComposition.SaveAsync(config, cacheStream, cancellationToken).ConfigureAwait(false); } +#if NET File.Move(tempFilePath, compositionCacheFile, overwrite: true); +#else + // On .NET Framework, File.Move doesn't support overwriting the destination file. Use File.Delete first + // to ensure the destination file is removed before moving the temp file. File.Delete will not throw if + // the file doesn't exist. + File.Delete(compositionCacheFile); + File.Move(tempFilePath, compositionCacheFile); +#endif } catch (Exception ex) { - logger.LogError($"Failed to save MEF cache: {ex}"); + logger?.LogError($"Failed to save MEF cache: {ex}"); } } - private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog, ILogger logger) + private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog, ILogger? logger) { // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. - // Currently we are expecting the following: - // "----- CompositionError level 1 ------ - // Microsoft.CodeAnalysis.ExternalAccess.Pythia.PythiaSignatureHelpProvider.ctor(implementation): expected exactly 1 export matching constraints: - // Contract name: Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api.IPythiaSignatureHelpProviderImplementation - // TypeIdentityName: Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api.IPythiaSignatureHelpProviderImplementation - // but found 0. - // part definition Microsoft.CodeAnalysis.ExternalAccess.Pythia.PythiaSignatureHelpProvider var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; - var expectedErroredParts = new string[] { "PythiaSignatureHelpProvider" }; + var expectedErroredParts = new HashSet(["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory"]); var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErroredParts.Contains(part)); if (hasUnexpectedErroredParts || !catalog.DiscoveredParts.DiscoveryErrors.IsEmpty) @@ -201,7 +194,7 @@ private static void ThrowOnUnexpectedErrors(CompositionConfiguration configurati catch (CompositionFailedException ex) { // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately - logger.LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); + logger?.LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); throw; } } diff --git a/src/Workspaces/Remote/Core/IRemoteInitializationService.cs b/src/Workspaces/Remote/Core/IRemoteInitializationService.cs new file mode 100644 index 0000000000000..f0ccd22e187d9 --- /dev/null +++ b/src/Workspaces/Remote/Core/IRemoteInitializationService.cs @@ -0,0 +1,19 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Remote; + +internal interface IRemoteInitializationService +{ + /// + /// Initializes values including for the process. + /// Called as soon as the remote process is created but can't guarantee that solution entities (projects, documents, syntax trees) have not been created beforehand. + /// + /// Process ID of the remote process. + ValueTask InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj index 53dfd7ec0fd75..5252a61cb3c47 100644 --- a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj +++ b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj @@ -24,6 +24,7 @@ + diff --git a/src/Workspaces/Remote/Core/RemoteWorkspacesResources.resx b/src/Workspaces/Remote/Core/RemoteWorkspacesResources.resx index d7a0780f01053..4aef0ad539f80 100644 --- a/src/Workspaces/Remote/Core/RemoteWorkspacesResources.resx +++ b/src/Workspaces/Remote/Core/RemoteWorkspacesResources.resx @@ -237,4 +237,7 @@ Extension message handler + + Initialization + \ No newline at end of file diff --git a/src/Workspaces/Remote/Core/ServiceDescriptors.cs b/src/Workspaces/Remote/Core/ServiceDescriptors.cs index 376fe434d2b9f..b2e113683a662 100644 --- a/src/Workspaces/Remote/Core/ServiceDescriptors.cs +++ b/src/Workspaces/Remote/Core/ServiceDescriptors.cs @@ -75,6 +75,7 @@ internal sealed class ServiceDescriptors (typeof(IRemoteNavigateToSearchService), typeof(IRemoteNavigateToSearchService.ICallback)), (typeof(IRemoteNavigationBarItemService), null), (typeof(IRemoteProcessTelemetryService), null), + (typeof(IRemoteInitializationService), null), (typeof(IRemoteRelatedDocumentsService), typeof(IRemoteRelatedDocumentsService.ICallback)), (typeof(IRemoteRenamerService), null), (typeof(IRemoteSemanticClassificationService), null), diff --git a/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs b/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs index 18f00af2c8e7a..32ed8c5366c3f 100644 --- a/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs +++ b/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs @@ -54,6 +54,7 @@ private ServiceHubRemoteHostClient( public static async Task CreateAsync( SolutionServices services, RemoteProcessConfiguration configuration, + string localSettingsDirectory, AsynchronousOperationListenerProvider listenerProvider, IServiceBroker serviceBroker, RemoteServiceCallbackDispatcherRegistry callbackDispatchers, @@ -72,8 +73,8 @@ public static async Task CreateAsync( var workspaceConfigurationService = services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( - (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, cancellationToken), + var remoteProcessId = await client.TryInvokeAsync( + (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, localSettingsDirectory, cancellationToken), cancellationToken).ConfigureAwait(false); if (remoteProcessId.HasValue) diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.cs.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.cs.xlf index c13b28a824e53..e0cf716580e3b 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.cs.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.cs.xlf @@ -87,6 +87,11 @@ Míra dědičnosti + + Initialization + Initialization + + Keep alive service Služba responzivity diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.de.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.de.xlf index 7d6b8a5105019..c61e7c1749e13 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.de.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.de.xlf @@ -87,6 +87,11 @@ Vererbungsrand + + Initialization + Initialization + + Keep alive service Keepalive-Dienst diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.es.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.es.xlf index 2d89bd4c37442..a5cc2d538350a 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.es.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.es.xlf @@ -87,6 +87,11 @@ Margen de herencia + + Initialization + Initialization + + Keep alive service Mantener el servicio activo diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.fr.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.fr.xlf index 8909739999915..f989563604567 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.fr.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.fr.xlf @@ -87,6 +87,11 @@ Marge d’héritage + + Initialization + Initialization + + Keep alive service Maintenir le service actif diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.it.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.it.xlf index 66074ff323f59..149c0bb4324a9 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.it.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.it.xlf @@ -87,6 +87,11 @@ Margine di ereditarietà + + Initialization + Initialization + + Keep alive service Servizio keep-alive diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ja.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ja.xlf index 9bd73d55d6e0a..282462e1a440c 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ja.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ja.xlf @@ -87,6 +87,11 @@ 継承の余白 + + Initialization + Initialization + + Keep alive service キープ アライブ サービス diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ko.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ko.xlf index df2ec3c901330..85c6ac95dfffc 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ko.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ko.xlf @@ -87,6 +87,11 @@ 상속 여백 + + Initialization + Initialization + + Keep alive service 서비스 연결 유지 diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pl.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pl.xlf index d034f1c2c3b99..3e34ecd0bb45b 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pl.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pl.xlf @@ -87,6 +87,11 @@ Margines dziedziczenia + + Initialization + Initialization + + Keep alive service Utrzymywanie aktywności usługi diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pt-BR.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pt-BR.xlf index 0371b6a72d63e..d13db956d4483 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pt-BR.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pt-BR.xlf @@ -87,6 +87,11 @@ Margem de herança + + Initialization + Initialization + + Keep alive service Serviço Keep alive diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ru.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ru.xlf index 5fe0ce251989c..7ca04d5b162a3 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ru.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ru.xlf @@ -87,6 +87,11 @@ Граница наследования + + Initialization + Initialization + + Keep alive service Служба проверки активности diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.tr.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.tr.xlf index c162e57f0d35e..c9089a3fa2f88 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.tr.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.tr.xlf @@ -87,6 +87,11 @@ Devralma boşluğu + + Initialization + Initialization + + Keep alive service Etkin tutma hizmeti diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hans.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hans.xlf index 9d1e91d5f58aa..290651aa5bd14 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hans.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hans.xlf @@ -87,6 +87,11 @@ 继承边距 + + Initialization + Initialization + + Keep alive service 保持活动状态的服务 diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hant.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hant.xlf index b155db43b50a5..ead886b90f311 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hant.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hant.xlf @@ -87,6 +87,11 @@ 繼承邊界 + + Initialization + Initialization + + Keep alive service 保持運作服務 diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs new file mode 100644 index 0000000000000..d3c34e8e16242 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs @@ -0,0 +1,69 @@ +// 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.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.Internal.EmbeddedLanguages; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Composition; + +namespace Microsoft.CodeAnalysis.Remote; + +internal class RemoteExportProvider +{ + internal static readonly ImmutableArray RemoteHostAssemblies = + MefHostServices.DefaultAssemblies + .Add(typeof(AspNetCoreEmbeddedLanguageClassifier).Assembly) + .Add(typeof(BrokeredServiceBase).Assembly) + .Add(typeof(IRazorLanguageServerTarget).Assembly) + .Add(typeof(RemoteWorkspacesResources).Assembly) + .Add(typeof(IExtensionWorkspaceMessageHandler<,>).Assembly); + + private static ExportProvider? s_instance; + internal static ExportProvider ExportProvider + { + get + { + Contract.ThrowIfNull(s_instance, "Default export provider not initialized. Call InitializeAsync first."); + return s_instance; + } + } + + public static async Task InitializeAsync(string localSettingsDirectory, CancellationToken cancellationToken) + { + var resolver = new Resolver(SimpleAssemblyLoader.Instance); + var assemblyPaths = RemoteHostAssemblies.SelectAsArray(static a => a.Location); + + var cacheDirectory = Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"); + var catalogPrefix = "RoslynRemoteHost"; + + s_instance = await ExportProviderBuilder.CreateExportProviderAsync(assemblyPaths, resolver, cacheDirectory, catalogPrefix, performCleanup: true, loggerFactory: null, cancellationToken).ConfigureAwait(false); + } + + private sealed class SimpleAssemblyLoader : IAssemblyLoader + { + public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader(); + + public Assembly LoadAssembly(AssemblyName assemblyName) + => Assembly.Load(assemblyName); + + public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath) + { + var assemblyName = new AssemblyName(assemblyFullName); + if (!string.IsNullOrEmpty(codeBasePath)) + { +#pragma warning disable SYSLIB0044 // https://github.com/dotnet/roslyn/issues/71510 + assemblyName.CodeBase = codeBasePath; +#pragma warning restore SYSLIB0044 + } + + return LoadAssembly(assemblyName); + } + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs index 274e260b057f2..a754e7b5a6a87 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs @@ -3,16 +3,10 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Immutable; -using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Extensions; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.Internal.EmbeddedLanguages; -using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.ServiceHub.Framework; -using Microsoft.VisualStudio.Composition; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Remote; @@ -23,14 +17,6 @@ namespace Microsoft.CodeAnalysis.Remote; /// internal class RemoteWorkspaceManager { - internal static readonly ImmutableArray RemoteHostAssemblies = - MefHostServices.DefaultAssemblies - .Add(typeof(AspNetCoreEmbeddedLanguageClassifier).Assembly) - .Add(typeof(BrokeredServiceBase).Assembly) - .Add(typeof(IRazorLanguageServerTarget).Assembly) - .Add(typeof(RemoteWorkspacesResources).Assembly) - .Add(typeof(IExtensionWorkspaceMessageHandler<,>).Assembly); - /// /// Default workspace manager used by the product. Tests may specify a custom in order to override workspace services. @@ -78,27 +64,9 @@ public RemoteWorkspaceManager( SolutionAssetCache = createAssetCache(workspace); } - private static ComposableCatalog CreateCatalog(ImmutableArray assemblies) - { - var resolver = new Resolver(SimpleAssemblyLoader.Instance); - var discovery = new AttributedPartDiscovery(resolver, isNonPublicSupported: true); - var parts = Task.Run(async () => await discovery.CreatePartsAsync(assemblies).ConfigureAwait(false)).GetAwaiter().GetResult(); - return ComposableCatalog.Create(resolver).AddParts(parts); - } - - private static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog) - { - var configuration = CompositionConfiguration.Create(catalog); - var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration); - return runtimeComposition.CreateExportProviderFactory(); - } - private static RemoteWorkspace CreatePrimaryWorkspace() { - var catalog = CreateCatalog(RemoteHostAssemblies); - var exportProviderFactory = CreateExportProviderFactory(catalog); - var exportProvider = exportProviderFactory.CreateExportProvider(); - + var exportProvider = RemoteExportProvider.ExportProvider; return new RemoteWorkspace(VisualStudioMefHostServices.Create(exportProvider)); } @@ -143,25 +111,4 @@ public async ValueTask RunServiceAsync( return result; } - - private sealed class SimpleAssemblyLoader : IAssemblyLoader - { - public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader(); - - public Assembly LoadAssembly(AssemblyName assemblyName) - => Assembly.Load(assemblyName); - - public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath) - { - var assemblyName = new AssemblyName(assemblyFullName); - if (!string.IsNullOrEmpty(codeBasePath)) - { -#pragma warning disable SYSLIB0044 // https://github.com/dotnet/roslyn/issues/71510 - assemblyName.CodeBase = codeBasePath; -#pragma warning restore SYSLIB0044 - } - - return LoadAssembly(assemblyName); - } - } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.cs b/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.cs index 27d5a0a2ebb47..b7d92866bf9b7 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/BrokeredServiceBase.cs @@ -23,7 +23,8 @@ namespace Microsoft.CodeAnalysis.Remote; internal abstract partial class BrokeredServiceBase : IDisposable { protected readonly TraceSource TraceLogger; - protected readonly RemoteWorkspaceManager WorkspaceManager; + protected RemoteWorkspaceManager WorkspaceManager + => TestData?.WorkspaceManager ?? RemoteWorkspaceManager.Default; protected readonly SolutionAssetSource SolutionAssetSource; protected readonly ServiceBrokerClient ServiceBrokerClient; @@ -60,7 +61,6 @@ protected BrokeredServiceBase(in ServiceConstructionArguments arguments) TraceLogger = traceSource; TestData = (RemoteHostTestData?)arguments.ServiceProvider.GetService(typeof(RemoteHostTestData)); - WorkspaceManager = TestData?.WorkspaceManager ?? RemoteWorkspaceManager.Default; #pragma warning disable VSTHRD012 // Provide JoinableTaskFactory where allowed ServiceBrokerClient = new ServiceBrokerClient(arguments.ServiceBroker); diff --git a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs new file mode 100644 index 0000000000000..7bd2a6f86251a --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs @@ -0,0 +1,38 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Remote; + +internal sealed class RemoteInitializationService( + BrokeredServiceBase.ServiceConstructionArguments arguments) + : BrokeredServiceBase(arguments), IRemoteInitializationService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteInitializationService CreateService(in ServiceConstructionArguments arguments) + => new RemoteInitializationService(arguments); + } + + /// + /// Remote API. + /// + public async ValueTask InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken) + { + await RemoteExportProvider.InitializeAsync(localSettingsDirectory, cancellationToken).ConfigureAwait(false); + + return await RunServiceAsync(cancellationToken => + { + var service = (RemoteWorkspaceConfigurationService)GetWorkspaceServices().GetRequiredService(); + service.InitializeOptions(options); + + return ValueTaskFactory.FromResult(Process.GetCurrentProcess().Id); + }, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Services/ProcessTelemetry/RemoteProcessTelemetryService.cs b/src/Workspaces/Remote/ServiceHub/Services/ProcessTelemetry/RemoteProcessTelemetryService.cs index 5f29640ef50e3..c9e4f97237eb2 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/ProcessTelemetry/RemoteProcessTelemetryService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/ProcessTelemetry/RemoteProcessTelemetryService.cs @@ -5,12 +5,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Remote.Diagnostics; using Microsoft.CodeAnalysis.Telemetry; @@ -102,18 +100,4 @@ private static void SetRoslynLogger(ImmutableArray loggerTypes, Func< RoslynLogger.SetLogger(AggregateLogger.Remove(RoslynLogger.GetLogger(), l => l is T)); } } - - /// - /// Remote API. - /// - public ValueTask InitializeAsync(WorkspaceConfigurationOptions options, CancellationToken cancellationToken) - { - return RunServiceAsync(cancellationToken => - { - var service = (RemoteWorkspaceConfigurationService)GetWorkspaceServices().GetRequiredService(); - service.InitializeOptions(options); - - return ValueTaskFactory.FromResult(Process.GetCurrentProcess().Id); - }, cancellationToken); - } } From f1dcf0ae93a019aa67afecf9fb67a4a602b5a36d Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Fri, 11 Apr 2025 12:58:02 -0700 Subject: [PATCH 02/20] 1) Get rid of MS.Extensions.Logging dependency 2) Create arguments record instead of adding more parameters to various methods --- .../LanguageServerExportProviderBuilder.cs | 16 ++++- .../Remote/Core/ExportProviderBuilder.cs | 65 +++++++++---------- ...soft.CodeAnalysis.Remote.Workspaces.csproj | 1 - .../ServiceHub/Host/RemoteExportProvider.cs | 16 +++-- 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index d0787093bbf23..5521641faa3b8 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -39,10 +39,20 @@ public static async Task CreateExportProviderAsync( // Add the extension assemblies to the MEF catalog. assemblyPaths = assemblyPaths.Concat(extensionManager.ExtensionAssemblyPaths); + var logger = loggerFactory.CreateLogger(); + // Create a MEF resolver that can resolve assemblies in the extension contexts. - var resolver = new Resolver(assemblyLoader); - var catalogPrefix = "c#-languageserver"; - var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(assemblyPaths.ToImmutableArray(), resolver, cacheDirectory, catalogPrefix, performCleanup: false, loggerFactory, cancellationToken); + var args = new ExportProviderBuilder.ExportProviderCreationArguments( + AssemblyPaths: assemblyPaths.ToImmutableArray(), + Resolver: new Resolver(assemblyLoader), + CacheDirectory: cacheDirectory, + CatalogPrefix: "c#-languageserver", + ExpectedErrorParts: ["PythiaSignatureHelpProvider"], + PerformCleanup: false, + LogError: text => logger.LogError(text), + LogTrace: text => logger.LogTrace(text)); + + var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(args, cancellationToken); // Also add the ExtensionAssemblyManager so it will be available for the rest of the composition. exportProvider.GetExportedValue().SetMefExtensionAssemblyManager(extensionManager); diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 83ee2d1fef1db..2bfb62000ab88 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.IO.Hashing; @@ -12,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Collections; -using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; using Roslyn.Utilities; @@ -20,17 +18,22 @@ namespace Microsoft.CodeAnalysis.Remote; internal sealed class ExportProviderBuilder { + public record ExportProviderCreationArguments( + ImmutableArray AssemblyPaths, + Resolver Resolver, + string CacheDirectory, + string CatalogPrefix, + ImmutableArray ExpectedErrorParts, + bool PerformCleanup, + Action LogError, + Action LogTrace); + // For testing purposes, track the last cache write task. private static Task? _cacheWriteTask; private const string CatalogSuffix = ".mef-composition"; public static async Task CreateExportProviderAsync( - ImmutableArray assemblyPaths, - Resolver resolver, - string cacheDirectory, - string catalogPrefix, - bool performCleanup, - ILoggerFactory? loggerFactory, + ExportProviderCreationArguments args, CancellationToken cancellationToken) { // Clear any previous cache write task, so that it is easy to discern whether @@ -38,7 +41,7 @@ public static async Task CreateExportProviderAsync( _cacheWriteTask = null; // Get the cached MEF composition or create a new one. - var exportProviderFactory = await GetCompositionConfigurationAsync(assemblyPaths, resolver, cacheDirectory, catalogPrefix, performCleanup, loggerFactory, cancellationToken).ConfigureAwait(false); + var exportProviderFactory = await GetCompositionConfigurationAsync(args, cancellationToken).ConfigureAwait(false); // Create an export provider, which represents a unique container of values. // You can create as many of these as you want, but typically an app needs just one. @@ -48,27 +51,21 @@ public static async Task CreateExportProviderAsync( } private static async Task GetCompositionConfigurationAsync( - ImmutableArray assemblyPaths, - Resolver resolver, - string cacheDirectory, - string catalogPrefix, - bool performCleanup, - ILoggerFactory? loggerFactory, + ExportProviderCreationArguments args, CancellationToken cancellationToken) { - var logger = loggerFactory?.CreateLogger(); - var compositionCacheFile = GetCompositionCacheFilePath(cacheDirectory, catalogPrefix, assemblyPaths); + var compositionCacheFile = GetCompositionCacheFilePath(args.CacheDirectory, args.CatalogPrefix, args.AssemblyPaths); // Try to load a cached composition. try { if (File.Exists(compositionCacheFile)) { - logger?.LogTrace($"Loading cached MEF catalog: {compositionCacheFile}"); + args.LogTrace($"Loading cached MEF catalog: {compositionCacheFile}"); CachedComposition cachedComposition = new(); using FileStream cacheStream = new(compositionCacheFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true); - var exportProviderFactory = await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, resolver, cancellationToken).ConfigureAwait(false); + var exportProviderFactory = await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, args.Resolver, cancellationToken).ConfigureAwait(false); return exportProviderFactory; } @@ -76,18 +73,18 @@ private static async Task GetCompositionConfigurationAsy catch (Exception ex) { // Log the error, and move on to recover by recreating the MEF composition. - logger?.LogError($"Loading cached MEF composition failed: {ex}"); + args.LogError($"Loading cached MEF composition failed: {ex}"); } - logger?.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", assemblyPaths)}."); + args.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", args.AssemblyPaths)}."); var discovery = PartDiscovery.Combine( - resolver, - new AttributedPartDiscovery(resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) - new AttributedPartDiscoveryV1(resolver)); + args.Resolver, + new AttributedPartDiscovery(args.Resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) + new AttributedPartDiscoveryV1(args.Resolver)); - var parts = await discovery.CreatePartsAsync(assemblyPaths, progress: null, cancellationToken).ConfigureAwait(false); - var catalog = ComposableCatalog.Create(resolver) + var parts = await discovery.CreatePartsAsync(args.AssemblyPaths, progress: null, cancellationToken).ConfigureAwait(false); + var catalog = ComposableCatalog.Create(args.Resolver) .AddParts(parts) .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import @@ -95,10 +92,10 @@ private static async Task GetCompositionConfigurationAsy var config = CompositionConfiguration.Create(catalog); // Verify we only have expected errors. - ThrowOnUnexpectedErrors(config, catalog, logger); + ThrowOnUnexpectedErrors(config, catalog, args.ExpectedErrorParts, args.LogError); // Try to cache the composition. - _cacheWriteTask = WriteCompositionCacheAsync(compositionCacheFile, config, performCleanup, logger, cancellationToken).ReportNonFatalErrorAsync(); + _cacheWriteTask = WriteCompositionCacheAsync(compositionCacheFile, config, args.PerformCleanup, args.LogError, cancellationToken).ReportNonFatalErrorAsync(); // Prepare an ExportProvider factory based on this graph. return config.CreateExportProviderFactory(); @@ -136,7 +133,7 @@ static string ComputeAssemblyHash(ImmutableArray assemblyPaths) } } - private static async Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, bool performCleanup, ILogger? logger, CancellationToken cancellationToken) + private static async Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, bool performCleanup, Action logError, CancellationToken cancellationToken) { try { @@ -173,16 +170,16 @@ private static async Task WriteCompositionCacheAsync(string compositionCacheFile } catch (Exception ex) { - logger?.LogError($"Failed to save MEF cache: {ex}"); + logError($"Failed to save MEF cache: {ex}"); } } - private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog, ILogger? logger) + private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog, ImmutableArray expectedErrorParts, Action logError) { // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; - var expectedErroredParts = new HashSet(["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory"]); - var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErroredParts.Contains(part)); + var expectedErrorPartsSet = expectedErrorParts.ToSet(); + var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); if (hasUnexpectedErroredParts || !catalog.DiscoveredParts.DiscoveryErrors.IsEmpty) { @@ -194,7 +191,7 @@ private static void ThrowOnUnexpectedErrors(CompositionConfiguration configurati catch (CompositionFailedException ex) { // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately - logger?.LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); + logError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); throw; } } diff --git a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj index 5252a61cb3c47..53dfd7ec0fd75 100644 --- a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj +++ b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj @@ -24,7 +24,6 @@ - diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs index d3c34e8e16242..62409e9b59170 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs @@ -37,13 +37,17 @@ internal static ExportProvider ExportProvider public static async Task InitializeAsync(string localSettingsDirectory, CancellationToken cancellationToken) { - var resolver = new Resolver(SimpleAssemblyLoader.Instance); - var assemblyPaths = RemoteHostAssemblies.SelectAsArray(static a => a.Location); + var args = new ExportProviderBuilder.ExportProviderCreationArguments( + AssemblyPaths: RemoteHostAssemblies.SelectAsArray(static a => a.Location), + Resolver: new Resolver(SimpleAssemblyLoader.Instance), + CacheDirectory: Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"), + CatalogPrefix: "RoslynRemoteHost", + ExpectedErrorParts: ["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory"], + PerformCleanup: true, + LogError: _ => { }, + LogTrace: _ => { }); - var cacheDirectory = Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"); - var catalogPrefix = "RoslynRemoteHost"; - - s_instance = await ExportProviderBuilder.CreateExportProviderAsync(assemblyPaths, resolver, cacheDirectory, catalogPrefix, performCleanup: true, loggerFactory: null, cancellationToken).ConfigureAwait(false); + s_instance = await ExportProviderBuilder.CreateExportProviderAsync(args, cancellationToken).ConfigureAwait(false); } private sealed class SimpleAssemblyLoader : IAssemblyLoader From 9ff80c08190f98f328632786cffc98aa4471abe3 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Fri, 11 Apr 2025 13:18:21 -0700 Subject: [PATCH 03/20] Check cancellation token after doing expensive operation --- src/Workspaces/Remote/Core/ExportProviderBuilder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 2bfb62000ab88..bca295320ded1 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -147,7 +147,10 @@ private static async Task WriteCompositionCacheAsync(string compositionCacheFile { // Delete any existing cached files. foreach (var fileInfo in directoryInfo.EnumerateFiles($"*{CatalogSuffix}")) + { fileInfo.Delete(); + cancellationToken.ThrowIfCancellationRequested(); + } } } From 17d73d7a3e1f2cfc74d4100ce626c3bb9f62e049 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Fri, 11 Apr 2025 14:18:38 -0700 Subject: [PATCH 04/20] Include the Env version in the hash, not in the path --- .../Remote/Core/ExportProviderBuilder.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index bca295320ded1..5058a765ff090 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -103,31 +103,32 @@ private static async Task GetCompositionConfigurationAsy private static string GetCompositionCacheFilePath(string cacheDirectory, string catalogPrefix, ImmutableArray assemblyPaths) { - // This should vary based on .NET runtime major version so that as some of our processes switch between our target - // .NET version and the user's selected SDK runtime version (which may be newer), the MEF cache is kept isolated. - // This can be important when the MEF catalog records full assembly names such as "System.Runtime, 8.0.0.0" yet - // we might be running on .NET 7 or .NET 8, depending on the particular session and user settings. - var cacheSubdirectory = $".NET {Environment.Version.Major}"; - - return Path.Combine(cacheDirectory, cacheSubdirectory, $"{catalogPrefix}.{ComputeAssemblyHash(assemblyPaths)}{CatalogSuffix}"); + return Path.Combine(cacheDirectory, $"{catalogPrefix}.{ComputeAssemblyHash(assemblyPaths)}{CatalogSuffix}"); static string ComputeAssemblyHash(ImmutableArray assemblyPaths) { // Ensure AssemblyPaths are always in the same order. assemblyPaths = assemblyPaths.Sort(); - var assemblies = new StringBuilder(); + var hashContents = new StringBuilder(); + + // This should vary based on .NET runtime major version so that as some of our processes switch between our target + // .NET version and the user's selected SDK runtime version (which may be newer), the MEF cache is kept isolated. + // This can be important when the MEF catalog records full assembly names such as "System.Runtime, 8.0.0.0" yet + // we might be running on .NET 7 or .NET 8, depending on the particular session and user settings. + hashContents.Append(Environment.Version.Major); + foreach (var assemblyPath in assemblyPaths) { // Include assembly path in the hash so that changes to the set of included // assemblies cause the composition to be rebuilt. - assemblies.Append(assemblyPath); + hashContents.Append(assemblyPath); // Include the last write time in the hash so that newer assemblies written // to the same location cause the composition to be rebuilt. - assemblies.Append(File.GetLastWriteTimeUtc(assemblyPath).ToString("F")); + hashContents.Append(File.GetLastWriteTimeUtc(assemblyPath).ToString("F")); } - var hash = XxHash128.Hash(Encoding.UTF8.GetBytes(assemblies.ToString())); + var hash = XxHash128.Hash(Encoding.UTF8.GetBytes(hashContents.ToString())); // Convert to filename safe base64 string. return Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').TrimEnd('='); } From dbfb724485ee1895cd29bc867cc4702525d54e8d Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Fri, 11 Apr 2025 15:14:24 -0700 Subject: [PATCH 05/20] Remove unused variable --- .../LanguageServerExportProviderBuilder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index 5521641faa3b8..0c310f18f6846 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -57,8 +57,6 @@ public static async Task CreateExportProviderAsync( // Also add the ExtensionAssemblyManager so it will be available for the rest of the composition. exportProvider.GetExportedValue().SetMefExtensionAssemblyManager(extensionManager); - var exportProviderBuilderLogger = loggerFactory.CreateLogger(); - // Immediately set the logger factory, so that way it'll be available for the rest of the composition exportProvider.GetExportedValue().SetFactory(loggerFactory); From 7103a4f0eaca43833710d40e675d30ca8b4542c9 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 12 Apr 2025 07:38:10 -0700 Subject: [PATCH 06/20] fix a unit test issue switch to record struct for args encapsulation --- .../LanguageServerExportProviderBuilder.cs | 2 +- src/Workspaces/Remote/Core/ExportProviderBuilder.cs | 2 +- .../Remote/ServiceHub/Host/RemoteExportProvider.cs | 6 +++--- .../Remote/ServiceHub/Host/RemoteWorkspaceManager.cs | 11 +++++++++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index 0c310f18f6846..c6fd619ba082a 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer; -internal class LanguageServerExportProviderBuilder +internal sealed class LanguageServerExportProviderBuilder { public static async Task CreateExportProviderAsync( ExtensionAssemblyManager extensionManager, diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 5058a765ff090..a6d3536ac32f4 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -18,7 +18,7 @@ namespace Microsoft.CodeAnalysis.Remote; internal sealed class ExportProviderBuilder { - public record ExportProviderCreationArguments( + public record struct ExportProviderCreationArguments( ImmutableArray AssemblyPaths, Resolver Resolver, string CacheDirectory, diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs index 62409e9b59170..0bd46581c6dd1 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Remote; -internal class RemoteExportProvider +internal static class RemoteExportProvider { internal static readonly ImmutableArray RemoteHostAssemblies = MefHostServices.DefaultAssemblies @@ -44,8 +44,8 @@ public static async Task InitializeAsync(string localSettingsDirectory, Cancella CatalogPrefix: "RoslynRemoteHost", ExpectedErrorParts: ["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory"], PerformCleanup: true, - LogError: _ => { }, - LogTrace: _ => { }); + LogError: static _ => { }, + LogTrace: static _ => { }); s_instance = await ExportProviderBuilder.CreateExportProviderAsync(args, cancellationToken).ConfigureAwait(false); } diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs index a754e7b5a6a87..27f30ada7ceb2 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs @@ -45,8 +45,15 @@ internal class RemoteWorkspaceManager /// /// /// - internal static readonly RemoteWorkspaceManager Default = new( - workspace => new SolutionAssetCache(workspace, cleanupInterval: TimeSpan.FromSeconds(30), purgeAfter: TimeSpan.FromMinutes(1))); + private static readonly Lazy s_default = new(static () => + { + return new RemoteWorkspaceManager(CreateAssetCache); + + static SolutionAssetCache CreateAssetCache(RemoteWorkspace workspace) + => new SolutionAssetCache(workspace, cleanupInterval: TimeSpan.FromSeconds(30), purgeAfter: TimeSpan.FromMinutes(1)); + }); + + internal static RemoteWorkspaceManager Default => s_default.Value; private readonly RemoteWorkspace _workspace; internal readonly SolutionAssetCache SolutionAssetCache; From 0014f71932a2cb245d9f96f770f3f6c8bd2a7074 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Tue, 15 Apr 2025 09:53:25 -0700 Subject: [PATCH 07/20] 1) Properly fix merge conflict 2) Add new expected error'ed type in CA process MEF composition --- .../LanguageServerExportProviderBuilder.cs | 2 +- src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index c6fd619ba082a..ffd31ce4180bc 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -47,7 +47,7 @@ public static async Task CreateExportProviderAsync( Resolver: new Resolver(assemblyLoader), CacheDirectory: cacheDirectory, CatalogPrefix: "c#-languageserver", - ExpectedErrorParts: ["PythiaSignatureHelpProvider"], + ExpectedErrorParts: ["CSharpMapCodeService", "PythiaSignatureHelpProvider", "CopilotSemanticSearchQueryExecutor"], PerformCleanup: false, LogError: text => logger.LogError(text), LogTrace: text => logger.LogTrace(text)); diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs index 0bd46581c6dd1..98f28c85e7199 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs @@ -42,7 +42,7 @@ public static async Task InitializeAsync(string localSettingsDirectory, Cancella Resolver: new Resolver(SimpleAssemblyLoader.Instance), CacheDirectory: Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"), CatalogPrefix: "RoslynRemoteHost", - ExpectedErrorParts: ["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory"], + ExpectedErrorParts: ["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService"], PerformCleanup: true, LogError: static _ => { }, LogTrace: static _ => { }); From 36df091bd3ea8c15bba89fd0d1cf92a347553a4a Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Tue, 15 Apr 2025 12:59:58 -0700 Subject: [PATCH 08/20] 1) Use ArrayBuilder instead of Enumerable.Concat 2) Add comments 3) Use Checksum instead of XXHash128 directly 4) Add CA(false) to Task.Yield call 5) Reorder some variable definitions --- .../LanguageServerExportProviderBuilder.cs | 16 ++++------ .../Remote/Core/ExportProviderBuilder.cs | 32 ++++++++++++------- .../ServiceHub/Host/RemoteExportProvider.cs | 10 ++---- .../RemoteInitializationService.cs | 1 + 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index ffd31ce4180bc..e5d757bad6d49 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -2,13 +2,12 @@ // 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 Microsoft.CodeAnalysis.LanguageServer.Logging; using Microsoft.CodeAnalysis.LanguageServer.Services; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer; @@ -25,25 +24,24 @@ public static async Task CreateExportProviderAsync( var baseDirectory = AppContext.BaseDirectory; // Load any Roslyn assemblies from the extension directory - var assemblyPaths = Directory.EnumerateFiles(baseDirectory, "Microsoft.CodeAnalysis*.dll"); - assemblyPaths = assemblyPaths.Concat(Directory.EnumerateFiles(baseDirectory, "Microsoft.ServiceHub*.dll")); + using var _ = ArrayBuilder.GetInstance(out var assemblyPathsBuilder); + assemblyPathsBuilder.AddRange(Directory.EnumerateFiles(baseDirectory, "Microsoft.CodeAnalysis*.dll")); + assemblyPathsBuilder.AddRange(Directory.EnumerateFiles(baseDirectory, "Microsoft.ServiceHub*.dll")); // DevKit assemblies are not shipped in the main language server folder // and not included in ExtensionAssemblyPaths (they get loaded into the default ALC). // So manually add them to the MEF catalog here. if (devKitDependencyPath != null) - { - assemblyPaths = assemblyPaths.Concat(devKitDependencyPath); - } + assemblyPathsBuilder.AddRange(devKitDependencyPath); // Add the extension assemblies to the MEF catalog. - assemblyPaths = assemblyPaths.Concat(extensionManager.ExtensionAssemblyPaths); + assemblyPathsBuilder.AddRange(extensionManager.ExtensionAssemblyPaths); var logger = loggerFactory.CreateLogger(); // Create a MEF resolver that can resolve assemblies in the extension contexts. var args = new ExportProviderBuilder.ExportProviderCreationArguments( - AssemblyPaths: assemblyPaths.ToImmutableArray(), + AssemblyPaths: assemblyPathsBuilder.ToImmutableAndClear(), Resolver: new Resolver(assemblyLoader), CacheDirectory: cacheDirectory, CatalogPrefix: "c#-languageserver", diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index a6d3536ac32f4..a5f40d2a3a76c 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -5,12 +5,12 @@ using System; using System.Collections.Immutable; using System.IO; -using System.IO.Hashing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Threading; using Microsoft.VisualStudio.Composition; using Roslyn.Utilities; @@ -18,6 +18,11 @@ namespace Microsoft.CodeAnalysis.Remote; internal sealed class ExportProviderBuilder { + private const string CatalogSuffix = ".mef-composition"; + + // For testing purposes, track the last cache write task. + private static Task? s_cacheWriteTask; + public record struct ExportProviderCreationArguments( ImmutableArray AssemblyPaths, Resolver Resolver, @@ -28,17 +33,13 @@ public record struct ExportProviderCreationArguments( Action LogError, Action LogTrace); - // For testing purposes, track the last cache write task. - private static Task? _cacheWriteTask; - private const string CatalogSuffix = ".mef-composition"; - public static async Task CreateExportProviderAsync( ExportProviderCreationArguments args, CancellationToken cancellationToken) { // Clear any previous cache write task, so that it is easy to discern whether // a cache write was attempted. - _cacheWriteTask = null; + s_cacheWriteTask = null; // Get the cached MEF composition or create a new one. var exportProviderFactory = await GetCompositionConfigurationAsync(args, cancellationToken).ConfigureAwait(false); @@ -54,6 +55,7 @@ private static async Task GetCompositionConfigurationAsy ExportProviderCreationArguments args, CancellationToken cancellationToken) { + // Determine the path to the MEF composition cache file for the given assembly paths. var compositionCacheFile = GetCompositionCacheFilePath(args.CacheDirectory, args.CatalogPrefix, args.AssemblyPaths); // Try to load a cached composition. @@ -95,12 +97,18 @@ private static async Task GetCompositionConfigurationAsy ThrowOnUnexpectedErrors(config, catalog, args.ExpectedErrorParts, args.LogError); // Try to cache the composition. - _cacheWriteTask = WriteCompositionCacheAsync(compositionCacheFile, config, args.PerformCleanup, args.LogError, cancellationToken).ReportNonFatalErrorAsync(); + s_cacheWriteTask = WriteCompositionCacheAsync(compositionCacheFile, config, args.PerformCleanup, args.LogError, cancellationToken).ReportNonFatalErrorAsync(); // Prepare an ExportProvider factory based on this graph. return config.CreateExportProviderFactory(); } + /// + /// Returns the path to the MEF composition cache file. Inputs used to determine the file name include: + /// 1) The given assembly paths + /// 2) The last write times of the given assembly paths + /// 3) The .NET runtime major version + /// private static string GetCompositionCacheFilePath(string cacheDirectory, string catalogPrefix, ImmutableArray assemblyPaths) { return Path.Combine(cacheDirectory, $"{catalogPrefix}.{ComputeAssemblyHash(assemblyPaths)}{CatalogSuffix}"); @@ -128,9 +136,11 @@ static string ComputeAssemblyHash(ImmutableArray assemblyPaths) hashContents.Append(File.GetLastWriteTimeUtc(assemblyPath).ToString("F")); } - var hash = XxHash128.Hash(Encoding.UTF8.GetBytes(hashContents.ToString())); + // Create base64 string of the hash. + var hashAsBase64String = Checksum.Create(hashContents.ToString()).ToString(); + // Convert to filename safe base64 string. - return Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').TrimEnd('='); + return hashAsBase64String.Replace('+', '-').Replace('/', '_').TrimEnd('='); } } @@ -138,7 +148,7 @@ private static async Task WriteCompositionCacheAsync(string compositionCacheFile { try { - await Task.Yield(); + await Task.Yield().ConfigureAwait(false); if (Path.GetDirectoryName(compositionCacheFile) is string directory) { @@ -204,7 +214,7 @@ private static void ThrowOnUnexpectedErrors(CompositionConfiguration configurati internal static class TestAccessor { #pragma warning disable VSTHRD200 // Use "Async" suffix for async methods - public static Task? GetCacheWriteTask() => _cacheWriteTask; + public static Task? GetCacheWriteTask() => s_cacheWriteTask; #pragma warning restore VSTHRD200 // Use "Async" suffix for async methods } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs index 98f28c85e7199..0c30388dc828e 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.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; using System.Collections.Immutable; using System.IO; using System.Reflection; @@ -27,13 +28,7 @@ internal static class RemoteExportProvider private static ExportProvider? s_instance; internal static ExportProvider ExportProvider - { - get - { - Contract.ThrowIfNull(s_instance, "Default export provider not initialized. Call InitializeAsync first."); - return s_instance; - } - } + => s_instance ?? throw new InvalidOperationException("Default export provider not initialized. Call InitializeAsync first."); public static async Task InitializeAsync(string localSettingsDirectory, CancellationToken cancellationToken) { @@ -62,6 +57,7 @@ public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath) var assemblyName = new AssemblyName(assemblyFullName); if (!string.IsNullOrEmpty(codeBasePath)) { + // Set the codebase path, if known, as a hint for the assembly loader. #pragma warning disable SYSLIB0044 // https://github.com/dotnet/roslyn/issues/71510 assemblyName.CodeBase = codeBasePath; #pragma warning restore SYSLIB0044 diff --git a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs index 7bd2a6f86251a..44ee5a698a7df 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs @@ -25,6 +25,7 @@ protected override IRemoteInitializationService CreateService(in ServiceConstruc /// public async ValueTask InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken) { + // Performed before RunServiceAsync to ensure that the export provider is initialized before the RemoteWorkspaceManager is created. await RemoteExportProvider.InitializeAsync(localSettingsDirectory, cancellationToken).ConfigureAwait(false); return await RunServiceAsync(cancellationToken => From 38d05670b4cb9de9a0fc0e50dacd052a1356fbde Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Wed, 16 Apr 2025 14:52:02 -0700 Subject: [PATCH 09/20] Move some methods from static to instance based and make the LanguageServer and ServiceHub have implementations of ExportProviderBuilder instead of passing around a bunch of options. --- .../ExportProviderBuilderTests.cs | 5 +- .../LanguageServerTestComposition.cs | 1 - .../LanguageServerExportProviderBuilder.cs | 75 +++++++++--- .../MEF/FeaturesTestCompositions.cs | 2 +- .../Remote/Core/ExportProviderBuilder.cs | 109 ++++++++---------- ...ider.cs => RemoteExportProviderBuilder.cs} | 49 +++++--- .../ServiceHub/Host/RemoteWorkspaceManager.cs | 2 +- .../RemoteInitializationService.cs | 5 +- 8 files changed, 149 insertions(+), 99 deletions(-) rename src/Workspaces/Remote/ServiceHub/Host/{RemoteExportProvider.cs => RemoteExportProviderBuilder.cs} (58%) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs index 63dd89636e2ca..85d5d3d542de9 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs @@ -4,7 +4,6 @@ using Basic.Reference.Assemblies; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; @@ -149,7 +148,7 @@ public ImportType(ExportedType t) { } private async Task AssertCacheWriteWasAttemptedAsync() { - var cacheWriteTask = ExportProviderBuilder.TestAccessor.GetCacheWriteTask(); + var cacheWriteTask = LanguageServerExportProviderBuilder.TestAccessor.GetCacheWriteTask(); Assert.NotNull(cacheWriteTask); await cacheWriteTask; @@ -157,7 +156,7 @@ private async Task AssertCacheWriteWasAttemptedAsync() private void AssertNoCacheWriteWasAttempted() { - var cacheWriteTask2 = ExportProviderBuilder.TestAccessor.GetCacheWriteTask(); + var cacheWriteTask2 = LanguageServerExportProviderBuilder.TestAccessor.GetCacheWriteTask(); Assert.Null(cacheWriteTask2); } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs index 6ec172539eb66..9215dfc08b82b 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis.LanguageServer.Services; -using Microsoft.CodeAnalysis.Remote; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index e5d757bad6d49..0e2d30b9bf0c4 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.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 Microsoft.CodeAnalysis.LanguageServer.Logging; using Microsoft.CodeAnalysis.LanguageServer.Services; using Microsoft.CodeAnalysis.PooledObjects; @@ -11,8 +12,25 @@ namespace Microsoft.CodeAnalysis.LanguageServer; -internal sealed class LanguageServerExportProviderBuilder +internal sealed class LanguageServerExportProviderBuilder : ExportProviderBuilder { + private readonly ILogger _logger; + + // For testing purposes, track the last cache write task. + private static Task? s_cacheWriteTask; + + private LanguageServerExportProviderBuilder( + ImmutableArray assemblyPaths, + Resolver resolver, + string cacheDirectory, + string catalogPrefix, + ImmutableArray expectedErrorParts, + ILoggerFactory loggerFactory) + : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix, expectedErrorParts) + { + _logger = loggerFactory.CreateLogger(); + } + public static async Task CreateExportProviderAsync( ExtensionAssemblyManager extensionManager, IAssemblyLoader assemblyLoader, @@ -37,20 +55,15 @@ public static async Task CreateExportProviderAsync( // Add the extension assemblies to the MEF catalog. assemblyPathsBuilder.AddRange(extensionManager.ExtensionAssemblyPaths); - var logger = loggerFactory.CreateLogger(); - // Create a MEF resolver that can resolve assemblies in the extension contexts. - var args = new ExportProviderBuilder.ExportProviderCreationArguments( - AssemblyPaths: assemblyPathsBuilder.ToImmutableAndClear(), - Resolver: new Resolver(assemblyLoader), - CacheDirectory: cacheDirectory, - CatalogPrefix: "c#-languageserver", - ExpectedErrorParts: ["CSharpMapCodeService", "PythiaSignatureHelpProvider", "CopilotSemanticSearchQueryExecutor"], - PerformCleanup: false, - LogError: text => logger.LogError(text), - LogTrace: text => logger.LogTrace(text)); - - var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(args, cancellationToken); + var builder = new LanguageServerExportProviderBuilder( + assemblyPathsBuilder.ToImmutableAndClear(), + new Resolver(assemblyLoader), + cacheDirectory, + catalogPrefix: "c#-languageserver", + expectedErrorParts: ["CSharpMapCodeService", "PythiaSignatureHelpProvider", "CopilotSemanticSearchQueryExecutor"], + loggerFactory); + var exportProvider = await builder.CreateExportProviderAsync(cancellationToken); // Also add the ExtensionAssemblyManager so it will be available for the rest of the composition. exportProvider.GetExportedValue().SetMefExtensionAssemblyManager(extensionManager); @@ -60,4 +73,38 @@ public static async Task CreateExportProviderAsync( return exportProvider; } + + protected override void LogError(string message) + => _logger.LogError(message); + + protected override void LogTrace(string message) + => _logger.LogTrace(message); + + protected override Task CreateExportProviderAsync(CancellationToken cancellationToken) + { + // Clear any previous cache write task, so that it is easy to discern whether + // a cache write was attempted. + s_cacheWriteTask = null; + + return base.CreateExportProviderAsync(cancellationToken); + } + + protected override Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, CancellationToken cancellationToken) + { + s_cacheWriteTask = base.WriteCompositionCacheAsync(compositionCacheFile, config, cancellationToken); + + return s_cacheWriteTask; + } + + protected override void PerformCacheDirectoryCleanup(DirectoryInfo directoryInfo, CancellationToken cancellationToken) + { + // No cache directory cleanup is needed for the language server. + } + + internal static class TestAccessor + { +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods + public static Task? GetCacheWriteTask() => s_cacheWriteTask; +#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods + } } diff --git a/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs b/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs index ed7653976f276..4454997b54745 100644 --- a/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs +++ b/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs @@ -21,7 +21,7 @@ public static class FeaturesTestCompositions // We need to update tests to handle the options correctly before enabling the default provider. public static readonly TestComposition RemoteHost = TestComposition.Empty - .AddAssemblies(RemoteExportProvider.RemoteHostAssemblies) + .AddAssemblies(RemoteExportProviderBuilder.RemoteHostAssemblies) .AddParts(typeof(TestSerializerService.Factory)); public static TestComposition WithTestHostParts(this TestComposition composition, TestHost host) diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index a5f40d2a3a76c..79e69a83a9609 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -16,33 +16,28 @@ namespace Microsoft.CodeAnalysis.Remote; -internal sealed class ExportProviderBuilder +internal abstract class ExportProviderBuilder( + ImmutableArray assemblyPaths, + Resolver resolver, + string cacheDirectory, + string catalogPrefix, + ImmutableArray expectedErrorParts) { private const string CatalogSuffix = ".mef-composition"; - // For testing purposes, track the last cache write task. - private static Task? s_cacheWriteTask; - - public record struct ExportProviderCreationArguments( - ImmutableArray AssemblyPaths, - Resolver Resolver, - string CacheDirectory, - string CatalogPrefix, - ImmutableArray ExpectedErrorParts, - bool PerformCleanup, - Action LogError, - Action LogTrace); - - public static async Task CreateExportProviderAsync( - ExportProviderCreationArguments args, - CancellationToken cancellationToken) - { - // Clear any previous cache write task, so that it is easy to discern whether - // a cache write was attempted. - s_cacheWriteTask = null; + protected ImmutableArray _assemblyPaths = assemblyPaths; + protected Resolver _resolver = resolver; + protected string _cacheDirectory = cacheDirectory; + protected string _catalogPrefix = catalogPrefix; + protected ImmutableArray _expectedErrorParts = expectedErrorParts; + + protected abstract void LogError(string message); + protected abstract void LogTrace(string message); + protected virtual async Task CreateExportProviderAsync(CancellationToken cancellationToken) + { // Get the cached MEF composition or create a new one. - var exportProviderFactory = await GetCompositionConfigurationAsync(args, cancellationToken).ConfigureAwait(false); + var exportProviderFactory = await GetCompositionConfigurationAsync(cancellationToken).ConfigureAwait(false); // Create an export provider, which represents a unique container of values. // You can create as many of these as you want, but typically an app needs just one. @@ -51,23 +46,21 @@ public static async Task CreateExportProviderAsync( return exportProvider; } - private static async Task GetCompositionConfigurationAsync( - ExportProviderCreationArguments args, - CancellationToken cancellationToken) + private async Task GetCompositionConfigurationAsync(CancellationToken cancellationToken) { // Determine the path to the MEF composition cache file for the given assembly paths. - var compositionCacheFile = GetCompositionCacheFilePath(args.CacheDirectory, args.CatalogPrefix, args.AssemblyPaths); + var compositionCacheFile = GetCompositionCacheFilePath(); // Try to load a cached composition. try { if (File.Exists(compositionCacheFile)) { - args.LogTrace($"Loading cached MEF catalog: {compositionCacheFile}"); + LogTrace($"Loading cached MEF catalog: {compositionCacheFile}"); CachedComposition cachedComposition = new(); using FileStream cacheStream = new(compositionCacheFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true); - var exportProviderFactory = await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, args.Resolver, cancellationToken).ConfigureAwait(false); + var exportProviderFactory = await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, _resolver, cancellationToken).ConfigureAwait(false); return exportProviderFactory; } @@ -75,18 +68,18 @@ private static async Task GetCompositionConfigurationAsy catch (Exception ex) { // Log the error, and move on to recover by recreating the MEF composition. - args.LogError($"Loading cached MEF composition failed: {ex}"); + LogError($"Loading cached MEF composition failed: {ex}"); } - args.LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", args.AssemblyPaths)}."); + LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", _assemblyPaths)}."); var discovery = PartDiscovery.Combine( - args.Resolver, - new AttributedPartDiscovery(args.Resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) - new AttributedPartDiscoveryV1(args.Resolver)); + _resolver, + new AttributedPartDiscovery(_resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) + new AttributedPartDiscoveryV1(_resolver)); - var parts = await discovery.CreatePartsAsync(args.AssemblyPaths, progress: null, cancellationToken).ConfigureAwait(false); - var catalog = ComposableCatalog.Create(args.Resolver) + var parts = await discovery.CreatePartsAsync(_assemblyPaths, progress: null, cancellationToken).ConfigureAwait(false); + var catalog = ComposableCatalog.Create(_resolver) .AddParts(parts) .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import @@ -94,10 +87,10 @@ private static async Task GetCompositionConfigurationAsy var config = CompositionConfiguration.Create(catalog); // Verify we only have expected errors. - ThrowOnUnexpectedErrors(config, catalog, args.ExpectedErrorParts, args.LogError); + ThrowOnUnexpectedErrors(config, catalog); // Try to cache the composition. - s_cacheWriteTask = WriteCompositionCacheAsync(compositionCacheFile, config, args.PerformCleanup, args.LogError, cancellationToken).ReportNonFatalErrorAsync(); + _ = WriteCompositionCacheAsync(compositionCacheFile, config, cancellationToken).ReportNonFatalErrorAsync(); // Prepare an ExportProvider factory based on this graph. return config.CreateExportProviderFactory(); @@ -109,9 +102,9 @@ private static async Task GetCompositionConfigurationAsy /// 2) The last write times of the given assembly paths /// 3) The .NET runtime major version /// - private static string GetCompositionCacheFilePath(string cacheDirectory, string catalogPrefix, ImmutableArray assemblyPaths) + private string GetCompositionCacheFilePath() { - return Path.Combine(cacheDirectory, $"{catalogPrefix}.{ComputeAssemblyHash(assemblyPaths)}{CatalogSuffix}"); + return Path.Combine(_cacheDirectory, $"{_catalogPrefix}.{ComputeAssemblyHash(_assemblyPaths)}{CatalogSuffix}"); static string ComputeAssemblyHash(ImmutableArray assemblyPaths) { @@ -144,7 +137,7 @@ static string ComputeAssemblyHash(ImmutableArray assemblyPaths) } } - private static async Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, bool performCleanup, Action logError, CancellationToken cancellationToken) + protected virtual async Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, CancellationToken cancellationToken) { try { @@ -153,16 +146,7 @@ private static async Task WriteCompositionCacheAsync(string compositionCacheFile if (Path.GetDirectoryName(compositionCacheFile) is string directory) { var directoryInfo = Directory.CreateDirectory(directory); - - if (performCleanup) - { - // Delete any existing cached files. - foreach (var fileInfo in directoryInfo.EnumerateFiles($"*{CatalogSuffix}")) - { - fileInfo.Delete(); - cancellationToken.ThrowIfCancellationRequested(); - } - } + PerformCacheDirectoryCleanup(directoryInfo, cancellationToken); } CachedComposition cachedComposition = new(); @@ -184,15 +168,25 @@ private static async Task WriteCompositionCacheAsync(string compositionCacheFile } catch (Exception ex) { - logError($"Failed to save MEF cache: {ex}"); + LogError($"Failed to save MEF cache: {ex}"); + } + } + + protected virtual void PerformCacheDirectoryCleanup(DirectoryInfo directoryInfo, CancellationToken cancellationToken) + { + // Delete any existing cached files. + foreach (var fileInfo in directoryInfo.EnumerateFiles($"*{CatalogSuffix}")) + { + fileInfo.Delete(); + cancellationToken.ThrowIfCancellationRequested(); } } - private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog, ImmutableArray expectedErrorParts, Action logError) + private void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog) { // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; - var expectedErrorPartsSet = expectedErrorParts.ToSet(); + var expectedErrorPartsSet = _expectedErrorParts.ToSet(); var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); if (hasUnexpectedErroredParts || !catalog.DiscoveredParts.DiscoveryErrors.IsEmpty) @@ -205,16 +199,9 @@ private static void ThrowOnUnexpectedErrors(CompositionConfiguration configurati catch (CompositionFailedException ex) { // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately - logError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); + LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); throw; } } } - - internal static class TestAccessor - { -#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods - public static Task? GetCacheWriteTask() => s_cacheWriteTask; -#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods - } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs similarity index 58% rename from src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs rename to src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs index 0c30388dc828e..dc5fdeedb9a1c 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProvider.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs @@ -16,33 +16,50 @@ namespace Microsoft.CodeAnalysis.Remote; -internal static class RemoteExportProvider +internal sealed class RemoteExportProviderBuilder : ExportProviderBuilder { internal static readonly ImmutableArray RemoteHostAssemblies = MefHostServices.DefaultAssemblies - .Add(typeof(AspNetCoreEmbeddedLanguageClassifier).Assembly) - .Add(typeof(BrokeredServiceBase).Assembly) - .Add(typeof(IRazorLanguageServerTarget).Assembly) - .Add(typeof(RemoteWorkspacesResources).Assembly) - .Add(typeof(IExtensionWorkspaceMessageHandler<,>).Assembly); + .Add(typeof(AspNetCoreEmbeddedLanguageClassifier).Assembly) // Microsoft.CodeAnalysis.ExternalAccess.AspNetCore + .Add(typeof(BrokeredServiceBase).Assembly) // Microsoft.CodeAnalysis.Remote.ServiceHub + .Add(typeof(IRazorLanguageServerTarget).Assembly) // Microsoft.CodeAnalysis.ExternalAccess.Razor.Features + .Add(typeof(RemoteWorkspacesResources).Assembly) // Microsoft.CodeAnalysis.Remote.Workspaces + .Add(typeof(IExtensionWorkspaceMessageHandler<,>).Assembly); // Microsoft.CodeAnalysis.ExternalAccess.Extensions private static ExportProvider? s_instance; internal static ExportProvider ExportProvider => s_instance ?? throw new InvalidOperationException("Default export provider not initialized. Call InitializeAsync first."); + private RemoteExportProviderBuilder( + ImmutableArray assemblyPaths, + Resolver resolver, + string cacheDirectory, + string catalogPrefix, + ImmutableArray expectedErrorParts) + : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix, expectedErrorParts) + { + } + public static async Task InitializeAsync(string localSettingsDirectory, CancellationToken cancellationToken) { - var args = new ExportProviderBuilder.ExportProviderCreationArguments( - AssemblyPaths: RemoteHostAssemblies.SelectAsArray(static a => a.Location), - Resolver: new Resolver(SimpleAssemblyLoader.Instance), - CacheDirectory: Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"), - CatalogPrefix: "RoslynRemoteHost", - ExpectedErrorParts: ["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService"], - PerformCleanup: true, - LogError: static _ => { }, - LogTrace: static _ => { }); + System.Diagnostics.Debugger.Launch(); + + var builder = new RemoteExportProviderBuilder( + assemblyPaths: RemoteHostAssemblies.SelectAsArray(static a => a.Location), + resolver: new Resolver(SimpleAssemblyLoader.Instance), + cacheDirectory: Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"), + catalogPrefix: "RoslynRemoteHost", + expectedErrorParts: ["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService"]); + + s_instance = await builder.CreateExportProviderAsync(cancellationToken).ConfigureAwait(false); + } + + protected override void LogError(string message) + { + } - s_instance = await ExportProviderBuilder.CreateExportProviderAsync(args, cancellationToken).ConfigureAwait(false); + protected override void LogTrace(string message) + { } private sealed class SimpleAssemblyLoader : IAssemblyLoader diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs index 27f30ada7ceb2..29b8ed058f4cc 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs @@ -73,7 +73,7 @@ public RemoteWorkspaceManager( private static RemoteWorkspace CreatePrimaryWorkspace() { - var exportProvider = RemoteExportProvider.ExportProvider; + var exportProvider = RemoteExportProviderBuilder.ExportProvider; return new RemoteWorkspace(VisualStudioMefHostServices.Create(exportProvider)); } diff --git a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs index 44ee5a698a7df..6389e02f28ff4 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs @@ -25,8 +25,9 @@ protected override IRemoteInitializationService CreateService(in ServiceConstruc /// public async ValueTask InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken) { - // Performed before RunServiceAsync to ensure that the export provider is initialized before the RemoteWorkspaceManager is created. - await RemoteExportProvider.InitializeAsync(localSettingsDirectory, cancellationToken).ConfigureAwait(false); + // Performed before RunServiceAsync to ensure that the export provider is initialized before the RemoteWorkspaceManager is created + // as part of the RunServiceAsync call. + await RemoteExportProviderBuilder.InitializeAsync(localSettingsDirectory, cancellationToken).ConfigureAwait(false); return await RunServiceAsync(cancellationToken => { From b4e5b8e48a1172829dd5659927c3a78514f3e531 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Wed, 16 Apr 2025 14:59:54 -0700 Subject: [PATCH 10/20] remove accidental debug aid --- .../Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs index dc5fdeedb9a1c..be1a2fda79e9a 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs @@ -42,8 +42,6 @@ private RemoteExportProviderBuilder( public static async Task InitializeAsync(string localSettingsDirectory, CancellationToken cancellationToken) { - System.Diagnostics.Debugger.Launch(); - var builder = new RemoteExportProviderBuilder( assemblyPaths: RemoteHostAssemblies.SelectAsArray(static a => a.Location), resolver: new Resolver(SimpleAssemblyLoader.Instance), From b542f434136bef44b84f964a305afc765c4338eb Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Wed, 16 Apr 2025 17:02:54 -0700 Subject: [PATCH 11/20] 1) Return an error message (if encountered) during initialization and log. 2) Use StringComparer.Ordinal in MEF assembly path sorting 3) Cleanup File IO in MEF caching --- .../Remote/Core/ExportProviderBuilder.cs | 25 ++++++------------- .../Core/IRemoteInitializationService.cs | 6 ++--- .../Remote/Core/ServiceHubRemoteHostClient.cs | 9 ++++--- .../Host/RemoteExportProviderBuilder.cs | 11 ++++++-- .../RemoteInitializationService.cs | 8 +++--- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 79e69a83a9609..1c90c2d36c6bf 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Threading; using Microsoft.VisualStudio.Composition; using Roslyn.Utilities; @@ -109,7 +110,7 @@ private string GetCompositionCacheFilePath() static string ComputeAssemblyHash(ImmutableArray assemblyPaths) { // Ensure AssemblyPaths are always in the same order. - assemblyPaths = assemblyPaths.Sort(); + assemblyPaths = assemblyPaths.Sort(StringComparer.Ordinal); var hashContents = new StringBuilder(); @@ -143,28 +144,18 @@ protected virtual async Task WriteCompositionCacheAsync(string compositionCacheF { await Task.Yield().ConfigureAwait(false); - if (Path.GetDirectoryName(compositionCacheFile) is string directory) - { - var directoryInfo = Directory.CreateDirectory(directory); - PerformCacheDirectoryCleanup(directoryInfo, cancellationToken); - } + var directory = Path.GetDirectoryName(compositionCacheFile)!; + var directoryInfo = Directory.CreateDirectory(directory); + PerformCacheDirectoryCleanup(directoryInfo, cancellationToken); CachedComposition cachedComposition = new(); - var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + var tempFilePath = Path.Combine(directory, Path.GetRandomFileName()); using (FileStream cacheStream = new(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true)) { await cachedComposition.SaveAsync(config, cacheStream, cancellationToken).ConfigureAwait(false); } -#if NET - File.Move(tempFilePath, compositionCacheFile, overwrite: true); -#else - // On .NET Framework, File.Move doesn't support overwriting the destination file. Use File.Delete first - // to ensure the destination file is removed before moving the temp file. File.Delete will not throw if - // the file doesn't exist. - File.Delete(compositionCacheFile); File.Move(tempFilePath, compositionCacheFile); -#endif } catch (Exception ex) { @@ -175,9 +166,9 @@ protected virtual async Task WriteCompositionCacheAsync(string compositionCacheF protected virtual void PerformCacheDirectoryCleanup(DirectoryInfo directoryInfo, CancellationToken cancellationToken) { // Delete any existing cached files. - foreach (var fileInfo in directoryInfo.EnumerateFiles($"*{CatalogSuffix}")) + foreach (var fileInfo in directoryInfo.EnumerateFiles()) { - fileInfo.Delete(); + IOUtilities.PerformIO(fileInfo.Delete); cancellationToken.ThrowIfCancellationRequested(); } } diff --git a/src/Workspaces/Remote/Core/IRemoteInitializationService.cs b/src/Workspaces/Remote/Core/IRemoteInitializationService.cs index f0ccd22e187d9..9f3bc49fd8183 100644 --- a/src/Workspaces/Remote/Core/IRemoteInitializationService.cs +++ b/src/Workspaces/Remote/Core/IRemoteInitializationService.cs @@ -12,8 +12,8 @@ internal interface IRemoteInitializationService { /// /// Initializes values including for the process. - /// Called as soon as the remote process is created but can't guarantee that solution entities (projects, documents, syntax trees) have not been created beforehand. + /// Called as soon as the remote process is created. /// - /// Process ID of the remote process. - ValueTask InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken); + /// Process ID of the remote process and an error message if the server encountered initialization issues. + ValueTask<(int ProcessId, string? ErrorMessage)> InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken); } diff --git a/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs b/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs index 32ed8c5366c3f..6b630bbe06285 100644 --- a/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs +++ b/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs @@ -73,15 +73,18 @@ public static async Task CreateAsync( var workspaceConfigurationService = services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( + var remoteProcessIdAnderrorMessage = await client.TryInvokeAsync( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, localSettingsDirectory, cancellationToken), cancellationToken).ConfigureAwait(false); - if (remoteProcessId.HasValue) + if (remoteProcessIdAnderrorMessage.HasValue) { + if (remoteProcessIdAnderrorMessage.Value.ErrorMessage != null) + hubClient.Logger.TraceEvent(TraceEventType.Error, 1, $"ServiceHub initializion error: {remoteProcessIdAnderrorMessage.Value.ErrorMessage}"); + try { - client._remoteProcess = Process.GetProcessById(remoteProcessId.Value); + client._remoteProcess = Process.GetProcessById(remoteProcessIdAnderrorMessage.Value.ProcessId); } catch (Exception e) { diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs index be1a2fda79e9a..56470bfcdde08 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.IO; using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Extensions; @@ -28,7 +29,9 @@ internal sealed class RemoteExportProviderBuilder : ExportProviderBuilder private static ExportProvider? s_instance; internal static ExportProvider ExportProvider - => s_instance ?? throw new InvalidOperationException("Default export provider not initialized. Call InitializeAsync first."); + => s_instance ?? throw new InvalidOperationException($"Default export provider not initialized. Call {nameof(InitializeAsync)} first."); + + private StringBuilder? _errorMessages; private RemoteExportProviderBuilder( ImmutableArray assemblyPaths, @@ -40,7 +43,7 @@ private RemoteExportProviderBuilder( { } - public static async Task InitializeAsync(string localSettingsDirectory, CancellationToken cancellationToken) + public static async Task InitializeAsync(string localSettingsDirectory, CancellationToken cancellationToken) { var builder = new RemoteExportProviderBuilder( assemblyPaths: RemoteHostAssemblies.SelectAsArray(static a => a.Location), @@ -50,10 +53,14 @@ public static async Task InitializeAsync(string localSettingsDirectory, Cancella expectedErrorParts: ["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService"]); s_instance = await builder.CreateExportProviderAsync(cancellationToken).ConfigureAwait(false); + + return builder._errorMessages?.ToString(); } protected override void LogError(string message) { + _errorMessages ??= new StringBuilder(); + _errorMessages.AppendLine(message); } protected override void LogTrace(string message) diff --git a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs index 6389e02f28ff4..baae37fe41d66 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs @@ -23,18 +23,20 @@ protected override IRemoteInitializationService CreateService(in ServiceConstruc /// /// Remote API. /// - public async ValueTask InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken) + public async ValueTask<(int ProcessId, string? ErrorMessage)> InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken) { // Performed before RunServiceAsync to ensure that the export provider is initialized before the RemoteWorkspaceManager is created // as part of the RunServiceAsync call. - await RemoteExportProviderBuilder.InitializeAsync(localSettingsDirectory, cancellationToken).ConfigureAwait(false); + var errorMessage = await RemoteExportProviderBuilder.InitializeAsync(localSettingsDirectory, cancellationToken).ConfigureAwait(false); - return await RunServiceAsync(cancellationToken => + var processId = await RunServiceAsync(cancellationToken => { var service = (RemoteWorkspaceConfigurationService)GetWorkspaceServices().GetRequiredService(); service.InitializeOptions(options); return ValueTaskFactory.FromResult(Process.GetCurrentProcess().Id); }, cancellationToken).ConfigureAwait(false); + + return (processId, errorMessage); } } From acbb93de4efe68e97880fd70ef263a94a09100c0 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Thu, 17 Apr 2025 07:04:48 -0700 Subject: [PATCH 12/20] Fix build, cosmetic suggestions in PR --- .../Services/ServiceHubServicesTests.cs | 8 +++--- .../SolutionWithSourceGeneratorTests.cs | 2 +- .../Remote/Core/ExportProviderBuilder.cs | 28 +++++++++---------- .../Remote/Core/ServiceHubRemoteHostClient.cs | 10 +++---- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs index 68590264929c9..bd7e8d7d6ff42 100644 --- a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs @@ -738,7 +738,7 @@ internal async Task TestSourceGenerationExecution_RegenerateOnEdit( var workspaceConfigurationService = workspace.Services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( + _ = await client.TryInvokeAsync( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); @@ -822,7 +822,7 @@ internal async Task TestSourceGenerationExecution_MinorVersionChange_NoActualCha var workspaceConfigurationService = workspace.Services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( + _ = await client.TryInvokeAsync( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); @@ -877,7 +877,7 @@ internal async Task TestSourceGenerationExecution_MajorVersionChange_NoActualCha var workspaceConfigurationService = workspace.Services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( + _ = await client.TryInvokeAsync( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); @@ -1686,7 +1686,7 @@ private static Solution Populate(Solution solution) ], [ "vb additional file content" - ], [solution.ProjectIds.First()]); + ], [solution.ProjectIds[0]]); solution = AddProject(solution, LanguageNames.CSharp, [ diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs index 2340be874e3f0..ed6cc9b72ee9d 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs @@ -1387,7 +1387,7 @@ internal async Task UpdatingAnalyzerReferenceReloadsGenerators( var workspaceConfigurationService = workspace.Services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( + _ = await client.TryInvokeAsync( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options with { SourceGeneratorExecution = executionPreference }, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 1c90c2d36c6bf..0a3aec1d7915d 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -26,11 +26,11 @@ internal abstract class ExportProviderBuilder( { private const string CatalogSuffix = ".mef-composition"; - protected ImmutableArray _assemblyPaths = assemblyPaths; - protected Resolver _resolver = resolver; - protected string _cacheDirectory = cacheDirectory; - protected string _catalogPrefix = catalogPrefix; - protected ImmutableArray _expectedErrorParts = expectedErrorParts; + protected ImmutableArray AssemblyPaths { get; } = assemblyPaths; + protected Resolver Resolver { get; } = resolver; + protected string CacheDirectory { get; } = cacheDirectory; + protected string CatalogPrefix { get; } = catalogPrefix; + protected ImmutableArray ExpectedErrorParts { get; } = expectedErrorParts; protected abstract void LogError(string message); protected abstract void LogTrace(string message); @@ -61,7 +61,7 @@ private async Task GetCompositionConfigurationAsync(Canc CachedComposition cachedComposition = new(); using FileStream cacheStream = new(compositionCacheFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true); - var exportProviderFactory = await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, _resolver, cancellationToken).ConfigureAwait(false); + var exportProviderFactory = await cachedComposition.LoadExportProviderFactoryAsync(cacheStream, Resolver, cancellationToken).ConfigureAwait(false); return exportProviderFactory; } @@ -72,15 +72,15 @@ private async Task GetCompositionConfigurationAsync(Canc LogError($"Loading cached MEF composition failed: {ex}"); } - LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", _assemblyPaths)}."); + LogTrace($"Composing MEF catalog using:{Environment.NewLine}{string.Join($" {Environment.NewLine}", AssemblyPaths)}."); var discovery = PartDiscovery.Combine( - _resolver, - new AttributedPartDiscovery(_resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) - new AttributedPartDiscoveryV1(_resolver)); + Resolver, + new AttributedPartDiscovery(Resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) + new AttributedPartDiscoveryV1(Resolver)); - var parts = await discovery.CreatePartsAsync(_assemblyPaths, progress: null, cancellationToken).ConfigureAwait(false); - var catalog = ComposableCatalog.Create(_resolver) + var parts = await discovery.CreatePartsAsync(AssemblyPaths, progress: null, cancellationToken).ConfigureAwait(false); + var catalog = ComposableCatalog.Create(Resolver) .AddParts(parts) .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import @@ -105,7 +105,7 @@ private async Task GetCompositionConfigurationAsync(Canc /// private string GetCompositionCacheFilePath() { - return Path.Combine(_cacheDirectory, $"{_catalogPrefix}.{ComputeAssemblyHash(_assemblyPaths)}{CatalogSuffix}"); + return Path.Combine(CacheDirectory, $"{CatalogPrefix}.{ComputeAssemblyHash(AssemblyPaths)}{CatalogSuffix}"); static string ComputeAssemblyHash(ImmutableArray assemblyPaths) { @@ -177,7 +177,7 @@ private void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, Com { // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; - var expectedErrorPartsSet = _expectedErrorParts.ToSet(); + var expectedErrorPartsSet = ExpectedErrorParts.ToSet(); var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); if (hasUnexpectedErroredParts || !catalog.DiscoveredParts.DiscoveryErrors.IsEmpty) diff --git a/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs b/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs index 6b630bbe06285..a67be6534d8a2 100644 --- a/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs +++ b/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs @@ -73,18 +73,18 @@ public static async Task CreateAsync( var workspaceConfigurationService = services.GetRequiredService(); - var remoteProcessIdAnderrorMessage = await client.TryInvokeAsync( + var remoteProcessIdAndErrorMessage = await client.TryInvokeAsync( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, localSettingsDirectory, cancellationToken), cancellationToken).ConfigureAwait(false); - if (remoteProcessIdAnderrorMessage.HasValue) + if (remoteProcessIdAndErrorMessage.HasValue) { - if (remoteProcessIdAnderrorMessage.Value.ErrorMessage != null) - hubClient.Logger.TraceEvent(TraceEventType.Error, 1, $"ServiceHub initializion error: {remoteProcessIdAnderrorMessage.Value.ErrorMessage}"); + if (remoteProcessIdAndErrorMessage.Value.ErrorMessage != null) + hubClient.Logger.TraceEvent(TraceEventType.Error, 1, $"ServiceHub initialization error: {remoteProcessIdAndErrorMessage.Value.ErrorMessage}"); try { - client._remoteProcess = Process.GetProcessById(remoteProcessIdAnderrorMessage.Value.ProcessId); + client._remoteProcess = Process.GetProcessById(remoteProcessIdAndErrorMessage.Value.ProcessId); } catch (Exception e) { From fdaa909c20d7c13672e28c83522f045ab5f6b171 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 19 Apr 2025 10:46:16 -0700 Subject: [PATCH 13/20] Temporary workaround until the razor EA issue is addressed --- src/Workspaces/Remote/Core/ExportProviderBuilder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 0a3aec1d7915d..75891515fae41 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -88,7 +88,9 @@ private async Task GetCompositionConfigurationAsync(Canc var config = CompositionConfiguration.Create(catalog); // Verify we only have expected errors. - ThrowOnUnexpectedErrors(config, catalog); + + // TODO: UNCOMMENT THIS AFTER RAZOR EA ISSUE IS ADDRESSED + //ThrowOnUnexpectedErrors(config, catalog); // Try to cache the composition. _ = WriteCompositionCacheAsync(compositionCacheFile, config, cancellationToken).ReportNonFatalErrorAsync(); From a6e5fbb92b97966b2fcd32b74efe59caea452409 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Sat, 19 Apr 2025 14:23:20 -0700 Subject: [PATCH 14/20] fix linting issue when temporarily commenting out code --- .../Remote/Core/ExportProviderBuilder.cs | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 75891515fae41..366766d8d037a 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -175,26 +175,26 @@ protected virtual void PerformCacheDirectoryCleanup(DirectoryInfo directoryInfo, } } - private void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog) - { - // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. - var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; - var expectedErrorPartsSet = ExpectedErrorParts.ToSet(); - var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); - - if (hasUnexpectedErroredParts || !catalog.DiscoveredParts.DiscoveryErrors.IsEmpty) - { - try - { - catalog.DiscoveredParts.ThrowOnErrors(); - configuration.ThrowOnErrors(); - } - catch (CompositionFailedException ex) - { - // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately - LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); - throw; - } - } - } + //private void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog) + //{ + // // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. + // var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; + // var expectedErrorPartsSet = ExpectedErrorParts.ToSet(); + // var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); + + // if (hasUnexpectedErroredParts || !catalog.DiscoveredParts.DiscoveryErrors.IsEmpty) + // { + // try + // { + // catalog.DiscoveredParts.ThrowOnErrors(); + // configuration.ThrowOnErrors(); + // } + // catch (CompositionFailedException ex) + // { + // // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately + // LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); + // throw; + // } + // } + //} } From bbaff4e8c9ac59b9eb95b96e32d56c0e19621d41 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Mon, 21 Apr 2025 10:27:38 -0700 Subject: [PATCH 15/20] Handlre razor external access assembly mef loading issue better --- .../LanguageServerExportProviderBuilder.cs | 13 +++-- .../Remote/Core/ExportProviderBuilder.cs | 52 +++++++++---------- .../Host/RemoteExportProviderBuilder.cs | 41 ++++++++++++--- 3 files changed, 70 insertions(+), 36 deletions(-) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index 0e2d30b9bf0c4..d82d2405a822f 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -24,9 +24,8 @@ private LanguageServerExportProviderBuilder( Resolver resolver, string cacheDirectory, string catalogPrefix, - ImmutableArray expectedErrorParts, ILoggerFactory loggerFactory) - : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix, expectedErrorParts) + : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix) { _logger = loggerFactory.CreateLogger(); } @@ -61,7 +60,6 @@ public static async Task CreateExportProviderAsync( new Resolver(assemblyLoader), cacheDirectory, catalogPrefix: "c#-languageserver", - expectedErrorParts: ["CSharpMapCodeService", "PythiaSignatureHelpProvider", "CopilotSemanticSearchQueryExecutor"], loggerFactory); var exportProvider = await builder.CreateExportProviderAsync(cancellationToken); @@ -89,6 +87,15 @@ protected override Task CreateExportProviderAsync(CancellationTo return base.CreateExportProviderAsync(cancellationToken); } + protected override bool ContainsUnexpectedErrors(IEnumerable erroredParts, ImmutableList partDiscoveryExceptions) + { + // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. + var expectedErrorPartsSet = new HashSet(["CSharpMapCodeService", "PythiaSignatureHelpProvider", "CopilotSemanticSearchQueryExecutor"]); + var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); + + return hasUnexpectedErroredParts || !partDiscoveryExceptions.IsEmpty; + } + protected override Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, CancellationToken cancellationToken) { s_cacheWriteTask = base.WriteCompositionCacheAsync(compositionCacheFile, config, cancellationToken); diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index 366766d8d037a..a938b1a5dccfc 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; @@ -21,8 +22,7 @@ internal abstract class ExportProviderBuilder( ImmutableArray assemblyPaths, Resolver resolver, string cacheDirectory, - string catalogPrefix, - ImmutableArray expectedErrorParts) + string catalogPrefix) { private const string CatalogSuffix = ".mef-composition"; @@ -30,7 +30,6 @@ internal abstract class ExportProviderBuilder( protected Resolver Resolver { get; } = resolver; protected string CacheDirectory { get; } = cacheDirectory; protected string CatalogPrefix { get; } = catalogPrefix; - protected ImmutableArray ExpectedErrorParts { get; } = expectedErrorParts; protected abstract void LogError(string message); protected abstract void LogTrace(string message); @@ -89,8 +88,7 @@ private async Task GetCompositionConfigurationAsync(Canc // Verify we only have expected errors. - // TODO: UNCOMMENT THIS AFTER RAZOR EA ISSUE IS ADDRESSED - //ThrowOnUnexpectedErrors(config, catalog); + ThrowOnUnexpectedErrors(config, catalog); // Try to cache the composition. _ = WriteCompositionCacheAsync(compositionCacheFile, config, cancellationToken).ReportNonFatalErrorAsync(); @@ -175,26 +173,26 @@ protected virtual void PerformCacheDirectoryCleanup(DirectoryInfo directoryInfo, } } - //private void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog) - //{ - // // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. - // var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; - // var expectedErrorPartsSet = ExpectedErrorParts.ToSet(); - // var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); - - // if (hasUnexpectedErroredParts || !catalog.DiscoveredParts.DiscoveryErrors.IsEmpty) - // { - // try - // { - // catalog.DiscoveredParts.ThrowOnErrors(); - // configuration.ThrowOnErrors(); - // } - // catch (CompositionFailedException ex) - // { - // // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately - // LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); - // throw; - // } - // } - //} + protected abstract bool ContainsUnexpectedErrors(IEnumerable erroredParts, ImmutableList partDiscoveryExceptions); + + private void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ComposableCatalog catalog) + { + // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. + var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; + + if (ContainsUnexpectedErrors(erroredParts, catalog.DiscoveredParts.DiscoveryErrors)) + { + try + { + catalog.DiscoveredParts.ThrowOnErrors(); + configuration.ThrowOnErrors(); + } + catch (CompositionFailedException ex) + { + // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately + LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); + throw; + } + } + } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs index 56470bfcdde08..6eaca448a2bce 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs @@ -3,8 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.Linq; using System.Reflection; using System.Text; using System.Threading; @@ -14,6 +16,7 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.VisualStudio.Composition; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Remote; @@ -23,7 +26,7 @@ internal sealed class RemoteExportProviderBuilder : ExportProviderBuilder MefHostServices.DefaultAssemblies .Add(typeof(AspNetCoreEmbeddedLanguageClassifier).Assembly) // Microsoft.CodeAnalysis.ExternalAccess.AspNetCore .Add(typeof(BrokeredServiceBase).Assembly) // Microsoft.CodeAnalysis.Remote.ServiceHub - .Add(typeof(IRazorLanguageServerTarget).Assembly) // Microsoft.CodeAnalysis.ExternalAccess.Razor.Features + .Add(typeof(IRazorLanguageServerTarget).Assembly) // Microsoft.CodeAnalysis.ExternalAccess.Razor .Add(typeof(RemoteWorkspacesResources).Assembly) // Microsoft.CodeAnalysis.Remote.Workspaces .Add(typeof(IExtensionWorkspaceMessageHandler<,>).Assembly); // Microsoft.CodeAnalysis.ExternalAccess.Extensions @@ -37,9 +40,8 @@ private RemoteExportProviderBuilder( ImmutableArray assemblyPaths, Resolver resolver, string cacheDirectory, - string catalogPrefix, - ImmutableArray expectedErrorParts) - : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix, expectedErrorParts) + string catalogPrefix) + : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix) { } @@ -49,8 +51,7 @@ private RemoteExportProviderBuilder( assemblyPaths: RemoteHostAssemblies.SelectAsArray(static a => a.Location), resolver: new Resolver(SimpleAssemblyLoader.Instance), cacheDirectory: Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"), - catalogPrefix: "RoslynRemoteHost", - expectedErrorParts: ["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService"]); + catalogPrefix: "RoslynRemoteHost"); s_instance = await builder.CreateExportProviderAsync(cancellationToken).ConfigureAwait(false); @@ -67,6 +68,34 @@ protected override void LogTrace(string message) { } + protected override bool ContainsUnexpectedErrors(IEnumerable erroredParts, ImmutableList partDiscoveryExceptions) + { + // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. + var expectedErrorPartsSet = new HashSet(["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService", "RazorDynamicFileInfoProviderWrapper"]); + var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); + + if (hasUnexpectedErroredParts) + return true; + + return partDiscoveryExceptions.Any(partDiscoveryException => !IsKnownPartDiscoveryException(partDiscoveryException)); + } + + private static bool IsKnownPartDiscoveryException(PartDiscoveryException partDiscoveryException) + { + // Razor EA assembly has types that reference Microsoft.VisualStudio.LanguageServer.Client, which is not loadable OOP + if (partDiscoveryException.AssemblyPath == typeof(IRazorLanguageServerTarget).Assembly.Location + && partDiscoveryException.InnerException is ReflectionTypeLoadException reflectionTypeLoadException + && reflectionTypeLoadException.LoaderExceptions.Length == 1 + && reflectionTypeLoadException.LoaderExceptions[0] is FileNotFoundException fileNotFoundException + && fileNotFoundException.FileName is string fileNameNotFound + && fileNameNotFound.StartsWith("Microsoft.VisualStudio.LanguageServer.Client,")) + { + return true; + } + + return false; + } + private sealed class SimpleAssemblyLoader : IAssemblyLoader { public static readonly IAssemblyLoader Instance = new SimpleAssemblyLoader(); From e9176c9c7a7e27be9a268c8f69d16c2b00e8c9d1 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Mon, 21 Apr 2025 12:56:31 -0700 Subject: [PATCH 16/20] 1) Fix more unit tests due to razor EA 2) Try to not load assemblies during when mef cache is used --- .../Host/Mef/CodeStyleHostLanguageServices.cs | 2 +- .../Workspace/Host/Mef/MefHostServices.cs | 6 ++-- .../MEF/FeaturesTestCompositions.cs | 2 +- .../Host/RemoteExportProviderBuilder.cs | 26 +++++++++------- .../Core/Helpers/MefHostServicesHelpers.cs | 30 +++++++++++++++---- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/CodeStyle/Core/CodeFixes/Host/Mef/CodeStyleHostLanguageServices.cs b/src/CodeStyle/Core/CodeFixes/Host/Mef/CodeStyleHostLanguageServices.cs index 38d1eefe4a63b..340b67afd3947 100644 --- a/src/CodeStyle/Core/CodeFixes/Host/Mef/CodeStyleHostLanguageServices.cs +++ b/src/CodeStyle/Core/CodeFixes/Host/Mef/CodeStyleHostLanguageServices.cs @@ -49,7 +49,7 @@ private static ImmutableArray CreateAssemblies(string languageName) } return MefHostServices.DefaultAssemblies.Concat( - MefHostServicesHelpers.LoadNearbyAssemblies(assemblyNames)); + MefHostServicesHelpers.LoadNearbyAssemblies(assemblyNames.ToImmutableAndClear())); } IEnumerable> IMefHostExportProvider.GetExports() diff --git a/src/Workspaces/Core/Portable/Workspace/Host/Mef/MefHostServices.cs b/src/Workspaces/Core/Portable/Workspace/Host/Mef/MefHostServices.cs index 38a129b38e69b..f5283e66ea43a 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/Mef/MefHostServices.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/Mef/MefHostServices.cs @@ -104,7 +104,7 @@ public static ImmutableArray DefaultAssemblies // Used to build a MEF composition using the main workspaces assemblies and the known VisualBasic/CSharp workspace assemblies. // updated: includes feature assemblies since they now have public API's. - private static readonly string[] s_defaultAssemblyNames = + internal static ImmutableArray DefaultAssemblyNames = [ "Microsoft.CodeAnalysis.Workspaces", "Microsoft.CodeAnalysis.CSharp.Workspaces", @@ -117,11 +117,11 @@ public static ImmutableArray DefaultAssemblies internal static bool IsDefaultAssembly(Assembly assembly) { var name = assembly.GetName().Name; - return s_defaultAssemblyNames.Contains(name); + return DefaultAssemblyNames.Contains(name); } private static ImmutableArray LoadDefaultAssemblies() - => MefHostServicesHelpers.LoadNearbyAssemblies(s_defaultAssemblyNames); + => MefHostServicesHelpers.LoadNearbyAssemblies(DefaultAssemblyNames); #endregion diff --git a/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs b/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs index 4454997b54745..a929d0b77a49a 100644 --- a/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs +++ b/src/Workspaces/CoreTestUtilities/MEF/FeaturesTestCompositions.cs @@ -21,7 +21,7 @@ public static class FeaturesTestCompositions // We need to update tests to handle the options correctly before enabling the default provider. public static readonly TestComposition RemoteHost = TestComposition.Empty - .AddAssemblies(RemoteExportProviderBuilder.RemoteHostAssemblies) + .AddAssemblies(MefHostServicesHelpers.LoadNearbyAssemblies(RemoteExportProviderBuilder.RemoteHostAssemblyNames)) .AddParts(typeof(TestSerializerService.Factory)); public static TestComposition WithTestHostParts(this TestComposition composition, TestHost host) diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs index 6eaca448a2bce..0a72a190e032d 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs @@ -11,8 +11,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Extensions; -using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.Internal.EmbeddedLanguages; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.VisualStudio.Composition; @@ -22,13 +21,13 @@ namespace Microsoft.CodeAnalysis.Remote; internal sealed class RemoteExportProviderBuilder : ExportProviderBuilder { - internal static readonly ImmutableArray RemoteHostAssemblies = - MefHostServices.DefaultAssemblies - .Add(typeof(AspNetCoreEmbeddedLanguageClassifier).Assembly) // Microsoft.CodeAnalysis.ExternalAccess.AspNetCore - .Add(typeof(BrokeredServiceBase).Assembly) // Microsoft.CodeAnalysis.Remote.ServiceHub - .Add(typeof(IRazorLanguageServerTarget).Assembly) // Microsoft.CodeAnalysis.ExternalAccess.Razor - .Add(typeof(RemoteWorkspacesResources).Assembly) // Microsoft.CodeAnalysis.Remote.Workspaces - .Add(typeof(IExtensionWorkspaceMessageHandler<,>).Assembly); // Microsoft.CodeAnalysis.ExternalAccess.Extensions + internal static readonly ImmutableArray RemoteHostAssemblyNames = + MefHostServices.DefaultAssemblyNames + .Add("Microsoft.CodeAnalysis.ExternalAccess.AspNetCore") + .Add("Microsoft.CodeAnalysis.Remote.ServiceHub") + .Add("Microsoft.CodeAnalysis.ExternalAccess.Razor") + .Add("Microsoft.CodeAnalysis.Remote.Workspaces") + .Add("Microsoft.CodeAnalysis.ExternalAccess.Extensions"); private static ExportProvider? s_instance; internal static ExportProvider ExportProvider @@ -47,8 +46,13 @@ private RemoteExportProviderBuilder( public static async Task InitializeAsync(string localSettingsDirectory, CancellationToken cancellationToken) { + var assemblyPaths = RemoteHostAssemblyNames + .Select(static assemblyName => MefHostServicesHelpers.TryFindNearbyAssemblyLocation(assemblyName)) + .WhereNotNull() + .AsImmutable(); + var builder = new RemoteExportProviderBuilder( - assemblyPaths: RemoteHostAssemblies.SelectAsArray(static a => a.Location), + assemblyPaths: assemblyPaths, resolver: new Resolver(SimpleAssemblyLoader.Instance), cacheDirectory: Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"), catalogPrefix: "RoslynRemoteHost"); @@ -71,7 +75,7 @@ protected override void LogTrace(string message) protected override bool ContainsUnexpectedErrors(IEnumerable erroredParts, ImmutableList partDiscoveryExceptions) { // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. - var expectedErrorPartsSet = new HashSet(["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService", "RazorDynamicFileInfoProviderWrapper"]); + var expectedErrorPartsSet = new HashSet(["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService", "RazorDynamicFileInfoProviderWrapper", "RazorCSharpInterceptionMiddleLayerWrapper"]); var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); if (hasUnexpectedErroredParts) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs index bc4197cc204c1..a5ccfd0927d5b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs @@ -2,11 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Reflection; @@ -14,9 +13,9 @@ namespace Microsoft.CodeAnalysis.Host.Mef; internal static class MefHostServicesHelpers { - public static ImmutableArray LoadNearbyAssemblies(IEnumerable assemblyNames) + public static ImmutableArray LoadNearbyAssemblies(ImmutableArray assemblyNames) { - var assemblies = new List(); + var assemblies = new List(assemblyNames.Length); foreach (var assemblyName in assemblyNames) { @@ -30,12 +29,12 @@ public static ImmutableArray LoadNearbyAssemblies(IEnumerable return [.. assemblies]; } - private static Assembly TryLoadNearbyAssembly(string assemblySimpleName) + private static Assembly? TryLoadNearbyAssembly(string assemblySimpleName) { var thisAssemblyName = typeof(MefHostServicesHelpers).GetTypeInfo().Assembly.GetName(); var assemblyShortName = thisAssemblyName.Name; var assemblyVersion = thisAssemblyName.Version; - var publicKeyToken = thisAssemblyName.GetPublicKeyToken().Aggregate(string.Empty, (s, b) => s + b.ToString("x2")); + var publicKeyToken = thisAssemblyName.GetPublicKeyToken()?.Aggregate(string.Empty, (s, b) => s + b.ToString("x2")); if (string.IsNullOrEmpty(publicKeyToken)) { @@ -53,4 +52,23 @@ private static Assembly TryLoadNearbyAssembly(string assemblySimpleName) return null; } } + + public static string? TryFindNearbyAssemblyLocation(string assemblySimpleName) + { + // Try to find the assembly location by looking for a filename matching that assembly name + // at the same location as this assembly. + var thisAssemblyName = typeof(MefHostServicesHelpers).GetTypeInfo().Assembly.Location; + var thisAssemblyFolder = Path.GetDirectoryName(thisAssemblyName); + var potentialAssemblyPath = thisAssemblyFolder != null + ? Path.Combine(thisAssemblyFolder, assemblySimpleName + ".dll") + : null; + + if (File.Exists(potentialAssemblyPath)) + return potentialAssemblyPath; + + // Otherwise, fall back to loading the assembly to find the file locations + var assembly = TryLoadNearbyAssembly(assemblySimpleName); + + return assembly?.Location; + } } From 6886404e8b66fc9f94e1eeaad2a5aed40ea8fe8b Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Mon, 21 Apr 2025 19:26:05 -0700 Subject: [PATCH 17/20] Cleanup after razor EA cleanup --- .../Host/RemoteExportProviderBuilder.cs | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs index 0a72a190e032d..5e91239704227 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs @@ -12,7 +12,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.VisualStudio.Composition; using Roslyn.Utilities; @@ -75,29 +74,13 @@ protected override void LogTrace(string message) protected override bool ContainsUnexpectedErrors(IEnumerable erroredParts, ImmutableList partDiscoveryExceptions) { // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. - var expectedErrorPartsSet = new HashSet(["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "RazorTestLanguageServerFactory", "CodeFixService", "RazorDynamicFileInfoProviderWrapper", "RazorCSharpInterceptionMiddleLayerWrapper"]); + var expectedErrorPartsSet = new HashSet(["PythiaSignatureHelpProvider", "VSTypeScriptAnalyzerService", "CodeFixService"]); var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); if (hasUnexpectedErroredParts) return true; - return partDiscoveryExceptions.Any(partDiscoveryException => !IsKnownPartDiscoveryException(partDiscoveryException)); - } - - private static bool IsKnownPartDiscoveryException(PartDiscoveryException partDiscoveryException) - { - // Razor EA assembly has types that reference Microsoft.VisualStudio.LanguageServer.Client, which is not loadable OOP - if (partDiscoveryException.AssemblyPath == typeof(IRazorLanguageServerTarget).Assembly.Location - && partDiscoveryException.InnerException is ReflectionTypeLoadException reflectionTypeLoadException - && reflectionTypeLoadException.LoaderExceptions.Length == 1 - && reflectionTypeLoadException.LoaderExceptions[0] is FileNotFoundException fileNotFoundException - && fileNotFoundException.FileName is string fileNameNotFound - && fileNameNotFound.StartsWith("Microsoft.VisualStudio.LanguageServer.Client,")) - { - return true; - } - - return false; + return partDiscoveryExceptions.Count > 0); } private sealed class SimpleAssemblyLoader : IAssemblyLoader From 3020cb906cc75ddc1d544aeff2f21a31f567b5f7 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Tue, 22 Apr 2025 07:51:54 -0700 Subject: [PATCH 18/20] cleanup the cleanup --- .../Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs index 5e91239704227..c5b11d31b372b 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs @@ -80,7 +80,7 @@ protected override bool ContainsUnexpectedErrors(IEnumerable erroredPart if (hasUnexpectedErroredParts) return true; - return partDiscoveryExceptions.Count > 0); + return partDiscoveryExceptions.Count > 0; } private sealed class SimpleAssemblyLoader : IAssemblyLoader From 57da43b43203d9cab4889e58f44f29da7ac83680 Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Tue, 22 Apr 2025 11:15:17 -0700 Subject: [PATCH 19/20] 1) Rename variable 2) Add comment around IO exception expectations 3) Change AddRange => Add call --- .../LanguageServerExportProviderBuilder.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index d82d2405a822f..82eb3cf9d89d7 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -17,7 +17,7 @@ internal sealed class LanguageServerExportProviderBuilder : ExportProviderBuilde private readonly ILogger _logger; // For testing purposes, track the last cache write task. - private static Task? s_cacheWriteTask; + private static Task? s_cacheWriteTask_forTestingPurposesOnly; private LanguageServerExportProviderBuilder( ImmutableArray assemblyPaths, @@ -42,6 +42,9 @@ public static async Task CreateExportProviderAsync( // Load any Roslyn assemblies from the extension directory using var _ = ArrayBuilder.GetInstance(out var assemblyPathsBuilder); + + // Don't catch IO exceptions as it's better to fail to build the catalog than give back + // a partial catalog that will surely blow up later. assemblyPathsBuilder.AddRange(Directory.EnumerateFiles(baseDirectory, "Microsoft.CodeAnalysis*.dll")); assemblyPathsBuilder.AddRange(Directory.EnumerateFiles(baseDirectory, "Microsoft.ServiceHub*.dll")); @@ -49,7 +52,7 @@ public static async Task CreateExportProviderAsync( // and not included in ExtensionAssemblyPaths (they get loaded into the default ALC). // So manually add them to the MEF catalog here. if (devKitDependencyPath != null) - assemblyPathsBuilder.AddRange(devKitDependencyPath); + assemblyPathsBuilder.Add(devKitDependencyPath); // Add the extension assemblies to the MEF catalog. assemblyPathsBuilder.AddRange(extensionManager.ExtensionAssemblyPaths); @@ -82,7 +85,7 @@ protected override Task CreateExportProviderAsync(CancellationTo { // Clear any previous cache write task, so that it is easy to discern whether // a cache write was attempted. - s_cacheWriteTask = null; + s_cacheWriteTask_forTestingPurposesOnly = null; return base.CreateExportProviderAsync(cancellationToken); } @@ -98,9 +101,9 @@ protected override bool ContainsUnexpectedErrors(IEnumerable erroredPart protected override Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, CancellationToken cancellationToken) { - s_cacheWriteTask = base.WriteCompositionCacheAsync(compositionCacheFile, config, cancellationToken); + s_cacheWriteTask_forTestingPurposesOnly = base.WriteCompositionCacheAsync(compositionCacheFile, config, cancellationToken); - return s_cacheWriteTask; + return s_cacheWriteTask_forTestingPurposesOnly; } protected override void PerformCacheDirectoryCleanup(DirectoryInfo directoryInfo, CancellationToken cancellationToken) @@ -111,7 +114,7 @@ protected override void PerformCacheDirectoryCleanup(DirectoryInfo directoryInfo internal static class TestAccessor { #pragma warning disable VSTHRD200 // Use "Async" suffix for async methods - public static Task? GetCacheWriteTask() => s_cacheWriteTask; + public static Task? GetCacheWriteTask() => s_cacheWriteTask_forTestingPurposesOnly; #pragma warning restore VSTHRD200 // Use "Async" suffix for async methods } } From 012c2bdb2f9fefc00e6c617ae78c46d224df210d Mon Sep 17 00:00:00 2001 From: Todd Grunke Date: Tue, 22 Apr 2025 13:42:48 -0700 Subject: [PATCH 20/20] 1) Remove unuseful doc comment 2) Tuples are camel cased (blech) 3) Comment IO exception strategy 4) Use threading context in call to ServiceHubRemoteHostClient.CreateAsync --- .../Def/Remote/VisualStudioRemoteHostClientProvider.cs | 6 ++++-- src/Workspaces/Remote/Core/ExportProviderBuilder.cs | 6 ++++++ .../Remote/Core/IRemoteInitializationService.cs | 2 +- src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs | 8 ++++---- .../Initialization/RemoteInitializationService.cs | 5 +---- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/VisualStudio/Core/Def/Remote/VisualStudioRemoteHostClientProvider.cs b/src/VisualStudio/Core/Def/Remote/VisualStudioRemoteHostClientProvider.cs index 9ad95766f4178..76e12659cbadb 100644 --- a/src/VisualStudio/Core/Def/Remote/VisualStudioRemoteHostClientProvider.cs +++ b/src/VisualStudio/Core/Def/Remote/VisualStudioRemoteHostClientProvider.cs @@ -100,6 +100,7 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) private readonly AsynchronousOperationListenerProvider _listenerProvider; private readonly RemoteServiceCallbackDispatcherRegistry _callbackDispatchers; private readonly TaskCompletionSource _clientCreationSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly IThreadingContext _threadingContext; private VisualStudioRemoteHostClientProvider( SolutionServices services, @@ -116,10 +117,11 @@ private VisualStudioRemoteHostClientProvider( _serviceProvider = serviceProvider; _listenerProvider = listenerProvider; _callbackDispatchers = callbackDispatchers; + _threadingContext = threadingContext; // using VS AsyncLazy here since Roslyn's is not compatible with JTF. // Our ServiceBroker services may be invoked by other VS components under JTF. - _lazyClient = new VSThreading.AsyncLazy(CreateHostClientAsync, threadingContext.JoinableTaskFactory); + _lazyClient = new VSThreading.AsyncLazy(CreateHostClientAsync, _threadingContext.JoinableTaskFactory); } private async Task CreateHostClientAsync() @@ -134,7 +136,7 @@ private VisualStudioRemoteHostClientProvider( var localSettingsDirectory = new ShellSettingsManager(_serviceProvider).GetApplicationDataFolder(ApplicationDataFolder.LocalSettings); // VS AsyncLazy does not currently support cancellation: - var client = await ServiceHubRemoteHostClient.CreateAsync(Services, configuration, localSettingsDirectory, _listenerProvider, serviceBroker, _callbackDispatchers, CancellationToken.None).ConfigureAwait(false); + var client = await ServiceHubRemoteHostClient.CreateAsync(Services, configuration, localSettingsDirectory, _listenerProvider, serviceBroker, _callbackDispatchers, _threadingContext.DisposalToken).ConfigureAwait(false); // proffer in-proc brokered services: _ = brokeredServiceContainer.Proffer(SolutionAssetProvider.ServiceDescriptor, (_, _, _, _) => ValueTaskFactory.FromResult(new SolutionAssetProvider(Services))); diff --git a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs index a938b1a5dccfc..2eb905ffb7314 100644 --- a/src/Workspaces/Remote/Core/ExportProviderBuilder.cs +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -140,6 +140,10 @@ static string ComputeAssemblyHash(ImmutableArray assemblyPaths) protected virtual async Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, CancellationToken cancellationToken) { + // Generally, it's not a hard failure if this code doesn't execute to completion or even fails. The end effect would simply + // either be a non-existent or invalid file cached to disk. In the case of the file not getting cached, the next VS session + // will just detect the file doesn't exist and attempt to recreate the cache. In the case where the cached file contents are + // invalid, the next VS session will throw when attempting to read in the cached contents, and again, just recreate the cache. try { await Task.Yield().ConfigureAwait(false); @@ -168,6 +172,8 @@ protected virtual void PerformCacheDirectoryCleanup(DirectoryInfo directoryInfo, // Delete any existing cached files. foreach (var fileInfo in directoryInfo.EnumerateFiles()) { + // Failing to delete any file is fine, we'll just try again the next VS session in which we attempt + // to write a new cache IOUtilities.PerformIO(fileInfo.Delete); cancellationToken.ThrowIfCancellationRequested(); } diff --git a/src/Workspaces/Remote/Core/IRemoteInitializationService.cs b/src/Workspaces/Remote/Core/IRemoteInitializationService.cs index 9f3bc49fd8183..547e09a8262f0 100644 --- a/src/Workspaces/Remote/Core/IRemoteInitializationService.cs +++ b/src/Workspaces/Remote/Core/IRemoteInitializationService.cs @@ -15,5 +15,5 @@ internal interface IRemoteInitializationService /// Called as soon as the remote process is created. /// /// Process ID of the remote process and an error message if the server encountered initialization issues. - ValueTask<(int ProcessId, string? ErrorMessage)> InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken); + ValueTask<(int processId, string? errorMessage)> InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken); } diff --git a/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs b/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs index a67be6534d8a2..085b92694ef20 100644 --- a/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs +++ b/src/Workspaces/Remote/Core/ServiceHubRemoteHostClient.cs @@ -73,18 +73,18 @@ public static async Task CreateAsync( var workspaceConfigurationService = services.GetRequiredService(); - var remoteProcessIdAndErrorMessage = await client.TryInvokeAsync( + var remoteProcessIdAndErrorMessage = await client.TryInvokeAsync( (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, localSettingsDirectory, cancellationToken), cancellationToken).ConfigureAwait(false); if (remoteProcessIdAndErrorMessage.HasValue) { - if (remoteProcessIdAndErrorMessage.Value.ErrorMessage != null) - hubClient.Logger.TraceEvent(TraceEventType.Error, 1, $"ServiceHub initialization error: {remoteProcessIdAndErrorMessage.Value.ErrorMessage}"); + if (remoteProcessIdAndErrorMessage.Value.errorMessage != null) + hubClient.Logger.TraceEvent(TraceEventType.Error, 1, $"ServiceHub initialization error: {remoteProcessIdAndErrorMessage.Value.errorMessage}"); try { - client._remoteProcess = Process.GetProcessById(remoteProcessIdAndErrorMessage.Value.ProcessId); + client._remoteProcess = Process.GetProcessById(remoteProcessIdAndErrorMessage.Value.processId); } catch (Exception e) { diff --git a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs index baae37fe41d66..a6ac637812202 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs @@ -20,10 +20,7 @@ protected override IRemoteInitializationService CreateService(in ServiceConstruc => new RemoteInitializationService(arguments); } - /// - /// Remote API. - /// - public async ValueTask<(int ProcessId, string? ErrorMessage)> InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken) + public async ValueTask<(int processId, string? errorMessage)> InitializeAsync(WorkspaceConfigurationOptions options, string localSettingsDirectory, CancellationToken cancellationToken) { // Performed before RunServiceAsync to ensure that the export provider is initialized before the RemoteWorkspaceManager is created // as part of the RunServiceAsync call.