//========= Copyright Valve Corporation, All rights reserved. ============// // //=======================================================================================// #include "cl_sessionblockdownloader.h" #include "replay/ienginereplay.h" #include "cl_recordingsessionblockmanager.h" #include "cl_replaycontext.h" #include "cl_recordingsession.h" #include "cl_recordingsessionblock.h" #include "errorsystem.h" #include "convar.h" #include "vprof.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" //---------------------------------------------------------------------------------------- extern IEngineReplay *g_pEngine; extern ConVar replay_maxconcurrentdownloads; //---------------------------------------------------------------------------------------- int CSessionBlockDownloader::sm_nNumCurrentDownloads = 0; //---------------------------------------------------------------------------------------- CSessionBlockDownloader::CSessionBlockDownloader() : m_nMaxBlock( -1 ) { } void CSessionBlockDownloader::Shutdown() { AbortDownloadsAndCleanup( NULL ); Assert( sm_nNumCurrentDownloads == 0 ); } void CSessionBlockDownloader::AbortDownloadsAndCleanup( CClientRecordingSession *pSession ) { // NOTE: sm_nNumCurrentDownloads will be decremented in OnDownloadComplete(), which is // invoked by CHttpDownloader::AbortDownloadAndCleanup() // Abort any remaining downloads - callbacks will be invoked, so this shutdown // should be called before any of those objects are cleaned up. FOR_EACH_LL( m_lstDownloaders, i ) { CHttpDownloader *pCurDownloader = m_lstDownloaders[ i ]; // If a session was passed in, make sure it has the same handle as the block CClientRecordingSessionBlock *pBlock = (CClientRecordingSessionBlock *)pCurDownloader->GetUserData(); if ( pSession && ( !pBlock || pBlock->m_hSession != pSession->GetHandle() ) ) continue; pCurDownloader->AbortDownloadAndCleanup(); delete pCurDownloader; } m_lstDownloaders.RemoveAll(); } bool CSessionBlockDownloader::AtMaxConcurrentDownloads() const { return ( sm_nNumCurrentDownloads >= replay_maxconcurrentdownloads.GetInt() ); } float CSessionBlockDownloader::GetNextThinkTime() const { return g_pEngine->GetHostTime() + 0.5f; } void CSessionBlockDownloader::Think() { VPROF_BUDGET( "CSessionBlockDownloader::Think", VPROF_BUDGETGROUP_REPLAY ); CBaseThinker::Think(); // Hack to not think right away if ( g_pEngine->GetHostTime() < 3 ) return; // Don't go over the desired maximum # of concurrent downloads if ( !AtMaxConcurrentDownloads() ) { // Go through all blocks and begin downloading any that the server index downloader // has determined are ready CClientRecordingSessionBlockManager *pBlockManager = CL_GetRecordingSessionBlockManager(); FOR_EACH_OBJ( pBlockManager, i ) { CClientRecordingSessionBlock *pBlock = CL_CastBlock( pBlockManager->m_vecObjs[ i ] ); // Checks to see if the remote status is marked as ready for download if ( !pBlock->ShouldDownloadNow() ) continue; // Lookup the session for the block CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->Find( pBlock->m_hSession ) ); if ( !pSession ) { AssertMsg( 0, "Session for block not found! This should never happen!" ); continue; } // Do we need any blocks at all from this session? Is this block within range? int iLastBlockToDownload = pSession->GetLastBlockToDownload(); if ( iLastBlockToDownload < 0 || pBlock->m_iReconstruction > iLastBlockToDownload ) { continue; } // Begin the download CHttpDownloader *pDownloader = new CHttpDownloader( this ); const char *pFilename = V_UnqualifiedFileName( pBlock->m_szFullFilename ); #ifdef _DEBUG extern ConVar replay_forcedownloadurl; const char *pForceURL = replay_forcedownloadurl.GetString(); const char *pURL = pForceURL[0] ? pForceURL : Replay_va( "%s%s", pSession->m_strBaseDownloadURL.Get(), pFilename ); #else const char *pURL = Replay_va( "%s%s", pSession->m_strBaseDownloadURL.Get(), pFilename ); #endif const char *pGamePath = Replay_va( "%s%s", CL_GetRecordingSessionBlockManager()->GetSavePath(), pFilename ); pDownloader->BeginDownload( pURL, pGamePath, (void *)pBlock, &pBlock->m_uBytesDownloaded ); IF_REPLAY_DBG( Warning ( "%s block %i from %s to path %s...\n", pBlock->GetNumDownloadAttempts() ? "RETRYING download for" : "Downloading" , pBlock->m_iReconstruction, pURL, pGamePath ) ); // Add the downloader m_lstDownloaders.AddToTail( pDownloader ); // Update block's status pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADING; // Mark as dirty CL_GetRecordingSessionBlockManager()->FlagForFlush( pBlock, false ); // Update # of concurrent downloads ++sm_nNumCurrentDownloads; // Get out if we're at max downloads now if ( AtMaxConcurrentDownloads() ) break; } } int it = m_lstDownloaders.Head(); while ( it != m_lstDownloaders.InvalidIndex() ) { // Remove finished downloaders CHttpDownloader *pCurDownloader = m_lstDownloaders[ it ]; if ( pCurDownloader->IsDone() && pCurDownloader->CanDelete() ) { int itRemove = it; // Next it = m_lstDownloaders.Next( it ); // Remove the downloader from the list m_lstDownloaders.Remove( itRemove ); // Free the downloader delete pCurDownloader; } else { // Let the downloader think pCurDownloader->Think(); // Next it = m_lstDownloaders.Next( it ); } } } void CSessionBlockDownloader::OnConnecting( CHttpDownloader *pDownloader ) { CClientRecordingSessionBlock *pBlock = (CClientRecordingSessionBlock *)pDownloader->GetUserData(); AssertValidReadPtr( pBlock ); pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_CONNECTING; } void CSessionBlockDownloader::OnFetch( CHttpDownloader *pDownloader ) { CClientRecordingSessionBlock *pBlock = (CClientRecordingSessionBlock *)pDownloader->GetUserData(); AssertValidReadPtr( pBlock ); pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADING; } void CSessionBlockDownloader::OnDownloadComplete( CHttpDownloader *pDownloader, const unsigned char *pData ) { // TODO: Compare downloaded byte size (pDownloader->GetBytesDownloaded()) to size in block // Write block size into session info on server int it = m_lstDownloaders.Find( pDownloader ); if ( it == m_lstDownloaders.InvalidIndex() ) { AssertMsg( 0, "Downloader now found in session block downloader list! This should never happen!" ); return; } CClientRecordingSessionBlock *pBlock = (CClientRecordingSessionBlock *)pDownloader->GetUserData(); AssertValidReadPtr( pBlock ); const int nSize = pDownloader->GetSize(); HTTPStatus_t nStatus = pDownloader->GetStatus(); #if _DEBUG extern ConVar replay_simulatedownloadfailure; if ( replay_simulatedownloadfailure.GetInt() == 3 ) { nStatus = HTTP_ERROR; } #endif switch ( nStatus ) { case HTTP_ABORTED: pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_ABORTED; break; case HTTP_DONE: { unsigned char aLocalHash[16]; #if _DEBUG extern ConVar replay_simulate_size_discrepancy; extern ConVar replay_simulate_bad_hash; const bool bSizesDiffer = replay_simulate_size_discrepancy.GetBool() || pBlock->m_uFileSize != pDownloader->GetBytesDownloaded(); const bool bHashFail = replay_simulate_bad_hash.GetBool() || !pBlock->ValidateData( pData, nSize, aLocalHash ); #else const bool bSizesDiffer = pBlock->m_uFileSize != pDownloader->GetBytesDownloaded(); const bool bHashFail = !pBlock->ValidateData( pData, nSize ); #endif bool bTryAgain = false; if ( bSizesDiffer ) { AssertMsg( 0, "Number of bytes downloaded differs from size specified in session info file." ); bTryAgain = true; } else if ( bHashFail ) { DBG( "Download failed - either data validation failed\n" ); // Data validation failed pBlock->m_bDataInvalid = true; bTryAgain = true; } else { DBG( "Data validation successful.\n" ); // Data validation succeeded pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_DOWNLOADED; // Clear out any previous errors pBlock->m_nHttpError = HTTP_ERROR_NONE; pBlock->m_bDataInvalid = false; } // Failed? if ( bTryAgain ) { // Attempt to download again if necessary pBlock->AttemptToResetForDownload(); // Report error to OGS. CL_GetErrorSystem()->OGS_ReportSessionBlockDownloadError( pDownloader, pBlock, pDownloader->GetBytesDownloaded(), m_nMaxBlock, &bSizesDiffer, &bHashFail, aLocalHash ); } } break; case HTTP_ERROR: // If we've attempted and failed to download the block 3 times, report the error and // put the block in error state. if ( pBlock->AttemptToResetForDownload() ) break; // Otherwise, we've max'd out attempts - cache the error state pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_ERROR; pBlock->m_nHttpError = pDownloader->GetError(); // Now that the block is in the error state, the replay's status will be updated to // the error state as well (see pSession->UpdateReplayStatuses() below). // Report the error to user & OGS { // Create a session block download error. CL_GetErrorSystem()->OGS_ReportSessionBlockDownloadError( pDownloader, pBlock, pDownloader->GetBytesDownloaded(), m_nMaxBlock, NULL, NULL, NULL ); // Report error to user. const char *pToken = CHttpDownloader::GetHttpErrorToken( pDownloader->GetError() ); CL_GetErrorSystem()->AddFormattedErrorFromTokenName( "#Replay_DL_Err_HTTP_Prefix", new KeyValues( "args", "err", pToken ) ); } break; default: AssertMsg( 0, "Invalid download state in CSessionBlockDownloader::OnDownloadComplete()" ); } // Flag block for flush CL_GetRecordingSessionBlockManager()->FlagForFlush( pBlock, false ); CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSession( pBlock->m_hSession ) ); Assert( pSession ); // Update all replays that care about this block pSession->UpdateReplayStatuses( pBlock ); // Decrement # of downloads --sm_nNumCurrentDownloads; } //----------------------------------------------------------------------------------------