source-engine/replay/cl_downloader.cpp

300 lines
7.6 KiB
C++
Raw Normal View History

2020-04-22 16:56:21 +00:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=======================================================================================//
#if defined( WIN32 )
#include "winlite.h"
#include <WinInet.h>
#endif
#include "cl_downloader.h"
#include "engine/requestcontext.h"
#include "replaysystem.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//----------------------------------------------------------------------------------------
extern IEngineClientReplay *g_pEngineClient;
extern IEngineReplay *g_pEngine;
//----------------------------------------------------------------------------------------
CHttpDownloader::CHttpDownloader( IDownloadHandler *pHandler )
: m_pHandler( pHandler ),
m_flNextThinkTime( 0.0f ),
m_uBytesDownloaded( 0 ),
m_uSize( 0 ),
m_pThreadState( NULL ),
m_bDone( false ),
m_nHttpError( HTTP_ERROR_NONE ),
m_nHttpStatus( HTTP_INVALID ),
m_pBytesDownloaded( NULL ),
m_pUserData( NULL )
{
}
CHttpDownloader::~CHttpDownloader()
{
CleanupThreadIfDone();
}
bool CHttpDownloader::CleanupThreadIfDone()
{
if ( !m_pThreadState || !m_pThreadState->threadDone )
return false;
// NOTE: The context's "data" member will have already been cleaned up by the
// download thread at this point.
delete m_pThreadState;
m_pThreadState = NULL;
return true;
}
bool CHttpDownloader::BeginDownload( const char *pURL, const char *pGamePath, void *pUserData, uint32 *pBytesDownloaded )
{
if ( !pURL || !pURL[0] )
return false;
m_pThreadState = new RequestContext_t();
if ( !m_pThreadState )
return false;
// Cache any user data
m_pUserData = pUserData;
// Cache bytes downloaded
m_pBytesDownloaded = pBytesDownloaded;
// Setup request context
Replay_CrackURL( pURL, m_pThreadState->baseURL, m_pThreadState->urlPath );
m_pThreadState->bAsHTTP = true;
if ( pGamePath )
{
m_pThreadState->bSuppressFileWrite = false;
V_strcpy( m_pThreadState->gamePath, pGamePath );
// Generate the actual filename to save. Well, it's not
// absolute, but this will work.
V_strcpy_safe( m_pThreadState->absLocalPath, g_pEngine->GetGameDir() );
V_AppendSlash( m_pThreadState->absLocalPath, sizeof(m_pThreadState->absLocalPath) );
V_strcat_safe( m_pThreadState->absLocalPath, pGamePath );
}
else
{
m_pThreadState->bSuppressFileWrite = true;
}
// Cache URL - for debugging
V_strcpy( m_szURL, pURL );
// Spawn the download thread
extern IDownloadSystem *g_pDownloadSystem;
return g_pDownloadSystem->CreateDownloadThread( m_pThreadState ) != 0;
}
void CHttpDownloader::AbortDownloadAndCleanup()
{
// Make sure that this function isn't executed simultaneously by
// multiple threads in order to avoid use-after-free crashes during
// shutdown.
AUTO_LOCK( m_lock );
if ( !m_pThreadState )
return;
// Thread already completed?
if ( m_pThreadState->threadDone )
{
CleanupThreadIfDone();
return;
}
// Loop until the thread cleans up
m_pThreadState->shouldStop = true;
while ( !m_pThreadState->threadDone )
;
// Cache state for handler
m_nHttpError = m_pThreadState->error;
m_nHttpStatus = HTTP_ABORTED; // Force this to be safe
m_uBytesDownloaded = 0;
m_uSize = m_pThreadState->nBytesTotal;
m_bDone = true;
InvokeHandler();
CleanupThreadIfDone();
}
void CHttpDownloader::Think()
{
const float flHostTime = g_pEngine->GetHostTime();
if ( m_flNextThinkTime > flHostTime )
return;
if ( !m_pThreadState )
return;
// If thread is done, cleanup now
if ( CleanupThreadIfDone() )
return;
// If we haven't already set shouldStop, check the download status
if ( !m_pThreadState->shouldStop )
{
// Security measure: make sure the file size isn't outrageous
const bool bEvilFileSize = m_pThreadState->nBytesTotal &&
m_pThreadState->nBytesTotal >= DOWNLOAD_MAX_SIZE;
#if _DEBUG
extern ConVar replay_simulate_evil_download_size;
if ( replay_simulate_evil_download_size.GetBool() || bEvilFileSize )
#else
if ( bEvilFileSize )
#endif
{
AbortDownloadAndCleanup();
return;
}
bool bConnecting = false; // For fall-through in HTTP_CONNECTING case.
#if _DEBUG
extern ConVar replay_simulatedownloadfailure;
if ( replay_simulatedownloadfailure.GetInt() == 1 )
{
m_pThreadState->status = HTTP_ERROR;
}
#endif
switch ( m_pThreadState->status )
{
case HTTP_CONNECTING:
// Call connecting handler
if ( m_pHandler )
{
m_pHandler->OnConnecting( this );
}
bConnecting = true;
// Fall-through
case HTTP_FETCH:
m_uBytesDownloaded = (uint32)m_pThreadState->nBytesCurrent;
m_uSize = m_pThreadState->nBytesTotal;
Assert( m_uBytesDownloaded <= m_uSize );
// Call fetch handle
if ( !bConnecting && m_pHandler )
{
m_pHandler->OnFetch( this );
}
break;
case HTTP_ABORTED:
case HTTP_DONE:
case HTTP_ERROR:
// Cache state
m_nHttpError = m_pThreadState->error;
m_nHttpStatus = m_pThreadState->status;
m_uBytesDownloaded = (uint32)m_pThreadState->nBytesCurrent;
m_uSize = m_pThreadState->nBytesTotal; // NOTE: Need to do this here in the case that a file is small enough that we never hit HTTP_FETCH
m_bDone = true;
// Call handler
InvokeHandler();
// Tell the thread to cleanup so we can free it
m_pThreadState->shouldStop = true;
break;
}
}
// Write bytes for user if changed
if ( m_pBytesDownloaded && *m_pBytesDownloaded != m_uBytesDownloaded )
{
*m_pBytesDownloaded = m_uBytesDownloaded;
IF_REPLAY_DBG( Warning( "%s: Downloaded %i/%i bytes\n", m_szURL, m_uBytesDownloaded, m_uSize ) );
}
// Set next think time
m_flNextThinkTime = flHostTime + 0.1f;
}
void CHttpDownloader::InvokeHandler()
{
if ( !m_pHandler )
return;
// NOTE: Don't delete the downloader in OnDownloadComplete()!
m_pHandler->OnDownloadComplete( this, m_pThreadState->data );
}
// This does not increment the "ErrorCounter" field and should only be called from code
// that eventually calls into OGS_ReportGenericError().
KeyValues *CHttpDownloader::GetOgsRow( int nErrorCounter ) const
{
KeyValues *pResult = new KeyValues( "TF2ReplayHttpDownloadErrors" );
pResult->SetInt( "ErrorCounter", nErrorCounter );
pResult->SetInt( "BytesDownloaded", (int)m_uBytesDownloaded );
pResult->SetInt( "BytesTotal", (int)m_uSize );
pResult->SetInt( "HttpStatus", m_nHttpStatus );
pResult->SetInt( "HttpError", m_nHttpError );
pResult->SetString( "URL", m_szURL );
return pResult;
}
/*static*/ const char *CHttpDownloader::GetHttpErrorToken( HTTPError_t nError )
{
switch ( nError )
{
case HTTP_ERROR_ZERO_LENGTH_FILE: return "#HTTPError_ZeroLengthFile";
case HTTP_ERROR_CONNECTION_CLOSED: return "#HTTPError_ConnectionClosed";
case HTTP_ERROR_INVALID_URL: return "#HTTPError_InvalidURL";
case HTTP_ERROR_INVALID_PROTOCOL: return "#HTTPError_InvalidProtocol";
case HTTP_ERROR_CANT_BIND_SOCKET: return "#HTTPError_CantBindSocket";
case HTTP_ERROR_CANT_CONNECT: return "#HTTPError_CantConnect";
case HTTP_ERROR_NO_HEADERS: return "#HTTPError_NoHeaders";
case HTTP_ERROR_FILE_NONEXISTENT: return "#HTTPError_NonExistent";
}
return "#HTTPError_Unknown";
}
//----------------------------------------------------------------------------------------
#ifdef _DEBUG
CHttpDownloader *g_pTestDownload = NULL;
ConVar replay_forcedownloadurl( "replay_forcedownloadurl", "" );
CON_COMMAND( replay_testdownloader_start, "" )
{
const char *pGamePath = Replay_va( "%s%s", CL_GetRecordingSessionBlockManager()->GetSavePath(), "testdownload" );
g_pTestDownload = new CHttpDownloader();
g_pTestDownload->BeginDownload( args[1], pGamePath );
}
CON_COMMAND( replay_testdownloader_abort, "" )
{
if ( !g_pTestDownload )
return;
g_pTestDownload->AbortDownloadAndCleanup();
delete g_pTestDownload;
g_pTestDownload = NULL;
}
#endif