Thomas Stallinger 4bf1992eb9 fix: Autoklicker wartet auf Ubiquitys Hintergrundpruefung statt sie zu unterbrechen
Timing-Hypothese fuer den partman-rebuild-cache-Loop: der $bootable{}-Fix
brachte live keine Aenderung (siehe ADR-0023-Nachtrag), was gegen einen
rein rezeptseitigen Fehler spricht. Stattdessen zeigt die 'Etwas
Anderes'-Seite waehrend der Klickphase ein Status-Panel 'Konfiguration
der Installation wird ueberprueft ...' - ein Klick auf 'Jetzt
installieren' WAEHREND dieser Pruefung noch laeuft koennte sie
unterbrechen/neu anstossen statt sie abschliessen zu lassen.

atspi_click.py sucht jetzt vor jedem Klickversuch aktiv nach diesem
sichtbaren Text (find_showing_text(), rollenunabhaengig) und gibt bei
Treffer Exit-Code 3 zurueck, ohne zu klicken. autoclicker.sh behandelt
das als eigenen, nicht als Fehlversuch gezaehlten Zustand.

Noch nicht live verifiziert - naechster Schritt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 17:53:10 +02:00

284 lines
9.9 KiB
Python
Executable File

#!/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.
WICHTIG (real beim Testen entdeckt, 29.08.2026): trotz fest gesetztem
"debian-installer/language=de" sind NICHT alle Assistentenseiten auf
Deutsch beschriftet - die Mint-eigene "Multimedia-Codecs"-Seite zeigt reale
englische Buttons ("Quit"/"Back"/"Continue"), vermutlich dieselbe Ursache
wie der bereits gepatchte ubi-prepare.py-None-Bug (fehlender/unvollstaendiger
.mo-Uebersetzungskatalog fuer dieses Plugin auf dem Live-Medium). Eine reine
Werteliste bekannter deutscher Beschriftungen (fruehere Version dieses
Skripts) haette hier fuer immer haengen bleiben. Die Logik wurde deshalb auf
eine sprachunabhaengige Ausschluss-Strategie umgestellt: bekannte
rueckwaerts-/abbrechende Beschriftungen (zweisprachig) werden aus der
Kandidatenliste entfernt, der bzw. die verbleibenden Kandidaten werden
geklickt - siehe select_target() fuer die genaue Regel samt
Sicherheitsverhalten bei Mehrdeutigkeit.
Sprache ist durch "debian-installer/language=de" im Kexec-Append-Preseed
grundsaetzlich auf de_DE gesetzt (siehe backend.sh), s.o. aber nicht
verlaesslich fuer jede einzelne Seite.
NACHTRAG (real beim Testen entdeckt, 29.08.2026): auf der manuellen
Partitionierungsseite ("Etwas Anderes") lief der Klick auf "Jetzt
installieren" nachweislich zuverlaessig (AT-SPI meldete jedes Mal Erfolg),
aber die Seite kam nie voran - stattdessen loeste jeder Klick erneut die
Debconf-Frage "ubiquity/partman-rebuild-cache" aus
(/lib/partman/update.d/99signal_ubiquity), was ubi-partman.py zurueck auf
dieselbe Seite wirft (siehe ADR-0023-Nachtrag). Ein gezielter Fix am
Partitionsrezept (kein zusaetzliches $bootable{ } auf der Root-Partition
unter UEFI) aenderte NICHTS - das deutet auf einen zeitlichen statt
strukturellen Fehler hin: die Seite zeigt waehrend dieser Phase ein
Status-Panel "Konfiguration der Installation wird ueberprueft ..." - klickt
der Autoklicker WAEHREND dieser Hintergrundpruefung noch laeuft, koennte
er sie unterbrechen/neu anstossen, statt ihr Zeit zum Abschliessen zu
geben. Dieses Skript wartet deshalb jetzt aktiv, solange dieser Text
sichtbar ist (siehe VALIDATION_TEXT_MARKERS/find_showing_text()), und
klickt erst, wenn er verschwunden ist.
Exit-Codes: 0 = ein Button wurde gefunden und geklickt,
1 = kein eindeutiger Button gefunden (z.B. Seite im Ladezustand, oder echte
Mehrdeutigkeit - dann bewusst NICHT geklickt, siehe select_target()),
2 = pyatspi/AT-SPI-Bus nicht erreichbar (Bruecke evtl. nicht aktiv),
3 = Ubiquitys eigene Hintergrundpruefung laeuft noch sichtbar - bewusst
nicht geklickt, siehe Nachtrag oben.
"""
import sys
try:
import pyatspi
except ImportError:
print("pyatspi nicht verfuegbar", file=sys.stderr)
sys.exit(2)
# Zweisprachig, da nicht jede Seite verlaesslich deutsch beschriftet ist
# (siehe Modul-Docstring). Diese Namen werden aus der Kandidatenliste
# AUSGESCHLOSSEN, nie geklickt - unabhaengig davon, ob sie der einzige
# sichtbare Button waeren.
EXCLUDE_LABELS = {
"zurück",
"zurueck",
"back",
"abbrechen",
"abbruch",
"cancel",
"beenden",
"quit",
"exit",
"verwerfen",
}
# Bevorzugt geklickt, falls mehrere Kandidaten nach dem Ausschluss uebrig
# bleiben (siehe select_target()). Kein Ausschlusskriterium fuer
# Kandidaten, die HIER nicht drin stehen - die duerfen trotzdem als
# alleiniger Kandidat geklickt werden (Sole-Survivor-Regel), damit auch
# unbekannte/nicht uebersetzte Beschriftungen wie das reale "Continue"
# oben nicht zum Stillstand fuehren.
PREFER_LABELS = {
"weiter",
"continue",
"next",
"jetzt installieren",
"install now",
"fortfahren",
"proceed",
"installieren",
"install",
"neu starten",
"restart now",
"schließen",
"schliessen",
"close",
"finish",
"fertigstellen",
"done",
}
MAX_DEPTH = 40
# Zweisprachig vorsichtshalber (siehe Multimedia-Codecs-Erfahrung oben),
# auch wenn dieser konkrete Text bisher immer deutsch beobachtet wurde.
VALIDATION_TEXT_MARKERS = (
"wird überprüft",
"wird ueberprueft",
"being checked",
"checking the install",
)
def find_showing_text(node, needle, depth=0):
"""Sucht ein sichtbares Accessible, dessen Name ODER Beschreibung den
Suchtext enthaelt - unabhaengig von seiner Rolle (Label/Expander/...),
da Ubiquitys Status-Panel-Text nicht zuverlaessig unter derselben Rolle
exponiert sein muss. Bei Unsicherheit ueber den Sichtbarkeitsstatus
bewusst konservativ (lieber einen Zyklus zu viel warten als einen
Klick mitten in die Pruefung setzen)."""
if node is None or depth > MAX_DEPTH:
return False
try:
name = (node.name or "").strip().lower()
except Exception:
name = ""
try:
desc = (node.description or "").strip().lower()
except Exception:
desc = ""
if needle in name or needle in desc:
try:
state = node.getState()
return state.contains(pyatspi.STATE_SHOWING)
except Exception:
return True
try:
count = node.childCount
except Exception:
return False
for i in range(count):
try:
child = node.getChildAtIndex(i)
except Exception:
continue
if find_showing_text(child, needle, depth + 1):
return True
return False
def collect_buttons(node, depth=0, acc=None):
"""Alle sichtbaren+aktivierten Push-Buttons im Baum einsammeln (Rolle,
Zustand). Absichtlich ALLE Kandidaten sammeln statt beim ersten Treffer
abzubrechen - select_target() braucht die vollstaendige Liste, um
Mehrdeutigkeit erkennen zu koennen."""
if acc is None:
acc = []
if node is None or depth > MAX_DEPTH:
return acc
try:
role = node.getRole()
except Exception:
return acc
if role == pyatspi.ROLE_PUSH_BUTTON:
try:
state = node.getState()
if state.contains(pyatspi.STATE_SENSITIVE) and state.contains(
pyatspi.STATE_SHOWING
):
acc.append(node)
except Exception:
pass
return acc
try:
count = node.childCount
except Exception:
return acc
for i in range(count):
try:
child = node.getChildAtIndex(i)
except Exception:
continue
collect_buttons(child, depth + 1, acc)
return acc
def select_target(buttons):
"""Waehlt aus allen sichtbaren+aktivierten Buttons genau einen aus, der
geklickt werden darf - oder None, wenn das nicht sicher moeglich ist.
Regel (bewusst sicherheitsorientiert, siehe Modul-Docstring):
1. Bekannte rueckwaerts-/abbrechende Beschriftungen (EXCLUDE_LABELS)
werden nie geklickt, unabhaengig vom Rest.
2. Von den verbleibenden Kandidaten wird bevorzugt, wer eine bekannte
vorwaerts-/abschliessende Beschriftung traegt (PREFER_LABELS).
3. Bleibt nach 1./2. KEIN bekannter Treffer, aber genau EIN Kandidat
uebrig, wird dieser geklickt (Sole-Survivor-Regel - deckt unbekannte/
nicht uebersetzte Beschriftungen wie das real beobachtete "Continue"
ab, siehe Modul-Docstring).
4. Bleiben mehrere unbekannte Kandidaten gleichzeitig uebrig, wird NICHT
geklickt (echte Mehrdeutigkeit - ein Fehlklick waere hier riskanter
als ein uebersprungener Zyklus, die Schleife in autoclicker.sh
versucht es 2 Sekunden spaeter erneut)."""
candidates = []
for btn in buttons:
name = (btn.name or "").strip().lower()
if name in EXCLUDE_LABELS:
continue
candidates.append(btn)
if not candidates:
return None
preferred = [b for b in candidates if (b.name or "").strip().lower() in PREFER_LABELS]
if preferred:
return preferred[0]
if len(candidates) == 1:
return candidates[0]
return None
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
apps = []
for i in range(desktop.childCount):
try:
app = desktop.getChildAtIndex(i)
except Exception:
continue
if app is not None:
apps.append(app)
for marker in VALIDATION_TEXT_MARKERS:
for app in apps:
if find_showing_text(app, marker):
print(
f"Hintergrundpruefung noch sichtbar ('{marker}'), warte statt zu klicken.",
file=sys.stderr,
)
return 3
all_buttons = []
for app in apps:
collect_buttons(app, 0, all_buttons)
target = select_target(all_buttons)
if target is None:
names = [repr((b.name or "").strip()) for b in all_buttons]
print(
"kein eindeutiger Button gefunden, sichtbare Buttons: "
+ (", ".join(names) if names else "(keine)"),
file=sys.stderr,
)
return 1
try:
action = target.queryAction()
action.doAction(0)
except Exception as exc:
print(f"Klick auf '{target.name}' fehlgeschlagen: {exc}", file=sys.stderr)
return 1
print(f"geklickt: {target.name}")
return 0
if __name__ == "__main__":
sys.exit(main())