File: MetadataAsSource\MetadataAsSourceFileService.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Composition;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Structure;
using Microsoft.CodeAnalysis.SymbolMapping;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.MetadataAsSource;
 
[Export(typeof(IMetadataAsSourceFileService)), Shared]
internal sealed class MetadataAsSourceFileService : IMetadataAsSourceFileService
{
    private const string MetadataAsSource = nameof(MetadataAsSource);
 
    /// <summary>
    /// Set of providers that can be used to generate source for a symbol (for example, by decompiling, or by
    /// extracting it from a pdb).
    /// </summary>
    private readonly Lazy<ImmutableArray<Lazy<IMetadataAsSourceFileProvider, MetadataAsSourceFileProviderMetadata>>> _providers;
 
    /// <summary>
    /// Workspace created the first time we generate any metadata for any symbol.
    /// </summary>
    private MetadataAsSourceWorkspace? _workspace;
 
    /// <summary>
    /// A lock to ensure we initialize <see cref="_workspace"/> and cleanup stale data only once.
    /// </summary>
    private readonly SemaphoreSlim _gate = new(initialCount: 1);
 
    /// <summary>
    /// We create a mutex so other processes can see if our directory is still alive.  As long as we own the mutex, no
    /// other VS instance will try to delete our _rootTemporaryPathWithGuid folder.
    /// </summary>
    private readonly Mutex _mutex;
    private readonly string _rootTemporaryPathWithGuid;
    private readonly string _rootTemporaryPath = Path.Combine(Path.GetTempPath(), MetadataAsSource);
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public MetadataAsSourceFileService(
        [ImportMany] IEnumerable<Lazy<IMetadataAsSourceFileProvider, MetadataAsSourceFileProviderMetadata>> providers)
    {
        _providers = new(() => [.. ExtensionOrderer.Order(providers)]);
 
        var guidString = Guid.NewGuid().ToString("N");
        _rootTemporaryPathWithGuid = Path.Combine(_rootTemporaryPath, guidString);
        _mutex = new Mutex(initiallyOwned: true, name: CreateMutexName(guidString));
    }
 
    private static string CreateMutexName(string directoryName)
        => $"{MetadataAsSource}-{directoryName}";
 
    public async Task<MetadataAsSourceFile> GetGeneratedFileAsync(
        Workspace sourceWorkspace,
        Project sourceProject,
        ISymbol symbol,
        bool signaturesOnly,
        MetadataAsSourceOptions options,
        CancellationToken cancellationToken)
    {
        if (sourceProject == null)
            throw new ArgumentNullException(nameof(sourceProject));
 
        if (symbol == null)
            throw new ArgumentNullException(nameof(symbol));
 
        if (symbol.Kind == SymbolKind.Namespace)
            throw new ArgumentException(FeaturesResources.symbol_cannot_be_a_namespace, nameof(symbol));
 
        symbol = symbol.GetOriginalUnreducedDefinition();
 
        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
        {
            if (_workspace is null)
            {
                _workspace = new MetadataAsSourceWorkspace(this, sourceWorkspace.Services.HostServices);
 
                // We're being initialized the first time.  Use this time to clean up any stale metadata-as-source files
                // from previous VS sessions.
                CleanupGeneratedFiles(_rootTemporaryPath);
            }
 
            Contract.ThrowIfNull(_workspace);
 
            // We don't want to track telemetry for signatures only requests, only where we try to show source
            using var telemetryMessage = signaturesOnly ? null : new TelemetryMessage(cancellationToken);
 
            foreach (var lazyProvider in _providers.Value)
            {
                var provider = lazyProvider.Value;
                var providerTempPath = Path.Combine(_rootTemporaryPathWithGuid, provider.GetType().Name);
                var result = await provider.GetGeneratedFileAsync(_workspace, sourceWorkspace, sourceProject, symbol, signaturesOnly, options, providerTempPath, telemetryMessage, cancellationToken).ConfigureAwait(false);
                if (result is not null)
                    return result;
            }
        }
 
        // The decompilation provider can always return something
        throw ExceptionUtilities.Unreachable();
 
        static void CleanupGeneratedFiles(string rootDirectory)
        {
            try
            {
                if (Directory.Exists(rootDirectory))
                {
                    // Let's look through directories to delete.
                    foreach (var directoryInfo in new DirectoryInfo(rootDirectory).EnumerateDirectories())
                    {
                        // Is there a mutex for this one?  If so, that means it's a folder open in another VS instance.
                        // We should leave it alone.  If not, then it's a folder from a previous VS run.  Delete that
                        // now.
                        if (Mutex.TryOpenExisting(CreateMutexName(directoryInfo.Name), out var acquiredMutex))
                        {
                            acquiredMutex.Dispose();
                        }
                        else
                        {
                            TryDeleteFolderWhichContainsReadOnlyFiles(directoryInfo.FullName);
                        }
                    }
                }
            }
            catch (Exception)
            {
            }
        }
 
        static void TryDeleteFolderWhichContainsReadOnlyFiles(string directoryPath)
        {
            try
            {
                foreach (var fileInfo in new DirectoryInfo(directoryPath).EnumerateFiles("*", SearchOption.AllDirectories))
                    IOUtilities.PerformIO(() => fileInfo.IsReadOnly = false);
 
                IOUtilities.PerformIO(() => Directory.Delete(directoryPath, recursive: true));
            }
            catch (Exception)
            {
            }
        }
    }
 
