#define JSON_USE_IMPLICIT_CONVERSIONS 0 #include "debugger/frontend.h" #include #include #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 #include #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(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(); 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(); 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()) { 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(); if (args["stopOnEntry"].is_boolean() && args["stopOnEntry"].get()) 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() : 0; int count = count_.is_number() ? count_.get() : MAX_VAR; int var_ref = args["variablesReference"].get(); if (var_ref == VREF_GLOBALS) { resp["success"] = true; Json& variables = resp["body"]["variables"]; int end = std::min(start + count, static_cast(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()) { case VREF_GLOBALS: { int var = symbols.lookup_variable(args["name"].get().c_str()); if (var < 0) { resp["success"] = false; resp["message"] = "Invalid variable name"; return; } int parsed_value; if (sscanf(args["value"].get().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().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(); 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().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(); 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 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 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