linux observer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

activity: skip FDO ScreenSaver probe on GNOME (idle-inhibit-only endpoint)

GNOME's org.freedesktop.ScreenSaver serves only the idle-inhibit
endpoints defined in the FDO idle-inhibit spec (Inhibit, UnInhibit,
SimulateUserActivity). GetActive is not part of the spec, so calling
it on GNOME raises DBusError("This method is not part of the idle
inhibition specification") — not ServiceUnknown/NameHasNoOwner. The
observable-probes rewrite in cbf581d made this surface as a WARN
every 5 s on GNOME Wayland hosts.

Detect GNOME at call time via case-insensitive token-equality match
on XDG_CURRENT_DESKTOP (colon-split, whitespace-stripped) and skip
the FDO branch entirely on GNOME. Non-GNOME desktops are unchanged
— FDO-first, GNOME fallback. The FDO block remains reachable on
KDE/XFCE/Cinnamon/MATE/etc.

Surfacing trail: lode 7ppx4a7g. Decision to go with
GNOME-as-early-skip (option b) recorded 2026-04-22.

Co-Authored-By: OpenAI Codex <codex@openai.com>

+99 -22
+36 -22
src/solstone_linux/activity.py
··· 76 76 ) 77 77 78 78 79 + def _is_gnome_desktop() -> bool: 80 + """True if any XDG_CURRENT_DESKTOP token equals 'gnome' (case-insensitive).""" 81 + return any( 82 + token.strip().casefold() == "gnome" 83 + for token in os.environ.get("XDG_CURRENT_DESKTOP", "").split(":") 84 + ) 85 + 86 + 79 87 async def _name_has_owner(bus: MessageBus, bus_name: str) -> bool: 80 88 """Ask the bus daemon whether a well-known name is currently owned. 81 89 ··· 145 153 146 154 147 155 async def is_screen_locked(bus: MessageBus) -> bool: 148 - """Check if the screen is locked via FDO ScreenSaver, then GNOME ScreenSaver. 156 + """Check if the screen is locked. 149 157 150 - Returns True if locked, False if unlocked or all backends unavailable. 158 + On GNOME, probes only org.gnome.ScreenSaver — the FDO ScreenSaver bus 159 + on GNOME serves idle-inhibit endpoints only and does not implement 160 + GetActive. On non-GNOME desktops, tries FDO ScreenSaver first (KDE 161 + kwin and other compliant desktops), then falls back to GNOME 162 + ScreenSaver. Returns True if locked, False if unlocked or all 163 + backends unavailable. 151 164 """ 152 - # Try freedesktop.org ScreenSaver first (KDE kwin + GNOME) 153 - try: 154 - intro = await bus.introspect(FDO_SCREENSAVER_BUS, FDO_SCREENSAVER_PATH) 155 - obj = bus.get_proxy_object(FDO_SCREENSAVER_BUS, FDO_SCREENSAVER_PATH, intro) 156 - iface = obj.get_interface(FDO_SCREENSAVER_IFACE) 157 - return bool(await iface.call_get_active()) 158 - except ( 159 - DBusError, 160 - InvalidMemberNameError, 161 - InvalidIntrospectionError, 162 - OSError, 163 - ) as exc: 164 - if not _is_service_missing(exc): 165 - logger.warning( 166 - "is_screen_locked FDO backend failed: service=%s path=%s: %s: %s", 167 - FDO_SCREENSAVER_BUS, 168 - FDO_SCREENSAVER_PATH, 169 - type(exc).__name__, 170 - exc, 171 - ) 165 + if not _is_gnome_desktop(): 166 + # Try freedesktop.org ScreenSaver first (KDE kwin and other non-GNOME desktops) 167 + try: 168 + intro = await bus.introspect(FDO_SCREENSAVER_BUS, FDO_SCREENSAVER_PATH) 169 + obj = bus.get_proxy_object(FDO_SCREENSAVER_BUS, FDO_SCREENSAVER_PATH, intro) 170 + iface = obj.get_interface(FDO_SCREENSAVER_IFACE) 171 + return bool(await iface.call_get_active()) 172 + except ( 173 + DBusError, 174 + InvalidMemberNameError, 175 + InvalidIntrospectionError, 176 + OSError, 177 + ) as exc: 178 + if not _is_service_missing(exc): 179 + logger.warning( 180 + "is_screen_locked FDO backend failed: service=%s path=%s: %s: %s", 181 + FDO_SCREENSAVER_BUS, 182 + FDO_SCREENSAVER_PATH, 183 + type(exc).__name__, 184 + exc, 185 + ) 172 186 173 187 # Fall back to GNOME ScreenSaver 174 188 try:
+63
tests/test_activity.py
··· 49 49 class TestIsScreenLocked: 50 50 """Test screen lock fallback order.""" 51 51 52 + @pytest.fixture(autouse=True) 53 + def _clear_xdg_desktop(self, monkeypatch): 54 + monkeypatch.delenv("XDG_CURRENT_DESKTOP", raising=False) 55 + 52 56 @pytest.mark.asyncio 53 57 async def test_fdo_backend_returns_true_without_gnome_fallback(self): 58 + bus = MagicMock() 59 + bus.introspect = AsyncMock(return_value=object()) 60 + iface = MagicMock() 61 + iface.call_get_active = AsyncMock(return_value=True) 62 + bus.get_proxy_object.return_value = _make_proxy_with_interface(iface) 63 + 64 + result = await activity.is_screen_locked(bus) 65 + 66 + assert result is True 67 + assert bus.introspect.await_count == 1 68 + bus.introspect.assert_awaited_once_with( 69 + activity.FDO_SCREENSAVER_BUS, activity.FDO_SCREENSAVER_PATH 70 + ) 71 + 72 + @pytest.mark.asyncio 73 + async def test_xdg_current_desktop_ubuntu_gnome_skips_fdo_and_returns_gnome_state( 74 + self, monkeypatch, caplog 75 + ): 76 + monkeypatch.setenv("XDG_CURRENT_DESKTOP", "ubuntu:GNOME") 77 + bus = MagicMock() 78 + bus.introspect = AsyncMock(return_value=object()) 79 + iface = MagicMock() 80 + iface.call_get_active = AsyncMock(return_value=True) 81 + bus.get_proxy_object.return_value = _make_proxy_with_interface(iface) 82 + 83 + with caplog.at_level(logging.WARNING): 84 + result = await activity.is_screen_locked(bus) 85 + 86 + assert result is True 87 + assert bus.introspect.await_args_list == [ 88 + call(activity.GNOME_SCREENSAVER_BUS, activity.GNOME_SCREENSAVER_PATH) 89 + ] 90 + assert not any( 91 + "is_screen_locked FDO backend failed" in record.message 92 + for record in caplog.records 93 + ) 94 + 95 + @pytest.mark.asyncio 96 + async def test_xdg_current_desktop_kde_still_probes_fdo_first(self, monkeypatch): 97 + monkeypatch.setenv("XDG_CURRENT_DESKTOP", "KDE") 98 + bus = MagicMock() 99 + bus.introspect = AsyncMock(return_value=object()) 100 + iface = MagicMock() 101 + iface.call_get_active = AsyncMock(return_value=True) 102 + bus.get_proxy_object.return_value = _make_proxy_with_interface(iface) 103 + 104 + result = await activity.is_screen_locked(bus) 105 + 106 + assert result is True 107 + assert bus.introspect.await_count == 1 108 + bus.introspect.assert_awaited_once_with( 109 + activity.FDO_SCREENSAVER_BUS, activity.FDO_SCREENSAVER_PATH 110 + ) 111 + 112 + @pytest.mark.asyncio 113 + async def test_xdg_current_desktop_not_gnome_does_not_match_substring( 114 + self, monkeypatch 115 + ): 116 + monkeypatch.setenv("XDG_CURRENT_DESKTOP", "NOT-GNOME") 54 117 bus = MagicMock() 55 118 bus.introspect = AsyncMock(return_value=object()) 56 119 iface = MagicMock()