Logo in der Statuszeile, Bau an einer Stelle, Startprobe fuer die EXE
Logo Das Programmlogo steht jetzt klein neben "powered by ravokk" - dort sitzt die Kennung des Werkzeugs, nicht die des Kunden. Eine eigene Kopfzeile hat dieses Programm nicht; oben sind die Reiter. bauen.py Der Weg von der Quelle zur Setup-Datei war eine Folge von Befehlen, die jemand von Hand eintippen musste. Wer einen Schritt vergisst, liefert eine Setup-Datei mit einer alten EXE aus - und sieht es ihr nicht an. Genau das ist heute passiert. Jetzt ist es ein Befehl, die Nummer kommt aus version.py, und am Ende stehen die SHA256-Summen fuer den Release-Text. pruef_start_exe.py Prueft, was kein anderer Pruefstand prueft: ob die GEBAUTE EXE ein sichtbares Fenster zeigt. Alle uebrigen fahren das Programm aus der Quelle, dort trat der Fehler nie auf. "Der Prozess laeuft" beweist dabei nichts - er lief ja, waehrend nichts zu sehen war. Aufgeraeumt "Rechnung Campinghof.exe" ist weg; gebaute Dateien gehoeren ins Release, nicht ins Repo. Die .spec-Dateien dagegen schon: ohne sie kann niemand die EXE nachbauen, die der Updater ausliefert. Fassung 1.7.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
parent
fec0e8449c
commit
73042b771c
20 changed files with 1445 additions and 14 deletions
137
pruef_start_exe.py
Normal file
137
pruef_start_exe.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# -*- 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
|
||||
|
||||
fehler = []
|
||||
|
||||
|
||||
def pruefe(bedingung, text):
|
||||
zeile = (" OK | " if bedingung else "FEHLER | ") + text
|
||||
kod = sys.stdout.encoding or "utf-8"
|
||||
print(zeile.encode(kod, "replace").decode(kod))
|
||||
if not bedingung:
|
||||
fehler.append(text)
|
||||
|
||||
|
||||
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
|
||||
p = subprocess.Popen([EXE])
|
||||
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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue