Neue Rechnungen tauchen von selbst auf - das Journal hoert auf die gemeinsame SQLite

Stehen beide Programme offen und wird im Rechnungstool eine Rechnung
geschrieben, erscheint sie hier ohne Zutun. Das Journal traegt sich dafuer beim
Start mit seiner PID in der gemeinsamen SQLite ein (Tabelle "laeuft") und traegt
sich beim Schliessen wieder aus; das Rechnungstool sieht daran, dass jemand
zuhoert, und legt eine Zeile in die Tabelle "nachrichten".

Nachgesehen wird nur dort - erst ein os.stat auf die eine Datei, und nur wenn
sich etwas geruehrt hat, wird sie geoeffnet. Kein Absuchen des
Rechnungsordners, kein Server, kein Port. Beim Zurueckklicken ins Fenster wird
sofort nachgesehen statt zwei Sekunden zu warten.

Die Wache faengt jeden Fehler ab und stirbt nie - ein Netzlaufwerk, das kurz
weg ist, darf das Fenster nicht lahmlegen.

⚠ Ein Denkfehler daraus gleich behoben: der erste Blick nach dem Start merkt
sich den Stand AUCH dann, wenn es die Datei noch gar nicht gibt. Sonst galt die
erste Meldung danach als "schon gesehen" und wurde verschluckt.

Pruefstand pruef_wache.py (faehrt BEIDE Programme): 23 Pruefungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYeAfLtccFrbU3MTj1MMTx
This commit is contained in:
TheMockTv 2026-09-03 21:02:24 +02:00
parent 59d2db75af
commit 31bb0c600b
3 changed files with 478 additions and 0 deletions

93
app.py
View file

