/
HostFactoryResolver.cs
355 lines (302 loc) · 13.8 KB
/
HostFactoryResolver.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
// 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 System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace Microsoft.Extensions.Hosting
{
internal sealed class HostFactoryResolver
{
private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
public const string BuildWebHost = nameof(BuildWebHost);
public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder);
public const string CreateHostBuilder = nameof(CreateHostBuilder);
private const string TimeoutEnvironmentKey = "DOTNET_HOST_FACTORY_RESOLVER_DEFAULT_TIMEOUT_IN_SECONDS";
// The amount of time we wait for the diagnostic source events to fire
private static readonly TimeSpan s_defaultWaitTimeout = SetupDefaultTimout();
private static TimeSpan SetupDefaultTimout()
{
if (Debugger.IsAttached)
{
return Timeout.InfiniteTimeSpan;
}
if (uint.TryParse(Environment.GetEnvironmentVariable(TimeoutEnvironmentKey), out uint timeoutInSeconds))
{
return TimeSpan.FromSeconds((int)timeoutInSeconds);
}
return TimeSpan.FromMinutes(5);
}
public static Func<string[], TWebHost>? ResolveWebHostFactory<TWebHost>(Assembly assembly)
{
return ResolveFactory<TWebHost>(assembly, BuildWebHost);
}
public static Func<string[], TWebHostBuilder>? ResolveWebHostBuilderFactory<TWebHostBuilder>(Assembly assembly)
{
return ResolveFactory<TWebHostBuilder>(assembly, CreateWebHostBuilder);
}
public static Func<string[], THostBuilder>? ResolveHostBuilderFactory<THostBuilder>(Assembly assembly)
{
return ResolveFactory<THostBuilder>(assembly, CreateHostBuilder);
}
// 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)
{
return null;
}
try
{
// Attempt to load hosting and check the version to make sure the events
// 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)
{
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, entrypointCompleted).CreateHost();
}
private static Func<string[], T>? ResolveFactory<T>(Assembly assembly, string name)
{
var programType = assembly?.EntryPoint?.DeclaringType;
if (programType == null)
{
return null;
}
var factory = programType.GetMethod(name, DeclaredOnlyLookup);
if (!IsFactory<T>(factory))
{
return null;
}
return args => (T)factory!.Invoke(null, new object[] { args })!;
}
// TReturn Factory(string[] args);
private static bool IsFactory<TReturn>(MethodInfo? factory)
{
return factory != null
&& typeof(TReturn).IsAssignableFrom(factory.ReturnType)
&& factory.GetParameters().Length == 1
&& typeof(string[]).Equals(factory.GetParameters()[0].ParameterType);
}
// Used by EF tooling without any Hosting references. Looses some return type safety checks.
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);
if (webHostFactory != null)
{
return args =>
{
var webHost = webHostFactory(args);
return GetServiceProvider(webHost);
};
}
var webHostBuilderFactory = ResolveWebHostBuilderFactory<object>(assembly);
if (webHostBuilderFactory != null)
{
return args =>
{
var webHostBuilder = webHostBuilderFactory(args);
var webHost = Build(webHostBuilder);
return GetServiceProvider(webHost);
};
}
var hostBuilderFactory = ResolveHostBuilderFactory<object>(assembly);
if (hostBuilderFactory != null)
{
return args =>
{
var hostBuilder = hostBuilderFactory(args);
var host = Build(hostBuilder);
return GetServiceProvider(host);
};
}
var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout);
if (hostFactory != null)
{
return args =>
{
static bool IsApplicationNameArg(string arg)
=> arg.Equals("--applicationName", StringComparison.OrdinalIgnoreCase) ||
arg.Equals("/applicationName", StringComparison.OrdinalIgnoreCase);
args = args.Any(arg => IsApplicationNameArg(arg)) || assembly.FullName is null
? args
: args.Concat(new[] { "--applicationName", assembly.FullName }).ToArray();
var host = hostFactory(args);
return GetServiceProvider(host);
};
}
return null;
}
private static object? Build(object builder)
{
var buildMethod = builder.GetType().GetMethod("Build");
return buildMethod?.Invoke(builder, Array.Empty<object>());
}
private static IServiceProvider? GetServiceProvider(object? host)
{
if (host == null)
{
return null;
}
var hostType = host.GetType();
var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup);
return (IServiceProvider?)servicesProperty?.GetValue(host);
}
private sealed 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 readonly Action<object>? _configure;
private readonly Action<Exception?>? _entrypointCompleted;
private static readonly AsyncLocal<HostingListener> _currentListener = new();
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()
{
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(() =>
{
Exception? exception = null;
try
{
// Set the async local to the instance of the HostingListener so we can filter events that
// aren't scoped to this execution of the entry point.
_currentListener.Value = this;
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("The entry point exited without ever building an IHost."));
}
catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException)
{
// The host was stopped by our own logic
}
catch (TargetInvocationException tie)
{
exception = tie.InnerException ?? tie;
// Another exception happened, propagate that to the caller
_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
IsBackground = true
};
// Start the thread
thread.Start();
try
{
// Wait before throwing an exception
if (!_hostTcs.Task.Wait(_waitTimeout))
{
throw new InvalidOperationException($"Timed out waiting for the entry point to build the IHost after {s_defaultWaitTimeout}. This timeout can be modified using the '{TimeoutEnvironmentKey}' environment variable.");
}
}
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 (_currentListener.Value != this)
{
// Ignore events that aren't for this listener
return;
}
if (value.Name == "Microsoft.Extensions.Hosting")
{
_disposable = value.Subscribe(this);
}
}
public void OnNext(KeyValuePair<string, object?> value)
{
if (_currentListener.Value != this)
{
// Ignore events that aren't for this listener
return;
}
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 sealed class StopTheHostException : Exception
{
}
}
}
}