Fix another shutdown race and appofline race (#1071)

This commit is contained in:
Pavel Krymets 2018-07-26 08:12:08 -07:00 committed by GitHub
parent ba18129d79
commit 61b4473abe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 733 additions and 540 deletions

View File

@ -230,6 +230,8 @@
<ClInclude Include="globalmodule.h" />
<ClInclude Include="applicationmanager.h" />
<ClInclude Include="proxymodule.h" />
<ClInclude Include="ServerErrorApplication.h" />
<ClInclude Include="ServerErrorHandler.h" />
<ClInclude Include="stdafx.h" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
#pragma once
#include "PollingAppOfflineApplication.h"
#include "requesthandler.h"
#include "ServerErrorHandler.h"
class ServerErrorApplication : public PollingAppOfflineApplication
{
public:
ServerErrorApplication(IHttpApplication& pApplication, HRESULT hr)
: m_HR(hr),
PollingAppOfflineApplication(pApplication, PollingAppOfflineApplicationMode::StopWhenAdded)
{
m_status = APPLICATION_STATUS::RUNNING;
}
~ServerErrorApplication() = default;
HRESULT CreateHandler(IHttpContext * pHttpContext, IREQUEST_HANDLER ** pRequestHandler) override
{
*pRequestHandler = new ServerErrorHandler(pHttpContext, m_HR);
return S_OK;
}
HRESULT OnAppOfflineFound() override { return S_OK; }
private:
HRESULT m_HR;
};

View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
#pragma once
#include "requesthandler.h"
class ServerErrorHandler : public REQUEST_HANDLER
{
public:
ServerErrorHandler(IHttpContext* pContext, HRESULT hr) : m_pContext(pContext), m_HR(hr)
{
}
REQUEST_NOTIFICATION_STATUS OnExecuteRequestHandler() override
{
m_pContext->GetResponse()->SetStatus(500, "Internal Server Error", 0, m_HR);
return RQ_NOTIFICATION_FINISH_REQUEST;
}
private:
IHttpContext * m_pContext;
HRESULT m_HR;
};

View File

@ -12,8 +12,10 @@
#include "SRWExclusiveLock.h"
#include "GlobalVersionUtility.h"
#include "exceptions.h"
#include "PollingAppOfflineApplication.h"
#include "EventLog.h"
#include "HandleWrapper.h"
#include "ServerErrorApplication.h"
#include "AppOfflineApplication.h"
extern HINSTANCE g_hModule;
@ -29,21 +31,7 @@ PFN_ASPNETCORE_CREATE_APPLICATION APPLICATION_INFO::s_pfnAspNetCoreCreateApplica
APPLICATION_INFO::~APPLICATION_INFO()
{
if (m_pApplication != NULL)
{
// shutdown the application
m_pApplication->ShutDown();
m_pApplication->DereferenceApplication();
m_pApplication = NULL;
}
// configuration should be dereferenced after application shutdown
// since the former will use it during shutdown
if (m_pConfiguration != NULL)
{
delete m_pConfiguration;
m_pConfiguration = NULL;
}
ShutDownApplication();
}
HRESULT
@ -51,7 +39,7 @@ APPLICATION_INFO::Initialize(
_In_ IHttpApplication &pApplication
)
{
m_pConfiguration = new ASPNETCORE_SHIM_CONFIG();
m_pConfiguration.reset(new ASPNETCORE_SHIM_CONFIG());
RETURN_IF_FAILED(m_pConfiguration->Populate(&m_pServer, &pApplication));
RETURN_IF_FAILED(m_struInfoKey.Copy(pApplication.GetApplicationId()));
@ -60,22 +48,16 @@ APPLICATION_INFO::Initialize(
HRESULT
APPLICATION_INFO::EnsureApplicationCreated(
IHttpContext *pHttpContext
APPLICATION_INFO::GetOrCreateApplication(
IHttpContext *pHttpContext,
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER>& pApplication
)
{
HRESULT hr = S_OK;
IAPPLICATION *pApplication = NULL;
STRU struExeLocation;
STRU struHostFxrDllLocation;
STACK_STRU(struFileName, 300); // >MAX_PATH
SRWExclusiveLock lock(m_applicationLock);
if (m_pApplication != nullptr && m_pApplication->QueryStatus() != RECYCLED)
{
return S_OK;
}
SRWExclusiveLock lock(m_srwLock);
auto& httpApplication = *pHttpContext->GetApplication();
if (m_pApplication != nullptr)
{
@ -84,7 +66,6 @@ APPLICATION_INFO::EnsureApplicationCreated(
LOG_INFO("Application went offline");
// Application that went offline
// are supposed to recycle themselves
m_pApplication->DereferenceApplication();
m_pApplication = nullptr;
}
else
@ -93,26 +74,15 @@ APPLICATION_INFO::EnsureApplicationCreated(
FINISHED(S_OK);
}
}
else if (m_fAppCreationAttempted)
{
// previous CreateApplication failed
FINISHED(E_APPLICATION_ACTIVATION_EXEC_FAILURE);
}
auto& httpApplication = *pHttpContext->GetApplication();
if (PollingAppOfflineApplication::ShouldBeStarted(httpApplication))
if (AppOfflineApplication::ShouldBeStarted(httpApplication))
{
LOG_INFO("Detected app_ofline file, creating polling application");
m_pApplication = new PollingAppOfflineApplication(httpApplication);
LOG_INFO("Detected app_offline file, creating polling application");
m_pApplication.reset(new AppOfflineApplication(httpApplication));
}
else
{
// Move the request handler check inside of the lock
// such that only one request finds and loads it.
// FindRequestHandlerAssembly obtains a global lock, but after releasing the lock,
// there is a period where we could call
m_fAppCreationAttempted = TRUE;
STRU struExeLocation;
FINISHED_IF_FAILED(FindRequestHandlerAssembly(struExeLocation));
if (m_pfnAspNetCoreCreateApplication == NULL)
@ -123,15 +93,17 @@ APPLICATION_INFO::EnsureApplicationCreated(
std::array<APPLICATION_PARAMETER, 1> parameters {
{"InProcessExeLocation", struExeLocation.QueryStr()}
};
LOG_INFO("Creating handler application");
IAPPLICATION * newApplication;
FINISHED_IF_FAILED(m_pfnAspNetCoreCreateApplication(
&m_pServer,
pHttpContext->GetApplication(),
&httpApplication,
parameters.data(),
static_cast<DWORD>(parameters.size()),
&pApplication));
&newApplication));
m_pApplication = pApplication;
m_pApplication.reset(newApplication);
}
Finished:
@ -143,8 +115,15 @@ Finished:
EVENTLOG_ERROR_TYPE,
ASPNETCORE_EVENT_ADD_APPLICATION_ERROR,
ASPNETCORE_EVENT_ADD_APPLICATION_ERROR_MSG,
pHttpContext->GetApplication()->GetApplicationId(),
httpApplication.GetApplicationId(),
hr);
m_pApplication.reset(new ServerErrorApplication(httpApplication, hr));
}
if (m_pApplication)
{
pApplication = ReferenceApplication(m_pApplication.get());
}
return hr;
@ -413,107 +392,47 @@ Finished:
VOID
APPLICATION_INFO::RecycleApplication()
{
IAPPLICATION* pApplication;
HANDLE hThread = INVALID_HANDLE_VALUE;
SRWExclusiveLock lock(m_applicationLock);
if (m_pApplication != NULL)
if (m_pApplication)
{
SRWExclusiveLock lock(m_srwLock);
const auto pApplication = m_pApplication.release();
if (m_pApplication != NULL)
{
pApplication = m_pApplication;
if (m_pConfiguration->QueryHostingModel() == HOSTING_OUT_PROCESS)
{
//
// For inprocess, need to set m_pApplication to NULL first to
// avoid mapping new request to the recycled application.
// Outofprocess application instance will be created for new request
// For inprocess, as recycle will lead to shutdown later, leave m_pApplication
// to not block incoming requests till worker process shutdown
//
m_pApplication = NULL;
}
else
{
//
// For inprocess, need hold the application till shutdown is called
// Bump the reference counter as DoRecycleApplication will do dereference
//
pApplication->ReferenceApplication();
}
hThread = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE)DoRecycleApplication,
pApplication, // thread function arguments
0, // default creation flags
NULL); // receive thread identifier
}
else
{
if (m_pConfiguration->QueryHostingModel() == HOSTING_IN_PROCESS)
{
// In process application failed to start for whatever reason, need to recycle the work process
m_pServer.RecycleProcess(L"AspNetCore InProcess Recycle Process on Demand");
}
}
if (hThread == NULL)
{
if (!g_fRecycleProcessCalled)
{
g_fRecycleProcessCalled = TRUE;
m_pServer.RecycleProcess(L"On Demand by AspNetCore Module for recycle application failure");
}
}
else
{
// Closing a thread handle does not terminate the associated thread or remove the thread object.
CloseHandle(hThread);
}
HandleWrapper<InvalidHandleTraits> hThread = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE)DoRecycleApplication,
pApplication, // thread function arguments
0, // default creation flags
NULL); // receive thread identifier
}
}
VOID
DWORD WINAPI
APPLICATION_INFO::DoRecycleApplication(
LPVOID lpParam)
{
IAPPLICATION* pApplication = static_cast<IAPPLICATION*>(lpParam);
auto pApplication = std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER>(static_cast<IAPPLICATION*>(lpParam));
// No lock required
if (pApplication != NULL)
if (pApplication)
{
// Recycle will call shutdown for out of process
pApplication->Recycle();
// Decrement the ref count as we reference it in RecycleApplication.
pApplication->DereferenceApplication();
pApplication->Stop(/*fServerInitiated*/ false);
}
return 0;
}
VOID
APPLICATION_INFO::ShutDownApplication()
{
IAPPLICATION* pApplication = NULL;
SRWExclusiveLock lock(m_applicationLock);
// pApplication can be NULL due to app_offline
if (m_pApplication != NULL)
if (m_pApplication)
{
SRWExclusiveLock lock(m_srwLock);
if (m_pApplication != NULL)
{
pApplication = m_pApplication;
// Set m_pApplication to NULL first to prevent anyone from using it
m_pApplication = NULL;
pApplication->ShutDown();
pApplication->DereferenceApplication();
}
m_pApplication ->Stop(/* fServerInitiated */ true);
m_pApplication = nullptr;
}
}

