From 15008b0b7fcb54235a9de3ab844c066aaf42ea44 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 27 Jun 2017 16:58:02 -0700 Subject: [PATCH] Add WaitForShutdown to WebHostExtensions --- .../WebHostExtensions.cs | 145 +++++++++++------- .../ShutdownTests.cs | 57 ++++++- ...rosoft.AspNetCore.Hosting.TestSites.csproj | 1 + .../Program.cs | 26 +++- 4 files changed, 169 insertions(+), 60 deletions(-) diff --git a/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs b/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs index 3dcd322602..38807e11e3 100644 --- a/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs +++ b/src/Microsoft.AspNetCore.Hosting/WebHostExtensions.cs @@ -23,6 +23,32 @@ namespace Microsoft.AspNetCore.Hosting return host.StopAsync(new CancellationTokenSource(timeout).Token); } + /// + /// Block the calling thread until shutdown is triggered via Ctrl+C or SIGTERM. + /// + /// The running . + public static void WaitForShutdown(this IWebHost host) + { + host.WaitForShutdownAsync().GetAwaiter().GetResult(); + } + + /// + /// Returns a Task that completes when shutdown is triggered via the given token, Ctrl+C or SIGTERM. + /// + /// The running . + /// The token to trigger shutdown. + public static async Task WaitForShutdownAsync(this IWebHost host, CancellationToken token = default(CancellationToken)) + { + var done = new ManualResetEventSlim(false); + using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token)) + { + AttachCtrlcSigtermShutdown(cts, done, shutdownMessage: string.Empty); + + await host.WaitForTokenShutdownAsync(cts.Token); + done.Set(); + } + } + /// /// Runs a web application and block the calling thread until host shutdown. /// @@ -33,54 +59,30 @@ namespace Microsoft.AspNetCore.Hosting } /// - /// Runs a web application and returns a Task that only completes on host shutdown. + /// Runs a web application and returns a Task that only completes when the token is triggered or shutdown is triggered. /// /// The to run. - public static async Task RunAsync(this IWebHost host) + /// The token to trigger shutdown. + public static async Task RunAsync(this IWebHost host, CancellationToken token = default(CancellationToken)) { + // Wait for token shutdown if it can be canceled + if (token.CanBeCanceled) + { + await host.RunAsync(token, shutdownMessage: null); + return; + } + + // If token cannot be canceled, attach Ctrl+C and SIGTERM shutdown var done = new ManualResetEventSlim(false); using (var cts = new CancellationTokenSource()) { - Action shutdown = () => - { - if (!cts.IsCancellationRequested) - { - Console.WriteLine("Application is shutting down..."); - try - { - cts.Cancel(); - } - catch (ObjectDisposedException) - { - } - } - - done.Wait(); - }; - - AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => shutdown(); - Console.CancelKeyPress += (sender, eventArgs) => - { - shutdown(); - // Don't terminate the process immediately, wait for the Main thread to exit gracefully. - eventArgs.Cancel = true; - }; + AttachCtrlcSigtermShutdown(cts, done, shutdownMessage: "Application is shutting down..."); await host.RunAsync(cts.Token, "Application started. Press Ctrl+C to shut down."); done.Set(); } } - /// - /// Runs a web application and and returns a Task that only completes when the token is triggered or shutdown is triggered. - /// - /// The to run. - /// The token to trigger shutdown. - public static Task RunAsync(this IWebHost host, CancellationToken token) - { - return host.RunAsync(token, shutdownMessage: null); - } - private static async Task RunAsync(this IWebHost host, CancellationToken token, string shutdownMessage) { using (host) @@ -107,24 +109,61 @@ namespace Microsoft.AspNetCore.Hosting Console.WriteLine(shutdownMessage); } - token.Register(state => - { - ((IApplicationLifetime)state).StopApplication(); - }, - applicationLifetime); - - var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - applicationLifetime.ApplicationStopping.Register(obj => - { - var tcs = (TaskCompletionSource)obj; - tcs.TrySetResult(null); - }, waitForStop); - - await waitForStop.Task; - - // WebHost will use its default ShutdownTimeout if none is specified. - await host.StopAsync(); + await host.WaitForTokenShutdownAsync(token); } } + + private static void AttachCtrlcSigtermShutdown(CancellationTokenSource cts, ManualResetEventSlim resetEvent, string shutdownMessage) + { + void Shutdown() + { + if (!cts.IsCancellationRequested) + { + if (!string.IsNullOrEmpty(shutdownMessage)) + { + Console.WriteLine(shutdownMessage); + } + try + { + cts.Cancel(); + } + catch (ObjectDisposedException) { } + } + + // Wait on the given reset event + resetEvent.Wait(); + }; + + AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => Shutdown(); + Console.CancelKeyPress += (sender, eventArgs) => + { + Shutdown(); + // Don't terminate the process immediately, wait for the Main thread to exit gracefully. + eventArgs.Cancel = true; + }; + } + + private static async Task WaitForTokenShutdownAsync(this IWebHost host, CancellationToken token) + { + var applicationLifetime = host.Services.GetService(); + + token.Register(state => + { + ((IApplicationLifetime)state).StopApplication(); + }, + applicationLifetime); + + var waitForStop = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + applicationLifetime.ApplicationStopping.Register(obj => + { + var tcs = (TaskCompletionSource)obj; + tcs.TrySetResult(null); + }, waitForStop); + + await waitForStop.Task; + + // WebHost will use its default ShutdownTimeout if none is specified. + await host.StopAsync(); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Hosting.FunctionalTests/ShutdownTests.cs b/test/Microsoft.AspNetCore.Hosting.FunctionalTests/ShutdownTests.cs index b01b4481b9..d9a17b70e7 100644 --- a/test/Microsoft.AspNetCore.Hosting.FunctionalTests/ShutdownTests.cs +++ b/test/Microsoft.AspNetCore.Hosting.FunctionalTests/ShutdownTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading.Tasks; @@ -22,11 +23,11 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests [ConditionalFact] [OSSkipCondition(OperatingSystems.Windows)] [OSSkipCondition(OperatingSystems.MacOSX)] - public async Task ShutdownTest() + public async Task ShutdownTestRun() { using (StartLog(out var loggerFactory)) { - var logger = loggerFactory.CreateLogger(nameof(ShutdownTest)); + var logger = loggerFactory.CreateLogger(nameof(ShutdownTestRun)); var applicationPath = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("Hosting"), "test", "Microsoft.AspNetCore.Hosting.TestSites"); @@ -43,13 +44,12 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests PublishApplicationBeforeDeployment = true }; + deploymentParameters.EnvironmentVariables.Add(new KeyValuePair("ASPNETCORE_STARTMECHANIC", "Run")); + using (var deployer = new SelfHostDeployer(deploymentParameters, loggerFactory)) { await deployer.DeployAsync(); - // Wait for application to start - await Task.Delay(1000); - string output = string.Empty; deployer.HostProcess.OutputDataReceived += (sender, args) => output += args.Data + '\n'; @@ -68,6 +68,53 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests } } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Windows)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ShutdownTestWaitForShutdown() + { + using (StartLog(out var loggerFactory)) + { + var logger = loggerFactory.CreateLogger(nameof(ShutdownTestWaitForShutdown)); + + var applicationPath = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("Hosting"), "test", + "Microsoft.AspNetCore.Hosting.TestSites"); + + var deploymentParameters = new DeploymentParameters( + applicationPath, + ServerType.Kestrel, + RuntimeFlavor.CoreClr, + RuntimeArchitecture.x64) + { + EnvironmentName = "Shutdown", + TargetFramework = "netcoreapp2.0", + ApplicationType = ApplicationType.Portable, + PublishApplicationBeforeDeployment = true + }; + + deploymentParameters.EnvironmentVariables.Add(new KeyValuePair("ASPNETCORE_STARTMECHANIC", "WaitForShutdown")); + + using (var deployer = new SelfHostDeployer(deploymentParameters, loggerFactory)) + { + await deployer.DeployAsync(); + + string output = string.Empty; + deployer.HostProcess.OutputDataReceived += (sender, args) => output += args.Data + '\n'; + + SendSIGINT(deployer.HostProcess.Id); + + WaitForExitOrKill(deployer.HostProcess); + + output = output.Trim('\n'); + + Assert.Equal(output, "Stopping firing\n" + + "Stopping end\n" + + "Stopped firing\n" + + "Stopped end"); + } + } + } + private static void SendSIGINT(int processId) { diff --git a/test/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj b/test/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj index 69b8d0068c..44e074e476 100644 --- a/test/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj +++ b/test/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj @@ -15,6 +15,7 @@ + diff --git a/test/Microsoft.AspNetCore.Hosting.TestSites/Program.cs b/test/Microsoft.AspNetCore.Hosting.TestSites/Program.cs index d3e284aa09..42390fa73b 100644 --- a/test/Microsoft.AspNetCore.Hosting.TestSites/Program.cs +++ b/test/Microsoft.AspNetCore.Hosting.TestSites/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; @@ -19,6 +20,7 @@ namespace ServerComparison.TestSites { var config = new ConfigurationBuilder() .AddCommandLine(args) + .AddEnvironmentVariables(prefix: "ASPNETCORE_") .Build(); var builder = new WebHostBuilder() @@ -31,9 +33,29 @@ namespace ServerComparison.TestSites }) .UseStartup("Microsoft.AspNetCore.Hosting.TestSites"); - var host = builder.Build(); + if (config["STARTMECHANIC"] == "Run") + { + var host = builder.Build(); - host.Run(); + host.Run(); + } + else if (config["STARTMECHANIC"] == "WaitForShutdown") + { + using (var host = builder.Build()) + { + host.Start(); + + // Mimic application startup messages so application deployer knows that the application has started + Console.WriteLine("Application started. Press Ctrl+C to shut down."); + Console.WriteLine("Now listening on: http://localhost:5000"); + + host.WaitForShutdown(); + } + } + else + { + throw new InvalidOperationException("Starting mechanic not specified"); + } } }