From ba4ef61c15a303e35397dadce0507ad2e22b81a8 Mon Sep 17 00:00:00 2001 From: Ralph Sennhauser Date: Thu, 24 Oct 2024 19:58:16 +0200 Subject: [PATCH] Use cpp-httplib instead of mongoose Use cpp-httplib for Profiler2 and RLInterface instead of mongoose. Signed-off-by: Ralph Sennhauser --- build/premake/premake5.lua | 4 + .../lib/sysdep/os/win/wposix/wposix_types.h | 4 +- source/main.cpp | 2 +- source/network/HttpServer.cpp | 62 ++++ source/network/HttpServer.h | 30 ++ source/ps/GameSetup/GameSetup.cpp | 1 + source/ps/Profiler2.cpp | 191 ++++------ source/ps/Profiler2.h | 8 +- source/rlinterface/RLInterface.cpp | 325 +++++++----------- source/rlinterface/RLInterface.h | 14 +- 10 files changed, 307 insertions(+), 334 deletions(-) create mode 100644 source/network/HttpServer.cpp create mode 100644 source/network/HttpServer.h diff --git a/build/premake/premake5.lua b/build/premake/premake5.lua index d87b9c2851..24bc1046e8 100644 --- a/build/premake/premake5.lua +++ b/build/premake/premake5.lua @@ -710,6 +710,7 @@ function setup_all_libs () "network", } extern_libs = { + "cpp_httplib", "spidermonkey", "enet", "sdl", @@ -730,6 +731,7 @@ function setup_all_libs () "boost", -- dragged in via simulation.h and scriptinterface.h "fmt", "spidermonkey", + "cpp_httplib", } setup_static_lib_project("rlinterface", source_dirs, extern_libs, { no_pch = 1 }) @@ -872,6 +874,7 @@ function setup_all_libs () "libpng", "fmt", "freetype", + "cpp_httplib", } @@ -1108,6 +1111,7 @@ used_extern_libs = { "libxml2", "boost", + "cpp_httplib", "cxxtest", "comsuppw", "enet", diff --git a/source/lib/sysdep/os/win/wposix/wposix_types.h b/source/lib/sysdep/os/win/wposix/wposix_types.h index d6e0ad547a..d466a34f5a 100644 --- a/source/lib/sysdep/os/win/wposix/wposix_types.h +++ b/source/lib/sysdep/os/win/wposix/wposix_types.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -36,6 +36,8 @@ typedef intptr_t ssize_t; // prevent wxWidgets from (incompatibly) redefining it #define HAVE_SSIZE_T +// prevent cpp-httplib from (incompatibly) redefining it +#define _SSIZE_T_DEFINED // VC9 defines off_t as long, but we need 64-bit file offsets even in // 32-bit builds. to avoid conflicts, we have to define _OFF_T_DEFINED, diff --git a/source/main.cpp b/source/main.cpp index 24432f8f69..d97461d808 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -537,7 +537,7 @@ static std::optional CreateRLInterface(const CmdLineArgs& args) g_ConfigDB.Get("rlinterface.address", std::string{}) : args.Get("rl-interface")}; debug_printf("RL interface listening on %s\n", server_address.c_str()); - return std::make_optional(server_address.c_str()); + return std::make_optional(server_address); } #if CONFIG2_DAP_INTERFACE diff --git a/source/network/HttpServer.cpp b/source/network/HttpServer.cpp new file mode 100644 index 0000000000..62e30fa55f --- /dev/null +++ b/source/network/HttpServer.cpp @@ -0,0 +1,62 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "HttpServer.h" + +#include "ps/Future.h" +#include "ps/TaskManager.h" + +#include +#include + +namespace +{ +class TaskQueueAdapter : public httplib::TaskQueue +{ +public: + bool enqueue(std::function fn) override + { + // Remove finished + while (!m_Futures.empty() && m_Futures.front().IsDone()) + { + m_Futures.front().Get(); + m_Futures.pop(); + } + + m_Futures.push({g_TaskManager, fn}); + return true; + } + + void shutdown() override + { + m_Futures = {}; + } +private: + std::queue> m_Futures; +}; +} // namespace + +namespace PS::Net +{ +std::unique_ptr createHttpServer() { + auto server = std::make_unique(); + server->new_task_queue = [] { return new TaskQueueAdapter(); }; + return server; +} +} // namespace PS::Net diff --git a/source/network/HttpServer.h b/source/network/HttpServer.h new file mode 100644 index 0000000000..a2d8f171b4 --- /dev/null +++ b/source/network/HttpServer.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef HTTPSEVER_H +#define HTTPSEVER_H + +#include + +namespace httplib { class Server; } + +namespace PS::Net +{ +std::unique_ptr createHttpServer(); +} + +#endif diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 2c41194476..d2055d1aea 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -381,6 +381,7 @@ void ShutdownNetworkAndUI() g_RenderingOptions.ClearHooks(); + g_Profiler2.ShutDownHTTP(); g_Profiler2.ShutdownGPU(); if (hasRenderer) diff --git a/source/ps/Profiler2.cpp b/source/ps/Profiler2.cpp index b7334bd2b3..f1fbaf3808 100644 --- a/source/ps/Profiler2.cpp +++ b/source/ps/Profiler2.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -28,6 +28,7 @@ #include "lib/code_generation.h" #include "lib/os_path.h" #include "lib/path.h" +#include "network/HttpServer.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/ConfigDB.h" @@ -35,13 +36,13 @@ #include "ps/Profiler2GPU.h" #include "ps/Pyrogenesis.h" #include "ps/TaskManager.h" -#include "third_party/mongoose/mongoose.h" #include #include #include #include #include +#include #include #include #include @@ -64,7 +65,7 @@ const u8 CProfiler2::RESYNC_MAGIC[8] = {0x11, 0x22, 0x33, 0x44, 0xf4, 0x93, 0xbe thread_local CProfiler2::ThreadStorage* CProfiler2::m_CurrentStorage = nullptr; CProfiler2::CProfiler2() : - m_Initialised(false), m_FrameNumber(0), m_MgContext(NULL), m_GPU(NULL) + m_Initialised{false}, m_FrameNumber{0}, m_HttpServer{nullptr}, m_HttpServerThread{}, m_GPU{nullptr} { } @@ -74,103 +75,6 @@ CProfiler2::~CProfiler2() Shutdown(); } -/** - * Mongoose callback. Run in an arbitrary thread (possibly concurrently with other requests). - */ -static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) -{ - CProfiler2* profiler = (CProfiler2*)request_info->user_data; - ENSURE(profiler); - - void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling - - const char* header200 = - "HTTP/1.1 200 OK\r\n" - "Access-Control-Allow-Origin: *\r\n" // TODO: not great for security - "Content-Type: text/plain; charset=utf-8\r\n\r\n"; - - const char* header404 = - "HTTP/1.1 404 Not Found\r\n" - "Content-Type: text/plain; charset=utf-8\r\n\r\n" - "Unrecognised URI"; - - const char* header400 = - "HTTP/1.1 400 Bad Request\r\n" - "Content-Type: text/plain; charset=utf-8\r\n\r\n" - "Invalid request"; - - switch (event) - { - case MG_NEW_REQUEST: - { - std::stringstream stream; - - std::string uri = request_info->uri; - - if (uri == "/download") - Future{g_TaskManager, std::bind_front(&CProfiler2::SaveToFile, profiler)}.Get(); - else if (uri == "/overview") - { - Future{g_TaskManager, - std::bind_front(&CProfiler2::ConstructJSONOverview, profiler, std::ref(stream))}.Get(); - } - else if (uri == "/query") - { - if (!request_info->query_string) - { - mg_printf(conn, "%s (no query string)", header400); - return handled; - } - - // Identify the requested thread - char buf[256]; - int len = mg_get_var(request_info->query_string, strlen(request_info->query_string), "thread", buf, ARRAY_SIZE(buf)); - if (len < 0) - { - mg_printf(conn, "%s (no 'thread')", header400); - return handled; - } - std::string thread(buf); - - const char* err = Future{g_TaskManager, - std::bind_front(&CProfiler2::ConstructJSONResponse, profiler, std::ref(stream), - std::ref(thread))}.Get(); - - if (err) - { - mg_printf(conn, "%s (%s)", header400, err); - return handled; - } - } - else - { - mg_printf(conn, "%s", header404); - return handled; - } - - mg_printf(conn, "%s", header200); - std::string str = stream.str(); - mg_write(conn, str.c_str(), str.length()); - return handled; - } - - case MG_HTTP_ERROR: - return NULL; - - case MG_EVENT_LOG: - // Called by Mongoose's cry() - LOGERROR("Mongoose error: %s", request_info->log_message); - return NULL; - - case MG_INIT_SSL: - return NULL; - - default: - debug_warn(L"Invalid Mongoose event type"); - return NULL; - } -}; - void CProfiler2::Initialise() { ENSURE(!m_Initialised); @@ -190,23 +94,53 @@ void CProfiler2::EnableHTTP() ENSURE(m_Initialised); LOGMESSAGERENDER("Starting profiler2 HTTP server"); + std::lock_guard lock{m_Mutex}; + // Ignore multiple enablings - if (m_MgContext) + if (m_HttpServer) return; - using namespace std::literals; - const std::string listeningPort{CConfigDB::GetIfInitialised("profiler2.server.port", "8000"s)}; - const std::string listeningServer{CConfigDB::GetIfInitialised("profiler2.server", "127.0.0.1"s)}; - const std::string numThreads{CConfigDB::GetIfInitialised("profiler2.server.threads", "6"s)}; + m_HttpServer = PS::Net::createHttpServer(); - std::string listening_ports = fmt::format("{0}:{1}", listeningServer, listeningPort); - const char* options[] = { - "listening_ports", listening_ports.c_str(), - "num_threads", numThreads.c_str(), - nullptr - }; - m_MgContext = mg_start(MgCallback, this, options); - ENSURE(m_MgContext); + m_HttpServer->Get("/download", [this](const httplib::Request &, httplib::Response &) { + SaveToFile(); + }); + + m_HttpServer->Get("/overview", [this](const httplib::Request &, httplib::Response &res) { + std::stringstream stream; + ConstructJSONOverview(stream); + res.set_content(stream.str(), "application/json"); + }); + + m_HttpServer->Get("/query", [this](const httplib::Request &req, httplib::Response &res) { + if (!req.has_param("thread")) + { + res.set_content("Request \"query\" needs parameter \"thread\"", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + + std::string thread = req.get_param_value("thread"); + std::stringstream stream; + ConstructJSONResponse(stream, thread); + res.set_content(stream.str(), "application/json"); + }); + + m_HttpServer->set_post_routing_handler([](const httplib::Request&, httplib::Response& res) { + // TODO: Not ideal for security reasons + res.set_header("Access-Control-Allow-Origin", "*"); + }); + + m_HttpServerThread = std::thread([this](){ + using namespace std::literals; + const int listeningPort{CConfigDB::GetIfInitialised("profiler2.server.port", 8000)}; + const std::string listeningServer{CConfigDB::GetIfInitialised("profiler2.server", "127.0.0.1"s)}; + + if (!m_HttpServer->listen(listeningServer, listeningPort)) + { + LOGERROR("Failed to start http server"); + } + }); } void CProfiler2::EnableGPU() @@ -228,26 +162,34 @@ void CProfiler2::ShutdownGPU() void CProfiler2::ShutDownHTTP() { LOGMESSAGERENDER("Shutting down profiler2 HTTP server"); - if (m_MgContext) - { - mg_stop(m_MgContext); - m_MgContext = NULL; - } + std::lock_guard lock{m_Mutex}; + + if (!m_HttpServer) + return; + + m_HttpServer->stop(); + if(m_HttpServerThread.joinable()) + m_HttpServerThread.join(); + m_HttpServer.reset(); } void CProfiler2::Toggle() { - // TODO: Maybe we can open the browser to the profiler page automatically - if (m_GPU && m_MgContext) + LOGMESSAGERENDER("Toggle profiler http"); + if (m_GPU && m_HttpServer) { ShutdownGPU(); ShutDownHTTP(); } - else if (!m_GPU && !m_MgContext) + else if (!m_GPU && !m_HttpServer) { EnableGPU(); EnableHTTP(); } + else + { + LOGMESSAGERENDER("Toggle profile bad state!"); + } } void CProfiler2::Shutdown() @@ -255,12 +197,7 @@ void CProfiler2::Shutdown() ENSURE(m_Initialised); ENSURE(!m_GPU); // must shutdown GPU before profiler - - if (m_MgContext) - { - mg_stop(m_MgContext); - m_MgContext = NULL; - } + ENSURE(!m_HttpServer); // must shutdown HTTP server before profiler // the destructor is not called for the main thread // we have to call it manually to avoid memory leaks diff --git a/source/ps/Profiler2.h b/source/ps/Profiler2.h index 612675cf9f..eba9f42cc8 100644 --- a/source/ps/Profiler2.h +++ b/source/ps/Profiler2.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -90,11 +90,12 @@ #include #include #include +#include #include class CProfiler2GPU; namespace Renderer::Backend { class IDeviceCommandContext; } -struct mg_context; +namespace httplib { class Server; } // Note: Lots of functions are defined inline, to hypothetically // minimise performance overhead. @@ -388,7 +389,8 @@ private: int m_FrameNumber; - mg_context* m_MgContext; + std::unique_ptr m_HttpServer; + std::thread m_HttpServerThread; CProfiler2GPU* m_GPU; diff --git a/source/rlinterface/RLInterface.cpp b/source/rlinterface/RLInterface.cpp index 9ffb050ab7..8f220b39e2 100644 --- a/source/rlinterface/RLInterface.cpp +++ b/source/rlinterface/RLInterface.cpp @@ -24,6 +24,7 @@ #include "gui/GUIManager.h" #include "lib/debug.h" #include "lib/types.h" +#include "network/HttpServer.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Errors.h" @@ -31,6 +32,7 @@ #include "ps/GameSetup/GameSetup.h" #include "ps/Loader.h" #include "scriptinterface/JSON.h" +#include "ps/TaskManager.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptRequest.h" @@ -42,32 +44,149 @@ #include "simulation2/system/TurnManager.h" #include +#include +#include #include #include #include +#include #include +#include #include #include namespace RL { -Interface::Interface(const char* server_address) +Interface::Interface(std::string const serverAddress) { LOGMESSAGERENDER("Starting RL interface HTTP server"); + m_HttpServer = PS::Net::createHttpServer(); - const char *options[] = { - "listening_ports", server_address, - "num_threads", "1", - nullptr - }; - m_Context = mg_start(MgCallback, this, options); - if (!m_Context) - throw SetupError{}; + m_HttpServer->Post("/reset", [this](const httplib::Request &req, httplib::Response &res) { + if(req.body.empty()) + { + res.set_content("No POST data found.", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + + ScenarioConfig scenario; + scenario.saveReplay = req.has_param("saveReplay"); + scenario.playerID = 1; + if (req.has_param("playerID")) + { + scenario.playerID = std::stoi(req.get_param_value("playerID")); + } + + scenario.content = req.body; + + const std::string gameState = Reset(std::move(scenario)); + + res.set_content(gameState, "text/plain"); + }); + + m_HttpServer->Post("/step", [this](const httplib::Request &req, httplib::Response &res) { + if (!IsGameRunning()) + { + res.set_content("Game not running. Please create a scenario first.", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + std::stringstream postStream(req.body); + std::string line; + std::vector commands; + while (std::getline(postStream, line, '\n')) + { + const std::size_t splitPos = line.find(";"); + if (splitPos == std::string::npos) + continue; + + GameCommand cmd; + cmd.playerID = std::stoi(line.substr(0, splitPos)); + cmd.json_cmd = line.substr(splitPos + 1); + commands.push_back(std::move(cmd)); + } + const std::string gameState = Step(std::move(commands)); + if (gameState.empty()) + { + res.set_content("Game not running. Please create a scenario first.", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + else + res.set_content(gameState, "text/plain"); + }); + + m_HttpServer->Post("/evaluate", [this](const httplib::Request &req, httplib::Response &res) { + if (!IsGameRunning()) + { + res.set_content("Game not running. Please create a scenario first.", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + if (req.body.empty()) + { + res.set_content("No POST data found.", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + std::string code{req.body}; + const std::string codeResult = Evaluate(std::move(code)); + if (codeResult.empty()) + { + res.set_content("Game not running. Please create a scenario first.", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + else + res.set_content(codeResult, "text/plain"); + }); + + m_HttpServer->Get("/templates", [this](const httplib::Request &req, httplib::Response &res) { + if (!IsGameRunning()) { + res.set_content("Game not running. Please create a scenario first.", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + if (req.body.empty()) + { + res.set_content("No POST data found.", "text/plain"); + res.status = httplib::StatusCode::BadRequest_400; + return; + } + std::stringstream postStream(req.body); + std::string line; + std::vector templateNames; + while (std::getline(postStream, line, '\n')) + templateNames.push_back(line); + + std::stringstream stream; + for (std::string templateStr : GetTemplates(templateNames)) + stream << templateStr.c_str() << "\n"; + res.set_content(stream.str(), "text/plain"); + }); + + std::size_t sepIndex = serverAddress.find(":"); + if (sepIndex == std::string::npos) + { + throw std::invalid_argument{fmt::format("Invalid server address for RL interface '{}'", serverAddress)}; + } + std::string address = serverAddress.substr(0, sepIndex); + int port = std::stoi(serverAddress.substr(sepIndex + 1, serverAddress.length() - sepIndex - 1)); + + m_HttpServerThread = std::thread([this, address, port](){ + if (!m_HttpServer->listen(address, port)) + { + LOGERROR("Failed to start http server"); + } + }); } -Interface::~Interface() -{ - mg_stop(m_Context); +Interface::~Interface() { + m_HttpServer->stop(); + if(m_HttpServerThread.joinable()) { + m_HttpServerThread.join(); + } } // Interactions with the game engine (g_Game) must be done in the main @@ -120,188 +239,6 @@ std::vector Interface::GetTemplates(const std::vector& return templates; } -void* Interface::MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) -{ - Interface* interface = (Interface*)request_info->user_data; - ENSURE(interface); - - void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling - - const char* header200 = - "HTTP/1.1 200 OK\r\n" - "Access-Control-Allow-Origin: *\r\n" - "Content-Type: text/plain; charset=utf-8\r\n\r\n"; - - const char* header404 = - "HTTP/1.1 404 Not Found\r\n" - "Content-Type: text/plain; charset=utf-8\r\n\r\n" - "Unrecognised URI"; - - const char* noPostData = - "HTTP/1.1 400 Bad Request\r\n" - "Content-Type: text/plain; charset=utf-8\r\n\r\n" - "No POST data found."; - - const char* notRunningResponse = - "HTTP/1.1 400 Bad Request\r\n" - "Content-Type: text/plain; charset=utf-8\r\n\r\n" - "Game not running. Please create a scenario first."; - - switch (event) - { - case MG_NEW_REQUEST: - { - std::stringstream stream; - - const std::string uri = request_info->uri; - - if (uri == "/reset") - { - std::string data = GetRequestContent(conn); - if (data.empty()) - { - mg_printf(conn, "%s", noPostData); - return handled; - } - ScenarioConfig scenario; - const char *queryString = request_info->query_string; - - const std::string_view qs(queryString ? queryString : ""); - scenario.saveReplay = qs.find("saveReplay") != std::string_view::npos; - - char playerID[1]; - const int len = mg_get_var(queryString, qs.length(), "playerID", playerID, 1); - scenario.playerID = len == -1 ? 1 : std::stoi(playerID); - - scenario.content = std::move(data); - - const std::string gameState = interface->Reset(std::move(scenario)); - - stream << gameState.c_str(); - } - else if (uri == "/step") - { - if (!interface->IsGameRunning()) - { - mg_printf(conn, "%s", notRunningResponse); - return handled; - } - - std::string data = GetRequestContent(conn); - std::stringstream postStream(data); - std::string line; - std::vector commands; - - while (std::getline(postStream, line, '\n')) - { - const std::size_t splitPos = line.find(";"); - if (splitPos == std::string::npos) - continue; - - GameCommand cmd; - cmd.playerID = std::stoi(line.substr(0, splitPos)); - cmd.json_cmd = line.substr(splitPos + 1); - commands.push_back(std::move(cmd)); - } - const std::string gameState = interface->Step(std::move(commands)); - if (gameState.empty()) - { - mg_printf(conn, "%s", notRunningResponse); - return handled; - } - else - stream << gameState.c_str(); - } - else if (uri == "/evaluate") - { - if (!interface->IsGameRunning()) - { - mg_printf(conn, "%s", notRunningResponse); - return handled; - } - - std::string code = GetRequestContent(conn); - if (code.empty()) - { - mg_printf(conn, "%s", noPostData); - return handled; - } - - const std::string codeResult = interface->Evaluate(std::move(code)); - if (codeResult.empty()) - { - mg_printf(conn, "%s", notRunningResponse); - return handled; - } - else - stream << codeResult.c_str(); - } - else if (uri == "/templates") - { - if (!interface->IsGameRunning()) { - mg_printf(conn, "%s", notRunningResponse); - return handled; - } - const std::string data = GetRequestContent(conn); - if (data.empty()) - { - mg_printf(conn, "%s", noPostData); - return handled; - } - std::stringstream postStream(data); - std::string line; - std::vector templateNames; - while (std::getline(postStream, line, '\n')) - templateNames.push_back(line); - - for (std::string templateStr : interface->GetTemplates(templateNames)) - stream << templateStr.c_str() << "\n"; - - } - else - { - mg_printf(conn, "%s", header404); - return handled; - } - - mg_printf(conn, "%s", header200); - const std::string str = stream.str(); - mg_write(conn, str.c_str(), str.length()); - return handled; - } - - case MG_HTTP_ERROR: - return nullptr; - - case MG_EVENT_LOG: - // Called by Mongoose's cry() - LOGERROR("Mongoose error: %s", request_info->log_message); - return nullptr; - - case MG_INIT_SSL: - return nullptr; - - default: - debug_warn(L"Invalid Mongoose event type"); - return nullptr; - } -} - -std::string Interface::GetRequestContent(struct mg_connection *conn) -{ - const static std::string NO_CONTENT; - const char* val = mg_get_header(conn, "Content-Length"); - if (!val) - { - return NO_CONTENT; - } - const int contentSize = std::atoi(val); - - std::string content(contentSize, ' '); - mg_read(conn, content.data(), contentSize); - return content; -} - bool Interface::TryGetGameMessage(GameMessage& msg) { if (m_GameMessage.type != GameMessageType::None) diff --git a/source/rlinterface/RLInterface.h b/source/rlinterface/RLInterface.h index 106a55ce7e..cb05e92f25 100644 --- a/source/rlinterface/RLInterface.h +++ b/source/rlinterface/RLInterface.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -20,15 +20,15 @@ #include "lib/code_annotation.h" #include "simulation2/helpers/Player.h" -#include "third_party/mongoose/mongoose.h" #include #include #include #include +#include #include -struct mg_context; +namespace httplib { class Server; } namespace RL { @@ -93,7 +93,7 @@ class Interface { NONCOPYABLE(Interface); public: - Interface(const char* server_address); + Interface(std::string const serverAddress); ~Interface(); /** @@ -103,9 +103,6 @@ public: void TryApplyMessage(); private: - static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info); - static std::string GetRequestContent(struct mg_connection *conn); - /** * Process commands, update the simulation by one turn. * @return the gamestate after processing commands. @@ -170,7 +167,8 @@ private: std::condition_variable m_MsgApplied; std::string m_Code; - mg_context* m_Context; + std::unique_ptr m_HttpServer; + std::thread m_HttpServerThread; }; }