View File

@ -32,11 +32,10 @@ public:
m_pServer(pServer),
m_cRefs(1),
m_fValid(FALSE),
m_fAppCreationAttempted(FALSE),
m_pConfiguration(NULL),
m_pConfiguration(nullptr),
m_pfnAspNetCoreCreateApplication(NULL)
{
InitializeSRWLock(&m_srwLock);
InitializeSRWLock(&m_applicationLock);
}
PCWSTR
@ -90,19 +89,7 @@ public:
ASPNETCORE_SHIM_CONFIG*
QueryConfig()
{
return m_pConfiguration;
}
//
// ExtractApplication will increase the reference counter of the application
// Caller is responsible for dereference the application.
// Otherwise memory leak
//
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER>
ExtractApplication() const
{
SRWSharedLock lock(m_srwLock);
return ReferenceApplication(m_pApplication);
return m_pConfiguration.get();
}
VOID
@ -112,8 +99,9 @@ public:
ShutDownApplication();
HRESULT
EnsureApplicationCreated(
IHttpContext *pHttpContext
GetOrCreateApplication(
IHttpContext *pHttpContext,
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER>& pApplication
);
private:
@ -121,17 +109,18 @@ private:
HRESULT FindNativeAssemblyFromGlobalLocation(PCWSTR libraryName, STRU* location);
HRESULT FindNativeAssemblyFromHostfxr(HOSTFXR_OPTIONS* hostfxrOptions, PCWSTR libraryName, STRU* location);
static VOID DoRecycleApplication(LPVOID lpParam);
static DWORD WINAPI DoRecycleApplication(LPVOID lpParam);
mutable LONG m_cRefs;
STRU m_struInfoKey;
BOOL m_fValid;
BOOL m_fAppCreationAttempted;
ASPNETCORE_SHIM_CONFIG *m_pConfiguration;
IAPPLICATION *m_pApplication;
SRWLOCK m_srwLock;
SRWLOCK m_applicationLock;
IHttpServer &m_pServer;
PFN_ASPNETCORE_CREATE_APPLICATION m_pfnAspNetCoreCreateApplication;
std::unique_ptr<ASPNETCORE_SHIM_CONFIG> m_pConfiguration;
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER> m_pApplication;
static const PCWSTR s_pwzAspnetcoreInProcessRequestHandlerName;
static const PCWSTR s_pwzAspnetcoreOutOfProcessRequestHandlerName;

View File

@ -97,7 +97,6 @@ APPLICATION_MANAGER::GetOrCreateApplicationInfo(
*ppApplicationInfo = pApplicationInfo;
pApplicationInfo = NULL;
}
Finished:
// log the error

View File

@ -95,20 +95,18 @@ ASPNET_CORE_PROXY_MODULE::OnExecuteRequestHandler(
// the error should already been logged to window event log for the first request
FINISHED(E_APPLICATION_ACTIVATION_EXEC_FAILURE);
}
// make sure assmebly is loaded and application is created
FINISHED_IF_FAILED(m_pApplicationInfo->EnsureApplicationCreated(pHttpContext));
auto pApplication = m_pApplicationInfo->ExtractApplication();
DBG_ASSERT(pHttpContext);
// We allow OFFLINE application to serve pages
DBG_ASSERT(pHttpContext);
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER> pApplication;
FINISHED_IF_FAILED(m_pApplicationInfo->GetOrCreateApplication(pHttpContext, pApplication));
// We allow RECYCLED application to serve pages
if (pApplication->QueryStatus() != APPLICATION_STATUS::RUNNING &&
pApplication->QueryStatus() != APPLICATION_STATUS::STARTING)
pApplication->QueryStatus() != APPLICATION_STATUS::STARTING &&
pApplication->QueryStatus() != APPLICATION_STATUS::RECYCLED)
{
hr = HRESULT_FROM_WIN32(ERROR_SERVER_DISABLED);
goto Finished;
FINISHED(HRESULT_FROM_WIN32(ERROR_SERVER_DISABLED));
}
IREQUEST_HANDLER* pHandler;
@ -124,7 +122,7 @@ ASPNET_CORE_PROXY_MODULE::OnExecuteRequestHandler(
}
Finished:
if (FAILED(hr))
if (LOG_IF_FAILED(hr))
{
retVal = RQ_NOTIFICATION_FINISH_REQUEST;
if (hr == HRESULT_FROM_WIN32(ERROR_SERVER_SHUTDOWN_IN_PROGRESS))

View File

@ -0,0 +1,61 @@
// 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.
#include "AppOfflineApplication.h"
#include <experimental/filesystem>
#include "HandleWrapper.h"
#include "AppOfflineHandler.h"
HRESULT AppOfflineApplication::CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler)
{
try
{
*pRequestHandler = new AppOfflineHandler(pHttpContext, m_strAppOfflineContent);
}
CATCH_RETURN();
return S_OK;
}
HRESULT AppOfflineApplication::OnAppOfflineFound()
{
LARGE_INTEGER li = {};
HandleWrapper<InvalidHandleTraits> handle = CreateFile(m_appOfflineLocation.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
RETURN_LAST_ERROR_IF(handle == INVALID_HANDLE_VALUE);
RETURN_LAST_ERROR_IF(!GetFileSizeEx(handle, &li));
if (li.HighPart != 0)
{
// > 4gb file size not supported
// todo: log a warning at event log
return E_INVALIDARG;
}
if (li.LowPart > 0)
{
DWORD bytesRead = 0;
std::string pszBuff(li.LowPart + 1, '\0');
RETURN_LAST_ERROR_IF(!ReadFile(handle, pszBuff.data(), li.LowPart, &bytesRead, NULL));
pszBuff.resize(bytesRead);
m_strAppOfflineContent = pszBuff;
}
return S_OK;
}
bool AppOfflineApplication::ShouldBeStarted(IHttpApplication& pApplication)
{
return is_regular_file(GetAppOfflineLocation(pApplication));
}

View File

@ -0,0 +1,27 @@
// 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.
#pragma once
#include "application.h"
#include "requesthandler.h"
#include "PollingAppOfflineApplication.h"
class AppOfflineApplication: public PollingAppOfflineApplication
{
public:
AppOfflineApplication(IHttpApplication& pApplication)
: PollingAppOfflineApplication(pApplication, PollingAppOfflineApplicationMode::StopWhenRemoved)
{
}
HRESULT CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler) override;
HRESULT OnAppOfflineFound() override;
static bool ShouldBeStarted(IHttpApplication& pApplication);
private:
std::string m_strAppOfflineContent;
};

