// Copyright Epic Games, Inc. All Rights Reserved. #include "ConcertServer.h" #include "ConcertUtil.h" #include "ConcertServerUtil.h" #include "ConcertServerSettings.h" #include "ConcertServerSession.h" #include "ConcertServerSessionRepositories.h" #include "ConcertLogGlobal.h" #include "ConcertTransportEvents.h" #include "IConcertServerEventSink.h" #include "Algo/AnyOf.h" #include "Misc/App.h" #include "Misc/Paths.h" #include "Backends/JsonStructDeserializerBackend.h" #include "Backends/JsonStructSerializerBackend.h" #include "HAL/FileManager.h" #include "StructDeserializer.h" #include "StructSerializer.h" #include "Templates/NonNullPointer.h" #include "Runtime/Launch/Resources/Version.h" #define LOCTEXT_NAMESPACE "ConcertServer" namespace ConcertServerUtil { static const TCHAR* GetServerSystemMutexName() { // A system wide mutex name used by this application instances that will unlikely be found in other applications. return TEXT("Unreal_ConcertServer_67822dAB"); } static FString GetArchiveName(const FString& SessionName, const FConcertSessionSettings& Settings) { if (Settings.ArchiveNameOverride.IsEmpty()) { return FString::Printf(TEXT("%s_%s"), *SessionName, *FDateTime::UtcNow().ToString()); } else { return Settings.ArchiveNameOverride; } } static FString GetSessionRepositoryDatabasePathname(const FString& Role) { return FPaths::ProjectSavedDir() / Role / TEXT("Repositories.json"); } static bool SaveSessionRepositoryDatabase(const FString& Role, const FConcertServerSessionRepositoryDatabase& RepositoryDb) { if (TUniquePtr FileWriter = TUniquePtr(IFileManager::Get().CreateFileWriter(*GetSessionRepositoryDatabasePathname(Role)))) { FJsonStructSerializerBackend Backend(*FileWriter, EStructSerializerBackendFlags::Default); FStructSerializer::Serialize(RepositoryDb, Backend); FileWriter->Close(); return !FileWriter->IsError(); } return false; } static bool LoadSessionRepositoryDatabase(const FString& Role, FConcertServerSessionRepositoryDatabase& RepositoryDb) { if (TUniquePtr FileReader = TUniquePtr(IFileManager::Get().CreateFileReader(*GetSessionRepositoryDatabasePathname(Role)))) { FJsonStructDeserializerBackend Backend(*FileReader); FStructDeserializer::Deserialize(RepositoryDb, Backend); FileReader->Close(); return !FileReader->IsError(); } return false; } } FConcertServer::FConcertServer(const FString& InRole, const FConcertSessionFilter& InAutoArchiveSessionFilter, IConcertServerEventSink* InEventSink, const TSharedPtr& InEndpointProvider) : Role(InRole) , DefaultSessionRepositoryStatus(LOCTEXT("SessionRepository_NotConfigured", "Repository not configured.")) , AutoArchiveSessionFilter(InAutoArchiveSessionFilter) , EventSink(InEventSink) , EndpointProvider(InEndpointProvider) { check(EventSink); } FConcertServer::~FConcertServer() { // if ServerAdminEndpoint is valid, then Shutdown wasn't called check(!ServerAdminEndpoint.IsValid()); } const FString& FConcertServer::GetRole() const { return Role; } void FConcertServer::Configure(const UConcertServerConfig* InSettings) { check(!Settings); // Server do not support reconfiguration. ServerInfo.Initialize(); check(InSettings != nullptr); Settings = TStrongObjectPtr(InSettings); if (!InSettings->ServerName.IsEmpty()) { ServerInfo.ServerName = InSettings->ServerName; } if (InSettings->ServerSettings.bIgnoreSessionSettingsRestriction) { ServerInfo.ServerFlags |= EConcertServerFlags::IgnoreSessionRequirement; } SessionRepositoryRootDir = FPaths::ProjectSavedDir() / Role / TEXT("Sessions"); // Server default session repository root dir. if (!Settings->SessionRepositoryRootDir.IsEmpty()) { if (IFileManager::Get().DirectoryExists(*Settings->SessionRepositoryRootDir) || IFileManager::Get().MakeDirectory(*Settings->SessionRepositoryRootDir, /*Tree*/true)) { SessionRepositoryRootDir = Settings->SessionRepositoryRootDir; // Overwrite the default. } else { UE_LOG(LogConcert, Warning, TEXT("Invalid session repository root directory. Falling back on %s default."), *SessionRepositoryRootDir); } } } bool FConcertServer::IsConfigured() const { // if the instance id hasn't been set yet, then Configure wasn't called. return Settings && ServerInfo.InstanceInfo.InstanceId.IsValid(); } const UConcertServerConfig* FConcertServer::GetConfiguration() const { return Settings.Get(); } const FConcertServerInfo& FConcertServer::GetServerInfo() const { return ServerInfo; } TArray FConcertServer::GetRemoteAdminEndpoints() const { if (IsStarted()) { return ServerAdminEndpoint->GetRemoteEndpoints(); } return {}; } FOnConcertRemoteEndpointConnectionChanged& FConcertServer::OnRemoteEndpointConnectionChanged() { return OnConcertRemoteEndpointConnectionChangedDelegate; } FMessageAddress FConcertServer::GetRemoteAddress(const FGuid& AdminEndpointId) const { if (IsStarted()) { return ServerAdminEndpoint->GetRemoteAddress(AdminEndpointId); } return {}; } FOnConcertMessageAcknowledgementReceivedFromLocalEndpoint& FConcertServer::OnConcertMessageAcknowledgementReceived() { return OnConcertMessageAcknowledgementReceivedFromLocalEndpoint; } bool FConcertServer::IsStarted() const { return ServerAdminEndpoint.IsValid(); } void FConcertServer::Startup() { check(IsConfigured()); if (!ServerAdminEndpoint.IsValid() && EndpointProvider.IsValid()) { // Create the server administration endpoint ServerAdminEndpoint = EndpointProvider->CreateLocalEndpoint(TEXT("Admin"), Settings->EndpointSettings, [this](const FConcertEndpointContext& Context) { return ConcertUtil::CreateLogger(Context, [this](const FConcertLog& Log) { ConcertTransportEvents::OnConcertServerLogEvent().Broadcast(*this, Log); }); }); ServerInfo.AdminEndpointId = ServerAdminEndpoint->GetEndpointContext().EndpointId; ServerAdminEndpoint->OnConcertMessageAcknowledgementReceived().AddLambda( [this](const FConcertEndpointContext& LocalEndpoint, const FConcertEndpointContext& RemoteEndpoint, const TSharedRef& AckedMessage, const FConcertMessageContext& MessageContext) { OnConcertMessageAcknowledgementReceivedFromLocalEndpoint.Broadcast(LocalEndpoint, RemoteEndpoint, AckedMessage, MessageContext); }); ServerAdminEndpoint->OnRemoteEndpointConnectionChanged().AddLambda([this](const FConcertEndpointContext& Context, EConcertRemoteEndpointConnection Connection) { OnConcertRemoteEndpointConnectionChangedDelegate.Broadcast(Context, Connection); }); // Make it discoverable ServerAdminEndpoint->SubscribeEventHandler(this, &FConcertServer::HandleDiscoverServersEvent); // Add Session connection handling ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleCreateSessionRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleFindSessionRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleCopySessionRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleArchiveSessionRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleRenameSessionRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleDeleteSessionRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleBatchDeleteSessionRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleGetAllSessionsRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleGetLiveSessionsRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleGetArchivedSessionsRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleGetSessionClientsRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleGetSessionActivitiesRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleMountSessionRepositoryRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleGetSessionRepositoriesRequest); ServerAdminEndpoint->RegisterRequestHandler(this, &FConcertServer::HandleDropSessionRepositoriesRequest); // Perform maintenance tasks on the session repositories database. { FSystemWideCriticalSection ScopedSystemWideMutex(ConcertServerUtil::GetServerSystemMutexName()); // Load the file containing the repositories. FConcertServerSessionRepositoryDatabase SessionRepositoryDb; ConcertServerUtil::LoadSessionRepositoryDatabase(Role, SessionRepositoryDb); // Unmap repositories that doesn't exist anymore on disk (were deleted manually). int RemovedNum = SessionRepositoryDb.Repositories.RemoveAll([](const FConcertServerSessionRepository& RemoveCandidate) { if (!RemoveCandidate.RepositoryRootDir.IsEmpty()) // Under a single standard root? { FString Pathname = RemoveCandidate.RepositoryRootDir / RemoveCandidate.RepositoryId.ToString(); return !IFileManager::Get().DirectoryExists(*Pathname); } return false; // Not under a single root (Multi-User backward compatibility mode) leave it. }); if (RemovedNum) { ConcertServerUtil::SaveSessionRepositoryDatabase(Role, SessionRepositoryDb); } // Walk the root directory containing the server managed repositories and find those that aren't mapped anymore. TArray ExpiredDirectories; IFileManager::Get().IterateDirectory(*GetSessionRepositoriesRootDir(), [this, &SessionRepositoryDb, &ExpiredDirectories](const TCHAR* Pathname, bool bIsDirectory) { if (bIsDirectory) { // Check if the repository is still mapped. FString RootReposDir = GetSessionRepositoriesRootDir(); if (!SessionRepositoryDb.Repositories.ContainsByPredicate([&RootReposDir, Pathname](const FConcertServerSessionRepository& Repository) { return RootReposDir / Repository.RepositoryId.ToString() == Pathname; })) { ExpiredDirectories.Emplace(Pathname); // The visited directory was not found in the list of mapped repositories. } } return true; }); // Delete the directories that are not mapped anymore. for (const FString& Dir : ExpiredDirectories) { FGuid Dummy; if (FGuid::Parse(FPaths::GetPathLeaf(Dir), Dummy)) // Ensure the directory name is a GUID as the repositories base dir is the repository ID. { ConcertUtil::DeleteDirectoryTree(*Dir); } } } // Try to mount the default session repository configured (if one is configured) to lock the non-sharable session files away from concurrent processes. MountDefaultSessionRepository(Settings.Get()); OnConcertServerStartupDelegate.Broadcast(); } } void FConcertServer::Shutdown() { // Server Query if (ServerAdminEndpoint.IsValid()) { // Discovery ServerAdminEndpoint->UnsubscribeEventHandler(); // Session connection ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint->UnregisterRequestHandler(); ServerAdminEndpoint.Reset(); } // Destroy the live sessions { TArray LiveSessionIds; LiveSessions.GetKeys(LiveSessionIds); for (const FGuid& LiveSessionId : LiveSessionIds) { bool bDeleteSessionData = false; if (Settings->bAutoArchiveOnShutdown) { bDeleteSessionData = ArchiveLiveSession(LiveSessionId, FString(), AutoArchiveSessionFilter).IsValid(); } DestroyLiveSession(LiveSessionId, bDeleteSessionData); } LiveSessions.Reset(); } // Destroy the archived sessions { TArray ArchivedSessionIds; ArchivedSessions.GetKeys(ArchivedSessionIds); for (const FGuid& ArchivedSessionId : ArchivedSessionIds) { DestroyArchivedSession(ArchivedSessionId, /*bDeleteSessionData*/false); } ArchivedSessions.Reset(); } // Concurrent server instances may fight to get ownership of the info file. FSystemWideCriticalSection ScopedSystemWideMutex(ConcertServerUtil::GetServerSystemMutexName()); // Load the file containing the instance info. FConcertServerSessionRepositoryDatabase SessionRepositoryDb; ConcertServerUtil::LoadSessionRepositoryDatabase(Role, SessionRepositoryDb); bool bSaveRepositoryDatabase = false; // Unmount all repositories mounted by this instance. int32 ProcessId = FPlatformProcess::GetCurrentProcessId(); for (FConcertServerSessionRepository& Repository : SessionRepositoryDb.Repositories) { if (Repository.bMounted && Repository.ProcessId == ProcessId) { Repository.bMounted = false; Repository.ProcessId = 0; bSaveRepositoryDatabase = true; } } if (bSaveRepositoryDatabase) { ConcertServerUtil::SaveSessionRepositoryDatabase(Role, SessionRepositoryDb); } } FGuid FConcertServer::GetLiveSessionIdByName(const FString& InName) const { for (const auto& LiveSessionPair : LiveSessions) { if (LiveSessionPair.Value->GetName() == InName) { return LiveSessionPair.Key; } } return FGuid(); } FGuid FConcertServer::GetArchivedSessionIdByName(const FString& InName) const { for (const auto& ArchivedSessionPair : ArchivedSessions) { if (ArchivedSessionPair.Value.SessionName == InName) { return ArchivedSessionPair.Key; } } return FGuid(); } FConcertSessionInfo FConcertServer::CreateSessionInfo() const { FConcertSessionInfo SessionInfo; SessionInfo.ServerInstanceId = ServerInfo.InstanceInfo.InstanceId; SessionInfo.OwnerInstanceId = ServerInfo.InstanceInfo.InstanceId; SessionInfo.OwnerUserName = FApp::GetSessionOwner(); SessionInfo.OwnerDeviceName = FPlatformProcess::ComputerName(); SessionInfo.SessionId = FGuid::NewGuid(); return SessionInfo; } TSharedPtr FConcertServer::CreateSession(const FConcertSessionInfo& SessionInfo, FText& OutFailureReason) { if (!SessionInfo.SessionId.IsValid() || SessionInfo.SessionName.IsEmpty()) { OutFailureReason = LOCTEXT("Error_CreateSession_EmptySessionIdOrName", "Empty session ID or name"); UE_LOG(LogConcert, Error, TEXT("An attempt to create a session was made, but the session info was missing an ID or name!")); return nullptr; } if (!Settings->ServerSettings.bIgnoreSessionSettingsRestriction && SessionInfo.VersionInfos.Num() == 0) { OutFailureReason = LOCTEXT("Error_CreateSession_EmptyVersionInfo", "Empty version info"); UE_LOG(LogConcert, Error, TEXT("An attempt to create a session was made, but the session info was missing version info!")); return nullptr; } if (LiveSessions.Contains(SessionInfo.SessionId)) { OutFailureReason = FText::Format(LOCTEXT("Error_CreateSession_AlreadyExists", "Session '{0}' already exists"), FText::AsCultureInvariant(SessionInfo.SessionId.ToString())); UE_LOG(LogConcert, Error, TEXT("An attempt to create a session with ID '%s' was made, but that session already exists!"), *SessionInfo.SessionId.ToString()); return nullptr; } if (GetLiveSessionIdByName(SessionInfo.SessionName).IsValid()) { OutFailureReason = FText::Format(LOCTEXT("Error_CreateSession_AlreadyExists", "Session '{0}' already exists"), FText::AsCultureInvariant(SessionInfo.SessionName)); UE_LOG(LogConcert, Error, TEXT("An attempt to create a session with name '%s' was made, but that session already exists!"), *SessionInfo.SessionName); return nullptr; } // If the default session repository is not set, check if one is configured and try to mount it. This may be a time-costly operation. This is to addresses the case where a user // has/had two concurrent Multi-User servers using the same sessions directories without noticing and fail to create a session on the newest server instance because the folder is/was // locked by the older instance when the new one started. if (!DefaultSessionRepository && !MountDefaultSessionRepository(Settings.Get())) { OutFailureReason = FText::Format(LOCTEXT("Error_CreateSession_NoRepository", "Session '{0}' could not be created. The default repository used to store sessions files is not mounted. Reason: {1}"), FText::AsCultureInvariant(SessionInfo.SessionName), DefaultSessionRepositoryStatus); UE_LOG(LogConcert, Error, TEXT("An attempt to create a session with name '%s' was made, but the server did not have any repository mounted to store it! The repository may already be mounted by another process."), *SessionInfo.SessionName); return nullptr; } return CreateLiveSession(SessionInfo, DefaultSessionRepository.GetValue()); } TSharedPtr FConcertServer::RestoreSession(const FGuid& SessionId, const FConcertSessionInfo& SessionInfo, const FConcertSessionFilter& SessionFilter, FText& OutFailureReason) { if (ArchivedSessions.Contains(SessionId)) { return CopySession(SessionId, SessionInfo, SessionFilter, OutFailureReason); } OutFailureReason = FText::Format(LOCTEXT("Error_RestoreSession_NotFound", "Session '{0}' not found"), FText::AsCultureInvariant(SessionId.ToString())); UE_LOG(LogConcert, Error, TEXT("An attempt to restore session '%s' was made, but that session could not be found!"), *SessionId.ToString()); return nullptr; } TSharedPtr FConcertServer::CopySession(const FGuid& SrcSessionId, const FConcertSessionInfo& NewSessionInfo, const FConcertSessionFilter& SessionFilter, FText& OutFailureReason) { if (!NewSessionInfo.SessionId.IsValid() || NewSessionInfo.SessionName.IsEmpty()) { OutFailureReason = LOCTEXT("Error_CopySession_EmptySessionIdOrName", "Empty session ID or name"); UE_LOG(LogConcert, Error, TEXT("An attempt to copy a session was made, but the session info was missing an ID or name!")); return nullptr; } else if (!Settings->ServerSettings.bIgnoreSessionSettingsRestriction && NewSessionInfo.VersionInfos.Num() == 0) { OutFailureReason = LOCTEXT("Error_CopySession_EmptyVersionInfo", "Empty version info"); UE_LOG(LogConcert, Error, TEXT("An attempt to copy a session was made, but the session info was missing version info!")); return nullptr; } else if (LiveSessions.Contains(NewSessionInfo.SessionId)) { OutFailureReason = FText::Format(LOCTEXT("Error_CopySession_AlreadyExists", "Session '{0}' already exists"), FText::AsCultureInvariant(NewSessionInfo.SessionId.ToString())); UE_LOG(LogConcert, Error, TEXT("An attempt to copy a session with ID '%s' was made, but that session already exists!"), *NewSessionInfo.SessionId.ToString()); return nullptr; } else if (GetLiveSessionIdByName(NewSessionInfo.SessionName).IsValid()) { OutFailureReason = FText::Format(LOCTEXT("Error_CopySession_AlreadyExists", "Session '{0}' already exists"), FText::AsCultureInvariant(NewSessionInfo.SessionName)); UE_LOG(LogConcert, Error, TEXT("An attempt to copy a session with name '%s' was made, but that session already exists!"), *NewSessionInfo.SessionName); return nullptr; } else if (ArchivedSessions.Contains(SrcSessionId)) { return RestoreArchivedSession(SrcSessionId, NewSessionInfo, SessionFilter, OutFailureReason); } else if (TSharedPtr LiveSession = LiveSessions.FindRef(SrcSessionId)) { // Copy the live session in the default repository (where new sessions should be created), unless it is unset. const FConcertSessionInfo& LiveSessionInfo = LiveSession->GetSessionInfo(); const FConcertServerSessionRepository& CopySessionRepository = DefaultSessionRepository.IsSet() ? DefaultSessionRepository.GetValue() : GetSessionRepository(LiveSession->GetSessionInfo().SessionId); if (EventSink->CopySession(*this, LiveSession.ToSharedRef(), CopySessionRepository.GetSessionWorkingDir(NewSessionInfo.SessionId), SessionFilter)) { UE_LOG(LogConcert, Display, TEXT("Live session '%s' (%s) was copied as '%s' (%s)"), *LiveSessionInfo.SessionName, *LiveSessionInfo.SessionId.ToString(), *NewSessionInfo.SessionName, *NewSessionInfo.SessionId.ToString()); return CreateLiveSession(NewSessionInfo, CopySessionRepository); } else { UE_LOG(LogConcert, Error, TEXT("An attempt to copy a session '%s' was made, but failed!"), *SrcSessionId.ToString()); return nullptr; } } OutFailureReason = FText::Format(LOCTEXT("Error_CopySession_NotFound", "Session '{0}' not found"), FText::AsCultureInvariant(SrcSessionId.ToString())); UE_LOG(LogConcert, Error, TEXT("An attempt to copy a session '%s' was made, but that session could not be found!"), *SrcSessionId.ToString()); return nullptr; } void FConcertServer::RecoverSessions(const FConcertServerSessionRepository& InRepository, bool bCleanupExpiredSessions) { // Find any existing live sessions to automatically restore when recovering from an improper server shutdown TArray LiveSessionInfos; TArray LiveSessionCreationTimes; EventSink->GetSessionsFromPath(*this, InRepository.WorkingDir, LiveSessionInfos, &LiveSessionCreationTimes); UpdateLastModified(LiveSessionInfos, LiveSessionCreationTimes); // Restore any existing live sessions for (FConcertSessionInfo& LiveSessionInfo : LiveSessionInfos) { // Update the session info with new server info LiveSessionInfo.ServerInstanceId = ServerInfo.InstanceInfo.InstanceId; if (!LiveSessions.Contains(LiveSessionInfo.SessionId) && !GetLiveSessionIdByName(LiveSessionInfo.SessionName).IsValid() && CreateLiveSession(LiveSessionInfo, InRepository)) { UE_LOG(LogConcert, Display, TEXT("Live session '%s' (%s) was recovered."), *LiveSessionInfo.SessionName, *LiveSessionInfo.SessionId.ToString()); } } if (bCleanupExpiredSessions && Settings->NumSessionsToKeep == 0) { ConcertUtil::DeleteDirectoryTree(*InRepository.SavedDir); } else { // Find any existing archived sessions TArray ArchivedSessionInfos; TArray ArchivedSessionCreationTimes; // In theory, archives are immutable, but the server will end up touching the files and change the 'modification time'. Ensure to look at 'creation time'. EventSink->GetSessionsFromPath(*this, InRepository.SavedDir, ArchivedSessionInfos, &ArchivedSessionCreationTimes); check(ArchivedSessionInfos.Num() == ArchivedSessionCreationTimes.Num()); UpdateLastModified(ArchivedSessionInfos, ArchivedSessionCreationTimes); // Trim the oldest archived sessions. if (bCleanupExpiredSessions && Settings->NumSessionsToKeep > 0 && ArchivedSessionInfos.Num() > Settings->NumSessionsToKeep) { typedef TTuple FSavedSessionInfo; // Build the list of sorted session TArray SortedSessions; for (int32 LiveSessionInfoIndex = 0; LiveSessionInfoIndex < ArchivedSessionInfos.Num(); ++LiveSessionInfoIndex) { SortedSessions.Add(MakeTuple(LiveSessionInfoIndex, ArchivedSessionCreationTimes[LiveSessionInfoIndex])); } SortedSessions.Sort([](const FSavedSessionInfo& InOne, const FSavedSessionInfo& InTwo) { return InOne.Value < InTwo.Value; }); // Keep the most recent sessions TArray ArchivedSessionsToKeep; { const int32 FirstSortedSessionIndexToKeep = SortedSessions.Num() - Settings->NumSessionsToKeep; for (int32 SortedSessionIndex = FirstSortedSessionIndexToKeep; SortedSessionIndex < SortedSessions.Num(); ++SortedSessionIndex) { ArchivedSessionsToKeep.Add(ArchivedSessionInfos[SortedSessions[SortedSessionIndex].Key]); } SortedSessions.RemoveAt(FirstSortedSessionIndexToKeep, Settings->NumSessionsToKeep, EAllowShrinking::No); } // Remove the oldest sessions for (const FSavedSessionInfo& SortedSession : SortedSessions) { ConcertUtil::DeleteDirectoryTree(*InRepository.GetSessionSavedDir(ArchivedSessionInfos[SortedSession.Key].SessionId)); } // Update the list of sessions to restore ArchivedSessionInfos = MoveTemp(ArchivedSessionsToKeep); ArchivedSessionCreationTimes.Reset(); } // Create any existing archived sessions for (FConcertSessionInfo& ArchivedSessionInfo : ArchivedSessionInfos) { // Update the session info with new server info ArchivedSessionInfo.ServerInstanceId = ServerInfo.InstanceInfo.InstanceId; if (!ArchivedSessions.Contains(ArchivedSessionInfo.SessionId) && !GetArchivedSessionIdByName(ArchivedSessionInfo.SessionName).IsValid() && CreateArchivedSession(ArchivedSessionInfo)) { UE_LOG(LogConcert, Display, TEXT("Archived session '%s' (%s) was discovered."), *ArchivedSessionInfo.SessionName, *ArchivedSessionInfo.SessionId.ToString()); } } } } void FConcertServer::UpdateLastModified(TArray& SessionInfos, const TArray& SessionCreationTimes) { for (int32 i = 0; i < SessionInfos.Num(); ++i) { SessionInfos[i].SetLastModified(SessionCreationTimes[i]); } } void FConcertServer::ArchiveOfflineSessions(const FConcertServerSessionRepository& InRepository) { // Find existing live session files to automatically archive them when recovering from an improper server shutdown. TArray LiveSessionInfos; EventSink->GetSessionsFromPath(*this, InRepository.WorkingDir, LiveSessionInfos); // Migrate the live sessions files into their archived form. for (FConcertSessionInfo& LiveSessionInfo : LiveSessionInfos) { LiveSessionInfo.ServerInstanceId = ServerInfo.InstanceInfo.InstanceId; FConcertSessionInfo ArchivedSessionInfo = LiveSessionInfo; ArchivedSessionInfo.SessionId = FGuid::NewGuid(); ArchivedSessionInfo.SessionName = ConcertServerUtil::GetArchiveName(LiveSessionInfo.SessionName, LiveSessionInfo.Settings); ArchivedSessionInfo.SetLastModifiedToNow(); if (EventSink->ArchiveSession(*this, InRepository.GetSessionWorkingDir(LiveSessionInfo.SessionId), InRepository.GetSessionSavedDir(ArchivedSessionInfo.SessionId), ArchivedSessionInfo, AutoArchiveSessionFilter)) { UE_LOG(LogConcert, Display, TEXT("Deleting %s"), *InRepository.GetSessionWorkingDir(LiveSessionInfo.SessionId)); ConcertUtil::DeleteDirectoryTree(*InRepository.GetSessionWorkingDir(LiveSessionInfo.SessionId)); UE_LOG(LogConcert, Display, TEXT("Live session '%s' (%s) was archived on reboot."), *LiveSessionInfo.SessionName, *LiveSessionInfo.SessionId.ToString()); } } } FGuid FConcertServer::ArchiveSession(const FGuid& SessionId, const FString& ArchiveNameOverride, const FConcertSessionFilter& SessionFilter, FText& OutFailureReason, FGuid ArchiveSessionIdOverride) { if (GetArchivedSessionIdByName(ArchiveNameOverride).IsValid()) { OutFailureReason = FText::Format(LOCTEXT("Error_ArchiveSession_AlreadyExists", "Archived session '{0}' already exists"), FText::AsCultureInvariant(ArchiveNameOverride)); return FGuid(); } const FGuid ArchivedSessionId = ArchiveLiveSession(SessionId, ArchiveNameOverride, SessionFilter, MoveTemp(ArchiveSessionIdOverride)); if (!ArchivedSessionId.IsValid()) { OutFailureReason = LOCTEXT("Error_ArchiveSession_FailedToCopy", "Could not copy session data to the archive"); return FGuid(); } return ArchivedSessionId; } bool FConcertServer::ExportSession(const FGuid& SessionId, const FConcertSessionFilter& SessionFilter, const FString& DestDir, bool bAnonymizeData, FText& OutFailureReason) { return EventSink->ExportSession(*this, SessionId, DestDir, SessionFilter, bAnonymizeData); } bool FConcertServer::RenameSession(const FGuid& SessionId, const FString& NewName, FText& OutFailureReason) { // NOTE: This function is exposed to the server internals and should not be directly called by connected clients. Clients // send requests (see HandleRenameSessionRequest()). When this function is called, the caller is treated as an 'Admin'. FConcertAdmin_RenameSessionRequest Request; Request.SessionId = SessionId; Request.NewName = NewName; Request.UserName = TEXT("Admin"); Request.DeviceName = FString(); bool bCheckPermissions = false; // The caller is expected to be a server Admin, bypass permissions. FConcertAdmin_RenameSessionResponse Response = RenameSessionInternal(Request, bCheckPermissions); OutFailureReason = Response.Reason; return Response.ResponseCode == EConcertResponseCode::Success; } bool FConcertServer::DestroySession(const FGuid& SessionId, FText& OutFailureReason) { // NOTE: This function is exposed to the server internals and should not be directly called by connected clients. Clients // send requests (see HandleDeleteSessionRequest()). When this function is called, the caller is treated as an 'Admin'. FConcertAdmin_DeleteSessionRequest Request; Request.SessionId = SessionId; Request.UserName = TEXT("Admin"); Request.DeviceName = FString(); bool bCheckPermissions = false; // The caller is expected to be a server Admin, bypass permissions. FConcertAdmin_DeleteSessionResponse Response = DeleteSessionInternal(Request, bCheckPermissions); OutFailureReason = Response.Reason; return Response.ResponseCode == EConcertResponseCode::Success; } FOnConcertServerSessionStartup& FConcertServer::OnConcertServerSessionStartup() { return OnConcertServerSessionStartupDelegate; } FOnConcertServerStartup& FConcertServer::OnConcertServerStartup() { return OnConcertServerStartupDelegate; } FOnConcertParticipantCanJoinSession& FConcertServer::OnConcertParticipantCanJoinSession() { return OnConcertParticipantCanJoinSessionDelegate; } TArray FConcertServer::GetLiveSessionInfos() const { TArray SessionsInfo; SessionsInfo.Reserve(LiveSessions.Num()); for (auto& SessionPair : LiveSessions) { SessionsInfo.Add(SessionPair.Value->GetSessionInfo()); } return SessionsInfo; } TArray FConcertServer::GetArchivedSessionInfos() const { TArray SessionsInfo; SessionsInfo.Reserve(ArchivedSessions.Num()); for (auto& SessionPair : ArchivedSessions) { SessionsInfo.Add(SessionPair.Value); } return SessionsInfo; } TArray> FConcertServer::GetLiveSessions() const { TArray> SessionsArray; SessionsArray.Reserve(LiveSessions.Num()); for (auto& SessionPair : LiveSessions) { SessionsArray.Add(SessionPair.Value); } return SessionsArray; } TSharedPtr FConcertServer::GetLiveSession(const FGuid& SessionId) const { return LiveSessions.FindRef(SessionId); } TOptional FConcertServer::GetArchivedSessionInfo(const FGuid& SessionId) const { const FConcertSessionInfo* SessionInfo = ArchivedSessions.Find(SessionId); return SessionInfo ? *SessionInfo : TOptional{}; } const FString& FConcertServer::GetSessionRepositoriesRootDir() const { return SessionRepositoryRootDir; } const FConcertServerSessionRepository& FConcertServer::GetSessionRepository(const FGuid& SessionId) const { const FConcertServerSessionRepository* SessionRepository = MountedSessionRepositories.FindByPredicate([SessionId](const FConcertServerSessionRepository& MountedRepository) { return IFileManager::Get().DirectoryExists(*MountedRepository.GetSessionWorkingDir(SessionId)) || IFileManager::Get().DirectoryExists(*MountedRepository.GetSessionSavedDir(SessionId)); }); check(SessionRepository); // If the session is in memory, its repository must be mounted. return *SessionRepository; } FString FConcertServer::GetSessionSavedDir(const FGuid& SessionId) const { return GetSessionRepository(SessionId).GetSessionSavedDir(SessionId); } FString FConcertServer::GetSessionWorkingDir(const FGuid& SessionId) const { return GetSessionRepository(SessionId).GetSessionWorkingDir(SessionId); } EConcertSessionRepositoryMountResponseCode FConcertServer::MountSessionRepository(FConcertServerSessionRepository Repository, bool bCreateIfNotExist, bool bCleanWorkingDir, bool bCleanExpiredSessions, bool bSearchByPaths, bool bAsDefault) { EConcertSessionRepositoryMountResponseCode MountStatus = EConcertSessionRepositoryMountResponseCode::Mounted; FText MountStatusText = LOCTEXT("SessionRepository_Mounted", "Repository mounted."); bool bAlreadyMountedByThisProcess = false; // Exclusive access scope to the session repository db. { // Load the file containing the instance/repository info. FSystemWideCriticalSection ScopedSystemWideMutex(ConcertServerUtil::GetServerSystemMutexName()); FConcertServerSessionRepositoryDatabase SessionRepositoryDb; ConcertServerUtil::LoadSessionRepositoryDatabase(Role, SessionRepositoryDb); check(!Repository.bMounted && Repository.ProcessId == 0); // Should not be mounted. // Check if the repository can be found in the database. if (FConcertServerSessionRepository* ExistingRepository = SessionRepositoryDb.Repositories.FindByPredicate( [&Repository, bSearchByPaths](const FConcertServerSessionRepository& Candidate){ return bSearchByPaths ? Candidate.WorkingDir == Repository.WorkingDir && Candidate.SavedDir == Repository.SavedDir : Candidate.RepositoryId == Repository.RepositoryId; })) { if (!ExistingRepository->bMounted || !FPlatformProcess::IsApplicationRunning(ExistingRepository->ProcessId)) // Not mounted or mounted by a dead process. { check(Repository.RepositoryRootDir == ExistingRepository->RepositoryRootDir) // The client changed the root dir? ExistingRepository->bMounted = true; ExistingRepository->ProcessId = FPlatformProcess::GetCurrentProcessId(); Repository = *ExistingRepository; MountedSessionRepositories.Add(Repository); ConcertServerUtil::SaveSessionRepositoryDatabase(Role, SessionRepositoryDb); } else if (ExistingRepository->ProcessId == FPlatformProcess::GetCurrentProcessId() && MountedSessionRepositories.ContainsByPredicate([&Repository](const FConcertServerSessionRepository& MatchCandidate){ return MatchCandidate.RepositoryId == Repository.RepositoryId; })) // Already mounted by this process? { UE_LOG(LogConcert, Display, TEXT("Remounted repository %s. The repository is already mounted by this process."), *Repository.RepositoryId.ToString()); bAlreadyMountedByThisProcess = true; // Already mounted by this process, don't process the session files again. } else { UE_LOG(LogConcert, Warning, TEXT("Failed to mount repository %s. The repository is already mounted by another process."), *Repository.RepositoryId.ToString()); MountStatus = EConcertSessionRepositoryMountResponseCode::AlreadyMounted; // Already mounted by another process, cannot mount it, the files are not shareable. MountStatusText = LOCTEXT("SessionRepository_AlreadyMounted", "Repository locked by another process."); } } else if (bCreateIfNotExist) { Repository.bMounted = true; Repository.ProcessId = FPlatformProcess::GetCurrentProcessId(); MountedSessionRepositories.Add(Repository); SessionRepositoryDb.Repositories.Add(Repository); ConcertServerUtil::SaveSessionRepositoryDatabase(Role, SessionRepositoryDb); } else { UE_LOG(LogConcert, Warning, TEXT("Failed to mount repository %s. The repository was not found."), *Repository.RepositoryId.ToString()); MountStatus = EConcertSessionRepositoryMountResponseCode::NotFound; MountStatusText = LOCTEXT("SessionRepository_NotFound", "Repository not found."); } } // Should the mounted repository be used as default? if (bAsDefault) { if (MountStatus == EConcertSessionRepositoryMountResponseCode::Mounted) { DefaultSessionRepository = Repository; UE_LOG(LogConcert, Display, TEXT("Default session repository %s set successfully."), *Repository.RepositoryId.ToString()); } else { DefaultSessionRepository.Reset(); UE_LOG(LogConcert, Warning, TEXT("Default session repository %s failed to mount."), *Repository.RepositoryId.ToString()); } DefaultSessionRepositoryStatus = MountStatusText; } // Should the sessions in the repository processed? if (MountStatus == EConcertSessionRepositoryMountResponseCode::Mounted && !bAlreadyMountedByThisProcess) { // Process the sessions in the repository. if (bCleanWorkingDir) { ConcertUtil::DeleteDirectoryTree(*Repository.WorkingDir); } else if (Settings->bAutoArchiveOnReboot) // Honor the auto-archive settings when mounting a new repository. { // Migrate live sessions files (session is not restored yet) to its archive form and directory. ArchiveOfflineSessions(Repository); } // Reload the archived/live sessions and possibly rotate the list of archives to prevent having too many of them. RecoverSessions(Repository, bCleanExpiredSessions); } return MountStatus; } bool FConcertServer::UnmountSessionRepository(const FGuid& RepositoryId, bool bDropped) { // Search the repository in the list of mounted repositories. int32 Index = MountedSessionRepositories.IndexOfByPredicate([&RepositoryId](const FConcertServerSessionRepository& MatchCandidate) { return RepositoryId == MatchCandidate.RepositoryId; }); if (Index == INDEX_NONE) { return false; // Not mounted by this process. } FConcertServerSessionRepository& Repository = MountedSessionRepositories[Index]; check(Repository.bMounted); // Must be mounted if present in the 'mounted' list. check(Repository.ProcessId == FPlatformProcess::GetCurrentProcessId()); // Must be mounted by this process to be in the list. // Unload the live sessions hosted in that repository. TArray LiveSessionIds; LiveSessions.GetKeys(LiveSessionIds); for (const FGuid& LiveSessionId : LiveSessionIds) { const FConcertServerSessionRepository& SessionRepository = GetSessionRepository(LiveSessionId); if (SessionRepository.RepositoryId == RepositoryId) { DestroyLiveSession(LiveSessionId, /*bDeleteSessionData*/bDropped); } } // Unload the archived sessions hosted in that repository. TArray ArchivedSessionIds; ArchivedSessions.GetKeys(ArchivedSessionIds); for (const FGuid& ArchivedSessionId : ArchivedSessionIds) { const FConcertServerSessionRepository& SessionRepository = GetSessionRepository(ArchivedSessionId); if (SessionRepository.RepositoryId == RepositoryId) { DestroyArchivedSession(ArchivedSessionId, /*bDeleteSessionData*/bDropped); } } if (DefaultSessionRepository && DefaultSessionRepository->RepositoryId == RepositoryId) { DefaultSessionRepository.Reset(); // Will not be able to create new sessions until a mounted repository is set as default. DefaultSessionRepositoryStatus = LOCTEXT("SessionRepository_Unmounted", "Repository unmounted."); UE_LOG(LogConcert, Warning, TEXT("Default repository %s unmounted. No session will be created until a mounted repository is set as default"), *RepositoryId.ToString()); } else { UE_LOG(LogConcert, Display, TEXT("Repository %s unmounted."), *RepositoryId.ToString()) } if (bDropped && !Repository.RepositoryRootDir.IsEmpty()) // When dropped, the repository can be deleted if it has the standard root structure. { FString RepositoryDir = Repository.RepositoryRootDir / Repository.RepositoryId.ToString(); if (ConcertUtil::DeleteDirectoryTree(*RepositoryDir)) { UE_LOG(LogConcert, Display, TEXT("Repository %s deleted."), *Repository.RepositoryId.ToString()) } } // Remove the repository from the of mounted repository list. MountedSessionRepositories.RemoveAt(Index); // Update the repository database file { FSystemWideCriticalSection ScopedSystemWideMutex(ConcertServerUtil::GetServerSystemMutexName()); FConcertServerSessionRepositoryDatabase SessionRepositoryDb; ConcertServerUtil::LoadSessionRepositoryDatabase(Role, SessionRepositoryDb); if (bDropped) { SessionRepositoryDb.Repositories.RemoveAll([&RepositoryId](const FConcertServerSessionRepository& RemoveCandidate) { return RepositoryId == RemoveCandidate.RepositoryId; }); } else if (FConcertServerSessionRepository* UnmountedRepo = SessionRepositoryDb.Repositories.FindByPredicate([&RepositoryId](const FConcertServerSessionRepository& MatchRepository) { return RepositoryId == MatchRepository.RepositoryId; })) { UnmountedRepo->bMounted = false; UnmountedRepo->ProcessId = 0; } ConcertServerUtil::SaveSessionRepositoryDatabase(Role, SessionRepositoryDb); } return true; } bool FConcertServer::MountDefaultSessionRepository(const UConcertServerConfig* ServerConfig) { if (DefaultSessionRepository) { return true; // A default session repository is already mounted. } // If the server was configured to use a custom working/archive dir, create a corresponding repository and try to mount it. if (!ServerConfig->WorkingDir.IsEmpty() || !ServerConfig->ArchiveDir.IsEmpty()) { FConcertServerSessionRepository Repository(Role, FGuid::NewGuid(), ServerConfig->WorkingDir, ServerConfig->ArchiveDir); return MountSessionRepository(MoveTemp(Repository), /*bCreateIfNotExist*/true, ServerConfig->bCleanWorkingDir, /*bCleanupExpiredSession*/true, /*bSearchByPath*/true, /*bAsDefault*/true) == EConcertSessionRepositoryMountResponseCode::Mounted; } // If the server was configured to mount a default server managed repository. else if (ServerConfig->bMountDefaultSessionRepository) { FConcertServerSessionRepository Repository(GetSessionRepositoriesRootDir(), FGuid()); // Invalid GUID is used for the default server repository. return MountSessionRepository(MoveTemp(Repository), /*bCreateIfNotExist*/true, ServerConfig->bCleanWorkingDir, /*bCleanupExpiredSession*/true, /*bSearchByPath*/false, /*bAsDefault*/true) == EConcertSessionRepositoryMountResponseCode::Mounted; } return false; // No session repository was mounted as default. } void FConcertServer::HandleDiscoverServersEvent(const FConcertMessageContext& Context) { const FConcertAdmin_DiscoverServersEvent* Message = Context.GetMessage(); if (Message->ConcertProtocolVersion == EConcertMessageVersion::LatestVersion && ServerAdminEndpoint.IsValid() && Message->RequiredRole == Role && Message->RequiredVersion == VERSION_STRINGIFY(ENGINE_MAJOR_VERSION) TEXT(".") VERSION_STRINGIFY(ENGINE_MINOR_VERSION)) { if (Settings->AuthorizedClientKeys.Num() == 0 || Settings->AuthorizedClientKeys.Contains(Message->ClientAuthenticationKey)) // Can the client discover this server? { FConcertAdmin_ServerDiscoveredEvent DiscoveryInfo; DiscoveryInfo.ConcertProtocolVersion = EConcertMessageVersion::LatestVersion; DiscoveryInfo.ServerName = ServerInfo.ServerName; DiscoveryInfo.InstanceInfo = ServerInfo.InstanceInfo; DiscoveryInfo.ServerFlags = ServerInfo.ServerFlags; ServerAdminEndpoint->SendEvent(DiscoveryInfo, Context.SenderConcertEndpointId); } } } TFuture FConcertServer::HandleMountSessionRepositoryRequest(const FConcertMessageContext& Context) { const FConcertAdmin_MountSessionRepositoryRequest* Message = Context.GetMessage(); FConcertAdmin_MountSessionRepositoryResponse ResponseData; if (Message->RepositoryRootDir.IsEmpty()) // Use the server configured repository root dir? { FConcertServerSessionRepository Repository(GetSessionRepositoriesRootDir(), Message->RepositoryId); ResponseData.MountStatus = MountSessionRepository(MoveTemp(Repository), Message->bCreateIfNotExist, /*bCleanWorkingDir*/false, /*bCleanExpiredSessions*/false, /*bSearchByPaths*/false, Message->bAsServerDefault); } else // Use the client supplied repository root dir. { FConcertServerSessionRepository Repository(Message->RepositoryRootDir, Message->RepositoryId); ResponseData.MountStatus = MountSessionRepository(MoveTemp(Repository), Message->bCreateIfNotExist, /*bCleanWorkingDir*/false, /*bCleanExpiredSessions*/false, /*bSearchByPaths*/false, Message->bAsServerDefault); } return FConcertAdmin_MountSessionRepositoryResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleGetSessionRepositoriesRequest(const FConcertMessageContext& Context) { // Prevent concurrent access to the instance file. FSystemWideCriticalSection ScopedSystemWideMutex(ConcertServerUtil::GetServerSystemMutexName()); // Load the global database containing all known repositories. FConcertServerSessionRepositoryDatabase SessionRepositoryDb; ConcertServerUtil::LoadSessionRepositoryDatabase(Role, SessionRepositoryDb); bool bDatabaseUpdated = false; // Fill up the response. FConcertAdmin_GetSessionRepositoriesResponse ResponseData; for (FConcertServerSessionRepository& Repository : SessionRepositoryDb.Repositories) { if (Repository.bMounted && !FPlatformProcess::IsApplicationRunning(Repository.ProcessId)) // Check if the state still hold. { Repository.bMounted = false; // Update the state. Repository.ProcessId = 0; bDatabaseUpdated = true; } ResponseData.SessionRepositories.Add(FConcertSessionRepositoryInfo{Repository.RepositoryId, Repository.bMounted}); } if (bDatabaseUpdated) { ConcertServerUtil::SaveSessionRepositoryDatabase(Role, SessionRepositoryDb); } return FConcertAdmin_GetSessionRepositoriesResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleDropSessionRepositoriesRequest(const FConcertMessageContext& Context) { const FConcertAdmin_DropSessionRepositoriesRequest* Message = Context.GetMessage(); FConcertAdmin_DropSessionRepositoriesResponse ResponseData; // Drop the repository currently mounted by this process. for (const FGuid& RepositoryId : Message->RepositoryIds) { if (UnmountSessionRepository(RepositoryId, /*bDropped*/true)) { ResponseData.DroppedRepositoryIds.Add(RepositoryId); } } // Drop the repository that aren't mounted, but found in the global repository database. { FSystemWideCriticalSection ScopedSystemWideMutex(ConcertServerUtil::GetServerSystemMutexName()); FConcertServerSessionRepositoryDatabase SessionRepositoryDb; ConcertServerUtil::LoadSessionRepositoryDatabase(Role, SessionRepositoryDb); // Drop the repositories. for (const FGuid& RepositoryId : Message->RepositoryIds) { int32 Index = SessionRepositoryDb.Repositories.IndexOfByPredicate([&RepositoryId](const FConcertServerSessionRepository& Repository) { return Repository.RepositoryId == RepositoryId; }); if (Index == INDEX_NONE) { ResponseData.DroppedRepositoryIds.Add(RepositoryId); // Not mapped in the DB -> successufully dropped. continue; } FConcertServerSessionRepository& Repository = SessionRepositoryDb.Repositories[Index]; if (!Repository.bMounted || !FPlatformProcess::IsApplicationRunning(Repository.ProcessId)) // Not mounted or mounted by a dead process. { // Check if the server can delete the folder safely i.e. it has the standard structure managed by the server. if (!Repository.RepositoryRootDir.IsEmpty()) { FString ReposDir = Repository.RepositoryRootDir / Repository.RepositoryId.ToString(); ConcertUtil::DeleteDirectoryTree(*ReposDir); } // Unmap it. SessionRepositoryDb.Repositories.RemoveAt(Index); ResponseData.DroppedRepositoryIds.Add(RepositoryId); } } if (ResponseData.DroppedRepositoryIds.Num()) { ConcertServerUtil::SaveSessionRepositoryDatabase(Role, SessionRepositoryDb); } } return FConcertAdmin_DropSessionRepositoriesResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleCreateSessionRequest(const FConcertMessageContext& Context) { const FConcertAdmin_CreateSessionRequest* Message = Context.GetMessage(); // Create a new server session FText CreateFailureReason; TSharedPtr NewServerSession; { FConcertSessionInfo SessionInfo = CreateSessionInfo(); SessionInfo.OwnerInstanceId = Message->OwnerClientInfo.InstanceInfo.InstanceId; SessionInfo.OwnerUserName = Message->OwnerClientInfo.UserName; SessionInfo.OwnerDeviceName = Message->OwnerClientInfo.DeviceName; SessionInfo.SessionName = Message->SessionName; SessionInfo.Settings = Message->SessionSettings; SessionInfo.VersionInfos.Add(Message->VersionInfo); NewServerSession = CreateSession(SessionInfo, CreateFailureReason); } // We have a valid session if it succeeded FConcertAdmin_SessionInfoResponse ResponseData; if (NewServerSession) { ResponseData.SessionInfo = NewServerSession->GetSessionInfo(); ResponseData.ResponseCode = EConcertResponseCode::Success; } else { ResponseData.ResponseCode = EConcertResponseCode::Failed; ResponseData.Reason = CreateFailureReason; UE_LOG(LogConcert, Display, TEXT("Session creation failed. (User: %s, Reason: %s)"), *Message->OwnerClientInfo.UserName, *ResponseData.Reason.ToString()); } return FConcertAdmin_SessionInfoResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleFindSessionRequest(const FConcertMessageContext& Context) { const FConcertAdmin_FindSessionRequest* Message = Context.GetMessage(); FConcertAdmin_SessionInfoResponse ResponseData; // Find the session requested TSharedPtr ServerSession = GetLiveSession(Message->SessionId); const TCHAR* ServerSessionNamePtr = ServerSession ? *ServerSession->GetName() : TEXT(""); if (CanJoinSession(ServerSession, Message->SessionSettings, Message->VersionInfo, Message->ConcertEndpointId, Message->OwnerClientInfo, &ResponseData.Reason)) { ResponseData.ResponseCode = EConcertResponseCode::Success; ResponseData.SessionInfo = ServerSession->GetSessionInfo(); UE_LOG(LogConcert, Display, TEXT("Allowing user %s to join session %s (Id: %s, Owner: %s)"), *Message->OwnerClientInfo.UserName, ServerSessionNamePtr, *Message->SessionId.ToString(), *ServerSession->GetSessionInfo().OwnerUserName); } else { ResponseData.ResponseCode = EConcertResponseCode::Failed; UE_LOG(LogConcert, Display, TEXT("Refusing user %s to join session %s (Id: %s, Owner: %s, Reason: %s)"), *Message->OwnerClientInfo.UserName, ServerSessionNamePtr, *Message->SessionId.ToString(), ServerSession ? *ServerSession->GetSessionInfo().OwnerUserName : TEXT("unknown owner"), *ResponseData.Reason.ToString()); } return FConcertAdmin_SessionInfoResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleCopySessionRequest(const FConcertMessageContext& Context) { const FConcertAdmin_CopySessionRequest* Message = Context.GetMessage(); // Restore the server session FText FailureReason; TSharedPtr NewServerSession; { FConcertSessionInfo SessionInfo = CreateSessionInfo(); SessionInfo.OwnerInstanceId = Message->OwnerClientInfo.InstanceInfo.InstanceId; SessionInfo.OwnerUserName = Message->OwnerClientInfo.UserName; SessionInfo.OwnerDeviceName = Message->OwnerClientInfo.DeviceName; SessionInfo.SessionName = Message->SessionName; SessionInfo.Settings = Message->SessionSettings; SessionInfo.VersionInfos.Add(Message->VersionInfo); NewServerSession = Message->bRestoreOnly ? RestoreSession(Message->SessionId, SessionInfo, Message->SessionFilter, FailureReason) : CopySession(Message->SessionId, SessionInfo, Message->SessionFilter, FailureReason); } // We have a valid session if it succeeded FConcertAdmin_SessionInfoResponse ResponseData; if (NewServerSession) { ResponseData.SessionInfo = NewServerSession->GetSessionInfo(); ResponseData.ResponseCode = EConcertResponseCode::Success; } else { ResponseData.ResponseCode = EConcertResponseCode::Failed; ResponseData.Reason = FailureReason; UE_LOG(LogConcert, Display, TEXT("Session copy failed. (User: %s, Reason: %s)"), *Message->OwnerClientInfo.UserName, *ResponseData.Reason.ToString()); } return FConcertAdmin_SessionInfoResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleArchiveSessionRequest(const FConcertMessageContext& Context) { const FConcertAdmin_ArchiveSessionRequest* Message = Context.GetMessage(); FConcertAdmin_ArchiveSessionResponse ResponseData; // Find the session requested. TSharedPtr ServerSession = GetLiveSession(Message->SessionId); ResponseData.SessionId = Message->SessionId; ResponseData.SessionName = ServerSession ? ServerSession->GetName() : TEXT(""); if (ServerSession) { FText FailureReason; const FGuid ArchivedSessionId = ArchiveSession(Message->SessionId, Message->ArchiveNameOverride, Message->SessionFilter, FailureReason); if (ArchivedSessionId.IsValid()) { const FConcertSessionInfo& ArchivedSessionInfo = ArchivedSessions.FindChecked(ArchivedSessionId); ResponseData.ResponseCode = EConcertResponseCode::Success; ResponseData.ArchiveId = ArchivedSessionId; ResponseData.ArchiveName = ArchivedSessionInfo.SessionName; UE_LOG(LogConcert, Display, TEXT("User %s archived session %s (%s) as %s (%s)"), *Message->UserName, *ResponseData.SessionName, *ResponseData.SessionId.ToString(), *ResponseData.ArchiveName, *ResponseData.ArchiveId.ToString()); } else { ResponseData.ResponseCode = EConcertResponseCode::Failed; ResponseData.Reason = FailureReason; UE_LOG(LogConcert, Display, TEXT("User %s failed to archive session %s (Id: %s, Reason: %s)"), *Message->UserName, *ResponseData.SessionName, *ResponseData.SessionId.ToString(), *ResponseData.Reason.ToString()); } } else { ResponseData.ResponseCode = EConcertResponseCode::Failed; ResponseData.Reason = LOCTEXT("Error_SessionDoesNotExist", "Session does not exist."); UE_LOG(LogConcert, Display, TEXT("User %s failed to archive session %s (Id: %s, Reason: %s)"), *Message->UserName, *ResponseData.SessionName, *ResponseData.SessionId.ToString(), *ResponseData.Reason.ToString()); } return FConcertAdmin_ArchiveSessionResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleRenameSessionRequest(const FConcertMessageContext& Context) { return FConcertAdmin_RenameSessionResponse::AsFuture(RenameSessionInternal(*Context.GetMessage(), /*bCheckPermission*/true)); } FConcertAdmin_RenameSessionResponse FConcertServer::RenameSessionInternal(const FConcertAdmin_RenameSessionRequest& Request, bool bCheckPermission) { FConcertAdmin_RenameSessionResponse ResponseData; ResponseData.SessionId = Request.SessionId; ResponseData.ResponseCode = EConcertResponseCode::Failed; if (TSharedPtr ServerSession = GetLiveSession(Request.SessionId)) // Live session? { ResponseData.OldName = ServerSession->GetName(); if (bCheckPermission && !IsRequestFromSessionOwner(ServerSession, Request.UserName, Request.DeviceName)) // Not owner? { ResponseData.Reason = LOCTEXT("Error_Rename_InvalidPerms_NotOwner", "Not the session owner."); UE_LOG(LogConcert, Error, TEXT("User %s failed to rename live session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ServerSession->GetName(), *ResponseData.SessionId.ToString(), *ServerSession->GetSessionInfo().OwnerUserName, *ResponseData.Reason.ToString()); } else if (GetLiveSessionIdByName(Request.NewName).IsValid()) // Name collision? { ResponseData.Reason = FText::Format(LOCTEXT("Error_Rename_SessionAlreadyExists", "Session '{0}' already exists"), FText::AsCultureInvariant(Request.NewName)); UE_LOG(LogConcert, Error, TEXT("User %s failed to rename live session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ServerSession->GetName(), *ResponseData.SessionId.ToString(), *ServerSession->GetSessionInfo().OwnerUserName, *ResponseData.Reason.ToString()); } else { ServerSession->SetName(Request.NewName); EventSink->OnLiveSessionRenamed(*this, ServerSession.ToSharedRef()); ResponseData.ResponseCode = EConcertResponseCode::Success; UE_LOG(LogConcert, Display, TEXT("User %s renamed live session %s from %s to %s"), *Request.UserName, *ResponseData.SessionId.ToString(), *ResponseData.OldName, *ServerSession->GetName()); } } else if (FConcertSessionInfo* ArchivedSessionInfo = ArchivedSessions.Find(Request.SessionId)) // Archive session? { ResponseData.OldName = ArchivedSessionInfo->SessionName; if (bCheckPermission && (ArchivedSessionInfo->OwnerUserName != Request.UserName || ArchivedSessionInfo->OwnerDeviceName != Request.DeviceName)) // Not the owner? { ResponseData.Reason = LOCTEXT("Error_Rename_InvalidPerms_NotOwner", "Not the session owner."); UE_LOG(LogConcert, Display, TEXT("User %s failed to rename archived session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ArchivedSessionInfo->SessionName, *ResponseData.SessionId.ToString(), *ArchivedSessionInfo->OwnerUserName, *ResponseData.Reason.ToString()); } else if (GetArchivedSessionIdByName(Request.NewName).IsValid()) // Name collision? { ResponseData.Reason = FText::Format(LOCTEXT("Error_Rename_ArchiveAlreadyExists", "Archive '{0}' already exists"), FText::AsCultureInvariant(Request.NewName)); UE_LOG(LogConcert, Error, TEXT("User %s failed to rename archived session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ArchivedSessionInfo->SessionName, *ResponseData.SessionId.ToString(), *ArchivedSessionInfo->OwnerUserName, *ResponseData.Reason.ToString()); } else { ArchivedSessionInfo->SessionName = Request.NewName; EventSink->OnArchivedSessionRenamed(*this, GetSessionSavedDir(Request.SessionId), *ArchivedSessionInfo); ResponseData.ResponseCode = EConcertResponseCode::Success; UE_LOG(LogConcert, Display, TEXT("User %s renamed archived session %s from %s to %s"), *Request.UserName, *ResponseData.SessionId.ToString(), *ResponseData.OldName, *Request.NewName); } } else // Not found? { ResponseData.Reason = LOCTEXT("Error_Rename_DoesNotExist", "Session does not exist."); UE_LOG(LogConcert, Display, TEXT("User %s failed to rename session (Id: %s, Reason: %s)"), *Request.UserName, *ResponseData.SessionId.ToString(), *ResponseData.Reason.ToString()); } return ResponseData; } TFuture FConcertServer::HandleBatchDeleteSessionRequest(const FConcertMessageContext& Context) { FConcertAdmin_BatchDeleteSessionResponse ResponseData; ResponseData.ResponseCode = EConcertResponseCode::Failed; const FConcertAdmin_BatchDeleteSessionRequest& Request = *Context.GetMessage(); TMap SessionNames; if (ValidateBatchDeletionRequest(Request, ResponseData, SessionNames)) { ResponseData.ResponseCode = EConcertResponseCode::Success; FConcertAdmin_DeleteSessionRequest DeleteSingleSessionRequest; for (const FGuid& SessionToDelete : Request.SessionIds) { const bool bSkip = ResponseData.NotOwnedByClient.ContainsByPredicate([&SessionToDelete](const FDeletedSessionInfo& Info ){ return Info.SessionId == SessionToDelete; }); if (bSkip) { continue; } DeleteSingleSessionRequest.SessionId = SessionToDelete; const FConcertAdmin_DeleteSessionResponse DeleteResponse = DeleteSessionInternal(DeleteSingleSessionRequest, false); if (DeleteResponse.ResponseCode == EConcertResponseCode::Success) { ResponseData.DeletedItems.Add({ SessionToDelete, SessionNames[SessionToDelete] }); } else { // We may already have deleted some files ... maybe we should restore them in the future... ResponseData.ResponseCode = EConcertResponseCode::Failed; break; } } } return FConcertAdmin_BatchDeleteSessionResponse::AsFuture(MoveTemp(ResponseData)); } bool FConcertServer::ValidateBatchDeletionRequest(const FConcertAdmin_BatchDeleteSessionRequest& Request, FConcertAdmin_BatchDeleteSessionResponse& OutResponse, TMap& PreparedSessionInfo) const { TArray NotOwnedByClient; for (const FGuid& SessionToDelete : Request.SessionIds) { if (TSharedPtr ServerSession = GetLiveSession(SessionToDelete)) { const bool bHasPermission = IsRequestFromSessionOwner(ServerSession, Request.UserName, Request.DeviceName); if (!bHasPermission && (Request.Flags & EBatchSessionDeletionFlags::SkipForbiddenSessions) != EBatchSessionDeletionFlags::Strict) { NotOwnedByClient.Add({ SessionToDelete, ServerSession->GetName() }); } else if (!bHasPermission) { OutResponse.Reason = LOCTEXT("Error_BatchDelete_InvalidPerms_NotOwner", "Not the session owner."); UE_LOG(LogConcert, Display, TEXT("User %s failed to delete live session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ServerSession->GetName(), *SessionToDelete.ToString(), *ServerSession->GetSessionInfo().OwnerUserName, *OutResponse.Reason.ToString()); return false; } PreparedSessionInfo.Add(SessionToDelete, ServerSession->GetName()); } else if (const FConcertSessionInfo* ArchivedSessionInfo = ArchivedSessions.Find(SessionToDelete)) { const bool bHasPermission = IsRequestFromSessionOwner(*ArchivedSessionInfo, Request.UserName, Request.DeviceName); if (!bHasPermission && (Request.Flags & EBatchSessionDeletionFlags::SkipForbiddenSessions) != EBatchSessionDeletionFlags::Strict) { NotOwnedByClient.Add({ SessionToDelete, ArchivedSessionInfo->SessionName }); } else if (!bHasPermission) { OutResponse.Reason = LOCTEXT("Error_BatchDelete_InvalidPerms_NotOwner", "Not the session owner."); UE_LOG(LogConcert, Display, TEXT("User %s failed to delete live session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ArchivedSessionInfo->SessionName, *SessionToDelete.ToString(), *ArchivedSessionInfo->OwnerUserName, *OutResponse.Reason.ToString()); return false; } PreparedSessionInfo.Add(SessionToDelete, ArchivedSessionInfo->SessionName); } else { OutResponse.Reason = FText::Format(LOCTEXT("Error_BatchDelete_SessionDoesNotExist", "Session ID {0} does not exist."), FText::FromString(SessionToDelete.ToString())); UE_LOG(LogConcert, Display, TEXT("User %s failed to delete session (Id: %s, Reason: %s)"), *Request.UserName, *SessionToDelete.ToString(), *OutResponse.Reason.ToString()); return false; } } OutResponse.NotOwnedByClient = MoveTemp(NotOwnedByClient); return true; } TFuture FConcertServer::HandleDeleteSessionRequest(const FConcertMessageContext & Context) { return FConcertAdmin_DeleteSessionResponse::AsFuture(DeleteSessionInternal(*Context.GetMessage(), /*bCheckPermission*/true)); } FConcertAdmin_DeleteSessionResponse FConcertServer::DeleteSessionInternal(const FConcertAdmin_DeleteSessionRequest& Request, bool bCheckPermission) { FConcertAdmin_DeleteSessionResponse ResponseData; ResponseData.SessionId = Request.SessionId; ResponseData.ResponseCode = EConcertResponseCode::Failed; if (TSharedPtr ServerSession = GetLiveSession(Request.SessionId)) // Live session? { ResponseData.SessionName = ServerSession->GetName(); if (bCheckPermission && !IsRequestFromSessionOwner(ServerSession, Request.UserName, Request.DeviceName)) { ResponseData.Reason = LOCTEXT("Error_Delete_InvalidPerms_NotOwner", "Not the session owner."); UE_LOG(LogConcert, Display, TEXT("User %s failed to delete live session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ResponseData.SessionName, *ResponseData.SessionId.ToString(), *ServerSession->GetSessionInfo().OwnerUserName, *ResponseData.Reason.ToString()); } else if (!DestroyLiveSession(Request.SessionId, /*bDeleteSessionData*/true)) { ResponseData.Reason = LOCTEXT("Error_Delete_SessionFailedToDestroy", "Failed to destroy session."); UE_LOG(LogConcert, Display, TEXT("User %s failed to delete live session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ResponseData.SessionName, *ResponseData.SessionId.ToString(), *ServerSession->GetSessionInfo().OwnerUserName, *ResponseData.Reason.ToString()); } else // Succeeded to delete the session. { ResponseData.ResponseCode = EConcertResponseCode::Success; UE_LOG(LogConcert, Display, TEXT("User %s deleted live session %s (%s)"), *Request.UserName, *ResponseData.SessionName, *ResponseData.SessionId.ToString()); } } else if (const FConcertSessionInfo* ArchivedSessionInfo = ArchivedSessions.Find(Request.SessionId)) // Archived session? { ResponseData.SessionName = ArchivedSessionInfo->SessionName; if (bCheckPermission && (ArchivedSessionInfo->OwnerUserName != Request.UserName || ArchivedSessionInfo->OwnerDeviceName != Request.DeviceName)) // Not the owner? { ResponseData.Reason = LOCTEXT("Error_Delete_InvalidPerms_NotOwner", "Not the session owner."); UE_LOG(LogConcert, Display, TEXT("User %s failed to delete archived session '%s' (Id: %s, Owner: %s, Reason: %s)"), *Request.UserName, *ArchivedSessionInfo->SessionName, *ResponseData.SessionId.ToString(), *ArchivedSessionInfo->OwnerUserName, *ResponseData.Reason.ToString()); } else if (!DestroyArchivedSession(Request.SessionId, /*bDeleteSessionData*/true)) { ResponseData.Reason = LOCTEXT("Error_Delete_SessionFailedToDestroy", "Failed to destroy session."); UE_LOG(LogConcert, Display, TEXT("User %s failed to delete archived session '%s' (Id: %s, Reason: %s)"), *Request.UserName, *ResponseData.SessionName, *ResponseData.SessionId.ToString(), *ResponseData.Reason.ToString()); } else // Succeeded to delete the session. { ResponseData.ResponseCode = EConcertResponseCode::Success; UE_LOG(LogConcert, Display, TEXT("User %s deleted archived session %s (%s)"), *Request.UserName, *ResponseData.SessionName, *ResponseData.SessionId.ToString()); } } else // Not found? { ResponseData.Reason = LOCTEXT("Error_Delete_SessionDoesNotExist", "Session does not exist."); UE_LOG(LogConcert, Display, TEXT("User %s failed to delete session (Id: %s, Reason: %s)"), *Request.UserName, *ResponseData.SessionId.ToString(), *ResponseData.Reason.ToString()); } return ResponseData; } TFuture FConcertServer::HandleGetAllSessionsRequest(const FConcertMessageContext& Context) { const FConcertAdmin_GetAllSessionsRequest* Message = Context.GetMessage(); FConcertAdmin_GetAllSessionsResponse ResponseData; ResponseData.LiveSessions = GetLiveSessionInfos(); for (const auto& ArchivedSessionPair : ArchivedSessions) { ResponseData.ArchivedSessions.Add(ArchivedSessionPair.Value); } return FConcertAdmin_GetAllSessionsResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleGetLiveSessionsRequest(const FConcertMessageContext& Context) { const FConcertAdmin_GetLiveSessionsRequest* Message = Context.GetMessage(); FConcertAdmin_GetSessionsResponse ResponseData; ResponseData.Sessions = GetLiveSessionInfos(); return FConcertAdmin_GetSessionsResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleGetArchivedSessionsRequest(const FConcertMessageContext& Context) { FConcertAdmin_GetSessionsResponse ResponseData; ResponseData.ResponseCode = EConcertResponseCode::Success; for (const auto& ArchivedSessionPair : ArchivedSessions) { ResponseData.Sessions.Add(ArchivedSessionPair.Value); } return FConcertAdmin_GetSessionsResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleGetSessionClientsRequest(const FConcertMessageContext& Context) { const FConcertAdmin_GetSessionClientsRequest* Message = Context.GetMessage(); FConcertAdmin_GetSessionClientsResponse ResponseData; ResponseData.SessionClients = ConcertUtil::GetSessionClients(*this, Message->SessionId); return FConcertAdmin_GetSessionClientsResponse::AsFuture(MoveTemp(ResponseData)); } TFuture FConcertServer::HandleGetSessionActivitiesRequest(const FConcertMessageContext& Context) { FConcertAdmin_GetSessionActivitiesResponse ResponseData; const FConcertAdmin_GetSessionActivitiesRequest* Message = Context.GetMessage(); if (EventSink->GetUnmutedSessionActivities(*this, Message->SessionId, Message->FromActivityId, Message->ActivityCount, ResponseData.Activities, ResponseData.EndpointClientInfoMap, Message->bIncludeDetails)) { ResponseData.ResponseCode = EConcertResponseCode::Success; } else // The only reason to get here is when the session is not found. { ResponseData.ResponseCode = EConcertResponseCode::Failed; ResponseData.Reason = LOCTEXT("Error_SessionActivities_SessionDoesNotExist", "Session does not exist or its database is corrupted."); UE_LOG(LogConcert, Display, TEXT("Failed to fetch activities from session (Id: %s, Reason: %s)"), *Message->SessionId.ToString(), *ResponseData.Reason.ToString()); } return FConcertAdmin_GetSessionActivitiesResponse::AsFuture(MoveTemp(ResponseData)); } bool FConcertServer::CanJoinSession(const TSharedPtr& ServerSession, const FConcertSessionSettings& SessionSettings, const FConcertSessionVersionInfo& SessionVersionInfo, const FGuid& EndpointId, const FConcertClientInfo& ClientInfo, FText* OutFailureReason) { if (!ServerSession) { if (OutFailureReason) { *OutFailureReason = LOCTEXT("Error_CanJoinSession_UnknownSession", "Unknown session"); } return false; } if (OnConcertParticipantCanJoinSessionDelegate.IsBound()) { if (!OnConcertParticipantCanJoinSessionDelegate.Execute(ServerSession->GetId(), EndpointId, ClientInfo, OutFailureReason)) { return false; } } if (Settings->ServerSettings.bIgnoreSessionSettingsRestriction) { return true; } if (!ServerSession->GetSessionInfo().Settings.ValidateRequirements(SessionSettings, OutFailureReason)) { return false; } if (ServerSession->GetSessionInfo().VersionInfos.Num() > 0 && !ServerSession->GetSessionInfo().VersionInfos.Last().Validate(SessionVersionInfo, EConcertVersionValidationMode::PatchCompatible, OutFailureReason)) { return false; } return true; } bool FConcertServer::IsRequestFromSessionOwner(const TSharedPtr& SessionToDelete, const FString& FromUserName, const FString& FromDeviceName) const { if (SessionToDelete) { const FConcertSessionInfo& SessionInfo = SessionToDelete->GetSessionInfo(); return IsRequestFromSessionOwner(SessionInfo, FromUserName, FromDeviceName); } return false; } bool FConcertServer::IsRequestFromSessionOwner(const FConcertSessionInfo& SessionInfo, const FString& FromUserName, const FString& FromDeviceName) const { return SessionInfo.OwnerUserName == FromUserName && SessionInfo.OwnerDeviceName == FromDeviceName; } TSharedPtr FConcertServer::CreateLiveSession(const FConcertSessionInfo& SessionInfo, const FConcertServerSessionRepository& InRepository) { check(SessionInfo.SessionId.IsValid() && !SessionInfo.SessionName.IsEmpty()); check(!LiveSessions.Contains(SessionInfo.SessionId) && !GetLiveSessionIdByName(SessionInfo.SessionName).IsValid()); // Strip version info when using -CONCERTIGNORE FConcertSessionInfo LiveSessionInfo = SessionInfo; if (Settings->ServerSettings.bIgnoreSessionSettingsRestriction) { UE_CLOG(LiveSessionInfo.VersionInfos.Num() > 0, LogConcert, Warning, TEXT("Clearing version information when creating session '%s' due to -CONCERTIGNORE. This session will be unversioned!"), *LiveSessionInfo.SessionName); LiveSessionInfo.VersionInfos.Reset(); } TSharedPtr SessionEndpoint = EndpointProvider->CreateLocalEndpoint(LiveSessionInfo.SessionName, Settings->EndpointSettings, [this](const FConcertEndpointContext& Context) { return ConcertUtil::CreateLogger(Context, [this](const FConcertLog& Log) { ConcertTransportEvents::OnConcertServerLogEvent().Broadcast(*this, Log); }); }); SessionEndpoint->OnConcertMessageAcknowledgementReceived().AddLambda( [this](const FConcertEndpointContext& LocalEndpoint, const FConcertEndpointContext& RemoteEndpoint, const TSharedRef& AckedMessage, const FConcertMessageContext& MessageContext) { OnConcertMessageAcknowledgementReceivedFromLocalEndpoint.Broadcast(LocalEndpoint, RemoteEndpoint, AckedMessage, MessageContext); }); TSharedPtr LiveSession = MakeShared( LiveSessionInfo, Settings->ServerSettings, SessionEndpoint, InRepository.GetSessionWorkingDir(LiveSessionInfo.SessionId) ); FInternalLiveSessionCreationParams CreationParams; CreationParams.OnModifiedCallback.BindSP(LiveSession.Get(), &FConcertServerSession::SetLastModifiedToNow); if (EventSink->OnLiveSessionCreated(*this, LiveSession.ToSharedRef(), CreationParams)) // EventSync could complete the session initialization? { LiveSessions.Add(LiveSessionInfo.SessionId, LiveSession); LiveSession->Startup(); OnConcertServerSessionStartupDelegate.Broadcast(LiveSession); return LiveSession; } return nullptr; } bool FConcertServer::DestroyLiveSession(const FGuid& LiveSessionId, const bool bDeleteSessionData) { TSharedPtr LiveSession = LiveSessions.FindRef(LiveSessionId); if (LiveSession) { EventSink->OnLiveSessionDestroyed(*this, LiveSession.ToSharedRef()); LiveSession->Shutdown(); LiveSessions.Remove(LiveSessionId); if (bDeleteSessionData) { ConcertUtil::DeleteDirectoryTree(*GetSessionWorkingDir(LiveSessionId)); } return true; } return false; } FGuid FConcertServer::ArchiveLiveSession(const FGuid& LiveSessionId, const FString& ArchivedSessionNameOverride, const FConcertSessionFilter& SessionFilter, FGuid ArchiveSessionIdOverride) { TSharedPtr LiveSession = LiveSessions.FindRef(LiveSessionId); if (LiveSession) { FString ArchivedSessionName = ArchivedSessionNameOverride; if (ArchivedSessionName.IsEmpty()) { ArchivedSessionName = ConcertServerUtil::GetArchiveName(*LiveSession->GetName(), LiveSession->GetSessionInfo().Settings); } { const FGuid ArchivedSessionId = GetArchivedSessionIdByName(ArchivedSessionName); DestroyArchivedSession(ArchivedSessionId, /*bDeleteSessionData*/true); } // Find the live session repository to stored the archive in the same one. const FConcertServerSessionRepository& SessionRepository = GetSessionRepository(LiveSession->GetSessionInfo().SessionId); FConcertSessionInfo ArchivedSessionInfo = LiveSession->GetSessionInfo(); ArchivedSessionInfo.SessionId = ensure(ArchiveSessionIdOverride.IsValid()) ? MoveTemp(ArchiveSessionIdOverride) : FGuid::NewGuid(); ArchivedSessionInfo.SessionName = MoveTemp(ArchivedSessionName); if (EventSink->ArchiveSession(*this, LiveSession.ToSharedRef(), SessionRepository.GetSessionSavedDir(ArchivedSessionInfo.SessionId), ArchivedSessionInfo, SessionFilter)) { UE_LOG(LogConcert, Display, TEXT("Live session '%s' (%s) was archived as '%s' (%s)"), *LiveSession->GetName(), *LiveSession->GetId().ToString(), *ArchivedSessionInfo.SessionName, *ArchivedSessionInfo.SessionId.ToString()); if (CreateArchivedSession(ArchivedSessionInfo)) { return ArchivedSessionInfo.SessionId; } } } return FGuid(); } bool FConcertServer::CreateArchivedSession(const FConcertSessionInfo& SessionInfo) { check(SessionInfo.SessionId.IsValid() && !SessionInfo.SessionName.IsEmpty()); check(!ArchivedSessions.Contains(SessionInfo.SessionId) && !GetArchivedSessionIdByName(SessionInfo.SessionName).IsValid()); ArchivedSessions.Add(SessionInfo.SessionId, SessionInfo); return EventSink->OnArchivedSessionCreated(*this, GetSessionSavedDir(SessionInfo.SessionId), SessionInfo); } bool FConcertServer::DestroyArchivedSession(const FGuid& ArchivedSessionId, const bool bDeleteSessionData) { if (ArchivedSessions.Contains(ArchivedSessionId)) { EventSink->OnArchivedSessionDestroyed(*this, ArchivedSessionId); ArchivedSessions.Remove(ArchivedSessionId); if (bDeleteSessionData) { ConcertUtil::DeleteDirectoryTree(*GetSessionSavedDir(ArchivedSessionId)); } return true; } return false; } TSharedPtr FConcertServer::RestoreArchivedSession(const FGuid& ArchivedSessionId, const FConcertSessionInfo& NewSessionInfo, const FConcertSessionFilter& SessionFilter, FText& OutFailureReason) { check(NewSessionInfo.SessionId.IsValid()); if (const FConcertSessionInfo* ArchivedSessionInfo = ArchivedSessions.Find(ArchivedSessionId)) { // Find the archived session repository to restore the session in the same one. const FConcertServerSessionRepository& ArchivedSessionRepository = GetSessionRepository(ArchivedSessionId); FString LiveSessionName = NewSessionInfo.SessionName; if (LiveSessionName.IsEmpty()) { LiveSessionName = ArchivedSessionInfo->SessionName; } { const FGuid LiveSessionId = GetLiveSessionIdByName(LiveSessionName); DestroyLiveSession(LiveSessionId, /*bDeleteSessionData*/true); } FConcertSessionInfo LiveSessionInfo = NewSessionInfo; // Detect restoring the same archived session twice by hashing archived ID LiveSessionInfo.SessionId = FGuid::NewGuid(); LiveSessionInfo.SessionName = MoveTemp(LiveSessionName); LiveSessionInfo.VersionInfos = ArchivedSessionInfo->VersionInfos; LiveSessionInfo.SetLastModifiedToNow(); // Ensure the new version is compatible with the old version, and append this new version if it is different to the last used version // Note: Older archived sessions didn't used to have any version info stored for them, and the version info may be missing completely when using -CONCERTIGNORE if (Settings->ServerSettings.bIgnoreSessionSettingsRestriction) { UE_CLOG(LiveSessionInfo.VersionInfos.Num() > 0, LogConcert, Warning, TEXT("Clearing version information when restoring session '%s' due to -CONCERTIGNORE. This may lead to instability and crashes!"), *NewSessionInfo.SessionName); LiveSessionInfo.VersionInfos.Reset(); } else if (NewSessionInfo.VersionInfos.Num() > 0) { check(NewSessionInfo.VersionInfos.Num() == 1); const FConcertSessionVersionInfo& NewVersionInfo = NewSessionInfo.VersionInfos[0]; if (LiveSessionInfo.VersionInfos.Num() > 0) { if (!LiveSessionInfo.VersionInfos.Last().Validate(NewVersionInfo, EConcertVersionValidationMode::Compatible, &OutFailureReason)) { UE_LOG(LogConcert, Error, TEXT("An attempt to restore session '%s' was rejected due to a versioning incompatibility: %s"), *NewSessionInfo.SessionName, *OutFailureReason.ToString()); return nullptr; } if (!LiveSessionInfo.VersionInfos.Last().Validate(NewVersionInfo, EConcertVersionValidationMode::Identical)) { LiveSessionInfo.VersionInfos.Add(NewVersionInfo); } } else { LiveSessionInfo.VersionInfos.Add(NewVersionInfo); } } // Restore the session in the default repository (where new sessions should be created), unless it is unset. const FConcertServerSessionRepository& RestoredSessionRepository = DefaultSessionRepository.IsSet() ? DefaultSessionRepository.GetValue() : ArchivedSessionRepository; if (EventSink->RestoreSession(*this, ArchivedSessionId, RestoredSessionRepository.GetSessionWorkingDir(LiveSessionInfo.SessionId), LiveSessionInfo, SessionFilter)) { UE_LOG(LogConcert, Display, TEXT("Archived session '%s' (%s) was restored as '%s' (%s)"), *ArchivedSessionInfo->SessionName, *ArchivedSessionInfo->SessionId.ToString(), *LiveSessionInfo.SessionName, *LiveSessionInfo.SessionId.ToString()); return CreateLiveSession(LiveSessionInfo, RestoredSessionRepository); } } OutFailureReason = LOCTEXT("Error_RestoreSession_FailedToCopy", "Could not copy session data from the archive"); return nullptr; } #undef LOCTEXT_NAMESPACE