diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eaef44af..d6be963de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Features**: +- Windows: report WINE and Proton metadata in a separate runtime context. ([#1995](https://github.com/getsentry/sentry-native/pull/1995)) - Add a public custom HTTP transport client interface (`sentry_http_transport_new`) so applications can plug in their own HTTP client (e.g. platform-native ones) while sentry-native continues to own request queueing, retry/backoff, offline caching, rate-limiting, and client reports. The built-in curl and WinHTTP transports are now implemented against this same interface. ([#1987](https://github.com/getsentry/sentry-native/pull/1987)) - Native/Windows: capture WER report ID and expose as `contexts.wer.report_id` in crash events when the WER integration is enabled. ([#1970](https://github.com/getsentry/sentry-native/pull/1970)) - Add `sentry_set_tags` and `sentry_scope_set_tags` for updating multiple tags with a single scope flush, improving bulk-update performance. ([#1993](https://github.com/getsentry/sentry-native/pull/1993)) diff --git a/src/sentry_os.c b/src/sentry_os.c index cd8ed2553..9bff5de75 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -1,4 +1,5 @@ #include "sentry_os.h" +#include "sentry_path.h" #include "sentry_slice.h" #include "sentry_string.h" #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_WINDOWS) @@ -177,6 +178,206 @@ sentry__get_windows_version(windows_version_t *win_ver) return 1; } +static bool +string_ends_with(const char *value, size_t value_len, const char *suffix) +{ + const size_t suffix_len = strlen(suffix); + return value_len >= suffix_len + && memcmp(value + value_len - suffix_len, suffix, suffix_len) == 0; +} + +static char * +make_wine_path(const char *path, const char *suffix) +{ + sentry_stringbuilder_t sb; + sentry__stringbuilder_init(&sb); + if (sentry__stringbuilder_append(&sb, "Z:") + || sentry__stringbuilder_append(&sb, path) + || sentry__stringbuilder_append(&sb, suffix)) { + sentry__stringbuilder_cleanup(&sb); + return NULL; + } + return sentry__stringbuilder_into_string(&sb); +} + +static char * +read_wine_file(const char *path) +{ + sentry_path_t *file_path = sentry__path_from_str(path); + char *contents + = file_path ? sentry__path_read_to_buffer(file_path, NULL) : NULL; + sentry__path_free(file_path); + return contents; +} + +static char * +get_proton_version(bool *is_proton) +{ + *is_proton = false; + char *compat_path + = sentry__string_from_wstr(_wgetenv(L"STEAM_COMPAT_DATA_PATH")); + if (!compat_path || !compat_path[0]) { + sentry_free(compat_path); + return NULL; + } + + char *config_path = make_wine_path(compat_path, "/config_info"); + sentry_free(compat_path); + char *config = config_path ? read_wine_file(config_path) : NULL; + sentry_free(config_path); + if (!config) { + return NULL; + } + + char *fonts_path = strchr(config, '\n'); + if (!fonts_path) { + sentry_free(config); + return NULL; + } + fonts_path++; + while (*fonts_path == ' ' || *fonts_path == '\t') { + fonts_path++; + } + size_t fonts_path_len = strcspn(fonts_path, "\r\n"); + while (fonts_path_len > 0 + && (fonts_path[fonts_path_len - 1] == ' ' + || fonts_path[fonts_path_len - 1] == '\t')) { + fonts_path_len--; + } + + const char *fonts_suffix = NULL; + if (string_ends_with(fonts_path, fonts_path_len, "/files/share/fonts/")) { + fonts_suffix = "/files/share/fonts/"; + } else if (string_ends_with( + fonts_path, fonts_path_len, "/dist/share/fonts/")) { + fonts_suffix = "/dist/share/fonts/"; + } + if (!fonts_suffix) { + sentry_free(config); + return NULL; + } + + const size_t root_len = fonts_path_len - strlen(fonts_suffix); + char *proton_root = sentry__string_clone_n(fonts_path, root_len); + sentry_free(config); + if (!proton_root) { + return NULL; + } + + char *proton_path = make_wine_path(proton_root, "/proton"); + sentry_path_t *proton_file = sentry__path_from_str(proton_path); + *is_proton = proton_file && sentry__path_is_file(proton_file); + sentry__path_free(proton_file); + sentry_free(proton_path); + + char *version_path = make_wine_path(proton_root, "/version"); + sentry_free(proton_root); + char *version_file = version_path ? read_wine_file(version_path) : NULL; + sentry_free(version_path); + if (!version_file) { + return NULL; + } + + char *version = strpbrk(version_file, " \t"); + if (!version) { + sentry_free(version_file); + return NULL; + } + while (*version == ' ' || *version == '\t') { + version++; + } + size_t version_len = strcspn(version, "\r\n"); + while (version_len > 0 + && (version[version_len - 1] == ' ' + || version[version_len - 1] == '\t')) { + version_len--; + } + + char *result + = version_len ? sentry__string_clone_n(version, version_len) : NULL; + sentry_free(version_file); + return result; +} + +static bool +string_starts_with(const char *value, const char *prefix) +{ + const size_t value_len = strlen(value); + const size_t prefix_len = strlen(prefix); + return value_len >= prefix_len && memcmp(value, prefix, prefix_len) == 0; +} + +typedef const char *(CDECL *sentry__wine_get_version_t)(void); + +static sentry_value_t +make_wine_context(sentry__wine_get_version_t wine_get_version, + const char *proton_version, bool is_proton) +{ + if (!wine_get_version) { + return sentry_value_new_null(); + } + + const char *runtime_name = "Wine"; + const char *runtime_version = wine_get_version(); + if (!runtime_version || !runtime_version[0]) { + return sentry_value_new_null(); + } + + if (proton_version && proton_version[0]) { + runtime_version = proton_version; + if (is_proton) { + if (string_starts_with(proton_version, "proton-")) { + runtime_name = "Proton"; + runtime_version += strlen("proton-"); + } else if (string_starts_with(proton_version, "experimental-")) { + runtime_name = "Proton Experimental"; + runtime_version += strlen("experimental-"); + } else if (string_starts_with(proton_version, "GE-Proton")) { + runtime_name = "GE-Proton"; + runtime_version += strlen("GE-Proton"); + } else if (string_starts_with(proton_version, "hotfix-")) { + runtime_name = "Proton Hotfix"; + runtime_version += strlen("hotfix-"); + } else { + runtime_name = "Proton Custom"; + } + } + } + + sentry_value_t context = sentry_value_new_object(); + if (sentry_value_is_null(context)) { + return context; + } + sentry_value_set_by_key( + context, "type", sentry_value_new_string("runtime")); + sentry_value_set_by_key( + context, "name", sentry_value_new_string(runtime_name)); + if (runtime_version[0]) { + sentry_value_set_by_key( + context, "version", sentry_value_new_string(runtime_version)); + } + sentry_value_freeze(context); + return context; +} + +sentry_value_t +sentry__get_wine_context(void) +{ + const HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (!ntdll) { + return sentry_value_new_null(); + } + + const sentry__wine_get_version_t wine_get_version + = (sentry__wine_get_version_t)GetProcAddress(ntdll, "wine_get_version"); + bool is_proton = false; + char *proton_version = get_proton_version(&is_proton); + sentry_value_t context + = make_wine_context(wine_get_version, proton_version, is_proton); + sentry_free(proton_version); + return context; +} + # endif // !defined(SENTRY_PLATFORM_XBOX) sentry_value_t diff --git a/src/sentry_os.h b/src/sentry_os.h index 41d1b6b14..8686abeef 100644 --- a/src/sentry_os.h +++ b/src/sentry_os.h @@ -52,6 +52,11 @@ typedef struct { int sentry__get_kernel_version(windows_version_t *win_ver); int sentry__get_windows_version(windows_version_t *win_ver); + +# if !defined(SENTRY_PLATFORM_XBOX) +sentry_value_t sentry__get_wine_context(void); +# endif + void sentry__set_default_thread_stack_guarantee(void); void sentry__init_cached_kernel32_functions(void); void sentry__get_system_time(LPFILETIME filetime); diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 0bb7057e1..35a0cc654 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -109,6 +109,14 @@ get_scope(void) init_scope(&g_scope); g_scope.user = sentry_value_new_object(); sentry_value_set_by_key(g_scope.contexts, "os", sentry__get_os_context()); +#if defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX) + sentry_value_t wine_context = sentry__get_wine_context(); + if (!sentry_value_is_null(wine_context)) { + sentry_value_set_by_key(g_scope.contexts, "wine", wine_context); + } else { + sentry_value_decref(wine_context); + } +#endif g_scope.client_sdk = get_client_sdk(); g_scope_initialized = true; diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index 54ea52faf..ebb605b50 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -386,3 +386,32 @@ def test_breakpad_stack_overflow_stdout(cmake, stack_size): assert_attachment(envelope) assert_minidump(envelope) assert_breakpad_crash(envelope) + + +@pytest.mark.skipif(not is_wine, reason="test needs Wine") +def test_wine_context(cmake): + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + }, + ) + env = dict(os.environ) + env.pop("STEAM_COMPAT_DATA_PATH", None) + + output = check_output( + tmp_path, + "sentry_example", + ["stdout", "capture-event"], + env=env, + ) + context = Envelope.deserialize(output).get_event()["contexts"]["wine"] + + version = subprocess.check_output(["wine", "--version"], text=True).split()[0] + assert version.startswith("wine-") + assert context == { + "type": "runtime", + "name": "Wine", + "version": version.removeprefix("wine-"), + }