File: DiagnosticAnalyzer\ShadowCopyAnalyzerPathResolver.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// 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.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Roslyn.Utilities;
using System.Collections.Immutable;
using System.Reflection;
using System.Globalization;
using System.Diagnostics;
 
namespace Microsoft.CodeAnalysis
{
    internal sealed class ShadowCopyAnalyzerPathResolver : IAnalyzerPathResolver
    {
        /// <summary>
        /// The base directory for shadow copies. Each instance of
        /// <see cref="ShadowCopyAnalyzerPathResolver"/> gets its own
        /// subdirectory under this directory. This is also the starting point
        /// for scavenge operations.
        /// </summary>
        internal string BaseDirectory { get; }
 
        internal string ShadowDirectory { get; }
 
        /// <summary>
        /// As long as this mutex is alive, other instances of this type will not try to clean
        /// up the shadow directory.
        /// </summary>
        private Mutex Mutex { get; }
 
        internal Task DeleteLeftoverDirectoriesTask { get; }
 
        /// <summary>
        /// This is a counter that is incremented each time a new shadow sub directory is created to ensure they 
        /// have unique names.
        /// </summary>
        private int _directoryCount;
 
        /// <summary>
        /// This is a map from the original directory name to the numbered directory name it 
        /// occupies in the shadow directory.
        /// </summary>
        private ConcurrentDictionary<string, int> OriginalDirectoryMap { get; } = new(AnalyzerAssemblyLoader.OriginalPathComparer);
 
        /// <summary>
        /// This interface can be called from multiple threads for the same original assembly path. This
        /// is a map between the original path and the Task that completes when the shadow copy for that
        /// original path completes.
        /// </summary>
        private ConcurrentDictionary<string, Task<string>> CopyMap { get; } = new(AnalyzerAssemblyLoader.OriginalPathComparer);
 
        /// <summary>
        /// This is the number of shadow copies that have occurred in this instance.
        /// </summary>
        /// <remarks>
        /// This is used for testing, it should not be used for any other purpose.
        /// </remarks>
        internal int CopyCount => CopyMap.Count;
 
        public ShadowCopyAnalyzerPathResolver(string baseDirectory)
        {
            if (baseDirectory is null)
            {
                throw new ArgumentNullException(nameof(baseDirectory));
            }
 
            // The shadow copy analyzer should only be created on Windows. To create on Linux we cannot use 
            // GetTempPath as it's not per-user. Generally there is no need as LoadFromStream achieves the same
            // effect
            if (!Path.IsPathRooted(baseDirectory))
            {
                throw new ArgumentException($"Must be a full path: {baseDirectory}", nameof(baseDirectory));
            }
 
            BaseDirectory = baseDirectory;
            var shadowDirectoryName = Guid.NewGuid().ToString("N").ToLowerInvariant();
 
            // The directory is deliberately _not_ created at this point. It will only be created when the first
            // request comes in. This avoids creating unnecessary directories when no analyzers are loaded 
            // via the shadow layer.
            ShadowDirectory = Path.Combine(BaseDirectory, shadowDirectoryName);
            Mutex = new Mutex(initiallyOwned: false, name: shadowDirectoryName);
            DeleteLeftoverDirectoriesTask = Task.Run(DeleteLeftoverDirectories);
        }
 
        private void DeleteLeftoverDirectories()
        {
            // Avoid first chance exception
            if (!Directory.Exists(BaseDirectory))
                return;
 
            IEnumerable<string> subDirectories;
            try
            {
                subDirectories = Directory.EnumerateDirectories(BaseDirectory);
            }
            catch (DirectoryNotFoundException)
            {
                return;
            }
 
            foreach (var subDirectory in subDirectories)
            {
                string name = Path.GetFileName(subDirectory).ToLowerInvariant();
                Mutex? mutex = null;
                try
                {
                    // We only want to try deleting the directory if no-one else is currently
                    // using it. That is, if there is no corresponding mutex.
                    if (!Mutex.TryOpenExisting(name, out mutex))
                    {
                        try
                        {
                            // Avoid calling ClearReadOnlyFlagOnFiles before calling Directory.Delete. In general, files
                            // created by the shadow copy should not be marked read-only (CopyFile also clears the
                            // read-only flag), and clearing the read-only flag for the entire directory requires
                            // significant disk access.
                            //
                            // If the deletion fails for an IOException, it may have been the result of a file being
                            // marked read-only. We catch that exception and perform an explicit clear before trying
                            // again.
                            Directory.Delete(subDirectory, recursive: true);
                        }
                        catch (IOException)
                        {
                            // Retry after clearing the read-only flag
                            ClearReadOnlyFlagOnFiles(subDirectory);
                            Directory.Delete(subDirectory, recursive: true);
                        }
                    }
                }
                catch
                {
                    // If something goes wrong we will leave it to the next run to clean up.
                    // Just swallow the exception and move on.
                }
                finally
                {
                    if (mutex != null)
                    {
                        mutex.Dispose();
                    }
                }
            }
        }
 
