diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.h b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.h index 33afc803bc87..e7da69eff2f6 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/AppOfflineHandler.h @@ -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) { } diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ApplicationFactory.h b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ApplicationFactory.h index 23c5a67d409f..9906251f532f 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ApplicationFactory.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ApplicationFactory.h @@ -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 parameters { + std::array parameters { { {"InProcessExeLocation", location.data()}, {"TraceContext", pHttpContext->GetTraceContext()}, - {"Site", pHttpContext->GetSite()} + {"Site", pHttpContext->GetSite()}, + {"ShadowCopyDirectory", shadowCopyDirectory.data()} } }; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp index 2a1303bcdf2f..03096d658699 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp @@ -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& pApplicationFactory, ErrorContext& errorContext) @@ -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)); @@ -125,7 +126,7 @@ HandlerResolver::LoadRequestHandlerAssembly(const IHttpApplication &pApplication } HRESULT -HandlerResolver::GetApplicationFactory(const IHttpApplication& pApplication, std::unique_ptr& pApplicationFactory, const ShimOptions& options, ErrorContext& errorContext) +HandlerResolver::GetApplicationFactory(const IHttpApplication& pApplication, const std::filesystem::path& shadowCopyPath, std::unique_ptr& pApplicationFactory, const ShimOptions& options, ErrorContext& errorContext) { SRWExclusiveLock lock(m_requestHandlerLoadLock); if (m_loadedApplicationHostingModel != HOSTING_UNKNOWN) @@ -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; } @@ -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, diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h index 59e1b01ac63b..4ca4d231d950 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/HandlerResolver.h @@ -15,11 +15,12 @@ class HandlerResolver { public: HandlerResolver(HMODULE hModule, const IHttpServer &pServer); - HRESULT GetApplicationFactory(const IHttpApplication &pApplication, std::unique_ptr& pApplicationFactory, const ShimOptions& options, ErrorContext& errorContext); + HRESULT GetApplicationFactory(const IHttpApplication &pApplication, const std::filesystem::path& shadowCopyPath, std::unique_ptr& pApplicationFactory, const ShimOptions& options, ErrorContext& errorContext); void ResetHostingModel(); + APP_HOSTING_MODEL GetHostingModel(); private: - HRESULT LoadRequestHandlerAssembly(const IHttpApplication &pApplication, const ShimOptions& pConfiguration, std::unique_ptr& pApplicationFactory, ErrorContext& errorContext); + HRESULT LoadRequestHandlerAssembly(const IHttpApplication &pApplication, const std::filesystem::path& shadowCopyPath, const ShimOptions& pConfiguration, std::unique_ptr& pApplicationFactory, ErrorContext& errorContext); HRESULT FindNativeAssemblyFromGlobalLocation(const ShimOptions& pConfiguration, PCWSTR libraryName, std::wstring& handlerDllPath); HRESULT FindNativeAssemblyFromHostfxr( const HostFxrResolutionResult& hostfxrOptions, diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.cpp b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.cpp index 353b0be78866..c43b2c62a0c2 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.cpp @@ -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), @@ -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); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.h b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.h index 5d1c04f8ab07..9674e9a0244f 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/ShimOptions.h @@ -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: @@ -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; }; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp index 994061cadcfb..043e59f08743 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.cpp @@ -18,6 +18,7 @@ #include "file_utility.h" extern HINSTANCE g_hServerModule; +extern BOOL g_fInAppOfflineShutdown; HRESULT APPLICATION_INFO::CreateHandler( @@ -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"); @@ -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( + 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; @@ -130,6 +144,7 @@ APPLICATION_INFO::CreateApplication(IHttpContext& pHttpContext) } catch (...) { + OBSERVE_CAUGHT_EXCEPTION(); EventLog::Error( ASPNETCORE_CONFIGURATION_LOAD_ERROR, ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG, @@ -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); @@ -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(); + + // 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(); + } } + + return shadowCopyPath; } diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.h b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.h index e16eae191337..a54107eb8bee 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationinfo.h @@ -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; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp index d940b39302b7..7bc3bd2599dd 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/applicationmanager.cpp @@ -10,6 +10,7 @@ #include "EventLog.h" extern BOOL g_fInShutdown; +extern BOOL g_fInAppOfflineShutdown; // // Retrieves the application info from the application manager @@ -108,14 +109,33 @@ APPLICATION_MANAGER::RecycleApplicationFromManager( if (itr->second->ConfigurationPathApplies(configurationPath)) { applicationsToRecycle.emplace_back(itr->second); - itr = m_pApplicationInfoHash.erase(itr); + // Delay deleting an in-process app until after shutting the application down to avoid creating + // another application info, which would just return app_offline. + if (m_handlerResolver.GetHostingModel() == APP_HOSTING_MODEL::HOSTING_IN_PROCESS) + { + ++itr; + } + else + { + itr = m_pApplicationInfoHash.erase(itr); + } } else { ++itr; + } } + if (m_handlerResolver.GetHostingModel() == APP_HOSTING_MODEL::HOSTING_IN_PROCESS) + { + // For detecting app_offline when the app_offline file isn't present. + // Normally, app_offline state is independent of application + // (Just checks for app_offline file). + // For shadow copying, we need some other indication that the app is offline. + g_fInAppOfflineShutdown = true; + } + // All applications were unloaded reset handler resolver validation logic if (m_pApplicationInfoHash.empty()) { @@ -155,6 +175,26 @@ APPLICATION_MANAGER::RecycleApplicationFromManager( } } } + { + SRWExclusiveLock lock(m_srwLock); + const std::wstring configurationPath = pszApplicationId; + + // Remove apps after calling shutdown on each of them + // This is exclusive to in-process, as the shutdown of an inprocess app recycles + // the entire worker process. + auto itr = m_pApplicationInfoHash.begin(); + while (itr != m_pApplicationInfoHash.end()) + { + if (itr->second != nullptr && itr->second->ConfigurationPathApplies(configurationPath)) + { + itr = m_pApplicationInfoHash.erase(itr); + } + else + { + ++itr; + } + } + } } CATCH_RETURN() @@ -172,6 +212,7 @@ APPLICATION_MANAGER::ShutDown() // However, it is possible to receive multiple OnGlobalStopListening events // Protect against this by checking if we already shut down. g_fInShutdown = TRUE; + g_fInAppOfflineShutdown = true; // During shutdown we lock until we delete the application SRWExclusiveLock lock(m_srwLock); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/dllmain.cpp b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/dllmain.cpp index b3a98f0c7e3e..acf90e07c622 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/dllmain.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/dllmain.cpp @@ -17,6 +17,7 @@ DECLARE_DEBUG_PRINT_OBJECT("aspnetcorev2.dll"); HANDLE g_hEventLog = nullptr; BOOL g_fRecycleProcessCalled = FALSE; BOOL g_fInShutdown = FALSE; +BOOL g_fInAppOfflineShutdown = FALSE; HINSTANCE g_hServerModule; VOID diff --git a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp index e3a435bc9b87..162c0fea907b 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/AspNetCore/proxymodule.cpp @@ -100,7 +100,12 @@ ASPNET_CORE_PROXY_MODULE::OnExecuteRequestHandler( *pHttpContext, m_pApplicationInfo)); - FINISHED_IF_FAILED(m_pApplicationInfo->CreateHandler(*pHttpContext, m_pHandler)); + FINISHED_IF_FAILED(hr = m_pApplicationInfo->CreateHandler(*pHttpContext, m_pHandler)); + + if (m_pHandler == nullptr) + { + FINISHED(HRESULT_FROM_WIN32(ERROR_SERVER_SHUTDOWN_IN_PROGRESS)); + } SetupDisconnectHandler(pHttpContext); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp index fb63cdb20c99..95fa1bce3c61 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.cpp @@ -151,3 +151,89 @@ bool Environment::IsRunning64BitProcess() GetNativeSystemInfo(&systemInfo); return systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64; } + +HRESULT Environment::CopyToDirectory(const std::wstring& source, const std::filesystem::path& destination, bool cleanDest, const std::filesystem::path& directoryToIgnore) +{ + if (cleanDest && std::filesystem::exists(destination)) + { + std::filesystem::remove_all(destination); + } + + Environment::CopyToDirectoryInner(source, destination, directoryToIgnore); + return S_OK; +} + +void Environment::CopyToDirectoryInner(const std::filesystem::path& source, const std::filesystem::path& destination, const std::filesystem::path& directoryToIgnore) +{ + auto destinationDirEntry = std::filesystem::directory_entry(destination); + if (!destinationDirEntry.exists()) + { + CreateDirectory(destination.wstring().c_str(), NULL); + } + + for (auto& path : std::filesystem::directory_iterator(source)) + { + if (path.is_regular_file()) + { + auto sourceFile = path.path().filename(); + auto destinationPath = (destination / sourceFile); + + if (std::filesystem::directory_entry(destinationPath).exists()) + { + auto sourceFileTime = std::filesystem::last_write_time(path); + auto destinationFileTime = std::filesystem::last_write_time(destinationPath); + if (sourceFileTime <= destinationFileTime) // file write time is the same + { + continue; + } + } + + CopyFile(path.path().wstring().c_str(), destinationPath.wstring().c_str(), FALSE); + } + else if (path.is_directory()) + { + auto sourceInnerDirectory = path.path(); + + // Make sure we aren't navigating into shadow copy directory. + if (sourceInnerDirectory.wstring().rfind(directoryToIgnore, 0) != 0) + { + CopyToDirectoryInner(path.path(), destination / path.path().filename(), directoryToIgnore); + } + } + } +} + +bool Environment::CheckUpToDate(const std::wstring& source, const std::filesystem::path& destination, const std::wstring& extension, const std::filesystem::path& directoryToIgnore) +{ + for (auto& path : std::filesystem::directory_iterator(source)) + { + if (path.is_regular_file() + && path.path().has_extension() + && path.path().filename().extension().wstring() == extension) + { + auto sourceFile = path.path().filename(); + auto destinationPath = (destination / sourceFile); + + if (std::filesystem::directory_entry(destinationPath).exists()) + { + auto originalFileTime = std::filesystem::last_write_time(path); + auto destFileTime = std::filesystem::last_write_time(destinationPath); + if (originalFileTime > destFileTime) // file write time is the same + { + return false; + } + } + + CopyFile(path.path().wstring().c_str(), destinationPath.wstring().c_str(), FALSE); + } + else if (path.is_directory()) + { + auto sourceInnerDirectory = std::filesystem::directory_entry(path); + if (sourceInnerDirectory.path() != directoryToIgnore) + { + CheckUpToDate(destination / path.path().filename(), path.path(), extension, directoryToIgnore); + } + } + } + return true; +} diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h index 4f2611ef71f2..a0a4d4013c42 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/Environment.h @@ -22,5 +22,12 @@ class Environment std::wstring GetDllDirectoryValue(); static bool IsRunning64BitProcess(); + static + HRESULT CopyToDirectory(const std::wstring& source, const std::filesystem::path& destination, bool cleanDest, const std::filesystem::path& directoryToIgnore); + static + bool CheckUpToDate(const std::wstring& source, const std::filesystem::path& destination, const std::wstring& extension, const std::filesystem::path& directoryToIgnore); +private: + static + void CopyToDirectoryInner(const std::filesystem::path& source_folder, const std::filesystem::path& target_folder, const std::filesystem::path& directoryToIgnore); }; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h index a6cf2bb889ac..5bb696da311c 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/HostFxr.h @@ -17,6 +17,7 @@ struct hostfxr_initialize_parameters #define DOTNETCORE_STARTUP_HOOK L"STARTUP_HOOKS" #define DOTNETCORE_USE_ENTRYPOINT_FILTER L"USE_ENTRYPOINT_FILTER" #define DOTNETCORE_STACK_SIZE L"DEFAULT_STACK_SIZE" +#define APP_CONTEXT_BASE_DIRECTORY L"APP_CONTEXT_BASE_DIRECTORY" #define ASPNETCORE_STARTUP_ASSEMBLY L"Microsoft.AspNetCore.Server.IIS" typedef INT(*hostfxr_get_native_search_directories_fn) (INT argc, CONST PCWSTR* argv, PWSTR buffer, DWORD buffer_size, DWORD* required_buffer_size); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.cpp index cf2395e7b585..9353e427cf77 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/PollingAppOfflineApplication.cpp @@ -11,6 +11,7 @@ HRESULT PollingAppOfflineApplication::TryCreateHandler(_In_ IHttpContext* pHttpContext, _Outptr_result_maybenull_ IREQUEST_HANDLER** pRequestHandler) { CheckAppOffline(); + return LOG_IF_FAILED(APPLICATION::TryCreateHandler(pHttpContext, pRequestHandler)); } diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/application.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/application.h index ae30844330cc..742ee81a97fd 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/application.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/application.h @@ -129,11 +129,11 @@ class APPLICATION : public IAPPLICATION SRWLOCK m_stopLock{}; SRWLOCK m_dataLock {}; bool m_fStopCalled; + std::wstring m_applicationPhysicalPath; private: mutable LONG m_cRefs; - std::wstring m_applicationPhysicalPath; std::wstring m_applicationVirtualPath; std::wstring m_applicationConfigPath; std::wstring m_applicationId; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/aspnetcore_msg.mc b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/aspnetcore_msg.mc index 20d04bd56bf6..ace6bb1b98be 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/aspnetcore_msg.mc +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/aspnetcore_msg.mc @@ -207,6 +207,12 @@ Language=English %1 . +Messageid=1037 +SymbolicName=ASPNETCORE_EVENT_RECYCLE_FILECHANGE +Language=English +%1 +. + ; ;#endif // _ASPNETCORE_MODULE_MSG_H_ diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/resources.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/resources.h index 9535d1c0cbd8..a561fa50c458 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/resources.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/resources.h @@ -33,6 +33,7 @@ #define ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT_MSG L"Application '%s' with physical root '%s' has exited from Program.Main with exit code = '%d'. First 30KB characters of captured stdout and stderr logs:\r\n%s" #define ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_MSG L"Application '%s' with physical root '%s' has exited from Program.Main with exit code = '%d'. Please check the stderr logs for more information." #define ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG L"Application '%s' was recycled after detecting app_offline.htm." +#define ASPNETCORE_EVENT_RECYCLE_FILECHANGE_MSG L"Application '%s' was recycled after detecting file change in application directory." #define ASPNETCORE_EVENT_MONITOR_APPOFFLINE_ERROR_MSG L"Failed to monitor app_offline.htm for application '%s', ErrorCode '0x%x'. " #define ASPNETCORE_EVENT_RECYCLE_CONFIGURATION_MSG L"Application '%s' was recycled due to configuration change" #define ASPNETCORE_EVENT_RECYCLE_FAILURE_CONFIGURATION_MSG L"Failed to recycle application after a configuration change at '%s'. Recycling worker process." diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/sttimer.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/sttimer.h index 26d79d073781..86b62b6fcf2a 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/sttimer.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/sttimer.h @@ -118,13 +118,11 @@ class STTIMER VOID CALLBACK TimerCallback( - _In_ PTP_CALLBACK_INSTANCE Instance, + _In_ PTP_CALLBACK_INSTANCE , _In_ PVOID Context, - _In_ PTP_TIMER Timer + _In_ PTP_TIMER ) { - Instance; - Timer; STRU* pstruLogFilePath = (STRU*)Context; HANDLE hStdoutHandle = NULL; SECURITY_ATTRIBUTES saAttr = { 0 }; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp index fb9fe8a01502..b876d6dc2656 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp @@ -29,6 +29,7 @@ InProcessApplicationBase::StopInternal(bool fServerInitiated) // We don't actually handle any shutdown logic here. // Instead, we notify IIS that the process needs to be recycled, which will call // ApplicationManager->Shutdown(). This will call shutdown on the application. + LOG_INFO(L"AspNetCore InProcess Recycle Process on Demand"); m_pHttpServer.RecycleProcess(L"AspNetCore InProcess Recycle Process on Demand"); } else diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index 8643f8089aae..c94236af42e6 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -14,7 +14,7 @@ #include "Environment.h" #include "HostFxr.h" -IN_PROCESS_APPLICATION* IN_PROCESS_APPLICATION::s_Application = NULL; +IN_PROCESS_APPLICATION* IN_PROCESS_APPLICATION::s_Application = NULL; IN_PROCESS_APPLICATION::IN_PROCESS_APPLICATION( IHttpServer& pHttpServer, @@ -22,7 +22,8 @@ IN_PROCESS_APPLICATION::IN_PROCESS_APPLICATION( std::unique_ptr pConfig, APPLICATION_PARAMETER* pParameters, DWORD nParameters) : - InProcessApplicationBase(pHttpServer, pApplication), + InProcessApplicationBase(pHttpServer, + pApplication), m_Initialized(false), m_blockManagedCallbacks(true), m_waitForShutdown(true), @@ -37,6 +38,14 @@ IN_PROCESS_APPLICATION::IN_PROCESS_APPLICATION( m_dotnetExeKnownLocation = knownLocation; } + const auto shadowCopyDirectory = FindParameter(s_shadowCopyDirectoryName, pParameters, nParameters); + if (shadowCopyDirectory != nullptr) + { + m_shadowCopyDirectory = shadowCopyDirectory; + } + + m_shutdownTimeout = m_pConfig.get()->QueryShutdownTimeLimitInMS(); + m_stringRedirectionOutput = std::make_shared(); } @@ -48,6 +57,9 @@ IN_PROCESS_APPLICATION::~IN_PROCESS_APPLICATION() VOID IN_PROCESS_APPLICATION::StopInternal(bool fServerInitiated) { + // Stop app offline tracking before shutting down CLR. + // This is to help with shadow copy scenario where the app is shutting down. + AppOfflineTrackingApplication::StopInternal(fServerInitiated); StopClr(); InProcessApplicationBase::StopInternal(fServerInitiated); } @@ -58,7 +70,19 @@ IN_PROCESS_APPLICATION::StopClr() // This has the state lock around it. LOG_INFO(L"Stopping CLR"); - if (!m_blockManagedCallbacks) + // Signal shutdown + if (m_pShutdownEvent != nullptr) + { + LOG_IF_FAILED(SetEvent(m_pShutdownEvent)); + } + + // Need to wait for either the app to be initialized, the worker thread to exit, or the shutdown timeout. + const HANDLE waitHandles[2] = { m_pInitializeEvent, m_workerThread.native_handle() }; + + const auto waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, m_pConfig->QueryShutdownTimeLimitInMS()); + + // If waitResults != WAIT_OBJECT_0 + 1, it means main hasn't returned, so okay to call into it. + if (!m_blockManagedCallbacks && waitResult != WAIT_OBJECT_0 + 1) { // We cannot call into managed if the dll is detaching from the process. // Calling into managed code when the dll is detaching is strictly a bad idea, @@ -80,18 +104,17 @@ IN_PROCESS_APPLICATION::StopClr() } } - // Signal shutdown - if (m_pShutdownEvent != nullptr) - { - LOG_IF_FAILED(SetEvent(m_pShutdownEvent)); - } - if (m_workerThread.joinable()) { // Worker thread would wait for clr to finish and log error if required m_workerThread.join(); } + if (m_folderCleanupThread.joinable()) + { + m_folderCleanupThread.join(); + } + s_Application = nullptr; } @@ -146,12 +169,43 @@ IN_PROCESS_APPLICATION::LoadManagedApplication(ErrorContext& errorContext) LOG_INFO(L"Waiting for initialization"); - m_workerThread = std::thread([](std::unique_ptr application) + THROW_IF_FAILED(StartMonitoringAppOffline()); + + if (!m_shadowCopyDirectory.empty()) { - LOG_INFO(L"Starting in-process worker thread"); - application->ExecuteApplication(); - LOG_INFO(L"Stopping in-process worker thread"); - }, ::ReferenceApplication(this)); + if (!Environment::CheckUpToDate(QueryApplicationPhysicalPath(), m_shadowCopyDirectory, L".dll", std::filesystem::path(m_shadowCopyDirectory).parent_path())) + { + Stop(/* fServerInitiated */false); + throw InvalidOperationException(L"File changed between copy and start of application, restarting."); + } + // Cleanup other directories that haven't been removed. + m_folderCleanupThread = std::thread([](std::wstring shadowCopyDir) + { + auto parentDir = std::filesystem::path(shadowCopyDir).parent_path(); + for (auto& p : std::filesystem::directory_iterator(parentDir)) + { + if (p.path() != shadowCopyDir) + { + try + { + std::filesystem::remove_all(p.path()); + } + catch (...) + { + OBSERVE_CAUGHT_EXCEPTION(); + } + } + } + + }, m_shadowCopyDirectory); + } + + m_workerThread = std::thread([](std::unique_ptr application) + { + LOG_INFO(L"Starting in-process worker thread"); + application->ExecuteApplication(); + LOG_INFO(L"Stopping in-process worker thread"); + }, ::ReferenceApplication(this)); const HANDLE waitHandles[2] = { m_pInitializeEvent, m_workerThread.native_handle() }; @@ -188,8 +242,6 @@ IN_PROCESS_APPLICATION::LoadManagedApplication(ErrorContext& errorContext) throw InvalidOperationException(format(L"CLR worker thread exited prematurely")); } - THROW_IF_FAILED(StartMonitoringAppOffline()); - return S_OK; } @@ -209,11 +261,11 @@ IN_PROCESS_APPLICATION::ExecuteApplication() THROW_IF_FAILED(HostFxrResolutionResult::Create( m_dotnetExeKnownLocation, m_pConfig->QueryProcessPath(), - QueryApplicationPhysicalPath(), + m_shadowCopyDirectory.empty() ? QueryApplicationPhysicalPath() : m_shadowCopyDirectory, m_pConfig->QueryArguments(), errorContext, hostFxrResolutionResult - )); + )); hostFxrResolutionResult->GetArguments(context->m_argc, context->m_argv); THROW_IF_FAILED(SetEnvironmentVariablesOnWorkerProcess()); @@ -294,6 +346,11 @@ IN_PROCESS_APPLICATION::ExecuteApplication() RETURN_IF_NOT_ZERO(context->m_hostFxr.SetRuntimePropertyValue(DOTNETCORE_USE_ENTRYPOINT_FILTER, L"1")); RETURN_IF_NOT_ZERO(context->m_hostFxr.SetRuntimePropertyValue(DOTNETCORE_STACK_SIZE, m_pConfig->QueryStackSize().c_str())); + if (!m_shadowCopyDirectory.empty()) + { + RETURN_IF_NOT_ZERO(context->m_hostFxr.SetRuntimePropertyValue(APP_CONTEXT_BASE_DIRECTORY, Environment::GetCurrentDirectoryValue().c_str())); + } + bool clrThreadExited; { //Start CLR thread @@ -393,11 +450,11 @@ void IN_PROCESS_APPLICATION::QueueStop() LOG_INFO(L"Queueing in-process stop thread"); std::thread stoppingThread([](std::unique_ptr application) - { - LOG_INFO(L"Starting in-process stop thread"); - application->Stop(false); - LOG_INFO(L"Stopping in-process stop thread"); - }, ::ReferenceApplication(this)); + { + LOG_INFO(L"Starting in-process stop thread"); + application->Stop(false); + LOG_INFO(L"Stopping in-process stop thread"); + }, ::ReferenceApplication(this)); stoppingThread.detach(); } @@ -458,7 +515,7 @@ IN_PROCESS_APPLICATION::ExecuteClr(const std::shared_ptr& con context->m_exitCode = exitCode; context->m_hostFxr.Close(); } - __except(GetExceptionCode() != 0) + __except (GetExceptionCode() != 0) { LOG_INFOF(L"Managed threw an exception %d", GetExceptionCode()); @@ -472,7 +529,7 @@ IN_PROCESS_APPLICATION::ExecuteClr(const std::shared_ptr& con // in case of startup timeout // VOID -IN_PROCESS_APPLICATION::ClrThreadEntryPoint(const std::shared_ptr &context) +IN_PROCESS_APPLICATION::ClrThreadEntryPoint(const std::shared_ptr& context) { HandleWrapper moduleHandle; @@ -503,7 +560,7 @@ IN_PROCESS_APPLICATION::SetEnvironmentVariablesOnWorkerProcess() QueryApplicationPhysicalPath().c_str(), nullptr); - for (const auto & variable : variables) + for (const auto& variable : variables) { LOG_INFOF(L"Setting environment variable %ls=%ls", variable.first.c_str(), variable.second.c_str()); SetEnvironmentVariable(variable.first.c_str(), variable.second.c_str()); @@ -537,7 +594,7 @@ IN_PROCESS_APPLICATION::UnexpectedThreadExit(const ExecuteClrContext& context) c QueryApplicationId().c_str(), QueryApplicationPhysicalPath().c_str(), context.m_exceptionCode - ); + ); } return; } @@ -571,13 +628,12 @@ IN_PROCESS_APPLICATION::UnexpectedThreadExit(const ExecuteClrContext& context) c HRESULT IN_PROCESS_APPLICATION::CreateHandler( - _In_ IHttpContext *pHttpContext, - _Out_ IREQUEST_HANDLER **pRequestHandler) + _In_ IHttpContext* pHttpContext, + _Out_ IREQUEST_HANDLER** pRequestHandler) { try { SRWSharedLock dataLock(m_dataLock); - DBG_ASSERT(!m_fStopCalled); m_requestCount++; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h index 4eda1fd7dadc..2cefb9bbcd04 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h @@ -145,6 +145,8 @@ class IN_PROCESS_APPLICATION : public InProcessApplicationBase std::thread m_clrThread; // Thread tracking the CLR thread, this one is always joined on shutdown std::thread m_workerThread; + // Thread for cleaning up existing shadow copy folders + std::thread m_folderCleanupThread; // The event that gets triggered when managed initialization is complete HandleWrapper m_pInitializeEvent; // The event that gets triggered when worker thread should exit @@ -177,6 +179,7 @@ class IN_PROCESS_APPLICATION : public InProcessApplicationBase std::shared_ptr m_stringRedirectionOutput; inline static const LPCSTR s_exeLocationParameterName = "InProcessExeLocation"; + inline static const LPCSTR s_shadowCopyDirectoryName = "ShadowCopyDirectory"; VOID UnexpectedThreadExit(const ExecuteClrContext& context) const; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp index 7bd0a5fa83bf..897a97d17c05 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/OutOfProcessRequestHandler/outprocessapplication.cpp @@ -2,7 +2,6 @@ // Licensed under the MIT License. See License.txt in the project root for license information. #include "outprocessapplication.h" - #include "SRWExclusiveLock.h" #include "exceptions.h" diff --git a/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp index a946e7c2dc5a..e7dbdf196d47 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.cpp @@ -25,13 +25,13 @@ HRESULT AppOfflineTrackingApplication::StartMonitoringAppOffline() void AppOfflineTrackingApplication::StopInternal(bool fServerInitiated) { - APPLICATION::StopInternal(fServerInitiated); - if (m_fileWatcher) { m_fileWatcher->StopMonitor(); m_fileWatcher = nullptr; } + + APPLICATION::StopInternal(fServerInitiated); } HRESULT AppOfflineTrackingApplication::StartMonitoringAppOflineImpl() @@ -44,7 +44,9 @@ HRESULT AppOfflineTrackingApplication::StartMonitoringAppOflineImpl() m_fileWatcher = std::make_unique(); RETURN_IF_FAILED(m_fileWatcher->Create(m_applicationPath.c_str(), L"app_offline.htm", - this)); + m_shadowCopyDirectory, + this, + m_shutdownTimeout)); return S_OK; } @@ -56,11 +58,22 @@ void AppOfflineTrackingApplication::OnAppOffline() return; } - LOG_INFOF(L"Received app_offline notification in application '%ls'", m_applicationPath.c_str()); - EventLog::Info( - ASPNETCORE_EVENT_RECYCLE_APPOFFLINE, - ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG, - m_applicationPath.c_str()); + if (m_detectedAppOffline) + { + LOG_INFOF(L"Received app_offline notification in application '%ls'", m_applicationPath.c_str()); + EventLog::Info( + ASPNETCORE_EVENT_RECYCLE_APPOFFLINE, + ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG, + m_applicationPath.c_str()); + } + else + { + LOG_INFOF(L"Received file change notification in application '%ls'", m_applicationPath.c_str()); + EventLog::Info( + ASPNETCORE_EVENT_RECYCLE_APPOFFLINE, + ASPNETCORE_EVENT_RECYCLE_FILECHANGE_MSG, + m_applicationPath.c_str()); + } Stop(/*fServerInitiated*/ false); } diff --git a/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h b/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h index b504a730fd62..8d4eafc0aad0 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/AppOfflineTrackingApplication.h @@ -15,7 +15,8 @@ class AppOfflineTrackingApplication: public APPLICATION : APPLICATION(application), m_applicationPath(application.GetApplicationPhysicalPath()), m_fileWatcher(nullptr), - m_fAppOfflineProcessed(false) + m_fAppOfflineProcessed(false), + m_shutdownTimeout(120000) // default to 2 minutes { } @@ -37,6 +38,10 @@ class AppOfflineTrackingApplication: public APPLICATION VOID OnAppOffline(); + // TODO protected + bool m_detectedAppOffline; + std::wstring m_shadowCopyDirectory; + DWORD m_shutdownTimeout; private: HRESULT StartMonitoringAppOflineImpl(); diff --git a/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp b/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp index 9d17f95b37c3..f4baec9f40d2 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.cpp @@ -6,21 +6,32 @@ #include "debugutil.h" #include "AppOfflineTrackingApplication.h" #include "exceptions.h" +#include FILE_WATCHER::FILE_WATCHER() : m_hCompletionPort(NULL), m_hChangeNotificationThread(NULL), - m_fThreadExit(FALSE) + m_fThreadExit(FALSE), + m_fShadowCopyEnabled(FALSE), + m_copied(false) { + m_pDoneCopyEvent = CreateEvent( + nullptr, // default security attributes + TRUE, // manual reset event + FALSE, // not set + nullptr); // name } FILE_WATCHER::~FILE_WATCHER() { StopMonitor(); + WaitForMonitor(20); // wait for 1 second total +} +void FILE_WATCHER::WaitForMonitor(DWORD dwRetryCounter) +{ if (m_hChangeNotificationThread != NULL) { - DWORD dwRetryCounter = 20; // totally wait for 1s DWORD dwExitCode = STILL_ACTIVE; while (!m_fThreadExit && dwRetryCounter > 0) @@ -54,9 +65,14 @@ HRESULT FILE_WATCHER::Create( _In_ PCWSTR pszDirectoryToMonitor, _In_ PCWSTR pszFileNameToMonitor, - _In_ AppOfflineTrackingApplication *pApplication + _In_ const std::wstring& shadowCopyPath, + _In_ AppOfflineTrackingApplication* pApplication, + _In_ DWORD shutdownTimeout ) { + m_shadowCopyPath = shadowCopyPath; + m_fShadowCopyEnabled = !shadowCopyPath.empty(); + m_shutdownTimeout = shutdownTimeout; RETURN_LAST_ERROR_IF_NULL(m_hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)); @@ -130,10 +146,10 @@ Win32 error --*/ { - FILE_WATCHER * pFileMonitor; + FILE_WATCHER* pFileMonitor; BOOL fSuccess = FALSE; DWORD cbCompletion = 0; - OVERLAPPED * pOverlapped = NULL; + OVERLAPPED* pOverlapped = NULL; DWORD dwErrorStatus; ULONG_PTR completionKey; @@ -177,7 +193,15 @@ Win32 error pFileMonitor->m_fThreadExit = TRUE; + if (pFileMonitor->m_fShadowCopyEnabled) + { + // Cancel the timer to avoid it calling copy. + pFileMonitor->m_Timer.CancelTimer(); + FILE_WATCHER::CopyAndShutdown(pFileMonitor); + } + LOG_INFO(L"Stopping file watcher thread"); + ExitThread(0); } @@ -204,7 +228,8 @@ HRESULT --*/ { - BOOL fFileChanged = FALSE; + BOOL fAppOfflineChanged = FALSE; + BOOL fDllChanged = FALSE; // When directory handle is closed then HandleChangeCompletion // happens with cbCompletion = 0 and dwCompletionStatus = 0 @@ -226,7 +251,7 @@ HRESULT // if (cbCompletion == 0) { - fFileChanged = TRUE; + fAppOfflineChanged = TRUE; } else { @@ -242,9 +267,22 @@ HRESULT _strFileName.QueryStr(), pNotificationInfo->FileNameLength / sizeof(WCHAR)) == 0) { - fFileChanged = TRUE; + fAppOfflineChanged = TRUE; + auto app = _pApplication.get(); + app->m_detectedAppOffline = true; break; } + + // + // Look for changes to dlls when shadow copying is enabled. + // + std::wstring notification(pNotificationInfo->FileName, pNotificationInfo->FileNameLength / sizeof(WCHAR)); + std::filesystem::path notificationPath(notification); + if (m_fShadowCopyEnabled && notificationPath.extension().compare(L".dll") == 0) + { + fDllChanged = TRUE; + } + // // Advance to next notification // @@ -261,16 +299,92 @@ HRESULT } } - if (fFileChanged && !_lStopMonitorCalled) + if (fAppOfflineChanged && !_lStopMonitorCalled) { // Reference application before _pApplication->ReferenceApplication(); RETURN_LAST_ERROR_IF(!QueueUserWorkItem(RunNotificationCallback, _pApplication.get(), WT_EXECUTEDEFAULT)); } + if (fDllChanged && m_fShadowCopyEnabled && !_lStopMonitorCalled) + { + // Reset timer for dll checks + LOG_INFO(L"Detected dll change, resetting timer callback which will eventually trigger shutdown."); + m_Timer.CancelTimer(); + m_Timer.InitializeTimer(FILE_WATCHER::TimerCallback, this, 5000, INFINITE); + } + return S_OK; } + +VOID +CALLBACK +FILE_WATCHER::TimerCallback( + _In_ PTP_CALLBACK_INSTANCE Instance, + _In_ PVOID Context, + _In_ PTP_TIMER Timer +) +{ + Instance; + Timer; + CopyAndShutdown((FILE_WATCHER*)Context); +} + +DWORD WINAPI FILE_WATCHER::CopyAndShutdown(FILE_WATCHER* watcher) +{ + // Only copy and shutdown once + SRWExclusiveLock lock(watcher->m_copyLock); + if (watcher->m_copied) + { + return 0; + } + + watcher->m_copied = true; + + LOG_INFO(L"Starting copy on shutdown in filewatcher, creating directory."); + + auto directoryNameInt = 0; + auto currentShadowCopyDirectory = std::filesystem::path(watcher->m_shadowCopyPath); + auto parentDirectory = currentShadowCopyDirectory.parent_path(); + try + { + directoryNameInt = std::stoi(currentShadowCopyDirectory.filename().string()); + } + catch (...) + { + OBSERVE_CAUGHT_EXCEPTION(); + return 0; + } + + // Add one to the directory we want to copy to. + directoryNameInt++; + auto destination = parentDirectory / std::to_wstring(directoryNameInt); + + LOG_INFOF(L"Copying new shadow copy directory to %ls.", destination.wstring().c_str()); + + // Copy contents before shutdown + try + { + Environment::CopyToDirectory(watcher->_strDirectoryName.QueryStr(), destination, false, std::filesystem::canonical(parentDirectory)); + } + catch (...) + { + OBSERVE_CAUGHT_EXCEPTION(); + return 0; + } + + LOG_INFOF(L"Finished copy on shutdown to %ls.", destination.wstring().c_str()); + + SetEvent(watcher->m_pDoneCopyEvent); + + // reference application before callback (same thing we do with app_offline). + watcher->_pApplication->ReferenceApplication(); + QueueUserWorkItem(RunNotificationCallback, watcher->_pApplication.get(), WT_EXECUTEDEFAULT); + + return 0; +} + DWORD WINAPI FILE_WATCHER::RunNotificationCallback( @@ -279,7 +393,6 @@ FILE_WATCHER::RunNotificationCallback( { // Recapture application instance into unique_ptr auto pApplication = std::unique_ptr(static_cast(pvArg)); - DBG_ASSERT(pFileMonitor != NULL); pApplication->OnAppOffline(); return 0; @@ -319,9 +432,23 @@ FILE_WATCHER::StopMonitor() // we know that HandleChangeCompletion() call // can be ignored // - InterlockedExchange(&_lStopMonitorCalled, 1); + if (InterlockedExchange(&_lStopMonitorCalled, 1) == 1) + { + return; + } + + LOG_INFO(L"Stopping file watching."); + // signal the file watch thread to exit PostQueuedCompletionStatus(m_hCompletionPort, 0, FILE_WATCHER_SHUTDOWN_KEY, NULL); + WaitForMonitor(200); + + if (m_fShadowCopyEnabled) + { + // If we are shadow copying, wait for the copying to finish. + WaitForSingleObject(m_pDoneCopyEvent, m_shutdownTimeout); + } + // Release application reference _pApplication.reset(nullptr); } diff --git a/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h b/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h index 46ea74453322..470edbb5eb21 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/RequestHandlerLib/filewatcher.h @@ -8,6 +8,8 @@ #include #include "iapplication.h" #include "HandleWrapper.h" +#include "Environment.h" +#include #define FILE_WATCHER_SHUTDOWN_KEY (ULONG_PTR)(-1) #define FILE_WATCHER_ENTRY_BUFFER_SIZE 4096 @@ -22,10 +24,14 @@ class FILE_WATCHER{ ~FILE_WATCHER(); + void WaitForMonitor(DWORD dwRetryCounter); + HRESULT Create( _In_ PCWSTR pszDirectoryToMonitor, _In_ PCWSTR pszFileNameToMonitor, - _In_ AppOfflineTrackingApplication *pApplication + _In_ const std::wstring& shadowCopyPath, + _In_ AppOfflineTrackingApplication *pApplication, + _In_ DWORD shutdownTimeout ); static @@ -36,6 +42,14 @@ class FILE_WATCHER{ DWORD WINAPI RunNotificationCallback(LPVOID); + static + VOID + WINAPI TimerCallback(_In_ PTP_CALLBACK_INSTANCE Instance, + _In_ PVOID Context, + _In_ PTP_TIMER Timer); + + static DWORD WINAPI CopyAndShutdown(FILE_WATCHER* watcher); + HRESULT HandleChangeCompletion(DWORD cbCompletion); HRESULT Monitor(); @@ -45,13 +59,20 @@ class FILE_WATCHER{ HandleWrapper m_hCompletionPort; HandleWrapper m_hChangeNotificationThread; HandleWrapper _hDirectory; + HandleWrapper m_pDoneCopyEvent; volatile BOOL m_fThreadExit; + STTIMER m_Timer; + SRWLOCK m_copyLock{}; + BOOL m_copied; BUFFER _buffDirectoryChanges; STRU _strFileName; STRU _strDirectoryName; STRU _strFullName; LONG _lStopMonitorCalled {}; + bool m_fShadowCopyEnabled; + std::wstring m_shadowCopyPath; + DWORD m_shutdownTimeout; OVERLAPPED _overlapped; std::unique_ptr _pApplication; }; diff --git a/src/Servers/IIS/IIS/perf/Microbenchmarks/Properties/launchSettings.json b/src/Servers/IIS/IIS/perf/Microbenchmarks/Properties/launchSettings.json new file mode 100644 index 000000000000..fe76b29b53d8 --- /dev/null +++ b/src/Servers/IIS/IIS/perf/Microbenchmarks/Properties/launchSettings.json @@ -0,0 +1,40 @@ +{ + "iisSettings": { + "windowsAuthentication": true, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5762/", + "sslPort": 0 + } + }, + "profiles": { + "ANCM IIS Express": { + "commandName": "Executable", + "executablePath": "$(IISExpressPath)", + "commandLineArgs": "$(IISExpressArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + }, + "ANCM IIS": { + "commandName": "Executable", + "executablePath": "$(IISPath)", + "commandLineArgs": "$(IISArguments)", + "environmentVariables": { + "IIS_SITE_PATH": "$(MSBuildThisFileDirectory)", + "ANCMV2_PATH": "$(AspNetCoreModuleV2ShimDll)", + "ASPNETCORE_MODULE_OUTOFPROCESS_HANDLER": "$(AspNetCoreModuleV2OutOfProcessHandlerDll)", + "LAUNCHER_ARGS": "$(TargetPath)", + "ASPNETCORE_ENVIRONMENT": "Development", + "LAUNCHER_PATH": "$(DotNetPath)", + "ASPNETCORE_MODULE_DEBUG": "console" + } + } + } +} diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/EventLogHelpers.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/EventLogHelpers.cs index 9332b5deb61c..744ea078193c 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/EventLogHelpers.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Infrastructure/EventLogHelpers.cs @@ -167,6 +167,11 @@ public static string InProcessShutdown() return "Application 'MACHINE/WEBROOT/APPHOST/.*?' has shutdown."; } + public static string ShutdownFileChange(IISDeploymentResult deploymentResult) + { + return $"Application '{EscapedContentRoot(deploymentResult)}' was recycled after detecting file change in application directory."; + } + public static string InProcessFailedToStop(IISDeploymentResult deploymentResult, string reason) { return "Failed to gracefully shutdown application 'MACHINE/WEBROOT/APPHOST/.*?'."; diff --git a/src/Servers/IIS/IIS/test/IIS.FunctionalTests/ShadowCopyTests.cs b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/ShadowCopyTests.cs new file mode 100644 index 000000000000..fed5da275e4a --- /dev/null +++ b/src/Servers/IIS/IIS/test/IIS.FunctionalTests/ShadowCopyTests.cs @@ -0,0 +1,261 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ShadowCopyTests : IISFunctionalTestBase + { + public ShadowCopyTests(PublishedSitesFixture fixture) : base(fixture) + { + } + + [ConditionalFact] + [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2, SkipReason = "Shutdown hangs https://github.com/dotnet/aspnetcore/issues/25107")] + public async Task ShadowCopyDoesNotLockFiles() + { + using var directory = TempDirectory.Create(); + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.HandlerSettings["experimentalEnableShadowCopy"] = "true"; + deploymentParameters.HandlerSettings["shadowCopyDirectory"] = directory.DirectoryPath; + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("Wow!"); + + Assert.True(response.IsSuccessStatusCode); + var directoryInfo = new DirectoryInfo(deploymentResult.ContentRoot); + + // Verify that we can delete all files in the content root (nothing locked) + foreach (var fileInfo in directoryInfo.GetFiles()) + { + fileInfo.Delete(); + } + + foreach (var dirInfo in directoryInfo.GetDirectories()) + { + dirInfo.Delete(recursive: true); + } + } + + [ConditionalFact] + [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2, SkipReason = "Shutdown hangs https://github.com/dotnet/aspnetcore/issues/25107")] + public async Task ShadowCopyRelativeInSameDirectoryWorks() + { + var directoryName = Path.GetRandomFileName(); + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.HandlerSettings["experimentalEnableShadowCopy"] = "true"; + deploymentParameters.HandlerSettings["shadowCopyDirectory"] = directoryName; + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("Wow!"); + + Assert.True(response.IsSuccessStatusCode); + var directoryInfo = new DirectoryInfo(deploymentResult.ContentRoot); + + // Verify that we can delete all files in the content root (nothing locked) + foreach (var fileInfo in directoryInfo.GetFiles()) + { + fileInfo.Delete(); + } + + var tempDirectoryPath = Path.Combine(deploymentResult.ContentRoot, directoryName); + foreach (var dirInfo in directoryInfo.GetDirectories()) + { + if (!tempDirectoryPath.Equals(dirInfo.FullName)) + { + dirInfo.Delete(recursive: true); + } + } + } + + [ConditionalFact] + [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2, SkipReason = "Shutdown hangs https://github.com/dotnet/aspnetcore/issues/25107")] + public async Task ShadowCopyRelativeOutsideDirectoryWorks() + { + using var directory = TempDirectory.Create(); + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.HandlerSettings["experimentalEnableShadowCopy"] = "true"; + deploymentParameters.HandlerSettings["shadowCopyDirectory"] = $"..\\{directory.DirectoryInfo.Name}"; + deploymentParameters.ApplicationPath = directory.DirectoryPath; + + var deploymentResult = await DeployAsync(deploymentParameters); + var response = await deploymentResult.HttpClient.GetAsync("Wow!"); + + // Check if directory can be deleted. + // Can't delete the folder but can delete all content in it. + + Assert.True(response.IsSuccessStatusCode); + var directoryInfo = new DirectoryInfo(deploymentResult.ContentRoot); + + // Verify that we can delete all files in the content root (nothing locked) + foreach (var fileInfo in directoryInfo.GetFiles()) + { + fileInfo.Delete(); + } + + foreach (var dirInfo in directoryInfo.GetDirectories()) + { + dirInfo.Delete(recursive: true); + } + + StopServer(); + deploymentResult.AssertWorkerProcessStop(); + } + + [ConditionalFact] + [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2, SkipReason = "Shutdown hangs https://github.com/dotnet/aspnetcore/issues/25107")] + public async Task ShadowCopySingleFileChangedWorks() + { + using var directory = TempDirectory.Create(); + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.HandlerSettings["experimentalEnableShadowCopy"] = "true"; + deploymentParameters.HandlerSettings["shadowCopyDirectory"] = directory.DirectoryPath; + + var deploymentResult = await DeployAsync(deploymentParameters); + + DirectoryCopy(deploymentResult.ContentRoot, directory.DirectoryPath, copySubDirs: true); + + var response = await deploymentResult.HttpClient.GetAsync("Wow!"); + Assert.True(response.IsSuccessStatusCode); + // Rewrite file + var dirInfo = new DirectoryInfo(deploymentResult.ContentRoot); + + string dllPath = ""; + foreach (var file in dirInfo.EnumerateFiles()) + { + if (file.Extension == ".dll") + { + dllPath = file.FullName; + break; + } + } + var fileContents = File.ReadAllBytes(dllPath); + File.WriteAllBytes(dllPath, fileContents); + + deploymentResult.AssertWorkerProcessStop(); + + response = await deploymentResult.HttpClient.GetAsync("Wow!"); + Assert.True(response.IsSuccessStatusCode); + } + + [ConditionalFact] + [MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2, SkipReason = "Shutdown hangs https://github.com/dotnet/aspnetcore/issues/25107")] + public async Task ShadowCopyE2EWorksWithFolderPresent() + { + using var directory = TempDirectory.Create(); + var deploymentParameters = Fixture.GetBaseDeploymentParameters(); + deploymentParameters.HandlerSettings["experimentalEnableShadowCopy"] = "true"; + deploymentParameters.HandlerSettings["shadowCopyDirectory"] = directory.DirectoryPath; + var deploymentResult = await DeployAsync(deploymentParameters); + + DirectoryCopy(deploymentResult.ContentRoot, Path.Combine(directory.DirectoryPath, "0"), copySubDirs: true); + + var response = await deploymentResult.HttpClient.GetAsync("Wow!"); + Assert.True(response.IsSuccessStatusCode); + + using var secondTempDir = TempDirectory.Create(); + + // copy back and forth to cause file change notifications. + DirectoryCopy(deploymentResult.ContentRoot, secondTempDir.DirectoryPath, copySubDirs: true); + DirectoryCopy(secondTempDir.DirectoryPath, deploymentResult.ContentRoot, copySubDirs: true); + + deploymentResult.AssertWorkerProcessStop(); + response = await deploymentResult.HttpClient.GetAsync("Wow!"); + Assert.True(response.IsSuccessStatusCode); + } + + public class TempDirectory : IDisposable + { + public static TempDirectory Create() + { + var directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var directoryInfo = Directory.CreateDirectory(directoryPath); + return new TempDirectory(directoryInfo); + } + + public TempDirectory(DirectoryInfo directoryInfo) + { + DirectoryInfo = directoryInfo; + + DirectoryPath = directoryInfo.FullName; + } + + public string DirectoryPath { get; } + public DirectoryInfo DirectoryInfo { get; } + + public void Dispose() + { + DeleteDirectory(DirectoryPath); + } + + private static void DeleteDirectory(string directoryPath) + { + foreach (var subDirectoryPath in Directory.EnumerateDirectories(directoryPath)) + { + DeleteDirectory(subDirectoryPath); + } + + try + { + foreach (var filePath in Directory.EnumerateFiles(directoryPath)) + { + var fileInfo = new FileInfo(filePath) + { + Attributes = FileAttributes.Normal + }; + fileInfo.Delete(); + } + Directory.Delete(directoryPath); + } + catch (Exception e) + { + Console.WriteLine($@"Failed to delete directory {directoryPath}: {e.Message}"); + } + } + } + + // copied from https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-copy-directories + private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) + { + // Get the subdirectories for the specified directory. + DirectoryInfo dir = new DirectoryInfo(sourceDirName); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException( + "Source directory does not exist or could not be found: " + + sourceDirName); + } + + DirectoryInfo[] dirs = dir.GetDirectories(); + + // If the destination directory doesn't exist, create it. + Directory.CreateDirectory(destDirName); + + // Get the files in the directory and copy them to the new location. + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string tempPath = Path.Combine(destDirName, file.Name); + file.CopyTo(tempPath, true); + } + + // If copying subdirectories, copy them and their contents to new location. + if (copySubDirs) + { + foreach (DirectoryInfo subdir in dirs) + { + string tempPath = Path.Combine(destDirName, subdir.Name); + DirectoryCopy(subdir.FullName, tempPath, copySubDirs); + } + } + } + } +} diff --git a/src/Servers/IIS/IISIntegration.slnf b/src/Servers/IIS/IISIntegration.slnf index e8ce42f08921..f5f7ddf1ec9a 100644 --- a/src/Servers/IIS/IISIntegration.slnf +++ b/src/Servers/IIS/IISIntegration.slnf @@ -36,6 +36,7 @@ "src\\Servers\\IIS\\IIS\\perf\\Microbenchmarks\\IIS.Microbenchmarks.csproj", "src\\Servers\\IIS\\IIS\\samples\\NativeIISSample\\NativeIISSample.csproj", "src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj", + "src\\Servers\\IIS\\IIS\\samples\\NativeIISSample\\NativeIISSample.csproj", "src\\Servers\\IIS\\IIS\\test\\IIS.FunctionalTests\\IIS.FunctionalTests.csproj", "src\\Servers\\IIS\\IIS\\test\\IIS.NewHandler.FunctionalTests\\IIS.NewHandler.FunctionalTests.csproj", "src\\Servers\\IIS\\IIS\\test\\IIS.NewShim.FunctionalTests\\IIS.NewShim.FunctionalTests.csproj",