feat(runserver): Selbst-supervidierenden Dev-Server, -D-Parameter funktional

- Supervisor-Modus im Dev-Betrieb: Der Server läuft als Subprozess und
  wird nach einem Abbruch neu gestartet. Der Werkzeug-Reloader beendet
  sich sonst komplett, wenn der App-Prozess beim Start crasht — als
  Hauptprozess des Containers würde damit der gesamte Dev-Container
  stoppen. Der Supervisor importiert die App nicht und überlebt so
  jeden Anwendungsfehler.
- -D-Parameter ist jetzt funktional: Dev-Start direkt von der
  Befehlszeile ohne Devcontainer/Env-Variable. Debug-Entscheidung:
  -D ODER APP_DEBUG.
- approot.py: init_app(debug) reicht die Debug-Entscheidung durch,
  damit Server-Typ und JS-Bundle-Auswahl (.js/.min.js) konsistent
  bleiben. wsgi.py (init_app() ohne Argument) unverändert.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-22 19:12:27 +02:00
1 parent 4e721e9a47
commit 24b492c5c1
2 files changed
+102 -21

No files matched your search

+94 -19
View File
@@ -1,23 +1,98 @@
from waitress import serve
from werkzeug import serving
from approot import init_app
import os
import signal
import subprocess
import sys
import time
def run_server():
try:
if app.config["DEBUG"] is False:
_server = "waitress"
# serve(app, listen='nginx-proxy:8080') # Funktioniert aus unbekannten Gründen nicht. Der Hostname wird im Docker Container nicht korrekt aufgelöst
print(f"Server startet mit Server type: {_server}", flush=True)
serve(app, listen='*:8080')
else:
_server = "werkzeug"
print(f"Server startet mit Server type: {_server}", flush=True)
serving.run_simple("0.0.0.0", 8080, app, use_reloader=True)
except Exception as ex:
print(str(ex))
print(f"Server gestartet. Server type: {_server}", flush=True)
def _is_debug_requested() -> bool:
"""
True, wenn -D übergeben wurde oder APP_DEBUG gesetzt ist.
-D erlaubt den Dev-Start direkt von der Befehlszeile — ohne Devcontainer
und ohne Env-Variable setzen zu müssen. APP_DEBUG steuert den
Devcontainer-Betrieb (docker-compose.dev.yaml).
"""
if "-D" in sys.argv[1:]:
return True
return os.environ.get("APP_DEBUG", "false").lower() in ("true", "1", "yes")
def run_server(debug: bool):
"""
Startet den Server. Prod: Waitress. Dev: Werkzeug mit Reloader.
Der Aufruf erfolgt entweder direkt (Prod) oder als supervidierter
Subprozess (Dev, siehe run_dev_supervised).
debug wird an init_app() durchgereicht, damit app.config["DEBUG"]
konsistent ist — davon hängt außer dem Server-Typ auch die Auswahl
des JS-Bundles ab (routes/general.py: .js im Dev, .min.js in Prod).
"""
from waitress import serve
from werkzeug import serving
from approot import init_app
app = init_app(debug=debug)
if not debug:
print("Server startet mit Server type: waitress", flush=True)
serve(app, listen='*:8080')
else:
print("Server startet mit Server type: werkzeug", flush=True)
serving.run_simple("0.0.0.0", 8080, app, use_reloader=True)
def run_dev_supervised():
"""
Crash-toleranter Dev-Betrieb.
Der Werkzeug-Reloader beendet sich komplett, wenn der App-Prozess beim
Start crasht (z.B. SQLAlchemy-Fehler beim Import der Modelle). Wäre der
Server direkt der Hauptprozess des Containers, würde damit der gesamte
Dev-Container stoppen. Der Supervisor startet den Server daher als
Subprozess und nach einem Abbruch einfach neu — sobald die fehlerhafte
Datei korrigiert ist, läuft der Server automatisch wieder an.
Der Supervisor importiert die Anwendung absichtlich nicht
(run_server importiert approot erst im Subprozess): Ein defekter
Anwendungscode kann ihn nicht mitreißen.
"""
child = None
def _forward_signal(signum, frame):
# SIGTERM/SIGINT an den Subprozess weiterleiten, damit der
# Container sauber heruntergefahren werden kann.
if child is not None and child.poll() is None:
child.terminate()
sys.exit(0)
signal.signal(signal.SIGTERM, _forward_signal)
signal.signal(signal.SIGINT, _forward_signal)
env = dict(os.environ)
env["KBP_DEV_SUPERVISED"] = "true"
while True:
print("=== Supervisor: starte Dev-Server ===", flush=True)
# sys.argv[1:] durchreichen, damit -D (und künftige Parameter)
# im Subprozess ebenfalls wirksam sind.
proc = subprocess.Popen(
[sys.executable, os.path.abspath(__file__)] + sys.argv[1:],
env=env,
)
child = proc
code = proc.wait()
print(f"=== Dev-Server beendet (Exit-Code {code}) — Neustart in 2 Sekunden ===",
flush=True)
time.sleep(2)
if __name__ == '__main__':
app = init_app()
run_server()
_debug = _is_debug_requested()
if _debug and os.environ.get("KBP_DEV_SUPERVISED") != "true":
# Dev-Modus und noch nicht beaufsichtigt: Supervisor werden
# (im Devcontainer ist das PID 1 und hält den Container am Leben)
run_dev_supervised()
else:
# Prod-Modus, oder wir sind bereits der beaufsichtigte
# Dev-Subprozess (der Supervisor setzt KBP_DEV_SUPERVISED):
# als eigentlicher Server starten.
run_server(_debug)