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 0000000..e077326 Binary files /dev/null and b/live-updates/opt/tuxflotte/scripts/__pycache__/atspi_click.cpython-311.pyc differ 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__":