From 7bccd5487e83d66351c8b8cd17ab1b6ce719df09 Mon Sep 17 00:00:00 2001 From: Ev1lbl0w Date: Fri, 4 Jun 2021 19:39:38 +0100 Subject: [PATCH] Implemented initial DAP support Implemented "output" event Refactored "seq" field generation Prevent debugging when editor and client are in different projects Removed unneeded references to peer on the parser Refactored way to detect project path Implemented "setBreakpoints" request Fix double events when terminating from client Refactored "stopped" event Implemented "stopped" with breakpoint event Implemented "stackTrace", "scopes" and "variables" request Report incoming number of stack dump variables Implemented proper reporting of scopes and variables from stack frames Prevent editor from grabbing focus when a DAP session is active Implemented "next" and "stepIn" requests Implemented "Source" checksum computing Switched expected errors from macros to silent guards Refactored message_id Respect client settings regarding lines/columns behavior Refactored nested DAP fields Implement reporting of "Members" and "Globals" scopes as well Fix error messages not being shown, and improved wrong path message --- core/debugger/remote_debugger.cpp | 7 +- editor/debugger/SCsub | 2 + editor/debugger/debug_adapter/SCsub | 5 + .../debug_adapter/debug_adapter_parser.cpp | 425 +++++++++++++++ .../debug_adapter/debug_adapter_parser.h | 88 ++++ .../debug_adapter/debug_adapter_protocol.cpp | 497 ++++++++++++++++++ .../debug_adapter/debug_adapter_protocol.h | 140 +++++ .../debug_adapter/debug_adapter_server.cpp | 102 ++++ .../debug_adapter/debug_adapter_server.h | 59 +++ .../debug_adapter/debug_adapter_types.h | 270 ++++++++++ editor/debugger/editor_debugger_node.cpp | 15 +- editor/debugger/editor_debugger_node.h | 5 +- editor/debugger/script_editor_debugger.cpp | 49 +- editor/debugger/script_editor_debugger.h | 10 + editor/editor_node.cpp | 2 + editor/plugins/editor_debugger_plugin.cpp | 2 +- editor/plugins/editor_debugger_plugin.h | 2 +- .../gdscript_language_protocol.cpp | 4 +- 18 files changed, 1666 insertions(+), 18 deletions(-) create mode 100644 editor/debugger/debug_adapter/SCsub create mode 100644 editor/debugger/debug_adapter/debug_adapter_parser.cpp create mode 100644 editor/debugger/debug_adapter/debug_adapter_parser.h create mode 100644 editor/debugger/debug_adapter/debug_adapter_protocol.cpp create mode 100644 editor/debugger/debug_adapter/debug_adapter_protocol.h create mode 100644 editor/debugger/debug_adapter/debug_adapter_server.cpp create mode 100644 editor/debugger/debug_adapter/debug_adapter_server.h create mode 100644 editor/debugger/debug_adapter/debug_adapter_types.h diff --git a/core/debugger/remote_debugger.cpp b/core/debugger/remote_debugger.cpp index bdbb7766fa..f5595ac037 100644 --- a/core/debugger/remote_debugger.cpp +++ b/core/debugger/remote_debugger.cpp @@ -706,6 +706,8 @@ void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { Array msg; msg.push_back(p_can_continue); msg.push_back(error_str); + ERR_FAIL_COND(!script_lang); + msg.push_back(script_lang->debug_get_stack_level_count() > 0); send_message("debug_enter", msg); servers_profiler->skip_profile_frame = true; // Avoid frame time spike in debug. @@ -754,7 +756,6 @@ void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { break; } else if (command == "get_stack_dump") { - ERR_FAIL_COND(!script_lang); DebuggerMarshalls::ScriptStackDump dump; int slc = script_lang->debug_get_stack_level_count(); for (int i = 0; i < slc; i++) { @@ -790,7 +791,9 @@ void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { script_lang->debug_get_globals(&globals, &globals_vals); ERR_FAIL_COND(globals.size() != globals_vals.size()); - send_message("stack_frame_vars", Array()); + Array var_size; + var_size.push_back(local_vals.size() + member_vals.size() + globals_vals.size()); + send_message("stack_frame_vars", var_size); _send_stack_vars(locals, local_vals, 0); _send_stack_vars(members, member_vals, 1); _send_stack_vars(globals, globals_vals, 2); diff --git a/editor/debugger/SCsub b/editor/debugger/SCsub index 359d04e5df..99f1c888f0 100644 --- a/editor/debugger/SCsub +++ b/editor/debugger/SCsub @@ -3,3 +3,5 @@ Import("env") env.add_source_files(env.editor_sources, "*.cpp") + +SConscript("debug_adapter/SCsub") diff --git a/editor/debugger/debug_adapter/SCsub b/editor/debugger/debug_adapter/SCsub new file mode 100644 index 0000000000..359d04e5df --- /dev/null +++ b/editor/debugger/debug_adapter/SCsub @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +Import("env") + +env.add_source_files(env.editor_sources, "*.cpp") diff --git a/editor/debugger/debug_adapter/debug_adapter_parser.cpp b/editor/debugger/debug_adapter/debug_adapter_parser.cpp new file mode 100644 index 0000000000..945291b163 --- /dev/null +++ b/editor/debugger/debug_adapter/debug_adapter_parser.cpp @@ -0,0 +1,425 @@ +/*************************************************************************/ +/* debug_adapter_parser.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "debug_adapter_parser.h" + +#include "editor/debugger/editor_debugger_node.h" +#include "editor/debugger/script_editor_debugger.h" +#include "editor/editor_node.h" + +void DebugAdapterParser::_bind_methods() { + // Requests + ClassDB::bind_method(D_METHOD("req_initialize", "params"), &DebugAdapterParser::req_initialize); + ClassDB::bind_method(D_METHOD("req_disconnect", "params"), &DebugAdapterParser::prepare_success_response); + ClassDB::bind_method(D_METHOD("req_launch", "params"), &DebugAdapterParser::req_launch); + ClassDB::bind_method(D_METHOD("req_terminate", "params"), &DebugAdapterParser::req_terminate); + ClassDB::bind_method(D_METHOD("req_configurationDone", "params"), &DebugAdapterParser::prepare_success_response); + ClassDB::bind_method(D_METHOD("req_pause", "params"), &DebugAdapterParser::req_pause); + ClassDB::bind_method(D_METHOD("req_continue", "params"), &DebugAdapterParser::req_continue); + ClassDB::bind_method(D_METHOD("req_threads", "params"), &DebugAdapterParser::req_threads); + ClassDB::bind_method(D_METHOD("req_stackTrace", "params"), &DebugAdapterParser::req_stackTrace); + ClassDB::bind_method(D_METHOD("req_setBreakpoints", "params"), &DebugAdapterParser::req_setBreakpoints); + ClassDB::bind_method(D_METHOD("req_scopes", "params"), &DebugAdapterParser::req_scopes); + ClassDB::bind_method(D_METHOD("req_variables", "params"), &DebugAdapterParser::req_variables); + ClassDB::bind_method(D_METHOD("req_next", "params"), &DebugAdapterParser::req_next); + ClassDB::bind_method(D_METHOD("req_stepIn", "params"), &DebugAdapterParser::req_stepIn); +} + +Dictionary DebugAdapterParser::prepare_base_event() const { + Dictionary event; + event["type"] = "event"; + + return event; +} + +Dictionary DebugAdapterParser::prepare_success_response(const Dictionary &p_params) const { + Dictionary response; + response["type"] = "response"; + response["request_seq"] = p_params["seq"]; + response["command"] = p_params["command"]; + response["success"] = true; + + return response; +} + +Dictionary DebugAdapterParser::prepare_error_response(const Dictionary &p_params, DAP::ErrorType err_type, const Dictionary &variables) const { + Dictionary response, body; + response["type"] = "response"; + response["request_seq"] = p_params["seq"]; + response["command"] = p_params["command"]; + response["success"] = false; + response["body"] = body; + + DAP::Message message; + String error, error_desc; + switch (err_type) { + case DAP::ErrorType::UNKNOWN: + error = "unknown"; + error_desc = "An unknown error has ocurred when processing the request."; + break; + case DAP::ErrorType::WRONG_PATH: + error = "wrong_path"; + error_desc = "The editor and client are working on different paths; the client is on \"{clientPath}\", but the editor is on \"{editorPath}\""; + } + + message.id = err_type; + message.format = error_desc; + message.variables = variables; + response["message"] = error; + body["error"] = message.to_json(); + + return response; +} + +Dictionary DebugAdapterParser::req_initialize(const Dictionary &p_params) const { + Dictionary response = prepare_success_response(p_params); + Dictionary args = p_params["arguments"]; + + Ref peer = DebugAdapterProtocol::get_singleton()->get_current_peer(); + + peer->linesStartAt1 = args.get("linesStartAt1", false); + peer->columnsStartAt1 = args.get("columnsStartAt1", false); + peer->supportsVariableType = args.get("supportsVariableType", false); + peer->supportsInvalidatedEvent = args.get("supportsInvalidatedEvent", false); + + DAP::Capabilities caps; + response["body"] = caps.to_json(); + + DebugAdapterProtocol::get_singleton()->notify_initialized(); + + return response; +} + +Dictionary DebugAdapterParser::req_launch(const Dictionary &p_params) { + Dictionary args = p_params["arguments"]; + if (args.has("project") && !is_valid_path(args["project"])) { + Dictionary variables; + variables["clientPath"] = args["project"]; + variables["editorPath"] = ProjectSettings::get_singleton()->get_resource_path(); + return prepare_error_response(p_params, DAP::ErrorType::WRONG_PATH, variables); + } + + ScriptEditorDebugger *dbg = EditorDebuggerNode::get_singleton()->get_default_debugger(); + if ((bool)args["noDebug"] != dbg->is_skip_breakpoints()) { + dbg->debug_skip_breakpoints(); + } + + EditorNode::get_singleton()->run_play(); + DebugAdapterProtocol::get_singleton()->notify_process(); + + return prepare_success_response(p_params); +} + +Dictionary DebugAdapterParser::req_terminate(const Dictionary &p_params) const { + EditorNode::get_singleton()->run_stop(); + + return prepare_success_response(p_params); +} + +Dictionary DebugAdapterParser::req_pause(const Dictionary &p_params) const { + EditorNode::get_singleton()->get_pause_button()->set_pressed(true); + EditorDebuggerNode::get_singleton()->_paused(); + + DebugAdapterProtocol::get_singleton()->notify_stopped_paused(); + + return prepare_success_response(p_params); +} + +Dictionary DebugAdapterParser::req_continue(const Dictionary &p_params) const { + EditorNode::get_singleton()->get_pause_button()->set_pressed(false); + EditorDebuggerNode::get_singleton()->_paused(); + + DebugAdapterProtocol::get_singleton()->notify_continued(); + + return prepare_success_response(p_params); +} + +Dictionary DebugAdapterParser::req_threads(const Dictionary &p_params) const { + Dictionary response = prepare_success_response(p_params), body; + response["body"] = body; + + Array arr; + DAP::Thread thread; + + thread.id = 1; // Hardcoded because Godot only supports debugging one thread at the moment + thread.name = "Main"; + arr.push_back(thread.to_json()); + body["threads"] = arr; + + return response; +} + +Dictionary DebugAdapterParser::req_stackTrace(const Dictionary &p_params) const { + if (DebugAdapterProtocol::get_singleton()->_processing_stackdump) { + return Dictionary(); + } + + Dictionary response = prepare_success_response(p_params), body; + response["body"] = body; + + bool lines_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->linesStartAt1; + bool columns_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->columnsStartAt1; + + Array arr; + DebugAdapterProtocol *dap = DebugAdapterProtocol::get_singleton(); + for (Map>::Element *E = dap->stackframe_list.front(); E; E = E->next()) { + DAP::StackFrame sf = E->key(); + if (!lines_at_one) { + sf.line--; + } + if (!columns_at_one) { + sf.column--; + } + + arr.push_back(sf.to_json()); + } + + body["stackFrames"] = arr; + return response; +} + +Dictionary DebugAdapterParser::req_setBreakpoints(const Dictionary &p_params) { + Dictionary response = prepare_success_response(p_params), body; + response["body"] = body; + + Dictionary args = p_params["arguments"]; + DAP::Source source; + source.from_json(args["source"]); + + bool lines_at_one = DebugAdapterProtocol::get_singleton()->get_current_peer()->linesStartAt1; + + if (!is_valid_path(source.path)) { + Dictionary variables; + variables["clientPath"] = source.path; + variables["editorPath"] = ProjectSettings::get_singleton()->get_resource_path(); + return prepare_error_response(p_params, DAP::ErrorType::WRONG_PATH, variables); + } + + Array breakpoints = args["breakpoints"], lines; + for (int i = 0; i < breakpoints.size(); i++) { + DAP::SourceBreakpoint breakpoint; + breakpoint.from_json(breakpoints[i]); + + lines.push_back(breakpoint.line + !lines_at_one); + } + + EditorDebuggerNode::get_singleton()->set_breakpoints(ProjectSettings::get_singleton()->localize_path(source.path), lines); + Array updated_breakpoints = DebugAdapterProtocol::get_singleton()->update_breakpoints(source.path, lines); + body["breakpoints"] = updated_breakpoints; + + return response; +} + +Dictionary DebugAdapterParser::req_scopes(const Dictionary &p_params) { + Dictionary response = prepare_success_response(p_params), body; + response["body"] = body; + + Dictionary args = p_params["arguments"]; + int frame_id = args["frameId"]; + Array scope_list; + + DAP::StackFrame frame; + frame.id = frame_id; + Map>::Element *E = DebugAdapterProtocol::get_singleton()->stackframe_list.find(frame); + if (E) { + ERR_FAIL_COND_V(E->value().size() != 3, prepare_error_response(p_params, DAP::ErrorType::UNKNOWN)); + for (int i = 0; i < 3; i++) { + DAP::Scope scope; + scope.variablesReference = E->value()[i]; + switch (i) { + case 0: + scope.name = "Locals"; + scope.presentationHint = "locals"; + break; + case 1: + scope.name = "Members"; + scope.presentationHint = "members"; + break; + case 2: + scope.name = "Globals"; + scope.presentationHint = "globals"; + } + + scope_list.push_back(scope.to_json()); + } + } + + EditorDebuggerNode::get_singleton()->get_default_debugger()->request_stack_dump(frame_id); + DebugAdapterProtocol::get_singleton()->_current_frame = frame_id; + + body["scopes"] = scope_list; + return response; +} + +Dictionary DebugAdapterParser::req_variables(const Dictionary &p_params) const { + // If _remaining_vars > 0, the debugee is still sending a stack dump to the editor. + if (DebugAdapterProtocol::get_singleton()->_remaining_vars > 0) { + return Dictionary(); + } + + Dictionary response = prepare_success_response(p_params), body; + response["body"] = body; + + Dictionary args = p_params["arguments"]; + int variable_id = args["variablesReference"]; + + Map::Element *E = DebugAdapterProtocol::get_singleton()->variable_list.find(variable_id); + if (E) { + body["variables"] = E ? E->value() : Array(); + return response; + } else { + return Dictionary(); + } +} + +Dictionary DebugAdapterParser::req_next(const Dictionary &p_params) const { + EditorDebuggerNode::get_singleton()->get_default_debugger()->debug_next(); + DebugAdapterProtocol::get_singleton()->_stepping = true; + + return prepare_success_response(p_params); +} + +Dictionary DebugAdapterParser::req_stepIn(const Dictionary &p_params) const { + EditorDebuggerNode::get_singleton()->get_default_debugger()->debug_step(); + DebugAdapterProtocol::get_singleton()->_stepping = true; + + return prepare_success_response(p_params); +} + +Dictionary DebugAdapterParser::ev_initialized() const { + Dictionary event = prepare_base_event(); + event["event"] = "initialized"; + + return event; +} + +Dictionary DebugAdapterParser::ev_process(const String &p_command) const { + Dictionary event = prepare_base_event(), body; + event["event"] = "process"; + event["body"] = body; + + body["name"] = OS::get_singleton()->get_executable_path(); + body["startMethod"] = p_command; + + return event; +} + +Dictionary DebugAdapterParser::ev_terminated() const { + Dictionary event = prepare_base_event(); + event["event"] = "terminated"; + + return event; +} + +Dictionary DebugAdapterParser::ev_exited(const int &p_exitcode) const { + Dictionary event = prepare_base_event(), body; + event["event"] = "exited"; + event["body"] = body; + + body["exitCode"] = p_exitcode; + + return event; +} + +Dictionary DebugAdapterParser::ev_stopped() const { + Dictionary event = prepare_base_event(), body; + event["event"] = "stopped"; + event["body"] = body; + + body["threadId"] = 1; + + return event; +} + +Dictionary DebugAdapterParser::ev_stopped_paused() const { + Dictionary event = ev_stopped(); + Dictionary body = event["body"]; + + body["reason"] = "paused"; + body["description"] = "Paused"; + + return event; +} + +Dictionary DebugAdapterParser::ev_stopped_exception(const String &p_error) const { + Dictionary event = ev_stopped(); + Dictionary body = event["body"]; + + body["reason"] = "exception"; + body["description"] = "Exception"; + body["text"] = p_error; + + return event; +} + +Dictionary DebugAdapterParser::ev_stopped_breakpoint(const int &p_id) const { + Dictionary event = ev_stopped(); + Dictionary body = event["body"]; + + body["reason"] = "breakpoint"; + body["description"] = "Breakpoint"; + + Array breakpoints; + breakpoints.push_back(p_id); + body["hitBreakpointIds"] = breakpoints; + + return event; +} + +Dictionary DebugAdapterParser::ev_stopped_step() const { + Dictionary event = ev_stopped(); + Dictionary body = event["body"]; + + body["reason"] = "step"; + body["description"] = "Breakpoint"; + + return event; +} + +Dictionary DebugAdapterParser::ev_continued() const { + Dictionary event = prepare_base_event(), body; + event["event"] = "continued"; + event["body"] = body; + + body["threadId"] = 1; + + return event; +} + +Dictionary DebugAdapterParser::ev_output(const String &p_message) const { + Dictionary event = prepare_base_event(), body; + event["event"] = "output"; + event["body"] = body; + + body["category"] = "stdout"; + body["output"] = p_message + "\r\n"; + + return event; +} diff --git a/editor/debugger/debug_adapter/debug_adapter_parser.h b/editor/debugger/debug_adapter/debug_adapter_parser.h new file mode 100644 index 0000000000..b86b37d067 --- /dev/null +++ b/editor/debugger/debug_adapter/debug_adapter_parser.h @@ -0,0 +1,88 @@ +/*************************************************************************/ +/* debug_adapter_parser.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef DEBUG_ADAPTER_PARSER_H +#define DEBUG_ADAPTER_PARSER_H + +#include "core/config/project_settings.h" +#include "debug_adapter_protocol.h" +#include "debug_adapter_types.h" + +struct DAPeer; +class DebugAdapterProtocol; + +class DebugAdapterParser : public Object { + GDCLASS(DebugAdapterParser, Object); + +private: + friend DebugAdapterProtocol; + + _FORCE_INLINE_ bool is_valid_path(const String &p_path) { + return p_path.begins_with(ProjectSettings::get_singleton()->get_resource_path()); + } + +protected: + static void _bind_methods(); + + Dictionary prepare_base_event() const; + Dictionary prepare_success_response(const Dictionary &p_params) const; + Dictionary prepare_error_response(const Dictionary &p_params, DAP::ErrorType err_type, const Dictionary &variables = Dictionary()) const; + + Dictionary ev_stopped() const; + +public: + // Requests + Dictionary req_initialize(const Dictionary &p_params) const; + Dictionary req_launch(const Dictionary &p_params); + Dictionary req_terminate(const Dictionary &p_params) const; + Dictionary req_pause(const Dictionary &p_params) const; + Dictionary req_continue(const Dictionary &p_params) const; + Dictionary req_threads(const Dictionary &p_params) const; + Dictionary req_stackTrace(const Dictionary &p_params) const; + Dictionary req_setBreakpoints(const Dictionary &p_params); + Dictionary req_scopes(const Dictionary &p_params); + Dictionary req_variables(const Dictionary &p_params) const; + Dictionary req_next(const Dictionary &p_params) const; + Dictionary req_stepIn(const Dictionary &p_params) const; + + // Events + Dictionary ev_initialized() const; + Dictionary ev_process(const String &p_command) const; + Dictionary ev_terminated() const; + Dictionary ev_exited(const int &p_exitcode) const; + Dictionary ev_stopped_paused() const; + Dictionary ev_stopped_exception(const String &p_error) const; + Dictionary ev_stopped_breakpoint(const int &p_id) const; + Dictionary ev_stopped_step() const; + Dictionary ev_continued() const; + Dictionary ev_output(const String &p_message) const; +}; + +#endif diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.cpp b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp new file mode 100644 index 0000000000..0482271432 --- /dev/null +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.cpp @@ -0,0 +1,497 @@ +/*************************************************************************/ +/* debug_adapter_protocol.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "debug_adapter_protocol.h" + +#include "core/config/project_settings.h" +#include "core/debugger/debugger_marshalls.h" +#include "core/io/json.h" +#include "editor/debugger/script_editor_debugger.h" +#include "editor/doc_tools.h" +#include "editor/editor_log.h" +#include "editor/editor_node.h" + +DebugAdapterProtocol *DebugAdapterProtocol::singleton = nullptr; + +Error DAPeer::handle_data() { + int read = 0; + // Read headers + if (!has_header) { + if (!connection->get_available_bytes()) { + return OK; + } + while (true) { + if (req_pos >= DAP_MAX_BUFFER_SIZE) { + req_pos = 0; + ERR_FAIL_COND_V_MSG(true, ERR_OUT_OF_MEMORY, "Response header too big"); + } + Error err = connection->get_partial_data(&req_buf[req_pos], 1, read); + if (err != OK) { + return FAILED; + } else if (read != 1) { // Busy, wait until next poll + return ERR_BUSY; + } + char *r = (char *)req_buf; + int l = req_pos; + + // End of headers + if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') { + r[l - 3] = '\0'; // Null terminate to read string + String header; + header.parse_utf8(r); + content_length = header.substr(16).to_int(); + has_header = true; + req_pos = 0; + break; + } + req_pos++; + } + } + if (has_header) { + while (req_pos < content_length) { + if (content_length >= DAP_MAX_BUFFER_SIZE) { + req_pos = 0; + has_header = false; + ERR_FAIL_COND_V_MSG(req_pos >= DAP_MAX_BUFFER_SIZE, ERR_OUT_OF_MEMORY, "Response content too big"); + } + Error err = connection->get_partial_data(&req_buf[req_pos], content_length - req_pos, read); + if (err != OK) { + return FAILED; + } else if (read < content_length - req_pos) { + return ERR_BUSY; + } + req_pos += read; + } + + // Parse data + String msg; + msg.parse_utf8((const char *)req_buf, req_pos); + + // Response + if (DebugAdapterProtocol::get_singleton()->process_message(msg)) { + // Reset to read again + req_pos = 0; + has_header = false; + } + } + return OK; +} + +Error DAPeer::send_data() { + while (res_queue.size()) { + Dictionary data = res_queue.front()->get(); + if (!data.has("seq")) { + data["seq"] = ++seq; + } + String formatted_data = format_output(data); + + int data_sent = 0; + while (data_sent < formatted_data.length()) { + int curr_sent = 0; + Error err = connection->put_partial_data((const uint8_t *)formatted_data.utf8().get_data(), formatted_data.size() - data_sent - 1, curr_sent); + if (err != OK) { + return err; + } + data_sent += curr_sent; + } + res_queue.pop_front(); + } + return OK; +} + +String DAPeer::format_output(const Dictionary &p_params) const { + String response = Variant(p_params).to_json_string(); + String header = "Content-Length: "; + CharString charstr = response.utf8(); + size_t len = charstr.length(); + header += itos(len); + header += "\r\n\r\n"; + + return header + response; +} + +Error DebugAdapterProtocol::on_client_connected() { + ERR_FAIL_COND_V_MSG(clients.size() >= DAP_MAX_CLIENTS, FAILED, "Max client limits reached"); + + Ref tcp_peer = server->take_connection(); + tcp_peer->set_no_delay(true); + Ref peer = memnew(DAPeer); + peer->connection = tcp_peer; + clients.push_back(peer); + + EditorDebuggerNode::get_singleton()->get_default_debugger()->set_move_to_foreground(false); + EditorNode::get_log()->add_message("[DAP] Connection Taken", EditorLog::MSG_TYPE_EDITOR); + return OK; +} + +void DebugAdapterProtocol::on_client_disconnected(const Ref &p_peer) { + clients.erase(p_peer); + if (!clients.size()) { + reset_ids(); + EditorDebuggerNode::get_singleton()->get_default_debugger()->set_move_to_foreground(true); + } + EditorNode::get_log()->add_message("[DAP] Disconnected", EditorLog::MSG_TYPE_EDITOR); +} + +void DebugAdapterProtocol::reset_current_info() { + _current_request = ""; + _current_peer.unref(); +} + +void DebugAdapterProtocol::reset_ids() { + breakpoint_id = 0; + breakpoint_list.clear(); + + reset_stack_info(); +} + +void DebugAdapterProtocol::reset_stack_info() { + stackframe_id = 0; + variable_id = 1; + + stackframe_list.clear(); + variable_list.clear(); +} + +bool DebugAdapterProtocol::process_message(const String &p_text) { + JSON json; + ERR_FAIL_COND_V_MSG(json.parse(p_text) != OK, true, "Mal-formed message!"); + Dictionary params = json.get_data(); + bool completed = true; + + // Append "req_" to any command received; prevents name clash with existing functions, and possibly exploiting + String command = "req_" + (String)params["command"]; + if (parser->has_method(command)) { + _current_request = params["command"]; + + Array args; + args.push_back(params); + Dictionary response = parser->callv(command, args); + if (!response.is_empty()) { + _current_peer->res_queue.push_front(response); + } else { + completed = false; + } + } + + reset_current_info(); + return completed; +} + +void DebugAdapterProtocol::notify_initialized() { + Dictionary event = parser->ev_initialized(); + _current_peer->res_queue.push_back(event); +} + +void DebugAdapterProtocol::notify_process() { + String launch_mode = _current_request.is_empty() ? "launch" : _current_request; + + Dictionary event = parser->ev_process(launch_mode); + for (List>::Element *E = clients.front(); E; E = E->next()) { + E->get()->res_queue.push_back(event); + } +} + +void DebugAdapterProtocol::notify_terminated() { + Dictionary event = parser->ev_terminated(); + for (List>::Element *E = clients.front(); E; E = E->next()) { + if (_current_request == "launch" && _current_peer == E->get()) { + continue; + } + E->get()->res_queue.push_back(event); + } +} + +void DebugAdapterProtocol::notify_exited(const int &p_exitcode) { + Dictionary event = parser->ev_exited(p_exitcode); + for (List>::Element *E = clients.front(); E; E = E->next()) { + if (_current_request == "launch" && _current_peer == E->get()) { + continue; + } + E->get()->res_queue.push_back(event); + } +} + +void DebugAdapterProtocol::notify_stopped_paused() { + Dictionary event = parser->ev_stopped_paused(); + for (List>::Element *E = clients.front(); E; E = E->next()) { + E->get()->res_queue.push_back(event); + } +} + +void DebugAdapterProtocol::notify_stopped_exception(const String &p_error) { + Dictionary event = parser->ev_stopped_exception(p_error); + for (List>::Element *E = clients.front(); E; E = E->next()) { + E->get()->res_queue.push_back(event); + } +} + +void DebugAdapterProtocol::notify_stopped_breakpoint(const int &p_id) { + Dictionary event = parser->ev_stopped_breakpoint(p_id); + for (List>::Element *E = clients.front(); E; E = E->next()) { + E->get()->res_queue.push_back(event); + } +} + +void DebugAdapterProtocol::notify_stopped_step() { + Dictionary event = parser->ev_stopped_step(); + for (List>::Element *E = clients.front(); E; E = E->next()) { + E->get()->res_queue.push_back(event); + } +} + +void DebugAdapterProtocol::notify_continued() { + Dictionary event = parser->ev_continued(); + for (List>::Element *E = clients.front(); E; E = E->next()) { + if (_current_request == "continue" && E->get() == _current_peer) { + continue; + } + E->get()->res_queue.push_back(event); + } + + reset_stack_info(); +} + +void DebugAdapterProtocol::notify_output(const String &p_message) { + Dictionary event = parser->ev_output(p_message); + for (List>::Element *E = clients.front(); E; E = E->next()) { + E->get()->res_queue.push_back(event); + } +} + +Array DebugAdapterProtocol::update_breakpoints(const String &p_path, const Array &p_lines) { + Array updated_breakpoints; + + for (int i = 0; i < p_lines.size(); i++) { + DAP::Breakpoint breakpoint; + breakpoint.verified = true; + breakpoint.source.path = p_path; + breakpoint.source.compute_checksums(); + breakpoint.line = p_lines[i]; + + List::Element *E = breakpoint_list.find(breakpoint); + if (E) { + breakpoint.id = E->get().id; + } else { + breakpoint.id = breakpoint_id++; + breakpoint_list.push_back(breakpoint); + } + + updated_breakpoints.push_back(breakpoint.to_json()); + } + + return updated_breakpoints; +} + +void DebugAdapterProtocol::on_debug_paused() { + if (EditorNode::get_singleton()->get_pause_button()->is_pressed()) { + notify_stopped_paused(); + } else { + notify_continued(); + } +} + +void DebugAdapterProtocol::on_debug_stopped() { + notify_exited(); + notify_terminated(); +} + +void DebugAdapterProtocol::on_debug_output(const String &p_message) { + notify_output(p_message); +} + +void DebugAdapterProtocol::on_debug_breaked(const bool &p_reallydid, const bool &p_can_debug, const String &p_reason, const bool &p_has_stackdump) { + if (!p_reallydid) { + notify_continued(); + return; + } + + if (p_reason == "Breakpoint") { + if (_stepping) { + notify_stopped_step(); + _stepping = false; + } else { + _processing_breakpoint = true; // Wait for stack_dump to find where the breakpoint happened + } + } else { + notify_stopped_exception(p_reason); + } + + _processing_stackdump = p_has_stackdump; +} + +void DebugAdapterProtocol::on_debug_stack_dump(const Array &p_stack_dump) { + if (_processing_breakpoint && !p_stack_dump.is_empty()) { + // Find existing breakpoint + Dictionary d = p_stack_dump[0]; + DAP::Breakpoint breakpoint; + breakpoint.source.path = ProjectSettings::get_singleton()->globalize_path(d["file"]); + breakpoint.line = d["line"]; + + List::Element *E = breakpoint_list.find(breakpoint); + if (E) { + notify_stopped_breakpoint(E->get().id); + } + + _processing_breakpoint = false; + } + + stackframe_id = 0; + stackframe_list.clear(); + + // Fill in stacktrace information + for (int i = 0; i < p_stack_dump.size(); i++) { + Dictionary stack_info = p_stack_dump[i]; + DAP::StackFrame stackframe; + stackframe.id = stackframe_id++; + stackframe.name = stack_info["function"]; + stackframe.line = stack_info["line"]; + stackframe.column = 0; + stackframe.source.path = ProjectSettings::get_singleton()->globalize_path(stack_info["file"]); + stackframe.source.compute_checksums(); + + // Information for "Locals", "Members" and "Globals" variables respectively + List scope_ids; + for (int j = 0; j < 3; j++) { + scope_ids.push_back(variable_id++); + } + + stackframe_list.insert(stackframe, scope_ids); + } + + _current_frame = 0; + _processing_stackdump = false; +} + +void DebugAdapterProtocol::on_debug_stack_frame_vars(const int &p_size) { + _remaining_vars = p_size; + DAP::StackFrame frame; + frame.id = _current_frame; + ERR_FAIL_COND(!stackframe_list.has(frame)); + List scope_ids = stackframe_list.find(frame)->value(); + for (List::Element *E = scope_ids.front(); E; E = E->next()) { + int variable_id = E->get(); + if (variable_list.has(variable_id)) { + variable_list.find(variable_id)->value().clear(); + } else { + variable_list.insert(variable_id, Array()); + } + } +} + +void DebugAdapterProtocol::on_debug_stack_frame_var(const Array &p_data) { + DebuggerMarshalls::ScriptStackVariable stack_var; + stack_var.deserialize(p_data); + + ERR_FAIL_COND(stackframe_list.is_empty()); + DAP::StackFrame frame; + frame.id = _current_frame; + + List scope_ids = stackframe_list.find(frame)->value(); + ERR_FAIL_COND(scope_ids.size() != 3); + ERR_FAIL_INDEX(stack_var.type, 3); + int variable_id = scope_ids[stack_var.type]; + + DAP::Variable variable; + + variable.name = stack_var.name; + variable.value = stack_var.value; + variable.type = Variant::get_type_name(stack_var.value.get_type()); + + variable_list.find(variable_id)->value().push_back(variable.to_json()); + _remaining_vars--; +} + +void DebugAdapterProtocol::poll() { + if (server->is_connection_available()) { + on_client_connected(); + } + List> to_delete; + for (List>::Element *E = clients.front(); E; E = E->next()) { + Ref peer = E->get(); + StreamPeerTCP::Status status = peer->connection->get_status(); + if (status == StreamPeerTCP::STATUS_NONE || status == StreamPeerTCP::STATUS_ERROR) { + to_delete.push_back(peer); + } else { + _current_peer = peer; + Error err = peer->handle_data(); + if (err != OK && err != ERR_BUSY) { + to_delete.push_back(peer); + } + err = peer->send_data(); + if (err != OK && err != ERR_BUSY) { + to_delete.push_back(peer); + } + } + } + + for (List>::Element *E = to_delete.front(); E; E = E->next()) { + on_client_disconnected(E->get()); + } + to_delete.clear(); +} + +Error DebugAdapterProtocol::start(int p_port, const IPAddress &p_bind_ip) { + _initialized = true; + return server->listen(p_port, p_bind_ip); +} + +void DebugAdapterProtocol::stop() { + for (List>::Element *E = clients.front(); E; E = E->next()) { + E->get()->connection->disconnect_from_host(); + } + + clients.clear(); + server->stop(); + _initialized = false; +} + +DebugAdapterProtocol::DebugAdapterProtocol() { + server.instantiate(); + singleton = this; + parser = memnew(DebugAdapterParser); + + reset_ids(); + + EditorNode *node = EditorNode::get_singleton(); + node->get_pause_button()->connect("pressed", callable_mp(this, &DebugAdapterProtocol::on_debug_paused)); + + EditorDebuggerNode *debugger_node = EditorDebuggerNode::get_singleton(); + debugger_node->get_default_debugger()->connect("stopped", callable_mp(this, &DebugAdapterProtocol::on_debug_stopped)); + debugger_node->get_default_debugger()->connect("output", callable_mp(this, &DebugAdapterProtocol::on_debug_output)); + debugger_node->get_default_debugger()->connect("breaked", callable_mp(this, &DebugAdapterProtocol::on_debug_breaked)); + debugger_node->get_default_debugger()->connect("stack_dump", callable_mp(this, &DebugAdapterProtocol::on_debug_stack_dump)); + debugger_node->get_default_debugger()->connect("stack_frame_vars", callable_mp(this, &DebugAdapterProtocol::on_debug_stack_frame_vars)); + debugger_node->get_default_debugger()->connect("stack_frame_var", callable_mp(this, &DebugAdapterProtocol::on_debug_stack_frame_var)); +} + +DebugAdapterProtocol::~DebugAdapterProtocol() { + memdelete(parser); +} diff --git a/editor/debugger/debug_adapter/debug_adapter_protocol.h b/editor/debugger/debug_adapter/debug_adapter_protocol.h new file mode 100644 index 0000000000..6b542033ed --- /dev/null +++ b/editor/debugger/debug_adapter/debug_adapter_protocol.h @@ -0,0 +1,140 @@ +/*************************************************************************/ +/* debug_adapter_protocol.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef DEBUG_ADAPTER_PROTOCOL_H +#define DEBUG_ADAPTER_PROTOCOL_H + +#include "core/io/stream_peer.h" +#include "core/io/stream_peer_tcp.h" +#include "core/io/tcp_server.h" + +#include "debug_adapter_parser.h" +#include "debug_adapter_types.h" + +#define DAP_MAX_BUFFER_SIZE 4194304 // 4MB +#define DAP_MAX_CLIENTS 8 + +class DebugAdapterParser; + +struct DAPeer : RefCounted { + Ref connection; + + uint8_t req_buf[DAP_MAX_BUFFER_SIZE]; + int req_pos = 0; + bool has_header = false; + int content_length = 0; + List res_queue; + int seq = 0; + + // Client specific info + bool linesStartAt1 = false; + bool columnsStartAt1 = false; + bool supportsVariableType = false; + bool supportsInvalidatedEvent = false; + + Error handle_data(); + Error send_data(); + String format_output(const Dictionary &p_params) const; +}; + +class DebugAdapterProtocol : public Object { + GDCLASS(DebugAdapterProtocol, Object) + + friend class DebugAdapterParser; + +private: + static DebugAdapterProtocol *singleton; + DebugAdapterParser *parser; + + List> clients; + Ref server; + + Error on_client_connected(); + void on_client_disconnected(const Ref &p_peer); + void on_debug_paused(); + void on_debug_stopped(); + void on_debug_output(const String &p_message); + void on_debug_breaked(const bool &p_reallydid, const bool &p_can_debug, const String &p_reason, const bool &p_has_stackdump); + void on_debug_stack_dump(const Array &p_stack_dump); + void on_debug_stack_frame_vars(const int &p_size); + void on_debug_stack_frame_var(const Array &p_data); + + void reset_current_info(); + void reset_ids(); + void reset_stack_info(); + + bool _initialized = false; + bool _processing_breakpoint = false; + bool _stepping = false; + bool _processing_stackdump = false; + int _remaining_vars = 0; + int _current_frame = 0; + + String _current_request; + Ref _current_peer; + + int breakpoint_id; + int stackframe_id; + int variable_id; + List breakpoint_list; + Map> stackframe_list; + Map variable_list; + +public: + _FORCE_INLINE_ static DebugAdapterProtocol *get_singleton() { return singleton; } + _FORCE_INLINE_ bool is_active() const { return _initialized && clients.size() > 0; } + + bool process_message(const String &p_text); + + String get_current_request() const { return _current_request; } + Ref get_current_peer() const { return _current_peer; } + + void notify_initialized(); + void notify_process(); + void notify_terminated(); + void notify_exited(const int &p_exitcode = 0); + void notify_stopped_paused(); + void notify_stopped_exception(const String &p_error); + void notify_stopped_breakpoint(const int &p_id); + void notify_stopped_step(); + void notify_continued(); + void notify_output(const String &p_message); + + Array update_breakpoints(const String &p_path, const Array &p_breakpoints); + + void poll(); + Error start(int p_port, const IPAddress &p_bind_ip); + void stop(); + + DebugAdapterProtocol(); + ~DebugAdapterProtocol(); +}; + +#endif diff --git a/editor/debugger/debug_adapter/debug_adapter_server.cpp b/editor/debugger/debug_adapter/debug_adapter_server.cpp new file mode 100644 index 0000000000..f9092a1791 --- /dev/null +++ b/editor/debugger/debug_adapter/debug_adapter_server.cpp @@ -0,0 +1,102 @@ +/*************************************************************************/ +/* debug_adapter_server.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "debug_adapter_server.h" + +#include "core/os/os.h" +#include "editor/editor_log.h" +#include "editor/editor_node.h" + +DebugAdapterServer::DebugAdapterServer() { + _EDITOR_DEF("network/debug_adapter/remote_port", remote_port); + _EDITOR_DEF("network/debug_adapter/use_thread", use_thread); +} + +void DebugAdapterServer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + start(); + break; + case NOTIFICATION_EXIT_TREE: + stop(); + break; + case NOTIFICATION_INTERNAL_PROCESS: { + // The main loop can be run again during request processing, which modifies internal state of the protocol. + // Thus, "polling" is needed to prevent it from parsing other requests while the current one isn't finished. + if (started && !use_thread && !polling) { + polling = true; + protocol.poll(); + polling = false; + } + } break; + case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { + int remote_port = (int)_EDITOR_GET("network/debug_adapter/remote_port"); + bool use_thread = (bool)_EDITOR_GET("network/debug_adapter/use_thread"); + if (remote_port != this->remote_port || use_thread != this->use_thread) { + this->stop(); + this->start(); + } + } break; + } +} + +void DebugAdapterServer::thread_func(void *p_userdata) { + DebugAdapterServer *self = static_cast(p_userdata); + while (self->thread_running) { + // Poll 20 times per second + self->protocol.poll(); + OS::get_singleton()->delay_usec(50000); + } +} + +void DebugAdapterServer::start() { + remote_port = (int)_EDITOR_GET("network/debug_adapter/remote_port"); + use_thread = (bool)_EDITOR_GET("network/debug_adapter/use_thread"); + if (protocol.start(remote_port, IPAddress("127.0.0.1")) == OK) { + EditorNode::get_log()->add_message("--- Debug adapter server started ---", EditorLog::MSG_TYPE_EDITOR); + if (use_thread) { + thread_running = true; + thread.start(DebugAdapterServer::thread_func, this); + } + set_process_internal(!use_thread); + started = true; + } +} + +void DebugAdapterServer::stop() { + if (use_thread) { + ERR_FAIL_COND(!thread.is_started()); + thread_running = false; + thread.wait_to_finish(); + } + protocol.stop(); + started = false; + EditorNode::get_log()->add_message("--- Debug adapter server stopped ---", EditorLog::MSG_TYPE_EDITOR); +} diff --git a/editor/debugger/debug_adapter/debug_adapter_server.h b/editor/debugger/debug_adapter/debug_adapter_server.h new file mode 100644 index 0000000000..f8a38965cc --- /dev/null +++ b/editor/debugger/debug_adapter/debug_adapter_server.h @@ -0,0 +1,59 @@ +/*************************************************************************/ +/* debug_adapter_server.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef DEBUG_ADAPTER_SERVER_H +#define DEBUG_ADAPTER_SERVER_H + +#include "debug_adapter_protocol.h" +#include "editor/editor_plugin.h" + +class DebugAdapterServer : public EditorPlugin { + GDCLASS(DebugAdapterServer, EditorPlugin); + + DebugAdapterProtocol protocol; + + Thread thread; + int remote_port = 6006; + bool thread_running = false; + bool started = false; + bool use_thread = false; + bool polling = false; + static void thread_func(void *p_userdata); + +private: + void _notification(int p_what); + +public: + DebugAdapterServer(); + void start(); + void stop(); +}; + +#endif diff --git a/editor/debugger/debug_adapter/debug_adapter_types.h b/editor/debugger/debug_adapter/debug_adapter_types.h new file mode 100644 index 0000000000..aa9ab1adcd --- /dev/null +++ b/editor/debugger/debug_adapter/debug_adapter_types.h @@ -0,0 +1,270 @@ +/*************************************************************************/ +/* debug_adapter_types.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef DEBUG_ADAPTER_TYPES_H +#define DEBUG_ADAPTER_TYPES_H + +#include "core/io/json.h" +#include "core/variant/dictionary.h" + +namespace DAP { + +enum ErrorType { + UNKNOWN, + WRONG_PATH +}; + +struct Checksum { + String algorithm; + String checksum; + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["algorithm"] = algorithm; + dict["checksum"] = checksum; + + return dict; + } +}; + +struct Source { +private: + Array _checksums; + +public: + String name; + String path; + + void compute_checksums() { + ERR_FAIL_COND(path.is_empty()); + + // MD5 + Checksum md5; + md5.algorithm = "MD5"; + md5.checksum = FileAccess::get_md5(path); + + // SHA-256 + Checksum sha256; + sha256.algorithm = "SHA256"; + sha256.checksum = FileAccess::get_sha256(path); + + _checksums.push_back(md5.to_json()); + _checksums.push_back(sha256.to_json()); + } + + _FORCE_INLINE_ void from_json(const Dictionary &p_params) { + name = p_params["name"]; + path = p_params["path"]; + _checksums = p_params["checksums"]; + } + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["name"] = name; + dict["path"] = path; + dict["checksums"] = _checksums; + + return dict; + } +}; + +struct Breakpoint { + int id; + bool verified; + Source source; + int line; + + bool operator==(const Breakpoint &p_other) const { + return source.path == p_other.source.path && line == p_other.line; + } + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["id"] = id; + dict["verified"] = verified; + dict["source"] = source.to_json(); + dict["line"] = line; + + return dict; + } +}; + +struct BreakpointLocation { + int line; + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["line"] = line; + + return dict; + } +}; + +struct Capabilities { + bool supportsConfigurationDoneRequest = true; + bool supportsEvaluateForHovers = true; + bool supportsSetVariable = true; + String supportedChecksumAlgorithms[2] = { "MD5", "SHA256" }; + bool supportsRestartRequest = true; + bool supportsValueFormattingOptions = true; + bool supportTerminateDebuggee = true; + bool supportSuspendDebuggee = true; + bool supportsTerminateRequest = true; + bool supportsBreakpointLocationsRequest = true; + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["supportsConfigurationDoneRequest"] = supportsConfigurationDoneRequest; + dict["supportsEvaluateForHovers"] = supportsEvaluateForHovers; + dict["supportsSetVariable"] = supportsSetVariable; + dict["supportsRestartRequest"] = supportsRestartRequest; + dict["supportsValueFormattingOptions"] = supportsValueFormattingOptions; + dict["supportTerminateDebuggee"] = supportTerminateDebuggee; + dict["supportSuspendDebuggee"] = supportSuspendDebuggee; + dict["supportsTerminateRequest"] = supportsTerminateRequest; + dict["supportsBreakpointLocationsRequest"] = supportsBreakpointLocationsRequest; + + Array arr; + arr.push_back(supportedChecksumAlgorithms[0]); + arr.push_back(supportedChecksumAlgorithms[1]); + dict["supportedChecksumAlgorithms"] = arr; + + return dict; + } +}; + +struct Message { + int id; + String format; + bool sendTelemetry = false; // Just in case :) + bool showUser; + Dictionary variables; + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["id"] = id; + dict["format"] = format; + dict["sendTelemetry"] = sendTelemetry; + dict["showUser"] = showUser; + dict["variables"] = variables; + + return dict; + } +}; + +struct Scope { + String name; + String presentationHint; + int variablesReference; + bool expensive; + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["name"] = name; + dict["presentationHint"] = presentationHint; + dict["variablesReference"] = variablesReference; + dict["expensive"] = expensive; + + return dict; + } +}; + +struct SourceBreakpoint { + int line; + + _FORCE_INLINE_ void from_json(const Dictionary &p_params) { + line = p_params["line"]; + } +}; + +struct StackFrame { + int id; + String name; + Source source; + int line; + int column; + + bool operator<(const StackFrame &p_other) const { + return id < p_other.id; + } + + _FORCE_INLINE_ void from_json(const Dictionary &p_params) { + id = p_params["id"]; + name = p_params["name"]; + source.from_json(p_params["source"]); + line = p_params["line"]; + column = p_params["column"]; + } + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["id"] = id; + dict["name"] = name; + dict["source"] = source.to_json(); + dict["line"] = line; + dict["column"] = column; + + return dict; + } +}; + +struct Thread { + int id; + String name; + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["id"] = id; + dict["name"] = name; + + return dict; + } +}; + +struct Variable { + String name; + String value; + String type; + int variablesReference = 0; + + _FORCE_INLINE_ Dictionary to_json() const { + Dictionary dict; + dict["name"] = name; + dict["value"] = value; + dict["type"] = type; + dict["variablesReference"] = variablesReference; + + return dict; + } +}; + +} // namespace DAP + +#endif diff --git a/editor/debugger/editor_debugger_node.cpp b/editor/debugger/editor_debugger_node.cpp index 690ce98cb9..5d654ca756 100644 --- a/editor/debugger/editor_debugger_node.cpp +++ b/editor/debugger/editor_debugger_node.cpp @@ -466,7 +466,7 @@ void EditorDebuggerNode::_paused() { }); } -void EditorDebuggerNode::_breaked(bool p_breaked, bool p_can_debug, int p_debugger) { +void EditorDebuggerNode::_breaked(bool p_breaked, bool p_can_debug, String p_message, bool p_has_stackdump, int p_debugger) { if (get_current_debugger() != get_debugger(p_debugger)) { if (!p_breaked) { return; @@ -489,6 +489,19 @@ void EditorDebuggerNode::set_breakpoint(const String &p_path, int p_line, bool p }); } +void EditorDebuggerNode::set_breakpoints(const String &p_path, Array p_lines) { + for (int i = 0; i < p_lines.size(); i++) { + set_breakpoint(p_path, p_lines[i], true); + } + + for (Map::Element *E = breakpoints.front(); E; E = E->next()) { + Breakpoint b = E->key(); + if (b.source == p_path && !p_lines.has(b.line)) { + set_breakpoint(p_path, b.line, false); + } + } +} + void EditorDebuggerNode::reload_scripts() { _for_all(tabs, [&](ScriptEditorDebugger *dbg) { dbg->reload_scripts(); diff --git a/editor/debugger/editor_debugger_node.h b/editor/debugger/editor_debugger_node.h index 9a40383c17..0849ecf1c9 100644 --- a/editor/debugger/editor_debugger_node.h +++ b/editor/debugger/editor_debugger_node.h @@ -35,6 +35,7 @@ #include "scene/gui/margin_container.h" class Button; +class DebugAdapterParser; class EditorDebuggerTree; class EditorDebuggerRemoteObject; class MenuButton; @@ -109,6 +110,7 @@ private: EditorDebuggerRemoteObject *get_inspected_remote_object(); friend class DebuggerEditorPlugin; + friend class DebugAdapterParser; static EditorDebuggerNode *singleton; EditorDebuggerNode(); @@ -129,7 +131,7 @@ protected: void _text_editor_stack_goto(const ScriptEditorDebugger *p_debugger); void _stack_frame_selected(int p_debugger); void _error_selected(const String &p_file, int p_line, int p_debugger); - void _breaked(bool p_breaked, bool p_can_debug, int p_debugger); + void _breaked(bool p_breaked, bool p_can_debug, String p_message, bool p_has_stackdump, int p_debugger); void _paused(); void _break_state_changed(); void _menu_option(int p_id); @@ -164,6 +166,7 @@ public: bool is_skip_breakpoints() const; void set_breakpoint(const String &p_path, int p_line, bool p_enabled); + void set_breakpoints(const String &p_path, Array p_lines); void reload_scripts(); // Remote inspector/edit. diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index e704609639..85657fa456 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -37,6 +37,7 @@ #include "core/string/ustring.h" #include "core/version.h" #include "core/version_hash.gen.h" +#include "editor/debugger/debug_adapter/debug_adapter_protocol.h" #include "editor/debugger/editor_network_profiler.h" #include "editor/debugger/editor_performance_profiler.h" #include "editor/debugger/editor_profiler.h" @@ -298,15 +299,18 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da if (p_msg == "debug_enter") { _put_msg("get_stack_dump", Array()); - ERR_FAIL_COND(p_data.size() != 2); + ERR_FAIL_COND(p_data.size() != 3); bool can_continue = p_data[0]; String error = p_data[1]; + bool has_stackdump = p_data[2]; breaked = true; can_debug = can_continue; _update_buttons_state(); _set_reason_text(error, MESSAGE_ERROR); - emit_signal(SNAME("breaked"), true, can_continue); - DisplayServer::get_singleton()->window_move_to_foreground(); + emit_signal(SNAME("breaked"), true, can_continue, error, has_stackdump); + if (is_move_to_foreground()) { + DisplayServer::get_singleton()->window_move_to_foreground(); + } if (error != "") { tabs->set_current_tab(0); } @@ -319,7 +323,7 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da _clear_execution(); _update_buttons_state(); _set_reason_text(TTR("Execution resumed."), MESSAGE_SUCCESS); - emit_signal(SNAME("breaked"), false, false); + emit_signal(SNAME("breaked"), false, false, "", false); profiler->set_enabled(true); profiler->disable_seeking(); } else if (p_msg == "set_pid") { @@ -373,6 +377,8 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da inspector->clear_stack_variables(); TreeItem *r = stack_dump->create_item(); + Array stack_dump_info; + for (int i = 0; i < stack.frames.size(); i++) { TreeItem *s = stack_dump->create_item(r); Dictionary d; @@ -380,6 +386,7 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da d["file"] = stack.frames[i].file; d["function"] = stack.frames[i].func; d["line"] = stack.frames[i].line; + stack_dump_info.push_back(d); s->set_metadata(0, d); String line = itos(i) + " - " + String(d["file"]) + ":" + itos(d["line"]) + " - at function: " + d["function"]; @@ -389,11 +396,15 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da s->select(0); } } + emit_signal("stack_dump", stack_dump_info); } else if (p_msg == "stack_frame_vars") { inspector->clear_stack_variables(); + ERR_FAIL_COND(p_data.size() != 1); + emit_signal("stack_frame_vars", p_data[0]); } else if (p_msg == "stack_frame_var") { inspector->add_stack_variable(p_data); + emit_signal("stack_frame_var", p_data); } else if (p_msg == "output") { ERR_FAIL_COND(p_data.size() != 2); @@ -422,6 +433,7 @@ void ScriptEditorDebugger::_parse_message(const String &p_msg, const Array &p_da } break; } EditorNode::get_log()->add_message(output_strings[i], msg_type); + emit_signal("output", output_strings[i]); } } else if (p_msg == "performance:profile_frame") { Vector frame_data; @@ -963,11 +975,7 @@ void ScriptEditorDebugger::_stack_dump_frame_selected() { int frame = get_stack_script_frame(); - if (is_session_active() && frame >= 0) { - Array msg; - msg.push_back(frame); - _put_msg("get_stack_frame_vars", msg); - } else { + if (!request_stack_dump(frame)) { inspector->edit(nullptr); } } @@ -1130,6 +1138,14 @@ void ScriptEditorDebugger::_property_changed(Object *p_base, const StringName &p } } +bool ScriptEditorDebugger::is_move_to_foreground() const { + return move_to_foreground; +} + +void ScriptEditorDebugger::set_move_to_foreground(const bool &p_move_to_foreground) { + move_to_foreground = p_move_to_foreground; +} + String ScriptEditorDebugger::get_stack_script_file() const { TreeItem *ti = stack_dump->get_selected(); if (!ti) { @@ -1157,6 +1173,15 @@ int ScriptEditorDebugger::get_stack_script_frame() const { return d["frame"]; } +bool ScriptEditorDebugger::request_stack_dump(const int &p_frame) { + ERR_FAIL_COND_V(!is_session_active() || p_frame < 0, false); + + Array msg; + msg.push_back(p_frame); + _put_msg("get_stack_frame_vars", msg); + return true; +} + void ScriptEditorDebugger::set_live_debugging(bool p_enable) { live_debug = p_enable; } @@ -1469,11 +1494,15 @@ void ScriptEditorDebugger::_bind_methods() { ADD_SIGNAL(MethodInfo("error_selected", PropertyInfo(Variant::INT, "error"))); ADD_SIGNAL(MethodInfo("set_execution", PropertyInfo("script"), PropertyInfo(Variant::INT, "line"))); ADD_SIGNAL(MethodInfo("clear_execution", PropertyInfo("script"))); - ADD_SIGNAL(MethodInfo("breaked", PropertyInfo(Variant::BOOL, "reallydid"), PropertyInfo(Variant::BOOL, "can_debug"))); + ADD_SIGNAL(MethodInfo("breaked", PropertyInfo(Variant::BOOL, "reallydid"), PropertyInfo(Variant::BOOL, "can_debug"), PropertyInfo(Variant::STRING, "reason"), PropertyInfo(Variant::BOOL, "has_stackdump"))); ADD_SIGNAL(MethodInfo("remote_object_requested", PropertyInfo(Variant::INT, "id"))); ADD_SIGNAL(MethodInfo("remote_object_updated", PropertyInfo(Variant::INT, "id"))); ADD_SIGNAL(MethodInfo("remote_object_property_updated", PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::STRING, "property"))); ADD_SIGNAL(MethodInfo("remote_tree_updated")); + ADD_SIGNAL(MethodInfo("output")); + ADD_SIGNAL(MethodInfo("stack_dump", PropertyInfo(Variant::ARRAY, "stack_dump"))); + ADD_SIGNAL(MethodInfo("stack_frame_vars", PropertyInfo(Variant::INT, "num_vars"))); + ADD_SIGNAL(MethodInfo("stack_frame_var", PropertyInfo(Variant::ARRAY, "data"))); } void ScriptEditorDebugger::add_debugger_plugin(const Ref