mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Use cpp-httplib instead of mongoose
Use cpp-httplib for Profiler2 and RLInterface instead of mongoose. Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
This commit is contained in:
parent
7e575aa855
commit
ba4ef61c15
10 changed files with 307 additions and 334 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -537,7 +537,7 @@ static std::optional<RL::Interface> 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<RL::Interface>(server_address.c_str());
|
||||
return std::make_optional<RL::Interface>(server_address);
|
||||
}
|
||||
|
||||
#if CONFIG2_DAP_INTERFACE
|
||||
|
|
|
|||
62
source/network/HttpServer.cpp
Normal file
62
source/network/HttpServer.cpp
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "precompiled.h"
|
||||
|
||||
#include "HttpServer.h"
|
||||
|
||||
#include "ps/Future.h"
|
||||
#include "ps/TaskManager.h"
|
||||
|
||||
#include <httplib.h>
|
||||
#include <queue>
|
||||
|
||||
namespace
|
||||
{
|
||||
class TaskQueueAdapter : public httplib::TaskQueue
|
||||
{
|
||||
public:
|
||||
bool enqueue(std::function<void()> 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<Future<void>> m_Futures;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
namespace PS::Net
|
||||
{
|
||||
std::unique_ptr<httplib::Server> createHttpServer() {
|
||||
auto server = std::make_unique<httplib::Server>();
|
||||
server->new_task_queue = [] { return new TaskQueueAdapter(); };
|
||||
return server;
|
||||
}
|
||||
} // namespace PS::Net
|
||||
30
source/network/HttpServer.h
Normal file
30
source/network/HttpServer.h
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef HTTPSEVER_H
|
||||
#define HTTPSEVER_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace httplib { class Server; }
|
||||
|
||||
namespace PS::Net
|
||||
{
|
||||
std::unique_ptr<httplib::Server> createHttpServer();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -381,6 +381,7 @@ void ShutdownNetworkAndUI()
|
|||
|
||||
g_RenderingOptions.ClearHooks();
|
||||
|
||||
g_Profiler2.ShutDownHTTP();
|
||||
g_Profiler2.ShutdownGPU();
|
||||
|
||||
if (hasRenderer)
|
||||
|
|
|
|||
|
|
@ -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 <algorithm>
|
||||
#include <cstdio>
|
||||
#include <fmt/format.h>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <httplib.h>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
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<httplib::Server> m_HttpServer;
|
||||
std::thread m_HttpServerThread;
|
||||
|
||||
CProfiler2GPU* m_GPU;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <cstdlib>
|
||||
#include <fmt/format.h>
|
||||
#include <httplib.h>
|
||||
#include <js/RootingAPI.h>
|
||||
#include <js/TypeDecls.h>
|
||||
#include <js/Value.h>
|
||||
#include <queue>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
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<GameCommand> 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<std::string> 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<std::string> Interface::GetTemplates(const std::vector<std::string>&
|
|||
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<GameCommand> 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<std::string> 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)
|
||||
|
|
|
|||
|
|
@ -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 <condition_variable>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
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<httplib::Server> m_HttpServer;
|
||||
std::thread m_HttpServerThread;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue