未验证 提交 543a9836 编写于 作者: D David Fowler 提交者: GitHub

Fire diagnostic source events from IHostBuilder.Build (#53757)

* Fire diagnostic source events from IHostBuilder.Build
- We want to enable getting access to the IHostBuilder and IHost during the call to IHostBuilder.Build so that we can access the application's IServiceProvider from various tools. Usually, this is enabled by exposing a different entry point from the application but this technique allows us to let the application execute normally and hook important events via a diagnostic source.
- Add support for the new pattern in HostFactoryResolver
- Added support for resolving an IServiceProvider using the new diagnostics source pattern (which doesn't require a different entrypoint)
- Detect the version of Microsoft.Extensions.Hosting before waiting for events to fire
- We want to fail fast if we know this version of hosting will never fire events of if the hosting assembly fails to load. Added a version check.
- Allow the caller to specify a wait timeout.
-  Added a flag to allow running the application to completion
上级 9af4b97f
......@@ -2,7 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
#nullable enable
......@@ -16,6 +22,9 @@ internal sealed class HostFactoryResolver
public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder);
public const string CreateHostBuilder = nameof(CreateHostBuilder);
// The amount of time we wait for the diagnostic source events to fire
private static readonly TimeSpan s_defaultWaitTimeout = TimeSpan.FromSeconds(5);
public static Func<string[], TWebHost>? ResolveWebHostFactory<TWebHost>(Assembly assembly)
{
return ResolveFactory<TWebHost>(assembly, BuildWebHost);
......@@ -31,6 +40,35 @@ internal sealed class HostFactoryResolver
return ResolveFactory<THostBuilder>(assembly, CreateHostBuilder);
}
public static Func<string[], object>? ResolveHostFactory(Assembly assembly, TimeSpan? waitTimeout = null, bool stopApplication = true, Action<object>? configureHostBuilder = null)
{
if (assembly.EntryPoint is null)
{
return null;
}
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)
var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting");
if (hostingAssembly.GetName().Version is Version version && version.Major < 6)
{
return null;
}
// We're using a version >= 6 so the events can fire. If they don't fire
// then it's because the application isn't using the hosting APIs
}
catch
{
// There was an error loading the extensions assembly, return null.
return null;
}
return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder).CreateHost();
}
private static Func<string[], T>? ResolveFactory<T>(Assembly assembly, string name)
{
var programType = assembly?.EntryPoint?.DeclaringType;
......@@ -58,7 +96,7 @@ private static bool IsFactory<TReturn>(MethodInfo? factory)
}
// Used by EF tooling without any Hosting references. Looses some return type safety checks.
public static Func<string[], IServiceProvider?>? ResolveServiceProviderFactory(Assembly assembly)
public static Func<string[], IServiceProvider?>? ResolveServiceProviderFactory(Assembly assembly, TimeSpan? waitTimeout = null)
{
// Prefer the older patterns by default for back compat.
var webHostFactory = ResolveWebHostFactory<object>(assembly);
......@@ -93,6 +131,16 @@ private static bool IsFactory<TReturn>(MethodInfo? factory)
};
}
var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout);
if (hostFactory != null)
{
return args =>
{
var host = hostFactory(args);
return GetServiceProvider(host);
};
}
return null;
}
......@@ -112,5 +160,133 @@ private static bool IsFactory<TReturn>(MethodInfo? factory)
var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup);
return (IServiceProvider?)servicesProperty?.GetValue(host);
}
private class HostingListener : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object?>>
{
private readonly string[] _args;
private readonly MethodInfo _entryPoint;
private readonly TimeSpan _waitTimeout;
private readonly bool _stopApplication;
private readonly TaskCompletionSource<object> _hostTcs = new();
private IDisposable? _disposable;
private Action<object>? _configure;
public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action<object>? configure)
{
_args = args;
_entryPoint = entryPoint;
_waitTimeout = waitTimeout;
_stopApplication = stopApplication;
_configure = configure;
}
public object CreateHost()
{
using var subscription = DiagnosticListener.AllListeners.Subscribe(this);
// Kick off the entry point on a new thread so we don't block the current one
// in case we need to timeout the execution
var thread = new Thread(() =>
{
try
{
var parameters = _entryPoint.GetParameters();
if (parameters.Length == 0)
{
_entryPoint.Invoke(null, Array.Empty<object>());
}
else
{
_entryPoint.Invoke(null, new object[] { _args });
}
// Try to set an exception if the entry point returns gracefully, this will force
// build to throw
_hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost"));
}
catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException)
{
// The host was stopped by our own logic
}
catch (TargetInvocationException tie)
{
// Another exception happened, propagate that to the caller
_hostTcs.TrySetException(tie.InnerException ?? tie);
}
catch (Exception ex)
{
// Another exception happened, propagate that to the caller
_hostTcs.TrySetException(ex);
}
})
{
// Make sure this doesn't hang the process
IsBackground = true
};
// Start the thread
thread.Start();
try
{
// Wait before throwing an exception
if (!_hostTcs.Task.Wait(_waitTimeout))
{
throw new InvalidOperationException("Unable to build IHost");
}
}
catch (AggregateException) when (_hostTcs.Task.IsCompleted)
{
// Lets this propagate out of the call to GetAwaiter().GetResult()
}
Debug.Assert(_hostTcs.Task.IsCompleted);
return _hostTcs.Task.GetAwaiter().GetResult();
}
public void OnCompleted()
{
_disposable?.Dispose();
}
public void OnError(Exception error)
{
}
public void OnNext(DiagnosticListener value)
{
if (value.Name == "Microsoft.Extensions.Hosting")
{
_disposable = value.Subscribe(this);
}
}
public void OnNext(KeyValuePair<string, object?> value)
{
if (value.Key == "HostBuilding")
{
_configure?.Invoke(value.Value!);
}
if (value.Key == "HostBuilt")
{
_hostTcs.TrySetResult(value.Value!);
if (_stopApplication)
{
// Stop the host from running further
throw new StopTheHostException();
}
}
}
private class StopTheHostException : Exception
{
}
}
}
}
......@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using MockHostTypes;
using Microsoft.Extensions.Hosting;
namespace CreateHostBuilderInvalidSignature
{
......@@ -9,7 +10,7 @@ public class Program
{
public static void Main(string[] args)
{
var webHost = CreateHostBuilder(null, args).Build();
var webHost = CreateHostBuilder(null, args)?.Build();
}
// Extra parameter
......
......@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using MockHostTypes;
using Microsoft.Extensions.Hosting;
namespace CreateHostBuilderPatternTestSite
{
......
......@@ -4,12 +4,18 @@
using MockHostTypes;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;
namespace Microsoft.Extensions.Hosting.Tests
{
public class HostFactoryResolverTests
{
public static bool RequirementsMet => RemoteExecutor.IsSupported && PlatformDetection.IsThreadingSupported;
private static readonly TimeSpan s_WaitTimeout = TimeSpan.FromSeconds(20);
[Fact]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BuildWebHostPatternTestSite.Program))]
public void BuildWebHostPattern_CanFindWebHost()
......@@ -46,7 +52,7 @@ public void BuildWebHostPattern__Invalid_CantFindServiceProvider()
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(BuildWebHostInvalidSignature.Program).Assembly);
Assert.Null(factory);
Assert.NotNull(factory);
}
[Fact]
......@@ -119,13 +125,95 @@ public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder()
Assert.Null(factory);
}
[Fact]
[ConditionalFact(nameof(RequirementsMet))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderInvalidSignature.Program))]
public void CreateHostBuilderPattern__Invalid_CantFindServiceProvider()
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly);
using var _ = RemoteExecutor.Invoke(() =>
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly, s_WaitTimeout);
Assert.Null(factory);
Assert.NotNull(factory);
Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
});
}
[ConditionalFact(nameof(RequirementsMet))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))]
public void NoSpecialEntryPointPattern()
{
using var _ = RemoteExecutor.Invoke(() =>
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPattern.Program).Assembly, s_WaitTimeout);
Assert.NotNull(factory);
Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
});
}
[ConditionalFact(nameof(RequirementsMet))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternThrows.Program))]
public void NoSpecialEntryPointPatternThrows()
{
using var _ = RemoteExecutor.Invoke(() =>
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPatternThrows.Program).Assembly, s_WaitTimeout);
Assert.NotNull(factory);
Assert.Throws<Exception>(() => factory(Array.Empty<string>()));
});
}
[ConditionalFact(nameof(RequirementsMet))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternExits.Program))]
public void NoSpecialEntryPointPatternExits()
{
using var _ = RemoteExecutor.Invoke(() =>
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPatternExits.Program).Assembly, s_WaitTimeout);
Assert.NotNull(factory);
Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
});
}
[ConditionalFact(nameof(RequirementsMet))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternHangs.Program))]
public void NoSpecialEntryPointPatternHangs()
{
using var _ = RemoteExecutor.Invoke(() =>
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPatternHangs.Program).Assembly, s_WaitTimeout);
Assert.NotNull(factory);
Assert.Throws<InvalidOperationException>(() => factory(Array.Empty<string>()));
});
}
[ConditionalFact(nameof(RequirementsMet))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternMainNoArgs.Program))]
public void NoSpecialEntryPointPatternMainNoArgs()
{
using var _ = RemoteExecutor.Invoke(() =>
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPatternMainNoArgs.Program).Assembly, s_WaitTimeout);
Assert.NotNull(factory);
Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
});
}
[ConditionalFact(nameof(RequirementsMet))]
public void TopLevelStatements()
{
using var _ = RemoteExecutor.Invoke(() =>
{
var assembly = Assembly.Load("TopLevelStatements");
var factory = HostFactoryResolver.ResolveServiceProviderFactory(assembly, s_WaitTimeout);
Assert.NotNull(factory);
Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
});
}
}
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);net461</TargetFrameworks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
</PropertyGroup>
<ItemGroup>
......@@ -20,5 +21,15 @@
<ProjectReference Include="CreateHostBuilderPatternTestSite\CreateHostBuilderPatternTestSite.csproj" />
<ProjectReference Include="CreateWebHostBuilderInvalidSignature\CreateWebHostBuilderInvalidSignature.csproj" />
<ProjectReference Include="CreateWebHostBuilderPatternTestSite\CreateWebHostBuilderPatternTestSite.csproj" />
<ProjectReference Include="NoSpecialEntryPointPattern\NoSpecialEntryPointPattern.csproj" />
<ProjectReference Include="NoSpecialEntryPointPatternThrows\NoSpecialEntryPointPatternThrows.csproj" />
<ProjectReference Include="NoSpecialEntryPointPatternExits\NoSpecialEntryPointPatternExits.csproj" />
<ProjectReference Include="NoSpecialEntryPointPatternHangs\NoSpecialEntryPointPatternHangs.csproj" />
<ProjectReference Include="NoSpecialEntryPointPatternMainNoArgs\NoSpecialEntryPointPatternMainNoArgs.csproj" />
<ProjectReference Include="TopLevelStatements\TopLevelStatements.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Hosting.Abstractions\src\Microsoft.Extensions.Hosting.Abstractions.csproj" />
</ItemGroup>
</Project>
......@@ -5,4 +5,8 @@
<EnableDefaultItems>true</EnableDefaultItems>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Hosting\src\Microsoft.Extensions.Hosting.csproj" />
</ItemGroup>
</Project>
<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>
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Hosting;
namespace NoSpecialEntryPointPattern
{
public class Program
{
public static void Main(string[] args)
{
var host = new HostBuilder().Build();
}
}
}
<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>
......@@ -3,10 +3,13 @@
using System;
namespace MockHostTypes
namespace NoSpecialEntryPointPatternExits
{
public class Host : IHost
public class Program
{
public IServiceProvider Services { get; } = new ServiceProvider();
public static void Main(string[] args)
{
}
}
}
<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>
......@@ -3,10 +3,13 @@
using System;
namespace MockHostTypes
namespace NoSpecialEntryPointPatternHangs
{
public interface IHost
public class Program
{
IServiceProvider Services { get; }
public static void Main(string[] args)
{
Console.ReadLine();
}
}
}
<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>
// 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 NoSpecialEntryPointPatternMainNoArgs
{
public class Program
{
public static void Main()
{
var host = new HostBuilder().Build();
}
}
}
<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>
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace MockHostTypes
using System;
namespace NoSpecialEntryPointPatternThrows
{
public class HostBuilder : IHostBuilder
public class Program
{
public IHost Build() => new Host();
public static void Main(string[] args)
{
throw new Exception("Main just throws");
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace MockHostTypes
{
public interface IHostBuilder
{
IHost Build();
}
}
using System;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder().Build();
\ No newline at end of file
<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>
......@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using Microsoft.Extensions.Configuration;
......@@ -127,13 +129,40 @@ public IHost Build()
}
_hostBuilt = true;
// REVIEW: If we want to raise more events outside of these calls then we will need to
// stash this in a field.
using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting");
const string hostBuildingEventName = "HostBuilding";
const string hostBuiltEventName = "HostBuilt";
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName))
{
Write(diagnosticListener, hostBuildingEventName, this);
}
BuildHostConfiguration();
CreateHostingEnvironment();
CreateHostBuilderContext();
BuildAppConfiguration();
CreateServiceProvider();
return _appServices.GetRequiredService<IHost>();
var host = _appServices.GetRequiredService<IHost>();
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName))
{
Write(diagnosticListener, hostBuiltEventName, host);
}
return host;
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern",
Justification = "The values being passed into Write are being consumed by the application already.")]
private static void Write<T>(
DiagnosticSource diagnosticSource,
string name,
T value)
{
diagnosticSource.Write(name, value);
}
private void BuildHostConfiguration()
......
......@@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
......@@ -32,6 +34,37 @@ public void DefaultConfigIsMutable()
}
}
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public void BuildFiresEvents()
{
using var _ = RemoteExecutor.Invoke(() =>
{
IHostBuilder hostBuilderFromEvent = null;
IHost hostFromEvent = null;
var listener = new HostingListener((pair) =>
{
if (pair.Key == "HostBuilding")
{
hostBuilderFromEvent = (IHostBuilder)pair.Value;
}
if (pair.Key == "HostBuilt")
{
hostFromEvent = (IHost)pair.Value;
}
});
using var sub = DiagnosticListener.AllListeners.Subscribe(listener);
var hostBuilder = new HostBuilder();
var host = hostBuilder.Build();
Assert.Same(hostBuilder, hostBuilderFromEvent);
Assert.Same(host, hostFromEvent);
});
}
[Fact]
public void ConfigureHostConfigurationPropagated()
{
......@@ -657,6 +690,32 @@ public void HostBuilderConfigureDefaultsInterleavesMissingConfigValues()
options.Value.BackgroundServiceExceptionBehavior);
}
private class HostingListener : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object?>>
{
private IDisposable? _disposable;
private readonly Action<KeyValuePair<string, object?>> _callback;
public HostingListener(Action<KeyValuePair<string, object?>> callback)
{
_callback = callback;
}
public void OnCompleted() { _disposable?.Dispose(); }
public void OnError(Exception error) { }
public void OnNext(DiagnosticListener value)
{
if (value.Name == "Microsoft.Extensions.Hosting")
{
_disposable = value.Subscribe(this);
}
}
public void OnNext(KeyValuePair<string, object?> value)
{
_callback(value);
}
}
private class FakeFileProvider : IFileProvider, IDisposable
{
public bool Disposed { get; private set; }
......
......@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent);net461</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
</PropertyGroup>
<ItemGroup>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册