diff --git a/src/Build.UnitTests/BackEnd/DebugUtils_tests.cs b/src/Build.UnitTests/BackEnd/DebugUtils_tests.cs index cd8e07ac4fd..24a34de7b81 100644 --- a/src/Build.UnitTests/BackEnd/DebugUtils_tests.cs +++ b/src/Build.UnitTests/BackEnd/DebugUtils_tests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using Shouldly; @@ -58,8 +59,8 @@ public void SetDebugPath_WhenUserSetRelativePath() var transientDebugEngine = env.SetEnvironmentVariable("MSBuildDebugEngine", "1"); try { - DebugUtils.SetDebugPath(); - string resultPath = DebugUtils.DebugPath; + FrameworkDebugUtils.SetDebugPath(); + string resultPath = FrameworkDebugUtils.DebugPath; resultPath.ShouldNotBeNull(); resultPath.ShouldBe(Path.Combine(relativePath, ".MSBuild_Logs")); Directory.Exists(resultPath).ShouldBeTrue(); @@ -69,7 +70,7 @@ public void SetDebugPath_WhenUserSetRelativePath() // Reset DebugPath to not affect other tests transientEnvVar.Revert(); transientDebugEngine.Revert(); - DebugUtils.SetDebugPath(); + FrameworkDebugUtils.SetDebugPath(); } } } @@ -93,8 +94,8 @@ public void SetDebugPath_WhenUserSetAbsolutePath() var transientDebugEngine = env.SetEnvironmentVariable("MSBuildDebugEngine", "1"); try { - DebugUtils.SetDebugPath(); - string resultPath = DebugUtils.DebugPath; + FrameworkDebugUtils.SetDebugPath(); + string resultPath = FrameworkDebugUtils.DebugPath; resultPath.ShouldNotBeNull(); resultPath.ShouldBe(Path.Combine(fullInSolutionPath, ".MSBuild_Logs")); } @@ -103,7 +104,7 @@ public void SetDebugPath_WhenUserSetAbsolutePath() // Reset DebugPath to not affect other tests transientEnvVar.Revert(); transientDebugEngine.Revert(); - DebugUtils.SetDebugPath(); + FrameworkDebugUtils.SetDebugPath(); } } } @@ -124,8 +125,8 @@ public void SetDebugPath_WhenUserNotSetDebugPath() var transientDebugEngine = env.SetEnvironmentVariable("MSBuildDebugEngine", "1"); try { - DebugUtils.SetDebugPath(); - string resultPath = DebugUtils.DebugPath; + FrameworkDebugUtils.SetDebugPath(); + string resultPath = FrameworkDebugUtils.DebugPath; resultPath.ShouldNotBeNull(); resultPath.ShouldBe(Path.Combine(Directory.GetCurrentDirectory(), ".MSBuild_Logs")); } @@ -134,7 +135,7 @@ public void SetDebugPath_WhenUserNotSetDebugPath() // Reset DebugPath to not affect other tests transientEnvVar.Revert(); transientDebugEngine.Revert(); - DebugUtils.SetDebugPath(); + FrameworkDebugUtils.SetDebugPath(); } } } @@ -144,7 +145,7 @@ public void IsInTaskHostNode_ReturnsFalseForCentralNode() { // When running in the main test process (no /nodemode argument), // we should not be in a TaskHost node - DebugUtils.IsInTaskHostNode().ShouldBeFalse(); + FrameworkDebugUtils.IsInTaskHostNode().ShouldBeFalse(); } } } diff --git a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs index cedfcf48166..24f959157d4 100644 --- a/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskExecutionHost_Tests.cs @@ -1073,7 +1073,7 @@ public void TaskExceptionHandlingTest(Type exceptionType, bool isCritical) // Force initing the DebugPath from the env var - as we need it to be unique for those tests. // The ProjectCacheTests DataMemberAttribute usages (specifically SuccessfulGraphsWithBuildParameters) lead // to the DebugPath being set before this test runs - and hence the env var is ignored. - DebugUtils.SetDebugPath(); + FrameworkDebugUtils.SetDebugPath(); ObjectModelHelpers.BuildProjectExpectFailure($""" @@ -1102,7 +1102,7 @@ public void TaskExceptionHandlingTest(Type exceptionType, bool isCritical) // Reset DebugPath to not affect other tests transientEnvVar.Revert(); - DebugUtils.SetDebugPath(); + FrameworkDebugUtils.SetDebugPath(); } [Fact] diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index c563c4f5b93..6cba67e231c 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -724,13 +724,13 @@ ILoggingService InitializeLoggingService() // VS builds discard many msbuild events so attach a binlogger to capture them all. IEnumerable AppendDebuggingLoggers(IEnumerable loggers) { - if (DebugUtils.ShouldDebugCurrentProcess is false || + if (FrameworkDebugUtils.ShouldDebugCurrentProcess is false || Traits.Instance.DebugEngine is false) { return loggers; } - var binlogPath = DebugUtils.FindNextAvailableDebugFilePath($"{DebugUtils.ProcessInfoString}_BuildManager_{_hostName}.binlog"); + var binlogPath = DebugUtils.FindNextAvailableDebugFilePath($"{FrameworkDebugUtils.ProcessInfoString}_BuildManager_{_hostName}.binlog"); var logger = new BinaryLogger { Parameters = binlogPath }; @@ -811,7 +811,7 @@ private static void AttachDebugger() return; } - if (!DebugUtils.ShouldDebugCurrentProcess) + if (!FrameworkDebugUtils.ShouldDebugCurrentProcess) { return; } @@ -2239,7 +2239,7 @@ static void DumpGraph(ProjectGraph graph, IReadOnlyDictionary(other._environmentProperties) : null; _forwardingLoggers = other._forwardingLoggers != null ? new List(other._forwardingLoggers) : null; @@ -1040,7 +1040,7 @@ private static int GetStaticIntVariableOrDefault(string environmentVariable, ref /// private void Initialize(PropertyDictionary environmentProperties, ProjectRootElementCacheBase projectRootElementCache, ToolsetProvider toolsetProvider) { - _buildProcessEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + _buildProcessEnvironment = FrameworkCommunicationsUtilities.GetEnvironmentVariables(); _environmentProperties = environmentProperties; ProjectRootElementCache = projectRootElementCache; ResetCaches = true; diff --git a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs index 616075f775a..42d90d11290 100644 --- a/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs +++ b/src/Build/BackEnd/Components/BuildRequestEngine/BuildRequestEngine.cs @@ -131,7 +131,7 @@ internal class BuildRequestEngine : IBuildRequestEngine, IBuildComponent internal BuildRequestEngine() { _debugDumpState = Traits.Instance.DebugScheduler; - _debugDumpPath = DebugUtils.DebugPath; + _debugDumpPath = FrameworkDebugUtils.DebugPath; _debugForceCaching = Environment.GetEnvironmentVariable("MSBUILDDEBUGFORCECACHING") == "1"; if (String.IsNullOrEmpty(_debugDumpPath)) diff --git a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs index 402ab04ea5d..8306de90171 100644 --- a/src/Build/BackEnd/Components/Scheduler/Scheduler.cs +++ b/src/Build/BackEnd/Components/Scheduler/Scheduler.cs @@ -14,7 +14,6 @@ using Microsoft.Build.Framework; using Microsoft.Build.ProjectCache; using Microsoft.Build.Shared; -using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Shared.FileSystem; using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException; using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; @@ -212,7 +211,7 @@ public Scheduler() { // Be careful moving these to Traits, changing the timing of reading environment variables has a breaking potential. _debugDumpState = Traits.Instance.DebugScheduler; - _debugDumpPath = DebugUtils.DebugPath; + _debugDumpPath = FrameworkDebugUtils.DebugPath; _schedulingUnlimitedVariable = Environment.GetEnvironmentVariable("MSBUILDSCHEDULINGUNLIMITED"); _nodeLimitOffset = 0; diff --git a/src/Build/BackEnd/Node/InProcNode.cs b/src/Build/BackEnd/Node/InProcNode.cs index 3aea5d39fa4..2e61667a0c2 100644 --- a/src/Build/BackEnd/Node/InProcNode.cs +++ b/src/Build/BackEnd/Node/InProcNode.cs @@ -9,7 +9,6 @@ using Microsoft.Build.BackEnd.Components.Caching; using Microsoft.Build.Execution; using Microsoft.Build.Framework; -using Microsoft.Build.Internal; using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService; @@ -352,7 +351,7 @@ private NodeEngineShutdownReason HandleShutdown(out Exception exception) NativeMethodsShared.SetCurrentDirectory(_savedCurrentDirectory); // Restore the original environment. - CommunicationsUtilities.SetEnvironment(_savedEnvironment); + FrameworkCommunicationsUtilities.SetEnvironment(_savedEnvironment); } exception = _shutdownException; @@ -482,7 +481,7 @@ private void HandleNodeConfiguration(NodeConfiguration configuration) CultureInfo.CurrentUICulture = configuration.BuildParameters.UICulture; // Snapshot the initial environment. - _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + _savedEnvironment = FrameworkCommunicationsUtilities.GetEnvironmentVariables(); // Save the current directory. _savedCurrentDirectory = NativeMethodsShared.GetCurrentDirectory(); diff --git a/src/Build/BackEnd/Node/OutOfProcNode.cs b/src/Build/BackEnd/Node/OutOfProcNode.cs index c691a1c69c2..70915c42942 100644 --- a/src/Build/BackEnd/Node/OutOfProcNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcNode.cs @@ -500,7 +500,7 @@ private NodeEngineShutdownReason HandleShutdown(out Exception exception) { try { - CommunicationsUtilities.SetEnvironment(_savedEnvironment); + FrameworkCommunicationsUtilities.SetEnvironment(_savedEnvironment); } catch (Exception ex) { @@ -714,7 +714,7 @@ private void HandleNodeConfiguration(NodeConfiguration configuration) _buildParameters.ProjectRootElementCache = s_projectRootElementCacheBase; // Snapshot the current environment - _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + _savedEnvironment = FrameworkCommunicationsUtilities.GetEnvironmentVariables(); // Change to the startup directory try diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index bd1aae2a681..afda1281ee0 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -380,7 +380,7 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) // Set build process context Directory.SetCurrentDirectory(command.StartupDirectory); - CommunicationsUtilities.SetEnvironment(command.BuildProcessEnvironment); + FrameworkCommunicationsUtilities.SetEnvironment(command.BuildProcessEnvironment); Traits.UpdateFromEnvironment(); Thread.CurrentThread.CurrentCulture = command.Culture; diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 3bf8513c8f7..45f58aeba84 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -400,8 +400,6 @@ - - diff --git a/src/Build/Utilities/Utilities.cs b/src/Build/Utilities/Utilities.cs index b3936b737e2..e1ad5e06aae 100644 --- a/src/Build/Utilities/Utilities.cs +++ b/src/Build/Utilities/Utilities.cs @@ -475,7 +475,7 @@ private static bool UsingDifferentToolsVersionFromProjectFile(string toolsVersio /// internal static PropertyDictionary GetEnvironmentProperties(bool makeReadOnly) { - IDictionary environmentVariablesBag = CommunicationsUtilities.GetEnvironmentVariables(); + IDictionary environmentVariablesBag = FrameworkCommunicationsUtilities.GetEnvironmentVariables(); var envPropertiesHashSet = new RetrievableValuedEntryHashSet(environmentVariablesBag.Count + 2, MSBuildNameIgnoreCaseComparer.Default); diff --git a/src/Framework/CommunicationsUtilities.cs b/src/Framework/CommunicationsUtilities.cs new file mode 100644 index 00000000000..59cd908f6a1 --- /dev/null +++ b/src/Framework/CommunicationsUtilities.cs @@ -0,0 +1,296 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.NET.StringTools; + +#nullable disable + +namespace Microsoft.Build.Framework +{ + /// + /// Utilities for environment variable operations in the Framework project. + /// Contains environment variable methods needed by MultiProcessTaskEnvironmentDriver. + /// + internal static class FrameworkCommunicationsUtilities + { + /// + /// Case-insensitive string comparer for environment variable names. + /// + internal static StringComparer EnvironmentVariableComparer => StringComparer.OrdinalIgnoreCase; + + /// + /// A set of environment variables cached from the last time we called GetEnvironmentVariables. + /// Used to avoid allocations if the environment has not changed. + /// + private static EnvironmentState s_environmentState; + + /// + /// Get environment block. + /// + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + internal static extern unsafe char* GetEnvironmentStrings(); + + /// + /// Free environment block. + /// + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + internal static extern unsafe bool FreeEnvironmentStrings(char* pStrings); + +#if NETFRAMEWORK + /// + /// Set environment variable P/Invoke. + /// + [DllImport("kernel32.dll", EntryPoint = "SetEnvironmentVariable", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetEnvironmentVariableNative(string name, string value); + + /// + /// Sets an environment variable using P/Invoke to workaround the .NET Framework BCL implementation. + /// + /// + /// .NET Framework implementation of SetEnvironmentVariable checks the length of the value and throws an exception if + /// it's greater than or equal to 32,767 characters. This limitation does not exist on modern Windows or .NET. + /// + internal static void SetEnvironmentVariable(string name, string value) + { + if (!SetEnvironmentVariableNative(name, value)) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + } +#endif + + /// + /// A container to atomically swap a cached set of environment variables and the block string used to create it. + /// The environment block property will only be set on Windows, since on Unix we need to directly call + /// Environment.GetEnvironmentVariables(). + /// + private sealed record class EnvironmentState(FrozenDictionary EnvironmentVariables, ReadOnlyMemory EnvironmentBlock = default); + + /// + /// Returns key value pairs of environment variables in a new dictionary + /// with a case-insensitive key comparer. + /// + /// + /// Copied from the BCL implementation to eliminate some expensive security asserts on .NET Framework. + /// + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + private static FrozenDictionary GetEnvironmentVariablesWindows() + { + // The FrameworkDebugUtils static constructor can set the MSBUILDDEBUGPATH environment variable to propagate the debug path to out of proc nodes. + // Need to ensure that constructor is called before this method returns in order to capture its env var write. + // Otherwise the env var is not captured and thus gets deleted when RequestBuilder resets the environment based on the cached results of this method. + FrameworkErrorUtilities.VerifyThrowInternalNull(FrameworkDebugUtils.ProcessInfoString, nameof(FrameworkDebugUtils.DebugPath)); + + unsafe + { + char* pEnvironmentBlock = null; + + try + { + pEnvironmentBlock = GetEnvironmentStrings(); + if (pEnvironmentBlock == null) + { + throw new OutOfMemoryException(); + } + + // Search for terminating \0\0 (two unicode \0's). + char* pEnvironmentBlockEnd = pEnvironmentBlock; + while (!(*pEnvironmentBlockEnd == '\0' && *(pEnvironmentBlockEnd + 1) == '\0')) + { + pEnvironmentBlockEnd++; + } + long stringBlockLength = pEnvironmentBlockEnd - pEnvironmentBlock; + + // Avoid allocating any objects if the environment still matches the last state. + // We speed this up by comparing the full block instead of individual key-value pairs. + ReadOnlySpan stringBlock = new(pEnvironmentBlock, (int)stringBlockLength); + EnvironmentState lastState = s_environmentState; + if (lastState?.EnvironmentBlock.Span.SequenceEqual(stringBlock) == true) + { + return lastState.EnvironmentVariables; + } + + Dictionary table = new(200, StringComparer.OrdinalIgnoreCase); // Razzle has 150 environment variables + + // Copy strings out, parsing into pairs and inserting into the table. + // The first few environment variable entries start with an '='! + // The current working directory of every drive (except for those drives + // you haven't cd'ed into in your DOS window) are stored in the + // environment block (as =C:=pwd) and the program's exit code is + // as well (=ExitCode=00000000) Skip all that start with =. + // Read docs about Environment Blocks on MSDN's CreateProcess page. + + // Format for GetEnvironmentStrings is: + // (=HiddenVar=value\0 | Variable=value\0)* \0 + // See the description of Environment Blocks in MSDN's + // CreateProcess page (null-terminated array of null-terminated strings). + // Note the =HiddenVar's aren't always at the beginning. + for (int i = 0; i < stringBlockLength; i++) + { + int startKey = i; + + // Skip to key + // On some old OS, the environment block can be corrupted. + // Some lines will not have '=', so we need to check for '\0'. + while (*(pEnvironmentBlock + i) != '=' && *(pEnvironmentBlock + i) != '\0') + { + i++; + } + + if (*(pEnvironmentBlock + i) == '\0') + { + continue; + } + + // Skip over environment variables starting with '=' + if (i - startKey == 0) + { + while (*(pEnvironmentBlock + i) != 0) + { + i++; + } + + continue; + } + + string key = Strings.WeakIntern(new ReadOnlySpan(pEnvironmentBlock + startKey, i - startKey)); + + i++; + + // skip over '=' + int startValue = i; + + while (*(pEnvironmentBlock + i) != 0) + { + // Read to end of this entry + i++; + } + + string value = Strings.WeakIntern(new ReadOnlySpan(pEnvironmentBlock + startValue, i - startValue)); + + // skip over 0 handled by for loop's i++ + table[key] = value; + } + + // Update with the current state. + EnvironmentState currentState = + new(table.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase), stringBlock.ToArray()); + s_environmentState = currentState; + return currentState.EnvironmentVariables; + } + finally + { + if (pEnvironmentBlock != null) + { + FreeEnvironmentStrings(pEnvironmentBlock); + } + } + } + } + +#if NET + /// + /// Sets an environment variable using . + /// + internal static void SetEnvironmentVariable(string name, string value) + => Environment.SetEnvironmentVariable(name, value); +#endif + + /// + /// Returns key value pairs of environment variables in a read-only dictionary + /// with a case-insensitive key comparer. + /// + /// If the environment variables have not changed since the last time + /// this method was called, the same dictionary instance will be returned. + /// + internal static FrozenDictionary GetEnvironmentVariables() + { + // Always call the native method on Windows, as we'll be able to avoid the internal + // string and Hashtable allocations caused by Environment.GetEnvironmentVariables(). + if (NativeMethods.IsWindows) + { + return GetEnvironmentVariablesWindows(); + } + + IDictionary vars = Environment.GetEnvironmentVariables(); + + // Directly use the enumerator since Current will box DictionaryEntry. + IDictionaryEnumerator enumerator = vars.GetEnumerator(); + + // If every key-value pair matches the last state, return a cached dictionary. + FrozenDictionary lastEnvironmentVariables = s_environmentState?.EnvironmentVariables; + if (vars.Count == lastEnvironmentVariables?.Count) + { + bool sameState = true; + + while (enumerator.MoveNext() && sameState) + { + DictionaryEntry entry = enumerator.Entry; + if (!lastEnvironmentVariables.TryGetValue((string)entry.Key, out string value) + || !string.Equals((string)entry.Value, value, StringComparison.Ordinal)) + { + sameState = false; + } + } + + if (sameState) + { + return lastEnvironmentVariables; + } + } + + // Otherwise, allocate and update with the current state. + Dictionary table = new(vars.Count, EnvironmentVariableComparer); + + enumerator.Reset(); + while (enumerator.MoveNext()) + { + DictionaryEntry entry = enumerator.Entry; + string key = Strings.WeakIntern((string)entry.Key); + string value = Strings.WeakIntern((string)entry.Value); + table[key] = value; + } + + EnvironmentState newState = new(table.ToFrozenDictionary(EnvironmentVariableComparer)); + s_environmentState = newState; + + return newState.EnvironmentVariables; + } + + /// + /// Updates the environment to match the provided dictionary. + /// + internal static void SetEnvironment(IDictionary newEnvironment) + { + if (newEnvironment != null) + { + // First, delete all no longer set variables + IDictionary currentEnvironment = GetEnvironmentVariables(); + foreach (KeyValuePair entry in currentEnvironment) + { + if (!newEnvironment.ContainsKey(entry.Key)) + { + SetEnvironmentVariable(entry.Key, null); + } + } + + // Then, make sure the new ones have their new values. + foreach (KeyValuePair entry in newEnvironment) + { + if (!currentEnvironment.TryGetValue(entry.Key, out string currentValue) || currentValue != entry.Value) + { + SetEnvironmentVariable(entry.Key, entry.Value); + } + } + } + } + } +} diff --git a/src/Framework/DebugUtils.cs b/src/Framework/DebugUtils.cs new file mode 100644 index 00000000000..49d10f69d18 --- /dev/null +++ b/src/Framework/DebugUtils.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; + +#nullable disable + +namespace Microsoft.Build.Framework +{ + internal static class FrameworkDebugUtils + { +#pragma warning disable CA1810 // Intentional: static constructor catches exceptions to prevent TypeInitializationException + static FrameworkDebugUtils() +#pragma warning restore CA1810 + { + try + { + SetDebugPath(); + } + catch (Exception ex) + { + // A failure in SetDebugPath must not prevent MSBuild from starting. + // DebugPath will remain null — debugging/logging features will be + // unavailable for this session, but the build can still proceed. + // + // Known failure scenarios: + // - Directory.GetCurrentDirectory() throws DirectoryNotFoundException + // if the working directory was deleted before MSBuild started. + // - FileUtilities.EnsureDirectoryExists() throws UnauthorizedAccessException + // or IOException when the target path is on a read-only volume or an + // offline network share. + // - Path.Combine() throws ArgumentException when MSBUILDDEBUGPATH contains + // illegal path characters (e.g., '<', '>', '|'). + // - PathTooLongException when the resolved path exceeds MAX_PATH on + // .NET Framework without long-path support. + try + { + Console.Error.WriteLine("MSBuild debug path initialization failed: " + ex); + } + catch + { + // Console may not be available. + } + } + + // Initialize diagnostic fields inside the static constructor so failures + // are caught here rather than poisoning the type with an unrecoverable + // TypeInitializationException. On .NET Framework, EnvironmentUtilities + // accesses Process.GetCurrentProcess() which can throw Win32Exception + // in restricted environments or when performance counters are corrupted. + try + { + ProcessInfoString = GetProcessInfoString(); + ShouldDebugCurrentProcess = CurrentProcessMatchesDebugName(); + } + catch + { + ProcessInfoString ??= "Unknown"; + ShouldDebugCurrentProcess = false; + } + } + + // FrameworkDebugUtils is initialized early on by the test runner - during preparing data for DataMemberAttribute of some test, + // for that reason it is not easily possible to inject the DebugPath in tests via env var (unless we want to run expensive exec style test). + internal static void SetDebugPath() + { + string environmentDebugPath = FileUtilities.TrimAndStripAnyQuotes(Environment.GetEnvironmentVariable("MSBUILDDEBUGPATH")); + string debugDirectory = environmentDebugPath; + if (Traits.Instance.DebugEngine) + { + if (!string.IsNullOrWhiteSpace(debugDirectory) && FileUtilities.CanWriteToDirectory(debugDirectory)) + { + // Add a dedicated ".MSBuild_Logs" folder inside the user-specified path, either always or when in solution directory. + debugDirectory = System.IO.Path.Combine(debugDirectory, ".MSBuild_Logs"); + } + else if (FileUtilities.CanWriteToDirectory(Directory.GetCurrentDirectory())) + { + debugDirectory = System.IO.Path.Combine(Directory.GetCurrentDirectory(), ".MSBuild_Logs"); + } + else + { + debugDirectory = System.IO.Path.Combine(FileUtilities.TempFileDirectory, ".MSBuild_Logs"); + } + + // Out of proc nodes do not know the startup directory so set the environment variable for them. + if (string.IsNullOrWhiteSpace(environmentDebugPath)) + { + Environment.SetEnvironmentVariable("MSBUILDDEBUGPATH", debugDirectory); + } + } + + if (debugDirectory is not null) + { + FileUtilities.EnsureDirectoryExists(debugDirectory); + } + + DebugPath = debugDirectory; + } + + private static readonly Lazy ProcessNodeMode = new( + () => NodeModeHelper.ExtractFromCommandLine(Environment.CommandLine)); + + private static bool CurrentProcessMatchesDebugName() + { + var processNameToBreakInto = Environment.GetEnvironmentVariable("MSBuildDebugProcessName"); + var thisProcessMatchesName = string.IsNullOrWhiteSpace(processNameToBreakInto) || + EnvironmentUtilities.ProcessName.Contains(processNameToBreakInto); + + return thisProcessMatchesName; + } + + /// + /// Builds a diagnostic string identifying this process (node mode, name, PID, bitness). + /// Must be called from the static constructor rather than as a field initializer because + /// on .NET Framework, and + /// access + /// Process.GetCurrentProcess() which can throw + /// in restricted environments or when performance counters are corrupted. + /// A field-initializer failure would produce an unrecoverable + /// that poisons the entire type, whereas the static constructor's + /// try/catch lets the type initialize successfully with a safe fallback value. + /// + private static string GetProcessInfoString() => $"{(ProcessNodeMode.Value?.ToString() ?? "CentralNode")}_{EnvironmentUtilities.ProcessName}_PID={EnvironmentUtilities.CurrentProcessId}_x{(Environment.Is64BitProcess ? "64" : "86")}"; + + public static readonly string ProcessInfoString; + + public static readonly bool ShouldDebugCurrentProcess; + + public static string DebugPath { get; private set; } + + /// + /// Returns true if the current process is an out-of-proc TaskHost node. + /// + /// + /// True if this process was launched with /nodemode:2 (indicating it's a TaskHost process), + /// false otherwise. This is useful for conditionally enabling debugging or other behaviors + /// based on whether the code is running in the main MSBuild process or a child TaskHost process. + /// + public static bool IsInTaskHostNode() => ProcessNodeMode.Value == NodeMode.OutOfProcTaskHostNode; + } +} diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Framework/MultiProcessTaskEnvironmentDriver.cs similarity index 81% rename from src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs rename to src/Framework/MultiProcessTaskEnvironmentDriver.cs index 490ed0ca9bc..cc1874d5a94 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs +++ b/src/Framework/MultiProcessTaskEnvironmentDriver.cs @@ -4,10 +4,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using Microsoft.Build.Framework; -using Microsoft.Build.Internal; -namespace Microsoft.Build.BackEnd +namespace Microsoft.Build.Framework { /// /// Default implementation of that directly interacts with the file system @@ -33,8 +31,8 @@ private MultiProcessTaskEnvironmentDriver() { } /// public AbsolutePath ProjectDirectory { - get => new AbsolutePath(NativeMethodsShared.GetCurrentDirectory(), ignoreRootedCheck: true); - set => NativeMethodsShared.SetCurrentDirectory(value.Value); + get => new AbsolutePath(NativeMethods.GetCurrentDirectory(), ignoreRootedCheck: true); + set => NativeMethods.SetCurrentDirectory(value.Value); } /// @@ -52,19 +50,19 @@ public AbsolutePath GetAbsolutePath(string path) /// public IReadOnlyDictionary GetEnvironmentVariables() { - return CommunicationsUtilities.GetEnvironmentVariables(); + return FrameworkCommunicationsUtilities.GetEnvironmentVariables(); } /// public void SetEnvironmentVariable(string name, string? value) { - CommunicationsUtilities.SetEnvironmentVariable(name, value); + FrameworkCommunicationsUtilities.SetEnvironmentVariable(name, value); } /// public void SetEnvironment(IDictionary newEnvironment) { - CommunicationsUtilities.SetEnvironment(newEnvironment); + FrameworkCommunicationsUtilities.SetEnvironment(newEnvironment); } /// @@ -79,4 +77,4 @@ public void Dispose() // Singleton instance, no cleanup needed. } } -} +} \ No newline at end of file diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Framework/MultiThreadedTaskEnvironmentDriver.cs similarity index 95% rename from src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs rename to src/Framework/MultiThreadedTaskEnvironmentDriver.cs index e46edd1aa1d..da8e6bc4430 100644 --- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs +++ b/src/Framework/MultiThreadedTaskEnvironmentDriver.cs @@ -5,10 +5,8 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using Microsoft.Build.Framework; -using Microsoft.Build.Internal; -namespace Microsoft.Build.BackEnd +namespace Microsoft.Build.Framework { /// /// Implementation of that virtualizes environment variables and current directory @@ -33,7 +31,7 @@ public MultiThreadedTaskEnvironmentDriver( string currentDirectoryFullPath, IDictionary environmentVariables) { - _environmentVariables = new Dictionary(environmentVariables, CommunicationsUtilities.EnvironmentVariableComparer); + _environmentVariables = new Dictionary(environmentVariables, FrameworkCommunicationsUtilities.EnvironmentVariableComparer); ProjectDirectory = new AbsolutePath(currentDirectoryFullPath, ignoreRootedCheck: true); } @@ -45,7 +43,7 @@ public MultiThreadedTaskEnvironmentDriver( public MultiThreadedTaskEnvironmentDriver(string currentDirectoryFullPath) { IDictionary variables = Environment.GetEnvironmentVariables(); - _environmentVariables = new Dictionary(variables.Count, CommunicationsUtilities.EnvironmentVariableComparer); + _environmentVariables = new Dictionary(variables.Count, FrameworkCommunicationsUtilities.EnvironmentVariableComparer); foreach (DictionaryEntry entry in variables) { _environmentVariables[(string)entry.Key] = (string)entry.Value!; diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 3ab9829ed02..f8dfcd6c8ce 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -696,7 +696,7 @@ public NodeEngineShutdownReason Run(out Exception shutdownException, bool nodeRe shutdownException = null; // Snapshot the current environment - _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + _savedEnvironment = FrameworkCommunicationsUtilities.GetEnvironmentVariables(); _nodeReuse = nodeReuse; _nodeEndpoint = new NodeEndpointOutOfProcTaskHost(nodeReuse, parentPacketVersion); @@ -978,7 +978,7 @@ private NodeEngineShutdownReason HandleShutdown() // Restore the original environment, best effort. try { - CommunicationsUtilities.SetEnvironment(_savedEnvironment); + FrameworkCommunicationsUtilities.SetEnvironment(_savedEnvironment); } catch (Exception ex) { @@ -1114,7 +1114,7 @@ private void RunTask(object state) { _isTaskExecuting = false; - IDictionary currentEnvironment = CommunicationsUtilities.GetEnvironmentVariables(); + IDictionary currentEnvironment = FrameworkCommunicationsUtilities.GetEnvironmentVariables(); currentEnvironment = UpdateEnvironmentForMainNode(currentEnvironment); taskResult ??= new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure); @@ -1138,7 +1138,7 @@ private void RunTask(object state) #endif // Restore the original clean environment - CommunicationsUtilities.SetEnvironment(_savedEnvironment); + FrameworkCommunicationsUtilities.SetEnvironment(_savedEnvironment); } catch (Exception e) { @@ -1227,7 +1227,7 @@ private void SetTaskHostEnvironment(IDictionary environment) updatedEnvironment = environment; } - CommunicationsUtilities.SetEnvironment(updatedEnvironment); + FrameworkCommunicationsUtilities.SetEnvironment(updatedEnvironment); } /// diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 4fab975382b..c383c72705d 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -617,7 +617,7 @@ private static void DebuggerLaunchCheck() break; case "3": // Value "3" debugs the main MSBuild process but skips debugging child TaskHost processes - if (!DebugUtils.IsInTaskHostNode()) + if (!FrameworkDebugUtils.IsInTaskHostNode()) { Debugger.Launch(); } @@ -1830,7 +1830,7 @@ private static bool PrintTargets(string projectFile, string toolsVersion, Dictio new BuildManager.DeferredBuildMessage( ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( "MSBuildDebugPath", - DebugUtils.DebugPath), + FrameworkDebugUtils.DebugPath), MessageImportance.High)); } diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs index 3261eb11b45..0dfbf01bf2c 100644 --- a/src/Shared/CommunicationsUtilities.cs +++ b/src/Shared/CommunicationsUtilities.cs @@ -7,7 +7,11 @@ using System.Globalization; using System.IO; using System.IO.Pipes; + +#if RUNTIME_TYPE_NETCORE using System.Runtime.InteropServices; +#endif + #if FEATURE_SECURITY_PRINCIPAL_WINDOWS || RUNTIME_TYPE_NETCORE using System.Security.Principal; #endif @@ -20,11 +24,6 @@ using Microsoft.Build.Shared; using Microsoft.Build.BackEnd; -using Microsoft.Build.Shared.Debugging; -using System.Collections; -using System.Collections.Frozen; -using Microsoft.NET.StringTools; - #if !FEATURE_APM using System.Threading.Tasks; #endif @@ -433,12 +432,6 @@ internal static class CommunicationsUtilities /// private static long s_lastLoggedTicks = DateTime.UtcNow.Ticks; - /// - /// A set of environment variables cached from the last time we called GetEnvironmentVariables. - /// Used to avoid allocations if the environment has not changed. - /// - private static EnvironmentState s_environmentState; - /// /// Delegate to debug the communication utilities. /// @@ -459,270 +452,6 @@ internal static int NodeConnectionTimeout get { return GetIntegerVariableOrDefault("MSBUILDNODECONNECTIONTIMEOUT", DefaultNodeConnectionTimeout); } } - /// - /// Get environment block. - /// - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - internal static extern unsafe char* GetEnvironmentStrings(); - - /// - /// Free environment block. - /// - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - internal static extern unsafe bool FreeEnvironmentStrings(char* pStrings); - -#if NETFRAMEWORK - /// - /// Set environment variable P/Invoke. - /// - [DllImport("kernel32.dll", EntryPoint = "SetEnvironmentVariable", SetLastError = true, CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool SetEnvironmentVariableNative(string name, string value); - - /// - /// Sets an environment variable using P/Invoke to workaround the .NET Framework BCL implementation. - /// - /// - /// .NET Framework implementation of SetEnvironmentVariable checks the length of the value and throws an exception if - /// it's greater than or equal to 32,767 characters. This limitation does not exist on modern Windows or .NET. - /// - internal static void SetEnvironmentVariable(string name, string value) - { - if (!SetEnvironmentVariableNative(name, value)) - { - throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()); - } - } -#endif - - /// - /// A container to atomically swap a cached set of environment variables and the block string used to create it. - /// The environment block property will only be set on Windows, since on Unix we need to directly call - /// Environment.GetEnvironmentVariables(). - /// - private sealed record class EnvironmentState(FrozenDictionary EnvironmentVariables, ReadOnlyMemory EnvironmentBlock = default); - - /// - /// Returns key value pairs of environment variables in a new dictionary - /// with a case-insensitive key comparer. - /// - /// - /// Copied from the BCL implementation to eliminate some expensive security asserts on .NET Framework. - /// - [System.Runtime.Versioning.SupportedOSPlatform("windows")] - private static FrozenDictionary GetEnvironmentVariablesWindows() - { - // The DebugUtils static constructor can set the MSBUILDDEBUGPATH environment variable to propagate the debug path to out of proc nodes. - // Need to ensure that constructor is called before this method returns in order to capture its env var write. - // Otherwise the env var is not captured and thus gets deleted when RequiestBuilder resets the environment based on the cached results of this method. - ErrorUtilities.VerifyThrowInternalNull(DebugUtils.ProcessInfoString, nameof(DebugUtils.DebugPath)); - - unsafe - { - char* pEnvironmentBlock = null; - - try - { - pEnvironmentBlock = GetEnvironmentStrings(); - if (pEnvironmentBlock == null) - { - throw new OutOfMemoryException(); - } - - // Search for terminating \0\0 (two unicode \0's). - char* pEnvironmentBlockEnd = pEnvironmentBlock; - while (!(*pEnvironmentBlockEnd == '\0' && *(pEnvironmentBlockEnd + 1) == '\0')) - { - pEnvironmentBlockEnd++; - } - long stringBlockLength = pEnvironmentBlockEnd - pEnvironmentBlock; - - // Avoid allocating any objects if the environment still matches the last state. - // We speed this up by comparing the full block instead of individual key-value pairs. - ReadOnlySpan stringBlock = new(pEnvironmentBlock, (int)stringBlockLength); - EnvironmentState lastState = s_environmentState; - if (lastState?.EnvironmentBlock.Span.SequenceEqual(stringBlock) == true) - { - return lastState.EnvironmentVariables; - } - - Dictionary table = new(200, StringComparer.OrdinalIgnoreCase); // Razzle has 150 environment variables - - // Copy strings out, parsing into pairs and inserting into the table. - // The first few environment variable entries start with an '='! - // The current working directory of every drive (except for those drives - // you haven't cd'ed into in your DOS window) are stored in the - // environment block (as =C:=pwd) and the program's exit code is - // as well (=ExitCode=00000000) Skip all that start with =. - // Read docs about Environment Blocks on MSDN's CreateProcess page. - - // Format for GetEnvironmentStrings is: - // (=HiddenVar=value\0 | Variable=value\0)* \0 - // See the description of Environment Blocks in MSDN's - // CreateProcess page (null-terminated array of null-terminated strings). - // Note the =HiddenVar's aren't always at the beginning. - for (int i = 0; i < stringBlockLength; i++) - { - int startKey = i; - - // Skip to key - // On some old OS, the environment block can be corrupted. - // Some lines will not have '=', so we need to check for '\0'. - while (*(pEnvironmentBlock + i) != '=' && *(pEnvironmentBlock + i) != '\0') - { - i++; - } - - if (*(pEnvironmentBlock + i) == '\0') - { - continue; - } - - // Skip over environment variables starting with '=' - if (i - startKey == 0) - { - while (*(pEnvironmentBlock + i) != 0) - { - i++; - } - - continue; - } - - string key = Strings.WeakIntern(new ReadOnlySpan(pEnvironmentBlock + startKey, i - startKey)); - - i++; - - // skip over '=' - int startValue = i; - - while (*(pEnvironmentBlock + i) != 0) - { - // Read to end of this entry - i++; - } - - string value = Strings.WeakIntern(new ReadOnlySpan(pEnvironmentBlock + startValue, i - startValue)); - - // skip over 0 handled by for loop's i++ - table[key] = value; - } - - // Update with the current state. - EnvironmentState currentState = - new(table.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase), stringBlock.ToArray()); - s_environmentState = currentState; - return currentState.EnvironmentVariables; - } - finally - { - if (pEnvironmentBlock != null) - { - FreeEnvironmentStrings(pEnvironmentBlock); - } - } - } - } - -#if NET - /// - /// Sets an environment variable using . - /// - internal static void SetEnvironmentVariable(string name, string value) - => Environment.SetEnvironmentVariable(name, value); -#endif - - /// - /// Returns key value pairs of environment variables in a read-only dictionary - /// with a case-insensitive key comparer. - /// - /// If the environment variables have not changed since the last time - /// this method was called, the same dictionary instance will be returned. - /// - internal static FrozenDictionary GetEnvironmentVariables() - { - // Always call the native method on Windows, as we'll be able to avoid the internal - // string and Hashtable allocations caused by Environment.GetEnvironmentVariables(). - if (NativeMethodsShared.IsWindows) - { - return GetEnvironmentVariablesWindows(); - } - - IDictionary vars = Environment.GetEnvironmentVariables(); - - // Directly use the enumerator since Current will box DictionaryEntry. - IDictionaryEnumerator enumerator = vars.GetEnumerator(); - - // If every key-value pair matches the last state, return a cached dictionary. - FrozenDictionary lastEnvironmentVariables = s_environmentState?.EnvironmentVariables; - if (vars.Count == lastEnvironmentVariables?.Count) - { - bool sameState = true; - - while (enumerator.MoveNext() && sameState) - { - DictionaryEntry entry = enumerator.Entry; - if (!lastEnvironmentVariables.TryGetValue((string)entry.Key, out string value) - || !string.Equals((string)entry.Value, value, StringComparison.Ordinal)) - { - sameState = false; - } - } - - if (sameState) - { - return lastEnvironmentVariables; - } - } - - // Otherwise, allocate and update with the current state. - Dictionary table = new(vars.Count, EnvironmentVariableComparer); - - enumerator.Reset(); - while (enumerator.MoveNext()) - { - DictionaryEntry entry = enumerator.Entry; - string key = Strings.WeakIntern((string)entry.Key); - string value = Strings.WeakIntern((string)entry.Value); - table[key] = value; - } - - EnvironmentState newState = new(table.ToFrozenDictionary(EnvironmentVariableComparer)); - s_environmentState = newState; - - return newState.EnvironmentVariables; - } - - /// - /// Updates the environment to match the provided dictionary. - /// - internal static void SetEnvironment(IDictionary newEnvironment) - { - if (newEnvironment != null) - { - // First, delete all no longer set variables - IDictionary currentEnvironment = GetEnvironmentVariables(); - foreach (KeyValuePair entry in currentEnvironment) - { - if (!newEnvironment.ContainsKey(entry.Key)) - { - SetEnvironmentVariable(entry.Key, null); - } - } - - // Then, make sure the new ones have their new values. - foreach (KeyValuePair entry in newEnvironment) - { - if (!currentEnvironment.TryGetValue(entry.Key, out string currentValue) || currentValue != entry.Value) - { - SetEnvironmentVariable(entry.Key, entry.Value); - } - } - } - } - #nullable enable /// /// Indicate to the client that all elements of the Handshake have been sent. @@ -1157,7 +886,7 @@ private static void TraceCore(int nodeId, string message) { lock (s_traceLock) { - s_debugDumpPath ??= DebugUtils.DebugPath; + s_debugDumpPath ??= FrameworkDebugUtils.DebugPath; if (String.IsNullOrEmpty(s_debugDumpPath)) { diff --git a/src/Shared/Debugging/DebugUtils.cs b/src/Shared/Debugging/DebugUtils.cs index b4643cda115..3c6788f9f7f 100644 --- a/src/Shared/Debugging/DebugUtils.cs +++ b/src/Shared/Debugging/DebugUtils.cs @@ -17,94 +17,6 @@ namespace Microsoft.Build.Shared.Debugging { internal static class DebugUtils { -#pragma warning disable CA1810 // Intentional: static constructor catches exceptions to prevent TypeInitializationException - static DebugUtils() -#pragma warning restore CA1810 - { - try - { - SetDebugPath(); - } - catch (Exception ex) - { - // A failure in SetDebugPath must not prevent MSBuild from starting. - // DebugPath will remain null — debugging/logging features will be - // unavailable for this session, but the build can still proceed. - // - // Known failure scenarios: - // - Directory.GetCurrentDirectory() throws DirectoryNotFoundException - // if the working directory was deleted before MSBuild started. - // - FileUtilities.EnsureDirectoryExists() throws UnauthorizedAccessException - // or IOException when the target path is on a read-only volume or an - // offline network share. - // - Path.Combine() throws ArgumentException when MSBUILDDEBUGPATH contains - // illegal path characters (e.g., '<', '>', '|'). - // - PathTooLongException when the resolved path exceeds MAX_PATH on - // .NET Framework without long-path support. - try - { - Console.Error.WriteLine("MSBuild debug path initialization failed: " + ex); - } - catch - { - // Console may not be available. - } - } - - // Initialize diagnostic fields inside the static constructor so failures - // are caught here rather than poisoning the type with an unrecoverable - // TypeInitializationException. On .NET Framework, EnvironmentUtilities - // accesses Process.GetCurrentProcess() which can throw Win32Exception - // in restricted environments or when performance counters are corrupted. - try - { - ProcessInfoString = GetProcessInfoString(); - ShouldDebugCurrentProcess = CurrentProcessMatchesDebugName(); - } - catch - { - ProcessInfoString ??= "Unknown"; - ShouldDebugCurrentProcess = false; - } - } - - // DebugUtils are initialized early on by the test runner - during preparing data for DataMemeberAttribute of some test, - // for that reason it is not easily possible to inject the DebugPath in tests via env var (unless we want to run expensive exec style test). - internal static void SetDebugPath() - { - string environmentDebugPath = FileUtilities.TrimAndStripAnyQuotes(Environment.GetEnvironmentVariable("MSBUILDDEBUGPATH")); - string debugDirectory = environmentDebugPath; - if (Traits.Instance.DebugEngine) - { - if (!string.IsNullOrWhiteSpace(debugDirectory) && FileUtilities.CanWriteToDirectory(debugDirectory)) - { - // Add a dedicated ".MSBuild_Logs" folder inside the user-specified path, either always or when in solution directory. - debugDirectory = Path.Combine(debugDirectory, ".MSBuild_Logs"); - } - else if (FileUtilities.CanWriteToDirectory(Directory.GetCurrentDirectory())) - { - debugDirectory = Path.Combine(Directory.GetCurrentDirectory(), ".MSBuild_Logs"); - } - else - { - debugDirectory = Path.Combine(FileUtilities.TempFileDirectory, ".MSBuild_Logs"); - } - - // Out of proc nodes do not know the startup directory so set the environment variable for them. - if (string.IsNullOrWhiteSpace(environmentDebugPath)) - { - Environment.SetEnvironmentVariable("MSBUILDDEBUGPATH", debugDirectory); - } - } - - if (debugDirectory is not null) - { - FileUtilities.EnsureDirectoryExists(debugDirectory); - } - - DebugPath = debugDirectory; - } - private static readonly string s_debugDumpPath = GetDebugDumpPath(); /// @@ -113,7 +25,7 @@ internal static void SetDebugPath() /// private static string GetDebugDumpPath() { - string debugPath = DebugPath; + string debugPath = FrameworkDebugUtils.DebugPath; return !string.IsNullOrEmpty(debugPath) ? debugPath @@ -157,65 +69,23 @@ internal static string DebugDumpPath /// private static string s_dumpFileName; - private static readonly Lazy ProcessNodeMode = new( - () => NodeModeHelper.ExtractFromCommandLine(Environment.CommandLine)); - - private static bool CurrentProcessMatchesDebugName() - { - var processNameToBreakInto = Environment.GetEnvironmentVariable("MSBuildDebugProcessName"); - var thisProcessMatchesName = string.IsNullOrWhiteSpace(processNameToBreakInto) || - EnvironmentUtilities.ProcessName.Contains(processNameToBreakInto); - - return thisProcessMatchesName; - } - - /// - /// Builds a diagnostic string identifying this process (node mode, name, PID, bitness). - /// Must be called from the static constructor rather than as a field initializer because - /// on .NET Framework, and - /// access - /// Process.GetCurrentProcess() which can throw - /// in restricted environments or when performance counters are corrupted. - /// A field-initializer failure would produce an unrecoverable - /// that poisons the entire type, whereas the static constructor's - /// try/catch lets the type initialize successfully with a safe fallback value. - /// - private static string GetProcessInfoString() => $"{(ProcessNodeMode.Value?.ToString() ?? "CentralNode")}_{EnvironmentUtilities.ProcessName}_PID={EnvironmentUtilities.CurrentProcessId}_x{(Environment.Is64BitProcess ? "64" : "86")}"; - - public static readonly string ProcessInfoString; - - public static readonly bool ShouldDebugCurrentProcess; - - public static string DebugPath { get; private set; } - - /// - /// Returns true if the current process is an out-of-proc TaskHost node. - /// - /// - /// True if this process was launched with /nodemode:2 (indicating it's a TaskHost process), - /// false otherwise. This is useful for conditionally enabling debugging or other behaviors - /// based on whether the code is running in the main MSBuild process or a child TaskHost process. - /// - public static bool IsInTaskHostNode() => ProcessNodeMode.Value == NodeMode.OutOfProcTaskHostNode; - public static string FindNextAvailableDebugFilePath(string fileName) { var extension = Path.GetExtension(fileName); var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); - var fullPath = Path.Combine(DebugPath, fileName); + var fullPath = Path.Combine(FrameworkDebugUtils.DebugPath, fileName); var counter = 0; while (FileSystems.Default.FileExists(fullPath)) { fileName = $"{fileNameWithoutExtension}_{counter++}{extension}"; - fullPath = Path.Combine(DebugPath, fileName); + fullPath = Path.Combine(FrameworkDebugUtils.DebugPath, fileName); } return fullPath; } - /// /// Dump any unhandled exceptions to a file so they can be diagnosed /// diff --git a/src/Shared/Debugging/PrintLineDebugger.cs b/src/Shared/Debugging/PrintLineDebugger.cs index 11618bc3cc6..99ba816c8bb 100644 --- a/src/Shared/Debugging/PrintLineDebugger.cs +++ b/src/Shared/Debugging/PrintLineDebugger.cs @@ -115,7 +115,7 @@ public static PrintLineDebugger Create( { return new PrintLineDebugger( prependProcessInfo - ? $"{DebugUtils.ProcessInfoString}_{id}" + ? $"{FrameworkDebugUtils.ProcessInfoString}_{id}" : id, writer); } diff --git a/src/Shared/UnitTests/CommunicationUtilities_Tests.cs b/src/Shared/UnitTests/CommunicationUtilities_Tests.cs index 63e1e2a7197..418afd8edd4 100644 --- a/src/Shared/UnitTests/CommunicationUtilities_Tests.cs +++ b/src/Shared/UnitTests/CommunicationUtilities_Tests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Shouldly; using Xunit; +using Microsoft.Build.Framework; using CommunicationsUtilities = Microsoft.Build.Internal.CommunicationsUtilities; namespace Microsoft.Build.UnitTests @@ -18,7 +19,7 @@ public class CommunicationUtilitiesTests [Fact] public void GetEnvVars() { - IDictionary envVars = CommunicationsUtilities.GetEnvironmentVariables(); + IDictionary envVars = FrameworkCommunicationsUtilities.GetEnvironmentVariables(); IDictionary referenceVars = Environment.GetEnvironmentVariables(); IDictionary referenceVars2 = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -42,23 +43,23 @@ public void RestoreEnvVars() // A long value exceeding the former limit of 32,767 characters. string testValue = new string('a', 1_000_000); - CommunicationsUtilities.SetEnvironmentVariable(testName1, testValue); + FrameworkCommunicationsUtilities.SetEnvironmentVariable(testName1, testValue); try { - IDictionary envVars = CommunicationsUtilities.GetEnvironmentVariables(); + IDictionary envVars = FrameworkCommunicationsUtilities.GetEnvironmentVariables(); - CommunicationsUtilities.SetEnvironmentVariable(testName1, null); - CommunicationsUtilities.SetEnvironmentVariable(testName2, testValue); + FrameworkCommunicationsUtilities.SetEnvironmentVariable(testName1, null); + FrameworkCommunicationsUtilities.SetEnvironmentVariable(testName2, testValue); - CommunicationsUtilities.SetEnvironment(envVars); + FrameworkCommunicationsUtilities.SetEnvironment(envVars); Environment.GetEnvironmentVariable(testName1).ShouldBe(testValue); Environment.GetEnvironmentVariable(testName2).ShouldBe(null); } finally { - CommunicationsUtilities.SetEnvironmentVariable(testName1, null); - CommunicationsUtilities.SetEnvironmentVariable(testName2, null); + FrameworkCommunicationsUtilities.SetEnvironmentVariable(testName1, null); + FrameworkCommunicationsUtilities.SetEnvironmentVariable(testName2, null); } } }