File: Exceptions\InternalErrorException.cs
Web Access
Project: ..\..\..\src\MSBuildTaskHost\MSBuildTaskHost.csproj (MSBuildTaskHost)
// 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.Diagnostics;
using System.Runtime.Serialization;
 
namespace Microsoft.Build.TaskHost.Exceptions;
 
/// <summary>
/// This exception is to be thrown whenever an assumption we have made in the code turns out to be false. Thus, if this
/// exception ever gets thrown, it is because of a bug in our own code, not because of something the user or project author
/// did wrong.
/// </summary>
[Serializable]
internal sealed class InternalErrorException : BuildExceptionBase
{
    private InternalErrorException()
        : base()
    {
    }
 
    internal InternalErrorException(string message)
        : base($"MSB0001: Internal MSBuild Error: {message}")
    {
        ConsiderDebuggerLaunch(message, null);
    }
 
    /// <summary>
    /// Creates an instance of this exception using the given message and inner exception.
    /// Adds the inner exception's details to the exception message because most bug reporters don't bother
    /// to provide the inner exception details which is typically what we care about.
    /// </summary>
    internal InternalErrorException(string message, Exception? innerException)
        : this(message, innerException, calledFromDeserialization: false)
    {
    }
 
    internal static InternalErrorException CreateFromRemote(string message, Exception? innerException)
        => new(message, innerException, calledFromDeserialization: true);
 
    private InternalErrorException(string message, Exception? innerException, bool calledFromDeserialization)
        : base(GetMessage(calledFromDeserialization, message, innerException))
    {
        if (!calledFromDeserialization)
        {
            ConsiderDebuggerLaunch(message, innerException);
        }
    }
 
    private static string GetMessage(bool calledFromDeserialization, string message, Exception? innerException)
        => calledFromDeserialization
            ? message
            : innerException is null
                ? $"MSB0001: Internal MSBuild Error: {message}"
                : $"MSB0001: Internal MSBuild Error: {message}\n=============\n{innerException}\n\n";
 
    /// <summary>
    /// Private constructor used for (de)serialization. The constructor is private as this class is sealed
    /// If we ever add new members to this class, we'll need to update this.
    /// </summary>
    private InternalErrorException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        // Do nothing: no fields
    }
 
    /// <summary>
    /// A fatal internal error due to a bug has occurred. Give the dev a chance to debug it, if possible.
    ///
    /// Will in all cases launch the debugger, if the environment variable "MSBUILDLAUNCHDEBUGGER" is set.
    ///
    /// In DEBUG build, will always launch the debugger, unless we are in razzle (_NTROOT is set) or in NUnit,
    /// or MSBUILDDONOTLAUNCHDEBUGGER is set (that could be useful in suite runs).
    /// We don't launch in retail or LKG so builds don't jam; they get a callstack, and continue or send a mail, etc.
    /// We don't launch in NUnit as tests often intentionally cause InternalErrorExceptions.
    ///
    /// Because we only call this method from this class, just before throwing an InternalErrorException, there is
    /// no danger that this suppression will cause a bug to only manifest itself outside NUnit
    /// (which would be most unfortunate!). Do not make this non-private.
    ///
    /// Unfortunately NUnit can't handle unhandled exceptions like InternalErrorException on anything other than
    /// the main test thread. However, there's still a callstack displayed before it quits.
    ///
    /// If it is going to launch the debugger, it first does a Debug.Fail to give information about what needs to
    /// be debugged -- the exception hasn't been thrown yet. This automatically displays the current callstack.
    /// </summary>
    private static void ConsiderDebuggerLaunch(string message, Exception? innerException)
    {
        string innerMessage = innerException == null ? string.Empty : innerException.ToString();
 
        if (Environment.GetEnvironmentVariable("MSBUILDLAUNCHDEBUGGER") != null)
        {
            LaunchDebugger(message, innerMessage);
            return;
        }
 
#if DEBUG
        if (Environment.GetEnvironmentVariable("MSBUILDDONOTLAUNCHDEBUGGER") == null
            && Environment.GetEnvironmentVariable("_NTROOT") == null)
        {
            LaunchDebugger(message, innerMessage);
            return;
        }
#endif
    }
 
    private static void LaunchDebugger(string message, string innerMessage)
    {
#if FEATURE_DEBUG_LAUNCH
        Debug.Fail(message, innerMessage);
        Debugger.Launch();
#else
        Console.WriteLine("MSBuild Failure: " + message);
        if (!string.IsNullOrEmpty(innerMessage))
        {
            Console.WriteLine(innerMessage);
        }
        Console.WriteLine("Waiting for debugger to attach to process: " + Process.GetCurrentProcess().Id);
        while (!Debugger.IsAttached)
        {
            System.Threading.Thread.Sleep(100);
        }
#endif
    }
}