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.
1253 lines
34 KiB
1253 lines
34 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
// |
|
//=============================================================================// |
|
|
|
#ifdef _WIN32 |
|
|
|
#if !defined( _X360 ) |
|
#include <winsock.h> |
|
#else |
|
#include "winsockx.h" |
|
#endif |
|
|
|
#elif POSIX |
|
#define INVALID_SOCKET -1 |
|
#define SOCKET_ERROR -1 |
|
#include <sys/types.h> |
|
#include <sys/socket.h> |
|
#include <netinet/in.h> |
|
//$ #include <uuid/uuid.h> |
|
typedef unsigned char uuid_t[16]; |
|
#ifdef OSX |
|
#include <uuid/uuid.h> |
|
#else |
|
typedef unsigned char uuid_t[16]; |
|
#endif |
|
#include <pwd.h> |
|
#define closesocket close |
|
#include "quakedef.h" // build_number() |
|
#endif |
|
|
|
#include "net.h" |
|
#include "quakedef.h" |
|
#include "sv_uploadgamestats.h" |
|
#include "host.h" |
|
#include "host_phonehome.h" |
|
#include "mathlib/IceKey.H" |
|
#include "bitbuf.h" |
|
#include "tier0/icommandline.h" |
|
#include "tier0/vcrmode.h" |
|
#include "blockingudpsocket.h" |
|
#include "cserserverprotocol_engine.h" |
|
#include "utlbuffer.h" |
|
#include "eiface.h" |
|
#include "FindSteamServers.h" |
|
#include <vstdlib/random.h> |
|
#include "iregistry.h" |
|
#include "filesystem_engine.h" |
|
#include "checksum_md5.h" |
|
#include "cl_steamauth.h" |
|
#include "steam/steam_gameserver.h" |
|
#include "materialsystem/imaterialsystemhardwareconfig.h" |
|
#include "tier2/tier2.h" |
|
#include "server.h" |
|
#include "sv_steamauth.h" |
|
#include "host_state.h" |
|
|
|
#if defined( _X360 ) |
|
#include "xbox/xbox_win32stubs.h" |
|
#endif |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
typedef unsigned int u32; |
|
typedef unsigned char u8; |
|
typedef unsigned short u16; |
|
|
|
namespace GameStatsHarvester |
|
{ |
|
|
|
enum EFileType |
|
{ |
|
eFileTypeGameStats, |
|
|
|
eFILETYPECOUNT // Count number of legal values |
|
}; |
|
|
|
|
|
enum ESendMethod |
|
{ |
|
eSendMethodWholeRawFileNoBlocks, |
|
eSendMethodCompressedBlocks, // TODO: Reenable compressed sending of minidumps |
|
|
|
eSENDMETHODCOUNT // Count number of legal values |
|
}; |
|
|
|
} |
|
|
|
using namespace GameStatsHarvester; |
|
|
|
// TODO: cut protocol version down to u8 if possible, to reduce bandwidth usage |
|
// for very frequent but tiny commands. |
|
typedef u32 ProtocolVersion_t; |
|
|
|
typedef u8 ProtocolAcceptanceFlag_t; |
|
typedef u8 ProtocolUnacceptableAck_t; |
|
|
|
typedef u32 MessageSequenceId_t; |
|
|
|
typedef u32 ServerSessionHandle_t; |
|
typedef u32 ClientSessionHandle_t; |
|
|
|
typedef u32 NetworkTransactionId_t; |
|
|
|
// Command codes are intentionally as small as possible to minimize bandwidth usage |
|
// for very frequent but tiny commands (e.g. GDS 'FindServer' commands). |
|
typedef u8 Command_t; |
|
|
|
// ... likewise response codes are as small as possible - we use this when we |
|
// ... can and revert to large types on a case by case basis. |
|
typedef u8 CommandResponse_t; |
|
|
|
|
|
// This define our standard type for length prefix for variable length messages |
|
// in wire protocols. |
|
// This is specifically used by CWSABUFWrapper::PrepareToReceiveLengthPrefixedMessage() |
|
// and its supporting functions. |
|
// It is defined here for generic (portable) network code to use when constructing |
|
// messages to be sent to peers that use the above function. |
|
// e.g. SteamValidateUserIDTickets.dll uses this for that purpose. |
|
|
|
// We support u16 or u32 (obviously switching between them breaks existing protocols |
|
// unless all components are switched simultaneously). |
|
typedef u32 NetworkMessageLengthPrefix_t; |
|
|
|
|
|
// Similarly, strings should be preceeded by their length. |
|
typedef u16 StringLengthPrefix_t; |
|
|
|
|
|
const ProtocolAcceptanceFlag_t cuProtocolIsNotAcceptable |
|
= static_cast<ProtocolAcceptanceFlag_t>( 0 ); |
|
|
|
const ProtocolAcceptanceFlag_t cuProtocolIsAcceptable |
|
= static_cast<ProtocolAcceptanceFlag_t>( 1 ); |
|
|
|
const Command_t cuMaxCommand |
|
= static_cast<Command_t>(255); |
|
|
|
const CommandResponse_t cuMaxCommandResponse |
|
= static_cast<CommandResponse_t>(255); |
|
|
|
// This is for mapping requests back to error ids for placing into the database appropriately. |
|
typedef u32 ContextID_t; |
|
|
|
// This is the version of the protocol used by latest-build clients. |
|
const ProtocolVersion_t cuCurrentProtocolVersion = 1; |
|
|
|
// This is the minimum protocol version number that the client must |
|
// be able to speak in order to communicate with the server. |
|
// The client sends its protocol version this before every command, and if we |
|
// don't support that version anymore then we tell it nicely. The client |
|
// should respond by doing an auto-update. |
|
const ProtocolVersion_t cuRequiredProtocolVersion = 1; |
|
|
|
|
|
namespace Commands |
|
{ |
|
const Command_t cuGracefulClose = 0; |
|
const Command_t cuSendGameStats = 1; |
|
const Command_t cuNumCommands = 2; |
|
const Command_t cuNoCommandReceivedYet = cuMaxCommand; |
|
} |
|
|
|
|
|
namespace HarvestFileCommand |
|
{ |
|
typedef u32 SenderTypeId_t; |
|
typedef u32 SenderTypeUniqueId_t; |
|
typedef u32 SenderSourceCodeControlId_t; |
|
typedef u32 FileSize_t; |
|
|
|
// Legal values defined by EFileType |
|
typedef u32 FileType_t; |
|
|
|
// Legal values defined by ESendMethod |
|
typedef u32 SendMethod_t; |
|
|
|
const CommandResponse_t cuOkToSendFile = 0; |
|
const CommandResponse_t cuFileTooBig = 1; |
|
const CommandResponse_t cuInvalidSendMethod = 2; |
|
const CommandResponse_t cuInvalidMaxCompressedChunkSize = 3; |
|
const CommandResponse_t cuInvalidGameStatsContext = 4; |
|
const uint cuNumCommandResponses = 5; |
|
} |
|
|
|
//############################################################################# |
|
// |
|
// Class declaration: CWin32UploadGameStats |
|
// |
|
//############################################################################# |
|
// |
|
// Authors: |
|
// |
|
// Yahn Bernier |
|
// |
|
// Description and general notes: |
|
// |
|
// Handles uploading game stats data blobs to the CSERServer |
|
// (Client Stats & Error Reporting Server) |
|
|
|
typedef enum |
|
{ |
|
// General status |
|
eGameStatsUploadSucceeded = 0, |
|
eGameStatsUploadFailed, |
|
|
|
// Specific status |
|
eGameStatsBadParameter, |
|
eGameStatsUnknownStatus, |
|
eGameStatsSendingGameStatsHeaderSucceeded, |
|
eGameStatsSendingGameStatsHeaderFailed, |
|
eGameStatsReceivingResponseSucceeded, |
|
eGameStatsReceivingResponseFailed, |
|
eGameStatsConnectToCSERServerSucceeded, |
|
eGameStatsConnectToCSERServerFailed, |
|
eGameStatsUploadingGameStatsSucceeded, |
|
eGameStatsUploadingGameStatsFailed |
|
} EGameStatsUploadStatus; |
|
|
|
struct TGameStatsProgress |
|
{ |
|
// A text string describing the current progress |
|
char m_sStatus[ 512 ]; |
|
}; |
|
|
|
typedef void ( *GAMESTATSREPORTPROGRESSFUNC )( u32 uContext, const TGameStatsProgress & rGameStatsProgress ); |
|
|
|
struct TGameStatsParameters |
|
{ |
|
TGameStatsParameters() : |
|
m_uAppId( 0 ) |
|
{ |
|
} |
|
|
|
// IP Address of the CSERServer to send the report to |
|
netadr_t m_ipCSERServer; |
|
|
|
// Source Control Id (or build_number) of the product |
|
u32 m_uEngineBuildNumber; |
|
// Name of the .exe |
|
char m_sExecutableName[ 64 ]; |
|
// Game directory |
|
char m_sGameDirectory[ 64 ]; |
|
// Map name the server wants to upload statistics about |
|
char m_sMapName[ 64 ]; |
|
|
|
// Version id for stats blob |
|
u32 m_uStatsBlobVersion; |
|
|
|
u32 m_uStatsBlobSize; |
|
void *m_pStatsBlobData; |
|
|
|
u32 m_uProgressContext; |
|
GAMESTATSREPORTPROGRESSFUNC m_pOptionalProgressFunc; |
|
|
|
u32 m_uAppId; |
|
}; |
|
|
|
// Note that this API is blocking, though the callback, if passed, can occur during execution. |
|
EGameStatsUploadStatus Win32UploadGameStatsBlocking |
|
( |
|
const TGameStatsParameters & rGameStatsParameters // Input |
|
); |
|
|
|
class CUploadGameStats : public IUploadGameStats |
|
{ |
|
public: |
|
|
|
#define GAMESTATSUPLOADER_CONNECT_RETRY_TIME 1.0 |
|
|
|
CUploadGameStats() : m_bConnected(false), m_flNextConnectAttempt(0) {} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Initializes the connection to the CSER |
|
//----------------------------------------------------------------------------- |
|
void InitConnection( void ) |
|
{ |
|
AsyncUpload_Shutdown(); |
|
|
|
m_bConnected = false; |
|
m_Adr.Clear(); |
|
m_Adr.SetType( NA_IP ); |
|
m_flNextConnectAttempt = 0; |
|
// don't call UpdateConnection here, does bad things |
|
} |
|
|
|
void UpdateConnection( void ) |
|
{ |
|
if ( m_bConnected || HostState_IsShuttingDown() ) |
|
return; |
|
|
|
// try getting client SteamUtils interface |
|
ISteamUtils *pSteamUtils = NULL; |
|
#ifndef SWDS |
|
pSteamUtils = Steam3Client().SteamUtils(); |
|
#endif |
|
// if that fails, try the game server SteamUtils interface |
|
if ( !pSteamUtils ) |
|
{ |
|
pSteamUtils = Steam3Server().SteamGameServerUtils(); |
|
} |
|
|
|
// can't determine CSER if Steam not running |
|
if ( !pSteamUtils ) |
|
return; |
|
|
|
float curTime = Sys_FloatTime(); |
|
|
|
if ( curTime < m_flNextConnectAttempt ) |
|
return; |
|
|
|
uint32 unIP = 0; |
|
uint16 usPort = 0; |
|
#if !defined( NO_VCR ) |
|
if ( VCRGetMode() != VCR_Playback ) |
|
#endif |
|
{ |
|
pSteamUtils->GetCSERIPPort( &unIP, &usPort ); |
|
} |
|
#if !defined( NO_VCR ) |
|
VCRGenericValue( "a", &unIP, sizeof( unIP ) ); |
|
VCRGenericValue( "b", &usPort, sizeof( usPort ) ); |
|
#endif |
|
|
|
if ( unIP == 0 ) |
|
{ |
|
m_flNextConnectAttempt = curTime + GAMESTATSUPLOADER_CONNECT_RETRY_TIME; |
|
return; |
|
} |
|
else |
|
{ |
|
m_Adr.SetIP( unIP ); |
|
m_Adr.SetPort( usPort ); |
|
m_Adr.SetType( NA_IP ); |
|
m_bConnected = true; |
|
} |
|
} |
|
|
|
virtual bool UploadGameStats( char const *mapname, |
|
unsigned int blobversion, unsigned int blobsize, const void *pvBlobData ) |
|
{ |
|
AsyncUpload_QueueData( mapname, blobversion, blobsize, pvBlobData ); |
|
return true; |
|
//return UploadGameStatsInternal( mapname, blobversion, blobsize, pvBlobData ); |
|
} |
|
|
|
// If user has disabled stats tracking, do nothing |
|
virtual bool IsGameStatsLoggingEnabled() |
|
{ |
|
if ( CommandLine()->FindParm( "-nogamestats" ) ) |
|
return false; |
|
|
|
#ifdef SWDS |
|
return true; |
|
#else |
|
IRegistry *temp = InstanceRegistry( "Steam" ); |
|
Assert( temp ); |
|
// Check registry |
|
int iDisable = temp->ReadInt( "DisableGameStats", 0 ); |
|
|
|
ReleaseInstancedRegistry( temp ); |
|
|
|
if ( iDisable != 0 ) |
|
{ |
|
return false; |
|
} |
|
|
|
return true; |
|
#endif |
|
} |
|
|
|
// Gets a non-personally identifiable unique ID for this steam user, used for tracking total gameplay time across |
|
// multiple stats sessions, but isn't trackable back to their Steam account or id. |
|
// Buffer should be 16 bytes, ID will come back as a hexadecimal string version of a GUID |
|
virtual void GetPseudoUniqueId( char *buf, size_t bufsize ) |
|
{ |
|
Q_memset( buf, 0, bufsize ); |
|
|
|
#ifndef SWDS |
|
IRegistry *temp = InstanceRegistry( "Steam" ); |
|
Assert( temp ); |
|
// Check registry |
|
char const *uuid = temp->ReadString( "PseudoUUID", "" ); |
|
|
|
if ( !uuid || !*uuid ) |
|
{ |
|
// Create a new one |
|
#ifdef WIN32 |
|
UUID newId; |
|
UuidCreate( &newId ); |
|
#elif defined(POSIX) |
|
uuid_t newId; |
|
#ifdef OSX |
|
uuid_generate( newId ); |
|
#endif |
|
#else |
|
#error |
|
#endif |
|
char hex[ 17 ]; |
|
Q_memset( hex, 0, sizeof( hex ) ); |
|
Q_binarytohex( (const byte *)&newId, sizeof( newId ), hex, sizeof( hex ) ); |
|
|
|
// If running at Valve, copy in the users name here |
|
if ( Steam3Client().SteamUtils() && ( Steam3Client().SteamUser()->BLoggedOn() ) && |
|
( k_EUniverseBeta == Steam3Client().SteamUtils()->GetConnectedUniverse() ) ) |
|
{ |
|
bool bOk = true; |
|
char username[ 64 ]; |
|
#if defined( _WIN32 ) |
|
Q_memset( username, 0, sizeof( username ) ); |
|
DWORD length = sizeof( username ) - 1; |
|
if ( !GetUserName( username, &length ) ) |
|
{ |
|
bOk = false; |
|
} |
|
#else |
|
struct passwd *pass = getpwuid( getuid() ); |
|
if ( pass ) |
|
{ |
|
Q_strncpy( username, pass->pw_name, sizeof( username ) ); |
|
} |
|
else |
|
{ |
|
bOk = false; |
|
} |
|
username[sizeof(username)-1] = '\0'; |
|
#endif |
|
if ( bOk ) |
|
{ |
|
int nBytesToCopy = min( (size_t)Q_strlen( username ), sizeof( hex ) - 1 ); |
|
// NOTE: This doesn't copy the NULL terminator from username because we want the "random" bits after the name |
|
Q_memcpy( hex, username, nBytesToCopy ); |
|
} |
|
} |
|
|
|
temp->WriteString( "PseudoUUID", hex ); |
|
|
|
Q_strncpy( buf, hex, bufsize ); |
|
} |
|
else |
|
{ |
|
Q_strncpy( buf, uuid, bufsize ); |
|
} |
|
|
|
ReleaseInstancedRegistry( temp ); |
|
#endif |
|
|
|
if ( ( buf[0] == 0 ) && sv.IsDedicated() ) |
|
{ |
|
// For Linux dedicated servers, where we won't get a unique ID: set the ID to "unknown" so we have something. (If there's no ID, |
|
// stats don't get sent.) This will later get altered to be a hash of IP&port, but this gets called early before IP is determined |
|
// so we can't make the hash now. |
|
Q_strncpy( buf, "unknown", bufsize ); |
|
} |
|
} |
|
|
|
virtual bool IsCyberCafeUser( void ) |
|
{ |
|
// TODO: convert this to be aware of proper Steam3'ified cafes once we actually implement that |
|
return false; |
|
} |
|
|
|
// Only works in single player |
|
virtual bool IsHDREnabled( void ) |
|
{ |
|
#if defined( SWDS ) || defined( _X360 ) |
|
return false; |
|
#else |
|
return g_pMaterialSystemHardwareConfig->GetHDREnabled(); |
|
#endif |
|
} |
|
|
|
bool UploadGameStatsInternal( char const *mapname, |
|
unsigned int blobversion, unsigned int blobsize, const void *pvBlobData ) |
|
{ |
|
// Attempt connection, for backwards compatibility |
|
UpdateConnection(); |
|
|
|
if ( !m_bConnected ) |
|
return false; |
|
|
|
unsigned int useAppId = GetSteamAppID(); |
|
if ( useAppId == 0 ) |
|
return false; |
|
|
|
TGameStatsParameters params; |
|
Q_memset( ¶ms, 0, sizeof( params ) ); |
|
|
|
params.m_ipCSERServer = m_Adr; |
|
|
|
params.m_uEngineBuildNumber = build_number(); |
|
Q_strncpy( params.m_sExecutableName, "hl2.exe", sizeof( params.m_sExecutableName ) ); |
|
Q_FileBase( com_gamedir, params.m_sGameDirectory, sizeof( params.m_sGameDirectory ) ); |
|
Q_FileBase( mapname, params.m_sMapName, sizeof( params.m_sMapName ) ); |
|
params.m_uStatsBlobVersion = blobversion; |
|
params.m_uStatsBlobSize = blobsize; |
|
params.m_pStatsBlobData = ( void * )pvBlobData; |
|
|
|
//////////////////////////////////////////////////////////////////////////// |
|
// New protocol sorts things by Steam AppId (4/6/06 ywb) |
|
params.m_uAppId = useAppId; |
|
//////////////////////////////////////////////////////////////////////////// |
|
|
|
EGameStatsUploadStatus result = Win32UploadGameStatsBlocking( params ); |
|
return ( result == eGameStatsUploadSucceeded ) ? true : false; |
|
} |
|
private: |
|
netadr_t m_Adr; |
|
float m_flNextConnectAttempt; |
|
bool m_bConnected; |
|
}; |
|
|
|
static CUploadGameStats g_UploadGameStats; |
|
IUploadGameStats *g_pUploadGameStats = &g_UploadGameStats; |
|
EXPOSE_SINGLE_INTERFACE_GLOBALVAR( CUploadGameStats, IUploadGameStats, INTERFACEVERSION_UPLOADGAMESTATS, g_UploadGameStats ); |
|
|
|
void UpdateProgress( const TGameStatsParameters & params, char const *fmt, ... ) |
|
{ |
|
if ( !params.m_pOptionalProgressFunc ) |
|
{ |
|
return; |
|
} |
|
|
|
char str[ 2048 ]; |
|
va_list argptr; |
|
va_start( argptr, fmt ); |
|
_vsnprintf( str, sizeof( str ) - 1, fmt, argptr ); |
|
va_end( argptr ); |
|
|
|
char outstr[ 2060 ]; |
|
Q_snprintf( outstr, sizeof( outstr ), "(%u): %s", params.m_uProgressContext, str ); |
|
|
|
TGameStatsProgress progress; |
|
Q_strncpy( progress.m_sStatus, outstr, sizeof( progress.m_sStatus ) ); |
|
|
|
// Invoke the callback |
|
( *params.m_pOptionalProgressFunc )( params.m_uProgressContext, progress ); |
|
} |
|
|
|
class CWin32UploadGameStats |
|
{ |
|
public: |
|
explicit CWin32UploadGameStats( |
|
const netadr_t & harvester, |
|
const TGameStatsParameters & rGameStatsParameters, |
|
u32 contextid ); |
|
~CWin32UploadGameStats(); |
|
|
|
EGameStatsUploadStatus Upload( CUtlBuffer& buf ); |
|
|
|
private: |
|
|
|
enum States |
|
{ |
|
eCreateTCPSocket = 0, |
|
eConnectToHarvesterServer, |
|
eSendProtocolVersion, |
|
eReceiveProtocolOkay, |
|
eSendUploadCommand, |
|
eReceiveOKToSendFile, |
|
eSendWholeFile, // This could push chunks onto the wire, but we'll just use a whole buffer for now. |
|
eReceiveFileUploadSuccess, |
|
eSendGracefulClose, |
|
eCloseTCPSocket |
|
}; |
|
|
|
bool CreateTCPSocket( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool ConnectToHarvesterServer( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool SendProtocolVersion( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool ReceiveProtocolOkay( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool SendUploadCommand( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool ReceiveOKToSendFile( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool SendWholeFile( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool ReceiveFileUploadSuccess( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool SendGracefulClose( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
bool CloseTCPSocket( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
|
|
typedef bool ( CWin32UploadGameStats::*pfnProtocolStateHandler )( EGameStatsUploadStatus& status, CUtlBuffer& buf ); |
|
struct FSMState_t |
|
{ |
|
FSMState_t( uint f, pfnProtocolStateHandler s ) : |
|
first( f ), |
|
second( s ) |
|
{ |
|
} |
|
|
|
uint first; |
|
pfnProtocolStateHandler second; |
|
}; |
|
|
|
void AddState( uint StateIndex, pfnProtocolStateHandler handler ); |
|
void SetNextState( uint StateIndex ); |
|
bool DoBlockingReceive( uint bytesExpected, CUtlBuffer& buf ); |
|
|
|
CUtlVector< FSMState_t > m_States; |
|
uint m_uCurrentState; |
|
struct sockaddr_in m_HarvesterSockAddr; |
|
uint m_SocketTCP; |
|
const TGameStatsParameters &m_rCrashParameters; //lint !e1725 |
|
u32 m_ContextID; |
|
}; |
|
|
|
CWin32UploadGameStats::CWin32UploadGameStats( |
|
const netadr_t & harvester, |
|
const TGameStatsParameters & rGameStatsParameters, |
|
u32 contextid ) : |
|
m_States(), |
|
m_uCurrentState( eCreateTCPSocket ), |
|
m_HarvesterSockAddr(), |
|
m_SocketTCP( 0 ), |
|
m_rCrashParameters( rGameStatsParameters ), |
|
m_ContextID( contextid ) |
|
{ |
|
harvester.ToSockadr( (struct sockaddr *)&m_HarvesterSockAddr ); |
|
|
|
AddState( eCreateTCPSocket, &CWin32UploadGameStats::CreateTCPSocket ); |
|
AddState( eConnectToHarvesterServer, &CWin32UploadGameStats::ConnectToHarvesterServer ); |
|
AddState( eSendProtocolVersion, &CWin32UploadGameStats::SendProtocolVersion ); |
|
AddState( eReceiveProtocolOkay, &CWin32UploadGameStats::ReceiveProtocolOkay ); |
|
AddState( eSendUploadCommand, &CWin32UploadGameStats::SendUploadCommand ); |
|
AddState( eReceiveOKToSendFile, &CWin32UploadGameStats::ReceiveOKToSendFile ); |
|
AddState( eSendWholeFile, &CWin32UploadGameStats::SendWholeFile ); |
|
AddState( eReceiveFileUploadSuccess, &CWin32UploadGameStats::ReceiveFileUploadSuccess ); |
|
AddState( eSendGracefulClose, &CWin32UploadGameStats::SendGracefulClose ); |
|
AddState( eCloseTCPSocket, &CWin32UploadGameStats::CloseTCPSocket ); |
|
} |
|
|
|
CWin32UploadGameStats::~CWin32UploadGameStats() |
|
{ |
|
if ( m_SocketTCP != 0 ) |
|
{ |
|
closesocket( m_SocketTCP ); //lint !e534 |
|
m_SocketTCP = 0; |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// |
|
// Function: DoBlockingReceive() |
|
// |
|
//----------------------------------------------------------------------------- |
|
bool CWin32UploadGameStats::DoBlockingReceive( uint bytesExpected, CUtlBuffer& buf ) |
|
{ |
|
uint totalReceived = 0; |
|
|
|
buf.Purge(); |
|
for ( ;; ) |
|
{ |
|
char temp[ 8192 ]; |
|
|
|
int bytesReceived = recv( m_SocketTCP, temp, sizeof( temp ), 0 ); |
|
if ( bytesReceived <= 0 ) |
|
return false; |
|
|
|
buf.Put( ( const void * )temp, (u32)bytesReceived ); |
|
totalReceived = buf.TellPut(); |
|
if ( totalReceived >= bytesExpected ) |
|
break; |
|
|
|
} |
|
return true; |
|
} |
|
|
|
void CWin32UploadGameStats::AddState( uint StateIndex, pfnProtocolStateHandler handler ) |
|
{ |
|
FSMState_t newState( StateIndex, handler ); |
|
m_States.AddToTail( newState ); |
|
} |
|
|
|
EGameStatsUploadStatus CWin32UploadGameStats::Upload( CUtlBuffer& buf ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Commencing game stats upload connection." ); |
|
|
|
EGameStatsUploadStatus result = eGameStatsUploadSucceeded; |
|
// Run the state machine |
|
while ( 1 ) |
|
{ |
|
Assert( m_States[ m_uCurrentState ].first == m_uCurrentState ); |
|
pfnProtocolStateHandler handler = m_States[ m_uCurrentState ].second; |
|
|
|
if ( !(this->*handler)( result, buf ) ) |
|
{ |
|
return result; |
|
} |
|
} |
|
} |
|
|
|
void CWin32UploadGameStats::SetNextState( uint StateIndex ) |
|
{ |
|
Assert( StateIndex > m_uCurrentState ); |
|
m_uCurrentState = StateIndex; |
|
} |
|
|
|
bool CWin32UploadGameStats::CreateTCPSocket( EGameStatsUploadStatus& status, CUtlBuffer& /*buf*/ ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Creating game stats upload socket." ); |
|
|
|
m_SocketTCP = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); |
|
if ( m_SocketTCP == (uint)SOCKET_ERROR ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Socket creation failed." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
SetNextState( eConnectToHarvesterServer ); |
|
return true; |
|
} |
|
|
|
bool CWin32UploadGameStats::ConnectToHarvesterServer( EGameStatsUploadStatus& status, CUtlBuffer& /*buf*/ ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Connecting to game stats harvesting server." ); |
|
|
|
if ( connect( m_SocketTCP, (const sockaddr *)&m_HarvesterSockAddr, sizeof( m_HarvesterSockAddr ) ) == SOCKET_ERROR ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Connection failed." ); |
|
|
|
status = eGameStatsConnectToCSERServerFailed; |
|
return false; |
|
} |
|
|
|
SetNextState( eSendProtocolVersion ); |
|
return true; |
|
} |
|
|
|
bool CWin32UploadGameStats::SendProtocolVersion( EGameStatsUploadStatus& status, CUtlBuffer& buf ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Sending game stats harvester protocol info." ); |
|
buf.SetBigEndian( true ); |
|
// Send protocol version |
|
buf.Purge(); |
|
buf.PutInt( cuCurrentProtocolVersion ); |
|
|
|
if ( send( m_SocketTCP, (const char *)buf.Base(), (int)buf.TellPut(), 0 ) == SOCKET_ERROR ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Send failed." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
SetNextState( eReceiveProtocolOkay ); |
|
return true; |
|
} |
|
|
|
bool CWin32UploadGameStats::ReceiveProtocolOkay( EGameStatsUploadStatus& status, CUtlBuffer& buf ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Receiving harvesting protocol acknowledgement." ); |
|
buf.Purge(); |
|
|
|
// Now receive the protocol is acceptable token from the server |
|
if ( !DoBlockingReceive( 1, buf ) ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Didn't receive protocol failure data." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
bool protocolokay = buf.GetChar() ? true : false; |
|
if ( !protocolokay ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Server rejected protocol." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
UpdateProgress( m_rCrashParameters, "Protocol OK." ); |
|
|
|
SetNextState( eSendUploadCommand ); |
|
return true; |
|
} |
|
|
|
bool CWin32UploadGameStats::SendUploadCommand( EGameStatsUploadStatus& status, CUtlBuffer& buf ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Sending harvesting protocol upload request." ); |
|
// Send upload command |
|
buf.Purge(); |
|
|
|
NetworkMessageLengthPrefix_t messageSize |
|
( |
|
sizeof( Command_t ) |
|
+ sizeof( ContextID_t ) |
|
+ sizeof( HarvestFileCommand::FileSize_t ) |
|
+ sizeof( HarvestFileCommand::SendMethod_t ) |
|
+ sizeof( HarvestFileCommand::FileSize_t ) |
|
); |
|
|
|
// Prefix the length to the command |
|
buf.PutInt( (int)messageSize ); |
|
buf.PutChar( Commands::cuSendGameStats ); |
|
buf.PutInt( (int)m_ContextID ); |
|
|
|
buf.PutInt( (int)m_rCrashParameters.m_uStatsBlobSize ); |
|
buf.PutInt( static_cast<HarvestFileCommand::SendMethod_t>( eSendMethodWholeRawFileNoBlocks ) ); |
|
buf.PutInt( static_cast<HarvestFileCommand::FileSize_t>( 0 ) ); |
|
|
|
// Send command to server |
|
if ( send( m_SocketTCP, (const char *)buf.Base(), (int)buf.TellPut(), 0 ) == SOCKET_ERROR ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Send failed." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
SetNextState( eReceiveOKToSendFile ); |
|
return true; |
|
} |
|
|
|
bool CWin32UploadGameStats::ReceiveOKToSendFile( EGameStatsUploadStatus& status, CUtlBuffer& buf ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Receive game stats harvesting protocol upload permissible." ); |
|
|
|
// Now receive the protocol is acceptable token from the server |
|
if ( !DoBlockingReceive( 1, buf ) ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Receive failed." ); |
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
bool dosend = false; |
|
CommandResponse_t cmd = (CommandResponse_t)buf.GetChar(); |
|
switch ( cmd ) |
|
{ |
|
case HarvestFileCommand::cuOkToSendFile: |
|
{ |
|
dosend = true; |
|
} |
|
break; |
|
case HarvestFileCommand::cuFileTooBig: |
|
case HarvestFileCommand::cuInvalidSendMethod: |
|
case HarvestFileCommand::cuInvalidMaxCompressedChunkSize: |
|
case HarvestFileCommand::cuInvalidGameStatsContext: |
|
default: |
|
break; |
|
} |
|
|
|
if ( !dosend ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Server rejected upload command." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
SetNextState( eSendWholeFile ); |
|
return true; |
|
} |
|
|
|
bool CWin32UploadGameStats::SendWholeFile( EGameStatsUploadStatus& status, CUtlBuffer& /*buf*/ ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Uploading game stats data." ); |
|
// Send to server |
|
bool bret = true; |
|
if ( send( m_SocketTCP, (const char *)m_rCrashParameters.m_pStatsBlobData, (int)m_rCrashParameters.m_uStatsBlobSize, 0 ) == SOCKET_ERROR ) |
|
{ |
|
bret = false; |
|
UpdateProgress( m_rCrashParameters, "Send failed." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
} |
|
else |
|
{ |
|
SetNextState( eReceiveFileUploadSuccess ); |
|
} |
|
|
|
return bret; |
|
} |
|
|
|
bool CWin32UploadGameStats::ReceiveFileUploadSuccess( EGameStatsUploadStatus& status, CUtlBuffer& buf ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Receiving game stats upload success/fail message." ); |
|
|
|
// Now receive the protocol is acceptable token from the server |
|
if ( !DoBlockingReceive( 1, buf ) ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Receive failed." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
bool success = buf.GetChar() == 1 ? true : false; |
|
if ( !success ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Upload failed." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
UpdateProgress( m_rCrashParameters, "Upload OK." ); |
|
|
|
SetNextState( eSendGracefulClose ); |
|
return true; |
|
} |
|
|
|
bool CWin32UploadGameStats::SendGracefulClose( EGameStatsUploadStatus& status, CUtlBuffer& buf ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Closing connection to server." ); |
|
|
|
// Now send disconnect command |
|
buf.Purge(); |
|
|
|
size_t messageSize = sizeof( Command_t ); |
|
|
|
buf.PutInt( (int)messageSize ); |
|
buf.PutChar( Commands::cuGracefulClose ); |
|
|
|
if ( send( m_SocketTCP, (const char *)buf.Base(), (int)buf.TellPut(), 0 ) == SOCKET_ERROR ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Send failed." ); |
|
|
|
status = eGameStatsUploadFailed; |
|
return false; |
|
} |
|
|
|
SetNextState( eCloseTCPSocket ); |
|
return true; |
|
} |
|
|
|
bool CWin32UploadGameStats::CloseTCPSocket( EGameStatsUploadStatus& status, CUtlBuffer& /*buf*/ ) |
|
{ |
|
UpdateProgress( m_rCrashParameters, "Closing socket, upload succeeded." ); |
|
|
|
closesocket( m_SocketTCP );//lint !e534 |
|
m_SocketTCP = 0; |
|
|
|
status = eGameStatsUploadSucceeded; |
|
// NOTE: Returning false here ends the state machine!!! |
|
return false; |
|
} |
|
|
|
EGameStatsUploadStatus Win32UploadGameStatsBlocking |
|
( |
|
const TGameStatsParameters & rGameStatsParameters |
|
) |
|
{ |
|
EGameStatsUploadStatus status = eGameStatsUploadSucceeded; |
|
|
|
CUtlBuffer buf( rGameStatsParameters.m_uStatsBlobSize + 4096 ); |
|
|
|
UpdateProgress( rGameStatsParameters, "Creating initial report." ); |
|
|
|
buf.SetBigEndian( false ); |
|
|
|
buf.Purge(); |
|
buf.PutChar( C2M_REPORT_GAMESTATISTICS ); |
|
buf.PutChar( '\n' ); |
|
buf.PutChar( C2M_REPORT_GAMESTATISTICS_PROTOCOL_VERSION ); |
|
|
|
// See CSERServerProtocol.h for format |
|
|
|
if ( 0 ) // This is the old protocol |
|
{ |
|
buf.PutInt( (int)rGameStatsParameters.m_uEngineBuildNumber ); |
|
buf.PutString( rGameStatsParameters.m_sExecutableName ); // exe name |
|
buf.PutString( rGameStatsParameters.m_sGameDirectory ); // gamedir |
|
buf.PutString( rGameStatsParameters.m_sMapName ); |
|
|
|
buf.PutInt( (int)rGameStatsParameters.m_uStatsBlobVersion ); // game stats blob version |
|
buf.PutInt( (int)rGameStatsParameters.m_uStatsBlobSize ); // game stats blob size |
|
} |
|
else |
|
{ |
|
buf.PutInt( (int)rGameStatsParameters.m_uAppId ); |
|
buf.PutInt( (int)rGameStatsParameters.m_uStatsBlobSize ); // game stats blob size |
|
} |
|
|
|
CBlockingUDPSocket bcs; |
|
if ( !bcs.IsValid() ) |
|
{ |
|
return eGameStatsUploadFailed; |
|
} |
|
|
|
struct sockaddr_in sa; |
|
rGameStatsParameters.m_ipCSERServer.ToSockadr( (struct sockaddr *)&sa ); |
|
|
|
UpdateProgress( rGameStatsParameters, "Sending game stats to server %s.", rGameStatsParameters.m_ipCSERServer.ToString() ); |
|
|
|
bcs.SendSocketMessage( sa, (const u8 *)buf.Base(), buf.TellPut() ); //lint !e534 |
|
|
|
UpdateProgress( rGameStatsParameters, "Waiting for response." ); |
|
|
|
if ( bcs.WaitForMessage( 2.0f ) ) |
|
{ |
|
UpdateProgress( rGameStatsParameters, "Received response." ); |
|
|
|
struct sockaddr_in replyaddress; |
|
buf.EnsureCapacity( 4096 ); |
|
|
|
uint bytesReceived = bcs.ReceiveSocketMessage( &replyaddress, (u8 *)buf.Base(), 4096 ); |
|
if ( bytesReceived > 0 ) |
|
{ |
|
// Fixup actual size |
|
buf.SeekPut( CUtlBuffer::SEEK_HEAD, bytesReceived ); |
|
|
|
UpdateProgress( rGameStatsParameters, "Checking response." ); |
|
|
|
// Parse out data |
|
u8 msgtype = (u8)buf.GetChar(); |
|
if ( M2C_ACKREPORT_GAMESTATISTICS != msgtype ) |
|
{ |
|
UpdateProgress( rGameStatsParameters, "Request denied, invalid message type." ); |
|
return eGameStatsSendingGameStatsHeaderFailed; |
|
} |
|
bool validProtocol = (u8)buf.GetChar() == 1 ? true : false; |
|
if ( !validProtocol ) |
|
{ |
|
UpdateProgress( rGameStatsParameters, "Request denied, invalid message protocol." ); |
|
return eGameStatsSendingGameStatsHeaderFailed; |
|
} |
|
|
|
u8 disposition = (u8)buf.GetChar(); |
|
if ( GS_UPLOAD_REQESTED != disposition ) |
|
{ |
|
// Server doesn't want a gamestats, oh well |
|
UpdateProgress( rGameStatsParameters, "Stats report accepted, data upload skipped." ); |
|
|
|
return eGameStatsUploadSucceeded; |
|
} |
|
|
|
// Read in the game stats info parameters |
|
u32 harvester_ip = (u32)buf.GetInt(); |
|
u16 harvester_port = (u16)buf.GetShort(); |
|
u32 dumpcontext = (u32)buf.GetInt(); |
|
|
|
sockaddr_in adr; |
|
adr.sin_family = AF_INET; |
|
adr.sin_port = htons( harvester_port ); |
|
#ifdef _WIN32 |
|
adr.sin_addr.S_un.S_addr = harvester_ip; |
|
#elif POSIX |
|
adr.sin_addr.s_addr = harvester_ip; |
|
#endif |
|
|
|
netadr_t GameStatsHarvesterFSMIPAddress; |
|
GameStatsHarvesterFSMIPAddress.SetFromSockadr( (struct sockaddr *)&adr ); |
|
|
|
UpdateProgress( rGameStatsParameters, "Server requested game stats upload to %s.", GameStatsHarvesterFSMIPAddress.ToString() ); |
|
|
|
// Keep using the same scratch buffer for messaging |
|
CWin32UploadGameStats uploader( GameStatsHarvesterFSMIPAddress, rGameStatsParameters, dumpcontext ); |
|
status = uploader.Upload( buf ); |
|
} |
|
} |
|
else |
|
{ |
|
UpdateProgress( rGameStatsParameters, "No response from server." ); |
|
} |
|
|
|
return status; |
|
} |
|
////////////////////////////////////////////////////////////////////////// |
|
// |
|
// Implementation of async uploading |
|
// |
|
|
|
class CAsyncUploaderThread |
|
{ |
|
public: |
|
CAsyncUploaderThread() |
|
: m_hThread( NULL ), m_bRunning( false ), m_eventQueue(false), m_eventInitShutdown(false) {} |
|
|
|
ThreadHandle_t m_hThread; |
|
|
|
protected: |
|
|
|
struct DataEntry |
|
{ |
|
char const *szMapName; |
|
uint uiBlobVersion; |
|
uint uiBlobSize; |
|
void const *pvBlob; |
|
|
|
DataEntry *AllocCopy() const; |
|
void Free() { delete [] ( (char*)this ); } |
|
}; |
|
|
|
CThreadEvent m_eventQueue; |
|
CThreadEvent m_eventInitShutdown; |
|
CThreadFastMutex m_mtx; |
|
CUtlVector< DataEntry * > m_queue; |
|
bool m_bRunning; |
|
|
|
void ThreadProc(); |
|
|
|
enum { |
|
SLEEP_ENTRY_UPLOADED = 10 * 1000 |
|
}; |
|
|
|
public: |
|
static unsigned CallbackThreadProc( void *pvParam ) { ((CAsyncUploaderThread*) pvParam)->ThreadProc(); return 0; } |
|
void QueueData( char const *szMapName, uint uiBlobVersion, uint uiBlobSize, const void *pvBlob ); |
|
void TerminateAndSelfDelete(); |
|
}; |
|
|
|
static CAsyncUploaderThread *g_pAsyncUploader = NULL; |
|
|
|
CAsyncUploaderThread::DataEntry * CAsyncUploaderThread::DataEntry::AllocCopy() const |
|
{ |
|
// Find out how much memory we would need |
|
uint lenMapName = ( szMapName ? strlen( szMapName ) : 0 ); |
|
uint numBytes = sizeof( DataEntry ) + uiBlobSize + lenMapName + 1; |
|
|
|
char *pbData = new char[ numBytes ]; |
|
DataEntry *pNew = ( DataEntry * )( pbData ); |
|
if ( !pNew ) |
|
return NULL; |
|
|
|
pNew->uiBlobVersion = uiBlobVersion; |
|
pNew->uiBlobSize = uiBlobSize; |
|
|
|
char *pbWriteMapName = ( char * )( pNew + 1 ); |
|
pNew->szMapName = pbWriteMapName; |
|
memcpy( pbWriteMapName, szMapName, lenMapName ); |
|
pbWriteMapName[ lenMapName ] = 0; |
|
|
|
char *pbWriteBlob = pbWriteMapName + lenMapName + 1; |
|
pNew->pvBlob = pbWriteBlob; |
|
memcpy( pbWriteBlob, pvBlob, uiBlobSize ); |
|
|
|
return pNew; |
|
} |
|
|
|
void CAsyncUploaderThread::QueueData( char const *szMapName, uint uiBlobVersion, uint uiBlobSize, const void *pvBlob ) |
|
{ |
|
// DevMsg( 3, "AsyncUploaderThread: Queue [%.*s]\n", uiBlobSize, pvBlob ); |
|
|
|
if ( !m_hThread ) |
|
{ |
|
// Start the thread and wait for it to be running. |
|
// There is a slightly complicated two-event negotiation: |
|
// ThreadProc signals eventInitShutdown then waits on eventQueue |
|
Assert( m_bRunning == false ); |
|
m_hThread = CreateSimpleThread( CallbackThreadProc, this ); |
|
m_eventInitShutdown.Wait(); |
|
Assert( m_bRunning == true ); |
|
// At this point, both events are unsignaled and the thread |
|
// is in its main loop. |
|
} |
|
|
|
// Prepare for a DataEntry |
|
DataEntry de = { szMapName, uiBlobVersion, uiBlobSize, pvBlob }; |
|
if ( DataEntry *pNew = de.AllocCopy() ) |
|
{ |
|
{ |
|
AUTO_LOCK( m_mtx ); |
|
m_queue.AddToTail( pNew ); |
|
} |
|
m_eventQueue.Set(); |
|
} |
|
} |
|
|
|
void CAsyncUploaderThread::TerminateAndSelfDelete() |
|
{ |
|
ThreadHandle_t hThread = m_hThread; |
|
if ( hThread ) |
|
{ |
|
m_bRunning = false; |
|
m_eventQueue.Set(); |
|
m_eventInitShutdown.Set(); // MUST BE LAST MEMBER ACCESS, other thread may now delete this |
|
|
|
// Wait for a while for upload to finish, but don't wait forever |
|
ThreadJoin( hThread, 10 * 1000 ); |
|
ReleaseThreadHandle( hThread ); |
|
} |
|
else |
|
{ |
|
delete this; |
|
} |
|
} |
|
|
|
void CAsyncUploaderThread::ThreadProc() |
|
{ |
|
bool bQueueDrained = true; |
|
bool bGotShutdownEvent = false; |
|
|
|
m_bRunning = true; |
|
m_eventInitShutdown.Set(); |
|
|
|
while ( m_bRunning ) |
|
{ |
|
if ( bQueueDrained ) |
|
{ |
|
m_eventQueue.Wait(); |
|
} |
|
|
|
DataEntry *pUpload = NULL; |
|
{ |
|
AUTO_LOCK( m_mtx ); |
|
if ( m_queue.Count() ) |
|
{ |
|
pUpload = m_queue[0]; |
|
m_queue.Remove( 0 ); |
|
} |
|
bQueueDrained = ( m_queue.Count() == 0 ); |
|
} |
|
|
|
if ( m_bRunning && pUpload ) |
|
{ |
|
// DevMsg( 3, "AsyncUploaderThread: Uploading [%.*s]\n", pUpload->uiBlobSize, pUpload->pvBlob ); |
|
|
|
// Attempt to upload the data until successful |
|
bool bSuccess = g_UploadGameStats.UploadGameStatsInternal( pUpload->szMapName, pUpload->uiBlobVersion, pUpload->uiBlobSize, pUpload->pvBlob ); |
|
bSuccess; |
|
|
|
pUpload->Free(); |
|
|
|
// After the data entry got uploaded, grab the next one |
|
// DevMsg( 3, "AsyncUploaderThread: Upload finished (status=%d) for data [%.*s]\n", bSuccess, pUpload->uiBlobSize, pUpload->pvBlob ); |
|
|
|
// Using an event as an interruptable sleep; signaled if m_bRunning is cleared |
|
bGotShutdownEvent |= m_eventInitShutdown.Wait( SLEEP_ENTRY_UPLOADED ); |
|
} |
|
} |
|
|
|
Assert( !m_bRunning ); |
|
|
|
// Deletes self at end of execution! |
|
if ( !bGotShutdownEvent ) |
|
{ |
|
m_eventInitShutdown.Wait(); |
|
} |
|
delete this; |
|
} |
|
|
|
void AsyncUpload_QueueData( char const *szMapName, uint uiBlobVersion, uint uiBlobSize, const void *pvBlob ) |
|
{ |
|
if ( !g_pAsyncUploader ) |
|
{ |
|
g_pAsyncUploader = new CAsyncUploaderThread; |
|
} |
|
g_pAsyncUploader->QueueData( szMapName, uiBlobVersion, uiBlobSize, pvBlob ); |
|
} |
|
|
|
void AsyncUpload_Shutdown() |
|
{ |
|
if ( g_pAsyncUploader ) |
|
{ |
|
g_pAsyncUploader->TerminateAndSelfDelete(); |
|
g_pAsyncUploader = NULL; |
|
} |
|
}
|
|
|