        public bool IsAnalyzerPathHandled(string analyzerFilePath) => true;
 
        public string GetResolvedAnalyzerPath(string originalAnalyzerPath)
        {
            var analyzerShadowDir = GetAnalyzerShadowDirectory(originalAnalyzerPath);
            var analyzerShadowPath = Path.Combine(analyzerShadowDir, Path.GetFileName(originalAnalyzerPath));
            ShadowCopyFile(originalAnalyzerPath, analyzerShadowPath);
            return analyzerShadowPath;
        }
 
        public string? GetResolvedSatellitePath(string originalAnalyzerPath, CultureInfo cultureInfo)
        {
            var satelliteFilePath = AnalyzerAssemblyLoader.GetSatelliteAssemblyPath(originalAnalyzerPath, cultureInfo);
            if (satelliteFilePath is null)
            {
                return null;
            }
 
            var analyzerShadowDir = GetAnalyzerShadowDirectory(originalAnalyzerPath);
            var satelliteFileName = Path.GetFileName(satelliteFilePath);
            var satelliteDirectoryName = Path.GetFileName(Path.GetDirectoryName(satelliteFilePath));
            var shadowSatellitePath = Path.Combine(analyzerShadowDir, satelliteDirectoryName!, satelliteFileName);
            ShadowCopyFile(satelliteFilePath, shadowSatellitePath);
            return shadowSatellitePath;
        }
 
        /// <summary>
        /// Get the shadow directory for the given original analyzer file path.
        /// </summary>
        private string GetAnalyzerShadowDirectory(string analyzerFilePath)
        {
            var originalDirName = Path.GetDirectoryName(analyzerFilePath)!;
            var shadowDirName = OriginalDirectoryMap.GetOrAdd(originalDirName, _ => Interlocked.Increment(ref _directoryCount)).ToString();
            return Path.Combine(ShadowDirectory, shadowDirName);
        }
 
        /// <summary>
        /// This type has to account for multiple threads calling into the various resolver APIs. To avoid two threads
        /// writing at the same time this method is used to ensure only one thread _wins_ and both can wait for 
        /// that thread to complete the copy.
        /// </summary>
        private void ShadowCopyFile(string originalFilePath, string shadowCopyPath)
        {
            if (CopyMap.TryGetValue(originalFilePath, out var copyTask))
            {
                copyTask.Wait();
                return;
            }
 
            var tcs = new TaskCompletionSource<string>();
            var task = CopyMap.GetOrAdd(originalFilePath, tcs.Task);
            if (object.ReferenceEquals(task, tcs.Task))
            {
                // This thread won and we need to do the copy.
                try
                {
                    copyFile(originalFilePath, shadowCopyPath);
                    tcs.SetResult(shadowCopyPath);
                }
                catch (Exception ex)
                {
                    tcs.SetException(ex);
                    throw;
                }
            }
            else
            {
                // This thread lost and we need to wait for the winner to finish the copy.
                task.Wait();
                Debug.Assert(AnalyzerAssemblyLoader.GeneratedPathComparer.Equals(shadowCopyPath, task.Result));
            }
 
            static void copyFile(string originalPath, string shadowCopyPath)
            {
                var directory = Path.GetDirectoryName(shadowCopyPath);
                if (directory is null)
                {
                    throw new ArgumentException($"Shadow copy path '{shadowCopyPath}' must not be the root directory");
                }
 
                _ = Directory.CreateDirectory(directory);
 
                // The shadow copy should only copy files that exist. For files that don't exist, this best
                // emulates not having the shadow copy layer
                if (File.Exists(originalPath))
                {
                    File.Copy(originalPath, shadowCopyPath);
                    ClearReadOnlyFlagOnFile(new FileInfo(shadowCopyPath));
                }
            }
        }
 
        private static void ClearReadOnlyFlagOnFiles(string directoryPath)
        {
            DirectoryInfo directory = new DirectoryInfo(directoryPath);
 
            foreach (var file in directory.EnumerateFiles(searchPattern: "*", searchOption: SearchOption.AllDirectories))
            {
                ClearReadOnlyFlagOnFile(file);
            }
        }
 
        private static void ClearReadOnlyFlagOnFile(FileInfo fileInfo)
        {
            try
            {
                if (fileInfo.IsReadOnly)
                {
                    fileInfo.IsReadOnly = false;
                }
            }
            catch
            {
                // There are many reasons this could fail. Ignore it and keep going.
            }
        }
 
    }
}