From a9e7948d726c1d66dd5a7865ebcb4f60a6ec6c0d Mon Sep 17 00:00:00 2001 From: Chris R Date: Wed, 23 Sep 2015 20:54:42 -0700 Subject: [PATCH] #77 Catch startup exceptions and show them in the browser. --- .../Internal/HostingEngine.cs | 110 +++- src/Microsoft.AspNet.Hosting/Program.cs | 2 +- .../Startup/StartupExceptionPage.cs | 520 ++++++++++++++++++ .../WebHostBuilder.cs | 10 +- .../resources/Compilation_Exception.html | 6 + .../compiler/resources/GenericError.html | 151 +++++ .../resources/GenericError_Exception.html | 8 + .../resources/GenericError_Footer.html | 3 + .../resources/GenericError_Message.html | 3 + src/Microsoft.AspNet.Hosting/project.json | 4 +- .../Fakes/RuntimeEnvironment.cs | 20 + .../Fakes/StartupConfigureServicesThrows.cs | 22 + .../Fakes/StartupConfigureThrows.cs | 21 + .../Fakes/StartupCtorThrows.cs | 20 + .../Fakes/StartupStaticCtorThrows.cs | 20 + .../WebHostBuilderTests.cs | 115 +++- 16 files changed, 1001 insertions(+), 34 deletions(-) create mode 100644 src/Microsoft.AspNet.Hosting/Startup/StartupExceptionPage.cs create mode 100644 src/Microsoft.AspNet.Hosting/compiler/resources/Compilation_Exception.html create mode 100644 src/Microsoft.AspNet.Hosting/compiler/resources/GenericError.html create mode 100644 src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Exception.html create mode 100644 src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Footer.html create mode 100644 src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Message.html create mode 100644 test/Microsoft.AspNet.Hosting.Tests/Fakes/RuntimeEnvironment.cs create mode 100644 test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupConfigureServicesThrows.cs create mode 100644 test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupConfigureThrows.cs create mode 100644 test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupCtorThrows.cs create mode 100644 test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupStaticCtorThrows.cs diff --git a/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs b/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs index 986def36af..97bdc06e23 100644 --- a/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs +++ b/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs @@ -14,6 +14,7 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features.Internal; using Microsoft.AspNet.Server.Features; +using Microsoft.Dnx.Runtime; using Microsoft.Framework.Configuration; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Logging; @@ -24,11 +25,13 @@ namespace Microsoft.AspNet.Hosting.Internal { // This is defined by IIS's HttpPlatformHandler. private static readonly string ServerPort = "HTTP_PLATFORM_PORT"; + private static readonly string DetailedErrors = "Hosting:DetailedErrors"; private readonly IServiceCollection _applicationServiceCollection; private readonly IStartupLoader _startupLoader; private readonly ApplicationLifetime _applicationLifetime; private readonly IConfiguration _config; + private readonly bool _captureStartupErrors; private IServiceProvider _applicationServices; @@ -45,7 +48,8 @@ namespace Microsoft.AspNet.Hosting.Internal public HostingEngine( IServiceCollection appServices, IStartupLoader startupLoader, - IConfiguration config) + IConfiguration config, + bool captureStartupErrors) { if (appServices == null) { @@ -65,6 +69,7 @@ namespace Microsoft.AspNet.Hosting.Internal _config = config; _applicationServiceCollection = appServices; _startupLoader = startupLoader; + _captureStartupErrors = captureStartupErrors; _applicationLifetime = new ApplicationLifetime(); } @@ -79,8 +84,6 @@ namespace Microsoft.AspNet.Hosting.Internal public virtual IApplication Start() { - EnsureApplicationServices(); - var application = BuildApplication(); var logger = _applicationServices.GetRequiredService>(); @@ -174,6 +177,67 @@ namespace Microsoft.AspNet.Hosting.Internal } private RequestDelegate BuildApplication() + { + try + { + EnsureApplicationServices(); + EnsureServer(); + + var builderFactory = _applicationServices.GetRequiredService(); + var builder = builderFactory.CreateBuilder(_serverInstance); + builder.ApplicationServices = _applicationServices; + + var startupFilters = _applicationServices.GetService>(); + var configure = Startup.ConfigureDelegate; + foreach (var filter in startupFilters) + { + configure = filter.Configure(configure); + } + + configure(builder); + + return builder.Build(); + } + catch (Exception ex) + { + if (!_captureStartupErrors) + { + throw; + } + + // EnsureApplicationServices may have failed due to a missing or throwing Startup class. + if (_applicationServices == null) + { + _applicationServices = _applicationServiceCollection.BuildServiceProvider(); + } + + EnsureServer(); + + // Write errors to standard out so they can be retrieved when not in development mode. + Console.Out.WriteLine("Application startup exception: " + ex.ToString()); + var logger = _applicationServices.GetRequiredService>(); + logger.LogError("Application startup exception", ex); + + // Generate an HTML error page. + var runtimeEnv = _applicationServices.GetRequiredService(); + var hostingEnv = _applicationServices.GetRequiredService(); + var showDetailedErrors = hostingEnv.IsDevelopment() + || string.Equals("true", _config[DetailedErrors], StringComparison.OrdinalIgnoreCase) + || string.Equals("1", _config[DetailedErrors], StringComparison.OrdinalIgnoreCase); + var errorBytes = StartupExceptionPage.GenerateErrorHtml(showDetailedErrors, runtimeEnv, ex); + + return context => + { + context.Response.StatusCode = 500; + context.Response.Headers["Cache-Control"] = "private, max-age=0"; + context.Response.ContentType = "text/html; charset=utf-8"; + context.Response.ContentLength = errorBytes.Length; + return context.Response.Body.WriteAsync(errorBytes, 0, errorBytes.Length); + }; + } + } + + private void EnsureServer() { if (ServerFactory == null) { @@ -186,37 +250,25 @@ namespace Microsoft.AspNet.Hosting.Internal ServerFactory = _applicationServices.GetRequiredService().LoadServerFactory(ServerFactoryLocation); } - _serverInstance = ServerFactory.Initialize(_config); - var builderFactory = _applicationServices.GetRequiredService(); - var builder = builderFactory.CreateBuilder(_serverInstance); - builder.ApplicationServices = _applicationServices; - - var addresses = builder.ServerFeatures?.Get()?.Addresses; - if (addresses != null && !addresses.IsReadOnly) + if (_serverInstance == null) { - var port = _config[ServerPort]; - if (!string.IsNullOrEmpty(port)) + _serverInstance = ServerFactory.Initialize(_config); + var addresses = _serverInstance?.Get()?.Addresses; + if (addresses != null && !addresses.IsReadOnly) { - addresses.Add("http://localhost:" + port); - } + var port = _config[ServerPort]; + if (!string.IsNullOrEmpty(port)) + { + addresses.Add("http://localhost:" + port); + } - // Provide a default address if there aren't any configured. - if (addresses.Count == 0) - { - addresses.Add("http://localhost:5000"); + // Provide a default address if there aren't any configured. + if (addresses.Count == 0) + { + addresses.Add("http://localhost:5000"); + } } } - - var startupFilters = _applicationServices.GetService>(); - var configure = Startup.ConfigureDelegate; - foreach (var filter in startupFilters) - { - configure = filter.Configure(configure); - } - - configure(builder); - - return builder.Build(); } private string GetRequestIdentifier(HttpContext httpContext) diff --git a/src/Microsoft.AspNet.Hosting/Program.cs b/src/Microsoft.AspNet.Hosting/Program.cs index 4cde138e65..fe4b261ffd 100644 --- a/src/Microsoft.AspNet.Hosting/Program.cs +++ b/src/Microsoft.AspNet.Hosting/Program.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Hosting builder.AddCommandLine(args); var config = builder.Build(); - var host = new WebHostBuilder(_serviceProvider, config).Build(); + var host = new WebHostBuilder(_serviceProvider, config, captureStartupErrors: true).Build(); using (var app = host.Start()) { var hostingEnv = app.Services.GetRequiredService(); diff --git a/src/Microsoft.AspNet.Hosting/Startup/StartupExceptionPage.cs b/src/Microsoft.AspNet.Hosting/Startup/StartupExceptionPage.cs new file mode 100644 index 0000000000..70b6befd8d --- /dev/null +++ b/src/Microsoft.AspNet.Hosting/Startup/StartupExceptionPage.cs @@ -0,0 +1,520 @@ +// 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.Dnx.Runtime; +using Microsoft.Framework.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 + 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(); + for (; ex != null; ex = ex.InnerException) + { + list.Add(ex); + } + list.Reverse(); + return list; + } + } +} diff --git a/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs b/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs index 4d2bc9eebf..2563776356 100644 --- a/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs +++ b/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs @@ -39,6 +39,7 @@ namespace Microsoft.AspNet.Hosting private StartupMethods _startup; private Type _startupType; private string _startupAssemblyName; + private readonly bool _captureStartupErrors; // Only one of these should be set private string _serverFactoryLocation; @@ -50,6 +51,11 @@ namespace Microsoft.AspNet.Hosting } public WebHostBuilder(IServiceProvider services, IConfiguration config) + : this(services, config: config, captureStartupErrors: false) + { + } + + public WebHostBuilder(IServiceProvider services, IConfiguration config, bool captureStartupErrors) { if (services == null) { @@ -65,6 +71,7 @@ namespace Microsoft.AspNet.Hosting _loggerFactory = new LoggerFactory(); _services = services; _config = config; + _captureStartupErrors = captureStartupErrors; } private IServiceCollection BuildHostingServices() @@ -117,8 +124,7 @@ namespace Microsoft.AspNet.Hosting var startupLoader = hostingContainer.GetRequiredService(); _hostingEnvironment.Initialize(appEnvironment.ApplicationBasePath, _config?[EnvironmentKey] ?? _config?[OldEnvironmentKey]); - - var engine = new HostingEngine(hostingServices, startupLoader, _config); + var engine = new HostingEngine(hostingServices, startupLoader, _config, _captureStartupErrors); // Only one of these should be set, but they are used in priority engine.ServerFactory = _serverFactory; diff --git a/src/Microsoft.AspNet.Hosting/compiler/resources/Compilation_Exception.html b/src/Microsoft.AspNet.Hosting/compiler/resources/Compilation_Exception.html new file mode 100644 index 0000000000..4a4faf1d23 --- /dev/null +++ b/src/Microsoft.AspNet.Hosting/compiler/resources/Compilation_Exception.html @@ -0,0 +1,6 @@ +
+ One or more compilation errors occured:
+
+ {0} +
+
diff --git a/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError.html b/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError.html new file mode 100644 index 0000000000..e7b4dc1bbd --- /dev/null +++ b/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError.html @@ -0,0 +1,151 @@ + + + + + + 500 Internal Server Error + + + + + + + + [[[0]]] + + [[[1]]] + + [[[2]]] + + diff --git a/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Exception.html b/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Exception.html new file mode 100644 index 0000000000..012e05bee7 --- /dev/null +++ b/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Exception.html @@ -0,0 +1,8 @@ +
+ {0}
+ {1}
+ {2} +
+ {3} +
+
diff --git a/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Footer.html b/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Footer.html new file mode 100644 index 0000000000..54042e8bf9 --- /dev/null +++ b/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Footer.html @@ -0,0 +1,3 @@ +
+ .NET Framework {0} version {1}   |   DNX {2} version {3}   |   Microsoft.AspNet.Hosting version {4}   |   {5} {6}   |   Need help? +
diff --git a/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Message.html b/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Message.html new file mode 100644 index 0000000000..39a83d8754 --- /dev/null +++ b/src/Microsoft.AspNet.Hosting/compiler/resources/GenericError_Message.html @@ -0,0 +1,3 @@ +
+ {0}
+
diff --git a/src/Microsoft.AspNet.Hosting/project.json b/src/Microsoft.AspNet.Hosting/project.json index c8b50aad56..ac5468cff2 100644 --- a/src/Microsoft.AspNet.Hosting/project.json +++ b/src/Microsoft.AspNet.Hosting/project.json @@ -18,6 +18,7 @@ "Microsoft.Framework.Configuration.Json": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*", "Microsoft.Framework.Logging": "1.0.0-*", + "Microsoft.Dnx.Compilation.Abstractions": "1.0.0-*", "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-*", "Newtonsoft.Json": "6.0.6", "System.Diagnostics.Tracing.Telemetry": "4.0.0-beta-*" @@ -30,7 +31,8 @@ }, "dnxcore50": { "dependencies": { - "System.Console": "4.0.0-beta-*" + "System.Console": "4.0.0-beta-*", + "System.Diagnostics.StackTrace": "4.0.1-beta-*" } } } diff --git a/test/Microsoft.AspNet.Hosting.Tests/Fakes/RuntimeEnvironment.cs b/test/Microsoft.AspNet.Hosting.Tests/Fakes/RuntimeEnvironment.cs new file mode 100644 index 0000000000..276ba0ea8c --- /dev/null +++ b/test/Microsoft.AspNet.Hosting.Tests/Fakes/RuntimeEnvironment.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.Dnx.Runtime; + +namespace Microsoft.AspNet.Hosting.Fakes +{ + public class RuntimeEnvironment : IRuntimeEnvironment + { + public string OperatingSystem { get; } = "TestOs"; + + public string OperatingSystemVersion { get; } = "TestOsVersion"; + + public string RuntimeArchitecture { get; } = "TestArch"; + + public string RuntimeType { get; } = "TestRuntime"; + + public string RuntimeVersion { get; } = "TestRuntimeVersion"; + } +} diff --git a/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupConfigureServicesThrows.cs b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupConfigureServicesThrows.cs new file mode 100644 index 0000000000..b7b3cfcdef --- /dev/null +++ b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupConfigureServicesThrows.cs @@ -0,0 +1,22 @@ +// 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 Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Hosting.Fakes +{ + public class StartupConfigureServicesThrows + { + public void ConfigureServices(IServiceCollection services) + { + throw new Exception("Exception from ConfigureServices"); + } + + public void Configure(IApplicationBuilder builder) + { + + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupConfigureThrows.cs b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupConfigureThrows.cs new file mode 100644 index 0000000000..063404a4d1 --- /dev/null +++ b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupConfigureThrows.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Hosting.Fakes +{ + public class StartupConfigureThrows + { + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder builder) + { + throw new Exception("Exception from Configure"); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupCtorThrows.cs b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupCtorThrows.cs new file mode 100644 index 0000000000..b0a88f3a85 --- /dev/null +++ b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupCtorThrows.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.AspNet.Builder; + +namespace Microsoft.AspNet.Hosting.Fakes +{ + public class StartupCtorThrows + { + public StartupCtorThrows() + { + throw new Exception("Exception from constructor"); + } + + public void Configure(IApplicationBuilder app) + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupStaticCtorThrows.cs b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupStaticCtorThrows.cs new file mode 100644 index 0000000000..cd90f5181d --- /dev/null +++ b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupStaticCtorThrows.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.AspNet.Builder; + +namespace Microsoft.AspNet.Hosting.Fakes +{ + public class StartupStaticCtorThrows + { + static StartupStaticCtorThrows() + { + throw new Exception("Exception from static constructor"); + } + + public void Configure(IApplicationBuilder app) + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs b/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs index 21f58bddbc..8d1b812ad8 100644 --- a/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs +++ b/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs @@ -1,8 +1,17 @@ // 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.IO; +using System.Threading.Tasks; +using Microsoft.AspNet.Hosting.Fakes; using Microsoft.AspNet.Hosting.Internal; +using Microsoft.AspNet.Hosting.Server; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Http.Internal; using Microsoft.Dnx.Runtime.Infrastructure; +using Microsoft.Framework.Configuration; using Xunit; namespace Microsoft.AspNet.Hosting @@ -29,6 +38,110 @@ namespace Microsoft.AspNet.Hosting Assert.Equal("MyStartupAssembly", engine.StartupAssemblyName); } - private WebHostBuilder CreateWebHostBuilder() => new WebHostBuilder(CallContextServiceLocator.Locator.ServiceProvider); + [Fact] + public async Task StartupMissing_Fallback() + { + var builder = CreateWebHostBuilder(); + var serverFactory = new TestServerFactory(); + var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup("MissingStartupAssembly").Build(); + using (engine.Start()) + { + await AssertResponseContains(serverFactory.Application, "MissingStartupAssembly"); + } + } + + [Fact] + public async Task StartupStaticCtorThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var serverFactory = new TestServerFactory(); + var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup().Build(); + using (engine.Start()) + { + await AssertResponseContains(serverFactory.Application, "Exception from static constructor"); + } + } + + [Fact] + public async Task StartupCtorThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var serverFactory = new TestServerFactory(); + var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup().Build(); + using (engine.Start()) + { + await AssertResponseContains(serverFactory.Application, "Exception from constructor"); + } + } + + [Fact] + public async Task StartupConfigureServicesThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var serverFactory = new TestServerFactory(); + var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup().Build(); + using (engine.Start()) + { + await AssertResponseContains(serverFactory.Application, "Exception from ConfigureServices"); + } + } + + [Fact] + public async Task StartupConfigureThrows_Fallback() + { + var builder = CreateWebHostBuilder(); + var serverFactory = new TestServerFactory(); + var engine = (HostingEngine)builder.UseServer(serverFactory).UseStartup().Build(); + using (engine.Start()) + { + await AssertResponseContains(serverFactory.Application, "Exception from Configure"); + } + } + + private WebHostBuilder CreateWebHostBuilder() + { + var vals = new Dictionary + { + { "server", "Microsoft.AspNet.Hosting.Tests" }, + { "Hosting:DetailedErrors", "true" }, + }; + var builder = new ConfigurationBuilder() + .AddInMemoryCollection(vals); + var config = builder.Build(); + return new WebHostBuilder(CallContextServiceLocator.Locator.ServiceProvider, config, captureStartupErrors: true); + } + + private async Task AssertResponseContains(Func app, string expectedText) + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.Body = new MemoryStream(); + await app(httpContext.Features); + httpContext.Response.Body.Seek(0, SeekOrigin.Begin); + var bodyText = new StreamReader(httpContext.Response.Body).ReadToEnd(); + Assert.Contains(expectedText, bodyText); + } + + private class TestServerFactory : IServerFactory + { + public Func Application { get; set; } + + public IFeatureCollection Initialize(IConfiguration configuration) + { + return new FeatureCollection(); + } + + public IDisposable Start(IFeatureCollection serverFeatures, Func application) + { + Application = application; + return new Disposable(); + } + + private class Disposable : IDisposable + { + public void Dispose() + { + } + } + } } }