diff options
Diffstat (limited to 'replay/cl_sessioninfodownloader.cpp')
| -rw-r--r-- | replay/cl_sessioninfodownloader.cpp | 444 |
1 files changed, 444 insertions, 0 deletions
diff --git a/replay/cl_sessioninfodownloader.cpp b/replay/cl_sessioninfodownloader.cpp new file mode 100644 index 0000000..4cd586d --- /dev/null +++ b/replay/cl_sessioninfodownloader.cpp @@ -0,0 +1,444 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//=======================================================================================// + +#include "cl_sessioninfodownloader.h" +#include "replay/ienginereplay.h" +#include "replay/shared_defs.h" +#include "cl_recordingsession.h" +#include "cl_recordingsessionblock.h" +#include "cl_replaycontext.h" +#include "cl_sessionblockdownloader.h" +#include "KeyValues.h" +#include "convar.h" +#include "dbg.h" +#include "vprof.h" +#include "sessioninfoheader.h" +#include "utlbuffer.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//---------------------------------------------------------------------------------------- + +extern IEngineReplay *g_pEngine; + +//---------------------------------------------------------------------------------------- + +CSessionInfoDownloader::CSessionInfoDownloader() +: m_pDownloader( NULL ), + m_pSession( NULL ), + m_flLastDownloadTime( 0.0f ), + m_nError( ERROR_NONE ), + m_nHttpError( HTTP_ERROR_NONE ), + m_bDone( false ) +{ +} + +CSessionInfoDownloader::~CSessionInfoDownloader() +{ + Assert( m_pDownloader == NULL ); // We should have deleted the downloader already +} + +void CSessionInfoDownloader::CleanupDownloader() +{ + if ( m_pDownloader ) + { + m_pDownloader->AbortDownloadAndCleanup(); + m_pDownloader = NULL; + } +} + +void CSessionInfoDownloader::DownloadSessionInfoAndUpdateBlocks( CBaseRecordingSession *pSession ) +{ + Assert( m_pDownloader == NULL ); + + // Cache session + m_pSession = pSession; + + // Download the session info now + m_pDownloader = new CHttpDownloader( this ); + m_pDownloader->BeginDownload( pSession->GetSessionInfoURL(), NULL ); +} + +float CSessionInfoDownloader::GetNextThinkTime() const +{ + extern ConVar replay_sessioninfo_updatefrequency; + return m_flLastDownloadTime + replay_sessioninfo_updatefrequency.GetFloat(); +} + +void CSessionInfoDownloader::Think() +{ + VPROF_BUDGET( "CSessionInfoDownloader::Think", VPROF_BUDGETGROUP_REPLAY ); + + CBaseThinker::Think(); + + // If we're not downloading, no need to think + if ( !m_pDownloader ) + return; + + // If the download's complete + if ( m_pDownloader->IsDone() && m_pDownloader->CanDelete() ) + { + // We're done - CanDelete() will now return true + delete m_pDownloader; + m_pDownloader = NULL; + } + else + { + // Otherwise, think... + m_pDownloader->Think(); + } +} + +void CSessionInfoDownloader::OnDownloadComplete( CHttpDownloader *pDownloader, const unsigned char *pData ) +{ + Assert( pDownloader ); + + // Clear out any previous error + m_nError = ERROR_NONE; + m_nHttpError = HTTP_ERROR_NONE; + + bool bUpdatedSomething = false; + +#if _DEBUG + extern ConVar replay_simulatedownloadfailure; + const bool bForceError = replay_simulatedownloadfailure.GetInt() == 2; +#else + const bool bForceError = false; +#endif + + if ( pDownloader->GetStatus() != HTTP_DONE || bForceError ) + { + m_nError = ERROR_DOWNLOAD_FAILED; + m_nHttpError = pDownloader->GetError(); + DBG( "Session info download FAILED.\n" ); + } + else + { + Assert( pData ); + + // Read header + SessionInfoHeader_t header; + if ( !ReadSessionInfoHeader( pData, pDownloader->GetSize(), header ) ) + { + m_nError = ERROR_NOT_ENOUGH_DATA; + } + else + { + IF_REPLAY_DBG( Warning( "Session info downloaded successfully for session %s\n", header.m_szSessionName ) ); + + // Get number of blocks for data validation + const int nNumBlocks = header.m_nNumBlocks; + if ( nNumBlocks <= 0 ) + { + m_nError = ERROR_BAD_NUM_BLOCKS; + } + else + { + // The block has either been found or created - now, fill it with data + const char *pSessionName = header.m_szSessionName; + if ( !pSessionName || !pSessionName[0] ) + { + m_nError = ERROR_NO_SESSION_NAME; + } + else + { + // Now that we have the session name, find it in the client session manager + CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSessionByName( pSessionName ) ); Assert( pSession ); + if ( !pSession ) + { + AssertMsg( 0, "Session should always exist by this point" ); + m_nError = ERROR_UNKNOWN_SESSION; + } + else + { + // Get recording state for session + pSession->m_bRecording = header.m_bRecording; + + const CompressorType_t nHeaderCompressorType = header.m_nCompressorType; + uint8 *pPayload = (uint8 *)pData + sizeof( SessionInfoHeader_t ); + uint8 *pUncompressedPayload = NULL; + unsigned int uUncompressedPayloadSize = header.m_uPayloadSizeUC; + + // Validate the payload with the MD5 digest + bool bPayloadValid = g_pEngine->MD5_HashBuffer( header.m_aHash, (const uint8 *)pPayload, header.m_uPayloadSize, NULL ); + + if ( !bPayloadValid ) + { + m_nError = ERROR_PAYLOAD_HASH_FAILED; + } + else + { + if ( nHeaderCompressorType == COMPRESSORTYPE_INVALID ) + { + // Uncompressed payload is read to go + pUncompressedPayload = pPayload; + } + else + { + // Attempt to decompress the payload + ICompressor *pCompressor = CreateCompressor( header.m_nCompressorType ); + if ( !pCompressor ) + { + bPayloadValid = false; + m_nError = ERROR_COULD_NOT_CREATE_COMPRESSOR; + } + else + { + // Uncompressed size big enough to read at least one block? + if ( header.m_uPayloadSizeUC <= MIN_SESSION_INFO_PAYLOAD_SIZE ) + { + bPayloadValid = false; + m_nError = ERROR_INVALID_UNCOMPRESSED_SIZE; + } + else + { + // Attempt to decompress payload now + pUncompressedPayload = new uint8[ uUncompressedPayloadSize ]; + if ( !pCompressor->Decompress( (char *)pUncompressedPayload, &uUncompressedPayloadSize, (const char *)pPayload, header.m_uPayloadSize ) ) + { + bPayloadValid = false; + m_nError = ERROR_PAYLOAD_DECOMPRESS_FAILED; + } + } + } + } + + if ( bPayloadValid ) + { + AssertMsg( pUncompressedPayload, "This should never be NULL here." ); + AssertMsg( uUncompressedPayloadSize >= MIN_SESSION_INFO_PAYLOAD_SIZE, "This size should always be valid here." ); + + RecordingSessionBlockSpec_t DummyBlock; + CUtlBuffer buf( pUncompressedPayload, uUncompressedPayloadSize, CUtlBuffer::READ_ONLY ); + + // Optimization: start the read at the first block we care about, which is one block after the last consecutive, downloaded block. + // This optimization should come in handy on servers that run lengthy rounds, so we're not reiterating over inconsequential blocks + // (i.e. they were already downloaded). + int iStartBlock = pSession->GetGreatestConsecutiveBlockDownloaded() + 1; + if ( iStartBlock > 0 ) + { + buf.SeekGet( CUtlBuffer::SEEK_HEAD, iStartBlock * sizeof( RecordingSessionBlockSpec_t ) ); + } + + // If for some reason the seek caused a 'get' overflow, try reading from the start of the buffer. + if ( !buf.IsValid() ) + { + iStartBlock = 0; + buf.SeekGet( CUtlBuffer::SEEK_HEAD, 0 ); + } + + // Read blocks, starting from the calculated start block. + for ( int i = iStartBlock; i < header.m_nNumBlocks; ++i ) + { + // Attempt to read the current block from the buffer + buf.Get( &DummyBlock, sizeof( DummyBlock ) ); + if ( !buf.IsValid() ) + { + m_nError = ERROR_BLOCK_READ_FAILED; + break; + } + + IF_REPLAY_DBG( Warning( "processing block with recon index: %i\n", DummyBlock.m_iReconstruction ) ); + + // Get reconstruction index + const int iBlockReconstruction = (ReplayHandle_t)DummyBlock.m_iReconstruction; + if ( iBlockReconstruction < 0 ) + { + m_nError = ERROR_INVALID_ORDER; + continue; + } + + // Check status + const int nRemoteStatus = (int)DummyBlock.m_uRemoteStatus; + if ( nRemoteStatus < 0 || nRemoteStatus >= CBaseRecordingSessionBlock::MAX_STATUS ) + { + // Status not found or invalid status + m_nError = ERROR_INVALID_REPLAY_STATUS; + continue; + } + + // Get the block file size + const uint32 uFileSize = (uint32)DummyBlock.m_uFileSize; + + // Get the uncompressed block size + const uint32 uUncompressedSize = (uint32)DummyBlock.m_uUncompressedSize; + + // Get the compressor type + const int nCompressorType = (uint32)DummyBlock.m_nCompressorType; + + // Attempt to find the block in the session + CClientRecordingSessionBlock *pBlock = CL_CastBlock( CL_GetRecordingSessionBlockManager()->FindBlockForSession( m_pSession->GetHandle(), iBlockReconstruction ) ); + + // If the block exists and has already been downloaded, we have nothing more to update + if ( pBlock && !pBlock->NeedsUpdate() ) + continue; + + bool bBlockDataChanged = false; + + // If the block doesn't exist in the session block manager, create it now + if ( !pBlock ) + { + CClientRecordingSessionBlock *pNewBlock = CL_CastBlock( CL_GetRecordingSessionBlockManager()->CreateAndGenerateHandle() ); + pNewBlock->m_iReconstruction = iBlockReconstruction; + // pNewBlock->m_strFullFilename = Replay_va( + const char *pFullFilename = Replay_va( + "%s%s_part_%i.%s", + pNewBlock->GetPath(), + pSession->m_strName.Get(), pNewBlock->m_iReconstruction, + BLOCK_FILE_EXTENSION + ); + V_strcpy( pNewBlock->m_szFullFilename, pFullFilename ); + pNewBlock->m_hSession = pSession->GetHandle(); + + // Add to session block manager + CL_GetRecordingSessionBlockManager()->Add( pNewBlock ); + + // Add the block to the session (marks session as dirty) + pSession->AddBlock( pNewBlock, false ); + + // Use the new block + pBlock = pNewBlock; + + bBlockDataChanged = true; + } + + IF_REPLAY_DBG2( Warning( " Block %i status=%s\n", pBlock->m_iReconstruction, pBlock->GetRemoteStatusStringSafe( pBlock->m_nRemoteStatus ) ) ); + + // Now that we've got a block, replicate the server data/fill it in + if ( pBlock->m_nRemoteStatus != nRemoteStatus ) + { + pBlock->m_nRemoteStatus = (CBaseRecordingSessionBlock::RemoteStatus_t)nRemoteStatus; + bBlockDataChanged = true; + } + + // Block file size needs to be set? + if ( pBlock->m_uFileSize != uFileSize ) + { + pBlock->m_uFileSize = uFileSize; + bBlockDataChanged = true; + } + + // Uncompressed block file size needs to be set? + if ( pBlock->m_uUncompressedSize != uUncompressedSize ) + { + Assert( nCompressorType >= COMPRESSORTYPE_INVALID ); + pBlock->m_uUncompressedSize = uUncompressedSize; + bBlockDataChanged = true; + } + + // Compressor type needs to be set? + if ( pBlock->m_nCompressorType != (CompressorType_t)nCompressorType ) + { + pBlock->m_nCompressorType = (CompressorType_t)nCompressorType; + bBlockDataChanged = true; + } + + // Attempt to read the hash if we haven't already done so + if ( !pBlock->HasValidHash() ) + { + V_memcpy( pBlock->m_aHash, DummyBlock.m_aHash, sizeof( pBlock->m_aHash ) ); + bBlockDataChanged = true; + } + + // Shift the block's state from waiting to ready-to-download if the block is ready on the server. + if ( pBlock->m_nDownloadStatus == CClientRecordingSessionBlock::DOWNLOADSTATUS_WAITING && + nRemoteStatus == CBaseRecordingSessionBlock::STATUS_READYFORDOWNLOAD ) + { + pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_READYTODOWNLOAD; + } + + // Save + if ( bBlockDataChanged ) + { + CL_GetRecordingSessionBlockManager()->FlagForFlush( pBlock, false ); + + bUpdatedSomething = true; + } + } + + // If this session is not recording, make sure the max number of blocks in the session is in check. + if ( !pSession->m_bRecording && pSession->GetLastBlockToDownload() >= nNumBlocks ) + { + // This will adjust all replay max blocks + pSession->AdjustLastBlockToDownload( nNumBlocks - 1 ); + } + + // If we've updated something, set cache the current time in the session + if ( bUpdatedSomething ) + { + pSession->RefreshLastUpdateTime(); + } + else if ( pSession->GetLastUpdateTime() >= 0.0f && ( g_pEngine->GetHostTime() - pSession->GetLastUpdateTime() > DOWNLOAD_TIMEOUT_THRESHOLD ) ) + { + pSession->OnDownloadTimeout(); + } + } + } + } + } + } + } + } + + // Display a message for the given download error + if ( m_nError != ERROR_NONE ) + { + // Report an error to the user + const char *pErrorToken = GetErrorString( m_nError, m_nHttpError ); + if ( m_nError == ERROR_DOWNLOAD_FAILED ) + { + KeyValues *pParams = new KeyValues( "args", "url", pDownloader->GetURL() ); + CL_GetErrorSystem()->AddFormattedErrorFromTokenName( pErrorToken, pParams ); + } + else + { + CL_GetErrorSystem()->AddErrorFromTokenName( pErrorToken ); + } + + // Report error to OGS + CL_GetErrorSystem()->OGS_ReportSessioInfoDownloadError( pDownloader, pErrorToken ); + } + + // Flag as done + m_bDone = true; + + // Cache download time + m_flLastDownloadTime = g_pEngine->GetHostTime(); +} + +const char *CSessionInfoDownloader::GetErrorString( int nError, HTTPError_t nHttpError ) const +{ + switch ( nError ) + { + case ERROR_NO_SESSION_NAME: return "#Replay_DL_Err_SI_NoSessionName"; + case ERROR_REPLAY_NOT_FOUND: return "#Replay_DL_Err_SI_ReplayNotFound"; + case ERROR_INVALID_REPLAY_STATUS: return "#Replay_DL_Err_SI_InvalidReplayStatus"; + case ERROR_INVALID_ORDER: return "#Replay_DL_Err_SI_InvalidOrder"; + case ERROR_UNKNOWN_SESSION: return "#Replay_DL_Err_SI_Unknown_Session"; + case ERROR_BLOCK_READ_FAILED: return "#Replay_DL_Err_SI_BlockReadFailed"; + case ERROR_NOT_ENOUGH_DATA: return "#Replay_DL_Err_SI_NotEnoughData"; + case ERROR_COULD_NOT_CREATE_COMPRESSOR: return "#Replay_DL_Err_SI_CouldNotCreateCompressor"; + case ERROR_INVALID_UNCOMPRESSED_SIZE: return "#Replay_DL_Err_SI_InvalidUncompressedSize"; + case ERROR_PAYLOAD_DECOMPRESS_FAILED: return "#Replay_DL_Err_SI_PayloadDecompressFailed"; + case ERROR_PAYLOAD_HASH_FAILED: return "#Replay_DL_Err_SI_PayloadHashFailed"; + + case ERROR_DOWNLOAD_FAILED: + switch ( m_nHttpError ) + { + case HTTP_ERROR_ZERO_LENGTH_FILE: return "#Replay_DL_Err_SI_DownloadFailed_ZeroLengthFile"; + case HTTP_ERROR_CONNECTION_CLOSED: return "#Replay_DL_Err_SI_DownloadFailed_ConnectionClosed"; + case HTTP_ERROR_INVALID_URL: return "#Replay_DL_Err_SI_DownloadFailed_InvalidURL"; + case HTTP_ERROR_INVALID_PROTOCOL: return "#Replay_DL_Err_SI_DownloadFailed_InvalidProtocol"; + case HTTP_ERROR_CANT_BIND_SOCKET: return "#Replay_DL_Err_SI_DownloadFailed_CantBindSocket"; + case HTTP_ERROR_CANT_CONNECT: return "#Replay_DL_Err_SI_DownloadFailed_CantConnect"; + case HTTP_ERROR_NO_HEADERS: return "#Replay_DL_Err_SI_DownloadFailed_NoHeaders"; + case HTTP_ERROR_FILE_NONEXISTENT: return "#Replay_DL_Err_SI_DownloadFailed_FileNonExistent"; + default: return "#Replay_DL_Err_SI_DownloadFailed_UnknownError"; + } + } + return "#Replay_DL_Err_SI_Unknown"; +} + +//---------------------------------------------------------------------------------------- |