// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if BUILDINGAPPXTASKS namespace Microsoft.Build.AppxPackage.Shared #else using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Security; using System.Text; using System.Threading; using System.Xml; using Microsoft.Build.Shared.FileSystem; using System.Xml.Schema; using System.Runtime.Serialization; //EPIC BEGIN #nullable disable //EPIC END namespace Microsoft.Build.Shared #endif { /// /// Utility methods for classifying and handling exceptions. /// //EPIC BEGIN [SuppressMessage("Performance", "CA1837:Use 'Environment.ProcessId'", Justification = "")] [SuppressMessage("Reliability", "CA2002:Do not lock on objects with weak identity", Justification = "")] [SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "")] //EPIC END internal static class ExceptionHandling { private static readonly string s_debugDumpPath; static ExceptionHandling() { s_debugDumpPath = GetDebugDumpPath(); } /// /// Gets the location of the directory used for diagnostic log files. /// /// private static string GetDebugDumpPath() { string debugPath = Environment.GetEnvironmentVariable("MSBUILDDEBUGPATH"); return !string.IsNullOrEmpty(debugPath) ? debugPath : Path.GetTempPath(); } /// /// The directory used for diagnostic log files. /// internal static string DebugDumpPath => s_debugDumpPath; #if !BUILDINGAPPXTASKS /// /// The filename that exceptions will be dumped to /// private static string s_dumpFileName; #endif /// /// If the given exception is "ignorable under some circumstances" return false. /// Otherwise it's "really bad", and return true. /// This makes it possible to catch(Exception ex) without catching disasters. /// /// The exception to check. /// True if exception is critical. internal static bool IsCriticalException(Exception e) { if (e is OutOfMemoryException || e is StackOverflowException || e is ThreadAbortException || e is ThreadInterruptedException || e is AccessViolationException #if !BUILDINGAPPXTASKS || e is InternalErrorException #endif ) { // Ideally we would include NullReferenceException, because it should only ever be thrown by CLR (use ArgumentNullException for arguments) // but we should handle it if tasks and loggers throw it. // ExecutionEngineException has been deprecated by the CLR return true; } #if !CLR2COMPATIBILITY // Check if any critical exceptions var aggregateException = e as AggregateException; if (aggregateException != null) { // If the aggregate exception contains a critical exception it is considered a critical exception if (aggregateException.InnerExceptions.Any(innerException => IsCriticalException(innerException))) { return true; } } #endif return false; } /// /// If the given exception is file IO related or expected return false. /// Otherwise, return true. /// /// The exception to check. /// True if exception is not IO related or expected otherwise false. internal static bool NotExpectedException(Exception e) { return !IsIoRelatedException(e); } /// /// Determine whether the exception is file-IO related. /// /// The exception to check. /// True if exception is IO related. internal static bool IsIoRelatedException(Exception e) { // These all derive from IOException // DirectoryNotFoundException // DriveNotFoundException // EndOfStreamException // FileLoadException // FileNotFoundException // PathTooLongException // PipeException return e is UnauthorizedAccessException || e is NotSupportedException || (e is ArgumentException && !(e is ArgumentNullException)) || e is SecurityException || e is IOException; } /// Checks if the exception is an XML one. /// Exception to check. /// True if exception is related to XML parsing. internal static bool IsXmlException(Exception e) { return e is XmlException || e is XmlSyntaxException || e is XmlSchemaException || e is UriFormatException; // XmlTextReader for example uses this under the covers } /// Extracts line and column numbers from the exception if it is XML-related one. /// XML-related exception. /// Line and column numbers if available, (0,0) if not. /// This function works around the fact that XmlException and XmlSchemaException are not directly related. internal static LineAndColumn GetXmlLineAndColumn(Exception e) { var line = 0; var column = 0; var xmlException = e as XmlException; if (xmlException != null) { line = xmlException.LineNumber; column = xmlException.LinePosition; } else { var schemaException = e as XmlSchemaException; if (schemaException != null) { line = schemaException.LineNumber; column = schemaException.LinePosition; } } return new LineAndColumn { Line = line, Column = column }; } #if !BUILDINGAPPXTASKS /// /// If the given exception is file IO related or Xml related return false. /// Otherwise, return true. /// /// The exception to check. internal static bool NotExpectedIoOrXmlException(Exception e) { if ( IsXmlException(e) || !NotExpectedException(e) ) { return false; } return true; } /// /// If the given exception is reflection-related return false. /// Otherwise, return true. /// /// The exception to check. internal static bool NotExpectedReflectionException(Exception e) { // We are explicitly not handling TargetInvocationException. Those are just wrappers around // exceptions thrown by the called code (such as a task or logger) which callers will typically // want to treat differently. if ( e is TypeLoadException // thrown when the common language runtime cannot find the assembly, the type within the assembly, or cannot load the type || e is MethodAccessException // thrown when a class member is not found or access to the member is not permitted || e is MissingMethodException // thrown when code in a dependent assembly attempts to access a missing method in an assembly that was modified || e is MemberAccessException // thrown when a class member is not found or access to the member is not permitted || e is BadImageFormatException // thrown when the file image of a DLL or an executable program is invalid || e is ReflectionTypeLoadException // thrown by the Module.GetTypes method if any of the classes in a module cannot be loaded || e is TargetParameterCountException // thrown when the number of parameters for an invocation does not match the number expected || e is InvalidCastException || e is AmbiguousMatchException // thrown when binding to a member results in more than one member matching the binding criteria || e is CustomAttributeFormatException // thrown if a custom attribute on a data type is formatted incorrectly || e is InvalidFilterCriteriaException // thrown in FindMembers when the filter criteria is not valid for the type of filter you are using || e is TargetException // thrown when an attempt is made to invoke a non-static method on a null object. This may occur because the caller does not // have access to the member, or because the target does not define the member, and so on. || e is MissingFieldException // thrown when code in a dependent assembly attempts to access a missing field in an assembly that was modified. || !NotExpectedException(e) // Reflection can throw IO exceptions if the assembly cannot be opened ) { return false; } return true; } /// /// Serialization has been observed to throw TypeLoadException as /// well as SerializationException and IO exceptions. (Obviously /// it has to do reflection but it ought to be wrapping the exceptions.) /// internal static bool NotExpectedSerializationException(Exception e) { if ( e is SerializationException || !NotExpectedReflectionException(e) ) { return false; } return true; } /// /// Returns false if this is a known exception thrown by the registry API. /// internal static bool NotExpectedRegistryException(Exception e) { if (e is SecurityException || e is UnauthorizedAccessException || e is IOException || e is ObjectDisposedException || e is ArgumentException) { return false; } return true; } /// /// Returns false if this is a known exception thrown by function evaluation /// internal static bool NotExpectedFunctionException(Exception e) { if (e is InvalidCastException || e is ArgumentNullException || e is FormatException || e is InvalidOperationException || !NotExpectedReflectionException(e)) { return false; } return true; } #if FEATURE_APPDOMAIN_UNHANDLED_EXCEPTION /// /// Dump any unhandled exceptions to a file so they can be diagnosed /// [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); } #endif /// /// Dump the exception information to a file /// internal static void DumpExceptionToFile(Exception ex) { // 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 (!FileSystems.Default.DirectoryExists(DebugDumpPath)) { // 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 = Process.GetCurrentProcess().Id; // 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("==================="); } } } /// /// Returns the content of any exception dump files modified /// since the provided time, otherwise returns an empty string. /// internal static string ReadAnyExceptionFromFile(DateTime fromTimeUtc) { var builder = new StringBuilder(); IEnumerable files = FileSystems.Default.EnumerateFiles(DebugDumpPath, "MSBuild*failure.txt"); foreach (string file in files) { if (File.GetLastWriteTimeUtc(file) >= fromTimeUtc) { builder.Append(Environment.NewLine); builder.Append(file); builder.Append(":"); builder.Append(Environment.NewLine); builder.Append(File.ReadAllText(file)); builder.Append(Environment.NewLine); } } return builder.ToString(); } #endif /// Line and column pair. internal struct LineAndColumn { /// Gets or sets line number. internal int Line { get; set; } /// Gets or sets column position. internal int Column { get; set; } } } }