Skip to content

Implements shadow copying for ASP.NET Core + IIS #28357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 20, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class AppOfflineHandler: public REQUEST_HANDLER
public:
AppOfflineHandler(IHttpContext& pContext, const std::string& appOfflineContent)
: REQUEST_HANDLER(pContext),
m_pContext(pContext),
m_strAppOfflineContent(appOfflineContent)
m_pContext(pContext),
m_strAppOfflineContent(appOfflineContent)
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ class ApplicationFactory
HRESULT Execute(
_In_ IHttpServer *pServer,
_In_ IHttpContext *pHttpContext,
_In_ std::wstring& shadowCopyDirectory,
_Outptr_ IAPPLICATION **pApplication) const
{
// m_location.data() is const ptr copy to local to get mutable pointer
auto location = m_location;
std::array<APPLICATION_PARAMETER, 3> parameters {
std::array<APPLICATION_PARAMETER, 4> parameters {
{
{"InProcessExeLocation", location.data()},
{"TraceContext", pHttpContext->GetTraceContext()},
{"Site", pHttpContext->GetSite()}
{"Site", pHttpContext->GetSite()},
{"ShadowCopyDirectory", shadowCopyDirectory.data()}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ HandlerResolver::HandlerResolver(HMODULE hModule, const IHttpServer &pServer)

HRESULT
HandlerResolver::LoadRequestHandlerAssembly(const IHttpApplication &pApplication,
const std::filesystem::path& shadowCopyPath,
const ShimOptions& pConfiguration,
std::unique_ptr<ApplicationFactory>& pApplicationFactory,
ErrorContext& errorContext)
Expand Down Expand Up @@ -62,7 +63,7 @@ HandlerResolver::LoadRequestHandlerAssembly(const IHttpApplication &pApplication
RETURN_IF_FAILED(HostFxrResolutionResult::Create(
L"",
pConfiguration.QueryProcessPath(),
pApplication.GetApplicationPhysicalPath(),
shadowCopyPath.empty() ? pApplication.GetApplicationPhysicalPath() : shadowCopyPath,
pConfiguration.QueryArguments(),
errorContext,
options));
Expand Down Expand Up @@ -125,7 +126,7 @@ HandlerResolver::LoadRequestHandlerAssembly(const IHttpApplication &pApplication
}

HRESULT
HandlerResolver::GetApplicationFactory(const IHttpApplication& pApplication, std::unique_ptr<ApplicationFactory>& pApplicationFactory, const ShimOptions& options, ErrorContext& errorContext)
HandlerResolver::GetApplicationFactory(const IHttpApplication& pApplication, const std::filesystem::path& shadowCopyPath, std::unique_ptr<ApplicationFactory>& pApplicationFactory, const ShimOptions& options, ErrorContext& errorContext)
{
SRWExclusiveLock lock(m_requestHandlerLoadLock);
if (m_loadedApplicationHostingModel != HOSTING_UNKNOWN)
Expand Down Expand Up @@ -168,7 +169,7 @@ HandlerResolver::GetApplicationFactory(const IHttpApplication& pApplication, std

m_loadedApplicationHostingModel = options.QueryHostingModel();
m_loadedApplicationId = pApplication.GetApplicationId();
RETURN_IF_FAILED(LoadRequestHandlerAssembly(pApplication, options, pApplicationFactory, errorContext));
RETURN_IF_FAILED(LoadRequestHandlerAssembly(pApplication, shadowCopyPath, options, pApplicationFactory, errorContext));

return S_OK;
}
Expand All @@ -181,6 +182,13 @@ void HandlerResolver::ResetHostingModel()
m_loadedApplicationId.resize(0);
}

APP_HOSTING_MODEL HandlerResolver::GetHostingModel()
{
SRWExclusiveLock lock(m_requestHandlerLoadLock);

return m_loadedApplicationHostingModel;
}

HRESULT
HandlerResolver::FindNativeAssemblyFromGlobalLocation(
const ShimOptions& pConfiguration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ class HandlerResolver
{
public:
HandlerResolver(HMODULE hModule, const IHttpServer &pServer);
HRESULT GetApplicationFactory(const IHttpApplication &pApplication, std::unique_ptr<ApplicationFactory>& pApplicationFactory, const ShimOptions& options, ErrorContext& errorContext);
HRESULT GetApplicationFactory(const IHttpApplication &pApplication, const std::filesystem::path& shadowCopyPath, std::unique_ptr<ApplicationFactory>& pApplicationFactory, const ShimOptions& options, ErrorContext& errorContext);
void ResetHostingModel();
APP_HOSTING_MODEL GetHostingModel();

private:
HRESULT LoadRequestHandlerAssembly(const IHttpApplication &pApplication, const ShimOptions& pConfiguration, std::unique_ptr<ApplicationFactory>& pApplicationFactory, ErrorContext& errorContext);
HRESULT LoadRequestHandlerAssembly(const IHttpApplication &pApplication, const std::filesystem::path& shadowCopyPath, const ShimOptions& pConfiguration, std::unique_ptr<ApplicationFactory>& pApplicationFactory, ErrorContext& errorContext);
HRESULT FindNativeAssemblyFromGlobalLocation(const ShimOptions& pConfiguration, PCWSTR libraryName, std::wstring& handlerDllPath);
HRESULT FindNativeAssemblyFromHostfxr(
const HostFxrResolutionResult& hostfxrOptions,
Expand Down
15 changes: 14 additions & 1 deletion src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
#include "Environment.h"

#define CS_ASPNETCORE_HANDLER_VERSION L"handlerVersion"
#define CS_ASPNETCORE_SHADOW_COPY L"experimentalEnableShadowCopy"
#define CS_ASPNETCORE_SHADOW_COPY_DIRECTORY L"shadowCopyDirectory"
#define CS_ASPNETCORE_CLEAN_SHADOW_DIRECTORY_CONTENT L"cleanShadowCopyDirectory"

ShimOptions::ShimOptions(const ConfigurationSource &configurationSource) :
m_hostingModel(HOSTING_UNKNOWN),
Expand All @@ -31,12 +34,22 @@ ShimOptions::ShimOptions(const ConfigurationSource &configurationSource) :
"or hostingModel=\"outofprocess\" in the web.config file.", hostingModel.c_str()));
}

const auto handlerSettings = section->GetKeyValuePairs(CS_ASPNETCORE_HANDLER_SETTINGS);

if (m_hostingModel == HOSTING_OUT_PROCESS)
{
const auto handlerSettings = section->GetKeyValuePairs(CS_ASPNETCORE_HANDLER_SETTINGS);
m_strHandlerVersion = find_element(handlerSettings, CS_ASPNETCORE_HANDLER_VERSION).value_or(std::wstring());
}

auto experimentalEnableShadowCopyElement = find_element(handlerSettings, CS_ASPNETCORE_SHADOW_COPY).value_or(std::wstring());
m_fexperimentalEnableShadowCopying = equals_ignore_case(L"true", experimentalEnableShadowCopyElement);

auto cleanShadowCopyDirectory = find_element(handlerSettings, CS_ASPNETCORE_CLEAN_SHADOW_DIRECTORY_CONTENT).value_or(std::wstring());
m_fCleanShadowCopyDirectory = equals_ignore_case(L"true", cleanShadowCopyDirectory);

m_strShadowCopyingDirectory = find_element(handlerSettings, CS_ASPNETCORE_SHADOW_COPY_DIRECTORY)
.value_or(m_fexperimentalEnableShadowCopying ? L"ShadowCopyDirectory" : std::wstring());

m_strProcessPath = section->GetRequiredString(CS_ASPNETCORE_PROCESS_EXE_PATH);
m_strArguments = section->GetString(CS_ASPNETCORE_PROCESS_ARGUMENTS).value_or(CS_ASPNETCORE_PROCESS_ARGUMENTS_DEFAULT);
m_fStdoutLogEnabled = section->GetRequiredBool(CS_ASPNETCORE_STDOUT_LOG_ENABLED);
Expand Down
21 changes: 21 additions & 0 deletions src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ class ShimOptions: NonCopyable
return m_fShowDetailedErrors;
}

bool
QueryShadowCopyEnabled() const noexcept
{
return m_fexperimentalEnableShadowCopying;
}

bool
QueryCleanShadowCopyDirectory() const noexcept
{
return m_fCleanShadowCopyDirectory;
}

const std::wstring&
QueryShadowCopyDirectory() const noexcept
{
return m_strShadowCopyingDirectory;
}

ShimOptions(const ConfigurationSource &configurationSource);

private:
Expand All @@ -76,4 +94,7 @@ class ShimOptions: NonCopyable
bool m_fStdoutLogEnabled;
bool m_fDisableStartupPage;
bool m_fShowDetailedErrors;
bool m_fexperimentalEnableShadowCopying;
bool m_fCleanShadowCopyDirectory;
std::wstring m_strShadowCopyingDirectory;
};
105 changes: 98 additions & 7 deletions src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "file_utility.h"

extern HINSTANCE g_hServerModule;
extern BOOL g_fInAppOfflineShutdown;

HRESULT
APPLICATION_INFO::CreateHandler(
Expand Down Expand Up @@ -49,7 +50,6 @@ APPLICATION_INFO::CreateHandler(
while (hr != S_OK)
{
// At this point application is either null or shutdown and is returning S_FALSE

if (m_pApplication != nullptr)
{
LOG_INFO(L"Application went offline");
Expand Down Expand Up @@ -80,11 +80,25 @@ APPLICATION_INFO::CreateApplication(IHttpContext& pHttpContext)

return S_OK;
}

try
{
const WebConfigConfigurationSource configurationSource(m_pServer.GetAdminManager(), pHttpApplication);
ShimOptions options(configurationSource);

if (g_fInAppOfflineShutdown)
{
m_pApplication = make_application<ServerErrorApplication>(
pHttpApplication,
E_FAIL,
options.QueryDisableStartupPage() /* disableStartupPage */,
"" /* responseContent */,
503i16 /* statusCode */,
0i16 /* subStatusCode */,
"Application Shutting Down");
return S_OK;
}

ErrorContext errorContext;
errorContext.statusCode = 500i16;
errorContext.subStatusCode = 0i16;
Expand Down Expand Up @@ -130,6 +144,7 @@ APPLICATION_INFO::CreateApplication(IHttpContext& pHttpContext)
}
catch (...)
{
OBSERVE_CAUGHT_EXCEPTION();
EventLog::Error(
ASPNETCORE_CONFIGURATION_LOAD_ERROR,
ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG,
Expand Down Expand Up @@ -175,13 +190,17 @@ APPLICATION_INFO::TryCreateApplication(IHttpContext& pHttpContext, const ShimOpt
}
}

RETURN_IF_FAILED(m_handlerResolver.GetApplicationFactory(*pHttpContext.GetApplication(), m_pApplicationFactory, options, error));
auto shadowCopyPath = HandleShadowCopy(options, pHttpContext);

RETURN_IF_FAILED(m_handlerResolver.GetApplicationFactory(*pHttpContext.GetApplication(), shadowCopyPath, m_pApplicationFactory, options, error));
LOG_INFO(L"Creating handler application");

IAPPLICATION * newApplication;
std::wstring shadowCopyWstring = shadowCopyPath.wstring();
RETURN_IF_FAILED(m_pApplicationFactory->Execute(
&m_pServer,
&pHttpContext,
shadowCopyWstring,
&newApplication));

m_pApplication.reset(newApplication);
Expand All @@ -206,19 +225,91 @@ APPLICATION_INFO::TryCreateHandler(
return S_OK;
}
}

return S_FALSE;
}

VOID
APPLICATION_INFO::ShutDownApplication(const bool fServerInitiated)
{
IAPPLICATION* app = nullptr;
{
SRWExclusiveLock lock(m_applicationLock);
if (!m_pApplication)
{
return;
}
app = m_pApplication.get();
}

LOG_INFOF(L"Stopping application '%ls'", QueryApplicationInfoKey().c_str());
app->Stop(fServerInitiated);

SRWExclusiveLock lock(m_applicationLock);

if (m_pApplication)
m_pApplication = nullptr;
m_pApplicationFactory = nullptr;
}

std::filesystem::path
APPLICATION_INFO::HandleShadowCopy(const ShimOptions& options, IHttpContext& pHttpContext)
{
std::filesystem::path shadowCopyPath;

// Only support shadow copying for IIS.
if (options.QueryShadowCopyEnabled() && !m_pServer.IsCommandLineLaunch())
{
LOG_INFOF(L"Stopping application '%ls'", QueryApplicationInfoKey().c_str());
m_pApplication->Stop(fServerInitiated);
m_pApplication = nullptr;
m_pApplicationFactory = nullptr;
shadowCopyPath = options.QueryShadowCopyDirectory();
std::wstring physicalPath = pHttpContext.GetApplication()->GetApplicationPhysicalPath();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: auto or PCWSTR to avoid the alloc


// Make shadow copy path absolute.
if (!shadowCopyPath.is_absolute())
{
shadowCopyPath = std::filesystem::absolute(std::filesystem::path(physicalPath) / shadowCopyPath);
}

// The shadow copy directory itself isn't copied to directly.
// Instead subdirectories with numerically increasing names are created.
// This is because on shutdown, the app itself will still have all dlls loaded,
// meaning we can't copy to the same subdirectory. Therefore, on shutdown,
// we create a directory that is one larger than the previous largest directory number.
auto directoryName = 0;
std::string directoryNameStr = "0";
auto shadowCopyBaseDirectory = std::filesystem::directory_entry(shadowCopyPath);
if (!shadowCopyBaseDirectory.exists())
{
CreateDirectory(shadowCopyBaseDirectory.path().wstring().c_str(), NULL);
}

for (auto& entry : std::filesystem::directory_iterator(shadowCopyPath))
{
if (entry.is_directory())
{
try
{
auto tempDirName = entry.path().filename().string();
int intFileName = std::stoi(tempDirName);
if (intFileName > directoryName)
{
directoryName = intFileName;
directoryNameStr = tempDirName;
}
}
catch (...)
{
OBSERVE_CAUGHT_EXCEPTION();
// Ignore any folders that can't be converted to an int.
}
}
}

shadowCopyPath = shadowCopyPath / directoryNameStr;
HRESULT hr = Environment::CopyToDirectory(physicalPath, shadowCopyPath, options.QueryCleanShadowCopyDirectory(), std::filesystem::canonical(shadowCopyBaseDirectory.path()));
if (hr != S_OK)
{
return std::wstring();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the behavior we want? Just fallback to non-shadow copying?
Does this cause issues when the next file changes happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think so, the file watcher wouldn't look for dll changes if the directory is empty.

}
}

return shadowCopyPath;
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class APPLICATION_INFO: NonCopyable
HRESULT
TryCreateApplication(IHttpContext& pHttpContext, const ShimOptions& options, ErrorContext& error);

std::filesystem::path
HandleShadowCopy(const ShimOptions& options, IHttpContext& pHttpContext);

IHttpServer &m_pServer;
HandlerResolver &m_handlerResolver;

Expand Down
Loading