initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled
This commit is contained in:
6
core/debugger/SCsub
Normal file
6
core/debugger/SCsub
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
from misc.utility.scons_hints import *
|
||||
|
||||
Import("env")
|
||||
|
||||
env.add_source_files(env.core_sources, "*.cpp")
|
179
core/debugger/debugger_marshalls.cpp
Normal file
179
core/debugger/debugger_marshalls.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
/**************************************************************************/
|
||||
/* debugger_marshalls.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 "debugger_marshalls.h"
|
||||
|
||||
#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()))
|
||||
|
||||
Array DebuggerMarshalls::ScriptStackDump::serialize() {
|
||||
Array arr = { frames.size() * 3 };
|
||||
for (const ScriptLanguage::StackInfo &frame : frames) {
|
||||
arr.push_back(frame.file);
|
||||
arr.push_back(frame.line);
|
||||
arr.push_back(frame.func);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
bool DebuggerMarshalls::ScriptStackDump::deserialize(const Array &p_arr) {
|
||||
CHECK_SIZE(p_arr, 1, "ScriptStackDump");
|
||||
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;
|
||||
sf.file = p_arr[idx];
|
||||
sf.line = p_arr[idx + 1];
|
||||
sf.func = p_arr[idx + 2];
|
||||
frames.push_back(sf);
|
||||
idx += 3;
|
||||
}
|
||||
CHECK_END(p_arr, idx, "ScriptStackDump");
|
||||
return true;
|
||||
}
|
||||
|
||||
Array DebuggerMarshalls::ScriptStackVariable::serialize(int max_size) {
|
||||
Array arr = { name, type, value.get_type() };
|
||||
|
||||
Variant var = value;
|
||||
if (value.get_type() == Variant::OBJECT && value.get_validated_object() == nullptr) {
|
||||
var = Variant();
|
||||
}
|
||||
|
||||
int len = 0;
|
||||
Error err = encode_variant(var, nullptr, len, false);
|
||||
if (err != OK) {
|
||||
ERR_PRINT("Failed to encode variant.");
|
||||
}
|
||||
|
||||
if (len > max_size) {
|
||||
arr.push_back(Variant());
|
||||
} else {
|
||||
arr.push_back(var);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
bool DebuggerMarshalls::ScriptStackVariable::deserialize(const Array &p_arr) {
|
||||
CHECK_SIZE(p_arr, 4, "ScriptStackVariable");
|
||||
name = p_arr[0];
|
||||
type = p_arr[1];
|
||||
var_type = p_arr[2];
|
||||
value = p_arr[3];
|
||||
CHECK_END(p_arr, 4, "ScriptStackVariable");
|
||||
return true;
|
||||
}
|
||||
|
||||
Array DebuggerMarshalls::OutputError::serialize() {
|
||||
unsigned int size = callstack.size();
|
||||
Array arr = {
|
||||
hr,
|
||||
min,
|
||||
sec, msec,
|
||||
source_file,
|
||||
source_func,
|
||||
source_line,
|
||||
error,
|
||||
error_descr,
|
||||
warning,
|
||||
size * 3
|
||||
};
|
||||
const ScriptLanguage::StackInfo *r = callstack.ptr();
|
||||
for (int i = 0; i < callstack.size(); i++) {
|
||||
arr.push_back(r[i].file);
|
||||
arr.push_back(r[i].func);
|
||||
arr.push_back(r[i].line);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
bool DebuggerMarshalls::OutputError::deserialize(const Array &p_arr) {
|
||||
CHECK_SIZE(p_arr, 11, "OutputError");
|
||||
hr = p_arr[0];
|
||||
min = p_arr[1];
|
||||
sec = p_arr[2];
|
||||
msec = p_arr[3];
|
||||
source_file = p_arr[4];
|
||||
source_func = p_arr[5];
|
||||
source_line = p_arr[6];
|
||||
error = p_arr[7];
|
||||
error_descr = p_arr[8];
|
||||
warning = p_arr[9];
|
||||
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();
|
||||
for (unsigned int i = 0; i < stack_size / 3; i++) {
|
||||
w[i].file = p_arr[idx];
|
||||
w[i].func = p_arr[idx + 1];
|
||||
w[i].line = p_arr[idx + 2];
|
||||
idx += 3;
|
||||
}
|
||||
CHECK_END(p_arr, idx, "OutputError");
|
||||
return true;
|
||||
}
|
||||
|
||||
Array DebuggerMarshalls::serialize_key_shortcut(const Ref<Shortcut> &p_shortcut) {
|
||||
ERR_FAIL_COND_V(p_shortcut.is_null(), Array());
|
||||
Array keys;
|
||||
for (const Ref<InputEvent> ev : p_shortcut->get_events()) {
|
||||
const Ref<InputEventKey> kev = ev;
|
||||
ERR_CONTINUE(kev.is_null());
|
||||
if (kev->get_physical_keycode() != Key::NONE) {
|
||||
keys.push_back(true);
|
||||
keys.push_back(kev->get_physical_keycode_with_modifiers());
|
||||
} else {
|
||||
keys.push_back(false);
|
||||
keys.push_back(kev->get_keycode_with_modifiers());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
Ref<Shortcut> DebuggerMarshalls::deserialize_key_shortcut(const Array &p_keys) {
|
||||
Array key_events;
|
||||
ERR_FAIL_COND_V(p_keys.size() % 2 != 0, Ref<Shortcut>());
|
||||
for (int i = 0; i < p_keys.size(); i += 2) {
|
||||
ERR_CONTINUE(p_keys[i].get_type() != Variant::BOOL);
|
||||
ERR_CONTINUE(p_keys[i + 1].get_type() != Variant::INT);
|
||||
key_events.push_back(InputEventKey::create_reference((Key)p_keys[i + 1].operator int(), p_keys[i].operator bool()));
|
||||
}
|
||||
if (key_events.is_empty()) {
|
||||
return Ref<Shortcut>();
|
||||
}
|
||||
Ref<Shortcut> shortcut;
|
||||
shortcut.instantiate();
|
||||
shortcut->set_events(key_events);
|
||||
return shortcut;
|
||||
}
|
74
core/debugger/debugger_marshalls.h
Normal file
74
core/debugger/debugger_marshalls.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/**************************************************************************/
|
||||
/* debugger_marshalls.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/input/shortcut.h"
|
||||
#include "core/object/script_language.h"
|
||||
|
||||
struct DebuggerMarshalls {
|
||||
struct ScriptStackVariable {
|
||||
String name;
|
||||
Variant value;
|
||||
int type = -1;
|
||||
int var_type = -1;
|
||||
|
||||
Array serialize(int max_size = 1 << 20); // 1 MiB default.
|
||||
bool deserialize(const Array &p_arr);
|
||||
};
|
||||
|
||||
struct ScriptStackDump {
|
||||
List<ScriptLanguage::StackInfo> frames;
|
||||
ScriptStackDump() {}
|
||||
|
||||
Array serialize();
|
||||
bool deserialize(const Array &p_arr);
|
||||
};
|
||||
|
||||
struct OutputError {
|
||||
int hr = -1;
|
||||
int min = -1;
|
||||
int sec = -1;
|
||||
int msec = -1;
|
||||
String source_file;
|
||||
String source_func;
|
||||
int source_line = -1;
|
||||
String error;
|
||||
String error_descr;
|
||||
bool warning = false;
|
||||
Vector<ScriptLanguage::StackInfo> callstack;
|
||||
|
||||
Array serialize();
|
||||
bool deserialize(const Array &p_arr);
|
||||
};
|
||||
|
||||
static Array serialize_key_shortcut(const Ref<Shortcut> &p_shortcut);
|
||||
static Ref<Shortcut> deserialize_key_shortcut(const Array &p_keys);
|
||||
};
|
196
core/debugger/engine_debugger.cpp
Normal file
196
core/debugger/engine_debugger.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
/**************************************************************************/
|
||||
/* engine_debugger.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 "engine_debugger.h"
|
||||
|
||||
#include "core/debugger/local_debugger.h"
|
||||
#include "core/debugger/remote_debugger.h"
|
||||
#include "core/debugger/remote_debugger_peer.h"
|
||||
#include "core/debugger/script_debugger.h"
|
||||
#include "core/os/os.h"
|
||||
|
||||
void (*EngineDebugger::allow_focus_steal_fn)();
|
||||
|
||||
void EngineDebugger::register_profiler(const StringName &p_name, const Profiler &p_func) {
|
||||
ERR_FAIL_COND_MSG(profilers.has(p_name), vformat("Profiler already registered: '%s'.", p_name));
|
||||
profilers.insert(p_name, p_func);
|
||||
}
|
||||
|
||||
void EngineDebugger::unregister_profiler(const StringName &p_name) {
|
||||
ERR_FAIL_COND_MSG(!profilers.has(p_name), vformat("Profiler not registered: '%s'.", p_name));
|
||||
Profiler &p = profilers[p_name];
|
||||
if (p.active && p.toggle) {
|
||||
p.toggle(p.data, false, Array());
|
||||
p.active = false;
|
||||
}
|
||||
profilers.erase(p_name);
|
||||
}
|
||||
|
||||
void EngineDebugger::register_message_capture(const StringName &p_name, Capture p_func) {
|
||||
ERR_FAIL_COND_MSG(captures.has(p_name), vformat("Capture already registered: '%s'.", p_name));
|
||||
captures.insert(p_name, p_func);
|
||||
}
|
||||
|
||||
void EngineDebugger::unregister_message_capture(const StringName &p_name) {
|
||||
ERR_FAIL_COND_MSG(!captures.has(p_name), vformat("Capture not registered: '%s'.", p_name));
|
||||
captures.erase(p_name);
|
||||
}
|
||||
|
||||
void EngineDebugger::register_uri_handler(const String &p_protocol, CreatePeerFunc p_func) {
|
||||
ERR_FAIL_COND_MSG(protocols.has(p_protocol), vformat("Protocol handler already registered: '%s'.", p_protocol));
|
||||
protocols.insert(p_protocol, p_func);
|
||||
}
|
||||
|
||||
void EngineDebugger::profiler_enable(const StringName &p_name, bool p_enabled, const Array &p_opts) {
|
||||
ERR_FAIL_COND_MSG(!profilers.has(p_name), vformat("Can't change profiler state, no profiler: '%s'.", p_name));
|
||||
Profiler &p = profilers[p_name];
|
||||
if (p.toggle) {
|
||||
p.toggle(p.data, p_enabled, p_opts);
|
||||
}
|
||||
p.active = p_enabled;
|
||||
}
|
||||
|
||||
void EngineDebugger::profiler_add_frame_data(const StringName &p_name, const Array &p_data) {
|
||||
ERR_FAIL_COND_MSG(!profilers.has(p_name), vformat("Can't add frame data, no profiler: '%s'.", p_name));
|
||||
Profiler &p = profilers[p_name];
|
||||
if (p.add) {
|
||||
p.add(p.data, p_data);
|
||||
}
|
||||
}
|
||||
|
||||
bool EngineDebugger::is_profiling(const StringName &p_name) {
|
||||
return profilers.has(p_name) && profilers[p_name].active;
|
||||
}
|
||||
|
||||
bool EngineDebugger::has_profiler(const StringName &p_name) {
|
||||
return profilers.has(p_name);
|
||||
}
|
||||
|
||||
bool EngineDebugger::has_capture(const StringName &p_name) {
|
||||
return captures.has(p_name);
|
||||
}
|
||||
|
||||
Error EngineDebugger::capture_parse(const StringName &p_name, const String &p_msg, const Array &p_args, bool &r_captured) {
|
||||
r_captured = false;
|
||||
ERR_FAIL_COND_V_MSG(!captures.has(p_name), ERR_UNCONFIGURED, vformat("Capture not registered: '%s'.", p_name));
|
||||
const Capture &cap = captures[p_name];
|
||||
return cap.capture(cap.data, p_msg, p_args, r_captured);
|
||||
}
|
||||
|
||||
void EngineDebugger::iteration(uint64_t p_frame_ticks, uint64_t p_process_ticks, uint64_t p_physics_ticks, double p_physics_frame_time) {
|
||||
frame_time = USEC_TO_SEC(p_frame_ticks);
|
||||
process_time = USEC_TO_SEC(p_process_ticks);
|
||||
physics_time = USEC_TO_SEC(p_physics_ticks);
|
||||
physics_frame_time = p_physics_frame_time;
|
||||
// Notify tick to running profilers
|
||||
for (KeyValue<StringName, Profiler> &E : profilers) {
|
||||
Profiler &p = E.value;
|
||||
if (!p.active || !p.tick) {
|
||||
continue;
|
||||
}
|
||||
p.tick(p.data, frame_time, process_time, physics_time, physics_frame_time);
|
||||
}
|
||||
singleton->poll_events(true);
|
||||
}
|
||||
|
||||
void EngineDebugger::initialize(const String &p_uri, bool p_skip_breakpoints, bool p_ignore_error_breaks, const Vector<String> &p_breakpoints, void (*p_allow_focus_steal_fn)()) {
|
||||
register_uri_handler("tcp://", RemoteDebuggerPeerTCP::create); // TCP is the default protocol. Platforms/modules can add more.
|
||||
if (p_uri.is_empty()) {
|
||||
return;
|
||||
}
|
||||
if (p_uri == "local://") {
|
||||
singleton = memnew(LocalDebugger);
|
||||
script_debugger = memnew(ScriptDebugger);
|
||||
// Tell the OS that we want to handle termination signals.
|
||||
OS::get_singleton()->initialize_debugging();
|
||||
} else if (p_uri.contains("://")) {
|
||||
const String proto = p_uri.substr(0, p_uri.find("://") + 3);
|
||||
if (!protocols.has(proto)) {
|
||||
return;
|
||||
}
|
||||
RemoteDebuggerPeer *peer = protocols[proto](p_uri);
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
singleton = memnew(RemoteDebugger(Ref<RemoteDebuggerPeer>(peer)));
|
||||
script_debugger = memnew(ScriptDebugger);
|
||||
// Notify editor of our pid (to allow focus stealing).
|
||||
Array msg = { OS::get_singleton()->get_process_id() };
|
||||
singleton->send_message("set_pid", msg);
|
||||
}
|
||||
if (!singleton) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There is a debugger, parse breakpoints.
|
||||
ScriptDebugger *singleton_script_debugger = singleton->get_script_debugger();
|
||||
singleton_script_debugger->set_skip_breakpoints(p_skip_breakpoints);
|
||||
singleton_script_debugger->set_ignore_error_breaks(p_ignore_error_breaks);
|
||||
|
||||
for (int i = 0; i < p_breakpoints.size(); i++) {
|
||||
const String &bp = p_breakpoints[i];
|
||||
int sp = bp.rfind_char(':');
|
||||
ERR_CONTINUE_MSG(sp == -1, vformat("Invalid breakpoint: '%s', expected file:line format.", bp));
|
||||
|
||||
singleton_script_debugger->insert_breakpoint(bp.substr(sp + 1).to_int(), bp.substr(0, sp));
|
||||
}
|
||||
|
||||
allow_focus_steal_fn = p_allow_focus_steal_fn;
|
||||
}
|
||||
|
||||
void EngineDebugger::deinitialize() {
|
||||
if (singleton) {
|
||||
// Stop all profilers
|
||||
for (const KeyValue<StringName, Profiler> &E : profilers) {
|
||||
if (E.value.active) {
|
||||
singleton->profiler_enable(E.key, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush any remaining message
|
||||
singleton->poll_events(false);
|
||||
|
||||
memdelete(singleton);
|
||||
singleton = nullptr;
|
||||
}
|
||||
|
||||
// Clear profilers/captures/protocol handlers.
|
||||
profilers.clear();
|
||||
captures.clear();
|
||||
protocols.clear();
|
||||
}
|
||||
|
||||
EngineDebugger::~EngineDebugger() {
|
||||
if (script_debugger) {
|
||||
memdelete(script_debugger);
|
||||
}
|
||||
script_debugger = nullptr;
|
||||
singleton = nullptr;
|
||||
}
|
141
core/debugger/engine_debugger.h
Normal file
141
core/debugger/engine_debugger.h
Normal file
@@ -0,0 +1,141 @@
|
||||
/**************************************************************************/
|
||||
/* engine_debugger.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/string/string_name.h"
|
||||
#include "core/string/ustring.h"
|
||||
#include "core/templates/hash_map.h"
|
||||
#include "core/templates/vector.h"
|
||||
#include "core/variant/array.h"
|
||||
|
||||
class RemoteDebuggerPeer;
|
||||
class ScriptDebugger;
|
||||
|
||||
class EngineDebugger {
|
||||
public:
|
||||
typedef void (*ProfilingToggle)(void *p_user, bool p_enable, const Array &p_opts);
|
||||
typedef void (*ProfilingTick)(void *p_user, double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time);
|
||||
typedef void (*ProfilingAdd)(void *p_user, const Array &p_arr);
|
||||
|
||||
typedef Error (*CaptureFunc)(void *p_user, const String &p_msg, const Array &p_args, bool &r_captured);
|
||||
|
||||
typedef RemoteDebuggerPeer *(*CreatePeerFunc)(const String &p_uri);
|
||||
|
||||
class Profiler {
|
||||
friend class EngineDebugger;
|
||||
|
||||
ProfilingToggle toggle = nullptr;
|
||||
ProfilingAdd add = nullptr;
|
||||
ProfilingTick tick = nullptr;
|
||||
void *data = nullptr;
|
||||
bool active = false;
|
||||
|
||||
public:
|
||||
Profiler() {}
|
||||
Profiler(void *p_data, ProfilingToggle p_toggle, ProfilingAdd p_add, ProfilingTick p_tick) {
|
||||
data = p_data;
|
||||
toggle = p_toggle;
|
||||
add = p_add;
|
||||
tick = p_tick;
|
||||
}
|
||||
};
|
||||
|
||||
class Capture {
|
||||
friend class EngineDebugger;
|
||||
|
||||
CaptureFunc capture = nullptr;
|
||||
void *data = nullptr;
|
||||
|
||||
public:
|
||||
Capture() {}
|
||||
Capture(void *p_data, CaptureFunc p_capture) {
|
||||
data = p_data;
|
||||
capture = p_capture;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
double frame_time = 0.0;
|
||||
double process_time = 0.0;
|
||||
double physics_time = 0.0;
|
||||
double physics_frame_time = 0.0;
|
||||
|
||||
uint32_t poll_every = 0;
|
||||
|
||||
protected:
|
||||
static inline EngineDebugger *singleton = nullptr;
|
||||
static inline ScriptDebugger *script_debugger = nullptr;
|
||||
|
||||
static inline HashMap<StringName, Profiler> profilers;
|
||||
static inline HashMap<StringName, Capture> captures;
|
||||
static inline HashMap<String, CreatePeerFunc> protocols;
|
||||
|
||||
static void (*allow_focus_steal_fn)();
|
||||
|
||||
public:
|
||||
_FORCE_INLINE_ static EngineDebugger *get_singleton() { return singleton; }
|
||||
_FORCE_INLINE_ static bool is_active() { return singleton != nullptr && script_debugger != nullptr; }
|
||||
|
||||
_FORCE_INLINE_ static ScriptDebugger *get_script_debugger() { return script_debugger; }
|
||||
|
||||
static void initialize(const String &p_uri, bool p_skip_breakpoints, bool p_ignore_error_breaks, const Vector<String> &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 unregister_profiler(const StringName &p_name);
|
||||
static bool is_profiling(const StringName &p_name);
|
||||
static bool has_profiler(const StringName &p_name);
|
||||
static void profiler_add_frame_data(const StringName &p_name, const Array &p_data);
|
||||
|
||||
static void register_message_capture(const StringName &p_name, Capture p_func);
|
||||
static void unregister_message_capture(const StringName &p_name);
|
||||
static bool has_capture(const StringName &p_name);
|
||||
|
||||
static void register_uri_handler(const String &p_protocol, CreatePeerFunc p_func);
|
||||
|
||||
void iteration(uint64_t p_frame_ticks, uint64_t p_process_ticks, uint64_t p_physics_ticks, double p_physics_frame_time);
|
||||
void profiler_enable(const StringName &p_name, bool p_enabled, const Array &p_opts = Array());
|
||||
Error capture_parse(const StringName &p_name, const String &p_msg, const Array &p_args, bool &r_captured);
|
||||
|
||||
void line_poll() {
|
||||
// The purpose of this is just processing events every now and then when the script might get too busy otherwise bugs like infinite loops can't be caught.
|
||||
if (unlikely(poll_every % 2048) == 0) {
|
||||
poll_events(false);
|
||||
}
|
||||
poll_every++;
|
||||
}
|
||||
|
||||
virtual void poll_events(bool p_is_idle) {}
|
||||
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;
|
||||
|
||||
virtual ~EngineDebugger();
|
||||
};
|
82
core/debugger/engine_profiler.cpp
Normal file
82
core/debugger/engine_profiler.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
/**************************************************************************/
|
||||
/* engine_profiler.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 "engine_profiler.h"
|
||||
|
||||
#include "core/debugger/engine_debugger.h"
|
||||
|
||||
void EngineProfiler::_bind_methods() {
|
||||
GDVIRTUAL_BIND(_toggle, "enable", "options");
|
||||
GDVIRTUAL_BIND(_add_frame, "data");
|
||||
GDVIRTUAL_BIND(_tick, "frame_time", "process_time", "physics_time", "physics_frame_time");
|
||||
}
|
||||
|
||||
void EngineProfiler::toggle(bool p_enable, const Array &p_array) {
|
||||
GDVIRTUAL_CALL(_toggle, p_enable, p_array);
|
||||
}
|
||||
|
||||
void EngineProfiler::add(const Array &p_data) {
|
||||
GDVIRTUAL_CALL(_add_frame, p_data);
|
||||
}
|
||||
|
||||
void EngineProfiler::tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
|
||||
GDVIRTUAL_CALL(_tick, p_frame_time, p_process_time, p_physics_time, p_physics_frame_time);
|
||||
}
|
||||
|
||||
Error EngineProfiler::bind(const String &p_name) {
|
||||
ERR_FAIL_COND_V(is_bound(), ERR_ALREADY_IN_USE);
|
||||
EngineDebugger::Profiler prof(
|
||||
this,
|
||||
[](void *p_user, bool p_enable, const Array &p_opts) {
|
||||
static_cast<EngineProfiler *>(p_user)->toggle(p_enable, p_opts);
|
||||
},
|
||||
[](void *p_user, const Array &p_data) {
|
||||
static_cast<EngineProfiler *>(p_user)->add(p_data);
|
||||
},
|
||||
[](void *p_user, double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
|
||||
static_cast<EngineProfiler *>(p_user)->tick(p_frame_time, p_process_time, p_physics_time, p_physics_frame_time);
|
||||
});
|
||||
registration = p_name;
|
||||
EngineDebugger::register_profiler(p_name, prof);
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error EngineProfiler::unbind() {
|
||||
ERR_FAIL_COND_V(!is_bound(), ERR_UNCONFIGURED);
|
||||
EngineDebugger::unregister_profiler(registration);
|
||||
registration.clear();
|
||||
return OK;
|
||||
}
|
||||
|
||||
EngineProfiler::~EngineProfiler() {
|
||||
if (is_bound()) {
|
||||
unbind();
|
||||
}
|
||||
}
|
60
core/debugger/engine_profiler.h
Normal file
60
core/debugger/engine_profiler.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/**************************************************************************/
|
||||
/* engine_profiler.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/object/gdvirtual.gen.inc"
|
||||
#include "core/object/ref_counted.h"
|
||||
|
||||
class EngineProfiler : public RefCounted {
|
||||
GDCLASS(EngineProfiler, RefCounted);
|
||||
|
||||
private:
|
||||
String registration;
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
public:
|
||||
virtual void toggle(bool p_enable, const Array &p_opts);
|
||||
virtual void add(const Array &p_data);
|
||||
virtual void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time);
|
||||
|
||||
Error bind(const String &p_name);
|
||||
Error unbind();
|
||||
bool is_bound() const { return registration.length() > 0; }
|
||||
|
||||
GDVIRTUAL2(_toggle, bool, Array);
|
||||
GDVIRTUAL1(_add_frame, Array);
|
||||
GDVIRTUAL4(_tick, double, double, double, double);
|
||||
|
||||
EngineProfiler() {}
|
||||
virtual ~EngineProfiler();
|
||||
};
|
390
core/debugger/local_debugger.cpp
Normal file
390
core/debugger/local_debugger.cpp
Normal file
@@ -0,0 +1,390 @@
|
||||
/**************************************************************************/
|
||||
/* local_debugger.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 "local_debugger.h"
|
||||
|
||||
#include "core/debugger/script_debugger.h"
|
||||
#include "core/os/os.h"
|
||||
|
||||
struct LocalDebugger::ScriptsProfiler {
|
||||
struct ProfileInfoSort {
|
||||
bool operator()(const ScriptLanguage::ProfilingInfo &A, const ScriptLanguage::ProfilingInfo &B) const {
|
||||
return A.total_time > B.total_time;
|
||||
}
|
||||
};
|
||||
|
||||
double frame_time = 0;
|
||||
uint64_t idle_accum = 0;
|
||||
Vector<ScriptLanguage::ProfilingInfo> pinfo;
|
||||
|
||||
void toggle(bool p_enable, const Array &p_opts) {
|
||||
if (p_enable) {
|
||||
for (int i = 0; i < ScriptServer::get_language_count(); i++) {
|
||||
ScriptServer::get_language(i)->profiling_start();
|
||||
}
|
||||
|
||||
print_line("BEGIN PROFILING");
|
||||
pinfo.resize(32768);
|
||||
} else {
|
||||
_print_frame_data(true);
|
||||
for (int i = 0; i < ScriptServer::get_language_count(); i++) {
|
||||
ScriptServer::get_language(i)->profiling_stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
|
||||
frame_time = p_frame_time;
|
||||
_print_frame_data(false);
|
||||
}
|
||||
|
||||
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
|
||||
return;
|
||||
}
|
||||
|
||||
idle_accum = OS::get_singleton()->get_ticks_usec();
|
||||
|
||||
int ofs = 0;
|
||||
for (int i = 0; i < ScriptServer::get_language_count(); i++) {
|
||||
if (p_accumulated) {
|
||||
ofs += ScriptServer::get_language(i)->profiling_get_accumulated_data(&pinfo.write[ofs], pinfo.size() - ofs);
|
||||
} else {
|
||||
ofs += ScriptServer::get_language(i)->profiling_get_frame_data(&pinfo.write[ofs], pinfo.size() - ofs);
|
||||
}
|
||||
}
|
||||
|
||||
SortArray<ScriptLanguage::ProfilingInfo, ProfileInfoSort> sort;
|
||||
sort.sort(pinfo.ptrw(), ofs);
|
||||
|
||||
// compute total script frame time
|
||||
uint64_t script_time_us = 0;
|
||||
for (int i = 0; i < ofs; i++) {
|
||||
script_time_us += pinfo[i].self_time;
|
||||
}
|
||||
double script_time = USEC_TO_SEC(script_time_us);
|
||||
double total_time = p_accumulated ? script_time : frame_time;
|
||||
|
||||
if (!p_accumulated) {
|
||||
print_line("FRAME: total: " + rtos(total_time) + " script: " + rtos(script_time) + "/" + itos(script_time * 100 / total_time) + " %");
|
||||
} else {
|
||||
print_line("ACCUMULATED: total: " + rtos(total_time));
|
||||
}
|
||||
|
||||
for (int i = 0; i < ofs; i++) {
|
||||
print_line(itos(i) + ":" + pinfo[i].signature);
|
||||
double tt = USEC_TO_SEC(pinfo[i].total_time);
|
||||
double st = USEC_TO_SEC(pinfo[i].self_time);
|
||||
print_line("\ttotal: " + rtos(tt) + "/" + itos(tt * 100 / total_time) + " % \tself: " + rtos(st) + "/" + itos(st * 100 / total_time) + " % tcalls: " + itos(pinfo[i].call_count));
|
||||
}
|
||||
}
|
||||
|
||||
ScriptsProfiler() {
|
||||
idle_accum = OS::get_singleton()->get_ticks_usec();
|
||||
}
|
||||
};
|
||||
|
||||
void LocalDebugger::debug(bool p_can_continue, bool p_is_error_breakpoint) {
|
||||
ScriptLanguage *script_lang = script_debugger->get_break_language();
|
||||
|
||||
if (!target_function.is_empty()) {
|
||||
String current_function = script_lang->debug_get_stack_level_function(0);
|
||||
if (current_function != target_function) {
|
||||
script_debugger->set_depth(0);
|
||||
script_debugger->set_lines_left(1);
|
||||
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_line("Enter \"help\" for assistance.");
|
||||
int current_frame = 0;
|
||||
int total_frames = script_lang->debug_get_stack_level_count();
|
||||
while (true) {
|
||||
OS::get_singleton()->print("debug> ");
|
||||
String line = OS::get_singleton()->get_stdin_string().strip_edges();
|
||||
|
||||
// Cache options
|
||||
String variable_prefix = options["variable_prefix"];
|
||||
|
||||
if (line.is_empty() && !feof(stdin)) {
|
||||
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_line("Enter \"help\" for assistance.");
|
||||
} else if (line == "c" || line == "continue") {
|
||||
break;
|
||||
} else if (line == "bt" || line == "breakpoint") {
|
||||
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) + "'");
|
||||
}
|
||||
|
||||
} 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) + "'");
|
||||
} 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) + "'");
|
||||
}
|
||||
}
|
||||
|
||||
} else if (line.begins_with("set")) {
|
||||
if (line.get_slice_count(" ") == 1) {
|
||||
for (const KeyValue<String, String> &E : options) {
|
||||
print_line("\t" + E.key + "=" + E.value);
|
||||
}
|
||||
|
||||
} else {
|
||||
String key_value = line.get_slicec(' ', 1);
|
||||
int value_pos = key_value.find_char('=');
|
||||
|
||||
if (value_pos < 0) {
|
||||
print_line("Error: Invalid set format. Use: set key=value");
|
||||
} else {
|
||||
String key = key_value.left(value_pos);
|
||||
|
||||
if (!options.has(key)) {
|
||||
print_line("Error: Unknown option " + key);
|
||||
} else {
|
||||
// Allow explicit tab character
|
||||
String value = key_value.substr(value_pos + 1).replace("\\t", "\t");
|
||||
|
||||
options[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (line == "lv" || line == "locals") {
|
||||
List<String> locals;
|
||||
List<Variant> values;
|
||||
script_lang->debug_get_stack_level_locals(current_frame, &locals, &values);
|
||||
print_variables(locals, values, variable_prefix);
|
||||
|
||||
} else if (line == "gv" || line == "globals") {
|
||||
List<String> globals;
|
||||
List<Variant> values;
|
||||
script_lang->debug_get_globals(&globals, &values);
|
||||
print_variables(globals, values, variable_prefix);
|
||||
|
||||
} else if (line == "mv" || line == "members") {
|
||||
List<String> members;
|
||||
List<Variant> values;
|
||||
script_lang->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")) {
|
||||
if (line.find_char(' ') < 0) {
|
||||
print_line("Usage: print <expression>");
|
||||
} else {
|
||||
String expr = line.split(" ", true, 1)[1];
|
||||
String res = script_lang->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);
|
||||
break;
|
||||
} else if (line == "n" || line == "next") {
|
||||
script_debugger->set_depth(0);
|
||||
script_debugger->set_lines_left(1);
|
||||
break;
|
||||
} else if (line == "fin" || line == "finish") {
|
||||
String current_function = script_lang->debug_get_stack_level_function(0);
|
||||
|
||||
for (int i = 0; i < total_frames; i++) {
|
||||
target_function = script_lang->debug_get_stack_level_function(i);
|
||||
if (target_function != current_function) {
|
||||
script_debugger->set_depth(0);
|
||||
script_debugger->set_lines_left(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
print_line("Error: Reached last frame.");
|
||||
target_function = "";
|
||||
|
||||
} else if (line.begins_with("br") || line.begins_with("break")) {
|
||||
if (line.get_slice_count(" ") <= 1) {
|
||||
const HashMap<int, HashSet<StringName>> &breakpoints = script_debugger->get_breakpoints();
|
||||
if (breakpoints.is_empty()) {
|
||||
print_line("No Breakpoints.");
|
||||
continue;
|
||||
}
|
||||
|
||||
print_line("Breakpoint(s): " + itos(breakpoints.size()));
|
||||
for (const KeyValue<int, HashSet<StringName>> &E : breakpoints) {
|
||||
print_line("\t" + String(*E.value.begin()) + ":" + itos(E.key));
|
||||
}
|
||||
|
||||
} else {
|
||||
Pair<String, int> breakpoint = to_breakpoint(line);
|
||||
|
||||
String source = breakpoint.first;
|
||||
int linenr = breakpoint.second;
|
||||
|
||||
if (source.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
script_debugger->insert_breakpoint(linenr, source);
|
||||
|
||||
print_line("Added breakpoint at " + source + ":" + itos(linenr));
|
||||
}
|
||||
|
||||
} else if (line == "q" || line == "quit" ||
|
||||
(line.is_empty() && feof(stdin))) {
|
||||
// Do not stop again on quit
|
||||
script_debugger->clear_breakpoints();
|
||||
script_debugger->set_depth(-1);
|
||||
script_debugger->set_lines_left(-1);
|
||||
|
||||
MainLoop *main_loop = OS::get_singleton()->get_main_loop();
|
||||
if (main_loop->get_class() == "SceneTree") {
|
||||
main_loop->call("quit");
|
||||
}
|
||||
break;
|
||||
} else if (line.begins_with("delete")) {
|
||||
if (line.get_slice_count(" ") <= 1) {
|
||||
script_debugger->clear_breakpoints();
|
||||
} else {
|
||||
Pair<String, int> breakpoint = to_breakpoint(line);
|
||||
|
||||
String source = breakpoint.first;
|
||||
int linenr = breakpoint.second;
|
||||
|
||||
if (source.is_empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
script_debugger->remove_breakpoint(linenr, source);
|
||||
|
||||
print_line("Removed breakpoint at " + source + ":" + itos(linenr));
|
||||
}
|
||||
|
||||
} else if (line == "h" || line == "help") {
|
||||
print_line("Built-In Debugger command list:\n");
|
||||
print_line("\tc,continue\t\t Continue execution.");
|
||||
print_line("\tbt,backtrace\t\t Show stack trace (frames).");
|
||||
print_line("\tfr,frame <frame>:\t Change current frame.");
|
||||
print_line("\tlv,locals\t\t Show local variables for current frame.");
|
||||
print_line("\tmv,members\t\t Show member variables for \"this\" in frame.");
|
||||
print_line("\tgv,globals\t\t Show global variables.");
|
||||
print_line("\tp,print <expr>\t\t Execute and print variable in expression.");
|
||||
print_line("\ts,step\t\t\t Step to next line.");
|
||||
print_line("\tn,next\t\t\t Next line.");
|
||||
print_line("\tfin,finish\t\t Step out of current frame.");
|
||||
print_line("\tbr,break [source:line]\t List all breakpoints or place a breakpoint.");
|
||||
print_line("\tdelete [source:line]:\t Delete one/all breakpoints.");
|
||||
print_line("\tset [key=value]:\t List all options, or set one.");
|
||||
print_line("\tq,quit\t\t\t Quit application.");
|
||||
} else {
|
||||
print_line("Error: Invalid command, enter \"help\" for assistance.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LocalDebugger::print_variables(const List<String> &names, const List<Variant> &values, const String &variable_prefix) {
|
||||
String value;
|
||||
Vector<String> value_lines;
|
||||
const List<Variant>::Element *V = values.front();
|
||||
for (const String &E : names) {
|
||||
value = String(V->get());
|
||||
|
||||
if (variable_prefix.is_empty()) {
|
||||
print_line(E + ": " + String(V->get()));
|
||||
} else {
|
||||
print_line(E + ":");
|
||||
value_lines = value.split("\n");
|
||||
for (int i = 0; i < value_lines.size(); ++i) {
|
||||
print_line(variable_prefix + value_lines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
V = V->next();
|
||||
}
|
||||
}
|
||||
|
||||
Pair<String, int> LocalDebugger::to_breakpoint(const String &p_line) {
|
||||
String breakpoint_part = p_line.get_slicec(' ', 1);
|
||||
Pair<String, int> breakpoint;
|
||||
|
||||
int last_colon = breakpoint_part.rfind_char(':');
|
||||
if (last_colon < 0) {
|
||||
print_line("Error: Invalid breakpoint format. Expected [source:line]");
|
||||
return breakpoint;
|
||||
}
|
||||
|
||||
breakpoint.first = script_debugger->breakpoint_find_source(breakpoint_part.left(last_colon).strip_edges());
|
||||
breakpoint.second = breakpoint_part.substr(last_colon).strip_edges().to_int();
|
||||
|
||||
return breakpoint;
|
||||
}
|
||||
|
||||
void LocalDebugger::send_message(const String &p_message, const Array &p_args) {
|
||||
// This needs to be cleaned up entirely.
|
||||
// print_line("MESSAGE: '" + p_message + "' - " + String(Variant(p_args)));
|
||||
}
|
||||
|
||||
void LocalDebugger::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) {
|
||||
_err_print_error(p_func.utf8().get_data(), p_file.utf8().get_data(), p_line, p_err, p_descr, p_editor_notify, p_type);
|
||||
}
|
||||
|
||||
LocalDebugger::LocalDebugger() {
|
||||
options["variable_prefix"] = "";
|
||||
|
||||
// Bind scripts profiler.
|
||||
scripts_profiler = memnew(ScriptsProfiler);
|
||||
Profiler scr_prof(
|
||||
scripts_profiler,
|
||||
[](void *p_user, bool p_enable, const Array &p_opts) {
|
||||
static_cast<ScriptsProfiler *>(p_user)->toggle(p_enable, p_opts);
|
||||
},
|
||||
nullptr,
|
||||
[](void *p_user, double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
|
||||
static_cast<ScriptsProfiler *>(p_user)->tick(p_frame_time, p_process_time, p_physics_time, p_physics_frame_time);
|
||||
});
|
||||
register_profiler("scripts", scr_prof);
|
||||
}
|
||||
|
||||
LocalDebugger::~LocalDebugger() {
|
||||
unregister_profiler("scripts");
|
||||
if (scripts_profiler) {
|
||||
memdelete(scripts_profiler);
|
||||
}
|
||||
}
|
56
core/debugger/local_debugger.h
Normal file
56
core/debugger/local_debugger.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/**************************************************************************/
|
||||
/* local_debugger.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/debugger/engine_debugger.h"
|
||||
#include "core/object/script_language.h"
|
||||
#include "core/templates/list.h"
|
||||
|
||||
class LocalDebugger : public EngineDebugger {
|
||||
private:
|
||||
struct ScriptsProfiler;
|
||||
|
||||
ScriptsProfiler *scripts_profiler = nullptr;
|
||||
|
||||
String target_function;
|
||||
HashMap<String, String> options;
|
||||
|
||||
Pair<String, int> to_breakpoint(const String &p_line);
|
||||
void print_variables(const List<String> &names, const List<Variant> &values, const String &variable_prefix);
|
||||
|
||||
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);
|
||||
|
||||
LocalDebugger();
|
||||
~LocalDebugger();
|
||||
};
|
772
core/debugger/remote_debugger.cpp
Normal file
772
core/debugger/remote_debugger.cpp
Normal file
@@ -0,0 +1,772 @@
|
||||
/**************************************************************************/
|
||||
/* remote_debugger.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 "remote_debugger.h"
|
||||
|
||||
#include "core/config/project_settings.h"
|
||||
#include "core/debugger/debugger_marshalls.h"
|
||||
#include "core/debugger/engine_debugger.h"
|
||||
#include "core/debugger/engine_profiler.h"
|
||||
#include "core/debugger/script_debugger.h"
|
||||
#include "core/input/input.h"
|
||||
#include "core/io/resource_loader.h"
|
||||
#include "core/math/expression.h"
|
||||
#include "core/object/script_language.h"
|
||||
#include "core/os/os.h"
|
||||
#include "servers/display_server.h"
|
||||
|
||||
class RemoteDebugger::PerformanceProfiler : public EngineProfiler {
|
||||
Object *performance = nullptr;
|
||||
int last_perf_time = 0;
|
||||
uint64_t last_monitor_modification_time = 0;
|
||||
|
||||
public:
|
||||
void toggle(bool p_enable, const Array &p_opts) {}
|
||||
void add(const Array &p_data) {}
|
||||
void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) {
|
||||
if (!performance) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t pt = OS::get_singleton()->get_ticks_msec();
|
||||
if (pt - last_perf_time < 1000) {
|
||||
return;
|
||||
}
|
||||
last_perf_time = pt;
|
||||
|
||||
Array custom_monitor_names = performance->call("get_custom_monitor_names");
|
||||
|
||||
uint64_t monitor_modification_time = performance->call("get_monitor_modification_time");
|
||||
if (monitor_modification_time > last_monitor_modification_time) {
|
||||
last_monitor_modification_time = monitor_modification_time;
|
||||
EngineDebugger::get_singleton()->send_message("performance:profile_names", custom_monitor_names);
|
||||
}
|
||||
|
||||
int max = performance->get("MONITOR_MAX");
|
||||
Array arr;
|
||||
arr.resize(max + custom_monitor_names.size());
|
||||
for (int i = 0; i < max; i++) {
|
||||
arr[i] = performance->call("get_monitor", i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < custom_monitor_names.size(); i++) {
|
||||
Variant monitor_value = performance->call("get_custom_monitor", custom_monitor_names[i]);
|
||||
if (!monitor_value.is_num()) {
|
||||
ERR_PRINT(vformat("Value of custom monitor '%s' is not a number.", String(custom_monitor_names[i])));
|
||||
arr[i + max] = Variant();
|
||||
} else {
|
||||
arr[i + max] = monitor_value;
|
||||
}
|
||||
}
|
||||
|
||||
EngineDebugger::get_singleton()->send_message("performance:profile_frame", arr);
|
||||
}
|
||||
|
||||
explicit PerformanceProfiler(Object *p_performance) {
|
||||
performance = p_performance;
|
||||
}
|
||||
};
|
||||
|
||||
Error RemoteDebugger::_put_msg(const String &p_message, const Array &p_data) {
|
||||
Array msg = { p_message, Thread::get_caller_id(), p_data };
|
||||
Error err = peer->put_message(msg);
|
||||
if (err != OK) {
|
||||
n_messages_dropped++;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
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) {
|
||||
RemoteDebugger *rd = static_cast<RemoteDebugger *>(p_this);
|
||||
if (rd->flushing && Thread::get_caller_id() == rd->flush_thread) { // Can't handle recursive errors during flush.
|
||||
return;
|
||||
}
|
||||
|
||||
Vector<ScriptLanguage::StackInfo> 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);
|
||||
}
|
||||
|
||||
void RemoteDebugger::_print_handler(void *p_this, const String &p_string, bool p_error, bool p_rich) {
|
||||
RemoteDebugger *rd = static_cast<RemoteDebugger *>(p_this);
|
||||
|
||||
if (rd->flushing && Thread::get_caller_id() == rd->flush_thread) { // Can't handle recursive prints during flush.
|
||||
return;
|
||||
}
|
||||
|
||||
String s = p_string;
|
||||
int allowed_chars = MIN(MAX(rd->max_chars_per_second - rd->char_count, 0), s.length());
|
||||
|
||||
if (allowed_chars == 0 && s.length() > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowed_chars < s.length()) {
|
||||
s = s.substr(0, allowed_chars);
|
||||
}
|
||||
|
||||
MutexLock lock(rd->mutex);
|
||||
|
||||
rd->char_count += allowed_chars;
|
||||
bool overflowed = rd->char_count >= rd->max_chars_per_second;
|
||||
if (rd->is_peer_connected()) {
|
||||
if (overflowed) {
|
||||
s += "[...]";
|
||||
}
|
||||
|
||||
OutputString output_string;
|
||||
output_string.message = s;
|
||||
if (p_error) {
|
||||
output_string.type = MESSAGE_TYPE_ERROR;
|
||||
} else if (p_rich) {
|
||||
output_string.type = MESSAGE_TYPE_LOG_RICH;
|
||||
} else {
|
||||
output_string.type = MESSAGE_TYPE_LOG;
|
||||
}
|
||||
rd->output_strings.push_back(output_string);
|
||||
|
||||
if (overflowed) {
|
||||
output_string.message = "[output overflow, print less text!]";
|
||||
output_string.type = MESSAGE_TYPE_ERROR;
|
||||
rd->output_strings.push_back(output_string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoteDebugger::ErrorMessage RemoteDebugger::_create_overflow_error(const String &p_what, const String &p_descr) {
|
||||
ErrorMessage oe;
|
||||
oe.error = p_what;
|
||||
oe.error_descr = p_descr;
|
||||
oe.warning = false;
|
||||
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;
|
||||
return oe;
|
||||
}
|
||||
|
||||
void RemoteDebugger::flush_output() {
|
||||
MutexLock lock(mutex);
|
||||
flush_thread = Thread::get_caller_id();
|
||||
flushing = true;
|
||||
if (!is_peer_connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (n_messages_dropped > 0) {
|
||||
ErrorMessage err_msg = _create_overflow_error("TOO_MANY_MESSAGES", "Too many messages! " + String::num_int64(n_messages_dropped) + " messages were dropped. Profiling might misbheave, try raising 'network/limits/debugger/max_queued_messages' in project setting.");
|
||||
if (_put_msg("error", err_msg.serialize()) == OK) {
|
||||
n_messages_dropped = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (output_strings.size()) {
|
||||
// Join output strings so we generate less messages.
|
||||
Vector<String> joined_log_strings;
|
||||
Vector<String> strings;
|
||||
Vector<int> types;
|
||||
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));
|
||||
types.push_back(MESSAGE_TYPE_LOG);
|
||||
joined_log_strings.clear();
|
||||
}
|
||||
strings.push_back(output_string.message);
|
||||
types.push_back(MESSAGE_TYPE_ERROR);
|
||||
} else if (output_string.type == MESSAGE_TYPE_LOG_RICH) {
|
||||
if (!joined_log_strings.is_empty()) {
|
||||
strings.push_back(String("\n").join(joined_log_strings));
|
||||
types.push_back(MESSAGE_TYPE_LOG_RICH);
|
||||
joined_log_strings.clear();
|
||||
}
|
||||
strings.push_back(output_string.message);
|
||||
types.push_back(MESSAGE_TYPE_LOG_RICH);
|
||||
} else {
|
||||
joined_log_strings.push_back(output_string.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!joined_log_strings.is_empty()) {
|
||||
strings.push_back(String("\n").join(joined_log_strings));
|
||||
types.push_back(MESSAGE_TYPE_LOG);
|
||||
}
|
||||
|
||||
Array arr = { strings, types };
|
||||
_put_msg("output", arr);
|
||||
output_strings.clear();
|
||||
}
|
||||
|
||||
while (errors.size()) {
|
||||
ErrorMessage oe = errors.front()->get();
|
||||
_put_msg("error", oe.serialize());
|
||||
errors.pop_front();
|
||||
}
|
||||
|
||||
// Update limits
|
||||
uint64_t ticks = OS::get_singleton()->get_ticks_usec() / 1000;
|
||||
|
||||
if (ticks - last_reset > 1000) {
|
||||
last_reset = ticks;
|
||||
char_count = 0;
|
||||
err_count = 0;
|
||||
n_errors_dropped = 0;
|
||||
warn_count = 0;
|
||||
n_warnings_dropped = 0;
|
||||
}
|
||||
flushing = false;
|
||||
}
|
||||
|
||||
void RemoteDebugger::send_message(const String &p_message, const Array &p_args) {
|
||||
MutexLock lock(mutex);
|
||||
if (is_peer_connected()) {
|
||||
_put_msg(p_message, 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) {
|
||||
ErrorMessage oe;
|
||||
oe.error = p_err;
|
||||
oe.error_descr = p_descr;
|
||||
oe.source_file = p_file;
|
||||
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;
|
||||
}
|
||||
|
||||
MutexLock lock(mutex);
|
||||
|
||||
if (oe.warning) {
|
||||
warn_count++;
|
||||
} else {
|
||||
err_count++;
|
||||
}
|
||||
|
||||
if (is_peer_connected()) {
|
||||
if (oe.warning) {
|
||||
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.");
|
||||
errors.push_back(overflow);
|
||||
}
|
||||
} else {
|
||||
errors.push_back(oe);
|
||||
}
|
||||
} else {
|
||||
if (err_count > max_errors_per_second) {
|
||||
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.");
|
||||
errors.push_back(overflow);
|
||||
}
|
||||
} else {
|
||||
errors.push_back(oe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteDebugger::_send_stack_vars(List<String> &p_names, List<Variant> &p_vals, int p_type) {
|
||||
DebuggerMarshalls::ScriptStackVariable stvar;
|
||||
List<String>::Element *E = p_names.front();
|
||||
List<Variant>::Element *F = p_vals.front();
|
||||
while (E) {
|
||||
stvar.name = E->get();
|
||||
stvar.value = F->get();
|
||||
stvar.type = p_type;
|
||||
send_message("stack_frame_var", stvar.serialize());
|
||||
E = E->next();
|
||||
F = F->next();
|
||||
}
|
||||
}
|
||||
|
||||
Error RemoteDebugger::_try_capture(const String &p_msg, const Array &p_data, bool &r_captured) {
|
||||
const int idx = p_msg.find_char(':');
|
||||
r_captured = false;
|
||||
if (idx < 0) { // No prefix, unknown message.
|
||||
return OK;
|
||||
}
|
||||
const String cap = p_msg.substr(0, idx);
|
||||
if (!has_capture(cap)) {
|
||||
return ERR_UNAVAILABLE; // Unknown message...
|
||||
}
|
||||
const String msg = p_msg.substr(idx + 1);
|
||||
return capture_parse(cap, msg, p_data, r_captured);
|
||||
}
|
||||
|
||||
void RemoteDebugger::_poll_messages() {
|
||||
MutexLock mutex_lock(mutex);
|
||||
|
||||
peer->poll();
|
||||
while (peer->has_message()) {
|
||||
Array cmd = peer->get_message();
|
||||
ERR_CONTINUE(cmd.size() != 3);
|
||||
ERR_CONTINUE(cmd[0].get_type() != Variant::STRING);
|
||||
ERR_CONTINUE(cmd[1].get_type() != Variant::INT);
|
||||
ERR_CONTINUE(cmd[2].get_type() != Variant::ARRAY);
|
||||
|
||||
Thread::ID thread = cmd[1];
|
||||
|
||||
if (!messages.has(thread)) {
|
||||
continue; // This thread is not around to receive the messages
|
||||
}
|
||||
|
||||
Message msg;
|
||||
msg.message = cmd[0];
|
||||
msg.data = cmd[2];
|
||||
messages[thread].push_back(msg);
|
||||
}
|
||||
}
|
||||
|
||||
bool RemoteDebugger::_has_messages() {
|
||||
MutexLock mutex_lock(mutex);
|
||||
return messages.has(Thread::get_caller_id()) && !messages[Thread::get_caller_id()].is_empty();
|
||||
}
|
||||
|
||||
Array RemoteDebugger::_get_message() {
|
||||
MutexLock mutex_lock(mutex);
|
||||
ERR_FAIL_COND_V(!messages.has(Thread::get_caller_id()), Array());
|
||||
List<Message> &message_list = messages[Thread::get_caller_id()];
|
||||
ERR_FAIL_COND_V(message_list.is_empty(), Array());
|
||||
|
||||
Array msg;
|
||||
msg.resize(2);
|
||||
msg[0] = message_list.front()->get().message;
|
||||
msg[1] = message_list.front()->get().data;
|
||||
message_list.pop_front();
|
||||
return msg;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
{
|
||||
MutexLock lock(mutex);
|
||||
// Tests that require mutex.
|
||||
if (script_debugger->is_skipping_breakpoints() && !p_is_error_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 (p_is_error_breakpoint && script_debugger->is_ignoring_error_breaks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScriptLanguage *script_lang = script_debugger->get_break_language();
|
||||
ERR_FAIL_NULL(script_lang);
|
||||
|
||||
Array msg = {
|
||||
p_can_continue,
|
||||
script_lang->debug_get_error(),
|
||||
script_lang->debug_get_stack_level_count() > 0,
|
||||
Thread::get_caller_id()
|
||||
};
|
||||
if (allow_focus_steal_fn) {
|
||||
allow_focus_steal_fn();
|
||||
}
|
||||
send_message("debug_enter", msg);
|
||||
|
||||
Input::MouseMode mouse_mode = Input::MOUSE_MODE_VISIBLE;
|
||||
|
||||
if (Thread::get_caller_id() == Thread::get_main_id()) {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
MutexLock mutex_lock(mutex);
|
||||
messages.insert(Thread::get_caller_id(), List<Message>());
|
||||
}
|
||||
|
||||
while (is_peer_connected()) {
|
||||
flush_output();
|
||||
|
||||
_poll_messages();
|
||||
|
||||
if (_has_messages()) {
|
||||
Array cmd = _get_message();
|
||||
|
||||
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];
|
||||
|
||||
if (command == "step") {
|
||||
script_debugger->set_depth(-1);
|
||||
script_debugger->set_lines_left(1);
|
||||
break;
|
||||
|
||||
} else if (command == "next") {
|
||||
script_debugger->set_depth(0);
|
||||
script_debugger->set_lines_left(1);
|
||||
break;
|
||||
|
||||
} else if (command == "continue") {
|
||||
script_debugger->set_depth(-1);
|
||||
script_debugger->set_lines_left(-1);
|
||||
break;
|
||||
|
||||
} else if (command == "break") {
|
||||
ERR_PRINT("Got break when already broke!");
|
||||
break;
|
||||
|
||||
} else 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);
|
||||
}
|
||||
send_message("stack_dump", dump.serialize());
|
||||
|
||||
} else if (command == "get_stack_frame_vars") {
|
||||
ERR_FAIL_COND(data.size() != 1);
|
||||
ERR_FAIL_NULL(script_lang);
|
||||
int lv = data[0];
|
||||
|
||||
List<String> members;
|
||||
List<Variant> member_vals;
|
||||
if (ScriptInstance *inst = script_lang->debug_get_stack_level_instance(lv)) {
|
||||
members.push_back("self");
|
||||
member_vals.push_back(inst->get_owner());
|
||||
}
|
||||
script_lang->debug_get_stack_level_members(lv, &members, &member_vals);
|
||||
ERR_FAIL_COND(members.size() != member_vals.size());
|
||||
|
||||
List<String> locals;
|
||||
List<Variant> local_vals;
|
||||
script_lang->debug_get_stack_level_locals(lv, &locals, &local_vals);
|
||||
ERR_FAIL_COND(locals.size() != local_vals.size());
|
||||
|
||||
List<String> globals;
|
||||
List<Variant> globals_vals;
|
||||
script_lang->debug_get_globals(&globals, &globals_vals);
|
||||
ERR_FAIL_COND(globals.size() != globals_vals.size());
|
||||
|
||||
Array var_size = { 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 (command == "reload_scripts") {
|
||||
script_paths_to_reload = data;
|
||||
} else if (command == "reload_all_scripts") {
|
||||
reload_all_scripts = true;
|
||||
} else if (command == "breakpoint") {
|
||||
ERR_FAIL_COND(data.size() < 3);
|
||||
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.is_empty());
|
||||
script_debugger->set_skip_breakpoints(data[0]);
|
||||
} else if (command == "set_ignore_error_breaks") {
|
||||
ERR_FAIL_COND(data.is_empty());
|
||||
script_debugger->set_ignore_error_breaks(data[0]);
|
||||
} else if (command == "evaluate") {
|
||||
String expression_str = data[0];
|
||||
int frame = data[1];
|
||||
|
||||
ScriptInstance *breaked_instance = script_debugger->get_break_language()->debug_get_stack_level_instance(frame);
|
||||
if (!breaked_instance) {
|
||||
break;
|
||||
}
|
||||
|
||||
PackedStringArray input_names;
|
||||
Array input_vals;
|
||||
|
||||
List<String> locals;
|
||||
List<Variant> local_vals;
|
||||
script_debugger->get_break_language()->debug_get_stack_level_locals(frame, &locals, &local_vals);
|
||||
ERR_FAIL_COND(locals.size() != local_vals.size());
|
||||
|
||||
for (const String &S : locals) {
|
||||
input_names.append(S);
|
||||
}
|
||||
|
||||
for (const Variant &V : local_vals) {
|
||||
input_vals.append(V);
|
||||
}
|
||||
|
||||
List<String> globals;
|
||||
List<Variant> globals_vals;
|
||||
script_debugger->get_break_language()->debug_get_globals(&globals, &globals_vals);
|
||||
ERR_FAIL_COND(globals.size() != globals_vals.size());
|
||||
|
||||
for (const String &S : globals) {
|
||||
input_names.append(S);
|
||||
}
|
||||
|
||||
for (const Variant &V : globals_vals) {
|
||||
input_vals.append(V);
|
||||
}
|
||||
|
||||
List<StringName> native_types;
|
||||
ClassDB::get_class_list(&native_types);
|
||||
for (const StringName &E : native_types) {
|
||||
if (!ClassDB::is_class_exposed(E) || !Engine::get_singleton()->has_singleton(E) || Engine::get_singleton()->is_singleton_editor_only(E)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
input_names.append(E);
|
||||
input_vals.append(Engine::get_singleton()->get_singleton_object(E));
|
||||
}
|
||||
|
||||
List<StringName> user_types;
|
||||
ScriptServer::get_global_class_list(&user_types);
|
||||
for (const StringName &S : user_types) {
|
||||
String scr_path = ScriptServer::get_global_class_path(S);
|
||||
Ref<Script> scr = ResourceLoader::load(scr_path, "Script");
|
||||
ERR_CONTINUE_MSG(scr.is_null(), vformat(R"(Could not load the global class %s from resource path: "%s".)", S, scr_path));
|
||||
|
||||
input_names.append(S);
|
||||
input_vals.append(scr);
|
||||
}
|
||||
|
||||
Expression expression;
|
||||
expression.parse(expression_str, input_names);
|
||||
const Variant return_val = expression.execute(input_vals, breaked_instance->get_owner());
|
||||
|
||||
DebuggerMarshalls::ScriptStackVariable stvar;
|
||||
stvar.name = expression_str;
|
||||
stvar.value = return_val;
|
||||
stvar.type = 3;
|
||||
|
||||
send_message("evaluation_return", stvar.serialize());
|
||||
} else {
|
||||
bool captured = false;
|
||||
ERR_CONTINUE(_try_capture(command, data, captured) != OK);
|
||||
if (!captured) {
|
||||
WARN_PRINT(vformat("Unknown message received from debugger: %s.", command));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
OS::get_singleton()->delay_usec(10000);
|
||||
if (Thread::get_caller_id() == Thread::get_main_id()) {
|
||||
// If this is a busy loop on the main thread, events still need to be processed.
|
||||
DisplayServer::get_singleton()->force_process_and_drop_events();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_message("debug_exit", Array());
|
||||
|
||||
if (Thread::get_caller_id() == Thread::get_main_id()) {
|
||||
if (mouse_mode != Input::MOUSE_MODE_VISIBLE) {
|
||||
Input::get_singleton()->set_mouse_mode(mouse_mode);
|
||||
}
|
||||
} else {
|
||||
MutexLock mutex_lock(mutex);
|
||||
messages.erase(Thread::get_caller_id());
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteDebugger::poll_events(bool p_is_idle) {
|
||||
if (peer.is_null()) {
|
||||
return;
|
||||
}
|
||||
|
||||
flush_output();
|
||||
|
||||
_poll_messages();
|
||||
|
||||
while (_has_messages()) {
|
||||
Array arr = _get_message();
|
||||
|
||||
ERR_CONTINUE(arr.size() != 2);
|
||||
ERR_CONTINUE(arr[0].get_type() != Variant::STRING);
|
||||
ERR_CONTINUE(arr[1].get_type() != Variant::ARRAY);
|
||||
|
||||
const String cmd = arr[0];
|
||||
const int idx = cmd.find_char(':');
|
||||
bool parsed = false;
|
||||
if (idx < 0) { // Not prefix, use scripts capture.
|
||||
capture_parse("core", cmd, arr[1], parsed);
|
||||
continue;
|
||||
}
|
||||
|
||||
const String cap = cmd.substr(0, idx);
|
||||
if (!has_capture(cap)) {
|
||||
continue; // Unknown message...
|
||||
}
|
||||
|
||||
const String msg = cmd.substr(idx + 1);
|
||||
capture_parse(cap, msg, arr[1], parsed);
|
||||
}
|
||||
|
||||
// Reload scripts during idle poll only.
|
||||
if (p_is_idle) {
|
||||
if (reload_all_scripts) {
|
||||
for (int i = 0; i < ScriptServer::get_language_count(); i++) {
|
||||
ScriptServer::get_language(i)->reload_all_scripts();
|
||||
}
|
||||
reload_all_scripts = false;
|
||||
} else if (!script_paths_to_reload.is_empty()) {
|
||||
Array scripts_to_reload;
|
||||
for (int i = 0; i < script_paths_to_reload.size(); ++i) {
|
||||
String path = script_paths_to_reload[i];
|
||||
Error err = OK;
|
||||
Ref<Script> script = ResourceLoader::load(path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err);
|
||||
ERR_CONTINUE_MSG(err != OK, vformat("Could not reload script '%s': %s", path, error_names[err]));
|
||||
ERR_CONTINUE_MSG(script.is_null(), vformat("Could not reload script '%s': Not a script!", path, error_names[err]));
|
||||
scripts_to_reload.push_back(script);
|
||||
}
|
||||
for (int i = 0; i < ScriptServer::get_language_count(); i++) {
|
||||
ScriptServer::get_language(i)->reload_scripts(scripts_to_reload, true);
|
||||
}
|
||||
}
|
||||
script_paths_to_reload.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Error RemoteDebugger::_core_capture(const String &p_cmd, const Array &p_data, bool &r_captured) {
|
||||
r_captured = true;
|
||||
if (p_cmd == "reload_scripts") {
|
||||
script_paths_to_reload = p_data;
|
||||
} else if (p_cmd == "reload_all_scripts") {
|
||||
reload_all_scripts = true;
|
||||
} else if (p_cmd == "breakpoint") {
|
||||
ERR_FAIL_COND_V(p_data.size() < 3, ERR_INVALID_DATA);
|
||||
bool set = p_data[2];
|
||||
if (set) {
|
||||
script_debugger->insert_breakpoint(p_data[1], p_data[0]);
|
||||
} else {
|
||||
script_debugger->remove_breakpoint(p_data[1], p_data[0]);
|
||||
}
|
||||
|
||||
} else if (p_cmd == "set_skip_breakpoints") {
|
||||
ERR_FAIL_COND_V(p_data.is_empty(), ERR_INVALID_DATA);
|
||||
script_debugger->set_skip_breakpoints(p_data[0]);
|
||||
} else if (p_cmd == "set_ignore_error_breaks") {
|
||||
ERR_FAIL_COND_V(p_data.is_empty(), ERR_INVALID_DATA);
|
||||
script_debugger->set_ignore_error_breaks(p_data[0]);
|
||||
} else if (p_cmd == "break") {
|
||||
script_debugger->debug(script_debugger->get_break_language());
|
||||
} else {
|
||||
r_captured = false;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
Error RemoteDebugger::_profiler_capture(const String &p_cmd, const Array &p_data, bool &r_captured) {
|
||||
r_captured = false;
|
||||
ERR_FAIL_COND_V(p_data.is_empty(), ERR_INVALID_DATA);
|
||||
ERR_FAIL_COND_V(p_data[0].get_type() != Variant::BOOL, ERR_INVALID_DATA);
|
||||
ERR_FAIL_COND_V(!has_profiler(p_cmd), ERR_UNAVAILABLE);
|
||||
Array opts;
|
||||
if (p_data.size() > 1) { // Optional profiler parameters.
|
||||
ERR_FAIL_COND_V(p_data[1].get_type() != Variant::ARRAY, ERR_INVALID_DATA);
|
||||
opts = p_data[1];
|
||||
}
|
||||
r_captured = true;
|
||||
profiler_enable(p_cmd, p_data[0], opts);
|
||||
return OK;
|
||||
}
|
||||
|
||||
RemoteDebugger::RemoteDebugger(Ref<RemoteDebuggerPeer> 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");
|
||||
max_warnings_per_second = GLOBAL_GET("network/limits/debugger/max_warnings_per_second");
|
||||
|
||||
// Performance Profiler
|
||||
Object *perf = Engine::get_singleton()->get_singleton_object("Performance");
|
||||
if (perf) {
|
||||
performance_profiler.instantiate(perf);
|
||||
performance_profiler->bind("performance");
|
||||
profiler_enable("performance", true);
|
||||
}
|
||||
|
||||
// Core and profiler captures.
|
||||
Capture core_cap(this,
|
||||
[](void *p_user, const String &p_cmd, const Array &p_data, bool &r_captured) {
|
||||
return static_cast<RemoteDebugger *>(p_user)->_core_capture(p_cmd, p_data, r_captured);
|
||||
});
|
||||
register_message_capture("core", core_cap);
|
||||
Capture profiler_cap(this,
|
||||
[](void *p_user, const String &p_cmd, const Array &p_data, bool &r_captured) {
|
||||
return static_cast<RemoteDebugger *>(p_user)->_profiler_capture(p_cmd, p_data, r_captured);
|
||||
});
|
||||
register_message_capture("profiler", profiler_cap);
|
||||
|
||||
// Error handlers
|
||||
phl.printfunc = _print_handler;
|
||||
phl.userdata = this;
|
||||
add_print_handler(&phl);
|
||||
|
||||
eh.errfunc = _err_handler;
|
||||
eh.userdata = this;
|
||||
add_error_handler(&eh);
|
||||
|
||||
messages.insert(Thread::get_main_id(), List<Message>());
|
||||
}
|
||||
|
||||
RemoteDebugger::~RemoteDebugger() {
|
||||
remove_print_handler(&phl);
|
||||
remove_error_handler(&eh);
|
||||
}
|
123
core/debugger/remote_debugger.h
Normal file
123
core/debugger/remote_debugger.h
Normal file
@@ -0,0 +1,123 @@
|
||||
/**************************************************************************/
|
||||
/* remote_debugger.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/debugger/debugger_marshalls.h"
|
||||
#include "core/debugger/engine_debugger.h"
|
||||
#include "core/debugger/remote_debugger_peer.h"
|
||||
#include "core/object/class_db.h"
|
||||
#include "core/string/string_name.h"
|
||||
#include "core/string/ustring.h"
|
||||
#include "core/variant/array.h"
|
||||
|
||||
class RemoteDebugger : public EngineDebugger {
|
||||
public:
|
||||
enum MessageType {
|
||||
MESSAGE_TYPE_LOG,
|
||||
MESSAGE_TYPE_ERROR,
|
||||
MESSAGE_TYPE_LOG_RICH,
|
||||
};
|
||||
|
||||
private:
|
||||
typedef DebuggerMarshalls::OutputError ErrorMessage;
|
||||
|
||||
class PerformanceProfiler;
|
||||
|
||||
Ref<PerformanceProfiler> performance_profiler;
|
||||
|
||||
Ref<RemoteDebuggerPeer> peer;
|
||||
|
||||
struct OutputString {
|
||||
String message;
|
||||
MessageType type;
|
||||
};
|
||||
List<OutputString> output_strings;
|
||||
List<ErrorMessage> errors;
|
||||
|
||||
int n_messages_dropped = 0;
|
||||
int max_errors_per_second = 0;
|
||||
int max_chars_per_second = 0;
|
||||
int max_warnings_per_second = 0;
|
||||
int n_errors_dropped = 0;
|
||||
int n_warnings_dropped = 0;
|
||||
int char_count = 0;
|
||||
int err_count = 0;
|
||||
int warn_count = 0;
|
||||
int last_reset = 0;
|
||||
bool reload_all_scripts = false;
|
||||
Array script_paths_to_reload;
|
||||
|
||||
// Make handlers and send_message thread safe.
|
||||
Mutex mutex;
|
||||
bool flushing = false;
|
||||
Thread::ID flush_thread = 0;
|
||||
|
||||
struct Message {
|
||||
String message;
|
||||
Array data;
|
||||
};
|
||||
|
||||
HashMap<Thread::ID, List<Message>> messages;
|
||||
|
||||
void _poll_messages();
|
||||
bool _has_messages();
|
||||
Array _get_message();
|
||||
|
||||
PrintHandlerList phl;
|
||||
static void _print_handler(void *p_this, const String &p_string, bool p_error, bool p_rich);
|
||||
ErrorHandlerList eh;
|
||||
static void _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);
|
||||
|
||||
ErrorMessage _create_overflow_error(const String &p_what, const String &p_descr);
|
||||
Error _put_msg(const String &p_message, const Array &p_data);
|
||||
|
||||
bool is_peer_connected() { return peer->is_peer_connected(); }
|
||||
void flush_output();
|
||||
|
||||
void _send_stack_vars(List<String> &p_names, List<Variant> &p_vals, int p_type);
|
||||
|
||||
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 <typename T>
|
||||
void _bind_profiler(const String &p_name, T *p_prof);
|
||||
Error _try_capture(const String &p_name, const Array &p_data, bool &r_captured);
|
||||
|
||||
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<RemoteDebuggerPeer> p_peer);
|
||||
~RemoteDebugger();
|
||||
};
|
248
core/debugger/remote_debugger_peer.cpp
Normal file
248
core/debugger/remote_debugger_peer.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
/**************************************************************************/
|
||||
/* remote_debugger_peer.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 "remote_debugger_peer.h"
|
||||
|
||||
#include "core/config/project_settings.h"
|
||||
#include "core/io/marshalls.h"
|
||||
#include "core/os/os.h"
|
||||
|
||||
bool RemoteDebuggerPeerTCP::is_peer_connected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
bool RemoteDebuggerPeerTCP::has_message() {
|
||||
return in_queue.size() > 0;
|
||||
}
|
||||
|
||||
Array RemoteDebuggerPeerTCP::get_message() {
|
||||
MutexLock lock(mutex);
|
||||
List<Array>::Element *E = in_queue.front();
|
||||
ERR_FAIL_NULL_V_MSG(E, Array(), "No remote debugger messages in queue.");
|
||||
|
||||
Array out = E->get();
|
||||
in_queue.pop_front();
|
||||
return out;
|
||||
}
|
||||
|
||||
Error RemoteDebuggerPeerTCP::put_message(const Array &p_arr) {
|
||||
MutexLock lock(mutex);
|
||||
if (out_queue.size() >= max_queued_messages) {
|
||||
return ERR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
out_queue.push_back(p_arr);
|
||||
return OK;
|
||||
}
|
||||
|
||||
int RemoteDebuggerPeerTCP::get_max_message_size() const {
|
||||
return 8 << 20; // 8 MiB
|
||||
}
|
||||
|
||||
void RemoteDebuggerPeerTCP::close() {
|
||||
running = false;
|
||||
if (thread.is_started()) {
|
||||
thread.wait_to_finish();
|
||||
}
|
||||
tcp_client->disconnect_from_host();
|
||||
out_buf.clear();
|
||||
in_buf.clear();
|
||||
}
|
||||
|
||||
RemoteDebuggerPeerTCP::RemoteDebuggerPeerTCP(Ref<StreamPeerTCP> 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
|
||||
tcp_client = p_tcp;
|
||||
if (tcp_client.is_valid()) { // Attaching to an already connected stream.
|
||||
connected = true;
|
||||
running = true;
|
||||
thread.start(_thread_func, this);
|
||||
} else {
|
||||
tcp_client.instantiate();
|
||||
}
|
||||
}
|
||||
|
||||
RemoteDebuggerPeerTCP::~RemoteDebuggerPeerTCP() {
|
||||
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) {
|
||||
mutex.lock();
|
||||
List<Array>::Element *E = out_queue.front();
|
||||
if (!E) {
|
||||
mutex.unlock();
|
||||
break;
|
||||
}
|
||||
Variant var = E->get();
|
||||
out_queue.pop_front();
|
||||
mutex.unlock();
|
||||
int size = 0;
|
||||
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;
|
||||
}
|
||||
int sent = 0;
|
||||
tcp_client->put_partial_data(buf + out_pos, out_left, sent);
|
||||
out_left -= sent;
|
||||
out_pos += sent;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
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;
|
||||
}
|
||||
int read = 0;
|
||||
tcp_client->get_partial_data(buf + in_pos, in_left, read);
|
||||
in_left -= read;
|
||||
in_pos += read;
|
||||
if (in_left == 0) {
|
||||
Variant var;
|
||||
Error err = decode_variant(var, buf, in_pos, &read);
|
||||
ERR_CONTINUE(read != in_pos || err != OK);
|
||||
ERR_CONTINUE_MSG(var.get_type() != Variant::ARRAY, "Malformed packet received, not an Array.");
|
||||
MutexLock lock(mutex);
|
||||
in_queue.push_back(var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Error RemoteDebuggerPeerTCP::connect_to_host(const String &p_host, uint16_t p_port) {
|
||||
IPAddress ip;
|
||||
if (p_host.is_valid_ip_address()) {
|
||||
ip = p_host;
|
||||
} else {
|
||||
ip = IP::get_singleton()->resolve_hostname(p_host);
|
||||
}
|
||||
|
||||
int port = p_port;
|
||||
|
||||
const int tries = 6;
|
||||
const int waits[tries] = { 1, 10, 100, 1000, 1000, 1000 };
|
||||
|
||||
Error err = tcp_client->connect_to_host(ip, port);
|
||||
ERR_FAIL_COND_V_MSG(err != OK, err, vformat("Remote Debugger: Unable to connect to host '%s:%d'.", p_host, port));
|
||||
|
||||
for (int i = 0; i < tries; i++) {
|
||||
tcp_client->poll();
|
||||
if (tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED) {
|
||||
print_verbose("Remote Debugger: Connected!");
|
||||
break;
|
||||
} else {
|
||||
const int ms = waits[i];
|
||||
OS::get_singleton()->delay_usec(ms * 1000);
|
||||
print_verbose("Remote Debugger: Connection failed with status: '" + String::num_int64(tcp_client->get_status()) + "', retrying in " + String::num_int64(ms) + " msec.");
|
||||
}
|
||||
}
|
||||
|
||||
if (tcp_client->get_status() != StreamPeerTCP::STATUS_CONNECTED) {
|
||||
ERR_PRINT(vformat("Remote Debugger: Unable to connect. Status: %s.", String::num_int64(tcp_client->get_status())));
|
||||
return FAILED;
|
||||
}
|
||||
connected = true;
|
||||
running = true;
|
||||
thread.start(_thread_func, this);
|
||||
return OK;
|
||||
}
|
||||
|
||||
void RemoteDebuggerPeerTCP::_thread_func(void *p_ud) {
|
||||
// Update in time for 144hz monitors
|
||||
const uint64_t min_tick = 6900;
|
||||
RemoteDebuggerPeerTCP *peer = static_cast<RemoteDebuggerPeerTCP *>(p_ud);
|
||||
while (peer->running && peer->is_peer_connected()) {
|
||||
uint64_t ticks_usec = OS::get_singleton()->get_ticks_usec();
|
||||
peer->_poll();
|
||||
if (!peer->is_peer_connected()) {
|
||||
break;
|
||||
}
|
||||
ticks_usec = OS::get_singleton()->get_ticks_usec() - ticks_usec;
|
||||
if (ticks_usec < min_tick) {
|
||||
OS::get_singleton()->delay_usec(min_tick - ticks_usec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteDebuggerPeerTCP::poll() {
|
||||
// Nothing to do, polling is done in thread.
|
||||
}
|
||||
|
||||
void RemoteDebuggerPeerTCP::_poll() {
|
||||
tcp_client->poll();
|
||||
if (connected) {
|
||||
_write_out();
|
||||
_read_in();
|
||||
connected = tcp_client->get_status() == StreamPeerTCP::STATUS_CONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
RemoteDebuggerPeer *RemoteDebuggerPeerTCP::create(const String &p_uri) {
|
||||
ERR_FAIL_COND_V(!p_uri.begins_with("tcp://"), nullptr);
|
||||
|
||||
String debug_host = p_uri.replace("tcp://", "");
|
||||
uint16_t debug_port = 6007;
|
||||
|
||||
if (debug_host.contains_char(':')) {
|
||||
int sep_pos = debug_host.rfind_char(':');
|
||||
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);
|
||||
if (err != OK) {
|
||||
memdelete(peer);
|
||||
return nullptr;
|
||||
}
|
||||
return peer;
|
||||
}
|
||||
|
||||
RemoteDebuggerPeer::RemoteDebuggerPeer() {
|
||||
max_queued_messages = (int)GLOBAL_GET("network/limits/debugger/max_queued_messages");
|
||||
}
|
93
core/debugger/remote_debugger_peer.h
Normal file
93
core/debugger/remote_debugger_peer.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/**************************************************************************/
|
||||
/* remote_debugger_peer.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#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 {
|
||||
protected:
|
||||
int max_queued_messages = 4096;
|
||||
|
||||
public:
|
||||
virtual bool is_peer_connected() = 0;
|
||||
virtual int get_max_message_size() const = 0;
|
||||
virtual bool has_message() = 0;
|
||||
virtual Error put_message(const Array &p_arr) = 0;
|
||||
virtual Array get_message() = 0;
|
||||
virtual void close() = 0;
|
||||
virtual void poll() = 0;
|
||||
virtual bool can_block() const { return true; } // If blocking io is allowed on main thread (debug).
|
||||
|
||||
RemoteDebuggerPeer();
|
||||
};
|
||||
|
||||
class RemoteDebuggerPeerTCP : public RemoteDebuggerPeer {
|
||||
private:
|
||||
Ref<StreamPeerTCP> tcp_client;
|
||||
Mutex mutex;
|
||||
Thread thread;
|
||||
List<Array> in_queue;
|
||||
List<Array> out_queue;
|
||||
int out_left = 0;
|
||||
int out_pos = 0;
|
||||
Vector<uint8_t> out_buf;
|
||||
int in_left = 0;
|
||||
int in_pos = 0;
|
||||
Vector<uint8_t> in_buf;
|
||||
bool connected = false;
|
||||
bool running = false;
|
||||
|
||||
static void _thread_func(void *p_ud);
|
||||
|
||||
void _poll();
|
||||
void _write_out();
|
||||
void _read_in();
|
||||
|
||||
public:
|
||||
static RemoteDebuggerPeer *create(const String &p_uri);
|
||||
|
||||
Error connect_to_host(const String &p_host, uint16_t p_port);
|
||||
|
||||
bool is_peer_connected() override;
|
||||
int get_max_message_size() const override;
|
||||
bool has_message() override;
|
||||
Error put_message(const Array &p_arr) override;
|
||||
Array get_message() override;
|
||||
void poll() override;
|
||||
void close() override;
|
||||
|
||||
RemoteDebuggerPeerTCP(Ref<StreamPeerTCP> p_stream = Ref<StreamPeerTCP>());
|
||||
~RemoteDebuggerPeerTCP();
|
||||
};
|
107
core/debugger/script_debugger.cpp
Normal file
107
core/debugger/script_debugger.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
/**************************************************************************/
|
||||
/* script_debugger.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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 "script_debugger.h"
|
||||
|
||||
#include "core/debugger/engine_debugger.h"
|
||||
|
||||
thread_local Vector<ScriptDebugger::StackInfo> ScriptDebugger::error_stack_info;
|
||||
|
||||
void ScriptDebugger::set_lines_left(int p_left) {
|
||||
lines_left = p_left;
|
||||
}
|
||||
|
||||
void ScriptDebugger::set_depth(int p_depth) {
|
||||
depth = p_depth;
|
||||
}
|
||||
|
||||
void ScriptDebugger::insert_breakpoint(int p_line, const StringName &p_source) {
|
||||
if (!breakpoints.has(p_line)) {
|
||||
breakpoints[p_line] = HashSet<StringName>();
|
||||
}
|
||||
breakpoints[p_line].insert(p_source);
|
||||
}
|
||||
|
||||
void ScriptDebugger::remove_breakpoint(int p_line, const StringName &p_source) {
|
||||
if (!breakpoints.has(p_line)) {
|
||||
return;
|
||||
}
|
||||
|
||||
breakpoints[p_line].erase(p_source);
|
||||
if (breakpoints[p_line].is_empty()) {
|
||||
breakpoints.erase(p_line);
|
||||
}
|
||||
}
|
||||
|
||||
String ScriptDebugger::breakpoint_find_source(const String &p_source) const {
|
||||
return p_source;
|
||||
}
|
||||
|
||||
void ScriptDebugger::clear_breakpoints() {
|
||||
breakpoints.clear();
|
||||
}
|
||||
|
||||
void ScriptDebugger::set_skip_breakpoints(bool p_skip_breakpoints) {
|
||||
skip_breakpoints = p_skip_breakpoints;
|
||||
}
|
||||
|
||||
bool ScriptDebugger::is_skipping_breakpoints() {
|
||||
return skip_breakpoints;
|
||||
}
|
||||
|
||||
void ScriptDebugger::set_ignore_error_breaks(bool p_ignore) {
|
||||
ignore_error_breaks = p_ignore;
|
||||
}
|
||||
|
||||
bool ScriptDebugger::is_ignoring_error_breaks() {
|
||||
return ignore_error_breaks;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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<StackInfo> &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(); // Clear because this is thread local
|
||||
}
|
||||
|
||||
Vector<ScriptLanguage::StackInfo> ScriptDebugger::get_error_stack_info() const {
|
||||
return error_stack_info;
|
||||
}
|
||||
|
||||
ScriptLanguage *ScriptDebugger::get_break_language() const {
|
||||
return break_lang;
|
||||
}
|
86
core/debugger/script_debugger.h
Normal file
86
core/debugger/script_debugger.h
Normal file
@@ -0,0 +1,86 @@
|
||||
/**************************************************************************/
|
||||
/* script_debugger.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* 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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/object/script_language.h"
|
||||
#include "core/string/string_name.h"
|
||||
#include "core/templates/hash_set.h"
|
||||
#include "core/templates/vector.h"
|
||||
|
||||
class ScriptDebugger {
|
||||
typedef ScriptLanguage::StackInfo StackInfo;
|
||||
|
||||
bool skip_breakpoints = false;
|
||||
bool ignore_error_breaks = false;
|
||||
|
||||
HashMap<int, HashSet<StringName>> breakpoints;
|
||||
|
||||
static inline thread_local int lines_left = -1;
|
||||
static inline thread_local int depth = -1;
|
||||
static inline thread_local ScriptLanguage *break_lang = nullptr;
|
||||
static thread_local Vector<StackInfo> error_stack_info;
|
||||
|
||||
public:
|
||||
void set_lines_left(int p_left);
|
||||
_ALWAYS_INLINE_ int get_lines_left() const {
|
||||
return lines_left;
|
||||
}
|
||||
|
||||
void set_depth(int p_depth);
|
||||
_ALWAYS_INLINE_ int get_depth() const {
|
||||
return depth;
|
||||
}
|
||||
|
||||
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 set_ignore_error_breaks(bool p_ignore);
|
||||
bool is_ignoring_error_breaks();
|
||||
void insert_breakpoint(int p_line, const StringName &p_source);
|
||||
void remove_breakpoint(int p_line, const StringName &p_source);
|
||||
_ALWAYS_INLINE_ bool is_breakpoint(int p_line, const StringName &p_source) const {
|
||||
if (likely(!breakpoints.has(p_line))) {
|
||||
return false;
|
||||
}
|
||||
return breakpoints[p_line].has(p_source);
|
||||
}
|
||||
void clear_breakpoints();
|
||||
const HashMap<int, HashSet<StringName>> &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;
|
||||
|
||||
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<StackInfo> &p_stack_info);
|
||||
Vector<StackInfo> get_error_stack_info() const;
|
||||
ScriptDebugger() {}
|
||||
};
|
Reference in New Issue
Block a user