    private static void AssertIsMainThread(MetadataAsSourceWorkspace workspace)
    {
        var threadingService = workspace.Services.GetRequiredService<IWorkspaceThreadingServiceProvider>().Service;
        Contract.ThrowIfFalse(threadingService.IsOnMainThread);
    }
 
    public bool TryAddDocumentToWorkspace(string filePath, SourceTextContainer sourceTextContainer, [NotNullWhen(true)] out DocumentId? documentId)
    {
        // If we haven't even created a MetadataAsSource workspace yet, then this file definitely cannot be added to
        // it. This happens when the MiscWorkspace calls in to just see if it can attach this document to the
        // MetadataAsSource instead of itself.
        var workspace = _workspace;
        if (workspace != null)
        {
            foreach (var provider in _providers.Value)
            {
                if (!provider.IsValueCreated)
                    continue;
 
                if (provider.Value.TryAddDocumentToWorkspace(workspace, filePath, sourceTextContainer, out documentId))
                {
                    return true;
                }
            }
        }
 
        documentId = null;
        return false;
    }
 
    public bool TryRemoveDocumentFromWorkspace(string filePath)
    {
        // If we haven't even created a MetadataAsSource workspace yet, then this file definitely cannot be removed
        // from it. This happens when the MiscWorkspace is hearing about a doc closing, and calls into the
        // MetadataAsSource system to see if it owns the file and should handle that event.
        var workspace = _workspace;
        if (workspace != null)
        {
            foreach (var provider in _providers.Value)
            {
                if (!provider.IsValueCreated)
                    continue;
 
                if (provider.Value.TryRemoveDocumentFromWorkspace(workspace, filePath))
                    return true;
            }
        }
 
        return false;
    }
 
    public bool ShouldCollapseOnOpen(string? filePath, BlockStructureOptions blockStructureOptions)
    {
        if (filePath is null)
            return false;
 
        var workspace = _workspace;
 
        if (workspace == null)
        {
            try
            {
                throw new InvalidOperationException(
                    $"'{nameof(ShouldCollapseOnOpen)}' should only be called once outlining has already confirmed that '{filePath}' is from the {nameof(MetadataAsSourceWorkspace)}");
            }
            catch (Exception ex) when (FatalError.ReportAndCatch(ex))
            {
            }
 
            return false;
        }
 
        AssertIsMainThread(workspace);
 
        foreach (var provider in _providers.Value)
        {
            if (!provider.IsValueCreated)
                continue;
 
            if (provider.Value.ShouldCollapseOnOpen(workspace, filePath, blockStructureOptions))
                return true;
        }
 
        return false;
    }
 
    internal async Task<SymbolMappingResult?> MapSymbolAsync(Document document, SymbolKey symbolId, CancellationToken cancellationToken)
    {
        Contract.ThrowIfNull(document.FilePath);
 
        Project? project = null;
        using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
        {
            foreach (var provider in _providers.Value)
            {
                if (!provider.IsValueCreated)
                    continue;
 
                Contract.ThrowIfNull(_workspace);
 
                project = provider.Value.MapDocument(document);
                if (project is not null)
                    break;
            }
        }
 
        if (project is null)
            return null;
 
        var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
        var resolutionResult = symbolId.Resolve(compilation, ignoreAssemblyKey: true, cancellationToken: cancellationToken);
        if (resolutionResult.Symbol == null)
            return null;
 
        return new SymbolMappingResult(project, resolutionResult.Symbol);
    }
 
    public bool IsNavigableMetadataSymbol(ISymbol symbol)
    {
        symbol = symbol.OriginalDefinition;
 
        if (!symbol.Locations.Any(static l => l.IsInMetadata))
        {
            return false;
        }
 
        switch (symbol.Kind)
        {
            case SymbolKind.Event:
            case SymbolKind.Field:
            case SymbolKind.Method:
            case SymbolKind.NamedType:
            case SymbolKind.Property:
            case SymbolKind.Parameter:
                return true;
        }
 
        return false;
    }
 
    public Workspace? TryGetWorkspace() => _workspace;
}