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