Skip to content

Commit

Permalink
Adds ShadowCopy to Neo4J Analyzer to fix file locking issues on compi…
Browse files Browse the repository at this point in the history
…lation (#5353)
  • Loading branch information
matt-psaltis committed Sep 12, 2022
1 parent a5b84b3 commit 79c8093
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 27 deletions.
Expand Up @@ -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 &&
Expand Down
8 changes: 4 additions & 4 deletions src/HotChocolate/Analyzers/src/Analyzers/Files.cs
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand All @@ -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<IExecutableDefinitionNode>().Any())
{
Expand Down
@@ -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;
}
}
}
Expand Up @@ -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 &&
Expand All @@ -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);
}
Expand Down
@@ -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
{
/// <summary>
/// The base directory for shadow copies. Each instance of
/// <see cref="ShadowCopyAnalyzerAssemblyLoader" /> gets its own
/// subdirectory under this directory. This is also the starting point
/// for scavenge operations.
/// </summary>
private readonly string _baseDirectory;

/// <summary>
/// The directory where this instance of <see cref="ShadowCopyAnalyzerAssemblyLoader" />
/// will shadow-copy assemblies, and the mutex created to mark that the owner of it is still active.
/// </summary>
private readonly Lazy<(string directory, Mutex)> _shadowCopyDirectoryAndMutex;

internal readonly Task DeleteLeftoverDirectoriesTask;

/// <summary>
/// Used to generate unique names for per-assembly directories. Should be updated with
/// <see cref="Interlocked.Increment(ref int)" />.
/// </summary>
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<string> 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);
}
}

0 comments on commit 79c8093

Please sign in to comment.