//========= Copyright Valve Corporation, All rights reserved. ============// // //=======================================================================================// #include "sv_sessionrecorder.h" #include "replay/replayutils.h" #include "replay/shared_defs.h" #include "baserecordingsessionblock.h" #include "replaysystem.h" #include "baserecordingsessionblockmanager.h" #include "sv_recordingsessionmanager.h" #include "sv_replaycontext.h" #include "sv_sessionpublishmanager.h" #include "sv_recordingsession.h" #include "sv_recordingsessionblock.h" #include "fmtstr.h" #include "vprof.h" #include "iserver.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" #undef CreateEvent //---------------------------------------------------------------------------------------- #define SERVER_REPLAY_INDEX_FILENAME ".replayindex" #define SERVER_REPLAY_ERROR_LOST "The server crashed before the replay could be finalized. Replay lost." //---------------------------------------------------------------------------------------- CSessionRecorder::CSessionRecorder() : m_bRecordingAborted( false ), m_nCurrentRecordingStartTick( -1 ) { } CSessionRecorder::~CSessionRecorder() { } bool CSessionRecorder::Init() { g_pFullFileSystem->CreateDirHierarchy( Replay_va( "%s%s", SV_GetBasePath(), SUBDIR_SESSIONS ) ); return true; } void CSessionRecorder::AbortCurrentSessionRecording() { StopRecording( true ); CSessionPublishManager *pCurrentPublishManager = GetCurrentPublishManager(); if ( !pCurrentPublishManager ) { AssertMsg( 0, "Could not get current publish manager." ); return; } pCurrentPublishManager->AbortPublish(); m_bRecordingAborted = true; } void CSessionRecorder::SetCurrentRecordingStartTick( int nStartTick ) { m_nCurrentRecordingStartTick = nStartTick; } void CSessionRecorder::PublishAllSynchronous() { FOR_EACH_LL( m_lstPublishManagers, i ) { m_lstPublishManagers[ i ]->PublishAllSynchronous(); } } void CSessionRecorder::StartRecording() { m_bRecordingAborted = false; IServer *pServer = ReplayServerAsIServer(); if ( !pServer || !pServer->IsActive() ) { ConMsg( "ERROR: Replay not active.\n" ); return; } // We only care about local fileserver path in the case that we aren't offloading files to an external sfileserver const char *pWritePath = g_pServerReplayContext->GetLocalFileServerPath(); if ( ( !pWritePath || !pWritePath[0] ) ) { ConMsg( "\n*\n* ERROR: Failed to begin record: make sure \"replay_local_fileserver_path\" refers to a valid path!\n** replay_local_fileserver_path is currently set to: \"%s\"\n*\n\n", pWritePath ); return; } IReplayServer *pReplayServer = ReplayServer(); if ( pReplayServer->IsRecording() ) { ConMsg( "ERROR: Replay already recording.\n" ); return; } // Tell the replay server to begin recording pReplayServer->StartRecording(); // Notify session manager CBaseRecordingSession *pSession = SV_GetRecordingSessionManager()->OnSessionStart( m_nCurrentRecordingStartTick, NULL ); // Create a new publish manager and add it. The dump interval and any additional setup is done there. CreateAndAddNewPublishManager( static_cast< CServerRecordingSession * >( pSession ) ); } void CSessionRecorder::CreateAndAddNewPublishManager( CServerRecordingSession *pSession ) { CSessionPublishManager *pNewPublishManager = new CSessionPublishManager( pSession ); // Let the publish manager know that it is the 'current' publish manager. pNewPublishManager->OnStartRecording(); // Add to the head of the list, since the desired convention is for the list to be // sorted from newest to oldest. m_lstPublishManagers.AddToHead( pNewPublishManager ); } float CSessionRecorder::GetNextThinkTime() const { return 0.0f; } void CSessionRecorder::Think() { CBaseThinker::Think(); VPROF_BUDGET( "CSessionRecorder::Think", VPROF_BUDGETGROUP_REPLAY ); // This gets called even if replay is disabled. This is intentional. PublishThink(); } CSessionPublishManager *CSessionRecorder::GetCurrentPublishManager() const { if ( !m_lstPublishManagers.Count() ) return NULL; return m_lstPublishManagers[ m_lstPublishManagers.Head() ]; } void CSessionRecorder::PublishThink() { UpdateSessionLocks(); } void CSessionRecorder::UpdateSessionLocks() { for ( int i = m_lstPublishManagers.Head(); i != m_lstPublishManagers.InvalidIndex(); ) { CSessionPublishManager *pCurManager = m_lstPublishManagers[ i ]; // Cache off 'next' in case we delete the current object const int itNext = m_lstPublishManagers.Next( i ); if ( pCurManager->IsDone() ) { #ifdef _DEBUG pCurManager->Validate(); #endif // We can unlock the associated session now. pCurManager->UnlockSession(); // Remove and delete it. m_lstPublishManagers.Remove( i ); delete pCurManager; IF_REPLAY_DBG( Warning( "\n---\n*\n* All publishing done for session. %i still publishing.\n*\n---\n", m_lstPublishManagers.Count() ) ); } else { pCurManager->Think(); } i = itNext; } } void CSessionRecorder::StopRecording( bool bAborting ) { #if !defined( DEDICATED ) if ( g_pEngineClient->IsPlayingReplayDemo() ) return; #endif if ( !ReplayServer() ) return; DBG( "StopRecording()\n" ); CServerRecordingSession *pSession = SV_GetRecordingSessionInProgress(); if ( pSession ) { // Mark the session as not recording pSession->OnStopRecording(); // Get the current publish manager and notify it that recording has stopped. CSessionPublishManager *pManager = GetCurrentPublishManager(); if ( pManager ) { pManager->OnStopRecord( bAborting ); } // Notify session manager - the session will be flagged for unload or deletion, but // will not actually be free'd until it is "unlocked" by the publish manager. SV_GetRecordingSessionManager()->OnSessionEnd(); } // Stop recording ReplayServer()->StopRecording(); // Clear replay_recording extern ConVar replay_recording; replay_recording.SetValue( 0 ); } //----------------------------------------------------------------------------------------