diff --git a/build/CodeSign.props b/build/CodeSign.props index 415c54d527..0d356b8ec6 100644 --- a/build/CodeSign.props +++ b/build/CodeSign.props @@ -44,6 +44,8 @@ + + diff --git a/build/CodeSign.targets b/build/CodeSign.targets index 13d89fc64f..ddc1e51d6e 100644 --- a/build/CodeSign.targets +++ b/build/CodeSign.targets @@ -1,6 +1,8 @@ + + true $(CodeSignDependsOn);CollectFileSignInfo diff --git a/build/artifacts.props b/build/artifacts.props index 4203c4cd99..c9c2348299 100644 --- a/build/artifacts.props +++ b/build/artifacts.props @@ -20,7 +20,6 @@ - @@ -134,7 +133,6 @@ - diff --git a/build/dependencies.props b/build/dependencies.props index 48d2a749f4..c69d15bb69 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -97,6 +97,7 @@ 2.2.0 2.2.0 + 2.2.0 0.6.0-rtm-final diff --git a/build/external-dependencies.props b/build/external-dependencies.props index e99d58281f..2ea0972af2 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -79,7 +79,7 @@ - + diff --git a/eng/dependencies.temp.props b/eng/dependencies.temp.props index c7e0c9bc50..3ebcecb640 100644 --- a/eng/dependencies.temp.props +++ b/eng/dependencies.temp.props @@ -6,6 +6,7 @@ This is required to provide dependencies for samples and tests. + diff --git a/src/Hosting/Hosting/src/Internal/HostingLoggerExtensions.cs b/src/Hosting/Hosting/src/Internal/HostingLoggerExtensions.cs index a0579880a0..ce333f232c 100644 --- a/src/Hosting/Hosting/src/Internal/HostingLoggerExtensions.cs +++ b/src/Hosting/Hosting/src/Internal/HostingLoggerExtensions.cs @@ -94,7 +94,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal private class HostingLogScope : IReadOnlyList> { - private readonly HttpContext _httpContext; + private readonly string _path; + private readonly string _traceIdentifier; private readonly string _correlationId; private string _cachedToString; @@ -113,11 +114,11 @@ namespace Microsoft.AspNetCore.Hosting.Internal { if (index == 0) { - return new KeyValuePair("RequestId", _httpContext.TraceIdentifier); + return new KeyValuePair("RequestId", _traceIdentifier); } else if (index == 1) { - return new KeyValuePair("RequestPath", _httpContext.Request.Path.ToString()); + return new KeyValuePair("RequestPath", _path); } else if (index == 2) { @@ -130,7 +131,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal public HostingLogScope(HttpContext httpContext, string correlationId) { - _httpContext = httpContext; + _traceIdentifier = httpContext.TraceIdentifier; + _path = httpContext.Request.Path.ToString(); _correlationId = correlationId; } @@ -141,8 +143,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal _cachedToString = string.Format( CultureInfo.InvariantCulture, "RequestId:{0} RequestPath:{1}", - _httpContext.TraceIdentifier, - _httpContext.Request.Path); + _traceIdentifier, + _path); } return _cachedToString; diff --git a/src/Hosting/Hosting/src/WebHostBuilder.cs b/src/Hosting/Hosting/src/WebHostBuilder.cs index 423b898cec..2238bd3b9b 100644 --- a/src/Hosting/Hosting/src/WebHostBuilder.cs +++ b/src/Hosting/Hosting/src/WebHostBuilder.cs @@ -174,13 +174,6 @@ namespace Microsoft.AspNetCore.Hosting } } - var logger = hostingServiceProvider.GetRequiredService>(); - // Warn about duplicate HostingStartupAssemblies - foreach (var assemblyName in _options.GetFinalHostingStartupAssemblies().GroupBy(a => a, StringComparer.OrdinalIgnoreCase).Where(g => g.Count() > 1)) - { - logger.LogWarning($"The assembly {assemblyName} was specified multiple times. Hosting startup assemblies should only be specified once."); - } - AddApplicationServices(applicationServices, hostingServiceProvider); var host = new WebHost( @@ -193,6 +186,14 @@ namespace Microsoft.AspNetCore.Hosting { host.Initialize(); + var logger = host.Services.GetRequiredService>(); + + // Warn about duplicate HostingStartupAssemblies + foreach (var assemblyName in _options.GetFinalHostingStartupAssemblies().GroupBy(a => a, StringComparer.OrdinalIgnoreCase).Where(g => g.Count() > 1)) + { + logger.LogWarning($"The assembly {assemblyName} was specified multiple times. Hosting startup assemblies should only be specified once."); + } + return host; } catch @@ -208,7 +209,7 @@ namespace Microsoft.AspNetCore.Hosting var provider = collection.BuildServiceProvider(); var factory = provider.GetService>(); - if (factory != null) + if (factory != null && !(factory is DefaultServiceProviderFactory)) { using (provider) { diff --git a/src/Hosting/Hosting/src/WebHostExtensions.cs b/src/Hosting/Hosting/src/WebHostExtensions.cs index 06a3e00cf8..e73399c419 100644 --- a/src/Hosting/Hosting/src/WebHostExtensions.cs +++ b/src/Hosting/Hosting/src/WebHostExtensions.cs @@ -45,8 +45,14 @@ namespace Microsoft.AspNetCore.Hosting { AttachCtrlcSigtermShutdown(cts, done, shutdownMessage: string.Empty); - await host.WaitForTokenShutdownAsync(cts.Token); - done.Set(); + try + { + await host.WaitForTokenShutdownAsync(cts.Token); + } + finally + { + done.Set(); + } } } @@ -80,8 +86,14 @@ namespace Microsoft.AspNetCore.Hosting var shutdownMessage = host.Services.GetRequiredService().SuppressStatusMessages ? string.Empty : "Application is shutting down..."; AttachCtrlcSigtermShutdown(cts, done, shutdownMessage: shutdownMessage); - await host.RunAsync(cts.Token, "Application started. Press Ctrl+C to shut down."); - done.Set(); + try + { + await host.RunAsync(cts.Token, "Application started. Press Ctrl+C to shut down."); + } + finally + { + done.Set(); + } } } @@ -92,7 +104,6 @@ namespace Microsoft.AspNetCore.Hosting await host.StartAsync(token); var hostingEnvironment = host.Services.GetService(); - var applicationLifetime = host.Services.GetService(); var options = host.Services.GetRequiredService(); if (!options.SuppressStatusMessages) diff --git a/src/Hosting/Server.IntegrationTesting/src/ApplicationPublisher.cs b/src/Hosting/Server.IntegrationTesting/src/ApplicationPublisher.cs new file mode 100644 index 0000000000..4d576db725 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/ApplicationPublisher.cs @@ -0,0 +1,125 @@ +// 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.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class ApplicationPublisher + { + public string ApplicationPath { get; } + + public ApplicationPublisher(string applicationPath) + { + ApplicationPath = applicationPath; + } + + public static readonly string DotnetCommandName = "dotnet"; + + public virtual Task Publish(DeploymentParameters deploymentParameters, ILogger logger) + { + var publishDirectory = CreateTempDirectory(); + using (logger.BeginScope("dotnet-publish")) + { + if (string.IsNullOrEmpty(deploymentParameters.TargetFramework)) + { + throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment"); + } + + var parameters = $"publish " + + $" --output \"{publishDirectory.FullName}\"" + + $" --framework {deploymentParameters.TargetFramework}" + + $" --configuration {deploymentParameters.Configuration}" + + " --no-restore -p:VerifyMatchingImplicitPackageVersion=false"; + // Set VerifyMatchingImplicitPackageVersion to disable errors when Microsoft.NETCore.App's version is overridden externally + // This verification doesn't matter if we are skipping restore during tests. + + if (deploymentParameters.ApplicationType == ApplicationType.Standalone) + { + parameters += $" --runtime {GetRuntimeIdentifier(deploymentParameters)}"; + } + + parameters += $" {deploymentParameters.AdditionalPublishParameters}"; + + var startInfo = new ProcessStartInfo + { + FileName = DotnetCommandName, + Arguments = parameters, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + WorkingDirectory = deploymentParameters.ApplicationPath, + }; + + ProcessHelpers.AddEnvironmentVariablesToProcess(startInfo, deploymentParameters.PublishEnvironmentVariables, logger); + + var hostProcess = new Process() { StartInfo = startInfo }; + + logger.LogInformation($"Executing command {DotnetCommandName} {parameters}"); + + hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", logger); + + // A timeout is passed to Process.WaitForExit() for two reasons: + // + // 1. When process output is read asynchronously, WaitForExit() without a timeout blocks until child processes + // are killed, which can cause hangs due to MSBuild NodeReuse child processes started by dotnet.exe. + // With a timeout, WaitForExit() returns when the parent process is killed and ignores child processes. + // https://stackoverflow.com/a/37983587/102052 + // + // 2. If "dotnet publish" does hang indefinitely for some reason, tests should fail fast with an error message. + const int timeoutMinutes = 5; + if (hostProcess.WaitForExit(milliseconds: timeoutMinutes * 60 * 1000)) + { + if (hostProcess.ExitCode != 0) + { + var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}"; + logger.LogError(message); + throw new Exception(message); + } + } + else + { + var message = $"{DotnetCommandName} publish failed to exit after {timeoutMinutes} minutes"; + logger.LogError(message); + throw new Exception(message); + } + + logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}"); + } + + return Task.FromResult(new PublishedApplication(publishDirectory.FullName, logger)); + } + + private static string GetRuntimeIdentifier(DeploymentParameters deploymentParameters) + { + var architecture = deploymentParameters.RuntimeArchitecture; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "win7-" + architecture; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "linux-" + architecture; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "osx-" + architecture; + } + throw new InvalidOperationException("Unrecognized operation system platform"); + } + + protected static DirectoryInfo CreateTempDirectory() + { + var tempPath = Path.GetTempPath() + Guid.NewGuid().ToString("N"); + var target = new DirectoryInfo(tempPath); + target.Create(); + return target; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/CachingApplicationPublisher.cs b/src/Hosting/Server.IntegrationTesting/src/CachingApplicationPublisher.cs new file mode 100644 index 0000000000..e32d8e22db --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/CachingApplicationPublisher.cs @@ -0,0 +1,96 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class CachingApplicationPublisher: ApplicationPublisher, IDisposable + { + private readonly Dictionary _publishCache = new Dictionary(); + + public CachingApplicationPublisher(string applicationPath) : base(applicationPath) + { + } + + public override async Task Publish(DeploymentParameters deploymentParameters, ILogger logger) + { + if (ApplicationPath != deploymentParameters.ApplicationPath) + { + throw new InvalidOperationException("ApplicationPath mismatch"); + } + + if (deploymentParameters.PublishEnvironmentVariables.Any()) + { + throw new InvalidOperationException("DeploymentParameters.PublishEnvironmentVariables not supported"); + } + + if (!string.IsNullOrEmpty(deploymentParameters.PublishedApplicationRootPath)) + { + throw new InvalidOperationException("DeploymentParameters.PublishedApplicationRootPath not supported"); + } + + var dotnetPublishParameters = new DotnetPublishParameters + { + TargetFramework = deploymentParameters.TargetFramework, + Configuration = deploymentParameters.Configuration, + ApplicationType = deploymentParameters.ApplicationType, + RuntimeArchitecture = deploymentParameters.RuntimeArchitecture + }; + + if (!_publishCache.TryGetValue(dotnetPublishParameters, out var publishedApplication)) + { + publishedApplication = await base.Publish(deploymentParameters, logger); + _publishCache.Add(dotnetPublishParameters, publishedApplication); + } + + return new PublishedApplication(CopyPublishedOutput(publishedApplication, logger), logger); + } + + private string CopyPublishedOutput(PublishedApplication application, ILogger logger) + { + var target = CreateTempDirectory(); + + var source = new DirectoryInfo(application.Path); + CopyFiles(source, target, logger); + return target.FullName; + } + + public static void CopyFiles(DirectoryInfo source, DirectoryInfo target, ILogger logger) + { + foreach (DirectoryInfo directoryInfo in source.GetDirectories()) + { + CopyFiles(directoryInfo, target.CreateSubdirectory(directoryInfo.Name), logger); + } + + logger.LogDebug($"Processing {target.FullName}"); + foreach (FileInfo fileInfo in source.GetFiles()) + { + logger.LogDebug($" Copying {fileInfo.Name}"); + var destFileName = Path.Combine(target.FullName, fileInfo.Name); + fileInfo.CopyTo(destFileName); + } + } + + public void Dispose() + { + foreach (var publishedApp in _publishCache.Values) + { + publishedApp.Dispose(); + } + } + + private struct DotnetPublishParameters + { + public string TargetFramework { get; set; } + public string Configuration { get; set; } + public ApplicationType ApplicationType { get; set; } + public RuntimeArchitecture RuntimeArchitecture { get; set; } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/ANCMVersion.cs b/src/Hosting/Server.IntegrationTesting/src/Common/ANCMVersion.cs new file mode 100644 index 0000000000..38cf93ffcd --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/ANCMVersion.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public enum AncmVersion + { + None, + AspNetCoreModule, + AspNetCoreModuleV2 + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/ApplicationType.cs b/src/Hosting/Server.IntegrationTesting/src/Common/ApplicationType.cs index 02d8715984..3a8afeb94d 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/ApplicationType.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/ApplicationType.cs @@ -5,7 +5,14 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { public enum ApplicationType { + /// + /// Does not target a specific platform. Requires the matching runtime to be installed. + /// Portable, + + /// + /// All dlls are published with the app for x-copy deploy. Net461 requires this because ASP.NET Core is not in the GAC. + /// Standalone } } diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentParameters.cs b/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentParameters.cs index 3d824741c3..5e347e4933 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentParameters.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/DeploymentParameters.cs @@ -13,6 +13,35 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// public class DeploymentParameters { + public DeploymentParameters() + { + EnvironmentVariables["ASPNETCORE_DETAILEDERRORS"] = "true"; + + var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute(); + if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration)) + { + Configuration = configAttribute.Configuration; + } + } + + public DeploymentParameters(TestVariant variant) + { + EnvironmentVariables["ASPNETCORE_DETAILEDERRORS"] = "true"; + + var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute(); + if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration)) + { + Configuration = configAttribute.Configuration; + } + + ServerType = variant.Server; + TargetFramework = variant.Tfm; + ApplicationType = variant.ApplicationType; + RuntimeArchitecture = variant.Architecture; + HostingModel = variant.HostingModel; + AncmVersion = variant.AncmVersion; + } + /// /// Creates an instance of . /// @@ -36,11 +65,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting throw new DirectoryNotFoundException(string.Format("Application path {0} does not exist.", applicationPath)); } - if (runtimeArchitecture == RuntimeArchitecture.x86 && runtimeFlavor == RuntimeFlavor.CoreClr) - { - throw new NotSupportedException("32 bit deployment is not yet supported for CoreCLR. Don't remove the tests, just disable them for now."); - } - ApplicationPath = applicationPath; ApplicationName = new DirectoryInfo(ApplicationPath).Name; ServerType = serverType; @@ -54,11 +78,34 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting } } - public ServerType ServerType { get; } + public DeploymentParameters(DeploymentParameters parameters) + { + foreach (var propertyInfo in typeof(DeploymentParameters).GetProperties()) + { + if (propertyInfo.CanWrite) + { + propertyInfo.SetValue(this, propertyInfo.GetValue(parameters)); + } + } - public RuntimeFlavor RuntimeFlavor { get; } + foreach (var kvp in parameters.EnvironmentVariables) + { + EnvironmentVariables.Add(kvp); + } - public RuntimeArchitecture RuntimeArchitecture { get; } = RuntimeArchitecture.x64; + foreach (var kvp in parameters.PublishEnvironmentVariables) + { + PublishEnvironmentVariables.Add(kvp); + } + } + + public ApplicationPublisher ApplicationPublisher { get; set; } + + public ServerType ServerType { get; set; } + + public RuntimeFlavor RuntimeFlavor { get; set; } + + public RuntimeArchitecture RuntimeArchitecture { get; set; } = RuntimeArchitecture.x64; /// /// Suggested base url for the deployed application. The final deployed url could be @@ -67,15 +114,20 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// public string ApplicationBaseUriHint { get; set; } + /// + /// Scheme used by the deployed application if is empty. + /// + public string Scheme { get; set; } = Uri.UriSchemeHttp; + public string EnvironmentName { get; set; } public string ServerConfigTemplateContent { get; set; } public string ServerConfigLocation { get; set; } - public string SiteName { get; set; } + public string SiteName { get; set; } = "HttpTestSite"; - public string ApplicationPath { get; } + public string ApplicationPath { get; set; } /// /// Gets or sets the name of the application. This is used to execute the application when deployed. @@ -95,11 +147,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// public string AdditionalPublishParameters { get; set; } - /// - /// Publish restores by default, this property opts out by default. - /// - public bool RestoreOnPublish { get; set; } - /// /// To publish the application before deployment. /// @@ -115,6 +162,12 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting public HostingModel HostingModel { get; set; } + /// + /// When using the IISExpressDeployer, determines whether to use the older or newer version + /// of ANCM. + /// + public AncmVersion AncmVersion { get; set; } = AncmVersion.AspNetCoreModule; + /// /// Environment variables to be set before starting the host. /// Not applicable for IIS Scenarios. diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/DotNetCommands.cs b/src/Hosting/Server.IntegrationTesting/src/Common/DotNetCommands.cs new file mode 100644 index 0000000000..e30e9defc5 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/DotNetCommands.cs @@ -0,0 +1,71 @@ +// 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.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public static class DotNetCommands + { + private const string _dotnetFolderName = ".dotnet"; + + internal static string DotNetHome { get; } = GetDotNetHome(); + + // Compare to https://github.com/aspnet/BuildTools/blob/314c98e4533217a841ff9767bb38e144eb6c93e4/tools/KoreBuild.Console/Commands/CommandContext.cs#L76 + public static string GetDotNetHome() + { + var dotnetHome = Environment.GetEnvironmentVariable("DOTNET_HOME"); + var userProfile = Environment.GetEnvironmentVariable("USERPROFILE"); + var home = Environment.GetEnvironmentVariable("HOME"); + + var result = Path.Combine(Directory.GetCurrentDirectory(), _dotnetFolderName); + if (!string.IsNullOrEmpty(dotnetHome)) + { + result = dotnetHome; + } + else if (!string.IsNullOrEmpty(userProfile)) + { + result = Path.Combine(userProfile, _dotnetFolderName); + } + else if (!string.IsNullOrEmpty(home)) + { + result = home; + } + + return result; + } + + public static string GetDotNetInstallDir(RuntimeArchitecture arch) + { + var dotnetDir = DotNetHome; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + dotnetDir = Path.Combine(dotnetDir, arch.ToString()); + } + + return dotnetDir; + } + + public static string GetDotNetExecutable(RuntimeArchitecture arch) + { + var dotnetDir = GetDotNetInstallDir(arch); + + var dotnetFile = "dotnet"; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + dotnetFile += ".exe"; + } + + return Path.Combine(dotnetDir, dotnetFile); + } + + public static bool IsRunningX86OnX64(RuntimeArchitecture arch) + { + return (RuntimeInformation.OSArchitecture == Architecture.X64 || RuntimeInformation.OSArchitecture == Architecture.Arm64) + && arch == RuntimeArchitecture.x86; + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs b/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs index 1eb1aea8c0..5eea2b8ce3 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/HostingModel.cs @@ -3,8 +3,12 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { + /// + /// For ANCM + /// public enum HostingModel { + None, OutOfProcess, InProcess } diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/IWebHostExtensions.cs b/src/Hosting/Server.IntegrationTesting/src/Common/IWebHostExtensions.cs new file mode 100644 index 0000000000..732a598ab8 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/IWebHostExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Hosting.Server.Features; +using System.Linq; + +namespace Microsoft.AspNetCore.Hosting +{ + public static class IWebHostExtensions + { + public static string GetAddress(this IWebHost host) + { + return host.ServerFeatures.Get().Addresses.First(); + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeFlavor.cs b/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeFlavor.cs index 510c713f59..3d65f0eaca 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeFlavor.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/RuntimeFlavor.cs @@ -5,7 +5,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { public enum RuntimeFlavor { - Clr, - CoreClr + None, + CoreClr, + Clr } } diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/ServerType.cs b/src/Hosting/Server.IntegrationTesting/src/Common/ServerType.cs index 060c8ed0ca..ac84d0225f 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/ServerType.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/ServerType.cs @@ -5,9 +5,10 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { public enum ServerType { + None, IISExpress, IIS, - WebListener, + HttpSys, Kestrel, Nginx } diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/TestPortHelper.cs b/src/Hosting/Server.IntegrationTesting/src/Common/TestPortHelper.cs new file mode 100644 index 0000000000..05c6080cd5 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/TestPortHelper.cs @@ -0,0 +1,60 @@ +// 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.Net; +using System.Net.Sockets; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common +{ + public static class TestPortHelper + { + // Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508 + // + // This method is an attempt to safely get a free port from the OS. Most of the time, + // when binding to dynamic port "0" the OS increments the assigned port, so it's safe + // to re-use the assigned port in another process. However, occasionally the OS will reuse + // a recently assigned port instead of incrementing, which causes flaky tests with AddressInUse + // exceptions. This method should only be used when the application itself cannot use + // dynamic port "0" (e.g. IISExpress). Most functional tests using raw Kestrel + // (with status messages enabled) should directly bind to dynamic port "0" and scrape + // the assigned port from the status message, which should be 100% reliable since the port + // is bound once and never released. + public static int GetNextPort() + { + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + return ((IPEndPoint)socket.LocalEndPoint).Port; + } + } + + // IIS Express preregisteres 44300-44399 ports with SSL bindings. + // So some tests always have to use ports in this range, and we can't rely on OS-allocated ports without a whole lot of ceremony around + // creating self-signed certificates and registering SSL bindings with HTTP.sys + public static int GetNextSSLPort() + { + var next = 44300; + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + while (true) + { + try + { + var port = next++; + socket.Bind(new IPEndPoint(IPAddress.Loopback, port)); + return port; + } + catch (SocketException) + { + // Retry unless exhausted + if (next > 44399) + { + throw; + } + } + } + } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs b/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs index 7ebcb30367..5a79a31cef 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Common/TestUriHelper.cs @@ -2,27 +2,24 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Net; -using System.Net.Sockets; - namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common { public static class TestUriHelper { - public static Uri BuildTestUri() + public static Uri BuildTestUri(ServerType serverType) { - return BuildTestUri(null); + return BuildTestUri(serverType, hint: null); } - public static Uri BuildTestUri(string hint) + public static Uri BuildTestUri(ServerType serverType, string hint) { - // If this method is called directly, there is no way to know the server type or whether status messages - // are enabled. It's safest to assume the server is WebListener (which doesn't support binding to dynamic - // port "0") and status messages are not enabled (so the assigned port cannot be scraped from console output). - return BuildTestUri(hint, serverType: ServerType.WebListener, statusMessagesEnabled: false); + // Assume status messages are enabled for Kestrel and disabled for all other servers. + var statusMessagesEnabled = (serverType == ServerType.Kestrel); + + return BuildTestUri(serverType, Uri.UriSchemeHttp, hint, statusMessagesEnabled); } - internal static Uri BuildTestUri(string hint, ServerType serverType, bool statusMessagesEnabled) + internal static Uri BuildTestUri(ServerType serverType, string scheme, string hint, bool statusMessagesEnabled) { if (string.IsNullOrEmpty(hint)) { @@ -33,7 +30,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common // once and never released. Binding to dynamic port "0" on "localhost" (both IPv4 and IPv6) is not // supported, so the port is only bound on "127.0.0.1" (IPv4). If a test explicitly requires IPv6, // it should provide a hint URL with "localhost" (IPv4 and IPv6) or "[::1]" (IPv6-only). - return new UriBuilder("http", "127.0.0.1", 0).Uri; + return new UriBuilder(scheme, "127.0.0.1", 0).Uri; } else { @@ -41,7 +38,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common // from which to scrape the assigned port, so the less reliable GetNextPort() must be used. The // port is bound on "localhost" (both IPv4 and IPv6), since this is supported when using a specific // (non-zero) port. - return new UriBuilder("http", "localhost", GetNextPort()).Uri; + return new UriBuilder(scheme, "localhost", TestPortHelper.GetNextPort()).Uri; } } else @@ -52,7 +49,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common // Only a few tests use this codepath, so it's fine to use the less reliable GetNextPort() for simplicity. // The tests using this codepath will be reviewed to see if they can be changed to directly bind to dynamic // port "0" on "127.0.0.1" and scrape the assigned port from the status message (the default codepath). - return new UriBuilder(uriHint) { Port = GetNextPort() }.Uri; + return new UriBuilder(uriHint) { Port = TestPortHelper.GetNextPort() }.Uri; } else { @@ -61,25 +58,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common } } } - - // Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508 - // - // This method is an attempt to safely get a free port from the OS. Most of the time, - // when binding to dynamic port "0" the OS increments the assigned port, so it's safe - // to re-use the assigned port in another process. However, occasionally the OS will reuse - // a recently assigned port instead of incrementing, which causes flaky tests with AddressInUse - // exceptions. This method should only be used when the application itself cannot use - // dynamic port "0" (e.g. IISExpress). Most functional tests using raw Kestrel - // (with status messages enabled) should directly bind to dynamic port "0" and scrape - // the assigned port from the status message, which should be 100% reliable since the port - // is bound once and never released. - public static int GetNextPort() - { - using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) - { - socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - return ((IPEndPoint)socket.LocalEndPoint).Port; - } - } } } diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/TestUrlHelper.cs b/src/Hosting/Server.IntegrationTesting/src/Common/TestUrlHelper.cs new file mode 100644 index 0000000000..f03321eba6 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/TestUrlHelper.cs @@ -0,0 +1,11 @@ +namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common +{ + // Public for use in other test projects + public static class TestUrlHelper + { + public static string GetTestUrl(ServerType serverType) + { + return TestUriHelper.BuildTestUri(serverType).ToString(); + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs b/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs new file mode 100644 index 0000000000..56715d3c08 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Common/Tfm.cs @@ -0,0 +1,20 @@ +// 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; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public static class Tfm + { + public const string Net461 = "net461"; + public const string NetCoreApp20 = "netcoreapp2.0"; + public const string NetCoreApp21 = "netcoreapp2.1"; + public const string NetCoreApp22 = "netcoreapp2.2"; + + public static bool Matches(string tfm1, string tfm2) + { + return string.Equals(tfm1, tfm2, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployer.cs index 3c81f32902..5e1457e70f 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployer.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployer.cs @@ -16,88 +16,73 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// /// Abstract base class of all deployers with implementation of some of the common helpers. /// - public abstract class ApplicationDeployer : IApplicationDeployer + public abstract class ApplicationDeployer : IDisposable { public static readonly string DotnetCommandName = "dotnet"; - // This is the argument that separates the dotnet arguments for the args being passed to the - // app being run when running dotnet run - public static readonly string DotnetArgumentSeparator = "--"; - private readonly Stopwatch _stopwatch = new Stopwatch(); + private PublishedApplication _publishedApplication; + public ApplicationDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) { DeploymentParameters = deploymentParameters; LoggerFactory = loggerFactory; Logger = LoggerFactory.CreateLogger(GetType().FullName); + + ValidateParameters(); + } + + private void ValidateParameters() + { + if (DeploymentParameters.ServerType == ServerType.None) + { + throw new ArgumentException($"Invalid ServerType '{DeploymentParameters.ServerType}'."); + } + + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.None && !string.IsNullOrEmpty(DeploymentParameters.TargetFramework)) + { + DeploymentParameters.RuntimeFlavor = GetRuntimeFlavor(DeploymentParameters.TargetFramework); + } + + if (string.IsNullOrEmpty(DeploymentParameters.ApplicationPath)) + { + throw new ArgumentException("ApplicationPath cannot be null."); + } + + if (!Directory.Exists(DeploymentParameters.ApplicationPath)) + { + throw new DirectoryNotFoundException(string.Format("Application path {0} does not exist.", DeploymentParameters.ApplicationPath)); + } + + if (string.IsNullOrEmpty(DeploymentParameters.ApplicationName)) + { + DeploymentParameters.ApplicationName = new DirectoryInfo(DeploymentParameters.ApplicationPath).Name; + } + } + + private RuntimeFlavor GetRuntimeFlavor(string tfm) + { + if (Tfm.Matches(Tfm.Net461, tfm)) + { + return RuntimeFlavor.Clr; + } + return RuntimeFlavor.CoreClr; } protected DeploymentParameters DeploymentParameters { get; } protected ILoggerFactory LoggerFactory { get; } + protected ILogger Logger { get; } public abstract Task DeployAsync(); protected void DotnetPublish(string publishRoot = null) { - using (Logger.BeginScope("dotnet-publish")) - { - if (string.IsNullOrEmpty(DeploymentParameters.TargetFramework)) - { - throw new Exception($"A target framework must be specified in the deployment parameters for applications that require publishing before deployment"); - } - - DeploymentParameters.PublishedApplicationRootPath = publishRoot ?? Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - - var parameters = $"publish " - + $" --output \"{DeploymentParameters.PublishedApplicationRootPath}\"" - + $" --framework {DeploymentParameters.TargetFramework}" - + $" --configuration {DeploymentParameters.Configuration}" - + (DeploymentParameters.RestoreOnPublish - ? string.Empty - : " --no-restore -p:VerifyMatchingImplicitPackageVersion=false"); - // Set VerifyMatchingImplicitPackageVersion to disable errors when Microsoft.NETCore.App's version is overridden externally - // This verification doesn't matter if we are skipping restore during tests. - - if (DeploymentParameters.ApplicationType == ApplicationType.Standalone) - { - parameters += $" --runtime {GetRuntimeIdentifier()}"; - } - - parameters += $" {DeploymentParameters.AdditionalPublishParameters}"; - - var startInfo = new ProcessStartInfo - { - FileName = DotnetCommandName, - Arguments = parameters, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - WorkingDirectory = DeploymentParameters.ApplicationPath, - }; - - AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.PublishEnvironmentVariables); - - var hostProcess = new Process() { StartInfo = startInfo }; - - Logger.LogInformation($"Executing command {DotnetCommandName} {parameters}"); - - hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", Logger); - - hostProcess.WaitForExit(); - - if (hostProcess.ExitCode != 0) - { - var message = $"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}"; - Logger.LogError(message); - throw new Exception(message); - } - - Logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}"); - } + var publisher = DeploymentParameters.ApplicationPublisher ?? new ApplicationPublisher(DeploymentParameters.ApplicationPath); + _publishedApplication = publisher.Publish(DeploymentParameters, Logger).GetAwaiter().GetResult(); + DeploymentParameters.PublishedApplicationRootPath = _publishedApplication.Path; } protected void CleanPublishedOutput() @@ -112,15 +97,27 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting } else { - RetryHelper.RetryOperation( - () => Directory.Delete(DeploymentParameters.PublishedApplicationRootPath, true), - e => Logger.LogWarning($"Failed to delete directory : {e.Message}"), - retryCount: 3, - retryDelayMilliseconds: 100); + _publishedApplication.Dispose(); } } } + protected string GetDotNetExeForArchitecture() + { + var executableName = DotnetCommandName; + // We expect x64 dotnet.exe to be on the path but we have to go searching for the x86 version. + if (DotNetCommands.IsRunningX86OnX64(DeploymentParameters.RuntimeArchitecture)) + { + executableName = DotNetCommands.GetDotNetExecutable(DeploymentParameters.RuntimeArchitecture); + if (!File.Exists(executableName)) + { + throw new Exception($"Unable to find '{executableName}'.'"); + } + } + + return executableName; + } + protected void ShutDownIfAnyHostProcess(Process hostProcess) { if (hostProcess != null && !hostProcess.HasExited) @@ -147,26 +144,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting protected void AddEnvironmentVariablesToProcess(ProcessStartInfo startInfo, IDictionary environmentVariables) { var environment = startInfo.Environment; - SetEnvironmentVariable(environment, "ASPNETCORE_ENVIRONMENT", DeploymentParameters.EnvironmentName); - - foreach (var environmentVariable in environmentVariables) - { - SetEnvironmentVariable(environment, environmentVariable.Key, environmentVariable.Value); - } - } - - protected void SetEnvironmentVariable(IDictionary environment, string name, string value) - { - if (value == null) - { - Logger.LogInformation("Removing environment variable {name}", name); - environment.Remove(name); - } - else - { - Logger.LogInformation("SET {name}={value}", name, value); - environment[name] = value; - } + ProcessHelpers.SetEnvironmentVariable(environment, "ASPNETCORE_ENVIRONMENT", DeploymentParameters.EnvironmentName, Logger); + ProcessHelpers.AddEnvironmentVariablesToProcess(startInfo, environmentVariables, Logger); } protected void InvokeUserApplicationCleanup() @@ -214,39 +193,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting } public abstract void Dispose(); - - private string GetRuntimeIdentifier() - { - var architecture = GetArchitecture(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "win7-" + architecture; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "linux-" + architecture; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "osx-" + architecture; - } - else - { - throw new InvalidOperationException("Unrecognized operation system platform"); - } - } - - private string GetArchitecture() - { - switch (RuntimeInformation.OSArchitecture) - { - case Architecture.X86: - return "x86"; - case Architecture.X64: - return "x64"; - default: - throw new NotSupportedException($"Unsupported architecture: {RuntimeInformation.OSArchitecture}"); - } - } } } diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployerFactory.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployerFactory.cs index eb66761807..959f4ffeed 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployerFactory.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/ApplicationDeployerFactory.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting /// /// /// - public static IApplicationDeployer Create(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) + public static ApplicationDeployer Create(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) { if (deploymentParameters == null) { @@ -32,10 +32,9 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting switch (deploymentParameters.ServerType) { case ServerType.IISExpress: - return new IISExpressDeployer(deploymentParameters, loggerFactory); case ServerType.IIS: - throw new NotSupportedException("The IIS deployer is no longer supported"); - case ServerType.WebListener: + throw new NotSupportedException("Use Microsoft.AspNetCore.Server.IntegrationTesting.IIS package and IISApplicationDeployerFactory for IIS support."); + case ServerType.HttpSys: case ServerType.Kestrel: return new SelfHostDeployer(deploymentParameters, loggerFactory); case ServerType.Nginx: diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/IApplicationDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/IApplicationDeployer.cs deleted file mode 100644 index 400ae978ed..0000000000 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/IApplicationDeployer.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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.Tasks; - -namespace Microsoft.AspNetCore.Server.IntegrationTesting -{ - /// - /// Common operations on an application deployer. - /// - public interface IApplicationDeployer : IDisposable - { - /// - /// Deploys the application to the target with specified . - /// - /// - Task DeployAsync(); - } -} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/IISExpressDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/IISExpressDeployer.cs deleted file mode 100644 index bc7aecb700..0000000000 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/IISExpressDeployer.cs +++ /dev/null @@ -1,317 +0,0 @@ -// 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.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.AspNetCore.Server.IntegrationTesting.Common; -using Microsoft.AspNetCore.Testing; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.IntegrationTesting -{ - /// - /// Deployment helper for IISExpress. - /// - public class IISExpressDeployer : ApplicationDeployer - { - private const string IISExpressRunningMessage = "IIS Express is running."; - private const string FailedToInitializeBindingsMessage = "Failed to initialize site bindings"; - private const string UnableToStartIISExpressMessage = "Unable to start iisexpress."; - private const int MaximumAttempts = 5; - - private static readonly Regex UrlDetectorRegex = new Regex(@"^\s*Successfully registered URL ""(?[^""]+)"" for site.*$"); - - private Process _hostProcess; - - public IISExpressDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) - : base(deploymentParameters, loggerFactory) - { - } - - public bool IsWin8OrLater - { - get - { - var win8Version = new Version(6, 2); - - return (Environment.OSVersion.Version >= win8Version); - } - } - - public bool Is64BitHost - { - get - { - return Environment.Is64BitOperatingSystem; - } - } - - public override async Task DeployAsync() - { - using (Logger.BeginScope("Deployment")) - { - // Start timer - StartTimer(); - - // For now we always auto-publish. Otherwise we'll have to write our own local web.config for the HttpPlatformHandler - DeploymentParameters.PublishApplicationBeforeDeployment = true; - if (DeploymentParameters.PublishApplicationBeforeDeployment) - { - DotnetPublish(); - } - - var contentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath; - - var testUri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); - - // Launch the host process. - var (actualUri, hostExitToken) = await StartIISExpressAsync(testUri, contentRoot); - - Logger.LogInformation("Application ready at URL: {appUrl}", actualUri); - - // Right now this works only for urls like http://localhost:5001/. Does not work for http://localhost:5001/subpath. - return new DeploymentResult( - LoggerFactory, - DeploymentParameters, - applicationBaseUri: actualUri.ToString(), - contentRoot: contentRoot, - hostShutdownToken: hostExitToken); - } - } - - private async Task<(Uri url, CancellationToken hostExitToken)> StartIISExpressAsync(Uri uri, string contentRoot) - { - using (Logger.BeginScope("StartIISExpress")) - { - var port = uri.Port; - if (port == 0) - { - port = TestUriHelper.GetNextPort(); - } - - for (var attempt = 0; attempt < MaximumAttempts; attempt++) - { - Logger.LogInformation("Attempting to start IIS Express on port: {port}", port); - - if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigTemplateContent)) - { - var serverConfig = DeploymentParameters.ServerConfigTemplateContent; - - // Pass on the applicationhost.config to iis express. With this don't need to pass in the /path /port switches as they are in the applicationHost.config - // We take a copy of the original specified applicationHost.Config to prevent modifying the one in the repo. - - if (serverConfig.Contains("[ANCMPath]")) - { - // We need to pick the bitness based the OS / IIS Express, not the application. - // We'll eventually add support for choosing which IIS Express bitness to run: https://github.com/aspnet/Hosting/issues/880 - var ancmFile = Path.Combine(contentRoot, Is64BitHost ? @"x64\aspnetcore.dll" : @"x86\aspnetcore.dll"); - // Bin deployed by Microsoft.AspNetCore.AspNetCoreModule.nupkg - - if (!File.Exists(Environment.ExpandEnvironmentVariables(ancmFile))) - { - throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmFile); - } - - Logger.LogDebug("Writing ANCMPath '{ancmPath}' to config", ancmFile); - serverConfig = - serverConfig.Replace("[ANCMPath]", ancmFile); - } - - Logger.LogDebug("Writing ApplicationPhysicalPath '{applicationPhysicalPath}' to config", contentRoot); - Logger.LogDebug("Writing Port '{port}' to config", port); - serverConfig = - serverConfig - .Replace("[ApplicationPhysicalPath]", contentRoot) - .Replace("[PORT]", port.ToString()); - - DeploymentParameters.ServerConfigLocation = Path.GetTempFileName(); - - if (serverConfig.Contains("[HostingModel]")) - { - var hostingModel = DeploymentParameters.HostingModel.ToString(); - serverConfig.Replace("[HostingModel]", hostingModel); - Logger.LogDebug("Writing HostingModel '{hostingModel}' to config", hostingModel); - } - - Logger.LogDebug("Saving Config to {configPath}", DeploymentParameters.ServerConfigLocation); - - if (Logger.IsEnabled(LogLevel.Trace)) - { - Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", serverConfig); - } - - File.WriteAllText(DeploymentParameters.ServerConfigLocation, serverConfig); - } - - if (DeploymentParameters.HostingModel == HostingModel.InProcess) - { - ModifyWebConfigToInProcess(); - } - - var parameters = string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) ? - string.Format("/port:{0} /path:\"{1}\" /trace:error", uri.Port, contentRoot) : - string.Format("/site:{0} /config:{1} /trace:error", DeploymentParameters.SiteName, DeploymentParameters.ServerConfigLocation); - - var iisExpressPath = GetIISExpressPath(); - - Logger.LogInformation("Executing command : {iisExpress} {parameters}", iisExpressPath, parameters); - - var startInfo = new ProcessStartInfo - { - FileName = iisExpressPath, - Arguments = parameters, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true - }; - - AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); - - Uri url = null; - var started = new TaskCompletionSource(); - - var process = new Process() { StartInfo = startInfo }; - process.OutputDataReceived += (sender, dataArgs) => - { - if (string.Equals(dataArgs.Data, UnableToStartIISExpressMessage)) - { - // We completely failed to start and we don't really know why - started.TrySetException(new InvalidOperationException("Failed to start IIS Express")); - } - else if (string.Equals(dataArgs.Data, FailedToInitializeBindingsMessage)) - { - started.TrySetResult(false); - } - else if (string.Equals(dataArgs.Data, IISExpressRunningMessage)) - { - started.TrySetResult(true); - } - else if (!string.IsNullOrEmpty(dataArgs.Data)) - { - var m = UrlDetectorRegex.Match(dataArgs.Data); - if (m.Success) - { - url = new Uri(m.Groups["url"].Value); - } - } - }; - - process.EnableRaisingEvents = true; - var hostExitTokenSource = new CancellationTokenSource(); - process.Exited += (sender, e) => - { - Logger.LogInformation("iisexpress Process {pid} shut down", process.Id); - - // If TrySetResult was called above, this will just silently fail to set the new state, which is what we want - started.TrySetException(new Exception($"Command exited unexpectedly with exit code: {process.ExitCode}")); - - TriggerHostShutdown(hostExitTokenSource); - }; - process.StartAndCaptureOutAndErrToLogger("iisexpress", Logger); - Logger.LogInformation("iisexpress Process {pid} started", process.Id); - - if (process.HasExited) - { - Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, process.Id, process.ExitCode); - throw new Exception("Failed to start host"); - } - - // Wait for the app to start - // The timeout here is large, because we don't know how long the test could need - // We cover a lot of error cases above, but I want to make sure we eventually give up and don't hang the build - // just in case we missed one -anurse - if (!await started.Task.TimeoutAfter(TimeSpan.FromMinutes(10))) - { - Logger.LogInformation("iisexpress Process {pid} failed to bind to port {port}, trying again", _hostProcess.Id, port); - - // Wait for the process to exit and try again - process.WaitForExit(30 * 1000); - await Task.Delay(1000); // Wait a second to make sure the socket is completely cleaned up - } - else - { - _hostProcess = process; - Logger.LogInformation("Started iisexpress successfully. Process Id : {processId}, Port: {port}", _hostProcess.Id, port); - return (url: url, hostExitToken: hostExitTokenSource.Token); - } - } - - var message = $"Failed to initialize IIS Express after {MaximumAttempts} attempts to select a port"; - Logger.LogError(message); - throw new TimeoutException(message); - } - } - - private string GetIISExpressPath() - { - // Get path to program files - var iisExpressPath = Path.Combine(Environment.GetEnvironmentVariable("SystemDrive") + "\\", "Program Files", "IIS Express", "iisexpress.exe"); - - if (!File.Exists(iisExpressPath)) - { - throw new Exception("Unable to find IISExpress on the machine: " + iisExpressPath); - } - - return iisExpressPath; - } - - public override void Dispose() - { - using (Logger.BeginScope("Dispose")) - { - ShutDownIfAnyHostProcess(_hostProcess); - - if (!string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) - && File.Exists(DeploymentParameters.ServerConfigLocation)) - { - // Delete the temp applicationHostConfig that we created. - Logger.LogDebug("Deleting applicationHost.config file from {configLocation}", DeploymentParameters.ServerConfigLocation); - try - { - File.Delete(DeploymentParameters.ServerConfigLocation); - } - catch (Exception exception) - { - // Ignore delete failures - just write a log. - Logger.LogWarning("Failed to delete '{config}'. Exception : {exception}", DeploymentParameters.ServerConfigLocation, exception.Message); - } - } - - if (DeploymentParameters.PublishApplicationBeforeDeployment) - { - CleanPublishedOutput(); - } - - InvokeUserApplicationCleanup(); - - StopTimer(); - } - - // If by this point, the host process is still running (somehow), throw an error. - // A test failure is better than a silent hang and unknown failure later on - if (_hostProcess != null && !_hostProcess.HasExited) - { - throw new Exception($"iisexpress Process {_hostProcess.Id} failed to shutdown"); - } - } - - // Transforms the web.config file to include the hostingModel="inprocess" element - // and adds the server type = Microsoft.AspNetServer.IIS such that Kestrel isn't added again in ServerTests - private void ModifyWebConfigToInProcess() - { - var webConfigFile = $"{DeploymentParameters.PublishedApplicationRootPath}/web.config"; - var config = XDocument.Load(webConfigFile); - var element = config.Descendants("aspNetCore").FirstOrDefault(); - element.SetAttributeValue("hostingModel", "inprocess"); - config.Save(webConfigFile); - } - } -} diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs index 1bc6c4b766..262ff80ce8 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/NginxDeployer.cs @@ -4,7 +4,10 @@ using System; using System.Diagnostics; using System.IO; +using System.Net; using System.Net.Http; +using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.Extensions.Logging; @@ -18,6 +21,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { private string _configFile; private readonly int _waitTime = (int)TimeSpan.FromSeconds(30).TotalMilliseconds; + private Socket _portSelector; public NginxDeployer(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory) : base(deploymentParameters, loggerFactory) @@ -29,11 +33,37 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting using (Logger.BeginScope("Deploy")) { _configFile = Path.GetTempFileName(); + var uri = string.IsNullOrEmpty(DeploymentParameters.ApplicationBaseUriHint) ? - TestUriHelper.BuildTestUri() : + new Uri("http://localhost:0") : new Uri(DeploymentParameters.ApplicationBaseUriHint); - var redirectUri = TestUriHelper.BuildTestUri(); + if (uri.Port == 0) + { + var builder = new UriBuilder(uri); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // This works with nginx 1.9.1 and later using the reuseport flag, available on Ubuntu 16.04. + // Keep it open so nobody else claims the port + _portSelector = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _portSelector.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + builder.Port = ((IPEndPoint)_portSelector.LocalEndPoint).Port; + } + else + { + builder.Port = TestPortHelper.GetNextPort(); + } + uri = builder.Uri; + } + + var redirectUri = TestUriHelper.BuildTestUri(ServerType.Nginx); + + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr + && DeploymentParameters.ApplicationType == ApplicationType.Standalone) + { + // Publish is required to get the correct files in the output directory + DeploymentParameters.PublishApplicationBeforeDeployment = true; + } if (DeploymentParameters.PublishApplicationBeforeDeployment) { @@ -70,19 +100,51 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting } } + private string GetUserName() + { + var retVal = Environment.GetEnvironmentVariable("LOGNAME") + ?? Environment.GetEnvironmentVariable("USER") + ?? Environment.GetEnvironmentVariable("USERNAME"); + + if (!string.IsNullOrEmpty(retVal)) + { + return retVal; + } + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using (var process = new Process + { + StartInfo = + { + FileName = "whoami", + RedirectStandardOutput = true, + } + }) + { + process.Start(); + process.WaitForExit(10_000); + return process.StandardOutput.ReadToEnd(); + } + } + + return null; + } + private void SetupNginx(string redirectUri, Uri originalUri) { using (Logger.BeginScope("SetupNginx")) { + var userName = GetUserName() ?? throw new InvalidOperationException("Could not identify the current username"); // copy nginx.conf template and replace pertinent information var pidFile = Path.Combine(DeploymentParameters.ApplicationPath, $"{Guid.NewGuid()}.nginx.pid"); var errorLog = Path.Combine(DeploymentParameters.ApplicationPath, "nginx.error.log"); var accessLog = Path.Combine(DeploymentParameters.ApplicationPath, "nginx.access.log"); DeploymentParameters.ServerConfigTemplateContent = DeploymentParameters.ServerConfigTemplateContent - .Replace("[user]", Environment.GetEnvironmentVariable("LOGNAME")) + .Replace("[user]", userName) .Replace("[errorlog]", errorLog) .Replace("[accesslog]", accessLog) - .Replace("[listenPort]", originalUri.Port.ToString()) + .Replace("[listenPort]", originalUri.Port.ToString() + (_portSelector != null ? " reuseport" : "")) .Replace("[redirectUri]", redirectUri) .Replace("[pidFile]", pidFile); Logger.LogDebug("Using PID file: {pidFile}", pidFile); @@ -110,13 +172,14 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { runNginx.StartAndCaptureOutAndErrToLogger("nginx start", Logger); runNginx.WaitForExit(_waitTime); + if (runNginx.ExitCode != 0) { - throw new Exception("Failed to start nginx"); + throw new InvalidOperationException("Failed to start nginx"); } // Read the PID file - if(!File.Exists(pidFile)) + if (!File.Exists(pidFile)) { Logger.LogWarning("Unable to find nginx PID file: {pidFile}", pidFile); } @@ -158,6 +221,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting File.Delete(_configFile); } + _portSelector?.Dispose(); + base.Dispose(); } } diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs index a102cd02da..f33b285d63 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/RemoteWindowsDeployer/RemoteWindowsDeployer.cs @@ -33,43 +33,43 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting if (_deploymentParameters.ServerType != ServerType.IIS && _deploymentParameters.ServerType != ServerType.Kestrel - && _deploymentParameters.ServerType != ServerType.WebListener) + && _deploymentParameters.ServerType != ServerType.HttpSys) { throw new InvalidOperationException($"Server type {_deploymentParameters.ServerType} is not supported for remote deployment." + - $" Supported server types are {nameof(ServerType.Kestrel)}, {nameof(ServerType.IIS)} and {nameof(ServerType.WebListener)}"); + $" Supported server types are {nameof(ServerType.Kestrel)}, {nameof(ServerType.IIS)} and {nameof(ServerType.HttpSys)}"); } - if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerName)) + if (string.IsNullOrEmpty(_deploymentParameters.ServerName)) { throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerName}' for {nameof(RemoteWindowsDeploymentParameters.ServerName)}"); } - if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerAccountName)) + if (string.IsNullOrEmpty(_deploymentParameters.ServerAccountName)) { throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountName}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountName)}." + " Account credentials are required to enable creating a powershell session to the remote server."); } - if (string.IsNullOrWhiteSpace(_deploymentParameters.ServerAccountPassword)) + if (string.IsNullOrEmpty(_deploymentParameters.ServerAccountPassword)) { throw new ArgumentException($"Invalid value '{_deploymentParameters.ServerAccountPassword}' for {nameof(RemoteWindowsDeploymentParameters.ServerAccountPassword)}." + " Account credentials are required to enable creating a powershell session to the remote server."); } if (_deploymentParameters.ApplicationType == ApplicationType.Portable - && string.IsNullOrWhiteSpace(_deploymentParameters.DotnetRuntimePath)) + && string.IsNullOrEmpty(_deploymentParameters.DotnetRuntimePath)) { throw new ArgumentException($"Invalid value '{_deploymentParameters.DotnetRuntimePath}' for {nameof(RemoteWindowsDeploymentParameters.DotnetRuntimePath)}. " + "It must be non-empty for portable apps."); } - if (string.IsNullOrWhiteSpace(_deploymentParameters.RemoteServerFileSharePath)) + if (string.IsNullOrEmpty(_deploymentParameters.RemoteServerFileSharePath)) { throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.RemoteServerFileSharePath)}." + " . A file share is required to copy the application's published output."); } - if (string.IsNullOrWhiteSpace(_deploymentParameters.ApplicationBaseUriHint)) + if (string.IsNullOrEmpty(_deploymentParameters.ApplicationBaseUriHint)) { throw new ArgumentException($"Invalid value for {nameof(RemoteWindowsDeploymentParameters.ApplicationBaseUriHint)}."); } diff --git a/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs b/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs index 12f2b83de1..ae6a7a08af 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs +++ b/src/Hosting/Server.IntegrationTesting/src/Deployers/SelfHostDeployer.cs @@ -36,14 +36,29 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting // Start timer StartTimer(); + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr + && DeploymentParameters.RuntimeArchitecture == RuntimeArchitecture.x86) + { + // Publish is required to rebuild for the right bitness + DeploymentParameters.PublishApplicationBeforeDeployment = true; + } + + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr + && DeploymentParameters.ApplicationType == ApplicationType.Standalone) + { + // Publish is required to get the correct files in the output directory + DeploymentParameters.PublishApplicationBeforeDeployment = true; + } + if (DeploymentParameters.PublishApplicationBeforeDeployment) { DotnetPublish(); } var hintUrl = TestUriHelper.BuildTestUri( - DeploymentParameters.ApplicationBaseUriHint, DeploymentParameters.ServerType, + DeploymentParameters.Scheme, + DeploymentParameters.ApplicationBaseUriHint, DeploymentParameters.StatusMessagesEnabled); // Launch the host process. @@ -64,43 +79,42 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting { using (Logger.BeginScope("StartSelfHost")) { - string executableName; - string executableArgs = string.Empty; - string workingDirectory = string.Empty; + var executableName = string.Empty; + var executableArgs = string.Empty; + var workingDirectory = string.Empty; + var executableExtension = DeploymentParameters.ApplicationType == ApplicationType.Portable ? ".dll" + : (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : ""); + if (DeploymentParameters.PublishApplicationBeforeDeployment) { workingDirectory = DeploymentParameters.PublishedApplicationRootPath; - var executableExtension = - DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? ".exe" : - DeploymentParameters.ApplicationType == ApplicationType.Portable ? ".dll" : ""; - var executable = Path.Combine(DeploymentParameters.PublishedApplicationRootPath, DeploymentParameters.ApplicationName + executableExtension); - - if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - executableName = "mono"; - executableArgs = executable; - } - else if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable) - { - executableName = "dotnet"; - executableArgs = executable; - } - else - { - executableName = executable; - } } else { - workingDirectory = DeploymentParameters.ApplicationPath; - var targetFramework = DeploymentParameters.TargetFramework ?? (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? "net461" : "netcoreapp2.0"); - - executableName = DotnetCommandName; - executableArgs = $"run --no-build -c {DeploymentParameters.Configuration} --framework {targetFramework} {DotnetArgumentSeparator}"; + // Core+Standalone always publishes. This must be Clr+Standalone or Core+Portable. + // Run from the pre-built bin/{config}/{tfm} directory. + var targetFramework = DeploymentParameters.TargetFramework + ?? (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.Clr ? Tfm.Net461 : Tfm.NetCoreApp22); + workingDirectory = Path.Combine(DeploymentParameters.ApplicationPath, "bin", DeploymentParameters.Configuration, targetFramework); + // CurrentDirectory will point to bin/{config}/{tfm}, but the config and static files aren't copied, point to the app base instead. + DeploymentParameters.EnvironmentVariables["ASPNETCORE_CONTENTROOT"] = DeploymentParameters.ApplicationPath; } - executableArgs += $" --server.urls {hintUrl} " - + $" --server {(DeploymentParameters.ServerType == ServerType.WebListener ? "Microsoft.AspNetCore.Server.HttpSys" : "Microsoft.AspNetCore.Server.Kestrel")}"; + var executable = Path.Combine(workingDirectory, DeploymentParameters.ApplicationName + executableExtension); + + if (DeploymentParameters.RuntimeFlavor == RuntimeFlavor.CoreClr && DeploymentParameters.ApplicationType == ApplicationType.Portable) + { + executableName = GetDotNetExeForArchitecture(); + executableArgs = executable; + } + else + { + executableName = executable; + } + + var server = DeploymentParameters.ServerType == ServerType.HttpSys + ? "Microsoft.AspNetCore.Server.HttpSys" : "Microsoft.AspNetCore.Server.Kestrel"; + executableArgs += $" --urls {hintUrl} --server {server}"; Logger.LogInformation($"Executing {executableName} {executableArgs}"); diff --git a/src/Hosting/Server.IntegrationTesting/src/Http.config b/src/Hosting/Server.IntegrationTesting/src/Http.config new file mode 100644 index 0000000000..4508dea843 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/Http.config @@ -0,0 +1,1034 @@ + + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj index e693a22278..a469dc113a 100644 --- a/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj +++ b/src/Hosting/Server.IntegrationTesting/src/Microsoft.AspNetCore.Server.IntegrationTesting.csproj @@ -15,11 +15,16 @@ false + + + + + diff --git a/src/Hosting/Server.IntegrationTesting/src/ProcessHelpers.cs b/src/Hosting/Server.IntegrationTesting/src/ProcessHelpers.cs new file mode 100644 index 0000000000..c00b6c2b4c --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/ProcessHelpers.cs @@ -0,0 +1,36 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + internal class ProcessHelpers + { + public static void AddEnvironmentVariablesToProcess(ProcessStartInfo startInfo, IDictionary environmentVariables, ILogger logger) + { + var environment = startInfo.Environment; + + foreach (var environmentVariable in environmentVariables) + { + SetEnvironmentVariable(environment, environmentVariable.Key, environmentVariable.Value, logger); + } + } + + public static void SetEnvironmentVariable(IDictionary environment, string name, string value, ILogger logger) + { + if (value == null) + { + logger.LogInformation("Removing environment variable {name}", name); + environment.Remove(name); + } + else + { + logger.LogInformation("SET {name}={value}", name, value); + environment[name] = value; + } + } + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/PublishedApplication.cs b/src/Hosting/Server.IntegrationTesting/src/PublishedApplication.cs new file mode 100644 index 0000000000..3913a7e908 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/PublishedApplication.cs @@ -0,0 +1,31 @@ +// 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.IO; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class PublishedApplication: IDisposable + { + private readonly ILogger _logger; + + public string Path { get; } + + public PublishedApplication(string path, ILogger logger) + { + _logger = logger; + Path = path; + } + + public void Dispose() + { + RetryHelper.RetryOperation( + () => Directory.Delete(Path, true), + e => _logger.LogWarning($"Failed to delete directory : {e.Message}"), + retryCount: 3, + retryDelayMilliseconds: 100); + } + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/TestMatrix.cs b/src/Hosting/Server.IntegrationTesting/src/TestMatrix.cs new file mode 100644 index 0000000000..4353627fc5 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/TestMatrix.cs @@ -0,0 +1,361 @@ +// 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.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class TestMatrix : IEnumerable + { + public IList Servers { get; set; } = new List(); + public IList Tfms { get; set; } = new List(); + public IList ApplicationTypes { get; set; } = new List(); + public IList Architectures { get; set; } = new List(); + + // ANCM specific... + public IList HostingModels { get; set; } = new List(); + public IList AncmVersions { get; set; } = new List(); + + private IList, string>> Skips { get; } = new List, string>>(); + + public static TestMatrix ForServers(params ServerType[] types) + { + return new TestMatrix() + { + Servers = types + }; + } + + public TestMatrix WithTfms(params string[] tfms) + { + Tfms = tfms; + return this; + } + + public TestMatrix WithApplicationTypes(params ApplicationType[] types) + { + ApplicationTypes = types; + return this; + } + + public TestMatrix WithAllApplicationTypes() + { + ApplicationTypes.Add(ApplicationType.Portable); + ApplicationTypes.Add(ApplicationType.Standalone); + return this; + } + public TestMatrix WithArchitectures(params RuntimeArchitecture[] archs) + { + Architectures = archs; + return this; + } + + public TestMatrix WithAllArchitectures() + { + Architectures.Add(RuntimeArchitecture.x64); + Architectures.Add(RuntimeArchitecture.x86); + return this; + } + + public TestMatrix WithHostingModels(params HostingModel[] models) + { + HostingModels = models; + return this; + } + + public TestMatrix WithAllHostingModels() + { + HostingModels.Add(HostingModel.OutOfProcess); + HostingModels.Add(HostingModel.InProcess); + return this; + } + + public TestMatrix WithAncmVersions(params AncmVersion[] versions) + { + AncmVersions = versions; + return this; + } + + public TestMatrix WithAllAncmVersions() + { + AncmVersions.Add(AncmVersion.AspNetCoreModule); + AncmVersions.Add(AncmVersion.AspNetCoreModuleV2); + return this; + } + + /// + /// V2 + InProc + /// + /// + public TestMatrix WithAncmV2InProcess() => WithAncmVersions(AncmVersion.AspNetCoreModuleV2).WithHostingModels(HostingModel.InProcess); + + public TestMatrix Skip(string message, Func check) + { + Skips.Add(new Tuple, string>(check, message)); + return this; + } + + private IEnumerable Build() + { + if (!Servers.Any()) + { + throw new ArgumentException("No servers were specified."); + } + + // TFMs. + if (!Tfms.Any()) + { + throw new ArgumentException("No TFMs were specified."); + } + + ResolveDefaultArchitecture(); + + if (!ApplicationTypes.Any()) + { + ApplicationTypes.Add(ApplicationType.Portable); + } + + if (!AncmVersions.Any()) + { + AncmVersions.Add(AncmVersion.AspNetCoreModule); + } + + if (!HostingModels.Any()) + { + HostingModels.Add(HostingModel.OutOfProcess); + } + + var variants = new List(); + VaryByServer(variants); + + CheckForSkips(variants); + + return variants; + } + + private void ResolveDefaultArchitecture() + { + if (!Architectures.Any()) + { + switch (RuntimeInformation.OSArchitecture) + { + case Architecture.X86: + Architectures.Add(RuntimeArchitecture.x86); + break; + case Architecture.X64: + Architectures.Add(RuntimeArchitecture.x64); + break; + default: + throw new ArgumentException(RuntimeInformation.OSArchitecture.ToString()); + } + } + } + + private void VaryByServer(List variants) + { + foreach (var server in Servers) + { + var skip = SkipIfServerIsNotSupportedOnThisOS(server); + + VaryByTfm(variants, server, skip); + } + } + + private static string SkipIfServerIsNotSupportedOnThisOS(ServerType server) + { + var skip = false; + switch (server) + { + case ServerType.IIS: + case ServerType.IISExpress: + case ServerType.HttpSys: + skip = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + break; + case ServerType.Kestrel: + break; + case ServerType.Nginx: + // Technically it's possible but we don't test it. + skip = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + break; + default: + throw new ArgumentException(server.ToString()); + } + + return skip ? "This server is not supported on this operating system." : null; + } + + private void VaryByTfm(List variants, ServerType server, string skip) + { + foreach (var tfm in Tfms) + { + if (!CheckTfmIsSupportedForServer(tfm, server)) + { + // Don't generate net461 variations for nginx server. + continue; + } + + var skipTfm = skip ?? SkipIfTfmIsNotSupportedOnThisOS(tfm); + + VaryByApplicationType(variants, server, tfm, skipTfm); + } + } + + private bool CheckTfmIsSupportedForServer(string tfm, ServerType server) + { + // Not a combination we test + return !(Tfm.Matches(Tfm.Net461, tfm) && ServerType.Nginx == server); + } + + private static string SkipIfTfmIsNotSupportedOnThisOS(string tfm) + { + if (Tfm.Matches(Tfm.Net461, tfm) && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "This TFM is not supported on this operating system."; + } + + return null; + } + + private void VaryByApplicationType(List variants, ServerType server, string tfm, string skip) + { + foreach (var t in ApplicationTypes) + { + var type = t; + if (Tfm.Matches(Tfm.Net461, tfm) && type == ApplicationType.Portable) + { + if (ApplicationTypes.Count == 1) + { + // Override the default + type = ApplicationType.Standalone; + } + else + { + continue; + } + } + + VaryByArchitecture(variants, server, tfm, skip, type); + } + } + + private void VaryByArchitecture(List variants, ServerType server, string tfm, string skip, ApplicationType type) + { + foreach (var arch in Architectures) + { + if (!IsArchitectureSupportedOnServer(arch, server)) + { + continue; + } + var archSkip = skip ?? SkipIfArchitectureNotSupportedOnCurrentSystem(arch); + + if (server == ServerType.IISExpress || server == ServerType.IIS) + { + VaryByAncmVersion(variants, server, tfm, type, arch, archSkip); + } + else + { + variants.Add(new TestVariant() + { + Server = server, + Tfm = tfm, + ApplicationType = type, + Architecture = arch, + Skip = archSkip, + }); + } + } + } + + private string SkipIfArchitectureNotSupportedOnCurrentSystem(RuntimeArchitecture arch) + { + if (arch == RuntimeArchitecture.x64) + { + // Can't run x64 on a x86 OS. + return (RuntimeInformation.OSArchitecture == Architecture.Arm || RuntimeInformation.OSArchitecture == Architecture.X86) + ? $"Cannot run {arch} on your current system." : null; + } + + // No x86 runtimes available on MacOS or Linux. + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? null : $"No {arch} available for non-Windows systems."; + } + + private bool IsArchitectureSupportedOnServer(RuntimeArchitecture arch, ServerType server) + { + // No x86 Mac/Linux runtime, don't generate a test variation that will always be skipped. + return !(arch == RuntimeArchitecture.x86 && ServerType.Nginx == server); + } + + private void VaryByAncmVersion(IList variants, ServerType server, string tfm, ApplicationType type, RuntimeArchitecture arch, string skip) + { + foreach (var version in AncmVersions) + { + VaryByAncmHostingModel(variants, server, tfm, type, arch, skip, version); + } + } + + private void VaryByAncmHostingModel(IList variants, ServerType server, string tfm, ApplicationType type, RuntimeArchitecture arch, string skip, AncmVersion version) + { + foreach (var hostingModel in HostingModels) + { + var skipAncm = skip; + if (hostingModel == HostingModel.InProcess) + { + // Not supported + if (Tfm.Matches(Tfm.Net461, tfm) || Tfm.Matches(Tfm.NetCoreApp20, tfm) || version == AncmVersion.AspNetCoreModule) + { + continue; + } + + if (!IISExpressAncmSchema.SupportsInProcessHosting) + { + skipAncm = skipAncm ?? IISExpressAncmSchema.SkipReason; + } + } + + variants.Add(new TestVariant() + { + Server = server, + Tfm = tfm, + ApplicationType = type, + Architecture = arch, + AncmVersion = version, + HostingModel = hostingModel, + Skip = skipAncm, + }); + } + } + + private void CheckForSkips(List variants) + { + foreach (var variant in variants) + { + foreach (var skipPair in Skips) + { + if (skipPair.Item1(variant)) + { + variant.Skip = skipPair.Item2; + break; + } + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + + // This is what Xunit MemberData expects + public IEnumerator GetEnumerator() + { + foreach (var v in Build()) + { + yield return new[] { v }; + } + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/TestVariant.cs b/src/Hosting/Server.IntegrationTesting/src/TestVariant.cs new file mode 100644 index 0000000000..adf25b0dfa --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/TestVariant.cs @@ -0,0 +1,55 @@ +// 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 Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class TestVariant : IXunitSerializable + { + public ServerType Server { get; set; } + public string Tfm { get; set; } + public ApplicationType ApplicationType { get; set; } + public RuntimeArchitecture Architecture { get; set; } + + public string Skip { get; set; } + + // ANCM specifics... + public HostingModel HostingModel { get; set; } + public AncmVersion AncmVersion { get; set; } + + public override string ToString() + { + // For debug and test explorer view + var description = $"Server: {Server}, TFM: {Tfm}, Type: {ApplicationType}, Arch: {Architecture}"; + if (Server == ServerType.IISExpress || Server == ServerType.IIS) + { + var version = AncmVersion == AncmVersion.AspNetCoreModule ? "V1" : "V2"; + description += $", ANCM: {version}, Host: {HostingModel}"; + } + return description; + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(Skip), Skip, typeof(string)); + info.AddValue(nameof(Server), Server, typeof(ServerType)); + info.AddValue(nameof(Tfm), Tfm, typeof(string)); + info.AddValue(nameof(ApplicationType), ApplicationType, typeof(ApplicationType)); + info.AddValue(nameof(Architecture), Architecture, typeof(RuntimeArchitecture)); + info.AddValue(nameof(HostingModel), HostingModel, typeof(HostingModel)); + info.AddValue(nameof(AncmVersion), AncmVersion, typeof(AncmVersion)); + } + + public void Deserialize(IXunitSerializationInfo info) + { + Skip = info.GetValue(nameof(Skip)); + Server = info.GetValue(nameof(Server)); + Tfm = info.GetValue(nameof(Tfm)); + ApplicationType = info.GetValue(nameof(ApplicationType)); + Architecture = info.GetValue(nameof(Architecture)); + HostingModel = info.GetValue(nameof(HostingModel)); + AncmVersion = info.GetValue(nameof(AncmVersion)); + } + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/xunit/IISExpressAncmSchema.cs b/src/Hosting/Server.IntegrationTesting/src/xunit/IISExpressAncmSchema.cs new file mode 100644 index 0000000000..53ea67ddd2 --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/xunit/IISExpressAncmSchema.cs @@ -0,0 +1,55 @@ +// 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.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + public class IISExpressAncmSchema + { + public static bool SupportsInProcessHosting { get; } + public static string SkipReason { get; } + + static IISExpressAncmSchema() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + SkipReason = "IIS Express tests can only be run on Windows"; + return; + } + + var ancmConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + "IIS Express", "config", "schema", "aspnetcore_schema.xml"); + + if (!File.Exists(ancmConfigPath)) + { + SkipReason = "IIS Express is not installed."; + return; + } + + XDocument ancmConfig; + + try + { + ancmConfig = XDocument.Load(ancmConfigPath); + } + catch + { + SkipReason = "Could not read ANCM schema configuration"; + return; + } + + SupportsInProcessHosting = ancmConfig + .Root + .Descendants("attribute") + .Any(n => "hostingModel".Equals(n.Attribute("name")?.Value, StringComparison.Ordinal)); + + SkipReason = SupportsInProcessHosting ? null : "IIS Express must be upgraded to support in-process hosting."; + } + } +} \ No newline at end of file diff --git a/src/Hosting/Server.IntegrationTesting/src/xunit/SkipIfIISExpressSchemaMissingInProcessAttribute.cs b/src/Hosting/Server.IntegrationTesting/src/xunit/SkipIfIISExpressSchemaMissingInProcessAttribute.cs new file mode 100644 index 0000000000..ecadf4522a --- /dev/null +++ b/src/Hosting/Server.IntegrationTesting/src/xunit/SkipIfIISExpressSchemaMissingInProcessAttribute.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.AspNetCore.Testing.xunit; + +namespace Microsoft.AspNetCore.Server.IntegrationTesting +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Assembly | AttributeTargets.Class)] + public sealed partial class SkipIfIISExpressSchemaMissingInProcessAttribute : Attribute, ITestCondition + { + public bool IsMet => IISExpressAncmSchema.SupportsInProcessHosting; + + public string SkipReason => IISExpressAncmSchema.SkipReason; + } +} diff --git a/src/Hosting/Server.IntegrationTesting/src/xunit/SkipOn32BitOSAttribute.cs b/src/Hosting/Server.IntegrationTesting/src/xunit/SkipOn32BitOSAttribute.cs index 554cc63eda..066eb729c6 100644 --- a/src/Hosting/Server.IntegrationTesting/src/xunit/SkipOn32BitOSAttribute.cs +++ b/src/Hosting/Server.IntegrationTesting/src/xunit/SkipOn32BitOSAttribute.cs @@ -2,32 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.IO; +using System.Runtime.InteropServices; using Microsoft.AspNetCore.Testing.xunit; namespace Microsoft.AspNetCore.Server.IntegrationTesting { /// - /// Skips a 64 bit test if the current Windows OS is 32-bit. + /// Skips a 64 bit test if the current OS is 32-bit. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class SkipOn32BitOSAttribute : Attribute, ITestCondition { - public bool IsMet - { - get - { - // Directory found only on 64-bit OS. - return Directory.Exists(Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), "SysWOW64")); - } - } + public bool IsMet => + RuntimeInformation.OSArchitecture == Architecture.Arm64 + || RuntimeInformation.OSArchitecture == Architecture.X64; - public string SkipReason - { - get - { - return "Skipping the x64 test since Windows is 32-bit"; - } - } + public string SkipReason => "Skipping the x64 test since Windows is 32-bit"; } } \ No newline at end of file diff --git a/src/Hosting/TestHost/src/ResponseStream.cs b/src/Hosting/TestHost/src/ResponseStream.cs index 0cd3459a80..b2ababa182 100644 --- a/src/Hosting/TestHost/src/ResponseStream.cs +++ b/src/Hosting/TestHost/src/ResponseStream.cs @@ -109,34 +109,20 @@ namespace Microsoft.AspNetCore.TestHost var registration = cancellationToken.Register(Cancel); try { - // TODO: Usability issue. dotnet/corefx#27732 Flush or zero byte write causes ReadAsync to complete without data so I have to call ReadAsync in a loop. - while (true) + var result = await _pipe.Reader.ReadAsync(cancellationToken); + + if (result.Buffer.IsEmpty && result.IsCompleted) { - var result = await _pipe.Reader.ReadAsync(cancellationToken); - - var readableBuffer = result.Buffer; - if (!readableBuffer.IsEmpty) - { - var actual = Math.Min(readableBuffer.Length, count); - readableBuffer = readableBuffer.Slice(0, actual); - readableBuffer.CopyTo(new Span(buffer, offset, count)); - _pipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End); - return (int)actual; - } - - if (result.IsCompleted) - { - _pipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End); // TODO: Remove after https://github.com/dotnet/corefx/pull/27596 - _pipe.Reader.Complete(); - return 0; - } - - cancellationToken.ThrowIfCancellationRequested(); - Debug.Assert(!result.IsCanceled); // It should only be canceled by cancellationToken. - - // Try again. TODO: dotnet/corefx#27732 I shouldn't need to do this, there wasn't any data. - _pipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End); + _pipe.Reader.Complete(); + return 0; } + + var readableBuffer = result.Buffer; + var actual = Math.Min(readableBuffer.Length, count); + readableBuffer = readableBuffer.Slice(0, actual); + readableBuffer.CopyTo(new Span(buffer, offset, count)); + _pipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End); + return (int)actual; } finally { diff --git a/src/Hosting/TestHost/test/ClientHandlerTests.cs b/src/Hosting/TestHost/test/ClientHandlerTests.cs index 73f1c86d29..e90a79e97b 100644 --- a/src/Hosting/TestHost/test/ClientHandlerTests.cs +++ b/src/Hosting/TestHost/test/ClientHandlerTests.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -28,7 +29,7 @@ namespace Microsoft.AspNetCore.TestHost var handler = new ClientHandler(new PathString("/A/Path/"), new DummyApplication(context => { // TODO: Assert.True(context.RequestAborted.CanBeCanceled); -#if NETCOREAPP2_1 +#if NETCOREAPP2_2 Assert.Equal("HTTP/2.0", context.Request.Protocol); #elif NET461 || NETCOREAPP2_0 Assert.Equal("HTTP/1.1", context.Request.Protocol); @@ -60,7 +61,7 @@ namespace Microsoft.AspNetCore.TestHost var handler = new ClientHandler(new PathString("/A/Path/"), new InspectingApplication(features => { // TODO: Assert.True(context.RequestAborted.CanBeCanceled); -#if NETCOREAPP2_1 +#if NETCOREAPP2_2 Assert.Equal("HTTP/2.0", features.Get().Protocol); #elif NET461 || NETCOREAPP2_0 Assert.Equal("HTTP/1.1", features.Get().Protocol); @@ -210,8 +211,8 @@ namespace Microsoft.AspNetCore.TestHost Task readTask = responseStream.ReadAsync(new byte[100], 0, 100); Assert.False(readTask.IsCompleted); responseStream.Dispose(); - Assert.True(readTask.Wait(TimeSpan.FromSeconds(10)), "Finished"); - Assert.Equal(0, readTask.Result); + var result = await readTask.TimeoutAfter(TimeSpan.FromSeconds(10)); + Assert.Equal(0, result); block.Set(); } @@ -235,8 +236,7 @@ namespace Microsoft.AspNetCore.TestHost Task readTask = responseStream.ReadAsync(new byte[100], 0, 100, cts.Token); Assert.False(readTask.IsCompleted, "Not Completed"); cts.Cancel(); - var ex = Assert.Throws(() => readTask.Wait(TimeSpan.FromSeconds(10))); - Assert.IsAssignableFrom(ex.GetBaseException()); + await Assert.ThrowsAsync(() => readTask.TimeoutAfter(TimeSpan.FromSeconds(10))); block.Set(); } diff --git a/src/Hosting/samples/GenericWebHost/GenericWebHost.csproj b/src/Hosting/samples/GenericWebHost/GenericWebHost.csproj index 74f18791c8..e0adcd7357 100644 --- a/src/Hosting/samples/GenericWebHost/GenericWebHost.csproj +++ b/src/Hosting/samples/GenericWebHost/GenericWebHost.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1;net461 + netcoreapp2.2;net461 latest true diff --git a/src/Hosting/samples/SampleStartups/SampleStartups.csproj b/src/Hosting/samples/SampleStartups/SampleStartups.csproj index 9c881e56e3..1e1f2e37d0 100644 --- a/src/Hosting/samples/SampleStartups/SampleStartups.csproj +++ b/src/Hosting/samples/SampleStartups/SampleStartups.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;net461 + netcoreapp2.2;net461 SampleStartups.StartupInjection exe diff --git a/src/Hosting/samples/SampleStartups/StartupExternallyControlled.cs b/src/Hosting/samples/SampleStartups/StartupExternallyControlled.cs index 68ec11c9b0..ba45235a6a 100644 --- a/src/Hosting/samples/SampleStartups/StartupExternallyControlled.cs +++ b/src/Hosting/samples/SampleStartups/StartupExternallyControlled.cs @@ -38,8 +38,10 @@ namespace SampleStartups public async Task StopAsync() { - await _host.StopAsync(TimeSpan.FromSeconds(5)); - _host.Dispose(); + using (_host) + { + await _host.StopAsync(TimeSpan.FromSeconds(5)); + } } public void AddUrl(string url) diff --git a/src/Hosting/samples/SampleStartups/StartupFullControl.cs b/src/Hosting/samples/SampleStartups/StartupFullControl.cs index 272e747446..a7986d9e0c 100644 --- a/src/Hosting/samples/SampleStartups/StartupFullControl.cs +++ b/src/Hosting/samples/SampleStartups/StartupFullControl.cs @@ -15,9 +15,9 @@ namespace SampleStartups public static void Main(string[] args) { var config = new ConfigurationBuilder() - .AddCommandLine(args) .AddEnvironmentVariables(prefix: "ASPNETCORE_") .AddJsonFile("hosting.json", optional: true) + .AddCommandLine(args) .Build(); var host = new WebHostBuilder() diff --git a/src/Hosting/test/FunctionalTests/ShutdownTests.cs b/src/Hosting/test/FunctionalTests/ShutdownTests.cs index 1c229e96b8..4249627f56 100644 --- a/src/Hosting/test/FunctionalTests/ShutdownTests.cs +++ b/src/Hosting/test/FunctionalTests/ShutdownTests.cs @@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests RuntimeArchitecture.x64) { EnvironmentName = "Shutdown", - TargetFramework = "netcoreapp2.0", + TargetFramework = Tfm.NetCoreApp22, ApplicationType = ApplicationType.Portable, PublishApplicationBeforeDeployment = true, StatusMessagesEnabled = false diff --git a/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs b/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs index 4a56c432be..f594af25b0 100644 --- a/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs +++ b/src/Hosting/test/FunctionalTests/WebHostBuilderTests.cs @@ -17,17 +17,12 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests { public WebHostBuilderTests(ITestOutputHelper output) : base(output) { } - [Fact] - public async Task InjectedStartup_DefaultApplicationNameIsEntryAssembly_CoreClr() - => await InjectedStartup_DefaultApplicationNameIsEntryAssembly(RuntimeFlavor.CoreClr); + public static TestMatrix TestVariants => TestMatrix.ForServers(ServerType.Kestrel) + .WithTfms(Tfm.Net461, Tfm.NetCoreApp22); - [ConditionalFact] - [OSSkipCondition(OperatingSystems.MacOSX)] - [OSSkipCondition(OperatingSystems.Linux)] - public async Task InjectedStartup_DefaultApplicationNameIsEntryAssembly_Clr() - => await InjectedStartup_DefaultApplicationNameIsEntryAssembly(RuntimeFlavor.Clr); - - private async Task InjectedStartup_DefaultApplicationNameIsEntryAssembly(RuntimeFlavor runtimeFlavor) + [ConditionalTheory] + [MemberData(nameof(TestVariants))] + public async Task InjectedStartup_DefaultApplicationNameIsEntryAssembly(TestVariant variant) { using (StartLog(out var loggerFactory)) { @@ -35,14 +30,9 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests var applicationPath = Path.Combine(TestPathUtilities.GetSolutionRootDirectory("Hosting"), "test", "TestAssets", "IStartupInjectionAssemblyName"); - var deploymentParameters = new DeploymentParameters( - applicationPath, - ServerType.Kestrel, - runtimeFlavor, - RuntimeArchitecture.x64) + var deploymentParameters = new DeploymentParameters(variant) { - TargetFramework = runtimeFlavor == RuntimeFlavor.Clr ? "net461" : "netcoreapp2.0", - ApplicationType = ApplicationType.Portable, + ApplicationPath = applicationPath, StatusMessagesEnabled = false }; diff --git a/src/Hosting/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj b/src/Hosting/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj index 40bd0c87a3..2012fa4d19 100644 --- a/src/Hosting/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj +++ b/src/Hosting/test/testassets/BuildWebHostInvalidSignature/BuildWebHostInvalidSignature.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;netcoreapp2.0;net461 + netcoreapp2.2;netcoreapp2.0;net461 Exe diff --git a/src/Hosting/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj b/src/Hosting/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj index 40bd0c87a3..2012fa4d19 100644 --- a/src/Hosting/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj +++ b/src/Hosting/test/testassets/BuildWebHostPatternTestSite/BuildWebHostPatternTestSite.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;netcoreapp2.0;net461 + netcoreapp2.2;netcoreapp2.0;net461 Exe diff --git a/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj b/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj index 40bd0c87a3..2012fa4d19 100644 --- a/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj +++ b/src/Hosting/test/testassets/CreateWebHostBuilderInvalidSignature/CreateWebHostBuilderInvalidSignature.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;netcoreapp2.0;net461 + netcoreapp2.2;netcoreapp2.0;net461 Exe diff --git a/src/Hosting/test/testassets/IStartupInjectionAssemblyName/IStartupInjectionAssemblyName.csproj b/src/Hosting/test/testassets/IStartupInjectionAssemblyName/IStartupInjectionAssemblyName.csproj index 40bd0c87a3..2012fa4d19 100644 --- a/src/Hosting/test/testassets/IStartupInjectionAssemblyName/IStartupInjectionAssemblyName.csproj +++ b/src/Hosting/test/testassets/IStartupInjectionAssemblyName/IStartupInjectionAssemblyName.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;netcoreapp2.0;net461 + netcoreapp2.2;netcoreapp2.0;net461 Exe diff --git a/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj b/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj index 7b87b0eed5..ad137a1190 100644 --- a/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj +++ b/src/Hosting/test/testassets/Microsoft.AspNetCore.Hosting.TestSites/Microsoft.AspNetCore.Hosting.TestSites.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1;netcoreapp2.0;net461 + netcoreapp2.2;netcoreapp2.0;net461 Exe diff --git a/src/Hosting/test/testassets/TestStartupAssembly1/TestStartupAssembly1.csproj b/src/Hosting/test/testassets/TestStartupAssembly1/TestStartupAssembly1.csproj index 951d8c69e3..2695489597 100644 --- a/src/Hosting/test/testassets/TestStartupAssembly1/TestStartupAssembly1.csproj +++ b/src/Hosting/test/testassets/TestStartupAssembly1/TestStartupAssembly1.csproj @@ -1,7 +1,7 @@ - + - netcoreapp2.0;net461 + netcoreapp2.2;net461