User-Frage "wird die Pipeline bei jedem Deployment ausgefuehrt?". Antwort war NEIN: git commit check_nfalle + pytest (Hook installiert) tools/deploy.py prueft nur den LAUFENDEN Server, nicht den Code restart_server.bat gar nichts Aufgabe OilTradingServer -> start_server_hidden.vbs gar nichts Commit und Deployment sind zwei Ereignisse und fallen beliebig auseinander: eine geaenderte, nie committete Datei ging ueber restart_server.bat ungeprueft live; nach einem Reboot startet die Aufgabe, was auf der Platte liegt. "Der Server kommt hoch" ist kein Ersatz: die beiden log.-NameError in core/trader.py sassen MONATE in except-Zweigen (einer seit dem Initial-Commit), und der Server startete jedes Mal sauber. deploy.py bekommt Schritt 0: check_nfalle + pytest VOR dem Kill, bei Rot Abbruch mit Rueckgabecode 1. Die Reihenfolge ist der Punkt - ein laufender, funktionierender Server darf fuer kaputten Code nicht abgeschossen werden. Notausgang --ohne-pruefung. Beide Richtungen verifiziert: mit eingebautem F821-Fehler -> ABBRUCH, Exit 1, Server-PID vorher/nachher identisch (20892), der Prozess wurde nachweislich nicht angefasst. Ohne Fehler laeuft es durch. BEWUSST NICHT in restart_server.bat und die Aufgabenplanung - das sind WIEDERANLAUF-Pfade. Wuerde ein fehlschlagender Test dort den Start verhindern, staende eine offene Position ohne Trailing, Time-Stop und Notfall-Stop da, nur mit dem Broker-SL. Eine Pruefung, die den Bot offline laesst, ist schlimmer als der Fehler, den sie sucht. Der Batch gibt stattdessen einen Hinweis aus und verweist auf deploy.py. Regel: absichtliches Deployment blockieren, Wiederanlauf niemals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
207 lines
9.0 KiB
Python
207 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Stufe 5 der Pipeline: Server zuverlässig neu starten UND das Ergebnis prüfen.
|
|
|
|
⚠⚠ WARUM ES DIESES SKRIPT GIBT. `restart_server.bat` hat **zweimal still nicht
|
|
neu gestartet** (auch als `Start-Process cmd /c … -WindowStyle Minimized`) — der
|
|
alte Prozess lief weiter, und weil statische Dateien bei jedem Request frisch von
|
|
der Platte gelesen werden, **belegt eine korrekt ausgelieferte `app.js?v=N` GAR
|
|
NICHTS** über den geladenen Python-Code. Der Fehler fiel damals nur auf, weil ein
|
|
NEUES Snapshot-Feld fehlte (`hl_live.basis_stale`).
|
|
|
|
Dieses Skript macht daraus einen prüfbaren Ablauf:
|
|
0. CODE pruefen (check_nfalle + pytest) — VOR dem Kill, damit ein
|
|
laufender Server nicht fuer kaputten Code abgeschossen wird.
|
|
⚠ Ergaenzt 2026-08-07: die Pruefung hing bis dahin am COMMIT, nicht
|
|
am Deployment. Eine nie committete Datei ging ueber
|
|
`restart_server.bat` ungeprueft live.
|
|
1. Prozess auf dem Port gezielt beenden — **nicht** per `*server.py*`-Muster,
|
|
das trifft auch das Hyperliquid-Dashboard auf 8001 (real passiert: der
|
|
HL-Server wurde bei jedem MT5-Neustart still mit-erschlagen und riss die
|
|
Lücken in die Datensammlung).
|
|
2. Neu starten und warten, bis `/api/snapshot` wirklich antwortet.
|
|
3. **Genau EINE** Instanz je Port verifizieren.
|
|
4. Optional `--feld <name>` prüfen: ist dieses Snapshot-Feld da, ist der
|
|
PYTHON-Code nachweislich neu. Das ist der einzige belastbare Beweis.
|
|
5. Das Log seit dem Start auf Fehler durchsehen.
|
|
|
|
Rückgabecode 0 nur, wenn ALLE Schritte durchlaufen — so lässt sich das Skript
|
|
verketten, statt das Ergebnis von Hand zu lesen.
|
|
|
|
Aufruf:
|
|
python tools/deploy.py # Oil-Server (8000) neu starten
|
|
python tools/deploy.py --feld rec_track # zusätzlich Code-Neuheit belegen
|
|
python tools/deploy.py --hl # HL-Dashboard (8001) mitnehmen
|
|
python tools/deploy.py --nur-pruefen # nichts anfassen, nur Zustand
|
|
python tools/deploy.py --ohne-pruefung # Notausgang: Schritt 0 aus
|
|
"""
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
WURZEL = Path(__file__).resolve().parents[1]
|
|
PY = r"C:\Users\ah\AppData\Local\Programs\Python\Python312\python.exe"
|
|
HL = WURZEL.parent / "HyperLiquid-WTI Trader"
|
|
LOG = WURZEL / "oil_widget.log"
|
|
PORT, PORT_HL = 8000, 8001
|
|
START_TIMEOUT_S = 120
|
|
|
|
|
|
def ps(cmd: str) -> str:
|
|
"""PowerShell — für die Prozess-/Port-Abfragen der zuverlässigste Weg hier."""
|
|
r = subprocess.run(["powershell", "-NoProfile", "-Command", cmd],
|
|
capture_output=True, text=True, timeout=60)
|
|
return (r.stdout or "").strip()
|
|
|
|
|
|
def pids(port: int) -> list[int]:
|
|
out = ps(f"(Get-NetTCPConnection -LocalPort {port} -State Listen "
|
|
f"-ErrorAction SilentlyContinue | Select-Object -Expand "
|
|
f"OwningProcess -Unique) -join ','")
|
|
return [int(x) for x in out.split(",") if x.strip().isdigit()]
|
|
|
|
|
|
def snapshot(timeout: float = 5.0) -> dict | None:
|
|
try:
|
|
with urllib.request.urlopen(
|
|
f"http://127.0.0.1:{PORT}/api/snapshot", timeout=timeout) as r:
|
|
return json.loads(r.read().decode())
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def schritt(nr: int, text: str):
|
|
print(f" [{nr}] {text}")
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--feld", help="Snapshot-Feld, das den neuen Code belegt")
|
|
ap.add_argument("--hl", action="store_true", help="HL-Dashboard (8001) mit")
|
|
ap.add_argument("--nur-pruefen", action="store_true")
|
|
ap.add_argument("--ohne-pruefung", action="store_true",
|
|
help="Code-Prüfung überspringen (Notausgang, s. Schritt 0)")
|
|
a = ap.parse_args()
|
|
|
|
print("=" * 74)
|
|
print(" DEPLOY — Stufe 5 (Aktivierung) + Stufe 6 (Verifikation)")
|
|
print("=" * 74)
|
|
fehler: list[str] = []
|
|
|
|
if a.nur_pruefen:
|
|
for p in (PORT, PORT_HL):
|
|
print(f" Port {p}: {pids(p) or 'nichts'}")
|
|
s = snapshot()
|
|
print(f" Snapshot: {'antwortet' if s else 'KEINE Antwort'}")
|
|
return 0 if s else 1
|
|
|
|
# ── 0) CODE prüfen, BEVOR der laufende Server angefasst wird ─────────
|
|
# ⚠⚠ WARUM DAS HIER DAZUKAM (2026-08-07, User-Frage „wird die Pipeline bei
|
|
# jedem Deployment ausgeführt?"). Antwort war NEIN: die Prüfung hing am
|
|
# COMMIT, nicht am Deployment — und das sind zwei Ereignisse, die beliebig
|
|
# auseinanderfallen. Eine geänderte, nie committete Datei ging über
|
|
# `restart_server.bat` ungeprüft live.
|
|
# ⚠ Dass der Server hochkommt, ist KEIN Ersatz: die beiden `log.`-NameError
|
|
# in `core/trader.py` sassen MONATE in `except`-Zweigen (einer seit dem
|
|
# Initial-Commit), und der Server startete jedes Mal sauber. Genau solche
|
|
# Fehler findet `pytest`/`ruff`, nicht der Startvorgang.
|
|
# ⚠ Die Prüfung läuft VOR dem Kill — ein laufender, funktionierender Server
|
|
# darf nicht für kaputten Code abgeschossen werden.
|
|
if not a.ohne_pruefung:
|
|
schritt(0, "Code prüfen (check_nfalle + pytest) …")
|
|
r = subprocess.run([PY, "-X", "utf8", str(WURZEL / "tools" / "pre_commit.py")],
|
|
cwd=WURZEL, capture_output=True, text=True)
|
|
for z in (r.stdout or "").strip().splitlines():
|
|
print(f" {z}")
|
|
if r.returncode != 0:
|
|
print(" ❌ ABBRUCH — der laufende Server bleibt unangetastet.")
|
|
print(" Beheben, oder bewusst übergehen: --ohne-pruefung")
|
|
return 1
|
|
else:
|
|
print(" [0] Code-Prüfung ÜBERSPRUNGEN (--ohne-pruefung) ⚠")
|
|
|
|
# ── 1) gezielt beenden ───────────────────────────────────────────────
|
|
alt = pids(PORT)
|
|
schritt(1, f"Port {PORT}: {alt or 'nichts'} → beenden")
|
|
for pid in alt:
|
|
ps(f"Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue")
|
|
if alt:
|
|
time.sleep(3)
|
|
if a.hl and HL.exists():
|
|
for pid in pids(PORT_HL):
|
|
ps(f"Stop-Process -Id {pid} -Force -ErrorAction SilentlyContinue")
|
|
time.sleep(2)
|
|
ps(f"Start-Process -FilePath '{PY}' -ArgumentList '-X','utf8','server.py' "
|
|
f"-WorkingDirectory '{HL}' -WindowStyle Hidden")
|
|
schritt(1, f"HL-Dashboard ({PORT_HL}) neu gestartet")
|
|
|
|
log_ab = LOG.stat().st_size if LOG.exists() else 0
|
|
|
|
# ── 2) starten und auf echte Antwort warten ──────────────────────────
|
|
schritt(2, "Server starten …")
|
|
ps(f"Start-Process -FilePath '{PY}' -ArgumentList '-X','utf8','server.py' "
|
|
f"-WorkingDirectory '{WURZEL}' -WindowStyle Minimized")
|
|
t0 = time.time()
|
|
snap = None
|
|
while time.time() - t0 < START_TIMEOUT_S:
|
|
time.sleep(3)
|
|
snap = snapshot()
|
|
if snap:
|
|
break
|
|
if not snap:
|
|
print(f" ❌ /api/snapshot antwortet nach {START_TIMEOUT_S} s nicht.")
|
|
return 1
|
|
schritt(2, f"läuft nach {time.time() - t0:.0f} s")
|
|
|
|
# ── 3) genau EINE Instanz ────────────────────────────────────────────
|
|
for p, noetig in ((PORT, True), (PORT_HL, a.hl)):
|
|
ids = pids(p)
|
|
if not noetig and not ids:
|
|
continue
|
|
if len(ids) == 1:
|
|
schritt(3, f"Port {p}: genau eine Instanz (PID {ids[0]}) ✅")
|
|
else:
|
|
fehler.append(f"Port {p}: {len(ids)} Instanzen {ids}")
|
|
schritt(3, f"Port {p}: ⚠ {len(ids)} Instanzen {ids}")
|
|
|
|
# ── 4) beweist ein neues Feld, dass der PYTHON-Code neu ist? ─────────
|
|
if a.feld:
|
|
if a.feld in snap:
|
|
schritt(4, f"Feld `{a.feld}` vorhanden → Python-Code ist neu ✅")
|
|
else:
|
|
fehler.append(f"Feld `{a.feld}` fehlt im Snapshot")
|
|
schritt(4, f"⚠ Feld `{a.feld}` FEHLT — der alte Code läuft weiter!")
|
|
else:
|
|
schritt(4, "kein --feld angegeben ⚠ ohne das ist NICHT belegt, dass der "
|
|
"Python-Code neu ist (statische Dateien beweisen nichts)")
|
|
|
|
# ── 5) Log seit dem Start ────────────────────────────────────────────
|
|
treffer = []
|
|
if LOG.exists():
|
|
with LOG.open(encoding="utf-8", errors="replace") as fh:
|
|
fh.seek(log_ab)
|
|
for z in fh:
|
|
if any(w in z for w in ("ERROR", "Traceback", "CRITICAL")):
|
|
treffer.append(z.rstrip()[:130])
|
|
if treffer:
|
|
fehler.append(f"{len(treffer)} Fehlerzeile(n) im Log")
|
|
schritt(5, f"⚠ {len(treffer)} Fehlerzeile(n) seit dem Start:")
|
|
for z in treffer[:5]:
|
|
print(f" {z}")
|
|
else:
|
|
schritt(5, "Log seit dem Start ohne ERROR/Traceback ✅")
|
|
|
|
print("=" * 74)
|
|
if fehler:
|
|
print(" ERGEBNIS: ❌ " + " · ".join(fehler))
|
|
return 1
|
|
print(" ERGEBNIS: ✅ neu gestartet und verifiziert")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|