vkdoom_m/libraries/cppdap/examples/hello_debugger.cpp
Nikita 45ac7c1d9e
ZScript DAP Debug server (#3009)
* Add STL-standard type traits and functions to TMap to enable for loop iteration

* add third-party range-map library

I have e-mailed the author for clarifcation on the license, will update this when they respond

* vcpkg: Add cppdap and eventpp libraries, update baseline

* DAP implementation

* Add `FileSystem::FileHash()` to get the CRC32 hash

* add `starting_offset` param to `VMDisasm()` for debugger disassembly view

Defaults to `0`, should not change output if it's not set

* Add `VMFrameStack::HasFrames()` to prevent assertions when inspecting in the debugger

* Add `PC` field to VMFrame, ensure that it is updated whenever vm increments/decrements the pc

Does not change alignment, the offsets used in VMFrame still work
We need this for the debugger because we otherwise have no way to get the pc; it was a local in `ExecScriptFunc()`

* `ZCCCompiler::CreateClassTypes()`: ensure SourceLumpName is set for classes derived from non-native classes

* start debug server in d_main, add `vm_debug` cvars and `-debug` CLI arg

* Add documentation for `vm_debug` cvars

* vm_exec: Add debugger hooks for instruction execution events

* c_console: Add debugger hooks for logging events in `PrintString`

* add `.cache/` to gitignore

* vendor cppdap on main @ 6464cd7

Patches:
removed .gitmodules (submodules were thirdparty/json, thirdparty/googletest)
removed thirdparty/googletest, not needed
removed thirdparty/json/docs, thirdparty/json/test, thirdparty/json/benchmarks to prevent massive bloat

* vendor eventpp on master @ 1224dd6

* build: use internal cppdap and eventpp by default

* dap: fix Binary::GetFunctionLineRange()

* fix bug in range_map::find_ranges

* make Binary dap::Source dynamic

* cache source code upon retrieval

* refactor Binary into a class, cleanup PexCache

* fix ending session gracefully

* d_main: Stop debug server in D_Cleanup()

* Fix connecting to debugger when session already started

* cleanup unused stuff in ZScriptDebugger

* always send TerminateEvents on disconnect if initialized

* tweak color display

* Don't cache disassembly lines when scanning scripts

* Cache nodes when getting runtime state

* WIP display locals

* Fix display of static arrays

* Ensure names display in proper order

* dap: Fix struct locals display, add args display

* Support `start` parameter

* Support `filter` parameter

* remove struct unbound native data display

Practically useless for debugging zscript and didn't work properly anyway

* d_main: fix vm_jit and vm_jit not being disabled soon enough

* support breaking on abort exceptions

* dap: refactor game event emitter functions into seperate header

* dap: show native functions on stack

* dap: Remove "Native" from exception handling, simplify exception event emit

* dap: add instruction breakpoints

* dap: fix display of locals not in scope yet

* dap: Make disassembly view display invalid instructions for non-code addresses

* dap: remove dot initializers

* dap: fix local structs in scope

* dap: don't parse the non-used options in the launch/attach requests

* dap: fix local struct view

* dap: Fix displaying objects that aren't their actual types

* dap: Fix action and state handling

* dap: stack display view

* dap: fix object display view

* d_dehacked: set qualified name in addition to the printable name

* dap: fix displaying breakpoint errors when script isn't loaded

* dap: remove debug print

* dap: Display parameter names

* dap: Turn down verbosity of logging

* dap: fix disassembly view

* dap: fix performance problems with arrays

* c_console: emit event only if not PRINT_NODAPEVENT

* dap: improve logging

* dap: update upstream cppdap library to fix deadlocks on no bind

* dap: Fix ending session on client socket closed

* dap: prevent DebugServer.h from pulling in `dap` and `ZScriptDebugger.h`

* dap: fix pause event not being emitted on pause

* dap: remove eventpp emitters, way too slow

* Remove eventpp dependency

* dap: Display correct register names

* dap: Show special inits in registers

* dap: Add stack offset to VMLocalVariable

* dap: fix display of static arrays and local variables on stack vs. registers

* dap: fix displaying function pointers

* dap: tweak color display

* dap: fix scalar display < 4

* dap: add Globals display to debugger

* dap: unify methods to get vmvalue

* dap: rename free method to freeValue to avoid running afoul of macro defs

* fix windows builds

* fix compile on linux

* cleanup

* Fix display of function breakpoints

* dap: Don't send back binary files

* dap: include sbarinfo in script types (no debug support for anything but zscript yet, this is just for returning source info)

* dap: don't show ending session message unless initialized

* fix erroneous commit

* dap: handle evaluate requests

* Fix getting bitfield values

* Add CVars Scope and evaluation

* add running console commands from repl

* dap: disable commands via repl for now

* fix loading functions DECORATE scripts

* Add source information to Dehacked VM functions, add debugging support

* dap: cleanup

* fix resolving archive paths

* don't send source back on native stack frames

* handle `modules` request

* cleanup

* allow evaluating cvars on hover

* fix oob bpinfos

* fix restarting the game blowing out the debug server

* dap: process input events while paused to prevent deadlocks

* fix getting local state

* dap: fix LocalState alignment

* dap: fix DumpStateHelper

* update cppdap protocol version to 1.68.0

* remove cppdap from vcpkg deps

We can't use the upstream version anyway because the maintainers are not merging our patches

* dap: make named variable nodes derive from the same class

* dap: make cvar scope available in native stack frames

* handle local variables with conflicting names

* add I_GetWindowEvent() to win32 to only process window events when debugging is paused

* dap: fix evaluate

* dap: fix display of `out` variables
2025-07-18 00:01:18 -03:00

469 lines
16 KiB
C++

// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// hello_debugger is an example DAP server that provides single line stepping
// through a synthetic file.
#include "dap/io.h"
#include "dap/protocol.h"
#include "dap/session.h"
#include <condition_variable>
#include <cstdio>
#include <mutex>
#include <unordered_set>
#ifdef _MSC_VER
#define OS_WINDOWS 1
#endif
// Uncomment the line below and change <path-to-log-file> to a file path to
// write all DAP communications to the given path.
//
// #define LOG_TO_FILE "<path-to-log-file>"
#ifdef OS_WINDOWS
#include <fcntl.h> // _O_BINARY
#include <io.h> // _setmode
#endif // OS_WINDOWS
namespace {
// sourceContent holds the synthetic file source.
constexpr char sourceContent[] = R"(// Hello Debugger!
This is a synthetic source file provided by the DAP debugger.
You can set breakpoints, and single line step.
You may also notice that the locals contains a single variable for the currently executing line number.)";
// Total number of newlines in source.
constexpr int64_t numSourceLines = 7;
// Debugger holds the dummy debugger state and fires events to the EventHandler
// passed to the constructor.
class Debugger {
public:
enum class Event { BreakpointHit, Stepped, Paused };
using EventHandler = std::function<void(Event)>;
Debugger(const EventHandler&);
// run() instructs the debugger to continue execution.
void run();
// pause() instructs the debugger to pause execution.
void pause();
// currentLine() returns the currently executing line number.
int64_t currentLine();
// stepForward() instructs the debugger to step forward one line.
void stepForward();
// clearBreakpoints() clears all set breakpoints.
void clearBreakpoints();
// addBreakpoint() sets a new breakpoint on the given line.
void addBreakpoint(int64_t line);
private:
EventHandler onEvent;
std::mutex mutex;
int64_t line = 1;
std::unordered_set<int64_t> breakpoints;
};
Debugger::Debugger(const EventHandler& onEvent) : onEvent(onEvent) {}
void Debugger::run() {
std::unique_lock<std::mutex> lock(mutex);
for (int64_t i = 0; i < numSourceLines; i++) {
int64_t l = ((line + i) % numSourceLines) + 1;
if (breakpoints.count(l)) {
line = l;
lock.unlock();
onEvent(Event::BreakpointHit);
return;
}
}
}
void Debugger::pause() {
onEvent(Event::Paused);
}
int64_t Debugger::currentLine() {
std::unique_lock<std::mutex> lock(mutex);
return line;
}
void Debugger::stepForward() {
std::unique_lock<std::mutex> lock(mutex);
line = (line % numSourceLines) + 1;
lock.unlock();
onEvent(Event::Stepped);
}
void Debugger::clearBreakpoints() {
std::unique_lock<std::mutex> lock(mutex);
this->breakpoints.clear();
}
void Debugger::addBreakpoint(int64_t l) {
std::unique_lock<std::mutex> lock(mutex);
this->breakpoints.emplace(l);
}
// Event provides a basic wait and signal synchronization primitive.
class Event {
public:
// wait() blocks until the event is fired.
void wait();
// fire() sets signals the event, and unblocks any calls to wait().
void fire();
private:
std::mutex mutex;
std::condition_variable cv;
bool fired = false;
};
void Event::wait() {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&] { return fired; });
}
void Event::fire() {
std::unique_lock<std::mutex> lock(mutex);
fired = true;
cv.notify_all();
}
} // anonymous namespace
// main() entry point to the DAP server.
int main(int, char*[]) {
#ifdef OS_WINDOWS
// Change stdin & stdout from text mode to binary mode.
// This ensures sequences of \r\n are not changed to \n.
_setmode(_fileno(stdin), _O_BINARY);
_setmode(_fileno(stdout), _O_BINARY);
#endif // OS_WINDOWS
std::shared_ptr<dap::Writer> log;
#ifdef LOG_TO_FILE
log = dap::file(LOG_TO_FILE);
#endif
// Create the DAP session.
// This is used to implement the DAP server.
auto session = dap::Session::create();
// Hard-coded identifiers for the one thread, frame, variable and source.
// These numbers have no meaning, and just need to remain constant for the
// duration of the service.
const dap::integer threadId = 100;
const dap::integer frameId = 200;
const dap::integer variablesReferenceId = 300;
const dap::integer sourceReferenceId = 400;
// Signal events
Event configured;
Event terminate;
// Event handlers from the Debugger.
auto onDebuggerEvent = [&](Debugger::Event onEvent) {
switch (onEvent) {
case Debugger::Event::Stepped: {
// The debugger has single-line stepped. Inform the client.
dap::StoppedEvent event;
event.reason = "step";
event.threadId = threadId;
session->send(event);
break;
}
case Debugger::Event::BreakpointHit: {
// The debugger has hit a breakpoint. Inform the client.
dap::StoppedEvent event;
event.reason = "breakpoint";
event.threadId = threadId;
session->send(event);
break;
}
case Debugger::Event::Paused: {
// The debugger has been suspended. Inform the client.
dap::StoppedEvent event;
event.reason = "pause";
event.threadId = threadId;
session->send(event);
break;
}
}
};
// Construct the debugger.
Debugger debugger(onDebuggerEvent);
// Handle errors reported by the Session. These errors include protocol
// parsing errors and receiving messages with no handler.
session->onError([&](const char* msg) {
if (log) {
dap::writef(log, "dap::Session error: %s\n", msg);
log->close();
}
terminate.fire();
});
// The Initialize request is the first message sent from the client and
// the response reports debugger capabilities.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Initialize
session->registerHandler([](const dap::InitializeRequest&) {
dap::InitializeResponse response;
response.supportsConfigurationDoneRequest = true;
return response;
});
// When the Initialize response has been sent, we need to send the initialized
// event.
// We use the registerSentHandler() to ensure the event is sent *after* the
// initialize response.
// https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized
session->registerSentHandler(
[&](const dap::ResponseOrError<dap::InitializeResponse>&) {
session->send(dap::InitializedEvent());
});
// The Threads request queries the debugger's list of active threads.
// This example debugger only exposes a single thread.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Threads
session->registerHandler([&](const dap::ThreadsRequest&) {
dap::ThreadsResponse response;
dap::Thread thread;
thread.id = threadId;
thread.name = "TheThread";
response.threads.push_back(thread);
return response;
});
// The StackTrace request reports the stack frames (call stack) for a given
// thread. This example debugger only exposes a single stack frame for the
// single thread.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StackTrace
session->registerHandler(
[&](const dap::StackTraceRequest& request)
-> dap::ResponseOrError<dap::StackTraceResponse> {
if (request.threadId != threadId) {
return dap::Error("Unknown threadId '%d'", int(request.threadId));
}
dap::Source source;
source.sourceReference = sourceReferenceId;
source.name = "HelloDebuggerSource";
dap::StackFrame frame;
frame.line = debugger.currentLine();
frame.column = 1;
frame.name = "HelloDebugger";
frame.id = frameId;
frame.source = source;
dap::StackTraceResponse response;
response.stackFrames.push_back(frame);
return response;
});
// The Scopes request reports all the scopes of the given stack frame.
// This example debugger only exposes a single 'Locals' scope for the single
// frame.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes
session->registerHandler([&](const dap::ScopesRequest& request)
-> dap::ResponseOrError<dap::ScopesResponse> {
if (request.frameId != frameId) {
return dap::Error("Unknown frameId '%d'", int(request.frameId));
}
dap::Scope scope;
scope.name = "Locals";
scope.presentationHint = "locals";
scope.variablesReference = variablesReferenceId;
dap::ScopesResponse response;
response.scopes.push_back(scope);
return response;
});
// The Variables request reports all the variables for the given scope.
// This example debugger only exposes a single 'currentLine' variable for the
// single 'Locals' scope.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Variables
session->registerHandler([&](const dap::VariablesRequest& request)
-> dap::ResponseOrError<dap::VariablesResponse> {
if (request.variablesReference != variablesReferenceId) {
return dap::Error("Unknown variablesReference '%d'",
int(request.variablesReference));
}
dap::Variable currentLineVar;
currentLineVar.name = "currentLine";
currentLineVar.value = std::to_string(debugger.currentLine());
currentLineVar.type = "int";
dap::VariablesResponse response;
response.variables.push_back(currentLineVar);
return response;
});
// The Pause request instructs the debugger to pause execution of one or all
// threads.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Pause
session->registerHandler([&](const dap::PauseRequest&) {
debugger.pause();
return dap::PauseResponse();
});
// The Continue request instructs the debugger to resume execution of one or
// all threads.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Continue
session->registerHandler([&](const dap::ContinueRequest&) {
debugger.run();
return dap::ContinueResponse();
});
// The Next request instructs the debugger to single line step for a specific
// thread.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Next
session->registerHandler([&](const dap::NextRequest&) {
debugger.stepForward();
return dap::NextResponse();
});
// The StepIn request instructs the debugger to step-in for a specific thread.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepIn
session->registerHandler([&](const dap::StepInRequest&) {
// Step-in treated as step-over as there's only one stack frame.
debugger.stepForward();
return dap::StepInResponse();
});
// The StepOut request instructs the debugger to step-out for a specific
// thread.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_StepOut
session->registerHandler([&](const dap::StepOutRequest&) {
// Step-out is not supported as there's only one stack frame.
return dap::StepOutResponse();
});
// The SetBreakpoints request instructs the debugger to clear and set a number
// of line breakpoints for a specific source file.
// This example debugger only exposes a single source file.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetBreakpoints
session->registerHandler([&](const dap::SetBreakpointsRequest& request) {
dap::SetBreakpointsResponse response;
auto breakpoints = request.breakpoints.value({});
if (request.source.sourceReference.value(0) == sourceReferenceId) {
debugger.clearBreakpoints();
response.breakpoints.resize(breakpoints.size());
for (size_t i = 0; i < breakpoints.size(); i++) {
debugger.addBreakpoint(breakpoints[i].line);
response.breakpoints[i].verified = breakpoints[i].line < numSourceLines;
}
} else {
response.breakpoints.resize(breakpoints.size());
}
return response;
});
// The SetExceptionBreakpoints request configures the debugger's handling of
// thrown exceptions.
// This example debugger does not use any exceptions, so this is a no-op.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_SetExceptionBreakpoints
session->registerHandler([&](const dap::SetExceptionBreakpointsRequest&) {
return dap::SetExceptionBreakpointsResponse();
});
// The Source request retrieves the source code for a given source file.
// This example debugger only exposes one synthetic source file.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Source
session->registerHandler([&](const dap::SourceRequest& request)
-> dap::ResponseOrError<dap::SourceResponse> {
if (request.sourceReference != sourceReferenceId) {
return dap::Error("Unknown source reference '%d'",
int(request.sourceReference));
}
dap::SourceResponse response;
response.content = sourceContent;
return response;
});
// The Launch request is made when the client instructs the debugger adapter
// to start the debuggee. This request contains the launch arguments.
// This example debugger does nothing with this request.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Launch
session->registerHandler(
[&](const dap::LaunchRequest&) { return dap::LaunchResponse(); });
// Handler for disconnect requests
session->registerHandler([&](const dap::DisconnectRequest& request) {
if (request.terminateDebuggee.value(false)) {
terminate.fire();
}
return dap::DisconnectResponse();
});
// The ConfigurationDone request is made by the client once all configuration
// requests have been made.
// This example debugger uses this request to 'start' the debugger.
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_ConfigurationDone
session->registerHandler([&](const dap::ConfigurationDoneRequest&) {
configured.fire();
return dap::ConfigurationDoneResponse();
});
// All the handlers we care about have now been registered.
// We now bind the session to stdin and stdout to connect to the client.
// After the call to bind() we should start receiving requests, starting with
// the Initialize request.
std::shared_ptr<dap::Reader> in = dap::file(stdin, false);
std::shared_ptr<dap::Writer> out = dap::file(stdout, false);
if (log) {
session->bind(spy(in, log), spy(out, log));
} else {
session->bind(in, out);
}
// Wait for the ConfigurationDone request to be made.
configured.wait();
// Broadcast the existance of the single thread to the client.
dap::ThreadEvent threadStartedEvent;
threadStartedEvent.reason = "started";
threadStartedEvent.threadId = threadId;
session->send(threadStartedEvent);
// Start the debugger in a paused state.
// This sends a stopped event to the client.
debugger.pause();
// Block until we receive a 'terminateDebuggee' request or encounter a session
// error.
terminate.wait();
return 0;
}