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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
196 changes: 196 additions & 0 deletions src/sentry_os.c
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -177,6 +178,201 @@ 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 sentry_path_t *
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__path_from_str_owned(sentry__stringbuilder_into_string(&sb));
}

static char *
read_wine_file(const sentry_path_t *path)
{
return path ? sentry__path_read_to_buffer(path, NULL) : NULL;
}

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 (sentry__string_empty(compat_path)) {
sentry_free(compat_path);
return NULL;
}

sentry_path_t *config_path = make_wine_path(compat_path, "/config_info");
sentry_free(compat_path);
char *config = config_path ? read_wine_file(config_path) : NULL;
sentry__path_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;
}

sentry_path_t *proton_path = make_wine_path(proton_root, "/proton");
*is_proton = proton_path && sentry__path_is_file(proton_path);
sentry__path_free(proton_path);

sentry_path_t *version_path = make_wine_path(proton_root, "/version");
sentry_free(proton_root);
char *version_file = version_path ? read_wine_file(version_path) : NULL;
sentry__path_free(version_path);
if (!version_file) {
return NULL;
}

// Format: "<timestamp> <git-tag>", e.g. "1769167055 proton-10.0-4".
char *version = strpbrk(version_file, " \t");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
char *version = strpbrk(version_file, " \t");
// Format: "<timestamp> <git-tag>", e.g. "1769167055 proton-10.0-4".
char *version = strpbrk(version_file, " \t");

Here, the code is hard follow and needs some context: what are we parsing and extracting. Not every reader will have an easy access to a Linux box with a proton installation. Let's add a comment.

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 (!sentry__string_empty(proton_version)) {
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");
Comment thread
sentry[bot] marked this conversation as resolved.
} else if (string_starts_with(proton_version, "hotfix-")) {
runtime_name = "Proton Hotfix";
runtime_version += strlen("hotfix-");
} else {
runtime_name = "Proton Custom";
}
}
}
if (sentry__string_empty(runtime_version)) {
return sentry_value_new_null();
}

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));
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");
if (!wine_get_version) {
return sentry_value_new_null();
}
bool is_proton = false;
char *proton_version = get_proton_version(&is_proton);
Comment on lines +368 to +369

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the Godot implementation, which calls Proton detection only after wine_get_version confirms Wine, here Proton filesystem probing happens first. So native Windows processes could perform synchronous reads through Z: even though the resulting context is discarded.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I suggest returning early if the function address didn't resolve.

    const sentry__wine_get_version_t wine_get_version
        = (sentry__wine_get_version_t)GetProcAddress(ntdll, "wine_get_version");
    if (!wine_get_version) {
        return sentry_value_new_null();
    }

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
Expand Down
5 changes: 5 additions & 0 deletions src/sentry_os.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/sentry_scope.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions tests/test_integration_stdout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-"),
}
Loading