Skip to content

feat(windows): add WINE and Proton runtime context - #1995

Open
GtechGovind wants to merge 8 commits into
getsentry:masterfrom
GtechGovind:agent/add-wine-os-context
Open

feat(windows): add WINE and Proton runtime context#1995
GtechGovind wants to merge 8 commits into
getsentry:masterfrom
GtechGovind:agent/add-wine-os-context

Conversation

@GtechGovind

@GtechGovind GtechGovind commented Aug 16, 2026

Copy link
Copy Markdown

Closes #1004.

Summary

  • detect WINE through the exports exposed by ntdll.dll
  • detect the precise Proton compatibility tool from Steam config_info and version metadata
  • report Wine, Proton, Proton Experimental, Proton Hotfix, GE-Proton, and custom variants through a typed runtime context
  • avoid adding the context on native Windows or when the required WINE version export is unavailable
  • add unit coverage for runtime classification and missing metadata

Validation

  • make test-unit
  • clang-format verification via scripts/check-clang-format.py

Notes

The Proton lookup follows the reference implementation in getsentry/sentry-godot#591 while keeping this PR focused on compatibility runtime detection. Broader SteamOS, device, and host OS enrichment can be added separately.

@GtechGovind
GtechGovind marked this pull request as ready for review August 16, 2026 12:47
@jpnurmi

jpnurmi commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Thank you for the contribution, @GtechGovind!

While this implements the old minimal Wine metadata proposed in #1004, there's now a more recent and comprehensive reference implementation in sentry-godot:

@limbonaut What do you think? Should we rather port the Godot implementation?

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 14.70588% with 116 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.29%. Comparing base (69639a0) to head (1a30d04).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1995      +/-   ##
==========================================
- Coverage   74.69%   74.29%   -0.41%     
==========================================
  Files         104      104              
  Lines       26182    26318     +136     
  Branches     4740     4780      +40     
==========================================
- Hits        19557    19552       -5     
- Misses       5295     5431     +136     
- Partials     1330     1335       +5     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@limbonaut

limbonaut commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

The problem with the bare Wine context from ntdll is that it's misleading with the Proton distributions. The proton builds report wine-10.0. And while Proton is based on Wine, this misleads on which actual Wine/Proton is being used.

I see no reason why we shouldn't move Godot's implementation into native, maybe even device/OS detection too. Currently, we maintain parallel implementations in Godot and Unreal. Unity would probably need a separate treatment though, since it's based primarily on the .NET SDK. I think it would be best if we also add this in the .NET SDK.

What we do in Godot:

  • Fill runtime context with the proton/wine info.
  • Detect SteamOS, Bazzite and Linux distros and fill the os context.
  • Detect SteamDeck skews and fill the device context.
  • Detect Steam environment and add steam: true tag.

What else can we do (that I didn't get to):

  • Detect native Steam containers (scout, sniper, etc), fill runtime and report the actual host OS.

One contention point: we only have one runtime context, and both Wine and .NET compete for it (correction: specs indicate that we may add several runtime contexts). We can name it "compatibility" or "wine" context with the type "runtime".

@GtechGovind GtechGovind changed the title feat(windows): add WINE runtime context feat(windows): add WINE and Proton runtime context Aug 18, 2026
Comment thread src/sentry_os.c
Comment thread src/sentry_os.c
@jpnurmi

jpnurmi commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Hi @GtechGovind, I'm fixing up the basics for Wine and setting up a CI test pipeline at #2001. Once we have it merged, we can update this PR to test the Wine context in an actual Wine environment. I'd prefer it over the current unit test that not only forces us to expose internal details in the module API, but also doesn't guarantee that it actually works on Wine. :)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit be92308. Configure here.

Comment thread src/sentry_os.c Outdated

@jpnurmi jpnurmi left a comment

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.

#2001 has been merged. There's now a dedicated Wine CI job. Let's replace the unit test with an integration test that runs in an actual Wine environment. Here's a starting point:

diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py
index 54485adc..83cc0da8 100644
--- a/tests/test_integration_stdout.py
+++ b/tests/test_integration_stdout.py
@@ -389,3 +389,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-"),
+    }

Comment thread src/sentry_os.h Outdated
Comment thread src/sentry_os.c Outdated
*is_proton = false;
char *compat_path
= sentry__string_from_wstr(_wgetenv(L"STEAM_COMPAT_DATA_PATH"));
if (!compat_path || !compat_path[0]) {

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
if (!compat_path || !compat_path[0]) {
if (sentry__string_empty(compat_path)) {

@jpnurmi jpnurmi left a comment

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.

Thank you so much! It's starting to look good, I think. I left a few more minor findings in comments.

P.S. Feel free to let me know if you're tired of iterating the PR, and we can help with the last remaining touches. Thank you bearing with me through the constant rebases and nitpicks. :)

Comment thread src/sentry_os.c
Comment on lines +373 to +374
bool is_proton = false;
char *proton_version = get_proton_version(&is_proton);

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();
    }

Comment thread src/sentry_os.c Outdated
sentry__stringbuilder_cleanup(&sb);
return NULL;
}
return sentry__stringbuilder_into_string(&sb);

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.

all three callers convert the result to sentry_path_t. what about returning sentry__path_from_str_owned and changing read_wine_file to accept const sentry_path_t *?

@jpnurmi
jpnurmi requested a review from limbonaut August 21, 2026 09:03

@limbonaut limbonaut left a comment

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.

Thanks for adapting it! I've checked against different Proton distributions, and it works well:

Wine: {"type": "runtime", "name": "Wine", "version": "11.15"}
Proton 10.0: {"type": "runtime", "name": "Proton", "version": "10.0-4b"}
Proton Experimental: {"type": "runtime", "name": "Proton Experimental", "version": "11.0-20260805"}
Proton Hotfix: {"type": "runtime", "name": "Proton Hotfix", "version": "20260730"}
GE-Proton10-32: {"type": "runtime", "name": "GE-Proton", "version": "10-32"}
proton_tkg: {"type": "runtime", "name": "Proton Custom", "version": "TKG-proton-experimental.bleeding.edge.10.0.326241.20260312"}

Just needs a little bit more work before we can merge it.

Comment thread src/sentry_os.c Outdated
const char *runtime_name = "Wine";
const char *runtime_version = wine_get_version();
if (!runtime_version || !runtime_version[0]) {
return sentry_value_new_null();

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.

Let's move this check after proton version is checked, instead of bailing out here. If this returns emtpy/null before the proton version is ever consulted, the whole context will be dropped.

Comment thread src/sentry_os.c
return NULL;
}

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.

Comment thread src/sentry_os.c
Comment on lines +373 to +374
bool is_proton = false;
char *proton_version = get_proton_version(&is_proton);

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();
    }

Comment thread src/sentry_os.c Outdated

const char *runtime_name = "Wine";
const char *runtime_version = wine_get_version();
if (!runtime_version || !runtime_version[0]) {

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
if (!runtime_version || !runtime_version[0]) {
if (sentry__string_empty(runtime_version)) {

Comment thread src/sentry_os.c Outdated
return sentry_value_new_null();
}

if (proton_version && proton_version[0]) {

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
if (proton_version && proton_version[0]) {
if (!sentry__string_empty(proton_version)) {

Comment thread src/sentry_os.c Outdated
context, "type", sentry_value_new_string("runtime"));
sentry_value_set_by_key(
context, "name", sentry_value_new_string(runtime_name));
if (runtime_version[0]) {

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
if (runtime_version[0]) {
if (!sentry__string_empty(runtime_version)) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add WINE meta-data to OS context.

3 participants