#include "devtools/DebugAdapter.hpp" #include "common/ScriptCompiler.h" #include "devtools/Snapshot.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #else #include #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 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(std::hash{}(norm) % 100000); const_cast(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 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(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(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(addr); server_->setBlocking(false); const int bound = static_cast(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 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(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 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(frame.size())); return sent == static_cast(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(loc.line), 1}); // Hint the IDE; stackTrace still provides the authoritative source. body->set("source", makeSourceObject(loc.source)); body->set("line", static_cast(original.line)); body->set("column", static_cast(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(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(contentLength); if (recvBuf_.size() < total) break; const std::string json = recvBuf_.substr(headerEnd + 4, static_cast(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(); if (!root) return; const std::string type = root->optValue("type", ""); if (type != "request") return; const std::string command = root->optValue("command", ""); const int reqSeq = root->optValue("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("cwd", ""); if (cwd.empty()) cwd = args->optValue("program", ""); if (!cwd.empty()) setSourceRoot(std::move(cwd)); stopOnEntry_ = args->optValue("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(::GetCurrentProcessId())); #else body->set("systemProcessId", static_cast(::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("path", ""); if (sourcePath.empty()) sourcePath = src->optValue("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(i)); if (!bp) continue; const int line = bp->optValue("line", 0); const std::string condition = bp->optValue("condition", ""); const auto generated = script::ScriptCompiler::toGeneratedPosition( sourcePath, {static_cast(std::max(line, 1)), 1}); const int id = dbg.setBreakpoint(sourcePath, static_cast(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(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(original.line)); fo->set("column", static_cast(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(frames.size())); sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body)))); return; } if (command == "scopes") { const int frameId = args ? args->optValue("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("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(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("expression", "") : ""; const int frameId = args ? args->optValue("frameId", 0) : 0; const std::string context = args ? args->optValue("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("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(i)).convert(); 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 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