diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc36d783..3e5f58244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Features**: +- Windows: add WER integration for syncing tags and attachments to WER. ([#1837](https://github.com/getsentry/sentry-native/pull/1837)) - Report `cache_overflow` discards due to `cache_max_items` or `cache_max_size`. ([#1884](https://github.com/getsentry/sentry-native/pull/1884)) **Fixes**: diff --git a/CMakeLists.txt b/CMakeLists.txt index e26c4163f..5afd2a5b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -963,6 +963,14 @@ if(SENTRY_INTEGRATION_QT) target_link_libraries(sentry PRIVATE Qt${Qt_VERSION_MAJOR}::Core) endif() +option(SENTRY_INTEGRATION_WER "Build WER (Windows Error Reporting) integration") +if(SENTRY_INTEGRATION_WER AND WIN32 + AND (SENTRY_BACKEND_BREAKPAD OR SENTRY_BACKEND_CRASHPAD)) + message(WARNING + "SENTRY_BACKEND=${SENTRY_BACKEND} is not compatible with WER. " + "Use SENTRY_BACKEND=none, inproc, or native with SENTRY_INTEGRATION_WER.") +endif() + include(CMakePackageConfigHelpers) configure_package_config_file(sentry-config.cmake.in sentry-config.cmake INSTALL_DESTINATION "${CMAKE_INSTALL_CMAKEDIR}") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0a2f48839..bf37dd18a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -307,6 +307,13 @@ if(SENTRY_INTEGRATION_QT) integrations/sentry_integration_qt.h ) endif() +if(SENTRY_INTEGRATION_WER AND WIN32) + target_compile_definitions(sentry PRIVATE SENTRY_INTEGRATION_WER) + sentry_target_sources_cwd(sentry + integrations/sentry_integration_wer.c + integrations/sentry_integration_wer.h + ) +endif() # screenshot if(SENTRY_SCREENSHOT_WINDOWS) diff --git a/src/integrations/sentry_integration_wer.c b/src/integrations/sentry_integration_wer.c new file mode 100644 index 000000000..c83a39579 --- /dev/null +++ b/src/integrations/sentry_integration_wer.c @@ -0,0 +1,283 @@ +#include "sentry_integration_wer.h" + +#include "sentry_alloc.h" +#include "sentry_attachment.h" +#include "sentry_core.h" +#include "sentry_logger.h" +#include "sentry_path.h" +#include "sentry_scope.h" +#include "sentry_string.h" + +#include +#include +#include + +// Windows 8+ SDK +#ifndef WER_FILE_ANONYMOUS_DATA +# define WER_FILE_ANONYMOUS_DATA 0x2 +#endif +#ifndef WER_MAX_PARAM_LENGTH +# define WER_MAX_PARAM_LENGTH (MAX_PATH) +#endif +#ifndef WER_MAX_MEM_BLOCK_SIZE +# define WER_MAX_MEM_BLOCK_SIZE (64 * 1024) +#endif +#ifndef WER_METADATA_KEY_MAX_LENGTH +# define WER_METADATA_KEY_MAX_LENGTH 64 +#endif +#ifndef WER_METADATA_VALUE_MAX_LENGTH +# define WER_METADATA_VALUE_MAX_LENGTH 128 +#endif + +typedef struct sentry_integration_wer_data_s { + HRESULT(WINAPI *WerRegisterCustomMetadata)(PCWSTR, PCWSTR); + HRESULT(WINAPI *WerUnregisterCustomMetadata)(PCWSTR); + sentry_scope_t *scope; + sentry_scope_observer_t *observer; +} sentry_integration_wer_data_t; + +static void +wer_init(sentry_integration_wer_data_t *wer_data) +{ + if (wer_data->WerRegisterCustomMetadata) { + return; + } + HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); + if (kernel32) { + wer_data->WerRegisterCustomMetadata = (HRESULT(WINAPI *)(PCWSTR, + PCWSTR))GetProcAddress(kernel32, "WerRegisterCustomMetadata"); + wer_data->WerUnregisterCustomMetadata = (HRESULT(WINAPI *)( + PCWSTR))GetProcAddress(kernel32, "WerUnregisterCustomMetadata"); + } + if (!wer_data->WerRegisterCustomMetadata) { + SENTRY_DEBUG("WerRegisterCustomMetadata not available; " + "tag sync to WER will be skipped"); + } +} + +static void +sentry_integration_wer_free(void *data) +{ + sentry_free(data); +} + +static void +wer_set_tag(void *data, const char *key, const char *value) +{ + sentry_integration_wer_data_t *wer_data + = (sentry_integration_wer_data_t *)data; + if (!wer_data->WerRegisterCustomMetadata) { + return; + } + if (!key || !value) { + return; + } + + wchar_t *key_w = sentry__string_to_wstr(key); + wchar_t *value_w = sentry__string_to_wstr(value); + if (!key_w || !value_w || wcslen(key_w) > WER_METADATA_KEY_MAX_LENGTH + || wcslen(value_w) > WER_METADATA_VALUE_MAX_LENGTH) { + sentry_free(key_w); + sentry_free(value_w); + return; + } + + HRESULT hr = wer_data->WerRegisterCustomMetadata(key_w, value_w); + if (FAILED(hr)) { + SENTRY_WARNF( + "WerRegisterCustomMetadata failed: hr=0x%08lx", (unsigned long)hr); + } + + sentry_free(key_w); + sentry_free(value_w); +} + +static void +wer_remove_tag(void *data, const char *key) +{ + sentry_integration_wer_data_t *wer_data + = (sentry_integration_wer_data_t *)data; + if (!wer_data->WerUnregisterCustomMetadata) { + return; + } + if (!key) { + return; + } + + wchar_t *key_w = sentry__string_to_wstr(key); + if (!key_w || wcslen(key_w) > WER_METADATA_KEY_MAX_LENGTH) { + sentry_free(key_w); + return; + } + + HRESULT hr = wer_data->WerUnregisterCustomMetadata(key_w); + if (FAILED(hr)) { + SENTRY_WARNF("WerUnregisterCustomMetadata failed: hr=0x%08lx", + (unsigned long)hr); + } + + sentry_free(key_w); +} + +static void +wer_add_attachment(void *UNUSED(data), sentry_attachment_t *attachment) +{ + if (!attachment) { + return; + } + + if (attachment->path) { + sentry_path_t *path = sentry__path_absolute(attachment->path); + if (!path || wcslen(path->path_w) > WER_MAX_PARAM_LENGTH) { + sentry__path_free(path); + return; + } + HRESULT hr = WerRegisterFile( + path->path_w, WerRegFileTypeOther, WER_FILE_ANONYMOUS_DATA); + if (FAILED(hr)) { + SENTRY_WARNF( + "WerRegisterFile failed: hr=0x%08lx", (unsigned long)hr); + } + sentry__path_free(path); + return; + } + + if (attachment->buf && attachment->buf_len > 0 + && attachment->buf_len <= WER_MAX_MEM_BLOCK_SIZE) { + HRESULT hr = WerRegisterMemoryBlock( + (PVOID)attachment->buf, (DWORD)attachment->buf_len); + if (FAILED(hr)) { + SENTRY_WARNF( + "WerRegisterMemoryBlock failed: hr=0x%08lx", (unsigned long)hr); + } + return; + } +} + +static void +wer_remove_attachment(void *UNUSED(data), sentry_attachment_t *attachment) +{ + if (!attachment) { + return; + } + + if (attachment->path) { + sentry_path_t *path = sentry__path_absolute(attachment->path); + if (!path || wcslen(path->path_w) > WER_MAX_PARAM_LENGTH) { + sentry__path_free(path); + return; + } + HRESULT hr = WerUnregisterFile(path->path_w); + if (FAILED(hr)) { + SENTRY_WARNF( + "WerUnregisterFile failed: hr=0x%08lx", (unsigned long)hr); + } + sentry__path_free(path); + return; + } + + if (attachment->buf && attachment->buf_len > 0 + && attachment->buf_len <= WER_MAX_MEM_BLOCK_SIZE) { + HRESULT hr = WerUnregisterMemoryBlock((PVOID)attachment->buf); + if (FAILED(hr)) { + SENTRY_WARNF("WerUnregisterMemoryBlock failed: hr=0x%08lx", + (unsigned long)hr); + } + } +} + +static void +wer_cleanup_tag(const char *key, sentry_value_t UNUSED(value), void *data) +{ + wer_remove_tag(data, key); +} + +static void +wer_clear(void *data) +{ + sentry_integration_wer_data_t *wer_data + = (sentry_integration_wer_data_t *)data; + sentry_scope_t *scope = wer_data->scope; + if (!scope) { + return; + } + + sentry__value_foreach_key_value(scope->tags, wer_cleanup_tag, wer_data); + + for (sentry_attachment_t *attachment = scope->attachments; attachment; + attachment = attachment->next) { + wer_remove_attachment(wer_data, attachment); + } +} + +static void +register_wer( + void *data, sentry_scope_t *scope, const sentry_options_t *UNUSED(options)) +{ + sentry_integration_wer_data_t *wer_data + = (sentry_integration_wer_data_t *)data; + wer_init(wer_data); + + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + if (!observer) { + return; + } + observer->clear = wer_clear; + observer->set_tag = wer_set_tag; + observer->remove_tag = wer_remove_tag; + observer->add_attachment = wer_add_attachment; + observer->remove_attachment = wer_remove_attachment; + observer->data = wer_data; + + if (sentry__scope_add_observer(scope, observer)) { + wer_data->scope = scope; + wer_data->observer = observer; + for (sentry_attachment_t *attachment = scope->attachments; attachment; + attachment = attachment->next) { + wer_add_attachment(wer_data, attachment); + } + } +} + +static void +unregister_wer( + void *data, sentry_scope_t *scope, const sentry_options_t *UNUSED(options)) +{ + sentry_integration_wer_data_t *wer_data + = (sentry_integration_wer_data_t *)data; + if (!wer_data->observer) { + return; + } + + sentry__value_foreach_key_value(scope->tags, wer_cleanup_tag, wer_data); + + for (sentry_attachment_t *attachment = scope->attachments; attachment; + attachment = attachment->next) { + wer_remove_attachment(wer_data, attachment); + } + + sentry__scope_remove_observer(scope, wer_data->observer); + wer_data->scope = NULL; + wer_data->observer = NULL; +} + +sentry_integration_t * +sentry_integration_wer_new(void) +{ + sentry_integration_t *integration = SENTRY_MAKE(sentry_integration_t); + if (!integration) { + return NULL; + } + + integration->data = SENTRY_MAKE(sentry_integration_wer_data_t); + if (!integration->data) { + sentry_free(integration); + return NULL; + } + + integration->register_func = register_wer; + integration->unregister_func = unregister_wer; + integration->free_func = sentry_integration_wer_free; + + return integration; +} diff --git a/src/integrations/sentry_integration_wer.h b/src/integrations/sentry_integration_wer.h new file mode 100644 index 000000000..2fc00de08 --- /dev/null +++ b/src/integrations/sentry_integration_wer.h @@ -0,0 +1,11 @@ +#ifndef SENTRY_INTEGRATION_WER_H_INCLUDED +#define SENTRY_INTEGRATION_WER_H_INCLUDED + +#include "sentry_integration.h" + +/** + * Creates the WER integration. + */ +sentry_integration_t *sentry_integration_wer_new(void); + +#endif diff --git a/src/sentry_options.c b/src/sentry_options.c index f848a0487..7ea042851 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -17,6 +17,10 @@ # include "integrations/sentry_integration_qt.h" #endif +#ifdef SENTRY_INTEGRATION_WER +# include "integrations/sentry_integration_wer.h" +#endif + static double normalize_sample_rate(double sample_rate, double default_value) { @@ -126,6 +130,9 @@ sentry_options_new(void) #ifdef SENTRY_INTEGRATION_QT sentry__options_add_integration(opts, sentry_integration_qt_new()); #endif +#ifdef SENTRY_INTEGRATION_WER + sentry__options_add_integration(opts, sentry_integration_wer_new()); +#endif return opts; } diff --git a/tests/test_integration_wer.py b/tests/test_integration_wer.py new file mode 100644 index 000000000..3e5efc802 --- /dev/null +++ b/tests/test_integration_wer.py @@ -0,0 +1,187 @@ +import ctypes +from ctypes import wintypes +import os +import subprocess +import sys + +from pathlib import Path + +import pytest + +from . import run +from .assertions import wait_for +from .conditions import has_breakpad, has_crashpad, has_native, is_qemu + +pytestmark = [ + pytest.mark.skipif( + sys.platform != "win32" or bool(os.environ.get("TEST_MINGW")), + reason="WER integration tests are only available in MSVC Windows builds", + ), + pytest.mark.with_wer, +] + +S_OK = 0 + +E_STORE_USER_ARCHIVE = 0 +E_STORE_USER_QUEUE = 1 +E_STORE_MACHINE_ARCHIVE = 2 +E_STORE_MACHINE_QUEUE = 3 + + +class WerStore: + def __init__(self): + self._wer = ctypes.WinDLL("wer.dll") + self._wer.WerStoreOpen.argtypes = [ + ctypes.c_int, + ctypes.POINTER(wintypes.HANDLE), + ] + self._wer.WerStoreOpen.restype = ctypes.c_long + self._wer.WerStoreClose.argtypes = [wintypes.HANDLE] + self._wer.WerStoreClose.restype = None + self._wer.WerStoreGetFirstReportKey.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(ctypes.c_wchar_p), + ] + self._wer.WerStoreGetFirstReportKey.restype = ctypes.c_long + self._wer.WerStoreGetNextReportKey.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(ctypes.c_wchar_p), + ] + self._wer.WerStoreGetNextReportKey.restype = ctypes.c_long + self._wer.WerFreeString.argtypes = [ctypes.c_wchar_p] + self._wer.WerFreeString.restype = None + + def report_dirs(self): + for store_type in ( + E_STORE_USER_ARCHIVE, + E_STORE_USER_QUEUE, + E_STORE_MACHINE_ARCHIVE, + E_STORE_MACHINE_QUEUE, + ): + handle = wintypes.HANDLE() + hr = self._wer.WerStoreOpen(store_type, ctypes.byref(handle)) + if hr != S_OK: + continue + + try: + key = ctypes.c_wchar_p() + hr = self._wer.WerStoreGetFirstReportKey(handle, ctypes.byref(key)) + while hr == S_OK and key.value: + report_dir = key.value + self._wer.WerFreeString(key) + yield Path(report_dir) + + key = ctypes.c_wchar_p() + hr = self._wer.WerStoreGetNextReportKey(handle, ctypes.byref(key)) + finally: + self._wer.WerStoreClose(handle) + + +def wait_for_wer_report(store, test_id): + seen_report_dirs = 0 + readable_reports = 0 + matching_report = None + + def find_report(): + nonlocal matching_report + nonlocal seen_report_dirs + nonlocal readable_reports + + for report_dir in store.report_dirs(): + seen_report_dirs += 1 + report_path = report_dir / "Report.wer" + try: + report = report_path.read_text(encoding="utf-16-le") + except OSError: + continue + + readable_reports += 1 + report_lower = report.lower() + if "test.id" in report_lower and test_id in report: + matching_report = (report_path, report) + return True + + return False + + if wait_for(find_report): + return matching_report + + details = [ + f"searched {seen_report_dirs} WER report directories", + f"read {readable_reports} Report.wer files", + ] + pytest.fail( + f"WER report with test.id={test_id} was not found ({', '.join(details)})" + ) + + +@pytest.mark.parametrize( + "backend", + [ + "none", + "inproc", + pytest.param( + "breakpad", + marks=[ + pytest.mark.skipif( + not has_breakpad or is_qemu, reason="breakpad backend not available" + ), + pytest.mark.xfail( + reason="breakpad swallows the exception", + strict=True, + ), + ], + ), + pytest.param( + "crashpad", + marks=[ + pytest.mark.skipif( + not has_crashpad, reason="crashpad backend not available" + ), + pytest.mark.xfail( + reason="crashpad handler terminates the process", + strict=True, + ), + ], + ), + pytest.param( + "native", + marks=pytest.mark.skipif( + not has_native or is_qemu, reason="native backend not available" + ), + id="native", + ), + ], +) +def test_wer_custom_metadata(cmake, backend): + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": backend, + "SENTRY_TRANSPORT": "none", + "SENTRY_INTEGRATION_WER": "ON", + }, + ) + + completed = run( + tmp_path, + "sentry_example", + ["e2e-test", "crash"], + expect_failure=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + test_id = None + for line in completed.stdout.splitlines(): + if line.startswith("TEST_ID:"): + test_id = line.removeprefix("TEST_ID:") + break + assert test_id, completed.stdout + + report_path, report = wait_for_wer_report(WerStore(), test_id) + assert report_path.name == "Report.wer" + assert "expected-tag" in report + assert "some value" in report + assert "not-expected-tag" not in report