View File

@ -0,0 +1,30 @@
// 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.
#include "AppOfflineHandler.h"
#include "HandleWrapper.h"
REQUEST_NOTIFICATION_STATUS AppOfflineHandler::OnExecuteRequestHandler()
{
HTTP_DATA_CHUNK DataChunk;
IHttpResponse* pResponse = m_pContext->GetResponse();
DBG_ASSERT(pResponse);
// Ignore failure hresults as nothing we can do
// Set fTrySkipCustomErrors to true as we want client see the offline content
pResponse->SetStatus(503, "Service Unavailable", 0, S_OK, nullptr, TRUE);
pResponse->SetHeader("Content-Type",
"text/html",
static_cast<USHORT>(strlen("text/html")),
FALSE
);
DataChunk.DataChunkType = HttpDataChunkFromMemory;
DataChunk.FromMemory.pBuffer = m_strAppOfflineContent.data();
DataChunk.FromMemory.BufferLength = static_cast<ULONG>(m_strAppOfflineContent.size());
pResponse->WriteEntityChunkByReference(&DataChunk);
return REQUEST_NOTIFICATION_STATUS::RQ_NOTIFICATION_FINISH_REQUEST;
}

View File

@ -0,0 +1,23 @@
// 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.
#pragma once
#include "application.h"
#include "requesthandler.h"
class AppOfflineHandler: public REQUEST_HANDLER
{
public:
AppOfflineHandler(IHttpContext* pContext, const std::string appOfflineContent)
: m_pContext(pContext),
m_strAppOfflineContent(appOfflineContent)
{
}
REQUEST_NOTIFICATION_STATUS OnExecuteRequestHandler() override;
private:
IHttpContext* m_pContext;
std::string m_strAppOfflineContent;
};

View File

@ -193,6 +193,8 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="application.h" />
<ClInclude Include="AppOfflineApplication.h" />
<ClInclude Include="AppOfflineHandler.h" />
<ClInclude Include="config_utility.h" />
<ClInclude Include="Environment.h" />
<ClInclude Include="EventLog.h" />
@ -216,6 +218,8 @@
<ClInclude Include="utility.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="AppOfflineApplication.cpp" />
<ClCompile Include="AppOfflineHandler.cpp" />
<ClCompile Include="debugutil.cpp" />
<ClCompile Include="Environment.cpp" />
<ClCompile Include="fx_ver.cpp" />

View File

