diff --git a/src/AspNetCore/Inc/application.h b/src/AspNetCore/Inc/application.h index c06e763937..058bf8c731 100644 --- a/src/AspNetCore/Inc/application.h +++ b/src/AspNetCore/Inc/application.h @@ -202,12 +202,6 @@ public: return m_pAppOfflineHtm; } - STRU* - QueryApplicationPhysicalPath() - { - return &m_strAppPhysicalPath; - } - ~APPLICATION(); HRESULT diff --git a/src/AspNetCore/Inc/applicationmanager.h b/src/AspNetCore/Inc/applicationmanager.h index e1c48c69cd..3a6f934113 100644 --- a/src/AspNetCore/Inc/applicationmanager.h +++ b/src/AspNetCore/Inc/applicationmanager.h @@ -39,17 +39,9 @@ public: HRESULT GetApplication( _In_ IHttpContext* pContext, - _In_ LPCWSTR pszApplication, _Out_ APPLICATION ** ppApplication ); - static - VOID - RecycleOnFileChange( - APPLICATION_MANAGER* manager, - APPLICATION* application - ); - HRESULT RecycleApplication( _In_ LPCWSTR pszApplication diff --git a/src/AspNetCore/Inc/filewatcher.h b/src/AspNetCore/Inc/filewatcher.h index b1939a9e84..16d3942a2f 100644 --- a/src/AspNetCore/Inc/filewatcher.h +++ b/src/AspNetCore/Inc/filewatcher.h @@ -4,6 +4,7 @@ #pragma once #define FILE_WATCHER_SHUTDOWN_KEY (ULONG_PTR)(-1) +#define FILE_WATCHER_ENTRY_BUFFER_SIZE 4096 #ifndef CONTAINING_RECORD // // Calculate the address of the base of the structure given its type, and an @@ -53,14 +54,12 @@ public: private: HANDLE m_hCompletionPort; HANDLE m_hChangeNotificationThread; - CRITICAL_SECTION m_csSyncRoot; }; class FILE_WATCHER_ENTRY { public: FILE_WATCHER_ENTRY(FILE_WATCHER * pFileMonitor); - virtual ~FILE_WATCHER_ENTRY(); OVERLAPPED _overlapped; @@ -72,6 +71,33 @@ public: _In_ HANDLE hImpersonationToken ); + VOID + ReferenceFileWatcherEntry() const + { + InterlockedIncrement(&_cRefs); + } + + VOID + DereferenceFileWatcherEntry() const + { + if (InterlockedDecrement(&_cRefs) == 0) + { + delete this; + } + } + + BOOL + QueryIsValid() const + { + return _fIsValid; + } + + VOID + MarkEntryInValid() + { + _fIsValid = FALSE; + } + HRESULT Monitor(); VOID StopMonitor(); @@ -83,6 +109,8 @@ public: ); private: + virtual ~FILE_WATCHER_ENTRY(); + DWORD _dwSignature; BUFFER _buffDirectoryChanges; HANDLE _hImpersonationToken; @@ -92,5 +120,7 @@ private: STRU _strFileName; STRU _strDirectoryName; LONG _lStopMonitorCalled; + mutable LONG _cRefs; + BOOL _fIsValid; SRWLOCK _srwLock; }; diff --git a/src/AspNetCore/Inc/resource.h b/src/AspNetCore/Inc/resource.h index b4d8d2dd88..066c9792f5 100644 --- a/src/AspNetCore/Inc/resource.h +++ b/src/AspNetCore/Inc/resource.h @@ -7,12 +7,12 @@ #define IDS_SERVER_ERROR 1001 #define ASPNETCORE_EVENT_MSG_BUFFER_SIZE 256 -#define ASPNETCORE_EVENT_PROCESS_START_SUCCESS_MSG L"Process '%d' started successfully and is listening on port '%d'." +#define ASPNETCORE_EVENT_PROCESS_START_SUCCESS_MSG L"Application '%s' started process '%d' successfully and is listening on port '%d'." #define ASPNETCORE_EVENT_RAPID_FAIL_COUNT_EXCEEDED_MSG L"Maximum rapid fail count per minute of '%d' exceeded." -#define ASPNETCORE_EVENT_PROCESS_START_INTERNAL_ERROR_MSG L"Failed to parse processPath and arguments due to internal error, ErrorCode = '0x%x'." -#define ASPNETCORE_EVENT_PROCESS_START_POSTCREATE_ERROR_MSG L"Process was created with commandline '%s'but failed to get its status, ErrorCode = '0x%x'." -#define ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG L"Failed to start process with commandline '%s', ErrorCode = '0x%x'." -#define ASPNETCORE_EVENT_PROCESS_START_WRONGPORT_ERROR_MSG L"Process was created with commandline '%s' but did not listen on the given port '%d'" -#define ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG L"Process was created with commandline '%s' but either crashed or did not reponse within given time or did not listen on the given port '%d', ErrorCode = '0x%x'" +#define ASPNETCORE_EVENT_PROCESS_START_INTERNAL_ERROR_MSG L"Application '%s' failed to parse processPath and arguments due to internal error, ErrorCode = '0x%x'." +#define ASPNETCORE_EVENT_PROCESS_START_POSTCREATE_ERROR_MSG L"Application '%s' with physical root '%s' created process with commandline '%s'but failed to get its status, ErrorCode = '0x%x'." +#define ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG L"Application '%s' with physical root '%s' failed to start process with commandline '%s', ErrorCode = '0x%x : %x." +#define ASPNETCORE_EVENT_PROCESS_START_WRONGPORT_ERROR_MSG L"Application '%s' with physical root '%s' created process with commandline '%s' but failed to listen on the given port '%d'" +#define ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG L"Application '%s' with physical root '%s' created process with commandline '%s' but either crashed or did not reponse or did not listen on the given port '%d', ErrorCode = '0x%x'" #define ASPNETCORE_EVENT_INVALID_STDOUT_LOG_FILE_MSG L"Warning: Could not create stdoutLogFile %s, ErrorCode = %d." #define ASPNETCORE_EVENT_GRACEFUL_SHUTDOWN_FAILURE_MSG L"Failed to gracefully shutdown process '%d'." diff --git a/src/AspNetCore/Src/application.cxx b/src/AspNetCore/Src/application.cxx index 5db1445318..ff2a82d3f3 100644 --- a/src/AspNetCore/Src/application.cxx +++ b/src/AspNetCore/Src/application.cxx @@ -7,8 +7,11 @@ APPLICATION::~APPLICATION() { if (m_pFileWatcherEntry != NULL) { + // Mark the entry as invalid, + // StopMonitor will close the file handle and trigger a FCN + // the entry will delete itself when processing this FCN + m_pFileWatcherEntry->MarkEntryInValid(); m_pFileWatcherEntry->StopMonitor(); - delete m_pFileWatcherEntry; m_pFileWatcherEntry = NULL; } @@ -76,7 +79,7 @@ Finished: { if (m_pFileWatcherEntry != NULL) { - delete m_pFileWatcherEntry; + m_pFileWatcherEntry->DereferenceFileWatcherEntry(); m_pFileWatcherEntry = NULL; } diff --git a/src/AspNetCore/Src/applicationmanager.cxx b/src/AspNetCore/Src/applicationmanager.cxx index 785182220c..75c4d3d12e 100644 --- a/src/AspNetCore/Src/applicationmanager.cxx +++ b/src/AspNetCore/Src/applicationmanager.cxx @@ -8,7 +8,6 @@ APPLICATION_MANAGER* APPLICATION_MANAGER::sm_pApplicationManager = NULL; HRESULT APPLICATION_MANAGER::GetApplication( _In_ IHttpContext* pContext, - _In_ LPCWSTR pszApplication, _Out_ APPLICATION ** ppApplication ) { @@ -16,11 +15,15 @@ APPLICATION_MANAGER::GetApplication( APPLICATION *pApplication = NULL; APPLICATION_KEY key; BOOL fExclusiveLock = FALSE; - + PCWSTR pszApplicationId = NULL; *ppApplication = NULL; + + DBG_ASSERT(pContext != NULL); + DBG_ASSERT(pContext->GetApplication() != NULL); + pszApplicationId = pContext->GetApplication()->GetApplicationId(); - hr = key.Initialize(pszApplication); + hr = key.Initialize(pszApplicationId); if (FAILED(hr)) { goto Finished; @@ -50,7 +53,7 @@ APPLICATION_MANAGER::GetApplication( goto Finished; } - hr = pApplication->Initialize(this, pszApplication, pContext->GetApplication()->GetApplicationPhysicalPath()); + hr = pApplication->Initialize(this, pszApplicationId, pContext->GetApplication()->GetApplicationPhysicalPath()); if (FAILED(hr)) { goto Finished; @@ -88,14 +91,6 @@ Finished: return hr; } -VOID -APPLICATION_MANAGER::RecycleOnFileChange( - APPLICATION_MANAGER*, -APPLICATION* -) -{ - g_pHttpServer->RecycleProcess(L"Asp.Net Core Module Recycle Process on File Change Notification"); -} HRESULT APPLICATION_MANAGER::RecycleApplication( diff --git a/src/AspNetCore/Src/aspnetcoreconfig.cxx b/src/AspNetCore/Src/aspnetcoreconfig.cxx index 04f747894e..e4633806ac 100644 --- a/src/AspNetCore/Src/aspnetcoreconfig.cxx +++ b/src/AspNetCore/Src/aspnetcoreconfig.cxx @@ -79,7 +79,9 @@ ASPNETCORE_CONFIG::GetConfig( } else { - hr = pAspNetCoreConfig->QueryApplicationPath()->Copy(pHttpApplication->GetAppConfigPath()); + // set appliction info here instead of inside Populate() + // as the destructor will delete the backend process + hr = pAspNetCoreConfig->QueryApplicationPath()->Copy(pHttpApplication->GetApplicationId()); if (FAILED(hr)) { goto Finished; @@ -382,6 +384,7 @@ ASPNETCORE_CONFIG::Populate( } pcszEnvName = mszEnvNames.Next(pcszEnvName); } + // // let's disable this feature for now // diff --git a/src/AspNetCore/Src/filewatcher.cxx b/src/AspNetCore/Src/filewatcher.cxx index 5298a86dbe..545d76bc63 100644 --- a/src/AspNetCore/Src/filewatcher.cxx +++ b/src/AspNetCore/Src/filewatcher.cxx @@ -7,12 +7,15 @@ FILE_WATCHER::FILE_WATCHER() : m_hCompletionPort(NULL), m_hChangeNotificationThread(NULL) { - InitializeCriticalSection(&this->m_csSyncRoot); } FILE_WATCHER::~FILE_WATCHER() { - DeleteCriticalSection(&this->m_csSyncRoot); + if (m_hChangeNotificationThread != NULL) + { + CloseHandle(m_hChangeNotificationThread); + m_hChangeNotificationThread = NULL; + } } HRESULT @@ -76,29 +79,34 @@ Win32 error --*/ { FILE_WATCHER * pFileMonitor; - BOOL fRet = FALSE; + BOOL fSuccess = FALSE; DWORD cbCompletion = 0; OVERLAPPED * pOverlapped = NULL; DWORD dwErrorStatus; ULONG_PTR completionKey; pFileMonitor = (FILE_WATCHER*)pvArg; + DBG_ASSERT(pFileMonitor != NULL); + while (TRUE) { - fRet = GetQueuedCompletionStatus( + fSuccess = GetQueuedCompletionStatus( pFileMonitor->m_hCompletionPort, &cbCompletion, &completionKey, &pOverlapped, INFINITE); - dwErrorStatus = fRet ? ERROR_SUCCESS : GetLastError(); + DBG_ASSERT(fSuccess); + DebugPrint(1, "FILE_WATCHER::ChangeNotificationThread"); + dwErrorStatus = fSuccess ? ERROR_SUCCESS : GetLastError(); if (completionKey == FILE_WATCHER_SHUTDOWN_KEY) { continue; } + DBG_ASSERT(pOverlapped != NULL); if (pOverlapped != NULL) { FileWatcherCompletionRoutine( @@ -138,10 +146,26 @@ None { FILE_WATCHER_ENTRY * pMonitorEntry; pMonitorEntry = CONTAINING_RECORD(pOverlapped, FILE_WATCHER_ENTRY, _overlapped); - + pMonitorEntry->DereferenceFileWatcherEntry(); DBG_ASSERT(pMonitorEntry != NULL); - pMonitorEntry->HandleChangeCompletion(dwCompletionStatus, - cbCompletion); + + pMonitorEntry->HandleChangeCompletion(dwCompletionStatus, cbCompletion); + + if (pMonitorEntry->QueryIsValid()) + { + // + // Continue monitoring + // + pMonitorEntry->Monitor(); + } + else + { + // + // Marked by application distructor + // Deference the entry to delete it + // + pMonitorEntry->DereferenceFileWatcherEntry(); + } } @@ -150,7 +174,9 @@ FILE_WATCHER_ENTRY::FILE_WATCHER_ENTRY(FILE_WATCHER * pFileMonitor) : _hDirectory(INVALID_HANDLE_VALUE), _hImpersonationToken(NULL), _pApplication(NULL), - _lStopMonitorCalled(0) + _lStopMonitorCalled(0), + _cRefs(1), + _fIsValid(TRUE) { _dwSignature = FILE_WATCHER_ENTRY_SIGNATURE; InitializeSRWLock(&_srwLock); @@ -201,6 +227,12 @@ HRESULT FILE_NOTIFY_INFORMATION * pNotificationInfo; BOOL fFileChanged = FALSE; + AcquireSRWLockExclusive(&_srwLock); + if (!_fIsValid) + { + goto Finished; + } + // When directory handle is closed then HandleChangeCompletion // happens with cbCompletion = 0 and dwCompletionStatus = 0 // From documentation it is not clear if that combination @@ -211,32 +243,31 @@ HRESULT // if (_lStopMonitorCalled) { - hr = S_OK; goto Finished; } + // + // There could be a FCN overflow + // Let assume the file got changed instead of checking files + // Othersie we have to cache the file info + // if (cbCompletion == 0) - { - // - // There could be a FCN overflow - // Let assume the file got changed instead of checking files - // Othersie we have to cache the file info - // - + { fFileChanged = TRUE; - hr = HRESULT_FROM_WIN32(dwCompletionStatus); } else { pNotificationInfo = (FILE_NOTIFY_INFORMATION*)_buffDirectoryChanges.QueryPtr(); - _ASSERT(pNotificationInfo != NULL); + DBG_ASSERT(pNotificationInfo != NULL); while (pNotificationInfo != NULL) { // // check whether the monitored file got changed // - if (wcscmp(pNotificationInfo->FileName, _strFileName.QueryStr()) == 0) + if (wcsncmp(pNotificationInfo->FileName, + _strFileName.QueryStr(), + pNotificationInfo->FileNameLength/sizeof(WCHAR)) == 0) { fFileChanged = TRUE; break; @@ -255,13 +286,7 @@ HRESULT pNotificationInfo->NextEntryOffset); } } - - RtlZeroMemory(_buffDirectoryChanges.QueryPtr(), _buffDirectoryChanges.QuerySize()); } - // - //continue monitoring - // - StopMonitor(); if (fFileChanged) { @@ -271,9 +296,8 @@ HRESULT _pApplication->UpdateAppOfflineFileHandle(); } - hr = Monitor(); - Finished: + ReleaseSRWLockExclusive(&_srwLock); return hr; } @@ -285,69 +309,23 @@ FILE_WATCHER_ENTRY::Monitor(VOID) DWORD cbRead; AcquireSRWLockExclusive(&_srwLock); - + ReferenceFileWatcherEntry(); ZeroMemory(&_overlapped, sizeof(_overlapped)); - if (_hDirectory != INVALID_HANDLE_VALUE) - { - CloseHandle(_hDirectory); - _hDirectory = INVALID_HANDLE_VALUE; - } - _hDirectory = CreateFileW(_strDirectoryName.QueryStr(), - FILE_LIST_DIRECTORY, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - NULL, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, - NULL); - - if (_hDirectory == INVALID_HANDLE_VALUE) - { - hr = HRESULT_FROM_WIN32(GetLastError()); - goto Finished; - } - - if (CreateIoCompletionPort( - _hDirectory, - _pFileMonitor->QueryCompletionPort(), - NULL, - 0) == NULL) - { - hr = HRESULT_FROM_WIN32(GetLastError()); - goto Finished; - } - - // - // Resize change buffer to something "reasonable" - // - fRet = _buffDirectoryChanges.Resize(4096); - if (!fRet) - { - hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); - goto Finished; - } - - fRet = ReadDirectoryChangesW(_hDirectory, + if(!ReadDirectoryChangesW(_hDirectory, _buffDirectoryChanges.QueryPtr(), _buffDirectoryChanges.QuerySize(), - FALSE, // watch sub dirs. set to False now as only monitoring app_offline + FALSE, // Watching sub dirs. Set to False now as only monitoring app_offline FILE_NOTIFY_VALID_MASK & ~FILE_NOTIFY_CHANGE_LAST_ACCESS & ~FILE_NOTIFY_CHANGE_ATTRIBUTES, &cbRead, &_overlapped, - NULL); - - if (!fRet) + NULL)) { hr = HRESULT_FROM_WIN32(GetLastError()); } - InterlockedExchange(&_lStopMonitorCalled, 0); - -Finished: - ReleaseSRWLockExclusive(&_srwLock); return hr; - } VOID @@ -386,7 +364,7 @@ FILE_WATCHER_ENTRY::Create( pszFileNameToMonitor == NULL || pApplication == NULL) { - _ASSERT(FALSE); + DBG_ASSERT(FALSE); hr = HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); goto Finished; } @@ -406,6 +384,15 @@ FILE_WATCHER_ENTRY::Create( goto Finished; } + // + // Resize change buffer to something "reasonable" + // + if (!_buffDirectoryChanges.Resize(FILE_WATCHER_ENTRY_BUFFER_SIZE)) + { + hr = HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY); + goto Finished; + } + if (hImpersonationToken != NULL) { fRet = DuplicateHandle(GetCurrentProcess(), @@ -430,6 +417,32 @@ FILE_WATCHER_ENTRY::Create( _hImpersonationToken = NULL; } } + + _hDirectory = CreateFileW( + _strDirectoryName.QueryStr(), + FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + NULL); + + if (_hDirectory == INVALID_HANDLE_VALUE) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + + if (CreateIoCompletionPort( + _hDirectory, + _pFileMonitor->QueryCompletionPort(), + NULL, + 0) == NULL) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + goto Finished; + } + // // Start monitoring // diff --git a/src/AspNetCore/Src/forwardinghandler.cxx b/src/AspNetCore/Src/forwardinghandler.cxx index 6a6f44f685..f5a0ad00a4 100644 --- a/src/AspNetCore/Src/forwardinghandler.cxx +++ b/src/AspNetCore/Src/forwardinghandler.cxx @@ -359,18 +359,10 @@ FORWARDING_HANDLER::SetStatusAndHeaders( DWORD headerIndex = g_pResponseHeaderHash->GetIndex(strHeaderName.QueryStr()); if (headerIndex == UNKNOWN_INDEX) { - if (_strnicmp(strHeaderName.QueryStr(), "Sec-WebSocket", 13) != 0 ) - { - // - // Perf Opt: Avoid setting websocket headers, since IIS websocket module - // will anyways set these later in the pipeline. - // - - hr = pResponse->SetHeader(strHeaderName.QueryStr(), - strHeaderValue.QueryStr(), - static_cast(strHeaderValue.QueryCCH()), - FALSE); // fReplace - } + hr = pResponse->SetHeader(strHeaderName.QueryStr(), + strHeaderValue.QueryStr(), + static_cast(strHeaderValue.QueryCCH()), + FALSE); // fReplace } else { @@ -1074,7 +1066,6 @@ VOID } hr = pApplicationManager->GetApplication( m_pW3Context, - m_pW3Context->GetApplication()->GetAppConfigPath(), &m_pApplication ); if (FAILED(hr)) { diff --git a/src/AspNetCore/Src/serverprocess.cxx b/src/AspNetCore/Src/serverprocess.cxx index 4c0ca9b1b5..3b8c9c5252 100644 --- a/src/AspNetCore/Src/serverprocess.cxx +++ b/src/AspNetCore/Src/serverprocess.cxx @@ -129,6 +129,7 @@ SERVER_PROCESS::StartProcess( WCHAR* pszPath = NULL; WCHAR pszFullPath[_MAX_PATH]; LPCWSTR apsz[1]; + PCWSTR pszAppPath = NULL; GetStartupInfoW(&startupInfo); @@ -243,8 +244,6 @@ SERVER_PROCESS::StartProcess( struApplicationId.Copy( L"ASPNETCORE_APPL_PATH=" ); - PCWSTR pszAppPath = NULL; - // let's find the app path. IIS does not support nested sites // we can seek for the fourth '/' if it exits // MACHINE/WEBROOT/APPHOST//. @@ -277,7 +276,7 @@ SERVER_PROCESS::StartProcess( mszNewEnvironment.Append( struGuidEnv ); - pszRootApplicationPath = context->GetRootContext()->GetApplication()->GetApplicationPhysicalPath(); + pszRootApplicationPath = context->GetApplication()->GetApplicationPhysicalPath(); // // generate process command line. @@ -486,8 +485,11 @@ SERVER_PROCESS::StartProcess( // don't check return code as we already in error report strEventMsg.SafeSnwprintf( ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG, + pszAppPath, + pszRootApplicationPath, finalCommandLine.QueryStr(), - hr); + hr, + 0); goto Finished; } @@ -537,8 +539,11 @@ SERVER_PROCESS::StartProcess( hr = E_FAIL; strEventMsg.SafeSnwprintf( ASPNETCORE_EVENT_PROCESS_START_ERROR_MSG, + pszAppPath, + pszRootApplicationPath, finalCommandLine.QueryStr(), - hr); + hr, + processStatus); goto Finished; } } @@ -632,6 +637,8 @@ SERVER_PROCESS::StartProcess( hr = HRESULT_FROM_WIN32(ERROR_TIMEOUT); strEventMsg.SafeSnwprintf( ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG, + pszAppPath, + pszRootApplicationPath, finalCommandLine.QueryStr(), m_dwPort, hr); @@ -649,6 +656,8 @@ SERVER_PROCESS::StartProcess( fReady = FALSE; strEventMsg.SafeSnwprintf( ASPNETCORE_EVENT_PROCESS_START_WRONGPORT_ERROR_MSG, + pszAppPath, + pszRootApplicationPath, finalCommandLine.QueryStr(), m_dwPort, hr); @@ -678,6 +687,8 @@ SERVER_PROCESS::StartProcess( { strEventMsg.SafeSnwprintf( ASPNETCORE_EVENT_PROCESS_START_NOTREADY_ERROR_MSG, + pszAppPath, + pszRootApplicationPath, finalCommandLine.QueryStr(), m_dwPort, hr); @@ -724,6 +735,7 @@ SERVER_PROCESS::StartProcess( if (SUCCEEDED(strEventMsg.SafeSnwprintf( ASPNETCORE_EVENT_PROCESS_START_SUCCESS_MSG, + pszAppPath, m_dwProcessId, m_dwPort))) { @@ -754,11 +766,14 @@ Finished: { if (!fDonePrepareCommandLine) strEventMsg.SafeSnwprintf( + pszAppPath, ASPNETCORE_EVENT_PROCESS_START_INTERNAL_ERROR_MSG, hr); else strEventMsg.SafeSnwprintf( ASPNETCORE_EVENT_PROCESS_START_POSTCREATE_ERROR_MSG, + pszAppPath, + pszRootApplicationPath, finalCommandLine.QueryStr(), hr); }