Merge aspnet/Hosting release/2.2

This commit is contained in:
Nate McMaster 2018-11-20 09:18:15 -08:00
commit bfba741326
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
53 changed files with 2373 additions and 658 deletions

View File

@ -44,6 +44,8 @@
<FilesToSign Include="Microsoft.Extensions.FileProviders.Embedded.dll" Certificate="$(AssemblySigningCertName)" Container="Microsoft.AspNetCore.App" />
<FilesToSign Include="Microsoft.Extensions.FileProviders.Physical.dll" Certificate="$(AssemblySigningCertName)" Container="Microsoft.AspNetCore.App" />
<FilesToSign Include="Microsoft.Extensions.FileSystemGlobbing.dll" Certificate="$(AssemblySigningCertName)" Container="Microsoft.AspNetCore.App" />
<FilesToSign Include="Microsoft.Extensions.Hosting.Abstractions.dll" Certificate="$(AssemblySigningCertName)" Container="Microsoft.AspNetCore.App" />
<FilesToSign Include="Microsoft.Extensions.Hosting.dll" Certificate="$(AssemblySigningCertName)" Container="Microsoft.AspNetCore.App" />
<FilesToSign Include="Microsoft.Extensions.Http.dll" Certificate="$(AssemblySigningCertName)" Container="Microsoft.AspNetCore.App" />
<FilesToSign Include="Microsoft.Extensions.Logging.Abstractions.dll" Certificate="$(AssemblySigningCertName)" Container="Microsoft.AspNetCore.App" />
<FilesToSign Include="Microsoft.Extensions.Logging.Configuration.dll" Certificate="$(AssemblySigningCertName)" Container="Microsoft.AspNetCore.App" />

View File

@ -1,6 +1,8 @@
<Project>
<PropertyGroup>
<!-- _ProjectsOnly is for local builds and shouldn't be used on CI. -->
<DisableCodeSigning Condition=" '$(_ProjectsOnly)' == 'true' ">true</DisableCodeSigning>
<CodeSignDependsOn>$(CodeSignDependsOn);CollectFileSignInfo</CodeSignDependsOn>
</PropertyGroup>

View File

@ -20,7 +20,6 @@
<PackageArtifact Include="dotnet-user-secrets" Category="ship" />
<PackageArtifact Include="dotnet-watch" Category="ship" />
<PackageArtifact Include="Internal.AspNetCore.Universe.Lineup" Category="noship" />
<PackageArtifact Include="Internal.WebHostBuilderFactory.Sources" Category="noship" />
<PackageArtifact Include="Microsoft.AspNet.Identity.AspNetCoreCompat" Category="noship" />
<PackageArtifact Include="Microsoft.AspNetCore.All" Category="ship" />
<PackageArtifact Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Category="noship" />
@ -134,7 +133,6 @@
<PackageArtifact Include="Microsoft.AspNetCore.Server.IIS" Category="ship" />
<PackageArtifact Include="Microsoft.AspNetCore.Server.IISIntegration" Category="ship" />
<PackageArtifact Include="Microsoft.AspNetCore.Server.IntegrationTesting.IIS" Category="noship" />
<PackageArtifact Include="Microsoft.AspNetCore.Server.IntegrationTesting" Category="noship" />
<PackageArtifact Include="Microsoft.AspNetCore.Server.Kestrel.Core" Category="ship" />
<PackageArtifact Include="Microsoft.AspNetCore.Server.Kestrel.Https" Category="ship" />
<PackageArtifact Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions" Category="ship" />

View File

@ -97,6 +97,7 @@
<!-- These dependencies are temporary while we refactor package refs into project refs. -->
<MicrosoftExtensionsBuffersTestingSourcesPackageVersion>2.2.0</MicrosoftExtensionsBuffersTestingSourcesPackageVersion>
<MicrosoftExtensionsBuffersMemoryPoolSourcesPackageVersion>2.2.0</MicrosoftExtensionsBuffersMemoryPoolSourcesPackageVersion>
<InternalWebHostBuilderFactorySourcesPackageVersion>2.2.0</InternalWebHostBuilderFactorySourcesPackageVersion>
<MicrosoftAspNetCoreServerIntegrationTestingPackageVersion>0.6.0-rtm-final</MicrosoftAspNetCoreServerIntegrationTestingPackageVersion>
<!-- 3rd party dependencies -->

