diff --git a/CHANGELOG.md b/CHANGELOG.md index 98eacde70..5deb5da30 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 e1bb18cb9..b67a3d0cc 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) @@ -178,6 +179,204 @@ 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; +} + +sentry_value_t +sentry__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 = sentry__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..ed701dfb9 100644 --- a/src/sentry_os.h +++ b/src/sentry_os.h @@ -52,6 +52,16 @@ 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) +typedef const char *(CDECL *sentry__wine_get_version_t)(void); + +sentry_value_t sentry__make_wine_context( + sentry__wine_get_version_t wine_get_version, const char *proton_version, + bool is_proton); +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/unit/test_os.c b/tests/unit/test_os.c index 5a4d3337c..4f260a418 100644 --- a/tests/unit/test_os.c +++ b/tests/unit/test_os.c @@ -200,6 +200,21 @@ extern void(WINAPI *g_kernel32_GetCurrentThreadStackLimits)( PULONG_PTR, PULONG_PTR); static size_t g_kernel32_SetThreadStackGuaranteeCalled = 0; +# if !defined(SENTRY_PLATFORM_XBOX) +static const char *CDECL +wine_get_version(void) +{ + return "9.0"; +} + +static const char *CDECL +wine_get_empty_version(void) +{ + return ""; +} + +# endif + static BOOL WINAPI no_previous_guarantee(PULONG guarantee) { @@ -295,6 +310,105 @@ stack_reserve_exact_factor_minus_one(PULONG_PTR low, PULONG_PTR high) } #endif +SENTRY_TEST(wine_context) +{ +#if !defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_XBOX) + SKIP_TEST(); +#else + sentry_value_t context + = sentry__make_wine_context(wine_get_version, NULL, false); + TEST_CHECK(!sentry_value_is_null(context)); + TEST_CHECK(sentry_value_is_frozen(context)); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "type")), + "runtime"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "name")), + "Wine"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "version")), + "9.0"); + sentry_value_decref(context); + + context + = sentry__make_wine_context(wine_get_version, "proton-10.0-4", true); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "name")), + "Proton"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "version")), + "10.0-4"); + sentry_value_decref(context); + + context = sentry__make_wine_context( + wine_get_version, "experimental-10.0-20260113", true); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "name")), + "Proton Experimental"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "version")), + "10.0-20260113"); + sentry_value_decref(context); + + context + = sentry__make_wine_context(wine_get_version, "GE-Proton9-27", true); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "name")), + "GE-Proton"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "version")), + "9-27"); + sentry_value_decref(context); + + context + = sentry__make_wine_context(wine_get_version, "GE-Proton", true); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "name")), + "GE-Proton"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(context, "version"))); + sentry_value_decref(context); + + context + = sentry__make_wine_context(wine_get_version, "hotfix-20251031", true); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "name")), + "Proton Hotfix"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "version")), + "20251031"); + sentry_value_decref(context); + + context + = sentry__make_wine_context(wine_get_version, "custom-tool-1", true); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "name")), + "Proton Custom"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "version")), + "custom-tool-1"); + sentry_value_decref(context); + + context + = sentry__make_wine_context(wine_get_version, "wine-ge-8-26", false); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "name")), + "Wine"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(context, "version")), + "wine-ge-8-26"); + sentry_value_decref(context); + + context = sentry__make_wine_context(NULL, NULL, false); + TEST_CHECK(sentry_value_is_null(context)); + sentry_value_decref(context); + + context = sentry__make_wine_context(wine_get_empty_version, NULL, false); + TEST_CHECK(sentry_value_is_null(context)); + sentry_value_decref(context); +#endif +} + SENTRY_TEST(stack_guarantee) { #if !defined(SENTRY_PLATFORM_WINDOWS) diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index f3af596d0..dd27dc136 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -524,6 +524,7 @@ XX(value_uint64) XX(value_unicode) XX(value_user) XX(value_wrong_type) +XX(wine_context) XX(write_envelope_partial_write_fails) XX(write_raw_envelope_to_file) XX(writer_byte_count_stops_after_failure)