Support async main (#17673)
This commit is contained in:
parent
3fba107522
commit
3a93704737
|
|
@ -0,0 +1,89 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||||
|
{
|
||||||
|
internal static class EntrypointInvoker
|
||||||
|
{
|
||||||
|
// This method returns void because currently the JS side is not listening to any result,
|
||||||
|
// nor will it handle any exceptions. We handle all exceptions internally to this method.
|
||||||
|
// In the future we may want Blazor.start to return something that exposes the possibly-async
|
||||||
|
// entrypoint result to the JS caller. There's no requirement to do that today, and if we
|
||||||
|
// do change this it will be non-breaking.
|
||||||
|
public static void InvokeEntrypoint(string assemblyName, string[] args)
|
||||||
|
{
|
||||||
|
object entrypointResult;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.Load(assemblyName);
|
||||||
|
var entrypoint = FindUnderlyingEntrypoint(assembly);
|
||||||
|
var @params = entrypoint.GetParameters().Length == 1 ? new object[] { args } : new object[] { };
|
||||||
|
entrypointResult = entrypoint.Invoke(null, @params);
|
||||||
|
}
|
||||||
|
catch (Exception syncException)
|
||||||
|
{
|
||||||
|
HandleStartupException(syncException);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the entrypoint is async, handle async exceptions in the same way that we would
|
||||||
|
// have handled sync ones
|
||||||
|
if (entrypointResult is Task entrypointTask)
|
||||||
|
{
|
||||||
|
entrypointTask.ContinueWith(task =>
|
||||||
|
{
|
||||||
|
if (task.Exception != null)
|
||||||
|
{
|
||||||
|
HandleStartupException(task.Exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodBase FindUnderlyingEntrypoint(Assembly assembly)
|
||||||
|
{
|
||||||
|
// This is the entrypoint declared in .NET metadata. In the case of async main, it's the
|
||||||
|
// compiler-generated wrapper method. Otherwise it's the developer-defined method.
|
||||||
|
var metadataEntrypointMethodBase = assembly.EntryPoint;
|
||||||
|
|
||||||
|
// For "async Task Main", the C# compiler generates a method called "<Main>"
|
||||||
|
// that is marked as the assembly entrypoint. Detect this case, and instead of
|
||||||
|
// calling "<Whatever>", call the sibling "Whatever".
|
||||||
|
if (metadataEntrypointMethodBase.IsSpecialName)
|
||||||
|
{
|
||||||
|
var origName = metadataEntrypointMethodBase.Name;
|
||||||
|
var origNameLength = origName.Length;
|
||||||
|
if (origNameLength > 2)
|
||||||
|
{
|
||||||
|
var candidateMethodName = origName.Substring(1, origNameLength - 2);
|
||||||
|
var candidateMethod = metadataEntrypointMethodBase.DeclaringType.GetMethod(
|
||||||
|
candidateMethodName,
|
||||||
|
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
|
||||||
|
null,
|
||||||
|
metadataEntrypointMethodBase.GetParameters().Select(p => p.ParameterType).ToArray(),
|
||||||
|
null);
|
||||||
|
|
||||||
|
if (candidateMethod != null)
|
||||||
|
{
|
||||||
|
return candidateMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either it's not async main, or for some reason we couldn't locate the underlying entrypoint,
|
||||||
|
// so use the one from assembly metadata.
|
||||||
|
return metadataEntrypointMethodBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HandleStartupException(Exception exception)
|
||||||
|
{
|
||||||
|
// Logs to console, and causes the error UI to appear
|
||||||
|
Console.Error.WriteLine(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||||
|
{
|
||||||
|
public class EntrypointInvokerTest
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false, false)]
|
||||||
|
[InlineData(false, true)]
|
||||||
|
[InlineData(true, false)]
|
||||||
|
[InlineData(true, true)]
|
||||||
|
public void InvokesEntrypoint_Sync_Success(bool hasReturnValue, bool hasParams)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var returnType = hasReturnValue ? "int" : "void";
|
||||||
|
var paramsDecl = hasParams ? "string[] args" : string.Empty;
|
||||||
|
var returnStatement = hasReturnValue ? "return 123;" : "return;";
|
||||||
|
var assembly = CompileToAssembly(@"
|
||||||
|
static " + returnType + @" Main(" + paramsDecl + @")
|
||||||
|
{
|
||||||
|
DidMainExecute = true;
|
||||||
|
" + returnStatement + @"
|
||||||
|
}", out var didMainExecute);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(didMainExecute());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false, false)]
|
||||||
|
[InlineData(false, true)]
|
||||||
|
[InlineData(true, false)]
|
||||||
|
[InlineData(true, true)]
|
||||||
|
public void InvokesEntrypoint_Async_Success(bool hasReturnValue, bool hasParams)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var returnTypeGenericParam = hasReturnValue ? "<int>" : string.Empty;
|
||||||
|
var paramsDecl = hasParams ? "string[] args" : string.Empty;
|
||||||
|
var returnStatement = hasReturnValue ? "return 123;" : "return;";
|
||||||
|
var assembly = CompileToAssembly(@"
|
||||||
|
public static TaskCompletionSource<object> ContinueTcs { get; } = new TaskCompletionSource<object>();
|
||||||
|
|
||||||
|
static async Task" + returnTypeGenericParam + @" Main(" + paramsDecl + @")
|
||||||
|
{
|
||||||
|
await ContinueTcs.Task;
|
||||||
|
DidMainExecute = true;
|
||||||
|
" + returnStatement + @"
|
||||||
|
}", out var didMainExecute);
|
||||||
|
|
||||||
|
// Act/Assert 1: Waits for task
|
||||||
|
// The fact that we're not blocking here proves that we're not executing the
|
||||||
|
// metadata-declared entrypoint, as that would block
|
||||||
|
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||||
|
Assert.False(didMainExecute());
|
||||||
|
|
||||||
|
// Act/Assert 2: Continues
|
||||||
|
var tcs = (TaskCompletionSource<object>)assembly.GetType("SomeApp.Program").GetProperty("ContinueTcs").GetValue(null);
|
||||||
|
tcs.SetResult(null);
|
||||||
|
Assert.True(didMainExecute());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvokesEntrypoint_Sync_Exception()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var assembly = CompileToAssembly(@"
|
||||||
|
public static void Main()
|
||||||
|
{
|
||||||
|
DidMainExecute = true;
|
||||||
|
throw new InvalidTimeZoneException(""Test message"");
|
||||||
|
}", out var didMainExecute);
|
||||||
|
|
||||||
|
// Act/Assert
|
||||||
|
// The fact that this doesn't throw shows that EntrypointInvoker is doing something
|
||||||
|
// to handle the exception. We can't assert about what it does here, because that
|
||||||
|
// would involve capturing console output, which isn't safe in unit tests. Instead
|
||||||
|
// we'll check this in E2E tests.
|
||||||
|
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||||
|
Assert.True(didMainExecute());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvokesEntrypoint_Async_Exception()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var assembly = CompileToAssembly(@"
|
||||||
|
public static TaskCompletionSource<object> ContinueTcs { get; } = new TaskCompletionSource<object>();
|
||||||
|
|
||||||
|
public static async Task Main()
|
||||||
|
{
|
||||||
|
await ContinueTcs.Task;
|
||||||
|
DidMainExecute = true;
|
||||||
|
throw new InvalidTimeZoneException(""Test message"");
|
||||||
|
}", out var didMainExecute);
|
||||||
|
|
||||||
|
// Act/Assert 1: Waits for task
|
||||||
|
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||||
|
Assert.False(didMainExecute());
|
||||||
|
|
||||||
|
// Act/Assert 2: Continues
|
||||||
|
// As above, we can't directly observe the exception handling behavior here,
|
||||||
|
// so this is covered in E2E tests instead.
|
||||||
|
var tcs = (TaskCompletionSource<object>)assembly.GetType("SomeApp.Program").GetProperty("ContinueTcs").GetValue(null);
|
||||||
|
tcs.SetResult(null);
|
||||||
|
Assert.True(didMainExecute());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Assembly CompileToAssembly(string mainMethod, out Func<bool> didMainExecute)
|
||||||
|
{
|
||||||
|
var syntaxTree = CSharpSyntaxTree.ParseText(@"
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace SomeApp
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static bool DidMainExecute { get; private set; }
|
||||||
|
|
||||||
|
" + mainMethod + @"
|
||||||
|
}
|
||||||
|
}");
|
||||||
|
|
||||||
|
var compilation = CSharpCompilation.Create(
|
||||||
|
$"TestAssembly-{Guid.NewGuid().ToString("D")}",
|
||||||
|
new[] { syntaxTree },
|
||||||
|
new[] { MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) },
|
||||||
|
new CSharpCompilationOptions(OutputKind.ConsoleApplication));
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
var compilationResult = compilation.Emit(ms);
|
||||||
|
ms.Seek(0, SeekOrigin.Begin);
|
||||||
|
var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
|
||||||
|
|
||||||
|
var didMainExecuteProp = assembly.GetType("SomeApp.Program").GetProperty("DidMainExecute");
|
||||||
|
didMainExecute = () => (bool)didMainExecuteProp.GetValue(null);
|
||||||
|
|
||||||
|
return assembly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Blazor" />
|
<Reference Include="Microsoft.AspNetCore.Blazor" />
|
||||||
|
<Reference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
@ -11,6 +11,12 @@
|
||||||
<body>
|
<body>
|
||||||
<app>Loading...</app>
|
<app>Loading...</app>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled exception has occurred. See browser dev tools for details.
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">🗙</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mono.WebAssembly.Interop",
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "Blazor\Mono.WebAssembly.Interop\src\Mono.WebAssembly.Interop.csproj", "{D141CFEE-D10A-406B-8963-F86FA13732E3}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "Blazor\Mono.WebAssembly.Interop\src\Mono.WebAssembly.Interop.csproj", "{D141CFEE-D10A-406B-8963-F86FA13732E3}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentsApp.Server", "test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{F2E27E1C-2E47-42C1-9AC7-36265A381717}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -1518,6 +1520,18 @@ Global
|
||||||
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x64.Build.0 = Release|Any CPU
|
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x86.ActiveCfg = Release|Any CPU
|
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x86.Build.0 = Release|Any CPU
|
{D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -1632,6 +1646,7 @@ Global
|
||||||
{A5617A9D-C71E-44DE-936C-27611EB40A02} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
{A5617A9D-C71E-44DE-936C-27611EB40A02} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
||||||
{21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
{21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
|
||||||
{D141CFEE-D10A-406B-8963-F86FA13732E3} = {21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05}
|
{D141CFEE-D10A-406B-8963-F86FA13732E3} = {21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05}
|
||||||
|
{F2E27E1C-2E47-42C1-9AC7-36265A381717} = {44E0D4F3-4430-4175-B482-0D1AEE4BB699}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE}
|
SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE}
|
||||||
|
|
|
||||||
|
|
@ -73,5 +73,7 @@ interface BootJsonData {
|
||||||
|
|
||||||
window['Blazor'].start = boot;
|
window['Blazor'].start = boot;
|
||||||
if (shouldAutoStart()) {
|
if (shouldAutoStart()) {
|
||||||
boot();
|
boot().catch(error => {
|
||||||
|
Module.printErr(error); // Logs it, and causes the error UI to appear
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ let assembly_load: (assemblyName: string) => number;
|
||||||
let find_class: (assemblyHandle: number, namespace: string, className: string) => number;
|
let find_class: (assemblyHandle: number, namespace: string, className: string) => number;
|
||||||
let find_method: (typeHandle: number, methodName: string, unknownArg: number) => MethodHandle;
|
let find_method: (typeHandle: number, methodName: string, unknownArg: number) => MethodHandle;
|
||||||
let invoke_method: (method: MethodHandle, target: System_Object, argsArrayPtr: number, exceptionFlagIntPtr: number) => System_Object;
|
let invoke_method: (method: MethodHandle, target: System_Object, argsArrayPtr: number, exceptionFlagIntPtr: number) => System_Object;
|
||||||
let mono_call_assembly_entry_point: (assemblyName: string, args: System_Object[]) => System_Object;
|
let mono_string_array_new: (length: number) => System_Array<System_String>;
|
||||||
let mono_obj_array_new: (length: number) => System_Object;
|
|
||||||
let mono_string_get_utf8: (managedString: System_String) => Mono.Utf8Ptr;
|
let mono_string_get_utf8: (managedString: System_String) => Mono.Utf8Ptr;
|
||||||
let mono_string: (jsString: string) => System_String;
|
let mono_string: (jsString: string) => System_String;
|
||||||
const appBinDirName = 'appBinDir';
|
const appBinDirName = 'appBinDir';
|
||||||
|
|
@ -41,9 +40,18 @@ export const monoPlatform: Platform = {
|
||||||
|
|
||||||
findMethod: findMethod,
|
findMethod: findMethod,
|
||||||
|
|
||||||
callEntryPoint: function callEntryPoint(assemblyName: string): System_Object {
|
callEntryPoint: function callEntryPoint(assemblyName: string) {
|
||||||
const empty_array = mono_obj_array_new(0);
|
// Instead of using Module.mono_call_assembly_entry_point, we have our own logic for invoking
|
||||||
return mono_call_assembly_entry_point(assemblyName, [empty_array]);
|
// the entrypoint which adds support for async main.
|
||||||
|
// Currently we disregard the return value from the entrypoint, whether it's sync or async.
|
||||||
|
// In the future, we might want Blazor.start to return a Promise<Promise<value>>, where the
|
||||||
|
// outer promise reflects the startup process, and the inner one reflects the possibly-async
|
||||||
|
// .NET entrypoint method.
|
||||||
|
const invokeEntrypoint = findMethod('Microsoft.AspNetCore.Blazor', 'Microsoft.AspNetCore.Blazor.Hosting', 'EntrypointInvoker', 'InvokeEntrypoint');
|
||||||
|
this.callMethod(invokeEntrypoint, null, [
|
||||||
|
this.toDotNetString(assemblyName),
|
||||||
|
mono_string_array_new(0) // In the future, we may have a way of supplying arg strings. For now, we always supply an empty string[].
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
callMethod: function callMethod(method: MethodHandle, target: System_Object, args: System_Object[]): System_Object {
|
callMethod: function callMethod(method: MethodHandle, target: System_Object, args: System_Object[]): System_Object {
|
||||||
|
|
@ -263,11 +271,9 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: ()
|
||||||
'number',
|
'number',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mono_call_assembly_entry_point = Module.mono_call_assembly_entry_point;
|
|
||||||
|
|
||||||
mono_string_get_utf8 = Module.cwrap('mono_wasm_string_get_utf8', 'number', ['number']);
|
mono_string_get_utf8 = Module.cwrap('mono_wasm_string_get_utf8', 'number', ['number']);
|
||||||
mono_string = Module.cwrap('mono_wasm_string_from_js', 'number', ['string']);
|
mono_string = Module.cwrap('mono_wasm_string_from_js', 'number', ['string']);
|
||||||
mono_obj_array_new = Module.cwrap ('mono_wasm_obj_array_new', 'number', ['number']);
|
mono_string_array_new = Module.cwrap('mono_wasm_string_array_new', 'number', ['number']);
|
||||||
|
|
||||||
MONO.loaded_files = [];
|
MONO.loaded_files = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export interface Platform {
|
export interface Platform {
|
||||||
start(loadAssemblyUrls: string[]): Promise<void>;
|
start(loadAssemblyUrls: string[]): Promise<void>;
|
||||||
|
|
||||||
callEntryPoint(assemblyName: string): System_Object;
|
callEntryPoint(assemblyName: string): void;
|
||||||
findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle;
|
findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle;
|
||||||
callMethod(method: MethodHandle, target: System_Object | null, args: (System_Object | null)[]): System_Object;
|
callMethod(method: MethodHandle, target: System_Object | null, args: (System_Object | null)[]): System_Object;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,15 @@ using BasicTestApp;
|
||||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||||
using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;
|
using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;
|
||||||
using Microsoft.AspNetCore.E2ETesting;
|
using Microsoft.AspNetCore.E2ETesting;
|
||||||
using OpenQA.Selenium;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
{
|
{
|
||||||
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
|
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
|
||||||
public class ErrorNotificationServerSideTest : ErrorNotificationClientSideTest
|
public class ServerErrorNotificationTest : ErrorNotificationTest
|
||||||
{
|
{
|
||||||
public ErrorNotificationServerSideTest(
|
public ServerErrorNotificationTest(
|
||||||
BrowserFixture browserFixture,
|
BrowserFixture browserFixture,
|
||||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||||
ITestOutputHelper output)
|
ITestOutputHelper output)
|
||||||
|
|
@ -13,9 +13,9 @@ using Xunit.Abstractions;
|
||||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
{
|
{
|
||||||
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
|
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
|
||||||
public class ErrorNotificationClientSideTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
public class ErrorNotificationTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
||||||
{
|
{
|
||||||
public ErrorNotificationClientSideTest(
|
public ErrorNotificationTest(
|
||||||
BrowserFixture browserFixture,
|
BrowserFixture browserFixture,
|
||||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||||
ITestOutputHelper output)
|
ITestOutputHelper output)
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// 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 BasicTestApp;
|
||||||
|
using Microsoft.AspNetCore.E2ETesting;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
|
{
|
||||||
|
public class StartupErrorNotificationTest : ServerTestBase<DevHostServerFixture<Program>>
|
||||||
|
{
|
||||||
|
public StartupErrorNotificationTest(
|
||||||
|
BrowserFixture browserFixture,
|
||||||
|
DevHostServerFixture<Program> serverFixture,
|
||||||
|
ITestOutputHelper output)
|
||||||
|
: base(browserFixture, serverFixture, output)
|
||||||
|
{
|
||||||
|
_serverFixture.PathBase = ServerPathBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public void DisplaysNotificationForStartupException(bool errorIsAsync)
|
||||||
|
{
|
||||||
|
var url = $"{ServerPathBase}?error={(errorIsAsync ? "async" : "sync")}";
|
||||||
|
|
||||||
|
Navigate(url, noReload: true);
|
||||||
|
var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10));
|
||||||
|
Assert.NotNull(errorUiElem);
|
||||||
|
|
||||||
|
Browser.Equal("block", () => errorUiElem.GetCssValue("display"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,12 +87,6 @@
|
||||||
@((RenderFragment)RenderSelectedComponent)
|
@((RenderFragment)RenderSelectedComponent)
|
||||||
</app>
|
</app>
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
|
||||||
An unhandled error has occurred.
|
|
||||||
<a href class='reload'>Reload</a>
|
|
||||||
<a class='dismiss' style="cursor: pointer;">🗙</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
string SelectedComponentTypeName { get; set; } = "none";
|
string SelectedComponentTypeName { get; set; } = "none";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Blazor.Hosting;
|
using Microsoft.AspNetCore.Blazor.Hosting;
|
||||||
|
using Mono.WebAssembly.Interop;
|
||||||
|
|
||||||
namespace BasicTestApp
|
namespace BasicTestApp
|
||||||
{
|
{
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
|
await SimulateErrorsIfNeededForTest();
|
||||||
|
|
||||||
// We want the culture to be en-US so that the tests for bind can work consistently.
|
// We want the culture to be en-US so that the tests for bind can work consistently.
|
||||||
CultureInfo.CurrentCulture = new CultureInfo("en-US");
|
CultureInfo.CurrentCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
|
|
@ -19,5 +24,22 @@ namespace BasicTestApp
|
||||||
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
|
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
BlazorWebAssemblyHost.CreateDefaultBuilder()
|
BlazorWebAssemblyHost.CreateDefaultBuilder()
|
||||||
.UseBlazorStartup<Startup>();
|
.UseBlazorStartup<Startup>();
|
||||||
|
|
||||||
|
// Supports E2E tests in StartupErrorNotificationTest
|
||||||
|
private static async Task SimulateErrorsIfNeededForTest()
|
||||||
|
{
|
||||||
|
var currentUrl = new MonoWebAssemblyJSRuntime().Invoke<string>("getCurrentUrl");
|
||||||
|
if (currentUrl.Contains("error=sync"))
|
||||||
|
{
|
||||||
|
throw new InvalidTimeZoneException("This is a synchronous startup exception");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
if (currentUrl.Contains("error=async"))
|
||||||
|
{
|
||||||
|
throw new InvalidTimeZoneException("This is an asynchronous startup exception");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,13 @@
|
||||||
<body>
|
<body>
|
||||||
<root>Loading...</root>
|
<root>Loading...</root>
|
||||||
|
|
||||||
|
<!-- Explicit display:none required so StartupErrorNotificationTest can observe it change -->
|
||||||
|
<div id="blazor-error-ui" style="display: none;">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href class='reload'>Reload</a>
|
||||||
|
<a class='dismiss' style="cursor: pointer;">🗙</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Used for testing interop scenarios between JS and .NET -->
|
<!-- Used for testing interop scenarios between JS and .NET -->
|
||||||
<script src="js/jsinteroptests.js"></script>
|
<script src="js/jsinteroptests.js"></script>
|
||||||
|
|
||||||
|
|
@ -27,6 +34,10 @@
|
||||||
function navigationManagerNavigate() {
|
function navigationManagerNavigate() {
|
||||||
Blazor.navigateTo('/subdir/some-path');
|
Blazor.navigateTo('/subdir/some-path');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentUrl() {
|
||||||
|
return location.href;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#blazor-error-ui {
|
#blazor-error-ui {
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
display: none;
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#blazor-error-ui dismiss {
|
#blazor-error-ui .dismiss {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.validation-message {
|
.validation-message {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,12 @@
|
||||||
<!-- Used for testing interop scenarios between JS and .NET -->
|
<!-- Used for testing interop scenarios between JS and .NET -->
|
||||||
<script src="js/jsinteroptests.js"></script>
|
<script src="js/jsinteroptests.js"></script>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href class='reload'>Reload</a>
|
||||||
|
<a class='dismiss' style="cursor: pointer;">🗙</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Used by ElementRefComponent
|
// Used by ElementRefComponent
|
||||||
function setElementValue(element, newValue) {
|
function setElementValue(element, newValue) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue