File: LanguageServerExportProviderBuilder.cs
Web Access
Project: src\src\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj (Microsoft.CodeAnalysis.LanguageServer)
// 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<ExportProviderBuilder> _logger;
 
    // For testing purposes, track the last cache write task.
    private static Task? s_cacheWriteTask_forTestingPurposesOnly;
 
    private LanguageServerExportProviderBuilder(
        ImmutableArray<string> assemblyPaths,
        Resolver resolver,
        string cacheDirectory,
        string catalogPrefix,
        ILoggerFactory loggerFactory)
        : base(assemblyPaths, resolver, cacheDirectory, catalogPrefix)
    {
        _logger = loggerFactory.CreateLogger<ExportProviderBuilder>();
    }
 
    public static async Task<ExportProvider> 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<string>.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<ExtensionAssemblyManagerMefProvider>().SetMefExtensionAssemblyManager(extensionManager);
 
        // Immediately set the logger factory, so that way it'll be available for the rest of the composition
        exportProvider.GetExportedValue<ServerLoggerFactory>().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<ExportProvider> 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<string> erroredParts, ImmutableList<PartDiscoveryException> 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<string>(["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
    }
}