linux observer
at main 92 lines 3.0 kB view raw
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Desktop session environment checks and recovery. 5 6Extracted from solstone's observe/linux/observer.py (lines 598-666). 7 8_recover_session_env() is kept as fallback for manual CLI launch. 9For systemd service launch, PassEnvironment= in the unit file is 10the primary mechanism. 11""" 12 13import logging 14import os 15import shutil 16import subprocess 17 18logger = logging.getLogger(__name__) 19 20# Exit codes 21EXIT_TEMPFAIL = 75 # EX_TEMPFAIL: session not ready, retry later 22 23 24def _recover_session_env() -> None: 25 """Try to recover desktop session env vars from the systemd user manager. 26 27 On GNOME Wayland, gnome-shell pushes DISPLAY, WAYLAND_DISPLAY, and 28 DBUS_SESSION_BUS_ADDRESS into the systemd user environment on startup. 29 When the observer is launched from a non-desktop shell, these vars may be missing 30 from the inherited environment — but systemctl --user show-environment 31 has them. 32 """ 33 needed = {"DISPLAY", "WAYLAND_DISPLAY", "DBUS_SESSION_BUS_ADDRESS"} 34 missing = {v for v in needed if not os.environ.get(v)} 35 if not missing: 36 return 37 38 # Ensure XDG_RUNTIME_DIR is set (required for systemctl --user to connect) 39 if not os.environ.get("XDG_RUNTIME_DIR"): 40 os.environ["XDG_RUNTIME_DIR"] = f"/run/user/{os.getuid()}" 41 42 try: 43 result = subprocess.run( 44 ["systemctl", "--user", "show-environment"], 45 capture_output=True, 46 text=True, 47 timeout=5, 48 ) 49 if result.returncode != 0: 50 return 51 except (FileNotFoundError, subprocess.TimeoutExpired): 52 return 53 54 recovered = [] 55 for line in result.stdout.splitlines(): 56 key, _, value = line.partition("=") 57 if key in missing and value: 58 os.environ[key] = value 59 recovered.append(f"{key}={value}") 60 61 if recovered: 62 logger.info("Recovered session env from systemd: %s", ", ".join(recovered)) 63 64 65def check_session_ready() -> str | None: 66 """Check if the desktop session is ready for observation. 67 68 Returns None if ready, or a description of what's missing. 69 """ 70 # Try to recover missing session vars from systemd user manager 71 _recover_session_env() 72 73 # Display server 74 if not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): 75 return "no display server (DISPLAY/WAYLAND_DISPLAY not set)" 76 77 # DBus session bus 78 if not os.environ.get("DBUS_SESSION_BUS_ADDRESS"): 79 return "no DBus session bus (DBUS_SESSION_BUS_ADDRESS not set)" 80 81 # PulseAudio / PipeWire audio 82 pactl = shutil.which("pactl") 83 if pactl: 84 try: 85 subprocess.run( 86 [pactl, "info"], 87 capture_output=True, 88 timeout=5, 89 ).check_returncode() 90 except (subprocess.CalledProcessError, subprocess.TimeoutExpired): 91 return "audio server not responding (pactl info failed)" 92 return None