diff --git a/.gitignore b/.gitignore index 539003ca6b97..861d15ea9f42 100644 --- a/.gitignore +++ b/.gitignore @@ -284,6 +284,7 @@ Generated\ Files/ *.pidb *.svclog *.scc +*.obj.enc # Visual C++ cache files ipch/ diff --git a/core/debugger/debugger_marshalls.cpp b/core/debugger/debugger_marshalls.cpp index 4c69290c2e6b..1ad75c1ceef0 100644 --- a/core/debugger/debugger_marshalls.cpp +++ b/core/debugger/debugger_marshalls.cpp @@ -32,10 +32,10 @@ #include "core/io/marshalls.h" -#define CHECK_SIZE(arr, expected, what) ERR_FAIL_COND_V_MSG((uint32_t)arr.size() < (uint32_t)(expected), false, String("Malformed ") + what + " message from script debugger, message too short. Expected size: " + itos(expected) + ", actual size: " + itos(arr.size())) -#define CHECK_END(arr, expected, what) ERR_FAIL_COND_V_MSG((uint32_t)arr.size() > (uint32_t)expected, false, String("Malformed ") + what + " message from script debugger, message too long. Expected size: " + itos(expected) + ", actual size: " + itos(arr.size())) +#define CHECK_SIZE(arr, expected, what) ERR_FAIL_COND_V_MSG((uint32_t)(arr).size() < (uint32_t)(expected), false, String("Malformed ") + (what) + " message from script debugger, message too short. Expected size: " + itos(expected) + ", actual size: " + itos((arr).size())) +#define CHECK_END(arr, expected, what) ERR_FAIL_COND_V_MSG((uint32_t)(arr).size() > (uint32_t)(expected), false, String("Malformed ") + (what) + " message from script debugger, message too long. Expected size: " + itos(expected) + ", actual size: " + itos((arr).size())) -Array DebuggerMarshalls::ScriptStackDump::serialize() { +Array DebuggerMarshalls::ScriptStackDump::serialize() const { Array arr; arr.push_back(frames.size() * 3); for (int i = 0; i < frames.size(); i++) { @@ -43,27 +43,54 @@ Array DebuggerMarshalls::ScriptStackDump::serialize() { arr.push_back(frames[i].line); arr.push_back(frames[i].func); } + if (!tid.is_zero()) { + arr.push_back(tid); + } return arr; } bool DebuggerMarshalls::ScriptStackDump::deserialize(const Array &p_arr) { CHECK_SIZE(p_arr, 1, "ScriptStackDump"); - uint32_t size = p_arr[0]; + const uint32_t size = p_arr[0]; CHECK_SIZE(p_arr, size, "ScriptStackDump"); int idx = 1; for (uint32_t i = 0; i < size / 3; i++) { - ScriptLanguage::StackInfo sf; + StackInfo sf; sf.file = p_arr[idx]; sf.line = p_arr[idx + 1]; sf.func = p_arr[idx + 2]; frames.push_back(sf); idx += 3; } + if (idx == p_arr.size() - 1) { + // one extra data item is optional TID + tid = p_arr[idx]; + ++idx; + } else { + tid.zero(); + } CHECK_END(p_arr, idx, "ScriptStackDump"); return true; } -Array DebuggerMarshalls::ScriptStackVariable::serialize(int max_size) { +void DebuggerMarshalls::ScriptStackDump::populate(const ScriptLanguageThreadContext &p_context) { + tid = p_context.debug_get_thread_id(); + const int slc = p_context.debug_get_stack_level_count(); + for (int i = 0; i < slc; i++) { + StackInfo frame; + frame.file = p_context.debug_get_stack_level_source(i); + frame.line = p_context.debug_get_stack_level_line(i); + frame.func = p_context.debug_get_stack_level_function(i); + frames.push_back(frame); + } +} + +void DebuggerMarshalls::ScriptStackDump::clear() { + tid = DebugThreadID(); + frames.clear(); +} + +Array DebuggerMarshalls::ScriptStackVariable::serialize(int max_size) const { Array arr; arr.push_back(name); arr.push_back(type); @@ -74,7 +101,7 @@ Array DebuggerMarshalls::ScriptStackVariable::serialize(int max_size) { } int len = 0; - Error err = encode_variant(var, nullptr, len, true); + const Error err = encode_variant(var, nullptr, len, true); if (err != OK) { ERR_PRINT("Failed to encode variant."); } @@ -96,7 +123,7 @@ bool DebuggerMarshalls::ScriptStackVariable::deserialize(const Array &p_arr) { return true; } -Array DebuggerMarshalls::OutputError::serialize() { +Array DebuggerMarshalls::OutputError::serialize() const { Array arr; arr.push_back(hr); arr.push_back(min); @@ -108,8 +135,8 @@ Array DebuggerMarshalls::OutputError::serialize() { arr.push_back(error); arr.push_back(error_descr); arr.push_back(warning); - unsigned int size = callstack.size(); - const ScriptLanguage::StackInfo *r = callstack.ptr(); + const unsigned int size = callstack.size(); + const StackInfo *r = callstack.ptr(); arr.push_back(size * 3); for (int i = 0; i < callstack.size(); i++) { arr.push_back(r[i].file); @@ -131,11 +158,11 @@ bool DebuggerMarshalls::OutputError::deserialize(const Array &p_arr) { error = p_arr[7]; error_descr = p_arr[8]; warning = p_arr[9]; - unsigned int stack_size = p_arr[10]; + const unsigned int stack_size = p_arr[10]; CHECK_SIZE(p_arr, stack_size, "OutputError"); int idx = 11; - callstack.resize(stack_size / 3); - ScriptLanguage::StackInfo *w = callstack.ptrw(); + callstack.resize(static_cast(stack_size) / 3); + StackInfo *w = callstack.ptrw(); for (unsigned int i = 0; i < stack_size / 3; i++) { w[i].file = p_arr[idx]; w[i].func = p_arr[idx + 1]; diff --git a/core/debugger/debugger_marshalls.h b/core/debugger/debugger_marshalls.h index 751e8a637114..30e0dbca24b4 100644 --- a/core/debugger/debugger_marshalls.h +++ b/core/debugger/debugger_marshalls.h @@ -34,21 +34,29 @@ #include "core/object/script_language.h" struct DebuggerMarshalls { + using DebugThreadID = ScriptLanguageThreadContext::DebugThreadID; + using StackInfo = ScriptLanguageThreadContext::StackInfo; + struct ScriptStackVariable { String name; Variant value; int type = -1; - Array serialize(int max_size = 1 << 20); // 1 MiB default. + Array serialize(int max_size = 1 << 20) const; // 1 MiB default. bool deserialize(const Array &p_arr); }; struct ScriptStackDump { - List frames; - ScriptStackDump() {} + DebugThreadID tid; + List frames; + + ScriptStackDump() = default; - Array serialize(); + Array serialize() const; bool deserialize(const Array &p_arr); + + void populate(const ScriptLanguageThreadContext &p_context); + void clear(); }; struct OutputError { @@ -62,9 +70,9 @@ struct DebuggerMarshalls { String error; String error_descr; bool warning = false; - Vector callstack; + Vector callstack; - Array serialize(); + Array serialize() const; bool deserialize(const Array &p_arr); }; }; diff --git a/core/debugger/engine_debugger.h b/core/debugger/engine_debugger.h index 236d5e5f6318..b3375302577d 100644 --- a/core/debugger/engine_debugger.h +++ b/core/debugger/engine_debugger.h @@ -36,10 +36,10 @@ #include "core/templates/hash_map.h" #include "core/templates/vector.h" #include "core/variant/array.h" -#include "core/variant/variant.h" class RemoteDebuggerPeer; class ScriptDebugger; +class ScriptLanguageThreadContext; class EngineDebugger { public: @@ -61,7 +61,8 @@ class EngineDebugger { bool active = false; public: - Profiler() {} + Profiler() = default; + Profiler(void *p_data, ProfilingToggle p_toggle, ProfilingAdd p_add, ProfilingTick p_tick) { data = p_data; toggle = p_toggle; @@ -77,7 +78,8 @@ class EngineDebugger { void *data = nullptr; public: - Capture() {} + Capture() = default; + Capture(void *p_data, CaptureFunc p_capture) { data = p_data; capture = p_capture; @@ -110,7 +112,7 @@ class EngineDebugger { static void initialize(const String &p_uri, bool p_skip_breakpoints, Vector p_breakpoints, void (*p_allow_focus_steal_fn)()); static void deinitialize(); - static void register_profiler(const StringName &p_name, const Profiler &p_profiler); + static void register_profiler(const StringName &p_name, const Profiler &p_func); static void unregister_profiler(const StringName &p_name); static bool is_profiling(const StringName &p_name); static bool has_profiler(const StringName &p_name); @@ -128,10 +130,19 @@ class EngineDebugger { void line_poll(); - virtual void poll_events(bool p_is_idle) {} + virtual void poll_events(bool) {} virtual void send_message(const String &p_msg, const Array &p_data) = 0; virtual void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type) = 0; - virtual void debug(bool p_can_continue = true, bool p_is_error_breakpoint = false) = 0; + + // Initiate new debugging session on the caller thread and block the caller for the duration. + virtual void debug(ScriptLanguageThreadContext &p_focused_thread) = 0; + + // Notify that the caller thread would like to be selected for debugging, + // but debugger is busy with another thread. + virtual void request_debug(const ScriptLanguageThreadContext &p_any_thread) = 0; + + // Notify that the specified thread is held and can be inquired for stack info, but don't block. + virtual void thread_paused(const ScriptLanguageThreadContext &p_any_thread) = 0; virtual ~EngineDebugger(); }; diff --git a/core/debugger/local_debugger.cpp b/core/debugger/local_debugger.cpp index 58d239ccb986..1b9751538d68 100644 --- a/core/debugger/local_debugger.cpp +++ b/core/debugger/local_debugger.cpp @@ -68,7 +68,7 @@ struct LocalDebugger::ScriptsProfiler { void _print_frame_data(bool p_accumulated) { uint64_t diff = OS::get_singleton()->get_ticks_usec() - idle_accum; - if (!p_accumulated && diff < 1000000) { //show every one second + if (!p_accumulated && diff < 1000000) { // show every one second return; } @@ -113,24 +113,43 @@ struct LocalDebugger::ScriptsProfiler { } }; -void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { - ScriptLanguage *script_lang = script_debugger->get_break_language(); +void LocalDebugger::_print_stack_header(ScriptLanguageThreadContext &p_focused_thread) { + const PackedByteArray tid = p_focused_thread.debug_get_thread_id(); + print_line(vformat("#Thread %s", String::hex_encode_buffer(tid.ptr(), tid.size()))); +} + +void LocalDebugger::_print_status(ScriptLanguageThreadContext &p_focused_thread, int current_frame) { + print_line("\nDebugger Break, Reason: '" + p_focused_thread.debug_get_error() + "'"); + _print_stack_header(p_focused_thread); + _print_frame(p_focused_thread, 0, 0); +} +void LocalDebugger::_print_frame(ScriptLanguageThreadContext &p_focused_thread, int printed_frame, int current_frame) { + String cfi = (current_frame == printed_frame) ? "*" : " "; // current frame indicator + print_line(cfi + "Frame " + itos(printed_frame) + " - " + p_focused_thread.debug_get_stack_level_source(printed_frame) + ":" + + itos(p_focused_thread.debug_get_stack_level_line(printed_frame)) + " in function '" + + p_focused_thread.debug_get_stack_level_function(printed_frame) + "'"); +} + +void LocalDebugger::debug(ScriptLanguageThreadContext &p_focused_thread) { + if (!p_focused_thread.is_main_thread()) { + // REVISIT: Thread debugging is not (yet) supported for LocalDebugger, so just let the thread + // run and refuse to debug it. + return; + } if (!target_function.is_empty()) { - String current_function = script_lang->debug_get_stack_level_function(0); + String current_function = p_focused_thread.debug_get_stack_level_function(0); if (current_function != target_function) { - script_debugger->set_depth(0); - script_debugger->set_lines_left(1); + p_focused_thread.debug_step(); return; } target_function = ""; } - print_line("\nDebugger Break, Reason: '" + script_lang->debug_get_error() + "'"); - print_line("*Frame " + itos(0) + " - " + script_lang->debug_get_stack_level_source(0) + ":" + itos(script_lang->debug_get_stack_level_line(0)) + " in function '" + script_lang->debug_get_stack_level_function(0) + "'"); + _print_status(p_focused_thread, 0); print_line("Enter \"help\" for assistance."); int current_frame = 0; - int total_frames = script_lang->debug_get_stack_level_count(); + int total_frames = p_focused_thread.debug_get_stack_level_count(); while (true) { OS::get_singleton()->print("debug> "); String line = OS::get_singleton()->get_stdin_string().strip_edges(); @@ -139,27 +158,28 @@ void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { String variable_prefix = options["variable_prefix"]; if (line.is_empty()) { - print_line("\nDebugger Break, Reason: '" + script_lang->debug_get_error() + "'"); - print_line("*Frame " + itos(current_frame) + " - " + script_lang->debug_get_stack_level_source(current_frame) + ":" + itos(script_lang->debug_get_stack_level_line(current_frame)) + " in function '" + script_lang->debug_get_stack_level_function(current_frame) + "'"); + _print_status(p_focused_thread, current_frame); print_line("Enter \"help\" for assistance."); + } else if (line == "c" || line == "continue") { break; - } else if (line == "bt" || line == "breakpoint") { + + } else if (line == "bt" || line == "backtrace") { + _print_stack_header(p_focused_thread); for (int i = 0; i < total_frames; i++) { - String cfi = (current_frame == i) ? "*" : " "; //current frame indicator - print_line(cfi + "Frame " + itos(i) + " - " + script_lang->debug_get_stack_level_source(i) + ":" + itos(script_lang->debug_get_stack_level_line(i)) + " in function '" + script_lang->debug_get_stack_level_function(i) + "'"); + _print_frame(p_focused_thread, i, current_frame); } } else if (line.begins_with("fr") || line.begins_with("frame")) { if (line.get_slice_count(" ") == 1) { - print_line("*Frame " + itos(current_frame) + " - " + script_lang->debug_get_stack_level_source(current_frame) + ":" + itos(script_lang->debug_get_stack_level_line(current_frame)) + " in function '" + script_lang->debug_get_stack_level_function(current_frame) + "'"); + _print_frame(p_focused_thread, current_frame, current_frame); } else { int frame = line.get_slicec(' ', 1).to_int(); if (frame < 0 || frame >= total_frames) { print_line("Error: Invalid frame."); } else { current_frame = frame; - print_line("*Frame " + itos(frame) + " - " + script_lang->debug_get_stack_level_source(frame) + ":" + itos(script_lang->debug_get_stack_level_line(frame)) + " in function '" + script_lang->debug_get_stack_level_function(frame) + "'"); + _print_frame(p_focused_thread, current_frame, current_frame); } } @@ -192,19 +212,19 @@ void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { } else if (line == "lv" || line == "locals") { List locals; List values; - script_lang->debug_get_stack_level_locals(current_frame, &locals, &values); + p_focused_thread.debug_get_stack_level_locals(current_frame, &locals, &values); print_variables(locals, values, variable_prefix); } else if (line == "gv" || line == "globals") { List globals; List values; - script_lang->debug_get_globals(&globals, &values); + p_focused_thread.get_language()->debug_get_globals(&globals, &values); print_variables(globals, values, variable_prefix); } else if (line == "mv" || line == "members") { List members; List values; - script_lang->debug_get_stack_level_members(current_frame, &members, &values); + p_focused_thread.debug_get_stack_level_members(current_frame, &members, &values); print_variables(members, values, variable_prefix); } else if (line.begins_with("p") || line.begins_with("print")) { @@ -212,26 +232,23 @@ void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { print_line("Usage: print "); } else { String expr = line.get_slicec(' ', 2); - String res = script_lang->debug_parse_stack_level_expression(current_frame, expr); + String res = p_focused_thread.debug_parse_stack_level_expression(current_frame, expr); print_line(res); } } else if (line == "s" || line == "step") { - script_debugger->set_depth(-1); - script_debugger->set_lines_left(1); + p_focused_thread.debug_step(); break; } else if (line == "n" || line == "next") { - script_debugger->set_depth(0); - script_debugger->set_lines_left(1); + p_focused_thread.debug_next(); break; } else if (line == "fin" || line == "finish") { - String current_function = script_lang->debug_get_stack_level_function(0); + String current_function = p_focused_thread.debug_get_stack_level_function(0); for (int i = 0; i < total_frames; i++) { - target_function = script_lang->debug_get_stack_level_function(i); + target_function = p_focused_thread.debug_get_stack_level_function(i); if (target_function != current_function) { - script_debugger->set_depth(0); - script_debugger->set_lines_left(1); + p_focused_thread.debug_step_out(); return; } } @@ -270,8 +287,7 @@ void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { } else if (line == "q" || line == "quit") { // Do not stop again on quit script_debugger->clear_breakpoints(); - script_debugger->set_depth(-1); - script_debugger->set_lines_left(-1); + p_focused_thread.debug_continue(); MainLoop *main_loop = OS::get_singleton()->get_main_loop(); if (main_loop->get_class() == "SceneTree") { @@ -318,6 +334,16 @@ void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { } } +void LocalDebugger::request_debug(const ScriptLanguageThreadContext &p_context) { + // REVISIT: Thread debugging is not (yet) supported for LocalDebugger. + (void)p_context; +} + +void LocalDebugger::thread_paused(const ScriptLanguageThreadContext &p_context) { + // REVISIT: Thread debugging is not (yet) supported for LocalDebugger. + (void)p_context; +} + void LocalDebugger::print_variables(const List &names, const List &values, const String &variable_prefix) { String value; Vector value_lines; diff --git a/core/debugger/local_debugger.h b/core/debugger/local_debugger.h index c687214c653c..0c40cec4e689 100644 --- a/core/debugger/local_debugger.h +++ b/core/debugger/local_debugger.h @@ -36,7 +36,6 @@ #include "core/templates/list.h" class LocalDebugger : public EngineDebugger { -private: struct ScriptsProfiler; ScriptsProfiler *scripts_profiler = nullptr; @@ -47,10 +46,16 @@ class LocalDebugger : public EngineDebugger { Pair to_breakpoint(const String &p_line); void print_variables(const List &names, const List &values, const String &variable_prefix); + void _print_stack_header(ScriptLanguageThreadContext &p_focused_thread); + void _print_status(ScriptLanguageThreadContext &p_focused_thread, int current_frame); + void _print_frame(ScriptLanguageThreadContext &p_focused_thread, int printed_frame, int current_frame); + public: - void debug(bool p_can_continue, bool p_is_error_breakpoint); - void send_message(const String &p_message, const Array &p_args); - void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type); + void debug(ScriptLanguageThreadContext &p_focused_thread) override; + void request_debug(const ScriptLanguageThreadContext &p_context) override; + void thread_paused(const ScriptLanguageThreadContext &p_context) override; + void send_message(const String &p_message, const Array &p_args) override; + void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type) override; LocalDebugger(); ~LocalDebugger(); diff --git a/core/debugger/remote_debugger.cpp b/core/debugger/remote_debugger.cpp index 23ee977df463..d8cb9ebd0efd 100644 --- a/core/debugger/remote_debugger.cpp +++ b/core/debugger/remote_debugger.cpp @@ -39,6 +39,8 @@ #include "core/object/script_language.h" #include "core/os/os.h" +using Severity = ScriptLanguageThreadContext; + class RemoteDebugger::MultiplayerProfiler : public EngineProfiler { struct BandwidthFrame { uint32_t timestamp; @@ -178,7 +180,8 @@ Error RemoteDebugger::_put_msg(String p_message, Array p_data) { Array msg; msg.push_back(p_message); msg.push_back(p_data); - Error err = peer->put_message(msg); + const int channel = Thread::get_caller_id() == Thread::get_main_id() ? Peer::CHANNEL_MAIN_THREAD : Peer::CHANNEL_OTHER; + const Error err = peer->put_message(channel, msg); if (err != OK) { n_messages_dropped++; } @@ -187,7 +190,7 @@ Error RemoteDebugger::_put_msg(String p_message, Array p_data) { void RemoteDebugger::_err_handler(void *p_this, const char *p_func, const char *p_file, int p_line, const char *p_err, const char *p_descr, bool p_editor_notify, ErrorHandlerType p_type) { if (p_type == ERR_HANDLER_SCRIPT) { - return; //ignore script errors, those go through debugger + return; // ignore script errors, those go through debugger } RemoteDebugger *rd = static_cast(p_this); @@ -195,17 +198,8 @@ void RemoteDebugger::_err_handler(void *p_this, const char *p_func, const char * return; } - Vector si; - - for (int i = 0; i < ScriptServer::get_language_count(); i++) { - si = ScriptServer::get_language(i)->debug_get_current_stack_info(); - if (si.size()) { - break; - } - } - // send_error will lock internally. - rd->script_debugger->send_error(String::utf8(p_func), String::utf8(p_file), p_line, String::utf8(p_err), String::utf8(p_descr), p_editor_notify, p_type, si); + rd->send_error(String::utf8(p_func), String::utf8(p_file), p_line, String::utf8(p_err), String::utf8(p_descr), p_editor_notify, p_type); } void RemoteDebugger::_print_handler(void *p_this, const String &p_string, bool p_error, bool p_rich) { @@ -287,8 +281,7 @@ void RemoteDebugger::flush_output() { Vector joined_log_strings; Vector strings; Vector types; - for (int i = 0; i < output_strings.size(); i++) { - const OutputString &output_string = output_strings[i]; + for (const OutputString &output_string : output_strings) { if (output_string.type == MESSAGE_TYPE_ERROR) { if (!joined_log_strings.is_empty()) { strings.push_back(String("\n").join(joined_log_strings)); @@ -350,6 +343,11 @@ void RemoteDebugger::send_message(const String &p_message, const Array &p_args) } void RemoteDebugger::send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type) { + if (flushing && Thread::get_caller_id() == flush_thread) { + // Can't handle recursive errors during flush. + return; + } + ErrorMessage oe; oe.error = p_err; oe.error_descr = p_descr; @@ -357,16 +355,27 @@ void RemoteDebugger::send_error(const String &p_func, const String &p_file, int oe.source_line = p_line; oe.source_func = p_func; oe.warning = p_type == ERR_HANDLER_WARNING; - uint64_t time = OS::get_singleton()->get_ticks_msec(); - oe.hr = time / 3600000; - oe.min = (time / 60000) % 60; - oe.sec = (time / 1000) % 60; - oe.msec = time % 1000; - oe.callstack.append_array(script_debugger->get_error_stack_info()); - - if (flushing && Thread::get_caller_id() == flush_thread) { // Can't handle recursive errors during flush. - return; + const uint64_t time = OS::get_singleton()->get_ticks_msec(); + oe.hr = static_cast(time / 3600000ULL); + oe.min = static_cast(time / 60000ULL) % 60; + oe.sec = static_cast(time / 1000ULL) % 60; + oe.msec = static_cast(time % 1000ULL); + + Vector si; + // Right now this is always true, but if we were ever in a "switched off" state, we don't want to create thread contexts. + // In all other places, is_active() is checked before any debugging is done. + if (is_active()) { + // No mutex required since we are only accessing stable collection of languages and thread-local storage. + for (int i = 0; i < ScriptServer::get_language_count(); i++) { + si = ScriptServer::get_language(i)->current_thread().debug_get_current_stack_info(); + // FIXME This isn't technically correct if we have languages calling into each other, + // there could be multiple, so we should have a flag in the context to see if we are on the stack. + if (si.size()) { + break; + } + } } + oe.callstack.append_array(si); MutexLock lock(mutex); @@ -381,8 +390,8 @@ void RemoteDebugger::send_error(const String &p_func, const String &p_file, int if (warn_count > max_warnings_per_second) { n_warnings_dropped++; if (n_warnings_dropped == 1) { - // Only print one message about dropping per second - ErrorMessage overflow = _create_overflow_error("TOO_MANY_WARNINGS", "Too many warnings! Ignoring warnings for up to 1 second."); + // Only print one message about dropping per second. + const ErrorMessage overflow = _create_overflow_error("TOO_MANY_WARNINGS", "Too many warnings! Ignoring warnings for up to 1 second."); errors.push_back(overflow); } } else { @@ -393,7 +402,7 @@ void RemoteDebugger::send_error(const String &p_func, const String &p_file, int n_errors_dropped++; if (n_errors_dropped == 1) { // Only print one message about dropping per second - ErrorMessage overflow = _create_overflow_error("TOO_MANY_ERRORS", "Too many errors! Ignoring errors for up to 1 second."); + const ErrorMessage overflow = _create_overflow_error("TOO_MANY_ERRORS", "Too many errors! Ignoring errors for up to 1 second."); errors.push_back(overflow); } } else { @@ -403,6 +412,44 @@ void RemoteDebugger::send_error(const String &p_func, const String &p_file, int } } +// May be called back under lock by the script debugger. +void RemoteDebugger::send_stack_frame_variables(const ScriptLanguageThreadContext &p_any_thread, int p_level) { + List members; + List member_vals; + if (ScriptInstance *inst = p_any_thread.debug_get_stack_level_instance(p_level)) { + members.push_back("self"); + member_vals.push_back(inst->get_owner()); + } + p_any_thread.debug_get_stack_level_members(p_level, &members, &member_vals); + ERR_FAIL_COND(members.size() != member_vals.size()); + + List locals; + List local_vals; + p_any_thread.debug_get_stack_level_locals(p_level, &locals, &local_vals); + ERR_FAIL_COND(locals.size() != local_vals.size()); + + List globals; + List globals_vals; + p_any_thread.get_language()->debug_get_globals(&globals, &globals_vals); + ERR_FAIL_COND(globals.size() != globals_vals.size()); + + Array data; + data.push_back(local_vals.size() + member_vals.size() + globals_vals.size()); + data.push_back(p_any_thread.debug_get_thread_id()); + send_message("stack_frame_vars", data); + _send_stack_vars(locals, local_vals, 0); + _send_stack_vars(members, member_vals, 1); + _send_stack_vars(globals, globals_vals, 2); +} + +// May be called back under lock by the script debugger. +void RemoteDebugger::send_empty_stack_frame(const DebugThreadID &p_tid) { + Array data; + data.push_back(0); + data.push_back(p_tid); + send_message("stack_frame_vars", data); +} + void RemoteDebugger::_send_stack_vars(List &p_names, List &p_vals, int p_type) { DebuggerMarshalls::ScriptStackVariable stvar; List::Element *E = p_names.front(); @@ -431,125 +478,154 @@ Error RemoteDebugger::_try_capture(const String &p_msg, const Array &p_data, boo return capture_parse(cap, msg, p_data, r_captured); } -void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { - //this function is called when there is a debugger break (bug on script) - //or when execution is paused from editor +inline ScriptLanguageThreadContext::DebugThreadID _get_optional_thread_id(ScriptLanguageThreadContext &p_focused_thread, const Array &p_data, const int p_index) { + if (p_data.size() > p_index) { + return static_cast(p_data[p_index]); + } + return p_focused_thread.debug_get_thread_id(); +} + +void RemoteDebugger::_send_debug_enter_thread(ScriptLanguageThreadContext &p_focused_thread) { + Array msg; + msg.push_back(p_focused_thread.debug_get_error_severity() < Severity::SEVERITY_FATAL); + msg.push_back(p_focused_thread.debug_get_error()); + msg.push_back(p_focused_thread.debug_get_stack_level_count() > 0); + msg.push_back(p_focused_thread.debug_get_thread_id()); + msg.push_back(p_focused_thread.is_main_thread()); + msg.push_back(p_focused_thread.debug_get_error_severity()); + send_message("debug_enter_thread", msg); +} + +void RemoteDebugger::_send_debug_exit_thread(ScriptLanguageThreadContext &p_focused_thread) { + Array msg; + msg.push_back(p_focused_thread.debug_get_thread_id()); + send_message("debug_exit_thread", msg); +} + +bool RemoteDebugger::_get_next_message(ScriptLanguageThreadContext &p_focused_thread, Array &r_cmd) { + bool has_message = false; + if (p_focused_thread.is_main_thread()) { + // Also service captures while we sit here. In NO_THREADS compilations this will block, but OTHER won't. + peer->poll(Peer::CHANNEL_MAIN_THREAD); + has_message = peer->has_message(Peer::CHANNEL_MAIN_THREAD); + if (has_message) { + r_cmd = peer->get_message(Peer::CHANNEL_MAIN_THREAD); + } else { + // Also service discardable messages only on Main thread. + peer->poll(Peer::CHANNEL_DISCARDABLE); + has_message = peer->has_message(Peer::CHANNEL_DISCARDABLE); + if (has_message) { + r_cmd = peer->get_message(Peer::CHANNEL_DISCARDABLE); + } + } + } + if (!has_message) { + peer->poll(Peer::CHANNEL_OTHER); + has_message = peer->has_message(Peer::CHANNEL_OTHER); + if (has_message) { + r_cmd = peer->get_message(Peer::CHANNEL_OTHER); + } + } + return has_message; +} + +void RemoteDebugger::debug(ScriptLanguageThreadContext &p_focused_thread) { + // This function is called when there is a debugger break (error on script) + // or when execution is paused from editor. The caller is the thread being debugged + // and we block them here while data is being interrogated. - if (script_debugger->is_skipping_breakpoints() && !p_is_error_breakpoint) { + if (script_debugger->is_skipping_breakpoints() && p_focused_thread.debug_get_error_severity() <= Severity::SEVERITY_BREAKPOINT) { return; } ERR_FAIL_COND_MSG(!is_peer_connected(), "Script Debugger failed to connect, but being used anyway."); - if (!peer->can_block()) { - return; // Peer does not support blocking IO. We could at least send the error though. + if (!peer->can_block(Peer::CHANNEL_OTHER)) { + // Peer does not support blocking IO. + // REVISIT We could at least send the error though. + return; } - ScriptLanguage *script_lang = script_debugger->get_break_language(); - const String error_str = script_lang ? script_lang->debug_get_error() : ""; - 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); - if (allow_focus_steal_fn) { + if (allow_focus_steal_fn && p_focused_thread.is_main_thread()) { allow_focus_steal_fn(); } - send_message("debug_enter", msg); - Input::MouseMode mouse_mode = Input::get_singleton()->get_mouse_mode(); + _send_debug_enter_thread(p_focused_thread); + + const Input::MouseMode mouse_mode = Input::get_singleton()->get_mouse_mode(); if (mouse_mode != Input::MOUSE_MODE_VISIBLE) { Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); } while (is_peer_connected()) { flush_output(); - peer->poll(); - - if (peer->has_message()) { - Array cmd = peer->get_message(); - + Array cmd; + if (_get_next_message(p_focused_thread, cmd)) { ERR_CONTINUE(cmd.size() != 2); ERR_CONTINUE(cmd[0].get_type() != Variant::STRING); ERR_CONTINUE(cmd[1].get_type() != Variant::ARRAY); - String command = cmd[0]; - Array data = cmd[1]; + const String command = cmd[0]; + const Array data = cmd[1]; + // REVISIT implement commands to free other threads or step execute all? + if (command == "thread") { + // TODO thread safe tell script debugger to switch to that thread (if suspended or when it does suspend) + // then end debugging this thread + } if (command == "step") { - script_debugger->set_depth(-1); - script_debugger->set_lines_left(1); + p_focused_thread.debug_step(); break; - - } else if (command == "next") { - script_debugger->set_depth(0); - script_debugger->set_lines_left(1); + } + if (command == "next") { + p_focused_thread.debug_next(); break; - - } else if (command == "continue") { - script_debugger->set_depth(-1); - script_debugger->set_lines_left(-1); + } + if (command == "continue") { + p_focused_thread.debug_continue(); break; - - } else if (command == "break") { - ERR_PRINT("Got break when already broke!"); + } + if (command == "break") { + ERR_PRINT("Received break request when already debugging."); break; - - } else if (command == "get_stack_dump") { + } + if (command == "get_stack_dump") { DebuggerMarshalls::ScriptStackDump dump; - int slc = script_lang->debug_get_stack_level_count(); - for (int i = 0; i < slc; i++) { - ScriptLanguage::StackInfo frame; - frame.file = script_lang->debug_get_stack_level_source(i); - frame.line = script_lang->debug_get_stack_level_line(i); - frame.func = script_lang->debug_get_stack_level_function(i); - dump.frames.push_back(frame); + const DebugThreadID tid = _get_optional_thread_id(p_focused_thread, data, 0); + if (p_focused_thread.is_main_thread() && p_focused_thread.debug_get_thread_id() == tid) { + dump.populate(p_focused_thread); + } else { + // Get data or send empty if not paused. + // The UI would know if thread has paused from notifications. + script_debugger->try_get_stack_dump(tid, dump); } - send_message("stack_dump", dump.serialize()); - + Array send_data = dump.serialize(); + send_message("stack_dump", send_data); } else if (command == "get_stack_frame_vars") { - ERR_FAIL_COND(data.size() != 1); - ERR_FAIL_COND(!script_lang); - int lv = data[0]; - - List members; - List member_vals; - if (ScriptInstance *inst = script_lang->debug_get_stack_level_instance(lv)) { - members.push_back("self"); - member_vals.push_back(inst->get_owner()); + ERR_FAIL_COND(data.size() < 1); + const int lv = data[0]; + const DebugThreadID tid = _get_optional_thread_id(p_focused_thread, data, 1); + if (p_focused_thread.is_main_thread() && p_focused_thread.debug_get_thread_id() == tid) { + send_stack_frame_variables(p_focused_thread, lv); + } else { + // Get data or send empty if not paused. + // The UI would know if thread has paused from notifications. + script_debugger->try_send_stack_frame_variables(tid, lv, *this); } - script_lang->debug_get_stack_level_members(lv, &members, &member_vals); - ERR_FAIL_COND(members.size() != member_vals.size()); - - List locals; - List local_vals; - script_lang->debug_get_stack_level_locals(lv, &locals, &local_vals); - ERR_FAIL_COND(locals.size() != local_vals.size()); - - List globals; - List globals_vals; - script_lang->debug_get_globals(&globals, &globals_vals); - ERR_FAIL_COND(globals.size() != globals_vals.size()); - - 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); - + } else if (!p_focused_thread.is_main_thread()) { + // The remaining messages should have been sent on the main thread channel and can't + // be dispatched if we are debugging another thread. + WARN_PRINT(vformat("Disallowed message received on debugged thread channel: %s", command)); } else if (command == "reload_scripts") { reload_all_scripts = true; - } else if (command == "breakpoint") { ERR_FAIL_COND(data.size() < 3); - bool set = data[2]; + const bool set = data[2]; if (set) { script_debugger->insert_breakpoint(data[1], data[0]); } else { script_debugger->remove_breakpoint(data[1], data[0]); } - } else if (command == "set_skip_breakpoints") { ERR_FAIL_COND(data.size() < 1); script_debugger->set_skip_breakpoints(data[0]); @@ -566,22 +642,40 @@ void RemoteDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) { } } - send_message("debug_exit", Array()); + _send_debug_exit_thread(p_focused_thread); if (mouse_mode != Input::MOUSE_MODE_VISIBLE) { Input::get_singleton()->set_mouse_mode(mouse_mode); } } -void RemoteDebugger::poll_events(bool p_is_idle) { - if (peer.is_null()) { - return; - } +void RemoteDebugger::request_debug(const ScriptLanguageThreadContext &p_any_thread) { + Array msg; + msg.push_back(p_any_thread.debug_get_thread_id()); + msg.push_back(p_any_thread.is_main_thread()); + msg.push_back(p_any_thread.debug_get_error()); + msg.push_back(p_any_thread.debug_get_error_severity()); + msg.push_back(p_any_thread.debug_get_error_severity() < Severity::SEVERITY_FATAL); + msg.push_back(p_any_thread.debug_get_stack_level_count() > 0); + send_message("thread_alert", msg); +} - flush_output(); - peer->poll(); - while (peer->has_message()) { - Array arr = peer->get_message(); +void RemoteDebugger::thread_paused(const ScriptLanguageThreadContext &p_any_thread) { + Array msg; + msg.append(p_any_thread.debug_get_thread_id()); + msg.append(p_any_thread.is_main_thread()); + send_message("thread_paused", msg); +} + +void RemoteDebugger::_service_channel(int p_channel) { + // Special handling: only fire a single servers:draw even if we just read many of them + // REVISIT: We should handle the entire DISCARDABLE channel this way and only fire the latest of + // each unique capture:msg. + bool servers_draw = false; + Variant servers_draw_args; + peer->poll(p_channel); + while (peer->has_message(p_channel)) { + Array arr = peer->get_message(p_channel); ERR_CONTINUE(arr.size() != 2); ERR_CONTINUE(arr[0].get_type() != Variant::STRING); @@ -600,9 +694,30 @@ void RemoteDebugger::poll_events(bool p_is_idle) { continue; // Unknown message... } + if (cmd == "servers:draw") { + servers_draw = true; + // Will always be an empty array for now, but save it anyway. + servers_draw_args = arr[1]; + } const String msg = cmd.substr(idx + 1); capture_parse(cap, msg, arr[1], parsed); } + if (servers_draw) { + bool captured = false; + capture_parse(SNAME("servers"), "draw", servers_draw_args, captured); + (void)captured; + } +} + +void RemoteDebugger::poll_events(bool p_is_idle) { + ERR_FAIL_COND_MSG(Thread::get_caller_id() != Thread::get_main_id(), "Dispatching of debugger captures can only run on Main thread. Please report broken implementation."); + if (peer.is_null()) { + return; + } + + flush_output(); + _service_channel(Peer::CHANNEL_MAIN_THREAD); + _service_channel(Peer::CHANNEL_DISCARDABLE); // Reload scripts during idle poll only. if (p_is_idle && reload_all_scripts) { @@ -631,7 +746,7 @@ Error RemoteDebugger::_core_capture(const String &p_cmd, const Array &p_data, bo ERR_FAIL_COND_V(p_data.size() < 1, ERR_INVALID_DATA); script_debugger->set_skip_breakpoints(p_data[0]); } else if (p_cmd == "break") { - script_debugger->debug(script_debugger->get_break_language()); + script_debugger->debug_request_break(); } else { r_captured = false; } @@ -653,7 +768,7 @@ Error RemoteDebugger::_profiler_capture(const String &p_cmd, const Array &p_data return OK; } -RemoteDebugger::RemoteDebugger(Ref p_peer) { +RemoteDebugger::RemoteDebugger(Ref p_peer) { peer = p_peer; max_chars_per_second = GLOBAL_GET("network/limits/debugger/max_chars_per_second"); max_errors_per_second = GLOBAL_GET("network/limits/debugger/max_errors_per_second"); diff --git a/core/debugger/remote_debugger.h b/core/debugger/remote_debugger.h index fe4bbe86ea8e..b70f8474c712 100644 --- a/core/debugger/remote_debugger.h +++ b/core/debugger/remote_debugger.h @@ -48,7 +48,10 @@ class RemoteDebugger : public EngineDebugger { }; private: - typedef DebuggerMarshalls::OutputError ErrorMessage; + using Peer = RemoteDebuggerPeer; + using DebugThreadID = ScriptLanguageThreadContext::DebugThreadID; + using StackInfo = ScriptLanguageThreadContext::StackInfo; + using ErrorMessage = DebuggerMarshalls::OutputError; class MultiplayerProfiler; class PerformanceProfiler; @@ -98,18 +101,27 @@ class RemoteDebugger : public EngineDebugger { Error _profiler_capture(const String &p_cmd, const Array &p_data, bool &r_captured); Error _core_capture(const String &p_cmd, const Array &p_data, bool &r_captured); - template - void _bind_profiler(const String &p_name, T *p_prof); - Error _try_capture(const String &p_name, const Array &p_data, bool &r_captured); + Error _try_capture(const String &p_msg, const Array &p_data, bool &r_captured); + + void _send_debug_enter_thread(ScriptLanguageThreadContext &p_focused_thread); + void _service_channel(int p_channel); + bool _get_next_message(ScriptLanguageThreadContext &p_focused_thread, Array &r_cmd); + void _send_debug_exit_thread(ScriptLanguageThreadContext &p_focused_thread); public: // Overrides - void poll_events(bool p_is_idle); - void send_message(const String &p_message, const Array &p_args); - void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type); - void debug(bool p_can_continue = true, bool p_is_error_breakpoint = false); - - explicit RemoteDebugger(Ref p_peer); + void poll_events(bool p_is_idle) override; + void send_message(const String &p_message, const Array &p_args) override; + void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type) override; + void debug(ScriptLanguageThreadContext &p_focused_thread) override; + void request_debug(const ScriptLanguageThreadContext &p_any_thread) override; + void thread_paused(const ScriptLanguageThreadContext &p_any_thread) override; + + // Callbacks + void send_stack_frame_variables(const ScriptLanguageThreadContext &p_any_thread, int p_level); + void send_empty_stack_frame(const DebugThreadID &p_tid); + + explicit RemoteDebugger(Ref p_peer); ~RemoteDebugger(); }; diff --git a/core/debugger/remote_debugger_peer.cpp b/core/debugger/remote_debugger_peer.cpp index e9362b4ea4c6..6f37308ac2c8 100644 --- a/core/debugger/remote_debugger_peer.cpp +++ b/core/debugger/remote_debugger_peer.cpp @@ -38,44 +38,52 @@ bool RemoteDebuggerPeerTCP::is_peer_connected() { return connected; } -bool RemoteDebuggerPeerTCP::has_message() { - return in_queue.size() > 0; +bool RemoteDebuggerPeerTCP::has_message(int p_channel) { + MutexLock lock(channels[p_channel].shared.mutex); + return channels[p_channel].shared.in_queue.size() > 0; } -Array RemoteDebuggerPeerTCP::get_message() { - MutexLock lock(mutex); - ERR_FAIL_COND_V(!has_message(), Array()); - Array out = in_queue[0]; - in_queue.pop_front(); +Array RemoteDebuggerPeerTCP::get_message(int p_channel) { + MutexLock lock(channels[p_channel].shared.mutex); + ERR_FAIL_COND_V(!has_message(p_channel), Array()); + Array out = channels[p_channel].shared.in_queue[0]; + channels[p_channel].shared.in_queue.pop_front(); return out; } -Error RemoteDebuggerPeerTCP::put_message(const Array &p_arr) { - MutexLock lock(mutex); - if (out_queue.size() >= max_queued_messages) { +Error RemoteDebuggerPeerTCP::put_message(int p_channel, const Array &p_arr) { + MutexLock lock(channels[p_channel].shared.mutex); + if (channels[p_channel].shared.out_queue.size() >= max_queued_messages) { return ERR_OUT_OF_MEMORY; } - - out_queue.push_back(p_arr); + channels[p_channel].shared.out_queue.push_back(p_arr); return OK; } int RemoteDebuggerPeerTCP::get_max_message_size() const { - return 8 << 20; // 8 MiB + return MAX_MESSAGE_SIZE; // 8 MiB } -void RemoteDebuggerPeerTCP::close() { +void RemoteDebuggerPeerTCP::_close() { + // FIXME non volatile bool may never be delivered running = false; thread.wait_to_finish(); tcp_client->disconnect_from_host(); - out_buf.clear(); - in_buf.clear(); + for (Channel &channel : channels) { + channel.out_buf.clear(); + channel.in_buf.clear(); + } +} + +void RemoteDebuggerPeerTCP::close() { + _close(); } RemoteDebuggerPeerTCP::RemoteDebuggerPeerTCP(Ref p_tcp) { - // This means remote debugger takes 16 MiB just because it exists... - in_buf.resize((8 << 20) + 4); // 8 MiB should be way more than enough (need 4 extra bytes for encoding packet size). - out_buf.resize(8 << 20); // 8 MiB should be way more than enough + for (Channel &channel : channels) { + channel.in_buf.resize(MAX_MESSAGE_SIZE + 4); // Plus packet size | channel ID combo. + channel.out_buf.resize(MAX_MESSAGE_SIZE + 4); // Plus packet size | channel ID combo. + } tcp_client = p_tcp; if (tcp_client.is_valid()) { // Attaching to an already connected stream. connected = true; @@ -89,64 +97,121 @@ RemoteDebuggerPeerTCP::RemoteDebuggerPeerTCP(Ref p_tcp) { } RemoteDebuggerPeerTCP::~RemoteDebuggerPeerTCP() { - close(); + _close(); } void RemoteDebuggerPeerTCP::_write_out() { while (tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED && tcp_client->wait(NetSocket::POLL_TYPE_OUT) == OK) { - uint8_t *buf = out_buf.ptrw(); - if (out_left <= 0) { - if (out_queue.size() == 0) { - break; // Nothing left to send + if (current_write_channel < 0) { + // check in priority order + for (int channel_index : { CHANNEL_OTHER, CHANNEL_MAIN_THREAD, CHANNEL_DISCARDABLE }) { + MutexLock lock(channels[channel_index].shared.mutex); + if (channels[channel_index].shared.out_queue.size() == 0) { + continue; + } + current_write_channel = channel_index; + break; } - mutex.lock(); - Variant var = out_queue[0]; - out_queue.pop_front(); - mutex.unlock(); + } + if (current_write_channel < 0) { + // Nothing to do. + return; + } + + uint8_t *buf = channels[current_write_channel].out_buf.ptrw(); + + if (channels[current_write_channel].out_left <= 0) { + // Need to start next buffer + channels[current_write_channel].shared.mutex.lock(); + Variant var = channels[current_write_channel].shared.out_queue[0]; + channels[current_write_channel].shared.out_queue.pop_front(); + channels[current_write_channel].shared.mutex.unlock(); int size = 0; + const int OVERHEAD = 4; Error err = encode_variant(var, nullptr, size); - ERR_CONTINUE(err != OK || size > out_buf.size() - 4); // 4 bytes separator. - encode_uint32(size, buf); - encode_variant(var, buf + 4, size); - out_left = size + 4; - out_pos = 0; + if (err != OK || size > channels[current_write_channel].out_buf.size() - OVERHEAD) { + // Can't send, but we did service this item. + WARN_PRINT(vformat("Failed to send debugger message to channel %d; error %d.", current_write_channel, err)); + current_write_channel = -1; + continue; + } + static_assert(NUM_CHANNELS < 256); + static_assert(MAX_MESSAGE_SIZE < (1 << 24)); + encode_uint32(size | (current_write_channel << 24), buf); + encode_variant(var, buf + OVERHEAD, size); + channels[current_write_channel].out_left = size + OVERHEAD; + channels[current_write_channel].out_pos = 0; } + int sent = 0; - tcp_client->put_partial_data(buf + out_pos, out_left, sent); - out_left -= sent; - out_pos += sent; + tcp_client->put_partial_data(buf + channels[current_write_channel].out_pos, channels[current_write_channel].out_left, sent); + channels[current_write_channel].out_left -= sent; + channels[current_write_channel].out_pos += sent; + + if (channels[current_write_channel].out_left <= 0) { + // Done with the current transmission. + current_write_channel = -1; + } } } void RemoteDebuggerPeerTCP::_read_in() { while (tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED && tcp_client->wait(NetSocket::POLL_TYPE_IN) == OK) { - uint8_t *buf = in_buf.ptrw(); - if (in_left <= 0) { - if (in_queue.size() > max_queued_messages) { - break; // Too many messages already in queue. - } - if (tcp_client->get_available_bytes() < 4) { - break; // Need 4 more bytes. + if (current_read_channel < 0) { + // Could be reading any channel, so don't do it if any of them are blocked. + // REVISIT: this isn't completely correct. If Main is blocked on thread.join, it will never respond to any + // requests and we need to give up, so we do need per-channel flow control, i.e. multiple sockets or our + // own flow control (e.g. enet) or just conceptually tear down the Main connection and discard all Main + // messages. + int index = 0; + for (Channel &channel : channels) { + MutexLock lock(channel.shared.mutex); + if (channel.in_left <= 0) { + if (channel.shared.in_queue.size() > max_queued_messages) { + if (index == CHANNEL_DISCARDABLE) { + // Drop oldest message, since it is the most stale. + channel.shared.in_queue.pop_front(); + } else { + return; // Too many messages already in queue. + } + } + if (tcp_client->get_available_bytes() < 4) { + return; // Need 4 more bytes. + } + } + ++index; } uint32_t size = 0; int read = 0; - Error err = tcp_client->get_partial_data((uint8_t *)&size, 4, read); - ERR_CONTINUE(read != 4 || err != OK || size > (uint32_t)in_buf.size()); - in_left = size; - in_pos = 0; + const Error err = tcp_client->get_partial_data(reinterpret_cast(&size), 4, read); + ERR_CONTINUE(read != 4 || err != OK); + current_read_channel = static_cast(size >> 24U & 0xffU); + size = size & 0xffffffU; + if (current_read_channel < 0 || current_read_channel >= NUM_CHANNELS) { + WARN_PRINT(vformat("Ignored message with invalid channel ID: %d out of 0..%d", current_read_channel, NUM_CHANNELS - 1)); + current_read_channel = -1; + continue; + } + ERR_CONTINUE(size > static_cast(channels[current_read_channel].in_buf.size())); + channels[current_read_channel].in_left = size; + channels[current_read_channel].in_pos = 0; } + uint8_t *buf = channels[current_read_channel].in_buf.ptrw(); int read = 0; - tcp_client->get_partial_data(buf + in_pos, in_left, read); - in_left -= read; - in_pos += read; - if (in_left == 0) { + tcp_client->get_partial_data(buf + channels[current_read_channel].in_pos, channels[current_read_channel].in_left, read); + channels[current_read_channel].in_left -= read; + channels[current_read_channel].in_pos += read; + if (channels[current_read_channel].in_left == 0) { + // Restart with size marker, even if we error out below. + const int was_reading_channel = current_read_channel; + current_read_channel = -1; + Variant var; - Error err = decode_variant(var, buf, in_pos, &read); - ERR_CONTINUE(read != in_pos || err != OK); + const Error err = decode_variant(var, buf, channels[was_reading_channel].in_pos, &read); + ERR_CONTINUE(read != channels[was_reading_channel].in_pos || err != OK); ERR_CONTINUE_MSG(var.get_type() != Variant::ARRAY, "Malformed packet received, not an Array."); - mutex.lock(); - in_queue.push_back(var); - mutex.unlock(); + MutexLock lock(channels[was_reading_channel].shared.mutex); + channels[was_reading_channel].shared.in_queue.push_back(var); } } } @@ -159,7 +224,7 @@ Error RemoteDebuggerPeerTCP::connect_to_host(const String &p_host, uint16_t p_po ip = IP::get_singleton()->resolve_hostname(p_host); } - int port = p_port; + const int port = p_port; const int tries = 6; const int waits[tries] = { 1, 10, 100, 1000, 1000, 1000 }; @@ -192,11 +257,14 @@ Error RemoteDebuggerPeerTCP::connect_to_host(const String &p_host, uint16_t p_po void RemoteDebuggerPeerTCP::_thread_func(void *p_ud) { // Update in time for 144hz monitors + // FIXME that precision is nonsense, we don't have that kind of resolution from sleeping (ms on Windows, scheduling intervals, etc.) const uint64_t min_tick = 6900; RemoteDebuggerPeerTCP *peer = static_cast(p_ud); while (peer->running && peer->is_peer_connected()) { uint64_t ticks_usec = OS::get_singleton()->get_ticks_usec(); - peer->_poll(); + for (int channel_index = 0; channel_index < NUM_CHANNELS; ++channel_index) { + peer->_poll(channel_index); + } if (!peer->is_peer_connected()) { break; } @@ -207,17 +275,27 @@ void RemoteDebuggerPeerTCP::_thread_func(void *p_ud) { } } -void RemoteDebuggerPeerTCP::poll() { +void RemoteDebuggerPeerTCP::poll(int p_channel) { #ifdef NO_THREADS - _poll(); + if (p_channel == CHANNEL_MAIN_THREAD) { + // Block only for the first channel, because they are actually all the same socket. + _poll(p_channel); + } +#else + (void)p_channel; #endif } -void RemoteDebuggerPeerTCP::_poll() { +void RemoteDebuggerPeerTCP::_poll(const int p_channel) { tcp_client->poll(); if (connected) { - _write_out(); - _read_in(); + // In this implementation, we only have one connection on the wire, so + // we can't send from multiple channels fragmented. So we have to + // actually process all channels together. + if (p_channel == CHANNEL_MAIN_THREAD) { + _read_in(); + _write_out(); + } connected = tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED; } } @@ -229,13 +307,13 @@ RemoteDebuggerPeer *RemoteDebuggerPeerTCP::create(const String &p_uri) { uint16_t debug_port = 6007; if (debug_host.contains(":")) { - int sep_pos = debug_host.rfind(":"); + const int sep_pos = debug_host.rfind(":"); debug_port = debug_host.substr(sep_pos + 1).to_int(); debug_host = debug_host.substr(0, sep_pos); } RemoteDebuggerPeerTCP *peer = memnew(RemoteDebuggerPeerTCP); - Error err = peer->connect_to_host(debug_host, debug_port); + const Error err = peer->connect_to_host(debug_host, debug_port); if (err != OK) { memdelete(peer); return nullptr; diff --git a/core/debugger/remote_debugger_peer.h b/core/debugger/remote_debugger_peer.h index 473fd8d712d4..7a6fba8373ac 100644 --- a/core/debugger/remote_debugger_peer.h +++ b/core/debugger/remote_debugger_peer.h @@ -34,7 +34,6 @@ #include "core/io/stream_peer_tcp.h" #include "core/object/ref_counted.h" #include "core/os/mutex.h" -#include "core/os/thread.h" #include "core/string/ustring.h" class RemoteDebuggerPeer : public RefCounted { @@ -42,54 +41,94 @@ class RemoteDebuggerPeer : public RefCounted { int max_queued_messages = 4096; public: + enum { + CHANNEL_MAIN_THREAD = 0, + CHANNEL_OTHER, + CHANNEL_DISCARDABLE, + NUM_CHANNELS + }; + virtual bool is_peer_connected() = 0; - virtual bool has_message() = 0; - virtual Error put_message(const Array &p_arr) = 0; - virtual Array get_message() = 0; + virtual bool has_message(int p_channel) = 0; + virtual Error put_message(int p_channel, const Array &p_arr) = 0; + virtual Array get_message(int p_channel) = 0; virtual void close() = 0; - virtual void poll() = 0; + virtual void poll(int p_channel) = 0; virtual int get_max_message_size() const = 0; - virtual bool can_block() const { return true; } // If blocking io is allowed on main thread (debug). + + // If blocking io is allowed on main thread (debug). + virtual bool can_block(int p_channel) const { + (void)p_channel; + return true; + } RemoteDebuggerPeer(); }; class RemoteDebuggerPeerTCP : public RemoteDebuggerPeer { -private: Ref tcp_client; - Mutex mutex; Thread thread; - List in_queue; - List out_queue; - int out_left = 0; - int out_pos = 0; - Vector out_buf; - int in_left = 0; - int in_pos = 0; - Vector in_buf; bool connected = false; bool running = false; + // Each of these is a separately locked in and out queue, which are conceptually + // like SCTP streams that don't block each other. + struct Channel { + // Accessed only by TCP thread. + int out_left = 0; + int out_pos = 0; + Vector out_buf; + int in_left = 0; + int in_pos = 0; + Vector in_buf; + + // Shared with clients under lock. + struct Shared { + Mutex mutex; + List in_queue; + List out_queue; + }; + Shared shared; + }; + + Channel channels[NUM_CHANNELS]; + + // If >= 0, this channel has a partial read in progress. + int current_read_channel = -1; + + // If >= 0, this channel has a partial write in progress. + int current_write_channel = -1; + static void _thread_func(void *p_ud); - void _poll(); + void _poll(int p_channel); void _write_out(); void _read_in(); + // REVISIT This means remote debugger takes 16 MiB just because it exists (in/out buffers). + enum { + MAX_MESSAGE_SIZE = 8 << 20 + }; + +protected: + // Descendants must call this manually from _close, because the virtual + // close() is used in the destructor and won't actually call up. + void _close(); + public: static RemoteDebuggerPeer *create(const String &p_uri); Error connect_to_host(const String &p_host, uint16_t p_port); - void poll() override; + void poll(int p_channel) override; bool is_peer_connected() override; - bool has_message() override; - Array get_message() override; - Error put_message(const Array &p_arr) override; + bool has_message(int p_channel) override; + Array get_message(int p_channel) override; + Error put_message(int p_channel, const Array &p_arr) override; int get_max_message_size() const override; void close() override; - RemoteDebuggerPeerTCP(Ref p_stream = Ref()); + explicit RemoteDebuggerPeerTCP(Ref p_tcp = Ref()); ~RemoteDebuggerPeerTCP(); }; diff --git a/core/debugger/script_debugger.cpp b/core/debugger/script_debugger.cpp index e30f3e7886e6..318495a6cd1c 100644 --- a/core/debugger/script_debugger.cpp +++ b/core/debugger/script_debugger.cpp @@ -32,22 +32,6 @@ #include "core/debugger/engine_debugger.h" -void ScriptDebugger::set_lines_left(int p_left) { - lines_left = p_left; -} - -int ScriptDebugger::get_lines_left() const { - return lines_left; -} - -void ScriptDebugger::set_depth(int p_depth) { - depth = p_depth; -} - -int ScriptDebugger::get_depth() const { - return depth; -} - void ScriptDebugger::insert_breakpoint(int p_line, const StringName &p_source) { if (!breakpoints.has(p_line)) { breakpoints[p_line] = HashSet(); @@ -93,24 +77,131 @@ bool ScriptDebugger::is_skipping_breakpoints() { return skip_breakpoints; } -void ScriptDebugger::debug(ScriptLanguage *p_lang, bool p_can_continue, bool p_is_error_breakpoint) { - ScriptLanguage *prev = break_lang; - break_lang = p_lang; - EngineDebugger::get_singleton()->debug(p_can_continue, p_is_error_breakpoint); - break_lang = prev; +bool ScriptDebugger::_try_claim_debugger(const ScriptLanguageThreadContext &p_any_thread) { + MutexLock lock(_mutex_thread_transfer); + if (_focused_thread.is_valid() && !_focused_thread->is_dead()) { + return false; + } + _focused_thread = Ref(&p_any_thread); + return true; } -void ScriptDebugger::send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type, const Vector &p_stack_info) { - // Store stack info, this is ugly, but allows us to separate EngineDebugger and ScriptDebugger. There might be a better way. - error_stack_info.append_array(p_stack_info); - EngineDebugger::get_singleton()->send_error(p_func, p_file, p_line, p_err, p_descr, p_editor_notify, p_type); - error_stack_info.clear(); +void ScriptDebugger::step(ScriptLanguageThreadContext &p_any_thread) { + if (p_any_thread.is_main_thread()) { + EngineDebugger::get_singleton()->poll_events(false); + } + + if (_break_requested.is_set()) { + // Warning: there is a race condition here: Multiple threads + // may get this far, but that can be safely ignored. Those threads + // will simply compete for debug(...) normally, just like multiple threads + // hitting breakpoints. + _break_requested.clear(); + debug(p_any_thread); + // We may have failed to debug, continue below like a secondary thread. + } + + if (!_hold_threads.is_set()) { + // Only pause if requested. This is also where we return after successful debugging. + return; + } + + // Check in this context (now conceptually owned by debugger.) + const DebugThreadID tid = p_any_thread.debug_get_thread_id(); + { + MutexLock lock(_mutex_thread_transfer); + // Check again because we could have just been resumed and we + // were blocked while the previous batch was cleaning out. + if (!_hold_threads.is_set()) { + return; + } + _held_threads[tid] = Ref(&p_any_thread); + } + + EngineDebugger::get_singleton()->thread_paused(p_any_thread); + if (p_any_thread.is_main_thread()) { + // Keep servicing non-core debug messages while waiting to resume. + while (!p_any_thread.wait_resume_ms(1)) { + EngineDebugger::get_singleton()->poll_events(false); + } + } else { + p_any_thread.wait_resume(); + } + + // Reclaim our context to resume running. + MutexLock lock(_mutex_thread_transfer); + _held_threads.erase(tid); } -Vector ScriptDebugger::get_error_stack_info() const { - return error_stack_info; +void ScriptDebugger::debug_request_break() { + _break_requested.set(); } -ScriptLanguage *ScriptDebugger::get_break_language() const { - return break_lang; +void ScriptDebugger::debug(ScriptLanguageThreadContext &p_any_thread) { + const bool is_first = _try_claim_debugger(p_any_thread); + + if (!is_first) { + // A thread is already being debugged, just indicate that this thread has also hit a breakpoint or error. + EngineDebugger::get_singleton()->request_debug(p_any_thread); + + // Suspend on step() like all the other extra threads. + return; + } + + if (_hold_other_threads_on_debug_start) { + _hold_threads.set(); + } else { + _hold_threads.clear(); + } + + // This thread is also available for interrogation, so check it in, + // now conceptually owned by debugger. + // Not using smart lock here because of smart lock on same mutex + // below (in case of very poor optimizing compiler.) + const DebugThreadID tid = p_any_thread.debug_get_thread_id(); + _mutex_thread_transfer.lock(); + _held_threads[tid] = Ref(&p_any_thread); + _mutex_thread_transfer.unlock(); + + // Run the "OTHER" channel of debugging protocol on this thread, since it is the only thread that is + // guaranteed not to block. If Main gets blocked, then secondary debug captures won't get replies. + EngineDebugger::get_singleton()->debug(p_any_thread); + + { + MutexLock lock(_mutex_thread_transfer); + + // We don't need to be resumed. + _held_threads.erase(tid); + + // Release the debugger, so others can claim it. + const bool wrong_thread_released = _focused_thread.ptr() != &p_any_thread; + _focused_thread.unref(); + + // Resume everyone. + _hold_threads.clear(); + for (const KeyValue> &thread : _held_threads) { + // Thread will remove itself. + thread.value->resume(); + } + + ERR_FAIL_COND_MSG(wrong_thread_released, "Debugged thread reference was corrupted during debugging; please report broken implementation."); + } +} + +void ScriptDebugger::try_get_stack_dump(const DebugThreadID &p_tid, DebuggerMarshalls::ScriptStackDump &p_dump) { + MutexLock lock(_mutex_thread_transfer); + const ConstHeldThreadsIterator it = _held_threads.find(p_tid); + if (it != _held_threads.end()) { + p_dump.populate(**(it->value)); + } +} + +void ScriptDebugger::try_send_stack_frame_variables(const DebugThreadID &p_tid, int p_level, RemoteDebugger &p_remote_debugger) { + MutexLock lock(_mutex_thread_transfer); + const ConstHeldThreadsIterator it = _held_threads.find(p_tid); + if (it != _held_threads.end()) { + p_remote_debugger.send_stack_frame_variables(**(it->value), p_level); + } else { + p_remote_debugger.send_empty_stack_frame(p_tid); + } } diff --git a/core/debugger/script_debugger.h b/core/debugger/script_debugger.h index 5124b357a5c7..25e8a7b4a829 100644 --- a/core/debugger/script_debugger.h +++ b/core/debugger/script_debugger.h @@ -34,31 +34,41 @@ #include "core/object/script_language.h" #include "core/string/string_name.h" #include "core/templates/hash_set.h" -#include "core/templates/rb_map.h" #include "core/templates/vector.h" +#include "debugger_marshalls.h" +#include "remote_debugger.h" class ScriptDebugger { - typedef ScriptLanguage::StackInfo StackInfo; + using DebugThreadID = ScriptLanguageThreadContext::DebugThreadID; + using StackInfo = ScriptLanguageThreadContext::StackInfo; - int lines_left = -1; - int depth = -1; bool skip_breakpoints = false; HashMap> breakpoints; - ScriptLanguage *break_lang = nullptr; - Vector error_stack_info; + // TODO remote configuration? + bool _hold_other_threads_on_debug_start = true; -public: - void set_lines_left(int p_left); - int get_lines_left() const; + // If true, all non-focused threads hold on step execute also. + SafeFlag _hold_threads; + + // If true, there is a pending async request for break on any thread (usually from ctrl-c or similar.) + SafeFlag _break_requested; + + // Ownership of the thread context of a paused thread is temporarily granted to + // the debugger by storing it into _focused_thread or _held_threads under lock + // of _mutex_thread_transfer. + BinaryMutex _mutex_thread_transfer; + Ref _focused_thread; + HashMap, VariantHasher, VariantComparator> _held_threads; + typedef HashMap, VariantHasher, VariantComparator>::ConstIterator ConstHeldThreadsIterator; - void set_depth(int p_depth); - int get_depth() const; + bool _try_claim_debugger(const ScriptLanguageThreadContext &p_any_thread); + +public: + /* BREAKPOINTS */ String breakpoint_find_source(const String &p_source) const; - void set_break_language(ScriptLanguage *p_lang) { break_lang = p_lang; } - ScriptLanguage *get_break_language() { return break_lang; } void set_skip_breakpoints(bool p_skip_breakpoints); bool is_skipping_breakpoints(); void insert_breakpoint(int p_line, const StringName &p_source); @@ -68,12 +78,25 @@ class ScriptDebugger { void clear_breakpoints(); const HashMap> &get_breakpoints() const { return breakpoints; } - void debug(ScriptLanguage *p_lang, bool p_can_continue = true, bool p_is_error_breakpoint = false); - ScriptLanguage *get_break_language() const; + /* DEBUGGING */ + + // Start new debugging session, from breakpoint or error, expected to block the caller being debugged. + void debug(ScriptLanguageThreadContext &p_any_thread); + + // Called from all threads that are not the thread currently being debugged ("focused thread") + // for every opcode while debugger is active, may block the caller. + void step(ScriptLanguageThreadContext &p_any_thread); + + // Asynchronously request break by the next script language to execute something. + void debug_request_break(); + + // TODO can LocalDebugger use this or does this need a callback as below? + void try_get_stack_dump(const DebugThreadID &p_tid, DebuggerMarshalls::ScriptStackDump &p_dump); + + // FIXME make this not depend on RemoteDebugger + void try_send_stack_frame_variables(const DebugThreadID &p_tid, int p_level, RemoteDebugger &p_remote_debugger); - void send_error(const String &p_func, const String &p_file, int p_line, const String &p_err, const String &p_descr, bool p_editor_notify, ErrorHandlerType p_type, const Vector &p_stack_info); - Vector get_error_stack_info() const; - ScriptDebugger() {} + ScriptDebugger() = default; }; #endif // SCRIPT_DEBUGGER_H diff --git a/core/object/script_language.cpp b/core/object/script_language.cpp index e56d2e80b902..481828d4de9b 100644 --- a/core/object/script_language.cpp +++ b/core/object/script_language.cpp @@ -46,16 +46,6 @@ bool ScriptServer::reload_scripts_on_save = false; bool ScriptServer::languages_finished = false; ScriptEditRequestFunction ScriptServer::edit_request_func = nullptr; -void Script::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_POSTINITIALIZE: { - if (EngineDebugger::is_active()) { - EngineDebugger::get_script_debugger()->set_break_language(get_language()); - } - } break; - } -} - Variant Script::_get_property_default_value(const StringName &p_property) { Variant ret; get_property_default_value(p_property, ret); @@ -129,7 +119,7 @@ PropertyInfo Script::get_class_category() const { void Script::_bind_methods() { ClassDB::bind_method(D_METHOD("can_instantiate"), &Script::can_instantiate); - //ClassDB::bind_method(D_METHOD("instance_create","base_object"),&Script::instance_create); + // ClassDB::bind_method(D_METHOD("instance_create","base_object"),&Script::instance_create); ClassDB::bind_method(D_METHOD("instance_has", "base_object"), &Script::instance_has); ClassDB::bind_method(D_METHOD("has_source_code"), &Script::has_source_code); ClassDB::bind_method(D_METHOD("get_source_code"), &Script::get_source_code); @@ -183,7 +173,7 @@ void ScriptServer::unregister_language(const ScriptLanguage *p_language) { } void ScriptServer::init_languages() { - { //load global classes + { // load global classes global_classes_clear(); if (ProjectSettings::get_singleton()->has_setting("_global_script_classes")) { Array script_classes = ProjectSettings::get_singleton()->get("_global_script_classes"); @@ -199,7 +189,7 @@ void ScriptServer::init_languages() { } for (int i = 0; i < _language_count; i++) { - _languages[i]->init(); + _languages[i]->init(i); } } @@ -353,14 +343,94 @@ Variant ScriptInstance::property_get_fallback(const StringName &, bool *r_valid) return Variant(); } -ScriptInstance::~ScriptInstance() { -} +ScriptInstance::~ScriptInstance() = default; ScriptCodeCompletionCache *ScriptCodeCompletionCache::singleton = nullptr; ScriptCodeCompletionCache::ScriptCodeCompletionCache() { singleton = this; } +bool ScriptLanguageThreadContext::debug_record_step_taken() { + if (steps_left > 0) { + if (frames_left <= 0) { + --steps_left; + } + if (steps_left <= 0) { + return true; + } + } + return false; +} + +void ScriptLanguageThreadContext::debug_record_enter_frame() { + if (steps_left > 0 && frames_left >= 0) { + // Need to exit from this frame before steps count again. + ++frames_left; + } +} + +void ScriptLanguageThreadContext::debug_record_exit_frame() { + if (steps_left > 0 && frames_left >= 0) { + // Pop out until we start consuming steps again. + --frames_left; + } +} + +void ScriptLanguageThreadContext::wait_resume() const { + resumption.wait(); +} + +bool ScriptLanguageThreadContext::wait_resume_ms(int p_milliseconds) const { + // REVISIT: Godot Semaphore does not expose std::condition_variable::wait_for, + // so we just fake it by sleeping. The downside is that we will wait up to + // max(p_milliseconds, minimum OS timer interval) too long after the + // semaphore is set. + if (resumption.try_wait()) { + return true; + } + OS::get_singleton()->delay_usec(p_milliseconds * 1000); + return false; +} + +void ScriptLanguageThreadContext::resume() const { + resumption.post(); +} + +bool ScriptLanguageThreadContext::is_dead() const { + // TODO implement: associated thread may have died without removing this from all collections + return false; +} + +void ScriptLanguageThreadContext::debug_step() { + frames_left = -1; + steps_left = 1; +} + +void ScriptLanguageThreadContext::debug_next() { + // REVISIT: how does this work? + frames_left = 0; + steps_left = 1; +} + +void ScriptLanguageThreadContext::debug_step_out() { + // REVISIT: how does this work? why is it used for stepping out of frames and not 1,0? + frames_left = 0; + steps_left = 1; +} + +void ScriptLanguageThreadContext::debug_continue() { + frames_left = -1; + steps_left = -1; +} + +void ScriptLanguage::init(int p_language_index) { + language_index = p_language_index; +} + +int ScriptLanguage::get_language_index() const { + return language_index; +} + void ScriptLanguage::get_core_type_words(List *p_core_type_words) const { p_core_type_words->push_back("String"); p_core_type_words->push_back("Vector2"); @@ -531,7 +601,7 @@ void PlaceHolderScriptInstance::update(const List &p_properties, c Variant defval; if (script->get_property_default_value(E.key, defval)) { - //remove because it's the same as the default value + // remove because it's the same as the default value if (defval == E.value) { to_remove.push_back(E.key); } @@ -546,7 +616,7 @@ void PlaceHolderScriptInstance::update(const List &p_properties, c if (owner && owner->get_script_instance() == this) { owner->notify_property_list_changed(); } - //change notify + // change notify constants.clear(); script->get_constants(&constants); diff --git a/core/object/script_language.h b/core/object/script_language.h index 12a21150bc64..e50644b4ccaa 100644 --- a/core/object/script_language.h +++ b/core/object/script_language.h @@ -33,8 +33,9 @@ #include "core/doc_data.h" #include "core/io/resource.h" +#include "core/os/semaphore.h" +#include "core/os/thread.h" #include "core/templates/pair.h" -#include "core/templates/rb_map.h" class ScriptLanguage; template @@ -43,10 +44,12 @@ class TypedArray; typedef void (*ScriptEditRequestFunction)(const String &p_path); class ScriptServer { +public: enum { MAX_LANGUAGES = 16 }; +private: static ScriptLanguage *_languages[MAX_LANGUAGES]; static int _language_count; static bool scripting_enabled; @@ -103,7 +106,6 @@ class Script : public Resource { protected: virtual bool editor_can_reload_from_file() override { return false; } // this is handled by editor better - void _notification(int p_what); static void _bind_methods(); friend class PlaceHolderScriptInstance; @@ -118,7 +120,7 @@ class Script : public Resource { public: virtual bool can_instantiate() const = 0; - virtual Ref