From 9cf98e9d72be74a1dd4f2890b74e908a35e005c1 Mon Sep 17 00:00:00 2001 From: Chloe Pelling Date: Wed, 11 Oct 2023 08:42:54 +1100 Subject: [PATCH] vm_tools: sommelier: Parse steam_game_id from WM_CLASS as a fallback. Some games have the STEAM_GAME property set on their launcher window, but not on their game window. But in at least some cases, the WM_CLASS *is* set on the game window with the format "steam_app_\0steam_app_". We can use this to extract the steam_game_id in cases where STEAM_GAME is missing. BUG=b:281593467 TEST=run unit tests: meson setup builddir -Dquirks=true; pushd builddir; ninja && ./sommelier_test; popd Change-Id: Ia59d49ce7e7a91d380c07ba2b85ab46d1e1ce404 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/4930950 Reviewed-by: Lucy Qu Commit-Queue: Lucy Qu Auto-Submit: Chloe Pelling Tested-by: Chloe Pelling --- sommelier-x11event-test.cc | 82 ++++++++++++++++++++++++++++++++++++++ sommelier.cc | 19 ++++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/sommelier-x11event-test.cc b/sommelier-x11event-test.cc index f03ef5c..3e5a528 100644 --- a/sommelier-x11event-test.cc +++ b/sommelier-x11event-test.cc @@ -149,5 +149,87 @@ TEST_F(X11EventTest, PropertyNotifyStoresSteamId) { EXPECT_EQ(window->steam_game_id, steam_game_id); } +TEST_F(X11EventTest, MapRequestParsesSteamIdFromClass) { + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + const char clazz[] = "steam_app_7890\0steam_app_7890"; + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 8, sizeof(clazz), + clazz); + + xcb_map_request_event_t event; + event.response_type = XCB_MAP_REQUEST; + event.window = window->id; + sl_handle_map_request(&ctx, &event); + + EXPECT_EQ(window->steam_game_id, 7890); +} + +TEST_F(X11EventTest, MapRequestPrefersSteamGameIdOverClass) { + uint32_t steam_game_id = 123456; + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + const char clazz[] = "steam_app_7890\0steam_app_7890"; + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 8, sizeof(clazz), + clazz); + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + ctx.atoms[ATOM_STEAM_GAME].value, XCB_ATOM_CARDINAL, 32, + 1, &steam_game_id); + + // Act: map the window + xcb_map_request_event_t event; + event.response_type = XCB_MAP_REQUEST; + event.window = window->id; + sl_handle_map_request(&ctx, &event); + + // Assert: Uses the ID from the STEAM_GAME property, not from WM_CLASS + EXPECT_EQ(window->steam_game_id, steam_game_id); +} + +TEST_F(X11EventTest, PropertyNotifyParsesSteamIdFromClassAsFallback) { + uint32_t steam_game_id = 123456; + xcb.DelegateToFake(); + sl_window* window = CreateWindowWithoutRole(); + xcb.create_window(nullptr, 32, window->id, XCB_WINDOW_NONE, 0, 0, 800, 600, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, + nullptr); + + // Act: set STEAM_GAME to one ID, and WM_CLASS to specify another + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + ctx.atoms[ATOM_STEAM_GAME].value, XCB_ATOM_CARDINAL, 32, + 1, &steam_game_id); + { + xcb_property_notify_event_t event; + event.response_type = XCB_PROPERTY_NOTIFY; + event.window = window->id; + event.atom = ctx.atoms[ATOM_STEAM_GAME].value; + event.state = XCB_PROPERTY_NEW_VALUE; + sl_handle_property_notify(&ctx, &event); + } + + const char clazz[] = "steam_app_7890\0steam_app_7890"; + xcb.change_property(nullptr, XCB_PROP_MODE_REPLACE, window->id, + XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 8, sizeof(clazz), + clazz); + { + xcb_property_notify_event_t event; + event.response_type = XCB_PROPERTY_NOTIFY; + event.window = window->id; + event.atom = XCB_ATOM_WM_CLASS; + event.state = XCB_PROPERTY_NEW_VALUE; + sl_handle_property_notify(&ctx, &event); + } + + // Assert: STEAM_GAME is preferred, even though WM_CLASS was changed last. + EXPECT_EQ(window->steam_game_id, steam_game_id); +} + } // namespace sommelier } // namespace vm_tools diff --git a/sommelier.cc b/sommelier.cc index 46296b5..ebe31fe 100644 --- a/sommelier.cc +++ b/sommelier.cc @@ -12,6 +12,7 @@ #include "xcb/xcb-shim.h" #include +#include #include #include #include @@ -97,6 +98,8 @@ struct sl_data_source { #define MIN_AURA_SHELL_VERSION 6 #define MAX_AURA_SHELL_VERSION 38 +static const char STEAM_APP_CLASS_PREFIX[] = "steam_app_"; + int sl_open_wayland_socket(const char* socket_name, struct sockaddr_un* addr, int* lock_fd, @@ -1125,12 +1128,24 @@ static const char* sl_decode_wm_class(struct sl_window* window, // WM_CLASS property contains two consecutive null-terminated strings. // These specify the Instance and Class names. If a global app ID is // not set then use Class name for app ID. - const char* value = static_cast(xcb_get_property_value(reply)); - int value_length = xcb_get_property_value_length(reply); + const char* value = static_cast(xcb()->get_property_value(reply)); + int value_length = xcb()->get_property_value_length(reply); int instance_length = strnlen(value, value_length); if (value_length > instance_length) { window->clazz = strndup(value + instance_length + 1, value_length - instance_length - 1); + + if (!window->steam_game_id) { + // If there's no known Steam Game ID for this window, + // attempt to parse one from the class name. + if (strncmp(window->clazz, STEAM_APP_CLASS_PREFIX, + strlen(STEAM_APP_CLASS_PREFIX)) == 0) { + // atoi() returns 0 on error, in which case steam_game_id + // simply remains effectively unset. + window->steam_game_id = + atoi(window->clazz + strlen(STEAM_APP_CLASS_PREFIX)); + } + } return window->clazz; } return nullptr;