From 0459b6d0d48635e72939ecf789cad32fb41578ba Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Fri, 31 Aug 2018 09:05:46 -0700 Subject: [PATCH] Fix dotnet.exe abort exception and startup/shutdown timeouts (#1352) --- .../AspNetCore/HandlerResolver.cpp | 6 +- .../AspNetCore/applicationmanager.h | 2 - .../CommonLib/CommonLib.vcxproj | 3 +- .../CommonLib/ConfigurationSection.cpp | 10 + .../CommonLib/ConfigurationSection.h | 4 + .../CommonLib/InvalidOperationException.h | 22 + .../CommonLib/ModuleHelpers.h | 20 + .../WebConfigConfigurationSection.cpp | 11 + .../CommonLib/WebConfigConfigurationSection.h | 1 + .../CommonLib/application.h | 12 +- src/AspNetCoreModuleV2/CommonLib/exceptions.h | 12 +- .../CommonLib/hostfxr_utility.cpp | 20 +- .../CommonLib/hostfxr_utility.h | 16 +- .../CommonLib/hostfxroptions.cpp | 31 +- .../CommonLib/hostfxroptions.h | 8 +- src/AspNetCoreModuleV2/CommonLib/resources.h | 9 +- .../InProcessApplicationBase.cpp | 18 +- .../InProcessApplicationBase.h | 4 - .../InProcessOptions.cpp | 37 + .../InProcessOptions.h | 17 + .../StartupExceptionApplication.cpp | 2 +- .../StartupExceptionApplication.h | 23 - .../StartupExceptionHandler.cpp | 30 +- .../StartupExceptionHandler.h | 15 +- .../InProcessRequestHandler/dllmain.cpp | 36 +- .../inprocessapplication.cpp | 751 ++++++++---------- .../inprocessapplication.h | 155 ++-- .../inprocesshandler.h | 12 +- .../IISDeploymentParameterExtensions.cs | 20 + .../IISExpressDeployer.cs | 2 +- .../ConfigurationChangeTests.cs | 2 +- .../Inprocess/EventLogTests.cs | 4 +- .../Inprocess/StartupExceptionTests.cs | 28 +- .../MultiApplicationTests.cs | 4 +- .../Utilities/EventLogHelpers.cs | 107 ++- test/CommonLibTests/CommonLibTests.vcxproj | 8 +- test/CommonLibTests/hostfxr_utility_tests.cpp | 4 +- .../inprocess_application_tests.cpp | 2 +- .../Inprocess/StdOutRedirectionTests.cs | 10 +- .../InProcess/ShutdownTests.cs | 33 +- .../InProcess/StartupTests.cs | 114 ++- .../StartupExceptionWebSite/Program.cs | 93 ++- 42 files changed, 962 insertions(+), 756 deletions(-) create mode 100644 src/AspNetCoreModuleV2/CommonLib/InvalidOperationException.h create mode 100644 src/AspNetCoreModuleV2/CommonLib/ModuleHelpers.h diff --git a/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp b/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp index 2c79f60a85..bd7dacfb34 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp +++ b/src/AspNetCoreModuleV2/AspNetCore/HandlerResolver.cpp @@ -56,10 +56,10 @@ HandlerResolver::LoadRequestHandlerAssembly(IHttpApplication &pApplication, Shim std::unique_ptr outputManager; RETURN_IF_FAILED(HOSTFXR_OPTIONS::Create( - NULL, - pConfiguration.QueryProcessPath().c_str(), + L"", + pConfiguration.QueryProcessPath(), pApplication.GetApplicationPhysicalPath(), - pConfiguration.QueryArguments().c_str(), + pConfiguration.QueryArguments(), options)); location = options->GetDotnetExeLocation(); diff --git a/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.h b/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.h index 5cb341087d..ab3fbe9746 100644 --- a/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.h +++ b/src/AspNetCoreModuleV2/AspNetCore/applicationmanager.h @@ -8,8 +8,6 @@ #include "exceptions.h" #include -#define DEFAULT_HASH_BUCKETS 17 - // // This class will manage the lifecycle of all Asp.Net Core applciation // It should be global singleton. diff --git a/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj b/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj index ef9f1c1986..20e37c30f3 100644 --- a/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj +++ b/src/AspNetCoreModuleV2/CommonLib/CommonLib.vcxproj @@ -195,7 +195,6 @@ - @@ -211,9 +210,11 @@ + + diff --git a/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.cpp b/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.cpp index 77c3319dd1..11e790cff7 100644 --- a/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.cpp +++ b/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.cpp @@ -26,6 +26,16 @@ bool ConfigurationSection::GetRequiredBool(const std::wstring& name) const return result.value(); } +DWORD ConfigurationSection::GetRequiredLong(const std::wstring& name) const +{ + auto result = GetLong(name); + if (!result.has_value()) + { + ThrowRequiredException(name); + } + return result.value(); +} + DWORD ConfigurationSection::GetRequiredTimespan(const std::wstring& name) const { auto result = GetTimespan(name); diff --git a/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h b/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h index 163f50f8e5..f998478db2 100644 --- a/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h +++ b/src/AspNetCoreModuleV2/CommonLib/ConfigurationSection.h @@ -17,6 +17,8 @@ #define CS_ASPNETCORE_PROCESS_EXE_PATH L"processPath" #define CS_ASPNETCORE_PROCESS_ARGUMENTS L"arguments" #define CS_ASPNETCORE_PROCESS_ARGUMENTS_DEFAULT L"" +#define CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT L"startupTimeLimit" +#define CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT L"shutdownTimeLimit" #define CS_ASPNETCORE_HOSTING_MODEL_OUTOFPROCESS L"outofprocess" #define CS_ASPNETCORE_HOSTING_MODEL_INPROCESS L"inprocess" #define CS_ASPNETCORE_HOSTING_MODEL L"hostingModel" @@ -31,10 +33,12 @@ public: virtual ~ConfigurationSection() = default; virtual std::optional GetString(const std::wstring& name) const = 0; virtual std::optional GetBool(const std::wstring& name) const = 0; + virtual std::optional GetLong(const std::wstring& name) const = 0; virtual std::optional GetTimespan(const std::wstring& name) const = 0; std::wstring GetRequiredString(const std::wstring& name) const; bool GetRequiredBool(const std::wstring& name) const; + DWORD GetRequiredLong(const std::wstring& name) const; DWORD GetRequiredTimespan(const std::wstring& name) const; virtual std::vector> GetKeyValuePairs(const std::wstring& name) const = 0; diff --git a/src/AspNetCoreModuleV2/CommonLib/InvalidOperationException.h b/src/AspNetCoreModuleV2/CommonLib/InvalidOperationException.h new file mode 100644 index 0000000000..3963091911 --- /dev/null +++ b/src/AspNetCoreModuleV2/CommonLib/InvalidOperationException.h @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include + +class InvalidOperationException: public std::runtime_error +{ + public: + InvalidOperationException(std::wstring msg) + : runtime_error("InvalidOperationException"), message(std::move(msg)) + { + } + + std::wstring as_wstring() const + { + return message; + } + + private: + std::wstring message; +}; diff --git a/src/AspNetCoreModuleV2/CommonLib/ModuleHelpers.h b/src/AspNetCoreModuleV2/CommonLib/ModuleHelpers.h new file mode 100644 index 0000000000..0ef9a7662d --- /dev/null +++ b/src/AspNetCoreModuleV2/CommonLib/ModuleHelpers.h @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +#pragma once +#include "HandleWrapper.h" +#include "exceptions.h" + +extern HMODULE g_hModule; + +class ModuleHelpers +{ +public: + static + void IncrementCurrentModuleRefCount(HandleWrapper &handle) + { + WCHAR path[MAX_PATH]; + THROW_LAST_ERROR_IF(!GetModuleFileName(g_hModule, path, sizeof(path))); + THROW_LAST_ERROR_IF(!GetModuleHandleEx(0, path, &handle)); + } +}; diff --git a/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.cpp b/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.cpp index aa2495359b..7003442f01 100644 --- a/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.cpp +++ b/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.cpp @@ -28,6 +28,17 @@ std::optional WebConfigConfigurationSection::GetBool(const std::wstring& n return std::make_optional(result); } +std::optional WebConfigConfigurationSection::GetLong(const std::wstring& name) const +{ + DWORD result; + if (FAILED_LOG(GetElementDWORDProperty(m_element, name.c_str(), &result))) + { + return std::nullopt; + } + + return std::make_optional(result); +} + std::optional WebConfigConfigurationSection::GetTimespan(const std::wstring& name) const { ULONGLONG result; diff --git a/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.h b/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.h index c2f87a9cfe..b206c02049 100644 --- a/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.h +++ b/src/AspNetCoreModuleV2/CommonLib/WebConfigConfigurationSection.h @@ -17,6 +17,7 @@ public: std::optional GetString(const std::wstring& name) const override; std::optional GetBool(const std::wstring& name) const override; + std::optional GetLong(const std::wstring& name) const override; std::optional GetTimespan(const std::wstring& name) const override; std::vector> GetKeyValuePairs(const std::wstring& name) const override; diff --git a/src/AspNetCoreModuleV2/CommonLib/application.h b/src/AspNetCoreModuleV2/CommonLib/application.h index 5e87e4ddf8..e99159d324 100644 --- a/src/AspNetCoreModuleV2/CommonLib/application.h +++ b/src/AspNetCoreModuleV2/CommonLib/application.h @@ -7,6 +7,7 @@ #include "iapplication.h" #include "ntassert.h" #include "SRWExclusiveLock.h" +#include "SRWSharedLock.h" class APPLICATION : public IAPPLICATION { @@ -15,11 +16,6 @@ public: APPLICATION(const APPLICATION&) = delete; const APPLICATION& operator=(const APPLICATION&) = delete; - APPLICATION_STATUS - QueryStatus() override - { - return m_fStopCalled ? APPLICATION_STATUS::RECYCLED : APPLICATION_STATUS::RUNNING; - } APPLICATION(const IHttpApplication& pHttpApplication) : m_fStopCalled(false), @@ -32,6 +28,12 @@ public: m_applicationVirtualPath = ToVirtualPath(m_applicationConfigPath); } + APPLICATION_STATUS + QueryStatus() override + { + SRWSharedLock stateLock(m_stateLock); + return m_fStopCalled ? APPLICATION_STATUS::RECYCLED : APPLICATION_STATUS::RUNNING; + } VOID Stop(bool fServerInitiated) override diff --git a/src/AspNetCoreModuleV2/CommonLib/exceptions.h b/src/AspNetCoreModuleV2/CommonLib/exceptions.h index 8d9a50ded3..0bf30d1bcc 100644 --- a/src/AspNetCoreModuleV2/CommonLib/exceptions.h +++ b/src/AspNetCoreModuleV2/CommonLib/exceptions.h @@ -3,10 +3,12 @@ #pragma once +#include #include #include "debugutil.h" #include "StringHelpers.h" +#include "InvalidOperationException.h" #define LOCATION_INFO_ENABLED TRUE @@ -41,10 +43,10 @@ #define FINISHED_LAST_ERROR_IF(condition) do { if (condition) { hr = LogLastError(LOCATION_INFO); goto Finished; }} while (0, 0) #define FINISHED_LAST_ERROR_IF_NULL(ptr) do { if ((ptr) == nullptr) { hr = LogLastError(LOCATION_INFO); goto Finished; }} while (0, 0) -#define THROW_LAST_ERROR() do { ThrowResultException(LogLastError(LOCATION_INFO)); } while (0, 0) +#define THROW_LAST_ERROR() do { ThrowResultException(LOCATION_INFO, LogLastError(LOCATION_INFO)); } while (0, 0) #define THROW_IF_FAILED(hr) do { HRESULT __hrRet = hr; if (FAILED(__hrRet)) { ThrowResultException(LOCATION_INFO, __hrRet); }} while (0, 0) -#define THROW_LAST_ERROR_IF(condition) do { if (condition) { ThrowResultException(LogLastError(LOCATION_INFO)); }} while (0, 0) -#define THROW_LAST_ERROR_IF_NULL(ptr) do { if ((ptr) == nullptr) { ThrowResultException(LogLastError(LOCATION_INFO)); }} while (0, 0) +#define THROW_LAST_ERROR_IF(condition) do { if (condition) { ThrowResultException(LOCATION_INFO, LogLastError(LOCATION_INFO)); }} while (0, 0) +#define THROW_LAST_ERROR_IF_NULL(ptr) do { if ((ptr) == nullptr) { ThrowResultException(LOCATION_INFO, LogLastError(LOCATION_INFO)); }} while (0, 0) #define THROW_IF_NULL_ALLOC(ptr) Throw_IfNullAlloc(ptr) @@ -152,3 +154,7 @@ template auto Throw_IfNullAlloc(PointerT pointer) } return pointer; } +__declspec(noinline) inline std::wstring GetUnexpectedExceptionMessage(std::runtime_error& ex) +{ + return format(L"Unexpected exception: %S", ex.what()); +} diff --git a/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.cpp b/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.cpp index eb82490ced..bc27526a41 100644 --- a/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.cpp +++ b/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.cpp @@ -41,7 +41,7 @@ HOSTFXR_UTILITY::GetHostFxrParameters( } else if (!ends_with(expandedProcessPath, L".exe", true)) { - throw StartupParametersResolutionException(format(L"Process path '%s' doesn't have '.exe' extension.", expandedProcessPath.c_str())); + throw InvalidOperationException(format(L"Process path '%s' doesn't have '.exe' extension.", expandedProcessPath.c_str())); } // Check if the absolute path is to dotnet or not. @@ -51,7 +51,7 @@ HOSTFXR_UTILITY::GetHostFxrParameters( if (applicationArguments.empty()) { - throw StartupParametersResolutionException(L"Application arguments are empty."); + throw InvalidOperationException(L"Application arguments are empty."); } if (dotnetExePath.empty()) @@ -92,7 +92,7 @@ HOSTFXR_UTILITY::GetHostFxrParameters( LOG_INFOF(L"Checking application.dll at '%ls'", applicationDllPath.c_str()); if (!is_regular_file(applicationDllPath)) { - throw StartupParametersResolutionException(format(L"Application .dll was not found at %s", applicationDllPath.c_str())); + throw InvalidOperationException(format(L"Application .dll was not found at %s", applicationDllPath.c_str())); } hostFxrDllPath = executablePath.parent_path() / "hostfxr.dll"; @@ -131,7 +131,7 @@ HOSTFXR_UTILITY::GetHostFxrParameters( // If the processPath file does not exist and it doesn't include dotnet.exe or dotnet // then it is an invalid argument. // - throw StartupParametersResolutionException(format(L"Executable was not found at '%s'", executablePath.c_str())); + throw InvalidOperationException(format(L"Executable was not found at '%s'", executablePath.c_str())); } } } @@ -185,7 +185,7 @@ HOSTFXR_UTILITY::AppendArguments( auto pwzArgs = std::unique_ptr(CommandLineToArgvW(applicationArguments.c_str(), &argc)); if (!pwzArgs) { - throw StartupParametersResolutionException(format(L"Unable parse command line arguments '%s'", applicationArguments.c_str())); + throw InvalidOperationException(format(L"Unable parse command line arguments '%s'", applicationArguments.c_str())); } for (int intArgsProcessed = 0; intArgsProcessed < argc; intArgsProcessed++) @@ -246,7 +246,7 @@ HOSTFXR_UTILITY::GetAbsolutePathToDotnet( { LOG_INFOF(L"Absolute path to dotnet.exe was not found at '%ls'", requestedPath.c_str()); - throw StartupParametersResolutionException(format(L"Could not find dotnet.exe at '%s'", processPath.c_str())); + throw InvalidOperationException(format(L"Could not find dotnet.exe at '%s'", processPath.c_str())); } const auto dotnetViaWhere = InvokeWhereToFindDotnet(); @@ -266,7 +266,7 @@ HOSTFXR_UTILITY::GetAbsolutePathToDotnet( } LOG_INFOF(L"dotnet.exe not found"); - throw StartupParametersResolutionException(format( + throw InvalidOperationException(format( L"Could not find dotnet.exe at '%s' or using the system PATH environment variable." " Check that a valid path to dotnet is on the PATH and the bitness of dotnet matches the bitness of the IIS worker process.", processPath.c_str())); @@ -284,14 +284,14 @@ HOSTFXR_UTILITY::GetAbsolutePathToHostFxr( if (!is_directory(hostFxrBase)) { - throw StartupParametersResolutionException(format(L"Unable to find hostfxr directory at %s", hostFxrBase.c_str())); + throw InvalidOperationException(format(L"Unable to find hostfxr directory at %s", hostFxrBase.c_str())); } FindDotNetFolders(hostFxrBase, versionFolders); if (versionFolders.empty()) { - throw StartupParametersResolutionException(format(L"Hostfxr directory '%s' doesn't contain any version subdirectories", hostFxrBase.c_str())); + throw InvalidOperationException(format(L"Hostfxr directory '%s' doesn't contain any version subdirectories", hostFxrBase.c_str())); } const auto highestVersion = FindHighestDotNetVersion(versionFolders); @@ -299,7 +299,7 @@ HOSTFXR_UTILITY::GetAbsolutePathToHostFxr( if (!is_regular_file(hostFxrPath)) { - throw StartupParametersResolutionException(format(L"hostfxr.dll not found at '%s'", hostFxrPath.c_str())); + throw InvalidOperationException(format(L"hostfxr.dll not found at '%s'", hostFxrPath.c_str())); } LOG_INFOF(L"hostfxr.dll located at '%ls'", hostFxrPath.c_str()); diff --git a/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.h b/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.h index d13b7e139e..c015fadc88 100644 --- a/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.h +++ b/src/AspNetCoreModuleV2/CommonLib/hostfxr_utility.h @@ -29,20 +29,6 @@ public: std::vector &arguments ); - class StartupParametersResolutionException: public std::runtime_error - { - public: - StartupParametersResolutionException(std::wstring msg) - : runtime_error("Startup parameter resulution error occured"), message(std::move(msg)) - { - } - - std::wstring get_message() const { return message; } - - private: - std::wstring message; - }; - static void AppendArguments( @@ -96,7 +82,7 @@ private: struct LocalFreeDeleter { - void operator ()(LPWSTR* ptr) const + void operator ()(_In_ LPWSTR* ptr) const { LocalFree(ptr); } diff --git a/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.cpp b/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.cpp index 9e1d8ab609..060224c60d 100644 --- a/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.cpp +++ b/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.cpp @@ -9,18 +9,19 @@ #include "EventLog.h" HRESULT HOSTFXR_OPTIONS::Create( - _In_ PCWSTR pcwzDotnetExePath, - _In_ PCWSTR pcwzProcessPath, - _In_ PCWSTR pcwzApplicationPhysicalPath, - _In_ PCWSTR pcwzArguments, + _In_ const std::wstring& pcwzDotnetExePath, + _In_ const std::wstring& pcwzProcessPath, + _In_ const std::wstring& pcwzApplicationPhysicalPath, + _In_ const std::wstring& pcwzArguments, _Out_ std::unique_ptr& ppWrapper) { std::filesystem::path knownDotnetLocation; - if (pcwzDotnetExePath != nullptr) + if (!pcwzDotnetExePath.empty()) { knownDotnetLocation = pcwzDotnetExePath; } + try { std::filesystem::path hostFxrDllPath; @@ -40,17 +41,25 @@ HRESULT HOSTFXR_OPTIONS::Create( } ppWrapper = std::make_unique(knownDotnetLocation, hostFxrDllPath, arguments); } - catch (HOSTFXR_UTILITY::StartupParametersResolutionException &resolutionException) + catch (InvalidOperationException &ex) { - OBSERVE_CAUGHT_EXCEPTION(); - EventLog::Error( ASPNETCORE_EVENT_INPROCESS_START_ERROR, ASPNETCORE_EVENT_INPROCESS_START_ERROR_MSG, - pcwzApplicationPhysicalPath, - resolutionException.get_message().c_str()); + pcwzApplicationPhysicalPath.c_str(), + ex.as_wstring().c_str()); - return E_FAIL; + RETURN_CAUGHT_EXCEPTION(); + } + catch (std::runtime_error &ex) + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_START_ERROR, + ASPNETCORE_EVENT_INPROCESS_START_ERROR_MSG, + pcwzApplicationPhysicalPath.c_str(), + GetUnexpectedExceptionMessage(ex).c_str()); + + RETURN_CAUGHT_EXCEPTION(); } CATCH_RETURN(); diff --git a/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.h b/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.h index 50bdfe1ded..8c42d08c37 100644 --- a/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.h +++ b/src/AspNetCoreModuleV2/CommonLib/hostfxroptions.h @@ -48,10 +48,10 @@ public: static HRESULT Create( - _In_ PCWSTR pcwzExeLocation, - _In_ PCWSTR pcwzProcessPath, - _In_ PCWSTR pcwzApplicationPhysicalPath, - _In_ PCWSTR pcwzArguments, + _In_ const std::wstring& pcwzExeLocation, + _In_ const std::wstring& pcwzProcessPath, + _In_ const std::wstring& pcwzApplicationPhysicalPath, + _In_ const std::wstring& pcwzArguments, _Out_ std::unique_ptr& ppWrapper); private: diff --git a/src/AspNetCoreModuleV2/CommonLib/resources.h b/src/AspNetCoreModuleV2/CommonLib/resources.h index c9cca2b8b6..790e849516 100644 --- a/src/AspNetCoreModuleV2/CommonLib/resources.h +++ b/src/AspNetCoreModuleV2/CommonLib/resources.h @@ -24,14 +24,14 @@ #define ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE_MSG L"Failed to gracefully shutdown process '%d'." #define ASPNETCORE_EVENT_SENT_SHUTDOWN_HTTP_REQUEST_MSG L"Sent shutdown HTTP message to process '%d' and received http status '%d'." #define ASPNETCORE_EVENT_APP_SHUTDOWN_FAILURE_MSG L"Failed to gracefully shutdown application '%s'." -#define ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG L"Application '%s' with physical root '%s' failed to load clr and managed application, ErrorCode = '0x%x." +#define ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG L"Application '%s' with physical root '%s' failed to load clr and managed application. %s" #define ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL_MSG L"Application '%s' has shutdown." #define ASPNETCORE_EVENT_DUPLICATED_INPROCESS_APP_MSG L"Only one inprocess application is allowed per IIS application pool. Please assign the application '%s' to a different IIS application pool." #define ASPNETCORE_EVENT_MIXED_HOSTING_MODEL_ERROR_MSG L"Mixed hosting model is not supported. Application '%s' configured with different hostingModel value '%d' other than the one of running application(s)." #define ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG L"Configuration load error. %s" #define ASPNETCORE_EVENT_ADD_APPLICATION_ERROR_MSG L"Failed to start application '%s', ErrorCode '0x%x'." -#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT_MSG L"Application '%s' with physical root '%s' hit unexpected managed background thread exit, ErrorCode = '0x%x'. Last 4KB characters of captured stdout and stderr logs:\r\n%s" -#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_MSG L"Application '%s' with physical root '%s' hit unexpected managed background thread exit, ErrorCode = '0x%x'. Please check the stderr logs for more information." +#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT_MSG L"Application '%s' with physical root '%s' hit unexpected managed background thread exit, exit code = '%d'. Last 4KB characters of captured stdout and stderr logs:\r\n%s" +#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_MSG L"Application '%s' with physical root '%s' hit unexpected managed background thread exit, exit code = '%d'. Please check the stderr logs for more information." #define ASPNETCORE_EVENT_APP_IN_SHUTDOWN_MSG L"Application shutting down." #define ASPNETCORE_EVENT_RECYCLE_APPOFFLINE_MSG L"Application '%s' was recycled after detecting the app_offline file." #define ASPNETCORE_EVENT_MONITOR_APPOFFLINE_ERROR_MSG L"Monitoring app_offline.htm failed for application '%s', ErrorCode '0x%x'. " @@ -39,7 +39,8 @@ #define ASPNETCORE_EVENT_RECYCLE_FAILURE_CONFIGURATION_MSG L"Failed to recycle application due to a configuration change at '%s'. Recycling worker process." #define ASPNETCORE_EVENT_MODULE_DISABLED_MSG L"AspNetCore Module is disabled" #define ASPNETCORE_EVENT_HOSTFXR_DLL_INVALID_VERSION_MSG L"Hostfxr version used does not support 'hostfxr_get_native_search_directories', update the version of hostfxr to a higher version. Path to hostfxr: '%s'." -#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_MSG L"Application '%s' with physical root '%s' hit unexpected managed exception, ErrorCode = '0x%x. Please check the stderr logs for more information." +#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_MSG L"Application '%s' with physical root '%s' hit unexpected managed exception, exception code = '0x%x. Please check the stderr logs for more information." +#define ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_STDOUT_MSG L"Application '%s' with physical root '%s' hit unexpected managed exception, exception code = '0x%x. Last 4KB characters of captured stdout and stderr logs:\r\n%s" #define ASPNETCORE_EVENT_INPROCESS_RH_ERROR_MSG L"Could not find the assembly '%s' for in-process application. Please confirm the Microsoft.AspNetCore.Server.IIS package is referenced in your application. Captured output: %s" #define ASPNETCORE_EVENT_OUT_OF_PROCESS_RH_MISSING_MSG L"Could not find the assembly '%s' for out-of-process application. Please confirm the assembly is installed correctly for IIS or IISExpress." #define ASPNETCORE_EVENT_INPROCESS_START_SUCCESS_MSG L"Application '%s' started the coreclr in-process successfully." diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp index 0abdc42025..fb9fe8a015 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.cpp @@ -3,8 +3,6 @@ #include "InProcessApplicationBase.h" -hostfxr_main_fn InProcessApplicationBase::s_fMainCallback = NULL; - InProcessApplicationBase::InProcessApplicationBase( IHttpServer& pHttpServer, IHttpApplication& pHttpApplication) @@ -35,7 +33,21 @@ InProcessApplicationBase::StopInternal(bool fServerInitiated) } else { - exit(0); + // Send WM_QUIT to the main window to initiate graceful shutdown + EnumWindows([](HWND hwnd, LPARAM) -> BOOL + { + DWORD processId; + + if (GetWindowThreadProcessId(hwnd, &processId) && + processId == GetCurrentProcessId() && + GetConsoleWindow() != hwnd) + { + PostMessage(hwnd, WM_QUIT, 0, 0); + return false; + } + + return true; + }, 0); } } diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h index bb343725f5..5bc2dd41e1 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessApplicationBase.h @@ -3,7 +3,6 @@ #pragma once -#include "application.h" #include "AppOfflineTrackingApplication.h" typedef INT(*hostfxr_main_fn) (CONST DWORD argc, CONST PCWSTR argv[]); // TODO these may need to be BSTRs @@ -23,8 +22,5 @@ public: protected: BOOL m_fRecycleCalled; IHttpServer& m_pHttpServer; - // Allows to override call to hostfxr_main with custome callback - // used in testing - static hostfxr_main_fn s_fMainCallback; }; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp index 6b26b3dcc9..1b3e22e1dc 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.cpp @@ -2,6 +2,41 @@ // Licensed under the MIT License. See License.txt in the project root for license information. #include "InProcessOptions.h" +#include "InvalidOperationException.h" +#include "EventLog.h" + +HRESULT InProcessOptions::Create( + IHttpServer& pServer, + IHttpApplication& pHttpApplication, + std::unique_ptr& options) +{ + try + { + const WebConfigConfigurationSource configurationSource(pServer.GetAdminManager(), pHttpApplication); + options = std::make_unique(configurationSource); + } + catch (InvalidOperationException& ex) + { + EventLog::Error( + ASPNETCORE_CONFIGURATION_LOAD_ERROR, + ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG, + ex.as_wstring().c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + catch (std::runtime_error& ex) + { + EventLog::Error( + ASPNETCORE_CONFIGURATION_LOAD_ERROR, + ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG, + GetUnexpectedExceptionMessage(ex).c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + CATCH_RETURN(); + + return S_OK; +} InProcessOptions::InProcessOptions(const ConfigurationSource &configurationSource) : m_fStdoutLogEnabled(false), @@ -18,6 +53,8 @@ InProcessOptions::InProcessOptions(const ConfigurationSource &configurationSourc m_struStdoutLogFile = aspNetCoreSection->GetRequiredString(CS_ASPNETCORE_STDOUT_LOG_FILE); m_fDisableStartUpErrorPage = aspNetCoreSection->GetRequiredBool(CS_ASPNETCORE_DISABLE_START_UP_ERROR_PAGE); m_environmentVariables = aspNetCoreSection->GetKeyValuePairs(CS_ASPNETCORE_ENVIRONMENT_VARIABLES); + m_dwStartupTimeLimitInMS = aspNetCoreSection->GetRequiredLong(CS_ASPNETCORE_PROCESS_STARTUP_TIME_LIMIT) * 1000; + m_dwShutdownTimeLimitInMS = aspNetCoreSection->GetRequiredLong(CS_ASPNETCORE_PROCESS_SHUTDOWN_TIME_LIMIT) * 1000; const auto basicAuthSection = configurationSource.GetSection(CS_BASIC_AUTHENTICATION_SECTION); m_fBasicAuthEnabled = basicAuthSection && basicAuthSection->GetBool(CS_ENABLED).value_or(false); diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h index c0509b3003..e299c2033e 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/InProcessOptions.h @@ -5,6 +5,7 @@ #include #include "ConfigurationSource.h" +#include "WebConfigConfigurationSource.h" class InProcessOptions: NonCopyable { @@ -60,12 +61,22 @@ public: DWORD QueryStartupTimeLimitInMS() const { + if (IsDebuggerPresent()) + { + return INFINITE; + } + return m_dwStartupTimeLimitInMS; } DWORD QueryShutdownTimeLimitInMS() const { + if (IsDebuggerPresent()) + { + return INFINITE; + } + return m_dwShutdownTimeLimitInMS; } @@ -76,6 +87,12 @@ public: } InProcessOptions(const ConfigurationSource &configurationSource); + + static + HRESULT InProcessOptions::Create( + IHttpServer& pServer, + IHttpApplication& pHttpApplication, + std::unique_ptr& options); private: std::wstring m_strArguments; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.cpp index fbb5350d62..b198e95909 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.cpp @@ -5,6 +5,6 @@ HRESULT StartupExceptionApplication::CreateHandler(IHttpContext *pHttpContext, IREQUEST_HANDLER ** pRequestHandler) { - *pRequestHandler = new StartupExceptionHandler(pHttpContext, m_disableLogs, this); + *pRequestHandler = new StartupExceptionHandler(pHttpContext, m_disableLogs); return S_OK; } diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h index 369c57adfe..d3505315be 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionApplication.h @@ -16,29 +16,6 @@ public: : m_disableLogs(disableLogs), InProcessApplicationBase(pServer, pApplication) { - html500Page = std::string(" \ - \ - \ - \ - IIS 500.30 Error \ -
\ -

HTTP Error 500.30 - ANCM In-Process Start Failure

\ -
\ -

Common causes of this issue:

\ -
  • The application failed to start
  • \ -
  • The application started but then stopped
  • \ -
  • The application started but threw an exception during startup
\ -
\ -
\ -

Troubleshooting steps:

\ -
  • Check the system event log for error messages
  • \ -
  • Enable logging the application process' stdout messages
  • \ -
  • Attach a debugger to the application process and inspect
\ -

For more information visit: \ - https://go.microsoft.com/fwlink/?LinkID=808681

\ -
\ -
\ -
"); } ~StartupExceptionApplication() = default; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.cpp index f8041a0f13..d180460898 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.cpp @@ -4,6 +4,30 @@ #include "StartupExceptionApplication.h" #include "StartupExceptionHandler.h" +std::string StartupExceptionHandler::s_html500Page = " \ + \ + \ + \ + IIS 500.30 Error \ +
\ +

HTTP Error 500.30 - ANCM In-Process Start Failure

\ +
\ +

Common causes of this issue:

\ +
  • The application failed to start
  • \ +
  • The application started but then stopped
  • \ +
  • The application started but threw an exception during startup
\ +
\ +
\ +

Troubleshooting steps:

\ +
  • Check the system event log for error messages
  • \ +
  • Enable logging the application process' stdout messages
  • \ +
  • Attach a debugger to the application process and inspect
\ +

For more information visit: \ + https://go.microsoft.com/fwlink/?LinkID=808681

\ +
\ +
\ +
"; + REQUEST_NOTIFICATION_STATUS StartupExceptionHandler::OnExecuteRequestHandler() { if (!m_disableLogs) @@ -16,11 +40,9 @@ REQUEST_NOTIFICATION_STATUS StartupExceptionHandler::OnExecuteRequestHandler() (USHORT)strlen("text/html"), FALSE ); - const std::string& html500Page = m_pApplication->GetStaticHtml500Content(); - DataChunk.DataChunkType = HttpDataChunkFromMemory; - DataChunk.FromMemory.pBuffer = (PVOID)html500Page.c_str(); - DataChunk.FromMemory.BufferLength = (ULONG)html500Page.size(); + DataChunk.FromMemory.pBuffer = (PVOID)s_html500Page.c_str(); + DataChunk.FromMemory.BufferLength = (ULONG)s_html500Page.size(); pResponse->WriteEntityChunkByReference(&DataChunk); } else diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.h index 9b5a9ba45d..266474e1b5 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/StartupExceptionHandler.h @@ -3,6 +3,7 @@ #pragma once +#include #include "requesthandler.h" class StartupExceptionApplication; @@ -10,19 +11,23 @@ class StartupExceptionApplication; class StartupExceptionHandler : public REQUEST_HANDLER { public: - StartupExceptionHandler(IHttpContext* pContext, BOOL disableLogs, StartupExceptionApplication* pApplication) + StartupExceptionHandler(IHttpContext* pContext, BOOL disableLogs) : m_pContext(pContext), - m_disableLogs(disableLogs), - m_pApplication(pApplication) + m_disableLogs(disableLogs) { } - + ~StartupExceptionHandler() + { + + } REQUEST_NOTIFICATION_STATUS OnExecuteRequestHandler() override; private: IHttpContext * m_pContext; BOOL m_disableLogs; - StartupExceptionApplication* m_pApplication; + + static + std::string s_html500Page; }; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp index 90779ce6d8..32a314dd50 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/dllmain.cpp @@ -110,39 +110,25 @@ CreateApplication( return S_OK; } - const WebConfigConfigurationSource configurationSource(pServer->GetAdminManager(), *pHttpApplication); - auto pConfig = std::make_unique(configurationSource); - - BOOL disableStartupPage = pConfig->QueryDisableStartUpErrorPage(); - - auto pApplication = std::make_unique(*pServer, *pHttpApplication, std::move(pConfig), pParameters, nParameters); - // never create two inprocess applications in one process g_fInProcessApplicationCreated = true; - if (FAILED_LOG(pApplication->LoadManagedApplication())) + + std::unique_ptr inProcessApplication; + if (!FAILED_LOG(IN_PROCESS_APPLICATION::Start(*pServer, *pHttpApplication, pParameters, nParameters, inProcessApplication))) { + *ppApplication = inProcessApplication.release(); + } + else + { + std::unique_ptr options; + THROW_IF_FAILED(InProcessOptions::Create(*pServer, *pHttpApplication, options)); // Set the currently running application to a fake application that returns startup exceptions. - auto pErrorApplication = std::make_unique(*pServer, *pHttpApplication, disableStartupPage); + auto pErrorApplication = std::make_unique(*pServer, *pHttpApplication, options->QueryDisableStartUpErrorPage()); RETURN_IF_FAILED(pErrorApplication->StartMonitoringAppOffline()); *ppApplication = pErrorApplication.release(); } - else - { - RETURN_IF_FAILED(pApplication->StartMonitoringAppOffline()); - *ppApplication = pApplication.release(); - } - } - catch(ConfigurationLoadException &ex) - { - EventLog::Error( - ASPNETCORE_CONFIGURATION_LOAD_ERROR, - ASPNETCORE_CONFIGURATION_LOAD_ERROR_MSG, - ex.get_message().c_str()); - - RETURN_HR(E_FAIL); + return S_OK; } CATCH_RETURN(); - - return S_OK; } diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index 5f43952287..942bfe44b0 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -6,13 +6,11 @@ #include "hostfxroptions.h" #include "requesthandler_config.h" #include "environmentvariablehelpers.h" -#include "SRWExclusiveLock.h" #include "exceptions.h" #include "LoggingHelpers.h" #include "resources.h" #include "EventLog.h" - -const LPCSTR IN_PROCESS_APPLICATION::s_exeLocationParameterName = "InProcessExeLocation"; +#include "ModuleHelpers.h" IN_PROCESS_APPLICATION* IN_PROCESS_APPLICATION::s_Application = NULL; @@ -23,10 +21,9 @@ IN_PROCESS_APPLICATION::IN_PROCESS_APPLICATION( APPLICATION_PARAMETER *pParameters, DWORD nParameters) : InProcessApplicationBase(pHttpServer, pApplication), - m_ProcessExitCode(0), - m_fBlockCallbacksIntoManaged(FALSE), - m_fShutdownCalledFromNative(FALSE), - m_fShutdownCalledFromManaged(FALSE), + m_Initialized(false), + m_blockManagedCallbacks(true), + m_waitForShutdown(true), m_pConfig(std::move(pConfig)) { DBG_ASSERT(m_pConfig); @@ -35,161 +32,54 @@ IN_PROCESS_APPLICATION::IN_PROCESS_APPLICATION( { if (_stricmp(pParameters[i].pzName, s_exeLocationParameterName) == 0) { - m_struExeLocation.Copy(reinterpret_cast(pParameters[i].pValue)); + m_dotnetExeKnownLocation = reinterpret_cast(pParameters[i].pValue); } } - - m_status = MANAGED_APPLICATION_STATUS::STARTING; } IN_PROCESS_APPLICATION::~IN_PROCESS_APPLICATION() { - s_Application = NULL; + s_Application = nullptr; } -//static -DWORD WINAPI -IN_PROCESS_APPLICATION::DoShutDown( - LPVOID lpParam -) -{ - IN_PROCESS_APPLICATION* pApplication = static_cast(lpParam); - DBG_ASSERT(pApplication); - pApplication->ShutDownInternal(); - return 0; -} - -__override VOID IN_PROCESS_APPLICATION::StopInternal(bool fServerInitiated) -{ - UNREFERENCED_PARAMETER(fServerInitiated); - HRESULT hr = S_OK; - CHandle hThread; - DWORD dwThreadStatus = 0; - - DWORD dwTimeout = m_pConfig->QueryShutdownTimeLimitInMS(); - - if (IsDebuggerPresent()) - { - dwTimeout = INFINITE; - } - - hThread.Attach(CreateThread( - NULL, // default security attributes - 0, // default stack size - (LPTHREAD_START_ROUTINE)DoShutDown, - this, // thread function arguments - 0, // default creation flags - NULL)); // receive thread identifier - - if ((HANDLE)hThread == NULL) - { - hr = HRESULT_FROM_WIN32(GetLastError()); - goto Finished; - } - - if (WaitForSingleObject(hThread, dwTimeout) != WAIT_OBJECT_0) - { - // if the thread is still running, we need kill it first before exit to avoid AV - if (GetExitCodeThread(m_hThread, &dwThreadStatus) != 0 && dwThreadStatus == STILL_ACTIVE) - { - // Calling back into managed at this point is prone to have AVs - // Calling terminate thread here may be our best solution. - TerminateThread(hThread, STATUS_CONTROL_C_EXIT); - hr = HRESULT_FROM_WIN32(ERROR_TIMEOUT); - } - } - -Finished: - - if (FAILED(hr)) - { - EventLog::Warn( - ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE, - ASPNETCORE_EVENT_APP_SHUTDOWN_FAILURE_MSG, - QueryConfigPath().c_str()); - } - else - { - EventLog::Info( - ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL, - ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL_MSG, - QueryConfigPath().c_str()); - } - +{ + StopClr(); InProcessApplicationBase::StopInternal(fServerInitiated); } VOID -IN_PROCESS_APPLICATION::ShutDownInternal() -{ - DWORD dwThreadStatus = 0; - DWORD dwTimeout = m_pConfig->QueryShutdownTimeLimitInMS(); +IN_PROCESS_APPLICATION::StopClr() +{ + LOG_INFO(L"Stopping CLR"); - if (IsDebuggerPresent()) + if (!m_blockManagedCallbacks) { - dwTimeout = INFINITE; - } + // 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, + // and usually results in an AV saying "The string binding is invalid" + const auto shutdownHandler = m_ShutdownHandler; - if (m_fShutdownCalledFromNative || - m_status == MANAGED_APPLICATION_STATUS::STARTING || - m_status == MANAGED_APPLICATION_STATUS::FAIL) - { - return; - } - - { - if (m_fShutdownCalledFromNative || - m_status == MANAGED_APPLICATION_STATUS::STARTING || - m_status == MANAGED_APPLICATION_STATUS::FAIL) + if (!g_fProcessDetach && shutdownHandler != nullptr) { - return; - } - - // We need to keep track of when both managed and native initiate shutdown - // to avoid AVs. If shutdown has already been initiated in managed, we don't want to call into - // 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 = MANAGED_APPLICATION_STATUS::SHUTDOWN; - - if (!m_fShutdownCalledFromManaged) - { - // 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, - // and usually results in an AV saying "The string binding is invalid" - if (!g_fProcessDetach) - { - m_ShutdownHandler(m_ShutdownHandlerContext); - m_ShutdownHandler = NULL; - } - } - - // Release the lock before we wait on the thread to exit. - } - - if (!m_fShutdownCalledFromManaged) - { - if (m_hThread != NULL && - GetExitCodeThread(m_hThread, &dwThreadStatus) != 0 && - dwThreadStatus == STILL_ACTIVE) - { - // wait for graceful shutdown, i.e., the exit of the background thread or timeout - if (WaitForSingleObject(m_hThread, dwTimeout) != WAIT_OBJECT_0) - { - // if the thread is still running, we need kill it first before exit to avoid AV - if (GetExitCodeThread(m_hThread, &dwThreadStatus) != 0 && dwThreadStatus == STILL_ACTIVE) - { - // Calling back into managed at this point is prone to have AVs - // Calling terminate thread here may be our best solution. - TerminateThread(m_hThread, STATUS_CONTROL_C_EXIT); - } - } + shutdownHandler(m_ShutdownHandlerContext); } } - s_Application = NULL; + // 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(); + } + + s_Application = nullptr; } VOID @@ -201,169 +91,320 @@ IN_PROCESS_APPLICATION::SetCallbackHandles( _In_ VOID* pvShutdownHandlerContext ) { + LOG_INFO(L"In-process callbacks set"); + m_RequestHandler = request_handler; m_RequestHandlerContext = pvRequstHandlerContext; m_ShutdownHandler = shutdown_handler; m_ShutdownHandlerContext = pvShutdownHandlerContext; m_AsyncCompletionHandler = async_completion_handler; + m_blockManagedCallbacks = false; + m_Initialized = true; + // Can't check the std err handle as it isn't a critical error // Initialization complete EventLog::Info( ASPNETCORE_EVENT_INPROCESS_START_SUCCESS, ASPNETCORE_EVENT_INPROCESS_START_SUCCESS_MSG, QueryApplicationPhysicalPath().c_str()); - SetEvent(m_pInitalizeEvent); - m_fInitialized = TRUE; + + SetEvent(m_pInitializeEvent); } -// Will be called by the inprocesshandler HRESULT -IN_PROCESS_APPLICATION::LoadManagedApplication -( - VOID -) +IN_PROCESS_APPLICATION::LoadManagedApplication() { - HRESULT hr = S_OK; - DWORD dwTimeout; - DWORD dwResult; + THROW_LAST_ERROR_IF_NULL(m_pInitializeEvent = CreateEvent( + nullptr, // default security attributes + TRUE, // manual reset event + FALSE, // not set + nullptr)); // name - ReferenceApplication(); + THROW_LAST_ERROR_IF_NULL(m_pShutdownEvent = CreateEvent( + nullptr, // default security attributes + TRUE, // manual reset event + FALSE, // not set + nullptr)); // name - if (m_status != MANAGED_APPLICATION_STATUS::STARTING) + m_workerThread = std::thread([](std::unique_ptr application) { - // Core CLR has already been loaded. - // Cannot load more than once even there was a failure - if (m_status == MANAGED_APPLICATION_STATUS::FAIL) - { - hr = E_APPLICATION_ACTIVATION_EXEC_FAILURE; - } - else if (m_status == MANAGED_APPLICATION_STATUS::SHUTDOWN) - { - hr = HRESULT_FROM_WIN32(ERROR_SHUTDOWN_IS_SCHEDULED); - } + LOG_INFO(L"Starting in-process worker thread"); + application->ExecuteApplication(); + LOG_INFO(L"Stopping in-process worker thread"); + }, ::ReferenceApplication(this)); - goto Finished; + LOG_INFO(L"Waiting for initialization"); + + const HANDLE waitHandles[2] = { m_pInitializeEvent, m_workerThread.native_handle() }; + + // Wait for shutdown request + const auto waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, m_pConfig->QueryStartupTimeLimitInMS()); + THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); + + if (waitResult == WAIT_TIMEOUT) + { + // If server wasn't initialized in time shut application down without waiting for CLR thread to exit + m_waitForShutdown = false; + StopClr(); + throw InvalidOperationException(format(L"Managed server didn't initialize after %u ms.", m_pConfig->QueryStartupTimeLimitInMS())); } + // WAIT_OBJECT_0 + 1 is the worker thead handle + if (waitResult == WAIT_OBJECT_0 + 1) { - SRWExclusiveLock lock(m_stateLock); + // Worker thread exited stop + StopClr(); + throw InvalidOperationException(format(L"CLR worker thread exited prematurely")); + } - if (m_status != MANAGED_APPLICATION_STATUS::STARTING) + THROW_IF_FAILED(StartMonitoringAppOffline()); + + return S_OK; +} + + +void +IN_PROCESS_APPLICATION::ExecuteApplication() +{ + try + { + std::unique_ptr hostFxrOptions; + + auto context = std::make_shared(); + + auto pProc = s_fMainCallback; + if (pProc == nullptr) { - if (m_status == MANAGED_APPLICATION_STATUS::FAIL) + HMODULE hModule; + // hostfxr should already be loaded by the shim. If not, then we will need + // to load it ourselves by finding hostfxr again. + THROW_LAST_ERROR_IF_NULL(hModule = GetModuleHandle(L"hostfxr.dll")); + + // Get the entry point for main + pProc = reinterpret_cast(GetProcAddress(hModule, "hostfxr_main")); + THROW_LAST_ERROR_IF_NULL(pProc); + + THROW_IF_FAILED(HOSTFXR_OPTIONS::Create( + m_dotnetExeKnownLocation, + m_pConfig->QueryProcessPath(), + QueryApplicationPhysicalPath(), + m_pConfig->QueryArguments(), + hostFxrOptions + )); + + hostFxrOptions->GetArguments(context->m_argc, context->m_argv); + THROW_IF_FAILED(SetEnvironmentVariablesOnWorkerProcess()); + } + context->m_pProc = pProc; + + if (m_pLoggerProvider == nullptr) + { + THROW_IF_FAILED(LoggingHelpers::CreateLoggingProvider( + m_pConfig->QueryStdoutLogEnabled(), + !m_pHttpServer.IsCommandLineLaunch(), + m_pConfig->QueryStdoutLogFile().c_str(), + QueryApplicationPhysicalPath().c_str(), + m_pLoggerProvider)); + + LOG_IF_FAILED(m_pLoggerProvider->Start()); + } + + // There can only ever be a single instance of .NET Core + // loaded in the process but we need to get config information to boot it up in the + // first place. This is happening in an execute request handler and everyone waits + // until this initialization is done. + // We set a static so that managed code can call back into this instance and + // set the callbacks + s_Application = this; + + //Start CLR thread + m_clrThread = std::thread(ClrThreadEntryPoint, context); + + // Wait for thread exit or shutdown event + const HANDLE waitHandles[2] = { m_pShutdownEvent, m_clrThread.native_handle() }; + + // Wait for shutdown request + const auto waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE); + THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); + + LOG_INFOF(L"Starting shutdown sequence %d", waitResult); + + bool clrThreadExited = waitResult == (WAIT_OBJECT_0 + 1); + // shutdown was signaled + // only wait for shutdown in case of successful startup + if (m_waitForShutdown) + { + const auto clrWaitResult = WaitForSingleObject(m_clrThread.native_handle(), m_pConfig->QueryShutdownTimeLimitInMS()); + THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); + + clrThreadExited = clrWaitResult != WAIT_TIMEOUT; + } + + LOG_INFOF(L"Clr thread wait ended: clrThreadExited: %d", clrThreadExited); + + // At this point CLR thread either finished or timed out, abandon it. + m_clrThread.detach(); + + LOG_IF_FAILED(m_pLoggerProvider->Stop()); + + if (m_fStopCalled) + { + if (clrThreadExited) { - hr = E_APPLICATION_ACTIVATION_EXEC_FAILURE; + EventLog::Info( + ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL, + ASPNETCORE_EVENT_APP_SHUTDOWN_SUCCESSFUL_MSG, + QueryConfigPath().c_str()); } - else if (m_status == MANAGED_APPLICATION_STATUS::SHUTDOWN) + else { - hr = HRESULT_FROM_WIN32(ERROR_SHUTDOWN_IS_SCHEDULED); + EventLog::Warn( + ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE, + ASPNETCORE_EVENT_APP_SHUTDOWN_FAILURE_MSG, + QueryConfigPath().c_str()); } - - goto Finished; - } - m_hThread = CreateThread( - NULL, // default security attributes - 0, // default stack size - (LPTHREAD_START_ROUTINE)ExecuteAspNetCoreProcess, - this, // thread function arguments - 0, // default creation flags - NULL); // receive thread identifier - - if (m_hThread == NULL) - { - hr = HRESULT_FROM_WIN32(GetLastError()); - goto Finished; - } - - m_pInitalizeEvent = CreateEvent( - NULL, // default security attributes - TRUE, // manual reset event - FALSE, // not set - NULL); // name - - if (m_pInitalizeEvent == NULL) - { - hr = HRESULT_FROM_WIN32(GetLastError()); - } - - // If the debugger is attached, never timeout - if (IsDebuggerPresent()) - { - dwTimeout = INFINITE; } else { - dwTimeout = m_pConfig->QueryStartupTimeLimitInMS(); + if (clrThreadExited) + { + UnexpectedThreadExit(*context); + // If the inprocess server was initialized, we need to cause recycle to be called on the worker process. + // in case when it was not initialized we need to keep server running to serve 502 page + if (m_Initialized) + { + QueueStop(); + } + } } - - const HANDLE pHandles[2]{ m_hThread, m_pInitalizeEvent }; - - // Wait on either the thread to complete or the event to be set - dwResult = WaitForMultipleObjects(2, pHandles, FALSE, dwTimeout); - - // It all timed out - if (dwResult == WAIT_TIMEOUT) - { - // kill the backend thread as loading dotnet timedout - TerminateThread(m_hThread, 0); - hr = HRESULT_FROM_WIN32(dwResult); - goto Finished; - } - else if (dwResult == WAIT_FAILED) - { - hr = HRESULT_FROM_WIN32(GetLastError()); - goto Finished; - } - - // The thread ended it means that something failed - if (dwResult == WAIT_OBJECT_0) - { - hr = E_APPLICATION_ACTIVATION_EXEC_FAILURE; - goto Finished; - } - - m_status = MANAGED_APPLICATION_STATUS::RUNNING_MANAGED; } -Finished: - - if (FAILED(hr)) + catch (InvalidOperationException& ex) { - m_status = MANAGED_APPLICATION_STATUS::FAIL; - EventLog::Error( ASPNETCORE_EVENT_LOAD_CLR_FALIURE, ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG, QueryApplicationId().c_str(), QueryApplicationPhysicalPath().c_str(), - hr); - } - DereferenceApplication(); + ex.as_wstring().c_str()); - return hr; + OBSERVE_CAUGHT_EXCEPTION(); + } + catch (std::runtime_error& ex) + { + EventLog::Error( + ASPNETCORE_EVENT_LOAD_CLR_FALIURE, + ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + GetUnexpectedExceptionMessage(ex).c_str()); + + OBSERVE_CAUGHT_EXCEPTION(); + } } -// static -VOID -IN_PROCESS_APPLICATION::ExecuteAspNetCoreProcess( - _In_ LPVOID pContext -) +void IN_PROCESS_APPLICATION::QueueStop() { - HRESULT hr = S_OK; - IN_PROCESS_APPLICATION *pApplication = (IN_PROCESS_APPLICATION*)pContext; - DBG_ASSERT(pApplication != NULL); - hr = pApplication->ExecuteApplication(); - // - // no need to log the error here as if error happened, the thread will exit - // the error will ba catched by caller LoadManagedApplication which will log an error - // + if (m_fStopCalled) + { + return; + } + 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)); + + stoppingThread.detach(); +} + +HRESULT IN_PROCESS_APPLICATION::Start( + IHttpServer& pServer, + IHttpApplication& pHttpApplication, + APPLICATION_PARAMETER* pParameters, + DWORD nParameters, + std::unique_ptr& application) +{ + try + { + std::unique_ptr options; + THROW_IF_FAILED(InProcessOptions::Create(pServer, pHttpApplication, options)); + application = std::unique_ptr( + new IN_PROCESS_APPLICATION(pServer, pHttpApplication, std::move(options), pParameters, nParameters)); + THROW_IF_FAILED(application->LoadManagedApplication()); + return S_OK; + } + catch (InvalidOperationException& ex) + { + EventLog::Error( + ASPNETCORE_EVENT_LOAD_CLR_FALIURE, + ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG, + pHttpApplication.GetApplicationId(), + pHttpApplication.GetApplicationPhysicalPath(), + ex.as_wstring().c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + catch (std::runtime_error& ex) + { + EventLog::Error( + ASPNETCORE_EVENT_LOAD_CLR_FALIURE, + ASPNETCORE_EVENT_LOAD_CLR_FALIURE_MSG, + pHttpApplication.GetApplicationId(), + pHttpApplication.GetApplicationPhysicalPath(), + GetUnexpectedExceptionMessage(ex).c_str()); + + RETURN_CAUGHT_EXCEPTION(); + } + CATCH_RETURN(); +} + +// Required because __try and objects with destructors can not be mixed +void +IN_PROCESS_APPLICATION::ExecuteClr(const std::shared_ptr& context) +{ + __try + { + auto const exitCode = context->m_pProc(context->m_argc, context->m_argv.get()); + + LOG_INFOF(L"Managed application exited with code %d", exitCode); + + context->m_exitCode = exitCode; + } + __except(GetExceptionCode() != 0) + { + LOG_INFOF(L"Managed threw an exception %d", GetExceptionCode()); + + context->m_exceptionCode = GetExceptionCode(); + } +} + +// +// Calls hostfxr_main with the hostfxr and application as arguments. +// This method should not access IN_PROCESS_APPLICATION instance as it may be already freed +// in case of startup timeout +// +VOID +IN_PROCESS_APPLICATION::ClrThreadEntryPoint(const std::shared_ptr &context) +{ + LOG_INFO(L"Starting CLR thread"); + + // Keep aspnetcorev2_inprocess.dll loaded while this thread is running + // this is required because thread might be abandoned + HandleWrapper moduleHandle; + ModuleHelpers::IncrementCurrentModuleRefCount(moduleHandle); + + ExecuteClr(context); + + FreeLibraryAndExitThread(moduleHandle.release(), 0); } HRESULT -IN_PROCESS_APPLICATION::SetEnvironementVariablesOnWorkerProcess( - VOID -) +IN_PROCESS_APPLICATION::SetEnvironmentVariablesOnWorkerProcess() { auto variables = m_pConfig->QueryEnvironmentVariables(); auto inputTable = std::unique_ptr(new ENVIRONMENT_VAR_HASH()); @@ -397,128 +438,53 @@ IN_PROCESS_APPLICATION::SetEnvironementVariablesOnWorkerProcess( return S_OK; } -HRESULT -IN_PROCESS_APPLICATION::ExecuteApplication( - VOID -) -{ - HRESULT hr; - HMODULE hModule = nullptr; - hostfxr_main_fn pProc; - std::unique_ptr hostFxrOptions = NULL; - DWORD hostfxrArgc = 0; - std::unique_ptr hostfxrArgv; - DBG_ASSERT(m_status == MANAGED_APPLICATION_STATUS::STARTING); - - pProc = s_fMainCallback; - if (pProc == nullptr) - { - // hostfxr should already be loaded by the shim. If not, then we will need - // to load it ourselves by finding hostfxr again. - hModule = LoadLibraryW(L"hostfxr.dll"); - - if (hModule == NULL) - { - // .NET Core not installed (we can log a more detailed error message here) - hr = LOG_IF_FAILED(ERROR_BAD_ENVIRONMENT); - goto Finished; - } - - // Get the entry point for main - pProc = (hostfxr_main_fn)GetProcAddress(hModule, "hostfxr_main"); - if (pProc == NULL) - { - hr = LOG_IF_FAILED(ERROR_BAD_ENVIRONMENT); - goto Finished; - } - - FINISHED_IF_FAILED(hr = HOSTFXR_OPTIONS::Create( - m_struExeLocation.QueryStr(), - m_pConfig->QueryProcessPath().c_str(), - QueryApplicationPhysicalPath().c_str(), - m_pConfig->QueryArguments().c_str(), - hostFxrOptions - )); - hostFxrOptions->GetArguments(hostfxrArgc, hostfxrArgv); - FINISHED_IF_FAILED(SetEnvironementVariablesOnWorkerProcess()); - } - - LOG_INFO(L"Starting managed application"); - - if (m_pLoggerProvider == NULL) - { - FINISHED_IF_FAILED(hr = LoggingHelpers::CreateLoggingProvider( - m_pConfig->QueryStdoutLogEnabled(), - !m_pHttpServer.IsCommandLineLaunch(), - m_pConfig->QueryStdoutLogFile().c_str(), - QueryApplicationPhysicalPath().c_str(), - m_pLoggerProvider)); - - LOG_IF_FAILED(m_pLoggerProvider->Start()); - } - - // There can only ever be a single instance of .NET Core - // loaded in the process but we need to get config information to boot it up in the - // first place. This is happening in an execute request handler and everyone waits - // until this initialization is done. - // We set a static so that managed code can call back into this instance and - // set the callbacks - s_Application = this; - - hr = RunDotnetApplication(hostfxrArgc, hostfxrArgv.get(), pProc); - -Finished: - - // - // this method is called by the background thread and should never exit unless shutdown - // If main returned and shutdown was not called in managed, we want to block native from calling into - // managed. To do this, we can say that shutdown was called from managed. - // Don't bother locking here as there will always be a race between receiving a native shutdown - // notification and unexpected managed exit. - // - m_status = MANAGED_APPLICATION_STATUS::SHUTDOWN; - m_fShutdownCalledFromManaged = TRUE; - - m_pLoggerProvider->Stop(); - - if (!m_fShutdownCalledFromNative) - { - LogErrorsOnMainExit(hr); - if (m_fInitialized) - { - // - // If the inprocess server was initialized, we need to cause recycle to be called on the worker process. - // - Stop(/*fServerInitiated*/ false); - } - } - - return hr; -} - VOID -IN_PROCESS_APPLICATION::LogErrorsOnMainExit( - HRESULT hr -) +IN_PROCESS_APPLICATION::UnexpectedThreadExit(const ExecuteClrContext& context) const { + STRA straStdErrOutput; + STRU struStdMsg; + auto hasStdOut = m_pLoggerProvider->GetStdOutContent(&straStdErrOutput) && + SUCCEEDED(struStdMsg.CopyA(straStdErrOutput.QueryStr())); + + if (context.m_exceptionCode != 0) + { + if (hasStdOut) + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION, + ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_STDOUT_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + context.m_exceptionCode, + struStdMsg.QueryStr()); + } + else + { + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION, + ASPNETCORE_EVENT_INPROCESS_THREAD_EXCEPTION_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + context.m_exceptionCode + ); + } + return; + } + // // Ungraceful shutdown, try to log an error message. // This will be a common place for errors as it means the hostfxr_main returned // or there was an exception. // - STRA straStdErrOutput; - STRU struStdMsg; - if (m_pLoggerProvider->GetStdOutContent(&straStdErrOutput)) + if (hasStdOut) { - if (SUCCEEDED(struStdMsg.CopyA(straStdErrOutput.QueryStr()))) { - EventLog::Error( - ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT, - ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT_MSG, - QueryApplicationId().c_str(), - QueryApplicationPhysicalPath().c_str(), - hr, - struStdMsg.QueryStr()); - } + EventLog::Error( + ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT, + ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_STDOUT_MSG, + QueryApplicationId().c_str(), + QueryApplicationPhysicalPath().c_str(), + context.m_exitCode, + struStdMsg.QueryStr()); } else { @@ -527,53 +493,20 @@ IN_PROCESS_APPLICATION::LogErrorsOnMainExit( ASPNETCORE_EVENT_INPROCESS_THREAD_EXIT_MSG, QueryApplicationId().c_str(), QueryApplicationPhysicalPath().c_str(), - hr); + context.m_exitCode); } } -// -// Calls hostfxr_main with the hostfxr and application as arguments. -// -HRESULT -IN_PROCESS_APPLICATION::RunDotnetApplication(DWORD argc, CONST PCWSTR* argv, hostfxr_main_fn pProc) -{ - HRESULT hr = S_OK; - - __try - { - m_ProcessExitCode = pProc(argc, argv); - if (m_ProcessExitCode != 0) - { - hr = HRESULT_FROM_WIN32(GetLastError()); - } - - LOG_INFOF(L"Managed application exited with code %d", m_ProcessExitCode); - } - __except(GetExceptionCode() != 0) - { - - LOG_INFOF(L"Managed threw an exception %d", GetExceptionCode()); - hr = HRESULT_FROM_WIN32(GetLastError()); - } - - return hr; -} - HRESULT IN_PROCESS_APPLICATION::CreateHandler( _In_ IHttpContext *pHttpContext, _Out_ IREQUEST_HANDLER **pRequestHandler) { - HRESULT hr = S_OK; - IREQUEST_HANDLER* pHandler = NULL; - - pHandler = new IN_PROCESS_HANDLER(::ReferenceApplication(this), pHttpContext, m_RequestHandler, m_RequestHandlerContext, m_AsyncCompletionHandler); - - if (pHandler == NULL) + try { - hr = HRESULT_FROM_WIN32(ERROR_OUTOFMEMORY); + *pRequestHandler = new IN_PROCESS_HANDLER(::ReferenceApplication(this), pHttpContext, m_RequestHandler, m_RequestHandlerContext, m_AsyncCompletionHandler); } - - *pRequestHandler = pHandler; - return hr; + CATCH_RETURN(); + + return S_OK; } diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h index 7183ad86b3..cab7a49e9b 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h @@ -3,6 +3,7 @@ #pragma once +#include #include "InProcessApplicationBase.h" #include "IOutputManager.h" #include "InProcessOptions.h" @@ -45,35 +46,26 @@ public: override; // Executes the .NET Core process + void + ExecuteApplication(); + HRESULT - ExecuteApplication( - VOID - ); + LoadManagedApplication(); - HRESULT - LoadManagedApplication( - VOID - ); - VOID - LogErrorsOnMainExit( - HRESULT hr - ); - - VOID - StopCallsIntoManaged( - VOID - ) + void + QueueStop(); + + void + StopIncomingRequests() { - m_fBlockCallbacksIntoManaged = TRUE; + QueueStop(); } - VOID - StopIncomingRequests( - VOID - ) + void + StopCallsIntoManaged() { - m_fShutdownCalledFromManaged = TRUE; + m_blockManagedCallbacks = true; } static @@ -84,44 +76,64 @@ public: static IN_PROCESS_APPLICATION* - GetInstance( - VOID - ) + GetInstance() { return s_Application; } - PCWSTR - QueryExeLocation() + const std::wstring& + QueryExeLocation() const { - return m_struExeLocation.QueryStr(); + return m_dotnetExeKnownLocation; } const InProcessOptions& QueryConfig() const { - return *m_pConfig.get(); + return *m_pConfig; } bool QueryBlockCallbacksIntoManaged() const { - return m_fBlockCallbacksIntoManaged; + return m_blockManagedCallbacks; } + static + HRESULT Start( + IHttpServer& pServer, + IHttpApplication& pHttpApplication, + APPLICATION_PARAMETER* pParameters, + DWORD nParameters, + std::unique_ptr& application); + private: - - enum MANAGED_APPLICATION_STATUS + struct ExecuteClrContext: std::enable_shared_from_this { - UNKNOWN = 0, - STARTING, - RUNNING_MANAGED, - SHUTDOWN, - FAIL - }; + ExecuteClrContext(): + m_argc(0), + m_pProc(nullptr), + m_exitCode(0), + m_exceptionCode(0) + { + } - // Thread executing the .NET Core process - HandleWrapper m_hThread; + DWORD m_argc; + std::unique_ptr m_argv; + hostfxr_main_fn m_pProc; + + int m_exitCode; + int m_exceptionCode; + }; + + // Thread executing the .NET Core process this might be abandoned in timeout cases + std::thread m_clrThread; + // Thread tracking the CLR thread, this one is always joined on shutdown + std::thread m_workerThread; + // The event that gets triggered when managed initialization is complete + HandleWrapper m_pInitializeEvent; + // The event that gets triggered when worker thread should exit + HandleWrapper m_pShutdownEvent; // The request handler callback from managed code PFN_REQUEST_HANDLER m_RequestHandler; @@ -133,53 +145,38 @@ private: PFN_ASYNC_COMPLETION_HANDLER m_AsyncCompletionHandler; - // The event that gets triggered when managed initialization is complete - HandleWrapper m_pInitalizeEvent; + std::wstring m_dotnetExeKnownLocation; - STRU m_struExeLocation; + std::atomic_bool m_blockManagedCallbacks; + bool m_Initialized; + bool m_waitForShutdown; - // The exit code of the .NET Core process - INT m_ProcessExitCode; - - volatile BOOL m_fBlockCallbacksIntoManaged; - volatile BOOL m_fShutdownCalledFromNative; - volatile BOOL m_fShutdownCalledFromManaged; - BOOL m_fInitialized; - MANAGED_APPLICATION_STATUS m_status; std::unique_ptr m_pConfig; static IN_PROCESS_APPLICATION* s_Application; std::unique_ptr m_pLoggerProvider; - static const LPCSTR s_exeLocationParameterName; - - static - VOID - ExecuteAspNetCoreProcess( - _In_ LPVOID pContext - ); - - HRESULT - SetEnvironementVariablesOnWorkerProcess( - VOID - ); - - HRESULT - RunDotnetApplication( - DWORD argc, - CONST PCWSTR* argv, - hostfxr_main_fn pProc - ); - - static - DWORD WINAPI - DoShutDown( - LPVOID lpParam - ); + inline static const LPCSTR s_exeLocationParameterName = "InProcessExeLocation"; VOID - ShutDownInternal( - VOID - ); + UnexpectedThreadExit(const ExecuteClrContext& context) const; + + HRESULT + SetEnvironmentVariablesOnWorkerProcess(); + + void + StopClr(); + + static + void + ClrThreadEntryPoint(const std::shared_ptr &context); + + static + void + ExecuteClr(const std::shared_ptr &context); + + // Allows to override call to hostfxr_main with custom callback + // used in testing + inline static hostfxr_main_fn s_fMainCallback = nullptr; }; diff --git a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h index ac6aa24789..6089adc3ca 100644 --- a/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h +++ b/src/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h @@ -40,9 +40,7 @@ public: ) override; IHttpContext* - QueryHttpContext( - VOID - ) const + QueryHttpContext() const { return m_pW3Context; } @@ -53,9 +51,7 @@ public: ); VOID - IndicateManagedRequestComplete( - VOID - ); + IndicateManagedRequestComplete(); VOID SetAsyncCompletionStatus( @@ -68,11 +64,11 @@ public: static HRESULT - StaticInitialize(VOID); + StaticInitialize(); static void - StaticTerminate(VOID); + StaticTerminate(); private: REQUEST_NOTIFICATION_STATUS diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs index 485c61cf4c..3cb2d40c3a 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISDeploymentParameterExtensions.cs @@ -60,5 +60,25 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS deploymentParameters.WebConfigActionList.Add( WebConfigHelpers.AddOrModifyAspNetCoreSection("stdoutLogFile", Path.Combine(path, "std"))); } + + public static void TransformPath(this IISDeploymentParameters parameters, Func transformation) + { + parameters.WebConfigActionList.Add( + (config, contentRoot) => + { + var aspNetCoreElement = config.Descendants("aspNetCore").Single(); + aspNetCoreElement.SetAttributeValue("processPath", transformation((string)aspNetCoreElement.Attribute("processPath"), contentRoot)); + }); + } + + public static void TransformArguments(this IISDeploymentParameters parameters, Func transformation) + { + parameters.WebConfigActionList.Add( + (config, contentRoot) => + { + var aspNetCoreElement = config.Descendants("aspNetCore").Single(); + aspNetCoreElement.SetAttributeValue("arguments", transformation((string)aspNetCoreElement.Attribute("arguments"), contentRoot)); + }); + } } } diff --git a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs index 8464088f48..4604fb5652 100644 --- a/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs +++ b/src/Microsoft.AspNetCore.Server.IntegrationTesting.IIS/IISExpressDeployer.cs @@ -483,7 +483,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS } else { - throw new InvalidOperationException($"iisexpress Process {hostProcess.Id} crashed before shutdown was triggered."); + throw new InvalidOperationException($"iisexpress Process {hostProcess?.Id} crashed before shutdown was triggered."); } } } diff --git a/test/Common.FunctionalTests/ConfigurationChangeTests.cs b/test/Common.FunctionalTests/ConfigurationChangeTests.cs index d1e5f62ff1..014e2520c4 100644 --- a/test/Common.FunctionalTests/ConfigurationChangeTests.cs +++ b/test/Common.FunctionalTests/ConfigurationChangeTests.cs @@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests await deploymentResult.HttpClient.RetryRequestAsync("/HelloWorld", r => r.StatusCode == HttpStatusCode.InternalServerError); StopServer(); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, "Could not find the assembly 'aspnetcorev2_inprocess.dll'"); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Could not find the assembly 'aspnetcorev2_inprocess.dll'"); } [ConditionalTheory] diff --git a/test/Common.FunctionalTests/Inprocess/EventLogTests.cs b/test/Common.FunctionalTests/Inprocess/EventLogTests.cs index 296d99f789..1df7f3c077 100644 --- a/test/Common.FunctionalTests/Inprocess/EventLogTests.cs +++ b/test/Common.FunctionalTests/Inprocess/EventLogTests.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests StopServer(); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, "Application '.+' started the coreclr in-process successfully."); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Application '.+' started the coreclr in-process successfully."); } [ConditionalFact] @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests StopServer(); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, "Application '.+' has shutdown."); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Application '.+' has shutdown."); } } } diff --git a/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs b/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs index 12eae66d01..dbbf09f1ac 100644 --- a/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs +++ b/test/Common.FunctionalTests/Inprocess/StartupExceptionTests.cs @@ -5,6 +5,7 @@ using System; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; using Microsoft.AspNetCore.Testing.xunit; using Xunit; @@ -23,19 +24,14 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [ConditionalTheory] [InlineData("CheckLogFile")] [InlineData("CheckErrLogFile")] - public async Task CheckStdoutWithRandomNumber(string path) + public async Task CheckStdoutWithRandomNumber(string mode) { - // Forcing publish for now to have parity between IIS and IISExpress - // Reason is because by default for IISExpress, we expect there to not be a web.config file. - // However, for IIS, we need a web.config file because the default on generated on publish - // doesn't include V2. We can remove the publish flag once IIS supports non-publish running var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.StartupExceptionWebsite, publish: true); var randomNumberString = new Random(Guid.NewGuid().GetHashCode()).Next(10000000).ToString(); - deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_INPROCESS_STARTUP_VALUE"] = path; - deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_INPROCESS_RANDOM_VALUE"] = randomNumberString; + deploymentParameters.TransformArguments((a, _) => $"{a} {mode} {randomNumberString}"); - await AssertFailsToStart(path, deploymentParameters); + await AssertFailsToStart(deploymentParameters); Assert.Contains(TestSink.Writes, context => context.Message.Contains($"Random number: {randomNumberString}")); } @@ -45,12 +41,12 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [InlineData("CheckLargeStdOutWrites")] [InlineData("CheckOversizedStdErrWrites")] [InlineData("CheckOversizedStdOutWrites")] - public async Task CheckStdoutWithLargeWrites(string path) + public async Task CheckStdoutWithLargeWrites(string mode) { var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.StartupExceptionWebsite, publish: true); - deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_INPROCESS_STARTUP_VALUE"] = path; + deploymentParameters.TransformArguments((a, _) => $"{a} {mode}"); - await AssertFailsToStart(path, deploymentParameters); + await AssertFailsToStart(deploymentParameters); Assert.Contains(TestSink.Writes, context => context.Message.Contains(new string('a', 4096))); } @@ -58,20 +54,19 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests [ConditionalFact] public async Task CheckValidConsoleFunctions() { - var path = "CheckConsoleFunctions"; var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.StartupExceptionWebsite, publish: true); - deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_INPROCESS_STARTUP_VALUE"] = path; + deploymentParameters.TransformArguments((a, _) => $"{a} CheckConsoleFunctions"); - await AssertFailsToStart(path, deploymentParameters); + await AssertFailsToStart(deploymentParameters); Assert.Contains(TestSink.Writes, context => context.Message.Contains("Is Console redirection: True")); } - private async Task AssertFailsToStart(string path, IntegrationTesting.IIS.IISDeploymentParameters deploymentParameters) + private async Task AssertFailsToStart(IntegrationTesting.IIS.IISDeploymentParameters deploymentParameters) { var deploymentResult = await DeployAsync(deploymentParameters); - var response = await deploymentResult.HttpClient.GetAsync(path); + var response = await deploymentResult.HttpClient.GetAsync("/"); Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); @@ -92,5 +87,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests var responseText = await response.Content.ReadAsStringAsync(); Assert.Contains("500.30 - ANCM In-Process Start Failure", responseText); } + } } diff --git a/test/Common.FunctionalTests/MultiApplicationTests.cs b/test/Common.FunctionalTests/MultiApplicationTests.cs index e0adccfc54..3723369c89 100644 --- a/test/Common.FunctionalTests/MultiApplicationTests.cs +++ b/test/Common.FunctionalTests/MultiApplicationTests.cs @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests Assert.Equal(200, (int)result1.StatusCode); Assert.Equal(500, (int)result2.StatusCode); StopServer(); - EventLogHelpers.VerifyEventLogEvent(result, TestSink, "Only one inprocess application is allowed per IIS application pool"); + EventLogHelpers.VerifyEventLogEvent(result, "Only one inprocess application is allowed per IIS application pool"); } [ConditionalTheory] @@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests Assert.Equal(200, (int)result1.StatusCode); Assert.Equal(500, (int)result2.StatusCode); StopServer(); - EventLogHelpers.VerifyEventLogEvent(result, TestSink, "Mixed hosting model is not supported."); + EventLogHelpers.VerifyEventLogEvent(result, "Mixed hosting model is not supported."); } private void SetHostingModel(string directory, HostingModel model) diff --git a/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs b/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs index 6d2393f187..4fb129feaf 100644 --- a/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs +++ b/test/Common.FunctionalTests/Utilities/EventLogHelpers.cs @@ -1,54 +1,64 @@ // 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.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Server.IntegrationTesting; using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; -using Microsoft.Extensions.Logging.Testing; using Xunit; -using Xunit.Sdk; namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { public class EventLogHelpers { - private static readonly Regex EventLogRegex = new Regex("Event Log: (?.+?)End Event Log Message.", RegexOptions.Singleline | RegexOptions.Compiled); - - public static void VerifyEventLogEvent(IISDeploymentResult deploymentResult, ITestSink testSink, string expectedRegexMatchString) + public static void VerifyEventLogEvent(IISDeploymentResult deploymentResult, string expectedRegexMatchString) { Assert.True(deploymentResult.HostProcess.HasExited); - var builder = new StringBuilder(); + var entries = GetEntries(deploymentResult); + AssertSingleEntry(expectedRegexMatchString, entries); + } + + public static void VerifyEventLogEvents(IISDeploymentResult deploymentResult, params string[] expectedRegexMatchString) + { + Assert.True(deploymentResult.HostProcess.HasExited); - foreach (var context in testSink.Writes) + var entries = GetEntries(deploymentResult).ToList(); + foreach (var regexString in expectedRegexMatchString) { - builder.Append(context.Message); - } + var matchedEntries = AssertSingleEntry(regexString, entries); - var count = 0; - var expectedRegex = new Regex(expectedRegexMatchString, RegexOptions.Singleline); - foreach (Match match in EventLogRegex.Matches(builder.ToString())) - { - var eventLogText = match.Groups["EventLogMessage"].Value; - if (expectedRegex.IsMatch(eventLogText)) + foreach (var matchedEntry in matchedEntries) { - count++; + entries.Remove(matchedEntry); } } + + Assert.True(0 == entries.Count, $"Some entries were not matched by any regex {FormatEntries(entries)}"); + } - Assert.True(count > 0, $"'{expectedRegexMatchString}' didn't match any event log messaged"); - Assert.True(count < 2, $"'{expectedRegexMatchString}' matched more then one event log message"); + private static EventLogEntry[] AssertSingleEntry(string regexString, IEnumerable entries) + { + var expectedRegex = new Regex(regexString, RegexOptions.Singleline); + var matchedEntries = entries.Where(entry => expectedRegex.IsMatch(entry.Message)).ToArray(); + Assert.True(matchedEntries.Length > 0, $"No entries matched by '{regexString}'"); + Assert.True(matchedEntries.Length < 2, $"Multiple entries matched by '{regexString}': {FormatEntries(matchedEntries)}"); + return matchedEntries; + } + private static string FormatEntries(IEnumerable entries) + { + return string.Join(",", entries.Select(e => e.Message)); + } + + private static IEnumerable GetEntries(IISDeploymentResult deploymentResult) + { var eventLog = new EventLog("Application"); // Eventlog is already sorted based on time of event in ascending time. // Check results in reverse order. - var expectedRegexEventLog = new Regex(expectedRegexMatchString); var processIdString = $"Process Id: {deploymentResult.HostProcess.Id}."; // Event log messages round down to the nearest second, so subtract a second @@ -67,18 +77,15 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { continue; } - + // ReplacementStings == EventData collection in EventLog // This is unaffected if event providers are not registered correctly if (eventLogEntry.Source == AncmVersionToMatch(deploymentResult) && - processIdString == eventLogEntry.ReplacementStrings[1] && - expectedRegex.IsMatch(eventLogEntry.ReplacementStrings[0])) + processIdString == eventLogEntry.ReplacementStrings[1]) { - return; + yield return eventLogEntry; } } - - Assert.True(false, $"'{expectedRegexMatchString}' didn't match any event log messaged."); } private static string AncmVersionToMatch(IISDeploymentResult deploymentResult) @@ -88,5 +95,51 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests "AspNetCore Module" + (deploymentResult.DeploymentParameters.AncmVersion == AncmVersion.AspNetCoreModuleV2 ? " V2" : ""); } + + + public static string InProcessStarted(IISDeploymentResult deploymentResult) + { + return $"Application '{EscapedContentRoot(deploymentResult)}' started the coreclr in-process successfully"; + } + + public static string InProcessFailedToStart(IISDeploymentResult deploymentResult, string reason) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' failed to load clr and managed application. {reason}"; + } + + public static string InProcessFailedToStop(IISDeploymentResult deploymentResult, string reason) + { + return "Failed to gracefully shutdown application 'MACHINE/WEBROOT/APPHOST/HTTPTESTSITE'."; + } + + public static string InProcessThreadException(IISDeploymentResult deploymentResult, string reason) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' hit unexpected managed exception{reason}"; + } + + public static string InProcessThreadExit(IISDeploymentResult deploymentResult, string code) + { + return $"Application '/LM/W3SVC/1/ROOT' with physical root '{EscapedContentRoot(deploymentResult)}' hit unexpected managed background thread exit, exit code = '{code}'."; + } + + public static string FailedToStartApplication(IISDeploymentResult deploymentResult, string code) + { + return $"Failed to start application '/LM/W3SVC/1/ROOT', ErrorCode '{code}'."; + } + + public static string ConfigurationLoadError(IISDeploymentResult deploymentResult, string reason) + { + return $"Configuration load error. {reason}"; + } + + private static string EscapedContentRoot(IISDeploymentResult deploymentResult) + { + var contentRoot = deploymentResult.ContentRoot; + if (!contentRoot.EndsWith('\\')) + { + contentRoot += '\\'; + } + return Regex.Escape(contentRoot); + } } } diff --git a/test/CommonLibTests/CommonLibTests.vcxproj b/test/CommonLibTests/CommonLibTests.vcxproj index ef1cf020d5..87dbd16675 100644 --- a/test/CommonLibTests/CommonLibTests.vcxproj +++ b/test/CommonLibTests/CommonLibTests.vcxproj @@ -101,7 +101,7 @@ true Console ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\$(Configuration)\; - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) UseLinkTimeCodeGeneration @@ -128,7 +128,7 @@ true Console ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\x64\$(Configuration)\; - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) UseLinkTimeCodeGeneration @@ -156,7 +156,7 @@ true true ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\$(Configuration)\; - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) UseLinkTimeCodeGeneration @@ -184,7 +184,7 @@ true true ..\..\src\AspNetCoreModuleV2\InProcessRequestHandler\x64\$(Configuration)\; - kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;inprocessapplication.obj;inprocesshandler.obj;ahadmin.lib;Rpcrt4.lib;inprocessapplicationbase.obj;stdafx.obj;version.lib;inprocessoptions.obj;%(AdditionalDependencies) UseLinkTimeCodeGeneration diff --git a/test/CommonLibTests/hostfxr_utility_tests.cpp b/test/CommonLibTests/hostfxr_utility_tests.cpp index a9beb49676..01c9541429 100644 --- a/test/CommonLibTests/hostfxr_utility_tests.cpp +++ b/test/CommonLibTests/hostfxr_utility_tests.cpp @@ -64,7 +64,7 @@ TEST(ParseHostFxrArguments, ProvideNoArgs_InvalidArgs) struHostFxrDllLocation, struExeLocation, bstrArray), // args array. - HOSTFXR_UTILITY::StartupParametersResolutionException); + InvalidOperationException); } TEST(GetAbsolutePathToDotnetFromProgramFiles, BackupWorks) @@ -118,5 +118,5 @@ TEST(GetHostFxrArguments, InvalidParams) struHostFxrDllLocation, struExeLocation, bstrArray), // args array. - HOSTFXR_UTILITY::StartupParametersResolutionException); + InvalidOperationException); } diff --git a/test/CommonLibTests/inprocess_application_tests.cpp b/test/CommonLibTests/inprocess_application_tests.cpp index 06be5493bd..d2ec985723 100644 --- a/test/CommonLibTests/inprocess_application_tests.cpp +++ b/test/CommonLibTests/inprocess_application_tests.cpp @@ -40,7 +40,7 @@ namespace InprocessTests IN_PROCESS_APPLICATION *app = new IN_PROCESS_APPLICATION(server, application, std::move(requestHandlerConfig), parameters.data(), 1); - ASSERT_STREQ(app->QueryExeLocation(), L"hello"); + ASSERT_STREQ(app->QueryExeLocation().c_str(), L"hello"); } TEST(InProcessTest, GeneratesVirtualPath) diff --git a/test/IIS.FunctionalTests/Inprocess/StdOutRedirectionTests.cs b/test/IIS.FunctionalTests/Inprocess/StdOutRedirectionTests.cs index 49088180c1..fea6d3b5fb 100644 --- a/test/IIS.FunctionalTests/Inprocess/StdOutRedirectionTests.cs +++ b/test/IIS.FunctionalTests/Inprocess/StdOutRedirectionTests.cs @@ -51,7 +51,7 @@ namespace IIS.FunctionalTests.Inprocess StopServer(); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found."); } @@ -75,7 +75,7 @@ namespace IIS.FunctionalTests.Inprocess var contents = File.ReadAllText(Helpers.GetExpectedLogName(deploymentResult, _logFolderPath)); var expectedString = "The specified framework 'Microsoft.NETCore.App', version '2.9.9' was not found."; - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, expectedString); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, expectedString); Assert.Contains(expectedString, contents); } @@ -99,7 +99,7 @@ namespace IIS.FunctionalTests.Inprocess var fileInDirectory = Directory.GetFiles(_logFolderPath).Single(); var contents = File.ReadAllText(fileInDirectory); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, "Invoked hostfxr"); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); Assert.Contains("Invoked hostfxr", contents); } @@ -124,7 +124,7 @@ namespace IIS.FunctionalTests.Inprocess StopServer(); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, "Invoked hostfxr"); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); } [ConditionalTheory] @@ -153,7 +153,7 @@ namespace IIS.FunctionalTests.Inprocess var fileInDirectory = Directory.GetFiles(_logFolderPath).First(); var contents = File.ReadAllText(fileInDirectory); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, "Invoked hostfxr"); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, "Invoked hostfxr"); Assert.Contains("Invoked hostfxr", contents); } diff --git a/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs b/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs index 1e5af7c13f..caaf728c64 100644 --- a/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs +++ b/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Net.Http; +using System.Net.Sockets; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; using Microsoft.AspNetCore.Server.IntegrationTesting; @@ -26,16 +27,42 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests public async Task ServerShutsDownWhenMainExits() { var parameters = _fixture.GetBaseDeploymentParameters(publish: true); - var result = await DeployAsync(parameters); + var deploymentResult = await DeployAsync(parameters); try { - await result.HttpClient.GetAsync("/Shutdown"); + await deploymentResult.HttpClient.GetAsync("/Shutdown"); } catch (HttpRequestException ex) when (ex.InnerException is IOException) { // Server might close a connection before request completes } - Assert.True(result.HostShutdownToken.WaitHandle.WaitOne(TimeoutExtensions.DefaultTimeout)); + + deploymentResult.AssertWorkerProcessStop(); + } + + + [ConditionalFact] + public async Task ServerShutsDownWhenMainExitsStress() + { + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await StartAsync(parameters); + + var load = Helpers.StressLoad(deploymentResult.HttpClient, "/HelloWorld", response => { + var statusCode = (int)response.StatusCode; + Assert.True(statusCode == 200 || statusCode == 503, "Status code was " + statusCode); + }); + + try + { + await deploymentResult.HttpClient.GetAsync("/Shutdown"); + await load; + } + catch (HttpRequestException ex) when (ex.InnerException is IOException | ex.InnerException is SocketException) + { + // Server might close a connection before request completes + } + + deploymentResult.AssertWorkerProcessStop(); } [ConditionalFact] diff --git a/test/IISExpress.FunctionalTests/InProcess/StartupTests.cs b/test/IISExpress.FunctionalTests/InProcess/StartupTests.cs index cab621a3e6..4c288ef90e 100644 --- a/test/IISExpress.FunctionalTests/InProcess/StartupTests.cs +++ b/test/IISExpress.FunctionalTests/InProcess/StartupTests.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests StopServer(); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, $@"Application '{Regex.Escape(deploymentResult.ContentRoot)}\\' wasn't able to start. {subError}"); + EventLogHelpers.VerifyEventLogEvent(deploymentResult, $@"Application '{Regex.Escape(deploymentResult.ContentRoot)}\\' wasn't able to start. {subError}"); } [ConditionalFact] @@ -137,11 +137,67 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { var deploymentResult = await DeployAsync(_fixture.GetBaseDeploymentParameters(_fixture.OverriddenServerWebSite, publish: true)); var response = await deploymentResult.HttpClient.GetAsync("/"); - Assert.False(response.IsSuccessStatusCode); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "CLR worker thread exited prematurely"), + EventLogHelpers.InProcessThreadException(deploymentResult, ".*?Application is running inside IIS process but is not configured to use IIS server")); + } + + [ConditionalFact] + public async Task LogsUnexpectedThreadExitError() + { + var deploymentResult = await DeployAsync(_fixture.GetBaseDeploymentParameters(_fixture.StartupExceptionWebsite, publish: true)); + + var response = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); StopServer(); - Assert.Contains(TestSink.Writes, context => context.Message.Contains("Application is running inside IIS process but is not configured to use IIS server")); + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "CLR worker thread exited prematurely"), + EventLogHelpers.InProcessThreadExit(deploymentResult, "12")); + } + + [ConditionalFact] + public async Task StartupTimeoutIsApplied() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.StartupExceptionWebsite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} Hang"); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("startupTimeLimit", "1")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + var response = await deploymentResult.HttpClient.GetAsync("/"); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessFailedToStart(deploymentResult, "Managed server didn't initialize after 1000 ms.") + ); + } + + [ConditionalFact] + public async Task ShutdownTimeoutIsApplied() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.StartupExceptionWebsite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} HangOnStop"); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("shutdownTimeLimit", "1")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + Assert.Equal("OK", await deploymentResult.HttpClient.GetStringAsync("/")); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessStarted(deploymentResult), + EventLogHelpers.InProcessFailedToStop(deploymentResult, "")); } [ConditionalFact] @@ -158,10 +214,12 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests StopServer(); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, "Unknown hosting model 'bogus'. Please specify either hostingModel=\"inprocess\" or hostingModel=\"outofprocess\" in the web.config file."); + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.FailedToStartApplication(deploymentResult, "0x80004005"), + EventLogHelpers.ConfigurationLoadError(deploymentResult, "Unknown hosting model 'bogus'. Please specify either hostingModel=\"inprocess\" or hostingModel=\"outofprocess\" in the web.config file.") + ); } - private static Dictionary)> InvalidConfigTransformations = InitInvalidConfigTransformations(); public static IEnumerable InvalidConfigTransformationsScenarios => InvalidConfigTransformations.ToTheoryData(); @@ -177,7 +235,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode); StopServer(); - EventLogHelpers.VerifyEventLogEvent(deploymentResult, TestSink, "Configuration load error. " + expectedError); + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.FailedToStartApplication(deploymentResult, "0x80004005"), + EventLogHelpers.ConfigurationLoadError(deploymentResult, expectedError) + ); } public static Dictionary)> InitInvalidConfigTransformations() @@ -223,42 +284,42 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests dictionary.Add("App in bin subdirectory full path to dll using exec and quotes", parameters => { MoveApplication(parameters, "bin"); - TransformArguments(parameters, (arguments, root) => "exec " + Path.Combine(root, "bin", arguments)); + parameters.TransformArguments((arguments, root) => "exec " + Path.Combine(root, "bin", arguments)); return ""; }); dictionary.Add("App in subdirectory with space", parameters => { MoveApplication(parameters, pathWithSpace); - TransformArguments(parameters, (arguments, root) => Path.Combine(pathWithSpace, arguments)); + parameters.TransformArguments((arguments, root) => Path.Combine(pathWithSpace, arguments)); return ""; }); dictionary.Add("App in subdirectory with space and full path to dll", parameters => { MoveApplication(parameters, pathWithSpace); - TransformArguments(parameters, (arguments, root) => Path.Combine(root, pathWithSpace, arguments)); + parameters.TransformArguments((arguments, root) => Path.Combine(root, pathWithSpace, arguments)); return ""; }); dictionary.Add("App in bin subdirectory with space full path to dll using exec and quotes", parameters => { MoveApplication(parameters, pathWithSpace); - TransformArguments(parameters, (arguments, root) => "exec \"" + Path.Combine(root, pathWithSpace, arguments) + "\" extra arguments"); + parameters.TransformArguments((arguments, root) => "exec \"" + Path.Combine(root, pathWithSpace, arguments) + "\" extra arguments"); return "extra|arguments"; }); dictionary.Add("App in bin subdirectory and quoted argument", parameters => { MoveApplication(parameters, "bin"); - TransformArguments(parameters, (arguments, root) => Path.Combine("bin", arguments) + " \"extra argument\""); + parameters.TransformArguments((arguments, root) => Path.Combine("bin", arguments) + " \"extra argument\""); return "extra argument"; }); dictionary.Add("App in bin subdirectory full path to dll", parameters => { MoveApplication(parameters, "bin"); - TransformArguments(parameters, (arguments, root) => Path.Combine(root, "bin", arguments) + " extra arguments"); + parameters.TransformArguments((arguments, root) => Path.Combine(root, "bin", arguments) + " extra arguments"); return "extra|arguments"; }); return dictionary; @@ -288,16 +349,16 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests dictionary.Add("App in subdirectory", parameters => { MoveApplication(parameters, pathWithSpace); - TransformPath(parameters, (path, root) => Path.Combine(pathWithSpace, path)); - TransformArguments(parameters, (arguments, root) => "\"additional argument\""); + parameters.TransformPath((path, root) => Path.Combine(pathWithSpace, path)); + parameters.TransformArguments((arguments, root) => "\"additional argument\""); return "additional argument"; }); dictionary.Add("App in bin subdirectory full path", parameters => { MoveApplication(parameters, pathWithSpace); - TransformPath(parameters, (path, root) => Path.Combine(root, pathWithSpace, path)); - TransformArguments(parameters, (arguments, root) => "additional arguments"); + parameters.TransformPath((path, root) => Path.Combine(root, pathWithSpace, path)); + parameters.TransformArguments((arguments, root) => "additional arguments"); return "additional|arguments"; }); @@ -322,26 +383,5 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests } }); } - - private static void TransformPath(IISDeploymentParameters parameters, Func transformation) - { - parameters.WebConfigActionList.Add( - (config, contentRoot) => - { - var aspNetCoreElement = config.Descendants("aspNetCore").Single(); - aspNetCoreElement.SetAttributeValue("processPath", transformation((string)aspNetCoreElement.Attribute("processPath"), contentRoot)); - }); - } - - private static void TransformArguments(IISDeploymentParameters parameters, Func transformation) - { - parameters.WebConfigActionList.Add( - (config, contentRoot) => - { - var aspNetCoreElement = config.Descendants("aspNetCore").Single(); - aspNetCoreElement.SetAttributeValue("arguments", transformation((string)aspNetCoreElement.Attribute("arguments"), contentRoot)); - }); - } - } } diff --git a/test/WebSites/StartupExceptionWebSite/Program.cs b/test/WebSites/StartupExceptionWebSite/Program.cs index 44c8b1f461..750f247481 100644 --- a/test/WebSites/StartupExceptionWebSite/Program.cs +++ b/test/WebSites/StartupExceptionWebSite/Program.cs @@ -2,54 +2,75 @@ // 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.Linq; using System.Text; +using System.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; namespace IISTestSite { public static class Program { - public static void Main(string[] args) + public static int Main(string[] args) { - var envVariable = Environment.GetEnvironmentVariable("ASPNETCORE_INPROCESS_STARTUP_VALUE"); - var randomNumber = Environment.GetEnvironmentVariable("ASPNETCORE_INPROCESS_RANDOM_VALUE"); + var mode = args.FirstOrDefault(); - // Semicolons are appended to env variables; removing them. - if (envVariable == "CheckLargeStdOutWrites") + switch (mode) { - Console.WriteLine(new string('a', 4096)); - } - else if (envVariable == "CheckLargeStdErrWrites") - { - Console.Error.WriteLine(new string('a', 4096)); - Console.Error.Flush(); - } - else if (envVariable == "CheckLogFile") - { - Console.WriteLine($"Random number: {randomNumber}"); - } - else if (envVariable == "CheckErrLogFile") - { - Console.Error.WriteLine($"Random number: {randomNumber}"); - Console.Error.Flush(); - } - else if (envVariable == "CheckOversizedStdErrWrites") - { - Console.WriteLine(new string('a', 5000)); + // Semicolons are appended to env variables; removing them. + case "CheckLargeStdOutWrites": + Console.WriteLine(new string('a', 4096)); + break; + case "CheckLargeStdErrWrites": + Console.Error.WriteLine(new string('a', 4096)); + Console.Error.Flush(); + break; + case "CheckLogFile": + Console.WriteLine($"Random number: {args[1]}"); + break; + case "CheckErrLogFile": + Console.Error.WriteLine($"Random number: {args[1]}"); + Console.Error.Flush(); + break; + case "CheckOversizedStdErrWrites": + Console.WriteLine(new string('a', 5000)); + break; + case "CheckOversizedStdOutWrites": + Console.Error.WriteLine(new string('a', 4096)); + Console.Error.Flush(); + break; + case "Hang": + Thread.Sleep(Timeout.Infinite); + break; + case "HangOnStop": + + var host = new WebHostBuilder() + .UseIIS() + .UseStartup() + .Build(); + host.Run(); + Thread.Sleep(Timeout.Infinite); + break; + case "CheckConsoleFunctions": + // Call a bunch of console functions and make sure none return invalid handle. + Console.OutputEncoding = Encoding.UTF8; + Console.Title = "Test"; + Console.WriteLine($"Is Console redirection: {Console.IsOutputRedirected}"); + Console.BackgroundColor = ConsoleColor.Blue; + break; } - else if (envVariable == "CheckOversizedStdOutWrites") + + return 12; + } + + public partial class Startup + { + public void Configure(IApplicationBuilder app) { - Console.Error.WriteLine(new string('a', 4096)); - Console.Error.Flush(); - } - else if (envVariable == "CheckConsoleFunctions") - { - // Call a bunch of console functions and make sure none return invalid handle. - Console.OutputEncoding = Encoding.UTF8; - Console.Title = "Test"; - Console.WriteLine($"Is Console redirection: {Console.IsOutputRedirected}"); - Console.BackgroundColor = ConsoleColor.Blue; + app.Run(async context => await context.Response.WriteAsync("OK")); } } }