# -*- coding: utf-8 -*- """Nur ein Fenster zulassen und das offene nach vorn holen. Uebernommen aus dem Steuerrechner - dieselbe Sperre wie im Rechnungstool, nur mit eigenem Namen. Hier geht es um das Journal: zwei Fenster lesen denselben Rechnungsordner in dieselbe journal.sqlite3. Was das eine Fenster loescht, steht im anderen noch auf dem Schirm, und wer dort auf "Bericht fuers Amt" drueckt, meldet einen Stand, den es nicht mehr gibt. Der Jahresbericht ist eine Steuermeldung - daran darf kein zweites Fenster mitschreiben. """ import os import sys def einzelinstanz_pruefen(): """Sperrt das Programm gegen ein zweites Fenster. Beide Fenster wuerden beim Start denselben Rechnungsordner in dieselbe Datenbank einlesen und danach verschiedene Staende anzeigen. Darum: nur eine Instanz je Windows-Sitzung. Rueckgabe: Handle (offen halten, solange das Programm laeuft) oder False, wenn schon eine Instanz laeuft. """ if not sys.platform.startswith("win"): return None import ctypes from ctypes import wintypes ERROR_ALREADY_EXISTS = 183 k32 = ctypes.WinDLL("kernel32", use_last_error=True) k32.CreateMutexW.argtypes = [wintypes.LPVOID, wintypes.BOOL, wintypes.LPCWSTR] k32.CreateMutexW.restype = wintypes.HANDLE handle = k32.CreateMutexW(None, False, "BeherbergungssteuerCampinghofBartl") if ctypes.get_last_error() == ERROR_ALREADY_EXISTS: return False return handle # Merkzettel mit der PID der laufenden Instanz - liegt neben der config.json. def pid_datei(): """Pfad der PID-Datei - bewusst OHNE ablage/Datenordner. Sie liegt beim Benutzer (LOCALAPPDATA), nicht im Datenordner. Zwei Gruende: 1. Die Sperre muss greifen, BEVOR ablage.daten_ordner() laeuft: der legt beim ersten Start den Datenordner an und zieht alte Dateien um. Zwei Prozesse duerfen das nicht gleichzeitig tun. 2. Es geht um "laeuft dieses Programm schon fuer diesen Benutzer", nicht um "in diesem Ordner". Zwei Kopien aus verschiedenen Ordnern sind trotzdem zwei Fenster auf derselben Datenbank. """ heim = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~") ordner = os.path.join(heim, "Beherbergungssteuer") try: os.makedirs(ordner, exist_ok=True) except OSError: pass return os.path.join(ordner, "beherbergungssteuer.pid") def eigene_pid_merken(): """Schreibt die eigene PID, damit eine zweite Instanz das Fenster findet.""" try: with open(pid_datei(), "w", encoding="ascii") as f: f.write(str(os.getpid())) except OSError: # Der Merkzettel ist eine Bequemlichkeit, keine Bedingung: ohne ihn # findet eine zweite Instanz das laufende Fenster nicht und meldet # nur, dass das Programm schon laeuft. Daran darf der Start nicht # scheitern - der Ordner kann schreibgeschuetzt sein. pass def laufende_pid_lesen(): """PID der laufenden Instanz aus dem Merkzettel; None wenn unlesbar.""" try: with open(pid_datei(), "r", encoding="ascii") as f: return int(f.read().strip()) except (OSError, ValueError): return None def gehoert_uns(pid): """Laeuft unter dieser PID wirklich dieses Programm? PIDs werden wiederverwendet. Steht in der alten Datei die Nummer eines fremden Prozesses, holte das Programm sonst DESSEN Fenster nach vorn und beendete sich wortlos - der Nutzer sieht einen Doppelklick, der nichts tut. """ if not pid or not sys.platform.startswith("win"): return False import ctypes from ctypes import wintypes PROCESS_QUERY_LIMITED = 0x1000 k32 = ctypes.WinDLL("kernel32", use_last_error=True) k32.OpenProcess.restype = wintypes.HANDLE griff = k32.OpenProcess(PROCESS_QUERY_LIMITED, False, int(pid)) if not griff: return False try: puffer = ctypes.create_unicode_buffer(4096) laenge = wintypes.DWORD(len(puffer)) if not k32.QueryFullProcessImageNameW(griff, 0, puffer, ctypes.byref(laenge)): return False eigen = os.path.basename(sys.executable).lower() fremd = os.path.basename(puffer.value).lower() return eigen == fremd finally: k32.CloseHandle(griff) def fenster_nach_vorn(pid): """Holt das Fenster des Prozesses nach vorn. True bei Erfolg. Gesucht wird ueber die PID, nicht ueber den Fenstertitel - der Titel traegt den Firmennamen und aendert sich mit ihm. """ if not pid or not sys.platform.startswith("win") or not gehoert_uns(pid): return False import ctypes from ctypes import wintypes SW_RESTORE = 9 u32 = ctypes.WinDLL("user32", use_last_error=True) u32.IsWindowVisible.argtypes = [wintypes.HWND] u32.IsIconic.argtypes = [wintypes.HWND] u32.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int] u32.SetForegroundWindow.argtypes = [wintypes.HWND] u32.GetWindowTextLengthW.argtypes = [wintypes.HWND] u32.GetWindowThreadProcessId.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.DWORD)] treffer = [] def besuch(hwnd, _lparam): fremd = wintypes.DWORD() u32.GetWindowThreadProcessId(hwnd, ctypes.byref(fremd)) # nur das echte Hauptfenster: sichtbar und mit Titel if fremd.value == pid and u32.IsWindowVisible(hwnd) and u32.GetWindowTextLengthW(hwnd): treffer.append(hwnd) return False return True CB = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) u32.EnumWindows(CB(besuch), 0) if not treffer: return False hwnd = treffer[0] if u32.IsIconic(hwnd): u32.ShowWindow(hwnd, SW_RESTORE) u32.SetForegroundWindow(hwnd) return True