mirror of
https://github.com/kichikuou/system3-sdl2.git
synced 2026-09-29 02:18:09 +03:00
Centralize the differences between SDL2 and SDL3 behind compatibility interfaces and select the appropriate implementations with the SYSTEM3_SDL_VERSION CMake option. Android remains on SDL2 for now.
553 lines
14 KiB
C++
553 lines
14 KiB
C++
#define JSON_USE_IMPLICIT_CONVERSIONS 0
|
|
|
|
#include "debugger/frontend.h"
|
|
#include <algorithm>
|
|
#include <queue>
|
|
#include "sdl_compat.h"
|
|
#include "nlohmann/json.hpp"
|
|
#include "common.h"
|
|
#include "encoding.h"
|
|
#include "nact.h"
|
|
#include "ags.h"
|
|
#include "debugger/debugger.h"
|
|
#ifdef _WIN32
|
|
#include <fcntl.h>
|
|
#include <io.h>
|
|
#endif
|
|
using Json = nlohmann::json;
|
|
|
|
namespace debugger {
|
|
|
|
namespace {
|
|
|
|
enum VariablesReference {
|
|
VREF_GLOBALS = 1,
|
|
VREF_STRINGS,
|
|
};
|
|
|
|
void post_debugger_command(void *data) {
|
|
SDL_Event event = {};
|
|
event.user.type = sdl_custom_event_type;
|
|
event.user.code = DEBUGGER_COMMAND;
|
|
event.user.data1 = data;
|
|
SDL_PushEvent(&event);
|
|
}
|
|
|
|
int read_command_thread(void*) {
|
|
int content_length = -1;
|
|
char header[512];
|
|
while (fgets(header, sizeof(header), stdin)) {
|
|
if (sscanf(header, "Content-Length: %d", &content_length) == 1) {
|
|
continue;
|
|
} else if ((header[0] == '\r' && header[1] == '\n') || header[0] == '\n') {
|
|
if (content_length < 0) {
|
|
fprintf(stderr, "Debug Adapter Protocol error: no Content-Length header\n");
|
|
continue;
|
|
}
|
|
char* buf = (char*)malloc(content_length + 1);
|
|
fread(buf, content_length, 1, stdin);
|
|
buf[content_length] = '\0';
|
|
post_debugger_command(buf);
|
|
content_length = -1;
|
|
} else {
|
|
fprintf(stderr, "Unknown Debug Adapter Protocol header: %s", header);
|
|
}
|
|
}
|
|
post_debugger_command(NULL); // end of messages
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class DapFrontend : public Frontend {
|
|
public:
|
|
DapFrontend(Debugger* backend, const DebugInfo& symbols) : Frontend(backend, symbols) {
|
|
#ifdef _WIN32
|
|
_setmode(_fileno(stdin), _O_BINARY);
|
|
_setmode(_fileno(stdout), _O_BINARY);
|
|
#endif
|
|
SDL_CreateThread(read_command_thread, "Debugger", NULL);
|
|
}
|
|
|
|
void init() override {
|
|
while (!initialized && !g_nact->is_terminating()) {
|
|
g_nact->process_next_event();
|
|
while (!queue.empty()) {
|
|
char* msg = queue.front();
|
|
queue.pop();
|
|
if (!msg)
|
|
return;
|
|
handle_message(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
void repl(int bp_no) override {
|
|
emit_stopped_event();
|
|
backend->set_state(State::RUNNING);
|
|
|
|
bool continue_repl = true;
|
|
while (continue_repl && !g_nact->is_terminating()) {
|
|
g_nact->process_next_event();
|
|
while (!queue.empty()) {
|
|
char* msg = queue.front();
|
|
queue.pop();
|
|
if (!msg)
|
|
return;
|
|
continue_repl = handle_message(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
void on_command(void* data) override {
|
|
queue.push(static_cast<char*>(data));
|
|
}
|
|
|
|
void on_sleep() override {
|
|
while (!queue.empty()) {
|
|
char* msg = queue.front();
|
|
queue.pop();
|
|
if (!msg)
|
|
return;
|
|
handle_message(msg);
|
|
}
|
|
if (backend->get_state() == State::STOPPED_INTERRUPT || backend->get_state() == State::STOPPED_EXCEPTION)
|
|
backend->repl(0);
|
|
}
|
|
|
|
void on_palette_change() override {
|
|
Json event = {
|
|
{"type", "event"},
|
|
{"event", "xsystem35.paletteChanged"},
|
|
{"body", {
|
|
{"version", ++palette_version}
|
|
}}
|
|
};
|
|
send_json(event);
|
|
}
|
|
|
|
bool console_output(const char* format, va_list ap) override {
|
|
char buf[1024];
|
|
vsnprintf(buf, sizeof(buf), format, ap);
|
|
|
|
Json json = {
|
|
{"type", "event"},
|
|
{"event", "output"},
|
|
{"body", {
|
|
{"output", buf},
|
|
}}
|
|
};
|
|
const char *src = symbols.page2src(g_nact->sco.page());
|
|
if (src)
|
|
json["body"]["source"] = create_source(src);
|
|
int line = symbols.addr2line(g_nact->sco.page(), g_nact->sco.cmd_addr());
|
|
if (line >= 0)
|
|
json["body"]["line"] = line;
|
|
|
|
send_json(json);
|
|
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
Json create_source(const char *name) {
|
|
Json source;
|
|
source["name"] = name;
|
|
if (!src_dir.empty())
|
|
source["path"] = src_dir + "/" + name;
|
|
source["sourceReference"] = 0;
|
|
return source;
|
|
}
|
|
|
|
std::string format_string_value(const char* str) {
|
|
return "\"" + g_nact->encoding->toUtf8(str) + "\"";
|
|
}
|
|
|
|
void send_json(Json& json) {
|
|
json["seq"] = ++seq_;
|
|
std::string str = json.dump();
|
|
printf("Content-Length: %zu\r\n\r\n%s", str.size(), str.c_str());
|
|
fflush(stdout);
|
|
}
|
|
|
|
void emit_initialized_event() {
|
|
Json json = {
|
|
{"type", "event"},
|
|
{"event", "initialized"},
|
|
};
|
|
send_json(json);
|
|
}
|
|
|
|
void emit_stopped_event() {
|
|
std::string reason;
|
|
switch (backend->get_state()) {
|
|
case State::STOPPED_ENTRY: reason = "entry"; break;
|
|
case State::STOPPED_STEP: reason = "step"; break;
|
|
case State::STOPPED_NEXT: reason = "step"; break;
|
|
case State::STOPPED_BREAKPOINT: reason = "breakpoint"; break;
|
|
case State::STOPPED_INTERRUPT: reason = "pause"; break;
|
|
case State::STOPPED_EXCEPTION: reason = "exception"; break;
|
|
default: reason = "unknown"; break;
|
|
}
|
|
Json json = {
|
|
{"type", "event"},
|
|
{"event", "stopped"},
|
|
{"body", {
|
|
{"reason", reason},
|
|
{"allThreadsStopped", true},
|
|
}}
|
|
};
|
|
send_json(json);
|
|
}
|
|
|
|
bool handle_message(char* msg) {
|
|
Json json = Json::parse(msg);
|
|
auto type = json["type"].get<std::string>();
|
|
bool continue_repl = true;
|
|
if (type == "request")
|
|
continue_repl = handle_request(json);
|
|
free(msg);
|
|
return continue_repl;
|
|
}
|
|
|
|
bool handle_request(Json& request) {
|
|
Json resp;
|
|
resp["type"] = "response";
|
|
resp["request_seq"] = request["seq"];
|
|
resp["command"] = request["command"];
|
|
auto command = request["command"].get<std::string>();
|
|
Json& args = request["arguments"];
|
|
|
|
bool continue_repl = true;
|
|
if (command == "initialize") {
|
|
cmd_initialize(args, resp);
|
|
} else if (command == "disconnect") {
|
|
cmd_disconnect(args, resp);
|
|
continue_repl = false;
|
|
} else if (command == "launch") {
|
|
cmd_launch(args, resp);
|
|
} else if (command == "configurationDone") {
|
|
cmd_configurationDone(args, resp);
|
|
} else if (command == "threads") {
|
|
cmd_threads(args, resp);
|
|
} else if (command == "scopes") {
|
|
cmd_scopes(args, resp);
|
|
} else if (command == "variables") {
|
|
cmd_variables(args, resp);
|
|
} else if (command == "setVariable") {
|
|
cmd_setVariable(args, resp);
|
|
} else if (command == "stackTrace") {
|
|
cmd_stackTrace(args, resp);
|
|
} else if (command == "evaluate") {
|
|
cmd_evaluate(args, resp);
|
|
} else if (command == "setBreakpoints") {
|
|
cmd_setBreakpoints(args, resp);
|
|
} else if (command == "continue") {
|
|
cmd_continue(args, resp);
|
|
continue_repl = false;
|
|
} else if (command == "pause") {
|
|
cmd_pause(args, resp);
|
|
} else if (command == "stepIn") {
|
|
cmd_stepIn(args, resp);
|
|
continue_repl = false;
|
|
} else if (command == "stepOut") {
|
|
cmd_stepOut(args, resp);
|
|
continue_repl = false;
|
|
} else if (command == "next") {
|
|
cmd_next(args, resp);
|
|
continue_repl = false;
|
|
} else if (command == "xsystem35.palette") {
|
|
cmd_palette(args, resp);
|
|
} else {
|
|
fprintf(stderr, "Unknown command '%s'\n", command.c_str());
|
|
resp["success"] = false;
|
|
resp["message"] = "Unknown command " + command;
|
|
}
|
|
send_json(resp);
|
|
return continue_repl;
|
|
}
|
|
|
|
void cmd_initialize(Json& args, Json& resp) {
|
|
resp["success"] = true;
|
|
resp["body"] = {
|
|
{"supportsConfigurationDoneRequest", true},
|
|
{"supportsEvaluateForHovers", true},
|
|
{"supportsSetVariable", true},
|
|
};
|
|
}
|
|
|
|
void cmd_disconnect(Json& args, Json& resp) {
|
|
resp["success"] = true;
|
|
g_nact->quit(0);
|
|
}
|
|
|
|
void cmd_launch(Json& args, Json& resp) {
|
|
if (args["noDebug"].is_boolean() && args["noDebug"].get<bool>()) {
|
|
resp["success"] = true;
|
|
initialized = true;
|
|
return;
|
|
}
|
|
|
|
if (!symbols.loaded()) {
|
|
resp["success"] = false;
|
|
resp["message"] = "system3: Cannot load debug symbols";
|
|
return;
|
|
}
|
|
|
|
if (args["srcDir"].is_string())
|
|
src_dir = args["srcDir"].get<std::string>();
|
|
if (args["stopOnEntry"].is_boolean() && args["stopOnEntry"].get<bool>())
|
|
backend->set_state(State::STOPPED_ENTRY);
|
|
resp["success"] = true;
|
|
|
|
emit_initialized_event();
|
|
}
|
|
|
|
void cmd_configurationDone(Json& args, Json& resp) {
|
|
initialized = true;
|
|
resp["success"] = true;
|
|
}
|
|
|
|
void cmd_threads(Json& args, Json& resp) {
|
|
resp["success"] = true;
|
|
resp["body"] = {
|
|
{"threads", {
|
|
{{"id", 1}, {"name", "main thread"}},
|
|
}}
|
|
};
|
|
}
|
|
|
|
void cmd_scopes(Json& args, Json& resp) {
|
|
resp["success"] = true;
|
|
resp["body"] = {
|
|
{"scopes", {
|
|
{
|
|
{"name", "All Variables"},
|
|
{"variablesReference", VREF_GLOBALS},
|
|
{"namedVariables", symbols.num_variables()},
|
|
{"expensive", false}
|
|
},
|
|
{
|
|
{"name", "Strings"},
|
|
{"variablesReference", VREF_STRINGS},
|
|
{"indexedVariables", MAX_STRVAR},
|
|
{"expensive", false}
|
|
},
|
|
}}
|
|
};
|
|
}
|
|
|
|
void cmd_variables(Json& args, Json& resp) {
|
|
const Json& start_ = args["start"];
|
|
const Json& count_ = args["count"];
|
|
int start = start_.is_number() ? start_.get<int>() : 0;
|
|
int count = count_.is_number() ? count_.get<int>() : MAX_VAR;
|
|
|
|
int var_ref = args["variablesReference"].get<int>();
|
|
if (var_ref == VREF_GLOBALS) {
|
|
resp["success"] = true;
|
|
Json& variables = resp["body"]["variables"];
|
|
int end = std::min(start + count, static_cast<int>(symbols.num_variables()));
|
|
for (int i = start; i < end; i++) {
|
|
Json item = {
|
|
{"name", symbols.variable_name(i)},
|
|
{"value", std::to_string(g_nact->get_var(i))},
|
|
{"variablesReference", 0},
|
|
};
|
|
variables.push_back(std::move(item));
|
|
}
|
|
} else if (var_ref == VREF_STRINGS) {
|
|
resp["success"] = true;
|
|
Json& variables = resp["body"]["variables"];
|
|
int end = std::min(start + count, MAX_STRVAR);
|
|
for (int i = start; i < end; i++) {
|
|
char name[10];
|
|
snprintf(name, sizeof(name), "[%d]", i + 1);
|
|
Json item = {
|
|
{"name", name},
|
|
{"value", format_string_value(g_nact->get_string(i))},
|
|
{"variablesReference", 0},
|
|
};
|
|
variables.push_back(std::move(item));
|
|
}
|
|
} else {
|
|
resp["success"] = false;
|
|
resp["message"] = "Invalid variables reference";
|
|
}
|
|
}
|
|
|
|
void cmd_setVariable(Json& args, Json& resp) {
|
|
switch (args["variablesReference"].get<int>()) {
|
|
case VREF_GLOBALS:
|
|
{
|
|
int var = symbols.lookup_variable(args["name"].get<std::string>().c_str());
|
|
if (var < 0) {
|
|
resp["success"] = false;
|
|
resp["message"] = "Invalid variable name";
|
|
return;
|
|
}
|
|
int parsed_value;
|
|
if (sscanf(args["value"].get<std::string>().c_str(), "%i", &parsed_value) != 1) {
|
|
resp["success"] = false;
|
|
resp["message"] = "Syntax error";
|
|
return;
|
|
}
|
|
g_nact->set_var(var, parsed_value);
|
|
resp["success"] = true;
|
|
resp["body"] = {
|
|
{"value", std::to_string(g_nact->get_var(var))},
|
|
};
|
|
}
|
|
break;
|
|
case VREF_STRINGS:
|
|
{
|
|
int index;
|
|
if (sscanf(args["name"].get<std::string>().c_str(), "[%d]", &index) != 1 || index <= 0 || index > MAX_STRVAR) {
|
|
resp["success"] = false;
|
|
resp["message"] = "Invalid string index";
|
|
return;
|
|
}
|
|
index--; // 1-based to 0-based
|
|
|
|
std::string value = args["value"].get<std::string>();
|
|
if (value.size() < 2 || value[0] != '"' || value.back() != '"') {
|
|
resp["success"] = false;
|
|
resp["message"] = "Syntax error";
|
|
return;
|
|
}
|
|
value = value.substr(1, value.size() - 2);
|
|
|
|
g_nact->set_string(index, g_nact->encoding->fromUtf8(value));
|
|
|
|
resp["success"] = true;
|
|
resp["body"] = {
|
|
{"value", format_string_value(g_nact->get_string(index))},
|
|
};
|
|
}
|
|
break;
|
|
default:
|
|
resp["success"] = false;
|
|
resp["message"] = "Invalid variables reference";
|
|
break;
|
|
}
|
|
}
|
|
|
|
void cmd_stackTrace(Json& args, Json& resp) {
|
|
resp["success"] = true;
|
|
Json& body = resp["body"];
|
|
Json& stackFrames = body["stackFrames"];
|
|
int i = 0;
|
|
for (const StackFrame &frame : backend->stack_trace()) {
|
|
Json& item = stackFrames[i++];
|
|
item["id"] = i;
|
|
item["name"] = frame.src;
|
|
item["source"] = create_source(frame.src);
|
|
item["line"] = frame.line;
|
|
item["column"] = 0;
|
|
}
|
|
body["totalFrames"] = i;
|
|
}
|
|
|
|
void cmd_evaluate(Json& args, Json& resp) {
|
|
int var = symbols.lookup_variable(args["expression"].get<std::string>().c_str());
|
|
if (var < 0) {
|
|
resp["success"] = false;
|
|
resp["message"] = "Invalid expression";
|
|
return;
|
|
}
|
|
resp["success"] = true;
|
|
resp["body"] = {
|
|
{"result", std::to_string(g_nact->get_var(var))},
|
|
{"variablesReference", 0},
|
|
};
|
|
}
|
|
|
|
void cmd_setBreakpoints(Json& args, Json& resp) {
|
|
const auto filename = args["source"]["name"].get<std::string>();
|
|
int page = symbols.src2page(filename.c_str());
|
|
|
|
backend->delete_breakpoints_in_page(page);
|
|
|
|
resp["success"] = true;
|
|
Json& body = resp["body"];
|
|
Json& out_bps = body["breakpoints"];
|
|
|
|
for (const Json& srcbp : args["breakpoints"]) {
|
|
Json& item = out_bps.emplace_back();
|
|
|
|
int line = srcbp["line"].get<int>();
|
|
int addr = symbols.line2addr(page, line);
|
|
if (page < 0) {
|
|
item["verified"] = false;
|
|
item["message"] = "no source file named " + filename;
|
|
continue;
|
|
}
|
|
if (addr < 0) {
|
|
item["verified"] = false;
|
|
item["message"] = "no line " + std::to_string(line) + " in file " + filename;
|
|
continue;
|
|
}
|
|
int bp_no = backend->set_breakpoint(page, addr, false);
|
|
if (bp_no < 0) {
|
|
char message[256];
|
|
snprintf(message, sizeof(message), "failed to set breakpoint at %d:0x%x", page, addr);
|
|
item["verified"] = false;
|
|
item["message"] = message;
|
|
continue;
|
|
}
|
|
|
|
line = symbols.addr2line(page, addr);
|
|
item["id"] = bp_no;
|
|
item["verified"] = true;
|
|
item["source"] = create_source(filename.c_str());
|
|
item["line"] = line;
|
|
}
|
|
}
|
|
|
|
void cmd_continue(Json& args, Json& resp) {
|
|
// TODO: sdl_raiseWindow();
|
|
resp["success"] = true;
|
|
}
|
|
|
|
void cmd_pause(Json& args, Json& resp) {
|
|
backend->set_state(State::STOPPED_INTERRUPT);
|
|
resp["success"] = true;
|
|
}
|
|
|
|
void cmd_stepIn(Json& args, Json& resp) {
|
|
backend->stepin();
|
|
resp["success"] = true;
|
|
}
|
|
|
|
void cmd_stepOut(Json& args, Json& resp) {
|
|
backend->stepout();
|
|
resp["success"] = true;
|
|
}
|
|
|
|
void cmd_next(Json& args, Json& resp) {
|
|
backend->next();
|
|
resp["success"] = true;
|
|
}
|
|
|
|
void cmd_palette(Json& args, Json& resp) {
|
|
resp["success"] = true;
|
|
resp["body"] = {
|
|
{"version", palette_version},
|
|
{"palette", g_nact->ags->get_screen_palette()},
|
|
};
|
|
}
|
|
|
|
std::queue<char*> queue;
|
|
bool initialized = false;
|
|
int seq_ = 0;
|
|
std::string src_dir;
|
|
unsigned palette_version = 0;
|
|
};
|
|
|
|
// static
|
|
Frontend* Frontend::create_dap(Debugger* backend, const DebugInfo& symbols) {
|
|
return new DapFrontend(backend, symbols);
|
|
}
|
|
|
|
} // namespace debugger
|