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/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/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs index 7148ec5e9a274..85d5d3d542de9 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ExportProviderBuilderTests.cs @@ -148,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; @@ -156,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 9667713fc60bc..9215dfc08b82b 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs @@ -33,6 +33,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/ExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs deleted file mode 100644 index 6f8f7fbd154a7..0000000000000 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs +++ /dev/null @@ -1,229 +0,0 @@ -// 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.Hashing; -using System.Text; -using Microsoft.CodeAnalysis.LanguageServer.Logging; -using Microsoft.CodeAnalysis.LanguageServer.Services; -using Microsoft.CodeAnalysis.Shared.Collections; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.Composition; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.LanguageServer; - -internal sealed class ExportProviderBuilder -{ - // For testing purposes, track the last cache write task. - private static Task? _cacheWriteTask; - - public static async Task CreateExportProviderAsync( - ExtensionAssemblyManager extensionManager, - IAssemblyLoader assemblyLoader, - string? devKitDependencyPath, - string cacheDirectory, - ILoggerFactory loggerFactory) - { - // 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); - - // 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, - string cacheDirectory, - ILogger logger) - { - // Create a MEF resolver that can resolve assemblies in the extension contexts. - var resolver = new Resolver(assemblyLoader); - - var compositionCacheFile = GetCompositionCacheFilePath(cacheDirectory, assemblyPaths); - - // Try to load a cached composition. - try - { - if (File.Exists(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); - - return exportProviderFactory; - } - } - 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.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 catalog = ComposableCatalog.Create(resolver) - .AddParts(await discovery.CreatePartsAsync(assemblyPaths)) - .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import - - // Assemble the parts into a valid graph. - var config = CompositionConfiguration.Create(catalog); - - // Verify we only have expected errors. - ThrowOnUnexpectedErrors(config, catalog, logger); - - // Try to cache the composition. - _cacheWriteTask = WriteCompositionCacheAsync(compositionCacheFile, config, logger).ReportNonFatalErrorAsync(); - - // Prepare an ExportProvider factory based on this graph. - return config.CreateExportProviderFactory(); - } - - private static string GetCompositionCacheFilePath(string cacheDirectory, 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, $"c#-languageserver.{ComputeAssemblyHash(assemblyPaths)}.mef-composition"); - - static string ComputeAssemblyHash(ImmutableArray assemblyPaths) - { - // Ensure AssemblyPaths are always in the same order. - assemblyPaths = assemblyPaths.Sort(); - - var assemblies = new StringBuilder(); - 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); - // 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")); - } - - var hash = XxHash128.Hash(Encoding.UTF8.GetBytes(assemblies.ToString())); - // Convert to filename safe base64 string. - return Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').TrimEnd('='); - } - } - - private static async Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, ILogger logger) - { - try - { - await Task.Yield(); - - if (Path.GetDirectoryName(compositionCacheFile) is string directory) - { - Directory.CreateDirectory(directory); - } - - 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); - } - - File.Move(tempFilePath, compositionCacheFile, overwrite: true); - } - catch (Exception ex) - { - logger.LogError($"Failed to save MEF cache: {ex}"); - } - } - - 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.Copilot.Internal.CodeMapper.CSharpMapCodeService.ctor(service): expected exactly 1 export matching constraints: - // Contract name: Microsoft.CodeAnalysis.ExternalAccess.Copilot.CodeMapper.ICSharpCopilotMapCodeService - // TypeIdentityName: Microsoft.CodeAnalysis.ExternalAccess.Copilot.CodeMapper.ICSharpCopilotMapCodeService - // but found 0. - // part definition Microsoft.CodeAnalysis.ExternalAccess.Copilot.Internal.CodeMapper.CSharpMapCodeService - // - // 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 - // - // Microsoft.CodeAnalysis.ExternalAccess.Copilot.Internal.SemanticSearch.CopilotSemanticSearchQueryExecutor.ctor(workspaceProvider): expected exactly 1 export matching constraints: - // Contract name: Microsoft.CodeAnalysis.Host.IHostWorkspaceProvider - // TypeIdentityName: Microsoft.CodeAnalysis.Host.IHostWorkspaceProvider - // but found 0. - // part definition Microsoft.CodeAnalysis.ExternalAccess.Copilot.Internal.SemanticSearch.CopilotSemanticSearchQueryExecutor - - var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? []; - var expectedErroredParts = new string[] { "CSharpMapCodeService", "PythiaSignatureHelpProvider", "CopilotSemanticSearchQueryExecutor" }; - var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErroredParts.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 - logger.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() => _cacheWriteTask; -#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods - } -} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs new file mode 100644 index 0000000000000..82eb3cf9d89d7 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -0,0 +1,120 @@ +// 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.PooledObjects; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; + +namespace Microsoft.CodeAnalysis.LanguageServer; + +internal sealed class LanguageServerExportProviderBuilder : ExportProviderBuilder +{ + private readonly ILogger _logger; + + // For testing purposes, track the last cache write task. + private static Task? s_cacheWriteTask_forTestingPurposesOnly; + + private LanguageServerExportProviderBuilder( + ImmutableArray assemblyPaths, + Resolver resolver, + string cacheDirectory, + string catalogPrefix, + ILoggerFactory loggerFactory) + : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix) + { + _logger = loggerFactory.CreateLogger(); + } + + 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 + 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")); + + // 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) + assemblyPathsBuilder.Add(devKitDependencyPath); + + // Add the extension assemblies to the MEF catalog. + assemblyPathsBuilder.AddRange(extensionManager.ExtensionAssemblyPaths); + + // Create a MEF resolver that can resolve assemblies in the extension contexts. + var builder = new LanguageServerExportProviderBuilder( + assemblyPathsBuilder.ToImmutableAndClear(), + new Resolver(assemblyLoader), + cacheDirectory, + catalogPrefix: "c#-languageserver", + 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); + + // Immediately set the logger factory, so that way it'll be available for the rest of the composition + exportProvider.GetExportedValue().SetFactory(loggerFactory); + + 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_forTestingPurposesOnly = null; + + 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_forTestingPurposesOnly = base.WriteCompositionCacheAsync(compositionCacheFile, config, cancellationToken); + + return s_cacheWriteTask_forTestingPurposesOnly; + } + + 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_forTestingPurposesOnly; +#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs index 56c2d092bf6fe..779bb65eea742 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; @@ -19,7 +18,6 @@ using Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; 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 @@ -98,7 +96,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..76e12659cbadb 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,14 +96,17 @@ 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); + private readonly IThreadingContext _threadingContext; private VisualStudioRemoteHostClientProvider( SolutionServices services, IGlobalOptionService globalOptions, IVsService brokeredServiceContainer, + IServiceProvider serviceProvider, IThreadingContext threadingContext, AsynchronousOperationListenerProvider listenerProvider, RemoteServiceCallbackDispatcherRegistry callbackDispatchers) @@ -105,12 +114,14 @@ private VisualStudioRemoteHostClientProvider( Services = services; _globalOptions = globalOptions; _brokeredServiceContainer = brokeredServiceContainer; + _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() @@ -122,9 +133,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, _threadingContext.DisposalToken).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..bd7e8d7d6ff42 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), + _ = 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), + _ = 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), + _ = await client.TryInvokeAsync( + (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, TempRoot.Root, cancellationToken), CancellationToken.None).ConfigureAwait(false); var solution = workspace.CurrentSolution; @@ -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/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/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/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs index 095adae521835..ed6cc9b72ee9d 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), + _ = 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..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(RemoteWorkspaceManager.RemoteHostAssemblies) + .AddAssemblies(MefHostServicesHelpers.LoadNearbyAssemblies(RemoteExportProviderBuilder.RemoteHostAssemblyNames)) .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/Workspaces/Remote/Core/ExportProviderBuilder.cs b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs new file mode 100644 index 0000000000000..2eb905ffb7314 --- /dev/null +++ b/src/Workspaces/Remote/Core/ExportProviderBuilder.cs @@ -0,0 +1,204 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +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; + +namespace Microsoft.CodeAnalysis.Remote; + +internal abstract class ExportProviderBuilder( + ImmutableArray assemblyPaths, + Resolver resolver, + string cacheDirectory, + string catalogPrefix) +{ + private const string CatalogSuffix = ".mef-composition"; + + protected ImmutableArray AssemblyPaths { get; } = assemblyPaths; + protected Resolver Resolver { get; } = resolver; + protected string CacheDirectory { get; } = cacheDirectory; + protected string CatalogPrefix { get; } = catalogPrefix; + + 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(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(); + + return exportProvider; + } + + private async Task GetCompositionConfigurationAsync(CancellationToken cancellationToken) + { + // Determine the path to the MEF composition cache file for the given assembly paths. + var compositionCacheFile = GetCompositionCacheFilePath(); + + // Try to load a cached composition. + try + { + if (File.Exists(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, Resolver, cancellationToken).ConfigureAwait(false); + + return exportProviderFactory; + } + } + catch (Exception ex) + { + // Log the error, and move on to recover by recreating the MEF composition. + LogError($"Loading cached MEF composition failed: {ex}"); + } + + 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(parts) + .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import + + // Assemble the parts into a valid graph. + var config = CompositionConfiguration.Create(catalog); + + // Verify we only have expected errors. + + ThrowOnUnexpectedErrors(config, catalog); + + // Try to cache the composition. + _ = WriteCompositionCacheAsync(compositionCacheFile, config, 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 string GetCompositionCacheFilePath() + { + return Path.Combine(CacheDirectory, $"{CatalogPrefix}.{ComputeAssemblyHash(AssemblyPaths)}{CatalogSuffix}"); + + static string ComputeAssemblyHash(ImmutableArray assemblyPaths) + { + // Ensure AssemblyPaths are always in the same order. + assemblyPaths = assemblyPaths.Sort(StringComparer.Ordinal); + + 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. + 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. + hashContents.Append(File.GetLastWriteTimeUtc(assemblyPath).ToString("F")); + } + + // Create base64 string of the hash. + var hashAsBase64String = Checksum.Create(hashContents.ToString()).ToString(); + + // Convert to filename safe base64 string. + return hashAsBase64String.Replace('+', '-').Replace('/', '_').TrimEnd('='); + } + } + + 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); + + var directory = Path.GetDirectoryName(compositionCacheFile)!; + var directoryInfo = Directory.CreateDirectory(directory); + PerformCacheDirectoryCleanup(directoryInfo, cancellationToken); + + CachedComposition cachedComposition = new(); + 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); + } + + File.Move(tempFilePath, compositionCacheFile); + } + catch (Exception 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()) + { + // 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(); + } + } + + 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/Core/IRemoteInitializationService.cs b/src/Workspaces/Remote/Core/IRemoteInitializationService.cs new file mode 100644 index 0000000000000..547e09a8262f0 --- /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. + /// + /// 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/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..085b92694ef20 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,15 +73,18 @@ public static async Task CreateAsync( var workspaceConfigurationService = services.GetRequiredService(); - var remoteProcessId = await client.TryInvokeAsync( - (service, cancellationToken) => service.InitializeAsync(workspaceConfigurationService.Options, cancellationToken), + 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 initialization 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/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/RemoteExportProviderBuilder.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs new file mode 100644 index 0000000000000..c5b11d31b372b --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteExportProviderBuilder.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Composition; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Remote; + +internal sealed class RemoteExportProviderBuilder : ExportProviderBuilder +{ + 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 + => s_instance ?? throw new InvalidOperationException($"Default export provider not initialized. Call {nameof(InitializeAsync)} first."); + + private StringBuilder? _errorMessages; + + private RemoteExportProviderBuilder( + ImmutableArray assemblyPaths, + Resolver resolver, + string cacheDirectory, + string catalogPrefix) + : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix) + { + } + + 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: assemblyPaths, + resolver: new Resolver(SimpleAssemblyLoader.Instance), + cacheDirectory: Path.Combine(localSettingsDirectory, "Roslyn", "RemoteHost", "Cache"), + catalogPrefix: "RoslynRemoteHost"); + + 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) + { + } + + 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", "CodeFixService"]); + var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); + + if (hasUnexpectedErroredParts) + return true; + + return partDiscoveryExceptions.Count > 0; + } + + 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)) + { + // 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 + } + + return LoadAssembly(assemblyName); + } + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs index 274e260b057f2..29b8ed058f4cc 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. @@ -59,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; @@ -78,27 +71,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 = RemoteExportProviderBuilder.ExportProvider; return new RemoteWorkspace(VisualStudioMefHostServices.Create(exportProvider)); } @@ -143,25 +118,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..a6ac637812202 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/Initialization/RemoteInitializationService.cs @@ -0,0 +1,39 @@ +// 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); + } + + 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. + var errorMessage = await RemoteExportProviderBuilder.InitializeAsync(localSettingsDirectory, cancellationToken).ConfigureAwait(false); + + 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); + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Services/ProcessTelemetry/RemoteProcessTelemetryService.cs b/src/Workspaces/Remote/ServiceHub/Services/ProcessTelemetry/RemoteProcessTelemetryService.cs index 6a8226a095a76..8bc8e89d4c6d0 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); - } } 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; + } }