From 3e2c54f3b22a7926efa0b9729ef4157f3e035c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 21 Oct 2025 15:17:20 +0200 Subject: [PATCH 1/6] cleanup temp folder --- src/Shared/TempFileUtilities.cs | 55 ++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/Shared/TempFileUtilities.cs b/src/Shared/TempFileUtilities.cs index 2b77811c660..f685da12edc 100644 --- a/src/Shared/TempFileUtilities.cs +++ b/src/Shared/TempFileUtilities.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.CompilerServices; +using System.Threading; using Microsoft.Build.Shared.FileSystem; #nullable disable @@ -16,16 +17,46 @@ namespace Microsoft.Build.Shared /// internal static partial class FileUtilities { - // For the current user, these correspond to read, write, and execute permissions. - // Lower order bits correspond to the same for "group" or "other" users. private static string tempFileDirectory = null; + private static int cleanupRegistered = 0; private const string msbuildTempFolderPrefix = "MSBuildTemp"; internal static string TempFileDirectory { get { - return tempFileDirectory ??= CreateFolderUnderTemp(); + if (tempFileDirectory == null) + { + string path = CreateFolderUnderTemp(); + if (Interlocked.CompareExchange(ref tempFileDirectory, path, null) == null) + { + // We won the race - register cleanup for this path + RegisterCleanupOnExit(path); + } + } + + return tempFileDirectory; + } + } + + private static void RegisterCleanupOnExit(string pathToCleanup) + { + if (Interlocked.CompareExchange(ref cleanupRegistered, 1, 0) == 0) + { + AppDomain.CurrentDomain.ProcessExit += (_, _) => + { + try + { + if (Directory.Exists(pathToCleanup)) + { + Directory.Delete(pathToCleanup, recursive: true); + } + } + catch + { + // Best effort - ignore failures during cleanup + } + }; } } @@ -34,23 +65,17 @@ internal static void ClearTempFileDirectory() tempFileDirectory = null; } - // For all native calls, directly check their return values to prevent bad actors from getting in between checking if a directory exists and returning it. private static string CreateFolderUnderTemp() { string path = null; - if (NativeMethodsShared.IsLinux) - { -#if NET // always true, Linux implies NET - path = Directory.CreateTempSubdirectory(msbuildTempFolderPrefix).FullName; +#if NET + path = Directory.CreateTempSubdirectory(msbuildTempFolderPrefix).FullName; +#else + // the CreateTempSubdirectory API is not available in .NET Framework + path = Path.Combine(Path.GetTempPath(), $"{msbuildTempFolderPrefix}{Guid.NewGuid():N}"); #endif - } - else - { - path = Path.Combine(Path.GetTempPath(), msbuildTempFolderPrefix); - Directory.CreateDirectory(path); - } - + Directory.CreateDirectory(path); return FileUtilities.EnsureTrailingSlash(path); } From f53697966688a936289664fdfbb4dbec84d4e928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 21 Oct 2025 15:27:44 +0200 Subject: [PATCH 2/6] cleanup inline task temp code --- .../BackEnd/BuildManager/BuildManager.cs | 5 --- src/Shared/TaskFactoryUtilities.cs | 45 +------------------ src/Tasks.UnitTests/CodeTaskFactoryTests.cs | 3 -- .../RoslynCodeTaskFactory_Tests.cs | 3 -- .../TaskFactoryUtilities_Tests.cs | 1 - src/Tasks.UnitTests/XamlTaskFactory_Tests.cs | 3 -- 6 files changed, 1 insertion(+), 59 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 3032b80f93a..7a70791f17b 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1151,11 +1151,6 @@ public void EndBuild() Reset(); _buildManagerState = BuildManagerState.Idle; - if (Traits.Instance.ForceTaskFactoryOutOfProc || _buildParameters.MultiThreaded) - { - TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); - } - MSBuildEventSource.Log.BuildStop(); _threadException?.Throw(); diff --git a/src/Shared/TaskFactoryUtilities.cs b/src/Shared/TaskFactoryUtilities.cs index 0f671430f87..1dc8353799e 100644 --- a/src/Shared/TaskFactoryUtilities.cs +++ b/src/Shared/TaskFactoryUtilities.cs @@ -27,10 +27,6 @@ namespace Microsoft.Build.Shared /// internal static class TaskFactoryUtilities { - /// - /// The sub-path within the temporary directory where compiled inline tasks are located. - /// - public const string InlineTaskTempDllSubPath = nameof(InlineTaskTempDllSubPath); public const string InlineTaskSuffix = "inline_task.dll"; public const string InlineTaskLoadManifestSuffix = ".loadmanifest"; @@ -57,30 +53,13 @@ public CachedAssemblyEntry(Assembly assembly, string assemblyPath) public bool IsValid => string.IsNullOrEmpty(AssemblyPath) || FileUtilities.FileExistsNoThrow(AssemblyPath); } - - /// - /// Creates a process-specific temporary directory for inline task assemblies. - /// - /// The path to the created temporary directory. - public static string CreateProcessSpecificTemporaryTaskDirectory() - { - string processSpecificInlineTaskDir = Path.Combine( - FileUtilities.TempFileDirectory, - InlineTaskTempDllSubPath, - $"pid_{EnvironmentUtilities.CurrentProcessId}"); - - Directory.CreateDirectory(processSpecificInlineTaskDir); - return processSpecificInlineTaskDir; - } - /// /// Gets a temporary file path for an inline task assembly in the process-specific directory. /// /// The full path to the temporary file. public static string GetTemporaryTaskAssemblyPath() { - string taskDir = CreateProcessSpecificTemporaryTaskDirectory(); - return FileUtilities.GetTemporaryFile(taskDir, fileName: null, extension: "inline_task.dll", createFile: false); + return FileUtilities.GetTemporaryFile(directory: null, fileName: null, extension: "inline_task.dll", createFile: false); } /// @@ -256,28 +235,6 @@ public static bool ShouldCompileForOutOfProcess(IBuildEngine taskFactoryEngineCo return false; } - /// - /// Cleans up the current process's inline task directory by deleting the temporary directory - /// and its contents used for inline task assemblies for this specific process. - /// This should be called at the end of a build to prevent dangling DLL files. - /// - /// - /// On Windows platforms, this may fail to delete files that are still locked by the current process. - /// However, it will clean up any files that are no longer in use. - /// - public static void CleanCurrentProcessInlineTaskDirectory() - { - string processSpecificInlineTaskDir = Path.Combine( - FileUtilities.TempFileDirectory, - InlineTaskTempDllSubPath, - $"pid_{EnvironmentUtilities.CurrentProcessId}"); - - if (FileSystems.Default.DirectoryExists(processSpecificInlineTaskDir)) - { - FileUtilities.DeleteDirectoryNoThrow(processSpecificInlineTaskDir, recursive: true); - } - } - /// /// Attempts to load an assembly by searching in the specified directories. /// diff --git a/src/Tasks.UnitTests/CodeTaskFactoryTests.cs b/src/Tasks.UnitTests/CodeTaskFactoryTests.cs index b61dd893659..2ab31ff10f8 100644 --- a/src/Tasks.UnitTests/CodeTaskFactoryTests.cs +++ b/src/Tasks.UnitTests/CodeTaskFactoryTests.cs @@ -758,8 +758,6 @@ public void BuildTaskSimpleCodeFactoryTestExtraReference(bool forceOutOfProc) [Fact] public void OutOfProcCodeTaskFactoryCachesAssemblyPath() { - TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); - try { const string taskElementContents = @" @@ -805,7 +803,6 @@ public void OutOfProcCodeTaskFactoryCachesAssemblyPath() } finally { - TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); } } diff --git a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs index 4992ba064f7..814ea1e440a 100644 --- a/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs +++ b/src/Tasks.UnitTests/RoslynCodeTaskFactory_Tests.cs @@ -238,8 +238,6 @@ public void RoslynCodeTaskFactory_ReuseCompilation(bool forceOutOfProc) [Fact] public void OutOfProcRoslynTaskFactoryCachesAssemblyPath() { - TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); - try { const string taskBody = @" @@ -285,7 +283,6 @@ public void OutOfProcRoslynTaskFactoryCachesAssemblyPath() } finally { - TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); } } diff --git a/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs b/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs index 1bc4386371e..4e10d18e565 100644 --- a/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs +++ b/src/Tasks.UnitTests/TaskFactoryUtilities_Tests.cs @@ -26,7 +26,6 @@ public void GetTemporaryTaskAssemblyPath_ShouldReturnValidPath() // Assert assemblyPath.ShouldNotBeNull(); assemblyPath.ShouldEndWith(".dll"); - Path.GetDirectoryName(assemblyPath).ShouldContain(TaskFactoryUtilities.InlineTaskTempDllSubPath); } [Fact] diff --git a/src/Tasks.UnitTests/XamlTaskFactory_Tests.cs b/src/Tasks.UnitTests/XamlTaskFactory_Tests.cs index 370d6dd587b..8eb7e642931 100644 --- a/src/Tasks.UnitTests/XamlTaskFactory_Tests.cs +++ b/src/Tasks.UnitTests/XamlTaskFactory_Tests.cs @@ -450,8 +450,6 @@ public class CompilationTests [Fact] public void OutOfProcXamlTaskFactoryProvidesAssemblyPath() { - TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); - try { const string taskElementContents = @" @@ -480,7 +478,6 @@ public void OutOfProcXamlTaskFactoryProvidesAssemblyPath() } finally { - TaskFactoryUtilities.CleanCurrentProcessInlineTaskDirectory(); } } From d070013e81d028dafef4aa31c190b043c841e9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Tue, 21 Oct 2025 16:24:51 +0200 Subject: [PATCH 3/6] fix directory creation --- src/Shared/TempFileUtilities.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Shared/TempFileUtilities.cs b/src/Shared/TempFileUtilities.cs index f685da12edc..984ca387e51 100644 --- a/src/Shared/TempFileUtilities.cs +++ b/src/Shared/TempFileUtilities.cs @@ -63,19 +63,24 @@ private static void RegisterCleanupOnExit(string pathToCleanup) internal static void ClearTempFileDirectory() { tempFileDirectory = null; + // Note: We don't clear cleanupRegistered here because ProcessExit handlers + // can only be registered once per AppDomain, and we want to avoid registering + // multiple handlers. The existing handler will safely no-op if the directory + // doesn't exist. } private static string CreateFolderUnderTemp() { - string path = null; + string path; #if NET path = Directory.CreateTempSubdirectory(msbuildTempFolderPrefix).FullName; #else - // the CreateTempSubdirectory API is not available in .NET Framework + // CreateTempSubdirectory API is not available in .NET Framework path = Path.Combine(Path.GetTempPath(), $"{msbuildTempFolderPrefix}{Guid.NewGuid():N}"); -#endif Directory.CreateDirectory(path); +#endif + return FileUtilities.EnsureTrailingSlash(path); } From 4298f4a073e92b572491eb15f7053a494af71861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 22 Oct 2025 10:26:51 +0200 Subject: [PATCH 4/6] reset caching of temp directory in tests --- src/Shared/TempFileUtilities.cs | 5 +---- src/UnitTests.Shared/TestEnvironment.cs | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Shared/TempFileUtilities.cs b/src/Shared/TempFileUtilities.cs index 984ca387e51..1840d2b0f51 100644 --- a/src/Shared/TempFileUtilities.cs +++ b/src/Shared/TempFileUtilities.cs @@ -63,10 +63,7 @@ private static void RegisterCleanupOnExit(string pathToCleanup) internal static void ClearTempFileDirectory() { tempFileDirectory = null; - // Note: We don't clear cleanupRegistered here because ProcessExit handlers - // can only be registered once per AppDomain, and we want to avoid registering - // multiple handlers. The existing handler will safely no-op if the directory - // doesn't exist. + /* the directory on disk is deleted on process exit */ } private static string CreateFolderUnderTemp() diff --git a/src/UnitTests.Shared/TestEnvironment.cs b/src/UnitTests.Shared/TestEnvironment.cs index 1525ccbfa94..53ff185d7ad 100644 --- a/src/UnitTests.Shared/TestEnvironment.cs +++ b/src/UnitTests.Shared/TestEnvironment.cs @@ -600,6 +600,9 @@ public TransientTempPath(string tempPath, bool deleteTempDirectory) _deleteTempDirectory = deleteTempDirectory; _oldtempPaths = SetTempPath(tempPath); + + // Clear the cached temp directory so FileUtilities picks up the new TMPDIR/TMP/TEMP + FileUtilities.ClearTempFileDirectory(); } private static TempPaths SetTempPath(string tempPath) @@ -645,6 +648,9 @@ private static TempPaths GetTempPaths() public override void Revert() { SetTempPaths(_oldtempPaths); + + // Clear the cached temp directory so FileUtilities picks up the restored TMPDIR/TMP/TEMP + FileUtilities.ClearTempFileDirectory(); if (_deleteTempDirectory) { From 986ba3bdbd165064f948d36336b7521d6b8d63f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 22 Oct 2025 10:39:33 +0200 Subject: [PATCH 5/6] make the initialization lazy for threadsafety --- src/Shared/TempFileUtilities.cs | 52 ++++++++++++++------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/Shared/TempFileUtilities.cs b/src/Shared/TempFileUtilities.cs index 1840d2b0f51..909b9863dc1 100644 --- a/src/Shared/TempFileUtilities.cs +++ b/src/Shared/TempFileUtilities.cs @@ -17,53 +17,45 @@ namespace Microsoft.Build.Shared /// internal static partial class FileUtilities { - private static string tempFileDirectory = null; - private static int cleanupRegistered = 0; + private static Lazy tempFileDirectory = CreateTempFileDirectoryLazy(); + private const string msbuildTempFolderPrefix = "MSBuildTemp"; - internal static string TempFileDirectory + internal static string TempFileDirectory => tempFileDirectory.Value; + + private static Lazy CreateTempFileDirectoryLazy() { - get - { - if (tempFileDirectory == null) + return new Lazy( + () => { string path = CreateFolderUnderTemp(); - if (Interlocked.CompareExchange(ref tempFileDirectory, path, null) == null) - { - // We won the race - register cleanup for this path - RegisterCleanupOnExit(path); - } - } - - return tempFileDirectory; - } + RegisterCleanupOnExit(path); + return path; + }, + LazyThreadSafetyMode.ExecutionAndPublication); } private static void RegisterCleanupOnExit(string pathToCleanup) { - if (Interlocked.CompareExchange(ref cleanupRegistered, 1, 0) == 0) + AppDomain.CurrentDomain.ProcessExit += (_, _) => { - AppDomain.CurrentDomain.ProcessExit += (_, _) => + try { - try - { - if (Directory.Exists(pathToCleanup)) - { - Directory.Delete(pathToCleanup, recursive: true); - } - } - catch + if (Directory.Exists(pathToCleanup)) { - // Best effort - ignore failures during cleanup + Directory.Delete(pathToCleanup, recursive: true); } - }; - } + } + catch + { + // Best effort - ignore failures during cleanup + } + }; } internal static void ClearTempFileDirectory() { - tempFileDirectory = null; - /* the directory on disk is deleted on process exit */ + tempFileDirectory = CreateTempFileDirectoryLazy(); } private static string CreateFolderUnderTemp() From dd35714685b829b06b3462c05a6fec6faaf478b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 22 Oct 2025 11:24:30 +0200 Subject: [PATCH 6/6] fix test infra --- src/UnitTests.Shared/TestEnvironment.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/UnitTests.Shared/TestEnvironment.cs b/src/UnitTests.Shared/TestEnvironment.cs index 53ff185d7ad..586afed20e0 100644 --- a/src/UnitTests.Shared/TestEnvironment.cs +++ b/src/UnitTests.Shared/TestEnvironment.cs @@ -599,6 +599,10 @@ public TransientTempPath(string tempPath, bool deleteTempDirectory) TempPath = tempPath; _deleteTempDirectory = deleteTempDirectory; + // Ensure the temp directory exists before setting it as TMPDIR + // This is required because Directory.CreateTempSubdirectory() expects TMPDIR to exist + Directory.CreateDirectory(tempPath); + _oldtempPaths = SetTempPath(tempPath); // Clear the cached temp directory so FileUtilities picks up the new TMPDIR/TMP/TEMP