#include "AngelscriptDebugger.hpp" #include "ASVariableFormatter.hpp" #include "DebugAdapterProtocol/BaseProtocol.hpp" #include "DebugAdapterProtocol/Events.hpp" #include "DebugAdapterProtocol/Requests.hpp" void AngelscriptDebugger::Run(uint16_t port) { _server = new asio::ip::tcp::acceptor(_ioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)); std::thread([this]() { AcceptLoop(); }).detach(); } template std::string string_format(const std::string& format, Args... args) { int size_s = std::snprintf(nullptr, 0, format.c_str(), args...) + 1; // Extra space for '\0' if (size_s <= 0) { throw std::runtime_error("Error during formatting."); } auto size = static_cast(size_s); auto buf = std::make_unique(size); std::snprintf(buf.get(), size, format.c_str(), args...); return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside } std::string AngelscriptDebugger::GetResolvedScriptPath(const char* scriptSection) { if (_scriptPath.has_value()) { return string_format(_scriptPath.value(), scriptSection); } return scriptSection; } void AngelscriptDebugger::on_line_callback(asIScriptContext* ctx, AngelscriptDebugger* d) { if (ctx->GetState() == asEXECUTION_SUSPENDED) { return; } if (!d->HasDebuggerAttached()) { return; } const char* scriptSection = nullptr; int column = 0; int line = ctx->GetLineNumber(0, &column, &scriptSection); if (line == 0) { return; } if (d->_next && d->_nextContext == ctx && d->_nextDepth >= ctx->GetCallstackSize()) { d->_next = false; ctx->Suspend(); d->_pausedContexts.push_back(ctx); auto* o = new DebugAdapterProtocol::StoppedEvent(new DebugAdapterProtocol::StoppedEventBody("step", "", "")); d->Send(o); return; } if (d->_stepInto && d->_nextContext == ctx) { d->_stepInto = false; ctx->Suspend(); d->_pausedContexts.push_back(ctx); auto* o = new DebugAdapterProtocol::StoppedEvent(new DebugAdapterProtocol::StoppedEventBody("step", "", "")); d->Send(o); return; } if (d->_stepOut && d->_nextContext == ctx && d->_nextDepth > ctx->GetCallstackSize()) { d->_stepOut = false; ctx->Suspend(); d->_pausedContexts.push_back(ctx); auto* o = new DebugAdapterProtocol::StoppedEvent(new DebugAdapterProtocol::StoppedEventBody("step", "", "")); d->Send(o); return; } auto resolvedScriptPath = d->GetResolvedScriptPath(scriptSection); auto sectionBreakpoints = d->_breakpoints.find(resolvedScriptPath); if (sectionBreakpoints != d->_breakpoints.end()) { if (sectionBreakpoints->second.contains(line)) { d->EnterBreakpoint(ctx, resolvedScriptPath, line); } } } size_t GetBreakpointHash(const std::string& section, size_t line) { using std::hash; using std::size_t; using std::string; return ((hash()(section) ^ (hash()(line) << 1)) >> 1); } void AngelscriptDebugger::on_exception_callback(asIScriptContext* ctx, AngelscriptDebugger* d) { if (!d->HasDebuggerAttached()) { return; } ctx->Suspend(); d->_pausedContexts.push_back(ctx); const char* scriptSection = nullptr; int column = 0; int line = ctx->GetLineNumber(0, &column, &scriptSection); if (line == 0) return; auto exception = ctx->GetExceptionString(); auto* o = new DebugAdapterProtocol::StoppedEvent( new DebugAdapterProtocol::StoppedEventBody("exception", "Paused on exception", exception)); d->Send(o); } void AngelscriptDebugger::EnterBreakpoint(asIScriptContext* ctx, const std::string& section, size_t line) { ctx->Suspend(); _pausedContexts.push_back(ctx); auto* o = new DebugAdapterProtocol::StoppedEvent( new DebugAdapterProtocol::StoppedEventBody("breakpoint", GetBreakpointHash(section, line))); Send(o); } void AngelscriptDebugger::RegisterContext(asIScriptContext* ctx) { ctx->SetLineCallback(asFUNCTION(on_line_callback), this, asCALL_CDECL); ctx->SetExceptionCallback(asFUNCTION(on_exception_callback), this, asCALL_CDECL); } [[noreturn]] void AngelscriptDebugger::AcceptLoop() { while (true) { auto* client = new asio::ip::tcp::socket(_ioContext); _server->accept(*client); std::thread([this, client]() { ClientLoop(*client); }).detach(); } } inline std::string trim(const std::string& s) { auto wsfront = std::find_if_not(s.begin(), s.end(), [](int c) { return std::isspace(c); }); auto wsback = std::find_if_not(s.rbegin(), s.rend(), [](int c) { return std::isspace(c); }).base(); return (wsback <= wsfront ? std::string() : std::string(wsfront, wsback)); } void AngelscriptDebugger::ClientLoop(asio::ip::tcp::socket& client) { std::unordered_map headers; bool in_header_mode = true; asio::streambuf buffer; while (true) { if (!client.is_open()) { break; } if (client.available() == 0 && buffer.size() == 0) { continue; } if (client.available() > 0 && buffer.size() == 0) { asio::error_code error; asio::read_until(client, buffer, "\r\n", error); } if (in_header_mode) { std::istream str(&buffer); std::string s; std::getline(str, s); if (s == "\r") { if (headers.contains("content-length")) { in_header_mode = false; } continue; } auto delimiter = s.find(':'); std::string key = trim(s.substr(0, delimiter)); std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) { return std::tolower(c); }); std::string value = trim(s.substr(delimiter + 1)); headers[key] = value; } else { auto contentLength = headers["content-length"]; auto size = (size_t)std::strtol(contentLength.c_str(), nullptr, 10); std::cout << "message length: " << size << std::endl; std::stringstream msg; int64_t to_fetch = (int64_t)size - (int64_t)buffer.size(); auto to_read = std::min(buffer.size(), size); asio::streambuf::const_buffers_type constBuffer = buffer.data(); std::copy(asio::buffers_begin(constBuffer), asio::buffers_end(constBuffer), std::ostream_iterator(msg)); buffer.consume(to_read); if (to_fetch > 0) { std::vector v(to_fetch); asio::read(client, asio::buffer(v), asio::transfer_exactly(to_fetch)); msg << std::string(v.begin(), v.end()); } auto m = msg.str(); std::cout << "found message: " << m << std::endl; nlohmann::json j = nlohmann::json::parse(m.begin(), m.end()); auto protocolMessage = DebugAdapterProtocol::ProtocolMessage::FromJson(j); OnMessage(&client, protocolMessage); in_header_mode = true; } } } void AngelscriptDebugger::Send(DebugAdapterProtocol::ProtocolMessage* msg) { auto body = msg->ToJson().dump(); std::vector vec(body.begin(), body.end()); for (const auto& conn : _connections) { if (conn->is_open()) { asio::error_code ignored_error; asio::write(*conn, asio::buffer("Content-Length: " + std::to_string(vec.size()) + "\r\n\r\n"), ignored_error); asio::write(*conn, asio::buffer(vec), ignored_error); } } std::cout << "Sent message: " << body << std::endl; delete msg; } void AngelscriptDebugger::Send(asio::ip::tcp::socket* client, DebugAdapterProtocol::ProtocolMessage* msg) { auto body = msg->ToJson().dump(); std::vector vec(body.begin(), body.end()); if (client->is_open()) { asio::error_code ignored_error; asio::write(*client, asio::buffer("Content-Length: " + std::to_string(vec.size()) + "\r\n\r\n"), ignored_error); asio::write(*client, asio::buffer(vec), ignored_error); } std::cout << "Sent message: " << body << std::endl; delete msg; } void AngelscriptDebugger::OnMessage(asio::ip::tcp::socket* client, DebugAdapterProtocol::ProtocolMessage* msg) { if (msg->type == "request") { OnRequest(client, dynamic_cast(msg)); } delete msg; } const char empty[] = ""; void AngelscriptDebugger::OnRequest(asio::ip::tcp::socket* client, DebugAdapterProtocol::Request* msg) { if (msg->GetCommand() == "setBreakpoints") { auto* t = dynamic_cast(msg); auto& args = t->arguments.value(); auto& path = args->source.path.value(); auto* response = new DebugAdapterProtocol::SetBreakpointsResponse(msg->seq); auto* body = new DebugAdapterProtocol::SetBreakpointsResponseBody(); response->body = body; std::unordered_set sectionBreakpoints; if (args->breakpoints.has_value()) { for (auto& bp : args->breakpoints.value()) { sectionBreakpoints.insert(bp.line); body->breakpoints.emplace_back(GetBreakpointHash(path, bp.line), args->source, bp.line); } } _breakpoints[path] = sectionBreakpoints; Send(client, response); } else if (msg->GetCommand() == "initialize") { auto response = new DebugAdapterProtocol::InitializeResponse(msg->seq); auto* body = new DebugAdapterProtocol::InitializeResponseBody(); response->body = body; Send(client, response); auto r = new DebugAdapterProtocol::InitializedEvent(); Send(client, r); } else if (msg->GetCommand() == "disconnect") { auto response = new DebugAdapterProtocol::DefinedResponse(msg->seq); Send(client, response); _connections.erase(client); } else if (msg->GetCommand() == "configurationDone") { _connections.insert(client); Send(client, new DebugAdapterProtocol::DefinedResponse(msg->seq)); } else if (msg->GetCommand() == "attach") { auto t = dynamic_cast(msg); _scriptPath = t->arguments.value()->scriptPath; Send(client, new DebugAdapterProtocol::DefinedResponse(msg->seq)); } else if (msg->GetCommand() == "threads") { auto response = new DebugAdapterProtocol::ThreadsResponse(msg->seq); auto* body = new DebugAdapterProtocol::ThreadsResponseBody(); response->body = body; body->threads = {DebugAdapterProtocol::Thread(0, "main")}; Send(client, response); } else if (msg->GetCommand() == "continue") { Continue(); Send(client, new DebugAdapterProtocol::DefinedResponse(msg->seq)); } else if (msg->GetCommand() == "stackTrace") { auto ctx = _pausedContexts[0]; auto response = new DebugAdapterProtocol::StackTraceResponse(msg->seq); auto stackTrace = std::vector(); stackTrace.reserve(ctx->GetCallstackSize()); for (asUINT i = 0; i < ctx->GetCallstackSize(); ++i) { auto func = ctx->GetFunction(i); const char* scriptSection = nullptr; int column = 0; int line; if (i == 0 && ctx->GetExceptionFunction() != nullptr) { line = ctx->GetExceptionLineNumber(&column, &scriptSection); } else { line = ctx->GetLineNumber(i, &column, &scriptSection); } stackTrace.emplace_back(i, func->GetName(), DebugAdapterProtocol::Source(GetResolvedScriptPath(scriptSection)), line, column); } auto body = new DebugAdapterProtocol::StackTraceResponseBody(stackTrace); response->body = body; Send(client, response); } else if (msg->GetCommand() == "scopes") { auto t = dynamic_cast(msg); auto ctx = _pausedContexts[0]; auto response = new DebugAdapterProtocol::ScopesResponse(msg->seq); auto scopes = std::vector(); auto frameId = t->arguments.value()->frameId; auto varCount = 0; for (int i = 0; i < ctx->GetVarCount(t->arguments.value()->frameId); ++i) { if (ctx->IsVarInScope(i, frameId)) { varCount++; } } _storedVariableReferences.emplace_back(std::make_unique(frameId, 0)); scopes.emplace_back("Locals", _storedVariableReferences.size(), "locals", varCount); auto body = new DebugAdapterProtocol::ScopesResponseBody(scopes); response->body = body; Send(client, response); } else if (msg->GetCommand() == "variables") { auto t = dynamic_cast(msg); auto ctx = _pausedContexts[0]; auto response = new DebugAdapterProtocol::VariablesResponse(msg->seq); auto variables = std::vector(); auto reference = t->arguments.value()->variablesReference - 1; if (reference >= _storedVariableReferences.size()) return; auto& variant = _storedVariableReferences[reference]; auto e = ctx->GetEngine(); // We currently can't use asIScriptContext::PushBack on suspended script contexts due to limitations in // angelscript. Hence we make a new contenxt auto c = e->CreateContext(); if (holds_alternative>(variant)) { if (ctx->GetExceptionString() != nullptr) { variables.push_back( DebugAdapterProtocol::Variable("exception", ctx->GetExceptionString(), "exception", {})); } auto frameId = std::get>(variant)->StackLevel; // auto scopeType = get(variant).Type; auto varCount = ctx->GetVarCount(frameId); for (int i = 0; i < varCount; ++i) { if (!ctx->IsVarInScope(i, frameId)) { continue; } auto name = ctx->GetVarName(i, frameId); auto type = ctx->GetVarTypeId(i, frameId); auto val = ctx->GetAddressOfVar(i, frameId); variables.push_back(ASVariableFormatter::GetAsDAPVariable(e, c, this, name, type, val)); } } else { auto& v = std::get>(variant); try { ASVariableFormatter::GetChildDAPVariables(variables, e, c, this, v->TypeID, v->Address); } catch (const std::exception& e) { std::cout << e.what() << std::endl; } } c->Release(); auto body = new DebugAdapterProtocol::VariablesResponseBody(variables); response->body = body; Send(client, response); } else if (msg->GetCommand() == "next") { Next(_pausedContexts.at(0)); } else if (msg->GetCommand() == "stepIn") { StepInto(_pausedContexts.at(0)); } else if (msg->GetCommand() == "stepOut") { StepOut(_pausedContexts.at(0)); } else { std::cout << "Unhandled message: " << msg->GetCommand() << std::endl; Send(client, new DebugAdapterProtocol::DefinedResponse(msg->seq)); } } void AngelscriptDebugger::Next(asIScriptContext* ctx) { _nextContext = ctx; _next = true; _nextDepth = ctx->GetCallstackSize(); _storedVariableReferences.clear(); while (!_pausedContexts.empty()) { auto* c = _pausedContexts[_pausedContexts.size() - 1]; _pausedContexts.pop_back(); std::thread([](asIScriptContext* c) { c->Execute(); }, c).detach(); } } void AngelscriptDebugger::StepInto(asIScriptContext* ctx) { _nextContext = ctx; _stepInto = true; _nextDepth = ctx->GetCallstackSize(); _storedVariableReferences.clear(); while (!_pausedContexts.empty()) { auto* c = _pausedContexts[_pausedContexts.size() - 1]; _pausedContexts.pop_back(); std::thread([](asIScriptContext* c) { c->Execute(); }, c).detach(); } } void AngelscriptDebugger::StepOut(asIScriptContext* ctx) { _nextContext = ctx; _stepOut = true; _nextDepth = ctx->GetCallstackSize(); _storedVariableReferences.clear(); while (!_pausedContexts.empty()) { auto* c = _pausedContexts[_pausedContexts.size() - 1]; _pausedContexts.pop_back(); std::thread([](asIScriptContext* c) { c->Execute(); }, c).detach(); } }