@ -7,25 +7,9 @@
#include "SRWExclusiveLock.h"
#include "HandleWrapper.h"
HRESULT PollingAppOfflineApplication::CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler)
{
try
{
*pRequestHandler = new PollingAppOfflineHandler(pHttpContext, m_strAppOfflineContent);
}
CATCH_RETURN();
return S_OK;
}
APPLICATION_STATUS PollingAppOfflineApplication::QueryStatus()
{
if (AppOfflineExists())
{
return APPLICATION_STATUS::RUNNING;
}
return APPLICATION_STATUS::RECYCLED;
return (AppOfflineExists() == (m_mode == StopWhenRemoved)) ? APPLICATION_STATUS::RUNNING : APPLICATION_STATUS::RECYCLED;
}
bool
@ -35,7 +19,7 @@ PollingAppOfflineApplication::AppOfflineExists()
//
// we only care about app offline presented. If not, it means the application has started
// and is monitoring the app offline file
// we cache the file exist check result for 1 second
// we cache the file exist check result for 200 ms
//
if (ulCurrentTime - m_ulLastCheckTime > c_appOfflineRefreshIntervalMS)
{
@ -45,7 +29,7 @@ PollingAppOfflineApplication::AppOfflineExists()
m_fAppOfflineFound = is_regular_file(m_appOfflineLocation);
if(m_fAppOfflineFound)
{
LOG_IF_FAILED(LoadAppOfflineContent());
LOG_IF_FAILED(OnAppOfflineFound());
}
m_ulLastCheckTime = ulCurrentTime;
}
@ -53,81 +37,8 @@ PollingAppOfflineApplication::AppOfflineExists()
return m_fAppOfflineFound;
}
HRESULT PollingAppOfflineApplication::LoadAppOfflineContent()
{
LARGE_INTEGER li = {};
HandleWrapper<InvalidHandleTraits> handle = CreateFile(m_appOfflineLocation.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
RETURN_LAST_ERROR_IF(handle == INVALID_HANDLE_VALUE);
RETURN_LAST_ERROR_IF (!GetFileSizeEx(handle, &li));
if (li.HighPart != 0)
{
// > 4gb file size not supported
// todo: log a warning at event log
return TRUE;
}
if (li.LowPart > 0)
{
DWORD bytesRead = 0;
std::string pszBuff(li.LowPart + 1, '\0');
RETURN_LAST_ERROR_IF(!ReadFile(handle, pszBuff.data(), li.LowPart, &bytesRead, NULL));
pszBuff.resize(bytesRead);
m_strAppOfflineContent = pszBuff;
}
return S_OK;
}
bool PollingAppOfflineApplication::ShouldBeStarted(IHttpApplication& pApplication)
{
return is_regular_file(GetAppOfflineLocation(pApplication));
}
std::experimental::filesystem::path PollingAppOfflineApplication::GetAppOfflineLocation(IHttpApplication& pApplication)
{
return std::experimental::filesystem::path(pApplication.GetApplicationPhysicalPath()) / "app_offline.htm";
}
void PollingAppOfflineApplication::ShutDown()
{
}
void PollingAppOfflineApplication::Recycle()
{
}
REQUEST_NOTIFICATION_STATUS PollingAppOfflineHandler::OnExecuteRequestHandler()
{
HTTP_DATA_CHUNK DataChunk;
IHttpResponse* pResponse = m_pContext->GetResponse();
DBG_ASSERT(pResponse);
// Ignore failure hresults as nothing we can do
// Set fTrySkipCustomErrors to true as we want client see the offline content
pResponse->SetStatus(503, "Service Unavailable", 0, S_OK, nullptr, TRUE);
pResponse->SetHeader("Content-Type",
"text/html",
static_cast<USHORT>(strlen("text/html")),
FALSE
);
DataChunk.DataChunkType = HttpDataChunkFromMemory;
DataChunk.FromMemory.pBuffer = m_strAppOfflineContent.data();
DataChunk.FromMemory.BufferLength = static_cast<ULONG>(m_strAppOfflineContent.size());
pResponse->WriteEntityChunkByReference(&DataChunk);
return REQUEST_NOTIFICATION_STATUS::RQ_NOTIFICATION_FINISH_REQUEST;
}

View File

@ -4,54 +4,40 @@
#pragma once
#include <filesystem>
#include "application.h"
#include "requesthandler.h"
enum PollingAppOfflineApplicationMode
{
StopWhenAdded,
StopWhenRemoved
};
class PollingAppOfflineApplication: public APPLICATION
{
public:
PollingAppOfflineApplication(IHttpApplication& pApplication)
PollingAppOfflineApplication(IHttpApplication& pApplication, PollingAppOfflineApplicationMode mode)
:
m_ulLastCheckTime(0),
m_appOfflineLocation(GetAppOfflineLocation(pApplication)),
m_fAppOfflineFound(false)
m_fAppOfflineFound(false),
m_mode(mode)
{
InitializeSRWLock(&m_statusLock);
}
HRESULT CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler) override;
APPLICATION_STATUS QueryStatus() override;
bool AppOfflineExists();
HRESULT LoadAppOfflineContent();
static bool ShouldBeStarted(IHttpApplication& pApplication);
void ShutDown() override;
void Recycle() override;
virtual HRESULT OnAppOfflineFound() = 0;
void Stop(bool fServerInitiated) override { UNREFERENCED_PARAMETER(fServerInitiated); }
protected:
std::experimental::filesystem::path m_appOfflineLocation;
static std::experimental::filesystem::path GetAppOfflineLocation(IHttpApplication& pApplication);
private:
static const int c_appOfflineRefreshIntervalMS = 200;
static std::experimental::filesystem::path GetAppOfflineLocation(IHttpApplication& pApplication);
std::string m_strAppOfflineContent;
ULONGLONG m_ulLastCheckTime;
std::experimental::filesystem::path m_appOfflineLocation;
ULONGLONG m_ulLastCheckTime;
bool m_fAppOfflineFound;
SRWLOCK m_statusLock {};
PollingAppOfflineApplicationMode m_mode;
};
class PollingAppOfflineHandler: public REQUEST_HANDLER
{
public:
PollingAppOfflineHandler(IHttpContext* pContext, const std::string appOfflineContent)
: m_pContext(pContext),
m_strAppOfflineContent(appOfflineContent)
{
}
REQUEST_NOTIFICATION_STATUS OnExecuteRequestHandler() override;
private:
IHttpContext* m_pContext;
std::string m_strAppOfflineContent;
};

View File

