Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions code/framework/src/external/sentry/wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include <filesystem>
#include <memory>
#include <system_error>

namespace Framework::External::Sentry {
namespace {
Expand Down Expand Up @@ -51,9 +52,17 @@ namespace Framework::External::Sentry {
default: return "info";
}
}

void AddOptionsAttachment(sentry_options_t *options, const std::filesystem::path &path) {
#ifdef _WIN32
sentry_options_add_attachmentw(options, path.c_str());
#else
sentry_options_add_attachment(options, path.c_str());
#endif
}
} // namespace

Utils::Result<void, Framework::Error> Wrapper::Init(const InitOptions &options) {
Utils::Result<void, Framework::Error> Wrapper::Init(const InitOptions &options) try {
// sentry_init takes ownership below; every path that bails out before that
// has to free the payload itself, so let a guard own it until then.
std::unique_ptr<sentry_options_t, decltype(&sentry_options_free)> opts(sentry_options_new(), sentry_options_free);
Expand Down Expand Up @@ -89,17 +98,25 @@ namespace Framework::External::Sentry {
}
}

sentry_options_set_handler_path(opts.get(), breakpadFile.path().c_str());
sentry_options_set_database_path(opts.get(), cacheDirectory.path().c_str());
// The framework accepts native filesystem strings; Sentry's narrow Windows
// API requires UTF-8. Preserve the native path through its wide API instead.
const std::filesystem::path handlerPath(breakpadFile.path());
const std::filesystem::path databasePath(cacheDirectory.path());
#ifdef _WIN32
sentry_options_set_handler_pathw(opts.get(), handlerPath.c_str());
sentry_options_set_database_pathw(opts.get(), databasePath.c_str());
#else
sentry_options_set_handler_path(opts.get(), handlerPath.c_str());
sentry_options_set_database_path(opts.get(), databasePath.c_str());
#endif

// Crashpad reads attachments lazily at crash time, so cef.log carries the CHECK/FATAL
// line CEF writes before it fast-fails. Registered before init so the handler knows it.
const std::string cefLog = std::filesystem::absolute(std::filesystem::path(options.handlerPath) / "logs" / "cef.log").string();
sentry_options_add_attachment(opts.get(), cefLog.c_str());
const auto cefLog = std::filesystem::absolute(std::filesystem::path(options.handlerPath) / "logs" / "cef.log");
AddOptionsAttachment(opts.get(), cefLog);

for (const auto &attachment : options.attachments) {
const std::string absolute = std::filesystem::absolute(std::filesystem::path(attachment)).string();
sentry_options_add_attachment(opts.get(), absolute.c_str());
AddOptionsAttachment(opts.get(), std::filesystem::absolute(std::filesystem::path(attachment)));
}

if (sentry_init(opts.release()) != 0) {
Expand All @@ -112,6 +129,9 @@ namespace Framework::External::Sentry {
CrashTrail::Install();
return {};
}
catch (const std::system_error &error) {
return Framework::Error("Failed to configure Sentry paths: " + std::string(error.what()), error.code().value());
}

bool Wrapper::Flush(uint32_t timeoutMs) const {
if (!_initialized) {
Expand All @@ -129,14 +149,21 @@ namespace Framework::External::Sentry {
Lifecycle::Shutdown();
}

Utils::Result<void, Framework::Error> Wrapper::AddAttachment(const std::string &path) const {
Utils::Result<void, Framework::Error> Wrapper::AddAttachment(const std::string &path) const try {
if (!_initialized) {
return Framework::Error {"Sentry is not initialized"};
}
const std::string absolute = std::filesystem::absolute(std::filesystem::path(path)).string();
const auto absolute = std::filesystem::absolute(std::filesystem::path(path));
#ifdef _WIN32
sentry_attach_filew(absolute.c_str());
#else
sentry_attach_file(absolute.c_str());
#endif
return {};
}
catch (const std::system_error &error) {
return Framework::Error("Failed to configure Sentry attachment path: " + std::string(error.what()), error.code().value());
}

void Wrapper::SetTag(const std::string &key, const std::string &value) const {
if (!_initialized) {
Expand Down
1 change: 1 addition & 0 deletions code/framework/src/external/sentry/wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ namespace Framework::External::Sentry {

struct InitOptions {
std::string dsn;
// Filesystem paths use the native narrow encoding, including the Windows code page.
std::string handlerPath;
std::string release;
std::string environment;
Expand Down
11 changes: 11 additions & 0 deletions code/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ if(WIN32)
../framework/src/utils/hooking/hooking.cpp
../framework/src/graphics/backend/d3d12_ui_compositor.cpp)
target_link_libraries(FrameworkTests d3d12 dxgi d3dcompiler)

add_executable(FrameworkSentryTests sentry_ut.cpp)
target_include_directories(FrameworkSentryTests PRIVATE ../framework/src)
target_link_libraries(FrameworkSentryTests PRIVATE Framework)
add_custom_target(RunFrameworkSentryTests
COMMAND $<TARGET_FILE:FrameworkSentryTests>
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
DEPENDS FrameworkSentryTests)
endif()

target_include_directories(FrameworkTests PRIVATE . ../framework/src)
Expand All @@ -39,6 +47,9 @@ add_custom_target(RunFrameworkTests
COMMAND $<TARGET_FILE:FrameworkTests>
COMMAND $<TARGET_FILE:FrameworkClientScriptingTests>
DEPENDS FrameworkTests FrameworkClientScriptingTests)
if(WIN32)
add_dependencies(RunFrameworkTests RunFrameworkSentryTests)
endif()

# NodeEngine and V8Engine each initialize V8's process-wide platform, so exercise
# the client runtime in its own process.
Expand Down
148 changes: 148 additions & 0 deletions code/tests/sentry_ut.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* MafiaHub OSS license
* Copyright (c) 2026, MafiaHub. All rights reserved.
*
* This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework.
* See LICENSE file in the source repository for information regarding licensing.
*/

#include <utils/safe_win32.h>

#include <external/sentry/wrapper.h>
#include <logging/logger.h>

#include <cstdio>
#include <exception>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <string_view>

namespace {
namespace fs = std::filesystem;

fs::path NativeUnicodeDirectory() {
// Choose a lossless fixture for the active Windows code page; East Asian
// code pages cannot necessarily represent the Turkish desktop name.
for (const auto name : {L"Masa\u00FCst\u00FC", L"\u041F\u0440\u0438\u0432\u0435\u0442", L"\u65E5\u672C", L"\u4E2D\u6587", L"\uD55C\uAE00", L"\u03A9", L"\u05D0", L"\u0627", L"\u0E01"}) {
try {
const fs::path candidate(name);
if (fs::path(candidate.string()) == candidate) {
return candidate;
}
}
catch (const std::system_error &) {
}
}
throw std::runtime_error("No non-ASCII fixture is representable in the native code page.");
}

int RunCase(std::wstring_view scenario, const fs::path &sandbox, const fs::path &handler) {
Framework::Logging::GetInstance()->PauseLogging(true);
const fs::path root = sandbox / (scenario == L"native-unicode" ? NativeUnicodeDirectory() : fs::path(scenario));
fs::create_directories(root);
if (scenario != L"missing-handler") {
fs::copy_file(handler, root / "crashpad_handler.exe");
}
if (scenario == L"blocked-cache") {
std::ofstream(root / "cache") << "A file blocks the cache directory.";
}
fs::create_directories(root / "logs");
const fs::path attachment = root / "logs" / NativeUnicodeDirectory();
std::ofstream(attachment) << "attachment regression";

Framework::External::Sentry::InitOptions options;
// An empty DSN disables uploads while exercising the real Crashpad backend.
options.dsn = "";
options.handlerPath = root.string();
options.attachments.push_back(attachment.string());
if (scenario == L"relative") {
fs::current_path(root);
options.handlerPath = ".";
options.attachments = {"logs/log.txt"};
}

Framework::External::Sentry::Wrapper reporter;
const auto result = reporter.Init(options);
const bool expectFailure = scenario == L"missing-handler" || scenario == L"blocked-cache";
if (expectFailure) {
if (result || reporter.IsInitialized()) {
std::fprintf(stderr, "Expected initialization to return an error.\n");
return 1;
}
std::printf("Expected error: %s\n", result.GetError().message.c_str());
return 0;
}
if (!result) {
std::fprintf(stderr, "Initialization failed: %s\n", result.GetError().message.c_str());
return 1;
}
const bool initialized = reporter.IsInitialized() && fs::is_directory(root / "cache" / "sentry");
const auto attached = reporter.AddAttachment(attachment.string());
reporter.Shutdown();
return initialized && attached && !reporter.IsInitialized() ? 0 : 1;
}

bool RunChild(const fs::path &executable, std::wstring_view scenario, const fs::path &sandbox) {
std::wstring command = L"\"" + executable.wstring() + L"\" --case " + std::wstring(scenario) + L" \"" + sandbox.wstring() + L"\"";
STARTUPINFOW startup {sizeof(startup)};
PROCESS_INFORMATION process {};
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process)) {
std::fprintf(stderr, "CreateProcessW failed: %lu\n", GetLastError());
return false;
}
const DWORD wait = WaitForSingleObject(process.hProcess, 30000);
DWORD exitCode = 1;
if (wait == WAIT_OBJECT_0) {
GetExitCodeProcess(process.hProcess, &exitCode);
}
else {
TerminateProcess(process.hProcess, 1);
WaitForSingleObject(process.hProcess, 5000);
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
std::printf("%ls: %s (exit 0x%08lX)\n", std::wstring(scenario).c_str(), exitCode == 0 ? "PASS" : "FAIL", exitCode);
return exitCode == 0;
}
} // namespace

int wmain(int argc, wchar_t **argv) {
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
wchar_t executableName[32768] {};
if (!GetModuleFileNameW(nullptr, executableName, 32768)) {
return 1;
}
const fs::path executable(executableName);
const fs::path handler = executable.parent_path() / "crashpad_handler.exe";
try {
if (argc == 4 && std::wstring_view(argv[1]) == L"--case") {
return RunCase(argv[2], fs::path(argv[3]), handler);
}

// Each SDK instance gets its own process, so an access violation fails a case
// without preventing the remaining cases from running.
const fs::path sandbox = fs::current_path() / ("framework_ut_sentry_" + std::to_string(GetCurrentProcessId()));
if (!fs::create_directory(sandbox)) {
std::fprintf(stderr, "Test sandbox already exists.\n");
return 1;
}
std::printf("Windows native code page: %u\n", GetACP());
bool passed = true;
for (const auto scenario : {L"ascii", L"native-unicode", L"relative", L"missing-handler", L"blocked-cache"}) {
passed = RunChild(executable, scenario, sandbox) && passed;
}
std::error_code cleanupError;
fs::remove_all(sandbox, cleanupError);
if (cleanupError) {
std::fprintf(stderr, "Could not remove test sandbox: %s\n", cleanupError.message().c_str());
passed = false;
}
return passed ? 0 : 1;
}
catch (const std::exception &error) {
std::fprintf(stderr, "Sentry test failed: %s\n", error.what());
return 1;
}
}
Loading