Trading-Plattform-Pruefung: Host nicht erreichbar gibt eine Zeile, --tag frueh geprueft (Paket NT, Task 729)

- abendkontrolle.py: Exit 255 oder Zeitlimit bei Summe oder Bestaetigung beendet die Sicherung mit
  genau einer Zeile in der Notiz von heute und Exit 1; ein Abbruch im Archiv laesst nur die Instanz
  scheitern. --tag ausser der Form endet gleich mit Exit 2, ohne Notiz und ssh.
- abendkontrolle_dockerhost_angriffe.py: sieben neue Faelle, zwei angepasste, Mutanten E729-01 bis
  E729-15 und E729-25 (38 Faelle, 56 Mutanten, keiner ueberlebt).
- abendkontrolle_wsl.py: Kopf nennt die neuen fehler-Gruende.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0126RDfXiECscXipzf67uypJ
This commit is contained in:
Pascal MichelandClaude Opus 5.5 committed 2026-10-11 23:25:38 +02:00
1 parent 02ff0e878d
commit 88ec4fd40f
3 files changed
+355 -28

No files matched your search

@@ -35,6 +35,10 @@ die Summendatei auf D: fehlt oder abweicht, <tag>-<instanz>.tar ueber denselben
pruefen, nach SICHERUNG_ZIEL/<tag>/<instanz>.tar umbenennen, beim Host bestaetigen, zuletzt
<instanz>.tar.sha256 schreiben; eine Zeile in der Notiz je Tag mit Neuem. Solange SICHERUNG_ERSTER_TAG
None ist, holt sie nichts. Den Abend-Ausloeser legt abendkontrolle_einrichten.ps1 in Ortszeit an (S10).
Seit Paket NT (Quorum 4): Endet die Frage nach einer Summe oder die Bestaetigung mit ssh-Exit 255 oder
im Zeitlimit, ist der Docker-Host nicht erreichbar, und die Sicherung hoert sofort auf: eine Zeile in der
Notiz von heute, Exit 1. Bricht nur das Archiv so ab, scheitert allein diese Instanz. --tag ausser der
Form JJJJ-MM-TT oder ohne gueltiges Datum endet gleich nach dem Zerlegen mit Exit 2, ohne Notiz und ssh.
Aufruf von Hand: python abendkontrolle.py [--tag JJJJ-MM-TT] [--vault D:\\Claude] [--ohne-wsl <json-datei>]
[--quelle wsl|dockerhost] [--quelle-ibkr wsl|dockerhost] [--sicherung-ziel <ordner>]
"""
@@ -104,7 +108,7 @@ SICHERUNG_ZIEL = Path(r"D:\Trading-Sicherung")
SICHERUNG_ERSTER_TAG: date | None = None
# So viele Kalendertage vor heute fragt jeder Lauf, dazu heute. Das Fenster liegt in der Aufbewahrung
# des Hosts: Er haelt die neuesten zehn Sicherungstage, nur werktags gesichert, also gut zwei Kalenderwochen.
# (S3). Die Sicherung entsteht 16:45 New Yorker Zeit, in Berlin meist 22:45, vom 25.10. bis 31.10.2026
# Die Sicherung entsteht 16:45 New Yorker Zeit, in Berlin meist 22:45, vom 25.10. bis 31.10.2026
# und vom 14.3. bis 27.3.2027 schon 21:45. Die Aufgabe startet 23:15 Ortszeit (S10); fehlt die
# Sicherung von heute trotzdem noch, ist das kein Fehlschlag.
SICHERUNG_NACHHOLEN_TAGE = 10
@@ -119,6 +123,20 @@ SUMMENZEILE = re.compile(r"([0-9a-f]{64}) (\S+)\n")
TAGFORM = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
def tag_gueltig(tag: str) -> bool:
"""Ist tag ein Datum JJJJ-MM-TT, das es gibt, nur mit ASCII-Ziffern? Seit Paket NT fuer --tag in main.
TAGFORM allein liesse 2026-13-05 durch, date.fromisoformat allein seit Python 3.11 auch 20261005.
"""
if not TAGFORM.fullmatch(tag):
return False
try:
date.fromisoformat(tag)
except ValueError:
return False
return True
def faellige_tage(jetzt: datetime, ablage: Path, erster_tag: date = ERSTER_TAG, endung: str = "") -> list[str]:
tage = []
for zurueck in range(NACHHOLEN_TAGE, -1, -1):
@@ -223,16 +241,36 @@ def summe_von(datei: Path) -> str:
return summe.hexdigest()
class HostNichtErreichbar(Exception):
"""ssh selbst scheiterte (Exit 255) oder ein Befehl der Sicherung lief in sein Zeitlimit (seit Paket NT).
Beim Archiv macht hole_sicherung daraus den Fehlschlag dieser einen Instanz.
"""
def ssh_lauf(befehl: str, timeout, stdout=None) -> subprocess.CompletedProcess:
"""Ein Befehl der Sicherung beim Host. Mit stdout (offene Datei) geht die Ausgabe dorthin.
Eigene Funktion, damit die Angriffsfaelle Zeitlimit und Dauer jedes Befehls sehen.
Eigene Funktion, damit die Angriffsfaelle Zeitlimit und Dauer jedes Befehls sehen. Seit Paket NT
(Quorum 4, G1): abendkontrolle-lesen.sh endet mit 0 bis 4. Exit 255 meldet ssh selbst: keine
Verbindung oder Anmeldung, aber auch ein Befehl, den auf dem Host ein Signal beendet. Er und ein
abgelaufenes Zeitlimit heissen: Der Docker-Host ist nicht erreichbar (HostNichtErreichbar).
"""
if stdout is None:
return subprocess.run(ssh_aufruf(befehl), capture_output=True, timeout=timeout, creationflags=KEIN_FENSTER)
return subprocess.run(
ssh_aufruf(befehl), stdout=stdout, stderr=subprocess.PIPE, timeout=timeout, creationflags=KEIN_FENSTER,
)
try:
if stdout is None:
lauf = subprocess.run(
ssh_aufruf(befehl), capture_output=True, timeout=timeout, creationflags=KEIN_FENSTER,
)
else:
lauf = subprocess.run(
ssh_aufruf(befehl), stdout=stdout, stderr=subprocess.PIPE, timeout=timeout,
creationflags=KEIN_FENSTER,
)
except subprocess.TimeoutExpired:
raise HostNichtErreichbar(f"{befehl}: Zeitlimit {timeout} s abgelaufen") from None
if lauf.returncode == 255:
raise HostNichtErreichbar(f"{befehl}: {fehlerzeile(lauf)}")
return lauf
def summe_auf_d(ordner: Path, instanz: str) -> str | None:
@@ -265,10 +303,8 @@ def sicherungstage(jetzt: datetime, tag: str | None = None) -> list[str]:
if erster is None:
return []
if tag is not None:
# Der Tag wird Teil eines Pfads und eines Befehls auf dem Host: nur JJJJ-MM-TT.
if not TAGFORM.fullmatch(tag):
raise ValueError(f"--tag {tag!r} ist kein Datum JJJJ-MM-TT")
date.fromisoformat(tag)
# Der Tag wird Teil eines Pfads und eines Befehls auf dem Host. Seine Form (JJJJ-MM-TT, ein Datum,
# das es gibt) prueft seit Paket NT main gleich nach dem Zerlegen der Argumente (tag_gueltig).
return [tag]
tage = []
for zurueck in range(SICHERUNG_NACHHOLEN_TAGE, -1, -1):
@@ -337,7 +373,14 @@ def hole_sicherung(tag: str, instanz: str, ziel: Path, summe: str) -> str:
summendatei = ordner / f"{instanz}.tar.sha256"
try:
with teil.open("wb") as strom:
lauf = ssh_lauf(f"sicherung {name}", SICHERUNG_TIMEOUT_S, strom)
try:
lauf = ssh_lauf(f"sicherung {name}", SICHERUNG_TIMEOUT_S, strom)
except HostNichtErreichbar as abbruch:
# Seit Paket NT (Plankritik, Befund 1): Mitten im Archiv heissen Zeitlimit und Exit 255 nicht,
# dass der Host fehlt; er hat die Summe gerade beantwortet. Nur diese Instanz scheitert, und
# ein einzelnes zu langsames Archiv haelt nicht jeden Lauf an derselben Stelle an. Ist der
# Host wirklich weg, merkt es die naechste Frage nach einer Summe.
raise RuntimeError(f"Archiv nicht geholt ({abbruch})") from None
if lauf.returncode != 0:
raise RuntimeError(f"Archiv nicht geholt ({fehlerzeile(lauf)})")
if summe_von(teil) != summe:
@@ -393,6 +436,24 @@ def hole_sicherungen(jetzt: datetime, vault: Path, ablage: Path, ziel: Path, ins
if geholt:
getan = True
logge(ablage, f"sicherung {kandidat}-{instanz}: abgelegt und bestaetigt")
except HostNichtErreichbar as ausnahme:
# Seit Paket NT (Quorum 4, G1): Ohne Host scheiterte sonst jeder Tag des Fensters mit eigener
# Zeile, auch in neu angelegten Notizen von Wochenenden. Die Sicherung hoert hier auf: eine
# Zeile in der Notiz von heute, Exit 1. Fertige Tage behalten ihre Zeilen; was der
# angefangene Tag schon hat, nennt diese Zeile. Auf D: entsteht danach nichts mehr.
logge(ablage, f"sicherung {kandidat}-{instanz}: Docker-Host nicht erreichbar ({ausnahme}), Sicherung abgebrochen")
zeile = (
f"- {jetzt:%H:%M} Uhr — {SICHERUNG_KOPF}: **gescheitert** #needs-review. Docker-Host nicht "
f"erreichbar ({str(ausnahme)[:200]}), Sicherung abgebrochen."
)
if stuecke:
zeile += f" Bis dahin am {kandidat}: {'; '.join(stuecke)}."
zeile += " Der nächste Lauf versucht es erneut."
try:
schreibe_notiz(vault, jetzt.date().isoformat(), zeile)
except Exception as zweite:
logge(ablage, f"sicherung: Notiz nicht geschrieben {zweite!r}")
return 1
except Exception as ausnahme:
gescheitert = True
stuecke.append(f"{instanz} gescheitert ({type(ausnahme).__name__}: {str(ausnahme)[:200]})")
@@ -547,6 +608,13 @@ def main(argv=None) -> int:
help="Ordner fuer die Sicherungen statt SICHERUNG_ZIEL (Angriffsfaelle)",
)
argumente = zerleger.parse_args(argv)
# Seit Paket NT (Quorum 4): --tag gleich hier pruefen. Der Tag wird Name der Notiz, Pfad unter
# SICHERUNG_ZIEL und Teil eines Befehls auf dem Host; vorher legte ein Tag ausser der Form
# Inbox/<tag>.md an, mit ../ auch ausserhalb von Inbox/. Leer zaehlt als ausser der Form. Die
# Ausgabe ist ASCII (!a), damit kein Zeichen an der Kodierung von stderr scheitert.
if argumente.tag is not None and not tag_gueltig(argumente.tag):
print(f"abendkontrolle: --tag {argumente.tag!a} ist kein Datum JJJJ-MM-TT", file=sys.stderr)
return 2
# Seit Paket E9: Das Zeitbudget der Sicherung zaehlt ab hier, also mit der Zeit fuer die
# Ergebnisse (bis zu zwoelf Abfragen mit je SSH_TIMEOUT_S; Plankritik Runde 2, Befund 1).
frist = monotonic() + SICHERUNG_BUDGET_S