|
|
//========= 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
|