// Copyright Epic Games, Inc. All Rights Reserved. #include "TurnkeyEditorIOServer.h" #if WITH_TURNKEY_EDITOR_IO_SERVER #include "HAL/PlatformProcess.h" #include "HAL/RunnableThread.h" #include "HAL/Event.h" #include "Misc/MessageDialog.h" #include "Async/Async.h" #include "Sockets.h" #include "SocketSubsystem.h" #include "Framework/Docking/TabManager.h" #include "Framework/Application/SlateApplication.h" #include "Serialization/JsonReader.h" #include "Serialization/JsonSerializer.h" #include "STurnkeyIOWidgets.h" #define LOCTEXT_NAMESPACE "TurnkeyEditorIOServer" DEFINE_LOG_CATEGORY_STATIC(LogTurnkeyIO, Log, All); class FTurnkeyEditorIOServerRunnable : public FRunnable { public: static const int Version = 1; //keep in sync with .cs FTurnkeyEditorIOServerRunnable(TSharedPtr InServerSocket) : ServerSocket(InServerSocket) { ReceiveBuffer.AddZeroed(10240); ActionCompleteEvent = FPlatformProcess::GetSynchEventFromPool(true); } ~FTurnkeyEditorIOServerRunnable() { FPlatformProcess::ReturnSynchEventToPool(ActionCompleteEvent); ActionCompleteEvent = nullptr; } virtual uint32 Run() override { while(!IsEngineExitRequested() && ServerSocket.IsValid()) { // wait for a connection UE_LOG( LogTurnkeyIO, Verbose, TEXT("Turnkey IO waiting for connection...")); ClientSocket = TSharedPtr( ServerSocket->Accept(TEXT("TurnkeyIOClient")) ); if (!IsClientConnected()) { UE_LOG( LogTurnkeyIO, Verbose, TEXT("Client not connected")); continue; } UE_LOG( LogTurnkeyIO, Verbose, TEXT("Client connected")); // read the message TSharedPtr Message = ReceiveMessage(); if (Message.IsValid()) { TSharedPtr Action = Message->GetObjectField(TEXT("Action")); if (Action.IsValid()) { HandleActionMessage(Action); } } // clean up for next time UE_LOG( LogTurnkeyIO, Verbose, TEXT("Transaction complete. Disconnecting") ); if (ClientSocket) { ClientSocket->Close(); ClientSocket.Reset(); } // wait a short while before reading another message, to give the message log time to display any output from Turnkey if (ServerSocket.IsValid()) { FPlatformProcess::Sleep(0.1f); } } return 0; } virtual void Stop() override { if (ServerSocket.IsValid()) { ServerSocket->Close(); ServerSocket.Reset(); } if (ClientSocket.IsValid()) { ClientSocket->Close(); ClientSocket.Reset(); } if (ActionCompleteEvent) { ActionCompleteEvent->Trigger(); } } private: void OnActionFinish( TSharedPtr Result ) { if (Window.IsValid()) { TSharedPtr OldWindow = Window; Window.Reset(); OldWindow->RequestDestroyWindow(); } UE_LOG( LogTurnkeyIO, Verbose, TEXT("Turnkey action completed. Sending reply.")); SendMessage(Result); UE_LOG( LogTurnkeyIO, Verbose, TEXT("Turnkey awaiting ack")); TSharedPtr Ack = ReceiveMessage(); ActionCompleteEvent->Trigger(); } void HandleActionMessage( TSharedPtr Action ) { // read action type FString Type; if (!Action->TryGetStringField(TEXT("Type"), Type )) { UE_LOG( LogTurnkeyIO, Error, TEXT("Invalid action message") ); return; } // check the action type ActionCompleteEvent->Reset(); if (Type.Equals(TEXT("PauseForUser"), ESearchCase::IgnoreCase ) ) { DoPauseForUser(Action); } else if (Type.Equals(TEXT("ReadInput"), ESearchCase::IgnoreCase ) ) { DoReadInput(Action); } else if (Type.Equals(TEXT("ReadInputInt"), ESearchCase::IgnoreCase ) ) { DoReadInputInt(Action); } else if (Type.Equals(TEXT("GetConfirmation"), ESearchCase::IgnoreCase ) ) { DoGetUserConfirmation(Action); } else { UE_LOG(LogTurnkeyIO, Error, TEXT("Unknown action type %s"), *Type ); return; } // wait for the action to finish UE_LOG( LogTurnkeyIO, Verbose, TEXT("Turnkey action in progress. Waiting for completion") ); ActionCompleteEvent->Wait(); } void DoPauseForUser( TSharedPtr JsonObject ) { AsyncTask( ENamedThreads::GameThread, [this, JsonObject]() { // read parameters FString Message; JsonObject->TryGetStringField(TEXT("Message"), Message ); // display the message box FString Prompt = Message.IsEmpty() ? *LOCTEXT("ReadyToContinue", "Click OK To Continue").ToString() : Message; FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Prompt), LOCTEXT("Turnkey", "Turnkey") ); // send empty response TSharedPtr Result = MakeShared(); OnActionFinish( Result ); }); } void DoReadInput( TSharedPtr JsonObject ) { AsyncTask( ENamedThreads::GameThread, [this, JsonObject]() { // read parameters FString Prompt; JsonObject->TryGetStringField(TEXT("Prompt"), Prompt); FString Default; JsonObject->TryGetStringField(TEXT("Default"), Default ); // show the dialog ShowModal( SNew(STurnkeyReadInputModal) .Prompt(Prompt) .Default(Default) .OnFinished( FOnTurnkeyActionComplete::CreateRaw( this, &FTurnkeyEditorIOServerRunnable::OnActionFinish) ) ); }); } void DoReadInputInt( TSharedPtr JsonObject ) { AsyncTask( ENamedThreads::GameThread, [this, JsonObject]() { // read parameters FString Prompt; JsonObject->TryGetStringField(TEXT("Prompt"), Prompt); TArray Options; JsonObject->TryGetStringArrayField(TEXT("Options"), Options); bool bIsCancellable = true; JsonObject->TryGetBoolField(TEXT("IsCancellable"), bIsCancellable); int32 DefaultValue = 0; JsonObject->TryGetNumberField(TEXT("DefaultValue"), DefaultValue); // show the dialog ShowModal( SNew(STurnkeyReadInputIntModal) .Prompt(Prompt) .Options(Options) .IsCancellable(bIsCancellable) .DefaultValue(DefaultValue) .OnFinished( FOnTurnkeyActionComplete::CreateRaw( this, &FTurnkeyEditorIOServerRunnable::OnActionFinish) ) ); }); } void DoGetUserConfirmation( TSharedPtr JsonObject ) { AsyncTask( ENamedThreads::GameThread, [this, JsonObject]() { // read parameters FString Message; JsonObject->TryGetStringField(TEXT("Message"), Message ); bool bDefaultValue = true; JsonObject->TryGetBoolField(TEXT("DefaultValue"), bDefaultValue ); // display the message box EAppReturnType::Type Result = (bDefaultValue ? EAppReturnType::Yes : EAppReturnType::No ); Result = FMessageDialog::Open(EAppMsgType::YesNo, Result, FText::FromString(Message), LOCTEXT("Turnkey", "Turnkey") ); // send empty response TSharedPtr JsonResult = MakeShared(); JsonResult->SetBoolField(TEXT("Result"), (Result == EAppReturnType::Yes) ); OnActionFinish( JsonResult ); }); } void ShowModal( TSharedRef Modal ) { // create a window for the widget Window = SNew(SWindow) .Title( LOCTEXT("Turnkey","Turnkey") ) .SupportsMaximize(false) .SupportsMinimize(false) .HasCloseButton(false) .AutoCenter(EAutoCenter::PreferredWorkArea) .SizingRule(ESizingRule::Autosized) [ Modal ]; // even though HasCloseButton is false, the window can still be closed via Windows' alt-tab screen Window->GetOnWindowClosedEvent().AddLambda( [this]( const TSharedRef& InWindow ) { if (Window.IsValid()) { Window.Reset(); TSharedPtr Result = MakeShared(); Result->SetStringField(TEXT("Error"), TEXT("Turnkey window was closed")); OnActionFinish(Result); } }); TSharedPtr RootWindow = FGlobalTabmanager::Get()->GetRootWindow(); FSlateApplication::Get().AddModalWindow(Window.ToSharedRef(), RootWindow); } TSharedPtr ReceiveMessage() { UE_LOG(LogTurnkeyIO, Verbose, TEXT("Waiting for message from Turnkey client") ); // read the message bool bFinished = false; TArray Msg; int RecvFails = 0; do { if (!IsClientConnected()) { return nullptr; } if (ClientSocket->Wait(ESocketWaitConditions::WaitForRead, FTimespan::FromMilliseconds(100))) { int32 BytesRead = 0; if (ClientSocket->Recv(ReceiveBuffer.GetData(), ReceiveBuffer.Num()-1, BytesRead)) { Msg.Append( ReceiveBuffer.GetData(), BytesRead ); } else { RecvFails++; static const int MaxFails = 100; // arbirary, just to make sure it has definititely failed for good if (RecvFails > MaxFails) { UE_LOG(LogTurnkeyIO, Error, TEXT("Client not responding")); return TSharedPtr(); } } } } while( !Msg.Contains('\0') ); // decode the json object TSharedPtr JsonObject; TSharedRef> Reader = TJsonReaderFactory<>::Create(ANSI_TO_TCHAR((const ANSICHAR*)Msg.GetData())); if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) { UE_LOG(LogTurnkeyIO, Error, TEXT("Failed to parse message from Turnkey client %s"), *Reader->GetErrorMessage()); return TSharedPtr(); } return JsonObject; } bool SendMessage( TSharedPtr JsonObject ) { if (!IsClientConnected()) { return false; } // add version to message JsonObject->SetNumberField( TEXT("version"), static_cast(Version) ); // encode the json object and append the message terminator FString JsonString; TSharedRef> Writer = TJsonWriterFactory<>::Create(&JsonString); if (!FJsonSerializer::Serialize( JsonObject.ToSharedRef(), Writer )) { return false; } // send the message auto Msg = StringCast(*JsonString); int32 BytesToSend = Msg.Length()+1; //also send null terminator const ANSICHAR* Data = Msg.Get(); while (BytesToSend > 0) { while (!ClientSocket->Wait(ESocketWaitConditions::WaitForWrite, FTimespan::FromSeconds(1.0))) { if (ClientSocket->GetConnectionState() == SCS_ConnectionError) { return false; } } int32 BytesSent; if (!ClientSocket->Send( (const uint8*)Data, BytesToSend, BytesSent )) { return false; } BytesToSend -= BytesSent; Data += BytesSent; } return true; } bool IsClientConnected() const { return ClientSocket.IsValid() && ClientSocket->GetConnectionState() == SCS_Connected; } FEvent* ActionCompleteEvent; TArray ReceiveBuffer; TSharedPtr ClientSocket; TSharedPtr ServerSocket; TSharedPtr Window; }; FTurnkeyEditorIOServer::FTurnkeyEditorIOServer() : Port(0) , Runnable(nullptr) , Thread(nullptr) { Start(); } FTurnkeyEditorIOServer::~FTurnkeyEditorIOServer() { Stop(); } FString FTurnkeyEditorIOServer::GetUATParams() const { FString Result; if (Thread != nullptr) { Result += FString::Printf(TEXT("-EditorIOPort=%d "), Port); } return Result; } bool FTurnkeyEditorIOServer::Start() { if (Thread == nullptr) { ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM); if (SocketSubsystem) { // create an IPv4 TCP server on localhost FName ProtocolType = FNetworkProtocolTypes::IPv4; TSharedPtr NewSocket( SocketSubsystem->CreateSocket(NAME_Stream, TEXT("TurnkeyIOServer"), ProtocolType ) ); if (NewSocket.IsValid()) { TSharedRef LocalHostAddr = SocketSubsystem->CreateInternetAddr(ProtocolType); LocalHostAddr->SetLoopbackAddress(); LocalHostAddr->SetPort(0); if (NewSocket->Bind( *LocalHostAddr )) { if (NewSocket->Listen(1)) { Port = NewSocket->GetPortNo(); Socket = NewSocket; Runnable = new FTurnkeyEditorIOServerRunnable(Socket); Thread = FRunnableThread::Create( Runnable, TEXT("TurnkeyEditorIOServer"), 0, EThreadPriority::TPri_BelowNormal ); check( Thread ); UE_LOG(LogTurnkeyIO, Verbose, TEXT("using port %d"), Port ); } } } } } return (Thread != nullptr); } void FTurnkeyEditorIOServer::Stop() { if (Socket.IsValid()) { Socket->Close(); Socket.Reset(); } if (Thread) { Thread->Kill(); delete Thread; delete Runnable; Thread = nullptr; Runnable = nullptr; } } #undef LOCTEXT_NAMESPACE #endif //WITH_TURNKEY_EDITOR_IO_SERVER