diff --git a/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj b/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj index c0d926bd35..4895850cbc 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj +++ b/src/AspNetCoreModuleV2/AspNetCore/AspNetCore.vcxproj @@ -230,6 +230,8 @@ + + diff --git a/src/AspNetCoreModuleV2/AspNetCore/ServerErrorApplication.h b/src/AspNetCoreModuleV2/AspNetCore/ServerErrorApplication.h new file mode 100644 index 0000000000..1872675656 --- /dev/null +++ b/src/AspNetCoreModuleV2/AspNetCore/ServerErrorApplication.h @@ -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; +}; + diff --git a/src/AspNetCoreModuleV2/AspNetCore/ServerErrorHandler.h b/src/AspNetCoreModuleV2/AspNetCore/ServerErrorHandler.h new file mode 100644 index 0000000000..b3d917039d --- /dev/null +++ b/src/AspNetCoreModuleV2/AspNetCore/ServerErrorHandler.h @@ -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; +}; diff --git a/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp b/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp index edfc085f22..a65af02467 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp +++ b/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp @@ -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& 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 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(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 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(lpParam); + auto pApplication = std::unique_ptr(static_cast(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; } } diff --git a/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.h b/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.h index 49badd578a..989120a538 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.h +++ b/src/AspNetCoreModuleV2/AspNetCore/applicationinfo.h @@ -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 - 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& 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 m_pConfiguration; + std::unique_ptr m_pApplication; + static const PCWSTR s_pwzAspnetcoreInProcessRequestHandlerName; static const PCWSTR s_pwzAspnetcoreOutOfProcessRequestHandlerName; diff --git a/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp b/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp index a7f08e5d3e..9de2901be5 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp +++ b/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp @@ -97,7 +97,6 @@ APPLICATION_MANAGER::GetOrCreateApplicationInfo( *ppApplicationInfo = pApplicationInfo; pApplicationInfo = NULL; } - Finished: // log the error diff --git a/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp b/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp index 962d7202d1..0ed100fd47 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp +++ b/src/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp @@ -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 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)) diff --git a/src/AspNetCoreModuleV2/CommonLib/AppOfflineApplication.cpp b/src/AspNetCoreModuleV2/CommonLib/AppOfflineApplication.cpp new file mode 100644 index 0000000000..e42c6ca8e6 --- /dev/null +++ b/src/AspNetCoreModuleV2/CommonLib/AppOfflineApplication.cpp @@ -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 +#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 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)); +} diff --git a/src/AspNetCoreModuleV2/CommonLib/AppOfflineApplication.h b/src/AspNetCoreModuleV2/CommonLib/AppOfflineApplication.h new file mode 100644 index 0000000000..f6ec00cab0 --- /dev/null +++ b/src/AspNetCoreModuleV2/CommonLib/AppOfflineApplication.h @@ -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; +}; + diff --git a/src/AspNetCoreModuleV2/CommonLib/AppOfflineHandler.cpp b/src/AspNetCoreModuleV2/CommonLib/AppOfflineHandler.cpp new file mode 100644 index 0000000000..3efeb74463 --- /dev/null +++ b/src/AspNetCoreModuleV2/CommonLib/AppOfflineHandler.cpp @@ -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(strlen("text/html")), + FALSE + ); + + DataChunk.DataChunkType = HttpDataChunkFromMemory; + DataChunk.FromMemory.pBuffer = m_strAppOfflineContent.data(); + DataChunk.FromMemory.BufferLength = static_cast(m_strAppOfflineContent.size()); + pResponse->WriteEntityChunkByReference(&DataChunk); + + return REQUEST_NOTIFICATION_STATUS::RQ_NOTIFICATION_FINISH_REQUEST; +} diff --git a/src/AspNetCoreModuleV2/CommonLib/AppOfflineHandler.h b/src/AspNetCoreModuleV2/CommonLib/AppOfflineHandler.h new file mode 100644 index 0000000000..e9fd4b99da --- /dev/null +++ b/src/AspNetCoreModuleV2/CommonLib/AppOfflineHandler.h @@ -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; +}; diff --git a/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj b/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj index 5ff3e0103f..e905714bfa 100644 --- a/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj +++ b/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj @@ -193,6 +193,8 @@ + + @@ -216,6 +218,8 @@ + + diff --git a/src/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.cpp b/src/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.cpp index dff673fee3..1468c7ee37 100644 --- a/src/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.cpp +++ b/src/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.cpp @@ -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 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(strlen("text/html")), - FALSE - ); - - DataChunk.DataChunkType = HttpDataChunkFromMemory; - DataChunk.FromMemory.pBuffer = m_strAppOfflineContent.data(); - DataChunk.FromMemory.BufferLength = static_cast(m_strAppOfflineContent.size()); - pResponse->WriteEntityChunkByReference(&DataChunk); - - return REQUEST_NOTIFICATION_STATUS::RQ_NOTIFICATION_FINISH_REQUEST; -} diff --git a/src/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.h b/src/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.h index 592cdb2dc8..48e71f6641 100644 --- a/src/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.h +++ b/src/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.h @@ -4,54 +4,40 @@ #pragma once #include #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; -}; - - diff --git a/src/AspNetCoreModuleV2/CommonLib/application.h b/src/AspNetCoreModuleV2/CommonLib/application.h index 56cf5ce2ac..9761851002 100644 --- a/src/AspNetCoreModuleV2/CommonLib/application.h +++ b/src/AspNetCoreModuleV2/CommonLib/application.h @@ -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) { diff --git a/src/AspNetCoreModuleV2/CommonLib/iapplication.h b/src/AspNetCoreModuleV2/CommonLib/iapplication.h index ef5d2bf8d7..cdfd17ec03 100644 --- a/src/AspNetCoreModuleV2/CommonLib/iapplication.h +++ b/src/AspNetCoreModuleV2/CommonLib/iapplication.h @@ -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 { }; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp index 5848f37a2f..d271a3a30a 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp @@ -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); } } diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h index ca907d67c6..c189095ffa 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h @@ -18,7 +18,7 @@ public: ~InProcessApplicationBase() = default; - VOID Recycle(VOID) override; + VOID Stop(bool fServerInitiated) override; protected: BOOL m_fRecycleCalled; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRequestHandler.vcxproj b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRequestHandler.vcxproj index 6531befa35..addfb56214 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRequestHandler.vcxproj +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessRequestHandler.vcxproj @@ -226,6 +226,7 @@ + diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/ShuttingDownApplication.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/ShuttingDownApplication.h new file mode 100644 index 0000000000..4528a3f2b4 --- /dev/null +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/ShuttingDownApplication.h @@ -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; + } +}; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.cpp index 4aa5b9fb31..fbb5350d62 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.cpp @@ -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); diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h index 0a2dc4fe58..14d26bf3a4 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h @@ -44,7 +44,6 @@ public: ~StartupExceptionApplication() = default; - VOID ShutDown() override; HRESULT CreateHandler(IHttpContext * pHttpContext, IREQUEST_HANDLER ** pRequestHandler) override; std::string& diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp index f7b985aa0b..e0ed004a67 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp @@ -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 pRequestHandlerConfig(pConfig); @@ -102,6 +112,8 @@ CreateApplication( auto pApplication = std::make_unique(*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. diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index b52b08f833..756d78954c 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -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); } } diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h index 1f6e3da570..e23cf6b83a 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h @@ -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 ); diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp index de806e7f71..90b3bd9d53 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp @@ -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 diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp index 1db194bdbd..6e03ebd859 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp @@ -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); } diff --git a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp index 90d75b2991..5f5877edac 100644 --- a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp +++ b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp @@ -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, diff --git a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.h b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.h index 8f51bf4216..1667069c09 100644 --- a/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.h +++ b/src/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.h @@ -32,12 +32,7 @@ public: __override VOID - ShutDown() - override; - - __override - VOID - Recycle() + Stop(bool fServerInitiated) override; __override diff --git a/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp b/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp index 7c335ceff1..17c004b693 100644 --- a/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp +++ b/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp @@ -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); } diff --git a/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h b/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h index 776ae9913c..2b6c36ccfa 100644 --- a/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h +++ b/src/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h @@ -17,7 +17,13 @@ public: { } - ~AppOfflineTrackingApplication() override = default; + ~AppOfflineTrackingApplication() override + { + if (m_fileWatcherEntry) + { + m_fileWatcherEntry->StopMonitor(); + } + }; HRESULT StartMonitoringAppOffline(); diff --git a/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp b/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp index 37828b2623..721e248220 100644 --- a/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp +++ b/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp @@ -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" // diff --git a/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h b/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h index c010caefc0..4e37600c69 100644 --- a/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h +++ b/src/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h @@ -111,6 +111,7 @@ private: FILE_WATCHER* _pFileMonitor; STRU _strFileName; STRU _strDirectoryName; + STRU _strFullName; LONG _lStopMonitorCalled; mutable LONG _cRefs; BOOL _fIsValid; diff --git a/test/Common.FunctionalTests/AppOfflineTests.cs b/test/Common.FunctionalTests/AppOfflineTests.cs new file mode 100644 index 0000000000..b2c30663a9 --- /dev/null +++ b/test/Common.FunctionalTests/AppOfflineTests.cs @@ -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 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 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); + } + } + + } +} diff --git a/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs b/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs index 74af2e00cb..aa79727e19 100644 --- a/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs +++ b/test/Common.FunctionalTests/Inprocess/ServerVariablesTest.cs @@ -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 tasks = new List(); - 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); + }); } } } diff --git a/test/Common.FunctionalTests/Utilities/Helpers.cs b/test/Common.FunctionalTests/Utilities/Helpers.cs index f4348514d1..c067ff46e6 100644 --- a/test/Common.FunctionalTests/Utilities/Helpers.cs +++ b/test/Common.FunctionalTests/Utilities/Helpers.cs @@ -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 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 tasks = new List(); + for (int i = 0; i < 10; i++) + { + tasks.Add(Task.Run(RunRequests)); + } + + await Task.WhenAll(tasks); + } } } diff --git a/test/Common.FunctionalTests/Utilities/IISCapability.cs b/test/Common.FunctionalTests/Utilities/IISCapability.cs index e3a5bf6c4e..d62716db09 100644 --- a/test/Common.FunctionalTests/Utilities/IISCapability.cs +++ b/test/Common.FunctionalTests/Utilities/IISCapability.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests None = 0, Websockets = 1, WindowsAuthentication = 2, - PoolEnvironmentVariables = 4 + PoolEnvironmentVariables = 4, + ShutdownToken = 8 } } diff --git a/test/IIS.FunctionalTests/RequiresIISAttribute.cs b/test/IIS.FunctionalTests/RequiresIISAttribute.cs index 9befaeba96..f94459fd8f 100644 --- a/test/IIS.FunctionalTests/RequiresIISAttribute.cs +++ b/test/IIS.FunctionalTests/RequiresIISAttribute.cs @@ -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; diff --git a/test/IIS.Tests/TestServerTest.cs b/test/IIS.Tests/TestServerTest.cs index c168af99d8..79b1c42113 100644 --- a/test/IIS.Tests/TestServerTest.cs +++ b/test/IIS.Tests/TestServerTest.cs @@ -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); + } } } - } } diff --git a/test/IISExpress.FunctionalTests/InProcess/AppOfflineTests.cs b/test/IISExpress.FunctionalTests/InProcess/AppOfflineTests.cs deleted file mode 100644 index 265907542f..0000000000 --- a/test/IISExpress.FunctionalTests/InProcess/AppOfflineTests.cs +++ /dev/null @@ -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 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 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); - } - } -}