diff --git a/src/OmniSharp.Host/CompositionHostBuilder.cs b/src/OmniSharp.Host/CompositionHostBuilder.cs index 29b4e5023b..ad9be7a5aa 100644 --- a/src/OmniSharp.Host/CompositionHostBuilder.cs +++ b/src/OmniSharp.Host/CompositionHostBuilder.cs @@ -15,11 +15,11 @@ using OmniSharp.Eventing; using OmniSharp.FileSystem; using OmniSharp.FileWatching; -using OmniSharp.Host.Services; using OmniSharp.Mef; using OmniSharp.MSBuild.Discovery; using OmniSharp.Options; using OmniSharp.Roslyn; +using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; namespace OmniSharp @@ -137,7 +137,7 @@ public static IServiceProvider CreateDefaultServiceProvider( // Caching services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(sp => ShadowCopyAnalyzerAssemblyLoader.Instance); services.AddOptions(); // Setup the options from configuration diff --git a/src/OmniSharp.Host/Services/AnalyzerAssemblyLoader.cs b/src/OmniSharp.Host/Services/AnalyzerAssemblyLoader.cs deleted file mode 100644 index 032585dad3..0000000000 --- a/src/OmniSharp.Host/Services/AnalyzerAssemblyLoader.cs +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -// This is simplified version from roslyn codebase, originated from https://github.com/dotnet/roslyn/blob/master/src/Compilers/Shared/ShadowCopyAnalyzerAssemblyLoader.cs - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Reflection; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; -using System.Threading; -using Microsoft.CodeAnalysis; -using OmniSharp.Utilities; - -namespace OmniSharp.Host.Services -{ - // This is shadow copying loader. Makes sure that analyzer assemblies are not locked - // on disk during analysis. - public abstract class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader - { - private readonly string _baseDirectory; - private readonly Lazy _shadowCopyDirectoryAndMutex; - private int _assemblyDirectoryId; - - private readonly object _guard = new object(); - - // lock _guard to read/write - private readonly Dictionary _loadedAssembliesByPath = new Dictionary(); - private readonly Dictionary _loadedAssemblyIdentitiesByPath = new Dictionary(); - private readonly Dictionary _loadedAssembliesByIdentity = new Dictionary(); - - // maps file name to a full path (lock _guard to read/write): - private readonly Dictionary> _knownAssemblyPathsBySimpleName = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - protected abstract Assembly LoadFromPathImpl(string fullPath); - - public AnalyzerAssemblyLoader() - { - _baseDirectory = Path.Combine(Path.GetTempPath(), "CodeAnalysis", "AnalyzerShadowCopies"); - - _shadowCopyDirectoryAndMutex = new Lazy( - () => CreateUniqueDirectoryForProcess(), LazyThreadSafetyMode.ExecutionAndPublication); - } - - public void AddDependencyLocation(string fullPath) - { - string simpleName = Path.GetFileNameWithoutExtension(fullPath); - - lock (_guard) - { - if (!_knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out var paths)) - { - paths = new HashSet(); - _knownAssemblyPathsBySimpleName.Add(simpleName, paths); - } - - paths.Add(fullPath); - } - } - - public Assembly LoadFromPath(string fullPath) - { - return LoadFromPathUncheckedCore(fullPath); - } - - private Assembly LoadFromPathUncheckedCore(string fullPath, AssemblyIdentity identity = null) - { - - // Check if we have already loaded an assembly with the same identity or from the given path. - Assembly loadedAssembly = null; - lock (_guard) - { - if (_loadedAssembliesByPath.TryGetValue(fullPath, out var existingAssembly)) - { - loadedAssembly = existingAssembly; - } - else - { - identity = identity ?? GetOrAddAssemblyIdentity(fullPath); - if (identity != null && _loadedAssembliesByIdentity.TryGetValue(identity, out existingAssembly)) - { - loadedAssembly = existingAssembly; - } - } - } - - // Otherwise, load the assembly. - if (loadedAssembly == null) - { - loadedAssembly = LoadFromPathImpl(fullPath); - } - - // Add the loaded assembly to both path and identity cache. - return AddToCache(loadedAssembly, fullPath, identity); - } - - private AssemblyIdentity GetOrAddAssemblyIdentity(string fullPath) - { - lock (_guard) - { - if (_loadedAssemblyIdentitiesByPath.TryGetValue(fullPath, out var existingIdentity)) - { - return existingIdentity; - } - } - - var identity = TryGetAssemblyIdentity(fullPath); - return AddToCache(fullPath, identity); - } - - private Assembly AddToCache(Assembly assembly, string fullPath, AssemblyIdentity identity) - { - identity = AddToCache(fullPath, identity ?? AssemblyIdentity.FromAssemblyDefinition(assembly)); - - lock (_guard) - { - // The same assembly may be loaded from two different full paths (e.g. when loaded from GAC, etc.), - // or another thread might have loaded the assembly after we checked above. - if (_loadedAssembliesByIdentity.TryGetValue(identity, out var existingAssembly)) - { - assembly = existingAssembly; - } - else - { - _loadedAssembliesByIdentity.Add(identity, assembly); - } - - // An assembly file might be replaced by another file with a different identity. - // Last one wins. - _loadedAssembliesByPath[fullPath] = assembly; - - return assembly; - } - } - - private AssemblyIdentity AddToCache(string fullPath, AssemblyIdentity identity) - { - lock (_guard) - { - if (_loadedAssemblyIdentitiesByPath.TryGetValue(fullPath, out var existingIdentity) && existingIdentity != null) - { - identity = existingIdentity; - } - else - { - _loadedAssemblyIdentitiesByPath[fullPath] = identity; - } - } - - return identity; - } - - protected bool IsKnownDependencyLocation(string fullPath) - { - var simpleName = Path.GetFileNameWithoutExtension(fullPath); - if (!_knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out var paths)) - { - return false; - } - - if (!paths.Contains(fullPath)) - { - return false; - } - - return true; - } - - /// - /// Shadow copy the assembly prior to loading. Return the path to the shadow copied assembly. - /// - protected string GetPathToLoad(string fullPath) - { - var assemblyDirectory = CreateUniqueDirectoryForAssembly(); - return CopyFileAndResources(fullPath, assemblyDirectory); - } - - private static string CopyFileAndResources(string fullPath, string assemblyDirectory) - { - string fileNameWithExtension = Path.GetFileName(fullPath); - string shadowCopyPath = Path.Combine(assemblyDirectory, fileNameWithExtension); - - CopyFile(fullPath, shadowCopyPath); - - string originalDirectory = Path.GetDirectoryName(fullPath); - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileNameWithExtension); - string resourcesNameWithoutExtension = fileNameWithoutExtension + ".resources"; - string resourcesNameWithExtension = resourcesNameWithoutExtension + ".dll"; - - foreach (var directory in Directory.EnumerateDirectories(originalDirectory)) - { - string directoryName = Path.GetFileName(directory); - - string resourcesPath = Path.Combine(directory, resourcesNameWithExtension); - if (File.Exists(resourcesPath)) - { - string resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithExtension); - CopyFile(resourcesPath, resourcesShadowCopyPath); - } - - resourcesPath = Path.Combine(directory, resourcesNameWithoutExtension, resourcesNameWithExtension); - if (File.Exists(resourcesPath)) - { - string resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithoutExtension, resourcesNameWithExtension); - CopyFile(resourcesPath, resourcesShadowCopyPath); - } - } - - return shadowCopyPath; - } - - private static void CopyFile(string originalPath, string shadowCopyPath) - { - var directory = Path.GetDirectoryName(shadowCopyPath); - Directory.CreateDirectory(directory); - - File.Copy(originalPath, shadowCopyPath); - - ClearReadOnlyFlagOnFile(new FileInfo(shadowCopyPath)); - } - - 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. - } - } - - private string CreateUniqueDirectoryForAssembly() - { - int directoryId = Interlocked.Increment(ref _assemblyDirectoryId); - - string directory = Path.Combine(_shadowCopyDirectoryAndMutex.Value, directoryId.ToString()); - - Directory.CreateDirectory(directory); - return directory; - } - - private string CreateUniqueDirectoryForProcess() - { - string guid = Guid.NewGuid().ToString("N").ToLowerInvariant(); - string directory = Path.Combine(_baseDirectory, guid); - - Directory.CreateDirectory(directory); - return directory; - } - - public Assembly Load(string displayName) - { - if (!AssemblyIdentity.TryParseDisplayName(displayName, out var requestedIdentity)) - { - return null; - } - - ImmutableArray candidatePaths; - lock (_guard) - { - // First, check if this loader already loaded the requested assembly: - if (_loadedAssembliesByIdentity.TryGetValue(requestedIdentity, out var existingAssembly)) - { - return existingAssembly; - } - - // Second, check if an assembly file of the same simple name was registered with the loader: - if (!_knownAssemblyPathsBySimpleName.TryGetValue(requestedIdentity.Name, out var pathList)) - { - return null; - } - - candidatePaths = pathList.ToImmutableArray(); - } - - // Multiple assemblies of the same simple name but different identities might have been registered. - // Load the one that matches the requested identity (if any). - foreach (var candidatePath in candidatePaths) - { - var candidateIdentity = GetOrAddAssemblyIdentity(candidatePath); - - if (requestedIdentity.Equals(candidateIdentity)) - { - return LoadFromPathUncheckedCore(candidatePath, candidateIdentity); - } - } - - return null; - } - - private static AssemblyIdentity TryGetAssemblyIdentity(string filePath) - { - try - { - using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) - using (var peReader = new PEReader(stream)) - { - var metadataReader = peReader.GetMetadataReader(); - - AssemblyDefinition assemblyDefinition = metadataReader.GetAssemblyDefinition(); - - string name = metadataReader.GetString(assemblyDefinition.Name); - Version version = assemblyDefinition.Version; - - StringHandle cultureHandle = assemblyDefinition.Culture; - string cultureName = (!cultureHandle.IsNil) ? metadataReader.GetString(cultureHandle) : null; - AssemblyFlags flags = assemblyDefinition.Flags; - - bool hasPublicKey = (flags & AssemblyFlags.PublicKey) != 0; - BlobHandle publicKeyHandle = assemblyDefinition.PublicKey; - ImmutableArray publicKeyOrToken = !publicKeyHandle.IsNil - ? metadataReader.GetBlobBytes(publicKeyHandle).AsImmutableOrNull() - : default; - return new AssemblyIdentity(name, version, cultureName, publicKeyOrToken, hasPublicKey); - } - } - catch { } - - return null; - } - } -} diff --git a/src/OmniSharp.Host/Services/DefaultAnalyzerAssemblyLoader.Core.cs b/src/OmniSharp.Host/Services/DefaultAnalyzerAssemblyLoader.Core.cs deleted file mode 100644 index b029b1f69b..0000000000 --- a/src/OmniSharp.Host/Services/DefaultAnalyzerAssemblyLoader.Core.cs +++ /dev/null @@ -1,151 +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. - -#if NETCOREAPP - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.Loader; - -namespace OmniSharp.Host.Services -{ - internal class DefaultAnalyzerAssemblyLoader : AnalyzerAssemblyLoader - { - /// - ///

Typically a user analyzer has a reference to the compiler and some of the compiler's - /// dependencies such as System.Collections.Immutable. For the analyzer to correctly - /// interoperate with the compiler that created it, we need to ensure that we always use the - /// compiler's version of a given assembly over the analyzer's version.

- /// - ///

If we neglect to do this, then in the case where the user ships the compiler or its - /// dependencies in the analyzer's bin directory, we could end up loading a separate - /// instance of those assemblies in the process of loading the analyzer, which will surface - /// as a failure to load the analyzer.

- ///
- internal static readonly ImmutableHashSet CompilerAssemblySimpleNames = - ImmutableHashSet.Create( - StringComparer.OrdinalIgnoreCase, - "Microsoft.CodeAnalysis", - "Microsoft.CodeAnalysis.CSharp", - "Microsoft.CodeAnalysis.VisualBasic", - "System.Collections", - "System.Collections.Concurrent", - "System.Collections.Immutable", - "System.Console", - "System.Diagnostics.Debug", - "System.Diagnostics.StackTrace", - "System.Diagnostics.Tracing", - "System.IO.Compression", - "System.IO.FileSystem", - "System.Linq", - "System.Linq.Expressions", - "System.Memory", - "System.Reflection.Metadata", - "System.Reflection.Primitives", - "System.Resources.ResourceManager", - "System.Runtime", - "System.Runtime.CompilerServices.Unsafe", - "System.Runtime.Extensions", - "System.Runtime.InteropServices", - "System.Runtime.Loader", - "System.Runtime.Numerics", - "System.Runtime.Serialization.Primitives", - "System.Security.Cryptography.Algorithms", - "System.Security.Cryptography.Primitives", - "System.Text.Encoding.CodePages", - "System.Text.Encoding.Extensions", - "System.Text.RegularExpressions", - "System.Threading", - "System.Threading.Tasks", - "System.Threading.Tasks.Parallel", - "System.Threading.Thread", - "System.Threading.ThreadPool", - "System.Xml.ReaderWriter", - "System.Xml.XDocument", - "System.Xml.XPath.XDocument"); - - private readonly object _guard = new object(); - private readonly Dictionary _loadContextByDirectory = new Dictionary(StringComparer.Ordinal); - - protected override Assembly LoadFromPathImpl(string fullPath) - { - DirectoryLoadContext loadContext; - - var fullDirectoryPath = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException(message: null, paramName: nameof(fullPath)); - lock (_guard) - { - if (!_loadContextByDirectory.TryGetValue(fullDirectoryPath, out loadContext)) - { - loadContext = new DirectoryLoadContext(fullDirectoryPath, this); - _loadContextByDirectory[fullDirectoryPath] = loadContext; - } - } - - var name = AssemblyName.GetAssemblyName(fullPath); - return loadContext.LoadFromAssemblyName(name); - } - - internal static class TestAccessor - { - public static AssemblyLoadContext[] GetOrderedLoadContexts(DefaultAnalyzerAssemblyLoader loader) - { - return loader._loadContextByDirectory.Values.OrderBy(v => v.Directory).ToArray(); - } - } - - private sealed class DirectoryLoadContext : AssemblyLoadContext - { - internal string Directory { get; } - private readonly DefaultAnalyzerAssemblyLoader _loader; - - public DirectoryLoadContext(string directory, DefaultAnalyzerAssemblyLoader loader) - { - Directory = directory; - _loader = loader; - } - - protected override Assembly Load(AssemblyName assemblyName) - { - var simpleName = assemblyName.Name!; - if (CompilerAssemblySimpleNames.Contains(simpleName)) - { - // Delegate to the compiler's load context to load the compiler or anything - // referenced by the compiler - return null; - } - - var assemblyPath = Path.Combine(Directory, simpleName + ".dll"); - if (!_loader.IsKnownDependencyLocation(assemblyPath)) - { - // The analyzer didn't explicitly register this dependency. Most likely the - // assembly we're trying to load here is netstandard or a similar framework - // assembly. We assume that if that is not the case, then the parent ALC will - // fail to load this. - return null; - } - - var pathToLoad = _loader.GetPathToLoad(assemblyPath); - return LoadFromAssemblyPath(pathToLoad); - } - - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - var assemblyPath = Path.Combine(Directory, unmanagedDllName + ".dll"); - if (!_loader.IsKnownDependencyLocation(assemblyPath)) - { - return IntPtr.Zero; - } - - var pathToLoad = _loader.GetPathToLoad(assemblyPath); - return LoadUnmanagedDllFromPath(pathToLoad); - } - } - } -} - -#endif diff --git a/src/OmniSharp.Host/Services/DefaultAnalyzerAssemblyLoader.Desktop.cs b/src/OmniSharp.Host/Services/DefaultAnalyzerAssemblyLoader.Desktop.cs deleted file mode 100644 index bb93d0744c..0000000000 --- a/src/OmniSharp.Host/Services/DefaultAnalyzerAssemblyLoader.Desktop.cs +++ /dev/null @@ -1,53 +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. - -#if !NETCOREAPP - -using System; -using System.Reflection; -using System.Threading; - -namespace OmniSharp.Host.Services -{ - /// - /// Loads analyzer assemblies from their original locations in the file system. - /// Assemblies will only be loaded from the locations specified when the loader - /// is instantiated. - /// - /// - /// This type is meant to be used in scenarios where it is OK for the analyzer - /// assemblies to be locked on disk for the lifetime of the host; for example, - /// csc.exe and vbc.exe. In scenarios where support for updating or deleting - /// the analyzer on disk is required a different loader should be used. - /// - internal class DefaultAnalyzerAssemblyLoader : AnalyzerAssemblyLoader - { - private int _hookedAssemblyResolve; - - protected override Assembly LoadFromPathImpl(string fullPath) - { - if (Interlocked.CompareExchange(ref _hookedAssemblyResolve, 0, 1) == 0) - { - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; - } - - var pathToLoad = GetPathToLoad(fullPath); - return Assembly.LoadFrom(pathToLoad); - } - - private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) - { - try - { - return Load(AppDomain.CurrentDomain.ApplyPolicy(args.Name)); - } - catch - { - return null; - } - } - } -} - -#endif diff --git a/src/OmniSharp.Roslyn/Utilities/ShadowCopyAnalyzerAssemblyLoader.cs b/src/OmniSharp.Roslyn/Utilities/ShadowCopyAnalyzerAssemblyLoader.cs new file mode 100644 index 0000000000..a1bfd24717 --- /dev/null +++ b/src/OmniSharp.Roslyn/Utilities/ShadowCopyAnalyzerAssemblyLoader.cs @@ -0,0 +1,10 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.Analyzers; + +namespace OmniSharp.Roslyn.Utilities +{ + public static class ShadowCopyAnalyzerAssemblyLoader + { + public static readonly IAnalyzerAssemblyLoader Instance = OmnisharpAnalyzerAssemblyLoaderFactory.CreateShadowCopyAnalyzerAssemblyLoader(); + } +} diff --git a/tests/OmniSharp.MSBuild.Tests/AbstractMSBuildTestFixture.cs b/tests/OmniSharp.MSBuild.Tests/AbstractMSBuildTestFixture.cs index 6e96209bb4..96ce829924 100644 --- a/tests/OmniSharp.MSBuild.Tests/AbstractMSBuildTestFixture.cs +++ b/tests/OmniSharp.MSBuild.Tests/AbstractMSBuildTestFixture.cs @@ -4,8 +4,8 @@ using Microsoft.CodeAnalysis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using OmniSharp.Host.Services; using OmniSharp.MSBuild.Discovery; +using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; using TestUtility; using Xunit.Abstractions; @@ -22,7 +22,7 @@ public AbstractMSBuildTestFixture(ITestOutputHelper output) : base(output) { _assemblyLoader = new AssemblyLoader(this.LoggerFactory); - _analyzerAssemblyLoader = new DefaultAnalyzerAssemblyLoader(); + _analyzerAssemblyLoader = ShadowCopyAnalyzerAssemblyLoader.Instance; _msbuildLocator = MSBuildLocator.CreateStandAlone(this.LoggerFactory, _assemblyLoader); // Some tests require MSBuild to be discovered early diff --git a/tests/TestUtility/TestServiceProvider.cs b/tests/TestUtility/TestServiceProvider.cs index 00cba617b4..1cd6771ce3 100644 --- a/tests/TestUtility/TestServiceProvider.cs +++ b/tests/TestUtility/TestServiceProvider.cs @@ -14,9 +14,9 @@ using OmniSharp; using OmniSharp.Eventing; using OmniSharp.FileWatching; -using OmniSharp.Host.Services; using OmniSharp.MSBuild.Discovery; using OmniSharp.Options; +using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; using OmniSharp.Utilities; using TestUtility.Logging; @@ -87,7 +87,7 @@ public static IServiceProvider Create( var configuration = CreateConfiguration(configurationData); var msbuildLocator = CreateMSBuildLocator(loggerFactory, assemblyLoader); var sharedTextWriter = CreateSharedTextWriter(testOutput); - var analyzerAssemblyLoader = new DefaultAnalyzerAssemblyLoader(); + var analyzerAssemblyLoader = ShadowCopyAnalyzerAssemblyLoader.Instance; return new TestServiceProvider( environment, loggerFactory, assemblyLoader, analyzerAssemblyLoader, sharedTextWriter,