Skip to content

Commit

Permalink
Do not run IHostedService implementations (#2880)
Browse files Browse the repository at this point in the history
- Remove `IHostedService` implementations so the CLI doesn't run hosted services (and then hang/fail).
- Refactor CLI tests to reduce duplication.
- Use newer C# syntax as suggested by Visual Studio.

Co-authored-by: Sjoerd van der Meer <2460430+desjoerd@users.noreply.github.com>
  • Loading branch information
martincostello and desjoerd committed May 18, 2024
1 parent 26c78cb commit 432c417
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 145 deletions.
7 changes: 7 additions & 0 deletions Swashbuckle.AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "test\WebSites\Web
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Aot", "test\WebSites\WebApi.Aot\WebApi.Aot.csproj", "{07BB09CF-6C6F-4D00-A459-93586345C921}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalAppWithHostedServices", "test\WebSites\MinimalAppWithHostedServices\MinimalAppWithHostedServices.csproj", "{D06A88E8-6F42-4F40-943A-E266C0AE6EC9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -259,6 +261,10 @@ Global
{07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07BB09CF-6C6F-4D00-A459-93586345C921}.Release|Any CPU.Build.0 = Release|Any CPU
{D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -302,6 +308,7 @@ Global
{B6037A37-4A4F-438D-B18A-0C9D1408EAB2} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
{DE1D77F8-3916-4DEE-A57D-6DDC357F64C6} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
{07BB09CF-6C6F-4D00-A459-93586345C921} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
{D06A88E8-6F42-4F40-943A-E266C0AE6EC9} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51}
Expand Down
8 changes: 4 additions & 4 deletions src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ public CommandRunner(string commandName, string commandDescription, TextWriter o
{
CommandName = commandName;
CommandDescription = commandDescription;
_argumentDescriptors = new Dictionary<string, string>();
_optionDescriptors = new Dictionary<string, OptionDescriptor>();
_runFunc = (namedArgs) => { return 1; }; // noop
_subRunners = new List<CommandRunner>();
_argumentDescriptors = [];
_optionDescriptors = [];
_runFunc = (_) => 1; // no-op
_subRunners = [];
_output = output;
}

Expand Down
146 changes: 73 additions & 73 deletions src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ internal sealed class HostFactoryResolver
return null;
}

return args => (T)factory.Invoke(null, new object[] { args });
return args => (T)factory.Invoke(null, [args]);
}

// TReturn Factory(string[] args);
Expand Down Expand Up @@ -153,7 +153,7 @@ private static bool IsFactory<TReturn>(MethodInfo factory)
private static object Build(object builder)
{
var buildMethod = builder.GetType().GetMethod("Build");
return buildMethod?.Invoke(builder, Array.Empty<object>());
return buildMethod?.Invoke(builder, []);
}

private static IServiceProvider GetServiceProvider(object host)
Expand All @@ -174,13 +174,19 @@ private sealed class HostingListener : IObserver<DiagnosticListener>, IObserver<
private readonly TimeSpan _waitTimeout;
private readonly bool _stopApplication;

private readonly TaskCompletionSource<object> _hostTcs = new TaskCompletionSource<object>();
private readonly TaskCompletionSource<object> _hostTcs = new();
private IDisposable _disposable;
private Action<object> _configure;
private Action<Exception> _entrypointCompleted;
private static readonly AsyncLocal<HostingListener> _currentListener = new AsyncLocal<HostingListener>();

public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action<object> configure, Action<Exception> entrypointCompleted)
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;
Expand All @@ -192,84 +198,82 @@ public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeou

