From a415de1eaaa86bec65d93b5225be8fa740e50720 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Sat, 29 Aug 2026 15:34:28 +0200 Subject: [PATCH] feat: Autoklicker auf AT-SPI umgestellt statt Bildschirmkoordinaten xdotool/Enter-Taste-Ansatz war nicht zuverlaessig genug (Metacity erzeugt mehrere ueberlappende X11-Fenster aehnlicher Groesse, 'groesstes Fenster'- Heuristik traf wiederholt das falsche; Enter aktiviert auf der Partitionierungs-Uebersicht einen Ausklapp-Pfeil statt 'Jetzt installieren' - siehe ADR-0023-Nachtrag). - live-updates/usr/share/ubiquity/tuxflotte-atspi-env.sh (neu): aktiviert GTK_MODULES=gail:atk-bridge + Session-/A11y-Bus VOR dem GTK-Start von Ubiquity - muss vor dem Init passieren, eine Bruecke wird von einer bereits laufenden GTK-App nicht nachgeladen. - initrd-hooks/casper-bottom/99casperboot: bindet obige Datei per Ein-Zeilen-Sed-Patch in start-ubiquity-dm ein (dry-run gegen den echten Datei-Inhalt verifiziert). - live-updates/opt/tuxflotte/scripts/atspi_click.py (neu): findet+klickt den passenden 'Weiter'-Button ueber Rolle+Name im Accessibility-Baum, feste deutsche Werteliste (Sprache per Preseed fest de_DE), niemals Zurueck/Abbrechen/Beenden. - autoclicker.sh: ruft atspi_click.py in Schleife statt xdotool/Enter auf, wartet auf den ubiquity-Prozess statt auf den X-Socket. Noch nicht live verifiziert - naechster Schritt: QEMU-Testlauf auf lenny. Co-Authored-By: Claude Sonnet 5 --- initrd-hooks/casper-bottom/99casperboot | 14 ++ .../opt/tuxflotte/scripts/atspi_click.py | 146 ++++++++++++++++++ .../opt/tuxflotte/scripts/autoclicker.sh | 105 ++++++------- .../usr/share/ubiquity/tuxflotte-atspi-env.sh | 54 +++++++ 4 files changed, 264 insertions(+), 55 deletions(-) create mode 100755 live-updates/opt/tuxflotte/scripts/atspi_click.py create mode 100755 live-updates/usr/share/ubiquity/tuxflotte-atspi-env.sh diff --git a/initrd-hooks/casper-bottom/99casperboot b/initrd-hooks/casper-bottom/99casperboot index a7d7fa8..002056c 100755 --- a/initrd-hooks/casper-bottom/99casperboot +++ b/initrd-hooks/casper-bottom/99casperboot @@ -54,4 +54,18 @@ if [ -f "$ubi_prepare" ]; then "$ubi_prepare" fi +# AT-SPI-Bruecke fuer den Autoklicker (atspi_click.py) muss VOR dem +# GTK-Start von Ubiquity aktiv sein - siehe live-updates/usr/share/ubiquity/ +# tuxflotte-atspi-env.sh (dorthin per casper-bottom-Kopie oben bereits nach +# /root/usr/share/ubiquity/ ausgeliefert). Einzeilige Einbindung direkt nach +# dem fruehen Existenz-Check von ubiquity-dm - alles Weitere, inkl. +# Paketinstallation, uebernimmt die eingebundene Datei selbst. +start_ubiquity_dm="/root/usr/share/ubiquity/start-ubiquity-dm" +if [ -f "$start_ubiquity_dm" ]; then + sed -i \ + '/^test -x \/usr\/bin\/ubiquity-dm || exit 0$/a\ +. /usr/share/ubiquity/tuxflotte-atspi-env.sh' \ + "$start_ubiquity_dm" +fi + touch /run/.casper-boot diff --git a/live-updates/opt/tuxflotte/scripts/atspi_click.py b/live-updates/opt/tuxflotte/scripts/atspi_click.py new file mode 100755 index 0000000..34a0108 --- /dev/null +++ b/live-updates/opt/tuxflotte/scripts/atspi_click.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Klickt den primaeren "Weiter"-Button der aktuell sichtbaren Ubiquity-Seite +per AT-SPI (Linux-Barrierefreiheits-API) - wird von autoclicker.sh wiederholt +aufgerufen (ein Prozessstart pro Klickversuch, absichtlich einfach statt +dauerhaft im Hintergrund am Accessibility-Bus zu lauschen). + +Warum AT-SPI statt Bildschirmkoordinaten: der urspruengliche xdotool-Ansatz +scheiterte daran, dass Metacity mehrere ueberlappende X11-Fenster aehnlicher +Groesse erzeugt (WM-Dekorationsrahmen, Hilfsfenster) - eine "groesstes +sichtbares Fenster"-Heuristik traf wiederholt das falsche Fenster (siehe +ADR-0023-Nachtrag). AT-SPI identifiziert den Button stattdessen ueber seine +Rolle (PUSH_BUTTON) und seinen Namen im Accessibility-Baum der Anwendung +selbst - unabhaengig von Fenstergeometrie/-dekoration. + +Sprache ist durch "debian-installer/language=de" im Kexec-Append-Preseed +fest auf de_DE gesetzt (siehe backend.sh) - die Button-Beschriftungen unten +sind daher als feste deutsche Werteliste hinterlegt, nicht lokalisierungs- +unabhaengig ermittelt. + +Exit-Codes: 0 = ein passender Button wurde gefunden und geklickt, +1 = kein passender Button gefunden (z.B. Seite gerade im Ladezustand), +2 = pyatspi/AT-SPI-Bus nicht erreichbar (Bruecke evtl. nicht aktiv). +""" +import sys + +try: + import pyatspi +except ImportError: + print("pyatspi nicht verfuegbar", file=sys.stderr) + sys.exit(2) + +# Bewusst nur vorwaerts-/abschliessende Aktionen - "Zurueck", "Abbrechen" +# und "Beenden" (Installer-Quit) duerfen hier NIE stehen, auch nicht +# versehentlich, da ein falscher Klick sonst den automatisierten Ablauf +# abbricht statt ihn voranzutreiben. +FORWARD_LABELS = { + "Weiter", + "Jetzt installieren", + "Fortfahren", + "Installieren", + "Neu starten", + "Schließen", +} + +MAX_DEPTH = 40 + + +def find_forward_button(node, depth=0): + if node is None or depth > MAX_DEPTH: + return None + try: + role = node.getRole() + except Exception: + return None + + if role == pyatspi.ROLE_PUSH_BUTTON: + name = (node.name or "").strip() + if name in FORWARD_LABELS: + try: + state = node.getState() + if state.contains(pyatspi.STATE_SENSITIVE) and state.contains( + pyatspi.STATE_SHOWING + ): + return node + except Exception: + pass + return None + + try: + count = node.childCount + except Exception: + return None + for i in range(count): + try: + child = node.getChildAtIndex(i) + except Exception: + continue + found = find_forward_button(child, depth + 1) + if found is not None: + return found + return None + + +def collect_button_names(node, depth=0, acc=None): + """Nur fuer Diagnose-Logging: alle sichtbaren Buttonnamen einsammeln, + damit ein Nicht-Treffer im Log nachvollziehbar bleibt, ohne dass man + interaktiv am laufenden System nachsehen muss.""" + if acc is None: + acc = [] + if node is None or depth > MAX_DEPTH: + return acc + try: + if node.getRole() == pyatspi.ROLE_PUSH_BUTTON: + acc.append((node.name or "").strip()) + return acc + count = node.childCount + except Exception: + return acc + for i in range(count): + try: + child = node.getChildAtIndex(i) + except Exception: + continue + collect_button_names(child, depth + 1, acc) + return acc + + +def main(): + try: + desktop = pyatspi.Registry.getDesktop(0) + except Exception as exc: + print(f"AT-SPI-Desktop nicht erreichbar: {exc}", file=sys.stderr) + return 2 + + seen_buttons = [] + for i in range(desktop.childCount): + try: + app = desktop.getChildAtIndex(i) + except Exception: + continue + if app is None: + continue + + btn = find_forward_button(app) + if btn is not None: + try: + action = btn.queryAction() + action.doAction(0) + except Exception as exc: + print(f"Klick auf '{btn.name}' fehlgeschlagen: {exc}", file=sys.stderr) + return 1 + print(f"geklickt: {btn.name}") + return 0 + + seen_buttons.extend(collect_button_names(app)) + + print( + "kein passender Button gefunden, sichtbare Buttons: " + + (", ".join(repr(n) for n in seen_buttons) if seen_buttons else "(keine)"), + file=sys.stderr, + ) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/live-updates/opt/tuxflotte/scripts/autoclicker.sh b/live-updates/opt/tuxflotte/scripts/autoclicker.sh index a728d28..a667acd 100755 --- a/live-updates/opt/tuxflotte/scripts/autoclicker.sh +++ b/live-updates/opt/tuxflotte/scripts/autoclicker.sh @@ -5,30 +5,26 @@ ### kann (endlose choose_partition-Schleife in ubi-partman.py, real ### bestaetigt, siehe ADR-0023-Nachtrag). Der GTK-Assistent fuellt jede Seite ### bereits vollstaendig aus dem Preseed vor (siehe preseed.tpl) - es fehlt -### nur der "Weiter"/"Installieren"-Klick pro Seite, den dieses Skript per -### xdotool simuliert. +### nur der "Weiter"/"Installieren"-Klick pro Seite. ### -### WICHTIG (real beim Testen entdeckt, siehe ADR-0023-Nachtrag): das -### Ubiquity-Fenster heisst NICHT "ubiquity" - der Fenstertitel ist z.B. -### "Installation (as superuser)". Eine Fenstersuche nach dem Namen "ubiquity" -### (fruehere Version dieses Skripts) findet also nie etwas und wurde hier auf -### eine Suche nach JEDEM sichtbaren, betitelten Fenster umgestellt. +### Klickt ueber AT-SPI (Barrierefreiheits-API), nicht mehr ueber +### Bildschirmkoordinaten/Enter-Taste (siehe atspi_click.py fuer die +### eigentliche Klicklogik). Der fruehere xdotool-Ansatz scheiterte real +### daran, dass Metacity mehrere ueberlappende X11-Fenster aehnlicher +### Groesse erzeugt (WM-Dekorationsrahmen, Hilfsfenster) - weder eine +### "groesstes sichtbares Fenster"-Heuristik noch die Enter-Taste (aktiviert +### auf der Partitionierungs-Uebersichtsseite einen Ausklapp-Pfeil statt des +### "Jetzt installieren"-Buttons) waren zuverlaessig genug (siehe +### ADR-0023-Nachtrag). AT-SPI identifiziert den Button stattdessen ueber +### seine Rolle+seinen Namen im Accessibility-Baum der Anwendung selbst - +### unabhaengig von Fenstergeometrie/-dekoration. ### -### NOCH NICHT GELOEST (bewusst als bekannte Einschraenkung stehen gelassen, -### siehe ADR-0023-Nachtrag): auf der Partitionierungs-Uebersichtsseite -### aktiviert "Enter" NICHT den "Jetzt installieren"-Button, sondern einen -### Ausklapp-Pfeil eines darunterliegenden Status-Panels ("Konfiguration der -### Installation wird ueberprueft ..."). Ein Mausklick per xdotool auf -### errechnete Fensterkoordinaten wurde real ausprobiert, aber sowohl die -### Fenstererkennung (mehrere ueberlappende X11-Fenster gleicher/aehnlicher -### Groesse durch Metacity-Dekoration, "groesstes Fenster"-Heuristik traf -### wiederholt das falsche) als auch die Klick-Koordinaten selbst waren nicht -### zuverlaessig genug fuer den produktiven Einsatz - deshalb hier bewusst -### NICHT eingebaut, um kein Risiko falscher Klicks (z.B. auf eine -### "Formatieren?"-Checkbox) einzugehen. Die Enter-Taste bleibt der sicherere -### Kompromiss: sie tut auf den meisten Seiten das Richtige und ist auf dieser -### einen Seite bestenfalls wirkungslos (klappt nur einen Infobereich auf/zu), -### nie destruktiv. +### Die AT-SPI-Bruecke selbst (GTK_MODULES=gail:atk-bridge, Session-/A11y- +### Bus) wird NICHT hier, sondern bereits VOR dem GTK-Start von Ubiquity in +### tuxflotte-atspi-env.sh aktiviert (eingebunden von start-ubiquity-dm, +### siehe Sed-Patch in initrd-hooks/casper-bottom/99casperboot) - eine erst +### nachtraeglich aktivierte Bruecke wird von einer bereits laufenden +### GTK-Anwendung nicht mehr nachgeladen. ### ### Wird per systemd-Service (siehe ...service im selben Ausliefer-Layer) bei ### JEDEM Boot gestartet, bricht aber sofort ab, wenn das eigene Boot-Keyword @@ -47,64 +43,63 @@ grep -qw tuxflotte-autoclick /proc/cmdline || exit 0 log "tuxflotte-autoclick erkannt, starte Autoklicker." export DISPLAY=:0 -# ubiquity-dm startet X frueh im Boot zu einem nicht exakt vorhersagbaren -# Zeitpunkt - auf den Unix-Socket warten statt auf eine feste Wartezeit zu -# vertrauen. -x_ready=0 +is_ubiquity_running() { + pgrep -f '/usr/bin/ubiquity' >/dev/null 2>&1 +} + +# ubiquity-dm startet Ubiquity frueh im Boot zu einem nicht exakt +# vorhersagbaren Zeitpunkt - auf den Prozess warten statt auf eine feste +# Wartezeit zu vertrauen. AT-SPI braucht anders als der fruehere +# xdotool-Ansatz keinen eigenen X-Socket-Check - der Zugriff laeuft ueber +# den Accessibility-Bus, nicht direkt ueber X11. +ubiquity_ready=0 for _ in $(seq 1 120); do - if [ -S /tmp/.X11-unix/X0 ]; then - x_ready=1 + if is_ubiquity_running; then + ubiquity_ready=1 break fi sleep 1 done -if [ "${x_ready}" -ne 1 ]; then - log "X-Server nach 120s nicht erschienen, breche ab." +if [ "${ubiquity_ready}" -ne 1 ]; then + log "ubiquity-Prozess nach 120s nicht erschienen, breche ab." exit 1 fi -log "X-Server erkannt." +log "ubiquity-Prozess erkannt." -if ! command -v xdotool >/dev/null 2>&1; then - log "xdotool fehlt auf dem Live-Medium, installiere nach." - if ! apt-get install -y xdotool >>"${LOG}" 2>&1; then - log "xdotool-Installation fehlgeschlagen, breche ab." +if ! python3 -c 'import pyatspi' >/dev/null 2>&1; then + log "pyatspi fehlt auf dem Live-Medium, installiere nach." + if ! apt-get install -y python3-pyatspi >>"${LOG}" 2>&1; then + log "pyatspi-Installation fehlgeschlagen, breche ab." exit 1 fi fi -# Bewusst betiteltes Fenster suchen (leerer --name-Ausdruck matcht jeden -# Titel) statt nach "ubiquity" - siehe Kommentar oben, der tatsaechliche -# Fenstertitel enthaelt dieses Wort nie. -main_window() { - xdotool search --onlyvisible --name "" 2>/dev/null | head -n1 -} - -# Ubiquity-Fenster abwarten, bevor die Klick-Schleife beginnt. -for _ in $(seq 1 60); do - [ -n "$(main_window)" ] && break - sleep 1 -done - -is_ubiquity_running() { - pgrep -f '/usr/bin/ubiquity' >/dev/null 2>&1 -} +ATSPI_CLICK=/opt/tuxflotte/scripts/atspi_click.py log "Starte Klick-Schleife." # Harte Obergrenze als Sicherheitsnetz (60 Minuten) - falls die Installation # haengen bleibt, soll dieser Dienst nicht unbegrenzt weiterlaufen. end=$((SECONDS + 3600)) clicks=0 +misses=0 while [ "${SECONDS}" -lt "${end}" ]; do if ! is_ubiquity_running; then log "ubiquity-Prozess nicht mehr aktiv (vermutlich fertig oder abgestuerzt), beende Schleife nach ${clicks} Klick(s)." break fi - win="$(main_window)" - if [ -n "${win}" ]; then - xdotool windowactivate --sync "${win}" 2>/dev/null || true - xdotool key --window "${win}" Return 2>/dev/null || true + out="$(python3 "${ATSPI_CLICK}" 2>&1)" + rc=$? + if [ "${rc}" -eq 0 ]; then clicks=$((clicks + 1)) + misses=0 + log "Klick ${clicks}: ${out}" + elif [ "${rc}" -eq 2 ]; then + log "AT-SPI-Bus nicht erreichbar, breche ab: ${out}" + break + else + misses=$((misses + 1)) + log "kein Treffer (${misses}): ${out}" fi sleep 2 done diff --git a/live-updates/usr/share/ubiquity/tuxflotte-atspi-env.sh b/live-updates/usr/share/ubiquity/tuxflotte-atspi-env.sh new file mode 100755 index 0000000..4677341 --- /dev/null +++ b/live-updates/usr/share/ubiquity/tuxflotte-atspi-env.sh @@ -0,0 +1,54 @@ +#!/bin/sh +### Wird per Ein-Zeilen-Sed-Patch (siehe initrd-hooks/casper-bottom/99casperboot) +### GANZ AM ANFANG von start-ubiquity-dm eingebunden ("."), also BEVOR +### ubiquity-dm den X-Server startet und Ubiquity selbst exec't. +### +### Zweck: die AT-SPI-Barrierefreiheits-Bruecke (atk-bridge) fuer Ubiquitys +### GTK-Prozess aktivieren, damit atspi_click.py (siehe +### /opt/tuxflotte/scripts/) Buttons ueber ihre Rolle/ihren Namen im +### Accessibility-Baum anklicken kann statt ueber Bildschirmkoordinaten. +### Der xdotool-Koordinaten-Ansatz ist an ueberlappenden X11-Fenstern durch +### die Metacity-Fensterdekoration gescheitert (mehrere Fenster aehnlicher +### Groesse, "groesstes Fenster"-Heuristik traf wiederholt das falsche - +### siehe ADR-0023-Nachtrag). +### +### WICHTIG: das muss VOR dem GTK-Init von Ubiquity passieren - eine erst +### nachtraeglich verfuegbare Bruecke wird von einer bereits laufenden +### GTK-Anwendung nicht mehr nachgeladen (GTK_MODULES wird nur beim Start +### ausgewertet). Deshalb blockierend (kein Hintergrund-"&" fuer die +### Paketinstallation) und deshalb im Skript, das ubiquity-dm selbst +### aufruft, statt im spaeter laufenden Autoklicker-Service. +### +### Absichtlich EIN gemeinsamer apt-get-Aufruf fuer alle drei Pakete (statt +### getrennter Aufrufe je Skript) - der Autoklicker installiert xdotool +### zeitlich nah, ein zweiter paralleler apt-get/dpkg-Aufruf haette real +### beobachtete Lock-Konflikte riskiert (debconf: "config.dat is locked by +### another process"). + +for pkg in at-spi2-core dbus-x11 python3-pyatspi; do + dpkg -s "$pkg" >/dev/null 2>&1 && continue + if [ "${_tuxflotte_atspi_apt_updated:-}" != "1" ]; then + apt-get update -qq + _tuxflotte_atspi_apt_updated=1 + fi + DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg" +done + +export GTK_MODULES="gail:atk-bridge" +export NO_AT_BRIDGE=0 + +# Eigene Session-Bus-Instanz, falls keine geerbt wurde - die minimale +# ubiquity-dm-X-Sitzung startet anders als eine normale Desktop-Sitzung +# vermutlich keinen eigenen Session-Bus. +if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/dev/null 2>&1; then + eval "$(dbus-launch --sh-syntax)" + export DBUS_SESSION_BUS_ADDRESS DBUS_SESSION_BUS_PID +fi + +if command -v at-spi-bus-launcher >/dev/null 2>&1; then + at-spi-bus-launcher --launch-immediately >/var/log/tuxflotte-atspi-bus.log 2>&1 & + # Kurz abwarten, bis der a11y-Bus unter org.a11y.Bus registriert ist, + # bevor ubiquity-dm/Ubiquity (naechste Zeile in start-ubiquity-dm) + # GTK initialisiert. + sleep 1 +fi