// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text; using Microsoft.Dnx.Compilation; using Microsoft.Extensions.PlatformAbstractions; using Microsoft.Extensions.WebEncoders; namespace Microsoft.AspNet.Hosting.Startup { internal static class StartupExceptionPage { private const int MaxCompilationErrorsToShow = 20; private static readonly string _errorPageFormatString = GetResourceString("GenericError.html", escapeBraces: true); private static readonly string _errorMessageFormatString = GetResourceString("GenericError_Message.html"); private static readonly string _errorExceptionFormatString = GetResourceString("GenericError_Exception.html"); private static readonly string _errorFooterFormatString = GetResourceString("GenericError_Footer.html"); private static readonly string _compilationExceptionFormatString = GetResourceString("Compilation_Exception.html"); public static byte[] GenerateErrorHtml(bool showDetails, IRuntimeEnvironment runtimeEnvironment, params object[] errorDetails) { if (!showDetails) { errorDetails = new[] { "An error occurred while starting the application." }; } // Build the message for each error var wasSourceCodeWrittenOntoPage = false; var builder = new StringBuilder(); var rawExceptionDetails = new StringBuilder(); foreach (object error in errorDetails ?? new object[0]) { var ex = error as Exception; if (ex == null && error is ExceptionDispatchInfo) { ex = ((ExceptionDispatchInfo)error).SourceException; } if (ex != null) { var flattenedExceptions = FlattenAndReverseExceptionTree(ex); var compilationException = flattenedExceptions.OfType() .FirstOrDefault(); if (compilationException != null) { WriteException(compilationException, builder, ref wasSourceCodeWrittenOntoPage); var compilationErrorMessages = compilationException.CompilationFailures .SelectMany(f => f.Messages.Select(m => m.FormattedMessage)) .Take(MaxCompilationErrorsToShow); WriteRawExceptionDetails("Show raw compilation error details", compilationErrorMessages, rawExceptionDetails); } else { foreach (var innerEx in flattenedExceptions) { WriteException(innerEx, builder, ref wasSourceCodeWrittenOntoPage); } WriteRawExceptionDetails("Show raw exception details", new[] { ex.ToString() }, rawExceptionDetails); } } else { var message = Convert.ToString(error, CultureInfo.InvariantCulture); WriteMessage(message, builder); } } // Generate the footer var footer = showDetails ? GenerateFooterEncoded(runtimeEnvironment) : null; // And generate the full markup return Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, _errorPageFormatString, builder, rawExceptionDetails, footer)); } private static string BuildCodeSnippetDiv(StackFrame frame) { var filename = frame.GetFileName(); if (!string.IsNullOrEmpty(filename)) { int failingLineNumber = frame.GetFileLineNumber(); if (failingLineNumber >= 1) { var lines = GetFailingCallSiteInFile(filename, failingLineNumber); if (lines != null) { return @"
" + @"
" + HtmlEncodeAndReplaceLineBreaks(filename) + "
" + Environment.NewLine + string.Join(Environment.NewLine, lines) + "
" + Environment.NewLine; } } } // fallback return null; } private static string BuildLineForStackFrame(StackFrame frame) { var builder = new StringBuilder("
");
            var method = frame.GetMethod();

            // Special case: no method available
            if (method == null)
            {
                return null;
            }

            // First, write the type name
            var type = method.DeclaringType;
            if (type != null)
            {
                // Special-case ExceptionDispatchInfo.Throw()
                if (type == typeof(ExceptionDispatchInfo) && method.Name == "Throw")
                {
                    return @"
--- exception rethrown ---
"; } string prefix, friendlyName; SplitTypeIntoPrefixAndFriendlyName(type, out prefix, out friendlyName); builder.AppendFormat(CultureInfo.InvariantCulture, @"at {0}", HtmlEncodeAndReplaceLineBreaks(prefix)); builder.Append(HtmlEncodeAndReplaceLineBreaks(friendlyName)); } // Next, write the method signature builder.Append(HtmlEncodeAndReplaceLineBreaks("." + method.Name)); // Is this method generic? if (method.IsGenericMethod) { builder.Append(HtmlEncodeAndReplaceLineBreaks(BuildMethodGenericParametersUnescaped(method))); } // Build method parameters builder.AppendFormat(CultureInfo.InvariantCulture, @"{0}", HtmlEncodeAndReplaceLineBreaks(BuildMethodParametersUnescaped(method))); // Do we have source information for this frame? if (frame.GetILOffset() != -1) { var filename = frame.GetFileName(); if (!string.IsNullOrEmpty(filename)) { builder.AppendFormat(CultureInfo.InvariantCulture, " in {0}:line {1:D}", HtmlEncodeAndReplaceLineBreaks(filename), frame.GetFileLineNumber()); } } // Finish builder.Append("
"); return builder.ToString(); } private static string BuildMethodGenericParametersUnescaped(MethodBase method) { Debug.Assert(method.IsGenericMethod); return "<" + string.Join(", ", method.GetGenericArguments().Select(PrettyPrintTypeName)) + ">"; } private static string BuildMethodParametersUnescaped(MethodBase method) { return "(" + string.Join(", ", method.GetParameters().Select(p => { Type parameterType = p.ParameterType; return ((parameterType != null) ? PrettyPrintTypeName(parameterType) : "?") + " " + p.Name; })) + ")"; } private static void BuildCodeSnippetDiv(CompilationFailure failure, StringBuilder builder, ref int totalErrorsShown) { const int NumContextLines = 3; var fileName = failure.SourceFilePath; if (totalErrorsShown < MaxCompilationErrorsToShow && !string.IsNullOrEmpty(fileName)) { builder.Append(@"
") .AppendFormat(@"
{0}
", HtmlEncodeAndReplaceLineBreaks(fileName)) .AppendLine(); IEnumerable fileContent; if (string.IsNullOrEmpty(failure.SourceFileContent)) { fileContent = File.ReadLines(fileName); } else { fileContent = failure.SourceFileContent.Split(new[] { Environment.NewLine }, StringSplitOptions.None); } foreach (var message in failure.Messages) { if (totalErrorsShown++ > MaxCompilationErrorsToShow) { break; } if (totalErrorsShown > 1) { builder.AppendLine("
"); } builder.Append(@"
") .Append(HtmlEncodeAndReplaceLineBreaks(message.Message)) .Append("
"); // StartLine and EndLine are 1-based var startLine = message.StartLine - 1; var endLine = message.EndLine - 1; var preContextIndex = Math.Max(startLine - NumContextLines, 0); var index = preContextIndex + 1; foreach (var line in fileContent.Skip(preContextIndex).Take(startLine - preContextIndex)) { builder.Append(@"
") .AppendFormat(@"{0}", index++) .Append(HtmlEncodeAndReplaceLineBreaks(line)) .AppendLine("
"); } var numErrorLines = 1 + Math.Max(0, endLine - startLine); foreach (var line in fileContent.Skip(startLine).Take(numErrorLines)) { builder.Append(@"
") .AppendFormat(@"{0}", index++) .Append(HtmlEncodeAndReplaceLineBreaks(line)) .AppendLine("
"); } foreach (var line in fileContent.Skip(message.EndLine).Take(NumContextLines)) { builder.Append(@"
") .AppendFormat(@"{0}", index++) .Append(HtmlEncodeAndReplaceLineBreaks(line)) .AppendLine("
"); } } builder.AppendLine("
"); // Close codeSnippet div } } private static string GetResourceString(string name, bool escapeBraces = false) { // '{' and '}' are special in CSS, so we use "[[[0]]]" instead for {0} (and so on). var assembly = typeof(StartupExceptionPage).GetTypeInfo().Assembly; var resourceName = assembly.GetName().Name + ".compiler.resources." + name; var manifestStream = assembly.GetManifestResourceStream(resourceName); var formatString = new StreamReader(manifestStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false).ReadToEnd(); if (escapeBraces) { formatString = formatString.Replace("{", "{{").Replace("}", "}}").Replace("[[[", "{").Replace("]]]", "}"); } return formatString; } private static List GetFailingCallSiteInFile(string filename, int failedLineNumber) { // We figure out the [first, last] range of lines to read from the file. var firstLineNumber = failedLineNumber - 2; firstLineNumber = Math.Max(1, firstLineNumber); var lastLineNumber = failedLineNumber + 2; lastLineNumber = Math.Max(lastLineNumber, failedLineNumber); // Figure out how many characters lastLineNumber will take to print. var lastLineNumberCharLength = lastLineNumber.ToString("D", CultureInfo.InvariantCulture).Length; var errorSubContents = new List(); var didReadFailingLine = false; try { var thisLineNum = 0; foreach (var line in File.ReadLines(filename)) { thisLineNum++; // Are we within the correct range? if (thisLineNum < firstLineNumber) { continue; } if (thisLineNum > lastLineNumber) { break; } var encodedLine = HtmlEncodeAndReplaceLineBreaks("Line " + thisLineNum.ToString("D", CultureInfo.InvariantCulture).PadLeft(lastLineNumberCharLength) + ": " + line); if (thisLineNum == failedLineNumber) { didReadFailingLine = true; errorSubContents.Add(@"
" + encodedLine + "
"); } else { errorSubContents.Add(@"
" + encodedLine + "
"); } } } catch { // If there's an error for any reason, don't show source. return null; } return (didReadFailingLine) ? errorSubContents : null; } private static string PrettyPrintTypeName(Type t) { try { RuntimeHelpers.EnsureSufficientExecutionStack(); var name = t.Name; // Degenerate case if (string.IsNullOrEmpty(name)) { name = "?"; } // Handle generic types if (t.GetTypeInfo().IsGenericType) { // strip off the CLR generic type marker if it exists var indexOfGenericTypeMarker = name.LastIndexOf('`'); if (indexOfGenericTypeMarker >= 0) { name = name.Substring(0, indexOfGenericTypeMarker); name += "<" + string.Join(", ", t.GetGenericArguments().Select(PrettyPrintTypeName)) + ">"; } } // Handle nested types if (!t.IsGenericParameter) { var containerType = t.DeclaringType; if (containerType != null) { name = PrettyPrintTypeName(containerType) + "." + name; } } return name; } catch { // If anything at all goes wrong, fall back to the full type name so that we don't crash the server. return t.FullName; } } private static void SplitTypeIntoPrefixAndFriendlyName(Type type, out string prefix, out string friendlyName) { prefix = type.Namespace; friendlyName = PrettyPrintTypeName(type); if (!string.IsNullOrEmpty(friendlyName) && !string.IsNullOrEmpty(prefix)) { prefix += "."; } } private static string GenerateFooterEncoded(IRuntimeEnvironment environment) { var runtimeType = HtmlEncodeAndReplaceLineBreaks(environment.RuntimeType); #if DNXCORE50 || DOTNET5_4 var systemRuntimeAssembly = typeof(System.ComponentModel.DefaultValueAttribute).GetTypeInfo().Assembly; var assemblyVersion = new AssemblyName(systemRuntimeAssembly.FullName).Version.ToString(); var clrVersion = HtmlEncodeAndReplaceLineBreaks(assemblyVersion); #else var clrVersion = HtmlEncodeAndReplaceLineBreaks(Environment.Version.ToString()); #endif var runtimeArch = HtmlEncodeAndReplaceLineBreaks(environment.RuntimeArchitecture); var dnxVersion = HtmlEncodeAndReplaceLineBreaks(environment.RuntimeVersion); var currentAssembly = typeof(StartupExceptionPage).GetTypeInfo().Assembly; var currentAssemblyVersion = currentAssembly.GetCustomAttribute().InformationalVersion; currentAssemblyVersion = HtmlEncodeAndReplaceLineBreaks(currentAssemblyVersion); var os = HtmlEncodeAndReplaceLineBreaks(environment.OperatingSystem); var osVersion = HtmlEncodeAndReplaceLineBreaks(environment.OperatingSystemVersion); return string.Format(CultureInfo.InvariantCulture, _errorFooterFormatString, runtimeType, clrVersion, runtimeArch, dnxVersion, currentAssemblyVersion, os, osVersion); } private static string HtmlEncodeAndReplaceLineBreaks(string input) { if (string.IsNullOrEmpty(input)) { return string.Empty; } // Split on line breaks before passing it through the encoder. // We use the static default encoder since we can't depend on DI in the error handling logic. return string.Join("
" + Environment.NewLine, input.Split(new[] { "\r\n" }, StringSplitOptions.None) .SelectMany(s => s.Split(new[] { '\r', '\n' }, StringSplitOptions.None)) .Select(HtmlEncoder.Default.HtmlEncode)); } private static void WriteException(Exception ex, StringBuilder builder, ref bool wasFailingCallSiteSourceWritten) { string inlineSourceDiv = null; // First, build the stack trace var firstStackFrame = true; var stackTraceBuilder = new StringBuilder(); var needFileInfo = true; foreach (var frame in new StackTrace(ex, needFileInfo).GetFrames() ?? Enumerable.Empty()) { if (!firstStackFrame) { stackTraceBuilder.Append("
"); } firstStackFrame = false; var thisFrameLine = BuildLineForStackFrame(frame); stackTraceBuilder.AppendLine(thisFrameLine); // Try to include the source code in the error page if we can. if (!wasFailingCallSiteSourceWritten && inlineSourceDiv == null) { inlineSourceDiv = BuildCodeSnippetDiv(frame); if (inlineSourceDiv != null) { wasFailingCallSiteSourceWritten = true; } } } // Finally, build the rest of the
builder.AppendFormat(CultureInfo.InvariantCulture, _errorExceptionFormatString, HtmlEncodeAndReplaceLineBreaks(ex.GetType().FullName), HtmlEncodeAndReplaceLineBreaks(ex.Message), inlineSourceDiv, stackTraceBuilder); } private static void WriteRawExceptionDetails(string linkText, IEnumerable lines, StringBuilder rawExceptionDetails) { rawExceptionDetails .AppendLine("
") .AppendFormat($" ") .AppendLine() .AppendLine("
") .Append("
");

            foreach (var line in lines)
            {
                rawExceptionDetails.AppendLine(line);
            }

            rawExceptionDetails
                .AppendLine("
") .AppendLine("
") .AppendLine("
"); } private static void WriteException(ICompilationException compilationException, StringBuilder builder, ref bool wasSourceCodeWrittenOntoPage) { var totalErrorsShown = 0; var inlineSourceDiv = new StringBuilder(); var firstStackFrame = true; foreach (var failure in compilationException.CompilationFailures) { if (firstStackFrame) { firstStackFrame = false; } else { inlineSourceDiv.AppendLine("
"); } BuildCodeSnippetDiv(failure, inlineSourceDiv, ref totalErrorsShown); } wasSourceCodeWrittenOntoPage = totalErrorsShown > 0; builder.AppendFormat(CultureInfo.InvariantCulture, _compilationExceptionFormatString, inlineSourceDiv); } private static void WriteMessage(string message, StringBuilder builder) { // Build the
builder.AppendFormat(CultureInfo.InvariantCulture, _errorMessageFormatString, HtmlEncodeAndReplaceLineBreaks(message)); } private static IEnumerable FlattenAndReverseExceptionTree(Exception ex) { var list = new List(); while (ex != null) { list.Add(ex); ex = ex.InnerException; } list.Reverse(); return list; } } }