public object CreateHost()
{
using (var subscription = DiagnosticListener.AllListeners.Subscribe(this))
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;
// 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
{
Exception exception = null;
// 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;
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("Unable to build IHost"));
}
catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException)
var parameters = _entryPoint.GetParameters();
if (parameters.Length == 0)
{
// The host was stopped by our own logic
_entryPoint.Invoke(null, []);
}
catch (TargetInvocationException tie)
else
{
exception = tie.InnerException ?? tie;
// Another exception happened, propagate that to the caller
_hostTcs.TrySetException(exception);
_entryPoint.Invoke(null, [_args]);
}
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);
}
})
// 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)
{
// Make sure this doesn't hang the process
IsBackground = true
};

// Start the thread
thread.Start();
// The host was stopped by our own logic
}
catch (TargetInvocationException tie)
{
exception = tie.InnerException ?? tie;
try
// Another exception happened, propagate that to the caller
_hostTcs.TrySetException(exception);
}
catch (Exception ex)
{
// Wait before throwing an exception
if (!_hostTcs.Task.Wait(_waitTimeout))
{
throw new InvalidOperationException("Unable to build IHost");
}
exception = ex;
// Another exception happened, propagate that to the caller
_hostTcs.TrySetException(ex);
}
catch (AggregateException) when (_hostTcs.Task.IsCompleted)
finally
{
// Lets this propagate out of the call to GetAwaiter().GetResult()
// Signal that the entry point is completed
_entrypointCompleted?.Invoke(exception);
}
})
{
// Make sure this doesn't hang the process
IsBackground = true
};

Debug.Assert(_hostTcs.Task.IsCompleted);
// Start the thread
thread.Start();

return _hostTcs.Task.GetAwaiter().GetResult();
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()
Expand All @@ -279,7 +283,6 @@ public void OnCompleted()

public void OnError(Exception error)
{

}

public void OnNext(DiagnosticListener value)
Expand Down Expand Up @@ -321,10 +324,7 @@ public void OnNext(KeyValuePair<string, object> value)
}
}

private sealed class StopTheHostException : Exception
{

}
private sealed class StopTheHostException : Exception;
}
}
}
27 changes: 20 additions & 7 deletions src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features;
#if NETCOREAPP3_0_OR_GREATER
using Microsoft.Extensions.DependencyInjection;
#endif
using Microsoft.Extensions.Hosting;

namespace Swashbuckle.AspNetCore.Cli
Expand All @@ -31,6 +33,19 @@ void ConfigureHostBuilder(object hostBuilder)
{
services.AddSingleton<IServer, NoopServer>();
services.AddSingleton<IHostLifetime, NoopHostLifetime>();
for (var i = services.Count - 1; i >= 0; i--)
{
// exclude all implementations of IHostedService
// except Microsoft.AspNetCore.Hosting.GenericWebHostService because that one will build/configure
// the WebApplication/Middleware pipeline in the case of the GenericWebHostBuilder.
var registration = services[i];
if (registration.ServiceType == typeof(IHostedService)
&& registration.ImplementationType is not { FullName: "Microsoft.AspNetCore.Hosting.GenericWebHostService" })
{
services.RemoveAt(i);
}
}
});
}

Expand Down Expand Up @@ -69,18 +84,17 @@ void OnEntryPointExit(Exception exception)
// We set the application name in the hosting environment to the startup assembly
// to avoid falling back to the entry assembly (dotnet-swagger) when configuring our
// application.
var services = ((IHost)factory(new[] { $"--{HostDefaults.ApplicationKey}={assemblyName}" })).Services;
var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services;

// Wait for the application to start so that we know it's fully configured. This is important because
// we need the middleware pipeline to be configured before we access the ISwaggerProvider in
// in the IServiceProvider
var applicationLifetime = services.GetRequiredService<IHostApplicationLifetime>();

using (var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null)))
{
waitForStartTcs.Task.Wait();
return services;
}
using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null));
waitForStartTcs.Task.Wait();

return services;
}
catch (InvalidOperationException)
{
Expand All @@ -103,7 +117,6 @@ private class NoopServer : IServer
public void Dispose() { }
public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

}
}
}
Loading

0 comments on commit 432c417

Please sign in to comment.