载入中...
搜索中...
未找到
DebugAdapter.cpp
浏览该文件的文档.
2
4
5#include <Poco/JSON/Array.h>
6#include <Poco/JSON/Object.h>
7#include <Poco/JSON/Parser.h>
8#include <Poco/JSON/Stringifier.h>
9#include <Poco/Exception.h>
10#include <Poco/Net/NetException.h>
11#include <Poco/Net/ServerSocket.h>
12#include <Poco/Net/SocketAddress.h>
13#include <Poco/Net/StreamSocket.h>
14#include <Poco/Timespan.h>
15
16#include <algorithm>
17#include <chrono>
18#include <cstdlib>
19#include <filesystem>
20#include <sstream>
21#include <thread>
22
23#if defined(_WIN32)
24#ifndef WIN32_LEAN_AND_MEAN
25#define WIN32_LEAN_AND_MEAN
26#endif
27#include <windows.h>
28#else
29#include <unistd.h>
30#endif
31
32namespace eve::dev {
33namespace {
34
35std::string stringify(const Poco::Dynamic::Var& v) {
36 std::ostringstream oss;
37 Poco::JSON::Stringifier::stringify(v, oss);
38 return oss.str();
39}
40
41std::string jsonEscape(const std::string& s) {
42 std::string out;
43 out.reserve(s.size() + 8);
44 for (char c : s) {
45 switch (c) {
46 case '"':
47 out += "\\\"";
48 break;
49 case '\\':
50 out += "\\\\";
51 break;
52 case '\n':
53 out += "\\n";
54 break;
55 case '\r':
56 out += "\\r";
57 break;
58 case '\t':
59 out += "\\t";
60 break;
61 default:
62 out += c;
63 break;
64 }
65 }
66 return out;
67}
68
69} // namespace
70
72 static DebugAdapter inst;
73 return inst;
74}
75
76DebugAdapter::DebugAdapter() = default;
77
78DebugAdapter::~DebugAdapter() { stop(); }
79
80void DebugAdapter::setSourceRoot(std::string root) {
81 for (char& c : root) {
82 if (c == '\\') c = '/';
83 }
84 while (!root.empty() && (root.back() == '/' || root.back() == '\\')) root.pop_back();
85 sourceRoot_ = std::move(root);
86 discoverEngineScriptAliases();
87}
88
89void DebugAdapter::registerSource(std::string name, std::string content) {
91 if (name.empty() || content.empty()) return;
92 std::lock_guard<std::recursive_mutex> lock(ioMu_);
93 sourceContents_[name] = std::move(content);
94}
95
96Poco::JSON::Object::Ptr DebugAdapter::makeSourceObject(const std::string& source) const {
97 Poco::JSON::Object::Ptr src = new Poco::JSON::Object();
98 const std::string norm = Debugger::normalizeSource(source);
99 const auto it = sourceContents_.find(norm);
100 if (it != sourceContents_.end()) {
101 // Virtual source: hand VS Code the reference + inline content so it can
102 // render scripts compiled from memory (compilestring / packaged archives).
103 const int ref = 900000 + static_cast<int>(std::hash<std::string>{}(norm) % 100000);
104 const_cast<DebugAdapter*>(this)->sourceRefContents_[ref] = it->second;
105 src->set("name", Debugger::sourceBasename(norm));
106 src->set("sourceReference", ref);
107 src->set("content", it->second);
108 return src;
109 }
110 const std::string resolved = resolveSourcePath(norm);
111 const auto slash = resolved.find_last_of('/');
112 src->set("name", slash == std::string::npos ? resolved : resolved.substr(slash + 1));
113 src->set("path", resolved);
114 return src;
115}
116
117void DebugAdapter::discoverEngineScriptAliases() {
118 namespace fs = std::filesystem;
119 std::error_code ec;
120 fs::path cur;
121 if (!sourceRoot_.empty())
122 cur = fs::path(sourceRoot_);
123 else
124 cur = fs::current_path(ec);
125 if (ec) return;
126 for (int i = 0; i < 8; ++i) {
127 const fs::path cand = cur / "src" / "scripts" / "load.nut";
128 if (fs::exists(cand, ec) && !ec) {
129 rememberSourcePath(cand.string());
130 // Embedded root compiles as "load.nut" — force the alias even if
131 // rememberSourcePath also keyed by basename.
132 sourceAliases_["load.nut"] = Debugger::normalizeSource(cand.string());
133 sourceAliases_["buffer"] = sourceAliases_["load.nut"];
134 return;
135 }
136 if (!cur.has_parent_path()) break;
137 const fs::path parent = cur.parent_path();
138 if (parent == cur) break;
139 cur = parent;
140 }
141}
142
143void DebugAdapter::rememberSourcePath(const std::string& path) {
144 const std::string norm = Debugger::normalizeSource(path);
145 if (norm.empty()) return;
146 sourceAliases_[norm] = norm;
147 const std::string base = Debugger::sourceBasename(norm);
148 if (!base.empty()) sourceAliases_[base] = norm;
149 if (!sourceRoot_.empty() && Debugger::sourcesMatch(norm, sourceRoot_ + "/" + base)) {
150 // Prefer keeping a stable relative key under the game root.
151 std::string rel = norm;
152 if (rel.rfind(sourceRoot_, 0) == 0) {
153 rel = rel.substr(sourceRoot_.size());
154 while (!rel.empty() && rel[0] == '/') rel.erase(rel.begin());
155 if (!rel.empty()) sourceAliases_[rel] = norm;
156 }
157 }
158}
159
160std::string DebugAdapter::resolveSourcePath(std::string source) const {
161 source = Debugger::normalizeSource(std::move(source));
162 if (source.empty()) return source;
163
164 // Prefer paths VS Code already told us about (setBreakpoints).
165 {
166 auto it = sourceAliases_.find(source);
167 if (it != sourceAliases_.end()) return it->second;
168 const std::string base = Debugger::sourceBasename(source);
169 if (!base.empty()) {
170 it = sourceAliases_.find(base);
171 if (it != sourceAliases_.end()) return it->second;
172 }
173 }
174
175 // Absolute already (POSIX or Windows drive).
176 if (source[0] == '/' || (source.size() >= 2 && source[1] == ':')) return source;
177 if (sourceRoot_.empty()) return source;
178 try {
179 const auto joined = std::filesystem::path(sourceRoot_) / source;
180 std::error_code ec;
181 auto canon = std::filesystem::weakly_canonical(joined, ec);
182 std::string out = ec ? joined.string() : canon.string();
183 for (char& c : out) {
184 if (c == '\\') c = '/';
185 }
186 return out;
187 } catch (...) {
188 return sourceRoot_ + "/" + source;
189 }
190}
191
192int DebugAdapter::varRefForFrame(int frameId) {
193 if (frameId <= 0) return varRefLocals_;
194 const int ref = varRefFrameBase_ + frameId;
195 varScopes_[ref] = VarScope{0, frameId, {}};
196 return ref;
197}
198
199int DebugAdapter::allocVarRef(int kind, int frame, std::vector<std::string> path) {
200 const int ref = nextVarRef_++;
201 varScopes_[ref] = VarScope{kind, frame, std::move(path)};
202 return ref;
203}
204
205void DebugAdapter::handleBreakpointEvent(int id, const std::string& source, int line,
206 bool verified) {
207 Poco::JSON::Object::Ptr bp = new Poco::JSON::Object();
208 bp->set("id", id);
209 bp->set("verified", verified);
210 if (!source.empty() && line > 0) {
211 const std::string resolved = resolveSourcePath(source);
212 Poco::JSON::Object::Ptr src = new Poco::JSON::Object();
213 src->set("name", Debugger::sourceBasename(resolved));
214 src->set("path", resolved);
215 bp->set("source", src);
216 bp->set("line", line);
217 }
218 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
219 body->set("reason", "changed");
220 body->set("breakpoint", bp);
221 sendMessage(makeEvent("breakpoint", stringify(Poco::Dynamic::Var(body))));
222}
223
224int DebugAdapter::listen(uint16_t port) {
225 stop();
227 [this](int id, const std::string& source, int line, bool verified) {
228 handleBreakpointEvent(id, source, line, verified);
229 });
230 try {
231 Poco::Net::SocketAddress addr("127.0.0.1", port);
232 server_ = std::make_unique<Poco::Net::ServerSocket>(addr);
233 server_->setBlocking(false);
234 const int bound = static_cast<int>(server_->address().port());
235 port_.store(bound);
236 listening_.store(true);
237 // Default source root = process cwd (extension chdirs into the game folder).
238 try {
239 setSourceRoot(std::filesystem::current_path().string());
240 } catch (...) {
241 discoverEngineScriptAliases();
242 }
243 return bound;
244 } catch (...) {
245 server_.reset();
246 listening_.store(false);
247 port_.store(0);
248 return 0;
249 }
250}
251
253 std::lock_guard<std::recursive_mutex> lock(ioMu_);
255 if (client_) {
256 try {
257 client_->close();
258 } catch (...) {
259 }
260 client_.reset();
261 }
262 if (server_) {
263 try {
264 server_->close();
265 } catch (...) {
266 }
267 server_.reset();
268 }
269 recvBuf_.clear();
270 listening_.store(false);
271 hasClient_.store(false);
272 port_.store(0);
273 configured_ = false;
274 stopOnEntry_ = false;
275 sourceAliases_.clear();
276 varScopes_.clear();
277 lastException_.clear();
278}
279
280void DebugAdapter::acceptNonBlocking() {
281 if (!server_ || client_) return;
282 try {
283 Poco::Net::SocketAddress clientAddr;
284 Poco::Net::StreamSocket ss = server_->acceptConnection(clientAddr);
285 // Keep the DAP client socket blocking so small response writes are complete.
286 ss.setBlocking(true);
287 ss.setReceiveTimeout(Poco::Timespan(0, 1000)); // 1ms poll-friendly
288 client_ = std::make_unique<Poco::Net::StreamSocket>(ss);
289 hasClient_.store(true);
290 recvBuf_.clear();
291 } catch (const Poco::TimeoutException&) {
292 } catch (const Poco::Net::NetException&) {
293 // WouldBlock / no pending connection.
294 } catch (...) {
295 }
296}
297
298bool DebugAdapter::sendMessage(const std::string& json) {
299 std::lock_guard<std::recursive_mutex> lock(ioMu_);
300 if (!client_) return false;
301 const std::string frame =
302 "Content-Length: " + std::to_string(json.size()) + "\r\n\r\n" + json;
303 try {
304 const int sent = client_->sendBytes(frame.data(), static_cast<int>(frame.size()));
305 return sent == static_cast<int>(frame.size());
306 } catch (...) {
307 client_.reset();
308 hasClient_.store(false);
309 return false;
310 }
311}
312
313std::string DebugAdapter::makeResponse(int /*seq*/, int requestSeq, const std::string& command,
314 bool ok, const std::string& bodyJson,
315 const std::string& message) {
316 const int outSeq = seq_.fetch_add(1);
317 std::ostringstream oss;
318 oss << "{\"seq\":" << outSeq << ",\"type\":\"response\",\"request_seq\":" << requestSeq
319 << ",\"success\":" << (ok ? "true" : "false") << ",\"command\":\"" << jsonEscape(command)
320 << "\"";
321 if (!ok && !message.empty()) oss << ",\"message\":\"" << jsonEscape(message) << "\"";
322 if (!bodyJson.empty()) oss << ",\"body\":" << bodyJson;
323 oss << "}";
324 return oss.str();
325}
326
327std::string DebugAdapter::makeEvent(const std::string& event, const std::string& bodyJson) {
328 const int outSeq = seq_.fetch_add(1);
329 std::ostringstream oss;
330 oss << "{\"seq\":" << outSeq << ",\"type\":\"event\",\"event\":\"" << jsonEscape(event) << "\"";
331 if (!bodyJson.empty()) oss << ",\"body\":" << bodyJson;
332 oss << "}";
333 return oss.str();
334}
335
336std::string DebugAdapter::reasonString(PauseReason r) {
337 switch (r) {
339 return "breakpoint";
341 return "step";
343 return "exception";
346 default:
347 return "pause";
348 }
349}
350
352 const std::string& description) {
353 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
354 body->set("reason", reasonString(reason));
355 body->set("threadId", 1);
356 body->set("allThreadsStopped", true);
357 if (reason == PauseReason::Exception) {
358 lastException_ = description.empty() ? lastException_ : description;
359 if (!lastException_.empty()) {
360 body->set("description", lastException_);
361 body->set("text", lastException_);
362 }
363 }
364 if (!loc.source.empty() && loc.line > 0) {
365 // Hint the IDE; stackTrace still provides the authoritative source.
366 body->set("source", makeSourceObject(loc.source));
367 body->set("line", loc.line);
368 body->set("column", 1);
369 }
370 sendMessage(makeEvent("stopped", stringify(Poco::Dynamic::Var(body))));
371}
372
374 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
375 body->set("threadId", 1);
376 body->set("allThreadsContinued", true);
377 sendMessage(makeEvent("continued", stringify(Poco::Dynamic::Var(body))));
378}
379
381 sendMessage(makeEvent("terminated", "{}"));
382}
383
384void DebugAdapter::readAndDispatch() {
385 if (!client_) return;
386 char buf[4096];
387 try {
388 const int n = client_->receiveBytes(buf, sizeof(buf));
389 if (n <= 0) {
390 if (n == 0) {
391 client_.reset();
392 hasClient_.store(false);
393 }
394 return;
395 }
396 recvBuf_.append(buf, static_cast<size_t>(n));
397 } catch (const Poco::TimeoutException&) {
398 return;
399 } catch (const Poco::Net::NetException&) {
400 // Non-blocking: nothing to read yet.
401 return;
402 } catch (...) {
403 client_.reset();
404 hasClient_.store(false);
405 return;
406 }
407
408 while (true) {
409 const auto headerEnd = recvBuf_.find("\r\n\r\n");
410 if (headerEnd == std::string::npos) break;
411 const std::string header = recvBuf_.substr(0, headerEnd);
412 int contentLength = -1;
413 {
414 const std::string key = "Content-Length:";
415 auto pos = header.find(key);
416 if (pos != std::string::npos) {
417 pos += key.size();
418 while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) ++pos;
419 contentLength = std::atoi(header.c_str() + pos);
420 }
421 }
422 if (contentLength < 0) {
423 recvBuf_.erase(0, headerEnd + 4);
424 continue;
425 }
426 const size_t total = headerEnd + 4 + static_cast<size_t>(contentLength);
427 if (recvBuf_.size() < total) break;
428 const std::string json = recvBuf_.substr(headerEnd + 4, static_cast<size_t>(contentLength));
429 recvBuf_.erase(0, total);
430 handleRequest(json);
431 }
432}
433
434void DebugAdapter::handleRequest(const std::string& json) {
435 try {
436 Poco::JSON::Parser parser;
437 auto root = parser.parse(json).extract<Poco::JSON::Object::Ptr>();
438 if (!root) return;
439 const std::string type = root->optValue<std::string>("type", "");
440 if (type != "request") return;
441 const std::string command = root->optValue<std::string>("command", "");
442 const int reqSeq = root->optValue<int>("seq", 0);
443 Poco::JSON::Object::Ptr args =
444 root->has("arguments") ? root->getObject("arguments") : nullptr;
445
446 auto& dbg = Debugger::instance();
447
448 if (command == "initialize") {
449 // Capabilities VS Code uses to enable Continue / Step Over / Pause UI + keys.
450 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
451 body->set("supportsConfigurationDoneRequest", true);
452 body->set("supportsEvaluateForHovers", true);
453 body->set("supportsTerminateRequest", true);
454 body->set("supportsSingleThreadExecutionRequests", true);
455 body->set("supportsSetVariable", false);
456 body->set("supportsStepInTargetsRequest", false);
457 body->set("supportsStepBack", false);
458 body->set("supportTerminateDebuggee", true);
459 body->set("supportsCancelRequest", false);
460 body->set("supportsExceptionInfoRequest", true);
461 body->set("supportsConditionalBreakpoints", true);
462 // Expose "Break on Error" as an exception filter so VS Code renders a
463 // checkbox in the BREAKPOINTS panel (Godot's Break on Error).
464 {
465 Poco::JSON::Object::Ptr filter = new Poco::JSON::Object();
466 filter->set("filter", "script_error");
467 filter->set("label", "Script Errors");
468 filter->set("default", false);
469 filter->set("supportsCondition", false);
470 filter->set(
471 "description",
472 "Break when a script error is raised (caught or uncaught)");
473 Poco::JSON::Array::Ptr filters = new Poco::JSON::Array();
474 filters->add(filter);
475 body->set("exceptionBreakpointFilters", filters);
476 }
477 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
478 sendMessage(makeEvent("initialized", "{}"));
479 return;
480 }
481 if (command == "launch" || command == "attach") {
482 if (args) {
483 // Prefer explicit cwd / program from the VS Code launch config.
484 std::string root = args->optValue<std::string>("cwd", "");
485 if (root.empty()) root = args->optValue<std::string>("program", "");
486 if (!root.empty()) setSourceRoot(std::move(root));
487 stopOnEntry_ = args->optValue<bool>("stopOnEntry", false);
488 }
489 sendMessage(makeResponse(0, reqSeq, command, true, "{}"));
490 // Announce the debuggee process so the CALL STACK / debug toolbar light up.
491 {
492 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
493 body->set("name", "eve");
494#if defined(_WIN32)
495 body->set("systemProcessId", static_cast<int>(::GetCurrentProcessId()));
496#else
497 body->set("systemProcessId", static_cast<int>(::getpid()));
498#endif
499 body->set("isLocalProcess", true);
500 body->set("startMethod", command == "launch" ? "launch" : "attach");
501 sendMessage(makeEvent("process", stringify(Poco::Dynamic::Var(body))));
502 }
503 return;
504 }
505 if (command == "configurationDone") {
506 configured_ = true;
507 sendMessage(makeResponse(0, reqSeq, command, true, "{}"));
508 if (stopOnEntry_) {
509 // Break on the next script line so the IDE gets a real source location.
510 dbg.stepInto();
511 }
512 return;
513 }
514 if (command == "setBreakpoints") {
515 std::string sourcePath;
516 if (args && args->has("source")) {
517 auto src = args->getObject("source");
518 if (src) {
519 sourcePath = src->optValue<std::string>("path", "");
520 if (sourcePath.empty()) sourcePath = src->optValue<std::string>("name", "");
521 }
522 }
523 if (!sourcePath.empty()) rememberSourcePath(sourcePath);
524 dbg.clearBreakpoints(sourcePath);
525 Poco::JSON::Array::Ptr outBps = new Poco::JSON::Array();
526 if (args && args->has("breakpoints")) {
527 auto arr = args->getArray("breakpoints");
528 if (arr) {
529 for (size_t i = 0; i < arr->size(); ++i) {
530 auto bp = arr->getObject(i);
531 if (!bp) continue;
532 const int line = bp->optValue<int>("line", 0);
533 const std::string condition =
534 bp->optValue<std::string>("condition", "");
535 const int id = dbg.setBreakpoint(sourcePath, line, true, condition);
536 Poco::JSON::Object::Ptr ob = new Poco::JSON::Object();
537 ob->set("id", id);
538 // Verified lazily: the line hook marks the breakpoint real
539 // and we emit a `breakpoint` event once the line runs.
540 ob->set("verified", false);
541 ob->set("line", line);
542 if (id > 0) ob->set("message", "waiting for line to execute");
543 if (id > 0 && !sourcePath.empty()) {
544 Poco::JSON::Object::Ptr src = new Poco::JSON::Object();
545 const std::string resolved = resolveSourcePath(sourcePath);
546 src->set("name", Debugger::sourceBasename(resolved));
547 src->set("path", resolved);
548 ob->set("source", src);
549 }
550 outBps->add(ob);
551 }
552 }
553 }
554 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
555 body->set("breakpoints", outBps);
556 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
557 return;
558 }
559 if (command == "threads") {
560 Poco::JSON::Object::Ptr th = new Poco::JSON::Object();
561 th->set("id", 1);
562 th->set("name", "main");
563 Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
564 arr->add(th);
565 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
566 body->set("threads", arr);
567 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
568 return;
569 }
570 if (command == "stackTrace") {
571 auto frames = dbg.stackTrace(64);
572 // If paused between frames with no SQ stack, synthesize one from pauseLoc.
573 if (frames.empty() && !dbg.pauseLocation().empty()) {
574 StackFrameInfo f;
575 f.id = 1;
576 f.loc = dbg.pauseLocation();
577 f.name = f.loc.function.empty() ? "frame" : f.loc.function;
578 frames.push_back(f);
579 }
580 Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
581 for (const auto& f : frames) {
582 Poco::JSON::Object::Ptr fo = new Poco::JSON::Object();
583 fo->set("id", f.id);
584 fo->set("name", f.name);
585 fo->set("line", f.loc.line);
586 fo->set("column", 1);
587 fo->set("source", makeSourceObject(f.loc.source));
588 arr->add(fo);
589 }
590 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
591 body->set("stackFrames", arr);
592 body->set("totalFrames", static_cast<int>(frames.size()));
593 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
594 return;
595 }
596 if (command == "scopes") {
597 const int frameId = args ? args->optValue<int>("frameId", 0) : 0;
598 const int localsRef = varRefForFrame(frameId);
599 Poco::JSON::Object::Ptr locals = new Poco::JSON::Object();
600 locals->set("name", "Locals");
601 locals->set("variablesReference", localsRef);
602 locals->set("expensive", false);
603 Poco::JSON::Object::Ptr watches = new Poco::JSON::Object();
604 watches->set("name", "Watches");
605 watches->set("variablesReference", varRefWatches_);
606 watches->set("expensive", false);
607 Poco::JSON::Object::Ptr globals = new Poco::JSON::Object();
608 globals->set("name", "Globals");
609 globals->set("variablesReference", varRefGlobals_);
610 globals->set("expensive", false);
611 Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
612 arr->add(locals);
613 arr->add(watches);
614 arr->add(globals);
615 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
616 body->set("scopes", arr);
617 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
618 return;
619 }
620 if (command == "variables") {
621 const int ref = args ? args->optValue<int>("variablesReference", 0) : 0;
622 Poco::JSON::Array::Ptr arr = new Poco::JSON::Array();
623 const auto addChild = [&](const VariableInfo& v, const VarScope& base) {
624 Poco::JSON::Object::Ptr o = new Poco::JSON::Object();
625 o->set("name", v.name);
626 o->set("value", v.value);
627 o->set("type", v.type);
628 if (v.expandable) {
629 auto child = base;
630 child.path.push_back(v.name);
631 o->set("variablesReference", allocVarRef(child.kind, child.frame, child.path));
632 // VS Code VARIABLES context menu: only offer "Inspect
633 // Instance" on objects that can be expanded. Drives the
634 // `__vscodeVariableMenuContext == 'object'` when-clause in
635 // tools/vscode-eve-debug/package.json.
636 o->set("__vscodeVariableMenuContext", "object");
637 } else {
638 o->set("variablesReference", 0);
639 }
640 arr->add(o);
641 };
642
643 if (ref == varRefLocals_) {
644 VarScope base;
645 base.kind = 0;
646 for (const auto& v : dbg.locals(0)) addChild(v, base);
647 } else if (ref == varRefWatches_) {
648 dbg.refreshWatches();
649 for (const auto& w : dbg.watches()) {
650 Poco::JSON::Object::Ptr o = new Poco::JSON::Object();
651 o->set("name", w.expression);
652 o->set("value", w.value);
653 o->set("type", w.ok ? "watch" : "error");
654 o->set("variablesReference", 0);
655 arr->add(o);
656 }
657 } else if (ref == varRefGlobals_) {
658 VarScope base;
659 base.kind = 1;
660 for (const auto& v : dbg.globals()) addChild(v, base);
661 } else {
662 const auto it = varScopes_.find(ref);
663 if (it != varScopes_.end()) {
664 const auto children = dbg.containerChildren(
665 static_cast<VarKind>(it->second.kind), it->second.frame, it->second.path);
666 for (const auto& v : children) addChild(v, it->second);
667 }
668 }
669 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
670 body->set("variables", arr);
671 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
672 return;
673 }
674 if (command == "continue") {
675 dbg.resume();
677 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
678 body->set("allThreadsContinued", true);
679 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
680 return;
681 }
682 if (command == "next") {
683 // F10 — step over (skip call bodies).
684 dbg.stepOver();
686 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
687 body->set("threadId", 1);
688 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
689 return;
690 }
691 if (command == "stepIn") {
692 // F11 — step into calls.
693 dbg.stepInto();
695 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
696 body->set("threadId", 1);
697 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
698 return;
699 }
700 if (command == "stepOut") {
701 // Shift+F11 — run until return to caller.
702 dbg.stepOut();
704 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
705 body->set("threadId", 1);
706 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
707 return;
708 }
709 if (command == "stepFrame") {
710 // Custom: advance one game frame then pause (secondary to statement step).
711 dbg.stepFrame();
713 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
714 body->set("threadId", 1);
715 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
716 return;
717 }
718 if (command == "pause") {
719 // DAP pause = break at next script statement. Reply immediately with a
720 // stopped event so the IDE is not left hanging while the engine runs
721 // native code; the next script line emits a second stopped with a real
722 // source location once the line hook arms.
723 if (!dbg.isPaused()) {
724 dbg.stepInto();
725 Poco::JSON::Object::Ptr stoppedBody = new Poco::JSON::Object();
726 stoppedBody->set("reason", "pause");
727 stoppedBody->set("threadId", 1);
728 stoppedBody->set("allThreadsStopped", true);
729 sendMessage(makeEvent("stopped", stringify(Poco::Dynamic::Var(stoppedBody))));
730 }
731 sendMessage(makeResponse(0, reqSeq, command, true, "{}"));
732 return;
733 }
734 if (command == "evaluate") {
735 const std::string expr = args ? args->optValue<std::string>("expression", "") : "";
736 const int frameId = args ? args->optValue<int>("frameId", 0) : 0;
737 const std::string context = args ? args->optValue<std::string>("context", "") : "";
738 auto info = dbg.evaluate(expr, frameId);
739 // Only the Watch pane registers persistent expressions; hover / repl
740 // evaluations are transient.
741 if (context == "watch" && info.type != "error") dbg.addWatch(expr);
742 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
743 body->set("result", info.value);
744 body->set("type", info.type);
745 body->set("variablesReference", 0);
746 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
747 return;
748 }
749 if (command == "source") {
750 const int ref = args ? args->optValue<int>("sourceReference", 0) : 0;
751 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
752 const auto it = sourceRefContents_.find(ref);
753 if (it != sourceRefContents_.end()) {
754 body->set("content", it->second);
755 body->set("mimeType", "text/plain");
756 } else {
757 body->set("content", "");
758 }
759 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
760 return;
761 }
762 if (command == "setExceptionBreakpoints") {
763 // Filters: "all" / "uncaught" / "script_error" enable breaking on
764 // script errors (Godot "Break on Error"); empty disables it.
765 bool on = false;
766 Poco::JSON::Array::Ptr applied = new Poco::JSON::Array();
767 if (args && args->has("filters")) {
768 auto filters = args->getArray("filters");
769 if (filters) {
770 for (size_t i = 0; i < filters->size(); ++i) {
771 const std::string f = filters->get(i).convert<std::string>();
772 if (f == "all" || f == "uncaught" || f == "script_error" ||
773 f == "runtime_error") {
774 on = true;
775 applied->add(f);
776 }
777 }
778 }
779 }
780 dbg.setBreakOnError(on);
781 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
782 body->set("filters", applied);
783 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
784 return;
785 }
786 if (command == "exceptionInfo") {
787 Poco::JSON::Object::Ptr body = new Poco::JSON::Object();
788 body->set("exceptionId", "script_error");
789 body->set("description", lastException_.empty() ? "script error" : lastException_);
790 body->set("breakMode", dbg.breakOnError() ? "always" : "never");
791 Poco::JSON::Object::Ptr details = new Poco::JSON::Object();
792 details->set("message", lastException_);
793 body->set("details", details);
794 sendMessage(makeResponse(0, reqSeq, command, true, stringify(Poco::Dynamic::Var(body))));
795 return;
796 }
797 if (command == "disconnect" || command == "terminate") {
798 dbg.resume();
799 sendMessage(makeResponse(0, reqSeq, command, true, "{}"));
801 return;
802 }
803 // Custom: snapshot helpers via evaluate-like extensions are enough;
804 // acknowledge unknown with failure.
805 sendMessage(makeResponse(0, reqSeq, command, false, "{}", "unsupported: " + command));
806 } catch (const std::exception& e) {
807 // Never leave the IDE hanging on a BadCast / JSON error.
808 sendMessage(makeResponse(0, 0, "error", false, "{}", e.what()));
809 } catch (...) {
810 sendMessage(makeResponse(0, 0, "error", false, "{}", "unknown DAP handler error"));
811 }
812}
813
815 if (!listening_.load()) return;
816 std::lock_guard<std::recursive_mutex> lock(ioMu_);
817 acceptNonBlocking();
818 readAndDispatch();
819}
820
822 if (!listening_.load()) return false;
823 if (timeoutMs < 0) timeoutMs = 0;
824 using clock = std::chrono::steady_clock;
825 const auto deadline = clock::now() + std::chrono::milliseconds(timeoutMs);
826 // Fast path for `eve --dap-port` without an IDE: don't block the whole timeout.
827 const auto clientDeadline = clock::now() + std::chrono::milliseconds(
828 std::min(timeoutMs, 2500));
829 while (clock::now() < clientDeadline) {
830 poll();
831 if (configured_) return true;
832 if (hasClient_.load()) break;
833 std::this_thread::sleep_for(std::chrono::milliseconds(10));
834 }
835 if (!hasClient_.load()) {
836 poll();
837 return configured_;
838 }
839 while (clock::now() < deadline) {
840 poll();
841 if (configured_) return true;
842 std::this_thread::sleep_for(std::chrono::milliseconds(10));
843 }
844 poll();
845 return configured_;
846}
847
848} // namespace eve::dev
int line
Tok kind
std::string type
glm::vec3 n
Definition Grass.cpp:64
int w
JobFunc body
uint32_t c
float f
const char * name
Definition RockMesh.cpp:21
std::string filter
int v
int parent
Definition TreeMesh.cpp:175
int children
Definition TreeMesh.cpp:177
uint32_t s
Definition Weather.cpp:28
Minimal Debug Adapter Protocol (DAP) server for VS Code.
void setSourceRoot(std::string root)
Game directory used to resolve relative script paths in stack frames.
void poll()
Accept clients + process one request batch (non-blocking).
int listen(uint16_t port)
Bind TCP listen port (0 = ephemeral). Returns bound port or 0 on failure.
bool waitUntilConfigured(int timeoutMs=15000)
Block briefly until a DAP client finishes initialize/launch/setBreakpoints/ configurationDone so scri...
void registerSource(std::string name, std::string content)
Register virtual source content for compiled buffers (compilestring).
void notifyStopped(PauseReason reason, const SourceLoc &loc, const std::string &description={})
Emit stopped event to the connected client (if any).
static DebugAdapter & instance()
static std::string sourceBasename(const std::string &source)
Basename of a normalized path (empty-safe).
Definition Debugger.cpp:326
void setBreakpointEventFn(BreakpointEventFn fn)
Definition Debugger.hpp:176
static std::string normalizeSource(std::string source)
Normalize source paths for breakpoint matching (basename fallback).
Definition Debugger.cpp:309
static Debugger & instance()
Definition Debugger.cpp:304
static bool sourcesMatch(const std::string &a, const std::string &b)
True when two source paths refer to the same script file. Matches exact path, basename,...
Definition Debugger.cpp:332
ref is a smart pointer that automatically calls ref() and unref() on the object. Type T must be a sub...
Definition Object.h:54
VarKind
Where a variable tree node is rooted (used by containerChildren/resolvePath).
Definition Debugger.hpp:67
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:387
Source location in a Squirrel (or synthetic) script.
Definition CallGraph.hpp:16
std::string source
Definition CallGraph.hpp:17