#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 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()); } 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 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 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 UWingServer::HandleRequest(const TArray& RequestBytes) { TArray 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(reinterpret_cast(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 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 HandlerObj(NewObject(GetTransientPackage(), Found->HandlerClass.Get())); UWingHandler* Handler = Cast(HandlerObj.Get()); Handler->Configuration = Found; // Populate the handler object with argv parameters. TArray 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 Client = MakeShared(); 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 Client) { FSocket* Socket = Client->Socket; WaitForAssetRegistry(); TArray Request; if (!ReceiveRequest(Socket, Request)) { Client->bDone = true; return; } TArray 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& RequestBytes, TArray& 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(RequestBytes.GetData() + Offset), Length)); Offset += (int32)Length; } return true; } bool UWingServer::ReceiveRequest(FSocket* Socket, TArray& OutRequest) { constexpr int32 MaxRecvBufBytes = 1024 * 1024; constexpr int32 ChunkSize = 8192; TArray 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& Request, TArray& Response) { // Enqueue the message for game-thread processing. TSharedPtr Msg = MakeShared(); Msg->Request = Request; TFuture> 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( "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(Obj->GetClass()); H.Config = TStrongObjectPtr(Config); H.FactoryClass = TStrongObjectPtr(FactoryClass); H.Kind = Kind; GWingServer->WingHandlerRegistry.Add(MoveTemp(H)); } void UWingServer::BuildWingHandlerRegistry() { WingHandlerRegistry.Empty(); for (UClass* Class : WingUtils::CollectHandlerClasses()) { UWingHandler* CDO = Cast(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);