/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Scripting/Core/Hosting/AssemblyLoader/MetadataShadowCopyProvider.cs
636 строк
24 KB
Cyrus Najmabadi
use patterns
26 мар 2025, 08:05
26 мар 2025, 08:05
c5e675a
Код
Авторство
О чём код?
// 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. #nullable disable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Scripting.Hosting { /// <summary> /// Implements shadow-copying metadata file cache. /// </summary> public sealed class MetadataShadowCopyProvider : IDisposable { private readonly CultureInfo _documentationCommentsCulture; // normalized absolute path private readonly string _baseDirectory; // Normalized absolute path to a directory where assemblies are copied. Must contain nothing but shadow-copied assemblies. // Internal for testing. internal string ShadowCopyDirectory; // normalized absolute paths private readonly ImmutableArray<string> _noShadowCopyDirectories; private readonly struct CacheEntry<TPublic> { public readonly TPublic Public; public readonly Metadata Private; public CacheEntry(TPublic @public, Metadata @private) { Debug.Assert(@public != null); Debug.Assert(@private != null); Public = @public; Private = @private; } } // Cache for files that are shadow-copied: // (original path, last write timestamp) -> (public shadow copy, private metadata instance that owns the PE image) private readonly Dictionary<FileKey, CacheEntry<MetadataShadowCopy>> _shadowCopies = new Dictionary<FileKey, CacheEntry<MetadataShadowCopy>>(); // Cache for files that are not shadow-copied: // (path, last write timestamp) -> (public metadata, private metadata instance that owns the PE image) private readonly Dictionary<FileKey, CacheEntry<Metadata>> _noShadowCopyCache = new Dictionary<FileKey, CacheEntry<Metadata>>(); // files that should not be copied: private HashSet<string> _lazySuppressedFiles; private object Guard => _shadowCopies; /// <summary> /// Creates an instance of <see cref="MetadataShadowCopyProvider"/>. /// </summary> /// <param name="directory">The directory to use to store file copies.</param> /// <param name="noShadowCopyDirectories">Directories to exclude from shadow-copying.</param> /// <param name="documentationCommentsCulture">Culture of documentation comments to copy. If not specified no doc comment files are going to be copied.</param> /// <exception cref="ArgumentNullException"><paramref name="directory"/> is null.</exception> /// <exception cref="ArgumentException"><paramref name="directory"/> is not an absolute path.</exception> public MetadataShadowCopyProvider(string directory = null, IEnumerable<string> noShadowCopyDirectories = null, CultureInfo documentationCommentsCulture = null) { if (directory != null) { RequireAbsolutePath(directory, nameof(directory)); try { _baseDirectory = FileUtilities.NormalizeDirectoryPath(directory); } catch (Exception e) { throw new ArgumentException(e.Message, nameof(directory)); } } else { _baseDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); } if (noShadowCopyDirectories != null) { try { _noShadowCopyDirectories = ImmutableArray.CreateRange(noShadowCopyDirectories.Select(FileUtilities.NormalizeDirectoryPath)); } catch (Exception e) { throw new ArgumentException(e.Message, nameof(noShadowCopyDirectories)); } } else { _noShadowCopyDirectories = ImmutableArray<string>.Empty; } _documentationCommentsCulture = documentationCommentsCulture; } private static void RequireAbsolutePath(string path, string argumentName) { if (path == null) { throw new ArgumentNullException(argumentName); } if (!PathUtilities.IsAbsolute(path)) { throw new ArgumentException(ScriptingResources.AbsolutePathExpected, argumentName); } } /// <summary> /// Determine whether given path is under the shadow-copy directory managed by this shadow-copy provider. /// </summary> /// <param name="fullPath">Absolute path.</param> /// <exception cref="ArgumentNullException"><paramref name="fullPath"/> is null.</exception> /// <exception cref="ArgumentException"><paramref name="fullPath"/> is not an absolute path.</exception> public bool IsShadowCopy(string fullPath) { RequireAbsolutePath(fullPath, nameof(fullPath)); string directory = ShadowCopyDirectory; if (directory == null) { return false; } string normalizedPath; try { normalizedPath = FileUtilities.NormalizeDirectoryPath(fullPath); } catch { return false; } return normalizedPath.StartsWith(directory, StringComparison.OrdinalIgnoreCase); } ~MetadataShadowCopyProvider() { DisposeShadowCopies(); DeleteShadowCopyDirectory(); } /// <summary> /// Clears shadow-copy cache, disposes all allocated metadata, and attempts to delete copied files. /// </summary> public void Dispose() { GC.SuppressFinalize(this); lock (Guard) { DisposeShadowCopies(); _shadowCopies.Clear(); } DeleteShadowCopyDirectory(); } private void DisposeShadowCopies() { foreach (var entry in _shadowCopies.Values) { // metadata file handles have been disposed already, but the xml doc file handle hasn't: entry.Public.DisposeFileHandles(); // dispose metadata images: entry.Private.Dispose(); } } private void DeleteShadowCopyDirectory() { var directory = ShadowCopyDirectory; if (Directory.Exists(directory)) { try { // First, strip the read-only bit off of any files. var directoryInfo = new DirectoryInfo(directory); foreach (var fileInfo in directoryInfo.EnumerateFiles(searchPattern: "*", searchOption: SearchOption.AllDirectories)) { StripReadOnlyAttributeFromFile(fileInfo); } // Second, delete everything. Directory.Delete(directory, recursive: true); } catch { } } } private static void StripReadOnlyAttributeFromFile(FileInfo fileInfo) { try { if (fileInfo.IsReadOnly) { fileInfo.IsReadOnly = false; } } catch { // There are many reasons this could fail. Just ignore it and move on. } } /// <summary> /// Gets or creates metadata for specified file path. /// </summary> /// <param name="fullPath">Full path to an assembly manifest module file or a standalone module file.</param> /// <param name="kind">Metadata kind (assembly or module).</param> /// <returns>Metadata for the specified file.</returns> /// <exception cref="IOException">Error reading file <paramref name="fullPath"/>. See <see cref="Exception.InnerException"/> for details.</exception> public Metadata GetMetadata(string fullPath, MetadataImageKind kind) { if (NeedsShadowCopy(fullPath)) { return GetMetadataShadowCopyNoCheck(fullPath, kind).Metadata; } FileKey key = FileKey.Create(fullPath); lock (Guard) { CacheEntry<Metadata> existing; if (_noShadowCopyCache.TryGetValue(key, out existing)) { return existing.Public; } } Metadata newMetadata; if (kind == MetadataImageKind.Assembly) { newMetadata = AssemblyMetadata.CreateFromFile(fullPath); } else { newMetadata = ModuleMetadata.CreateFromFile(fullPath); } // the files are locked (memory mapped) now key = FileKey.Create(fullPath); lock (Guard) { CacheEntry<Metadata> existing; if (_noShadowCopyCache.TryGetValue(key, out existing)) { newMetadata.Dispose(); return existing.Public; } Metadata publicMetadata = newMetadata.Copy(); _noShadowCopyCache.Add(key, new CacheEntry<Metadata>(publicMetadata, newMetadata)); return publicMetadata; } } /// <summary> /// Gets or creates a copy of specified assembly or standalone module. /// </summary> /// <param name="fullPath">Full path to an assembly manifest module file or a standalone module file.</param> /// <param name="kind">Metadata kind (assembly or module).</param> /// <returns> /// Copy of the specified file, or null if the file doesn't need a copy (<see cref="NeedsShadowCopy"/>). /// Returns the same object if called multiple times with the same path. /// </returns> /// <exception cref="ArgumentNullException"><paramref name="fullPath"/> is null.</exception> /// <exception cref="ArgumentException"><paramref name="fullPath"/> is not an absolute path.</exception> /// <exception cref="IOException">Error reading file <paramref name="fullPath"/>. See <see cref="Exception.InnerException"/> for details.</exception> public MetadataShadowCopy GetMetadataShadowCopy(string fullPath, MetadataImageKind kind) { return NeedsShadowCopy(fullPath) ? GetMetadataShadowCopyNoCheck(fullPath, kind) : null; } private MetadataShadowCopy GetMetadataShadowCopyNoCheck(string fullPath, MetadataImageKind kind) { if (kind is < MetadataImageKind.Assembly or > MetadataImageKind.Module) { throw new ArgumentOutOfRangeException(nameof(kind)); } FileKey key = FileKey.Create(fullPath); lock (Guard) { CacheEntry<MetadataShadowCopy> existing; if (CopyExistsOrIsSuppressed(key, out existing)) { return existing.Public; } } CacheEntry<MetadataShadowCopy> newCopy = CreateMetadataShadowCopy(fullPath, kind); // last-write timestamp is copied from the original file at the time the snapshot was made: bool fault = true; try { key = new FileKey(fullPath, FileUtilities.GetFileTimeStamp(newCopy.Public.PrimaryModule.FullPath)); fault = false; } finally { if (fault) { newCopy.Private.Dispose(); } } lock (Guard) { CacheEntry<MetadataShadowCopy> existing; if (CopyExistsOrIsSuppressed(key, out existing)) { newCopy.Private.Dispose(); return existing.Public; } _shadowCopies.Add(key, newCopy); } return newCopy.Public; } private bool CopyExistsOrIsSuppressed(FileKey key, out CacheEntry<MetadataShadowCopy> existing) { if (_lazySuppressedFiles != null && _lazySuppressedFiles.Contains(key.FullPath)) { existing = default(CacheEntry<MetadataShadowCopy>); return true; } return _shadowCopies.TryGetValue(key, out existing); } /// <summary> /// Suppresses shadow-copying of specified path. /// </summary> /// <param name="originalPath">Full path.</param> /// <exception cref="ArgumentNullException"><paramref name="originalPath"/> is null.</exception> /// <exception cref="ArgumentException"><paramref name="originalPath"/> is not an absolute path.</exception> /// <remarks> /// Doesn't affect files that have already been shadow-copied. /// </remarks> public void SuppressShadowCopy(string originalPath) { RequireAbsolutePath(originalPath, nameof(originalPath)); lock (Guard) { _lazySuppressedFiles ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase); _lazySuppressedFiles.Add(originalPath); } } /// <summary> /// Determines whether given file is a candidate for shadow-copy. /// </summary> /// <param name="fullPath">An absolute path.</param> /// <returns>True if the shadow-copy policy applies to the specified path.</returns> /// <exception cref="NullReferenceException"><paramref name="fullPath"/> is null.</exception> /// <exception cref="ArgumentException"><paramref name="fullPath"/> is not absolute.</exception> public bool NeedsShadowCopy(string fullPath) { RequireAbsolutePath(fullPath, nameof(fullPath)); string directory = Path.GetDirectoryName(fullPath); // do not shadow-copy shadow-copies: string referencesDir = ShadowCopyDirectory; if (referencesDir != null && directory.StartsWith(referencesDir, StringComparison.Ordinal)) { return false; } return !_noShadowCopyDirectories.Any(static (dir, directory) => directory.StartsWith(dir, StringComparison.Ordinal), directory); } private CacheEntry<MetadataShadowCopy> CreateMetadataShadowCopy(string originalPath, MetadataImageKind kind) { int attempts = 10; while (true) { try { ShadowCopyDirectory ??= CreateUniqueDirectory(_baseDirectory); // Create directory for the assembly. // If the assembly has any modules they have to be copied to the same directory // and have the same names as specified in metadata. string assemblyCopyDir = CreateUniqueDirectory(ShadowCopyDirectory); string shadowCopyPath = Path.Combine(assemblyCopyDir, Path.GetFileName(originalPath)); FileShadowCopy documentationFileCopy = TryCopyDocumentationFile(originalPath, assemblyCopyDir, _documentationCommentsCulture); var manifestModuleCopyStream = CopyFile(originalPath, shadowCopyPath); var manifestModuleCopy = new FileShadowCopy(manifestModuleCopyStream, originalPath, shadowCopyPath); Metadata privateMetadata; if (kind == MetadataImageKind.Assembly) { privateMetadata = CreateAssemblyMetadata(manifestModuleCopyStream, originalPath, shadowCopyPath); } else { privateMetadata = CreateModuleMetadata(manifestModuleCopyStream); } var publicMetadata = privateMetadata.Copy(); return new CacheEntry<MetadataShadowCopy>(new MetadataShadowCopy(manifestModuleCopy, documentationFileCopy, publicMetadata), privateMetadata); } catch (DirectoryNotFoundException) { // the shadow copy directory has been deleted - try to copy all files again if (!Directory.Exists(ShadowCopyDirectory)) { ShadowCopyDirectory = null; if (attempts-- > 0) { continue; } } throw; } } } private AssemblyMetadata CreateAssemblyMetadata(FileStream manifestModuleCopyStream, string originalPath, string shadowCopyPath) { // We don't need to use the global metadata cache here since the shadow copy // won't change and is private to us - only users of the same shadow copy provider see it. ImmutableArray<ModuleMetadata>.Builder moduleBuilder = null; bool fault = true; ModuleMetadata manifestModule = null; try { manifestModule = CreateModuleMetadata(manifestModuleCopyStream); string originalDirectory = null, shadowCopyDirectory = null; foreach (string moduleName in manifestModule.GetModuleNames()) { if (moduleBuilder == null) { moduleBuilder = ImmutableArray.CreateBuilder<ModuleMetadata>(); moduleBuilder.Add(manifestModule); originalDirectory = Path.GetDirectoryName(originalPath); shadowCopyDirectory = Path.GetDirectoryName(shadowCopyPath); } FileStream moduleCopyStream = CopyFile( originalPath: Path.Combine(originalDirectory, moduleName), shadowCopyPath: Path.Combine(shadowCopyDirectory, moduleName)); moduleBuilder.Add(CreateModuleMetadata(moduleCopyStream)); } var modules = (moduleBuilder != null) ? moduleBuilder.ToImmutable() : ImmutableArray.Create(manifestModule); fault = false; return AssemblyMetadata.Create(modules); } finally { if (fault) { manifestModule?.Dispose(); if (moduleBuilder != null) { for (int i = 1; i < moduleBuilder.Count; i++) { moduleBuilder[i].Dispose(); } } } } } private static ModuleMetadata CreateModuleMetadata(FileStream stream) { // The Stream is held by the ModuleMetadata to read metadata on demand. // We hand off the responsibility for closing the stream to the metadata object. return ModuleMetadata.CreateFromStream(stream, leaveOpen: false); } private string CreateUniqueDirectory(string basePath) { int attempts = 10; while (true) { string dir = Path.Combine(basePath, Guid.NewGuid().ToString()); if (File.Exists(dir) || Directory.Exists(dir)) { // try a different name (guid): continue; } try { Directory.CreateDirectory(dir); return dir; } catch (IOException) { // Some other process might have created a file of the same name after we checked for its existence. if (File.Exists(dir)) { continue; } // This file might also have been deleted by now. So try again for a while and then give up. if (--attempts == 0) { throw; } } } } private static FileShadowCopy TryCopyDocumentationFile(string originalAssemblyPath, string assemblyCopyDirectory, CultureInfo docCultureOpt) { // Note: Doc comments are not supported for netmodules. string assemblyDirectory = Path.GetDirectoryName(originalAssemblyPath); string assemblyFileName = Path.GetFileName(originalAssemblyPath); string xmlSubdirectory; string xmlFileName; if (docCultureOpt == null || !TryFindCollocatedDocumentationFile(assemblyDirectory, assemblyFileName, docCultureOpt, out xmlSubdirectory, out xmlFileName)) { return null; } if (!xmlSubdirectory.IsEmpty()) { try { Directory.CreateDirectory(Path.Combine(assemblyCopyDirectory, xmlSubdirectory)); } catch { return null; } } string xmlCopyPath = Path.Combine(assemblyCopyDirectory, xmlSubdirectory, xmlFileName); string xmlOriginalPath = Path.Combine(assemblyDirectory, xmlSubdirectory, xmlFileName); var xmlStream = CopyFile(xmlOriginalPath, xmlCopyPath, fileMayNotExist: true); return (xmlStream != null) ? new FileShadowCopy(xmlStream, xmlOriginalPath, xmlCopyPath) : null; } private static bool TryFindCollocatedDocumentationFile( string assemblyDirectory, string assemblyFileName, CultureInfo culture, out string docSubdirectory, out string docFileName) { Debug.Assert(assemblyDirectory != null); Debug.Assert(assemblyFileName != null); Debug.Assert(culture != null); // 1. Look in subdirectories based on the current culture docFileName = Path.ChangeExtension(assemblyFileName, ".xml"); while (culture != CultureInfo.InvariantCulture) { docSubdirectory = culture.Name; if (File.Exists(Path.Combine(assemblyDirectory, docSubdirectory, docFileName))) { return true; } culture = culture.Parent; } // 2. Look in the same directory as the assembly itself docSubdirectory = string.Empty; if (File.Exists(Path.Combine(assemblyDirectory, docFileName))) { return true; } docFileName = null; return false; } private static FileStream CopyFile(string originalPath, string shadowCopyPath, bool fileMayNotExist = false) { try { File.Copy(originalPath, shadowCopyPath, overwrite: true); StripReadOnlyAttributeFromFile(new FileInfo(shadowCopyPath)); return new FileStream(shadowCopyPath, FileMode.Open, FileAccess.Read, FileShare.Read); } catch (Exception e) when (fileMayNotExist && e is FileNotFoundException or DirectoryNotFoundException) { return null; } } #region Test hooks // for testing only internal int CacheSize { get { return _shadowCopies.Count; } } #endregion } }