File: DebugUtils.cs
Web Access
Project: ..\..\..\src\Utilities\Microsoft.Build.Utilities.csproj (Microsoft.Build.Utilities.Core)
// 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.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Framework.Telemetry;
using Microsoft.Build.Shared.FileSystem;
 
#nullable disable
 
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();
 
        /// <summary>
        /// Gets the location of the directory used for diagnostic log files.
        /// </summary>
        /// <returns></returns>
        private static string GetDebugDumpPath()
        {
            string debugPath = DebugPath;
 
            return !string.IsNullOrEmpty(debugPath)
                    ? debugPath
                    : FileUtilities.TempFileDirectory;
        }
 
        private static string s_debugDumpPathInRunningTests = GetDebugDumpPath();
        internal static bool ResetDebugDumpPathInRunningTests = false;
 
        /// <summary>
        /// The directory used for diagnostic log files.
        /// </summary>
        internal static string DebugDumpPath
        {
            get
            {
                if (BuildEnvironmentHelper.Instance.RunningTests)
                {
                    if (ResetDebugDumpPathInRunningTests)
                    {
                        s_debugDumpPathInRunningTests = GetDebugDumpPath();
                        // reset dump file name so new one is created in new path
                        s_dumpFileName = null;
                        ResetDebugDumpPathInRunningTests = false;
                    }
 
                    return s_debugDumpPathInRunningTests;
                }
 
                return s_debugDumpPath;
            }
        }
 
        /// <summary>
        /// The file used for diagnostic log files.
        /// </summary>
        internal static string DumpFilePath => s_dumpFileName;
 
        /// <summary>
        /// The filename that exceptions will be dumped to
        /// </summary>
        private static string s_dumpFileName;
 
        private static readonly Lazy<NodeMode?> 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;
        }
 
        /// <summary>
        /// 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, <see cref="EnvironmentUtilities.ProcessName"/> and
        /// <see cref="EnvironmentUtilities.CurrentProcessId"/> access
        /// <c>Process.GetCurrentProcess()</c> which can throw <see cref="System.ComponentModel.Win32Exception"/>
        /// in restricted environments or when performance counters are corrupted.
        /// A field-initializer failure would produce an unrecoverable <see cref="TypeInitializationException"/>
        /// that poisons the entire <see cref="DebugUtils"/> type, whereas the static constructor's
        /// try/catch lets the type initialize successfully with a safe fallback value.
        /// </summary>
        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; }
 
        /// <summary>
        /// Returns true if the current process is an out-of-proc TaskHost node.
        /// </summary>
        /// <returns>
        /// 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.
        /// </returns>
        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 counter = 0;
            while (FileSystems.Default.FileExists(fullPath))
            {
                fileName = $"{fileNameWithoutExtension}_{counter++}{extension}";
                fullPath = Path.Combine(DebugPath, fileName);
            }
 
            return fullPath;
        }
 
 
        /// <summary>
        /// Dump any unhandled exceptions to a file so they can be diagnosed
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "It is called by the CLR")]
        internal static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;
            DumpExceptionToFile(ex);
#if !MICROSOFT_BUILD_ENGINE_OM_UNITTESTS
            RecordCrashTelemetryForUnhandledException(ex);
#endif
        }
 
#if !MICROSOFT_BUILD_ENGINE_OM_UNITTESTS
        /// <summary>
        /// Records and immediately flushes crash telemetry for an unhandled exception.
        /// Best effort - must never throw, as the process is already crashing.
        /// </summary>
        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static void RecordCrashTelemetryForUnhandledException(Exception ex)
        {
            CrashTelemetryRecorder.RecordAndFlushCrashTelemetry(
                ex,
                exitType: CrashExitType.UnhandledException,
                isUnhandled: true,
                isCritical: ExceptionHandling.IsCriticalException(ex));
        }
#endif
 
        /// <summary>
        /// Dump the exception information to a file
        /// </summary>
        internal static void DumpExceptionToFile(Exception ex)
        {
            try
            {
                // Locking on a type is not recommended.  However, we are doing it here to be extra cautious about compatibility because
                //  this method previously had a [MethodImpl(MethodImplOptions.Synchronized)] attribute, which does lock on the type when
                //  applied to a static method.
                lock (typeof(ExceptionHandling))
                {
                    if (s_dumpFileName == null)
                    {
                        Guid guid = Guid.NewGuid();
 
                        // For some reason we get Watson buckets because GetTempPath gives us a folder here that doesn't exist.
                        // Either because %TMP% is misdefined, or because they deleted the temp folder during the build.
                        // If this throws, no sense catching it, we can't log it now, and we're here
                        // because we're a child node with no console to log to, so die
                        Directory.CreateDirectory(DebugDumpPath);
 
                        var pid = EnvironmentUtilities.CurrentProcessId;
                        // This naming pattern is assumed in ReadAnyExceptionFromFile
                        s_dumpFileName = Path.Combine(DebugDumpPath, $"MSBuild_pid-{pid}_{guid:n}.failure.txt");
 
                        using (StreamWriter writer = FileUtilities.OpenWrite(s_dumpFileName, append: true))
                        {
                            writer.WriteLine("UNHANDLED EXCEPTIONS FROM PROCESS {0}:", pid);
                            writer.WriteLine("=====================");
                        }
                    }
 
                    using (StreamWriter writer = FileUtilities.OpenWrite(s_dumpFileName, append: true))
                    {
                        // "G" format is, e.g., 6/15/2008 9:15:07 PM
                        writer.WriteLine(DateTime.Now.ToString("G", CultureInfo.CurrentCulture));
                        writer.WriteLine(ex.ToString());
                        writer.WriteLine("===================");
                    }
                }
            }
 
            // Some customers experience exceptions such as 'OutOfMemory' errors when msbuild attempts to log errors to a local file.
            // This catch helps to prevent the application from crashing in this best-effort dump-diagnostics path,
            // but doesn't prevent the overall crash from going to Watson.
            catch
            {
            }
        }
 
        /// <summary>
        /// Returns the content of any exception dump files modified
        /// since the provided time, otherwise returns an empty string.
        /// </summary>
        internal static string ReadAnyExceptionFromFile(DateTime fromTimeUtc)
        {
            var builder = new StringBuilder();
            IEnumerable<string> files = FileSystems.Default.EnumerateFiles(DebugDumpPath, "MSBuild*failure.txt");
 
            foreach (string file in files)
            {
                if (FileSystems.Default.GetLastWriteTimeUtc(file) >= fromTimeUtc)
                {
                    builder.Append(Environment.NewLine);
                    builder.Append(file);
                    builder.Append(':');
                    builder.Append(Environment.NewLine);
                    builder.Append(FileSystems.Default.ReadFileAllText(file));
                    builder.Append(Environment.NewLine);
                }
            }
 
            return builder.ToString();
        }
    }
}