more logging and more resiliant port selection (#996)

This commit is contained in:
Andrew Stanton-Nurse 2017-03-30 09:46:50 -07:00 committed by GitHub
parent 7890fdbf94
commit f15c99c980
15 changed files with 796 additions and 699 deletions

View File

@ -0,0 +1,34 @@
// 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 Microsoft.Extensions.Logging;
namespace System.Diagnostics
{
public static class ProcessLoggingExtensions
{
public static void StartAndCaptureOutAndErrToLogger(this Process process, string prefix, ILogger logger)
{
process.EnableRaisingEvents = true;
process.OutputDataReceived += (_, dataArgs) =>
{
if (!string.IsNullOrEmpty(dataArgs.Data))
{
logger.LogWarning($"{prefix} stdout: {{line}}", dataArgs.Data);
}
};
process.ErrorDataReceived += (_, dataArgs) =>
{
if (!string.IsNullOrEmpty(dataArgs.Data))
{
logger.LogWarning($"{prefix} stderr: {{line}}", dataArgs.Data);
}
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
}
}
}

View File

@ -0,0 +1,35 @@
// 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 System.Threading.Tasks
{
internal static class TaskTimeoutExtensions
{
public static async Task OrTimeout(this Task task, TimeSpan timeout)
{
var completed = await Task.WhenAny(task, Task.Delay(timeout));
if (completed == task)
{
// Manifest any exception
task.GetAwaiter().GetResult();
}
else
{
throw new TimeoutException();
}
}
public static async Task<T> OrTimeout<T>(this Task<T> task, TimeSpan timeout)
{
var completed = await Task.WhenAny(task, Task.Delay(timeout));
if (completed == task)
{
return await task;
}
else
{
throw new TimeoutException();
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
{ {
public static Uri BuildTestUri() public static Uri BuildTestUri()
{ {
return new UriBuilder("http", "localhost", FindFreePort()).Uri; return new UriBuilder("http", "localhost", GetNextPort()).Uri;
} }
public static Uri BuildTestUri(string hint) public static Uri BuildTestUri(string hint)
@ -23,28 +23,28 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.Common
else else
{ {
var uriHint = new Uri(hint); var uriHint = new Uri(hint);
return new UriBuilder(uriHint) { Port = FindFreePort(uriHint.Port) }.Uri; if (uriHint.Port == 0)
}
}
public static int FindFreePort()
{ {
return FindFreePort(0); return new UriBuilder(uriHint) { Port = GetNextPort() }.Uri;
}
else
{
return uriHint;
}
}
} }
public static int FindFreePort(int initialPort) // Copied from https://github.com/aspnet/KestrelHttpServer/blob/47f1db20e063c2da75d9d89653fad4eafe24446c/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/AddressRegistrationTests.cs#L508
public static int GetNextPort()
{ {
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{ {
try // Let the OS assign the next available port. Unless we cycle through all ports
{ // on a test run, the OS will always increment the port number when making these calls.
socket.Bind(new IPEndPoint(IPAddress.Loopback, initialPort)); // This prevents races in parallel test runs where a test is already bound to
} // a given port, and a new test is able to bind to the same port due to port
catch (SocketException) // reuse being enabled by default by the OS.
{
socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
}
return ((IPEndPoint)socket.LocalEndPoint).Port; return ((IPEndPoint)socket.LocalEndPoint).Port;
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -6,6 +6,8 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -34,9 +36,11 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
protected ILogger Logger { get; } protected ILogger Logger { get; }
public abstract DeploymentResult Deploy(); public abstract Task<DeploymentResult> DeployAsync();
protected void DotnetPublish(string publishRoot = null) protected void DotnetPublish(string publishRoot = null)
{
using (Logger.BeginScope("dotnet-publish"))
{ {
if (string.IsNullOrEmpty(DeploymentParameters.TargetFramework)) if (string.IsNullOrEmpty(DeploymentParameters.TargetFramework))
{ {
@ -51,8 +55,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
+ $" --configuration {DeploymentParameters.Configuration}" + $" --configuration {DeploymentParameters.Configuration}"
+ $" {DeploymentParameters.AdditionalPublishParameters}"; + $" {DeploymentParameters.AdditionalPublishParameters}";
Logger.LogInformation($"Executing command {DotnetCommandName} {parameters}");
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
FileName = DotnetCommandName, FileName = DotnetCommandName,
@ -67,22 +69,27 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.PublishEnvironmentVariables); AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.PublishEnvironmentVariables);
var hostProcess = new Process() { StartInfo = startInfo }; var hostProcess = new Process() { StartInfo = startInfo };
hostProcess.ErrorDataReceived += (sender, dataArgs) => { Logger.LogWarning(dataArgs.Data ?? string.Empty); };
hostProcess.OutputDataReceived += (sender, dataArgs) => { Logger.LogInformation(dataArgs.Data ?? string.Empty); }; Logger.LogInformation($"Executing command {DotnetCommandName} {parameters}");
hostProcess.Start();
hostProcess.BeginErrorReadLine(); hostProcess.StartAndCaptureOutAndErrToLogger("dotnet-publish", Logger);
hostProcess.BeginOutputReadLine();
hostProcess.WaitForExit(); hostProcess.WaitForExit();
if (hostProcess.ExitCode != 0) if (hostProcess.ExitCode != 0)
{ {
throw new Exception($"{DotnetCommandName} publish exited with exit code : {hostProcess.ExitCode}"); 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}"); Logger.LogInformation($"{DotnetCommandName} publish finished with exit code : {hostProcess.ExitCode}");
} }
}
protected void CleanPublishedOutput() protected void CleanPublishedOutput()
{
using (Logger.BeginScope("CleanPublishedOutput"))
{ {
if (DeploymentParameters.PreservePublishedApplicationForDebugging) if (DeploymentParameters.PreservePublishedApplicationForDebugging)
{ {
@ -99,6 +106,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
retryDelayMilliseconds: 100); retryDelayMilliseconds: 100);
} }
} }
}
protected void ShutDownIfAnyHostProcess(Process hostProcess) protected void ShutDownIfAnyHostProcess(Process hostProcess)
{ {
@ -149,6 +157,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
} }
protected void InvokeUserApplicationCleanup() protected void InvokeUserApplicationCleanup()
{
using (Logger.BeginScope("UserAdditionalCleanup"))
{ {
if (DeploymentParameters.UserAdditionalCleanup != null) if (DeploymentParameters.UserAdditionalCleanup != null)
{ {
@ -163,6 +173,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
} }
} }
} }
}
protected void TriggerHostShutdown(CancellationTokenSource hostShutdownSource) protected void TriggerHostShutdown(CancellationTokenSource hostShutdownSource)
{ {

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -15,36 +15,31 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
/// Creates a deployer instance based on settings in <see cref="DeploymentParameters"/>. /// Creates a deployer instance based on settings in <see cref="DeploymentParameters"/>.
/// </summary> /// </summary>
/// <param name="deploymentParameters"></param> /// <param name="deploymentParameters"></param>
/// <param name="logger"></param> /// <param name="loggerFactory"></param>
/// <returns></returns> /// <returns></returns>
public static IApplicationDeployer Create(DeploymentParameters deploymentParameters, ILogger logger) public static IApplicationDeployer Create(DeploymentParameters deploymentParameters, ILoggerFactory loggerFactory)
{ {
if (deploymentParameters == null) if (deploymentParameters == null)
{ {
throw new ArgumentNullException(nameof(deploymentParameters)); throw new ArgumentNullException(nameof(deploymentParameters));
} }
if (logger == null) if (loggerFactory == null)
{ {
throw new ArgumentNullException(nameof(logger)); throw new ArgumentNullException(nameof(loggerFactory));
} }
switch (deploymentParameters.ServerType) switch (deploymentParameters.ServerType)
{ {
case ServerType.IISExpress: case ServerType.IISExpress:
return new IISExpressDeployer(deploymentParameters, logger); return new IISExpressDeployer(deploymentParameters, loggerFactory.CreateLogger<IISExpressDeployer>());
#if NET46
case ServerType.IIS: case ServerType.IIS:
return new IISDeployer(deploymentParameters, logger); throw new NotSupportedException("The IIS deployer is no longer supported");
#elif NETSTANDARD1_3
#else
#error Target framework needs to be updated.
#endif
case ServerType.WebListener: case ServerType.WebListener:
case ServerType.Kestrel: case ServerType.Kestrel:
return new SelfHostDeployer(deploymentParameters, logger); return new SelfHostDeployer(deploymentParameters, loggerFactory.CreateLogger<SelfHostDeployer>());
case ServerType.Nginx: case ServerType.Nginx:
return new NginxDeployer(deploymentParameters, logger); return new NginxDeployer(deploymentParameters, loggerFactory.CreateLogger<NginxDeployer>());
default: default:
throw new NotSupportedException( throw new NotSupportedException(
string.Format("Found no deployers suitable for server type '{0}' with the current runtime.", string.Format("Found no deployers suitable for server type '{0}' with the current runtime.",

View File

@ -1,7 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Server.IntegrationTesting namespace Microsoft.AspNetCore.Server.IntegrationTesting
{ {
@ -14,6 +15,6 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
/// Deploys the application to the target with specified <see cref="DeploymentParameters"/>. /// Deploys the application to the target with specified <see cref="DeploymentParameters"/>.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
DeploymentResult Deploy(); Task<DeploymentResult> DeployAsync();
} }
} }

View File

@ -1,154 +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.
#if NET46
using System;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
using Microsoft.Extensions.Logging;
using Microsoft.Web.Administration;
namespace Microsoft.AspNetCore.Server.IntegrationTesting
{
/// <summary>
/// Deployer for IIS.
/// </summary>
public class IISDeployer : ApplicationDeployer
{
private IISApplication _application;
private CancellationTokenSource _hostShutdownToken = new CancellationTokenSource();
private static object _syncObject = new object();
public IISDeployer(DeploymentParameters startParameters, ILogger logger)
: base(startParameters, logger)
{
}
public override DeploymentResult Deploy()
{
// Start timer
StartTimer();
// Only supports publish and run on IIS.
DeploymentParameters.PublishApplicationBeforeDeployment = true;
_application = new IISApplication(DeploymentParameters, Logger);
if (DeploymentParameters.PublishApplicationBeforeDeployment)
{
DotnetPublish(publishRoot: _application.WebSiteRootFolder);
}
// Drop a json file instead of setting environment variable.
SetAspEnvironmentWithJson();
var uri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint);
lock (_syncObject)
{
// To prevent modifying the IIS setup concurrently.
_application.Deploy(uri);
}
// Warm up time for IIS setup.
Thread.Sleep(1 * 1000);
Logger.LogInformation("Successfully finished IIS application directory setup.");
return new DeploymentResult
{
ContentRoot = DeploymentParameters.PublishedApplicationRootPath,
DeploymentParameters = DeploymentParameters,
// Accomodate the vdir name.
ApplicationBaseUri = uri.ToString(),
HostShutdownToken = _hostShutdownToken.Token
};
}
public override void Dispose()
{
if (_application != null)
{
lock (_syncObject)
{
// Sequentialize IIS operations.
_application.StopAndDeleteAppPool();
}
TriggerHostShutdown(_hostShutdownToken);
Thread.Sleep(TimeSpan.FromSeconds(3));
}
CleanPublishedOutput();
InvokeUserApplicationCleanup();
StopTimer();
}
private void SetAspEnvironmentWithJson()
{
////S Drop a hosting.json with environment information.
// Logger.LogInformation("Creating hosting.json file with environment information.");
// var jsonFile = Path.Combine(DeploymentParameters.ApplicationPath, "hosting.json");
// File.WriteAllText(jsonFile, string.Format("{ \"environment\":\"{0}\" }", DeploymentParameters.EnvironmentName));
}
private class IISApplication
{
private readonly ServerManager _serverManager = new ServerManager();
private readonly DeploymentParameters _deploymentParameters;
private readonly ILogger _logger;
public IISApplication(DeploymentParameters deploymentParameters, ILogger logger)
{
_deploymentParameters = deploymentParameters;
_logger = logger;
WebSiteName = CreateTestSiteName();
}
public string WebSiteName { get; }
public string WebSiteRootFolder => $"{Environment.GetEnvironmentVariable("SystemDrive")}\\inetpub\\{WebSiteName}";
public void Deploy(Uri uri)
{
var contentRoot = _deploymentParameters.PublishApplicationBeforeDeployment ? _deploymentParameters.PublishedApplicationRootPath : _deploymentParameters.ApplicationPath;
_serverManager.Sites.Add(WebSiteName, contentRoot, uri.Port);
_serverManager.CommitChanges();
}
public void StopAndDeleteAppPool()
{
if (string.IsNullOrEmpty(WebSiteName))
{
return;
}
var siteToRemove = _serverManager.Sites.FirstOrDefault(site => site.Name == WebSiteName);
if (siteToRemove != null)
{
siteToRemove.Stop();
_serverManager.Sites.Remove(siteToRemove);
_serverManager.CommitChanges();
}
}
private string CreateTestSiteName()
{
if (!string.IsNullOrEmpty(_deploymentParameters.SiteName))
{
return $"{_deploymentParameters.SiteName}{DateTime.Now.ToString("yyyyMMddHHmmss")}";
}
else
{
return $"testsite{DateTime.Now.ToString("yyyyMMddHHmmss")}";
}
}
}
}
}
#elif NETSTANDARD1_3
#else
#error Target framework needs to be updated.
#endif

View File

@ -1,11 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -16,6 +18,13 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
/// </summary> /// </summary>
public class IISExpressDeployer : ApplicationDeployer 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; private Process _hostProcess;
public IISExpressDeployer(DeploymentParameters deploymentParameters, ILogger logger) public IISExpressDeployer(DeploymentParameters deploymentParameters, ILogger logger)
@ -42,7 +51,9 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
} }
} }
public override DeploymentResult Deploy() public override async Task<DeploymentResult> DeployAsync()
{
using (Logger.BeginScope("Deployment"))
{ {
// Start timer // Start timer
StartTimer(); StartTimer();
@ -56,28 +67,46 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
var contentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath; var contentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath;
var uri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); var testUri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint);
// Launch the host process. // Launch the host process.
var hostExitToken = StartIISExpress(uri, contentRoot); var (actualUri, hostExitToken) = await StartIISExpressAsync(testUri, contentRoot);
Logger.LogInformation("Application ready at URL: {appUrl}", actualUri);
return new DeploymentResult return new DeploymentResult
{ {
ContentRoot = contentRoot, ContentRoot = contentRoot,
DeploymentParameters = DeploymentParameters, DeploymentParameters = DeploymentParameters,
// Right now this works only for urls like http://localhost:5001/. Does not work for http://localhost:5001/subpath. // Right now this works only for urls like http://localhost:5001/. Does not work for http://localhost:5001/subpath.
ApplicationBaseUri = uri.ToString(), ApplicationBaseUri = actualUri.ToString(),
HostShutdownToken = hostExitToken HostShutdownToken = hostExitToken
}; };
} }
}
private CancellationToken StartIISExpress(Uri uri, string contentRoot) 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)) 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 // 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. // We take a copy of the original specified applicationHost.Config to prevent modifying the one in the repo.
if (DeploymentParameters.ServerConfigTemplateContent.Contains("[ANCMPath]")) if (serverConfig.Contains("[ANCMPath]"))
{ {
string ancmPath; string ancmPath;
if (!IsWin8OrLater) if (!IsWin8OrLater)
@ -107,18 +136,28 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmPath); throw new FileNotFoundException("AspNetCoreModule could not be found.", ancmPath);
} }
DeploymentParameters.ServerConfigTemplateContent = Logger.LogDebug("Writing ANCMPath '{ancmPath}' to config", ancmPath);
DeploymentParameters.ServerConfigTemplateContent.Replace("[ANCMPath]", ancmPath); serverConfig =
serverConfig.Replace("[ANCMPath]", ancmPath);
} }
DeploymentParameters.ServerConfigTemplateContent = Logger.LogDebug("Writing ApplicationPhysicalPath '{applicationPhysicalPath}' to config", contentRoot);
DeploymentParameters.ServerConfigTemplateContent Logger.LogDebug("Writing Port '{port}' to config", port);
serverConfig =
serverConfig
.Replace("[ApplicationPhysicalPath]", contentRoot) .Replace("[ApplicationPhysicalPath]", contentRoot)
.Replace("[PORT]", uri.Port.ToString()); .Replace("[PORT]", port.ToString());
DeploymentParameters.ServerConfigLocation = Path.GetTempFileName(); DeploymentParameters.ServerConfigLocation = Path.GetTempFileName();
File.WriteAllText(DeploymentParameters.ServerConfigLocation, DeploymentParameters.ServerConfigTemplateContent); 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);
} }
var parameters = string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) ? var parameters = string.IsNullOrWhiteSpace(DeploymentParameters.ServerConfigLocation) ?
@ -127,7 +166,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
var iisExpressPath = GetIISExpressPath(); var iisExpressPath = GetIISExpressPath();
Logger.LogInformation("Executing command : {iisExpress} {args}", iisExpressPath, parameters); Logger.LogInformation("Executing command : {iisExpress} {parameters}", iisExpressPath, parameters);
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
@ -141,27 +180,71 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables);
_hostProcess = new Process() { StartInfo = startInfo }; Uri url = null;
_hostProcess.ErrorDataReceived += (sender, dataArgs) => { Logger.LogError(dataArgs.Data ?? string.Empty); }; var started = new TaskCompletionSource<bool>();
_hostProcess.OutputDataReceived += (sender, dataArgs) => { Logger.LogInformation(dataArgs.Data ?? string.Empty); };
_hostProcess.EnableRaisingEvents = true; var process = new Process() { StartInfo = startInfo };
var hostExitTokenSource = new CancellationTokenSource(); process.OutputDataReceived += (sender, dataArgs) =>
_hostProcess.Exited += (sender, e) =>
{ {
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);
TriggerHostShutdown(hostExitTokenSource); TriggerHostShutdown(hostExitTokenSource);
}; };
_hostProcess.Start(); process.StartAndCaptureOutAndErrToLogger("iisexpress", Logger);
_hostProcess.BeginErrorReadLine(); Logger.LogInformation("iisexpress Process {pid} started", process.Id);
_hostProcess.BeginOutputReadLine();
if (_hostProcess.HasExited) if (process.HasExited)
{ {
Logger.LogError("Host process {processName} exited with code {exitCode} or failed to start.", startInfo.FileName, _hostProcess.ExitCode); 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"); throw new Exception("Failed to start host");
} }
Logger.LogInformation("Started iisexpress. Process Id : {processId}", _hostProcess.Id); // Wait for the app to start
return hostExitTokenSource.Token; if (!await started.Task)
{
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() private string GetIISExpressPath()
@ -178,6 +261,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
} }
public override void Dispose() public override void Dispose()
{
using (Logger.BeginScope("Dispose"))
{ {
ShutDownIfAnyHostProcess(_hostProcess); ShutDownIfAnyHostProcess(_hostProcess);
@ -185,6 +270,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
&& File.Exists(DeploymentParameters.ServerConfigLocation)) && File.Exists(DeploymentParameters.ServerConfigLocation))
{ {
// Delete the temp applicationHostConfig that we created. // Delete the temp applicationHostConfig that we created.
Logger.LogDebug("Deleting applicationHost.config file from {configLocation}", DeploymentParameters.ServerConfigLocation);
try try
{ {
File.Delete(DeploymentParameters.ServerConfigLocation); File.Delete(DeploymentParameters.ServerConfigLocation);
@ -206,4 +292,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
StopTimer(); StopTimer();
} }
} }
}
} }

