859 lines
35 KiB
C++
859 lines
35 KiB
C++
#include "devtools/DebugAdapter.hpp"
|
|
|
|
#include "common/ScriptCompiler.h"
|
|
#include "devtools/Snapshot.hpp"
|
|
|
|
#include <Poco/JSON/Array.h>
|
|
#include <Poco/JSON/Object.h>
|
|
#include <Poco/JSON/Parser.h>
|
|
#include <Poco/JSON/Stringifier.h>
|
|
#include <Poco/Exception.h>
|
|
#include <Poco/Net/NetException.h>
|
|
#include <Poco/Net/ServerSocket.h>
|
|
#include <Poco/Net/SocketAddress.h>
|
|
#include <Poco/Net/StreamSocket.h>
|
|
#include <Poco/Timespan.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <sstream>
|
|
#include <thread>
|
|
|
|
#if defined(_WIN32)
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
namespace eve::dev {
|
|
namespace {
|
|
|
|
std::string stringify(const Poco::Dynamic::Var& v) {
|
|
std::ostringstream oss;
|
|
Poco::JSON::Stringifier::stringify(v, oss);
|
|
return oss.str();
|
|
}
|
|
|
|
std::string jsonEscape(const std::string& s) {
|
|
std::string out;
|
|
out.reserve(s.size() + 8);
|
|
for (char c : s) {
|
|
switch (c) {
|
|
case '"':
|
|
out += "\\\"";
|
|
break;
|
|
case '\\':
|
|
out += "\\\\";
|
|
break;
|
|
case '\n':
|
|
out += "\\n";
|
|
break;
|
|
case '\r':
|
|
out += "\\r";
|
|
break;
|
|
case '\t':
|
|
out += "\\t";
|
|
break;
|
|
default:
|
|
out += c;
|
|
break;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
DebugAdapter& DebugAdapter::instance() {
|
|
static DebugAdapter inst;
|
|
return inst;
|
|
}
|
|
|
|
DebugAdapter::DebugAdapter() = default;
|
|
|
|
DebugAdapter::~DebugAdapter() { stop(); }
|
|
|
|
void DebugAdapter::setSourceRoot(std::string root) {
|
|
for (char& c : root) {
|
|
if (c == '\\') c = '/';
|
|
}
|
|
while (!root.empty() && (root.back() == '/' || root.back() == '\\')) root.pop_back();
|
|
sourceRoot_ = std::move(root);
|
|
discoverEngineScriptAliases();
|
|
}
|
|
|
|
void DebugAdapter::registerSource(std::string name, std::string content) {
|
|
name = Debugger::normalizeSource(std::move(name));
|
|
if (name.empty() || content.empty()) return;
|
|
std::lock_guard<std::recursive_mutex> lock(ioMu_);
|
|
sourceContents_[name] = std::move(content);
|
|
}
|
|
|
|
Poco::JSON::Object::Ptr DebugAdapter::makeSourceObject(const std::string& source) const {
|
|
Poco::JSON::Object::Ptr src = new Poco::JSON::Object();
|
|
const std::string norm = Debugger::normalizeSource(source);
|
|
const auto it = sourceContents_.find(norm);
|
|
if (it != sourceContents_.end()) {
|
|
// Virtual source: hand VS Code the reference + inline content so it can
|
|
// render scripts compiled from memory (compilestring / packaged archives).
|
|
const int ref = 900000 + static_cast<int>(std::hash<std::string>{}(norm) % 100000);
|
|
const_cast<DebugAdapter*>(this)->sourceRefContents_[ref] = it->second;
|
|
src->set("name", Debugger::sourceBasename(norm));
|
|
src->set("sourceReference", ref);
|
|
src->set("content", it->second);
|
|
return src;
|
|
}
|
|
const std::string resolved = resolveSourcePath(norm);
|
|
const auto slash = resolved.find_last_of('/');
|
|
src->set("name", slash == std::string::npos ? resolved : resolved.substr(slash + 1));
|
|
src->set("path", resolved);
|
|
return src;
|
|
}
|
|
|
|
void DebugAdapter::discoverEngineScriptAliases() {
|
|
namespace fs = std::filesystem;
|
|
std::error_code ec;
|
|
fs::path cur;
|
|
if (!sourceRoot_.empty())
|
|
cur = fs::path(sourceRoot_);
|
|
else
|
|
cur = fs::current_path(ec);
|
|
if (ec) return;
|
|
for (int i = 0; i < 8; ++i) {
|
|
const fs::path cand = cur / "src" / "scripts" / "load.nut";
|
|
if (fs::exists(cand, ec) && !ec) {
|
|
rememberSourcePath(cand.string());
|
|
// Embedded root compiles as "load.nut" — force the alias even if
|
|
// rememberSourcePath also keyed by basename.
|
|
sourceAliases_["load.nut"] = Debugger::normalizeSource(cand.string());
|
|
sourceAliases_["buffer"] = sourceAliases_["load.nut"];
|
|
return;
|
|
}
|
|
if (!cur.has_parent_path()) break;
|
|
const fs::path parent = cur.parent_path();
|
|
if (parent == cur) break;
|
|
cur = parent;
|
|
}
|
|
}
|
|
|
|
void DebugAdapter::rememberSourcePath(const std::string& path) {
|
|
const std::string norm = Debugger::normalizeSource(path);
|
|
if (norm.empty()) return;
|
|
sourceAliases_[norm] = norm;
|
|
const std::string base = Debugger::sourceBasename(norm);
|
|
if (!base.empty()) sourceAliases_[base] = norm;
|
|
if (!sourceRoot_.empty() && Debugger::sourcesMatch(norm, sourceRoot_ + "/" + base)) {
|
|
// Prefer keeping a stable relative key under the game root.
|
|
std::string rel = norm;
|
|
if (rel.rfind(sourceRoot_, 0) == 0) {
|
|
rel = rel.substr(sourceRoot_.size());
|
|
while (!rel.empty() && rel[0] == '/') rel.erase(rel.begin());
|
|
if (!rel.empty()) sourceAliases_[rel] = norm;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string DebugAdapter::resolveSourcePath(std::string source) const {
|
|
source = Debugger::normalizeSource(std::move(source));
|
|
if (source.empty()) return source;
|
|
|
|
// Prefer paths VS Code already told us about (setBreakpoints).
|
|
{
|
|
auto it = sourceAliases_.find(source);
|
|
if (it != sourceAliases_.end()) return it->second;
|
|
const std::string base = Debugger::sourceBasename(source);
|
|
if (!base.empty()) {
|
|
it = sourceAliases_.find(base);
|
|
if (it != sourceAliases_.end()) return it->second;
|
|
}
|
|
}
|
|
|
|
// Absolute already (POSIX or Windows drive).
|
|
if (source[0] == '/' || (source.size() >= 2 && source[1] == ':')) return source;
|
|
if (sourceRoot_.empty()) return source;
|
|
try {
|
|
const auto joined = std::filesystem::path(sourceRoot_) / source;
|
|
std::error_code ec;
|
|
auto canon = std::filesystem::weakly_canonical(joined, ec);
|
|
std::string out = ec ? joined.string() : canon.string();
|
|
for (char& c : out) {
|
|
if (c == '\\') c = '/';
|
|
}
|
|
return out;
|
|
} catch (...) {
|
|
return sourceRoot_ + "/" + source;
|
|
}
|
|
}
|
|
|
|
int DebugAdapter::varRefForFrame(int frameId) {
|
|
if (frameId <= 0) return varRefLocals_;
|
|
const int ref = varRefFrameBase_ + frameId;
|
|
varScopes_[ref] = VarScope{0, frameId, {}};
|
|
return ref;
|
|
}
|
|
|
|
int DebugAdapter::allocVarRef(int kind, int frame, std::vector<std::string> path) {
|
|
const int ref = nextVarRef_++;
|
|
varScopes_[ref] = VarScope{kind, frame, std::move(path)};
|
|
return ref;
|
|
}
|
|
|
|
void DebugAdapter::handleBreakpointEvent(int id, const std::string& source, int line,
|
|
bool verified) {
|
|
const auto original = script::ScriptCompiler::toOriginalPosition(
|
|
source, {static_cast<uint32_t>(std::max(line, 1)), 1});
|
|
Poco::JSON::Object::Ptr bp = new Poco::JSON::Object();
|
|
bp->set("id", id);
|
|
bp->set("verified", verified);
|
|
if (!source.empty() && line > 0) {
|
|
const std::string resolved = resolveSourcePath(source);
|
|
Poco::JSON::Object::Ptr src = new Poco::JSON::Object();
|
|
src->set("name", Debugger::sourceBasename(resolved));
|
|
src->set("path", resolved);
|
|
bp->set("source", src);
|
|
bp->set("line", static_cast<int>(original.line));
|
|
}
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("reason", "changed");
|
|
body->set("breakpoint", bp);
|
|
sendMessage(makeEvent("breakpoint", stringify(Poco::Dynamic::Var(body))));
|
|
}
|
|
|
|
int DebugAdapter::listen(uint16_t port) {
|
|
stop();
|
|
Debugger::instance().setBreakpointEventFn(
|
|
[this](int id, const std::string& source, int line, bool verified) {
|
|
handleBreakpointEvent(id, source, line, verified);
|
|
});
|
|
try {
|
|
Poco::Net::SocketAddress addr("127.0.0.1", port);
|
|
server_ = std::make_unique<Poco::Net::ServerSocket>(addr);
|
|
server_->setBlocking(false);
|
|
const int bound = static_cast<int>(server_->address().port());
|
|
port_.store(bound);
|
|
listening_.store(true);
|
|
// Default source root = process cwd (extension chdirs into the game folder).
|
|
try {
|
|
setSourceRoot(std::filesystem::current_path().string());
|
|
} catch (...) {
|
|
discoverEngineScriptAliases();
|
|
}
|
|
return bound;
|
|
} catch (...) {
|
|
server_.reset();
|
|
listening_.store(false);
|
|
port_.store(0);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void DebugAdapter::stop() {
|
|
std::lock_guard<std::recursive_mutex> lock(ioMu_);
|
|
Debugger::instance().setBreakpointEventFn(nullptr);
|
|
if (client_) {
|
|
try {
|
|
client_->close();
|
|
} catch (...) {
|
|
}
|
|
client_.reset();
|
|
}
|
|
if (server_) {
|
|
try {
|
|
server_->close();
|
|
} catch (...) {
|
|
}
|
|
server_.reset();
|
|
}
|
|
recvBuf_.clear();
|
|
listening_.store(false);
|
|
hasClient_.store(false);
|
|
port_.store(0);
|
|
configured_ = false;
|
|
stopOnEntry_ = false;
|
|
sourceAliases_.clear();
|
|
varScopes_.clear();
|
|
lastException_.clear();
|
|
}
|
|
|
|
void DebugAdapter::acceptNonBlocking() {
|
|
if (!server_ || client_) return;
|
|
try {
|
|
Poco::Net::SocketAddress clientAddr;
|
|
Poco::Net::StreamSocket ss = server_->acceptConnection(clientAddr);
|
|
// Keep the DAP client socket blocking so small response writes are complete.
|
|
ss.setBlocking(true);
|
|
ss.setReceiveTimeout(Poco::Timespan(0, 1000)); // 1ms poll-friendly
|
|
client_ = std::make_unique<Poco::Net::StreamSocket>(ss);
|
|
hasClient_.store(true);
|
|
recvBuf_.clear();
|
|
} catch (const Poco::TimeoutException&) {
|
|
} catch (const Poco::Net::NetException&) {
|
|
// WouldBlock / no pending connection.
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
bool DebugAdapter::sendMessage(const std::string& json) {
|
|
std::lock_guard<std::recursive_mutex> lock(ioMu_);
|
|
if (!client_) return false;
|
|
const std::string frame =
|
|
"Content-Length: " + std::to_string(json.size()) + "\r\n\r\n" + json;
|
|
try {
|
|
const int sent = client_->sendBytes(frame.data(), static_cast<int>(frame.size()));
|
|
return sent == static_cast<int>(frame.size());
|
|
} catch (...) {
|
|
client_.reset();
|
|
hasClient_.store(false);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::string DebugAdapter::makeResponse(int /*seq*/, int requestSeq, const std::string& command,
|
|
bool ok, const std::string& bodyJson,
|
|
const std::string& message) {
|
|
const int outSeq = seq_.fetch_add(1);
|
|
std::ostringstream oss;
|
|
oss << "{\"seq\":" << outSeq << ",\"type\":\"response\",\"request_seq\":" << requestSeq
|
|
<< ",\"success\":" << (ok ? "true" : "false") << ",\"command\":\"" << jsonEscape(command)
|
|
<< "\"";
|
|
if (!ok && !message.empty()) oss << ",\"message\":\"" << jsonEscape(message) << "\"";
|
|
if (!bodyJson.empty()) oss << ",\"body\":" << bodyJson;
|
|
oss << "}";
|
|
return oss.str();
|
|
}
|
|
|
|
std::string DebugAdapter::makeEvent(const std::string& event, const std::string& bodyJson) {
|
|
const int outSeq = seq_.fetch_add(1);
|
|
std::ostringstream oss;
|
|
oss << "{\"seq\":" << outSeq << ",\"type\":\"event\",\"event\":\"" << jsonEscape(event) << "\"";
|
|
if (!bodyJson.empty()) oss << ",\"body\":" << bodyJson;
|
|
oss << "}";
|
|
return oss.str();
|
|
}
|
|
|
|
std::string DebugAdapter::reasonString(PauseReason r) {
|
|
switch (r) {
|
|
case PauseReason::Breakpoint:
|
|
return "breakpoint";
|
|
case PauseReason::Step:
|
|
return "step";
|
|
case PauseReason::Exception:
|
|
return "exception";
|
|
case PauseReason::PauseKey:
|
|
case PauseReason::Snapshot:
|
|
default:
|
|
return "pause";
|
|
}
|
|
}
|
|
|
|
void DebugAdapter::notifyStopped(PauseReason reason, const SourceLoc& loc,
|
|
const std::string& description) {
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("reason", reasonString(reason));
|
|
body->set("threadId", 1);
|
|
body->set("allThreadsStopped", true);
|
|
if (reason == PauseReason::Exception) {
|
|
lastException_ = description.empty() ? lastException_ : description;
|
|
if (!lastException_.empty()) {
|
|
body->set("description", lastException_);
|
|
body->set("text", lastException_);
|
|
}
|
|
}
|
|
if (!loc.source.empty() && loc.line > 0) {
|
|
const auto original = script::ScriptCompiler::toOriginalPosition(
|
|
loc.source, {static_cast<uint32_t>(loc.line), 1});
|
|
// Hint the IDE; stackTrace still provides the authoritative source.
|
|
body->set("source", makeSourceObject(loc.source));
|
|
body->set("line", static_cast<int>(original.line));
|
|
body->set("column", static_cast<int>(original.column));
|
|
}
|
|
sendMessage(makeEvent("stopped", stringify(Poco::Dynamic::Var(body))));
|
|
}
|
|
|
|
void DebugAdapter::notifyContinued() {
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("threadId", 1);
|
|
body->set("allThreadsContinued", true);
|
|
sendMessage(makeEvent("continued", stringify(Poco::Dynamic::Var(body))));
|
|
}
|
|
|
|
void DebugAdapter::notifyTerminated() {
|
|
sendMessage(makeEvent("terminated", "{}"));
|
|
}
|
|
|
|
void DebugAdapter::readAndDispatch() {
|
|
if (!client_) return;
|
|
char buf[4096];
|
|
try {
|
|
const int n = client_->receiveBytes(buf, sizeof(buf));
|
|
if (n <= 0) {
|
|
if (n == 0) {
|
|
client_.reset();
|
|
hasClient_.store(false);
|
|
}
|
|
return;
|
|
}
|
|
recvBuf_.append(buf, static_cast<size_t>(n));
|
|
} catch (const Poco::TimeoutException&) {
|
|
return;
|
|
} catch (const Poco::Net::NetException&) {
|
|
// Non-blocking: nothing to read yet.
|
|
return;
|
|
} catch (...) {
|
|
client_.reset();
|
|
hasClient_.store(false);
|
|
return;
|
|
}
|
|
|
|
while (true) {
|
|
const auto headerEnd = recvBuf_.find("\r\n\r\n");
|
|
if (headerEnd == std::string::npos) break;
|
|
const std::string header = recvBuf_.substr(0, headerEnd);
|
|
int contentLength = -1;
|
|
{
|
|
const std::string key = "Content-Length:";
|
|
auto pos = header.find(key);
|
|
if (pos != std::string::npos) {
|
|
pos += key.size();
|
|
while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) ++pos;
|
|
contentLength = std::atoi(header.c_str() + pos);
|
|
}
|
|
}
|
|
if (contentLength < 0) {
|
|
recvBuf_.erase(0, headerEnd + 4);
|
|
continue;
|
|
}
|
|
const size_t total = headerEnd + 4 + static_cast<size_t>(contentLength);
|
|
if (recvBuf_.size() < total) break;
|
|
const std::string json = recvBuf_.substr(headerEnd + 4, static_cast<size_t>(contentLength));
|
|
recvBuf_.erase(0, total);
|
|
handleRequest(json);
|
|
}
|
|
}
|
|
|
|
void DebugAdapter::handleRequest(const std::string& json) {
|
|
try {
|
|
Poco::JSON::Parser parser;
|
|
auto root = parser.parse(json).extract<Poco::JSON::Object::Ptr>();
|
|
if (!root) return;
|
|
const std::string type = root->optValue<std::string>("type", "");
|
|
if (type != "request") return;
|
|
const std::string command = root->optValue<std::string>("command", "");
|
|
const int reqSeq = root->optValue<int>("seq", 0);
|
|
Poco::JSON::Object::Ptr args =
|
|
root->has("arguments") ? root->getObject("arguments") : nullptr;
|
|
|
|
auto& dbg = Debugger::instance();
|
|
|
|
if (command == "initialize") {
|
|
// Capabilities VS Code uses to enable Continue / Step Over / Pause UI + keys.
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("supportsConfigurationDoneRequest", true);
|
|
body->set("supportsEvaluateForHovers", true);
|
|
body->set("supportsTerminateRequest", true);
|
|
body->set("supportsSingleThreadExecutionRequests", true);
|
|
body->set("supportsSetVariable", false);
|
|
body->set("supportsStepInTargetsRequest", false);
|
|
body->set("supportsStepBack", false);
|
|
body->set("supportTerminateDebuggee", true);
|
|
body->set("supportsCancelRequest", false);
|
|
body->set("supportsExceptionInfoRequest", true);
|
|
body->set("supportsConditionalBreakpoints", true);
|
|
// Expose "Break on Error" as an exception filter so VS Code renders a
|
|
// checkbox in the BREAKPOINTS panel (Godot's Break on Error).
|
|
{
|
|
Poco::JSON::Object::Ptr filter = new Poco::JSON::Object();
|
|
filter->set("filter", "script_error");
|
|
filter->set("label", "Script Errors");
|
|
filter->set("default", false);
|
|
filter->set("supportsCondition", false);
|
|
filter->set(
|
|
"description",
|
|
"Break when a script error is raised (caught or uncaught)");
|
|
Poco::JSON::Array::Ptr filters = new Poco::JSON::Array();
|
|
filters->add(filter);
|
|
body->set("exceptionBreakpointFilters", filters);
|
|
}
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
sendMessage(makeEvent("initialized", "{}"));
|
|
return;
|
|
}
|
|
if (command == "launch" || command == "attach") {
|
|
if (args) {
|
|
// Prefer explicit cwd / program from the VS Code launch config.
|
|
std::string cwd = args->optValue<std::string>("cwd", "");
|
|
if (cwd.empty()) cwd = args->optValue<std::string>("program", "");
|
|
if (!cwd.empty()) setSourceRoot(std::move(cwd));
|
|
stopOnEntry_ = args->optValue<bool>("stopOnEntry", false);
|
|
}
|
|
sendMessage(makeResponse(0, reqSeq, command, true, "{}"));
|
|
// Announce the debuggee process so the CALL STACK / debug toolbar light up.
|
|
{
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("name", "eve");
|
|
#if defined(_WIN32)
|
|
body->set("systemProcessId", static_cast<int>(::GetCurrentProcessId()));
|
|
#else
|
|
body->set("systemProcessId", static_cast<int>(::getpid()));
|
|
#endif
|
|
body->set("isLocalProcess", true);
|
|
body->set("startMethod", command == "launch" ? "launch" : "attach");
|
|
sendMessage(makeEvent("process", stringify(Poco::Dynamic::Var(body))));
|
|
}
|
|
return;
|
|
}
|
|
if (command == "configurationDone") {
|
|
configured_ = true;
|
|
sendMessage(makeResponse(0, reqSeq, command, true, "{}"));
|
|
if (stopOnEntry_) {
|
|
// Break on the next script line so the IDE gets a real source location.
|
|
dbg.stepInto();
|
|
}
|
|
return;
|
|
}
|
|
if (command == "setBreakpoints") {
|
|
std::string sourcePath;
|
|
if (args && args->has("source")) {
|
|
auto src = args->getObject("source");
|
|
if (src) {
|
|
sourcePath = src->optValue<std::string>("path", "");
|
|
if (sourcePath.empty()) sourcePath = src->optValue<std::string>("name", "");
|
|
}
|
|
}
|
|
if (!sourcePath.empty()) rememberSourcePath(sourcePath);
|
|
dbg.clearBreakpoints(sourcePath);
|
|
Poco::JSON::Array::Ptr outBps = new Poco::JSON::Array();
|
|
if (args && args->has("breakpoints")) {
|
|
auto arr = args->getArray("breakpoints");
|
|
if (arr) {
|
|
for (size_t i = 0; i < arr->size(); ++i) {
|
|
auto bp = arr->getObject(static_cast<unsigned int>(i));
|
|
if (!bp) continue;
|
|
const int line = bp->optValue<int>("line", 0);
|
|
const std::string condition =
|
|
bp->optValue<std::string>("condition", "");
|
|
const auto generated = script::ScriptCompiler::toGeneratedPosition(
|
|
sourcePath, {static_cast<uint32_t>(std::max(line, 1)), 1});
|
|
const int id = dbg.setBreakpoint(sourcePath, static_cast<int>(generated.line), true, condition);
|
|
Poco::JSON::Object::Ptr ob = new Poco::JSON::Object();
|
|
ob->set("id", id);
|
|
// Verified lazily: the line hook marks the breakpoint real
|
|
// and we emit a `breakpoint` event once the line runs.
|
|
ob->set("verified", false);
|
|
ob->set("line", line);
|
|
if (id > 0) ob->set("message", "waiting for line to execute");
|
|
if (id > 0 && !sourcePath.empty()) {
|
|
Poco::JSON::Object::Ptr src = new Poco::JSON::Object();
|
|
const std::string resolved = resolveSourcePath(sourcePath);
|
|
src->set("name", Debugger::sourceBasename(resolved));
|
|
src->set("path", resolved);
|
|
ob->set("source", src);
|
|
}
|
|
outBps->add(ob);
|
|
}
|
|
}
|
|
}
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("breakpoints", outBps);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "threads") {
|
|
Poco::JSON::Object::Ptr th = new Poco::JSON::Object();
|
|
th->set("id", 1);
|
|
th->set("name", "main");
|
|
Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
|
|
arr->add(th);
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("threads", arr);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "stackTrace") {
|
|
auto frames = dbg.stackTrace(64);
|
|
// If paused between frames with no SQ stack, synthesize one from pauseLoc.
|
|
if (frames.empty() && !dbg.pauseLocation().empty()) {
|
|
StackFrameInfo f;
|
|
f.id = 1;
|
|
f.loc = dbg.pauseLocation();
|
|
f.name = f.loc.function.empty() ? "frame" : f.loc.function;
|
|
frames.push_back(f);
|
|
}
|
|
Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
|
|
for (const auto& f : frames) {
|
|
const auto original = script::ScriptCompiler::toOriginalPosition(
|
|
f.loc.source, {static_cast<uint32_t>(std::max(f.loc.line, 1)), 1});
|
|
Poco::JSON::Object::Ptr fo = new Poco::JSON::Object();
|
|
fo->set("id", f.id);
|
|
fo->set("name", f.name);
|
|
fo->set("line", static_cast<int>(original.line));
|
|
fo->set("column", static_cast<int>(original.column));
|
|
fo->set("source", makeSourceObject(f.loc.source));
|
|
arr->add(fo);
|
|
}
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("stackFrames", arr);
|
|
body->set("totalFrames", static_cast<int>(frames.size()));
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "scopes") {
|
|
const int frameId = args ? args->optValue<int>("frameId", 0) : 0;
|
|
const int localsRef = varRefForFrame(frameId);
|
|
Poco::JSON::Object::Ptr locals = new Poco::JSON::Object();
|
|
locals->set("name", "Locals");
|
|
locals->set("variablesReference", localsRef);
|
|
locals->set("expensive", false);
|
|
Poco::JSON::Object::Ptr watches = new Poco::JSON::Object();
|
|
watches->set("name", "Watches");
|
|
watches->set("variablesReference", varRefWatches_);
|
|
watches->set("expensive", false);
|
|
Poco::JSON::Object::Ptr globals = new Poco::JSON::Object();
|
|
globals->set("name", "Globals");
|
|
globals->set("variablesReference", varRefGlobals_);
|
|
globals->set("expensive", false);
|
|
Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
|
|
arr->add(locals);
|
|
arr->add(watches);
|
|
arr->add(globals);
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("scopes", arr);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "variables") {
|
|
const int ref = args ? args->optValue<int>("variablesReference", 0) : 0;
|
|
Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
|
|
const auto addChild = [&](const VariableInfo& v, const VarScope& base) {
|
|
Poco::JSON::Object::Ptr o = new Poco::JSON::Object();
|
|
o->set("name", v.name);
|
|
o->set("value", v.value);
|
|
o->set("type", v.type);
|
|
if (v.expandable) {
|
|
auto child = base;
|
|
child.path.push_back(v.name);
|
|
o->set("variablesReference", allocVarRef(child.kind, child.frame, child.path));
|
|
// VS Code VARIABLES context menu: only offer "Inspect
|
|
// Instance" on objects that can be expanded. Drives the
|
|
// `__vscodeVariableMenuContext == 'object'` when-clause in
|
|
// tools/vscode-eve-debug/package.json.
|
|
o->set("__vscodeVariableMenuContext", "object");
|
|
} else {
|
|
o->set("variablesReference", 0);
|
|
}
|
|
arr->add(o);
|
|
};
|
|
|
|
if (ref == varRefLocals_) {
|
|
VarScope base;
|
|
base.kind = 0;
|
|
for (const auto& v : dbg.locals(0)) addChild(v, base);
|
|
} else if (ref == varRefWatches_) {
|
|
dbg.refreshWatches();
|
|
for (const auto& w : dbg.watches()) {
|
|
Poco::JSON::Object::Ptr o = new Poco::JSON::Object();
|
|
o->set("name", w.expression);
|
|
o->set("value", w.value);
|
|
o->set("type", w.ok ? "watch" : "error");
|
|
o->set("variablesReference", 0);
|
|
arr->add(o);
|
|
}
|
|
} else if (ref == varRefGlobals_) {
|
|
VarScope base;
|
|
base.kind = 1;
|
|
for (const auto& v : dbg.globals()) addChild(v, base);
|
|
} else {
|
|
const auto it = varScopes_.find(ref);
|
|
if (it != varScopes_.end()) {
|
|
const auto children = dbg.containerChildren(
|
|
static_cast<VarKind>(it->second.kind), it->second.frame, it->second.path);
|
|
for (const auto& v : children) addChild(v, it->second);
|
|
}
|
|
}
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("variables", arr);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "continue") {
|
|
dbg.resume();
|
|
notifyContinued();
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("allThreadsContinued", true);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "next") {
|
|
// F10 — step over (skip call bodies).
|
|
dbg.stepOver();
|
|
notifyContinued();
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("threadId", 1);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "stepIn") {
|
|
// F11 — step into calls.
|
|
dbg.stepInto();
|
|
notifyContinued();
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("threadId", 1);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "stepOut") {
|
|
// Shift+F11 — run until return to caller.
|
|
dbg.stepOut();
|
|
notifyContinued();
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("threadId", 1);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "stepFrame") {
|
|
// Custom: advance one game frame then pause (secondary to statement step).
|
|
dbg.stepFrame();
|
|
notifyContinued();
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("threadId", 1);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "pause") {
|
|
// DAP pause = break at next script statement. Reply immediately with a
|
|
// stopped event so the IDE is not left hanging while the engine runs
|
|
// native code; the next script line emits a second stopped with a real
|
|
// source location once the line hook arms.
|
|
if (!dbg.isPaused()) {
|
|
dbg.stepInto();
|
|
Poco::JSON::Object::Ptr stoppedBody = new Poco::JSON::Object();
|
|
stoppedBody->set("reason", "pause");
|
|
stoppedBody->set("threadId", 1);
|
|
stoppedBody->set("allThreadsStopped", true);
|
|
sendMessage(makeEvent("stopped", stringify(Poco::Dynamic::Var(stoppedBody))));
|
|
}
|
|
sendMessage(makeResponse(0, reqSeq, command, true, "{}"));
|
|
return;
|
|
}
|
|
if (command == "evaluate") {
|
|
const std::string expr = args ? args->optValue<std::string>("expression", "") : "";
|
|
const int frameId = args ? args->optValue<int>("frameId", 0) : 0;
|
|
const std::string context = args ? args->optValue<std::string>("context", "") : "";
|
|
auto info = dbg.evaluate(expr, frameId);
|
|
// Only the Watch pane registers persistent expressions; hover / repl
|
|
// evaluations are transient.
|
|
if (context == "watch" && info.type != "error") dbg.addWatch(expr);
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("result", info.value);
|
|
body->set("type", info.type);
|
|
body->set("variablesReference", 0);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "source") {
|
|
const int ref = args ? args->optValue<int>("sourceReference", 0) : 0;
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
const auto it = sourceRefContents_.find(ref);
|
|
if (it != sourceRefContents_.end()) {
|
|
body->set("content", it->second);
|
|
body->set("mimeType", "text/plain");
|
|
} else {
|
|
body->set("content", "");
|
|
}
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "setExceptionBreakpoints") {
|
|
// Filters: "all" / "uncaught" / "script_error" enable breaking on
|
|
// script errors (Godot "Break on Error"); empty disables it.
|
|
bool on = false;
|
|
Poco::JSON::Array::Ptr applied = new Poco::JSON::Array();
|
|
if (args && args->has("filters")) {
|
|
auto filters = args->getArray("filters");
|
|
if (filters) {
|
|
for (size_t i = 0; i < filters->size(); ++i) {
|
|
const std::string f =
|
|
filters->get(static_cast<unsigned int>(i)).convert<std::string>();
|
|
if (f == "all" || f == "uncaught" || f == "script_error" ||
|
|
f == "runtime_error") {
|
|
on = true;
|
|
applied->add(f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
dbg.setBreakOnError(on);
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("filters", applied);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "exceptionInfo") {
|
|
Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
|
|
body->set("exceptionId", "script_error");
|
|
body->set("description", lastException_.empty() ? "script error" : lastException_);
|
|
body->set("breakMode", dbg.breakOnError() ? "always" : "never");
|
|
Poco::JSON::Object::Ptr details = new Poco::JSON::Object();
|
|
details->set("message", lastException_);
|
|
body->set("details", details);
|
|
sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
|
|
return;
|
|
}
|
|
if (command == "disconnect" || command == "terminate") {
|
|
dbg.resume();
|
|
sendMessage(makeResponse(0, reqSeq, command, true, "{}"));
|
|
notifyTerminated();
|
|
return;
|
|
}
|
|
// Custom: snapshot helpers via evaluate-like extensions are enough;
|
|
// acknowledge unknown with failure.
|
|
sendMessage(makeResponse(0, reqSeq, command, false, "{}", "unsupported: " + command));
|
|
} catch (const std::exception& e) {
|
|
// Never leave the IDE hanging on a BadCast / JSON error.
|
|
sendMessage(makeResponse(0, 0, "error", false, "{}", e.what()));
|
|
} catch (...) {
|
|
sendMessage(makeResponse(0, 0, "error", false, "{}", "unknown DAP handler error"));
|
|
}
|
|
}
|
|
|
|
void DebugAdapter::poll() {
|
|
if (!listening_.load()) return;
|
|
std::lock_guard<std::recursive_mutex> lock(ioMu_);
|
|
acceptNonBlocking();
|
|
readAndDispatch();
|
|
}
|
|
|
|
bool DebugAdapter::waitUntilConfigured(int timeoutMs) {
|
|
if (!listening_.load()) return false;
|
|
if (timeoutMs < 0) timeoutMs = 0;
|
|
using clock = std::chrono::steady_clock;
|
|
const auto deadline = clock::now() + std::chrono::milliseconds(timeoutMs);
|
|
// Fast path for `eve --dap-port` without an IDE: don't block the whole timeout.
|
|
const auto clientDeadline = clock::now() + std::chrono::milliseconds(
|
|
std::min(timeoutMs, 2500));
|
|
while (clock::now() < clientDeadline) {
|
|
poll();
|
|
if (configured_) return true;
|
|
if (hasClient_.load()) break;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
}
|
|
if (!hasClient_.load()) {
|
|
poll();
|
|
return configured_;
|
|
}
|
|
while (clock::now() < deadline) {
|
|
poll();
|
|
if (configured_) return true;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
}
|
|
poll();
|
|
return configured_;
|
|
}
|
|
|
|
} // namespace eve::dev
|