Fix another shutdown race and appofline race (#1071)
This commit is contained in:
parent
ba18129d79
commit
61b4473abe
|
|
@ -230,6 +230,8 @@
|
|||
<ClInclude Include="globalmodule.h" />
|
||||
<ClInclude Include="applicationmanager.h" />
|
||||
<ClInclude Include="proxymodule.h" />
|
||||
<ClInclude Include="ServerErrorApplication.h" />
|
||||
<ClInclude Include="ServerErrorHandler.h" />
|
||||
<ClInclude Include="stdafx.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -12,8 +12,10 @@
|
|||
#include "SRWExclusiveLock.h"
|
||||
#include "GlobalVersionUtility.h"
|
||||
#include "exceptions.h"
|
||||
#include "PollingAppOfflineApplication.h"
|
||||
#include "EventLog.h"
|
||||
#include "HandleWrapper.h"
|
||||
#include "ServerErrorApplication.h"
|
||||
#include "AppOfflineApplication.h"
|
||||
|
||||
extern HINSTANCE g_hModule;
|
||||
|
||||
|
|
@ -29,21 +31,7 @@ PFN_ASPNETCORE_CREATE_APPLICATION APPLICATION_INFO::s_pfnAspNetCoreCreateApplica
|
|||
|
||||
APPLICATION_INFO::~APPLICATION_INFO()
|
||||
{
|
||||
if (m_pApplication != NULL)
|
||||
{
|
||||
// shutdown the application
|
||||
m_pApplication->ShutDown();
|
||||
m_pApplication->DereferenceApplication();
|
||||
m_pApplication = NULL;
|
||||
}
|
||||
|
||||
// configuration should be dereferenced after application shutdown
|
||||
// since the former will use it during shutdown
|
||||
if (m_pConfiguration != NULL)
|
||||
{
|
||||
delete m_pConfiguration;
|
||||
m_pConfiguration = NULL;
|
||||
}
|
||||
ShutDownApplication();
|
||||
}
|
||||
|
||||
HRESULT
|
||||
|
|
@ -51,7 +39,7 @@ APPLICATION_INFO::Initialize(
|
|||
_In_ IHttpApplication &pApplication
|
||||
)
|
||||
{
|
||||
m_pConfiguration = new ASPNETCORE_SHIM_CONFIG();
|
||||
m_pConfiguration.reset(new ASPNETCORE_SHIM_CONFIG());
|
||||
RETURN_IF_FAILED(m_pConfiguration->Populate(&m_pServer, &pApplication));
|
||||
RETURN_IF_FAILED(m_struInfoKey.Copy(pApplication.GetApplicationId()));
|
||||
|
||||
|
|
@ -60,22 +48,16 @@ APPLICATION_INFO::Initialize(
|
|||
|
||||
|
||||
HRESULT
|
||||
APPLICATION_INFO::EnsureApplicationCreated(
|
||||
IHttpContext *pHttpContext
|
||||
APPLICATION_INFO::GetOrCreateApplication(
|
||||
IHttpContext *pHttpContext,
|
||||
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER>& pApplication
|
||||
)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
IAPPLICATION *pApplication = NULL;
|
||||
STRU struExeLocation;
|
||||
STRU struHostFxrDllLocation;
|
||||
STACK_STRU(struFileName, 300); // >MAX_PATH
|
||||
|
||||
SRWExclusiveLock lock(m_applicationLock);
|
||||
|
||||
if (m_pApplication != nullptr && m_pApplication->QueryStatus() != RECYCLED)
|
||||
{
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
SRWExclusiveLock lock(m_srwLock);
|
||||
auto& httpApplication = *pHttpContext->GetApplication();
|
||||
|
||||
if (m_pApplication != nullptr)
|
||||
{
|
||||
|
|
@ -84,7 +66,6 @@ APPLICATION_INFO::EnsureApplicationCreated(
|
|||
LOG_INFO("Application went offline");
|
||||
// Application that went offline
|
||||
// are supposed to recycle themselves
|
||||
m_pApplication->DereferenceApplication();
|
||||
m_pApplication = nullptr;
|
||||
}
|
||||
else
|
||||
|
|
@ -93,26 +74,15 @@ APPLICATION_INFO::EnsureApplicationCreated(
|
|||
FINISHED(S_OK);
|
||||
}
|
||||
}
|
||||
else if (m_fAppCreationAttempted)
|
||||
{
|
||||
// previous CreateApplication failed
|
||||
FINISHED(E_APPLICATION_ACTIVATION_EXEC_FAILURE);
|
||||
}
|
||||
|
||||
auto& httpApplication = *pHttpContext->GetApplication();
|
||||
if (PollingAppOfflineApplication::ShouldBeStarted(httpApplication))
|
||||
if (AppOfflineApplication::ShouldBeStarted(httpApplication))
|
||||
{
|
||||
LOG_INFO("Detected app_ofline file, creating polling application");
|
||||
m_pApplication = new PollingAppOfflineApplication(httpApplication);
|
||||
LOG_INFO("Detected app_offline file, creating polling application");
|
||||
m_pApplication.reset(new AppOfflineApplication(httpApplication));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Move the request handler check inside of the lock
|
||||
// such that only one request finds and loads it.
|
||||
// FindRequestHandlerAssembly obtains a global lock, but after releasing the lock,
|
||||
// there is a period where we could call
|
||||
|
||||
m_fAppCreationAttempted = TRUE;
|
||||
STRU struExeLocation;
|
||||
FINISHED_IF_FAILED(FindRequestHandlerAssembly(struExeLocation));
|
||||
|
||||
if (m_pfnAspNetCoreCreateApplication == NULL)
|
||||
|
|
@ -123,15 +93,17 @@ APPLICATION_INFO::EnsureApplicationCreated(
|
|||
std::array<APPLICATION_PARAMETER, 1> parameters {
|
||||
{"InProcessExeLocation", struExeLocation.QueryStr()}
|
||||
};
|
||||
|
||||
LOG_INFO("Creating handler application");
|
||||
IAPPLICATION * newApplication;
|
||||
FINISHED_IF_FAILED(m_pfnAspNetCoreCreateApplication(
|
||||
&m_pServer,
|
||||
pHttpContext->GetApplication(),
|
||||
&httpApplication,
|
||||
parameters.data(),
|
||||
static_cast<DWORD>(parameters.size()),
|
||||
&pApplication));
|
||||
&newApplication));
|
||||
|
||||
m_pApplication = pApplication;
|
||||
m_pApplication.reset(newApplication);
|
||||
}
|
||||
|
||||
Finished:
|
||||
|
|
@ -143,8 +115,15 @@ Finished:
|
|||
EVENTLOG_ERROR_TYPE,
|
||||
ASPNETCORE_EVENT_ADD_APPLICATION_ERROR,
|
||||
ASPNETCORE_EVENT_ADD_APPLICATION_ERROR_MSG,
|
||||
pHttpContext->GetApplication()->GetApplicationId(),
|
||||
httpApplication.GetApplicationId(),
|
||||
hr);
|
||||
|
||||
m_pApplication.reset(new ServerErrorApplication(httpApplication, hr));
|
||||
}
|
||||
|
||||
if (m_pApplication)
|
||||
{
|
||||
pApplication = ReferenceApplication(m_pApplication.get());
|
||||
}
|
||||
|
||||
return hr;
|
||||
|
|
@ -413,107 +392,47 @@ Finished:
|
|||
VOID
|
||||
APPLICATION_INFO::RecycleApplication()
|
||||
{
|
||||
IAPPLICATION* pApplication;
|
||||
HANDLE hThread = INVALID_HANDLE_VALUE;
|
||||
SRWExclusiveLock lock(m_applicationLock);
|
||||
|
||||
if (m_pApplication != NULL)
|
||||
if (m_pApplication)
|
||||
{
|
||||
SRWExclusiveLock lock(m_srwLock);
|
||||
const auto pApplication = m_pApplication.release();
|
||||
|
||||
if (m_pApplication != NULL)
|
||||
{
|
||||
pApplication = m_pApplication;
|
||||
if (m_pConfiguration->QueryHostingModel() == HOSTING_OUT_PROCESS)
|
||||
{
|
||||
//
|
||||
// For inprocess, need to set m_pApplication to NULL first to
|
||||
// avoid mapping new request to the recycled application.
|
||||
// Outofprocess application instance will be created for new request
|
||||
// For inprocess, as recycle will lead to shutdown later, leave m_pApplication
|
||||
// to not block incoming requests till worker process shutdown
|
||||
//
|
||||
m_pApplication = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// For inprocess, need hold the application till shutdown is called
|
||||
// Bump the reference counter as DoRecycleApplication will do dereference
|
||||
//
|
||||
pApplication->ReferenceApplication();
|
||||
}
|
||||
|
||||
hThread = CreateThread(
|
||||
NULL, // default security attributes
|
||||
0, // default stack size
|
||||
(LPTHREAD_START_ROUTINE)DoRecycleApplication,
|
||||
pApplication, // thread function arguments
|
||||
0, // default creation flags
|
||||
NULL); // receive thread identifier
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_pConfiguration->QueryHostingModel() == HOSTING_IN_PROCESS)
|
||||
{
|
||||
// In process application failed to start for whatever reason, need to recycle the work process
|
||||
m_pServer.RecycleProcess(L"AspNetCore InProcess Recycle Process on Demand");
|
||||
}
|
||||
}
|
||||
|
||||
if (hThread == NULL)
|
||||
{
|
||||
if (!g_fRecycleProcessCalled)
|
||||
{
|
||||
g_fRecycleProcessCalled = TRUE;
|
||||
m_pServer.RecycleProcess(L"On Demand by AspNetCore Module for recycle application failure");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Closing a thread handle does not terminate the associated thread or remove the thread object.
|
||||
CloseHandle(hThread);
|
||||
}
|
||||
HandleWrapper<InvalidHandleTraits> hThread = CreateThread(
|
||||
NULL, // default security attributes
|
||||
0, // default stack size
|
||||
(LPTHREAD_START_ROUTINE)DoRecycleApplication,
|
||||
pApplication, // thread function arguments
|
||||
0, // default creation flags
|
||||
NULL); // receive thread identifier
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
VOID
|
||||
DWORD WINAPI
|
||||
APPLICATION_INFO::DoRecycleApplication(
|
||||
LPVOID lpParam)
|
||||
{
|
||||
IAPPLICATION* pApplication = static_cast<IAPPLICATION*>(lpParam);
|
||||
auto pApplication = std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER>(static_cast<IAPPLICATION*>(lpParam));
|
||||
|
||||
// No lock required
|
||||
|
||||
if (pApplication != NULL)
|
||||
if (pApplication)
|
||||
{
|
||||
// Recycle will call shutdown for out of process
|
||||
pApplication->Recycle();
|
||||
|
||||
// Decrement the ref count as we reference it in RecycleApplication.
|
||||
pApplication->DereferenceApplication();
|
||||
pApplication->Stop(/*fServerInitiated*/ false);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
VOID
|
||||
APPLICATION_INFO::ShutDownApplication()
|
||||
{
|
||||
IAPPLICATION* pApplication = NULL;
|
||||
SRWExclusiveLock lock(m_applicationLock);
|
||||
|
||||
// pApplication can be NULL due to app_offline
|
||||
if (m_pApplication != NULL)
|
||||
if (m_pApplication)
|
||||
{
|
||||
SRWExclusiveLock lock(m_srwLock);
|
||||
|
||||
if (m_pApplication != NULL)
|
||||
{
|
||||
pApplication = m_pApplication;
|
||||
|
||||
// Set m_pApplication to NULL first to prevent anyone from using it
|
||||
m_pApplication = NULL;
|
||||
pApplication->ShutDown();
|
||||
pApplication->DereferenceApplication();
|
||||
}
|
||||
m_pApplication ->Stop(/* fServerInitiated */ true);
|
||||
m_pApplication = nullptr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,11 +32,10 @@ public:
|
|||
m_pServer(pServer),
|
||||
m_cRefs(1),
|
||||
m_fValid(FALSE),
|
||||
m_fAppCreationAttempted(FALSE),
|
||||
m_pConfiguration(NULL),
|
||||
m_pConfiguration(nullptr),
|
||||
m_pfnAspNetCoreCreateApplication(NULL)
|
||||
{
|
||||
InitializeSRWLock(&m_srwLock);
|
||||
InitializeSRWLock(&m_applicationLock);
|
||||
}
|
||||
|
||||
PCWSTR
|
||||
|
|
@ -90,19 +89,7 @@ public:
|
|||
ASPNETCORE_SHIM_CONFIG*
|
||||
QueryConfig()
|
||||
{
|
||||
return m_pConfiguration;
|
||||
}
|
||||
|
||||
//
|
||||
// ExtractApplication will increase the reference counter of the application
|
||||
// Caller is responsible for dereference the application.
|
||||
// Otherwise memory leak
|
||||
//
|
||||
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER>
|
||||
ExtractApplication() const
|
||||
{
|
||||
SRWSharedLock lock(m_srwLock);
|
||||
return ReferenceApplication(m_pApplication);
|
||||
return m_pConfiguration.get();
|
||||
}
|
||||
|
||||
VOID
|
||||
|
|
@ -112,8 +99,9 @@ public:
|
|||
ShutDownApplication();
|
||||
|
||||
HRESULT
|
||||
EnsureApplicationCreated(
|
||||
IHttpContext *pHttpContext
|
||||
GetOrCreateApplication(
|
||||
IHttpContext *pHttpContext,
|
||||
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER>& pApplication
|
||||
);
|
||||
|
||||
private:
|
||||
|
|
@ -121,17 +109,18 @@ private:
|
|||
HRESULT FindNativeAssemblyFromGlobalLocation(PCWSTR libraryName, STRU* location);
|
||||
HRESULT FindNativeAssemblyFromHostfxr(HOSTFXR_OPTIONS* hostfxrOptions, PCWSTR libraryName, STRU* location);
|
||||
|
||||
static VOID DoRecycleApplication(LPVOID lpParam);
|
||||
static DWORD WINAPI DoRecycleApplication(LPVOID lpParam);
|
||||
|
||||
mutable LONG m_cRefs;
|
||||
STRU m_struInfoKey;
|
||||
BOOL m_fValid;
|
||||
BOOL m_fAppCreationAttempted;
|
||||
ASPNETCORE_SHIM_CONFIG *m_pConfiguration;
|
||||
IAPPLICATION *m_pApplication;
|
||||
SRWLOCK m_srwLock;
|
||||
SRWLOCK m_applicationLock;
|
||||
IHttpServer &m_pServer;
|
||||
PFN_ASPNETCORE_CREATE_APPLICATION m_pfnAspNetCoreCreateApplication;
|
||||
|
||||
std::unique_ptr<ASPNETCORE_SHIM_CONFIG> m_pConfiguration;
|
||||
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER> m_pApplication;
|
||||
|
||||
|
||||
static const PCWSTR s_pwzAspnetcoreInProcessRequestHandlerName;
|
||||
static const PCWSTR s_pwzAspnetcoreOutOfProcessRequestHandlerName;
|
||||
|
|
|
|||
|
|
@ -97,7 +97,6 @@ APPLICATION_MANAGER::GetOrCreateApplicationInfo(
|
|||
*ppApplicationInfo = pApplicationInfo;
|
||||
pApplicationInfo = NULL;
|
||||
}
|
||||
|
||||
Finished:
|
||||
|
||||
// log the error
|
||||
|
|
|
|||
|
|
@ -95,20 +95,18 @@ ASPNET_CORE_PROXY_MODULE::OnExecuteRequestHandler(
|
|||
// the error should already been logged to window event log for the first request
|
||||
FINISHED(E_APPLICATION_ACTIVATION_EXEC_FAILURE);
|
||||
}
|
||||
|
||||
// make sure assmebly is loaded and application is created
|
||||
FINISHED_IF_FAILED(m_pApplicationInfo->EnsureApplicationCreated(pHttpContext));
|
||||
|
||||
auto pApplication = m_pApplicationInfo->ExtractApplication();
|
||||
|
||||
DBG_ASSERT(pHttpContext);
|
||||
|
||||
// We allow OFFLINE application to serve pages
|
||||
DBG_ASSERT(pHttpContext);
|
||||
|
||||
std::unique_ptr<IAPPLICATION, IAPPLICATION_DELETER> pApplication;
|
||||
FINISHED_IF_FAILED(m_pApplicationInfo->GetOrCreateApplication(pHttpContext, pApplication));
|
||||
|
||||
// We allow RECYCLED application to serve pages
|
||||
if (pApplication->QueryStatus() != APPLICATION_STATUS::RUNNING &&
|
||||
pApplication->QueryStatus() != APPLICATION_STATUS::STARTING)
|
||||
pApplication->QueryStatus() != APPLICATION_STATUS::STARTING &&
|
||||
pApplication->QueryStatus() != APPLICATION_STATUS::RECYCLED)
|
||||
{
|
||||
hr = HRESULT_FROM_WIN32(ERROR_SERVER_DISABLED);
|
||||
goto Finished;
|
||||
FINISHED(HRESULT_FROM_WIN32(ERROR_SERVER_DISABLED));
|
||||
}
|
||||
|
||||
IREQUEST_HANDLER* pHandler;
|
||||
|
|
@ -124,7 +122,7 @@ ASPNET_CORE_PROXY_MODULE::OnExecuteRequestHandler(
|
|||
}
|
||||
|
||||
Finished:
|
||||
if (FAILED(hr))
|
||||
if (LOG_IF_FAILED(hr))
|
||||
{
|
||||
retVal = RQ_NOTIFICATION_FINISH_REQUEST;
|
||||
if (hr == HRESULT_FROM_WIN32(ERROR_SERVER_SHUTDOWN_IN_PROGRESS))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
#include "AppOfflineApplication.h"
|
||||
|
||||
#include <experimental/filesystem>
|
||||
#include "HandleWrapper.h"
|
||||
#include "AppOfflineHandler.h"
|
||||
|
||||
HRESULT AppOfflineApplication::CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler)
|
||||
{
|
||||
try
|
||||
{
|
||||
*pRequestHandler = new AppOfflineHandler(pHttpContext, m_strAppOfflineContent);
|
||||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT AppOfflineApplication::OnAppOfflineFound()
|
||||
{
|
||||
LARGE_INTEGER li = {};
|
||||
|
||||
HandleWrapper<InvalidHandleTraits> handle = CreateFile(m_appOfflineLocation.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
RETURN_LAST_ERROR_IF(handle == INVALID_HANDLE_VALUE);
|
||||
|
||||
RETURN_LAST_ERROR_IF(!GetFileSizeEx(handle, &li));
|
||||
|
||||
if (li.HighPart != 0)
|
||||
{
|
||||
// > 4gb file size not supported
|
||||
// todo: log a warning at event log
|
||||
return E_INVALIDARG;
|
||||
}
|
||||
|
||||
if (li.LowPart > 0)
|
||||
{
|
||||
DWORD bytesRead = 0;
|
||||
std::string pszBuff(li.LowPart + 1, '\0');
|
||||
|
||||
RETURN_LAST_ERROR_IF(!ReadFile(handle, pszBuff.data(), li.LowPart, &bytesRead, NULL));
|
||||
pszBuff.resize(bytesRead);
|
||||
|
||||
m_strAppOfflineContent = pszBuff;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
bool AppOfflineApplication::ShouldBeStarted(IHttpApplication& pApplication)
|
||||
{
|
||||
return is_regular_file(GetAppOfflineLocation(pApplication));
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
#include "AppOfflineHandler.h"
|
||||
|
||||
#include "HandleWrapper.h"
|
||||
|
||||
REQUEST_NOTIFICATION_STATUS AppOfflineHandler::OnExecuteRequestHandler()
|
||||
{
|
||||
HTTP_DATA_CHUNK DataChunk;
|
||||
IHttpResponse* pResponse = m_pContext->GetResponse();
|
||||
|
||||
DBG_ASSERT(pResponse);
|
||||
|
||||
// Ignore failure hresults as nothing we can do
|
||||
// Set fTrySkipCustomErrors to true as we want client see the offline content
|
||||
pResponse->SetStatus(503, "Service Unavailable", 0, S_OK, nullptr, TRUE);
|
||||
pResponse->SetHeader("Content-Type",
|
||||
"text/html",
|
||||
static_cast<USHORT>(strlen("text/html")),
|
||||
FALSE
|
||||
);
|
||||
|
||||
DataChunk.DataChunkType = HttpDataChunkFromMemory;
|
||||
DataChunk.FromMemory.pBuffer = m_strAppOfflineContent.data();
|
||||
DataChunk.FromMemory.BufferLength = static_cast<ULONG>(m_strAppOfflineContent.size());
|
||||
pResponse->WriteEntityChunkByReference(&DataChunk);
|
||||
|
||||
return REQUEST_NOTIFICATION_STATUS::RQ_NOTIFICATION_FINISH_REQUEST;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -193,6 +193,8 @@
|
|||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="application.h" />
|
||||
<ClInclude Include="AppOfflineApplication.h" />
|
||||
<ClInclude Include="AppOfflineHandler.h" />
|
||||
<ClInclude Include="config_utility.h" />
|
||||
<ClInclude Include="Environment.h" />
|
||||
<ClInclude Include="EventLog.h" />
|
||||
|
|
@ -216,6 +218,8 @@
|
|||
<ClInclude Include="utility.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="AppOfflineApplication.cpp" />
|
||||
<ClCompile Include="AppOfflineHandler.cpp" />
|
||||
<ClCompile Include="debugutil.cpp" />
|
||||
<ClCompile Include="Environment.cpp" />
|
||||
<ClCompile Include="fx_ver.cpp" />
|
||||
|
|
|
|||
|
|
@ -7,25 +7,9 @@
|
|||
#include "SRWExclusiveLock.h"
|
||||
#include "HandleWrapper.h"
|
||||
|
||||
HRESULT PollingAppOfflineApplication::CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler)
|
||||
{
|
||||
try
|
||||
{
|
||||
*pRequestHandler = new PollingAppOfflineHandler(pHttpContext, m_strAppOfflineContent);
|
||||
}
|
||||
CATCH_RETURN();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
APPLICATION_STATUS PollingAppOfflineApplication::QueryStatus()
|
||||
{
|
||||
if (AppOfflineExists())
|
||||
{
|
||||
return APPLICATION_STATUS::RUNNING;
|
||||
}
|
||||
|
||||
return APPLICATION_STATUS::RECYCLED;
|
||||
return (AppOfflineExists() == (m_mode == StopWhenRemoved)) ? APPLICATION_STATUS::RUNNING : APPLICATION_STATUS::RECYCLED;
|
||||
}
|
||||
|
||||
bool
|
||||
|
|
@ -35,7 +19,7 @@ PollingAppOfflineApplication::AppOfflineExists()
|
|||
//
|
||||
// we only care about app offline presented. If not, it means the application has started
|
||||
// and is monitoring the app offline file
|
||||
// we cache the file exist check result for 1 second
|
||||
// we cache the file exist check result for 200 ms
|
||||
//
|
||||
if (ulCurrentTime - m_ulLastCheckTime > c_appOfflineRefreshIntervalMS)
|
||||
{
|
||||
|
|
@ -45,7 +29,7 @@ PollingAppOfflineApplication::AppOfflineExists()
|
|||
m_fAppOfflineFound = is_regular_file(m_appOfflineLocation);
|
||||
if(m_fAppOfflineFound)
|
||||
{
|
||||
LOG_IF_FAILED(LoadAppOfflineContent());
|
||||
LOG_IF_FAILED(OnAppOfflineFound());
|
||||
}
|
||||
m_ulLastCheckTime = ulCurrentTime;
|
||||
}
|
||||
|
|
@ -53,81 +37,8 @@ PollingAppOfflineApplication::AppOfflineExists()
|
|||
return m_fAppOfflineFound;
|
||||
}
|
||||
|
||||
HRESULT PollingAppOfflineApplication::LoadAppOfflineContent()
|
||||
{
|
||||
LARGE_INTEGER li = {};
|
||||
|
||||
HandleWrapper<InvalidHandleTraits> handle = CreateFile(m_appOfflineLocation.c_str(),
|
||||
GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
RETURN_LAST_ERROR_IF(handle == INVALID_HANDLE_VALUE);
|
||||
|
||||
RETURN_LAST_ERROR_IF (!GetFileSizeEx(handle, &li));
|
||||
|
||||
if (li.HighPart != 0)
|
||||
{
|
||||
// > 4gb file size not supported
|
||||
// todo: log a warning at event log
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (li.LowPart > 0)
|
||||
{
|
||||
DWORD bytesRead = 0;
|
||||
std::string pszBuff(li.LowPart + 1, '\0');
|
||||
|
||||
RETURN_LAST_ERROR_IF(!ReadFile(handle, pszBuff.data(), li.LowPart, &bytesRead, NULL));
|
||||
pszBuff.resize(bytesRead);
|
||||
|
||||
m_strAppOfflineContent = pszBuff;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
bool PollingAppOfflineApplication::ShouldBeStarted(IHttpApplication& pApplication)
|
||||
{
|
||||
return is_regular_file(GetAppOfflineLocation(pApplication));
|
||||
}
|
||||
|
||||
std::experimental::filesystem::path PollingAppOfflineApplication::GetAppOfflineLocation(IHttpApplication& pApplication)
|
||||
{
|
||||
return std::experimental::filesystem::path(pApplication.GetApplicationPhysicalPath()) / "app_offline.htm";
|
||||
}
|
||||
|
||||
void PollingAppOfflineApplication::ShutDown()
|
||||
{
|
||||
}
|
||||
|
||||
void PollingAppOfflineApplication::Recycle()
|
||||
{
|
||||
}
|
||||
|
||||
REQUEST_NOTIFICATION_STATUS PollingAppOfflineHandler::OnExecuteRequestHandler()
|
||||
{
|
||||
HTTP_DATA_CHUNK DataChunk;
|
||||
IHttpResponse* pResponse = m_pContext->GetResponse();
|
||||
|
||||
DBG_ASSERT(pResponse);
|
||||
|
||||
// Ignore failure hresults as nothing we can do
|
||||
// Set fTrySkipCustomErrors to true as we want client see the offline content
|
||||
pResponse->SetStatus(503, "Service Unavailable", 0, S_OK, nullptr, TRUE);
|
||||
pResponse->SetHeader("Content-Type",
|
||||
"text/html",
|
||||
static_cast<USHORT>(strlen("text/html")),
|
||||
FALSE
|
||||
);
|
||||
|
||||
DataChunk.DataChunkType = HttpDataChunkFromMemory;
|
||||
DataChunk.FromMemory.pBuffer = m_strAppOfflineContent.data();
|
||||
DataChunk.FromMemory.BufferLength = static_cast<ULONG>(m_strAppOfflineContent.size());
|
||||
pResponse->WriteEntityChunkByReference(&DataChunk);
|
||||
|
||||
return REQUEST_NOTIFICATION_STATUS::RQ_NOTIFICATION_FINISH_REQUEST;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,54 +4,40 @@
|
|||
#pragma once
|
||||
#include <filesystem>
|
||||
#include "application.h"
|
||||
#include "requesthandler.h"
|
||||
|
||||
enum PollingAppOfflineApplicationMode
|
||||
{
|
||||
StopWhenAdded,
|
||||
StopWhenRemoved
|
||||
};
|
||||
|
||||
class PollingAppOfflineApplication: public APPLICATION
|
||||
{
|
||||
public:
|
||||
PollingAppOfflineApplication(IHttpApplication& pApplication)
|
||||
PollingAppOfflineApplication(IHttpApplication& pApplication, PollingAppOfflineApplicationMode mode)
|
||||
:
|
||||
m_ulLastCheckTime(0),
|
||||
m_appOfflineLocation(GetAppOfflineLocation(pApplication)),
|
||||
m_fAppOfflineFound(false)
|
||||
m_fAppOfflineFound(false),
|
||||
m_mode(mode)
|
||||
{
|
||||
InitializeSRWLock(&m_statusLock);
|
||||
}
|
||||
|
||||
HRESULT CreateHandler(IHttpContext* pHttpContext, IREQUEST_HANDLER** pRequestHandler) override;
|
||||
|
||||
APPLICATION_STATUS QueryStatus() override;
|
||||
bool AppOfflineExists();
|
||||
HRESULT LoadAppOfflineContent();
|
||||
static bool ShouldBeStarted(IHttpApplication& pApplication);
|
||||
void ShutDown() override;
|
||||
void Recycle() override;
|
||||
virtual HRESULT OnAppOfflineFound() = 0;
|
||||
void Stop(bool fServerInitiated) override { UNREFERENCED_PARAMETER(fServerInitiated); }
|
||||
|
||||
protected:
|
||||
std::experimental::filesystem::path m_appOfflineLocation;
|
||||
static std::experimental::filesystem::path GetAppOfflineLocation(IHttpApplication& pApplication);
|
||||
|
||||
private:
|
||||
static const int c_appOfflineRefreshIntervalMS = 200;
|
||||
static std::experimental::filesystem::path GetAppOfflineLocation(IHttpApplication& pApplication);
|
||||
std::string m_strAppOfflineContent;
|
||||
ULONGLONG m_ulLastCheckTime;
|
||||
std::experimental::filesystem::path m_appOfflineLocation;
|
||||
ULONGLONG m_ulLastCheckTime;
|
||||
bool m_fAppOfflineFound;
|
||||
SRWLOCK m_statusLock {};
|
||||
PollingAppOfflineApplicationMode m_mode;
|
||||
};
|
||||
|
||||
|
||||
class PollingAppOfflineHandler: public REQUEST_HANDLER
|
||||
{
|
||||
public:
|
||||
PollingAppOfflineHandler(IHttpContext* pContext, const std::string appOfflineContent)
|
||||
: m_pContext(pContext),
|
||||
m_strAppOfflineContent(appOfflineContent)
|
||||
{
|
||||
}
|
||||
|
||||
REQUEST_NOTIFICATION_STATUS OnExecuteRequestHandler() override;
|
||||
|
||||
private:
|
||||
IHttpContext* m_pContext;
|
||||
std::string m_strAppOfflineContent;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 { };
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public:
|
|||
|
||||
~InProcessApplicationBase() = default;
|
||||
|
||||
VOID Recycle(VOID) override;
|
||||
VOID Stop(bool fServerInitiated) override;
|
||||
|
||||
protected:
|
||||
BOOL m_fRecycleCalled;
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@
|
|||
<ClInclude Include="inprocessapplication.h" />
|
||||
<ClInclude Include="InProcessApplicationBase.h" />
|
||||
<ClInclude Include="inprocesshandler.h" />
|
||||
<ClInclude Include="ShuttingDownApplication.h" />
|
||||
<ClInclude Include="stdafx.h" />
|
||||
<ClInclude Include="StartupExceptionApplication.h" />
|
||||
<ClInclude Include="StartupExceptionHandler.h" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ public:
|
|||
|
||||
~StartupExceptionApplication() = default;
|
||||
|
||||
VOID ShutDown() override;
|
||||
HRESULT CreateHandler(IHttpContext * pHttpContext, IREQUEST_HANDLER ** pRequestHandler) override;
|
||||
|
||||
std::string&
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "debugutil.h"
|
||||
#include "resources.h"
|
||||
#include "exceptions.h"
|
||||
#include "ShuttingDownApplication.h"
|
||||
|
||||
DECLARE_DEBUG_PRINT_OBJECT("aspnetcorev2_inprocess.dll");
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ IHttpServer * g_pHttpServer = NULL;
|
|||
HINSTANCE g_hWinHttpModule;
|
||||
HINSTANCE g_hAspNetCoreModule;
|
||||
HANDLE g_hEventLog = NULL;
|
||||
bool g_fInProcessApplicationCreated = false;
|
||||
|
||||
HRESULT
|
||||
InitializeGlobalConfiguration(
|
||||
|
|
@ -93,7 +95,15 @@ CreateApplication(
|
|||
try
|
||||
{
|
||||
RETURN_IF_FAILED(InitializeGlobalConfiguration(pServer, pHttpApplication));
|
||||
|
||||
|
||||
// In process application was already created so another call to CreateApplication
|
||||
// means that server is shutting does and request arrived in the meantime
|
||||
if (g_fInProcessApplicationCreated)
|
||||
{
|
||||
*ppApplication = new ShuttingDownApplication(*pServer, *pHttpApplication);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
REQUESTHANDLER_CONFIG *pConfig = nullptr;
|
||||
RETURN_IF_FAILED(REQUESTHANDLER_CONFIG::CreateRequestHandlerConfig(pServer, pHttpApplication, &pConfig));
|
||||
std::unique_ptr<REQUESTHANDLER_CONFIG> pRequestHandlerConfig(pConfig);
|
||||
|
|
@ -102,6 +112,8 @@ CreateApplication(
|
|||
|
||||
auto pApplication = std::make_unique<IN_PROCESS_APPLICATION>(*pServer, *pHttpApplication, std::move(pRequestHandlerConfig), pParameters, nParameters);
|
||||
|
||||
// never create two inprocess applications in one process
|
||||
g_fInProcessApplicationCreated = true;
|
||||
if (FAILED_LOG(pApplication->LoadManagedApplication()))
|
||||
{
|
||||
// Set the currently running application to a fake application that returns startup exceptions.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -32,12 +32,7 @@ public:
|
|||
|
||||
__override
|
||||
VOID
|
||||
ShutDown()
|
||||
override;
|
||||
|
||||
__override
|
||||
VOID
|
||||
Recycle()
|
||||
Stop(bool fServerInitiated)
|
||||
override;
|
||||
|
||||
__override
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@ public:
|
|||
{
|
||||
}
|
||||
|
||||
~AppOfflineTrackingApplication() override = default;
|
||||
~AppOfflineTrackingApplication() override
|
||||
{
|
||||
if (m_fileWatcherEntry)
|
||||
{
|
||||
m_fileWatcherEntry->StopMonitor();
|
||||
}
|
||||
};
|
||||
|
||||
HRESULT
|
||||
StartMonitoringAppOffline();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
//
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ private:
|
|||
FILE_WATCHER* _pFileMonitor;
|
||||
STRU _strFileName;
|
||||
STRU _strDirectoryName;
|
||||
STRU _strFullName;
|
||||
LONG _lStopMonitorCalled;
|
||||
mutable LONG _cRefs;
|
||||
BOOL _fIsValid;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,278 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
|
||||
using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests;
|
||||
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.Inprocess
|
||||
{
|
||||
public class AppOfflineTests : IISFunctionalTestBase
|
||||
{
|
||||
[ConditionalTheory]
|
||||
[InlineData(HostingModel.InProcess)]
|
||||
[InlineData(HostingModel.OutOfProcess)]
|
||||
public async Task AppOfflineDroppedWhileSiteIsDown_SiteReturns503(HostingModel hostingModel)
|
||||
{
|
||||
var deploymentResult = await DeployApp(hostingModel);
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
await AssertAppOffline(deploymentResult);
|
||||
DeletePublishOutput(deploymentResult);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[InlineData(HostingModel.InProcess, 500, "500.0")]
|
||||
[InlineData(HostingModel.OutOfProcess, 502, "502.5")]
|
||||
public async Task AppOfflineDroppedWhileSiteFailedToStartInShim_AppOfflineServed(HostingModel hostingModel, int statusCode, string content)
|
||||
{
|
||||
|
||||
var deploymentParameters = Helpers.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true);
|
||||
deploymentParameters.WebConfigActionList.Add(WebConfigHelpers.AddOrModifyAspNetCoreSection("processPath", "nonexistent"));
|
||||
|
||||
var deploymentResult = await DeployAsync(deploymentParameters);
|
||||
|
||||
var result = await deploymentResult.HttpClient.GetAsync("/");
|
||||
Assert.Equal(statusCode, (int)result.StatusCode);
|
||||
Assert.Contains(content, await result.Content.ReadAsStringAsync());
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
await AssertAppOffline(deploymentResult);
|
||||
DeletePublishOutput(deploymentResult);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[RequiresIIS(IISCapability.ShutdownToken)]
|
||||
public async Task AppOfflineDroppedWhileSiteFailedToStartInRequestHandler_SiteStops_InProcess()
|
||||
{
|
||||
var deploymentResult = await DeployApp(HostingModel.InProcess);
|
||||
|
||||
// Set file content to empty so it fails at runtime
|
||||
File.WriteAllText(Path.Combine(deploymentResult.ContentRoot, "Microsoft.AspNetCore.Server.IIS.dll"), "");
|
||||
|
||||
var result = await deploymentResult.HttpClient.GetAsync("/");
|
||||
Assert.Equal(500, (int)result.StatusCode);
|
||||
Assert.Contains("500.30", await result.Content.ReadAsStringAsync());
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
AssertStopsProcess(deploymentResult);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[RequiresIIS(IISCapability.ShutdownToken)]
|
||||
public async Task AppOfflineDroppedWhileSiteStarting_SiteShutsDown_InProcess()
|
||||
{
|
||||
var deploymentResult = await DeployApp(HostingModel.InProcess);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
|
||||
// send first request and add app_offline while app is starting
|
||||
var runningTask = AssertAppOffline(deploymentResult);
|
||||
|
||||
// This test tries to hit a race where we drop app_offline file while
|
||||
// in process application is starting, application start takes at least 400ms
|
||||
// so we back off for 100ms to allow request to reach request handler
|
||||
// Test itself is racy and can result in two scenarios
|
||||
// 1. ANCM detects app_offline before it starts the request - if AssertAppOffline succeeds we've hit it
|
||||
// 2. Intended scenario where app starts and then shuts down
|
||||
// In first case we remove app_offline and try again
|
||||
await Task.Delay(100);
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
try
|
||||
{
|
||||
await runningTask.TimeoutAfterDefault();
|
||||
|
||||
// if AssertAppOffline succeeded ANCM have picked up app_offline before starting the app
|
||||
// try again
|
||||
RemoveAppOffline(deploymentResult.ContentRoot);
|
||||
}
|
||||
catch
|
||||
{
|
||||
AssertStopsProcess(deploymentResult);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(false);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[RequiresIIS(IISCapability.ShutdownToken)]
|
||||
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess()
|
||||
{
|
||||
var deploymentResult = await AssertStarts(HostingModel.InProcess);
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
AssertStopsProcess(deploymentResult);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[RequiresIIS(IISCapability.ShutdownToken)]
|
||||
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_OutOfProcess()
|
||||
{
|
||||
var deploymentResult = await AssertStarts(HostingModel.OutOfProcess);
|
||||
|
||||
// Repeat dropping file and restarting multiple times
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
await AssertAppOffline(deploymentResult);
|
||||
RemoveAppOffline(deploymentResult.ContentRoot);
|
||||
await AssertRunning(deploymentResult);
|
||||
}
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
await AssertAppOffline(deploymentResult);
|
||||
DeletePublishOutput(deploymentResult);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[InlineData(HostingModel.InProcess)]
|
||||
[InlineData(HostingModel.OutOfProcess)]
|
||||
public async Task AppOfflineDropped_CanRemoveAppOfflineAfterAddingAndSiteWorks(HostingModel hostingModel)
|
||||
{
|
||||
var deploymentResult = await DeployApp(hostingModel);
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
await AssertAppOffline(deploymentResult);
|
||||
|
||||
RemoveAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
await AssertRunning(deploymentResult);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[InlineData(HostingModel.InProcess)]
|
||||
[InlineData(HostingModel.OutOfProcess)]
|
||||
public async Task AppOfflineAddedAndRemovedStress(HostingModel hostingModel)
|
||||
{
|
||||
var deploymentResult = await AssertStarts(hostingModel);
|
||||
|
||||
var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => {
|
||||
var statusCode = (int)response.StatusCode;
|
||||
Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode);
|
||||
});
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
// AddAppOffline might fail if app_offline is being read by ANCM and deleted at the same time
|
||||
RetryHelper.RetryOperation(
|
||||
() => AddAppOffline(deploymentResult.ContentRoot),
|
||||
e => Logger.LogError($"Failed to create app_offline : {e.Message}"),
|
||||
retryCount: 3,
|
||||
retryDelayMilliseconds: 100);
|
||||
RemoveAppOffline(deploymentResult.ContentRoot);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await load;
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException)
|
||||
{
|
||||
// IOException in InProcess is fine, just means process stopped
|
||||
if (hostingModel != HostingModel.InProcess)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<IISDeploymentResult> DeployApp(HostingModel hostingModel = HostingModel.InProcess)
|
||||
{
|
||||
var deploymentParameters = Helpers.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true);
|
||||
|
||||
return await DeployAsync(deploymentParameters);
|
||||
}
|
||||
|
||||
private void AddAppOffline(string appPath, string content = "The app is offline.")
|
||||
{
|
||||
File.WriteAllText(Path.Combine(appPath, "app_offline.htm"), content);
|
||||
}
|
||||
|
||||
private void RemoveAppOffline(string appPath)
|
||||
{
|
||||
RetryHelper.RetryOperation(
|
||||
() => File.Delete(Path.Combine(appPath, "app_offline.htm")),
|
||||
e => Logger.LogError($"Failed to remove app_offline : {e.Message}"),
|
||||
retryCount: 3,
|
||||
retryDelayMilliseconds: 100);
|
||||
}
|
||||
|
||||
private async Task AssertAppOffline(IISDeploymentResult deploymentResult, string expectedResponse = "The app is offline.")
|
||||
{
|
||||
HttpResponseMessage response = null;
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
// Keep retrying until app_offline is present.
|
||||
response = await deploymentResult.HttpClient.GetAsync("HelloWorld");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||
|
||||
Assert.Equal(expectedResponse, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
private void AssertStopsProcess(IISDeploymentResult deploymentResult)
|
||||
{
|
||||
var hostShutdownToken = deploymentResult.HostShutdownToken;
|
||||
|
||||
Assert.True(hostShutdownToken.WaitHandle.WaitOne(TimeoutExtensions.DefaultTimeout));
|
||||
Assert.True(hostShutdownToken.IsCancellationRequested);
|
||||
}
|
||||
|
||||
private async Task<IISDeploymentResult> AssertStarts(HostingModel hostingModel)
|
||||
{
|
||||
var deploymentResult = await DeployApp(hostingModel);
|
||||
|
||||
await AssertRunning(deploymentResult);
|
||||
|
||||
return deploymentResult;
|
||||
}
|
||||
|
||||
private static async Task AssertRunning(IISDeploymentResult deploymentResult)
|
||||
{
|
||||
var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld");
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("Hello World", responseText);
|
||||
}
|
||||
|
||||
private void DeletePublishOutput(IISDeploymentResult deploymentResult)
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(deploymentResult.ContentRoot, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
// Out of process module dll is allowed to be locked
|
||||
var name = Path.GetFileName(file);
|
||||
if (name == "aspnetcore.dll" || name == "aspnetcorev2.dll" || name == "aspnetcorev2_outofprocess.dll")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -43,25 +43,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
|||
[ConditionalFact]
|
||||
public async Task GetServerVariableDoesNotCrash()
|
||||
{
|
||||
async Task RunRequests()
|
||||
{
|
||||
var client = new HttpClient() { BaseAddress = _fixture.Client.BaseAddress };
|
||||
|
||||
for (int j = 0; j < 10; j++)
|
||||
{
|
||||
var response = await client.GetStringAsync("/GetServerVariableStress");
|
||||
Assert.StartsWith("Response Begin", response);
|
||||
Assert.EndsWith("Response End", response);
|
||||
}
|
||||
}
|
||||
|
||||
List<Task> tasks = new List<Task>();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tasks.Add(Task.Run(RunRequests));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await Helpers.StressLoad(_fixture.Client, "/GetServerVariableStress", response => {
|
||||
var text = response.Content.ReadAsStringAsync().Result;
|
||||
Assert.StartsWith("Response Begin", text);
|
||||
Assert.EndsWith("Response End", text);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
||||
|
|
@ -51,5 +52,27 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
|||
|
||||
Assert.Equal("Hello World", responseText);
|
||||
}
|
||||
|
||||
public static async Task StressLoad(HttpClient httpClient, string path, Action<HttpResponseMessage> action)
|
||||
{
|
||||
async Task RunRequests()
|
||||
{
|
||||
var connection = new HttpClient() { BaseAddress = httpClient.BaseAddress };
|
||||
|
||||
for (int j = 0; j < 10; j++)
|
||||
{
|
||||
var response = await connection.GetAsync(path);
|
||||
action(response);
|
||||
}
|
||||
}
|
||||
|
||||
List<Task> tasks = new List<Task>();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tasks.Add(Task.Run(RunRequests));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
|||
None = 0,
|
||||
Websockets = 1,
|
||||
WindowsAuthentication = 2,
|
||||
PoolEnvironmentVariables = 4
|
||||
PoolEnvironmentVariables = 4,
|
||||
ShutdownToken = 8
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,159 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests;
|
||||
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.Inprocess
|
||||
{
|
||||
public class AppOfflineTests : IISFunctionalTestBase
|
||||
{
|
||||
// TODO these will differ between IIS and IISExpress
|
||||
[ConditionalTheory]
|
||||
[InlineData(HostingModel.InProcess)]
|
||||
[InlineData(HostingModel.OutOfProcess)]
|
||||
public async Task AppOfflineDroppedWhileSiteIsDown_SiteReturns503(HostingModel hostingModel)
|
||||
{
|
||||
var deploymentResult = await DeployApp(hostingModel);
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
await AssertAppOffline(deploymentResult);
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[InlineData(HostingModel.InProcess)]
|
||||
[InlineData(HostingModel.OutOfProcess)]
|
||||
public async Task AppOfflineDroppedWhileSiteIsDown_CustomResponse(HostingModel hostingModel)
|
||||
{
|
||||
var expectedResponse = "The app is offline.";
|
||||
var deploymentResult = await DeployApp(hostingModel);
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot, expectedResponse);
|
||||
|
||||
await AssertAppOffline(deploymentResult, expectedResponse);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess()
|
||||
{
|
||||
var deploymentResult = await AssertStarts(HostingModel.InProcess);
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
await AssertStopsProcess(deploymentResult);
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_OutOfProcess()
|
||||
{
|
||||
var deploymentResult = await AssertStarts(HostingModel.OutOfProcess);
|
||||
|
||||
// Repeat dropping file and restarting multiple times
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
await AssertAppOffline(deploymentResult);
|
||||
RemoveAppOffline(deploymentResult.ContentRoot);
|
||||
await AssertRunning(deploymentResult);
|
||||
}
|
||||
}
|
||||
|
||||
[ConditionalTheory]
|
||||
[InlineData(HostingModel.InProcess)]
|
||||
[InlineData(HostingModel.OutOfProcess)]
|
||||
public async Task AppOfflineDropped_CanRemoveAppOfflineAfterAddingAndSiteWorks(HostingModel hostingModel)
|
||||
{
|
||||
var deploymentResult = await DeployApp(hostingModel);
|
||||
|
||||
AddAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
await AssertAppOffline(deploymentResult);
|
||||
|
||||
RemoveAppOffline(deploymentResult.ContentRoot);
|
||||
|
||||
await AssertRunning(deploymentResult);
|
||||
}
|
||||
|
||||
private async Task<IISDeploymentResult> DeployApp(HostingModel hostingModel = HostingModel.InProcess)
|
||||
{
|
||||
var deploymentParameters = Helpers.GetBaseDeploymentParameters(hostingModel: hostingModel, publish: true);
|
||||
|
||||
return await DeployAsync(deploymentParameters);
|
||||
}
|
||||
|
||||
private void AddAppOffline(string appPath, string content = "")
|
||||
{
|
||||
File.WriteAllText(Path.Combine(appPath, "app_offline.htm"), content);
|
||||
}
|
||||
|
||||
private void RemoveAppOffline(string appPath)
|
||||
{
|
||||
RetryHelper.RetryOperation(
|
||||
() => File.Delete(Path.Combine(appPath, "app_offline.htm")),
|
||||
e => Logger.LogError($"Failed to remove app_offline : {e.Message}"),
|
||||
retryCount: 3,
|
||||
retryDelayMilliseconds: 100);
|
||||
}
|
||||
|
||||
private async Task AssertAppOffline(IISDeploymentResult deploymentResult, string expectedResponse = "")
|
||||
{
|
||||
var response = await deploymentResult.HttpClient.GetAsync("HelloWorld");
|
||||
|
||||
for (var i = 0; response.IsSuccessStatusCode && i < 5; i++)
|
||||
{
|
||||
// Keep retrying until app_offline is present.
|
||||
response = await deploymentResult.HttpClient.GetAsync("HelloWorld");
|
||||
}
|
||||
|
||||
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
||||
|
||||
Assert.Equal(expectedResponse, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
private async Task AssertStopsProcess(IISDeploymentResult deploymentResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld");
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
// dropping app_offline will kill the process
|
||||
}
|
||||
|
||||
var hostShutdownToken = deploymentResult.HostShutdownToken;
|
||||
|
||||
Assert.True(hostShutdownToken.WaitHandle.WaitOne(TimeoutExtensions.DefaultTimeout));
|
||||
Assert.True(hostShutdownToken.IsCancellationRequested);
|
||||
}
|
||||
|
||||
private async Task<IISDeploymentResult> AssertStarts(HostingModel hostingModel)
|
||||
{
|
||||
var deploymentResult = await DeployApp(hostingModel);
|
||||
|
||||
await AssertRunning(deploymentResult);
|
||||
|
||||
return deploymentResult;
|
||||
}
|
||||
|
||||
private static async Task AssertRunning(IISDeploymentResult deploymentResult)
|
||||
{
|
||||
var response = await deploymentResult.RetryingHttpClient.GetAsync("HelloWorld");
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal("Hello World", responseText);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue