diff --git a/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp b/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp index 6b0350a3..41de3313 100644 --- a/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp +++ b/Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp @@ -1,5 +1,6 @@ #include "FicsitRemoteMonitoring.h" +#include #include #include "Runtime/Core/Public/Logging/LogCategory.h" @@ -37,6 +38,36 @@ us_listen_socket_t* SocketListener; bool SocketRunning = false; +// Set as soon as teardown begins, cleared when a server starts. Deliberately a +// file-scope atomic rather than a member: once teardown starts, the subsystem +// actor and the world are being destroyed, so the uWS worker thread cannot +// safely touch `this`, the World, or any UObject — not even to call IsValid(), +// which must dereference the object to read its flags. A dangling pointer is +// not null, so the IsValid() guards inside CallEndpoint() are reached and then +// fault. This flag is the only state a request handler can consult without +// dereferencing anything that may already be freed. +std::atomic GFRMShuttingDown{false}; + +// Shutdown barrier for uWS request handlers. Answers 503 and reports true when +// teardown has begun, so the caller returns before touching the subsystem, the +// world, or any other UObject. Touches nothing but the response socket, which +// uWS owns and keeps alive for the duration of the callback. +static bool FRMRejectIfShuttingDown(uWS::HttpResponse* Res) +{ + if (!GFRMShuttingDown.load(std::memory_order_acquire)) + { + return false; + } + + if (Res) + { + Res->writeStatus("503 Service Unavailable"); + Res->end("{\"error\":\"Server is shutting down\"}"); + } + + return true; +} + AFicsitRemoteMonitoring* AFicsitRemoteMonitoring::Get(UWorld* WorldContext) { for (TActorIterator It(WorldContext, StaticClass(), EActorIteratorFlags::AllActors); It; ++It) { @@ -65,23 +96,33 @@ void AFicsitRemoteMonitoring::BeginPlay() // Load FRM's API Endpoints InitAPIRegistry(); + // Read the auth token ONCE here, on the game thread, and cache it in the + // AuthenticationToken member. The uWS request handlers must not read config + // per-request: that runs on the uWS worker thread and reaches GEngine, which + // is unsafe while the engine is tearing down (observed SIGSEGV). const FString AuthToken = UFRMConfigManager::GetConfigOrDefault(TEXT("uWS.AuthenticationToken"), ""); - + // Debug log to verify token retrieval -Porisius // UE_LOGFMT(LogHttpServer, Log, "DEBUG: AuthToken - {AuthToken}", *AuthToken); - + if (AuthToken.IsEmpty()) { - if (!UFRMConfigManager::SetConfigFromInput(TEXT("uWS.AuthenticationToken"), GenerateAuthToken(32), false)) + const FString GeneratedToken = GenerateAuthToken(32); + if (!UFRMConfigManager::SetConfigFromInput(TEXT("uWS.AuthenticationToken"), GeneratedToken, false)) { UE_LOG(LogTemp, Warning, TEXT("Failed to apply setting")); return; } - UE_LOG(LogTemp, Log, TEXT("Generated and saved new token: %s"), *AuthToken); + AuthenticationToken = GeneratedToken; + + // Log the token that was actually generated. This previously logged + // AuthToken, which is empty on this branch by definition. + UE_LOG(LogTemp, Log, TEXT("Generated and saved new token: %s"), *GeneratedToken); } else { + AuthenticationToken = AuthToken; UE_LOG(LogTemp, Log, TEXT("Token already exists.")); } @@ -128,6 +169,11 @@ void AFicsitRemoteMonitoring::EndPlay(const EEndPlayReason::Type EndPlayReason) void AFicsitRemoteMonitoring::StopWebSocketServer() { + // Raise the shutdown barrier FIRST, before anything is torn down, so any + // request already inside the uWS worker thread bails out with 503 instead + // of walking into a half-destroyed world. + GFRMShuttingDown.store(true, std::memory_order_release); + bShouldStop = true; // Signal the WebSocket server to stop @@ -168,10 +214,15 @@ FArduinoConfig AFicsitRemoteMonitoring::GetSerialConfig() return Config; } -void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) +void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) { UE_LOGFMT(LogHttpServer, Log, "Initializing WebSocket Service"); + // Lower the shutdown barrier: a previous StopWebSocketServer() raised it, and + // without this a server restarted in the same session (e.g. `/frm http start` + // after a stop) would answer 503 to every request forever. + GFRMShuttingDown.store(false, std::memory_order_release); + if (SocketRunning) { if (bSkipIfRunning) @@ -290,13 +341,14 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) }); app.get("/api/:APIEndpoint", [this, World](auto* res, auto* req) { + if (FRMRejectIfShuttingDown(res)) return; std::string url(req->getParameter("APIEndpoint")); FString Endpoint = FString(url.c_str()); // Log the request URL //UE_LOGFMT(LogHttpServer, Log, "Request URL: {0}", Endpoint); - const FString AuthToken = UFRMConfigManager::GetConfigOrDefault(TEXT("uWS.AuthenticationToken"), ""); + const FString AuthToken = AuthenticationToken; FRequestData RequestData; RequestData.bIsAuthorized = IsAuthorizedRequest(req, AuthToken); @@ -313,11 +365,13 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) app.post("/*", [this, World](auto* res, uWS::HttpRequest* req) { + if (FRMRejectIfShuttingDown(res)) return; const std::string URL(req->getUrl().begin(), req->getUrl().end()); FString RelativePath = FString(URL.c_str()).Mid(1); res->onData([this, res, req, World, RelativePath](const std::string_view data, bool) { + if (FRMRejectIfShuttingDown(res)) return; try { const std::string PostData(data); @@ -330,7 +384,7 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) return UFRM_RequestLibrary::SendErrorMessage(res, "400 Bad Request", FString("Invalid Request Body")); } - const FString AuthToken = UFRMConfigManager::GetConfigOrDefault(TEXT("uWS.AuthenticationToken"), ""); + const FString AuthToken = AuthenticationToken; FRequestData RequestData; RequestData.Method = "POST"; @@ -363,11 +417,12 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning) }); app.get("/*", [this, UIPath, World](auto* res, uWS::HttpRequest* req) { + if (FRMRejectIfShuttingDown(res)) return; if (!res) return; std::string url(req->getUrl().begin(), req->getUrl().end()); - const FString AuthToken = UFRMConfigManager::GetConfigOrDefault(TEXT("uWS.AuthenticationToken"), ""); + const FString AuthToken = AuthenticationToken; bool bFileExists = false; // Remove initial '/' diff --git a/Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h b/Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h index eb5ffe76..93d076a3 100644 --- a/Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h +++ b/Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h @@ -22,6 +22,17 @@ class FICSITREMOTEMONITORING_API UFRMConfigManager : public UBlueprintFunctionLi template static bool GetConfig(const FString& StrID, T& OutValue) { + // GetFGGameUserSettings() dereferences GEngine internally, so GEngine must + // be checked BEFORE the call — the UserSettings null-check below happens + // too late to help. During engine teardown GEngine is destroyed while the + // uWS worker thread may still be serving a request, and the dereference + // faults (SIGSEGV reading a small member offset). Degrade to the caller's + // default instead of crashing the server. + if (!GEngine) + { + return false; + } + UFGGameUserSettings* UserSettings = UFGGameUserSettings::GetFGGameUserSettings(); if (!UserSettings) {