AngelscriptDebuggerServer/src/AngelscriptDebugger.cpp

407 lines
17 KiB
C++

#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 <typename... Args> 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_t>(size_s);
auto buf = std::make_unique<char[]>(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<string>()(section) ^ (hash<size_t>()(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<std::string, std::string> 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<char>(msg));
buffer.consume(to_read);
if (to_fetch > 0) {
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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<DebugAdapterProtocol::Request*>(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<DebugAdapterProtocol::SetBreakpointsRequest*>(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<size_t> 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<DebugAdapterProtocol::ResponseBody, empty>(msg->seq);
Send(client, response);
_connections.erase(client);
} else if (msg->GetCommand() == "configurationDone") {
_connections.insert(client);
Send(client, new DebugAdapterProtocol::DefinedResponse<DebugAdapterProtocol::ResponseBody, empty>(msg->seq));
} else if (msg->GetCommand() == "attach") {
auto t = dynamic_cast<DebugAdapterProtocol::AttachRequest*>(msg);
_scriptPath = t->arguments.value()->scriptPath;
Send(client, new DebugAdapterProtocol::DefinedResponse<DebugAdapterProtocol::ResponseBody, empty>(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<DebugAdapterProtocol::ResponseBody, empty>(msg->seq));
} else if (msg->GetCommand() == "stackTrace") {
auto ctx = _pausedContexts[0];
auto response = new DebugAdapterProtocol::StackTraceResponse(msg->seq);
auto stackTrace = std::vector<DebugAdapterProtocol::StackFrame>();
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<DebugAdapterProtocol::ScopesRequest*>(msg);
auto ctx = _pausedContexts[0];
auto response = new DebugAdapterProtocol::ScopesResponse(msg->seq);
auto scopes = std::vector<DebugAdapterProtocol::Scope>();
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<StackScope>(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<DebugAdapterProtocol::VariablesRequest*>(msg);
auto ctx = _pausedContexts[0];
auto response = new DebugAdapterProtocol::VariablesResponse(msg->seq);
auto variables = std::vector<DebugAdapterProtocol::Variable>();
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<std::unique_ptr<StackScope>>(variant)) {
if (ctx->GetExceptionString() != nullptr) {
variables.push_back(
DebugAdapterProtocol::Variable("exception", ctx->GetExceptionString(), "exception", {}));
}
auto frameId = std::get<std::unique_ptr<StackScope>>(variant)->StackLevel;
// auto scopeType = get<StackScope>(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<std::unique_ptr<PointerVariable>>(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<DebugAdapterProtocol::ResponseBody, empty>(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();
}
}