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+ }
0 commit comments