diff options
author | Erlite <ys.aameziane@gmail.com> | 2022-10-12 02:52:51 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-12 01:52:51 +0100 |
commit | 6b09d99e892fb034c669a55ba2a803c3913fb7cc (patch) | |
tree | 3a4dc273b6454331ccf363da28acca4e38c628e3 | |
parent | b281d36088d1d92acb3db1677881bf984a7d0901 (diff) | |
download | NorthstarLauncher-6b09d99e892fb034c669a55ba2a803c3913fb7cc.tar.gz NorthstarLauncher-6b09d99e892fb034c669a55ba2a803c3913fb7cc.zip |
Fix the MS server registration issues. (#285)v1.9.8-rc3
* Port ms presence reporter to std::async
* Fix crash due to std::optional being assigned nullptr.
* Fix formatting.
* Wait 20 seconds if MS returns DUPLICATE_SERVER.
-rw-r--r-- | NorthstarDLL/masterserver.cpp | 680 | ||||
-rw-r--r-- | NorthstarDLL/masterserver.h | 61 | ||||
-rw-r--r-- | NorthstarDLL/serverpresence.cpp | 4 | ||||
-rw-r--r-- | NorthstarDLL/serverpresence.h | 1 |
4 files changed, 477 insertions, 269 deletions
diff --git a/NorthstarDLL/masterserver.cpp b/NorthstarDLL/masterserver.cpp index b8e00bdc..fc747e5d 100644 --- a/NorthstarDLL/masterserver.cpp +++ b/NorthstarDLL/masterserver.cpp @@ -17,6 +17,8 @@ #include <cstring> #include <regex> +using namespace std::chrono_literals; + MasterServerManager* g_pMasterServerManager; ConVar* Cvar_ns_masterserver_hostname; @@ -52,6 +54,9 @@ void SetCommonHttpClientOptions(CURL* curl) curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); curl_easy_setopt(curl, CURLOPT_VERBOSE, Cvar_ns_curl_log_enable->GetBool()); curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent); + // Timeout since the MS has fucky async functions without await, making curl hang due to a successful connection but no response for ~90 + // seconds. + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); // curl_easy_setopt(curl, CURLOPT_STDERR, stdout); if (Tier0::CommandLine()->FindParm("-msinsecure")) // TODO: this check doesn't seem to work { @@ -750,327 +755,464 @@ void MasterServerManager::WritePlayerPersistentData(const char* playerId, const requestThread.detach(); } -class MasterServerPresenceReporter : public ServerPresenceReporter +void ConCommand_ns_fetchservers(const CCommand& args) +{ + g_pMasterServerManager->RequestServerList(); +} + +MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {} + +ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, (ConCommand, ServerPresence), (CModule module)) { - const int MAX_REGISTRATION_ATTEMPTS = 5; + g_pMasterServerManager = new MasterServerManager; - bool m_bShouldTryRegisterServer; - bool m_bHasAddServerRequest; - int m_nNumRegistrationAttempts; + Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, ""); + Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console"); + + RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL); - void CreatePresence(const ServerPresence* pServerPresence) override + MasterServerPresenceReporter* presenceReporter = new MasterServerPresenceReporter; + g_pServerPresence->AddPresenceReporter(presenceReporter); +} + +void MasterServerPresenceReporter::CreatePresence(const ServerPresence* pServerPresence) +{ + m_nNumRegistrationAttempts = 0; +} + +void MasterServerPresenceReporter::ReportPresence(const ServerPresence* pServerPresence) +{ + // make a copy of presence for multithreading purposes + ServerPresence threadedPresence(pServerPresence); + + if (!*g_pMasterServerManager->m_sOwnServerId) { - m_bShouldTryRegisterServer = true; - m_bHasAddServerRequest = false; - m_nNumRegistrationAttempts = 0; + // Don't try if we've reached the max registration attempts. + // In the future, we should probably allow servers to re-authenticate after a while if the MS was down. + if (m_nNumRegistrationAttempts >= MAX_REGISTRATION_ATTEMPTS) + { + return; + } + + // Make sure to wait til the cooldown is over for DUPLICATE_SERVER failures. + if (Tier0::Plat_FloatTime() < m_fNextAddServerAttemptTime) + { + return; + } + + // If we're not running any InternalAddServer() attempt in the background. + if (!addServerFuture.valid()) + { + // Launch an attempt to add the local server to the master server. + InternalAddServer(pServerPresence); + } + } + else + { + // If we're not running any InternalUpdateServer() attempt in the background. + if (!updateServerFuture.valid()) + { + // Launch an attempt to update the local server on the master server. + InternalUpdateServer(pServerPresence); + } + } +} + +void MasterServerPresenceReporter::DestroyPresence(const ServerPresence* pServerPresence) +{ + // Don't call this if we don't have a server id. + if (!*g_pMasterServerManager->m_sOwnServerId) + { + return; } - void ReportPresence(const ServerPresence* pServerPresence) override + // Not bothering with better thread safety in this case since DestroyPresence() is called when the game is shutting down. + *g_pMasterServerManager->m_sOwnServerId = 0; + + std::thread requestThread( + [this] + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/server/remove_server?id={}", Cvar_ns_masterserver_hostname->GetString(), g_pMasterServerManager->m_sOwnServerId) + .c_str()); + + CURLcode result = curl_easy_perform(curl); + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerPresenceReporter::RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) +{ + // Check if we're already running an InternalAddServer() call in the background. + // If so, grab the result if it's ready. + if (addServerFuture.valid()) { - // make a copy of presence for multithreading purposes - ServerPresence threadedPresence(pServerPresence); + std::future_status status = addServerFuture.wait_for(0ms); + if (status != std::future_status::ready) + { + // Still running, no need to do anything. + return; + } + + // Check the result. + auto resultData = addServerFuture.get(); + + g_pMasterServerManager->m_bSuccessfullyConnected = resultData.result != MasterServerReportPresenceResult::FailedNoConnect; + + switch (resultData.result) + { + case MasterServerReportPresenceResult::Success: + // Copy over the server id and auth token granted by the MS. + strncpy_s( + g_pMasterServerManager->m_sOwnServerId, + sizeof(g_pMasterServerManager->m_sOwnServerId), + resultData.id.value().c_str(), + sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); + strncpy_s( + g_pMasterServerManager->m_sOwnServerAuthToken, + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), + resultData.serverAuthToken.value().c_str(), + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); + break; + case MasterServerReportPresenceResult::FailedNoRetry: + case MasterServerReportPresenceResult::FailedNoConnect: + // If we failed to connect to the master server, or failed with no retry, stop trying. + m_nNumRegistrationAttempts = MAX_REGISTRATION_ATTEMPTS; + break; + case MasterServerReportPresenceResult::Failed: + ++m_nNumRegistrationAttempts; + break; + case MasterServerReportPresenceResult::FailedDuplicateServer: + ++m_nNumRegistrationAttempts; + // Wait at least twenty seconds until we re-attempt to add the server. + m_fNextAddServerAttemptTime = Tier0::Plat_FloatTime() + 20.0f; + break; + } - if (!*g_pMasterServerManager->m_sOwnServerId) + if (m_nNumRegistrationAttempts >= MAX_REGISTRATION_ATTEMPTS) { - if (m_bShouldTryRegisterServer && !m_bHasAddServerRequest) + spdlog::error("Reached max ms server registration attempts."); + } + } + else if (updateServerFuture.valid()) + { + // Check if the InternalUpdateServer() call completed. + std::future_status status = updateServerFuture.wait_for(0ms); + if (status != std::future_status::ready) + { + // Still running, no need to do anything. + return; + } + + auto resultData = updateServerFuture.get(); + if (resultData.result == MasterServerReportPresenceResult::Success) + { + if (resultData.id) { - // add server - std::thread addServerThread( - [this, threadedPresence] - { - g_pMasterServerManager->m_sOwnServerId[0] = 0; - g_pMasterServerManager->m_sOwnServerAuthToken[0] = 0; + strncpy_s( + g_pMasterServerManager->m_sOwnServerId, + sizeof(g_pMasterServerManager->m_sOwnServerId), + resultData.id.value().c_str(), + sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); + } - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); + if (resultData.serverAuthToken) + { + strncpy_s( + g_pMasterServerManager->m_sOwnServerAuthToken, + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), + resultData.serverAuthToken.value().c_str(), + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); + } + } + } +} - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); +void MasterServerPresenceReporter::InternalAddServer(const ServerPresence* pServerPresence) +{ + const ServerPresence threadedPresence(pServerPresence); + // Never call this with an ongoing InternalAddServer() call. + assert(!addServerFuture.valid()); - curl_mime* mime = curl_mime_init(curl); - curl_mimepart* part = curl_mime_addpart(mime); + g_pMasterServerManager->m_sOwnServerId[0] = 0; + g_pMasterServerManager->m_sOwnServerAuthToken[0] = 0; - curl_mime_data( - part, g_pMasterServerManager->m_sOwnModInfoJson.c_str(), g_pMasterServerManager->m_sOwnModInfoJson.size()); - curl_mime_name(part, "modinfo"); - curl_mime_filename(part, "modinfo.json"); - curl_mime_type(part, "application/json"); + std::string modInfo = g_pMasterServerManager->m_sOwnModInfoJson; + std::string hostname = Cvar_ns_masterserver_hostname->GetString(); - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); + spdlog::info("Attempting to register the local server to the master server."); - // format every paramter because computers hate me - { - char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); - char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); - char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); - char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); - char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/" - "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", - Cvar_ns_masterserver_hostname->GetString(), - threadedPresence.m_iPort, - threadedPresence.m_iAuthPort, - nameEscaped, - descEscaped, - mapEscaped, - playlistEscaped, - threadedPresence.m_iMaxPlayers, - passwordEscaped) - .c_str()); - - curl_free(nameEscaped); - curl_free(descEscaped); - curl_free(mapEscaped); - curl_free(playlistEscaped); - curl_free(passwordEscaped); - } + addServerFuture = std::async( + std::launch::async, + [threadedPresence, modInfo, hostname] + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); - CURLcode result = curl_easy_perform(curl); + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - if (result == CURLcode::CURLE_OK) - { - g_pMasterServerManager->m_bSuccessfullyConnected = true; - - rapidjson_document serverAddedJson; - serverAddedJson.Parse(readBuffer.c_str()); - - if (serverAddedJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver authentication response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(serverAddedJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!serverAddedJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } - - if (serverAddedJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - - if (serverAddedJson["error"].HasMember("enum") && - !strcmp(serverAddedJson["error"]["enum"].GetString(), "DUPLICATE_SERVER")) - { - if (++m_nNumRegistrationAttempts == MAX_REGISTRATION_ATTEMPTS) - m_bShouldTryRegisterServer = false; - - goto REQUEST_END_CLEANUP_RETRY; - } - - goto REQUEST_END_CLEANUP; - } - - if (!serverAddedJson["success"].IsTrue()) - { - spdlog::error("Adding server to masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } - - if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString() || - !serverAddedJson.HasMember("serverAuthToken") || !serverAddedJson["serverAuthToken"].IsString()) - { - spdlog::error("Failed reading masterserver response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - strncpy_s( - g_pMasterServerManager->m_sOwnServerId, - sizeof(g_pMasterServerManager->m_sOwnServerId), - serverAddedJson["id"].GetString(), - sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); - - strncpy_s( - g_pMasterServerManager->m_sOwnServerAuthToken, - sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), - serverAddedJson["serverAuthToken"].GetString(), - sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); - } - else - { - spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result)); - g_pMasterServerManager->m_bSuccessfullyConnected = false; - } + curl_mime* mime = curl_mime_init(curl); + curl_mimepart* part = curl_mime_addpart(mime); + + // Lambda to quickly cleanup resources and return a value. + auto ReturnCleanup = + [curl, mime](MasterServerReportPresenceResult result, const char* id = "", const char* serverAuthToken = "") + { + curl_easy_cleanup(curl); + curl_mime_free(mime); + + MasterServerPresenceReporter::ReportPresenceResultData data; + data.result = result; + data.id = id; + data.serverAuthToken = serverAuthToken; + + return data; + }; + + curl_mime_data(part, modInfo.c_str(), modInfo.size()); + curl_mime_name(part, "modinfo"); + curl_mime_filename(part, "modinfo.json"); + curl_mime_type(part, "application/json"); - REQUEST_END_CLEANUP: - m_bShouldTryRegisterServer = false; + curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); - REQUEST_END_CLEANUP_RETRY: - m_bHasAddServerRequest = false; + // format every paramter because computers hate me + { + char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); + char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); + char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); + char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); + char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); - curl_easy_cleanup(curl); - curl_mime_free(mime); - }); + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/server/" + "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", + hostname.c_str(), + threadedPresence.m_iPort, + threadedPresence.m_iAuthPort, + nameEscaped, + descEscaped, + mapEscaped, + playlistEscaped, + threadedPresence.m_iMaxPlayers, + passwordEscaped) + .c_str()); - m_bHasAddServerRequest = true; - addServerThread.detach(); + curl_free(nameEscaped); + curl_free(descEscaped); + curl_free(mapEscaped); + curl_free(playlistEscaped); + curl_free(passwordEscaped); } - } - else - { - // update server - std::thread updateServerThread( - [threadedPresence] + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + { + rapidjson_document serverAddedJson; + serverAddedJson.Parse(readBuffer.c_str()); + + // If we could not parse the JSON or it isn't an object, assume the MS is either wrong or we're completely out of date. + // No retry. + if (serverAddedJson.HasParseError()) { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); + spdlog::error( + "Failed reading masterserver authentication response: encountered parse error \"{}\"", + rapidjson::GetParseError_En(serverAddedJson.GetParseError())); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); + } - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L); + if (!serverAddedJson.IsObject()) + { + spdlog::error("Failed reading masterserver authentication response: root object is not an object"); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); + } - // send all registration info so we have all necessary info to reregister our server if masterserver goes down, - // without a restart this isn't threadsafe :terror: + if (serverAddedJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + + // If this is DUPLICATE_SERVER, we'll retry adding the server every 20 seconds. + // The master server will only update its internal server list and clean up dead servers on certain events. + // And then again, only if a player requests the server list after the cooldown (1 second by default), or a server is + // added/updated/removed. In any case this needs to be fixed in the master server rewrite. + if (serverAddedJson["error"].HasMember("enum") && + strcmp(serverAddedJson["error"]["enum"].GetString(), "DUPLICATE_SERVER") == 0) { - char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); - char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); - char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); - char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); - char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/" - "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&" - "maxPlayers={}&password={}", - Cvar_ns_masterserver_hostname->GetString(), - g_pMasterServerManager->m_sOwnServerId, - threadedPresence.m_iPort, - threadedPresence.m_iAuthPort, - nameEscaped, - descEscaped, - mapEscaped, - playlistEscaped, - threadedPresence.m_iPlayerCount, - threadedPresence.m_iMaxPlayers, - passwordEscaped) - .c_str()); - - curl_free(nameEscaped); - curl_free(descEscaped); - curl_free(mapEscaped); - curl_free(playlistEscaped); - curl_free(passwordEscaped); + spdlog::error("Cooling down while the master server cleans the dead server entry, if any."); + return ReturnCleanup(MasterServerReportPresenceResult::FailedDuplicateServer); } - curl_mime* mime = curl_mime_init(curl); - curl_mimepart* part = curl_mime_addpart(mime); + // Retry until we reach max retries. + return ReturnCleanup(MasterServerReportPresenceResult::Failed); + } - curl_mime_data( - part, g_pMasterServerManager->m_sOwnModInfoJson.c_str(), g_pMasterServerManager->m_sOwnModInfoJson.size()); - curl_mime_name(part, "modinfo"); - curl_mime_filename(part, "modinfo.json"); - curl_mime_type(part, "application/json"); + if (!serverAddedJson["success"].IsTrue()) + { + spdlog::error("Adding server to masterserver failed: \"success\" is not true"); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); + } - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); + if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString() || + !serverAddedJson.HasMember("serverAuthToken") || !serverAddedJson["serverAuthToken"].IsString()) + { + spdlog::error("Failed reading masterserver response: malformed json object"); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); + } - CURLcode result = curl_easy_perform(curl); + spdlog::info("Successfully registered the local server to the master server."); + return ReturnCleanup( + MasterServerReportPresenceResult::Success, + serverAddedJson["id"].GetString(), + serverAddedJson["serverAuthToken"].GetString()); + } + else + { + spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result)); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoConnect); + } + }); +} - if (result == CURLcode::CURLE_OK) - { - rapidjson_document serverAddedJson; - serverAddedJson.Parse(readBuffer.c_str()); +void MasterServerPresenceReporter::InternalUpdateServer(const ServerPresence* pServerPresence) +{ + const ServerPresence threadedPresence(pServerPresence); - if (!serverAddedJson.HasParseError() && serverAddedJson.IsObject()) - { - if (serverAddedJson.HasMember("id") && serverAddedJson["id"].IsString()) - { - strncpy_s( - g_pMasterServerManager->m_sOwnServerId, - sizeof(g_pMasterServerManager->m_sOwnServerId), - serverAddedJson["id"].GetString(), - sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); - } - - if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString()) - { - - strncpy_s( - g_pMasterServerManager->m_sOwnServerAuthToken, - sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), - serverAddedJson["serverAuthToken"].GetString(), - sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); - } - } - } - else - spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result)); + // Never call this with an ongoing InternalUpdateServer() call. + assert(!updateServerFuture.valid()); - curl_easy_cleanup(curl); - }); - updateServerThread.detach(); - } - } + const std::string serverId = g_pMasterServerManager->m_sOwnServerId; + const std::string hostname = Cvar_ns_masterserver_hostname->GetString(); + const std::string modinfo = g_pMasterServerManager->m_sOwnModInfoJson; - void DestroyPresence(const ServerPresence* pServerPresence) override - { - // dont call this if we don't have a server id - if (!*g_pMasterServerManager->m_sOwnServerId) - return; + updateServerFuture = std::async( + std::launch::async, + [threadedPresence, serverId, hostname, modinfo] + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); - std::thread requestThread( - [this] + // Lambda to quickly cleanup resources and return a value. + auto ReturnCleanup = [curl](MasterServerReportPresenceResult result, const char* id = "", const char* serverAuthToken = "") { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); + curl_easy_cleanup(curl); + + MasterServerPresenceReporter::ReportPresenceResultData data; + data.result = result; + + if (id != nullptr) + { + data.id = id; + } + + if (serverAuthToken != nullptr) + { + data.serverAuthToken = serverAuthToken; + } + + return data; + }; + + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L); + + // send all registration info so we have all necessary info to reregister our server if masterserver goes down, + // without a restart this isn't threadsafe :terror: + { + char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); + char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); + char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); + char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); + char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); curl_easy_setopt( curl, CURLOPT_URL, fmt::format( - "{}/server/remove_server?id={}", Cvar_ns_masterserver_hostname->GetString(), g_pMasterServerManager->m_sOwnServerId) + "{}/server/" + "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&" + "maxPlayers={}&password={}", + hostname.c_str(), + serverId.c_str(), + threadedPresence.m_iPort, + threadedPresence.m_iAuthPort, + nameEscaped, + descEscaped, + mapEscaped, + playlistEscaped, + threadedPresence.m_iPlayerCount, + threadedPresence.m_iMaxPlayers, + passwordEscaped) .c_str()); - *g_pMasterServerManager->m_sOwnServerId = 0; - CURLcode result = curl_easy_perform(curl); + curl_free(nameEscaped); + curl_free(descEscaped); + curl_free(mapEscaped); + curl_free(playlistEscaped); + curl_free(passwordEscaped); + } - if (result == CURLcode::CURLE_OK) - g_pMasterServerManager->m_bSuccessfullyConnected = true; - else - g_pMasterServerManager->m_bSuccessfullyConnected = false; + curl_mime* mime = curl_mime_init(curl); + curl_mimepart* part = curl_mime_addpart(mime); - curl_easy_cleanup(curl); - }); + curl_mime_data(part, modinfo.c_str(), modinfo.size()); + curl_mime_name(part, "modinfo"); + curl_mime_filename(part, "modinfo.json"); + curl_mime_type(part, "application/json"); - requestThread.detach(); - } -}; + curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); -void ConCommand_ns_fetchservers(const CCommand& args) -{ - g_pMasterServerManager->RequestServerList(); -} + CURLcode result = curl_easy_perform(curl); -MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {} + if (result == CURLcode::CURLE_OK) + { + rapidjson_document serverAddedJson; + serverAddedJson.Parse(readBuffer.c_str()); -ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, (ConCommand, ServerPresence), (CModule module)) -{ - g_pMasterServerManager = new MasterServerManager; + const char* updatedId = nullptr; + const char* updatedAuthToken = nullptr; - Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, ""); - Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console"); + if (!serverAddedJson.HasParseError() && serverAddedJson.IsObject()) + { + if (serverAddedJson.HasMember("id") && serverAddedJson["id"].IsString()) + { + updatedId = serverAddedJson["id"].GetString(); + } - RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL); + if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString()) + { + updatedAuthToken = serverAddedJson["serverAuthToken"].GetString(); + } + } - MasterServerPresenceReporter* presenceReporter = new MasterServerPresenceReporter; - g_pServerPresence->AddPresenceReporter(presenceReporter); + return ReturnCleanup(MasterServerReportPresenceResult::Success, updatedId, updatedAuthToken); + } + else + { + spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result)); + return ReturnCleanup(MasterServerReportPresenceResult::Failed); + } + }); } diff --git a/NorthstarDLL/masterserver.h b/NorthstarDLL/masterserver.h index 5a4e2a91..0bbf7c76 100644 --- a/NorthstarDLL/masterserver.h +++ b/NorthstarDLL/masterserver.h @@ -1,9 +1,11 @@ #pragma once + #include "convar.h" #include "serverpresence.h" #include <winsock2.h> #include <string> #include <cstring> +#include <future> extern ConVar* Cvar_ns_masterserver_hostname; extern ConVar* Cvar_ns_curl_log_enable; @@ -124,3 +126,62 @@ class MasterServerManager extern MasterServerManager* g_pMasterServerManager; extern ConVar* Cvar_ns_masterserver_hostname; + +/** Result returned in the std::future of a MasterServerPresenceReporter::ReportPresence() call. */ +enum class MasterServerReportPresenceResult +{ + // Adding this server to the MS was successful. + Success, + // We failed to add this server to the MS and should retry. + Failed, + // We failed to add this server to the MS and shouldn't retry. + FailedNoRetry, + // We failed to even reach the MS. + FailedNoConnect, + // We failed to add the server because an existing server with the same ip:port exists. + FailedDuplicateServer, +}; + +class MasterServerPresenceReporter : public ServerPresenceReporter +{ + public: + /** Full data returned in the std::future of a MasterServerPresenceReporter::ReportPresence() call. */ + struct ReportPresenceResultData + { + MasterServerReportPresenceResult result; + + std::optional<std::string> id; + std::optional<std::string> serverAuthToken; + }; + + const int MAX_REGISTRATION_ATTEMPTS = 5; + + // Called to initialise the master server presence reporter's state. + void CreatePresence(const ServerPresence* pServerPresence) override; + + // Run on an internal to either add the server to the MS or update it. + void ReportPresence(const ServerPresence* pServerPresence) override; + + // Called when we need to remove the server from the master server. + void DestroyPresence(const ServerPresence* pServerPresence) override; + + // Called every frame. + void RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) override; + + protected: + // Contains the async logic to add the server to the MS. + void InternalAddServer(const ServerPresence* pServerPresence); + + // Contains the async logic to update the server on the MS. + void InternalUpdateServer(const ServerPresence* pServerPresence); + + // The future used for InternalAddServer() calls. + std::future<ReportPresenceResultData> addServerFuture; + + // The future used for InternalAddServer() calls. + std::future<ReportPresenceResultData> updateServerFuture; + + int m_nNumRegistrationAttempts; + + double m_fNextAddServerAttemptTime; +}; diff --git a/NorthstarDLL/serverpresence.cpp b/NorthstarDLL/serverpresence.cpp index 9b7b0f1a..fb8cf624 100644 --- a/NorthstarDLL/serverpresence.cpp +++ b/NorthstarDLL/serverpresence.cpp @@ -141,6 +141,10 @@ void ServerPresenceManager::RunFrame(double flCurrentTime) if (m_ServerPresence.m_bIsSingleplayerServer && !Cvar_ns_report_sp_server_to_masterserver->GetBool()) return; + // Call RunFrame() so that reporters can, for example, handle std::future results as soon as they arrive. + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->RunFrame(flCurrentTime, &m_ServerPresence); + // run on a specified delay if ((flCurrentTime - m_flLastPresenceUpdate) * 1000 < Cvar_ns_server_presence_update_rate->GetFloat()) return; diff --git a/NorthstarDLL/serverpresence.h b/NorthstarDLL/serverpresence.h index f27c9393..97b4654c 100644 --- a/NorthstarDLL/serverpresence.h +++ b/NorthstarDLL/serverpresence.h @@ -45,6 +45,7 @@ class ServerPresenceReporter virtual void CreatePresence(const ServerPresence* pServerPresence) {} virtual void ReportPresence(const ServerPresence* pServerPresence) {} virtual void DestroyPresence(const ServerPresence* pServerPresence) {} + virtual void RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) {} }; class ServerPresenceManager |