#!/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)
