diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /engine/sv_uploadgamestats.cpp | |
| download | archived-source-engine-2018-hl2-src-3bf9df6b2785fa6d951086978a3e66f49427166a.tar.xz archived-source-engine-2018-hl2-src-3bf9df6b2785fa6d951086978a3e66f49427166a.zip | |
Diffstat (limited to 'engine/sv_uploadgamestats.cpp')
| -rw-r--r-- | engine/sv_uploadgamestats.cpp | 1253 |
1 files changed, 1253 insertions, 0 deletions
diff --git a/engine/sv_uploadgamestats.cpp b/engine/sv_uploadgamestats.cpp new file mode 100644 index 0000000..8665b06 --- /dev/null +++ b/engine/sv_uploadgamestats.cpp @@ -0,0 +1,1253 @@ +//========= 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; + } +} |