@ -104,6 +104,13 @@ class App(tk.Tk):
if self.pdf_ordner and os.path.isdir(self.pdf_ordner):
self.after(200, lambda: self.einlesen(still=True))
self.after(400, self._frage_gemeinsam_falls_noetig)
# Beide Programme laufen nebeneinander: schreibt das Rechnungstool eine
# Rechnung, meldet es das hier herein - siehe _wache_jetzt().
self._briefkasten_stand = "?" # "?" = noch nicht nachgesehen
self._nachricht_stand = 0 # bis hierher sind Meldungen gelesen
self.after(600, self._wache)
# Beim Zurueckklicken sofort nachsehen statt zwei Sekunden zu warten.
self.bind("<FocusIn>", lambda _e: self._wache_jetzt())
# ------------------------------------------------------------ Aufbau ----
def _menue(self):
@ -253,6 +260,62 @@ class App(tk.Tk):
text=f"Rechnungen: {self.pdf_ordner}" if self.pdf_ordner
else "kein Rechnungs-Ordner gewählt (Datei → Rechnungs-Ordner wählen…)")
# ------------------------------------------------------------- Wache
# Das Rechnungstool MELDET, wenn es eine Rechnung geschrieben hat - es legt
# eine Zeile in den Briefkasten im gemeinsamen Ordner. Hier wird nur diese
# eine Datei angesehen (ein os.stat), nicht der ganze Rechnungsordner.
# Damit das Rechnungstool weiss, dass jemand zuhoert, traegt sich dieses
# Fenster beim Start mit seiner PID ein (siehe _anmelden).
WACHE_SEKUNDEN = 2
def _wache_jetzt(self):
"""Einmal nachsehen. True, wenn eine Meldung da war und eingelesen wurde.
Billig gehalten: erst ein os.stat auf die gemeinsame Datei. Nur wenn
sich dort etwas geruehrt hat, wird sie ueberhaupt geoeffnet.
"""
if self.buch is None:
return False
stand = gemeinsam.datei_stand(self.buch.ordner)
if self._briefkasten_stand == "?":
# Erster Blick nach dem Start: nur merken, auch wenn es die Datei
# noch gar nicht gibt - sonst gilt die erste Meldung danach als
# "schon gesehen" und wird verschluckt.
self._briefkasten_stand = stand
self._nachricht_stand = self._letzte_nachricht()
return False
if stand is None or stand == self._briefkasten_stand:
return False
self._briefkasten_stand = stand
try:
texte, neu = self.buch.nachrichten_ab(self._nachricht_stand)
except Exception as e: # noqa: BLE001
log.warning("Briefkasten nicht lesbar: %s", e)
return False
if not texte:
return False # das Buch hat sich geaendert, aber ohne Meldung
self._nachricht_stand = neu
text = texte[-1] if len(texte) == 1 else f"{len(texte)} Meldungen, zuletzt: {texte[-1]}"
log.info("Rechnungstool meldet: %s", text)
self.einlesen(still=True)
self.melde(f"Rechnungstool meldet: {text}")
return True
def _letzte_nachricht(self):
try:
return self.buch.letzte_nachricht() if self.buch else 0
except Exception: # noqa: BLE001
return 0
def _wache(self):
"""Der Wecker. Laeuft weiter, auch wenn ein Durchgang schiefgeht."""
try:
self._wache_jetzt()
except Exception as e: # noqa: BLE001 - die Wache darf nie sterben
log.warning("Wache: %s", e)
finally:
self.after(self.WACHE_SEKUNDEN * 1000, self._wache)
def suche_leeren(self):
self.suche.set("")
self.suchfeld.focus_set()
@ -262,6 +325,32 @@ class App(tk.Tk):
log.info(text)
# ---- Gemeinsames Nummernbuch -------------------------------------------
def _anmelden(self):
"""Mit der eigenen PID im gemeinsamen Ordner eintragen.
Daran sieht das Rechnungstool, dass dieses Fenster offen ist - es fragt
Windows nach der Prozessnummer, nicht nach einem Fenster.
"""
ordner = self.journal.hole("gemeinsam_ordner", "")
if not ordner:
return
if self.buch is None:
self._buch_oeffnen()
if self.buch is None:
return
try:
self.buch.anmelden("journal", os.getpid(), os.path.basename(sys.executable))
except Exception as e: # noqa: BLE001
log.warning("Anmeldung fehlgeschlagen: %s", e)
def _abmelden(self):
if self.buch is None:
return
try:
self.buch.abmelden("journal")
except Exception as e: # noqa: BLE001 - beim Schliessen nichts erzwingen
log.debug("Abmelden fehlgeschlagen: %s", e)
def _buch_oeffnen(self):
ordner = self.journal.hole("gemeinsam_ordner", "")
if not ordner:
@ -284,6 +373,7 @@ class App(tk.Tk):
self._buch_oeffnen()
if self.buch is None and not TESTLAUF:
self.dlg_gemeinsam(erststart=True)
self._anmelden()
self._buch_fuellen()
def dlg_gemeinsam(self, erststart=False):
@ -300,6 +390,7 @@ class App(tk.Tk):
return
self.journal.setze("gemeinsam_ordner", ordner)
self._buch_oeffnen()
self._anmelden()
self._buch_fuellen(melden=True)
def _buch_fuellen(self, melden=False):
@ -669,6 +760,8 @@ class App(tk.Tk):
os.startfile(pfad) # noqa: S606
def _beenden(self):
# Austragen, damit das Rechnungstool nicht weiter meldet.
self._abmelden()
self.journal.setze("jahr", self.jahr.get())
self.journal.schliessen()
self.destroy()

View file

