Team Fortress 2 Source Code as on 22/4/2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
15 KiB

  1. //========= Copyright Valve Corporation, All rights reserved. ============//
  2. //
  3. //=======================================================================================//
  4. #include "cl_sessioninfodownloader.h"
  5. #include "replay/ienginereplay.h"
  6. #include "replay/shared_defs.h"
  7. #include "cl_recordingsession.h"
  8. #include "cl_recordingsessionblock.h"
  9. #include "cl_replaycontext.h"
  10. #include "cl_sessionblockdownloader.h"
  11. #include "KeyValues.h"
  12. #include "convar.h"
  13. #include "dbg.h"
  14. #include "vprof.h"
  15. #include "sessioninfoheader.h"
  16. #include "utlbuffer.h"
  17. // memdbgon must be the last include file in a .cpp file!!!
  18. #include "tier0/memdbgon.h"
  19. //----------------------------------------------------------------------------------------
  20. extern IEngineReplay *g_pEngine;
  21. //----------------------------------------------------------------------------------------
  22. CSessionInfoDownloader::CSessionInfoDownloader()
  23. : m_pDownloader( NULL ),
  24. m_pSession( NULL ),
  25. m_flLastDownloadTime( 0.0f ),
  26. m_nError( ERROR_NONE ),
  27. m_nHttpError( HTTP_ERROR_NONE ),
  28. m_bDone( false )
  29. {
  30. }
  31. CSessionInfoDownloader::~CSessionInfoDownloader()
  32. {
  33. Assert( m_pDownloader == NULL ); // We should have deleted the downloader already
  34. }
  35. void CSessionInfoDownloader::CleanupDownloader()
  36. {
  37. if ( m_pDownloader )
  38. {
  39. m_pDownloader->AbortDownloadAndCleanup();
  40. m_pDownloader = NULL;
  41. }
  42. }
  43. void CSessionInfoDownloader::DownloadSessionInfoAndUpdateBlocks( CBaseRecordingSession *pSession )
  44. {
  45. Assert( m_pDownloader == NULL );
  46. // Cache session
  47. m_pSession = pSession;
  48. // Download the session info now
  49. m_pDownloader = new CHttpDownloader( this );
  50. m_pDownloader->BeginDownload( pSession->GetSessionInfoURL(), NULL );
  51. }
  52. float CSessionInfoDownloader::GetNextThinkTime() const
  53. {
  54. extern ConVar replay_sessioninfo_updatefrequency;
  55. return m_flLastDownloadTime + replay_sessioninfo_updatefrequency.GetFloat();
  56. }
  57. void CSessionInfoDownloader::Think()
  58. {
  59. VPROF_BUDGET( "CSessionInfoDownloader::Think", VPROF_BUDGETGROUP_REPLAY );
  60. CBaseThinker::Think();
  61. // If we're not downloading, no need to think
  62. if ( !m_pDownloader )
  63. return;
  64. // If the download's complete
  65. if ( m_pDownloader->IsDone() && m_pDownloader->CanDelete() )
  66. {
  67. // We're done - CanDelete() will now return true
  68. delete m_pDownloader;
  69. m_pDownloader = NULL;
  70. }
  71. else
  72. {
  73. // Otherwise, think...
  74. m_pDownloader->Think();
  75. }
  76. }
  77. void CSessionInfoDownloader::OnDownloadComplete( CHttpDownloader *pDownloader, const unsigned char *pData )
  78. {
  79. Assert( pDownloader );
  80. // Clear out any previous error
  81. m_nError = ERROR_NONE;
  82. m_nHttpError = HTTP_ERROR_NONE;
  83. bool bUpdatedSomething = false;
  84. #if _DEBUG
  85. extern ConVar replay_simulatedownloadfailure;
  86. const bool bForceError = replay_simulatedownloadfailure.GetInt() == 2;
  87. #else
  88. const bool bForceError = false;
  89. #endif
  90. if ( pDownloader->GetStatus() != HTTP_DONE || bForceError )
  91. {
  92. m_nError = ERROR_DOWNLOAD_FAILED;
  93. m_nHttpError = pDownloader->GetError();
  94. DBG( "Session info download FAILED.\n" );
  95. }
  96. else
  97. {
  98. Assert( pData );
  99. // Read header
  100. SessionInfoHeader_t header;
  101. if ( !ReadSessionInfoHeader( pData, pDownloader->GetSize(), header ) )
  102. {
  103. m_nError = ERROR_NOT_ENOUGH_DATA;
  104. }
  105. else
  106. {
  107. IF_REPLAY_DBG( Warning( "Session info downloaded successfully for session %s\n", header.m_szSessionName ) );
  108. // Get number of blocks for data validation
  109. const int nNumBlocks = header.m_nNumBlocks;
  110. if ( nNumBlocks <= 0 )
  111. {
  112. m_nError = ERROR_BAD_NUM_BLOCKS;
  113. }
  114. else
  115. {
  116. // The block has either been found or created - now, fill it with data
  117. const char *pSessionName = header.m_szSessionName;
  118. if ( !pSessionName || !pSessionName[0] )
  119. {
  120. m_nError = ERROR_NO_SESSION_NAME;
  121. }
  122. else
  123. {
  124. // Now that we have the session name, find it in the client session manager
  125. CClientRecordingSession *pSession = CL_CastSession( CL_GetRecordingSessionManager()->FindSessionByName( pSessionName ) ); Assert( pSession );
  126. if ( !pSession )
  127. {
  128. AssertMsg( 0, "Session should always exist by this point" );
  129. m_nError = ERROR_UNKNOWN_SESSION;
  130. }
  131. else
  132. {
  133. // Get recording state for session
  134. pSession->m_bRecording = header.m_bRecording;
  135. const CompressorType_t nHeaderCompressorType = header.m_nCompressorType;
  136. uint8 *pPayload = (uint8 *)pData + sizeof( SessionInfoHeader_t );
  137. uint8 *pUncompressedPayload = NULL;
  138. unsigned int uUncompressedPayloadSize = header.m_uPayloadSizeUC;
  139. // Validate the payload with the MD5 digest
  140. bool bPayloadValid = g_pEngine->MD5_HashBuffer( header.m_aHash, (const uint8 *)pPayload, header.m_uPayloadSize, NULL );
  141. if ( !bPayloadValid )
  142. {
  143. m_nError = ERROR_PAYLOAD_HASH_FAILED;
  144. }
  145. else
  146. {
  147. if ( nHeaderCompressorType == COMPRESSORTYPE_INVALID )
  148. {
  149. // Uncompressed payload is read to go
  150. pUncompressedPayload = pPayload;
  151. }
  152. else
  153. {
  154. // Attempt to decompress the payload
  155. ICompressor *pCompressor = CreateCompressor( header.m_nCompressorType );
  156. if ( !pCompressor )
  157. {
  158. bPayloadValid = false;
  159. m_nError = ERROR_COULD_NOT_CREATE_COMPRESSOR;
  160. }
  161. else
  162. {
  163. // Uncompressed size big enough to read at least one block?
  164. if ( header.m_uPayloadSizeUC <= MIN_SESSION_INFO_PAYLOAD_SIZE )
  165. {
  166. bPayloadValid = false;
  167. m_nError = ERROR_INVALID_UNCOMPRESSED_SIZE;
  168. }
  169. else
  170. {
  171. // Attempt to decompress payload now
  172. pUncompressedPayload = new uint8[ uUncompressedPayloadSize ];
  173. if ( !pCompressor->Decompress( (char *)pUncompressedPayload, &uUncompressedPayloadSize, (const char *)pPayload, header.m_uPayloadSize ) )
  174. {
  175. bPayloadValid = false;
  176. m_nError = ERROR_PAYLOAD_DECOMPRESS_FAILED;
  177. }
  178. }
  179. }
  180. }
  181. if ( bPayloadValid )
  182. {
  183. AssertMsg( pUncompressedPayload, "This should never be NULL here." );
  184. AssertMsg( uUncompressedPayloadSize >= MIN_SESSION_INFO_PAYLOAD_SIZE, "This size should always be valid here." );
  185. RecordingSessionBlockSpec_t DummyBlock;
  186. CUtlBuffer buf( pUncompressedPayload, uUncompressedPayloadSize, CUtlBuffer::READ_ONLY );
  187. // Optimization: start the read at the first block we care about, which is one block after the last consecutive, downloaded block.
  188. // This optimization should come in handy on servers that run lengthy rounds, so we're not reiterating over inconsequential blocks
  189. // (i.e. they were already downloaded).
  190. int iStartBlock = pSession->GetGreatestConsecutiveBlockDownloaded() + 1;
  191. if ( iStartBlock > 0 )
  192. {
  193. buf.SeekGet( CUtlBuffer::SEEK_HEAD, iStartBlock * sizeof( RecordingSessionBlockSpec_t ) );
  194. }
  195. // If for some reason the seek caused a 'get' overflow, try reading from the start of the buffer.
  196. if ( !buf.IsValid() )
  197. {
  198. iStartBlock = 0;
  199. buf.SeekGet( CUtlBuffer::SEEK_HEAD, 0 );
  200. }
  201. // Read blocks, starting from the calculated start block.
  202. for ( int i = iStartBlock; i < header.m_nNumBlocks; ++i )
  203. {
  204. // Attempt to read the current block from the buffer
  205. buf.Get( &DummyBlock, sizeof( DummyBlock ) );
  206. if ( !buf.IsValid() )
  207. {
  208. m_nError = ERROR_BLOCK_READ_FAILED;
  209. break;
  210. }
  211. IF_REPLAY_DBG( Warning( "processing block with recon index: %i\n", DummyBlock.m_iReconstruction ) );
  212. // Get reconstruction index
  213. const int iBlockReconstruction = (ReplayHandle_t)DummyBlock.m_iReconstruction;
  214. if ( iBlockReconstruction < 0 )
  215. {
  216. m_nError = ERROR_INVALID_ORDER;
  217. continue;
  218. }
  219. // Check status
  220. const int nRemoteStatus = (int)DummyBlock.m_uRemoteStatus;
  221. if ( nRemoteStatus < 0 || nRemoteStatus >= CBaseRecordingSessionBlock::MAX_STATUS )
  222. {
  223. // Status not found or invalid status
  224. m_nError = ERROR_INVALID_REPLAY_STATUS;
  225. continue;
  226. }
  227. // Get the block file size
  228. const uint32 uFileSize = (uint32)DummyBlock.m_uFileSize;
  229. // Get the uncompressed block size
  230. const uint32 uUncompressedSize = (uint32)DummyBlock.m_uUncompressedSize;
  231. // Get the compressor type
  232. const int nCompressorType = (uint32)DummyBlock.m_nCompressorType;
  233. // Attempt to find the block in the session
  234. CClientRecordingSessionBlock *pBlock = CL_CastBlock( CL_GetRecordingSessionBlockManager()->FindBlockForSession( m_pSession->GetHandle(), iBlockReconstruction ) );
  235. // If the block exists and has already been downloaded, we have nothing more to update
  236. if ( pBlock && !pBlock->NeedsUpdate() )
  237. continue;
  238. bool bBlockDataChanged = false;
  239. // If the block doesn't exist in the session block manager, create it now
  240. if ( !pBlock )
  241. {
  242. CClientRecordingSessionBlock *pNewBlock = CL_CastBlock( CL_GetRecordingSessionBlockManager()->CreateAndGenerateHandle() );
  243. pNewBlock->m_iReconstruction = iBlockReconstruction;
  244. // pNewBlock->m_strFullFilename = Replay_va(
  245. const char *pFullFilename = Replay_va(
  246. "%s%s_part_%i.%s",
  247. pNewBlock->GetPath(),
  248. pSession->m_strName.Get(), pNewBlock->m_iReconstruction,
  249. BLOCK_FILE_EXTENSION
  250. );
  251. V_strcpy( pNewBlock->m_szFullFilename, pFullFilename );
  252. pNewBlock->m_hSession = pSession->GetHandle();
  253. // Add to session block manager
  254. CL_GetRecordingSessionBlockManager()->Add( pNewBlock );
  255. // Add the block to the session (marks session as dirty)
  256. pSession->AddBlock( pNewBlock, false );
  257. // Use the new block
  258. pBlock = pNewBlock;
  259. bBlockDataChanged = true;
  260. }
  261. IF_REPLAY_DBG2( Warning( " Block %i status=%s\n", pBlock->m_iReconstruction, pBlock->GetRemoteStatusStringSafe( pBlock->m_nRemoteStatus ) ) );
  262. // Now that we've got a block, replicate the server data/fill it in
  263. if ( pBlock->m_nRemoteStatus != nRemoteStatus )
  264. {
  265. pBlock->m_nRemoteStatus = (CBaseRecordingSessionBlock::RemoteStatus_t)nRemoteStatus;
  266. bBlockDataChanged = true;
  267. }
  268. // Block file size needs to be set?
  269. if ( pBlock->m_uFileSize != uFileSize )
  270. {
  271. pBlock->m_uFileSize = uFileSize;
  272. bBlockDataChanged = true;
  273. }
  274. // Uncompressed block file size needs to be set?
  275. if ( pBlock->m_uUncompressedSize != uUncompressedSize )
  276. {
  277. Assert( nCompressorType >= COMPRESSORTYPE_INVALID );
  278. pBlock->m_uUncompressedSize = uUncompressedSize;
  279. bBlockDataChanged = true;
  280. }
  281. // Compressor type needs to be set?
  282. if ( pBlock->m_nCompressorType != (CompressorType_t)nCompressorType )
  283. {
  284. pBlock->m_nCompressorType = (CompressorType_t)nCompressorType;
  285. bBlockDataChanged = true;
  286. }
  287. // Attempt to read the hash if we haven't already done so
  288. if ( !pBlock->HasValidHash() )
  289. {
  290. V_memcpy( pBlock->m_aHash, DummyBlock.m_aHash, sizeof( pBlock->m_aHash ) );
  291. bBlockDataChanged = true;
  292. }
  293. // Shift the block's state from waiting to ready-to-download if the block is ready on the server.
  294. if ( pBlock->m_nDownloadStatus == CClientRecordingSessionBlock::DOWNLOADSTATUS_WAITING &&
  295. nRemoteStatus == CBaseRecordingSessionBlock::STATUS_READYFORDOWNLOAD )
  296. {
  297. pBlock->m_nDownloadStatus = CClientRecordingSessionBlock::DOWNLOADSTATUS_READYTODOWNLOAD;
  298. }
  299. // Save
  300. if ( bBlockDataChanged )
  301. {
  302. CL_GetRecordingSessionBlockManager()->FlagForFlush( pBlock, false );
  303. bUpdatedSomething = true;
  304. }
  305. }
  306. // If this session is not recording, make sure the max number of blocks in the session is in check.
  307. if ( !pSession->m_bRecording && pSession->GetLastBlockToDownload() >= nNumBlocks )
  308. {
  309. // This will adjust all replay max blocks
  310. pSession->AdjustLastBlockToDownload( nNumBlocks - 1 );
  311. }
  312. // If we've updated something, set cache the current time in the session
  313. if ( bUpdatedSomething )
  314. {
  315. pSession->RefreshLastUpdateTime();
  316. }
  317. else if ( pSession->GetLastUpdateTime() >= 0.0f && ( g_pEngine->GetHostTime() - pSession->GetLastUpdateTime() > DOWNLOAD_TIMEOUT_THRESHOLD ) )
  318. {
  319. pSession->OnDownloadTimeout();
  320. }
  321. }
  322. }
  323. }
  324. }
  325. }
  326. }
  327. }
  328. // Display a message for the given download error
  329. if ( m_nError != ERROR_NONE )
  330. {
  331. // Report an error to the user
  332. const char *pErrorToken = GetErrorString( m_nError, m_nHttpError );
  333. if ( m_nError == ERROR_DOWNLOAD_FAILED )
  334. {
  335. KeyValues *pParams = new KeyValues( "args", "url", pDownloader->GetURL() );
  336. CL_GetErrorSystem()->AddFormattedErrorFromTokenName( pErrorToken, pParams );
  337. }
  338. else
  339. {
  340. CL_GetErrorSystem()->AddErrorFromTokenName( pErrorToken );
  341. }
  342. // Report error to OGS
  343. CL_GetErrorSystem()->OGS_ReportSessioInfoDownloadError( pDownloader, pErrorToken );
  344. }
  345. // Flag as done
  346. m_bDone = true;
  347. // Cache download time
  348. m_flLastDownloadTime = g_pEngine->GetHostTime();
  349. }
  350. const char *CSessionInfoDownloader::GetErrorString( int nError, HTTPError_t nHttpError ) const
  351. {
  352. switch ( nError )
  353. {
  354. case ERROR_NO_SESSION_NAME: return "#Replay_DL_Err_SI_NoSessionName";
  355. case ERROR_REPLAY_NOT_FOUND: return "#Replay_DL_Err_SI_ReplayNotFound";
  356. case ERROR_INVALID_REPLAY_STATUS: return "#Replay_DL_Err_SI_InvalidReplayStatus";
  357. case ERROR_INVALID_ORDER: return "#Replay_DL_Err_SI_InvalidOrder";
  358. case ERROR_UNKNOWN_SESSION: return "#Replay_DL_Err_SI_Unknown_Session";
  359. case ERROR_BLOCK_READ_FAILED: return "#Replay_DL_Err_SI_BlockReadFailed";
  360. case ERROR_NOT_ENOUGH_DATA: return "#Replay_DL_Err_SI_NotEnoughData";
  361. case ERROR_COULD_NOT_CREATE_COMPRESSOR: return "#Replay_DL_Err_SI_CouldNotCreateCompressor";
  362. case ERROR_INVALID_UNCOMPRESSED_SIZE: return "#Replay_DL_Err_SI_InvalidUncompressedSize";
  363. case ERROR_PAYLOAD_DECOMPRESS_FAILED: return "#Replay_DL_Err_SI_PayloadDecompressFailed";
  364. case ERROR_PAYLOAD_HASH_FAILED: return "#Replay_DL_Err_SI_PayloadHashFailed";
  365. case ERROR_DOWNLOAD_FAILED:
  366. switch ( m_nHttpError )
  367. {
  368. case HTTP_ERROR_ZERO_LENGTH_FILE: return "#Replay_DL_Err_SI_DownloadFailed_ZeroLengthFile";
  369. case HTTP_ERROR_CONNECTION_CLOSED: return "#Replay_DL_Err_SI_DownloadFailed_ConnectionClosed";
  370. case HTTP_ERROR_INVALID_URL: return "#Replay_DL_Err_SI_DownloadFailed_InvalidURL";
  371. case HTTP_ERROR_INVALID_PROTOCOL: return "#Replay_DL_Err_SI_DownloadFailed_InvalidProtocol";
  372. case HTTP_ERROR_CANT_BIND_SOCKET: return "#Replay_DL_Err_SI_DownloadFailed_CantBindSocket";
  373. case HTTP_ERROR_CANT_CONNECT: return "#Replay_DL_Err_SI_DownloadFailed_CantConnect";
  374. case HTTP_ERROR_NO_HEADERS: return "#Replay_DL_Err_SI_DownloadFailed_NoHeaders";
  375. case HTTP_ERROR_FILE_NONEXISTENT: return "#Replay_DL_Err_SI_DownloadFailed_FileNonExistent";
  376. default: return "#Replay_DL_Err_SI_DownloadFailed_UnknownError";
  377. }
  378. }
  379. return "#Replay_DL_Err_SI_Unknown";
  380. }
  381. //----------------------------------------------------------------------------------------