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
This commit is contained in:
parent
fcf529590f
commit
45ac7c1d9e
235 changed files with 83166 additions and 62 deletions
807
src/common/scripting/dap/ZScriptDebugger.cpp
Normal file
807
src/common/scripting/dap/ZScriptDebugger.cpp
Normal file
|
|
@ -0,0 +1,807 @@
|
|||
#include "ZScriptDebugger.h"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <dap/protocol.h>
|
||||
#include <dap/session.h>
|
||||
|
||||
#include "Utilities.h"
|
||||
#include "GameInterfaces.h"
|
||||
#include "Nodes/StackFrameStateNode.h"
|
||||
#include "Nodes/StateNodeBase.h"
|
||||
#include "Nodes/CVarScopeStateNode.h"
|
||||
#include "common/scripting/dap/Nodes/LocalScopeStateNode.h"
|
||||
|
||||
|
||||
// This is the main class that handles the debug session and the debug requests/responses and events
|
||||
|
||||
namespace DebugServer
|
||||
{
|
||||
ZScriptDebugger::ZScriptDebugger()
|
||||
{
|
||||
m_pexCache = std::make_shared<PexCache>();
|
||||
|
||||
m_breakpointManager = std::make_shared<BreakpointManager>(m_pexCache.get());
|
||||
|
||||
m_idProvider = std::make_shared<IdProvider>();
|
||||
m_runtimeState = std::make_shared<RuntimeState>(m_idProvider);
|
||||
|
||||
m_executionManager = std::make_shared<DebugExecutionManager>(m_runtimeState.get(), m_breakpointManager.get());
|
||||
}
|
||||
|
||||
void ZScriptDebugger::StartSession(std::shared_ptr<dap::Session> session)
|
||||
{
|
||||
if (m_session)
|
||||
{
|
||||
if (m_initialized)
|
||||
{
|
||||
LogInternalError("Session is already active, ending it first!");
|
||||
}
|
||||
EndSession();
|
||||
}
|
||||
m_initialized = false;
|
||||
m_quitting = false;
|
||||
m_session = session;
|
||||
m_executionManager->Open(session);
|
||||
m_createStackEventHandle = RuntimeEvents::SubscribeToCreateStack(std::bind(&ZScriptDebugger::StackCreated, this, std::placeholders::_1));
|
||||
|
||||
m_cleanupStackEventHandle = RuntimeEvents::SubscribeToCleanupStack(std::bind(&ZScriptDebugger::StackCleanedUp, this, std::placeholders::_1));
|
||||
|
||||
m_instructionExecutionEventHandle = RuntimeEvents::SubscribeToInstructionExecution(
|
||||
std::bind(&ZScriptDebugger::InstructionExecution, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
|
||||
|
||||
// m_initScriptEventHandle = RuntimeEvents::SubscribeToInitScript(std::bind(&ZScriptDebugger::InitScriptEvent, this, std::placeholders::_1));
|
||||
m_logEventHandle = RuntimeEvents::SubscribeToLog(std::bind(&ZScriptDebugger::EventLogged, this, std::placeholders::_1, std::placeholders::_2));
|
||||
|
||||
m_breakpointChangedEventHandle
|
||||
= RuntimeEvents::SubscribeToBreakpointChanged(std::bind(&ZScriptDebugger::BreakpointChanged, this, std::placeholders::_1, std::placeholders::_2));
|
||||
|
||||
m_exceptionThrownEventHandle = RuntimeEvents::SubscribeToExceptionThrown(
|
||||
std::bind(&ZScriptDebugger::ExceptionThrown, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||
RegisterSessionHandlers();
|
||||
}
|
||||
|
||||
bool ZScriptDebugger::IsEndingSession()
|
||||
{
|
||||
return m_endingSession;
|
||||
}
|
||||
|
||||
bool ZScriptDebugger::EndSession(bool closed)
|
||||
{
|
||||
// This is just to prevent the closedHandler from ending the session again
|
||||
if (m_endingSession)
|
||||
{
|
||||
return m_quitting;
|
||||
}
|
||||
m_endingSession = true;
|
||||
m_executionManager->Close();
|
||||
if (m_session)
|
||||
{
|
||||
if (!closed && m_initialized)
|
||||
{
|
||||
LogInternal("Ending DAP debugging session.");
|
||||
SendEvent(dap::TerminatedEvent());
|
||||
}
|
||||
}
|
||||
m_initialized = false;
|
||||
m_session = nullptr;
|
||||
|
||||
RuntimeEvents::UnsubscribeFromLog(m_logEventHandle);
|
||||
// RuntimeEvents::UnsubscribeFromInitScript(m_initScriptEventHandle);
|
||||
RuntimeEvents::UnsubscribeFromInstructionExecution(m_instructionExecutionEventHandle);
|
||||
RuntimeEvents::UnsubscribeFromCreateStack(m_createStackEventHandle);
|
||||
RuntimeEvents::UnsubscribeFromCleanupStack(m_cleanupStackEventHandle);
|
||||
RuntimeEvents::UnsubscribeFromBreakpointChanged(m_breakpointChangedEventHandle);
|
||||
RuntimeEvents::UnsubscribeFromExceptionThrown(m_exceptionThrownEventHandle);
|
||||
m_logEventHandle = nullptr;
|
||||
m_instructionExecutionEventHandle = nullptr;
|
||||
m_createStackEventHandle = nullptr;
|
||||
m_cleanupStackEventHandle = nullptr;
|
||||
m_breakpointChangedEventHandle = nullptr;
|
||||
m_exceptionThrownEventHandle = nullptr;
|
||||
// clear session data
|
||||
m_projectArchive.clear();
|
||||
m_projectPath.clear();
|
||||
m_projectSources.clear();
|
||||
m_breakpointManager->ClearBreakpoints();
|
||||
m_endingSession = false;
|
||||
return m_quitting;
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::SetInstructionBreakpointsResponse> ZScriptDebugger::SetInstructionBreakpoints(const dap::SetInstructionBreakpointsRequest &request)
|
||||
{
|
||||
return m_breakpointManager->SetInstructionBreakpoints(request);
|
||||
}
|
||||
|
||||
void ZScriptDebugger::RegisterSessionHandlers()
|
||||
{
|
||||
// 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
|
||||
m_session->registerHandler([this](const dap::InitializeRequest &request) { return Initialize(request); });
|
||||
m_session->onError([this](const char *msg) { LogInternalError("%s", msg); });
|
||||
m_session->registerSentHandler(
|
||||
// After an intialize response is sent, we send an initialized event to indicate that the client can now send requests.
|
||||
[this](const dap::ResponseOrError<dap::InitializeResponse> &)
|
||||
{
|
||||
LogInternal("DAP debugging session started.");
|
||||
// enable event sending
|
||||
m_initialized = true;
|
||||
SendEvent(dap::InitializedEvent());
|
||||
});
|
||||
|
||||
// Client is done configuring.
|
||||
m_session->registerHandler([this](const dap::ConfigurationDoneRequest &) { return dap::ConfigurationDoneResponse {}; });
|
||||
|
||||
// The Disconnect request is sent by the client before it disconnects from the server.
|
||||
// https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Disconnect
|
||||
m_session->registerHandler(
|
||||
[this](const dap::DisconnectRequest &request)
|
||||
{
|
||||
// Client wants to disconnect.
|
||||
if (request.terminateDebuggee.value(false))
|
||||
{
|
||||
m_quitting = true;
|
||||
}
|
||||
return dap::DisconnectResponse {};
|
||||
});
|
||||
m_session->registerHandler([this](const dap::PDSLaunchRequest &request) { return Launch(request); });
|
||||
m_session->registerHandler([this](const dap::PDSAttachRequest &request) { return Attach(request); });
|
||||
m_session->registerHandler([this](const dap::PauseRequest &request) { return Pause(request); });
|
||||
m_session->registerHandler([this](const dap::ContinueRequest &request) { return Continue(request); });
|
||||
m_session->registerHandler([this](const dap::ThreadsRequest &request) { return GetThreads(request); });
|
||||
m_session->registerHandler([this](const dap::SetBreakpointsRequest &request) { return SetBreakpoints(request); });
|
||||
m_session->registerHandler([this](const dap::SetExceptionBreakpointsRequest &request) { return SetExceptionBreakpoints(request); });
|
||||
m_session->registerHandler([this](const dap::SetFunctionBreakpointsRequest &request) { return SetFunctionBreakpoints(request); });
|
||||
m_session->registerHandler([this](const dap::SetInstructionBreakpointsRequest &request) { return SetInstructionBreakpoints(request); });
|
||||
m_session->registerHandler([this](const dap::StackTraceRequest &request) { return GetStackTrace(request); });
|
||||
m_session->registerHandler([this](const dap::StepInRequest &request) { return StepIn(request); });
|
||||
m_session->registerHandler([this](const dap::StepOutRequest &request) { return StepOut(request); });
|
||||
m_session->registerHandler([this](const dap::NextRequest &request) { return Next(request); });
|
||||
m_session->registerHandler([this](const dap::ScopesRequest &request) { return GetScopes(request); });
|
||||
m_session->registerHandler([this](const dap::VariablesRequest &request) { return GetVariables(request); });
|
||||
m_session->registerHandler([this](const dap::SourceRequest &request) { return GetSource(request); });
|
||||
m_session->registerHandler([this](const dap::LoadedSourcesRequest &request) { return GetLoadedSources(request); });
|
||||
m_session->registerHandler([this](const dap::DisassembleRequest &request) { return Disassemble(request); });
|
||||
m_session->registerHandler([this](const dap::EvaluateRequest &request) { return Evaluate(request); });
|
||||
m_session->registerHandler([this](const dap::ModulesRequest &request) { return Modules(request); });
|
||||
}
|
||||
|
||||
dap::Error ZScriptDebugger::Error(const std::string &msg)
|
||||
{
|
||||
Printf("%s", msg.c_str());
|
||||
return dap::Error(msg);
|
||||
}
|
||||
|
||||
template <typename T, typename> void ZScriptDebugger::SendEvent(const T &event)
|
||||
{
|
||||
if (m_session && m_initialized)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_session->send(event);
|
||||
// catch signal 13
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
LogInternalError("Error sending event");
|
||||
EndSession(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ZScriptDebugger::EventLogged(int severity, const char *msg)
|
||||
{
|
||||
if (severity & PRINT_NODAPEVENT)
|
||||
{
|
||||
return;
|
||||
}
|
||||
dap::OutputEvent output;
|
||||
output.category = "console";
|
||||
output.output = std::string(msg) + "\r\n";
|
||||
// LogGameOutput(logEvent->severity, output.output);
|
||||
SendEvent(output);
|
||||
}
|
||||
|
||||
void ZScriptDebugger::StackCreated(VMFrameStack *stack)
|
||||
{
|
||||
#if 0
|
||||
const auto stackId = 0; // only one stack
|
||||
SendEvent(dap::ThreadEvent{
|
||||
.reason = "started",
|
||||
.threadId = stackId
|
||||
});
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
void ZScriptDebugger::StackCleanedUp(uint32_t stackId)
|
||||
{
|
||||
#if 0
|
||||
SendEvent(dap::ThreadEvent{
|
||||
.reason = "exited",
|
||||
.threadId = stackId
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void ZScriptDebugger::InstructionExecution(VMFrameStack *stack, VMReturn *ret, int numret, const VMOP *pc)
|
||||
{
|
||||
m_executionManager->HandleInstruction(stack, ret, numret, pc);
|
||||
}
|
||||
|
||||
// For source loaded events
|
||||
void ZScriptDebugger::CheckSourceLoaded(const std::string &scriptName)
|
||||
{
|
||||
auto binary = m_pexCache->GetScript(scriptName);
|
||||
if (binary && m_session)
|
||||
{
|
||||
dap::LoadedSourceEvent event;
|
||||
event.reason = "new";
|
||||
event.source = binary->GetDapSource();
|
||||
SendEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ZScriptDebugger::BreakpointChanged(const dap::Breakpoint &bpoint, const std::string &reason)
|
||||
{
|
||||
dap::BreakpointEvent event;
|
||||
event.breakpoint = bpoint;
|
||||
event.reason = reason;
|
||||
SendEvent(event);
|
||||
}
|
||||
|
||||
void ZScriptDebugger::ExceptionThrown(EVMAbortException reason, const std::string &message, const std::string &stackTrace)
|
||||
{
|
||||
m_executionManager->HandleException(reason, message, stackTrace);
|
||||
}
|
||||
|
||||
ZScriptDebugger::~ZScriptDebugger()
|
||||
{
|
||||
m_initialized = false;
|
||||
EndSession();
|
||||
m_runtimeState->Reset();
|
||||
m_pexCache->Clear();
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::InitializeResponse> ZScriptDebugger::Initialize(const dap::InitializeRequest &request)
|
||||
{
|
||||
m_clientCaps = request;
|
||||
dap::InitializeResponse response;
|
||||
LogInternal("Initializing DAP session...");
|
||||
response.supportsConfigurationDoneRequest = true;
|
||||
response.supportsLoadedSourcesRequest = true;
|
||||
response.supportedChecksumAlgorithms = {"CRC32"};
|
||||
response.supportsFunctionBreakpoints = true;
|
||||
#if !defined(_WIN32) && !defined(_WIN64)
|
||||
// TODO: remove this when disassemble is supported on windows
|
||||
response.supportsDisassembleRequest = true;
|
||||
response.supportsSteppingGranularity = true;
|
||||
#endif
|
||||
response.supportTerminateDebuggee = true;
|
||||
response.supportsInstructionBreakpoints = true;
|
||||
response.supportsEvaluateForHovers = true;
|
||||
response.supportsModulesRequest = true;
|
||||
response.exceptionBreakpointFilters = DebugExecutionManager::GetAllExceptionFilters();
|
||||
return response;
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::LaunchResponse> ZScriptDebugger::Launch(const dap::PDSLaunchRequest &request)
|
||||
{
|
||||
dap::PDSAttachRequest attach_request;
|
||||
attach_request.name = request.name;
|
||||
attach_request.type = request.type;
|
||||
attach_request.request = request.request;
|
||||
attach_request.projectSources = request.projectSources;
|
||||
|
||||
auto resp = Attach(attach_request);
|
||||
if (resp.error)
|
||||
{
|
||||
RETURN_DAP_ERROR(resp.error.message.c_str());
|
||||
}
|
||||
return dap::ResponseOrError<dap::LaunchResponse>();
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::AttachResponse> ZScriptDebugger::Attach(const dap::PDSAttachRequest &request)
|
||||
{
|
||||
m_projectSources.clear();
|
||||
if (!request.restart.has_value())
|
||||
{
|
||||
m_pexCache->Clear();
|
||||
}
|
||||
m_pexCache->ScanAllScripts();
|
||||
for (auto src : request.projectSources.value(std::vector<dap::Source>()))
|
||||
{
|
||||
auto binary = m_pexCache->GetScript(src);
|
||||
if (!binary)
|
||||
{ // no source ref or name, we'll ignore it
|
||||
continue;
|
||||
}
|
||||
auto source = binary->GetDapSource();
|
||||
m_projectSources[source.sourceReference.value()] = source;
|
||||
}
|
||||
|
||||
return dap::AttachResponse();
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::ContinueResponse> ZScriptDebugger::Continue(const dap::ContinueRequest &request)
|
||||
{
|
||||
if (m_executionManager->Continue()) return dap::ContinueResponse();
|
||||
RETURN_DAP_ERROR("Could not Continue");
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::PauseResponse> ZScriptDebugger::Pause(const dap::PauseRequest &request)
|
||||
{
|
||||
if (m_executionManager->Pause()) return dap::PauseResponse();
|
||||
RETURN_DAP_ERROR("Already paused!");
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::ThreadsResponse> ZScriptDebugger::GetThreads(const dap::ThreadsRequest &request)
|
||||
{
|
||||
dap::ThreadsResponse response;
|
||||
|
||||
// just one thread!
|
||||
dap::Thread thread;
|
||||
thread.id = 1;
|
||||
thread.name = "Main Thread";
|
||||
response.threads.push_back(thread);
|
||||
return response;
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::SetBreakpointsResponse> ZScriptDebugger::SetBreakpoints(const dap::SetBreakpointsRequest &request)
|
||||
{
|
||||
dap::Source source = request.source;
|
||||
auto ref = GetSourceReference(source);
|
||||
if (m_projectSources.find(ref) != m_projectSources.end())
|
||||
{
|
||||
source = m_projectSources[ref];
|
||||
}
|
||||
else if (ref > 0)
|
||||
{
|
||||
// It's not part of the project's imported sources, they have to get the decompiled source from us,
|
||||
// So we set sourceReference to make the debugger request the source from us
|
||||
source.sourceReference = ref;
|
||||
}
|
||||
return m_breakpointManager->SetBreakpoints(source, request);
|
||||
;
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::SetFunctionBreakpointsResponse> ZScriptDebugger::SetFunctionBreakpoints(const dap::SetFunctionBreakpointsRequest &request) { return m_breakpointManager->SetFunctionBreakpoints(request); }
|
||||
dap::ResponseOrError<dap::StackTraceResponse> ZScriptDebugger::GetStackTrace(const dap::StackTraceRequest &request)
|
||||
{
|
||||
dap::StackTraceResponse response;
|
||||
|
||||
if (request.threadId <= -1)
|
||||
{
|
||||
response.totalFrames = 0;
|
||||
RETURN_DAP_ERROR("No threadId specified");
|
||||
}
|
||||
auto frameVal = request.startFrame.value(0);
|
||||
auto levelVal = request.levels.value(0);
|
||||
std::vector<std::shared_ptr<StateNodeBase>> frameNodes;
|
||||
if (!m_runtimeState->ResolveChildrenByParentPath(std::to_string(request.threadId), frameNodes))
|
||||
{
|
||||
RETURN_DAP_ERROR("Could not find ThreadId");
|
||||
}
|
||||
uint32_t startFrame = static_cast<uint32_t>(frameVal > 0 ? frameVal : dap::integer(0));
|
||||
uint32_t levels = static_cast<uint32_t>(levelVal > 0 ? levelVal : dap::integer(0));
|
||||
|
||||
for (uint32_t frameIndex = startFrame; frameIndex < frameNodes.size() && frameIndex < startFrame + levels; frameIndex++)
|
||||
{
|
||||
const auto node = dynamic_cast<StackFrameStateNode *>(frameNodes.at(frameIndex).get());
|
||||
|
||||
dap::StackFrame frame;
|
||||
if (!node->SerializeToProtocol(frame, m_pexCache.get()))
|
||||
{
|
||||
RETURN_DAP_ERROR("Serialization error");
|
||||
}
|
||||
|
||||
response.stackFrames.push_back(frame);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
inline StepGranularity granularityStringToEnum(const std::string &granularity)
|
||||
{
|
||||
if (granularity == "instruction")
|
||||
{
|
||||
return StepGranularity::kInstruction;
|
||||
}
|
||||
else if (granularity == "statement")
|
||||
{
|
||||
return StepGranularity::kStatement;
|
||||
}
|
||||
return StepGranularity::kLine;
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::StepInResponse> ZScriptDebugger::StepIn(const dap::StepInRequest &request)
|
||||
{
|
||||
if (m_executionManager->Step(static_cast<uint32_t>(request.threadId), STEP_IN, granularityStringToEnum(request.granularity.value("line"))))
|
||||
{
|
||||
return dap::StepInResponse();
|
||||
}
|
||||
RETURN_DAP_ERROR("Could not StepIn");
|
||||
}
|
||||
dap::ResponseOrError<dap::StepOutResponse> ZScriptDebugger::StepOut(const dap::StepOutRequest &request)
|
||||
{
|
||||
if (m_executionManager->Step(static_cast<uint32_t>(request.threadId), STEP_OUT, granularityStringToEnum(request.granularity.value("line"))))
|
||||
{
|
||||
return dap::StepOutResponse();
|
||||
}
|
||||
RETURN_DAP_ERROR("Could not StepOut");
|
||||
}
|
||||
dap::ResponseOrError<dap::NextResponse> ZScriptDebugger::Next(const dap::NextRequest &request)
|
||||
{
|
||||
if (m_executionManager->Step(static_cast<uint32_t>(request.threadId), STEP_OVER, granularityStringToEnum(request.granularity.value("line"))))
|
||||
{
|
||||
return dap::NextResponse();
|
||||
}
|
||||
RETURN_DAP_ERROR("Could not Next");
|
||||
}
|
||||
dap::ResponseOrError<dap::ScopesResponse> ZScriptDebugger::GetScopes(const dap::ScopesRequest &request)
|
||||
{
|
||||
dap::ScopesResponse response;
|
||||
|
||||
std::vector<std::shared_ptr<StateNodeBase>> frameScopes;
|
||||
if (request.frameId < 0)
|
||||
{
|
||||
RETURN_DAP_ERROR(StringFormat("Invalid frameId %d", request.frameId).c_str());
|
||||
}
|
||||
auto frameId = static_cast<uint32_t>(request.frameId);
|
||||
if (!m_runtimeState->ResolveChildrenByParentId(frameId, frameScopes))
|
||||
{
|
||||
// Don't log, this happens as a result of a scopes request being sent after a step request that invalidates the state
|
||||
return dap::Error(StringFormat("No such frameId %d", frameId).c_str());
|
||||
}
|
||||
|
||||
for (const auto &frameScope : frameScopes)
|
||||
{
|
||||
auto asScopeSerializable = dynamic_cast<IProtocolScopeSerializable *>(frameScope.get());
|
||||
if (!asScopeSerializable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dap::Scope scope;
|
||||
if (!asScopeSerializable->SerializeToProtocol(scope))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
response.scopes.push_back(scope);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::VariablesResponse> ZScriptDebugger::GetVariables(const dap::VariablesRequest &request)
|
||||
{
|
||||
dap::VariablesResponse response;
|
||||
|
||||
std::vector<std::shared_ptr<StateNodeBase>> variableNodes;
|
||||
int64_t maxCount = request.count.value(INT_MAX);
|
||||
int64_t start = request.start.value(0);
|
||||
|
||||
if (!m_runtimeState->ResolveChildrenByParentId(static_cast<uint32_t>(request.variablesReference), variableNodes, start, maxCount))
|
||||
{
|
||||
// Don't log, this happens as a result of a variables request being sent after a step request that invalidates the state
|
||||
return dap::Error(StringFormat("No such variablesReference %d", request.variablesReference).c_str());
|
||||
}
|
||||
bool only_indexed = request.filter.value("") == "indexed";
|
||||
bool only_named = request.filter.value("") == "named";
|
||||
|
||||
for (int64_t i = 0; i < variableNodes.size(); i++)
|
||||
{
|
||||
auto asVariableSerializable = dynamic_cast<IProtocolVariableSerializable *>(variableNodes.at(i).get());
|
||||
if (!asVariableSerializable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dap::Variable variable;
|
||||
if (!asVariableSerializable->SerializeToProtocol(variable))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// if it's a number (i.e. the index of an array), we only want to show indexed variables
|
||||
if (only_indexed || only_named)
|
||||
{
|
||||
bool is_indexed = false;
|
||||
if (variable.name.size() > 0)
|
||||
{
|
||||
size_t offset = variable.name[0] == '[' && variable.name[variable.name.size() - 1] == ']' ? 1 : 0;
|
||||
is_indexed = std::all_of(variable.name.begin() + offset, variable.name.begin() + variable.name.size() - 1 - offset, ::isdigit);
|
||||
}
|
||||
if ((is_indexed && only_named) || (!is_indexed && only_indexed))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
response.variables.push_back(variable);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
dap::ResponseOrError<dap::SourceResponse> ZScriptDebugger::GetSource(const dap::SourceRequest &request)
|
||||
{
|
||||
if (!request.source.has_value() || !request.source.value().path.has_value() || !request.source.value().sourceReference.has_value())
|
||||
{
|
||||
RETURN_DAP_ERROR("No source path or reference");
|
||||
}
|
||||
auto source = request.source.value();
|
||||
dap::SourceResponse response;
|
||||
std::string sourceContent;
|
||||
if (m_pexCache->GetDecompiledSource(source, sourceContent))
|
||||
{
|
||||
response.content = sourceContent;
|
||||
return response;
|
||||
}
|
||||
RETURN_DAP_ERROR(StringFormat("No source found for %s", source.path.value("").c_str()).c_str());
|
||||
}
|
||||
dap::ResponseOrError<dap::LoadedSourcesResponse> ZScriptDebugger::GetLoadedSources(const dap::LoadedSourcesRequest &request) { return m_pexCache->GetLoadedSources(request); }
|
||||
|
||||
dap::ResponseOrError<dap::DisassembleResponse> ZScriptDebugger::Disassemble(const dap::DisassembleRequest &request)
|
||||
{
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
RETURN_DAP_ERROR("Disassemble not supported on Windows");
|
||||
#else
|
||||
auto ref = request.memoryReference;
|
||||
// ref is in the format "0x12345678", we need to convert it to a number
|
||||
if (ref.size() < 3 || ref[0] != '0' || ref[1] != 'x')
|
||||
{
|
||||
RETURN_DAP_ERROR("Invalid memoryReference");
|
||||
}
|
||||
const uint64_t req_address = std::stoull(ref.substr(2), nullptr, 16);
|
||||
const int64_t offset = request.instructionOffset.value(0);
|
||||
const VMOP *currCodePointer = (VMOP *)req_address;
|
||||
auto response = dap::DisassembleResponse();
|
||||
std::vector<std::shared_ptr<DisassemblyLine>> lines;
|
||||
m_pexCache->GetDisassemblyLines(currCodePointer, offset, request.instructionCount, lines);
|
||||
std::shared_ptr<Binary> bin;
|
||||
std::vector<std::string> instruction_addrs;
|
||||
for (auto &line : lines)
|
||||
{
|
||||
auto instruction = dap::DisassembledInstruction();
|
||||
instruction.instruction = line->instruction;
|
||||
instruction.address = StringFormat("%p", line->address);
|
||||
instruction_addrs.push_back(instruction.address);
|
||||
instruction.line = line->line;
|
||||
if (line->line != line->endLine && line->endLine > 0)
|
||||
{
|
||||
instruction.endLine = line->endLine;
|
||||
}
|
||||
|
||||
// TODO: turn this back on, vscode doesn't like it currently
|
||||
// only map the source for the first instruction, or if the source location has changed
|
||||
// if (!bin || bin->sourceData.sourceReference.value(-1) != line->ref){
|
||||
bin = m_pexCache->GetCachedScript(line->ref);
|
||||
if (bin)
|
||||
{
|
||||
instruction.location = bin->GetDapSource();
|
||||
}
|
||||
// }
|
||||
instruction.instructionBytes = line->bytes;
|
||||
response.instructions.push_back(instruction);
|
||||
}
|
||||
return response;
|
||||
#endif
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::SetExceptionBreakpointsResponse> ZScriptDebugger::SetExceptionBreakpoints(const dap::SetExceptionBreakpointsRequest &request)
|
||||
{
|
||||
auto response = dap::SetExceptionBreakpointsResponse();
|
||||
response.breakpoints = m_executionManager->SetExceptionBreakpointFilters(request.filters);
|
||||
return response;
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::ModulesResponse> ZScriptDebugger::Modules(const dap::ModulesRequest &request)
|
||||
{
|
||||
auto response = dap::ModulesResponse();
|
||||
response.modules = m_pexCache->GetModules();
|
||||
if (request.startModule.has_value()){
|
||||
response.modules = {response.modules.begin() + request.startModule.value(), response.modules.end()};
|
||||
}
|
||||
if (request.moduleCount.has_value()){
|
||||
response.modules = {response.modules.begin(), response.modules.begin() + request.moduleCount.value()};
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
dap::ResponseOrError<dap::EvaluateResponse> ZScriptDebugger::Evaluate(const dap::EvaluateRequest &request)
|
||||
{
|
||||
// get the type of evaluate
|
||||
auto context = request.context.value("repl");
|
||||
auto response = dap::EvaluateResponse();
|
||||
dap::Variable variable;
|
||||
bool found = false;
|
||||
auto isNonVariableContext = context != "variables";
|
||||
|
||||
auto TryPath = [&](const std::string &path){
|
||||
std::shared_ptr<StateNodeBase> node;
|
||||
if(m_runtimeState->ResolveStateByPath(path, node, isNonVariableContext)){
|
||||
auto asVariableSerializable = std::dynamic_pointer_cast<IProtocolVariableSerializable>(node);
|
||||
if(asVariableSerializable && asVariableSerializable->SerializeToProtocol(variable)){
|
||||
found = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (context == "variables"){
|
||||
isNonVariableContext = false;
|
||||
} else {
|
||||
isNonVariableContext = true;
|
||||
}
|
||||
if (context == "variables" || context == "hover" || context == "watch" || (context == "repl" && m_executionManager->IsPaused()))
|
||||
{
|
||||
int64_t frameId = request.frameId.value(0);
|
||||
int64_t evalLineNumber = request.line.value(-1);
|
||||
std::shared_ptr<StateNodeBase> _frameNode;
|
||||
if( m_runtimeState->ResolveStateById(frameId, _frameNode)){
|
||||
auto frameNode = std::dynamic_pointer_cast<StackFrameStateNode>(_frameNode);
|
||||
if (!frameNode){
|
||||
RETURN_DAP_ERROR(StringFormat("Could not find frameId %d", frameId).c_str());
|
||||
}
|
||||
auto frameNodePath = m_runtimeState->GetPathById(frameNode->GetId());
|
||||
if(frameNodePath.empty()){
|
||||
RETURN_DAP_ERROR(StringFormat("Could not find frameId %d", frameId).c_str());
|
||||
}
|
||||
// try locals first
|
||||
std::string localsPath = StringFormat("%s.%s", frameNodePath.c_str(), StackFrameStateNode::LOCAL_SCOPE_NAME);
|
||||
std::string path;
|
||||
int64_t funcStartingLineNumber = -1;
|
||||
|
||||
auto stackFrame = frameNode->GetStackFrame();
|
||||
if (stackFrame && !IsFunctionNative(stackFrame->Func)){
|
||||
auto vmscriptfunc = GetVMScriptFunction(stackFrame->Func);
|
||||
if (vmscriptfunc){
|
||||
funcStartingLineNumber = vmscriptfunc->PCToLine(vmscriptfunc->Code);
|
||||
if (evalLineNumber == -1){
|
||||
evalLineNumber = vmscriptfunc->PCToLine(stackFrame->PC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<LocalScopeStateNode> localScope;
|
||||
{
|
||||
std::shared_ptr<StateNodeBase> _localScopeNodeBase;
|
||||
if (m_runtimeState->ResolveStateByPath(localsPath, _localScopeNodeBase, true)){
|
||||
localScope = std::dynamic_pointer_cast<LocalScopeStateNode>(_localScopeNodeBase);
|
||||
}
|
||||
}
|
||||
std::vector<std::string> localChildrenNames;
|
||||
if (localScope){
|
||||
localScope->GetChildNames(localChildrenNames);
|
||||
caseless_path_set localChildrenNamesSet(localChildrenNames.begin(), localChildrenNames.end());
|
||||
|
||||
path = StringFormat("%s.%s", localsPath.c_str(), request.expression.c_str());
|
||||
if(!TryPath(path)){
|
||||
RETURN_DAP_ERROR(StringFormat("Could not serialize variable %s", request.expression.c_str()).c_str());
|
||||
}
|
||||
|
||||
if (!found && evalLineNumber > -1 && funcStartingLineNumber <= evalLineNumber && request.expression.find(" @") == std::string::npos){
|
||||
// try @ line number`
|
||||
std::string qualName;
|
||||
for (auto &childName : localChildrenNames) {
|
||||
// check if the child name contains an @ and starts with the expression
|
||||
if (childName.find(" @") != std::string::npos && CaseInsensitiveFind(childName, request.expression) == 0){
|
||||
// get the line number from the child name
|
||||
auto lineNum = LocalScopeStateNode::GetLineFromLineQualifiedName(childName);
|
||||
if (lineNum <= evalLineNumber){
|
||||
qualName = childName;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!qualName.empty()){
|
||||
path = StringFormat("%s.%s", localsPath.c_str(), qualName.c_str());
|
||||
if(!TryPath(path)){
|
||||
RETURN_DAP_ERROR(StringFormat("Could not serialize variable %s", request.expression.c_str()).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found && request.expression.find("self.") != 0){
|
||||
// if the first part isn't self, and the current function on the stack is an action or method, try "locals.self"
|
||||
if (stackFrame && (IsFunctionHasSelf(stackFrame->Func))){
|
||||
path = StringFormat("%s.self.%s", localsPath.c_str(), request.expression.c_str());
|
||||
if(!TryPath(path)){
|
||||
RETURN_DAP_ERROR(StringFormat("Could not serialize variable %s", request.expression.c_str()).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// try globals next
|
||||
if (!found && !TryPath(StringFormat("%s.%s.%s", frameNodePath.c_str(), StackFrameStateNode::GLOBALS_SCOPE_NAME, request.expression.c_str()))){
|
||||
RETURN_DAP_ERROR(StringFormat("Could not serialize variable %s", request.expression.c_str()).c_str());
|
||||
}
|
||||
// cvars?
|
||||
if (!found && context != "variables" && !TryPath(StringFormat("%s.%s.%s", frameNodePath.c_str(), StackFrameStateNode::CVAR_SCOPE_NAME, request.expression.c_str()))){
|
||||
RETURN_DAP_ERROR(StringFormat("Could not serialize variable %s", request.expression.c_str()).c_str());
|
||||
}
|
||||
}
|
||||
if (found){
|
||||
response.result = variable.value;
|
||||
response.variablesReference = variable.variablesReference;
|
||||
response.type = variable.type;
|
||||
response.namedVariables = variable.namedVariables;
|
||||
response.indexedVariables = variable.indexedVariables;
|
||||
response.memoryReference = variable.memoryReference;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
if (context == "repl" && !m_executionManager->IsPaused())
|
||||
{
|
||||
// TODO: This isn't safe to do from the debugger thread, figure out a way to safely dispatch to the main thread later
|
||||
#if 0
|
||||
// we don't support repl commands when not paused
|
||||
auto args = Split(request.expression, " ");
|
||||
if (args.size() == 0){
|
||||
return dap::Error(StringFormat("No command provided").c_str());
|
||||
}
|
||||
auto cmdstr = args.front();
|
||||
FConsoleCommand *cmd = FConsoleCommand::FindByName(args.front().c_str());
|
||||
if (cmd)
|
||||
{
|
||||
bool unsafe = false;
|
||||
if (cmd->IsAlias())
|
||||
{
|
||||
FUnsafeConsoleAlias *alias = dynamic_cast<FUnsafeConsoleAlias *>(cmd);
|
||||
unsafe = alias != nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
FUnsafeConsoleCommand *unsafeCmd = dynamic_cast<FUnsafeConsoleCommand *>(cmd);
|
||||
unsafe = unsafeCmd != nullptr;
|
||||
}
|
||||
if (unsafe)
|
||||
{
|
||||
return dap::Error(StringFormat("Cannot run command %s from debugger (unsafe context)!", cmdstr.c_str()).c_str());
|
||||
}
|
||||
|
||||
FCommandLine cmdline(request.expression.c_str());
|
||||
cmd->Run(cmdline, 0);
|
||||
// there's no response for this; the output will be in the debug console buffer
|
||||
return response;
|
||||
}
|
||||
|
||||
// try a c_var?
|
||||
auto cvar = FindConsoleVariable(cmdstr);
|
||||
if (cvar){
|
||||
|
||||
if (args.size() > 1){
|
||||
if (cmdstr == "vm_debug" || cmdstr == "vm_debug_port"){
|
||||
return dap::Error(StringFormat("Refusing change %s while debugging!", cmdstr.c_str()).c_str());
|
||||
}
|
||||
if (cvar->GetFlags() & CVAR_UNSAFECONTEXT) {
|
||||
return dap::Error(StringFormat("Cannot change cvar %s from debugger!", cmdstr.c_str()).c_str());
|
||||
}
|
||||
auto val = request.expression.substr(request.expression.find_first_of(" ") + 1);
|
||||
// trim whitespace
|
||||
while (val[0] == ' '){
|
||||
val = val.substr(1);
|
||||
}
|
||||
while (val[val.size() - 1] == ' '){
|
||||
val = val.substr(0, val.size() - 1);
|
||||
}
|
||||
cvar->SetGenericRep(val.c_str(), CVAR_String);
|
||||
} else {
|
||||
auto var = CVarStateNode::ToVariable(cvar);
|
||||
response.result = var.value;
|
||||
response.type = var.type;
|
||||
response.namedVariables = var.namedVariables;
|
||||
response.indexedVariables = var.indexedVariables;
|
||||
response.presentationHint = var.presentationHint;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
return dap::Error(StringFormat("Command %s not found!", request.expression.c_str()).c_str());
|
||||
#endif
|
||||
return dap::Error("Cannot run evaluate while running!");
|
||||
}
|
||||
return dap::Error(StringFormat("Could not evaluate expression %s", request.expression.c_str()).c_str());
|
||||
|
||||
}
|
||||
|
||||
} // namespace DebugServer
|
||||
Loading…
Add table
Add a link
Reference in a new issue