Files
UnrealEngineUWP/Engine/Plugins/Developer/Concert/ConcertMain/Source/ConcertClient/Private/ConcertClient.cpp

1462 lines
56 KiB
C++
Raw Normal View History

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ConcertClient.h"
#include "ConcertClientSession.h"
#include "ConcertMessages.h"
#include "ConcertUtil.h"
#include "ConcertLogGlobal.h"
#include "ConcertTransportEvents.h"
#include "ConcertClientSettings.h"
#include "Algo/Transform.h"
#include "Containers/Ticker.h"
#include "Misc/App.h"
#include "Misc/Paths.h"
#include "Misc/CoreDelegates.h"
#include "Misc/AsyncTaskNotification.h"
#include "HAL/FileManager.h"
#include "Stats/Stats.h"
#include "Runtime/Launch/Resources/Version.h"
#define LOCTEXT_NAMESPACE "ConcertClient"
LLM_DEFINE_TAG(Concert_ConcertClient);
namespace ConcertUtil
{
const FLogCategoryBase* GetLogConcertPtr()
{
#if NO_LOGGING
return nullptr;
#else
return &LogConcert;
#endif
}
// Connection Error code
constexpr uint32 CancelCode = 1;
constexpr uint32 ConectionAttemptAbortedErrorCode = 2;
constexpr uint32 ServerNotRespondingErrorCode = 3;
constexpr uint32 ServerErrorCode = 4;
}
class FConcertAutoConnection
{
public:
FConcertAutoConnection(FConcertClient* InClient, const UConcertClientConfig* InSettings)
: Client(InClient)
, Settings(InSettings)
{
// Make sure discovery is enabled on the client
Client->StartDiscovery();
Client->OnSessionConnectionChanged().AddRaw(this, &FConcertAutoConnection::HandleConnectionChanged);
Client->OnSessionStartup().AddRaw(this, &FConcertAutoConnection::HandleSessionStartup);
AutoConnectionNotification = MakeAutoConnectNotification();
AutoConnectionTickHandle = FTSTicker::GetCoreTicker().AddTicker(TEXT("ConcertAutoConnect"), 1, [this](float)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FConcertAutoConnection_Tick);
Tick();
return true;
});
}
~FConcertAutoConnection()
{
Client->StopDiscovery();
Client->OnSessionConnectionChanged().RemoveAll(this);
Client->OnSessionStartup().RemoveAll(this);
if (AutoConnectionTickHandle.IsValid())
{
FTSTicker::GetCoreTicker().RemoveTicker(AutoConnectionTickHandle);
AutoConnectionTickHandle.Reset();
}
if (AutoConnectionNotification) // Abort if it still ongoing.
{
AutoConnectionNotification->SetKeepOpenOnFailure(false); // Don't keep it open on abort.
AutoConnectionNotification->SetComplete(GetAutoConnectionCanceledMessage(), FText::GetEmpty(), false);
}
}
private:
FText GetAutoConnectionCanceledMessage() const
{
return FText::Format(LOCTEXT("AutoJoinSessionCanceled", "Connection to Session '{0}' Canceled."), FText::AsCultureInvariant(Settings->DefaultSessionName));
}
TUniquePtr<FAsyncTaskNotification> MakeAutoConnectNotification()
{
FAsyncTaskNotificationConfig NotificationConfig;
NotificationConfig.TitleText = FText::Format(LOCTEXT("AutoJoinSession", "Joining Session '{0}' on '{1}'..."), FText::AsCultureInvariant(Settings->DefaultSessionName), FText::AsCultureInvariant((Settings->DefaultServerURL)));
NotificationConfig.ProgressText = FText::Format(LOCTEXT("LookingForServer", "Looking for Server '{0}'..."), FText::AsCultureInvariant((Settings->DefaultServerURL)));
NotificationConfig.bIsHeadless = Settings->bIsHeadless;
NotificationConfig.bCanCancel = true;
NotificationConfig.LogCategory = ConcertUtil::GetLogConcertPtr();
return MakeUnique<FAsyncTaskNotification>(NotificationConfig);
}
void SetAsyncNotificationComplete(const FText& Msg, bool bSucceeded)
{
if (AutoConnectionNotification.IsValid())
{
AutoConnectionNotification->SetComplete(Msg, FText::GetEmpty(), bSucceeded);
AutoConnectionNotification.Reset();
}
}
void Tick()
{
// Already connected
if (IsConnected())
{
// Once connected if we aren't in auto connection mode, shut ourselves down
if (!Settings->bAutoConnect)
{
Client->StopAutoConnect(); // Indirect self-destruct.
}
return;
}
// Should cancel request?
if (AutoConnectionNotification.IsValid() && AutoConnectionNotification->GetPromptAction() == EAsyncTaskNotificationPromptAction::Cancel)
{
SetAsyncNotificationComplete(GetAutoConnectionCanceledMessage(), false);
Client->StopAutoConnect(); // Indirect self-destruct.
return;
}
// A create or join request is ongoing.
if (OngoingConnectionRequest.IsValid())
{
if (OngoingConnectionRequest.IsReady())
{
TSharedFuture<EConcertResponseCode> SessionJoined = OngoingConnectionRequest.Get();
if (SessionJoined.IsReady())
{
const EConcertResponseCode RequestResponseCode = SessionJoined.Get();
if (RequestResponseCode != EConcertResponseCode::Success)
{
// If last attempt failed, stop trying if auto-connect was turned off or if we are not allowed to retry on error.
if (RequestResponseCode == EConcertResponseCode::Failed && (!Settings->bAutoConnect || !Settings->bRetryAutoConnectOnError))
{
check(!AutoConnectionNotification.IsValid()); // If OngoingConnectionRequest is valid, the notification ownership was transfered to it.
Client->StopAutoConnect(); // Indirect self-destruct.
return;
}
}
// This ongoing request has completed, reset to retry later.
OngoingConnectionRequest = TFuture<TSharedFuture<EConcertResponseCode>>();
}
}
return;
}
// Ensure to display an async notification.
if (!AutoConnectionNotification.IsValid()) // Invalid if ownership was transfered to a failed create/join or if the session was joined and server went down.
{
AutoConnectionNotification = MakeAutoConnectNotification();
}
check(!IsConnecting()); // If it fails -> most likely because a 'cancel' occurred while connecting, but the cancel did not 'disconnect' the local session before the ongoing connection promise value was set.
// Clear our current session before initiating a new connection request
CurrentSession.Reset();
// Try to create or/and join the session.
for (const FConcertServerInfo& ServerInfo : Client->GetKnownServers())
{
if (ServerInfo.ServerName == Settings->DefaultServerURL)
{
CreateOrJoinDefaultSession(ServerInfo);
break; // Can only have one default server.
}
}
}
bool IsConnected() const
{
return CurrentSession.IsValid() ? CurrentSession.Pin()->GetConnectionStatus() == EConcertConnectionStatus::Connected : false;
}
bool IsConnecting() const
{
return CurrentSession.IsValid() ? CurrentSession.Pin()->GetConnectionStatus() == EConcertConnectionStatus::Connecting : false;
}
void CreateOrJoinDefaultSession(const FConcertServerInfo& ServerInfo)
{
// Prevents TFuture execution if this class gets deleted before TFuture executes (and dismisses previous execution if any).
AsyncRequestExecutionGuard = MakeShared<uint8>();
TWeakPtr<uint8> AsyncRequestExecutionToken = AsyncRequestExecutionGuard;
// Get the Server sessions list
OngoingConnectionRequest = Client->GetServerSessions(ServerInfo.AdminEndpointId)
.Next([LocalSettings = Settings](FConcertAdmin_GetAllSessionsResponse Response)
{
FGuid DefaultSessionId;
FGuid DefaultSessionToRestoreId;
if (Response.ResponseCode == EConcertResponseCode::Success)
{
// Find our default session IDs
for (const FConcertSessionInfo& SessionInfo : Response.LiveSessions)
{
if (SessionInfo.SessionName == LocalSettings->DefaultSessionName)
{
DefaultSessionId = SessionInfo.SessionId;
break;
}
}
for (const FConcertSessionInfo& SessionInfo : Response.ArchivedSessions)
{
if (SessionInfo.SessionName == LocalSettings->DefaultSessionToRestore)
{
DefaultSessionToRestoreId = SessionInfo.SessionId;
break;
}
}
}
return MakeTuple(Response.ResponseCode, DefaultSessionId, DefaultSessionToRestoreId);
})
.Next([this, AsyncRequestExecutionToken, ServerEndpoint = ServerInfo.AdminEndpointId](TTuple<EConcertResponseCode, FGuid, FGuid> RequestLiveSessionTuple)
{
const EConcertResponseCode OuterResponseCode = RequestLiveSessionTuple.Get<0>();
// The request was successful and execution not dismissed. (and 'this' is also valid)
if (OuterResponseCode == EConcertResponseCode::Success && AsyncRequestExecutionToken.IsValid())
{
const FGuid DefaultSessionId = RequestLiveSessionTuple.Get<1>();
const FGuid DefaultSessionToRestoreId = RequestLiveSessionTuple.Get<2>();
// We found the default session, join it.
if (DefaultSessionId.IsValid())
{
return Client->InternalJoinSession(ServerEndpoint, DefaultSessionId, MoveTemp(AutoConnectionNotification)).Share();
}
// We found the default session to restore, restore and join it.
if (DefaultSessionToRestoreId.IsValid())
{
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
FConcertCopySessionArgs RestoreSessionArgs;
RestoreSessionArgs.bAutoConnect = true;
RestoreSessionArgs.SessionId = DefaultSessionToRestoreId;
RestoreSessionArgs.SessionName = Settings->DefaultSessionName;
RestoreSessionArgs.ArchiveNameOverride = Settings->DefaultSaveSessionAs;
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
return Client->InternalCopySession(ServerEndpoint, RestoreSessionArgs, /*bRestoreOnlyConstraint*/true, MoveTemp(AutoConnectionNotification)).Share();
}
// No session found to join or restore, so create a new one.
{
FConcertCreateSessionArgs CreateSessionArgs;
CreateSessionArgs.SessionName = Settings->DefaultSessionName;
CreateSessionArgs.ArchiveNameOverride = Settings->DefaultSaveSessionAs;
return Client->InternalCreateSession(ServerEndpoint, CreateSessionArgs, MoveTemp(AutoConnectionNotification)).Share();
}
}
else // Request failed, was canceled or the captured 'this' was deleted.
{
return MakeFulfilledPromise<EConcertResponseCode>(OuterResponseCode == EConcertResponseCode::Success ? EConcertResponseCode::Failed : OuterResponseCode).GetFuture().Share();
}
});
}
void HandleConnectionChanged(IConcertClientSession& InSession, EConcertConnectionStatus ConnectionStatus)
{
// Once we get connected or disconnected, clear our ongoing request if we have one, if it comes from our current session
if (CurrentSession.IsValid()
&& CurrentSession.Pin().Get() == &InSession
&& (ConnectionStatus == EConcertConnectionStatus::Connected || ConnectionStatus == EConcertConnectionStatus::Disconnected))
{
OngoingConnectionRequest = TFuture<TSharedFuture<EConcertResponseCode>>();
}
}
void HandleSessionStartup(TSharedRef<IConcertClientSession> InSession)
{
CurrentSession = InSession;
}
TFuture<TSharedFuture<EConcertResponseCode>> OngoingConnectionRequest;
FTSTicker::FDelegateHandle AutoConnectionTickHandle;
FConcertClient* Client;
TWeakPtr<IConcertClientSession> CurrentSession;
const UConcertClientConfig* Settings;
TUniquePtr<FAsyncTaskNotification> AutoConnectionNotification;
TSharedPtr<uint8> AsyncRequestExecutionGuard;
};
/** Runs a set of tasks required to join a session. */
class FConcertPendingConnection : public TSharedFromThis<FConcertPendingConnection>
{
public:
struct FConfig
{
TAttribute<FText> PendingTitleText;
TAttribute<FText> SuccessTitleText;
TAttribute<FText> FailureTitleText;
TAttribute<bool> KeepNotificationOpenOnError;
bool bIsAutoConnection = false;
};
FConcertPendingConnection(FConcertClient* InClient, const FConfig& InConfig)
: Client(InClient)
, Config(InConfig)
{
}
~FConcertPendingConnection()
{
if (ConnectionTick.IsValid())
{
FTSTicker::GetCoreTicker().RemoveTicker(ConnectionTick);
}
if (ConnectionTasks.Num() > 0)
{
// Don't keep the notification open if canceled/aborted.
Notification->SetKeepOpenOnFailure(false);
// Abort the executing tasks.
ConnectionTasks[0]->Abort();
// Clear the tasks, set the notification text and fulfill the 'Execute()' promise.
SetResult(EConcertResponseCode::Failed, GetCanceledError());
}
}
/** Execute the connection. When the future is ready, the client is whether connected or not. The task may be canceled at anytime. */
TFuture<EConcertResponseCode> Execute(TArray<TUniquePtr<IConcertClientConnectionTask>>&& InConnectionTasks, TUniquePtr<FAsyncTaskNotification> OngoingNotification)
{
checkf(ConnectionTasks.Num() == 0, TEXT("Execute has already been called!"));
ConnectionTasks = MoveTemp(InConnectionTasks);
checkf(ConnectionTasks.Num() != 0, TEXT("Execute was not given any tasks!"));
// Set-up the task notification, continuing the ongoing one if any (like the auto connection one)
Notification = MoveTemp(OngoingNotification);
if (!Notification.IsValid())
{
FAsyncTaskNotificationConfig NotificationConfig;
NotificationConfig.TitleText = Config.PendingTitleText.Get(FText::GetEmpty());
NotificationConfig.bIsHeadless = Client->GetConfiguration()->bIsHeadless;
NotificationConfig.LogCategory = ConcertUtil::GetLogConcertPtr();
Notification = MakeUnique<FAsyncTaskNotification>(NotificationConfig);
}
Notification->SetCanCancel(TAttribute<bool>(this, &FConcertPendingConnection::CanCancel));
Notification->SetKeepOpenOnFailure(!Config.bIsAutoConnection);
Notification->SetProgressText(ConnectionTasks[0]->GetDescription());
ConnectionTasks[0]->Execute();
ConnectionTick = FTSTicker::GetCoreTicker().AddTicker(TEXT("ConcertPendingConnection"), 0.1f, [this](float)
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_FConcertPendingConnection_Tick);
Tick();
return true;
});
return ConnectionResult.GetFuture();
}
private:
static FConcertConnectionError GetCanceledError()
{
return FConcertConnectionError{ ConcertUtil::CancelCode, LOCTEXT("ConnectionProcessCanceled", "Connection Process Canceled") };
}
bool CanCancel() const
{
return ConnectionTasks.Num() > 0 && ConnectionTasks[0]->CanCancel();
}
void Tick()
{
auto GetTaskAction = [](EAsyncTaskNotificationPromptAction InPromptAction) ->EConcertConnectionTaskAction
{
switch (InPromptAction)
{
case EAsyncTaskNotificationPromptAction::None:
return EConcertConnectionTaskAction::None;
case EAsyncTaskNotificationPromptAction::Cancel:
return EConcertConnectionTaskAction::Cancel;
case EAsyncTaskNotificationPromptAction::Continue:
return EConcertConnectionTaskAction::Continue;
// unattended case resolve as a continue
default:
return EConcertConnectionTaskAction::Continue;
}
};
// We should only Tick while we have tasks to process
check(ConnectionTasks.Num() > 0);
EAsyncTaskNotificationPromptAction PromptAction = Notification->GetPromptAction();
EConcertConnectionTaskAction TaskAction = GetTaskAction(PromptAction);
const bool bCanceled = TaskAction == EConcertConnectionTaskAction::Cancel;
EConcertResponseCode TaskStatus = ConnectionTasks[0]->GetStatus();
if (bCanceled)
{
if (TaskStatus == EConcertResponseCode::Pending)
{
ConnectionTasks[0]->Tick(TaskAction); // Give it a last tick to give it a chance to cancel cleanly.
}
SetResultAndDelete(EConcertResponseCode::Failed, bCanceled, GetCanceledError()); // Cancelation has priority over other possible errors (it could hide some failure)
return; // Do not use 'this' anymore, it was deleted above.
}
// Update the current task
switch (TaskStatus)
{
// Pending state - update the task
case EConcertResponseCode::Pending:
ConnectionTasks[0]->Tick(TaskAction);
return;
// Prompt state - wait for user action to either proceed (success) or stop (fail)
case EConcertResponseCode::InvalidRequest:
{
FAsyncNotificationStateData StateData(Config.PendingTitleText.Get(FText::GetEmpty()), ConnectionTasks[0]->GetError().ErrorText, EAsyncTaskNotificationState::Prompt);
StateData.PromptText = ConnectionTasks[0]->GetPrompt();
StateData.HyperlinkText = LOCTEXT("PendingConnectionFailureDetails", "See Details...");
StateData.Hyperlink = ConnectionTasks[0]->GetErrorDelegate();
Notification->SetNotificationState(StateData);
ConnectionTasks[0]->Tick(TaskAction);
return;
}
// Success state - move on to the next task
case EConcertResponseCode::Success:
ConnectionTasks.RemoveAt(0, EAllowShrinking::No);
if (ConnectionTasks.Num() > 0)
{
Notification->SetNotificationState(FAsyncNotificationStateData(Config.PendingTitleText.Get(FText::GetEmpty()), ConnectionTasks[0]->GetDescription(), EAsyncTaskNotificationState::Pending));
ConnectionTasks[0]->Execute();
}
else
{
// Processed everything without error
SetResultAndDelete(EConcertResponseCode::Success, bCanceled); // do not use 'this' after this call!
}
return;
// Error state - fail the connection
default:
SetResultAndDelete(TaskStatus, bCanceled, ConnectionTasks[0]->GetError(), ConnectionTasks[0]->GetErrorDelegate()); // do not use 'this' after this call!
return;
}
}
/** Set the result */
void SetResult(const EConcertResponseCode InResult, const FConcertConnectionError InError = FConcertConnectionError(), const FSimpleDelegate& InErrorDelegate = FSimpleDelegate())
{
if (InResult == EConcertResponseCode::Success)
{
Notification->SetComplete(Config.SuccessTitleText.Get(FText::GetEmpty()), FText(), /*bSuccess*/true);
}
else
{
Client->InternalDisconnectSession();
if (InResult == EConcertResponseCode::Failed)
{
Notification->SetKeepOpenOnFailure(Config.KeepNotificationOpenOnError);
}
Notification->SetHyperlink(InErrorDelegate, LOCTEXT("PendingConnectionFailureDetails", "See Details..."));
Notification->SetComplete(Config.FailureTitleText.Get(FText::GetEmpty()), InError.ErrorText, /*bSuccess*/false);
}
ConnectionTasks.Reset();
ConnectionResult.SetValue(InResult);
Client->SetLastConnectionError(InError);
}
/** Set the result and delete ourself - 'this' will be garbage after calling this function! */
void SetResultAndDelete(const EConcertResponseCode InResult, bool bWasCanceled, const FConcertConnectionError InError = FConcertConnectionError(), const FSimpleDelegate& InErrorDelegate = FSimpleDelegate())
{
// Set the result and delete ourself
SetResult(InResult, InError, InErrorDelegate);
check(Client->PendingConnection.Get() == this);
// if the connection was canceled, also cancel the auto connection, so it won't retry on failure
if (bWasCanceled)
{
Client->AutoConnection.Reset();
}
Client->PendingConnection.Reset();
}
FConcertClient* Client;
FConfig Config;
FTSTicker::FDelegateHandle ConnectionTick;
TPromise<EConcertResponseCode> ConnectionResult;
TUniquePtr<FAsyncTaskNotification> Notification;
TArray<TUniquePtr<IConcertClientConnectionTask>> ConnectionTasks;
};
template <typename RequestType>
class TConcertClientConnectionRequestTask : public IConcertClientConnectionTask
{
public:
TConcertClientConnectionRequestTask(FConcertClient* InClient, RequestType&& InRequest, const FGuid& InServerAdminEndpointId)
: Client(InClient)
, Request(MoveTemp(InRequest))
, ServerAdminEndpointId(InServerAdminEndpointId)
, AsyncRequestExecutionGuard(MakeShared<uint8>(0))
{
}
virtual void Abort() override
{
Result.Reset();
}
virtual void Tick(EConcertConnectionTaskAction TaskAction) override
{
}
virtual bool CanCancel() const override
{
return true;
}
virtual EConcertResponseCode GetStatus() const override
{
if (Result.IsValid())
{
if (!Result.IsReady())
{
return EConcertResponseCode::Pending;
}
TSharedFuture<EConcertResponseCode> SessionJoined = Result.Get();
if (SessionJoined.IsValid())
{
return SessionJoined.IsReady() ? SessionJoined.Get() : EConcertResponseCode::Pending;
}
}
return EConcertResponseCode::Failed;
}
virtual FText GetPrompt() const override
{
// client connection task have no prompt
return FText::GetEmpty();
}
virtual FConcertConnectionError GetError() const override
{
return Result.IsValid() ? ConnectionError : FConcertConnectionError{ConcertUtil::ConectionAttemptAbortedErrorCode ,LOCTEXT("RemoteConnectionAttemptAborted", "Remote Connection Attempt Aborted.") };
}
virtual FSimpleDelegate GetErrorDelegate() const override
{
return FSimpleDelegate();
}
virtual FText GetDescription() const override
{
return LOCTEXT("AttemptingRemoteConnection", "Attempting Remote Connection...");
}
static FConcertConnectionError GetServerNotRespondingErrorMessage()
{
return FConcertConnectionError{ ConcertUtil::ServerNotRespondingErrorCode, LOCTEXT("JoinTask_ServerNotResponding", "Server Not Responding") };
}
protected:
FConcertClient* Client;
RequestType Request;
FGuid ServerAdminEndpointId;
TFuture<TSharedFuture<EConcertResponseCode>> Result;
FConcertConnectionError ConnectionError;
TSharedPtr<uint8> AsyncRequestExecutionGuard;
};
class FConcertClientJoinSessionTask : public TConcertClientConnectionRequestTask<FConcertAdmin_FindSessionRequest>
{
public:
FConcertClientJoinSessionTask(FConcertClient* InClient, FConcertAdmin_FindSessionRequest&& InRequest, const FGuid& InServerAdminEndpointId, TSharedRef<FText> OutResolvedSessionName)
: TConcertClientConnectionRequestTask(InClient, MoveTemp(InRequest), InServerAdminEndpointId)
, ResolvedSessionName(MoveTemp(OutResolvedSessionName))
{
}
virtual void Execute() override
{
TWeakPtr<uint8> AsyncExecutionToken = AsyncRequestExecutionGuard;
Result = Client->ClientAdminEndpoint->SendRequest<FConcertAdmin_FindSessionRequest, FConcertAdmin_SessionInfoResponse>(Request, ServerAdminEndpointId)
.Next([this, AsyncExecutionToken](const FConcertAdmin_SessionInfoResponse& SessionInfoResponse)
{
if (!AsyncExecutionToken.IsValid()) // The task was canceled/aborted and the object deleted. Don't care about the return value/error.
{
return MakeFulfilledPromise<EConcertResponseCode>(EConcertResponseCode::Failed).GetFuture().Share();
}
*ResolvedSessionName = FText::AsCultureInvariant(SessionInfoResponse.SessionInfo.SessionName);
if (SessionInfoResponse.ResponseCode == EConcertResponseCode::Success)
{
// If CreateClientSession() returns a failure, it is because the server did not reply to the 'join session' event and the endpoint timed out or the connection was canceled/aborted (for which there is a special message already).
ConnectionError = GetServerNotRespondingErrorMessage();
return Client->CreateClientSession(SessionInfoResponse.SessionInfo).Share();
}
else
{
ConnectionError.ErrorCode = ConcertUtil::ServerErrorCode;
ConnectionError.ErrorText = SessionInfoResponse.Reason;
return MakeFulfilledPromise<EConcertResponseCode>(SessionInfoResponse.ResponseCode).GetFuture().Share();
}
});
}
TSharedRef<FText> ResolvedSessionName;
};
class FConcertClientCreateSessionTask : public TConcertClientConnectionRequestTask<FConcertAdmin_CreateSessionRequest>
{
public:
FConcertClientCreateSessionTask(FConcertClient* InClient, FConcertAdmin_CreateSessionRequest&& InRequest, const FGuid& InServerAdminEndpointId)
: TConcertClientConnectionRequestTask(InClient, MoveTemp(InRequest), InServerAdminEndpointId)
{
}
virtual void Execute() override
{
TWeakPtr<uint8> AsyncExecutionToken = AsyncRequestExecutionGuard;
Result = Client->ClientAdminEndpoint->SendRequest<FConcertAdmin_CreateSessionRequest, FConcertAdmin_SessionInfoResponse>(Request, ServerAdminEndpointId)
.Next([this, AsyncExecutionToken](const FConcertAdmin_SessionInfoResponse& SessionInfoResponse)
{
if (!AsyncExecutionToken.IsValid()) // The task was canceled/aborted and the object deleted. Don't care about the return value/error.
{
return MakeFulfilledPromise<EConcertResponseCode>(EConcertResponseCode::Failed).GetFuture().Share();
}
else if (SessionInfoResponse.ResponseCode == EConcertResponseCode::Success)
{
// If CreateClientSession() returns a failure, it is because the server did not reply to the 'join session' event and the endpoint timed out or the connection was canceled/aborted (for which there is a special message already).
ConnectionError = GetServerNotRespondingErrorMessage();
return Client->CreateClientSession(SessionInfoResponse.SessionInfo).Share();
}
else
{
ConnectionError.ErrorCode = ConcertUtil::ServerErrorCode;
ConnectionError.ErrorText = SessionInfoResponse.Reason;
return MakeFulfilledPromise<EConcertResponseCode>(SessionInfoResponse.ResponseCode).GetFuture().Share();
}
});
}
};
FConcertClientPaths::FConcertClientPaths(const FString& InRole)
: WorkingDir(FPaths::ProjectIntermediateDir() / TEXT("Concert") / InRole / FApp::GetInstanceId().ToString())
{
}
FConcertClient::FConcertClient(const FString& InRole, const TSharedPtr<IConcertEndpointProvider>& InEndpointProvider)
: Role(InRole)
, Paths(InRole)
, EndpointProvider(InEndpointProvider)
, DiscoveryCount(0)
, bClientSessionPendingDestroy(false)
{
}
FConcertClient::~FConcertClient()
{
// if the ClientAdminEndpoint is valid, Shutdown wasn't called
check(!ClientAdminEndpoint.IsValid());
}
const FString& FConcertClient::GetRole() const
{
return Role;
}
void FConcertClient::Configure(const UConcertClientConfig* InSettings)
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
ClientInfo.Initialize();
check(InSettings != nullptr);
Settings = TStrongObjectPtr<const UConcertClientConfig>(InSettings);
// Set the display name from the settings or default to username (i.e. app session owner)
ClientInfo.DisplayName = Settings->ClientSettings.DisplayName.IsEmpty() ? ClientInfo.UserName : Settings->ClientSettings.DisplayName;
ClientInfo.AvatarColor = Settings->ClientSettings.AvatarColor;
ClientInfo.DesktopAvatarActorClass = Settings->ClientSettings.DesktopAvatarActorClass.ToString();
ClientInfo.VRAvatarActorClass = Settings->ClientSettings.VRAvatarActorClass.ToString();
ClientInfo.Tags = Settings->ClientSettings.Tags;
}
bool FConcertClient::IsConfigured() const
{
// if the instance id hasn't been set yet, then Configure wasn't called.
return Settings && ClientInfo.InstanceInfo.InstanceId.IsValid();
}
const UConcertClientConfig* FConcertClient::GetConfiguration() const
{
return Settings.Get();
}
const FConcertClientInfo& FConcertClient::GetClientInfo() const
{
// NOTE: The 'ClientSession->ClientInfo'can dynamically be updated during the session, e.g. avatar class can change to reflect the client state: PIE, VR, etc.
// The 'this->ClientInfo' member change when the Configure() function is called (when the settings panel is changed).
// IConcertSessionClient and IConcertClient must return the same client info to be consistent.
return ClientSession ? ClientSession->GetLocalClientInfo() : ClientInfo;
}
bool FConcertClient::IsStarted() const
{
return ClientAdminEndpoint.IsValid();
}
void FConcertClient::Startup()
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
check(IsConfigured());
if (!ClientAdminEndpoint.IsValid() && EndpointProvider.IsValid())
{
// Create the client administration endpoint
ClientAdminEndpoint = EndpointProvider->CreateLocalEndpoint(TEXT("Admin"), Settings->EndpointSettings, [this](const FConcertEndpointContext& Context)
{
return ConcertUtil::CreateLogger(Context, [this](const FConcertLog& Log)
{
ConcertTransportEvents::OnConcertClientLogEvent().Broadcast(*this, Log);
});
});
}
FCoreDelegates::OnEndFrame.AddRaw(this, &FConcertClient::OnEndFrame);
}
void FConcertClient::Shutdown()
{
FCoreDelegates::OnEndFrame.RemoveAll(this);
// Remove Auto Connection routine, if any
AutoConnection.Reset();
while (IsDiscoveryEnabled())
{
StopDiscovery();
}
ClientAdminEndpoint.Reset();
KnownServers.Empty();
if (ClientSession.IsValid())
{
ClientSession->Disconnect();
OnSessionShutdownDelegate.Broadcast(ClientSession.ToSharedRef());
ClientSession->Shutdown();
ClientSession.Reset();
}
// Clear the working directory for this instance
ConcertUtil::DeleteDirectoryTree(*Paths.GetWorkingDir());
}
bool FConcertClient::IsDiscoveryEnabled() const
{
return DiscoveryCount > 0;
}
void FConcertClient::StartDiscovery()
{
++DiscoveryCount;
if (ClientAdminEndpoint.IsValid() && !DiscoveryTick.IsValid())
{
ClientAdminEndpoint->RegisterEventHandler<FConcertAdmin_ServerDiscoveredEvent>(this, &FConcertClient::HandleServerDiscoveryEvent);
DiscoveryTick = FTSTicker::GetCoreTicker().AddTicker(TEXT("Discovery"), 1, [this](float DeltaSeconds) {
QUICK_SCOPE_CYCLE_COUNTER(STAT_FConcertClient_Discovery_Tick);
const FDateTime UtcNow = FDateTime::UtcNow();
SendDiscoverServersEvent();
TimeoutDiscovery(UtcNow);
return true;
});
}
}
void FConcertClient::StopDiscovery()
{
check(IsDiscoveryEnabled());
--DiscoveryCount;
if (DiscoveryCount > 0)
{
return;
}
if (ClientAdminEndpoint.IsValid())
{
ClientAdminEndpoint->UnregisterEventHandler<FConcertAdmin_ServerDiscoveredEvent>();
}
if (DiscoveryTick.IsValid())
{
FTSTicker::GetCoreTicker().RemoveTicker(DiscoveryTick);
DiscoveryTick.Reset();
}
}
bool FConcertClient::CanAutoConnect() const
{
return IsConfigured() && Settings && !Settings->DefaultServerURL.IsEmpty() && !Settings->DefaultSessionName.IsEmpty();
}
bool FConcertClient::IsAutoConnecting() const
{
return AutoConnection.IsValid();
}
void FConcertClient::StartAutoConnect()
{
check(IsStarted());
if (AutoConnection.IsValid())
{
return;
}
if (CanAutoConnect())
{
// Cancel the in-progress connection process if any, (even if it was connecting to the default session) to connect to the default session instead.
PendingConnection.Reset();
AutoConnection = MakeUnique<FConcertAutoConnection>(this, Settings.Get());
}
}
void FConcertClient::StopAutoConnect()
{
if (IsAutoConnecting())
{
// Cancel the in-progress auto-connection process (if any).
PendingConnection.Reset();
}
AutoConnection.Reset();
}
FConcertConnectionError FConcertClient::GetLastConnectionError() const
{
return LastConnectionError;
}
TArray<FConcertServerInfo> FConcertClient::GetKnownServers() const
{
TArray<FConcertServerInfo> ServerArray;
ServerArray.Empty(KnownServers.Num());
for (const auto& Server : KnownServers)
{
ServerArray.Emplace(Server.Value.ServerInfo);
}
return ServerArray;
}
FSimpleMulticastDelegate& FConcertClient::OnKnownServersUpdated()
{
return ServersUpdatedDelegate;
}
FOnConcertClientSessionStartupOrShutdown& FConcertClient::OnSessionStartup()
{
return OnSessionStartupDelegate;
}
FOnConcertClientSessionStartupOrShutdown& FConcertClient::OnSessionShutdown()
{
return OnSessionShutdownDelegate;
}
FOnConcertClientSessionGetPreConnectionTasks& FConcertClient::OnGetPreConnectionTasks()
{
return OnGetPreConnectionTasksDelegate;
}
FOnConcertClientSessionConnectionChanged& FConcertClient::OnSessionConnectionChanged()
{
return OnSessionConnectionChangedDelegate;
}
EConcertConnectionStatus FConcertClient::GetSessionConnectionStatus() const
{
return ClientSession.IsValid() ? ClientSession->GetConnectionStatus() : EConcertConnectionStatus::Disconnected;
}
TFuture<EConcertResponseCode> FConcertClient::CreateSession(const FGuid& ServerAdminEndpointId, const FConcertCreateSessionArgs& CreateSessionArgs)
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
// We don't want the client to get automatically reconnected to it's default session if something wrong happens
AutoConnection.Reset();
return InternalCreateSession(ServerAdminEndpointId, CreateSessionArgs);
}
TFuture<EConcertResponseCode> FConcertClient::JoinSession(const FGuid& ServerAdminEndpointId, const FGuid& SessionId)
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
// We don't want the client to get automatically reconnected to it's default session if something wrong happens
AutoConnection.Reset();
return InternalJoinSession(ServerAdminEndpointId, SessionId);
}
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
TFuture<EConcertResponseCode> FConcertClient::RestoreSession(const FGuid& ServerAdminEndpointId, const FConcertCopySessionArgs& RestoreSessionArgs)
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
// We don't want the client to get automatically reconnected to the default session if something wrong happens
if (RestoreSessionArgs.bAutoConnect)
{
AutoConnection.Reset();
}
return InternalCopySession(ServerAdminEndpointId, RestoreSessionArgs, /*bRestoreOnlyConstraint*/true);
}
TFuture<EConcertResponseCode> FConcertClient::CopySession(const FGuid& ServerAdminEndpointId, const FConcertCopySessionArgs& CopySessionArgs)
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
// We don't want the client to get automatically reconnected to the default session if the copy/connect fails.
if (CopySessionArgs.bAutoConnect)
{
AutoConnection.Reset();
}
return InternalCopySession(ServerAdminEndpointId, CopySessionArgs, /*bRestoreOnlyConstraint*/false);
}
TFuture<EConcertResponseCode> FConcertClient::ArchiveSession(const FGuid& ServerAdminEndpointId, const FConcertArchiveSessionArgs& ArchiveSessionArgs)
{
FConcertAdmin_ArchiveSessionRequest ArchiveSessionRequest;
ArchiveSessionRequest.SessionId = ArchiveSessionArgs.SessionId;
ArchiveSessionRequest.ArchiveNameOverride = ArchiveSessionArgs.ArchiveNameOverride;
ArchiveSessionRequest.SessionFilter = ArchiveSessionArgs.SessionFilter;
// Fill the information for the client identification
ArchiveSessionRequest.UserName = ClientInfo.UserName;
ArchiveSessionRequest.DeviceName = ClientInfo.DeviceName;
FAsyncTaskNotificationConfig NotificationConfig;
NotificationConfig.bIsHeadless = Settings->bIsHeadless;
NotificationConfig.bKeepOpenOnFailure = true;
NotificationConfig.TitleText = LOCTEXT("ArchivingSession", "Archiving Session...");
NotificationConfig.LogCategory = ConcertUtil::GetLogConcertPtr();
FAsyncTaskNotification Notification(NotificationConfig);
return ClientAdminEndpoint->SendRequest<FConcertAdmin_ArchiveSessionRequest, FConcertAdmin_ArchiveSessionResponse>(ArchiveSessionRequest, ServerAdminEndpointId)
.Next([this, Notification = MoveTemp(Notification)](const FConcertAdmin_ArchiveSessionResponse& RequestResponse) mutable
{
if (RequestResponse.ResponseCode == EConcertResponseCode::Success)
{
Notification.SetComplete(FText::Format(LOCTEXT("ArchivedSessionFmt", "Archived Session '{0}' as '{1}"), FText::FromString(RequestResponse.SessionName), FText::FromString(RequestResponse.ArchiveName)), FText(), true);
}
else
{
Notification.SetComplete(FText::Format(LOCTEXT("FailedToArchiveSessionFmt", "Failed to Archive Session '{0}'"), FText::FromString(RequestResponse.SessionName)), RequestResponse.Reason, false);
}
return RequestResponse.ResponseCode;
});
}
TFuture<EConcertResponseCode> FConcertClient::RenameSession(const FGuid& ServerAdminEndpointId, const FGuid& SessionId, const FString& NewName)
{
FConcertAdmin_RenameSessionRequest RenameSessionRequest;
RenameSessionRequest.SessionId = SessionId;
RenameSessionRequest.NewName = NewName;
// Fill the information for the client identification
RenameSessionRequest.UserName = ClientInfo.UserName;
RenameSessionRequest.DeviceName = ClientInfo.DeviceName;
FAsyncTaskNotificationConfig NotificationConfig;
NotificationConfig.bIsHeadless = Settings->bIsHeadless;
NotificationConfig.bKeepOpenOnFailure = true;
NotificationConfig.TitleText = LOCTEXT("RenamingSession", "Renaming Session...");
NotificationConfig.LogCategory = ConcertUtil::GetLogConcertPtr();
FAsyncTaskNotification Notification(NotificationConfig);
return ClientAdminEndpoint->SendRequest<FConcertAdmin_RenameSessionRequest, FConcertAdmin_RenameSessionResponse>(RenameSessionRequest, ServerAdminEndpointId)
.Next([this, NewName, Notification = MoveTemp(Notification)](const FConcertAdmin_RenameSessionResponse& RequestResponse) mutable
{
if (RequestResponse.ResponseCode == EConcertResponseCode::Success)
{
Notification.SetComplete(FText::Format(LOCTEXT("RenamedSessionFmt", "Renamed Session '{0}' as '{1}'"), FText::AsCultureInvariant(RequestResponse.OldName), FText::AsCultureInvariant(NewName)), FText(), true);
}
else
{
Notification.SetComplete(FText::Format(LOCTEXT("FailedToRenameSessionFmt", "Failed to Rename Session '{0}' as '{1}'"), FText::AsCultureInvariant(RequestResponse.OldName), FText::AsCultureInvariant(NewName)), RequestResponse.Reason, false);
}
return RequestResponse.ResponseCode;
});
}
TFuture<EConcertResponseCode> FConcertClient::DeleteSession(const FGuid& ServerAdminEndpointId, const FGuid& SessionId)
{
FConcertAdmin_DeleteSessionRequest DeleteSessionRequest;
DeleteSessionRequest.SessionId = SessionId;
// Fill the information for the client identification
DeleteSessionRequest.UserName = ClientInfo.UserName;
DeleteSessionRequest.DeviceName = ClientInfo.DeviceName;
FAsyncTaskNotificationConfig NotificationConfig;
NotificationConfig.bIsHeadless = Settings->bIsHeadless;
NotificationConfig.bKeepOpenOnFailure = true;
NotificationConfig.TitleText = LOCTEXT("DeletingSession", "Deleting Session...");
NotificationConfig.LogCategory = ConcertUtil::GetLogConcertPtr();
FAsyncTaskNotification Notification(NotificationConfig);
return ClientAdminEndpoint->SendRequest<FConcertAdmin_DeleteSessionRequest, FConcertAdmin_DeleteSessionResponse>(DeleteSessionRequest, ServerAdminEndpointId)
.Next([this, Notification = MoveTemp(Notification)](const FConcertAdmin_DeleteSessionResponse& RequestResponse) mutable
{
if (RequestResponse.ResponseCode == EConcertResponseCode::Success)
{
Notification.SetComplete(FText::Format(LOCTEXT("DeletedSessionFmt", "Deleted Session '{0}'"), FText::FromString(RequestResponse.SessionName)), FText(), true);
}
else
{
Notification.SetComplete(FText::Format(LOCTEXT("FailedToDeleteSessionFmt", "Failed to Delete Session '{0}'"), FText::FromString(RequestResponse.SessionName)), RequestResponse.Reason, false);
}
return RequestResponse.ResponseCode;
});
}
TFuture<FConcertAdmin_BatchDeleteSessionResponse> FConcertClient::BatchDeleteSessions(const FGuid& ServerAdminEndpointId, const FConcertBatchDeleteSessionsArgs& BatchDeletionArgs)
{
FConcertAdmin_BatchDeleteSessionRequest DeleteSessionRequest;
DeleteSessionRequest.SessionIds = BatchDeletionArgs.SessionIds;
DeleteSessionRequest.Flags = BatchDeletionArgs.Flags;
// Fill the information for the client identification
DeleteSessionRequest.UserName = ClientInfo.UserName;
DeleteSessionRequest.DeviceName = ClientInfo.DeviceName;
FAsyncTaskNotificationConfig NotificationConfig;
NotificationConfig.bIsHeadless = Settings->bIsHeadless;
NotificationConfig.bKeepOpenOnFailure = true;
NotificationConfig.TitleText = LOCTEXT("DeletingSessions", "Deleting Sessions...");
NotificationConfig.LogCategory = ConcertUtil::GetLogConcertPtr();
FAsyncTaskNotification Notification(NotificationConfig);
return ClientAdminEndpoint->SendRequest<FConcertAdmin_BatchDeleteSessionRequest, FConcertAdmin_BatchDeleteSessionResponse>(DeleteSessionRequest, ServerAdminEndpointId)
.Next([this, NumRequested = DeleteSessionRequest.SessionIds.Num(), Notification = MoveTemp(Notification)](const FConcertAdmin_BatchDeleteSessionResponse& RequestResponse) mutable
{
if (RequestResponse.ResponseCode == EConcertResponseCode::Success)
{
const bool bDeletedAll = RequestResponse.DeletedItems.Num() == NumRequested;
const FText Message = bDeletedAll
? FText::Format(LOCTEXT("DeletedSessionsFmt.All", "Deleted {0} Sessions"), RequestResponse.DeletedItems.Num())
: FText::Format(LOCTEXT("DeletedSessionsFmt.Some", "Deleted {0} of {1} Sessions"), RequestResponse.DeletedItems.Num(), NumRequested);
const FText ProgressText = bDeletedAll
? FText::GetEmpty()
: [&RequestResponse]()
{
TArray<FString> SessionNames;
Algo::Transform(RequestResponse.NotOwnedByClient, SessionNames, [](const FDeletedSessionInfo& Skipped){ return Skipped.SessionName; });
return FText::FromString(FString::Join(SessionNames, TEXT(", ")));
}();
Notification.SetComplete(Message, ProgressText, true);
}
else
{
Notification.SetComplete(LOCTEXT("FailedToDeleteSessionsFmt", "Failed to Delete Sessions"), RequestResponse.Reason, false);
}
return RequestResponse;
});
}
void FConcertClient::DisconnectSession()
{
// We don't want the client to get automatically reconnected to it's default session
AutoConnection.Reset();
InternalDisconnectSession();
// If async connection tasks were in-flight, cancel them.
PendingConnection.Reset();
}
EConcertSendReceiveState FConcertClient::GetSendReceiveState() const
{
if (ClientSession.IsValid())
{
return ClientSession->GetSendReceiveState();
}
return EConcertSendReceiveState::Default;
}
void FConcertClient::SetSendReceiveState(EConcertSendReceiveState InSendReceiveState)
{
if (ClientSession.IsValid())
{
ClientSession->SetSendReceiveState(InSendReceiveState);
}
}
bool FConcertClient::IsOwnerOf(const FConcertSessionInfo& InSessionInfo) const
{
return ClientInfo.UserName == InSessionInfo.OwnerUserName && ClientInfo.DeviceName == InSessionInfo.OwnerDeviceName;
}
TSharedPtr<IConcertClientSession> FConcertClient::GetCurrentSession() const
{
return ClientSession;
}
#jira UE-83339 - Disaster Recovery can fail to recover its session when the project is opened from the Project Browser - Fixed a disaster recovery bug preventing the Editor from recovering a session because another instance of the Editor on another project already locked all the sessions. Problem: On windows, the CrashReportClientEditor (hosting disaster recovery service) is started in the static initialization, before the engine is initialized, not allowing lot of command line configuration. The Editor project browser would start a first CrashReportClientEditor instance, which would load and lock all the available sessions (unless another CrashReportClientEditor was running). When the user selected a project, a new Editor and CrashReportClientEditor were launched before the first one was closed. The second instance could not access the existing sessions because they were still locked by the first instance. Solution: Because CrashReportClientEditor is launch before the engine is initialized, we don't have any context at the launch time. The best the was to delay the moment when the server reloads the existing sessions and enable each clients to store their sessions in different folders (repositories) mounted on demand by the server. Implementation details: - Implemented new RPC API to allow the client to list/create/load/drop specific repositories containing its own sessions on demand. - Updated the Concert server to manage multiples directories where session can be stored/found (session repositories) rather than just one. - Added a settings to allow the user to specify where the disaster recovery sessions should be stored on the disk. Now default in the current project folder. - Added a settings to prevent the Concert server from scanning the sessions in the default location. - Updated disaster recovery to start without any session repository and let the client decide if a new one needs to be created or an existing one be mounted to restore a previous session. - Changed the code to let disaster recovery client manage its session history rather than letting the server rotate the old session. Defaulted the history to 0, user has no flow to visualize and pick from the history. #rb Jamie.Dale #ROBOMERGE-SOURCE: CL 10260823 in //UE4/Release-4.24/... #ROBOMERGE-BOT: RELEASE (Release-4.24 -> Main) (v591-10236483) [CL 10260830 by patrick laflamme in Main branch]
2019-11-15 12:55:57 -05:00
TFuture<FConcertAdmin_MountSessionRepositoryResponse> FConcertClient::MountSessionRepository(const FGuid& ServerAdminEndpointId, const FString& RepositoryRootDir, const FGuid& RepositoryId, bool bCreateIfNotExist, bool bAsDefault) const
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
#jira UE-83339 - Disaster Recovery can fail to recover its session when the project is opened from the Project Browser - Fixed a disaster recovery bug preventing the Editor from recovering a session because another instance of the Editor on another project already locked all the sessions. Problem: On windows, the CrashReportClientEditor (hosting disaster recovery service) is started in the static initialization, before the engine is initialized, not allowing lot of command line configuration. The Editor project browser would start a first CrashReportClientEditor instance, which would load and lock all the available sessions (unless another CrashReportClientEditor was running). When the user selected a project, a new Editor and CrashReportClientEditor were launched before the first one was closed. The second instance could not access the existing sessions because they were still locked by the first instance. Solution: Because CrashReportClientEditor is launch before the engine is initialized, we don't have any context at the launch time. The best the was to delay the moment when the server reloads the existing sessions and enable each clients to store their sessions in different folders (repositories) mounted on demand by the server. Implementation details: - Implemented new RPC API to allow the client to list/create/load/drop specific repositories containing its own sessions on demand. - Updated the Concert server to manage multiples directories where session can be stored/found (session repositories) rather than just one. - Added a settings to allow the user to specify where the disaster recovery sessions should be stored on the disk. Now default in the current project folder. - Added a settings to prevent the Concert server from scanning the sessions in the default location. - Updated disaster recovery to start without any session repository and let the client decide if a new one needs to be created or an existing one be mounted to restore a previous session. - Changed the code to let disaster recovery client manage its session history rather than letting the server rotate the old session. Defaulted the history to 0, user has no flow to visualize and pick from the history. #rb Jamie.Dale #ROBOMERGE-SOURCE: CL 10260823 in //UE4/Release-4.24/... #ROBOMERGE-BOT: RELEASE (Release-4.24 -> Main) (v591-10236483) [CL 10260830 by patrick laflamme in Main branch]
2019-11-15 12:55:57 -05:00
FConcertAdmin_MountSessionRepositoryRequest MountRepositoryRequest;
MountRepositoryRequest.RepositoryId = RepositoryId;
MountRepositoryRequest.RepositoryRootDir = RepositoryRootDir;
MountRepositoryRequest.bCreateIfNotExist = bCreateIfNotExist;
MountRepositoryRequest.bAsServerDefault = bAsDefault;
return ClientAdminEndpoint->SendRequest<FConcertAdmin_MountSessionRepositoryRequest, FConcertAdmin_MountSessionRepositoryResponse>(MountRepositoryRequest, ServerAdminEndpointId);
}
TFuture<FConcertAdmin_GetSessionRepositoriesResponse> FConcertClient::GetSessionRepositories(const FGuid& ServerAdminEndpointId) const
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
#jira UE-83339 - Disaster Recovery can fail to recover its session when the project is opened from the Project Browser - Fixed a disaster recovery bug preventing the Editor from recovering a session because another instance of the Editor on another project already locked all the sessions. Problem: On windows, the CrashReportClientEditor (hosting disaster recovery service) is started in the static initialization, before the engine is initialized, not allowing lot of command line configuration. The Editor project browser would start a first CrashReportClientEditor instance, which would load and lock all the available sessions (unless another CrashReportClientEditor was running). When the user selected a project, a new Editor and CrashReportClientEditor were launched before the first one was closed. The second instance could not access the existing sessions because they were still locked by the first instance. Solution: Because CrashReportClientEditor is launch before the engine is initialized, we don't have any context at the launch time. The best the was to delay the moment when the server reloads the existing sessions and enable each clients to store their sessions in different folders (repositories) mounted on demand by the server. Implementation details: - Implemented new RPC API to allow the client to list/create/load/drop specific repositories containing its own sessions on demand. - Updated the Concert server to manage multiples directories where session can be stored/found (session repositories) rather than just one. - Added a settings to allow the user to specify where the disaster recovery sessions should be stored on the disk. Now default in the current project folder. - Added a settings to prevent the Concert server from scanning the sessions in the default location. - Updated disaster recovery to start without any session repository and let the client decide if a new one needs to be created or an existing one be mounted to restore a previous session. - Changed the code to let disaster recovery client manage its session history rather than letting the server rotate the old session. Defaulted the history to 0, user has no flow to visualize and pick from the history. #rb Jamie.Dale #ROBOMERGE-SOURCE: CL 10260823 in //UE4/Release-4.24/... #ROBOMERGE-BOT: RELEASE (Release-4.24 -> Main) (v591-10236483) [CL 10260830 by patrick laflamme in Main branch]
2019-11-15 12:55:57 -05:00
FConcertAdmin_GetSessionRepositoriesRequest GetRepositoryRequest;
return ClientAdminEndpoint->SendRequest<FConcertAdmin_GetSessionRepositoriesRequest, FConcertAdmin_GetSessionRepositoriesResponse>(GetRepositoryRequest, ServerAdminEndpointId);
}
TFuture<FConcertAdmin_DropSessionRepositoriesResponse> FConcertClient::DropSessionRepositories(const FGuid& ServerAdminEndpointId, const TArray<FGuid>& RepositoryIds) const
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
#jira UE-83339 - Disaster Recovery can fail to recover its session when the project is opened from the Project Browser - Fixed a disaster recovery bug preventing the Editor from recovering a session because another instance of the Editor on another project already locked all the sessions. Problem: On windows, the CrashReportClientEditor (hosting disaster recovery service) is started in the static initialization, before the engine is initialized, not allowing lot of command line configuration. The Editor project browser would start a first CrashReportClientEditor instance, which would load and lock all the available sessions (unless another CrashReportClientEditor was running). When the user selected a project, a new Editor and CrashReportClientEditor were launched before the first one was closed. The second instance could not access the existing sessions because they were still locked by the first instance. Solution: Because CrashReportClientEditor is launch before the engine is initialized, we don't have any context at the launch time. The best the was to delay the moment when the server reloads the existing sessions and enable each clients to store their sessions in different folders (repositories) mounted on demand by the server. Implementation details: - Implemented new RPC API to allow the client to list/create/load/drop specific repositories containing its own sessions on demand. - Updated the Concert server to manage multiples directories where session can be stored/found (session repositories) rather than just one. - Added a settings to allow the user to specify where the disaster recovery sessions should be stored on the disk. Now default in the current project folder. - Added a settings to prevent the Concert server from scanning the sessions in the default location. - Updated disaster recovery to start without any session repository and let the client decide if a new one needs to be created or an existing one be mounted to restore a previous session. - Changed the code to let disaster recovery client manage its session history rather than letting the server rotate the old session. Defaulted the history to 0, user has no flow to visualize and pick from the history. #rb Jamie.Dale #ROBOMERGE-SOURCE: CL 10260823 in //UE4/Release-4.24/... #ROBOMERGE-BOT: RELEASE (Release-4.24 -> Main) (v591-10236483) [CL 10260830 by patrick laflamme in Main branch]
2019-11-15 12:55:57 -05:00
FConcertAdmin_DropSessionRepositoriesRequest DropRepositoryRequest;
DropRepositoryRequest.RepositoryIds = RepositoryIds;
return ClientAdminEndpoint->SendRequest<FConcertAdmin_DropSessionRepositoriesRequest, FConcertAdmin_DropSessionRepositoriesResponse>(DropRepositoryRequest, ServerAdminEndpointId);
}
TFuture<FConcertAdmin_GetAllSessionsResponse> FConcertClient::GetServerSessions(const FGuid& ServerAdminEndpointId) const
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
FConcertAdmin_GetAllSessionsRequest GetSessionsRequest = FConcertAdmin_GetAllSessionsRequest();
return ClientAdminEndpoint->SendRequest<FConcertAdmin_GetAllSessionsRequest, FConcertAdmin_GetAllSessionsResponse>(GetSessionsRequest, ServerAdminEndpointId);
}
TFuture<FConcertAdmin_GetSessionsResponse> FConcertClient::GetLiveSessions(const FGuid& ServerAdminEndpointId) const
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
FConcertAdmin_GetLiveSessionsRequest GetLiveSessionsRequest;
return ClientAdminEndpoint->SendRequest<FConcertAdmin_GetLiveSessionsRequest, FConcertAdmin_GetSessionsResponse>(GetLiveSessionsRequest, ServerAdminEndpointId);
}
TFuture<FConcertAdmin_GetSessionsResponse> FConcertClient::GetArchivedSessions(const FGuid& ServerAdminEndpointId) const
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
FConcertAdmin_GetArchivedSessionsRequest GetArchivedSessionsRequest;
return ClientAdminEndpoint->SendRequest<FConcertAdmin_GetArchivedSessionsRequest, FConcertAdmin_GetSessionsResponse>(GetArchivedSessionsRequest, ServerAdminEndpointId);
}
TFuture<FConcertAdmin_GetSessionClientsResponse> FConcertClient::GetSessionClients(const FGuid& ServerAdminEndpointId, const FGuid& SessionId) const
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
FConcertAdmin_GetSessionClientsRequest GetSessionClientsRequest;
GetSessionClientsRequest.SessionId = SessionId;
return ClientAdminEndpoint->SendRequest<FConcertAdmin_GetSessionClientsRequest, FConcertAdmin_GetSessionClientsResponse>(GetSessionClientsRequest, ServerAdminEndpointId);
}
TFuture<FConcertAdmin_GetSessionActivitiesResponse> FConcertClient::GetSessionActivities(const FGuid& ServerAdminEndpointId, const FGuid& SessionId, int64 FromActivityId, int64 ActivityCount, bool bIncludeDetails) const
{
LLM_SCOPE_BYTAG(Concert_ConcertClient);
FConcertAdmin_GetSessionActivitiesRequest GetSessionActivitiesRequest;
GetSessionActivitiesRequest.SessionId = SessionId;
GetSessionActivitiesRequest.FromActivityId = FromActivityId;
GetSessionActivitiesRequest.ActivityCount = ActivityCount;
GetSessionActivitiesRequest.bIncludeDetails = bIncludeDetails;
return ClientAdminEndpoint->SendRequest<FConcertAdmin_GetSessionActivitiesRequest, FConcertAdmin_GetSessionActivitiesResponse>(GetSessionActivitiesRequest, ServerAdminEndpointId);
}
TFuture<EConcertResponseCode> FConcertClient::InternalCreateSession(const FGuid& ServerAdminEndpointId, const FConcertCreateSessionArgs& CreateSessionArgs, TUniquePtr<FAsyncTaskNotification> OngoingNotification)
{
// Reset last connection error
LastConnectionError = FConcertConnectionError();
// Cancel any pending connection (will be aborted)
PendingConnection.Reset();
// Build the tasks to execute
TArray<TUniquePtr<IConcertClientConnectionTask>> ConnectionTasks;
// Collect pre-connection tasks
OnGetPreConnectionTasksDelegate.Broadcast(*this, ConnectionTasks);
// Create session task
{
// Fill create session request;
FConcertAdmin_CreateSessionRequest CreateSessionRequest;
CreateSessionRequest.SessionName = CreateSessionArgs.SessionName;
CreateSessionRequest.OwnerClientInfo = ClientInfo;
CreateSessionRequest.VersionInfo.Initialize(GetConfiguration()->ClientSettings.bSupportMixedBuildTypes);
// Session settings
CreateSessionRequest.SessionSettings.Initialize();
CreateSessionRequest.SessionSettings.ArchiveNameOverride = CreateSessionArgs.ArchiveNameOverride;
ConnectionTasks.Emplace(MakeUnique<FConcertClientCreateSessionTask>(this, MoveTemp(CreateSessionRequest), ServerAdminEndpointId));
}
// Pending connection config
const FText SessionNameText = FText::FromString(CreateSessionArgs.SessionName);
FConcertPendingConnection::FConfig PendingConnectionConfig;
PendingConnectionConfig.PendingTitleText = FText::Format(LOCTEXT("CreatingSessionFmt", "Creating Session '{0}'..."), SessionNameText);
PendingConnectionConfig.SuccessTitleText = FText::Format(LOCTEXT("CreatedSessionFmt", "Created Session '{0}'"), SessionNameText);
PendingConnectionConfig.FailureTitleText = FText::Format(LOCTEXT("FailedToCreateSessionFmt", "Failed to Create Session '{0}'"), SessionNameText);
PendingConnectionConfig.KeepNotificationOpenOnError = TAttribute<bool>::Create([KeepErrorOnScreen = !Settings->bRetryAutoConnectOnError] { return KeepErrorOnScreen; }); // If the connection fails and retry is off, we can keep the error on screen.
PendingConnectionConfig.bIsAutoConnection = AutoConnection.IsValid();
// Kick off a pending connection to execute the tasks
PendingConnection = MakeShared<FConcertPendingConnection>(this, PendingConnectionConfig);
return PendingConnection->Execute(MoveTemp(ConnectionTasks), MoveTemp(OngoingNotification));
}
TFuture<EConcertResponseCode> FConcertClient::InternalJoinSession(const FGuid& ServerAdminEndpointId, const FGuid& SessionId, TUniquePtr<FAsyncTaskNotification> OngoingNotification)
{
// Reset last connection error
LastConnectionError = FConcertConnectionError();
// Cancel any pending connection (will be aborted)
PendingConnection.Reset();
// Build the tasks to execute
TArray<TUniquePtr<IConcertClientConnectionTask>> ConnectionTasks;
// Collect pre-connection tasks
OnGetPreConnectionTasksDelegate.Broadcast(*this, ConnectionTasks);
// Will be filled in with the resolved session name during the join process
TSharedRef<FText> ResolvedSessionName = MakeShared<FText>();
// Find session task
{
// Fill find session request
FConcertAdmin_FindSessionRequest FindSessionRequest;
FindSessionRequest.SessionId = SessionId;
FindSessionRequest.OwnerClientInfo = ClientInfo;
FindSessionRequest.VersionInfo.Initialize(GetConfiguration()->ClientSettings.bSupportMixedBuildTypes);
// Session settings
FindSessionRequest.SessionSettings.Initialize();
ConnectionTasks.Emplace(MakeUnique<FConcertClientJoinSessionTask>(this, MoveTemp(FindSessionRequest), ServerAdminEndpointId, ResolvedSessionName));
}
// Pending connection config
FConcertPendingConnection::FConfig PendingConnectionConfig;
PendingConnectionConfig.PendingTitleText = LOCTEXT("JoiningSession", "Joining Session...");
PendingConnectionConfig.SuccessTitleText.Bind(TAttribute<FText>::FGetter::CreateLambda([ResolvedSessionName]() { return FText::Format(LOCTEXT("JoinedSessionFmt", "Joined Session '{0}'"), *ResolvedSessionName); }));
PendingConnectionConfig.FailureTitleText.Bind(TAttribute<FText>::FGetter::CreateLambda([ResolvedSessionName]() { return ResolvedSessionName->IsEmpty() ? LOCTEXT("FailedToJoinSession", "Failed to Join Session") : FText::Format(LOCTEXT("FailedToJoinSessionFmt", "Failed to Join Session '{0}'"), *ResolvedSessionName); }));
PendingConnectionConfig.KeepNotificationOpenOnError = TAttribute<bool>::Create([KeepErrorOnScreen = !Settings->bRetryAutoConnectOnError] { return KeepErrorOnScreen; }); // If the connection fails and retry is off, we can keep the error on screen.
PendingConnectionConfig.bIsAutoConnection = AutoConnection.IsValid();
// Kick off a pending connection to execute the tasks
PendingConnection = MakeShared<FConcertPendingConnection>(this, PendingConnectionConfig);
return PendingConnection->Execute(MoveTemp(ConnectionTasks), MoveTemp(OngoingNotification));
}
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
TFuture<EConcertResponseCode> FConcertClient::InternalCopySession(const FGuid& ServerAdminEndpointId, const FConcertCopySessionArgs& CopySessionArgs, bool bRestoreOnlyConstraint, TUniquePtr<FAsyncTaskNotification> OngoingNotification)
{
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
FConcertAdmin_CopySessionRequest CopySessionRequest;
CopySessionRequest.SessionId = CopySessionArgs.SessionId;
CopySessionRequest.SessionName = CopySessionArgs.SessionName;
CopySessionRequest.SessionFilter = CopySessionArgs.SessionFilter;
CopySessionRequest.bRestoreOnly = bRestoreOnlyConstraint;
CopySessionRequest.OwnerClientInfo = ClientInfo;
CopySessionRequest.VersionInfo.Initialize(GetConfiguration()->ClientSettings.bSupportMixedBuildTypes);
// Session settings
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
CopySessionRequest.SessionSettings.Initialize();
CopySessionRequest.SessionSettings.ArchiveNameOverride = CopySessionArgs.ArchiveNameOverride;
TUniquePtr<FAsyncTaskNotification> Notification = MoveTemp(OngoingNotification);
if (!Notification.IsValid())
{
FAsyncTaskNotificationConfig NotificationConfig;
NotificationConfig.bIsHeadless = Settings->bIsHeadless;
NotificationConfig.bKeepOpenOnFailure = true;
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
NotificationConfig.TitleText = bRestoreOnlyConstraint ? LOCTEXT("RestoringSession", "Restoring Session...") : LOCTEXT("CopyingSession", "Copying Session...");
NotificationConfig.LogCategory = ConcertUtil::GetLogConcertPtr();
Notification = MakeUnique<FAsyncTaskNotification>(NotificationConfig);
}
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
return ClientAdminEndpoint->SendRequest<FConcertAdmin_CopySessionRequest, FConcertAdmin_SessionInfoResponse>(CopySessionRequest, ServerAdminEndpointId)
.Next([this, Notification = MoveTemp(Notification), bAutoConnect = CopySessionArgs.bAutoConnect, bRestoreOnly = bRestoreOnlyConstraint](const FConcertAdmin_SessionInfoResponse& RequestResponse) mutable
{
if (RequestResponse.ResponseCode == EConcertResponseCode::Success)
{
if (bAutoConnect)
{
InternalJoinSession(RequestResponse.ConcertEndpointId, RequestResponse.SessionInfo.SessionId, MoveTemp(Notification));
}
else
{
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
Notification->SetComplete(FText::Format(bRestoreOnly ? LOCTEXT("RestoreSessionFmt", "Restored Session '{0}'") : LOCTEXT("CopySessionFmt", "Copied Session '{0}'"), FText::FromString(RequestResponse.SessionInfo.SessionName)), FText(), true);
}
}
else
{
#jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - Added the ability to copy and restore a live session, preventing the need to archive it in first place, making the server exist fast (releasing the session lock very quickly) before showing the crash UI and before the next Editor instance could starts. Details: This bug could manifest if various ways. An issue causing this bug was fixed in 11252374. This bug can also be observed if the crash reporting process doesn't release its lock on the crashed session quickly. Archiving a session may takes several minutes (depending on the session size) and while a session is archiving, its database is locked and cannot be restored until the archiving process complets. When the Editor reboots after a crash, it searches for a session to recover, but skip over any session that is mounted/locked assuming the session is concurrently used by a concurrent Editor process, potentially preventing it from restoring. The optimal way to work around this problem is to skip the archiving step. Instead, the live session is never archived (saving a copy), which allows the recovery service to shutdown and release the session lock very quickly ensuring that the session will be unlocked when the Editor restarts. On Editor start, it a crashed session is found and the user decides to restore it, the live session is copied into a new live session. This changelist also affect those other jira in the following ways: #jira UE-87899 - Disaster recovery prevents showing the crash reporting UI in a timely manner if the session is large - This CL changes execution order to shut down the recovery service ASAP to release the lock, but the optimization above make it super fast, so the UI should always be shown in a timely manner. #jira UE-87927 - Disaster Recovery doesn't restore a crash from a restored session - This CL ensures the recovery service release the session lock faster than the next instance of the Editor can start. #jira UE-87900 - Disaster Recovery stops recording transactions if the UDP transport layer restarts or auto-repair #jira UE-88517 - Concert Log Spam - (ConcertKeepAlive) discarded - This CL fixes an issues with endpoints timeout logic. #jira UE-81049 - Clean up the DisasterRecovery Intermediate directory - This CL added code to clean up the intermediate directory left over by crashed client. #rb Francis.Hurteau #ROBOMERGE-SOURCE: CL 11632069 in //UE4/Release-4.25/... via CL 11632084 #ROBOMERGE-BOT: RELEASE (Release-4.25Plus -> Main) (v655-11596533) [CL 11632094 by patrick laflamme in Main branch]
2020-02-26 11:18:30 -05:00
Notification->SetComplete(bRestoreOnly ? LOCTEXT("FailedToRestoreSession", "Failed to Restore Session") : LOCTEXT("FailedToCopySession", "Failed to Copy Session"), RequestResponse.Reason, false);
}
return RequestResponse.ResponseCode;
});
}
void FConcertClient::InternalDisconnectSession()
{
if (ClientSession.IsValid())
{
ClientSession->Disconnect();
OnSessionShutdownDelegate.Broadcast(ClientSession.ToSharedRef());
ClientSession->Shutdown();
ClientSession.Reset();
}
bClientSessionPendingDestroy = false;
}
void FConcertClient::SetLastConnectionError(FConcertConnectionError LastError)
{
LastConnectionError = MoveTemp(LastError);
}
void FConcertClient::OnEndFrame()
{
if (bClientSessionPendingDestroy)
{
InternalDisconnectSession();
bClientSessionPendingDestroy = false;
}
}
void FConcertClient::TimeoutDiscovery(const FDateTime& UtcNow)
{
const FTimespan DiscoveryTimeoutSpan = FTimespan(0, 0, Settings->ClientSettings.DiscoveryTimeoutSeconds);
if (DiscoveryTimeoutSpan.IsZero())
{
return;
}
bool TimeoutOccured = false;
for (auto It = KnownServers.CreateIterator(); It; ++It)
{
if (It->Value.LastDiscoveryTime + DiscoveryTimeoutSpan <= UtcNow)
{
TimeoutOccured = true;
UE_LOG(LogConcert, Display, TEXT("Server %s lost."), *It->Value.ServerInfo.ServerName);
It.RemoveCurrent();
continue;
}
}
if (TimeoutOccured)
{
ServersUpdatedDelegate.Broadcast();
}
}
void FConcertClient::SendDiscoverServersEvent()
{
FConcertAdmin_DiscoverServersEvent DiscoverServersEvent;
DiscoverServersEvent.ConcertProtocolVersion = EConcertMessageVersion::LatestVersion;
DiscoverServersEvent.RequiredRole = Role;
DiscoverServersEvent.RequiredVersion = VERSION_STRINGIFY(ENGINE_MAJOR_VERSION) TEXT(".") VERSION_STRINGIFY(ENGINE_MINOR_VERSION);
DiscoverServersEvent.ClientAuthenticationKey = Settings->ClientSettings.ClientAuthenticationKey;
ClientAdminEndpoint->PublishEvent(DiscoverServersEvent);
}
void FConcertClient::HandleServerDiscoveryEvent(const FConcertMessageContext& Context)
{
const FConcertAdmin_ServerDiscoveredEvent* Message = Context.GetMessage<FConcertAdmin_ServerDiscoveredEvent>();
if (Message->ConcertProtocolVersion != EConcertMessageVersion::LatestVersion)
{
return;
}
FKnownServer* Info = KnownServers.Find(Message->ConcertEndpointId);
if (Info == nullptr)
{
UE_LOG(LogConcert, Display, TEXT("Server %s discovered."), *Message->ServerName);
KnownServers.Emplace(Message->ConcertEndpointId, FKnownServer{ Context.UtcNow, FConcertServerInfo{ Message->ConcertEndpointId, Message->ServerName, Message->InstanceInfo, Message->ServerFlags } });
ServersUpdatedDelegate.Broadcast();
}
else
{
Info->LastDiscoveryTime = Context.UtcNow;
}
}
TFuture<EConcertResponseCode> FConcertClient::CreateClientSession(const FConcertSessionInfo& SessionInfo)
{
check(SessionInfo.SessionId.IsValid() && !SessionInfo.SessionName.IsEmpty());
InternalDisconnectSession();
ClientSession = MakeShared<FConcertClientSession>(
SessionInfo,
ClientInfo,
Settings->ClientSettings,
EndpointProvider->CreateLocalEndpoint(SessionInfo.SessionName, Settings->EndpointSettings, [this](const FConcertEndpointContext& Context)
{
return ConcertUtil::CreateLogger(Context, [this](const FConcertLog& Log)
{
ConcertTransportEvents::OnConcertClientLogEvent().Broadcast(*this, Log);
});
}),
Paths.GetSessionWorkingDir(SessionInfo.SessionId)
);
OnSessionStartupDelegate.Broadcast(ClientSession.ToSharedRef());
ClientSession->OnConnectionChanged().AddRaw(this, &FConcertClient::HandleSessionConnectionChanged);
ClientSession->Startup();
ClientSession->Connect();
// Promise the caller to tell it once the client connection state is known (connected or not).
check(!ConnectionPromise.IsValid()); // InternalDisconnect() triggers a disconnnect and this will release of the promise.
ConnectionPromise = MakeUnique<TPromise<EConcertResponseCode>>();
return ConnectionPromise->GetFuture();
}
void FConcertClient::HandleSessionConnectionChanged(IConcertClientSession& InSession, EConcertConnectionStatus Status)
{
// If this session disconnected, make sure we fully destroy it at the end of the frame
if (Status == EConcertConnectionStatus::Disconnected)
{
bClientSessionPendingDestroy = true;
if (ConnectionPromise) // Tried to connect, but did not succeed? (Like an endpoint timeout/client shutting down while connecting)
{
ConnectionPromise->SetValue(EConcertResponseCode::Failed);
ConnectionPromise.Reset();
}
}
else if (Status == EConcertConnectionStatus::Connected)
{
check(ConnectionPromise.IsValid());
ConnectionPromise->SetValue(EConcertResponseCode::Success);
ConnectionPromise.Reset();
}
OnSessionConnectionChangedDelegate.Broadcast(InSession, Status);
}
namespace UE::ConcertClient
{
FOnConcertEvaluateHasRole& VPRoleEvaluator()
{
static FOnConcertEvaluateHasRole RoleEvaluator;
return RoleEvaluator;
}
}
#undef LOCTEXT_NAMESPACE