@ -31,13 +31,15 @@ public:
VOID
ReferenceApplication() override
{
DBG_ASSERT(m_cRefs > 0);
InterlockedIncrement(&m_cRefs);
}
VOID
DereferenceApplication() override
{
DBG_ASSERT(m_cRefs != 0);
DBG_ASSERT(m_cRefs > 0);
if (InterlockedDecrement(&m_cRefs) == 0)
{

View File

@ -25,14 +25,9 @@ struct APPLICATION_PARAMETER
class IAPPLICATION
{
public:
virtual
VOID
ShutDown() = 0;
virtual
VOID
Recycle() = 0;
Stop(bool fServerInitiated) = 0;
virtual
~IAPPLICATION() = 0 { };

View File

@ -18,9 +18,7 @@ InProcessApplicationBase::InProcessApplicationBase(
}
VOID
InProcessApplicationBase::Recycle(
VOID
)
InProcessApplicationBase::Stop(bool fServerInitiated)
{
// We need to guarantee that recycle is only called once, as calling pHttpServer->RecycleProcess
// multiple times can lead to AVs.
@ -40,6 +38,12 @@ InProcessApplicationBase::Recycle(
m_fRecycleCalled = true;
}
// Stop was initiated by server no need to do anything, server would stop on it's own
if (fServerInitiated)
{
return;
}
if (!m_pHttpServer.IsCommandLineLaunch())
{
// IIS scenario.
@ -50,13 +54,7 @@ InProcessApplicationBase::Recycle(
}
else
{
// If we set a static callback, we don't want to kill the current process as
// that will kill the test process and means we are running in hostable webcore mode.
if (m_pHttpServer.IsCommandLineLaunch()
&& s_fMainCallback == NULL)
{
exit(0);
}
exit(0);
}
}

View File

@ -18,7 +18,7 @@ public:
~InProcessApplicationBase() = default;
VOID Recycle(VOID) override;
VOID Stop(bool fServerInitiated) override;
protected:
BOOL m_fRecycleCalled;

View File

@ -226,6 +226,7 @@
<ClInclude Include="inprocessapplication.h" />
<ClInclude Include="InProcessApplicationBase.h" />
<ClInclude Include="inprocesshandler.h" />
<ClInclude Include="ShuttingDownApplication.h" />
<ClInclude Include="stdafx.h" />
<ClInclude Include="StartupExceptionApplication.h" />
<ClInclude Include="StartupExceptionHandler.h" />

View File

@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
#pragma once
#include "InProcessApplicationBase.h"
class ShuttingDownHandler : public REQUEST_HANDLER
{
public:
ShuttingDownHandler(IHttpContext* pContext) : m_pContext(pContext)
{
}
REQUEST_NOTIFICATION_STATUS OnExecuteRequestHandler() override
{
return ServerShutdownMessage(m_pContext);
}
static REQUEST_NOTIFICATION_STATUS ServerShutdownMessage(IHttpContext * pContext)
{
pContext->GetResponse()->SetStatus(503, "Server has been shutdown", 0, HRESULT_FROM_WIN32(ERROR_SHUTDOWN_IN_PROGRESS));
return RQ_NOTIFICATION_FINISH_REQUEST;
}
private:
IHttpContext * m_pContext;
};
class ShuttingDownApplication : public InProcessApplicationBase
{
public:
ShuttingDownApplication(IHttpServer& pHttpServer, IHttpApplication& pHttpApplication)
: InProcessApplicationBase(pHttpServer, pHttpApplication)
{
m_status = APPLICATION_STATUS::RUNNING;
}
~ShuttingDownApplication() = default;
HRESULT CreateHandler(IHttpContext * pHttpContext, IREQUEST_HANDLER ** pRequestHandler) override
{
*pRequestHandler = new ShuttingDownHandler(pHttpContext);
return S_OK;
}
};

View File

@ -3,10 +3,6 @@
#include "StartupExceptionApplication.h"
VOID StartupExceptionApplication::ShutDown()
{
}
HRESULT StartupExceptionApplication::CreateHandler(IHttpContext *pHttpContext, IREQUEST_HANDLER ** pRequestHandler)
{
*pRequestHandler = new StartupExceptionHandler(pHttpContext, m_disableLogs, this);

View File

@ -44,7 +44,6 @@ public:
~StartupExceptionApplication() = default;
VOID ShutDown() override;
HRESULT CreateHandler(IHttpContext * pHttpContext, IREQUEST_HANDLER ** pRequestHandler) override;
std::string&

View File

@ -12,6 +12,7 @@
#include "debugutil.h"
#include "resources.h"
#include "exceptions.h"
#include "ShuttingDownApplication.h"
DECLARE_DEBUG_PRINT_OBJECT("aspnetcorev2_inprocess.dll");
@ -22,6 +23,7 @@ IHttpServer * g_pHttpServer = NULL;
HINSTANCE g_hWinHttpModule;
HINSTANCE g_hAspNetCoreModule;
HANDLE g_hEventLog = NULL;
bool g_fInProcessApplicationCreated = false;
HRESULT
InitializeGlobalConfiguration(
@ -93,7 +95,15 @@ CreateApplication(
try
{
RETURN_IF_FAILED(InitializeGlobalConfiguration(pServer, pHttpApplication));
// In process application was already created so another call to CreateApplication
// means that server is shutting does and request arrived in the meantime
if (g_fInProcessApplicationCreated)
{
*ppApplication = new ShuttingDownApplication(*pServer, *pHttpApplication);
return S_OK;
}
REQUESTHANDLER_CONFIG *pConfig = nullptr;
RETURN_IF_FAILED(REQUESTHANDLER_CONFIG::CreateRequestHandlerConfig(pServer, pHttpApplication, &pConfig));
std::unique_ptr<REQUESTHANDLER_CONFIG> pRequestHandlerConfig(pConfig);
@ -102,6 +112,8 @@ CreateApplication(
auto pApplication = std::make_unique<IN_PROCESS_APPLICATION>(*pServer, *pHttpApplication, std::move(pRequestHandlerConfig), pParameters, nParameters);
// never create two inprocess applications in one process
g_fInProcessApplicationCreated = true;
if (FAILED_LOG(pApplication->LoadManagedApplication()))
{
// Set the currently running application to a fake application that returns startup exceptions.

View File

@ -50,7 +50,7 @@ IN_PROCESS_APPLICATION::~IN_PROCESS_APPLICATION()
}
//static
DWORD
DWORD WINAPI
IN_PROCESS_APPLICATION::DoShutDown(
LPVOID lpParam
)
@ -63,10 +63,9 @@ IN_PROCESS_APPLICATION::DoShutDown(
__override
VOID
IN_PROCESS_APPLICATION::ShutDown(
VOID
)
IN_PROCESS_APPLICATION::Stop(bool fServerInitiated)
{
UNREFERENCED_PARAMETER(fServerInitiated);
HRESULT hr = S_OK;
CHandle hThread;
DWORD dwThreadStatus = 0;
@ -118,15 +117,8 @@ Finished:
ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE,
ASPNETCORE_EVENT_APP_SHUTDOWN_FAILURE_MSG,
m_pConfig->QueryConfigPath()->QueryStr());
//
// Managed layer may block the shutdown and lead to shutdown timeout
// Assumption: only one inprocess application is hosted.
// Call process exit to force shutdown
//
exit(hr);
}
else
else
{
UTILITY::LogEventF(g_hEventLog,
EVENTLOG_INFORMATION_TYPE,
@ -134,6 +126,8 @@ Finished:
ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL_MSG,
m_pConfig->QueryConfigPath()->QueryStr());
}
InProcessApplicationBase::Stop(fServerInitiated);
}
VOID
@ -169,7 +163,7 @@ IN_PROCESS_APPLICATION::ShutDownInternal()
// managed. We still need to wait on main exiting no matter what. m_fShutdownCalledFromNative
// is used for detecting redundant calls and blocking more requests to OnExecuteRequestHandler.
m_fShutdownCalledFromNative = TRUE;
m_status = APPLICATION_STATUS::SHUTDOWN;
m_status = APPLICATION_STATUS::RECYCLED;
if (!m_fShutdownCalledFromManaged)
{
@ -515,7 +509,7 @@ Finished:
//
// If the inprocess server was initialized, we need to cause recycle to be called on the worker process.
//
Recycle();
Stop(/*fServerInitiated*/ false);
}
}

View File

@ -26,7 +26,7 @@ public:
__override
VOID
ShutDown();
Stop(bool fServerInitiated) override;
VOID
SetCallbackHandles(
@ -167,7 +167,7 @@ private:
);
static
DWORD
DWORD WINAPI
DoShutDown(
LPVOID lpParam
);

View File

@ -5,6 +5,7 @@
#include "inprocessapplication.h"
#include "aspnetcore_event.h"
#include "IOutputManager.h"
#include "ShuttingDownApplication.h"
ALLOC_CACHE_HANDLER * IN_PROCESS_HANDLER::sm_pAlloc = NULL;
@ -93,9 +94,7 @@ IN_PROCESS_HANDLER::OnAsyncCompletion(
REQUEST_NOTIFICATION_STATUS IN_PROCESS_HANDLER::ServerShutdownMessage() const
{
m_pW3Context->GetResponse()->SetStatus(503, "Server has been shutdown", 0,
(ULONG)HRESULT_FROM_WIN32(ERROR_SHUTDOWN_IN_PROGRESS));
return RQ_NOTIFICATION_FINISH_REQUEST;
return ShuttingDownHandler::ServerShutdownMessage(m_pW3Context);
}
VOID

View File

@ -4,6 +4,9 @@
#include "inprocessapplication.h"
#include "inprocesshandler.h"
#include "requesthandler_config.h"
extern bool g_fInProcessApplicationCreated;
//
// Initialization export
//
@ -469,6 +472,8 @@ EXTERN_C __MIDL_DECLSPEC_DLLEXPORT
VOID
set_main_handler(_In_ hostfxr_main_fn main)
{
// Allow inprocess application to be recreated as we reuse the same CLR
g_fInProcessApplicationCreated = false;
IN_PROCESS_APPLICATION::SetMainCallback(main);
}

View File

@ -63,8 +63,10 @@ OUT_OF_PROCESS_APPLICATION::GetProcess(
__override
VOID
OUT_OF_PROCESS_APPLICATION::ShutDown()
OUT_OF_PROCESS_APPLICATION::Stop(bool fServerInitiated)
{
UNREFERENCED_PARAMETER(fServerInitiated);
SRWExclusiveLock lock(m_srwLock);
if (m_pProcessManager != NULL)
{
@ -72,13 +74,6 @@ OUT_OF_PROCESS_APPLICATION::ShutDown()
}
}
__override
VOID
OUT_OF_PROCESS_APPLICATION::Recycle()
{
ShutDown();
}
HRESULT
OUT_OF_PROCESS_APPLICATION::CreateHandler(
_In_ IHttpContext *pHttpContext,

View File

@ -32,12 +32,7 @@ public:
__override
VOID
ShutDown()
override;
__override
VOID
Recycle()
Stop(bool fServerInitiated)
override;
__override

View File

@ -51,6 +51,7 @@ void AppOfflineTrackingApplication::OnAppOffline()
{
LOG_INFOF("Received app_offline notification in application %S", m_applicationPath.c_str());
m_fileWatcherEntry->StopMonitor();
m_fileWatcherEntry.reset(nullptr);
m_status = APPLICATION_STATUS::RECYCLED;
UTILITY::LogEventF(g_hEventLog,
EVENTLOG_INFORMATION_TYPE,
@ -58,5 +59,5 @@ void AppOfflineTrackingApplication::OnAppOffline()
ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG,
m_applicationPath.c_str());
Recycle();
Stop(/*fServerInitiated*/ false);
}

View File

@ -17,7 +17,13 @@ public:
{
}
~AppOfflineTrackingApplication() override = default;
~AppOfflineTrackingApplication() override
{
if (m_fileWatcherEntry)
{
m_fileWatcherEntry->StopMonitor();
}
};
HRESULT
StartMonitoringAppOffline();

View File

@ -227,7 +227,7 @@ FILE_WATCHER_ENTRY::FILE_WATCHER_ENTRY(FILE_WATCHER * pFileMonitor) :
FILE_WATCHER_ENTRY::~FILE_WATCHER_ENTRY()
{
StopMonitor();
DBG_ASSERT(_cRefs == 0);
_dwSignature = FILE_WATCHER_ENTRY_SIGNATURE_FREE;
@ -354,7 +354,6 @@ FILE_WATCHER_ENTRY::Monitor(VOID)
{
HRESULT hr = S_OK;
DWORD cbRead;
AcquireSRWLockExclusive(&_srwLock);
ReferenceFileWatcherEntry();
ZeroMemory(&_overlapped, sizeof(_overlapped));
@ -372,6 +371,12 @@ FILE_WATCHER_ENTRY::Monitor(VOID)
DereferenceFileWatcherEntry();
}
// Check if file exist because ReadDirectoryChangesW would not fire events for existing files
if (GetFileAttributes(_strFullName.QueryStr()) != INVALID_FILE_ATTRIBUTES)
{
PostQueuedCompletionStatus(_pFileMonitor->QueryCompletionPort(), 0, 0, &_overlapped);
}
ReleaseSRWLockExclusive(&_srwLock);
return hr;
}
@ -385,7 +390,7 @@ FILE_WATCHER_ENTRY::StopMonitor(VOID)
// can be ignored
//
InterlockedExchange(&_lStopMonitorCalled, 1);
MarkEntryInValid();
if (_hDirectory != INVALID_HANDLE_VALUE)
{
AcquireSRWLockExclusive(&_srwLock);
@ -431,6 +436,12 @@ FILE_WATCHER_ENTRY::Create(
goto Finished;
}
if (FAILED(hr = _strFullName.Append(_strDirectoryName)) ||
FAILED(hr = _strFullName.Append(_strFileName)))
{
goto Finished;
}
//
// Resize change buffer to something "reasonable"
//

View File

@ -111,6 +111,7 @@ private:
FILE_WATCHER* _pFileMonitor;
STRU _strFileName;
STRU _strDirectoryName;
STRU _strFullName;
LONG _lStopMonitorCalled;
mutable LONG _cRefs;
BOOL _fIsValid;

View File

@ -0,0 +1,278 @@
// 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.IO;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Logging;
using Xunit;
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.Inprocess
{
public class AppOfflineTests : IISFunctionalTestBase
{
[ConditionalTheory]
[InlineData(HostingModel.InProcess)]
[InlineData(HostingModel.OutOfProcess)]
public async Task AppOfflineDroppedWhileSiteIsDown_SiteReturns503(HostingModel hostingModel)
{
var deploymentResult = await DeployApp(hostingModel);
AddAppOffline(deploymentResult.ContentRoot);
await AssertAppOffline(deploymentResult);
DeletePublishOutput(deploymentResult);
}
[ConditionalTheory]
[InlineData(HostingModel.InProcess, 500, "500.0")]
[InlineData(HostingModel.OutOfProcess, 502, "502.5")]
public async Task AppOfflineDroppedWhileSiteFailedToStartInShim_AppOfflineServed(HostingModel hostingModel, int statusCode, string content)
{
var deploymentParameters = Helpers.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true);
deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", "nonexistent"));
var deploymentResult = await DeployAsync(deploymentParameters);
var result = await deploymentResult.HttpClient.GetAsync("/");
Assert.Equal(statusCode, (int)result.StatusCode);
Assert.Contains(content, await result.Content.ReadAsStringAsync());
AddAppOffline(deploymentResult.ContentRoot);
await AssertAppOffline(deploymentResult);
DeletePublishOutput(deploymentResult);
}
[ConditionalFact]
[RequiresIIS(IISCapability.ShutdownToken)]
public async Task AppOfflineDroppedWhileSiteFailedToStartInRequestHandler_SiteStops_InProcess()
{
var deploymentResult = await DeployApp(HostingModel.InProcess);
// Set file content to empty so it fails at runtime
File.WriteAllText(Path.Combine(deploymentResult.ContentRoot, "Microsoft.AspNetCore.Server.IIS.dll"), "");
var result = await deploymentResult.HttpClient.GetAsync("/");
Assert.Equal(500, (int)result.StatusCode);
Assert.Contains("500.30", await result.Content.ReadAsStringAsync());
AddAppOffline(deploymentResult.ContentRoot);
AssertStopsProcess(deploymentResult);
}
[ConditionalFact]
[RequiresIIS(IISCapability.ShutdownToken)]
public async Task AppOfflineDroppedWhileSiteStarting_SiteShutsDown_InProcess()
{
var deploymentResult = await DeployApp(HostingModel.InProcess);
for (int i = 0; i < 10; i++)
{
// send first request and add app_offline while app is starting
var runningTask = AssertAppOffline(deploymentResult);
// This test tries to hit a race where we drop app_offline file while
// in process application is starting, application start takes at least 400ms
// so we back off for 100ms to allow request to reach request handler
// Test itself is racy and can result in two scenarios
// 1. ANCM detects app_offline before it starts the request - if AssertAppOffline succeeds we've hit it
// 2. Intended scenario where app starts and then shuts down
// In first case we remove app_offline and try again
await Task.Delay(100);
AddAppOffline(deploymentResult.ContentRoot);
try
{
await runningTask.TimeoutAfterDefault();
// if AssertAppOffline succeeded ANCM have picked up app_offline before starting the app
// try again
RemoveAppOffline(deploymentResult.ContentRoot);
}
catch
{
AssertStopsProcess(deploymentResult);
return;
}
}
Assert.True(false);
}
[ConditionalFact]
[RequiresIIS(IISCapability.ShutdownToken)]
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess()
{
var deploymentResult = await AssertStarts(HostingModel.InProcess);
AddAppOffline(deploymentResult.ContentRoot);
AssertStopsProcess(deploymentResult);
}
[ConditionalFact]
[RequiresIIS(IISCapability.ShutdownToken)]
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_OutOfProcess()
{
var deploymentResult = await AssertStarts(HostingModel.OutOfProcess);
// Repeat dropping file and restarting multiple times
for (int i = 0; i < 5; i++)
{
AddAppOffline(deploymentResult.ContentRoot);
await AssertAppOffline(deploymentResult);
RemoveAppOffline(deploymentResult.ContentRoot);
await AssertRunning(deploymentResult);
}
AddAppOffline(deploymentResult.ContentRoot);
await AssertAppOffline(deploymentResult);
DeletePublishOutput(deploymentResult);
}
[ConditionalTheory]
[InlineData(HostingModel.InProcess)]
[InlineData(HostingModel.OutOfProcess)]
public async Task AppOfflineDropped_CanRemoveAppOfflineAfterAddingAndSiteWorks(HostingModel hostingModel)
{
var deploymentResult = await DeployApp(hostingModel);
AddAppOffline(deploymentResult.ContentRoot);
await AssertAppOffline(deploymentResult);
RemoveAppOffline(deploymentResult.ContentRoot);
await AssertRunning(deploymentResult);
}
[ConditionalTheory]
[InlineData(HostingModel.InProcess)]
[InlineData(HostingModel.OutOfProcess)]
public async Task AppOfflineAddedAndRemovedStress(HostingModel hostingModel)
{
var deploymentResult = await AssertStarts(hostingModel);
var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => {
var statusCode = (int)response.StatusCode;
Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode);
});
for (int i = 0; i < 100; i++)
{
// AddAppOffline might fail if app_offline is being read by ANCM and deleted at the same time
RetryHelper.RetryOperation(
() => AddAppOffline(deploymentResult.ContentRoot),
e => Logger.LogError($"Failed to create app_offline : {e.Message}"),
retryCount: 3,
retryDelayMilliseconds: 100);
RemoveAppOffline(deploymentResult.ContentRoot);
}
try
{
await load;
}
catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException)
{
// IOException in InProcess is fine, just means process stopped
if (hostingModel != HostingModel.InProcess)
{
throw;
}
}
}
private async Task<IISDeploymentResult> DeployApp(HostingModel hostingModel = HostingModel.InProcess)
{
var deploymentParameters = Helpers.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true);
return await DeployAsync(deploymentParameters);
}
private void AddAppOffline(string appPath, string content = "The app is offline.")
{
File.WriteAllText(Path.Combine(appPath, "app_offline.htm"), content);
}
private void RemoveAppOffline(string appPath)
{
RetryHelper.RetryOperation(
() => File.Delete(Path.Combine(appPath, "app_offline.htm")),
e => Logger.LogError($"Failed to remove app_offline : {e.Message}"),
retryCount: 3,
retryDelayMilliseconds: 100);
}
private async Task AssertAppOffline(IISDeploymentResult deploymentResult, string expectedResponse = "The app is offline.")
{
HttpResponseMessage response = null;
for (var i = 0; i < 5; i++)
{
// Keep retrying until app_offline is present.
response = await deploymentResult.HttpClient.GetAsync("HelloWorld");
if (!response.IsSuccessStatusCode)
{
break;
}
}
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.Equal(expectedResponse, await response.Content.ReadAsStringAsync());
}
private void AssertStopsProcess(IISDeploymentResult deploymentResult)
{
var hostShutdownToken = deploymentResult.HostShutdownToken;
Assert.True(hostShutdownToken.WaitHandle.WaitOne(TimeoutExtensions.DefaultTimeout));
Assert.True(hostShutdownToken.IsCancellationRequested);
}
private async Task<IISDeploymentResult> AssertStarts(HostingModel hostingModel)
{
var deploymentResult = await DeployApp(hostingModel);
await AssertRunning(deploymentResult);
return deploymentResult;
}
private static async Task AssertRunning(IISDeploymentResult deploymentResult)
{
var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld");
var responseText = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello World", responseText);
}
private void DeletePublishOutput(IISDeploymentResult deploymentResult)
{
foreach (var file in Directory.GetFiles(deploymentResult.ContentRoot, "*", SearchOption.AllDirectories))
{
// Out of process module dll is allowed to be locked
var name = Path.GetFileName(file);
if (name == "aspnetcore.dll" || name == "aspnetcorev2.dll" || name == "aspnetcorev2_outofprocess.dll")
{
continue;
}
File.Delete(file);
}
}
}
}

View File

@ -43,25 +43,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
[ConditionalFact]
public async Task GetServerVariableDoesNotCrash()
{
async Task RunRequests()
{
var client = new HttpClient() { BaseAddress = _fixture.Client.BaseAddress };
for (int j = 0; j < 10; j++)
{
var response = await client.GetStringAsync("/GetServerVariableStress");
Assert.StartsWith("Response Begin", response);
Assert.EndsWith("Response End", response);
}
}
List<Task> tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
tasks.Add(Task.Run(RunRequests));
}
await Task.WhenAll(tasks);
await Helpers.StressLoad(_fixture.Client, "/GetServerVariableStress", response => {
var text = response.Content.ReadAsStringAsync().Result;
Assert.StartsWith("Response Begin", text);
Assert.EndsWith("Response End", text);
});
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.AspNetCore.Server.IntegrationTesting;
@ -51,5 +52,27 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
Assert.Equal("Hello World", responseText);
}
public static async Task StressLoad(HttpClient httpClient, string path, Action<HttpResponseMessage> action)
{
async Task RunRequests()
{
var connection = new HttpClient() { BaseAddress = httpClient.BaseAddress };
for (int j = 0; j < 10; j++)
{
var response = await connection.GetAsync(path);
action(response);
}
}
List<Task> tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
tasks.Add(Task.Run(RunRequests));
}
await Task.WhenAll(tasks);
}
}
}

View File

@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
None = 0,
Websockets = 1,
WindowsAuthentication = 2,
PoolEnvironmentVariables = 4
PoolEnvironmentVariables = 4,
ShutdownToken = 8
}
}

