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>
284 lines
9.9 KiB
Python
Executable File
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())
|