View File

@ -79,7 +79,7 @@
<!-- These dependencies are temporary while we refactor package refs into project refs. -->
<ExternalDependency Include="Microsoft.Extensions.Buffers.Testing.Sources" Version="$(MicrosoftExtensionsBuffersTestingSourcesPackageVersion)" />
<ExternalDependency Include="Microsoft.Extensions.Buffers.MemoryPool.Sources" Version="$(MicrosoftExtensionsBuffersMemoryPoolSourcesPackageVersion)" />
<ExternalDependency Include="Microsoft.AspNetCore.Hosting.WebHostBuilderFactory.Sources" Version="$(MicrosoftAspNetCoreHostingWebHostBuilderFactorySourcesPackageVersion)" />
<ExternalDependency Include="Internal.WebHostBuilderFactory.Sources" Version="$(InternalWebHostBuilderFactorySourcesPackageVersion)" />
<ExternalDependency Include="Microsoft.AspNetCore.Server.IntegrationTesting" Version="$(MicrosoftAspNetCoreServerIntegrationTestingPackageVersion)" />
</ItemGroup>

View File

@ -6,6 +6,7 @@ This is required to provide dependencies for samples and tests.
<ItemGroup>
<LatestPackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="2.2.0" />
<LatestPackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="2.2.0" />
<LatestPackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<LatestPackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@ -94,7 +94,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal
private class HostingLogScope : IReadOnlyList<KeyValuePair<string, object>>
{
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<string, object>("RequestId", _httpContext.TraceIdentifier);
return new KeyValuePair<string, object>("RequestId", _traceIdentifier);
}
else if (index == 1)
{
return new KeyValuePair<string, object>("RequestPath", _httpContext.Request.Path.ToString());
return new KeyValuePair<string, object>("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;

View File

@ -174,13 +174,6 @@ namespace Microsoft.AspNetCore.Hosting
}
}
var logger = hostingServiceProvider.GetRequiredService<ILogger<WebHost>>();
// 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<ILogger<WebHost>>();
// 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<IServiceProviderFactory<IServiceCollection>>();
if (factory != null)
if (factory != null && !(factory is DefaultServiceProviderFactory))
{
using (provider)
{

View File

@ -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<WebHostOptions>().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<IHostingEnvironment>();
var applicationLifetime = host.Services.GetService<IApplicationLifetime>();
var options = host.Services.GetRequiredService<WebHostOptions>();
if (!options.SuppressStatusMessages)

View File

@ -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<PublishedApplication> 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;
}
}
}

View File

@ -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<DotnetPublishParameters, PublishedApplication> _publishCache = new Dictionary<DotnetPublishParameters, PublishedApplication>();
public CachingApplicationPublisher(string applicationPath) : base(applicationPath)
{
}
public override async Task<PublishedApplication> 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; }
}
}
}

View File

@ -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
}
}

View File

@ -5,7 +5,14 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
public enum ApplicationType
{
/// <summary>
/// Does not target a specific platform. Requires the matching runtime to be installed.
/// </summary>
Portable,
/// <summary>
/// All dlls are published with the app for x-copy deploy. Net461 requires this because ASP.NET Core is not in the GAC.
/// </summary>
Standalone
}
}

View File

