diff --git a/.github/workflows/linux-build-test.yml b/.github/workflows/linux-build-test.yml index c3d3b474..0dae16be 100644 --- a/.github/workflows/linux-build-test.yml +++ b/.github/workflows/linux-build-test.yml @@ -34,7 +34,8 @@ jobs: libevdev-dev \ libudev-dev \ libdrm-dev \ - libpci-dev + libpci-dev \ + libunwind-dev - name: Setup Rust uses: ATiltedTree/setup-rust@v1 @@ -117,6 +118,7 @@ jobs: libudev-dev \ libdrm-dev \ libpci-dev \ + libunwind-dev \ ${{ join(matrix.other_pkgs, ' ') }} - name: Setup Rust diff --git a/docker/wolf.Dockerfile b/docker/wolf.Dockerfile index 9590ce49..b3c84616 100644 --- a/docker/wolf.Dockerfile +++ b/docker/wolf.Dockerfile @@ -76,6 +76,7 @@ RUN apt-get update -y && \ libcurl4 \ libdrm2 \ libpci3 \ + libunwind8 \ && rm -rf /var/lib/apt/lists/* # gst-plugin-wayland runtime dependencies diff --git a/src/moonlight-server/CMakeLists.txt b/src/moonlight-server/CMakeLists.txt index 2fe215a4..7cd9e2ef 100644 --- a/src/moonlight-server/CMakeLists.txt +++ b/src/moonlight-server/CMakeLists.txt @@ -54,8 +54,15 @@ unset(CMAKE_PROJECT_INCLUDE_BEFORE) FetchContent_Declare( cpptrace GIT_REPOSITORY https://github.com/jeremy-rifkin/cpptrace.git - GIT_TAG v0.3.1 + GIT_TAG 448c325 ) +find_package(PkgConfig) +pkg_check_modules(LIBUNWIND QUIET libunwind) +if(LIBUNWIND_FOUND) + set(CPPTRACE_UNWIND_WITH_LIBUNWIND ON) +else () + message(WARNING "Missing libunwind, stacktraces will not be available.") +endif () FetchContent_MakeAvailable(cpptrace) ############################## diff --git a/src/moonlight-server/exceptions/exceptions.h b/src/moonlight-server/exceptions/exceptions.h new file mode 100644 index 00000000..61d5eb35 --- /dev/null +++ b/src/moonlight-server/exceptions/exceptions.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include + +static void safe_dump_stacktrace_to(std::ostream &out) { + constexpr std::size_t N = 100; + cpptrace::frame_ptr buffer[N]; + std::size_t count = cpptrace::safe_generate_raw_trace(buffer, N); + if (count > 0) { + out << count << '\n'; + for (std::size_t i = 0; i < count; i++) { + cpptrace::safe_object_frame frame{}; + cpptrace::get_safe_object_frame(buffer[i], &frame); + out << frame.address_relative_to_object_start << ' ' << frame.raw_address << ' '; + out.write(frame.object_path, sizeof(frame.object_path)); + out << '\n'; + } + } +} + +static std::unique_ptr load_stacktrace_from(std::istream &in) { + cpptrace::object_trace trace{}; + std::size_t count; + in >> count; + in.ignore(); // ignore newline + for (std::size_t i = 0; i < count; i++) { + cpptrace::safe_object_frame frame{}; + in >> frame.address_relative_to_object_start; + in.ignore(); // ignore space + in >> frame.raw_address; + in.ignore(); // ignore space + in.read(frame.object_path, sizeof(frame.object_path)); + in.ignore(); // ignore newline + try { + trace.frames.push_back(frame.resolve()); + } catch (std::exception &ex) { + logs::log(logs::debug, "Unable to parse stacktrace frame, skipping"); + } + } + return std::make_unique(trace); +} diff --git a/src/moonlight-server/wolf.cpp b/src/moonlight-server/wolf.cpp index d3a713af..f661b4a0 100644 --- a/src/moonlight-server/wolf.cpp +++ b/src/moonlight-server/wolf.cpp @@ -5,8 +5,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -402,9 +402,38 @@ auto setup_sessions_handlers(const immer::box &app_state, return handlers.persistent(); } -static void terminate_handler() { - cpptrace::generate_trace().print(); - std::abort(); +static std::string backtrace_file_src() { + return fmt::format("{}/backtrace.dump", utils::get_env("WOLF_CFG_FOLDER", ".")); +} + +static void shutdown_handler(int signum) { + logs::log(logs::info, "Received interrupt signal {}, clean exit", signum); + if (signum == SIGABRT || signum == SIGSEGV) { + auto stack_file = backtrace_file_src(); + logs::log(logs::error, "Runtime error, dumping stacktrace to {}", stack_file); + std::ofstream fs(stack_file); + safe_dump_stacktrace_to(fs); + fs.close(); + } + + logs::log(logs::info, "See ya!"); + exit(signum); +} + +/** + * @brief: if an exception was raised we should have created a dump file, here we can pretty print it + */ +static void check_exceptions() { + auto stack_file = backtrace_file_src(); + if (boost::filesystem::exists(stack_file)) { + std::ifstream ifs(stack_file); + load_stacktrace_from(ifs)->resolve().print(); + ifs.close(); + auto now = std::chrono::system_clock::now(); + boost::filesystem::rename( + stack_file, + fmt::format("{}/backtrace.{:%Y-%m-%d-%H-%M-%S}.dump", utils::get_env("WOLF_CFG_FOLDER", "."), now)); + } } /** @@ -413,7 +442,13 @@ static void terminate_handler() { int main(int argc, char *argv[]) { logs::init(logs::parse_level(utils::get_env("WOLF_LOG_LEVEL", "INFO"))); // Exception and termination handling - std::set_terminate(terminate_handler); + std::signal(SIGINT, shutdown_handler); + std::signal(SIGTERM, shutdown_handler); + std::signal(SIGQUIT, shutdown_handler); + std::signal(SIGSEGV, shutdown_handler); + std::signal(SIGABRT, shutdown_handler); + std::set_terminate([]() { shutdown_handler(SIGABRT); }); + check_exceptions(); streaming::init(); // Need to initialise gstreamer once control::init(); // Need to initialise enet once diff --git a/tests/testExceptions.cpp b/tests/testExceptions.cpp new file mode 100644 index 00000000..5ba988bf --- /dev/null +++ b/tests/testExceptions.cpp @@ -0,0 +1,21 @@ +#include "catch2/catch_all.hpp" +using Catch::Matchers::EndsWith; +using Catch::Matchers::Equals; + +#include +#include + +TEST_CASE("Exceptions", "[Exceptions]") { + std::ofstream ofs("stacktrace.txt"); + safe_dump_stacktrace_to(ofs); + ofs.close(); + + std::ifstream ifs("stacktrace.txt"); + auto trace = load_stacktrace_from(ifs); + REQUIRE(trace->frames.size() > 0); + + auto stacktrace = trace->resolve(); + REQUIRE(stacktrace.frames.size() > 0); + REQUIRE_THAT(stacktrace.frames[0].filename, EndsWith("src/moonlight-server/exceptions/exceptions.h")); + REQUIRE_THAT(stacktrace.frames[0].symbol, Equals("safe_dump_stacktrace_to")); +} \ No newline at end of file