From 79c8093d3026b4bda5ceb9063a937dd51f452442 Mon Sep 17 00:00:00 2001 From: Matt Psaltis Date: Mon, 12 Sep 2022 17:16:15 +1000 Subject: [PATCH] Adds ShadowCopy to Neo4J Analyzer to fix file locking issues on compilation (#5353) --- .../src/Analyzers/Diagnostics/ErrorHelper.cs | 4 +- .../Analyzers/src/Analyzers/Files.cs | 8 +- ...Neo4JSourceGenerator.TypeInitialization.cs | 55 +++-- .../src/Analyzers/Neo4JSourceGenerator.cs | 9 +- .../ShadowCopyAnalyzerAssemblyLoader.cs | 202 ++++++++++++++++++ 5 files changed, 251 insertions(+), 27 deletions(-) create mode 100644 src/HotChocolate/Analyzers/src/Analyzers/ShadowCopyAnalyzerAssemblyLoader.cs diff --git a/src/HotChocolate/Analyzers/src/Analyzers/Diagnostics/ErrorHelper.cs b/src/HotChocolate/Analyzers/src/Analyzers/Diagnostics/ErrorHelper.cs index cef4680b467..6552e874d9e 100644 --- a/src/HotChocolate/Analyzers/src/Analyzers/Diagnostics/ErrorHelper.cs +++ b/src/HotChocolate/Analyzers/src/Analyzers/Diagnostics/ErrorHelper.cs @@ -97,12 +97,12 @@ public static class ErrorHelper this GeneratorExecutionContext context, IError error) { - string title = + var title = error.Extensions is not null && error.Extensions.TryGetValue(ErrorHelper.Title, out var value) && value is string s ? s : nameof(ErrorCodes.Unexpected); - string code = error.Code ?? ErrorCodes.Unexpected; + var code = error.Code ?? ErrorCodes.Unexpected; if (error is { Locations: { Count: > 0 } locations } && error.Extensions is not null && diff --git a/src/HotChocolate/Analyzers/src/Analyzers/Files.cs b/src/HotChocolate/Analyzers/src/Analyzers/Files.cs index d5ac30ad468..28db92e0beb 100644 --- a/src/HotChocolate/Analyzers/src/Analyzers/Files.cs +++ b/src/HotChocolate/Analyzers/src/Analyzers/Files.cs @@ -24,8 +24,8 @@ public static class Files { try { - string json = File.ReadAllText(configLocation); - GraphQLConfig config = GraphQLConfig.FromJson(json); + var json = File.ReadAllText(configLocation); + var config = GraphQLConfig.FromJson(json); config.Location = configLocation; list.Add(config); } @@ -62,7 +62,7 @@ public static class Files var rootDirectory = GetDirectoryName(config.Location) + DirectorySeparatorChar; var glob = Glob.Parse(config.Documents); - foreach (string file in context.AdditionalFiles + foreach (var file in context.AdditionalFiles .Select(t => t.Path) .Where(t => GetExtension(t).Equals( GraphQLExtension, @@ -71,7 +71,7 @@ public static class Files { try { - DocumentNode document = Utf8GraphQLParser.Parse(File.ReadAllBytes(file)); + var document = Utf8GraphQLParser.Parse(File.ReadAllBytes(file)); if (!document.Definitions.OfType().Any()) { diff --git a/src/HotChocolate/Analyzers/src/Analyzers/Neo4JSourceGenerator.TypeInitialization.cs b/src/HotChocolate/Analyzers/src/Analyzers/Neo4JSourceGenerator.TypeInitialization.cs index a7149ddb8dd..d13ca1617ad 100644 --- a/src/HotChocolate/Analyzers/src/Analyzers/Neo4JSourceGenerator.TypeInitialization.cs +++ b/src/HotChocolate/Analyzers/src/Analyzers/Neo4JSourceGenerator.TypeInitialization.cs @@ -1,34 +1,55 @@ using System; +using System.Diagnostics; +using System.IO; using System.Reflection; using static System.IO.Path; -namespace HotChocolate.Analyzers +namespace HotChocolate.Analyzers; + +public partial class Neo4JSourceGenerator { - public partial class Neo4JSourceGenerator + private const string _dll = ".dll"; + + private static string _location = + GetFullPath(GetDirectoryName(typeof(Neo4JSourceGenerator).Assembly.Location)!); + + private static readonly ShadowCopyAnalyzerAssemblyLoader _loader; + + static Neo4JSourceGenerator() { - private const string _dll = ".dll"; - private static string _location = - GetDirectoryName(typeof(Neo4JSourceGenerator).Assembly.Location)!; + _loader = new ShadowCopyAnalyzerAssemblyLoader(); - static Neo4JSourceGenerator() - { - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; - } + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve; + } - private static Assembly? CurrentDomainOnAssemblyResolve( - object sender, - ResolveEventArgs args) + private static Assembly? CurrentDomainOnAssemblyResolve( + object sender, + ResolveEventArgs args) + { + var path = default(string); + try { - try + var assemblyName = new AssemblyName(args.Name); + path = Combine(_location, assemblyName.Name + _dll); + + if (!File.Exists(path)) { - var assemblyName = new AssemblyName(args.Name); - var path = Combine(_location, assemblyName.Name + _dll); - return Assembly.LoadFrom(path); + return null; } - catch + + Debug.WriteLine(path); + var shadowCopyPath = _loader.GetPathToLoad(path); + if (!File.Exists(shadowCopyPath)) { return null; } + + return Assembly.LoadFrom(shadowCopyPath); + } + catch(Exception ex) + { + Debug.WriteLine($@"Failure for {path}: {ex}"); + throw; } } } diff --git a/src/HotChocolate/Analyzers/src/Analyzers/Neo4JSourceGenerator.cs b/src/HotChocolate/Analyzers/src/Analyzers/Neo4JSourceGenerator.cs index f3b8611da17..c57a32bef56 100644 --- a/src/HotChocolate/Analyzers/src/Analyzers/Neo4JSourceGenerator.cs +++ b/src/HotChocolate/Analyzers/src/Analyzers/Neo4JSourceGenerator.cs @@ -27,7 +27,7 @@ private void ExecuteInternal(GeneratorExecutionContext context) { var codeGenerator = new Neo4JCodeGenerator(); - foreach (GraphQLConfig config in context.GetConfigurations()) + foreach (var config in context.GetConfigurations()) { if (config.Extensions.Neo4J is { } settings && @@ -36,11 +36,12 @@ private void ExecuteInternal(GeneratorExecutionContext context) var codeGeneratorContext = new CodeGeneratorContext( settings.Name, settings.DatabaseName, - settings.Namespace ?? throw new Exception("Namespace is required"), // TODO: Review in PR!! + // TODO: Review in PR!! + settings.Namespace ?? throw new Exception("Namespace is required"), schemaDocuments); - CodeGenerationResult? result = codeGenerator.Generate(codeGeneratorContext); - foreach (SourceFile? sourceFile in result.SourceFiles) + var result = codeGenerator.Generate(codeGeneratorContext); + foreach (var sourceFile in result.SourceFiles) { context.AddSource(sourceFile.Name, sourceFile.Source); } diff --git a/src/HotChocolate/Analyzers/src/Analyzers/ShadowCopyAnalyzerAssemblyLoader.cs b/src/HotChocolate/Analyzers/src/Analyzers/ShadowCopyAnalyzerAssemblyLoader.cs new file mode 100644 index 00000000000..c948730f65e --- /dev/null +++ b/src/HotChocolate/Analyzers/src/Analyzers/ShadowCopyAnalyzerAssemblyLoader.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using static System.IO.Path; + +namespace HotChocolate.Analyzers; + +internal class ShadowCopyAnalyzerAssemblyLoader +{ + /// + /// The base directory for shadow copies. Each instance of + /// gets its own + /// subdirectory under this directory. This is also the starting point + /// for scavenge operations. + /// + private readonly string _baseDirectory; + + /// + /// The directory where this instance of + /// will shadow-copy assemblies, and the mutex created to mark that the owner of it is still active. + /// + private readonly Lazy<(string directory, Mutex)> _shadowCopyDirectoryAndMutex; + + internal readonly Task DeleteLeftoverDirectoriesTask; + + /// + /// Used to generate unique names for per-assembly directories. Should be updated with + /// . + /// + private int _assemblyDirectoryId; + + public ShadowCopyAnalyzerAssemblyLoader(string? baseDirectory = null) + { + _baseDirectory = baseDirectory ?? + Combine(GetTempPath(), "HotChocolate", "AnalyzerShadowCopies"); + + _shadowCopyDirectoryAndMutex = new Lazy<(string directory, Mutex)>( + () => CreateUniqueDirectoryForProcess(), + LazyThreadSafetyMode.ExecutionAndPublication); + + DeleteLeftoverDirectoriesTask = Task.Run(DeleteLeftoverDirectories); + } + + private void DeleteLeftoverDirectories() + { + // Avoid first chance exception + if (!Directory.Exists(_baseDirectory)) + { + return; + } + + IEnumerable subDirectories; + try + { + subDirectories = Directory.EnumerateDirectories(_baseDirectory); + } + catch (DirectoryNotFoundException) + { + return; + } + + foreach (var subDirectory in subDirectories) + { + var name = 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)) + { + ClearReadOnlyFlagOnFiles(subDirectory); + Directory.Delete(subDirectory, 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 + { + mutex?.Dispose(); + } + } + } + + internal string GetPathToLoad(string fullPath) + { + var assemblyDirectory = CreateUniqueDirectoryForAssembly(); + var shadowCopyPath = CopyFileAndResources(fullPath, assemblyDirectory); + return shadowCopyPath; + } + + private static string CopyFileAndResources(string fullPath, string assemblyDirectory) + { + var fileNameWithExtension = GetFileName(fullPath); + var shadowCopyPath = Combine(assemblyDirectory, fileNameWithExtension); + + CopyFile(fullPath, shadowCopyPath); + + var originalDirectory = GetDirectoryName(fullPath)!; + var fileNameWithoutExtension = GetFileNameWithoutExtension(fileNameWithExtension); + var resourcesNameWithoutExtension = fileNameWithoutExtension + ".resources"; + var resourcesNameWithExtension = resourcesNameWithoutExtension + ".dll"; + + foreach (var directory in Directory.EnumerateDirectories(originalDirectory)) + { + var directoryName = GetFileName(directory); + + var resourcesPath = Combine(directory, resourcesNameWithExtension); + if (File.Exists(resourcesPath)) + { + var resourcesShadowCopyPath = Combine( + assemblyDirectory, + directoryName, + resourcesNameWithExtension); + CopyFile(resourcesPath, resourcesShadowCopyPath); + } + + resourcesPath = Combine( + directory, + resourcesNameWithoutExtension, + resourcesNameWithExtension); + + if (File.Exists(resourcesPath)) + { + var resourcesShadowCopyPath = Combine( + assemblyDirectory, + directoryName, + resourcesNameWithoutExtension, + resourcesNameWithExtension); + CopyFile(resourcesPath, resourcesShadowCopyPath); + } + } + + return shadowCopyPath; + } + + private static void CopyFile(string originalPath, string shadowCopyPath) + { + var directory = GetDirectoryName(shadowCopyPath); + Directory.CreateDirectory(directory!); + + File.Copy(originalPath, shadowCopyPath); + + ClearReadOnlyFlagOnFile(new FileInfo(shadowCopyPath)); + } + + private static void ClearReadOnlyFlagOnFiles(string directoryPath) + { + var directory = new DirectoryInfo(directoryPath); + + foreach (var file in directory.EnumerateFiles("*", 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. + } + } + + private string CreateUniqueDirectoryForAssembly() + { + var directoryId = Interlocked.Increment(ref _assemblyDirectoryId); + + var directory = Combine( + _shadowCopyDirectoryAndMutex.Value.directory, + directoryId.ToString()); + + Directory.CreateDirectory(directory); + return directory; + } + + private (string directory, Mutex mutex) CreateUniqueDirectoryForProcess() + { + var guid = Guid.NewGuid() + .ToString("N") + .ToLowerInvariant(); + var directory = Combine(_baseDirectory, guid); + + var mutex = new Mutex(false, guid); + + Directory.CreateDirectory(directory); + + return (directory, mutex); + } +}