From 2895cff5f246ad11f48f829d1af3fdddecd4aa92 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 5 Oct 2025 22:52:55 +0300 Subject: [PATCH 01/20] feat: max clients connected limit in TcpServer --- src/network/Network.cpp | 17 +++++++++-------- src/network/Network.hpp | 2 ++ src/network/Sockets.cpp | 24 ++++++++++++++++++++++++ src/network/commons.hpp | 2 ++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/network/Network.cpp b/src/network/Network.cpp index 04c8dbec..be3f8d33 100644 --- a/src/network/Network.cpp +++ b/src/network/Network.cpp @@ -164,15 +164,16 @@ void Network::update() { } ++socketiter; } - auto serveriter = servers.begin(); - while (serveriter != servers.end()) { - auto server = serveriter->second.get(); - if (!server->isOpen()) { - serveriter = servers.erase(serveriter); - continue; - } - ++serveriter; + } + auto serveriter = servers.begin(); + while (serveriter != servers.end()) { + auto server = serveriter->second.get(); + if (!server->isOpen()) { + serveriter = servers.erase(serveriter); + continue; } + server->update(); + ++serveriter; } } diff --git a/src/network/Network.hpp b/src/network/Network.hpp index a6c2fca2..047e07b4 100644 --- a/src/network/Network.hpp +++ b/src/network/Network.hpp @@ -37,6 +37,8 @@ namespace network { [[nodiscard]] TransportType getTransportType() const noexcept override { return TransportType::TCP; } + + virtual void setMaxClientsConnected(int count) = 0; }; class UdpServer : public Server { diff --git a/src/network/Sockets.cpp b/src/network/Sockets.cpp index 5152028b..9e145da0 100644 --- a/src/network/Sockets.cpp +++ b/src/network/Sockets.cpp @@ -304,6 +304,7 @@ class SocketTcpServer : public TcpServer { bool open = true; std::unique_ptr thread = nullptr; int port; + int maxConnected = -1; public: SocketTcpServer(u64id_t id, Network* network, SOCKET descriptor, int port) : id(id), network(network), descriptor(descriptor), port(port) {} @@ -312,6 +313,22 @@ public: closeSocket(); } + void setMaxClientsConnected(int count) override { + maxConnected = count; + } + + void update() override { + std::vector clients; + for (u64id_t cid : this->clients) { + if (auto client = network->getConnection(cid, true)) { + if (client->getState() != ConnectionState::CLOSED) { + clients.emplace_back(cid); + } + } + } + std::swap(clients, this->clients); + } + void startListen(ConnectCallback handler) override { thread = std::make_unique([this, handler]() { while (open) { @@ -328,6 +345,11 @@ public: close(); break; } + if (maxConnected >= 0 && clients.size() >= maxConnected) { + logger.info() << "refused connection attempt from " << to_string(address); + closesocket(clientDescriptor); + continue; + } logger.info() << "client connected: " << to_string(address); auto socket = std::make_shared( clientDescriptor, address @@ -575,6 +597,8 @@ public: SocketUdpServer::close(); } + void update() override {} + void startListen(ServerDatagramCallback handler) override { callback = std::move(handler); diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 83881c52..ad3c0806 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -78,6 +78,8 @@ namespace network { class Server { public: virtual ~Server() = default; + + virtual void update() = 0; virtual void close() = 0; virtual bool isOpen() = 0; [[nodiscard]] virtual TransportType getTransportType() const noexcept = 0; From 80cd187c6ff8d55cc6461896ad7ef542a1beab88 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 5 Oct 2025 23:54:29 +0300 Subject: [PATCH 02/20] add '--dbg-server' command-line argument & add debugging server draft --- src/devtools/DebuggingServer.cpp | 77 ++++++++++++++++++++++++++++ src/devtools/DebuggingServer.hpp | 29 +++++++++++ src/engine/Engine.cpp | 23 ++++++++- src/engine/Engine.hpp | 3 ++ src/logic/scripting/lua/lua_util.hpp | 4 +- src/main.cpp | 5 +- src/util/command_line.cpp | 6 ++- 7 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src/devtools/DebuggingServer.cpp create mode 100644 src/devtools/DebuggingServer.hpp diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp new file mode 100644 index 00000000..9cdb6427 --- /dev/null +++ b/src/devtools/DebuggingServer.cpp @@ -0,0 +1,77 @@ +#include "DebuggingServer.hpp" + +#include "engine/Engine.hpp" +#include "network/Network.hpp" +#include "debug/Logger.hpp" + +using namespace devtools; + +static debug::Logger logger("debug-server"); + +static network::Server& create_tcp_server( + DebuggingServer& dbgServer, Engine& engine, int port +) { + auto& network = engine.getNetwork(); + u64id_t serverId = network.openTcpServer( + port, + [&network, &dbgServer](u64id_t sid, u64id_t id) { + auto& connection = *network.getConnection(id, true); + connection.setPrivate(true); + logger.info() << "connected client " << id << ": " + << connection.getAddress() << ":" + << connection.getPort(); + dbgServer.setClient(connection); + } + ); + auto& server = *network.getServer(serverId, true); + server.setPrivate(true); + + auto& tcpServer = dynamic_cast(server); + tcpServer.setMaxClientsConnected(1); + + logger.info() << "tcp debugging server open at port " << server.getPort(); + + return tcpServer; +} + +static network::Server& create_server( + DebuggingServer& dbgServer, Engine& engine, const std::string& serverString +) { + logger.info() << "starting debugging server"; + + size_t sepPos = serverString.find(':'); + if (sepPos == std::string::npos) { + throw std::runtime_error("invalid debugging server configuration string"); + } + auto transport = serverString.substr(0, sepPos); + if (transport == "tcp") { + int port; + try { + port = std::stoi(serverString.substr(sepPos + 1)); + } catch (const std::exception& err) { + throw std::runtime_error("invalid tcp port"); + } + return create_tcp_server(dbgServer, engine, port); + } else { + throw std::runtime_error( + "unsupported debugging server transport '" + transport + "'" + ); + } +} + +DebuggingServer::DebuggingServer( + Engine& engine, const std::string& serverString +) + : engine(engine), + server(create_server(*this, engine, serverString)), + client(nullptr) { +} + +DebuggingServer::~DebuggingServer() { + logger.info() << "stopping debugging server"; +} + + +bool DebuggingServer::update() { + return false; +} diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp new file mode 100644 index 00000000..7f557935 --- /dev/null +++ b/src/devtools/DebuggingServer.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace network { + class Server; + class Connection; +} + +class Engine; + +namespace devtools { + class DebuggingServer { + public: + DebuggingServer(Engine& engine, const std::string& serverString); + ~DebuggingServer(); + + bool update(); + + void setClient(network::Connection& client) { + this->client = &client; + } + private: + Engine& engine; + network::Server& server; + network::Connection* client; + }; +} diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index d2a4eb20..23018946 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -14,6 +14,7 @@ #include "coders/commons.hpp" #include "devtools/Editor.hpp" #include "devtools/Project.hpp" +#include "devtools/DebuggingServer.hpp" #include "content/ContentControl.hpp" #include "core_defs.hpp" #include "io/io.hpp" @@ -115,6 +116,9 @@ void Engine::initializeClient() { if (ENGINE_DEBUG_BUILD) { title += " [debug]"; } + if (debuggingServer) { + title = "[debugging] " + title; + } auto [window, input] = Window::initialize(&settings.display, title); if (!window || !input){ throw initialize_error("could not initialize window"); @@ -173,6 +177,18 @@ void Engine::initialize(CoreParameters coreParameters) { cmd = std::make_unique(); network = network::Network::create(settings.network); + if (!params.debugServerString.empty()) { + try { + debuggingServer = std::make_unique( + *this, params.debugServerString + ); + } catch (const std::runtime_error& err) { + throw initialize_error( + "debugging server error: " + std::string(err.what()) + ); + } + } + if (!params.scriptFile.empty()) { paths.setScriptFolder(params.scriptFile.parent_path()); } @@ -299,7 +315,11 @@ void Engine::saveSettings() { io::write_string(EnginePaths::SETTINGS_FILE, toml::stringify(*settingsHandler)); if (!params.headless) { logger.info() << "saving bindings"; - io::write_string(EnginePaths::CONTROLS_FILE, input->getBindings().write()); + if (input) { + io::write_string( + EnginePaths::CONTROLS_FILE, input->getBindings().write() + ); + } } } @@ -318,6 +338,7 @@ void Engine::close() { logger.info() << "gui finished"; } audio::close(); + debuggingServer.reset(); network.reset(); clearKeepedObjects(); project.reset(); diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 1f9438c0..c523acb8 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -36,6 +36,7 @@ namespace network { namespace devtools { class Editor; + class DebuggingServer; } class initialize_error : public std::runtime_error { @@ -50,6 +51,7 @@ struct CoreParameters { std::filesystem::path userFolder = "."; std::filesystem::path scriptFile; std::filesystem::path projectFolder; + std::string debugServerString; int tps = 20; }; @@ -72,6 +74,7 @@ class Engine : public util::ObjectsKeeper { std::unique_ptr input; std::unique_ptr gui; std::unique_ptr editor; + std::unique_ptr debuggingServer; PostRunnables postRunnables; Time time; OnWorldOpen levelConsumer; diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index d7aed664..e72b56eb 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -617,7 +617,9 @@ namespace lua { void remove_environment(lua::State*, int id); inline void close(lua::State* L) { - lua_close(L); + if (L) { + lua_close(L); + } } inline void addfunc( diff --git a/src/main.cpp b/src/main.cpp index 40e9665a..256818c3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,7 +25,7 @@ int main(int argc, char** argv) { } std::signal(SIGTERM, sigterm_handler); - debug::Logger::init(coreParameters.userFolder.string()+"/latest.log"); + debug::Logger::init(coreParameters.userFolder.string() + "/latest.log"); platform::configure_encoding(); auto& engine = Engine::getInstance(); @@ -33,7 +33,8 @@ int main(int argc, char** argv) { engine.initialize(std::move(coreParameters)); engine.run(); } catch (const initialize_error& err) { - logger.error() << "could not to initialize engine\n" << err.what(); + logger.error() << err.what(); + logger.error() << "could not to initialize engine"; } #if defined(NDEBUG) and defined(_WIN32) catch (const std::exception& err) { diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 6d84ab28..3691b77b 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -70,11 +70,15 @@ static bool perform_keyword( std::cout << ENGINE_VERSION_STRING << std::endl; return false; }, "", "display the engine version."), + ArgC("--dbg-server", [¶ms, &reader]() -> bool { + params.debugServerString = reader.next(); + return true; + }, "", "open debugging server where is {transport}:{port}"), ArgC("--help", []() -> bool { std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "Command-line arguments:\n"; for (auto& a : argumentsCommandline) { - std::cout << std::setw(20) << std::left << (a.keyword + " " + a.args); + std::cout << std::setw(24) << std::left << (a.keyword + " " + a.args); std::cout << "- " << a.help << std::endl; } std::cout << std::endl; From 44bf4a2f9ec31b301f2cc38bd7ee6553373df2ff Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 6 Oct 2025 01:49:13 +0300 Subject: [PATCH 03/20] add ReadableConnection interface --- src/network/Network.hpp | 5 ++--- src/network/Sockets.cpp | 2 +- src/network/commons.hpp | 6 ++++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/network/Network.hpp b/src/network/Network.hpp index 047e07b4..1110c750 100644 --- a/src/network/Network.hpp +++ b/src/network/Network.hpp @@ -3,13 +3,12 @@ #include "commons.hpp" namespace network { - class TcpConnection : public Connection { + class TcpConnection : public ReadableConnection { public: ~TcpConnection() override = default; virtual void connect(runnable callback) = 0; - virtual int recv(char* buffer, size_t length) = 0; - virtual int available() = 0; + virtual void setNoDelay(bool noDelay) = 0; [[nodiscard]] virtual bool isNoDelay() const = 0; diff --git a/src/network/Sockets.cpp b/src/network/Sockets.cpp index 9e145da0..37d1190d 100644 --- a/src/network/Sockets.cpp +++ b/src/network/Sockets.cpp @@ -231,7 +231,7 @@ public: readBatch.clear(); if (state != ConnectionState::CLOSED) { - shutdown(descriptor, 2); + shutdown(descriptor, SHUT_RDWR); closesocket(descriptor); } } diff --git a/src/network/commons.hpp b/src/network/commons.hpp index ad3c0806..150bde25 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -75,6 +75,12 @@ namespace network { bool isprivate = false; }; + class ReadableConnection : public Connection { + public: + virtual int recv(char* buffer, size_t length) = 0; + virtual int available() = 0; + }; + class Server { public: virtual ~Server() = default; From 3fef284a02de5718eda19b64bebde45b3a264dc6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 6 Oct 2025 01:54:35 +0300 Subject: [PATCH 04/20] test debugger commands --- src/devtools/DebuggingServer.cpp | 102 +++++++++++++++++++++++++++++-- src/devtools/DebuggingServer.hpp | 32 ++++++++-- src/engine/Engine.cpp | 6 ++ src/engine/Engine.hpp | 2 +- 4 files changed, 133 insertions(+), 9 deletions(-) diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 9cdb6427..86e2bdae 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -3,11 +3,62 @@ #include "engine/Engine.hpp" #include "network/Network.hpp" #include "debug/Logger.hpp" +#include "coders/json.hpp" using namespace devtools; static debug::Logger logger("debug-server"); +ClientConnection::~ClientConnection() { + if (auto connection = dynamic_cast( + network.getConnection(this->connection, true) + )) { + connection->close(); + } +} + +std::string ClientConnection::read() { + auto connection = dynamic_cast( + network.getConnection(this->connection, true) + ); + if (connection == nullptr) { + return ""; + } + if (messageLength == 0) { + if (connection->available() >= sizeof(int32_t)) { + int32_t length = 0; + connection->recv(reinterpret_cast(&length), sizeof(int32_t)); + if (length <= 0) { + logger.error() << "invalid message length " << length; + } else { + messageLength = length; + } + } + } else if (connection->available() >= messageLength) { + std::string string(messageLength, 0); + connection->recv(string.data(), messageLength); + return string; + } + return ""; +} + +void ClientConnection::send(const dv::value& object) { + auto connection = dynamic_cast( + network.getConnection(this->connection, true) + ); + if (connection == nullptr) { + return; + } + auto message = json::stringify(object, false); + int32_t length = message.length(); + connection->send(reinterpret_cast(&length), sizeof(int32_t)); + connection->send(message.data(), length); +} + +void ClientConnection::sendResponse(const std::string& type) { + send(dv::object({{"type", type}})); +} + static network::Server& create_tcp_server( DebuggingServer& dbgServer, Engine& engine, int port ) { @@ -15,12 +66,14 @@ static network::Server& create_tcp_server( u64id_t serverId = network.openTcpServer( port, [&network, &dbgServer](u64id_t sid, u64id_t id) { - auto& connection = *network.getConnection(id, true); + auto& connection = dynamic_cast( + *network.getConnection(id, true) + ); connection.setPrivate(true); logger.info() << "connected client " << id << ": " << connection.getAddress() << ":" << connection.getPort(); - dbgServer.setClient(connection); + dbgServer.setClient(id); } ); auto& server = *network.getServer(serverId, true); @@ -64,14 +117,55 @@ DebuggingServer::DebuggingServer( ) : engine(engine), server(create_server(*this, engine, serverString)), - client(nullptr) { + connection(nullptr) { } DebuggingServer::~DebuggingServer() { logger.info() << "stopping debugging server"; + server.close(); } bool DebuggingServer::update() { - return false; + if (connection == nullptr) { + return true; + } + std::string message = connection->read(); + if (message.empty()) { + return true; + } + logger.debug() << "received: " << message; + try { + auto obj = json::parse(message); + if (!obj.has("type")) { + logger.error() << "missing message type"; + return true; + } + const auto& type = obj["type"].asString(); + return performCommand(type, obj); + } catch (const std::runtime_error& err) { + logger.error() << "could not to parse message: " << err.what(); + } + return true; +} + +bool DebuggingServer::performCommand( + const std::string& type, const dv::value& map +) { + if (type == "terminate") { + engine.quit(); + connection->sendResponse("success"); + } else if (type == "detach") { + connection->sendResponse("success"); + connection.reset(); + return false; + } else { + logger.error() << "unsupported command '" << type << "'"; + } + return true; +} + +void DebuggingServer::setClient(u64id_t client) { + this->connection = + std::make_unique(engine.getNetwork(), client); } diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index 7f557935..b8b103b8 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -3,14 +3,38 @@ #include #include +#include "typedefs.hpp" + namespace network { class Server; class Connection; + class ReadableConnection; + class Network; +} + +namespace dv { + class value; } class Engine; namespace devtools { + class ClientConnection { + public: + ClientConnection(network::Network& network, u64id_t connection) + : network(network), connection(connection) { + } + ~ClientConnection(); + + std::string read(); + void send(const dv::value& message); + void sendResponse(const std::string& type); + private: + network::Network& network; + size_t messageLength = 0; + u64id_t connection; + }; + class DebuggingServer { public: DebuggingServer(Engine& engine, const std::string& serverString); @@ -18,12 +42,12 @@ namespace devtools { bool update(); - void setClient(network::Connection& client) { - this->client = &client; - } + void setClient(u64id_t client); private: Engine& engine; network::Server& server; - network::Connection* client; + std::unique_ptr connection; + + bool performCommand(const std::string& type, const dv::value& map); }; } diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 23018946..65a0be87 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -282,6 +282,12 @@ void Engine::postUpdate() { network->update(); postRunnables.run(); scripting::process_post_runnables(); + + if (debuggingServer) { + if (!debuggingServer->update()) { + debuggingServer.reset(); + } + } } void Engine::updateFrontend() { diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index c523acb8..b0f0918d 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -51,7 +51,7 @@ struct CoreParameters { std::filesystem::path userFolder = "."; std::filesystem::path scriptFile; std::filesystem::path projectFolder; - std::string debugServerString; + std::string debugServerString = "tcp:9030"; int tps = 20; }; From b2e03a6d1821d42bc12e528d25d61a72c615add1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 6 Oct 2025 02:08:37 +0300 Subject: [PATCH 05/20] fix windows build --- src/network/Sockets.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/network/Sockets.cpp b/src/network/Sockets.cpp index 37d1190d..c1b6285f 100644 --- a/src/network/Sockets.cpp +++ b/src/network/Sockets.cpp @@ -10,6 +10,7 @@ #ifdef _WIN32 #include +#define SHUT_RDWR SD_BOTH #else #include #include From c152cfc03fa0d47faa837a1a585d8ed8ba977d88 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 6 Oct 2025 21:47:53 +0300 Subject: [PATCH 06/20] add debug.breakpoint --- src/devtools/DebuggingServer.cpp | 5 + src/devtools/DebuggingServer.hpp | 1 + src/engine/Engine.hpp | 4 + src/logic/scripting/lua/lua_extensions.cpp | 120 +++++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 86e2bdae..26a2ecfa 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -165,6 +165,11 @@ bool DebuggingServer::performCommand( return true; } +void DebuggingServer::onHitBreakpoint(dv::value&& stackTrace) { + logger.info() << "hit breakpoint:\n" + << json::stringify(stackTrace, true, " "); +} + void DebuggingServer::setClient(u64id_t client) { this->connection = std::make_unique(engine.getNetwork(), client); diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index b8b103b8..b379c3e3 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -41,6 +41,7 @@ namespace devtools { ~DebuggingServer(); bool update(); + void onHitBreakpoint(dv::value&& stackTrace); void setClient(u64id_t client); private: diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index b0f0918d..6d04da5c 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -185,4 +185,8 @@ public: const Project& getProject() { return *project; } + + devtools::DebuggingServer* getDebuggingServer() { + return debuggingServer.get(); + } }; diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index b9c25fc6..74be96de 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -1,9 +1,12 @@ #include #include #include +#include #include "libs/api_lua.hpp" #include "debug/Logger.hpp" +#include "engine/Engine.hpp" +#include "devtools/DebuggingServer.hpp" #include "logic/scripting/scripting.hpp" using namespace scripting; @@ -159,6 +162,120 @@ static int l_math_normal_random(lua::State* L) { return lua::pushnumber(L, randomFloats(generator)); } +constexpr inline int MAX_SHORT_STRING_LEN = 50; + +static dv::value create_stack_trace(lua_State* L, int initFrame = 2) { + auto entriesList = dv::list(); + + lua_Debug frame; + int level = initFrame; + + while (lua_getstack(L, level, &frame)) { + auto entry = dv::object(); + if (lua_getinfo(L, "nSlf", &frame) == 0) { + level++; + entriesList.add(std::move(entry)); + continue; + } + if (frame.name) { + entry["function"] = frame.name; + } + if (frame.source) { + const char* src = + (frame.source[0] == '@') ? frame.source + 1 : frame.source; + entry["source"] = src; + entry["line"] = frame.currentline; + } + entry["what"] = frame.what; + + auto locals = dv::object(); + + int localIndex = 1; + const char* name; + while ((name = lua_getlocal(L, &frame, localIndex++))) { + if (name[0] == '(') { + lua::pop(L); + continue; + } + auto local = dv::object(); + + int type = lua::type(L, -1); + switch (type) { + case LUA_TNIL: + local["short"] = "nil"; + local["type"] = "nil"; + break; + case LUA_TBOOLEAN: + local["short"] = lua::toboolean(L, -1) ? "true" : "false"; + local["type"] = "boolean"; + break; + case LUA_TNUMBER: { + std::stringstream ss; + ss << lua::tonumber(L, -1); + local["short"] = ss.str(); + local["type"] = "number"; + break; + } + case LUA_TSTRING: { + const char* str = lua::tostring(L, -1); + if (strlen(str) > MAX_SHORT_STRING_LEN) { + local["short"] = std::string(str, MAX_SHORT_STRING_LEN); + } else { + local["short"] = str; + } + local["type"] = "string"; + break; + } + case LUA_TTABLE: + local["short"] = "{...}"; + local["type"] = "table"; + break; + case LUA_TFUNCTION: { + std::stringstream ss; + ss << "function: 0x" << std::hex << lua::topointer(L, -1); + local["short"] = ss.str(); + local["type"] = "function"; + break; + } + case LUA_TUSERDATA: { + std::stringstream ss; + ss << "userdata: 0x" << std::hex << lua::topointer(L, -1); + local["short"] = ss.str(); + local["type"] = "userdata"; + break; + } + case LUA_TTHREAD: { + std::stringstream ss; + ss << "thread: 0x" << std::hex << lua::topointer(L, -1); + local["short"] = ss.str(); + local["type"] = "thread"; + break; + } + default: { + std::stringstream ss; + ss << "cdata: 0x" << std::hex << lua::topointer(L, -1); + local["short"] = ss.str(); + local["type"] = "cdata"; + break; + } + } + locals[name] = std::move(local); + lua::pop(L); + } + entry["locals"] = std::move(locals); + entriesList.add(std::move(entry)); + level++; + } + return entriesList; +} + +static int l_debug_breakpoint(lua::State* L) { + if (auto server = engine->getDebuggingServer()) { + server->onHitBreakpoint(create_stack_trace(L)); + } + return 0; +} + void initialize_libs_extends(lua::State* L) { if (lua::getglobal(L, "debug")) { lua::pushcfunction(L, lua::wrap); @@ -173,6 +290,9 @@ void initialize_libs_extends(lua::State* L) { lua::pushcfunction(L, lua::wrap); lua::setfield(L, "print"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "breakpoint"); + lua::pop(L); } if (lua::getglobal(L, "math")) { From 95f30da49b6bee2cf4a0e1f98051d8b236a93290 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 7 Oct 2025 18:02:06 +0300 Subject: [PATCH 07/20] add engine pause mode --- src/devtools/DebuggingServer.cpp | 9 ++ src/engine/Engine.cpp | 24 ++++ src/engine/Engine.hpp | 1 + src/logic/scripting/lua/lua_extensions.cpp | 157 +++++++++++---------- 4 files changed, 114 insertions(+), 77 deletions(-) diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 26a2ecfa..88466dd3 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -168,6 +168,15 @@ bool DebuggingServer::performCommand( void DebuggingServer::onHitBreakpoint(dv::value&& stackTrace) { logger.info() << "hit breakpoint:\n" << json::stringify(stackTrace, true, " "); + if (connection == nullptr) { + return; + } + connection->send(dv::object({ + {"type", std::string("hit-breakpoint")}, + {"stack", std::move(stackTrace)} + })); + + engine.startPauseLoop(); } void DebuggingServer::setClient(u64id_t client) { diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 65a0be87..64a89fb5 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -309,6 +309,30 @@ void Engine::nextFrame() { input->pollEvents(); } +void Engine::startPauseLoop() { + bool initialCursorLocked = false; + if (!isHeadless()) { + initialCursorLocked = input->isCursorLocked(); + if (initialCursorLocked) { + input->toggleCursor(); + } + } + while (!isQuitSignal()) { + if (!debuggingServer->update()) { + debuggingServer.reset(); + break; + } + if (isHeadless()) { + platform::sleep(1.0 / params.tps * 1000); + } else { + nextFrame(); + } + } + if (initialCursorLocked) { + input->toggleCursor(); + } +} + void Engine::renderFrame() { screen->draw(time.getDelta()); diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 6d04da5c..a9fb62ea 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -108,6 +108,7 @@ public: void updateFrontend(); void renderFrame(); void nextFrame(); + void startPauseLoop(); /// @brief Set screen (scene). /// nullptr may be used to delete previous screen before creating new one, diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index 74be96de..15749b47 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -164,7 +164,85 @@ static int l_math_normal_random(lua::State* L) { constexpr inline int MAX_SHORT_STRING_LEN = 50; -static dv::value create_stack_trace(lua_State* L, int initFrame = 2) { +static dv::value collect_locals(lua::State* L, lua_Debug& frame) { + auto locals = dv::object(); + + int localIndex = 1; + const char* name; + while ((name = lua_getlocal(L, &frame, localIndex++))) { + if (name[0] == '(') { + lua::pop(L); + continue; + } + auto local = dv::object(); + + int type = lua::type(L, -1); + switch (type) { + case LUA_TNIL: + local["short"] = "nil"; + local["type"] = "nil"; + break; + case LUA_TBOOLEAN: + local["short"] = lua::toboolean(L, -1) ? "true" : "false"; + local["type"] = "boolean"; + break; + case LUA_TNUMBER: { + std::stringstream ss; + ss << lua::tonumber(L, -1); + local["short"] = ss.str(); + local["type"] = "number"; + break; + } + case LUA_TSTRING: { + const char* str = lua::tostring(L, -1); + if (strlen(str) > MAX_SHORT_STRING_LEN) { + local["short"] = std::string(str, MAX_SHORT_STRING_LEN); + } else { + local["short"] = str; + } + local["type"] = "string"; + break; + } + case LUA_TTABLE: + local["short"] = "{...}"; + local["type"] = "table"; + break; + case LUA_TFUNCTION: { + std::stringstream ss; + ss << "function: 0x" << std::hex << lua::topointer(L, -1); + local["short"] = ss.str(); + local["type"] = "function"; + break; + } + case LUA_TUSERDATA: { + std::stringstream ss; + ss << "userdata: 0x" << std::hex << lua::topointer(L, -1); + local["short"] = ss.str(); + local["type"] = "userdata"; + break; + } + case LUA_TTHREAD: { + std::stringstream ss; + ss << "thread: 0x" << std::hex << lua::topointer(L, -1); + local["short"] = ss.str(); + local["type"] = "thread"; + break; + } + default: { + std::stringstream ss; + ss << "cdata: 0x" << std::hex << lua::topointer(L, -1); + local["short"] = ss.str(); + local["type"] = "cdata"; + break; + } + } + locals[name] = std::move(local); + lua::pop(L); + } + return locals; +} + +static dv::value create_stack_trace(lua::State* L, int initFrame = 2) { auto entriesList = dv::list(); lua_Debug frame; @@ -187,82 +265,7 @@ static dv::value create_stack_trace(lua_State* L, int initFrame = 2) { entry["line"] = frame.currentline; } entry["what"] = frame.what; - - auto locals = dv::object(); - - int localIndex = 1; - const char* name; - while ((name = lua_getlocal(L, &frame, localIndex++))) { - if (name[0] == '(') { - lua::pop(L); - continue; - } - auto local = dv::object(); - - int type = lua::type(L, -1); - switch (type) { - case LUA_TNIL: - local["short"] = "nil"; - local["type"] = "nil"; - break; - case LUA_TBOOLEAN: - local["short"] = lua::toboolean(L, -1) ? "true" : "false"; - local["type"] = "boolean"; - break; - case LUA_TNUMBER: { - std::stringstream ss; - ss << lua::tonumber(L, -1); - local["short"] = ss.str(); - local["type"] = "number"; - break; - } - case LUA_TSTRING: { - const char* str = lua::tostring(L, -1); - if (strlen(str) > MAX_SHORT_STRING_LEN) { - local["short"] = std::string(str, MAX_SHORT_STRING_LEN); - } else { - local["short"] = str; - } - local["type"] = "string"; - break; - } - case LUA_TTABLE: - local["short"] = "{...}"; - local["type"] = "table"; - break; - case LUA_TFUNCTION: { - std::stringstream ss; - ss << "function: 0x" << std::hex << lua::topointer(L, -1); - local["short"] = ss.str(); - local["type"] = "function"; - break; - } - case LUA_TUSERDATA: { - std::stringstream ss; - ss << "userdata: 0x" << std::hex << lua::topointer(L, -1); - local["short"] = ss.str(); - local["type"] = "userdata"; - break; - } - case LUA_TTHREAD: { - std::stringstream ss; - ss << "thread: 0x" << std::hex << lua::topointer(L, -1); - local["short"] = ss.str(); - local["type"] = "thread"; - break; - } - default: { - std::stringstream ss; - ss << "cdata: 0x" << std::hex << lua::topointer(L, -1); - local["short"] = ss.str(); - local["type"] = "cdata"; - break; - } - } - locals[name] = std::move(local); - lua::pop(L); - } - entry["locals"] = std::move(locals); + entry["locals"] = collect_locals(L, frame); entriesList.add(std::move(entry)); level++; } From 5972bc769b9664166c28ce5f765932e9d6c4c060 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 7 Oct 2025 21:07:02 +0300 Subject: [PATCH 08/20] add 'set-breakpoint', 'remove-breakpoint' commands --- res/scripts/stdlib.lua | 1 + res/scripts/stdmin.lua | 52 ++++++++++++++++++++++ src/devtools/DebuggingServer.cpp | 19 +++++++- src/devtools/DebuggingServer.hpp | 13 ++++++ src/engine/Engine.cpp | 1 + src/logic/scripting/lua/lua_extensions.cpp | 30 +++++++++++++ 6 files changed, 114 insertions(+), 2 deletions(-) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index b08e8c02..3f732e07 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -587,6 +587,7 @@ function __process_post_runnables() __vc_named_coroutines[name] = nil end + debug.pull_events() network.__process_events() block.__process_register_events() block.__perform_ticks(time.delta()) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 26d5d813..eb3ddbde 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -1,5 +1,57 @@ -- Lua has no parallelizm, also _set_data does not call any lua functions so -- may be reused one global ffi buffer per lua_State + +local breakpoints = {} + +debug.sethook(function (e, line) + local bps = breakpoints[line] + if not bps then + return + end + local source = debug.getinfo(2).source + if not bps[source] then + return + end + debug.breakpoint() + debug.pull_events() +end, "l") + +local DBG_EVENT_SET_BREAKPOINT = 1 +local DBG_EVENT_RM_BREAKPOINT = 2 +local __pull_events = debug.__pull_events +debug.__pull_events = nil + +function debug.pull_events() + local events = __pull_events() + if not events then + return + end + for i, event in ipairs(events) do + if event[1] == DBG_EVENT_SET_BREAKPOINT then + debug.set_breakpoint(event[2], event[3]) + elseif event[1] == DBG_EVENT_RM_BREAKPOINT then + debug.remove_breakpoint(event[2], event[3]) + end + end +end + +function debug.set_breakpoint(source, line) + local bps = breakpoints[line] + if not bps then + bps = {} + breakpoints[line] = bps + end + bps[source] = true +end + +function debug.remove_breakpoint(source, line) + local bps = breakpoints[line] + if not bps then + return + end + bps[source] = nil +end + local canvas_ffi_buffer local canvas_ffi_buffer_size = 0 diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 88466dd3..09c8c18f 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -31,12 +31,14 @@ std::string ClientConnection::read() { if (length <= 0) { logger.error() << "invalid message length " << length; } else { + logger.info() << "message length " << length; messageLength = length; } } } else if (connection->available() >= messageLength) { std::string string(messageLength, 0); connection->recv(string.data(), messageLength); + messageLength = 0; return string; } return ""; @@ -156,9 +158,20 @@ bool DebuggingServer::performCommand( engine.quit(); connection->sendResponse("success"); } else if (type == "detach") { + logger.info() << "detach received"; connection->sendResponse("success"); connection.reset(); return false; + } else if (type == "set-breakpoint" || type == "remove-breakpoint") { + if (!map.has("source") || !map.has("line")) + return true; + breakpointEvents.push_back(BreakpointEvent { + type[0] == 's' + ? BreakpointEventType::SET_BREAKPOINT + : BreakpointEventType::REMOVE_BREAKPOINT, + map["source"].asString(), + static_cast(map["line"].asInteger()), + }); } else { logger.error() << "unsupported command '" << type << "'"; } @@ -166,8 +179,6 @@ bool DebuggingServer::performCommand( } void DebuggingServer::onHitBreakpoint(dv::value&& stackTrace) { - logger.info() << "hit breakpoint:\n" - << json::stringify(stackTrace, true, " "); if (connection == nullptr) { return; } @@ -183,3 +194,7 @@ void DebuggingServer::setClient(u64id_t client) { this->connection = std::make_unique(engine.getNetwork(), client); } + +std::vector DebuggingServer::pullBreakpointEvents() { + return std::move(breakpointEvents); +} diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index b379c3e3..308cee87 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "typedefs.hpp" @@ -35,6 +36,16 @@ namespace devtools { u64id_t connection; }; + enum class BreakpointEventType { + SET_BREAKPOINT = 1, + REMOVE_BREAKPOINT, + }; + struct BreakpointEvent { + BreakpointEventType type; + std::string source; + int line; + }; + class DebuggingServer { public: DebuggingServer(Engine& engine, const std::string& serverString); @@ -44,10 +55,12 @@ namespace devtools { void onHitBreakpoint(dv::value&& stackTrace); void setClient(u64id_t client); + std::vector pullBreakpointEvents(); private: Engine& engine; network::Server& server; std::unique_ptr connection; + std::vector breakpointEvents; bool performCommand(const std::string& type, const dv::value& map); }; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 64a89fb5..2a7820a6 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -318,6 +318,7 @@ void Engine::startPauseLoop() { } } while (!isQuitSignal()) { + network->update(); if (!debuggingServer->update()) { debuggingServer.reset(); break; diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index 15749b47..e845f925 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -279,6 +279,33 @@ static int l_debug_breakpoint(lua::State* L) { return 0; } +static int l_debug_pull_events(lua::State* L) { + if (auto server = engine->getDebuggingServer()) { + auto events = server->pullBreakpointEvents(); + if (events.empty()) { + return 0; + } + lua::createtable(L, events.size(), 0); + for (int i = 0; i < events.size(); i++) { + const auto& event = events[i]; + lua::createtable(L, 3, 0); + + lua::pushinteger(L, static_cast(event.type)); + lua::rawseti(L, 1); + + lua::pushstring(L, event.source); + lua::rawseti(L, 2); + + lua::pushinteger(L, event.line); + lua::rawseti(L, 3); + + lua::rawseti(L, i + 1); + } + return 1; + } + return 0; +} + void initialize_libs_extends(lua::State* L) { if (lua::getglobal(L, "debug")) { lua::pushcfunction(L, lua::wrap); @@ -296,6 +323,9 @@ void initialize_libs_extends(lua::State* L) { lua::pushcfunction(L, lua::wrap); lua::setfield(L, "breakpoint"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "__pull_events"); + lua::pop(L); } if (lua::getglobal(L, "math")) { From 9372a5226ebbc01ecb37866f3c1640ce8ac642d8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 7 Oct 2025 21:53:46 +0300 Subject: [PATCH 09/20] add 'step' and 'resume' commands --- res/scripts/stdmin.lua | 15 ++++++++ src/devtools/DebuggingServer.cpp | 40 +++++++++++++++------- src/devtools/DebuggingServer.hpp | 26 ++++++++++---- src/engine/Engine.cpp | 13 +++---- src/engine/Engine.hpp | 2 ++ src/logic/scripting/lua/lua_extensions.cpp | 37 +++++++++++--------- 6 files changed, 91 insertions(+), 42 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index eb3ddbde..46976f77 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -2,8 +2,16 @@ -- may be reused one global ffi buffer per lua_State local breakpoints = {} +local dbg_steps_mode = false +local hook_lock = false debug.sethook(function (e, line) + if dbg_steps_mode and not hook_lock then + hook_lock = true + debug.breakpoint() + debug.pull_events() + end + hook_lock = false local bps = breakpoints[line] if not bps then return @@ -18,6 +26,9 @@ end, "l") local DBG_EVENT_SET_BREAKPOINT = 1 local DBG_EVENT_RM_BREAKPOINT = 2 +local DBG_EVENT_STEP = 3 +local DBG_EVENT_STEP_INTO_FUNCTION = 4 +local DBG_EVENT_RESUME = 5 local __pull_events = debug.__pull_events debug.__pull_events = nil @@ -31,6 +42,10 @@ function debug.pull_events() debug.set_breakpoint(event[2], event[3]) elseif event[1] == DBG_EVENT_RM_BREAKPOINT then debug.remove_breakpoint(event[2], event[3]) + elseif event[1] == DBG_EVENT_STEP then + dbg_steps_mode = true + elseif event[1] == DBG_EVENT_RESUME then + dbg_steps_mode = false end end end diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 09c8c18f..9ef677e3 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -130,25 +130,25 @@ DebuggingServer::~DebuggingServer() { bool DebuggingServer::update() { if (connection == nullptr) { - return true; + return false; } std::string message = connection->read(); if (message.empty()) { - return true; + return false; } logger.debug() << "received: " << message; try { auto obj = json::parse(message); if (!obj.has("type")) { logger.error() << "missing message type"; - return true; + return false; } const auto& type = obj["type"].asString(); return performCommand(type, obj); } catch (const std::runtime_error& err) { logger.error() << "could not to parse message: " << err.what(); } - return true; + return false; } bool DebuggingServer::performCommand( @@ -158,24 +158,38 @@ bool DebuggingServer::performCommand( engine.quit(); connection->sendResponse("success"); } else if (type == "detach") { - logger.info() << "detach received"; connection->sendResponse("success"); connection.reset(); + engine.detachDebugger(); return false; } else if (type == "set-breakpoint" || type == "remove-breakpoint") { if (!map.has("source") || !map.has("line")) - return true; - breakpointEvents.push_back(BreakpointEvent { + return false; + breakpointEvents.push_back(DebuggingEvent { type[0] == 's' - ? BreakpointEventType::SET_BREAKPOINT - : BreakpointEventType::REMOVE_BREAKPOINT, - map["source"].asString(), - static_cast(map["line"].asInteger()), + ? DebuggingEventType::SET_BREAKPOINT + : DebuggingEventType::REMOVE_BREAKPOINT, + BreakpointEventDto { + map["source"].asString(), + static_cast(map["line"].asInteger()), + } }); + } else if (type == "step" || type == "step-into-function") { + breakpointEvents.push_back(DebuggingEvent { + type == "step" + ? DebuggingEventType::STEP + : DebuggingEventType::STEP_INTO_FUNCTION, + SignalEventDto {} + }); + return true; + } else if (type == "resume") { + breakpointEvents.push_back(DebuggingEvent { + DebuggingEventType::RESUME, SignalEventDto {}}); + return true; } else { logger.error() << "unsupported command '" << type << "'"; } - return true; + return false; } void DebuggingServer::onHitBreakpoint(dv::value&& stackTrace) { @@ -195,6 +209,6 @@ void DebuggingServer::setClient(u64id_t client) { std::make_unique(engine.getNetwork(), client); } -std::vector DebuggingServer::pullBreakpointEvents() { +std::vector DebuggingServer::pullEvents() { return std::move(breakpointEvents); } diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index 308cee87..d7be32cf 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "typedefs.hpp" @@ -36,16 +37,27 @@ namespace devtools { u64id_t connection; }; - enum class BreakpointEventType { + enum class DebuggingEventType { SET_BREAKPOINT = 1, REMOVE_BREAKPOINT, + STEP, + STEP_INTO_FUNCTION, + RESUME, }; - struct BreakpointEvent { - BreakpointEventType type; + + struct BreakpointEventDto { std::string source; int line; }; + struct SignalEventDto { + }; + + struct DebuggingEvent { + DebuggingEventType type; + std::variant data; + }; + class DebuggingServer { public: DebuggingServer(Engine& engine, const std::string& serverString); @@ -55,13 +67,15 @@ namespace devtools { void onHitBreakpoint(dv::value&& stackTrace); void setClient(u64id_t client); - std::vector pullBreakpointEvents(); + std::vector pullEvents(); private: Engine& engine; network::Server& server; std::unique_ptr connection; - std::vector breakpointEvents; + std::vector breakpointEvents; - bool performCommand(const std::string& type, const dv::value& map); + bool performCommand( + const std::string& type, const dv::value& map + ); }; } diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 2a7820a6..a977f354 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -284,12 +284,14 @@ void Engine::postUpdate() { scripting::process_post_runnables(); if (debuggingServer) { - if (!debuggingServer->update()) { - debuggingServer.reset(); - } + debuggingServer->update(); } } +void Engine::detachDebugger() { + debuggingServer.reset(); +} + void Engine::updateFrontend() { double delta = time.getDelta(); updateHotkeys(); @@ -317,10 +319,9 @@ void Engine::startPauseLoop() { input->toggleCursor(); } } - while (!isQuitSignal()) { + while (!isQuitSignal() && debuggingServer) { network->update(); - if (!debuggingServer->update()) { - debuggingServer.reset(); + if (debuggingServer->update()) { break; } if (isHeadless()) { diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index a9fb62ea..67acc498 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -190,4 +190,6 @@ public: devtools::DebuggingServer* getDebuggingServer() { return debuggingServer.get(); } + + void detachDebugger(); }; diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index e845f925..fc312820 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -280,30 +280,33 @@ static int l_debug_breakpoint(lua::State* L) { } static int l_debug_pull_events(lua::State* L) { - if (auto server = engine->getDebuggingServer()) { - auto events = server->pullBreakpointEvents(); - if (events.empty()) { - return 0; - } - lua::createtable(L, events.size(), 0); - for (int i = 0; i < events.size(); i++) { - const auto& event = events[i]; - lua::createtable(L, 3, 0); + auto server = engine->getDebuggingServer(); + if (!server) { + return 0; + } + auto events = server->pullEvents(); + if (events.empty()) { + return 0; + } + lua::createtable(L, events.size(), 0); + for (int i = 0; i < events.size(); i++) { + const auto& event = events[i]; + lua::createtable(L, 3, 0); - lua::pushinteger(L, static_cast(event.type)); - lua::rawseti(L, 1); + lua::pushinteger(L, static_cast(event.type)); + lua::rawseti(L, 1); - lua::pushstring(L, event.source); + if (auto dto = std::get_if(&event.data)) { + lua::pushstring(L, dto->source); lua::rawseti(L, 2); - lua::pushinteger(L, event.line); + lua::pushinteger(L, dto->line); lua::rawseti(L, 3); - - lua::rawseti(L, i + 1); } - return 1; + + lua::rawseti(L, i + 1); } - return 0; + return 1; } void initialize_libs_extends(lua::State* L) { From aa42fb1e39154194a4dd72b79fcf757ef2f83be8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 7 Oct 2025 23:51:02 +0300 Subject: [PATCH 10/20] add 'step-into-function' command --- res/scripts/stdmin.lua | 46 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 46976f77..adf78fcf 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -3,11 +3,44 @@ local breakpoints = {} local dbg_steps_mode = false +local dbg_step_into_func = false local hook_lock = false +local current_func +local current_func_stack_size + +local _debug_getinfo = debug.getinfo + +-- 'return' hook not called for some functions +-- todo: speedup +local function calc_stack_size() + local s = debug.traceback("", 2) + local count = 0 + for i in s:gmatch("\n") do + count = count + 1 + end + return count +end debug.sethook(function (e, line) + if e == "return" then + local info = _debug_getinfo(2) + if info.func == current_func then + current_func = nil + end + end if dbg_steps_mode and not hook_lock then hook_lock = true + + if not dbg_step_into_func then + local func = _debug_getinfo(2).func + if func ~= current_func then + return + end + if current_func_stack_size ~= calc_stack_size() then + return + end + end + current_func = func debug.breakpoint() debug.pull_events() end @@ -16,13 +49,15 @@ debug.sethook(function (e, line) if not bps then return end - local source = debug.getinfo(2).source + local source = _debug_getinfo(2).source if not bps[source] then return end + current_func = _debug_getinfo(2).func + current_func_stack_size = calc_stack_size() debug.breakpoint() debug.pull_events() -end, "l") +end, "lr") local DBG_EVENT_SET_BREAKPOINT = 1 local DBG_EVENT_RM_BREAKPOINT = 2 @@ -44,8 +79,13 @@ function debug.pull_events() debug.remove_breakpoint(event[2], event[3]) elseif event[1] == DBG_EVENT_STEP then dbg_steps_mode = true + dbg_step_into_func = false + elseif event[1] == DBG_EVENT_STEP_INTO_FUNCTION then + dbg_steps_mode = true + dbg_step_into_func = true elseif event[1] == DBG_EVENT_RESUME then dbg_steps_mode = false + dbg_step_into_func = false end end end @@ -540,8 +580,6 @@ function file.readlines(path) return lines end -local _debug_getinfo = debug.getinfo - function debug.count_frames() local frames = 1 while true do From 8f569699979808aeb12cf03f09801e596d604bd7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 8 Oct 2025 21:35:37 +0300 Subject: [PATCH 11/20] add 'get-value' command --- res/scripts/stdmin.lua | 19 ++- src/devtools/DebuggingServer.cpp | 45 ++++++ src/devtools/DebuggingServer.hpp | 14 +- src/logic/scripting/lua/lua_extensions.cpp | 180 ++++++++++++++------- src/logic/scripting/lua/lua_util.hpp | 2 +- src/logic/scripting/lua/lua_wrapper.hpp | 4 +- 6 files changed, 195 insertions(+), 69 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index adf78fcf..5854f428 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -1,6 +1,3 @@ --- Lua has no parallelizm, also _set_data does not call any lua functions so --- may be reused one global ffi buffer per lua_State - local breakpoints = {} local dbg_steps_mode = false local dbg_step_into_func = false @@ -9,6 +6,7 @@ local current_func local current_func_stack_size local _debug_getinfo = debug.getinfo +local _debug_getlocal = debug.getlocal -- 'return' hook not called for some functions -- todo: speedup @@ -64,8 +62,11 @@ local DBG_EVENT_RM_BREAKPOINT = 2 local DBG_EVENT_STEP = 3 local DBG_EVENT_STEP_INTO_FUNCTION = 4 local DBG_EVENT_RESUME = 5 +local DBG_EVENT_GET_VALUE = 6 local __pull_events = debug.__pull_events +local __sendvalue = debug.__sendvalue debug.__pull_events = nil +debug.__sendvalue = nil function debug.pull_events() local events = __pull_events() @@ -86,6 +87,16 @@ function debug.pull_events() elseif event[1] == DBG_EVENT_RESUME then dbg_steps_mode = false dbg_step_into_func = false + elseif event[1] == DBG_EVENT_GET_VALUE then + local _, value = _debug_getlocal(event[2] + 3, event[3]) + for _, key in ipairs(event[4]) do + if value == nil then + value = "error: index nil value" + break + end + value = value[key] + end + __sendvalue(value, event[2], event[3], event[4]) end end end @@ -107,6 +118,8 @@ function debug.remove_breakpoint(source, line) bps[source] = nil end +-- Lua has no parallelizm, also _set_data does not call any lua functions so +-- may be reused one global ffi buffer per lua_State local canvas_ffi_buffer local canvas_ffi_buffer_size = 0 diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 9ef677e3..d6777db7 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -186,6 +186,27 @@ bool DebuggingServer::performCommand( breakpointEvents.push_back(DebuggingEvent { DebuggingEventType::RESUME, SignalEventDto {}}); return true; + } else if (type == "get-value") { + if (!map.has("frame") || !map.has("local") || !map.has("path")) + return false; + + int frame = map["frame"].asInteger(); + int localIndex = map["local"].asInteger(); + + ValuePath path; + for (const auto& segment : map["path"]) { + if (segment.isString()) { + path.emplace_back(segment.asString()); + } else { + path.emplace_back(static_cast(segment.asInteger())); + } + } + breakpointEvents.push_back(DebuggingEvent { + DebuggingEventType::GET_VALUE, GetValueEventDto { + frame, localIndex, std::move(path) + } + }); + return true; } else { logger.error() << "unsupported command '" << type << "'"; } @@ -204,6 +225,30 @@ void DebuggingServer::onHitBreakpoint(dv::value&& stackTrace) { engine.startPauseLoop(); } +void DebuggingServer::sendValue( + dv::value&& value, int frame, int local, ValuePath&& path +) { + auto pathValue = dv::list(); + for (const auto& segment : path) { + if (auto string = std::get_if(&segment)) { + pathValue.add(*string); + } else { + pathValue.add(std::get(segment)); + } + } + connection->send(dv::object({ + {"type", std::string("value")}, + {"frame", frame}, + {"local", local}, + {"path", std::move(pathValue)}, + {"value", std::move(value)}, + })); +} + +void DebuggingServer::pause() { + engine.startPauseLoop(); +} + void DebuggingServer::setClient(u64id_t client) { this->connection = std::make_unique(engine.getNetwork(), client); diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index d7be32cf..a4557a7a 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -43,6 +43,7 @@ namespace devtools { STEP, STEP_INTO_FUNCTION, RESUME, + GET_VALUE, }; struct BreakpointEventDto { @@ -53,9 +54,17 @@ namespace devtools { struct SignalEventDto { }; + using ValuePath = std::vector>; + + struct GetValueEventDto { + int frame; + int localIndex; + ValuePath path; + }; + struct DebuggingEvent { DebuggingEventType type; - std::variant data; + std::variant data; }; class DebuggingServer { @@ -65,6 +74,9 @@ namespace devtools { bool update(); void onHitBreakpoint(dv::value&& stackTrace); + void pause(); + + void sendValue(dv::value&& value, int frame, int local, ValuePath&& path); void setClient(u64id_t client); std::vector pullEvents(); diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index fc312820..e791a87d 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -9,6 +9,7 @@ #include "devtools/DebuggingServer.hpp" #include "logic/scripting/scripting.hpp" +using namespace devtools; using namespace scripting; static debug::Logger logger("lua-debug"); @@ -164,8 +165,52 @@ static int l_math_normal_random(lua::State* L) { constexpr inline int MAX_SHORT_STRING_LEN = 50; +static std::string get_short_value(lua::State* L, int idx, int type) { + switch (type) { + case LUA_TNIL: + return "nil"; + case LUA_TBOOLEAN: + return lua::toboolean(L, idx) ? "true" : "false"; + case LUA_TNUMBER: { + std::stringstream ss; + ss << lua::tonumber(L, idx); + return ss.str(); + } + case LUA_TSTRING: { + const char* str = lua::tostring(L, idx); + if (strlen(str) > MAX_SHORT_STRING_LEN) { + return std::string(str, MAX_SHORT_STRING_LEN); + } else { + return str; + } + } + case LUA_TTABLE: + return "{...}"; + case LUA_TFUNCTION: { + std::stringstream ss; + ss << "function: 0x" << std::hex << lua::topointer(L, idx); + return ss.str(); + } + case LUA_TUSERDATA: { + std::stringstream ss; + ss << "userdata: 0x" << std::hex << lua::topointer(L, idx); + return ss.str(); + } + case LUA_TTHREAD: { + std::stringstream ss; + ss << "thread: 0x" << std::hex << lua::topointer(L, idx); + return ss.str(); + } + default: { + std::stringstream ss; + ss << "cdata: 0x" << std::hex << lua::topointer(L, idx); + return ss.str(); + } + } +} + static dv::value collect_locals(lua::State* L, lua_Debug& frame) { - auto locals = dv::object(); + auto locals = dv::list(); int localIndex = 1; const char* name; @@ -175,68 +220,13 @@ static dv::value collect_locals(lua::State* L, lua_Debug& frame) { continue; } auto local = dv::object(); + local["name"] = name; + local["index"] = localIndex - 1; int type = lua::type(L, -1); - switch (type) { - case LUA_TNIL: - local["short"] = "nil"; - local["type"] = "nil"; - break; - case LUA_TBOOLEAN: - local["short"] = lua::toboolean(L, -1) ? "true" : "false"; - local["type"] = "boolean"; - break; - case LUA_TNUMBER: { - std::stringstream ss; - ss << lua::tonumber(L, -1); - local["short"] = ss.str(); - local["type"] = "number"; - break; - } - case LUA_TSTRING: { - const char* str = lua::tostring(L, -1); - if (strlen(str) > MAX_SHORT_STRING_LEN) { - local["short"] = std::string(str, MAX_SHORT_STRING_LEN); - } else { - local["short"] = str; - } - local["type"] = "string"; - break; - } - case LUA_TTABLE: - local["short"] = "{...}"; - local["type"] = "table"; - break; - case LUA_TFUNCTION: { - std::stringstream ss; - ss << "function: 0x" << std::hex << lua::topointer(L, -1); - local["short"] = ss.str(); - local["type"] = "function"; - break; - } - case LUA_TUSERDATA: { - std::stringstream ss; - ss << "userdata: 0x" << std::hex << lua::topointer(L, -1); - local["short"] = ss.str(); - local["type"] = "userdata"; - break; - } - case LUA_TTHREAD: { - std::stringstream ss; - ss << "thread: 0x" << std::hex << lua::topointer(L, -1); - local["short"] = ss.str(); - local["type"] = "thread"; - break; - } - default: { - std::stringstream ss; - ss << "cdata: 0x" << std::hex << lua::topointer(L, -1); - local["short"] = ss.str(); - local["type"] = "cdata"; - break; - } - } - locals[name] = std::move(local); + local["type"] = lua::type_name(L, type); + local["short"] = get_short_value(L, -1, type); + locals.add(std::move(local)); lua::pop(L); } return locals; @@ -279,6 +269,51 @@ static int l_debug_breakpoint(lua::State* L) { return 0; } +static int l_debug_sendvalue(lua::State* L) { + auto server = engine->getDebuggingServer(); + if (!server) { + return 0; + } + int frame = lua::tointeger(L, 2); + int local = lua::tointeger(L, 3); + + ValuePath path; + int pathSectors = lua::objlen(L, 4); + for (int i = 0; i < pathSectors; i++) { + lua::rawgeti(L, i + 1, 4); + if (lua::isstring(L, -1)) { + path.emplace_back(lua::tostring(L, -1)); + } else { + path.emplace_back(static_cast(lua::tointeger(L, -1))); + } + lua::pop(L); + } + + dv::value value = nullptr; + if (lua::istable(L, 1)) { + auto table = dv::object(); + + lua::pushnil(L); + while (lua::next(L, 1)) { + auto key = lua::tolstring(L, -2); + + int type = lua::type(L, -1); + table[std::string(key)] = dv::object({ + {"type", std::string(lua::type_name(L, type))}, + {"short", get_short_value(L, -1, type)}, + }); + lua::pop(L); + } + lua::pop(L); + value = std::move(table); + } else { + value = lua::tovalue(L, 1); + } + + server->sendValue(std::move(value), frame, local, std::move(path)); + return 0; +} + static int l_debug_pull_events(lua::State* L) { auto server = engine->getDebuggingServer(); if (!server) { @@ -296,12 +331,30 @@ static int l_debug_pull_events(lua::State* L) { lua::pushinteger(L, static_cast(event.type)); lua::rawseti(L, 1); - if (auto dto = std::get_if(&event.data)) { + if (auto dto = std::get_if(&event.data)) { lua::pushstring(L, dto->source); lua::rawseti(L, 2); lua::pushinteger(L, dto->line); lua::rawseti(L, 3); + } else if (auto dto = std::get_if(&event.data)) { + lua::pushinteger(L, dto->frame); + lua::rawseti(L, 2); + + lua::pushinteger(L, dto->localIndex); + lua::rawseti(L, 3); + + lua::createtable(L, dto->path.size(), 0); + for (int i = 0; i < dto->path.size(); i++) { + const auto& segment = dto->path[i]; + if (auto string = std::get_if(&segment)) { + lua::pushstring(L, *string); + } else { + lua::pushinteger(L, std::get(segment)); + } + lua::rawseti(L, i + 1); + } + lua::rawseti(L, 4); } lua::rawseti(L, i + 1); @@ -329,6 +382,9 @@ void initialize_libs_extends(lua::State* L) { lua::pushcfunction(L, lua::wrap); lua::setfield(L, "__pull_events"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "__sendvalue"); + lua::pop(L); } if (lua::getglobal(L, "math")) { diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index e72b56eb..a8fb246e 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -249,7 +249,7 @@ namespace lua { inline lua::Number tonumber(lua::State* L, int idx) { #ifndef NDEBUG if (lua_type(L, idx) != LUA_TNUMBER && !lua_isnoneornil(L, idx)) { - throw std::runtime_error("integer expected"); + throw std::runtime_error("number expected"); } #endif return lua_tonumber(L, idx); diff --git a/src/logic/scripting/lua/lua_wrapper.hpp b/src/logic/scripting/lua/lua_wrapper.hpp index f82f5bd4..df130af1 100644 --- a/src/logic/scripting/lua/lua_wrapper.hpp +++ b/src/logic/scripting/lua/lua_wrapper.hpp @@ -50,8 +50,8 @@ namespace lua { inline int type(lua::State* L, int idx) { return lua_type(L, idx); } - inline const char* type_name(lua::State* L, int idx) { - return lua_typename(L, idx); + inline const char* type_name(lua::State* L, int tp) { + return lua_typename(L, tp); } inline int rawget(lua::State* L, int idx = -2) { lua_rawget(L, idx); From 4f6a443fa3e59e0304315b274a1b31544701a76a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 8 Oct 2025 23:11:10 +0300 Subject: [PATCH 12/20] replace 'debug.breakpoint' with 'debug.pause' --- res/scripts/stdmin.lua | 6 ++++-- src/devtools/DebuggingServer.cpp | 18 ++++++++---------- src/devtools/DebuggingServer.hpp | 3 +-- src/logic/scripting/lua/lua_extensions.cpp | 12 ++++++++---- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 5854f428..60961653 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -39,7 +39,7 @@ debug.sethook(function (e, line) end end current_func = func - debug.breakpoint() + debug.pause() debug.pull_events() end hook_lock = false @@ -53,7 +53,7 @@ debug.sethook(function (e, line) end current_func = _debug_getinfo(2).func current_func_stack_size = calc_stack_size() - debug.breakpoint() + debug.pause() debug.pull_events() end, "lr") @@ -65,6 +65,7 @@ local DBG_EVENT_RESUME = 5 local DBG_EVENT_GET_VALUE = 6 local __pull_events = debug.__pull_events local __sendvalue = debug.__sendvalue +local __pause = debug.pause debug.__pull_events = nil debug.__sendvalue = nil @@ -97,6 +98,7 @@ function debug.pull_events() value = value[key] end __sendvalue(value, event[2], event[3], event[4]) + __pause() end end end diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index d6777db7..2cf67f48 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -213,15 +213,17 @@ bool DebuggingServer::performCommand( return false; } -void DebuggingServer::onHitBreakpoint(dv::value&& stackTrace) { +void DebuggingServer::pause(std::string&& message, dv::value&& stackTrace) { if (connection == nullptr) { return; } - connection->send(dv::object({ - {"type", std::string("hit-breakpoint")}, - {"stack", std::move(stackTrace)} - })); - + if (stackTrace != nullptr) { + connection->send(dv::object({ + {"type", std::string("hit-breakpoint")}, + {"message", std::move(message)}, + {"stack", std::move(stackTrace)} + })); + } engine.startPauseLoop(); } @@ -245,10 +247,6 @@ void DebuggingServer::sendValue( })); } -void DebuggingServer::pause() { - engine.startPauseLoop(); -} - void DebuggingServer::setClient(u64id_t client) { this->connection = std::make_unique(engine.getNetwork(), client); diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index a4557a7a..0f3b4566 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -73,8 +73,7 @@ namespace devtools { ~DebuggingServer(); bool update(); - void onHitBreakpoint(dv::value&& stackTrace); - void pause(); + void pause(std::string&& message, dv::value&& stackTrace); void sendValue(dv::value&& value, int frame, int local, ValuePath&& path); diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index e791a87d..88d8427f 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -262,9 +262,13 @@ static dv::value create_stack_trace(lua::State* L, int initFrame = 2) { return entriesList; } -static int l_debug_breakpoint(lua::State* L) { +static int l_debug_pause(lua::State* L) { if (auto server = engine->getDebuggingServer()) { - server->onHitBreakpoint(create_stack_trace(L)); + std::string message; + if (lua::isstring(L, 1)) { + message = lua::tolstring(L, 1); + } + server->pause(std::move(message), create_stack_trace(L)); } return 0; } @@ -376,8 +380,8 @@ void initialize_libs_extends(lua::State* L) { lua::pushcfunction(L, lua::wrap); lua::setfield(L, "print"); - lua::pushcfunction(L, lua::wrap); - lua::setfield(L, "breakpoint"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "pause"); lua::pushcfunction(L, lua::wrap); lua::setfield(L, "__pull_events"); From ddc56b46ce413c3481a9d1c3cd147203deea3a2f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 8 Oct 2025 23:20:41 +0300 Subject: [PATCH 13/20] cleanup --- res/scripts/stdmin.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 60961653..b0410bc2 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -7,6 +7,7 @@ local current_func_stack_size local _debug_getinfo = debug.getinfo local _debug_getlocal = debug.getlocal +local __pause = debug.pause -- 'return' hook not called for some functions -- todo: speedup @@ -39,7 +40,7 @@ debug.sethook(function (e, line) end end current_func = func - debug.pause() + __pause() debug.pull_events() end hook_lock = false @@ -53,7 +54,7 @@ debug.sethook(function (e, line) end current_func = _debug_getinfo(2).func current_func_stack_size = calc_stack_size() - debug.pause() + __pause("paused on breakpoint") debug.pull_events() end, "lr") @@ -65,7 +66,6 @@ local DBG_EVENT_RESUME = 5 local DBG_EVENT_GET_VALUE = 6 local __pull_events = debug.__pull_events local __sendvalue = debug.__sendvalue -local __pause = debug.pause debug.__pull_events = nil debug.__sendvalue = nil From 972181022a2fc1e9c3fc1826c9e756cc153c7ddf Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 8 Oct 2025 23:50:36 +0300 Subject: [PATCH 14/20] feat: pause on exception caused by 'error' function call --- res/scripts/stdmin.lua | 6 ++++++ src/logic/scripting/lua/lua_extensions.cpp | 12 ++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index b0410bc2..4487860f 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -8,6 +8,7 @@ local current_func_stack_size local _debug_getinfo = debug.getinfo local _debug_getlocal = debug.getlocal local __pause = debug.pause +local __error = error -- 'return' hook not called for some functions -- todo: speedup @@ -120,6 +121,11 @@ function debug.remove_breakpoint(source, line) bps[source] = nil end +function error(message, level) + __pause("paused on exception: " .. message) + __error(message, level) +end + -- Lua has no parallelizm, also _set_data does not call any lua functions so -- may be reused one global ffi buffer per lua_State local canvas_ffi_buffer diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index 88d8427f..f1b7638f 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -188,22 +188,26 @@ static std::string get_short_value(lua::State* L, int idx, int type) { return "{...}"; case LUA_TFUNCTION: { std::stringstream ss; - ss << "function: 0x" << std::hex << lua::topointer(L, idx); + ss << "function: 0x" << std::hex + << reinterpret_cast(lua::topointer(L, idx)); return ss.str(); } case LUA_TUSERDATA: { std::stringstream ss; - ss << "userdata: 0x" << std::hex << lua::topointer(L, idx); + ss << "userdata: 0x" << std::hex + << reinterpret_cast(lua::topointer(L, idx)); return ss.str(); } case LUA_TTHREAD: { std::stringstream ss; - ss << "thread: 0x" << std::hex << lua::topointer(L, idx); + ss << "thread: 0x" << std::hex + << reinterpret_cast(lua::topointer(L, idx)); return ss.str(); } default: { std::stringstream ss; - ss << "cdata: 0x" << std::hex << lua::topointer(L, idx); + ss << "cdata: 0x" << std::hex + << reinterpret_cast(lua::topointer(L, idx)); return ss.str(); } } From 33a5410ca2b23299768663cdc8eb12037c21cf7a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 9 Oct 2025 00:23:54 +0300 Subject: [PATCH 15/20] add 'reason' argument to debug.pause --- res/scripts/stdmin.lua | 4 ++-- src/devtools/DebuggingServer.cpp | 5 ++++- src/devtools/DebuggingServer.hpp | 4 +++- src/logic/scripting/lua/lua_extensions.cpp | 10 ++++++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 4487860f..3e07c26b 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -55,7 +55,7 @@ debug.sethook(function (e, line) end current_func = _debug_getinfo(2).func current_func_stack_size = calc_stack_size() - __pause("paused on breakpoint") + __pause("breakpoint") debug.pull_events() end, "lr") @@ -122,7 +122,7 @@ function debug.remove_breakpoint(source, line) end function error(message, level) - __pause("paused on exception: " .. message) + __pause("exception", message) __error(message, level) end diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 2cf67f48..505f1ff5 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -213,13 +213,16 @@ bool DebuggingServer::performCommand( return false; } -void DebuggingServer::pause(std::string&& message, dv::value&& stackTrace) { +void DebuggingServer::pause( + std::string&& reason, std::string&& message, dv::value&& stackTrace +) { if (connection == nullptr) { return; } if (stackTrace != nullptr) { connection->send(dv::object({ {"type", std::string("hit-breakpoint")}, + {"reason", std::move(reason)}, {"message", std::move(message)}, {"stack", std::move(stackTrace)} })); diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index 0f3b4566..cd4931cb 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -73,7 +73,9 @@ namespace devtools { ~DebuggingServer(); bool update(); - void pause(std::string&& message, dv::value&& stackTrace); + void pause( + std::string&& reason, std::string&& message, dv::value&& stackTrace + ); void sendValue(dv::value&& value, int frame, int local, ValuePath&& path); diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index f1b7638f..b2224104 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -268,11 +268,17 @@ static dv::value create_stack_trace(lua::State* L, int initFrame = 2) { static int l_debug_pause(lua::State* L) { if (auto server = engine->getDebuggingServer()) { + std::string reason; std::string message; if (lua::isstring(L, 1)) { - message = lua::tolstring(L, 1); + reason = lua::tolstring(L, 1); } - server->pause(std::move(message), create_stack_trace(L)); + if (lua::isstring(L, 2)) { + message = lua::tolstring(L, 2); + } + server->pause( + std::move(reason), std::move(message), create_stack_trace(L) + ); } return 0; } From f4c3b53cd13aa3b3983fe05abb026a19d597df66 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 9 Oct 2025 00:28:06 +0300 Subject: [PATCH 16/20] replace 'hit-breakpoint' with 'paused' --- res/scripts/stdmin.lua | 2 +- src/devtools/DebuggingServer.cpp | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 3e07c26b..8c9e0a42 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -41,7 +41,7 @@ debug.sethook(function (e, line) end end current_func = func - __pause() + __pause("step") debug.pull_events() end hook_lock = false diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 505f1ff5..a9a32667 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -219,14 +219,12 @@ void DebuggingServer::pause( if (connection == nullptr) { return; } - if (stackTrace != nullptr) { - connection->send(dv::object({ - {"type", std::string("hit-breakpoint")}, - {"reason", std::move(reason)}, - {"message", std::move(message)}, - {"stack", std::move(stackTrace)} - })); - } + connection->send(dv::object({ + {"type", std::string("paused")}, + {"reason", std::move(reason)}, + {"message", std::move(message)}, + {"stack", std::move(stackTrace)} + })); engine.startPauseLoop(); } From a33e3068c8d30567517b3cf73b48789432784d0a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 9 Oct 2025 00:42:19 +0300 Subject: [PATCH 17/20] feat: resume on disconnect & add 'resumed' response --- src/devtools/DebuggingServer.cpp | 30 +++++++++++++++++++++++------- src/devtools/DebuggingServer.hpp | 2 ++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index a9a32667..2baebf47 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -61,6 +61,10 @@ void ClientConnection::sendResponse(const std::string& type) { send(dv::object({{"type", type}})); } +bool ClientConnection::alive() const { + return network.getConnection(this->connection, true) != nullptr; +} + static network::Server& create_tcp_server( DebuggingServer& dbgServer, Engine& engine, int port ) { @@ -134,6 +138,10 @@ bool DebuggingServer::update() { } std::string message = connection->read(); if (message.empty()) { + if (!connection->alive()) { + connection.reset(); + return true; + } return false; } logger.debug() << "received: " << message; @@ -144,7 +152,10 @@ bool DebuggingServer::update() { return false; } const auto& type = obj["type"].asString(); - return performCommand(type, obj); + if (performCommand(type, obj)) { + connection->sendResponse("resumed"); + return true; + } } catch (const std::runtime_error& err) { logger.error() << "could not to parse message: " << err.what(); } @@ -219,12 +230,17 @@ void DebuggingServer::pause( if (connection == nullptr) { return; } - connection->send(dv::object({ - {"type", std::string("paused")}, - {"reason", std::move(reason)}, - {"message", std::move(message)}, - {"stack", std::move(stackTrace)} - })); + auto response = dv::object({{"type", std::string("paused")}}); + if (!reason.empty()) { + response["reason"] = std::move(reason); + } + if (!message.empty()) { + response["message"] = std::move(message); + } + if (stackTrace != nullptr) { + response["stack"] = std::move(stackTrace); + } + connection->send(std::move(response)); engine.startPauseLoop(); } diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index cd4931cb..13c9282b 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -31,6 +31,8 @@ namespace devtools { std::string read(); void send(const dv::value& message); void sendResponse(const std::string& type); + + bool alive() const; private: network::Network& network; size_t messageLength = 0; From 1a7bcf986517c32ffd622f1e455d98baaff5f3a6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 9 Oct 2025 01:04:08 +0300 Subject: [PATCH 18/20] add 'connect' command & feat: disconnect-action --- src/devtools/DebuggingServer.cpp | 16 +++++++++++++++- src/devtools/DebuggingServer.hpp | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index 2baebf47..b6a67c7e 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -139,8 +139,9 @@ bool DebuggingServer::update() { std::string message = connection->read(); if (message.empty()) { if (!connection->alive()) { + bool status = performCommand(disconnectAction, dv::object()); connection.reset(); - return true; + return status; } return false; } @@ -165,6 +166,15 @@ bool DebuggingServer::update() { bool DebuggingServer::performCommand( const std::string& type, const dv::value& map ) { + if (type == "connect" && !connectionEstablished) { + map.at("disconnect-action").get(disconnectAction); + connectionEstablished = true; + logger.info() << "client connection established"; + connection->sendResponse("success"); + } + if (!connectionEstablished) { + return false; + } if (type == "terminate") { engine.quit(); connection->sendResponse("success"); @@ -272,3 +282,7 @@ void DebuggingServer::setClient(u64id_t client) { std::vector DebuggingServer::pullEvents() { return std::move(breakpointEvents); } + +void DebuggingServer::setDisconnectAction(const std::string& action) { + disconnectAction = action; +} diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index 13c9282b..533453d7 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -83,11 +83,15 @@ namespace devtools { void setClient(u64id_t client); std::vector pullEvents(); + + void setDisconnectAction(const std::string& action); private: Engine& engine; network::Server& server; std::unique_ptr connection; + bool connectionEstablished = false; std::vector breakpointEvents; + std::string disconnectAction = "resume"; bool performCommand( const std::string& type, const dv::value& map From d861595f784601f72551d8f2dfcb1a2da52c5b2e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 9 Oct 2025 01:14:49 +0300 Subject: [PATCH 19/20] disable debugging if server is not running & make detach complete --- res/scripts/stdmin.lua | 79 +++++++++++++--------- src/logic/scripting/lua/lua_extensions.cpp | 7 ++ 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 8c9e0a42..df5c7ce2 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -9,6 +9,7 @@ local _debug_getinfo = debug.getinfo local _debug_getlocal = debug.getlocal local __pause = debug.pause local __error = error +local __sethook = debug.sethook -- 'return' hook not called for some functions -- todo: speedup @@ -21,43 +22,46 @@ local function calc_stack_size() return count end -debug.sethook(function (e, line) - if e == "return" then - local info = _debug_getinfo(2) - if info.func == current_func then - current_func = nil +local is_debugging = debug.is_debugging() +if is_debugging then + __sethook(function (e, line) + if e == "return" then + local info = _debug_getinfo(2) + if info.func == current_func then + current_func = nil + end end - end - if dbg_steps_mode and not hook_lock then - hook_lock = true + if dbg_steps_mode and not hook_lock then + hook_lock = true - if not dbg_step_into_func then - local func = _debug_getinfo(2).func - if func ~= current_func then - return - end - if current_func_stack_size ~= calc_stack_size() then - return + if not dbg_step_into_func then + local func = _debug_getinfo(2).func + if func ~= current_func then + return + end + if current_func_stack_size ~= calc_stack_size() then + return + end end + current_func = func + __pause("step") + debug.pull_events() end - current_func = func - __pause("step") + hook_lock = false + local bps = breakpoints[line] + if not bps then + return + end + local source = _debug_getinfo(2).source + if not bps[source] then + return + end + current_func = _debug_getinfo(2).func + current_func_stack_size = calc_stack_size() + __pause("breakpoint") debug.pull_events() - end - hook_lock = false - local bps = breakpoints[line] - if not bps then - return - end - local source = _debug_getinfo(2).source - if not bps[source] then - return - end - current_func = _debug_getinfo(2).func - current_func_stack_size = calc_stack_size() - __pause("breakpoint") - debug.pull_events() -end, "lr") + end, "lr") +end local DBG_EVENT_SET_BREAKPOINT = 1 local DBG_EVENT_RM_BREAKPOINT = 2 @@ -71,6 +75,13 @@ debug.__pull_events = nil debug.__sendvalue = nil function debug.pull_events() + if not is_debugging then + return + end + if not debug.is_debugging() then + is_debugging = false + __sethook() + end local events = __pull_events() if not events then return @@ -122,7 +133,9 @@ function debug.remove_breakpoint(source, line) end function error(message, level) - __pause("exception", message) + if is_debugging then + __pause("exception", message) + end __error(message, level) end diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index b2224104..27b47fae 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -376,6 +376,10 @@ static int l_debug_pull_events(lua::State* L) { return 1; } +static int l_debug_is_debugging(lua::State* L) { + return lua::pushboolean(L, engine->getDebuggingServer() != nullptr); +} + void initialize_libs_extends(lua::State* L) { if (lua::getglobal(L, "debug")) { lua::pushcfunction(L, lua::wrap); @@ -399,6 +403,9 @@ void initialize_libs_extends(lua::State* L) { lua::pushcfunction(L, lua::wrap); lua::setfield(L, "__sendvalue"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "is_debugging"); + lua::pop(L); } if (lua::getglobal(L, "math")) { From e83b3b3ce1697fd08b5ebcb8375698508e15352c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 9 Oct 2025 02:13:03 +0300 Subject: [PATCH 20/20] add protocol version check --- src/devtools/DebuggingServer.cpp | 31 +++++++++++++++++++++++++++++-- src/devtools/DebuggingServer.hpp | 6 ++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp index b6a67c7e..09a2b368 100644 --- a/src/devtools/DebuggingServer.cpp +++ b/src/devtools/DebuggingServer.cpp @@ -17,6 +17,27 @@ ClientConnection::~ClientConnection() { } } +bool ClientConnection::initiate(network::ReadableConnection* connection) { + if (connection->available() < 8) { + return false; + } + char buffer[8] {}; + char expected[8] {}; + std::memcpy(expected, VCDBG_MAGIC, sizeof(VCDBG_MAGIC)); + expected[6] = VCDBG_VERSION >> 8; + expected[7] = VCDBG_VERSION & 0xFF; + connection->recv(buffer, sizeof(VCDBG_MAGIC)); + + connection->send(expected, sizeof(VCDBG_MAGIC)); + if (std::memcmp(expected, buffer, sizeof(VCDBG_MAGIC)) == 0) { + initiated = true; + return false; + } else { + connection->close(true); + return true; + } +} + std::string ClientConnection::read() { auto connection = dynamic_cast( network.getConnection(this->connection, true) @@ -24,6 +45,11 @@ std::string ClientConnection::read() { if (connection == nullptr) { return ""; } + if (!initiated) { + if (initiate(connection)) { + return ""; + } + } if (messageLength == 0) { if (connection->available() >= sizeof(int32_t)) { int32_t length = 0; @@ -166,7 +192,7 @@ bool DebuggingServer::update() { bool DebuggingServer::performCommand( const std::string& type, const dv::value& map ) { - if (type == "connect" && !connectionEstablished) { + if (!connectionEstablished && type == "connect") { map.at("disconnect-action").get(disconnectAction); connectionEstablished = true; logger.info() << "client connection established"; @@ -275,8 +301,9 @@ void DebuggingServer::sendValue( } void DebuggingServer::setClient(u64id_t client) { - this->connection = + connection = std::make_unique(engine.getNetwork(), client); + connectionEstablished = false; } std::vector DebuggingServer::pullEvents() { diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp index 533453d7..b02229c6 100644 --- a/src/devtools/DebuggingServer.hpp +++ b/src/devtools/DebuggingServer.hpp @@ -21,6 +21,9 @@ namespace dv { class Engine; namespace devtools { + inline constexpr const char VCDBG_MAGIC[8] = "vc-dbg\0"; + inline constexpr int VCDBG_VERSION = 1; + class ClientConnection { public: ClientConnection(network::Network& network, u64id_t connection) @@ -37,6 +40,9 @@ namespace devtools { network::Network& network; size_t messageLength = 0; u64id_t connection; + bool initiated = false; + + bool initiate(network::ReadableConnection* connection); }; enum class DebuggingEventType {