View File

@ -1,10 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -23,30 +24,34 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{ {
} }
public override DeploymentResult Deploy() public override async Task<DeploymentResult> DeployAsync()
{
using (Logger.BeginScope("Deploy"))
{ {
_configFile = Path.GetTempFileName(); _configFile = Path.GetTempFileName();
var uri = new Uri(DeploymentParameters.ApplicationBaseUriHint); var uri = new Uri(DeploymentParameters.ApplicationBaseUriHint);
var redirectUri = $"http://localhost:{TestUriHelper.FindFreePort()}"; var redirectUri = $"http://localhost:{TestUriHelper.GetNextPort()}";
if (DeploymentParameters.PublishApplicationBeforeDeployment) if (DeploymentParameters.PublishApplicationBeforeDeployment)
{ {
DotnetPublish(); DotnetPublish();
} }
var exitToken = StartSelfHost(new Uri(redirectUri)); var (appUri, exitToken) = await StartSelfHostAsync(new Uri(redirectUri));
SetupNginx(redirectUri, uri); SetupNginx(appUri.ToString(), uri);
Logger.LogInformation("Application ready at URL: {appUrl}", uri);
// Wait for App to be loaded since Nginx returns 502 instead of 503 when App isn't loaded // Wait for App to be loaded since Nginx returns 502 instead of 503 when App isn't loaded
// Target actual address to avoid going through Nginx proxy // Target actual address to avoid going through Nginx proxy
using (var httpClient = new HttpClient()) using (var httpClient = new HttpClient())
{ {
var response = RetryHelper.RetryRequest(() => var response = await RetryHelper.RetryRequest(() =>
{ {
return httpClient.GetAsync(redirectUri); return httpClient.GetAsync(redirectUri);
}, Logger, exitToken).Result; }, Logger, exitToken);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
@ -62,17 +67,30 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
HostShutdownToken = exitToken HostShutdownToken = exitToken
}; };
} }
}
private void SetupNginx(string redirectUri, Uri originalUri) private void SetupNginx(string redirectUri, Uri originalUri)
{
using (Logger.BeginScope("SetupNginx"))
{ {
// copy nginx.conf template and replace pertinent information // 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 DeploymentParameters.ServerConfigTemplateContent = DeploymentParameters.ServerConfigTemplateContent
.Replace("[user]", Environment.GetEnvironmentVariable("LOGNAME")) .Replace("[user]", Environment.GetEnvironmentVariable("LOGNAME"))
.Replace("[errorlog]", Path.Combine(DeploymentParameters.ApplicationPath, "nginx.error.log")) .Replace("[errorlog]", errorLog)
.Replace("[accesslog]", Path.Combine(DeploymentParameters.ApplicationPath, "nginx.access.log")) .Replace("[accesslog]", accessLog)
.Replace("[listenPort]", originalUri.Port.ToString()) .Replace("[listenPort]", originalUri.Port.ToString())
.Replace("[redirectUri]", redirectUri) .Replace("[redirectUri]", redirectUri)
.Replace("[pidFile]", Path.Combine(DeploymentParameters.ApplicationPath, Guid.NewGuid().ToString())); .Replace("[pidFile]", pidFile);
Logger.LogDebug("Using PID file: {pidFile}", pidFile);
Logger.LogDebug("Using Error Log file: {errorLog}", pidFile);
Logger.LogDebug("Using Access Log file: {accessLog}", pidFile);
if (Logger.IsEnabled(LogLevel.Trace))
{
Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", DeploymentParameters.ServerConfigTemplateContent);
}
File.WriteAllText(_configFile, DeploymentParameters.ServerConfigTemplateContent); File.WriteAllText(_configFile, DeploymentParameters.ServerConfigTemplateContent);
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
@ -89,32 +107,30 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
using (var runNginx = new Process() { StartInfo = startInfo }) using (var runNginx = new Process() { StartInfo = startInfo })
{ {
runNginx.ErrorDataReceived += (sender, dataArgs) => runNginx.StartAndCaptureOutAndErrToLogger("nginx start", Logger);
{
if (!string.IsNullOrEmpty(dataArgs.Data))
{
Logger.LogWarning("nginx: " + dataArgs.Data);
}
};
runNginx.OutputDataReceived += (sender, dataArgs) =>
{
if (!string.IsNullOrEmpty(dataArgs.Data))
{
Logger.LogInformation("nginx: " + dataArgs.Data);
}
};
runNginx.Start();
runNginx.BeginErrorReadLine();
runNginx.BeginOutputReadLine();
runNginx.WaitForExit(_waitTime); runNginx.WaitForExit(_waitTime);
if (runNginx.ExitCode != 0) if (runNginx.ExitCode != 0)
{ {
throw new Exception("Failed to start Nginx"); throw new Exception("Failed to start nginx");
}
// Read the PID file
if(!File.Exists(pidFile))
{
Logger.LogWarning("Unable to find nginx PID file: {pidFile}", pidFile);
}
else
{
var pid = File.ReadAllText(pidFile);
Logger.LogInformation("nginx process ID {pid} started", pid);
}
} }
} }
} }
public override void Dispose() public override void Dispose()
{
using (Logger.BeginScope("Dispose"))
{ {
if (!string.IsNullOrEmpty(_configFile)) if (!string.IsNullOrEmpty(_configFile))
{ {
@ -132,14 +148,17 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
using (var runNginx = new Process() { StartInfo = startInfo }) using (var runNginx = new Process() { StartInfo = startInfo })
{ {
runNginx.Start(); runNginx.StartAndCaptureOutAndErrToLogger("nginx stop", Logger);
runNginx.WaitForExit(_waitTime); runNginx.WaitForExit(_waitTime);
Logger.LogInformation("nginx stop command issued");
} }
Logger.LogDebug("Deleting config file: {configFile}", _configFile);
File.Delete(_configFile); File.Delete(_configFile);
} }
base.Dispose(); base.Dispose();
} }
} }
}
} }

View File

@ -7,7 +7,9 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -73,7 +75,9 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
} }
} }
public override DeploymentResult Deploy() public override async Task<DeploymentResult> DeployAsync()
{
using (Logger.BeginScope("Deploy"))
{ {
if (_isDisposed) if (_isDisposed)
{ {
@ -97,7 +101,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
copySubDirs: true); copySubDirs: true);
Logger.LogInformation($"Copied the locally published folder to the file share path '{_deployedFolderPathInFileShare}'"); Logger.LogInformation($"Copied the locally published folder to the file share path '{_deployedFolderPathInFileShare}'");
RunScript("StartServer"); await RunScriptAsync("StartServer");
return new DeploymentResult return new DeploymentResult
{ {
@ -105,8 +109,11 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
DeploymentParameters = DeploymentParameters DeploymentParameters = DeploymentParameters
}; };
} }
}
public override void Dispose() public override void Dispose()
{
using (Logger.BeginScope("Dispose"))
{ {
if (_isDisposed) if (_isDisposed)
{ {
@ -118,7 +125,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
try try
{ {
Logger.LogInformation($"Stopping the application on the server '{_deploymentParameters.ServerName}'"); Logger.LogInformation($"Stopping the application on the server '{_deploymentParameters.ServerName}'");
RunScript("StopServer"); RunScriptAsync("StopServer").Wait();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -145,6 +152,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
Logger.LogWarning(0, $"Failed to delete the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'.", ex); Logger.LogWarning(0, $"Failed to delete the locally published folder '{DeploymentParameters.PublishedApplicationRootPath}'.", ex);
} }
} }
}
private void UpdateWebConfig() private void UpdateWebConfig()
{ {
@ -176,13 +184,20 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
environmentVariablesSection.Add(environmentVariable); environmentVariablesSection.Add(environmentVariable);
} }
if(Logger.IsEnabled(LogLevel.Trace))
{
Logger.LogTrace($"Config File Content:{Environment.NewLine}===START CONFIG==={Environment.NewLine}{{configContent}}{Environment.NewLine}===END CONFIG===", webConfig.ToString());
}
using (var fileStream = File.Open(webConfigFilePath, FileMode.Open)) using (var fileStream = File.Open(webConfigFilePath, FileMode.Open))
{ {
webConfig.Save(fileStream); webConfig.Save(fileStream);
} }
} }
private void RunScript(string serverAction) private async Task RunScriptAsync(string serverAction)
{
using (Logger.BeginScope($"RunScript:{serverAction}"))
{ {
var remotePSSessionHelperScript = _scripts.Value.RemotePSSessionHelper; var remotePSSessionHelperScript = _scripts.Value.RemotePSSessionHelper;
@ -237,26 +252,18 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
using (var runScriptsOnRemoteServerProcess = new Process() { StartInfo = startInfo }) using (var runScriptsOnRemoteServerProcess = new Process() { StartInfo = startInfo })
{ {
var processExited = new TaskCompletionSource<object>();
runScriptsOnRemoteServerProcess.EnableRaisingEvents = true; runScriptsOnRemoteServerProcess.EnableRaisingEvents = true;
runScriptsOnRemoteServerProcess.ErrorDataReceived += (sender, dataArgs) => runScriptsOnRemoteServerProcess.Exited += (sender, exitedArgs) =>
{ {
if (!string.IsNullOrEmpty(dataArgs.Data)) Logger.LogInformation($"[{_deploymentParameters.ServerName} {serverAction} stdout]: script complete");
{ processExited.TrySetResult(null);
Logger.LogWarning($"[{_deploymentParameters.ServerName}]: {dataArgs.Data}");
}
}; };
runScriptsOnRemoteServerProcess.OutputDataReceived += (sender, dataArgs) => runScriptsOnRemoteServerProcess.StartAndCaptureOutAndErrToLogger(serverAction, Logger);
{
if (!string.IsNullOrEmpty(dataArgs.Data))
{
Logger.LogInformation($"[{_deploymentParameters.ServerName}]: {dataArgs.Data}");
}
};
runScriptsOnRemoteServerProcess.Start(); await processExited.Task.OrTimeout(TimeSpan.FromMinutes(1));
runScriptsOnRemoteServerProcess.BeginErrorReadLine();
runScriptsOnRemoteServerProcess.BeginOutputReadLine();
runScriptsOnRemoteServerProcess.WaitForExit((int)TimeSpan.FromMinutes(1).TotalMilliseconds); runScriptsOnRemoteServerProcess.WaitForExit((int)TimeSpan.FromMinutes(1).TotalMilliseconds);
if (runScriptsOnRemoteServerProcess.HasExited && runScriptsOnRemoteServerProcess.ExitCode != 0) if (runScriptsOnRemoteServerProcess.HasExited && runScriptsOnRemoteServerProcess.ExitCode != 0)
@ -265,6 +272,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
} }
} }
} }
}
private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs)
{ {

View File

@ -1,11 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IntegrationTesting.Common; using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -16,6 +18,9 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
/// </summary> /// </summary>
public class SelfHostDeployer : ApplicationDeployer public class SelfHostDeployer : ApplicationDeployer
{ {
private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$");
private const string ApplicationStartedMessage = "Application started. Press Ctrl+C to shut down.";
public Process HostProcess { get; private set; } public Process HostProcess { get; private set; }
public SelfHostDeployer(DeploymentParameters deploymentParameters, ILogger logger) public SelfHostDeployer(DeploymentParameters deploymentParameters, ILogger logger)
@ -23,7 +28,9 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
{ {
} }
public override DeploymentResult Deploy() public override async Task<DeploymentResult> DeployAsync()
{
using (Logger.BeginScope("SelfHost.Deploy"))
{ {
// Start timer // Start timer
StartTimer(); StartTimer();
@ -33,20 +40,26 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
DotnetPublish(); DotnetPublish();
} }
var uri = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint); var hintUrl = TestUriHelper.BuildTestUri(DeploymentParameters.ApplicationBaseUriHint);
// Launch the host process. // Launch the host process.
var hostExitToken = StartSelfHost(uri); var (actualUrl, hostExitToken) = await StartSelfHostAsync(hintUrl);
Logger.LogInformation("Application ready at URL: {appUrl}", actualUrl);
return new DeploymentResult return new DeploymentResult
{ {
ContentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath, ContentRoot = DeploymentParameters.PublishApplicationBeforeDeployment ? DeploymentParameters.PublishedApplicationRootPath : DeploymentParameters.ApplicationPath,
DeploymentParameters = DeploymentParameters, DeploymentParameters = DeploymentParameters,
ApplicationBaseUri = uri.ToString(), ApplicationBaseUri = actualUrl.ToString(),
HostShutdownToken = hostExitToken HostShutdownToken = hostExitToken
}; };
} }
}
protected CancellationToken StartSelfHost(Uri uri) protected async Task<(Uri url, CancellationToken hostExitToken)> StartSelfHostAsync(Uri hintUrl)
{
using (Logger.BeginScope("StartSelfHost"))
{ {
string executableName; string executableName;
string executableArgs = string.Empty; string executableArgs = string.Empty;
@ -83,7 +96,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
executableArgs = $"run --framework {targetFramework} {DotnetArgumentSeparator}"; executableArgs = $"run --framework {targetFramework} {DotnetArgumentSeparator}";
} }
executableArgs += $" --server.urls {uri} " executableArgs += $" --server.urls {hintUrl} "
+ $" --server {(DeploymentParameters.ServerType == ServerType.WebListener ? "Microsoft.AspNetCore.Server.HttpSys" : "Microsoft.AspNetCore.Server.Kestrel")}"; + $" --server {(DeploymentParameters.ServerType == ServerType.WebListener ? "Microsoft.AspNetCore.Server.HttpSys" : "Microsoft.AspNetCore.Server.Kestrel")}";
Logger.LogInformation($"Executing {executableName} {executableArgs}"); Logger.LogInformation($"Executing {executableName} {executableArgs}");
@ -103,21 +116,36 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables); AddEnvironmentVariablesToProcess(startInfo, DeploymentParameters.EnvironmentVariables);
Uri actualUrl = null;
var started = new TaskCompletionSource<object>();
HostProcess = new Process() { StartInfo = startInfo }; HostProcess = new Process() { StartInfo = startInfo };
HostProcess.ErrorDataReceived += (sender, dataArgs) => { Logger.LogError(dataArgs.Data ?? string.Empty); };
HostProcess.OutputDataReceived += (sender, dataArgs) => { Logger.LogInformation(dataArgs.Data ?? string.Empty); };
HostProcess.EnableRaisingEvents = true; HostProcess.EnableRaisingEvents = true;
HostProcess.OutputDataReceived += (sender, dataArgs) =>
{
if (string.Equals(dataArgs.Data, ApplicationStartedMessage))
{
started.TrySetResult(null);
}
else if (!string.IsNullOrEmpty(dataArgs.Data))
{
var m = NowListeningRegex.Match(dataArgs.Data);
if (m.Success)
{
actualUrl = new Uri(m.Groups["url"].Value);
}
}
};
var hostExitTokenSource = new CancellationTokenSource(); var hostExitTokenSource = new CancellationTokenSource();
HostProcess.Exited += (sender, e) => HostProcess.Exited += (sender, e) =>
{ {
Logger.LogInformation("host process ID {pid} shut down", HostProcess.Id);
TriggerHostShutdown(hostExitTokenSource); TriggerHostShutdown(hostExitTokenSource);
}; };
try try
{ {
HostProcess.Start(); HostProcess.StartAndCaptureOutAndErrToLogger(executableName, Logger);
HostProcess.BeginErrorReadLine();
HostProcess.BeginOutputReadLine();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -126,15 +154,21 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
if (HostProcess.HasExited) if (HostProcess.HasExited)
{ {
Logger.LogError("Host process {processName} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.ExitCode); Logger.LogError("Host process {processName} {pid} exited with code {exitCode} or failed to start.", startInfo.FileName, HostProcess.Id, HostProcess.ExitCode);
throw new Exception("Failed to start host"); throw new Exception("Failed to start host");
} }
Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id); Logger.LogInformation("Started {fileName}. Process Id : {processId}", startInfo.FileName, HostProcess.Id);
return hostExitTokenSource.Token;
await started.Task;
return (url: actualUrl ?? hintUrl, hostExitToken: hostExitTokenSource.Token);
}
} }
public override void Dispose() public override void Dispose()
{
using (Logger.BeginScope("SelfHost.Dispose"))
{ {
ShutDownIfAnyHostProcess(HostProcess); ShutDownIfAnyHostProcess(HostProcess);
@ -148,4 +182,5 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting
StopTimer(); StopTimer();
} }
} }
}
} }

