From 6e99b3bf3a2f7aeea3e342fe7ff184bc8d3fcddb Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Sat, 29 Aug 2026 15:42:55 +0200 Subject: [PATCH] fix: atspi_click.py sprachunabhaengig statt reine deutsche Werteliste Real beim Testen entdeckt: die Mint-eigene 'Multimedia-Codecs'-Seite zeigt trotz debian-installer/language=de englische Buttons ('Quit'/'Back'/ 'Continue') - vermutlich derselbe fehlende .mo-Uebersetzungskatalog wie beim bereits gepatchten ubi-prepare.py-None-Bug. Die deutsche FORWARD_LABELS-Werteliste haette hier fuer immer haengen bleiben. Neue Strategie in select_target(): bekannte rueckwaerts-/abbrechende Beschriftungen (zweisprachig) werden ausgeschlossen, danach wird bevorzugt wer eine bekannte vorwaerts-Beschriftung traegt, sonst der einzige verbleibende Kandidat (Sole-Survivor) - deckt auch unbekannte/nicht uebersetzte Labels ab. Bei echter Mehrdeutigkeit (mehrere unbekannte Kandidaten gleichzeitig) wird bewusst NICHT geklickt statt zu raten. Co-Authored-By: Claude Sonnet 5 --- SOURCE_ISO.sha256 | 1 + .../__pycache__/atspi_click.cpython-311.pyc | Bin 0 -> 8814 bytes .../opt/tuxflotte/scripts/atspi_click.py | 218 +++++++++++------- 3 files changed, 140 insertions(+), 79 deletions(-) create mode 100644 SOURCE_ISO.sha256 create mode 100644 live-updates/opt/tuxflotte/scripts/__pycache__/atspi_click.cpython-311.pyc diff --git a/SOURCE_ISO.sha256 b/SOURCE_ISO.sha256 new file mode 100644 index 0000000..9ef7650 --- /dev/null +++ b/SOURCE_ISO.sha256 @@ -0,0 +1 @@ +fa2dc8d20357d0bd4443b724617498802d3ed772e2f154d3d3690e711595a74b Fedora-Cinnamon-Live-44-1.7.x86_64.iso diff --git a/live-updates/opt/tuxflotte/scripts/__pycache__/atspi_click.cpython-311.pyc b/live-updates/opt/tuxflotte/scripts/__pycache__/atspi_click.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e07732691554585a42109854cb14f8dd45a84ba8 GIT binary patch literal 8814 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 literal 0 HcmV?d00001 diff --git a/live-updates/opt/tuxflotte/scripts/atspi_click.py b/live-updates/opt/tuxflotte/scripts/atspi_click.py index 34a0108..36c0ed6 100755 --- a/live-updates/opt/tuxflotte/scripts/atspi_click.py +++ b/live-updates/opt/tuxflotte/scripts/atspi_click.py @@ -12,13 +12,27 @@ 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. +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. -Exit-Codes: 0 = ein passender Button wurde gefunden und geklickt, -1 = kein passender Button gefunden (z.B. Seite gerade im Ladezustand), +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. + +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). """ import sys @@ -29,70 +43,78 @@ 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", +# 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 -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.""" +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: - if node.getRole() == pyatspi.ROLE_PUSH_BUTTON: - acc.append((node.name or "").strip()) - return acc + 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 @@ -101,10 +123,47 @@ def collect_button_names(node, depth=0, acc=None): child = node.getChildAtIndex(i) except Exception: continue - collect_button_names(child, depth + 1, acc) + 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) @@ -112,7 +171,7 @@ def main(): print(f"AT-SPI-Desktop nicht erreichbar: {exc}", file=sys.stderr) return 2 - seen_buttons = [] + all_buttons = [] for i in range(desktop.childCount): try: app = desktop.getChildAtIndex(i) @@ -120,26 +179,27 @@ def main(): continue if app is None: continue + collect_buttons(app, 0, all_buttons) - 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 + 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 - seen_buttons.extend(collect_button_names(app)) + 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( - "kein passender Button gefunden, sichtbare Buttons: " - + (", ".join(repr(n) for n in seen_buttons) if seen_buttons else "(keine)"), - file=sys.stderr, - ) - return 1 + print(f"geklickt: {target.name}") + return 0 if __name__ == "__main__":