View File

@ -130,6 +130,12 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
_skipReason += "The machine does allow for setting environment variables on application pools.";
}
}
if (capabilities.HasFlag(IISCapability.ShutdownToken))
{
_isMet = false;
_skipReason += "https://github.com/aspnet/IISIntegration/issues/1074";
}
}
public bool IsMet => _isMet;

View File

@ -19,17 +19,17 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
var helloWorld = "Hello World";
var expectedPath = "/Path";
string path = null;
using (var testServer = await TestServer.Create(ctx =>
{
path = ctx.Request.Path.ToString();
return ctx.Response.WriteAsync(helloWorld);
}, LoggerFactory))
{
var result = await testServer.HttpClient.GetAsync(expectedPath);
Assert.Equal(helloWorld, await result.Content.ReadAsStringAsync());
Assert.Equal(expectedPath, path);
string path = null;
using (var testServer = await TestServer.Create(ctx =>
{
path = ctx.Request.Path.ToString();
return ctx.Response.WriteAsync(helloWorld);
}, LoggerFactory))
{
var result = await testServer.HttpClient.GetAsync(expectedPath);
Assert.Equal(helloWorld, await result.Content.ReadAsStringAsync());
Assert.Equal(expectedPath, path);
}
}
}
}
}

View File

@ -1,159 +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.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests;
using Microsoft.AspNetCore.Server.IntegrationTesting;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Logging;
using Xunit;
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.Inprocess
{
public class AppOfflineTests : IISFunctionalTestBase
{
// TODO these will differ between IIS and IISExpress
[ConditionalTheory]
[InlineData(HostingModel.InProcess)]
[InlineData(HostingModel.OutOfProcess)]
public async Task AppOfflineDroppedWhileSiteIsDown_SiteReturns503(HostingModel hostingModel)
{
var deploymentResult = await DeployApp(hostingModel);
AddAppOffline(deploymentResult.ContentRoot);
await AssertAppOffline(deploymentResult);
}
[ConditionalTheory]
[InlineData(HostingModel.InProcess)]
[InlineData(HostingModel.OutOfProcess)]
public async Task AppOfflineDroppedWhileSiteIsDown_CustomResponse(HostingModel hostingModel)
{
var expectedResponse = "The app is offline.";
var deploymentResult = await DeployApp(hostingModel);
AddAppOffline(deploymentResult.ContentRoot, expectedResponse);
await AssertAppOffline(deploymentResult, expectedResponse);
}
[ConditionalFact]
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess()
{
var deploymentResult = await AssertStarts(HostingModel.InProcess);
AddAppOffline(deploymentResult.ContentRoot);
await AssertStopsProcess(deploymentResult);
}
[ConditionalFact]
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_OutOfProcess()
{
var deploymentResult = await AssertStarts(HostingModel.OutOfProcess);
// Repeat dropping file and restarting multiple times
for (int i = 0; i < 5; i++)
{
AddAppOffline(deploymentResult.ContentRoot);
await AssertAppOffline(deploymentResult);
RemoveAppOffline(deploymentResult.ContentRoot);
await AssertRunning(deploymentResult);
}
}
[ConditionalTheory]
[InlineData(HostingModel.InProcess)]
[InlineData(HostingModel.OutOfProcess)]
public async Task AppOfflineDropped_CanRemoveAppOfflineAfterAddingAndSiteWorks(HostingModel hostingModel)
{
var deploymentResult = await DeployApp(hostingModel);
AddAppOffline(deploymentResult.ContentRoot);
await AssertAppOffline(deploymentResult);
RemoveAppOffline(deploymentResult.ContentRoot);
await AssertRunning(deploymentResult);
}
private async Task<IISDeploymentResult> DeployApp(HostingModel hostingModel = HostingModel.InProcess)
{
var deploymentParameters = Helpers.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true);
return await DeployAsync(deploymentParameters);
}
private void AddAppOffline(string appPath, string content = "")
{
File.WriteAllText(Path.Combine(appPath, "app_offline.htm"), content);
}
private void RemoveAppOffline(string appPath)
{
RetryHelper.RetryOperation(
() => File.Delete(Path.Combine(appPath, "app_offline.htm")),
e => Logger.LogError($"Failed to remove app_offline : {e.Message}"),
retryCount: 3,
retryDelayMilliseconds: 100);
}
private async Task AssertAppOffline(IISDeploymentResult deploymentResult, string expectedResponse = "")
{
var response = await deploymentResult.HttpClient.GetAsync("HelloWorld");
for (var i = 0; response.IsSuccessStatusCode && i < 5; i++)
{
// Keep retrying until app_offline is present.
response = await deploymentResult.HttpClient.GetAsync("HelloWorld");
}
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
Assert.Equal(expectedResponse, await response.Content.ReadAsStringAsync());
}
private async Task AssertStopsProcess(IISDeploymentResult deploymentResult)
{
try
{
var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld");
}
catch (HttpRequestException)
{
// dropping app_offline will kill the process
}
var hostShutdownToken = deploymentResult.HostShutdownToken;
Assert.True(hostShutdownToken.WaitHandle.WaitOne(TimeoutExtensions.DefaultTimeout));
Assert.True(hostShutdownToken.IsCancellationRequested);
}
private async Task<IISDeploymentResult> AssertStarts(HostingModel hostingModel)
{
var deploymentResult = await DeployApp(hostingModel);
await AssertRunning(deploymentResult);
return deploymentResult;
}
private static async Task AssertRunning(IISDeploymentResult deploymentResult)
{
var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld");
var responseText = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello World", responseText);
}
}
}