summaryrefslogtreecommitdiff
path: root/engine/sv_uploadgamestats.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'engine/sv_uploadgamestats.cpp')
-rw-r--r--engine/sv_uploadgamestats.cpp1253
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( &params, 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;
+ }
+}