diff --git a/CMakeLists.txt b/CMakeLists.txt index c113f7f0d..5714a2c79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,9 +13,6 @@ endif () # ======= option(CODE_COVERAGE "Enable code coverage reporting" OFF) option(ENDSTONE_ENABLE_DEVTOOLS "Build Endstone with DevTools enabled." OFF) -option(ENDSTONE_ENABLE_SENTRY "Enable Sentry integration for error reporting." OFF) -set(ENDSTONE_SENTRY_DSN "" CACHE STRING "Set the Sentry DSN (Data Source Name) for error tracking.") -set(ENDSTONE_SENTRY_ENVIRONMENT "development" CACHE STRING "Set the environment for Sentry (e.g., development, production).") if (NOT BUILD_TESTING STREQUAL OFF) enable_testing() diff --git a/conanfile.py b/conanfile.py index bffcf49ad..8eccaacaa 100644 --- a/conanfile.py +++ b/conanfile.py @@ -35,7 +35,7 @@ class EndstoneRecipe(ConanFile): "capstone/*:m680x": False, "capstone/*:evm": False, "date/*:header_only": True, - "sentry-native/*:backend": "breakpad", + "sentry-native/*:backend": "crashpad", } exports_sources = "CMakeLists.txt", "src/*", "include/*", "tests/*" @@ -164,7 +164,6 @@ def generate(self): tc = CMakeToolchain(self) tc.variables["ENDSTONE_VERSION"] = self.version tc.variables["ENDSTONE_ENABLE_DEVTOOLS"] = self._should_enable_devtools - tc.variables["ENDSTONE_SENTRY_ENVIRONMENT"] = "development" if self._is_dev_build else "production" tc.generate() def build(self): diff --git a/include/endstone/detail/sentry_handler.h b/include/endstone/detail/crash_handler.h similarity index 88% rename from include/endstone/detail/sentry_handler.h rename to include/endstone/detail/crash_handler.h index 5441ff01b..61eae4c23 100644 --- a/include/endstone/detail/sentry_handler.h +++ b/include/endstone/detail/crash_handler.h @@ -13,15 +13,13 @@ // limitations under the License. #pragma once -#ifdef ENDSTONE_WITH_SENTRY namespace endstone::detail { -class SentryHandler { +class CrashHandler { public: - SentryHandler(); - ~SentryHandler(); + CrashHandler(); + ~CrashHandler(); }; } // namespace endstone::detail -#endif diff --git a/include/endstone/detail/server.h b/include/endstone/detail/server.h index d464f6347..994840915 100644 --- a/include/endstone/detail/server.h +++ b/include/endstone/detail/server.h @@ -24,6 +24,7 @@ #include "endstone/detail/ban/ip_ban_list.h" #include "endstone/detail/ban/player_ban_list.h" #include "endstone/detail/command/command_map.h" +#include "endstone/detail/crash_handler.h" #include "endstone/detail/lang/language.h" #include "endstone/detail/level/level.h" #include "endstone/detail/packs/endstone_pack_source.h" @@ -121,6 +122,7 @@ class EndstoneServer : public Server { friend class EndstonePlayer; void enablePlugin(Plugin &plugin); + ServerInstance *server_instance_; Logger &logger_; std::unique_ptr player_ban_list_; @@ -136,10 +138,9 @@ class EndstoneServer : public Server { std::vector> scoreboards_; std::unordered_map> player_boards_; std::chrono::system_clock::time_point start_time_; - Bedrock::NonOwnerPointer resource_pack_repository_; std::unique_ptr resource_pack_source_; - + std::unique_ptr crash_handler_; int tick_counter_ = 0; float current_mspt_ = TargetMillisecondsPerTick * 1.0F; float average_mspt_[TargetTicksPerSecond] = {TargetMillisecondsPerTick}; diff --git a/src/endstone_core/server.cpp b/src/endstone_core/server.cpp index d85d56c73..73e1d3e46 100644 --- a/src/endstone_core/server.cpp +++ b/src/endstone_core/server.cpp @@ -53,13 +53,12 @@ namespace endstone::detail { EndstoneServer::EndstoneServer() : logger_(LoggerFactory::getLogger("Server")) { - register_signal_handler(); - player_ban_list_ = std::make_unique("banned-players.json"); ip_ban_list_ = std::make_unique("banned-ips.json"); language_ = std::make_unique(); plugin_manager_ = std::make_unique(*this); scheduler_ = std::make_unique(*this); + crash_handler_ = std::make_unique(); start_time_ = std::chrono::system_clock::now(); } diff --git a/src/endstone_runtime/CMakeLists.txt b/src/endstone_runtime/CMakeLists.txt index 25508337f..681c0a53e 100644 --- a/src/endstone_runtime/CMakeLists.txt +++ b/src/endstone_runtime/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.15) project(endstone_runtime LANGUAGES CXX) +find_package(sentry REQUIRED) include(FetchContent) FetchContent_Declare( funchook @@ -113,23 +114,15 @@ add_library(endstone_runtime SHARED windows/hook.cpp windows/os.cpp windows/signal_handler.cpp + crash_handler.cpp hook.cpp - loader.cpp main.cpp - sentry_handler.cpp ) add_library(endstone::runtime ALIAS endstone_runtime) -target_link_libraries(endstone_runtime PRIVATE endstone::core funchook::funchook) +target_link_libraries(endstone_runtime PRIVATE endstone::core funchook::funchook sentry::sentry) if (ENDSTONE_ENABLE_DEVTOOLS) target_link_libraries(endstone_runtime PRIVATE endstone::devtools) endif () -if (ENDSTONE_ENABLE_SENTRY) - find_package(sentry REQUIRED) - target_link_libraries(endstone_runtime PRIVATE sentry::sentry) - target_compile_definitions(endstone_runtime PRIVATE ENDSTONE_WITH_SENTRY) - target_compile_definitions(endstone_runtime PRIVATE ENDSTONE_SENTRY_DSN="${ENDSTONE_SENTRY_DSN}") - target_compile_definitions(endstone_runtime PRIVATE ENDSTONE_SENTRY_ENVIRONMENT="${ENDSTONE_SENTRY_ENVIRONMENT}") -endif () if (WIN32) target_link_libraries(endstone_runtime PRIVATE dbghelp.lib ws2_32.lib) target_link_options(endstone_runtime PRIVATE "/INCREMENTAL:NO") @@ -147,12 +140,14 @@ install(TARGETS endstone_runtime LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(TARGETS endstone_runtime DESTINATION "endstone/_internal/" COMPONENT endstone_wheel OPTIONAL) +install(DIRECTORY ${sentry_INCLUDE_DIRS}/../bin/ DESTINATION "endstone/_internal/" COMPONENT endstone_wheel OPTIONAL) + if (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo" AND MSVC) install(FILES $ DESTINATION "endstone/_internal/" COMPONENT endstone_wheel OPTIONAL) endif () if (WIN32) - add_library(endstone_runtime_loader SHARED "loader.cpp") - target_compile_definitions(endstone_runtime_loader PRIVATE _CRT_SECURE_NO_WARNINGS ENDSTONE_RUNTIME_LOADER) + add_library(endstone_runtime_loader SHARED "runtime_loader.cpp") + target_compile_definitions(endstone_runtime_loader PRIVATE _CRT_SECURE_NO_WARNINGS) install(TARGETS endstone_runtime_loader DESTINATION "endstone/_internal/" COMPONENT endstone_wheel OPTIONAL) -endif () \ No newline at end of file +endif () diff --git a/src/endstone_runtime/crash_handler.cpp b/src/endstone_runtime/crash_handler.cpp new file mode 100644 index 000000000..feaed2c5d --- /dev/null +++ b/src/endstone_runtime/crash_handler.cpp @@ -0,0 +1,137 @@ +// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "endstone/detail/crash_handler.h" + +#include + +#include +#include +#include + +#include +#include + +#include "endstone/detail/os.h" +#include "endstone/endstone.h" + +namespace fs = std::filesystem; + +namespace endstone::detail { + +namespace { + +void print_frame(std::ostream &stream, bool color, unsigned frame_number_width, std::size_t counter, + const cpptrace::stacktrace_frame &frame) +{ + const auto *reset = color ? "\033[0m" : ""; + const auto *green = color ? "\033[32m" : ""; + const auto *yellow = color ? "\033[33m" : ""; + const auto *blue = color ? "\033[34m" : ""; + std::string line = fmt::format("[{:<{}}] ", counter, frame_number_width); + if (frame.is_inline) { + line += fmt::format("{:<{}}", "(inlined)", 2 * sizeof(cpptrace::frame_ptr) + 2); + } + else { + line += fmt::format("{}0x{:<{}x}{}", blue, frame.raw_address, 2 * sizeof(cpptrace::frame_ptr), reset); + line += fmt::format(" {}(0x{:09x}){}", green, frame.object_address, reset); + } + if (!frame.symbol.empty()) { + line += fmt::format(" in {}{}{}", yellow, frame.symbol, reset); + } + if (!frame.filename.empty()) { + line += fmt::format(" at {}{}{}", green, frame.filename, reset); + if (frame.line.has_value()) { + line += fmt::format(":{}{}{}", blue, frame.line.value(), reset); + if (frame.column.has_value()) { + line += fmt::format(":{}{}{}", blue, frame.column.value(), reset); + } + } + } + stream << line; +} + +void print_crash_dump(const std::string &message, std::size_t skip = 0) +{ + printf("=== ENDSTONE CRASHED! - PLEASE REPORT THIS AS AN ISSUE ON GITHUB ===\n"); + printf("Operation system: %s\n", os::get_name().c_str()); + printf("Endstone version: %s\n", ENDSTONE_VERSION); + printf("Api version : %s\n", ENDSTONE_API_VERSION); + printf("Description : %s\n", message.c_str()); + + auto stacktrace = cpptrace::generate_trace(skip + 1); + auto &stream = std::cerr; + auto color = cpptrace::isatty(cpptrace::stderr_fileno); + stream << "Stack trace (most recent call first):" << '\n'; + + auto &frames = stacktrace.frames; + std::size_t counter = 0; + if (frames.empty()) { + stream << "" << '\n'; + return; + } + const auto frame_number_width = std::to_string(frames.size()).length(); + for (const auto &frame : frames) { + print_frame(stream, color, frame_number_width, counter, frame); + stream << '\n'; + if (frame.line.has_value() && !frame.filename.empty()) { + stream << cpptrace::get_snippet(frame.filename, frame.line.value(), 2, color); + } + counter++; + } +} + +sentry_value_t on_crash(const sentry_ucontext_t *ctx, sentry_value_t event, void *closure) +{ +#ifdef _WIN32 + print_crash_dump("Exception unhandled: " + + fmt::format("{0:#x}", ctx->exception_ptrs.ExceptionRecord->ExceptionCode)); +#else + print_crash_dump("Signal received: " + std::to_string(ctx.signum)); +#endif + + // TODO(crash_handler): filter out crashes originates from BDS / plugins + return event; +} +} // namespace + +CrashHandler::CrashHandler() +{ + constexpr auto dsn = + "https://69c28eeaef4651abcf0bbeace6a1175c@o4508553519431680.ingest.de.sentry.io/4508569040519248"; +#ifdef _WIN32 + fs::path handler_path = (fs::path{os::get_module_pathname()}.parent_path()) / "crashpad_handler.exe"; +#else + fs::path handler_path = (fs::path{os::get_module_pathname()}.parent_path()) / "crashpad_handler"; +#endif + constexpr std::string_view release = "endstone@" ENDSTONE_VERSION; + constexpr bool is_dev = release.find("dev") != std::string_view::npos; + + sentry_options_t *options = sentry_options_new(); + sentry_options_set_dsn(options, dsn); + sentry_options_set_database_path(options, ".sentry-native"); + sentry_options_set_handler_path(options, handler_path.string().c_str()); + sentry_options_set_release(options, std::string(release).c_str()); + sentry_options_set_debug(options, 1); + sentry_options_set_on_crash(options, on_crash, nullptr); + sentry_options_set_environment(options, is_dev ? "development" : "production"); + sentry_init(options); +} + +CrashHandler::~CrashHandler() +{ + sentry_close(); +} + +} // namespace endstone::detail diff --git a/src/endstone_runtime/loader.cpp b/src/endstone_runtime/runtime_loader.cpp similarity index 98% rename from src/endstone_runtime/loader.cpp rename to src/endstone_runtime/runtime_loader.cpp index 999865930..30f6fe30f 100644 --- a/src/endstone_runtime/loader.cpp +++ b/src/endstone_runtime/runtime_loader.cpp @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifdef ENDSTONE_RUNTIME_LOADER - #include #include @@ -58,5 +56,3 @@ BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID /*reserved*/) } return TRUE; } - -#endif diff --git a/src/endstone_runtime/sentry_handler.cpp b/src/endstone_runtime/sentry_handler.cpp deleted file mode 100644 index 172030aec..000000000 --- a/src/endstone_runtime/sentry_handler.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2024, The Endstone Project. (https://endstone.dev) All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifdef ENDSTONE_WITH_SENTRY - -#include "endstone/detail/sentry_handler.h" - -#include - -namespace endstone::detail { - -namespace { -sentry_value_t on_crash(const sentry_ucontext_t *ctx, sentry_value_t event, void *closure) -{ - // TODO(misc): print stack traces here - return event; -} -} // namespace - -SentryHandler::SentryHandler() -{ - static_assert(sizeof(ENDSTONE_SENTRY_DSN) > 1, "The Sentry DSN is not set"); - sentry_options_t *options = sentry_options_new(); - sentry_options_set_dsn(options, ENDSTONE_SENTRY_DSN); - sentry_options_set_database_path(options, "data/.sentry-native"); - // TODO: set crashpad handler path - sentry_options_set_release(options, "endstone@" ENDSTONE_VERSION); - sentry_options_set_environment(options, ENDSTONE_SENTRY_ENVIRONMENT); - sentry_init(options); -} - -SentryHandler::~SentryHandler() -{ - sentry_close(); -} - -} // namespace endstone::detail - -#endif