511 lines
13 KiB
C++
511 lines
13 KiB
C++
#include "WingServer.h"
|
|
#include "WingProperty.h"
|
|
#include "WingUtils.h"
|
|
#include "UObject/StrongObjectPtr.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "AssetRegistry/IAssetRegistry.h"
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "Misc/OutputDeviceRedirector.h"
|
|
#include "Serialization/JsonReader.h"
|
|
#include "Serialization/JsonSerializer.h"
|
|
#include "SocketSubsystem.h"
|
|
#include "Sockets.h"
|
|
#include "Async/Async.h"
|
|
|
|
UWingServer* UWingServer::GWingServer = nullptr;
|
|
|
|
// ============================================================
|
|
// Initialization and Shutdown
|
|
// ============================================================
|
|
|
|
void UWingServer::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
GWingServer = this;
|
|
|
|
// Create TCP listen socket
|
|
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
|
ListenSocket = SocketSub->CreateSocket(NAME_Stream, TEXT("WingServer"), false);
|
|
if (!ListenSocket)
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("UEWingman: Failed to create listen socket"));
|
|
return;
|
|
}
|
|
|
|
ListenSocket->SetReuseAddr(true);
|
|
ListenSocket->SetNonBlocking(true);
|
|
|
|
TSharedRef<FInternetAddr> Addr = SocketSub->CreateInternetAddr();
|
|
bool bIsValid = false;
|
|
Addr->SetIp(TEXT("127.0.0.1"), bIsValid);
|
|
Addr->SetPort(Port);
|
|
|
|
if (!ListenSocket->Bind(*Addr))
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("UEWingman: Failed to bind to port %d"), Port);
|
|
SocketSub->DestroySocket(ListenSocket);
|
|
ListenSocket = nullptr;
|
|
return;
|
|
}
|
|
|
|
if (!ListenSocket->Listen(4))
|
|
{
|
|
UE_LOG(LogTemp, Error, TEXT("UEWingman: Failed to listen on port %d"), Port);
|
|
SocketSub->DestroySocket(ListenSocket);
|
|
ListenSocket = nullptr;
|
|
return;
|
|
}
|
|
|
|
LoadingPhasesCompleteHandle = FCoreDelegates::OnAllModuleLoadingPhasesComplete.AddUObject(this, &UWingServer::BuildWingHandlerRegistry);
|
|
LogCapture.bEnabled = false;
|
|
GLog->AddOutputDevice(&LogCapture);
|
|
bRunning = true;
|
|
UE_LOG(LogTemp, Display, TEXT("UEWingman: MCP server listening on tcp://localhost:%d"), Port);
|
|
}
|
|
|
|
void UWingServer::Deinitialize()
|
|
{
|
|
FCoreDelegates::OnAllModuleLoadingPhasesComplete.Remove(LoadingPhasesCompleteHandle);
|
|
|
|
if (!bRunning)
|
|
{
|
|
Super::Deinitialize();
|
|
return;
|
|
}
|
|
|
|
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
|
|
|
// Set shutdown flag and drain pending messages under lock
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
bShuttingDown = true;
|
|
for (auto& Msg : PendingMessages)
|
|
{
|
|
Msg->Response.SetValue(TArray<uint8>());
|
|
}
|
|
PendingMessages.Empty();
|
|
}
|
|
|
|
// Close all client sockets (unblocks their blocking reads)
|
|
for (auto& Client : Clients)
|
|
{
|
|
if (Client->Socket)
|
|
{
|
|
Client->Socket->Close();
|
|
}
|
|
}
|
|
|
|
// Wait for client threads to exit
|
|
for (auto& Client : Clients)
|
|
{
|
|
Client->ThreadFuture.Wait();
|
|
if (Client->Socket)
|
|
{
|
|
SocketSub->DestroySocket(Client->Socket);
|
|
}
|
|
}
|
|
Clients.Empty();
|
|
|
|
// Close listen socket
|
|
if (ListenSocket)
|
|
{
|
|
ListenSocket->Close();
|
|
SocketSub->DestroySocket(ListenSocket);
|
|
ListenSocket = nullptr;
|
|
}
|
|
|
|
GLog->RemoveOutputDevice(&LogCapture);
|
|
bRunning = false;
|
|
bShuttingDown = false;
|
|
GWingServer = nullptr;
|
|
UE_LOG(LogTemp, Display, TEXT("UEWingman: Server stopped."));
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
// ============================================================
|
|
// FTickableEditorObject interface
|
|
// ============================================================
|
|
|
|
void UWingServer::Tick(float DeltaTime)
|
|
{
|
|
if (!bRunning) return;
|
|
|
|
// Accept new connections (non-blocking)
|
|
AcceptNewConnections();
|
|
|
|
// Clean up finished client threads
|
|
CleanupFinishedClients();
|
|
|
|
// Dequeue one pending message
|
|
TSharedPtr<FPendingMessage> Request;
|
|
{
|
|
FScopeLock Lock(&Mutex);
|
|
if (PendingMessages.Num() > 0)
|
|
{
|
|
Request = PendingMessages[0];
|
|
PendingMessages.RemoveAt(0);
|
|
}
|
|
}
|
|
|
|
// If we have a request, process it.
|
|
if (Request.IsValid())
|
|
{
|
|
TArray<uint8> Response = HandleRequest(Request->Request);
|
|
Request->Response.SetValue(Response);
|
|
}
|
|
}
|
|
|
|
void UWingServer::TickServer(float DeltaTime)
|
|
{
|
|
if (GWingServer) GWingServer->Tick(DeltaTime);
|
|
}
|
|
|
|
TStatId UWingServer::GetStatId() const
|
|
{
|
|
RETURN_QUICK_DECLARE_CYCLE_STAT(UWingServer, STATGROUP_Tickables);
|
|
}
|
|
|
|
// ============================================================
|
|
// HandleRequest — Given a command, execute it.
|
|
// ============================================================
|
|
|
|
TArray<uint8> UWingServer::HandleRequest(const TArray<uint8>& RequestBytes)
|
|
{
|
|
TArray<FString> Argv;
|
|
FString ResponseText;
|
|
|
|
if (DeserializeArgv(RequestBytes, Argv))
|
|
{
|
|
PreCallHandler();
|
|
TryCallHandler(Argv);
|
|
ResponseText = PostCallHandler();
|
|
}
|
|
else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n");
|
|
|
|
FTCHARToUTF8 Utf8(*ResponseText);
|
|
return TArray<uint8>(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
|
|
}
|
|
|
|
void UWingServer::PreCallHandler()
|
|
{
|
|
LogCapture.CapturedErrors.Empty();
|
|
LogCapture.bEnabled = true;
|
|
WingOut::StdoutBuffer.Reset();
|
|
SuggestedManualSections.Empty();
|
|
bSuggestHandlerHelp = false;
|
|
LastHandler = nullptr;
|
|
}
|
|
|
|
FString UWingServer::PostCallHandler()
|
|
{
|
|
Notifier.SendNotifications();
|
|
LogCapture.bEnabled = false;
|
|
for (const FString& Msg : LogCapture.CapturedErrors)
|
|
{
|
|
WingOut::Stdout.Printf(TEXT("UE_LOG: %s\n"), *Msg);
|
|
}
|
|
LogCapture.CapturedErrors.Empty();
|
|
if (bSuggestHandlerHelp || (!SuggestedManualSections.IsEmpty()))
|
|
{
|
|
if (LastHandler) WingManual::PrintHandlerHelp(*LastHandler);
|
|
if ((LastHandler == nullptr) || (LastHandler->Name != TEXT("Documentation_Manual")))
|
|
{
|
|
WingOut::Stdout.Print(TEXT("To see manual: command=Documentation_Manual\n"));
|
|
}
|
|
if (!SuggestedManualSections.IsEmpty())
|
|
{
|
|
WingManual::PrintSectionNames(TEXT("Suggested manual sections: "),
|
|
SuggestedManualSections, WingOut::Stdout);
|
|
}
|
|
}
|
|
FString Result = WingOut::StdoutBuffer.ToString();
|
|
WingOut::StdoutBuffer.Reset();
|
|
return Result;
|
|
}
|
|
|
|
void UWingServer::TryCallHandler(TArrayView<const FString> Argv)
|
|
{
|
|
FString Command = "Documentation_Manual";
|
|
if (Argv.Num() > 0)
|
|
{
|
|
Command = Argv[0];
|
|
Argv = Argv.RightChop(1);
|
|
}
|
|
|
|
if ((Command.Equals(TEXT("--help"))) ||
|
|
(Command.Equals(TEXT("-help"))) ||
|
|
(Command.Equals(TEXT("help"))))
|
|
{
|
|
Command = "Documentation_Manual";
|
|
}
|
|
|
|
// Find the handler for the specified command.
|
|
FWingHandlerConfig* Found = FindHandler(Command);
|
|
if (!Found)
|
|
{
|
|
WingOut::Stdout.Printf(TEXT("Unknown command: %s\n"), *Command);
|
|
UWingServer::SuggestManual(GET_FUNCTION_NAME_CHECKED(UWingManualSections, ImportantCommands));
|
|
return;
|
|
}
|
|
LastHandler = Found;
|
|
|
|
// Make an object of the handler class.
|
|
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), Found->HandlerClass.Get()));
|
|
UWingHandler* Handler = Cast<UWingHandler>(HandlerObj.Get());
|
|
Handler->Configuration = Found;
|
|
|
|
// Populate the handler object with argv parameters.
|
|
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
|
|
if (!FWingProperty::PopulateFromArgv(Props, Argv, WingOut::Stdout))
|
|
{
|
|
UWingServer::SuggestHandlerHelp();
|
|
return;
|
|
}
|
|
|
|
// MCP handlers must not run inside an undo transaction.
|
|
check(GUndo == nullptr);
|
|
|
|
// Invoke the handler.
|
|
Handler->Handle();
|
|
}
|
|
|
|
// ============================================================
|
|
// Connection Maintenance
|
|
// ============================================================
|
|
|
|
void UWingServer::AcceptNewConnections()
|
|
{
|
|
if (!ListenSocket) return;
|
|
|
|
bool bHasPending = false;
|
|
if (!ListenSocket->HasPendingConnection(bHasPending) || !bHasPending) return;
|
|
|
|
FSocket* ClientSocket = ListenSocket->Accept(TEXT("MCPClient"));
|
|
if (!ClientSocket) return;
|
|
|
|
ClientSocket->SetNonBlocking(false); // client threads use blocking I/O
|
|
|
|
TSharedPtr<FClientConnection> Client = MakeShared<FClientConnection>();
|
|
Client->Socket = ClientSocket;
|
|
Client->ThreadFuture = Async(EAsyncExecution::Thread, [this, Client]() { ClientThreadFunc(this, Client); });
|
|
Clients.Add(Client);
|
|
}
|
|
|
|
void UWingServer::CleanupFinishedClients()
|
|
{
|
|
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
|
|
|
for (int32 i = Clients.Num() - 1; i >= 0; --i)
|
|
{
|
|
if (!Clients[i]->bDone) continue;
|
|
|
|
Clients[i]->ThreadFuture.Wait();
|
|
if (Clients[i]->Socket)
|
|
{
|
|
SocketSub->DestroySocket(Clients[i]->Socket);
|
|
}
|
|
Clients.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Stuff Performed on the Client Thread
|
|
// ============================================================
|
|
|
|
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
|
|
{
|
|
FSocket* Socket = Client->Socket;
|
|
|
|
WaitForAssetRegistry();
|
|
|
|
TArray<uint8> Request;
|
|
if (!ReceiveRequest(Socket, Request))
|
|
{
|
|
Client->bDone = true;
|
|
return;
|
|
}
|
|
|
|
TArray<uint8> Response;
|
|
if (!ProcessRequestOnGameThread(Request, Response))
|
|
{
|
|
Client->bDone = true;
|
|
return;
|
|
}
|
|
|
|
SendAll(Socket, Response.GetData(), Response.Num());
|
|
Client->bDone = true;
|
|
}
|
|
|
|
uint32 UWingServer::UnpackBigEndian(const uint8 *Data)
|
|
{
|
|
return
|
|
((uint32)Data[0] << 24) |
|
|
((uint32)Data[1] << 16) |
|
|
((uint32)Data[2] << 8) |
|
|
(uint32)Data[3];
|
|
}
|
|
|
|
bool UWingServer::DeserializeArgv(
|
|
const TArray<uint8>& RequestBytes, TArray<FString>& Argv)
|
|
{
|
|
Argv.Empty();
|
|
|
|
int32 Offset = 0;
|
|
while (Offset < RequestBytes.Num())
|
|
{
|
|
if (RequestBytes.Num() - Offset < 4)
|
|
{
|
|
Argv.Empty();
|
|
return false;
|
|
}
|
|
|
|
uint32 Length = UnpackBigEndian(RequestBytes.GetData() + Offset);
|
|
Offset += 4;
|
|
|
|
if ((uint32)(RequestBytes.Num() - Offset) < Length)
|
|
{
|
|
Argv.Empty();
|
|
return false;
|
|
}
|
|
|
|
Argv.Add(FString::ConstructFromPtrSize(
|
|
reinterpret_cast<const UTF8CHAR*>(RequestBytes.GetData() + Offset),
|
|
Length));
|
|
Offset += (int32)Length;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UWingServer::ReceiveRequest(FSocket* Socket, TArray<uint8>& OutRequest)
|
|
{
|
|
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
|
|
constexpr int32 ChunkSize = 8192;
|
|
|
|
TArray<uint8> RecvBuf;
|
|
RecvBuf.Reserve(ChunkSize);
|
|
|
|
// Unreal's FSocket API is fundamentally broken: recv cannot
|
|
// differentiate between a socket that has been cleanly closed
|
|
// and a socket that has had an error. So we have no choice
|
|
// but to just read until recv returns false (which could be a
|
|
// clean close or an error). Then, we check if we have a cleanly
|
|
// encoded payload: if so, we assume everything is fine.
|
|
while (true)
|
|
{
|
|
uint8 Temp[ChunkSize];
|
|
int32 BytesRead = 0;
|
|
if (!Socket->Recv(Temp, ChunkSize, BytesRead))
|
|
{
|
|
break;
|
|
}
|
|
if (BytesRead <= 0) break;
|
|
if (RecvBuf.Num() + BytesRead > MaxRecvBufBytes)
|
|
{
|
|
return false;
|
|
}
|
|
RecvBuf.Append(Temp, BytesRead);
|
|
}
|
|
|
|
if (RecvBuf.Num() < 4) return false;
|
|
uint32 Size = UnpackBigEndian(RecvBuf.GetData());
|
|
if ((uint32)RecvBuf.Num() != (4u + Size)) return false;
|
|
RecvBuf.RemoveAt(0, 4);
|
|
|
|
OutRequest = MoveTemp(RecvBuf);
|
|
return true;
|
|
}
|
|
|
|
bool UWingServer::SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend)
|
|
{
|
|
while (BytesToSend > 0)
|
|
{
|
|
int32 BytesSent = 0;
|
|
if (!Socket->Send(Data, BytesToSend, BytesSent) || (BytesSent <= 0))
|
|
{
|
|
return false;
|
|
}
|
|
Data += BytesSent;
|
|
BytesToSend -= BytesSent;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool UWingServer::ProcessRequestOnGameThread(
|
|
const TArray<uint8>& Request, TArray<uint8>& Response)
|
|
{
|
|
// Enqueue the message for game-thread processing.
|
|
TSharedPtr<UWingServer::FPendingMessage> Msg =
|
|
MakeShared<UWingServer::FPendingMessage>();
|
|
Msg->Request = Request;
|
|
TFuture<TArray<uint8>> Future = Msg->Response.GetFuture();
|
|
|
|
{
|
|
FScopeLock Lock(&GWingServer->Mutex);
|
|
if (GWingServer->bShuttingDown)
|
|
{
|
|
return false;
|
|
}
|
|
GWingServer->PendingMessages.Add(Msg);
|
|
}
|
|
|
|
// Block until the game thread processes this message.
|
|
Response = Future.Get();
|
|
return true;
|
|
}
|
|
|
|
void UWingServer::WaitForAssetRegistry()
|
|
{
|
|
IAssetRegistry& AR =
|
|
FModuleManager::LoadModuleChecked<FAssetRegistryModule>(
|
|
"AssetRegistry").Get();
|
|
while (AR.IsLoadingAssets()) FPlatformProcess::Sleep(0.25f);
|
|
}
|
|
|
|
// ============================================================
|
|
// BuildWingHandlerRegistry
|
|
// ============================================================
|
|
|
|
void UWingServer::AddHandler(UObject* Obj, const FString& Documentation)
|
|
{
|
|
AddHandler(Obj, WingUtils::GetHandlerName(Obj->GetClass()), nullptr, EWingHandlerKind::Normal, nullptr, Documentation);
|
|
}
|
|
|
|
void UWingServer::AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation)
|
|
{
|
|
FWingHandlerConfig H;
|
|
H.Name = Name;
|
|
H.Documentation = Documentation;
|
|
H.HandlerClass = TStrongObjectPtr<UClass>(Obj->GetClass());
|
|
H.Config = TStrongObjectPtr<UObject>(Config);
|
|
H.FactoryClass = TStrongObjectPtr<UClass>(FactoryClass);
|
|
H.Kind = Kind;
|
|
|
|
GWingServer->WingHandlerRegistry.Add(MoveTemp(H));
|
|
}
|
|
|
|
void UWingServer::BuildWingHandlerRegistry()
|
|
{
|
|
WingHandlerRegistry.Empty();
|
|
for (UClass* Class : WingUtils::CollectHandlerClasses())
|
|
{
|
|
UWingHandler* CDO = Cast<UWingHandler>(Class->GetDefaultObject());
|
|
CDO->Register();
|
|
}
|
|
WingHandlerRegistry.Sort([](const FWingHandlerConfig& A, const FWingHandlerConfig& B) { return A.Name < B.Name; });
|
|
}
|
|
|
|
FWingHandlerConfig* UWingServer::FindHandler(const FString& Name)
|
|
{
|
|
int32 Index = Algo::LowerBoundBy(WingHandlerRegistry, Name, [](const FWingHandlerConfig& H) { return H.Name; });
|
|
if (Index < WingHandlerRegistry.Num() && WingHandlerRegistry[Index].Name == Name)
|
|
{
|
|
return &WingHandlerRegistry[Index];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
TStringBuilder<65536> WingOut::StdoutBuffer;
|
|
WingOut WingOut::Stdout(&WingOut::StdoutBuffer);
|
|
WingOut WingOut::None(nullptr);
|