Skip to content

Commit 2973167

Browse files
committed
Does this help
1 parent ee415b3 commit 2973167

File tree

2 files changed

+331
-1
lines changed

2 files changed

+331
-1
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Reflection;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
#nullable enable
12+
13+
namespace Microsoft.Extensions.Hosting
14+
{
15+
internal sealed class HostFactoryResolver
16+
{
17+
private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
18+
19+
public const string BuildWebHost = nameof(BuildWebHost);
20+
public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder);
21+
public const string CreateHostBuilder = nameof(CreateHostBuilder);
22+
23+
// The amount of time we wait for the diagnostic source events to fire
24+
private static readonly TimeSpan s_defaultWaitTimeout = TimeSpan.FromSeconds(5);
25+
26+
public static Func<string[], TWebHost>? ResolveWebHostFactory<TWebHost>(Assembly assembly)
27+
{
28+
return ResolveFactory<TWebHost>(assembly, BuildWebHost);
29+
}
30+
31+
public static Func<string[], TWebHostBuilder>? ResolveWebHostBuilderFactory<TWebHostBuilder>(Assembly assembly)
32+
{
33+
return ResolveFactory<TWebHostBuilder>(assembly, CreateWebHostBuilder);
34+
}
35+
36+
public static Func<string[], THostBuilder>? ResolveHostBuilderFactory<THostBuilder>(Assembly assembly)
37+
{
38+
return ResolveFactory<THostBuilder>(assembly, CreateHostBuilder);
39+
}
40+
41+
// This helpers encapsulates all of the complex logic required to:
42+
// 1. Execute the entry point of the specified assembly in a different thread.
43+
// 2. Wait for the diagnostic source events to fire
44+
// 3. Give the caller a chance to execute logic to mutate the IHostBuilder
45+
// 4. Resolve the instance of the applications's IHost
46+
// 5. Allow the caller to determine if the entry point has completed
47+
public static Func<string[], object>? ResolveHostFactory(Assembly assembly,
48+
TimeSpan? waitTimeout = null,
49+
bool stopApplication = true,
50+
Action<object>? configureHostBuilder = null,
51+
Action<Exception?>? entrypointCompleted = null)
52+
{
53+
if (assembly.EntryPoint is null)
54+
{
55+
return null;
56+
}
57+
58+
try
59+
{
60+
// Attempt to load hosting and check the version to make sure the events
61+
// even have a chance of firing (they were added in .NET >= 6)
62+
var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting");
63+
if (hostingAssembly.GetName().Version is Version version && version.Major < 6)
64+
{
65+
return null;
66+
}
67+
68+
// We're using a version >= 6 so the events can fire. If they don't fire
69+
// then it's because the application isn't using the hosting APIs
70+
}
71+
catch
72+
{
73+
// There was an error loading the extensions assembly, return null.
74+
return null;
75+
}
76+
77+
return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost();
78+
}
79+
80+
private static Func<string[], T>? ResolveFactory<T>(Assembly assembly, string name)
81+
{
82+
var programType = assembly?.EntryPoint?.DeclaringType;
83+
if (programType == null)
84+
{
85+
return null;
86+
}
87+
88+
var factory = programType.GetMethod(name, DeclaredOnlyLookup);
89+
if (!IsFactory<T>(factory))
90+
{
91+
return null;
92+
}
93+
94+
return args => (T)factory!.Invoke(null, new object[] { args })!;
95+
}
96+
97+
// TReturn Factory(string[] args);
98+
private static bool IsFactory<TReturn>(MethodInfo? factory)
99+
{
100+
return factory != null
101+
&& typeof(TReturn).IsAssignableFrom(factory.ReturnType)
102+
&& factory.GetParameters().Length == 1
103+
&& typeof(string[]).Equals(factory.GetParameters()[0].ParameterType);
104+
}
105+
106+
// Used by EF tooling without any Hosting references. Looses some return type safety checks.
107+
public static Func<string[], IServiceProvider?>? ResolveServiceProviderFactory(Assembly assembly, TimeSpan? waitTimeout = null)
108+
{
109+
// Prefer the older patterns by default for back compat.
110+
var webHostFactory = ResolveWebHostFactory<object>(assembly);
111+
if (webHostFactory != null)
112+
{
113+
return args =>
114+
{
115+
var webHost = webHostFactory(args);
116+
return GetServiceProvider(webHost);
117+
};
118+
}
119+
120+
var webHostBuilderFactory = ResolveWebHostBuilderFactory<object>(assembly);
121+
if (webHostBuilderFactory != null)
122+
{
123+
return args =>
124+
{
125+
var webHostBuilder = webHostBuilderFactory(args);
126+
var webHost = Build(webHostBuilder);
127+
return GetServiceProvider(webHost);
128+
};
129+
}
130+
131+
var hostBuilderFactory = ResolveHostBuilderFactory<object>(assembly);
132+
if (hostBuilderFactory != null)
133+
{
134+
return args =>
135+
{
136+
var hostBuilder = hostBuilderFactory(args);
137+
var host = Build(hostBuilder);
138+
return GetServiceProvider(host);
139+
};
140+
}
141+
142+
var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout);
143+
if (hostFactory != null)
144+
{
145+
return args =>
146+
{
147+
var host = hostFactory(args);
148+
return GetServiceProvider(host);
149+
};
150+
}
151+
152+
return null;
153+
}
154+
155+
private static object? Build(object builder)
156+
{
157+
var buildMethod = builder.GetType().GetMethod("Build");
158+
return buildMethod?.Invoke(builder, Array.Empty<object>());
159+
}
160+
161+
private static IServiceProvider? GetServiceProvider(object? host)
162+
{
163+
if (host == null)
164+
{
165+
return null;
166+
}
167+
var hostType = host.GetType();
168+
var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup);
169+
return (IServiceProvider?)servicesProperty?.GetValue(host);
170+
}
171+
172+
private sealed class HostingListener : IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object?>>
173+
{
174+
private readonly string[] _args;
175+
private readonly MethodInfo _entryPoint;
176+
private readonly TimeSpan _waitTimeout;
177+
private readonly bool _stopApplication;
178+
179+
private readonly TaskCompletionSource<object> _hostTcs = new();
180+
private IDisposable? _disposable;
181+
private Action<object>? _configure;
182+
private Action<Exception?>? _entrypointCompleted;
183+
private static readonly AsyncLocal<HostingListener> _currentListener = new();
184+
185+
public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action<object>? configure, Action<Exception?>? entrypointCompleted)
186+
{
187+
_args = args;
188+
_entryPoint = entryPoint;
189+
_waitTimeout = waitTimeout;
190+
_stopApplication = stopApplication;
191+
_configure = configure;
192+
_entrypointCompleted = entrypointCompleted;
193+
}
194+
195+
public object CreateHost()
196+
{
197+
using var subscription = DiagnosticListener.AllListeners.Subscribe(this);
198+
199+
// Kick off the entry point on a new thread so we don't block the current one
200+
// in case we need to timeout the execution
201+
var thread = new Thread(() =>
202+
{
203+
Exception? exception = null;
204+
205+
try
206+
{
207+
// Set the async local to the instance of the HostingListener so we can filter events that
208+
// aren't scoped to this execution of the entry point.
209+
_currentListener.Value = this;
210+
211+
var parameters = _entryPoint.GetParameters();
212+
if (parameters.Length == 0)
213+
{
214+
_entryPoint.Invoke(null, Array.Empty<object>());
215+
}
216+
else
217+
{
218+
_entryPoint.Invoke(null, new object[] { _args });
219+
}
220+
221+
// Try to set an exception if the entry point returns gracefully, this will force
222+
// build to throw
223+
_hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost"));
224+
}
225+
catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException)
226+
{
227+
// The host was stopped by our own logic
228+
}
229+
catch (TargetInvocationException tie)
230+
{
231+
exception = tie.InnerException ?? tie;
232+
233+
// Another exception happened, propagate that to the caller
234+
_hostTcs.TrySetException(exception);
235+
}
236+
catch (Exception ex)
237+
{
238+
exception = ex;
239+
240+
// Another exception happened, propagate that to the caller
241+
_hostTcs.TrySetException(ex);
242+
}
243+
finally
244+
{
245+
// Signal that the entry point is completed
246+
_entrypointCompleted?.Invoke(exception);
247+
}
248+
})
249+
{
250+
// Make sure this doesn't hang the process
251+
IsBackground = true
252+
};
253+
254+
// Start the thread
255+
thread.Start();
256+
257+
try
258+
{
259+
// Wait before throwing an exception
260+
if (!_hostTcs.Task.Wait(_waitTimeout))
261+
{
262+
throw new InvalidOperationException("Unable to build IHost");
263+
}
264+
}
265+
catch (AggregateException) when (_hostTcs.Task.IsCompleted)
266+
{
267+
// Lets this propagate out of the call to GetAwaiter().GetResult()
268+
}
269+
270+
Debug.Assert(_hostTcs.Task.IsCompleted);
271+
272+
return _hostTcs.Task.GetAwaiter().GetResult();
273+
}
274+
275+
public void OnCompleted()
276+
{
277+
_disposable?.Dispose();
278+
}
279+
280+
public void OnError(Exception error)
281+
{
282+
283+
}
284+
285+
public void OnNext(DiagnosticListener value)
286+
{
287+
if (_currentListener.Value != this)
288+
{
289+
// Ignore events that aren't for this listener
290+
return;
291+
}
292+
293+
if (value.Name == "Microsoft.Extensions.Hosting")
294+
{
295+
_disposable = value.Subscribe(this);
296+
}
297+
}
298+
299+
public void OnNext(KeyValuePair<string, object?> value)
300+
{
301+
if (_currentListener.Value != this)
302+
{
303+
// Ignore events that aren't for this listener
304+
return;
305+
}
306+
307+
if (value.Key == "HostBuilding")
308+
{
309+
_configure?.Invoke(value.Value!);
310+
}
311+
312+
if (value.Key == "HostBuilt")
313+
{
314+
_hostTcs.TrySetResult(value.Value!);
315+
316+
if (_stopApplication)
317+
{
318+
// Stop the host from running further
319+
throw new StopTheHostException();
320+
}
321+
}
322+
}
323+
324+
private sealed class StopTheHostException : Exception
325+
{
326+
327+
}
328+
}
329+
}
330+
}

src/Mvc/Mvc.Testing/src/Microsoft.AspNetCore.Mvc.Testing.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
1616
<Reference Include="Microsoft.Extensions.DependencyModel" />
1717
<Reference Include="Microsoft.Extensions.Hosting" />
18-
<Reference Include="Microsoft.Extensions.HostFactoryResolver.Sources" />
18+
<!-- <Reference Include="Microsoft.Extensions.HostFactoryResolver.Sources" /> -->
1919
</ItemGroup>
2020

2121
<ItemGroup>

0 commit comments

Comments
 (0)