diff --git a/src/BenchmarkDotNet/Helpers/FileCleanupHelper.cs b/src/BenchmarkDotNet/Helpers/FileCleanupHelper.cs new file mode 100644 index 0000000000..a916e7bae2 --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/FileCleanupHelper.cs @@ -0,0 +1,91 @@ +namespace BenchmarkDotNet.Helpers; + +internal static class FileCleanupHelper +{ + public static void Cleanup(string path) + { + if (File.Exists(path)) + { + CleanupFile(new FileInfo(path)); + return; + } + + if (Directory.Exists(path)) + { + CleanupDirectory(new DirectoryInfo(path)); + } + } + + private static void CleanupFile(FileInfo fileInfo) + { + TryExecute(() => + { + File.SetAttributes(fileInfo.FullName, FileAttributes.Normal); + File.Delete(fileInfo.FullName); + }); + } + + private static void CleanupDirectory(DirectoryInfo directoryInfo) + { + // Delete files + foreach (var file in EnumerateFiles(directoryInfo)) + { + CleanupFile(file); + } + + // Delete sub directories + foreach (var subDirectory in EnumerateSubDirectories(directoryInfo)) + { + CleanupDirectory(subDirectory); + } + + // Delete directory + TryExecute(() => directoryInfo.Delete(recursive: false)); + } + + private static void TryExecute(Action action) + { + try + { + action(); + } + catch + { + // Ignore exceptions for access denied, in use, etc. + } + } + +#if NETSTANDARD2_0 + private static IEnumerable EnumerateFiles(DirectoryInfo directory) + => TryExecute(() => directory.EnumerateFiles("*", SearchOption.TopDirectoryOnly), []); + + private static IEnumerable EnumerateSubDirectories(DirectoryInfo directory) + => TryExecute(() => directory.EnumerateDirectories(), []); + + private static T TryExecute(Func func, T fallback) + { + try + { + return func(); + } + catch + { + // Ignore exceptions for access denied, in use, etc. + return fallback; + } + } +#else + private static readonly EnumerationOptions EnumerationOptions = new() + { + RecurseSubdirectories = false, + IgnoreInaccessible = true, + AttributesToSkip = FileAttributes.ReparsePoint + }; + + private static IEnumerable EnumerateFiles(DirectoryInfo directory) + => directory.EnumerateFiles("*", EnumerationOptions); + + private static IEnumerable EnumerateSubDirectories(DirectoryInfo currentDir) + => currentDir.EnumerateDirectories("*", EnumerationOptions); +#endif +} diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index c306fbec21..291e1493ed 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -808,22 +808,7 @@ private static void Cleanup(ILogger logger, HashSet artifactsToCleanup) { foreach (string path in artifactsToCleanup) { - try - { - if (Directory.Exists(path)) - { - Directory.Delete(path, recursive: true); - } - else if (File.Exists(path)) - { - File.Delete(path); - } - } - catch (Exception) - { - // sth is locking our auto-generated files - // there is very little we can do about it - } + FileCleanupHelper.Cleanup(path); } } diff --git a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs index e8250796cb..177d0985af 100644 --- a/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs +++ b/src/BenchmarkDotNet/Toolchains/GeneratorBase.cs @@ -27,6 +27,10 @@ public async ValueTask GenerateProjectAsync(BuildPartition build { artifactsPaths = GetArtifactsPaths(buildPartition, rootArtifactsFolderPath); + // Cleanup artifact files/directories before generating project files. + foreach (var path in GetArtifactsToCleanup(artifactsPaths)) + FileCleanupHelper.Cleanup(path); + // There is no async file copy API, so we just do it synchronously. We are likely on a ThreadPool thread here anyway if this generator is ran in parallel. CopyAllRequiredFiles(artifactsPaths);