View File

@ -4,7 +4,7 @@
<PropertyGroup> <PropertyGroup>
<Description>ASP.NET Core helpers to deploy applications to IIS Express, IIS, WebListener and Kestrel for testing.</Description> <Description>ASP.NET Core helpers to deploy applications to IIS Express, IIS, WebListener and Kestrel for testing.</Description>
<VersionPrefix>0.3.0</VersionPrefix> <VersionPrefix>0.4.0</VersionPrefix>
<TargetFrameworks>net46;netstandard1.3</TargetFrameworks> <TargetFrameworks>net46;netstandard1.3</TargetFrameworks>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
@ -17,6 +17,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.ValueTuple" Version="$(CoreFxVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(AspNetCoreVersion)" />

View File

@ -3,6 +3,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.AspNetCore.Server.IntegrationTesting.xunit; using Microsoft.AspNetCore.Server.IntegrationTesting.xunit;
using Microsoft.AspNetCore.Testing.xunit; using Microsoft.AspNetCore.Testing.xunit;
@ -16,11 +17,12 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests
[ConditionalFact] [ConditionalFact]
[OSSkipCondition(OperatingSystems.Windows)] [OSSkipCondition(OperatingSystems.Windows)]
[OSSkipCondition(OperatingSystems.MacOSX)] [OSSkipCondition(OperatingSystems.MacOSX)]
public void ShutdownTest() public async Task ShutdownTest()
{ {
var logger = new LoggerFactory() var logger = new LoggerFactory()
.AddConsole() .AddConsole()
.CreateLogger(nameof(ShutdownTest)); .CreateLogger(nameof(ShutdownTest));
logger.LogInformation("Started test: {testName}", nameof(ShutdownTest));
var applicationPath = Path.Combine(TestProjectHelpers.GetSolutionRoot(), "test", var applicationPath = Path.Combine(TestProjectHelpers.GetSolutionRoot(), "test",
"Microsoft.AspNetCore.Hosting.TestSites"); "Microsoft.AspNetCore.Hosting.TestSites");
@ -39,10 +41,10 @@ namespace Microsoft.AspNetCore.Hosting.FunctionalTests
using (var deployer = new SelfHostDeployer(deploymentParameters, logger)) using (var deployer = new SelfHostDeployer(deploymentParameters, logger))
{ {
deployer.Deploy(); await deployer.DeployAsync();
// Wait for application to start // Wait for application to start
System.Threading.Thread.Sleep(1000); await Task.Delay(1000);
string output = string.Empty; string output = string.Empty;
deployer.HostProcess.OutputDataReceived += (sender, args) => output += args.Data + '\n'; deployer.HostProcess.OutputDataReceived += (sender, args) => output += args.Data + '\n';

View File

@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="..\..\build\common.props" /> <Import Project="..\..\build\common.props" />
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net46</TargetFrameworks> <TargetFrameworks>netcoreapp2.0;net46</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks> <TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
<OutputType>Exe</OutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,22 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:49570/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Microsoft.AspNetCore.Hosting.TestSites": {
"commandName": "Project"
}
}
}