diff --git a/src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs b/src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs index 75af09d24cf11..8e47f463cf0c0 100644 --- a/src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/AnalyzerAssemblyLoaderTests.cs @@ -5,19 +5,21 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; using Microsoft.CodeAnalysis.VisualBasic; #if NET @@ -368,32 +370,11 @@ string getExpectedLoadPath(string path) if (path.EndsWith(".resources.dll", StringComparison.Ordinal)) { - return getRealSatelliteLoadPath(path) ?? ""; + return loader.GetRealSatelliteLoadPath(path) ?? ""; } return loader.GetRealAnalyzerLoadPath(path ?? ""); } - // When PreparePathToLoad is overridden this returns the most recent - // real path for the given analyzer satellite assembly path - string? getRealSatelliteLoadPath(string originalSatelliteFullPath) - { - // This is a satellite assembly, need to find the mapped path of the real assembly, then - // adjust that mapped path for the suffix of the satellite assembly - // - // Example of dll and it's corresponding satellite assembly - // - // c:\some\path\en-GB\util.resources.dll - // c:\some\path\util.dll - var assemblyFileName = Path.ChangeExtension(Path.GetFileNameWithoutExtension(originalSatelliteFullPath), ".dll"); - - var assemblyDir = Path.GetDirectoryName(originalSatelliteFullPath)!; - var cultureInfo = CultureInfo.GetCultureInfo(Path.GetFileName(assemblyDir)); - assemblyDir = Path.GetDirectoryName(assemblyDir)!; - - // Real assembly is located in the directory above this one - var assemblyPath = Path.Combine(assemblyDir, assemblyFileName); - return loader.GetRealSatelliteLoadPath(assemblyPath, cultureInfo); - } } private static void VerifyAssemblies(AnalyzerAssemblyLoader loader, IEnumerable assemblies, int? copyCount, params string[] assemblyPaths) @@ -1443,31 +1424,8 @@ public void AssemblyLoading_Resources(AnalyzerTestKind kind) // dlls don't apply for this count. VerifyDependencyAssemblies(loader, copyCount: 1, analyzerPath, analyzerResourcesPath); }); - } - - [Theory] - [CombinatorialData] - public void AssemblyLoading_ResourcesInParent(AnalyzerTestKind kind) - { - Run(kind, static (AnalyzerAssemblyLoader loader, AssemblyLoadTestFixture testFixture) => - { - using var temp = new TempRoot(); - var tempDir = temp.CreateDirectory(); - var analyzerPath = tempDir.CreateFile("AnalyzerWithLoc.dll").CopyContentFrom(testFixture.AnalyzerWithLoc).Path; - var analyzerResourcesPath = tempDir.CreateDirectory("es").CreateFile("AnalyzerWithLoc.resources.dll").CopyContentFrom(testFixture.AnalyzerWithLocResourceEnGB).Path; - loader.AddDependencyLocation(analyzerPath); - var assembly = loader.LoadFromPath(analyzerPath); - var methodInfo = assembly - .GetType("AnalyzerWithLoc.Util")! - .GetMethod("Exec", BindingFlags.Static | BindingFlags.Public)!; - methodInfo.Invoke(null, ["es-ES"]); - // The copy count is 1 here as only one real assembly was copied, the resource - // dlls don't apply for this count. - VerifyDependencyAssemblies(loader, copyCount: 1, analyzerPath, analyzerResourcesPath); - }); } - #if NET [Theory] diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Core.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Core.cs index d3c5c1b354bd2..1088fb4b32225 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Core.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Core.cs @@ -144,7 +144,7 @@ public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader) var assemblyPath = Path.Combine(Directory, simpleName + ".dll"); if (_loader.IsAnalyzerDependencyPath(assemblyPath)) { - (_, var loadPath) = _loader.GetAssemblyInfoForPath(assemblyPath); + (_, var loadPath, _) = _loader.GetAssemblyInfoForPath(assemblyPath); return loadCore(loadPath); } @@ -156,11 +156,11 @@ public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader) // loader has a mode where it loads from Stream though and the runtime will not handle // that automatically. Rather than bifurcate our loading behavior between Disk and // Stream both modes just handle satellite loading directly - if (assemblyName.CultureInfo is not null && simpleName.EndsWith(".resources", StringComparison.Ordinal)) + if (!string.IsNullOrEmpty(assemblyName.CultureName) && simpleName.EndsWith(".resources", StringComparison.Ordinal)) { var analyzerFileName = Path.ChangeExtension(simpleName, ".dll"); var analyzerFilePath = Path.Combine(Directory, analyzerFileName); - var satelliteLoadPath = _loader.GetRealSatelliteLoadPath(analyzerFilePath, assemblyName.CultureInfo); + var satelliteLoadPath = _loader.GetSatelliteInfoForPath(analyzerFilePath, assemblyName.CultureName); if (satelliteLoadPath is not null) { return loadCore(satelliteLoadPath); @@ -173,8 +173,7 @@ public DirectoryLoadContext(string directory, AnalyzerAssemblyLoader loader) // be necessary but msbuild target defaults have caused a number of customers to // fall into this path. See discussion here for where it comes up // https://github.com/dotnet/roslyn/issues/56442 - var (_, bestRealPath) = _loader.GetBestPath(assemblyName); - if (bestRealPath is not null) + if (_loader.GetBestPath(assemblyName) is string bestRealPath) { return loadCore(bestRealPath); } @@ -201,7 +200,7 @@ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) var assemblyPath = Path.Combine(Directory, unmanagedDllName + ".dll"); if (_loader.IsAnalyzerDependencyPath(assemblyPath)) { - (_, var loadPath) = _loader.GetAssemblyInfoForPath(assemblyPath); + (_, var loadPath, _) = _loader.GetAssemblyInfoForPath(assemblyPath); return LoadUnmanagedDllFromPath(loadPath); } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Desktop.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Desktop.cs index 98bb543cdae6a..2acf1df809836 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Desktop.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.Desktop.cs @@ -117,30 +117,11 @@ internal bool EnsureResolvedUnhooked() { try { - const string resourcesExtension = ".resources"; var assemblyName = new AssemblyName(args.Name); - var simpleName = assemblyName.Name; - var isSatelliteAssembly = - assemblyName.CultureInfo is not null && - simpleName.EndsWith(resourcesExtension, StringComparison.Ordinal); - - if (isSatelliteAssembly) - { - // Satellite assemblies should get the best path information using the - // non-resource part of the assembly name. Once the path information is obtained - // GetSatelliteInfoForPath will translate to the resource assembly path. - assemblyName.Name = simpleName[..^resourcesExtension.Length]; - } - - var (originalPath, realPath) = GetBestPath(assemblyName); - if (isSatelliteAssembly && originalPath is not null) - { - realPath = GetRealSatelliteLoadPath(originalPath, assemblyName.CultureInfo); - } - - if (realPath is not null) + string? bestPath = GetBestPath(assemblyName); + if (bestPath is not null) { - return Assembly.LoadFrom(realPath); + return Assembly.LoadFrom(bestPath); } return null; diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs index 30755244d7869..027515ba255e0 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerAssemblyLoader.cs @@ -5,11 +5,15 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Globalization; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +<<<<<<< HEAD using Microsoft.CodeAnalysis.ErrorReporting; +======= +using Microsoft.CodeAnalysis.PooledObjects; +>>>>>>> parent of 02069cef3ee (Reduce File I/O under the AnalyzerAssemblyLoader folder (#72412)) using Roslyn.Utilities; namespace Microsoft.CodeAnalysis @@ -48,17 +52,7 @@ internal abstract partial class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader /// /// Access must be guarded by /// - private readonly Dictionary _analyzerAssemblyInfoMap = new(); - - /// - /// Mapping of analyzer dependency original full path and culture to the real satellite - /// assembly path. If the satellite assembly doesn't exist for the original analyzer and - /// culture, the real path value stored will be null. - /// - /// - /// Access must be guarded by - /// - private readonly Dictionary<(string OriginalAnalyzerPath, CultureInfo CultureInfo), string?> _analyzerSatelliteAssemblyRealPaths = new(); + private readonly Dictionary SatelliteCultureNames)?> _analyzerAssemblyInfoMap = new(); /// /// Maps analyzer dependency simple names to the set of original full paths it was loaded from. This _only_ @@ -149,7 +143,7 @@ public void AddDependencyLocation(string fullPath) _knownAssemblyPathsBySimpleName[simpleName] = paths.Add(fullPath); } - // This type assumes the file system is static for the duration of the + // This type assumses the file system is static for the duration of the // it's instance. Repeated calls to this method, even if the underlying // file system contents, should reuse the results of the first call. _ = _analyzerAssemblyInfoMap.TryAdd(fullPath, null); @@ -162,7 +156,7 @@ public Assembly LoadFromPath(string originalAnalyzerPath) CompilerPathUtilities.RequireAbsolutePath(originalAnalyzerPath, nameof(originalAnalyzerPath)); - (AssemblyName? assemblyName, _) = GetAssemblyInfoForPath(originalAnalyzerPath); + (AssemblyName? assemblyName, _, _) = GetAssemblyInfoForPath(originalAnalyzerPath); // Not a managed assembly, nothing else to do if (assemblyName is null) @@ -189,7 +183,7 @@ public Assembly LoadFromPath(string originalAnalyzerPath) /// because we only want information for registered paths. Using unregistered paths inside the /// implementation should result in errors. /// - protected (AssemblyName? AssemblyName, string RealAssemblyPath) GetAssemblyInfoForPath(string originalAnalyzerPath) + protected (AssemblyName? AssemblyName, string RealAssemblyPath, ImmutableHashSet SatelliteCultureNames) GetAssemblyInfoForPath(string originalAnalyzerPath) { CheckIfDisposed(); @@ -206,7 +200,8 @@ public Assembly LoadFromPath(string originalAnalyzerPath) } } - string realPath = PreparePathToLoad(originalAnalyzerPath); + var resourceAssemblyCultureNames = getResourceAssemblyCultureNames(originalAnalyzerPath); + string realPath = PreparePathToLoad(originalAnalyzerPath, resourceAssemblyCultureNames); AssemblyName? assemblyName; try { @@ -222,68 +217,35 @@ public Assembly LoadFromPath(string originalAnalyzerPath) lock (_guard) { - _analyzerAssemblyInfoMap[originalAnalyzerPath] = (assemblyName, realPath); + _analyzerAssemblyInfoMap[originalAnalyzerPath] = (assemblyName, realPath, resourceAssemblyCultureNames); } - return (assemblyName, realPath); - } - - /// - /// Get the path a satellite assembly should be loaded from for the given original - /// analyzer path and culture - /// - /// - /// This is used during assembly resolve for satellite assemblies to determine the - /// path from where the satellite assembly should be loaded for the specified culture. - /// This method calls to ensure this path - /// contains the satellite assembly. - /// - internal string? GetRealSatelliteLoadPath(string originalAnalyzerPath, CultureInfo cultureInfo) - { - CheckIfDisposed(); - - string? realSatelliteAssemblyPath = null; + return (assemblyName, realPath, resourceAssemblyCultureNames); - lock (_guard) + // Discover the culture names for any satellite dlls related to this analyzer. These + // need to be understood when handling the resource loading in certain cases. + static ImmutableHashSet getResourceAssemblyCultureNames(string originalAnalyzerPath) { - if (_analyzerSatelliteAssemblyRealPaths.TryGetValue((originalAnalyzerPath, cultureInfo), out realSatelliteAssemblyPath)) + var path = Path.GetDirectoryName(originalAnalyzerPath)!; + using var enumerator = Directory.EnumerateDirectories(path, "*").GetEnumerator(); + if (!enumerator.MoveNext()) { - return realSatelliteAssemblyPath; + return ImmutableHashSet.Empty; } - } - - var actualCultureName = getSatelliteCultureName(originalAnalyzerPath, cultureInfo); - if (actualCultureName != null) - { - realSatelliteAssemblyPath = PrepareSatelliteAssemblyToLoad(originalAnalyzerPath, actualCultureName); - } - - lock (_guard) - { - _analyzerSatelliteAssemblyRealPaths[(originalAnalyzerPath, cultureInfo)] = realSatelliteAssemblyPath; - } - - return realSatelliteAssemblyPath; - // Discover the most specific culture name to use for the specified analyzer path and culture - static string? getSatelliteCultureName(string originalAnalyzerPath, CultureInfo cultureInfo) - { - var path = Path.GetDirectoryName(originalAnalyzerPath)!; var resourceFileName = GetSatelliteFileName(Path.GetFileName(originalAnalyzerPath)); - - while (cultureInfo != CultureInfo.InvariantCulture) + var builder = ImmutableHashSet.CreateBuilder(StringComparer.OrdinalIgnoreCase); + do { - var resourceFilePath = Path.Combine(path, cultureInfo.Name, resourceFileName); - + var resourceFilePath = Path.Combine(enumerator.Current, resourceFileName); if (File.Exists(resourceFilePath)) { - return cultureInfo.Name; + builder.Add(Path.GetFileName(enumerator.Current)); } - - cultureInfo = cultureInfo.Parent; } + while (enumerator.MoveNext()); - return null; + return builder.ToImmutableHashSet(); } } @@ -291,18 +253,36 @@ public Assembly LoadFromPath(string originalAnalyzerPath) { CheckIfDisposed(); - return GetBestPath(assemblyName).BestOriginalPath; + return GetBestPath(assemblyName); } /// - /// Return the best (original, real) path information for loading an assembly with the specified . + /// Get the real load path of the satellite assembly given the original path to the analyzer + /// and the desired culture name. /// - protected (string? BestOriginalPath, string? BestRealPath) GetBestPath(AssemblyName requestedName) + protected string? GetSatelliteInfoForPath(string originalAnalyzerPath, string cultureName) + { + var (_, realAssemblyPath, satelliteCultureNames) = GetAssemblyInfoForPath(originalAnalyzerPath); + if (!satelliteCultureNames.Contains(cultureName)) + { + return null; + } + + var satelliteFileName = GetSatelliteFileName(Path.GetFileName(realAssemblyPath)); + var dir = Path.GetDirectoryName(realAssemblyPath)!; + return Path.Combine(dir, cultureName, satelliteFileName); + } + + /// + /// Return the best path for loading an assembly with the specified . This + /// return is a real path to load, not an original path. + /// + protected string? GetBestPath(AssemblyName requestedName) { CheckIfDisposed(); if (requestedName.Name is null) { - return (null, null); + return null; } ImmutableHashSet? paths; @@ -310,18 +290,17 @@ public Assembly LoadFromPath(string originalAnalyzerPath) { if (!_knownAssemblyPathsBySimpleName.TryGetValue(requestedName.Name, out paths)) { - return (null, null); + return null; } } // Sort the candidate paths by ordinal, to ensure determinism with the same inputs if you were to have // multiple assemblies providing the same version. - string? bestRealPath = null; - string? bestOriginalPath = null; + string? bestPath = null; AssemblyName? bestName = null; foreach (var candidateOriginalPath in paths.OrderBy(StringComparer.Ordinal)) { - (AssemblyName? candidateName, string candidateRealPath) = GetAssemblyInfoForPath(candidateOriginalPath); + (AssemblyName? candidateName, string candidateRealPath, _) = GetAssemblyInfoForPath(candidateOriginalPath); if (candidateName is null) { continue; @@ -331,19 +310,18 @@ public Assembly LoadFromPath(string originalAnalyzerPath) { if (candidateName.Version == requestedName.Version) { - return (candidateOriginalPath, candidateRealPath); + return candidateRealPath; } if (bestName is null || candidateName.Version > bestName.Version) { - bestOriginalPath = candidateOriginalPath; - bestRealPath = candidateRealPath; + bestPath = candidateRealPath; bestName = candidateName; } } } - return (bestOriginalPath, bestRealPath); + return bestPath; } protected static string GetSatelliteFileName(string assemblyFileName) => @@ -353,18 +331,15 @@ protected static string GetSatelliteFileName(string assemblyFileName) => /// When overridden in a derived class, allows substituting an assembly path after we've /// identified the context to load an assembly in, but before the assembly is actually /// loaded from disk. This is used to substitute out the original path with the shadow-copied version. + /// + /// In the case the is moved to a new location then + /// the resource DLLs for the specified must also be + /// moved _but_ retain their original relative location. /// - protected abstract string PreparePathToLoad(string assemblyFilePath); - - /// - /// When overridden in a derived class, allows substituting a satellite assembly path after we've - /// identified the context to load a satellite assembly in, but before the satellite assembly is actually - /// loaded from disk. This is used to substitute out the original path with the shadow-copied version. - /// - protected abstract string PrepareSatelliteAssemblyToLoad(string assemblyFilePath, string cultureName); + protected abstract string PreparePathToLoad(string assemblyFilePath, ImmutableHashSet resourceAssemblyCultureNames); /// - /// When is overridden this returns the most recent + /// When is overridden this returns the most recent /// real path calculated for the /// internal string GetRealAnalyzerLoadPath(string originalFullPath) @@ -382,6 +357,30 @@ internal string GetRealAnalyzerLoadPath(string originalFullPath) } } + /// + /// When is overridden this returns the most recent + /// real path for the given analyzer satellite assembly path + /// + internal string? GetRealSatelliteLoadPath(string originalSatelliteFullPath) + { + // This is a satellite assembly, need to find the mapped path of the real assembly, then + // adjust that mapped path for the suffix of the satellite assembly + // + // Example of dll and it's corresponding satellite assembly + // + // c:\some\path\en-GB\util.resources.dll + // c:\some\path\util.dll + var assemblyFileName = Path.ChangeExtension(Path.GetFileNameWithoutExtension(originalSatelliteFullPath), ".dll"); + + var assemblyDir = Path.GetDirectoryName(originalSatelliteFullPath)!; + var cultureName = Path.GetFileName(assemblyDir); + assemblyDir = Path.GetDirectoryName(assemblyDir)!; + + // Real assembly is located in the directory above this one + var assemblyPath = Path.Combine(assemblyDir, assemblyFileName); + return GetSatelliteInfoForPath(assemblyPath, cultureName); + } + internal (string OriginalAssemblyPath, string RealAssemblyPath)[] GetPathMapSnapshot() { CheckIfDisposed(); diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.cs index 40f7afa19d91f..5a5b30802ceb3 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DefaultAnalyzerAssemblyLoader.cs @@ -38,18 +38,7 @@ internal DefaultAnalyzerAssemblyLoader(System.Runtime.Loader.AssemblyLoadContext /// /// The default implementation is to simply load in place. /// - protected override string PreparePathToLoad(string fullPath) => fullPath; - - /// - /// The default implementation is to simply load in place. - /// - protected override string PrepareSatelliteAssemblyToLoad(string assemblyFilePath, string cultureName) - { - var directory = Path.GetDirectoryName(assemblyFilePath)!; - var fileName = GetSatelliteFileName(Path.GetFileName(assemblyFilePath)); - - return Path.Combine(directory, cultureName, fileName); - } + protected override string PreparePathToLoad(string fullPath, ImmutableHashSet satelliteCultureNames) => fullPath; /// /// Return an which does not lock assemblies on disk that is diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs index 1df2f9c6b5cf7..a895ede75a7e2 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/ShadowCopyAnalyzerAssemblyLoader.cs @@ -6,8 +6,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Collections.Immutable; using Roslyn.Utilities; using System.Collections.Immutable; using System.Reflection; @@ -37,7 +39,6 @@ internal sealed class ShadowCopyAnalyzerAssemblyLoader : AnalyzerAssemblyLoader private readonly Lazy<(string directory, Mutex)> _shadowCopyDirectoryAndMutex; private readonly ConcurrentDictionary> _mvidPathMap = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary<(Guid, string), Task> _mvidSatelliteAssemblyPathMap = new ConcurrentDictionary<(Guid, string), Task>(); internal string BaseDirectory => _baseDirectory; @@ -129,62 +130,22 @@ private void DeleteLeftoverDirectories() } } - protected override string PreparePathToLoad(string originalAnalyzerPath) + protected override string PreparePathToLoad(string originalAnalyzerPath, ImmutableHashSet cultureNames) { var mvid = AssemblyUtilities.ReadMvid(originalAnalyzerPath); - - return PrepareLoad(_mvidPathMap, mvid, copyAnalyzerContents); - - string copyAnalyzerContents() - { - var analyzerFileName = Path.GetFileName(originalAnalyzerPath); - var shadowDirectory = Path.Combine(_shadowCopyDirectoryAndMutex.Value.directory, mvid.ToString()); - var shadowAnalyzerPath = Path.Combine(shadowDirectory, analyzerFileName); - CopyFile(originalAnalyzerPath, shadowAnalyzerPath); - - return shadowAnalyzerPath; - } - } - - protected override string PrepareSatelliteAssemblyToLoad(string originalAnalyzerPath, string cultureName) - { - var mvid = AssemblyUtilities.ReadMvid(originalAnalyzerPath); - - return PrepareLoad(_mvidSatelliteAssemblyPathMap, (mvid, cultureName), copyAnalyzerContents); - - string copyAnalyzerContents() - { - var analyzerFileName = Path.GetFileName(originalAnalyzerPath); - var shadowDirectory = Path.Combine(_shadowCopyDirectoryAndMutex.Value.directory, mvid.ToString()); - var shadowAnalyzerPath = Path.Combine(shadowDirectory, analyzerFileName); - - var originalDirectory = Path.GetDirectoryName(originalAnalyzerPath)!; - var satelliteFileName = GetSatelliteFileName(analyzerFileName); - - var originalSatellitePath = Path.Combine(originalDirectory, cultureName, satelliteFileName); - var shadowSatellitePath = Path.Combine(shadowDirectory, cultureName, satelliteFileName); - CopyFile(originalSatellitePath, shadowSatellitePath); - - return shadowSatellitePath; - } - } - - private static string PrepareLoad(ConcurrentDictionary> mvidPathMap, TKey mvidKey, Func copyContents) - where TKey : notnull - { - if (mvidPathMap.TryGetValue(mvidKey, out Task? copyTask)) + if (_mvidPathMap.TryGetValue(mvid, out Task? copyTask)) { return copyTask.Result; } var tcs = new TaskCompletionSource(); - var task = mvidPathMap.GetOrAdd(mvidKey, tcs.Task); + var task = _mvidPathMap.GetOrAdd(mvid, tcs.Task); if (object.ReferenceEquals(task, tcs.Task)) { // This thread won and we need to do the copy. try { - var shadowAnalyzerPath = copyContents(); + var shadowAnalyzerPath = copyAnalyzerContents(); tcs.SetResult(shadowAnalyzerPath); return shadowAnalyzerPath; } @@ -199,19 +160,43 @@ private static string PrepareLoad(ConcurrentDictionary> // This thread lost and we need to wait for the winner to finish the copy. return task.Result; } - } - private static void CopyFile(string originalPath, string shadowCopyPath) - { - var directory = Path.GetDirectoryName(shadowCopyPath); - if (directory is null) + string copyAnalyzerContents() { - throw new ArgumentException($"Shadow copy path '{shadowCopyPath}' must not be the root directory"); + var analyzerFileName = Path.GetFileName(originalAnalyzerPath); + var shadowDirectory = Path.Combine(_shadowCopyDirectoryAndMutex.Value.directory, mvid.ToString()); + var shadowAnalyzerPath = Path.Combine(shadowDirectory, analyzerFileName); + copyFile(originalAnalyzerPath, shadowAnalyzerPath); + + if (cultureNames.IsEmpty) + { + return shadowAnalyzerPath; + } + + var originalDirectory = Path.GetDirectoryName(originalAnalyzerPath)!; + var satelliteFileName = GetSatelliteFileName(analyzerFileName); + foreach (var cultureName in cultureNames) + { + var originalSatellitePath = Path.Combine(originalDirectory, cultureName, satelliteFileName); + var shadowSatellitePath = Path.Combine(shadowDirectory, cultureName, satelliteFileName); + copyFile(originalSatellitePath, shadowSatellitePath); + } + + return shadowAnalyzerPath; } - _ = Directory.CreateDirectory(directory); - File.Copy(originalPath, shadowCopyPath); - ClearReadOnlyFlagOnFile(new FileInfo(shadowCopyPath)); + 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); + File.Copy(originalPath, shadowCopyPath); + ClearReadOnlyFlagOnFile(new FileInfo(shadowCopyPath)); + } } private static void ClearReadOnlyFlagOnFiles(string directoryPath) diff --git a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs index 664c25c3a3306..0c6be7ecdf9fd 100644 --- a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs +++ b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs @@ -709,10 +709,7 @@ private static AnalyzerFileReference CreateShadowCopiedAnalyzerReference(TempRoo private class MissingAnalyzerLoader() : AnalyzerAssemblyLoader([]) { - protected override string PreparePathToLoad(string fullPath) - => throw new FileNotFoundException(fullPath); - - protected override string PrepareSatelliteAssemblyToLoad(string fullPath, string cultureName) + protected override string PreparePathToLoad(string fullPath, ImmutableHashSet cultureNames) => throw new FileNotFoundException(fullPath); } diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs index fd36b3e185862..9e6aeeeae79b5 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteAnalyzerAssemblyLoader.cs @@ -2,8 +2,8 @@ // 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.IO; using System.Collections.Immutable; +using System.IO; namespace Microsoft.CodeAnalysis.Remote.Diagnostics; @@ -22,15 +22,9 @@ public RemoteAnalyzerAssemblyLoader(string baseDirectory, ImmutableArray cultureNames) { var fixedPath = Path.GetFullPath(Path.Combine(_baseDirectory, Path.GetFileName(fullPath))); return File.Exists(fixedPath) ? fixedPath : fullPath; } - - protected override string PrepareSatelliteAssemblyToLoad(string fullPath, string cultureName) - { - var fixedPath = Path.GetFullPath(Path.Combine(_baseDirectory, cultureName, Path.GetFileName(fullPath))); - return File.Exists(fixedPath) ? fixedPath : fullPath; - } }