aspnetcore/src/Microsoft.AspNet.Hosting/Startup/StartupExceptionPage.cs

522 lines
22 KiB
C#

// 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<ICompilationException>()
.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 @"<div class=""codeSnippet"">"
+ @"<div class=""filename""><code>" + HtmlEncodeAndReplaceLineBreaks(filename) + "</code></div>" + Environment.NewLine
+ string.Join(Environment.NewLine, lines) + "</div>" + Environment.NewLine;
}
}
}
// fallback
return null;
}
private static string BuildLineForStackFrame(StackFrame frame)
{
var builder = new StringBuilder("<pre>");
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 @"<pre><span class=""faded"">--- exception rethrown ---</span></pre>";
}
string prefix, friendlyName;
SplitTypeIntoPrefixAndFriendlyName(type, out prefix, out friendlyName);
builder.AppendFormat(CultureInfo.InvariantCulture, @"<span class=""faded"">at {0}</span>", 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, @"<span class=""faded"">{0}</span>", 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("</pre>");
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(@"<div class=""codeSnippet"">")
.AppendFormat(@"<div class=""filename""><code>{0}</code></div>", HtmlEncodeAndReplaceLineBreaks(fileName))
.AppendLine();
IEnumerable<string> 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("<br />");
}
builder.Append(@"<div class=""error-message"">")
.Append(HtmlEncodeAndReplaceLineBreaks(message.Message))
.Append("</div>");
// 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(@"<div class=""line faded"">")
.AppendFormat(@"<span class=""line-number"">{0}</span><code>", index++)
.Append(HtmlEncodeAndReplaceLineBreaks(line))
.AppendLine("</code></div>");
}
var numErrorLines = 1 + Math.Max(0, endLine - startLine);
foreach (var line in fileContent.Skip(startLine).Take(numErrorLines))
{
builder.Append(@"<div class=""line error"">")
.AppendFormat(@"<span class=""line-number"">{0}</span><code>", index++)
.Append(HtmlEncodeAndReplaceLineBreaks(line))
.AppendLine("</code></div>");
}
foreach (var line in fileContent.Skip(message.EndLine).Take(NumContextLines))
{
builder.Append(@"<div class=""line faded"">")
.AppendFormat(@"<span class=""line-number"">{0}</span><code>", index++)
.Append(HtmlEncodeAndReplaceLineBreaks(line))
.AppendLine("</code></div>");
}
}
builder.AppendLine("</div>"); // 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<string> 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<string>();
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(@"<div class=""line error""><code>" + encodedLine + "</code></div>");
}
else
{
errorSubContents.Add(@"<div class=""line""><code>" + encodedLine + "</code></div>");
}
}
}
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<AssemblyInformationalVersionAttribute>().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("<br />" + 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<StackFrame>())
{
if (!firstStackFrame)
{
stackTraceBuilder.Append("<br />");
}
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 <div>
builder.AppendFormat(CultureInfo.InvariantCulture, _errorExceptionFormatString,
HtmlEncodeAndReplaceLineBreaks(ex.GetType().FullName),
HtmlEncodeAndReplaceLineBreaks(ex.Message),
inlineSourceDiv,
stackTraceBuilder);
}
private static void WriteRawExceptionDetails(string linkText, IEnumerable<string> lines, StringBuilder rawExceptionDetails)
{
rawExceptionDetails
.AppendLine("<div class=\"rawExceptionBlock\">")
.AppendFormat($" <div><a href=\"#\" onclick=\"javascript: showRawException(); return false;\">{linkText}</a></div>")
.AppendLine()
.AppendLine(" <div id=\"rawException\">")
.Append(" <pre>");
foreach (var line in lines)
{
rawExceptionDetails.AppendLine(line);
}
rawExceptionDetails
.AppendLine("</pre>")
.AppendLine(" </div>")
.AppendLine("</div>");
}
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("<br/>");
}
BuildCodeSnippetDiv(failure, inlineSourceDiv, ref totalErrorsShown);
}
wasSourceCodeWrittenOntoPage = totalErrorsShown > 0;
builder.AppendFormat(CultureInfo.InvariantCulture,
_compilationExceptionFormatString,
inlineSourceDiv);
}
private static void WriteMessage(string message, StringBuilder builder)
{
// Build the <div>
builder.AppendFormat(CultureInfo.InvariantCulture, _errorMessageFormatString,
HtmlEncodeAndReplaceLineBreaks(message));
}
private static IEnumerable<Exception> FlattenAndReverseExceptionTree(Exception ex)
{
var list = new List<Exception>();
while (ex != null)
{
list.Add(ex);
ex = ex.InnerException;
}
list.Reverse();
return list;
}
}
}