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.
1254 lines
34 KiB
1254 lines
34 KiB
![]()
4 years ago
|
//========= 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;
|
||
|
}
|
||
|
}
|