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