1755 lines
80 KiB
C++
1755 lines
80 KiB
C++
// 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<FArchive> FileWriter = TUniquePtr<FArchive>(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<FArchive> FileReader = TUniquePtr<FArchive>(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<IConcertEndpointProvider>& 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<const UConcertServerConfig>(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<FConcertEndpointContext> 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<IConcertMessage>& 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<FConcertAdmin_DiscoverServersEvent>(this, &FConcertServer::HandleDiscoverServersEvent);
|
|
|
|
// Add Session connection handling
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_CreateSessionRequest, FConcertAdmin_SessionInfoResponse>(this, &FConcertServer::HandleCreateSessionRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_FindSessionRequest, FConcertAdmin_SessionInfoResponse>(this, &FConcertServer::HandleFindSessionRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_CopySessionRequest, FConcertAdmin_SessionInfoResponse>(this, &FConcertServer::HandleCopySessionRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_ArchiveSessionRequest, FConcertAdmin_ArchiveSessionResponse>(this, &FConcertServer::HandleArchiveSessionRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_RenameSessionRequest, FConcertAdmin_RenameSessionResponse>(this, &FConcertServer::HandleRenameSessionRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_DeleteSessionRequest, FConcertAdmin_DeleteSessionResponse>(this, &FConcertServer::HandleDeleteSessionRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_BatchDeleteSessionRequest, FConcertAdmin_BatchDeleteSessionResponse>(this, &FConcertServer::HandleBatchDeleteSessionRequest);
|
|
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_GetAllSessionsRequest, FConcertAdmin_GetAllSessionsResponse>(this, &FConcertServer::HandleGetAllSessionsRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_GetLiveSessionsRequest, FConcertAdmin_GetSessionsResponse>(this, &FConcertServer::HandleGetLiveSessionsRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_GetArchivedSessionsRequest, FConcertAdmin_GetSessionsResponse>(this, &FConcertServer::HandleGetArchivedSessionsRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_GetSessionClientsRequest, FConcertAdmin_GetSessionClientsResponse>(this, &FConcertServer::HandleGetSessionClientsRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_GetSessionActivitiesRequest, FConcertAdmin_GetSessionActivitiesResponse>(this, &FConcertServer::HandleGetSessionActivitiesRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_MountSessionRepositoryRequest, FConcertAdmin_MountSessionRepositoryResponse>(this, &FConcertServer::HandleMountSessionRepositoryRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_GetSessionRepositoriesRequest, FConcertAdmin_GetSessionRepositoriesResponse>(this, &FConcertServer::HandleGetSessionRepositoriesRequest);
|
|
ServerAdminEndpoint->RegisterRequestHandler<FConcertAdmin_DropSessionRepositoriesRequest, FConcertAdmin_DropSessionRepositoriesResponse>(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<FString> 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<FConcertAdmin_DiscoverServersEvent>();
|
|
|
|
// Session connection
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_CreateSessionRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_FindSessionRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_CopySessionRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_ArchiveSessionRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_RenameSessionRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_DeleteSessionRequest>();
|
|
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_GetAllSessionsRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_GetLiveSessionsRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_GetArchivedSessionsRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_GetSessionClientsRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_GetSessionActivitiesRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_MountSessionRepositoryRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_GetSessionRepositoriesRequest>();
|
|
ServerAdminEndpoint->UnregisterRequestHandler<FConcertAdmin_DropSessionRepositoriesRequest>();
|
|
|
|
ServerAdminEndpoint.Reset();
|
|
}
|
|
|
|
// Destroy the live sessions
|
|
{
|
|
TArray<FGuid> 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<FGuid> 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<IConcertServerSession> 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<IConcertServerSession> 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<IConcertServerSession> 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<IConcertServerSession> 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<FConcertSessionInfo> LiveSessionInfos;
|
|
TArray<FDateTime> 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<FConcertSessionInfo> ArchivedSessionInfos;
|
|
TArray<FDateTime> 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<int32, FDateTime> FSavedSessionInfo;
|
|
|
|
// Build the list of sorted session
|
|
TArray<FSavedSessionInfo> 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<FConcertSessionInfo> 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<FConcertSessionInfo>& SessionInfos, const TArray<FDateTime>& 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<FConcertSessionInfo> 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<FConcertSessionInfo> FConcertServer::GetLiveSessionInfos() const
|
|
{
|
|
TArray<FConcertSessionInfo> SessionsInfo;
|
|
SessionsInfo.Reserve(LiveSessions.Num());
|
|
for (auto& SessionPair : LiveSessions)
|
|
{
|
|
SessionsInfo.Add(SessionPair.Value->GetSessionInfo());
|
|
}
|
|
return SessionsInfo;
|
|
}
|
|
|
|
TArray<FConcertSessionInfo> FConcertServer::GetArchivedSessionInfos() const
|
|
{
|
|
TArray<FConcertSessionInfo> SessionsInfo;
|
|
SessionsInfo.Reserve(ArchivedSessions.Num());
|
|
for (auto& SessionPair : ArchivedSessions)
|
|
{
|
|
SessionsInfo.Add(SessionPair.Value);
|
|
}
|
|
return SessionsInfo;
|
|
}
|
|
|
|
TArray<TSharedPtr<IConcertServerSession>> FConcertServer::GetLiveSessions() const
|
|
{
|
|
TArray<TSharedPtr<IConcertServerSession>> SessionsArray;
|
|
SessionsArray.Reserve(LiveSessions.Num());
|
|
for (auto& SessionPair : LiveSessions)
|
|
{
|
|
SessionsArray.Add(SessionPair.Value);
|
|
}
|
|
return SessionsArray;
|
|
}
|
|
|
|
TSharedPtr<IConcertServerSession> FConcertServer::GetLiveSession(const FGuid& SessionId) const
|
|
{
|
|
return LiveSessions.FindRef(SessionId);
|
|
}
|
|
|
|
TOptional<FConcertSessionInfo> FConcertServer::GetArchivedSessionInfo(const FGuid& SessionId) const
|
|
{
|
|
const FConcertSessionInfo* SessionInfo = ArchivedSessions.Find(SessionId);
|
|
return SessionInfo ? *SessionInfo : TOptional<FConcertSessionInfo>{};
|
|
}
|
|
|
|
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<FGuid> 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<FGuid> 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<FConcertAdmin_DiscoverServersEvent>();
|
|
|
|
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<FConcertAdmin_MountSessionRepositoryResponse> FConcertServer::HandleMountSessionRepositoryRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_MountSessionRepositoryRequest* Message = Context.GetMessage<FConcertAdmin_MountSessionRepositoryRequest>();
|
|
|
|
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<FConcertAdmin_GetSessionRepositoriesResponse> 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<FConcertAdmin_DropSessionRepositoriesResponse> FConcertServer::HandleDropSessionRepositoriesRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_DropSessionRepositoriesRequest* Message = Context.GetMessage<FConcertAdmin_DropSessionRepositoriesRequest>();
|
|
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<FConcertAdmin_SessionInfoResponse> FConcertServer::HandleCreateSessionRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_CreateSessionRequest* Message = Context.GetMessage<FConcertAdmin_CreateSessionRequest>();
|
|
|
|
// Create a new server session
|
|
FText CreateFailureReason;
|
|
TSharedPtr<IConcertServerSession> 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<FConcertAdmin_SessionInfoResponse> FConcertServer::HandleFindSessionRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_FindSessionRequest* Message = Context.GetMessage<FConcertAdmin_FindSessionRequest>();
|
|
|
|
FConcertAdmin_SessionInfoResponse ResponseData;
|
|
|
|
// Find the session requested
|
|
TSharedPtr<IConcertServerSession> ServerSession = GetLiveSession(Message->SessionId);
|
|
const TCHAR* ServerSessionNamePtr = ServerSession ? *ServerSession->GetName() : TEXT("<unknown>");
|
|
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<FConcertAdmin_SessionInfoResponse> FConcertServer::HandleCopySessionRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_CopySessionRequest* Message = Context.GetMessage<FConcertAdmin_CopySessionRequest>();
|
|
|
|
// Restore the server session
|
|
FText FailureReason;
|
|
TSharedPtr<IConcertServerSession> 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<FConcertAdmin_ArchiveSessionResponse> FConcertServer::HandleArchiveSessionRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_ArchiveSessionRequest* Message = Context.GetMessage<FConcertAdmin_ArchiveSessionRequest>();
|
|
|
|
FConcertAdmin_ArchiveSessionResponse ResponseData;
|
|
|
|
// Find the session requested.
|
|
TSharedPtr<IConcertServerSession> ServerSession = GetLiveSession(Message->SessionId);
|
|
ResponseData.SessionId = Message->SessionId;
|
|
ResponseData.SessionName = ServerSession ? ServerSession->GetName() : TEXT("<unknown>");
|
|
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<FConcertAdmin_RenameSessionResponse> FConcertServer::HandleRenameSessionRequest(const FConcertMessageContext& Context)
|
|
{
|
|
return FConcertAdmin_RenameSessionResponse::AsFuture(RenameSessionInternal(*Context.GetMessage<FConcertAdmin_RenameSessionRequest>(), /*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<IConcertServerSession> 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<FConcertAdmin_BatchDeleteSessionResponse> FConcertServer::HandleBatchDeleteSessionRequest(const FConcertMessageContext& Context)
|
|
{
|
|
FConcertAdmin_BatchDeleteSessionResponse ResponseData;
|
|
ResponseData.ResponseCode = EConcertResponseCode::Failed;
|
|
|
|
const FConcertAdmin_BatchDeleteSessionRequest& Request = *Context.GetMessage<FConcertAdmin_BatchDeleteSessionRequest>();
|
|
TMap<FGuid, FString> 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<FGuid, FString>& PreparedSessionInfo) const
|
|
{
|
|
TArray<FDeletedSessionInfo> NotOwnedByClient;
|
|
for (const FGuid& SessionToDelete : Request.SessionIds)
|
|
{
|
|
if (TSharedPtr<IConcertServerSession> 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<FConcertAdmin_DeleteSessionResponse> FConcertServer::HandleDeleteSessionRequest(const FConcertMessageContext & Context)
|
|
{
|
|
return FConcertAdmin_DeleteSessionResponse::AsFuture(DeleteSessionInternal(*Context.GetMessage<FConcertAdmin_DeleteSessionRequest>(), /*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<IConcertServerSession> 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<FConcertAdmin_GetAllSessionsResponse> FConcertServer::HandleGetAllSessionsRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_GetAllSessionsRequest* Message = Context.GetMessage<FConcertAdmin_GetAllSessionsRequest>();
|
|
|
|
FConcertAdmin_GetAllSessionsResponse ResponseData;
|
|
ResponseData.LiveSessions = GetLiveSessionInfos();
|
|
for (const auto& ArchivedSessionPair : ArchivedSessions)
|
|
{
|
|
ResponseData.ArchivedSessions.Add(ArchivedSessionPair.Value);
|
|
}
|
|
|
|
return FConcertAdmin_GetAllSessionsResponse::AsFuture(MoveTemp(ResponseData));
|
|
}
|
|
|
|
TFuture<FConcertAdmin_GetSessionsResponse> FConcertServer::HandleGetLiveSessionsRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_GetLiveSessionsRequest* Message = Context.GetMessage<FConcertAdmin_GetLiveSessionsRequest>();
|
|
|
|
FConcertAdmin_GetSessionsResponse ResponseData;
|
|
ResponseData.Sessions = GetLiveSessionInfos();
|
|
|
|
return FConcertAdmin_GetSessionsResponse::AsFuture(MoveTemp(ResponseData));
|
|
}
|
|
|
|
TFuture<FConcertAdmin_GetSessionsResponse> 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<FConcertAdmin_GetSessionClientsResponse> FConcertServer::HandleGetSessionClientsRequest(const FConcertMessageContext& Context)
|
|
{
|
|
const FConcertAdmin_GetSessionClientsRequest* Message = Context.GetMessage<FConcertAdmin_GetSessionClientsRequest>();
|
|
|
|
FConcertAdmin_GetSessionClientsResponse ResponseData;
|
|
ResponseData.SessionClients = ConcertUtil::GetSessionClients(*this, Message->SessionId);
|
|
|
|
return FConcertAdmin_GetSessionClientsResponse::AsFuture(MoveTemp(ResponseData));
|
|
}
|
|
|
|
TFuture<FConcertAdmin_GetSessionActivitiesResponse> FConcertServer::HandleGetSessionActivitiesRequest(const FConcertMessageContext& Context)
|
|
{
|
|
FConcertAdmin_GetSessionActivitiesResponse ResponseData;
|
|
|
|
const FConcertAdmin_GetSessionActivitiesRequest* Message = Context.GetMessage<FConcertAdmin_GetSessionActivitiesRequest>();
|
|
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<IConcertServerSession>& 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<IConcertServerSession>& 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<IConcertServerSession> 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<IConcertLocalEndpoint> 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<IConcertMessage>& AckedMessage, const FConcertMessageContext& MessageContext)
|
|
{
|
|
OnConcertMessageAcknowledgementReceivedFromLocalEndpoint.Broadcast(LocalEndpoint, RemoteEndpoint, AckedMessage, MessageContext);
|
|
});
|
|
|
|
TSharedPtr<FConcertServerSession> LiveSession = MakeShared<FConcertServerSession>(
|
|
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<IConcertServerSession> 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<IConcertServerSession> 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<IConcertServerSession> 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
|