Follow up on HostFactoryResolver changes (#54052)
authorDavid Fowler <davidfowl@gmail.com>
Fri, 11 Jun 2021 15:29:39 +0000 (08:29 -0700)
committerGitHub <noreply@github.com>
Fri, 11 Jun 2021 15:29:39 +0000 (08:29 -0700)
- 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/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs
src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs
src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj
src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/NoSpecialEntryPointPatternBuildsThenThrows.csproj [new file with mode: 0644]
src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternBuildsThenThrows/Program.cs [new file with mode: 0644]

index 90c5dda..6992fd0 100644 (file)
@@ -38,7 +38,17 @@ namespace Microsoft.Extensions.Hosting
             return ResolveFactory<THostBuilder>(assembly, CreateHostBuilder);
         }
 
-        public static Func<string[], object>? ResolveHostFactory(Assembly assembly, TimeSpan? waitTimeout = null, bool stopApplication = true, Action<object>? 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<string[], object>? ResolveHostFactory(Assembly assembly, 
+                                                                 TimeSpan? waitTimeout = null, 
+                                                                 bool stopApplication = true, 
+                                                                 Action<object>? configureHostBuilder = null, 
+                                                                 Action<Exception?>? 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<string[], T>? ResolveFactory<T>(Assembly assembly, string name)
@@ -169,14 +179,16 @@ namespace Microsoft.Extensions.Hosting
             private readonly TaskCompletionSource<object> _hostTcs = new();
             private IDisposable? _disposable;
             private Action<object>? _configure;
+            private Action<Exception?>? _entrypointCompleted;
 
-            public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action<object>? configure)
+            public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action<object>? configure, Action<Exception?>? 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
index f68836a..6575392 100644 (file)
@@ -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<IHostBuilder>(hostBuilder);
+                    called = true;
+                }
+
+                var factory = HostFactoryResolver.ResolveHostFactory(typeof(NoSpecialEntryPointPattern.Program).Assembly, waitTimeout: s_WaitTimeout, configureHostBuilder: ConfigureHostBuilder);
+
+                Assert.NotNull(factory);
+                Assert.IsAssignableFrom<IHost>(factory(Array.Empty<string>()));
+                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<IHost>(factory(Array.Empty<string>()));
+                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<IHost>(factory(Array.Empty<string>()));
+                Assert.True(wait.Wait(s_WaitTimeout));
+                Assert.NotNull(entryPointException);
+            });
+        }
+
+        [ConditionalFact(nameof(RequirementsMet))]
         [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternThrows.Program))]
         public void NoSpecialEntryPointPatternThrows()
         {
index d065e56..070ac75 100644 (file)
@@ -22,6 +22,7 @@
     <ProjectReference Include="CreateWebHostBuilderInvalidSignature\CreateWebHostBuilderInvalidSignature.csproj" />
     <ProjectReference Include="CreateWebHostBuilderPatternTestSite\CreateWebHostBuilderPatternTestSite.csproj" />
     <ProjectReference Include="NoSpecialEntryPointPattern\NoSpecialEntryPointPattern.csproj" />
+    <ProjectReference Include="NoSpecialEntryPointPatternBuildsThenThrows\NoSpecialEntryPointPatternBuildsThenThrows.csproj" />
     <ProjectReference Include="NoSpecialEntryPointPatternThrows\NoSpecialEntryPointPatternThrows.csproj" />
     <ProjectReference Include="NoSpecialEntryPointPatternExits\NoSpecialEntryPointPatternExits.csproj" />
     <ProjectReference Include="NoSpecialEntryPointPatternHangs\NoSpecialEntryPointPatternHangs.csproj" />
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 (file)
index 0000000..716b25a
--- /dev/null
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFrameworks>$(NetCoreAppCurrent);net461</TargetFrameworks>
+    <EnableDefaultItems>true</EnableDefaultItems>
+    <OutputType>Exe</OutputType>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\MockHostTypes\MockHostTypes.csproj" />
+  </ItemGroup>
+
+</Project>
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 (file)
index 0000000..9e889d8
--- /dev/null
@@ -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");
+        }
+    }
+}