From b88b34c6e592229e092b873f2fc66b380a286c8d Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 11 Jun 2021 08:29:39 -0700 Subject: [PATCH] Follow up on HostFactoryResolver changes (#54052) - Cleaned up the comments to explain what ResolveHostFactory does. - Added entry point completed callback to let callers know when the entry point code is done running. - Added tests for the various callbacks. --- .../src/HostFactoryResolver.cs | 33 +++++++++-- .../tests/HostFactoryResolverTests.cs | 68 ++++++++++++++++++++++ ...oft.Extensions.HostFactoryResolver.Tests.csproj | 1 + ...SpecialEntryPointPatternBuildsThenThrows.csproj | 13 +++++ .../Program.cs | 18 ++++++ 5 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/NoSpecialEntryPointPatternBuildsThenThrows.csproj create mode 100644 src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/Program.cs diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs index 90c5dda..6992fd0 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs @@ -38,7 +38,17 @@ namespace Microsoft.Extensions.Hosting return ResolveFactory(assembly, CreateHostBuilder); } - public static Func? ResolveHostFactory(Assembly assembly, TimeSpan? waitTimeout = null, bool stopApplication = true, Action? configureHostBuilder = null) + // This helpers encapsulates all of the complex logic required to: + // 1. Execute the entry point of the specified assembly in a different thread. + // 2. Wait for the diagnostic source events to fire + // 3. Give the caller a chance to execute logic to mutate the IHostBuilder + // 4. Resolve the instance of the applications's IHost + // 5. Allow the caller to determine if the entry point has completed + public static Func? ResolveHostFactory(Assembly assembly, + TimeSpan? waitTimeout = null, + bool stopApplication = true, + Action? configureHostBuilder = null, + Action? entrypointCompleted = null) { if (assembly.EntryPoint is null) { @@ -48,7 +58,7 @@ namespace Microsoft.Extensions.Hosting try { // Attempt to load hosting and check the version to make sure the events - // even have a change of firing (they were adding in .NET >= 6) + // even have a chance of firing (they were added in .NET >= 6) var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting"); if (hostingAssembly.GetName().Version is Version version && version.Major < 6) { @@ -64,7 +74,7 @@ namespace Microsoft.Extensions.Hosting return null; } - return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder).CreateHost(); + return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost(); } private static Func? ResolveFactory(Assembly assembly, string name) @@ -169,14 +179,16 @@ namespace Microsoft.Extensions.Hosting private readonly TaskCompletionSource _hostTcs = new(); private IDisposable? _disposable; private Action? _configure; + private Action? _entrypointCompleted; - public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action? configure) + public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action? configure, Action? entrypointCompleted) { _args = args; _entryPoint = entryPoint; _waitTimeout = waitTimeout; _stopApplication = stopApplication; _configure = configure; + _entrypointCompleted = entrypointCompleted; } public object CreateHost() @@ -187,6 +199,8 @@ namespace Microsoft.Extensions.Hosting // in case we need to timeout the execution var thread = new Thread(() => { + Exception? exception = null; + try { var parameters = _entryPoint.GetParameters(); @@ -209,14 +223,23 @@ namespace Microsoft.Extensions.Hosting } catch (TargetInvocationException tie) { + exception = tie.InnerException ?? tie; + // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(tie.InnerException ?? tie); + _hostTcs.TrySetException(exception); } catch (Exception ex) { + exception = ex; + // Another exception happened, propagate that to the caller _hostTcs.TrySetException(ex); } + finally + { + // Signal that the entry point is completed + _entrypointCompleted?.Invoke(exception); + } }) { // Make sure this doesn't hang the process diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs index f68836a..6575392 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs @@ -5,6 +5,7 @@ using MockHostTypes; using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Threading; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -152,6 +153,73 @@ namespace Microsoft.Extensions.Hosting.Tests } [ConditionalFact(nameof(RequirementsMet))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))] + public void NoSpecialEntryPointPatternHostBuilderConfigureHostBuilderCallbackIsCalled() + { + using var _ = RemoteExecutor.Invoke(() => + { + bool called = false; + void ConfigureHostBuilder(object hostBuilder) + { + Assert.IsAssignableFrom(hostBuilder); + called = true; + } + + var factory = HostFactoryResolver.ResolveHostFactory(typeof(NoSpecialEntryPointPattern.Program).Assembly, waitTimeout: s_WaitTimeout, configureHostBuilder: ConfigureHostBuilder); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + Assert.True(called); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))] + public void NoSpecialEntryPointPatternBuildsThenThrowsCallsEntryPointCompletedCallback() + { + using var _ = RemoteExecutor.Invoke(() => + { + var wait = new ManualResetEventSlim(false); + Exception? entryPointException = null; + void EntryPointCompleted(Exception? exception) + { + entryPointException = exception; + wait.Set(); + } + + var factory = HostFactoryResolver.ResolveHostFactory(typeof(NoSpecialEntryPointPattern.Program).Assembly, waitTimeout: s_WaitTimeout, stopApplication: false, entrypointCompleted: EntryPointCompleted); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + Assert.True(wait.Wait(s_WaitTimeout)); + Assert.Null(entryPointException); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternBuildsThenThrows.Program))] + public void NoSpecialEntryPointPatternBuildsThenThrowsCallsEntryPointCompletedCallbackWithException() + { + using var _ = RemoteExecutor.Invoke(() => + { + var wait = new ManualResetEventSlim(false); + Exception? entryPointException = null; + void EntryPointCompleted(Exception? exception) + { + entryPointException = exception; + wait.Set(); + } + + var factory = HostFactoryResolver.ResolveHostFactory(typeof(NoSpecialEntryPointPatternBuildsThenThrows.Program).Assembly, waitTimeout: s_WaitTimeout, stopApplication: false, entrypointCompleted: EntryPointCompleted); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + Assert.True(wait.Wait(s_WaitTimeout)); + Assert.NotNull(entryPointException); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternThrows.Program))] public void NoSpecialEntryPointPatternThrows() { diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj index d065e56..070ac75 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/NoSpecialEntryPointPatternBuildsThenThrows.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/NoSpecialEntryPointPatternBuildsThenThrows.csproj new file mode 100644 index 0000000..716b25a --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/NoSpecialEntryPointPatternBuildsThenThrows.csproj @@ -0,0 +1,13 @@ + + + + $(NetCoreAppCurrent);net461 + true + Exe + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/Program.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/Program.cs new file mode 100644 index 0000000..9e889d8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/Program.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Hosting; + +namespace NoSpecialEntryPointPatternBuildsThenThrows +{ + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder().Build(); + + throw new Exception("Main just throws"); + } + } +} -- 2.7.4