Die Runde vom 07.09.2026 hat neben den zwei echten Fehlern eine Reihe Wiederholungen und stumme Stellen gemeldet. Abgearbeitet in Etappen, jede fuer sich pruefbar - nach jeder liefen beide Pruefketten gruen. * Menueleiste: der Wechsel von einem Titel zum anderen brauchte ZWEI Klicks. Der grab_set der offenen Liste schluckt den ersten, die <Button-1>-Bindung des Titels feuert gar nicht. Das native tk.Menu, das diese Leiste ersetzt hat, wechselte schon beim blossen Ueberfahren - zwei Klicks waren also eine Verschlechterung gegenueber dem, was vorher da war. Die Klappliste wird ausserdem jetzt wie jede andere Position ueber theme.auf_bildschirm() begrenzt; am unteren Fensterrand lief sie vorher hinaus. Neu: pruef_menueleiste.py misst beides ueber echte Klickereignisse. * %LOCALAPPDATA%/ravokk wurde an drei Stellen einzeln ausgerechnet. Jetzt fragen alle gemeinsam.standard_ordner(). Laufen die je auseinander, fuehren die beiden Programme zwei Nummernbuecher - und das faellt erst auf, wenn eine Rechnungsnummer zum zweiten Mal vergeben ist (§ 14 Abs. 4 Nr. 4 UStG). * pruefe() stand 35-mal im Quelltext, in sechs Fassungen -> pruefhelfer.py. Die zwei Ausreisser sind mitgezogen: pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug, pruef_bilder.py im Rechnungstool schrieb ein eigenes Format. * dlg_darstellung() und die drei Handgriffe des Umschaltens standen in beiden app.py fast wortgleich -> ThemeMixin in theme.py. Beide trugen inzwischen denselben langen Kommentar zu demselben Fehler; das war der Beweis, dass es eine Sache ist. Programmspezifisch bleiben der Hinweistext im Dialog (R31) und die Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen. * pruef_gemeinsam_automatisch.py sagte im Text "genau ein Buch" und prueft ">= 1" mit einem any() - eine Pruefung, die nicht rot werden kann. Jetzt == 1, und der Dateiname wird mitgeprueft. Dazu neu: die neun Dateien, die im Kopf zusagen, sie laegen in beiden Programmen gleich, werden byteweise verglichen. Bei hinweis.py stimmte die Zusage seit dem 06.09. nicht mehr - gleicher Inhalt, CRLF gegen LF. * .gitattributes, damit die Zeilenenden nicht von der Maschine abhaengen. Ohne das meldet genau diese Pruefung nach einem frischen Checkout einen Unterschied, den es im Repository gar nicht gibt. * Die stummen "except OSError" in api.py, updater.py und einzelinstanz.py sagen jetzt, warum sie schweigen duerfen. Die stummen tk.TclError bleiben: dort wird ein Widget angefasst, das gerade zerstoert wurde, und eine Meldung waere Rauschen. * Tote Parameter und Variablen aus dem Umbau des Vortags: symbol_setzen(dunkel=), kachel(grund=, radius=), mit_trennlinie, sechs x/y-Berechnungen, die von theme.mittig() sofort ueberschrieben wurden, vier lokale "import ctypes". 39 Pruefstaende, 0 rot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
133 lines
4.7 KiB
Python
133 lines
4.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pruefstand: kommt bei der GEBAUTEN EXE wirklich ein Fenster?
|
|
|
|
DER FALL, DER DAZU GEFUEHRT HAT (06.09.2026)
|
|
--------------------------------------------
|
|
Nach dem Installieren lief das Programm - im Taskmanager stand es da, es
|
|
verbrauchte Rechenzeit - und auf dem Bildschirm war NICHTS. Ursache: der
|
|
Erprobungshinweis ging auf, bevor das Hauptfenster gezeichnet war. Der modale
|
|
Dialog sperrte das Hauptfenster, und beide blieben unsichtbar.
|
|
|
|
WARUM DAS KEIN ANDERER PRUEFSTAND FINDET
|
|
----------------------------------------
|
|
Alle uebrigen Pruefstaende fahren das Programm aus der QUELLE. Dort ist das
|
|
Fenster sofort gezeichnet, der Fehler tritt nicht auf. Er gehoert zur gepackten
|
|
EXE - also muss genau die gestartet werden.
|
|
|
|
Und "der Prozess laeuft" beweist nichts: er lief ja. Geprueft wird deshalb, ob
|
|
Windows ein Fenster dieses Programms als SICHTBAR fuehrt.
|
|
|
|
ZWEI FALLEN, die hier schon zugeschnappt sind:
|
|
* Eine PyInstaller-Onefile-EXE ist zweistufig. Der gestartete Prozess packt
|
|
nur aus; das Fenster gehoert dem KIND. Wer nur die eigene PID betrachtet,
|
|
sieht nie ein Fenster.
|
|
* terminate() beendet nur den Auspacker. Das Kind laeuft weiter - und dessen
|
|
Einzelinstanz-Sperre haelt danach jeden weiteren Start ab. Es muss die
|
|
ganze Sippe beendet werden.
|
|
"""
|
|
import ctypes
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from ctypes import wintypes
|
|
|
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HIER)
|
|
|
|
import version # noqa: E402
|
|
from pruefhelfer import fehler, pruefe
|
|
|
|
|
|
stamm, endung = os.path.splitext(version.EXE_NAME)
|
|
EXE = os.path.join(HIER, "dist", f"{stamm}-{version.VERSION}{endung}")
|
|
|
|
if not os.path.exists(EXE):
|
|
print(f"UEBERSPRUNGEN | {os.path.basename(EXE)} ist nicht gebaut "
|
|
f"(vorher: python bauen.py)")
|
|
sys.exit(0)
|
|
|
|
u = ctypes.windll.user32
|
|
# Mit --test: dann geht das Fenster auf dem ZWEITEN Monitor auf und nicht
|
|
# mitten auf dem Hauptbildschirm, wo gerade jemand arbeitet. Seine Ansage am
|
|
# 07.09.2026 - und dieser Pruefstand war der eine, der sie noch nicht befolgte,
|
|
# weil er die gebaute EXE ganz normal startet. Sichtbar bleibt das Fenster,
|
|
# und genau das wird hier ja geprueft.
|
|
p = subprocess.Popen([EXE, "--test"])
|
|
print(f" (gestartet: {os.path.basename(EXE)}, PID {p.pid})")
|
|
|
|
RUECKRUF = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
gefunden = []
|
|
verwandte = {p.pid}
|
|
|
|
|
|
def sippe():
|
|
alle = {p.pid}
|
|
try:
|
|
e = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command",
|
|
"Get-CimInstance Win32_Process | Where-Object "
|
|
f"{{ $_.ParentProcessId -eq {p.pid} }} | ForEach-Object {{ $_.ProcessId }}"],
|
|
capture_output=True, text=True, timeout=20)
|
|
for zeile in e.stdout.split():
|
|
if zeile.strip().isdigit():
|
|
alle.add(int(zeile.strip()))
|
|
except Exception: # noqa: BLE001 - dann bleibt es bei der eigenen PID
|
|
pass
|
|
return alle
|
|
|
|
|
|
def je_fenster(hwnd, _):
|
|
pid = wintypes.DWORD()
|
|
u.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
|
if pid.value not in verwandte:
|
|
return True
|
|
laenge = u.GetWindowTextLengthW(hwnd)
|
|
puffer = ctypes.create_unicode_buffer(laenge + 1)
|
|
u.GetWindowTextW(hwnd, puffer, laenge + 1)
|
|
r = wintypes.RECT()
|
|
u.GetWindowRect(hwnd, ctypes.byref(r))
|
|
gefunden.append((puffer.value, bool(u.IsWindowVisible(hwnd)),
|
|
r.right - r.left, r.bottom - r.top))
|
|
return True
|
|
|
|
|
|
ende = time.time() + 60
|
|
sichtbar = False
|
|
vorzeitig_weg = False
|
|
while time.time() < ende and not sichtbar:
|
|
if p.poll() is not None:
|
|
vorzeitig_weg = True
|
|
break
|
|
verwandte = sippe()
|
|
gefunden.clear()
|
|
u.EnumWindows(RUECKRUF(je_fenster), 0)
|
|
# Ein "Fenster" von wenigen Pixeln ist Verwaltung (IME, Notifier), kein
|
|
# Programmfenster. Deshalb die Groesse mitpruefen.
|
|
sichtbar = any(s and b > 300 and h > 300 for _, s, b, h in gefunden)
|
|
time.sleep(0.5)
|
|
|
|
for titel, s, b, h in gefunden:
|
|
print(f" {titel!r:42s} sichtbar={s} {b}x{h}")
|
|
|
|
pruefe(not vorzeitig_weg,
|
|
f"das Programm bleibt am Leben (Rueckgabe {p.returncode})"
|
|
if vorzeitig_weg else "das Programm bleibt am Leben")
|
|
if vorzeitig_weg and p.returncode == 0:
|
|
print(" (Rueckgabe 0 heisst meist: es laeuft schon eine Fassung. "
|
|
"Erst beenden, dann noch einmal.)")
|
|
|
|
pruefe(sichtbar, "ein sichtbares Programmfenster ist da")
|
|
pruefe(any(str(t).startswith(stamm) and s for t, s, _, _ in gefunden),
|
|
f"und es traegt den Programmnamen ({stamm})")
|
|
|
|
subprocess.run(["taskkill", "/PID", str(p.pid), "/T", "/F"], capture_output=True)
|
|
try:
|
|
p.wait(10)
|
|
except subprocess.TimeoutExpired:
|
|
p.kill()
|
|
|
|
if fehler:
|
|
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
|
|
sys.exit(1)
|
|
print("GRUEN | Die gebaute EXE zeigt ein Fenster")
|