Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fire diagnostic source events from IHostBuilder.Build #53757

Merged
merged 17 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -31,6 +37,16 @@ internal sealed class HostFactoryResolver
return ResolveFactory<THostBuilder>(assembly, CreateHostBuilder);
}

public static Func<string[], IHostBuilder>? ResolveHostBuilderFactory(Assembly assembly)
davidfowl marked this conversation as resolved.
Show resolved Hide resolved
{
if (assembly.EntryPoint is null)
{
return null;
}

return args => new DeferredHostBuilder(args, assembly.EntryPoint);
}

private static Func<string[], T>? ResolveFactory<T>(Assembly assembly, string name)
{
var programType = assembly?.EntryPoint?.DeclaringType;
Expand Down Expand Up @@ -93,6 +109,17 @@ private static bool IsFactory<TReturn>(MethodInfo? factory)
};
}

var deferredFactory = ResolveHostBuilderFactory(assembly);
if (deferredFactory != null)
{
return args =>
{
var hostBuilder = deferredFactory(args);
var host = hostBuilder.Build();
return host.Services;
};
}

return null;
}

Expand All @@ -112,5 +139,184 @@ private static bool IsFactory<TReturn>(MethodInfo? factory)
var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup);
return (IServiceProvider?)servicesProperty?.GetValue(host);
}

// This host builder captures calls to the IHostBuilder then replays them on the application's
// IHostBuilder when the event fires
private class DeferredHostBuilder : IHostBuilder, IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object?>>
{
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();

private readonly string[] _args;
private readonly MethodInfo _entryPoint;

private readonly TaskCompletionSource<IHost> _hostTcs = new();
private IDisposable? _disposable;

private Action<IHostBuilder> _configure;

// The amount of time we wait for the diagnostic source events to fire
private static readonly TimeSpan _waitTimeout = TimeSpan.FromSeconds(20);
davidfowl marked this conversation as resolved.
Show resolved Hide resolved

public DeferredHostBuilder(string[] args, MethodInfo entryPoint)
{
_args = args;
_entryPoint = entryPoint;
_configure = b =>
davidfowl marked this conversation as resolved.
Show resolved Hide resolved
{
// Copy the properties from this builder into the builder
// that we're going to receive
foreach (var pair in Properties)
{
b.Properties[pair.Key] = pair.Value;
}
};
}

public IHost Build()
{
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 entrypoint 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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use a different message here - specifically call out that it timed out. That way we can tell the difference between the entrypoint returning gracefully vs. timing out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want the timeout to be an implementation detail. I'm also thinking this could also return null.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who do you expect to see the message? If the message is primarily shown to people who are trying to resolve the issue (or users will relay it to someone else who resolves the issue) then it might help to make it more descriptive.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to check the callers but its possible null is a better return than throwing.

}
}
catch (AggregateException) when (_hostTcs.Task.IsCompleted)
{
// Lets this propogate out of the call to GetAwaiter().GetResult()
davidfowl marked this conversation as resolved.
Show resolved Hide resolved
}

Debug.Assert(_hostTcs.Task.IsCompleted);
halter73 marked this conversation as resolved.
Show resolved Hide resolved

return _hostTcs.Task.GetAwaiter().GetResult();
}

public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
{
_configure += b => b.ConfigureAppConfiguration(configureDelegate);
return this;
}

public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate)
{
_configure += b => b.ConfigureContainer(configureDelegate);
return this;
}

public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
{
_configure += b => b.ConfigureHostConfiguration(configureDelegate);
return this;
}

public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
{
_configure += b => b.ConfigureServices(configureDelegate);
return this;
}

public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull
{
_configure += b => b.UseServiceProviderFactory(factory);
return this;
}

public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) where TContainerBuilder : notnull
{
_configure += b => b.UseServiceProviderFactory(factory);
return this;
}

public void OnCompleted()
{
_disposable?.Dispose();
}

public void OnError(Exception error)
{

}

public void OnNext(DiagnosticListener value)
{
if (value.Name == "Microsoft.Extensions.Hosting")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we wire up to the new event to intercept calls to build.

{
_disposable = value.Subscribe(this);
}
}

public void OnNext(KeyValuePair<string, object?> value)
{
if (value.Key == "HostBuilding")
{
if (value.Value is IHostBuilder builder)
{
_configure(builder);
}
}

if (value.Key == "HostBuilt")
{
if (value.Value is IHost host)
{
_hostTcs.TrySetResult(host);

// Stop the host from running further
throw new StopTheHostException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prize for dirty hackery this week : D

Is it important to guarantee that the thread really does stop? It would be easy enough for an app to throw a try/catch around the part of their code where this gets raised so that it never gets back to the entrypoint. Your tool code will be running in parallel with the user's unknown error handling app code. It doesn't seem obviously harmful to me but figured I mention it.

Copy link
Member Author

@davidfowl davidfowl Jun 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposal is one of the things I'm worried about and why I think throwing to stop execution makes the most sense here. The main app never gets a handle on the application here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be easy enough for an app to throw a try/catch around the part of their code where this gets raised so that it never gets back to the entrypoint.

The thing that stops this from happening in the throw case is the fact that the application never gets access to the IHost instance. They can't call build again on it because double building throws. Even if they catch the exception and do something else, the IHost isntance that came out of Build is never observed by the application.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wasn't imagining they continue normally, more like they have some complex error handling code that will be running in parallel.

the application never gets access to the IHost instance

[Joking] What do you mean? This PR just added the official mechanism that lets everyone access IHost without the pesky inconvenience of needing Build() to return it to you ;p

}
}
}

private class StopTheHostException : Exception
{

}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.

using MockHostTypes;
using Microsoft.Extensions.Hosting;
davidfowl marked this conversation as resolved.
Show resolved Hide resolved

namespace CreateHostBuilderInvalidSignature
{
public class Program
{
public static void Main(string[] args)
{
var webHost = CreateHostBuilder(null, args).Build();
var webHost = CreateHostBuilder(null, args)?.Build();
}

// Extra parameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using MockHostTypes;
using Microsoft.Extensions.Hosting;

namespace CreateHostBuilderPatternTestSite
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
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;

[Fact]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BuildWebHostPatternTestSite.Program))]
public void BuildWebHostPattern_CanFindWebHost()
Expand Down Expand Up @@ -46,7 +50,7 @@ public void BuildWebHostPattern__Invalid_CantFindServiceProvider()
{
var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(BuildWebHostInvalidSignature.Program).Assembly);

Assert.Null(factory);
Assert.NotNull(factory);
}

[Fact]
Expand Down Expand Up @@ -119,13 +123,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);

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);

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);

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);

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);

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);

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);

Assert.NotNull(factory);
Assert.IsAssignableFrom<IServiceProvider>(factory(Array.Empty<string>()));
});
}
}
}
Loading