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:
Nikita 2025-07-17 20:01:18 -07:00 committed by GitHub
commit 45ac7c1d9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
235 changed files with 83166 additions and 62 deletions

View file

@ -0,0 +1,337 @@
// 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.
#include "socket.h"
#include "rwmutex.h"
#if defined(_WIN32)
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
#if defined(_WIN32)
#include <atomic>
namespace {
std::atomic<int> wsaInitCount = {0};
} // anonymous namespace
#else
#include <fcntl.h>
#include <unistd.h>
namespace {
using SOCKET = int;
} // anonymous namespace
#endif
namespace {
constexpr SOCKET InvalidSocket = static_cast<SOCKET>(-1);
void init() {
#if defined(_WIN32)
if (wsaInitCount++ == 0) {
WSADATA winsockData;
(void)WSAStartup(MAKEWORD(2, 2), &winsockData);
}
#endif
}
void term() {
#if defined(_WIN32)
if (--wsaInitCount == 0) {
WSACleanup();
}
#endif
}
bool setBlocking(SOCKET s, bool blocking) {
#if defined(_WIN32)
u_long mode = blocking ? 0 : 1;
return ioctlsocket(s, FIONBIO, &mode) == NO_ERROR;
#else
auto arg = fcntl(s, F_GETFL, nullptr);
if (arg < 0) {
return false;
}
arg = blocking ? (arg & ~O_NONBLOCK) : (arg | O_NONBLOCK);
return fcntl(s, F_SETFL, arg) >= 0;
#endif
}
bool errored(SOCKET s) {
if (s == InvalidSocket) {
return true;
}
char error = 0;
socklen_t len = sizeof(error);
getsockopt(s, SOL_SOCKET, SO_ERROR, &error, &len);
return error != 0;
}
} // anonymous namespace
class dap::Socket::Shared : public dap::ReaderWriter {
public:
static std::shared_ptr<Shared> create(const char* address, const char* port) {
init();
addrinfo hints = {};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
addrinfo* info = nullptr;
getaddrinfo(address, port, &hints, &info);
if (info) {
auto socket =
::socket(info->ai_family, info->ai_socktype, info->ai_protocol);
auto out = std::make_shared<Shared>(info, socket);
out->setOptions();
return out;
}
term();
return nullptr;
}
Shared(SOCKET socket) : info(nullptr), s(socket) {}
Shared(addrinfo* info, SOCKET socket) : info(info), s(socket) {}
~Shared() {
if (info) {
freeaddrinfo(info);
}
close();
term();
}
template <typename FUNCTION>
void lock(FUNCTION&& f) {
RLock l(mutex);
f(s, info);
}
void setOptions() {
RLock l(mutex);
if (s == InvalidSocket) {
return;
}
int enable = 1;
#if !defined(_WIN32)
// Prevent sockets lingering after process termination, causing
// reconnection issues on the same port.
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(enable));
struct {
int l_onoff; /* linger active */
int l_linger; /* how many seconds to linger for */
} linger = {false, 0};
setsockopt(s, SOL_SOCKET, SO_LINGER, (char*)&linger, sizeof(linger));
#endif // !defined(_WIN32)
// Enable TCP_NODELAY.
// DAP usually consists of small packet requests, with small packet
// responses. When there are many frequent, blocking requests made,
// Nagle's algorithm can dramatically limit the request->response rates.
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, (char*)&enable, sizeof(enable));
}
// dap::ReaderWriter compliance
bool isOpen() {
{
RLock l(mutex);
if ((s != InvalidSocket) && !errored(s)) {
return true;
}
}
WLock lock(mutex);
s = InvalidSocket;
return false;
}
void close() {
{
RLock l(mutex);
if (s != InvalidSocket) {
#if defined(_WIN32)
closesocket(s);
#elif __APPLE__
// ::shutdown() *should* be sufficient to unblock ::accept(), but
// apparently on macos it can return ENOTCONN and ::accept() continues
// to block indefinitely.
// Note: There is a race here. Calling ::close() frees the socket ID,
// which may be reused before `s` is assigned InvalidSocket.
::shutdown(s, SHUT_RDWR);
::close(s);
#else
// ::shutdown() to unblock ::accept(). We'll actually close the socket
// under lock below.
::shutdown(s, SHUT_RDWR);
#endif
}
}
WLock l(mutex);
if (s != InvalidSocket) {
#if !defined(_WIN32) && !defined(__APPLE__)
::close(s);
#endif
s = InvalidSocket;
}
}
size_t read(void* buffer, size_t bytes) {
RLock lock(mutex);
if (s == InvalidSocket) {
return 0;
}
auto len =
recv(s, reinterpret_cast<char*>(buffer), static_cast<int>(bytes), 0);
return (len < 0) ? 0 : len;
}
bool write(const void* buffer, size_t bytes) {
RLock lock(mutex);
if (s == InvalidSocket) {
return false;
}
if (bytes == 0) {
return true;
}
return ::send(s, reinterpret_cast<const char*>(buffer),
static_cast<int>(bytes), 0) > 0;
}
private:
addrinfo* const info;
SOCKET s = InvalidSocket;
RWMutex mutex;
};
namespace dap {
Socket::Socket(const char* address, const char* port)
: shared(Shared::create(address, port)) {
if (shared) {
bool reset = false;
shared->lock([&](SOCKET socket, const addrinfo* info) {
if (bind(socket, info->ai_addr, (int)info->ai_addrlen) != 0) {
reset = true;
return;
}
if (listen(socket, 0) != 0) {
reset = true;
}
});
if (reset) {
shared.reset();
}
}
}
std::shared_ptr<ReaderWriter> Socket::accept() const {
std::shared_ptr<Shared> out;
if (shared) {
shared->lock([&](SOCKET socket, const addrinfo*) {
if (socket != InvalidSocket && !errored(socket)) {
init();
auto accepted = ::accept(socket, 0, 0);
if (accepted != InvalidSocket) {
out = std::make_shared<Shared>(accepted);
out->setOptions();
}
}
});
}
return out;
}
bool Socket::isOpen() const {
if (shared) {
return shared->isOpen();
}
return false;
}
void Socket::close() const {
if (shared) {
shared->close();
}
}
std::shared_ptr<ReaderWriter> Socket::connect(const char* address,
const char* port,
uint32_t timeoutMillis) {
auto shared = Shared::create(address, port);
if (!shared) {
return nullptr;
}
std::shared_ptr<ReaderWriter> out;
shared->lock([&](SOCKET socket, const addrinfo* info) {
if (socket == InvalidSocket) {
return;
}
if (timeoutMillis == 0) {
if (::connect(socket, info->ai_addr, (int)info->ai_addrlen) == 0) {
out = shared;
}
return;
}
if (!setBlocking(socket, false)) {
return;
}
auto res = ::connect(socket, info->ai_addr, (int)info->ai_addrlen);
if (res == 0) {
if (setBlocking(socket, true)) {
out = shared;
}
} else {
const auto microseconds = timeoutMillis * 1000;
fd_set fdset;
FD_ZERO(&fdset);
FD_SET(socket, &fdset);
timeval tv;
tv.tv_sec = microseconds / 1000000;
tv.tv_usec = microseconds - static_cast<uint32_t>(tv.tv_sec * 1000000);
res = select(static_cast<int>(socket + 1), nullptr, &fdset, nullptr, &tv);
if (res > 0 && !errored(socket) && setBlocking(socket, true)) {
out = shared;
}
}
});
if (!out) {
return nullptr;
}
return out->isOpen() ? out : nullptr;
}
} // namespace dap