@ -13,6 +13,35 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
/// </summary>
public class DeploymentParameters
{
public DeploymentParameters()
{
EnvironmentVariables["ASPNETCORE_DETAILEDERRORS"] = "true";
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
if (configAttribute != null && !string.IsNullOrEmpty(configAttribute.Configuration))
{
Configuration = configAttribute.Configuration;
}
}
public DeploymentParameters(TestVariant variant)
{
EnvironmentVariables["ASPNETCORE_DETAILEDERRORS"] = "true";
var configAttribute = Assembly.GetCallingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
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;
}
/// <summary>
/// Creates an instance of <see cref="DeploymentParameters"/>.
/// </summary>
@ -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;
/// <summary>
/// Suggested base url for the deployed application. The final deployed url could be
@ -67,15 +114,20 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
/// </summary>
public string ApplicationBaseUriHint { get; set; }
/// <summary>
/// Scheme used by the deployed application if <see cref="ApplicationBaseUriHint"/> is empty.
/// </summary>
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; }
/// <summary>
/// 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
/// </summary>
public string AdditionalPublishParameters { get; set; }
/// <summary>
/// Publish restores by default, this property opts out by default.
/// </summary>
public bool RestoreOnPublish { get; set; }
/// <summary>
/// To publish the application before deployment.
/// </summary>
@ -115,6 +162,12 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
public HostingModel HostingModel { get; set; }
/// <summary>
/// When using the IISExpressDeployer, determines whether to use the older or newer version
/// of ANCM.
/// </summary>
public AncmVersion AncmVersion { get; set; } = AncmVersion.AspNetCoreModule;
/// <summary>
/// Environment variables to be set before starting the host.
/// Not applicable for IIS Scenarios.

View File

@ -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;
}
}
}

View File

@ -3,8 +3,12 @@
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
/// <summary>
/// For ANCM
/// </summary>
public enum HostingModel
{
None,
OutOfProcess,
InProcess
}

View File

@ -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<IServerAddressesFeature>().Addresses.First();
}
}
}

View File

@ -5,7 +5,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
public enum RuntimeFlavor
{
Clr,
CoreClr
None,
CoreClr,
Clr
}
}

View File

@ -5,9 +5,10 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
public enum ServerType
{
None,
IISExpress,
IIS,
WebListener,
HttpSys,
Kestrel,
Nginx
}

View File

@ -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;
}
}
}
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -16,88 +16,73 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
/// <summary>
/// Abstract base class of all deployers with implementation of some of the common helpers.
/// </summary>
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<DeploymentResult> 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<string, string> 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<string, string> 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}");
}
}
}
}

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
/// <param name="deploymentParameters"></param>
/// <param name="loggerFactory"></param>
/// <returns></returns>
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:

View File

@ -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
{
/// <summary>
/// Common operations on an application deployer.
/// </summary>
public interface IApplicationDeployer : IDisposable
{
/// <summary>
/// Deploys the application to the target with specified <see cref="DeploymentParameters"/>.
/// </summary>
/// <returns></returns>
Task<DeploymentResult> DeployAsync();
}
}

View File

@ -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
{
/// <summary>
/// Deployment helper for IISExpress.
/// </summary>
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 ""(?<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<DeploymentResult> 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<bool>();
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);
}
}
}

View File

@ -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();
}
}

View File

@ -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)}.");
}

View File

@ -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}");

File diff suppressed because it is too large Load Diff

View File

@ -15,11 +15,16 @@
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Http.config" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Deployers\RemoteWindowsDeployer\RemotePSSessionHelper.ps1;Deployers\RemoteWindowsDeployer\StartServer.ps1;Deployers\RemoteWindowsDeployer\StopServer.ps1" Exclude="bin\**;obj\**;**\*.xproj;packages\**;@(EmbeddedResource)" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Testing" />
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
<Reference Include="Microsoft.Extensions.Logging" />

View File