@ -27,6 +27,8 @@ hinueber - sonst schreiben zwei Fassungen in dieselbe Datei.
import logging
import os
import sqlite3
import sys
import time
log = logging.getLogger("gemeinsam")
@ -56,6 +58,26 @@ CREATE TABLE IF NOT EXISTS nummern (
geaendert TEXT NOT NULL DEFAULT '' -- ISO-Zeitstempel
);
CREATE INDEX IF NOT EXISTS idx_nummern_jahr ON nummern (jahr);
-- Wer gerade laeuft. Eine Zeile je Programm, mit PID und EXE-Namen: ob es
-- wirklich noch laeuft, beantwortet Windows (prozess_laeuft), nicht ein
-- Zeitstempel. Ein abgestuerztes Programm hinterlaesst hier nur eine
-- Karteileiche, die beim naechsten Nachsehen weggeraeumt wird.
CREATE TABLE IF NOT EXISTS laeuft (
programm TEXT PRIMARY KEY, -- 'journal' | 'rechnungstool'
pid INTEGER NOT NULL DEFAULT 0,
exe TEXT NOT NULL DEFAULT '',
seit TEXT NOT NULL DEFAULT ''
);
-- Der Briefkasten. Das Rechnungstool legt hier ab, dass es eine Rechnung
-- geschrieben hat; das Journal liest die neuen Zeilen und liest die PDFs nach.
CREATE TABLE IF NOT EXISTS nachrichten (
id INTEGER PRIMARY KEY AUTOINCREMENT,
zeit TEXT NOT NULL DEFAULT '',
absender TEXT NOT NULL DEFAULT '',
text TEXT NOT NULL DEFAULT ''
);
"""
FELDER = ("jahr", "datum", "nachname", "vorname", "naechte", "zwischensumme",
@ -84,6 +106,78 @@ def ordner_taugt(ordner):
return True, ""
# ---------------------------------------------------------------------------
# Briefkasten und Herzschlag: das Rechnungstool sagt dem Steuerjournal Bescheid
#
# Kein Programm durchsucht dauernd den Rechnungsordner. Stattdessen legt das
# LAUFENDE Steuerjournal einen Herzschlag im gemeinsamen Ordner ab, und das
# Rechnungstool schaut nach dem Schreiben einer Rechnung nur kurz hinein: laeuft
# es, wirft es eine Nachricht in den Briefkasten. Das Journal sieht genau EINE
# Datei nach (os.stat) und liest die Rechnung dann von selbst ein.
#
# Ist das Journal zu, passiert nichts - es liest den Ordner beim naechsten Start
# ohnehin komplett. Die Nachricht ist eine Anklopfhilfe, keine Datenquelle.
def prozess_laeuft(pid, name=""):
"""Laeuft unter dieser Prozessnummer wirklich noch etwas?
Die Antwort kommt von Windows, nicht aus einer Uhrzeit und nicht aus einer
Fenstersuche. PIDs werden wiederverwendet - deshalb wird zusaetzlich der
Name der EXE verglichen, wenn einer eingetragen wurde. Sonst haette ein
fremder Prozess mit derselben Nummer das andere Programm "am Leben"
gehalten.
"""
if not pid:
return False
if not sys.platform.startswith("win"):
try:
os.kill(int(pid), 0)
return True
except (OSError, ValueError):
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:
if not name:
return True
puffer = ctypes.create_unicode_buffer(4096)
laenge = wintypes.DWORD(len(puffer))
if not k32.QueryFullProcessImageNameW(griff, 0, puffer, ctypes.byref(laenge)):
return False
return os.path.basename(puffer.value).lower() == str(name).lower()
finally:
k32.CloseHandle(griff)
def datei_stand(ordner):
"""(Aenderungszeit, Groesse) der gemeinsamen Datei - oder None.
Damit sieht ein Programm mit EINEM os.stat, ob sich im Buch etwas getan hat,
ohne dafuer die Datenbank zu oeffnen.
"""
try:
st = os.stat(pfad_zur_datei(ordner))
except OSError:
return None
return st.st_mtime, st.st_size
def journal_laeuft(ordner):
"""Ist das Steuerjournal gerade offen?"""
if not ordner or not os.path.exists(pfad_zur_datei(ordner)):
return False
try:
return Nummernbuch(ordner).laeuft("journal")
except Exception as e: # noqa: BLE001 - eine Auskunft darf nichts umwerfen
log.debug("Anmeldung nicht lesbar: %s", e)
return False
class Nummernbuch:
"""Das gemeinsame Register. Wird zum Schreiben kurz geoeffnet und wieder
geschlossen - zwei Programme haben die Datei sonst dauerhaft in der Hand,
@ -153,6 +247,67 @@ class Nummernbuch:
return {z["storno_zu"] for z in
con.execute("SELECT storno_zu FROM nummern WHERE storno_zu <> ''")}
# ---- Wer laeuft, und der Briefkasten -----------------------------------
def anmelden(self, programm, pid, exe):
"""Eintragen: dieses Programm ist offen."""
import datetime
with self._verbindung() as con:
con.execute(
"INSERT INTO laeuft (programm, pid, exe, seit) VALUES (?,?,?,?) "
"ON CONFLICT(programm) DO UPDATE SET pid=excluded.pid, exe=excluded.exe, "
"seit=excluded.seit",
(str(programm), int(pid), str(exe),
datetime.datetime.now().isoformat(timespec="seconds")))
log.info("angemeldet: %s (PID %s)", programm, pid)
def abmelden(self, programm):
with self._verbindung() as con:
con.execute("DELETE FROM laeuft WHERE programm=?", (str(programm),))
def laeuft(self, programm):
"""Laeuft dieses Programm noch? Karteileichen werden weggeraeumt."""
with self._verbindung() as con:
z = con.execute("SELECT pid, exe FROM laeuft WHERE programm=?",
(str(programm),)).fetchone()
if not z:
return False
if prozess_laeuft(z["pid"], z["exe"]):
return True
self.abmelden(programm)
return False
def melden(self, absender, text):
"""Eine Zeile in den Briefkasten legen."""
import datetime
with self._verbindung() as con:
con.execute("INSERT INTO nachrichten (zeit, absender, text) VALUES (?,?,?)",
(datetime.datetime.now().isoformat(timespec="seconds"),
str(absender), str(text)))
log.info("Nachricht von %s: %s", absender, text)
return True
def letzte_nachricht(self):
"""Hoechste Nachrichten-Nummer (0, wenn der Kasten leer ist)."""
with self._verbindung() as con:
z = con.execute("SELECT MAX(id) AS m FROM nachrichten").fetchone()
return int(z["m"] or 0)
def nachrichten_ab(self, seit_id):
"""Alle Nachrichten NACH dieser Nummer: (Liste von Texten, neue Nummer)."""
with self._verbindung() as con:
zeilen = list(con.execute(
"SELECT id, absender, text FROM nachrichten WHERE id > ? ORDER BY id",
(int(seit_id or 0),)))
if not zeilen:
return [], int(seit_id or 0)
return [z["text"] for z in zeilen], int(zeilen[-1]["id"])
def aufraeumen(self, behalten=200):
"""Alte Nachrichten wegwerfen - der Briefkasten ist kein Archiv."""
with self._verbindung() as con:
con.execute("DELETE FROM nachrichten WHERE id <= "
"(SELECT MAX(id) FROM nachrichten) - ?", (int(behalten),))
def entfernen(self, nummer):
"""Nur fuer den Rueckweg: ein verworfener Storno gibt seine Nummer zurueck."""
with self._verbindung() as con:

230
pruef_wache.py Normal file
View file

@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
"""Pruefstand: das Rechnungstool meldet, das Steuerjournal liest nach.
Sein Fall: beide Programme stehen offen. Er schreibt im Rechnungstool eine
Rechnung - dann soll sie hier auftauchen, ohne dass jemand "Rechnungen
einlesen" drueckt. Seine Ansage zum Weg:
"machs einfacher das rechnungstoll schaut ob der steuer ding da ist und
meldet dem hier neue rechnung wurde erstellt"
Der Weg, den er vorgegeben hat: das laufende Journal traegt sich mit seiner PID
in der GEMEINSAMEN SQLITE ein ("der briefkasten ist die sqlite"), das
Rechnungstool fragt WINDOWS, ob dieser Prozess laeuft ("nicht auf fenster
schauen"), und legt dann eine Zeile in den Briefkasten - dieselbe Datei. Das
Journal sieht nur dort nach. Geprueft wird mit BEIDEN echten Programmen.
"""
import json
import os
import shutil
import sys
import tempfile
import threading
import time
JOURNAL = r"C:\claude\beherbergungssteuer"
TOOL = r"C:\claude\rechnungstool"
fehler = []
def pruefe(bedingung, text):
zeile = (" OK | " if bedingung else "FEHLER | ") + text
kodierung = sys.stdout.encoding or "utf-8"
print(zeile.encode(kodierung, "replace").decode(kodierung))
if not bedingung:
fehler.append(text)
def _reissleine():
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
os._exit(2)
_wecker = threading.Timer(150, _reissleine)
_wecker.daemon = True
_wecker.start()
os.environ["CAMPINGHOF_TEST"] = "1"
tmp = tempfile.mkdtemp(prefix="bst_melden_")
gemeinsam_ordner = os.path.join(tmp, "install")
rechnungen = os.path.join(tmp, "rechnungen")
daten = os.path.join(tmp, "daten")
for d in (gemeinsam_ordner, rechnungen, daten):
os.makedirs(d)
# ---- Das Steuerjournal ----------------------------------------------------
sys.path.insert(0, JOURNAL)
import ablage # noqa: E402
ablage.daten_ordner = lambda: daten
import app as JA # noqa: E402
import gemeinsam # noqa: E402
del sys.modules["app"]
sys.path.remove(JOURNAL)
from tkinter import messagebox # noqa: E402
messagebox.showinfo = lambda *a, **k: None
messagebox.showwarning = lambda *a, **k: None
journal = JA.App()
journal.withdraw()
journal.pdf_ordner = rechnungen
journal.journal.setze("pdf_ordner", rechnungen)
journal.journal.setze("gemeinsam_ordner", gemeinsam_ordner)
journal._buch_oeffnen()
journal.jahr.set(2026)
journal.update()
# ---- 1. Anmeldung mit der PID --------------------------------------------
pruefe(not gemeinsam.journal_laeuft(gemeinsam_ordner),
"vor der Anmeldung gilt das Journal als geschlossen")
journal._anmelden()
pruefe(gemeinsam.journal_laeuft(gemeinsam_ordner),
"nach der Anmeldung sieht man an der PID, dass es laeuft")
pruefe(os.listdir(gemeinsam_ordner) == [gemeinsam.DATEINAME],
f"im gemeinsamen Ordner liegt genau EINE Datei ({os.listdir(gemeinsam_ordner)})")
pruefe(gemeinsam.prozess_laeuft(os.getpid(), os.path.basename(sys.executable)),
"die PID-Pruefung erkennt den eigenen Prozess")
pruefe(not gemeinsam.prozess_laeuft(999999, ""),
"eine Nummer, die es nicht gibt, laeuft nicht")
pruefe(not gemeinsam.prozess_laeuft(os.getpid(), "gibtsnicht.exe"),
"und ein fremder EXE-Name zaehlt nicht (PIDs werden wiederverwendet)")
# ---- Das Rechnungstool ----------------------------------------------------
sys.path.insert(0, TOOL)
import config # noqa: E402
shutil.copy(os.path.join(TOOL, "config.json"), os.path.join(tmp, "config.json"))
config.CONFIG_PATH = os.path.join(tmp, "config.json")
roh = json.load(open(config.CONFIG_PATH, encoding="utf-8"))
roh["rechnung_jahr"], roh["rechnung_zaehler"] = 2026, 0
roh["output_dir"] = rechnungen
roh["gemeinsam_ordner"] = gemeinsam_ordner
json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
import app as A # noqa: E402
antworten = {"Fertig": False, "endgültig erstellen": True, "Hausnummer": True,
"Storno wirklich": True, "Storno verwerfen": True}
def askyesno(titel, text, **kw):
for schlue, wert in antworten.items():
if schlue in titel:
return wert
raise AssertionError(f"unangemeldeter Dialog: {titel!r}")
messagebox.askyesno = askyesno
tool = A.RechnungsApp()
tool.cfg["output_dir"] = rechnungen
tool.cfg["gemeinsam_ordner"] = gemeinsam_ordner
tool._buch_oeffnen()
# ---- 2. Das Rechnungstool sieht: das Journal ist da -----------------------
pruefe(gemeinsam.journal_laeuft(gemeinsam_ordner),
"das Rechnungstool sieht an der PID, dass das Journal offen ist")
# ---- 3. Rechnung schreiben -> Nachricht in den Briefkasten ----------------
journal._wache_jetzt() # Stand merken, Briefkasten ist noch leer
tool.k_vorname.set("Anna")
tool.k_name.set("Meldung")
tool.k_strasse.set("Musterweg 1")
tool.k_plzort.set("12345 Musterstadt")
tool.r_anreise.set("01.08.2026")
tool.r_abreise.set("04.08.2026")
tool.anzahl_vars[sorted(tool.anzahl_vars)[0]].set("3")
tool.update()
tool.pdf_erstellen()
texte, _ = gemeinsam.Nummernbuch(gemeinsam_ordner).nachrichten_ab(0)
pruefe(any("Rechnung 2026-001 erstellt" in t for t in texte),
f"die Meldung steht in der SQLite ({texte})")
# ---- 4. Das Journal liest von selbst nach --------------------------------
pruefe(journal._wache_jetzt() is True, "die Wache sieht die Meldung")
journal.update()
buchungen = journal.journal.buchungen(jahr=2026)
pruefe(len(buchungen) == 1 and buchungen[0].nachname == "Meldung",
f"die Rechnung steht im Journal ({[(b.voll_nummer, b.nachname) for b in buchungen]})")
pruefe("Rechnungstool meldet" in journal.status.cget("text"),
f"und es steht in der Statuszeile ({journal.status.cget('text')!r})")
# Gebucht wird nach RECHNUNGSDATUM (heute), nicht nach Anreise.
import datetime as _dt
monat = _dt.date.today().month
zeilen = journal.monate[monat].baum.get_children()
pruefe(len(zeilen) == 1, f"das Monatsblatt zeigt sie ({len(zeilen)} im Monat {monat})")
# ---- 5. Ohne neue Meldung passiert nichts --------------------------------
pruefe(journal._wache_jetzt() is False, "ohne neue Meldung wird nicht eingelesen")
# ---- 6. Auch der Storno meldet sich --------------------------------------
import bestand as BE # noqa: E402
offen = [m for m in BE.rechnungen_im_ordner(rechnungen) if m.get("art") != "storno"]
tool._storno_anlegen(offen[0], rechnungen)
texte, _ = gemeinsam.Nummernbuch(gemeinsam_ordner).nachrichten_ab(0)
pruefe(any("Storno" in t and "2026-001" in t for t in texte),
f"der Storno meldet sich ebenfalls ({texte})")
pruefe(journal._wache_jetzt() is True, "und das Journal liest ihn ein")
storno = [b for b in journal.journal.buchungen(jahr=2026) if b.storno_zu]
pruefe(len(storno) == 1 and storno[0].storno_zu == "2026-001",
f"der Storno steht mit seinem Bezug im Journal ({[(b.voll_nummer, b.storno_zu) for b in storno]})")
# ---- 7. Ist das Journal zu, wird nicht gemeldet --------------------------
journal._abmelden()
pruefe(not gemeinsam.journal_laeuft(gemeinsam_ordner),
"nach dem Abmelden gilt das Journal als geschlossen")
vorher = gemeinsam.Nummernbuch(gemeinsam_ordner).letzte_nachricht()
tool.reset_formular()
tool.k_vorname.set("Bernd")
tool.k_name.set("Ohne")
tool.k_strasse.set("Musterweg 2")
tool.k_plzort.set("12345 Musterstadt")
tool.r_anreise.set("10.08.2026")
tool.r_abreise.set("12.08.2026")
tool.anzahl_vars[sorted(tool.anzahl_vars)[0]].set("2")
tool.update()
tool.pdf_erstellen()
pruefe(gemeinsam.Nummernbuch(gemeinsam_ordner).letzte_nachricht() == vorher,
"dann kommt keine Meldung dazu")
pruefe(len([d for d in os.listdir(rechnungen) if "Ohne" in d]) == 1,
"die Rechnung wird trotzdem geschrieben")
# ---- 8. Eine Karteileiche zaehlt nicht und wird weggeraeumt -------------
buch = gemeinsam.Nummernbuch(gemeinsam_ordner)
buch.anmelden("journal", 999999, "Beherbergungssteuer.exe")
pruefe(not gemeinsam.journal_laeuft(gemeinsam_ordner),
"eine tote PID (abgestuerzt) gilt nicht als laufend")
pruefe(not buch.laeuft("journal"), "und die Karteileiche ist gleich weggeraeumt")
# ---- 9. Die Wache darf nie sterben --------------------------------------
echt = gemeinsam.datei_stand
gemeinsam.datei_stand = lambda o: (_ for _ in ()).throw(OSError("Laufwerk weg"))
try:
journal._wache()
gestorben = False
except Exception: # noqa: BLE001
gestorben = True
gemeinsam.datei_stand = echt
pruefe(not gestorben, "ein Fehler im Durchgang wirft die Wache nicht um")
# ---- 10. Ohne gemeinsamen Ordner bleibt alles ruhig ---------------------
journal.buch = None
pruefe(journal._wache_jetzt() is False, "ohne gemeinsames Buch tut die Wache nichts")
tool.destroy()
journal.journal.schliessen()
journal.destroy()
if not fehler:
shutil.rmtree(tmp, ignore_errors=True)
else:
print(f" (Wegwerf-Ordner bleibt: {tmp})")
print("---")
print("alles gruen" if not fehler else f"{len(fehler)} Fehler: {fehler}")
sys.exit(1 if fehler else 0)