diff --git a/App_Server/root/approot.py b/App_Server/root/approot.py index da9961e..895cdb1 100644 --- a/App_Server/root/approot.py +++ b/App_Server/root/approot.py @@ -9,7 +9,7 @@ import os login_manager = LoginManager() -def init_app(): +def init_app(debug: bool | None = None): app = Flask(__name__) config: Configuration = Configuration("/usr/opt/pythonapp/App_Server/root/config.ini") @@ -20,7 +20,13 @@ def init_app(): "auth": "sqlite:////usr/var/pythonappdb/auth.db", "appdb": "sqlite:////usr/var/pythonappdb/{}.db".format(_projektname) } - app.config["DEBUG"] = os.environ.get("APP_DEBUG", "false").lower() in ("true", "1", "yes") + # Dev-Modus: CLI-Parameter -D (via runserver) oder APP_DEBUG-Env. + # debug=None = wie bisher allein über die Env-Variable entscheiden + # (unverändertes Verhalten für wsgi.py). + if debug is None: + app.config["DEBUG"] = os.environ.get("APP_DEBUG", "false").lower() in ("true", "1", "yes") + else: + app.config["DEBUG"] = debug app.config["PROJEKTNAME"] = config.app_projectname app.config["APPID"] = config.app_appid app.config["APPVERSION"] = config.clientapp_version diff --git a/App_Server/root/runserver.py b/App_Server/root/runserver.py index 5f072bf..98357a4 100644 --- a/App_Server/root/runserver.py +++ b/App_Server/root/runserver.py @@ -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() \ No newline at end of file + _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) \ No newline at end of file