@ -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<string, string> environmentVariables, ILogger logger)
{
var environment = startInfo.Environment;
foreach (var environmentVariable in environmentVariables)
{
SetEnvironmentVariable(environment, environmentVariable.Key, environmentVariable.Value, logger);
}
}
public static void SetEnvironmentVariable(IDictionary<string, string> 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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<object[]>
{
public IList<ServerType> Servers { get; set; } = new List<ServerType>();
public IList<string> Tfms { get; set; } = new List<string>();
public IList<ApplicationType> ApplicationTypes { get; set; } = new List<ApplicationType>();
public IList<RuntimeArchitecture> Architectures { get; set; } = new List<RuntimeArchitecture>();
// ANCM specific...
public IList<HostingModel> HostingModels { get; set; } = new List<HostingModel>();
public IList<AncmVersion> AncmVersions { get; set; } = new List<AncmVersion>();
private IList<Tuple<Func<TestVariant, bool>, string>> Skips { get; } = new List<Tuple<Func<TestVariant, bool>, 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;
}
/// <summary>
/// V2 + InProc
/// </summary>
/// <returns></returns>
public TestMatrix WithAncmV2InProcess() => WithAncmVersions(AncmVersion.AspNetCoreModuleV2).WithHostingModels(HostingModel.InProcess);
public TestMatrix Skip(string message, Func<TestVariant, bool> check)
{
Skips.Add(new Tuple<Func<TestVariant, bool>, string>(check, message));
return this;
}
private IEnumerable<TestVariant> 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<TestVariant>();
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<TestVariant> 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<TestVariant> 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<TestVariant> 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<TestVariant> 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<TestVariant> 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<TestVariant> 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<TestVariant> 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<object[]>)this).GetEnumerator();
}
// This is what Xunit MemberData expects
public IEnumerator<object[]> GetEnumerator()
{
foreach (var v in Build())
{
yield return new[] { v };
}
}
}
}

View File

@ -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<string>(nameof(Skip));
Server = info.GetValue<ServerType>(nameof(Server));
Tfm = info.GetValue<string>(nameof(Tfm));
ApplicationType = info.GetValue<ApplicationType>(nameof(ApplicationType));
Architecture = info.GetValue<RuntimeArchitecture>(nameof(Architecture));
HostingModel = info.GetValue<HostingModel>(nameof(HostingModel));
AncmVersion = info.GetValue<AncmVersion>(nameof(AncmVersion));
}
}
}

View File

@ -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.";
}
}
}

View File

@ -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;
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
[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";
}
}

View File

@ -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<byte>(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<byte>(buffer, offset, count));
_pipe.Reader.AdvanceTo(readableBuffer.End, readableBuffer.End);
return (int)actual;
}
finally
{

View File

@ -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<IHttpRequestFeature>().Protocol);
#elif NET461 || NETCOREAPP2_0
Assert.Equal("HTTP/1.1", features.Get<IHttpRequestFeature>().Protocol);
@ -210,8 +211,8 @@ namespace Microsoft.AspNetCore.TestHost
Task<int> 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<int> readTask = responseStream.ReadAsync(new byte[100], 0, 100, cts.Token);
Assert.False(readTask.IsCompleted, "Not Completed");
cts.Cancel();
var ex = Assert.Throws<AggregateException>(() => readTask.Wait(TimeSpan.FromSeconds(10)));
Assert.IsAssignableFrom<OperationCanceledException>(ex.GetBaseException());
await Assert.ThrowsAsync<OperationCanceledException>(() => readTask.TimeoutAfter(TimeSpan.FromSeconds(10)));
block.Set();
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
<LangVersion>latest</LangVersion>
<SignAssembly>true</SignAssembly>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
<StartupObject>SampleStartups.StartupInjection</StartupObject>
<OutputType>exe</OutputType>
</PropertyGroup>

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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
};

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;netcoreapp2.0;net461</TargetFrameworks>
<OutputType>Exe</OutputType>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;netcoreapp2.0;net461</TargetFrameworks>
<OutputType>Exe</OutputType>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;netcoreapp2.0;net461</TargetFrameworks>
<OutputType>Exe</OutputType>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;netcoreapp2.0;net461</TargetFrameworks>
<OutputType>Exe</OutputType>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;netcoreapp2.0;net461</TargetFrameworks>
<OutputType>Exe</OutputType>
</PropertyGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks>netcoreapp2.2;net461</TargetFrameworks>
</PropertyGroup>
<ItemGroup>