#!/usr/bin/python3 """Start AT-SPI on this nested desktop's live bus, after KWin has set its displays.""" import ast import os import signal import subprocess import sys import time DBUS = "org.freedesktop.DBus" DBUS_PATH = "/org/freedesktop/DBus" REGISTRY = "org.a11y.atspi.Registry" def call(address, destination, path, method, *args): return subprocess.run( ["gdbus", "call", "--address", address, "--dest", destination, "--object-path", path, "--method", method, "--timeout", "5", *args], check=True, capture_output=True, text=True, timeout=6).stdout.strip() def stop(child): if child.poll() is None: child.terminate() try: child.wait(timeout=3) except subprocess.TimeoutExpired: child.kill() child.wait() def run_registry(address, session): # SteamOS's registryd stays alive after a bus disconnect. Keep only our own # child tied to both private buses; no host PID lookup or broad process kill. registry = subprocess.Popen(["/usr/lib/at-spi2-registryd"]) try: while registry.poll() is None: try: call(session, DBUS, DBUS_PATH, DBUS + ".GetId") call(address, DBUS, DBUS_PATH, DBUS + ".GetId") except subprocess.SubprocessError: break # Normal session shutdown or loss of its accessibility bus. # Each check starts two gdbus processes, and SteamVR needs the CPU. time.sleep(5) finally: stop(registry) def start(): session = os.environ.get("DBUS_SESSION_BUS_ADDRESS") runtime = os.environ.get("XDG_RUNTIME_DIR", "") if not session or os.path.basename(runtime) != "frametop": return # Do not autolaunch a bus or touch the host desktop. os.environ.pop("AT_SPI_BUS_ADDRESS", None) reply = call(session, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus.GetAddress") address, = ast.literal_eval(reply) if not isinstance(address, str) or not address.startswith("unix:"): raise ValueError("org.a11y.Bus returned no local accessibility bus") if call(address, DBUS, DBUS_PATH, DBUS + ".NameHasOwner", REGISTRY) == "(true,)": return # Activation otherwise inherits the accessibility daemon's old environment (and # DBUS_SESSION_BUS_ADDRESS is the accessibility bus itself). Pin the live bus. env = {key: os.environ.get(key, "") for key in ( "DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "XDG_CONFIG_HOME", "DBUS_SESSION_BUS_ADDRESS")} env["AT_SPI_BUS_ADDRESS"] = address call(address, DBUS, DBUS_PATH, DBUS + ".UpdateActivationEnvironment", repr(env)) try: call(address, DBUS, DBUS_PATH, DBUS + ".StartServiceByName", REGISTRY, "0") except subprocess.CalledProcessError: # SteamOS's launcher can select dbus-broker because we are in a user unit, # but the private session bus has no systemd activation manager. Reuse its # bus rather than starting another launcher/bus. Never replace an owner. if call(address, DBUS, DBUS_PATH, DBUS + ".NameHasOwner", REGISTRY) == "(true,)": return os.environ["AT_SPI_BUS_ADDRESS"] = address # registryd itself refuses to queue behind an existing registry, including # races with native activation. The watcher cleans up only our own child. run_registry(address, session) if __name__ == "__main__": signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit(0)) try: start() except (OSError, ValueError, SyntaxError, subprocess.SubprocessError) as error: print(f"frametop: accessibility unavailable: {error}", file=sys.stderr)