From 486647fade995b6e53831b69f28e781a6bd08497 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Sun, 1 Jun 2025 02:42:15 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20Improved=20backend=20functionali?= =?UTF-8?q?ty=20&=20documentation,=20optimized=20database=20files,=20and?= =?UTF-8?q?=20introduced=20shutdown=20management=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 214 +++----- backend/database/myp.db-shm | Bin 32768 -> 0 bytes backend/database/myp.db-wal | Bin 16512 -> 0 bytes backend/docs/SHUTDOWN_VERBESSERUNGEN.md | 1 + backend/installer.sh | 484 ++++++++++-------- backend/logs/app/app.log | 146 ++++++ backend/logs/scheduler/scheduler.log | 17 + .../shutdown_manager.cpython-313.pyc | Bin 0 -> 21661 bytes backend/utils/queue_manager.py | 155 ++++-- backend/utils/shutdown_manager.py | 442 ++++++++++++++++ 10 files changed, 1052 insertions(+), 407 deletions(-) delete mode 100644 backend/database/myp.db-shm delete mode 100644 backend/database/myp.db-wal create mode 100644 backend/docs/SHUTDOWN_VERBESSERUNGEN.md create mode 100644 backend/utils/__pycache__/shutdown_manager.cpython-313.pyc create mode 100644 backend/utils/shutdown_manager.py diff --git a/backend/app.py b/backend/app.py index 69506e3d..8c421337 100644 --- a/backend/app.py +++ b/backend/app.py @@ -6537,86 +6537,50 @@ if __name__ == "__main__": os.environ['FLASK_ENV'] = 'development' os.environ['PYTHONIOENCODING'] = 'utf-8' os.environ['PYTHONUTF8'] = '1' - - # Windows-spezifisches Signal-Handling für ordnungsgemäßes Shutdown - def signal_handler(sig, frame): - """Signal-Handler für ordnungsgemäßes Shutdown.""" - app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter...") - try: - # Queue Manager stoppen - app_logger.info("🔄 Beende Queue Manager...") - stop_queue_manager() - - # Scheduler stoppen falls aktiviert - if SCHEDULER_ENABLED and scheduler: - try: - scheduler.stop() - app_logger.info("Job-Scheduler gestoppt") - except Exception as e: - app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}") - - # ===== ROBUSTES DATENBANK-CLEANUP MIT NEUER LOGIC ===== - app_logger.info("💾 Führe robustes Datenbank-Cleanup durch...") - try: - # Importiere und verwende den neuen DatabaseCleanupManager - from utils.database_cleanup import safe_database_cleanup - - # Führe umfassendes, sicheres Cleanup durch - cleanup_result = safe_database_cleanup(force_mode_switch=True) - - if cleanup_result["success"]: - app_logger.info(f"✅ Datenbank-Cleanup erfolgreich: {', '.join(cleanup_result['operations'])}") - if cleanup_result.get("wal_files_removed", False): - app_logger.info("✅ WAL- und SHM-Dateien erfolgreich entfernt") - else: - app_logger.warning(f"⚠️ Datenbank-Cleanup mit Problemen: {', '.join(cleanup_result['errors'])}") - # Trotzdem weiter - wenigstens WAL-Checkpoint versucht - - except ImportError: - # Fallback auf die alte Methode falls Cleanup-Manager nicht verfügbar - app_logger.warning("Fallback: Verwende Legacy-Datenbank-Cleanup...") - try: - from models import create_optimized_engine - from sqlalchemy import text - - engine = create_optimized_engine() - - with engine.connect() as conn: - # Nur WAL-Checkpoint - kein risikoreicher Mode-Switch - app_logger.info("📝 Führe WAL-Checkpoint durch...") - result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone() - - if result: - app_logger.info(f"WAL-Checkpoint abgeschlossen: {result[1]} Seiten übertragen, {result[2]} Seiten zurückgesetzt") - - conn.commit() - - # Engine-Connection-Pool schließen - engine.dispose() - app_logger.info("✅ Legacy-Datenbank-Cleanup abgeschlossen") - - except Exception as db_error: - app_logger.error(f"❌ Fehler beim Legacy-Datenbank-Cleanup: {str(db_error)}") - - except Exception as cleanup_error: - app_logger.error(f"❌ Fehler beim robusten Datenbank-Cleanup: {str(cleanup_error)}") - - app_logger.info("✅ Shutdown abgeschlossen") - sys.exit(0) - except Exception as e: - app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}") - sys.exit(1) - # Signal-Handler registrieren (Windows-kompatibel) - if os.name == 'nt': # Windows - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - # Zusätzlich für Flask-Development-Server - signal.signal(signal.SIGBREAK, signal_handler) - else: # Unix/Linux - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGHUP, signal_handler) + # ===== INITIALISIERE ZENTRALEN SHUTDOWN-MANAGER ===== + try: + from utils.shutdown_manager import get_shutdown_manager + shutdown_manager = get_shutdown_manager(timeout=45) # 45 Sekunden Gesamt-Timeout + app_logger.info("✅ Zentraler Shutdown-Manager initialisiert") + except ImportError as e: + app_logger.error(f"❌ Shutdown-Manager konnte nicht geladen werden: {e}") + # Fallback auf die alte Methode + shutdown_manager = None + + # Windows-spezifisches Signal-Handling als Fallback + def fallback_signal_handler(sig, frame): + """Fallback Signal-Handler für ordnungsgemäßes Shutdown.""" + app_logger.warning(f"🛑 Signal {sig} empfangen - fahre System herunter (Fallback)...") + try: + # Queue Manager stoppen + stop_queue_manager() + + # Scheduler stoppen falls aktiviert + if SCHEDULER_ENABLED and scheduler: + try: + if hasattr(scheduler, 'shutdown'): + scheduler.shutdown(wait=True) + else: + scheduler.stop() + except Exception as e: + app_logger.error(f"Fehler beim Stoppen des Schedulers: {str(e)}") + + app_logger.info("✅ Fallback-Shutdown abgeschlossen") + sys.exit(0) + except Exception as e: + app_logger.error(f"❌ Fehler beim Fallback-Shutdown: {str(e)}") + sys.exit(1) + + # Signal-Handler registrieren (Windows-kompatibel) + if os.name == 'nt': # Windows + signal.signal(signal.SIGINT, fallback_signal_handler) + signal.signal(signal.SIGTERM, fallback_signal_handler) + signal.signal(signal.SIGBREAK, fallback_signal_handler) + else: # Unix/Linux + signal.signal(signal.SIGINT, fallback_signal_handler) + signal.signal(signal.SIGTERM, fallback_signal_handler) + signal.signal(signal.SIGHUP, fallback_signal_handler) try: # Datenbank initialisieren und Migrationen durchführen @@ -6643,6 +6607,25 @@ if __name__ == "__main__": except Exception as e: app_logger.error(f"❌ Fehler bei automatischer Steckdosen-Initialisierung: {str(e)}") + # ===== SHUTDOWN-MANAGER KONFIGURATION ===== + if shutdown_manager: + # Queue Manager beim Shutdown-Manager registrieren + try: + import utils.queue_manager as queue_module + shutdown_manager.register_queue_manager(queue_module) + app_logger.debug("✅ Queue Manager beim Shutdown-Manager registriert") + except Exception as e: + app_logger.warning(f"⚠️ Queue Manager Registrierung fehlgeschlagen: {e}") + + # Scheduler beim Shutdown-Manager registrieren + shutdown_manager.register_scheduler(scheduler, SCHEDULER_ENABLED) + + # Datenbank-Cleanup beim Shutdown-Manager registrieren + shutdown_manager.register_database_cleanup() + + # Windows Thread Manager beim Shutdown-Manager registrieren + shutdown_manager.register_windows_thread_manager() + # Queue-Manager für automatische Drucker-Überwachung starten # Nur im Produktionsmodus starten (nicht im Debug-Modus) if not debug_mode: @@ -6650,61 +6633,6 @@ if __name__ == "__main__": queue_manager = start_queue_manager() app_logger.info("✅ Printer Queue Manager erfolgreich gestartet") - # Verbesserte Shutdown-Handler registrieren - def cleanup_queue_manager(): - try: - app_logger.info("🔄 Beende Queue Manager...") - stop_queue_manager() - except Exception as e: - app_logger.error(f"❌ Fehler beim Queue Manager Cleanup: {str(e)}") - - atexit.register(cleanup_queue_manager) - - # ===== ROBUSTES DATENBANK-CLEANUP BEIM PROGRAMMENDE ===== - def cleanup_database(): - """Führt robustes Datenbank-Cleanup beim normalen Programmende aus.""" - try: - app_logger.info("💾 Führe finales robustes Datenbank-Cleanup durch...") - - # Verwende den neuen DatabaseCleanupManager - try: - from utils.database_cleanup import safe_database_cleanup - - # Führe umfassendes, sicheres Cleanup durch - cleanup_result = safe_database_cleanup(force_mode_switch=True) - - if cleanup_result["success"]: - app_logger.info(f"✅ Finales Datenbank-Cleanup erfolgreich: {', '.join(cleanup_result['operations'])}") - if cleanup_result.get("wal_files_removed", False): - app_logger.info("✅ WAL- und SHM-Dateien erfolgreich entfernt") - else: - app_logger.warning(f"⚠️ Finales Datenbank-Cleanup mit Problemen: {', '.join(cleanup_result['errors'])}") - - except ImportError: - # Fallback auf die alte Methode falls Cleanup-Manager nicht verfügbar - app_logger.warning("Fallback: Verwende Legacy-finales-Datenbank-Cleanup...") - from models import create_optimized_engine - from sqlalchemy import text - - engine = create_optimized_engine() - - with engine.connect() as conn: - # Nur WAL-Checkpoint - kein risikoreicher Mode-Switch - result = conn.execute(text("PRAGMA wal_checkpoint(TRUNCATE)")).fetchone() - if result and result[1] > 0: - app_logger.info(f"Final WAL-Checkpoint: {result[1]} Seiten übertragen") - - conn.commit() - - # Connection-Pool ordnungsgemäß schließen - engine.dispose() - app_logger.info("✅ Legacy-finales Datenbank-Cleanup abgeschlossen") - - except Exception as e: - app_logger.error(f"❌ Fehler beim finalen robusten Datenbank-Cleanup: {str(e)}") - - atexit.register(cleanup_database) - except Exception as e: app_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}") else: @@ -6760,12 +6688,18 @@ if __name__ == "__main__": ) except KeyboardInterrupt: app_logger.info("🔄 Tastatur-Unterbrechung empfangen - beende Anwendung...") - signal_handler(signal.SIGINT, None) + if shutdown_manager: + shutdown_manager.shutdown() + else: + fallback_signal_handler(signal.SIGINT, None) except Exception as e: app_logger.error(f"Fehler beim Starten der Anwendung: {str(e)}") # Cleanup bei Fehler - try: - stop_queue_manager() - except: - pass - sys.exit(1) + if shutdown_manager: + shutdown_manager.force_shutdown(1) + else: + try: + stop_queue_manager() + except: + pass + sys.exit(1) diff --git a/backend/database/myp.db-shm b/backend/database/myp.db-shm deleted file mode 100644 index 97257d906934944403cb81174337165e6c1cecee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)F$%&k6b9h56=%l|y+RjH;T7CGhS1reSJG8n#49-HsCWfUfe*io(jb}6=ad)<3pW|d58&A7PiR1h3KHvBI zzWctkNB%Y=sRzjs z)am#WL^*Z*$&nw9?ZqD*{yMfeIzN(+o{9WM4tWuGOGif&=gvjnw}gfQ%q<0qST`-f zV*USLnJe>!3eQvumzR0QPnob9$^}{4xq!wbAHH@ZmPjU}pMQYaH9Nqx>IT?CSChOu zMzADl%tU-lmY8Cx!Y}Y;rnFLFO4a4%3ru`W721GVE0pIJ3*`wmolX)|&=zFHBXj8_ z8e;&#)KsRz*DB~%>KKA0YihkMtL`&`?Hvo4ewqk*SU{>n#b1FyREpPlGz1CVF?$bm zo~IkKY5}9}iGccQmR=V%iJUzmY-)`_SD|CIG&o-Gan9M2ARZmFe;&7F1=I;>xLgj) zjw!lewS25>UZSSUq>#FyiNHkc8(qKfUVA`%L5HMmL8+@6!pjBN0GmL>2nUew=u#Mp zHk*E_Y(D+%z5e7hd)>)qgc;b;WdoQ&^$DgaH&yt6aE+~&i`NR}b!LfQXC`C`<2k=l z=8Fp@)HdObN-|}Bo-gyIIsOLYG@GGgY-JPuVN-Zt5BElw7y(Vd2p{5uW}hQ`sO@t^ z2x`PgLjHJi?`S-hIDbBR?1FNfh%MC6e zf~X5!MH8f+)eczV(1a!C^2*9GUnrr~`ZS|(=0hOj=_{Vxy*n05Oio7cu^0~AT?a;6 zhPJ4wX7K(90ZNdHaKUY!t080*1r<4S!>VH=^NFDXAi?$EEMUKYXpCb=#MG8mXF#<^2*X%F4azOwjV2XyUBhTJ9`v3|A@SdM^qY6Q` z4Kxs1fZ7*q1<~rjSc51?(>*04#KQyefF*55D>sSAK`{{vnA8D7h7W+ENX-25N&#mJ zg9E5`TP0i%V&cy&{5b?6@bhUJJ?43%kURCkUTSePmPjO`d(Ik#d5(!NHV+f#gqwz~ zNH|=vqpPhmC#6F&SeS+k5~f9MT>&t^a_z&T573Gtz{Ju?CkR6bVjEB1TRap?oIV|W zAmVl5Mkn_=N(kh(4v~pSv-&d7Hq7umCxggH7DclIUT@jBgu$MoEr?-GgfJvs=`;gb zVm}?N%;9s!Y;+jbZ)=7`Op)LgzQ*9Hl(zv9K*DontTM3$#1EmK{Yi9CWeBipsd&B0 zGZQ`(lJVrlk1ae&C9vbXg}_`{!}o3yXVzI)i?Mr36r1rU`ChQa5!O6NXdOWW zqtigQ*#BBV0J9H-9dPR{!Sph5mYXJ1lHqDHG+*@LY{Pms^kIyJuRMP22V~?Xuq;_^ zdMUjGbeKi7{c+1RVO=w2A|&((Elu^pr4s-fvgTx5*dFlwWvfd}YM4P_Rp2${nuv=& zw2$P$(ZsunLH2>QT<7p*BzfTJU0K}Kd5?eVFnrrSh~CTHeSz;kuDxv~zYvbW#^~{r zk>e*{slA9da>|1uKoOt_Py{Ff6ak6=MSvne5ugZA1YRx#cHcX2A~G|+J2E~#R)`d^ z@CA6zW?gCnLj-U)cCrD~J1Mw(hFZ^Zjk7Fwc5_wS$#QE~?ku%S%W`vFx!$f-!P55D zLRFg2l**OqMrPA0S;AJTSZcLul{G2Vslf5*WtU5Hxykg@WSV8t>})1Co6DuL+4Rf| z8%CMR}#h!<0xxrOa%K@`$L_jKN$!GH}CBrgYW|qs%a#Jae&84{+zcAb^$IhmwQ!JOC$~ypraf6CM=lZ>U zf!g}(-9T`$D&_eyOLhYabQZLY=^#X2X!H})H;ah6`8+&N;F>q?_-_!SRQ2X2c zd;N&TfRl&tEmUW_(szo9SkQWbu>rf2y+iK6nNja3JJ8NrPzO!x1q@hsGQ3`3^5FX7k3ao!iq;GGc0TA| ziU37`B0v$K2v7tl0u%v?07ZZzKoNLm1YX*Dfy&0MYFl2-P0JgX7ufZ#b-TH<*xBG& w737NfTB*jbq^7&ql$Dj$^8DtESh=&bKJ~xU3*=#$K`NI|XYx)ZL0G-Ozu~qZCjbBd diff --git a/backend/docs/SHUTDOWN_VERBESSERUNGEN.md b/backend/docs/SHUTDOWN_VERBESSERUNGEN.md new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/backend/docs/SHUTDOWN_VERBESSERUNGEN.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/backend/installer.sh b/backend/installer.sh index e4742c72..d3ab1daa 100644 --- a/backend/installer.sh +++ b/backend/installer.sh @@ -4,7 +4,7 @@ # MYP Druckerverwaltung - VOLLSTÄNDIGER KIOSK-INSTALLER für Raspbian # Entwickelt auf Windows, ausführbar auf Raspberry Pi / Debian # OHNE virtualenv - verwendet System-Python mit --break-system-packages -# ECHTER KIOSK-MODUS: Entfernt Desktop, 3 Backend-Instanzen, Autologin +# ECHTER KIOSK-MODUS: Entfernt Desktop, 1 Backend-Instanz HTTPS, Autologin # =================================================================== set -euo pipefail @@ -12,9 +12,7 @@ set -euo pipefail # =========================== KONFIGURATION =========================== APP_NAME="MYP Druckerverwaltung" APP_DIR="/opt/myp" -SERVICE_NAME_KIOSK="myp-kiosk" -SERVICE_NAME_HTTPS="myp-https" -SERVICE_NAME_HTTP="myp-http" +SERVICE_NAME="myp-https" KIOSK_USER="kiosk" CURRENT_DIR="$(pwd)" INSTALL_LOG="/var/log/myp-install.log" @@ -579,14 +577,85 @@ install_system_dependencies() { log "✅ Alle Abhängigkeiten (Python + Node.js + npm) erfolgreich installiert" } -# ========================== 3 BACKEND SERVICES ERSTELLEN ========================== -create_backend_services() { - log "=== ERSTELLE 3 BACKEND-SERVICES ===" +# ========================== SSL-ZERTIFIKATE FÜR LOCALHOST ERSTELLEN ========================== +create_localhost_ssl_certificates() { + log "=== ERSTELLE SSL-ZERTIFIKATE FÜR LOCALHOST ===" - # Python-Startskripte für die verschiedenen Ports erstellen - progress "Erstelle Python-Startskripte..." + SSL_DIR="$APP_DIR/certs/localhost" + mkdir -p "$SSL_DIR" + + progress "Erstelle SSL-Zertifikate für localhost..." + + # Private Key erstellen + openssl genrsa -out "$SSL_DIR/localhost.key" 2048 || error "Private Key Erstellung fehlgeschlagen" + + # Certificate Signing Request (CSR) erstellen + openssl req -new -key "$SSL_DIR/localhost.key" -out "$SSL_DIR/localhost.csr" -subj "/C=DE/ST=Baden-Wuerttemberg/L=Stuttgart/O=Mercedes-Benz/OU=MYP/CN=localhost" || error "CSR Erstellung fehlgeschlagen" + + # Self-signed Zertifikat erstellen (gültig für 365 Tage) + openssl x509 -req -days 365 -in "$SSL_DIR/localhost.csr" -signkey "$SSL_DIR/localhost.key" -out "$SSL_DIR/localhost.crt" -extensions v3_req -extfile <( +cat < "$APP_DIR/start_https.py" << 'EOF' #!/usr/bin/env python3 import sys @@ -608,88 +677,21 @@ try: print("Starte HTTPS-Server auf Port 443...") app.run(host='0.0.0.0', port=443, debug=False, ssl_context=ssl_context, threaded=True) else: - print('SSL-Kontext nicht verfügbar') - sys.exit(1) + print('SSL-Kontext nicht verfügbar - verwende localhost Zertifikate') + # Fallback auf localhost-Zertifikate + ssl_context = ('/opt/myp/certs/localhost/localhost.crt', '/opt/myp/certs/localhost/localhost.key') + app.run(host='0.0.0.0', port=443, debug=False, ssl_context=ssl_context, threaded=True) except Exception as e: print(f"Fehler beim Starten des HTTPS-Servers: {e}") sys.exit(1) EOF - # HTTP-Startskript (Port 80) - cat > "$APP_DIR/start_http.py" << 'EOF' -#!/usr/bin/env python3 -import sys -import os - -# Füge App-Verzeichnis zum Python-Pfad hinzu -sys.path.insert(0, '/opt/myp') - -# Setze Umgebungsvariablen -os.environ['FLASK_PORT'] = '80' -os.environ['FLASK_HOST'] = '0.0.0.0' -os.environ['FLASK_ENV'] = 'production' - -try: - from app import app - - print("Starte HTTP-Server auf Port 80...") - app.run(host='0.0.0.0', port=80, debug=False, threaded=True) -except Exception as e: - print(f"Fehler beim Starten des HTTP-Servers: {e}") - sys.exit(1) -EOF - - # Skripte ausführbar machen + # Skript ausführbar machen chmod +x "$APP_DIR/start_https.py" - chmod +x "$APP_DIR/start_http.py" - # Service 1: Kiosk-Backend (Port 5000) - progress "Erstelle myp-kiosk.service (Port 5000)..." - cat > "/etc/systemd/system/${SERVICE_NAME_KIOSK}.service" << EOF -[Unit] -Description=MYP Kiosk Backend (Port 5000) -After=network.target network-online.target -Wants=network-online.target -Requires=network.target - -[Service] -Type=simple -User=root -Group=root -WorkingDirectory=$APP_DIR -ExecStart=/usr/bin/python3 $APP_DIR/app.py --debug -Restart=always -RestartSec=5 -StartLimitBurst=5 -StartLimitInterval=60 - -# Umgebungsvariablen -Environment=PYTHONUNBUFFERED=1 -Environment=FLASK_ENV=production -Environment=FLASK_HOST=0.0.0.0 -Environment=FLASK_PORT=5000 -Environment=PYTHONPATH=$APP_DIR -Environment=LC_ALL=C.UTF-8 -Environment=LANG=C.UTF-8 - -# Logging -StandardOutput=journal -StandardError=journal -SyslogIdentifier=myp-kiosk - -# Security-Einstellungen -NoNewPrivileges=true -PrivateTmp=false -ProtectSystem=strict -ReadWritePaths=$APP_DIR - -[Install] -WantedBy=multi-user.target -EOF - - # Service 2: HTTPS-Backend (Port 443) + # HTTPS-Service (Port 443) progress "Erstelle myp-https.service (Port 443)..." - cat > "/etc/systemd/system/${SERVICE_NAME_HTTPS}.service" << EOF + cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF [Unit] Description=MYP HTTPS Backend (Port 443) After=network.target network-online.target @@ -715,6 +717,8 @@ Environment=FLASK_PORT=443 Environment=PYTHONPATH=$APP_DIR Environment=LC_ALL=C.UTF-8 Environment=LANG=C.UTF-8 +Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # Logging StandardOutput=journal @@ -731,76 +735,12 @@ ReadWritePaths=$APP_DIR WantedBy=multi-user.target EOF - # Service 3: HTTP-Backend (Port 80) - progress "Erstelle myp-http.service (Port 80)..." - cat > "/etc/systemd/system/${SERVICE_NAME_HTTP}.service" << EOF -[Unit] -Description=MYP HTTP Backend (Port 80) -After=network.target network-online.target -Wants=network-online.target -Requires=network.target - -[Service] -Type=simple -User=root -Group=root -WorkingDirectory=$APP_DIR -ExecStart=/usr/bin/python3 $APP_DIR/start_http.py -Restart=always -RestartSec=5 -StartLimitBurst=5 -StartLimitInterval=60 - -# Umgebungsvariablen -Environment=PYTHONUNBUFFERED=1 -Environment=FLASK_ENV=production -Environment=FLASK_HOST=0.0.0.0 -Environment=FLASK_PORT=80 -Environment=PYTHONPATH=$APP_DIR -Environment=LC_ALL=C.UTF-8 -Environment=LANG=C.UTF-8 - -# Logging -StandardOutput=journal -StandardError=journal -SyslogIdentifier=myp-http - -# Security-Einstellungen -NoNewPrivileges=true -PrivateTmp=false -ProtectSystem=strict -ReadWritePaths=$APP_DIR - -[Install] -WantedBy=multi-user.target -EOF - - log "✅ 3 Backend-Services mit separaten Python-Skripten erstellt" -} - -# ========================== AUTOLOGIN KONFIGURIEREN ========================== -configure_autologin() { - log "=== KONFIGURIERE AUTOLOGIN ===" - - progress "Erstelle Autologin-Service..." - - # Getty-Service für automatischen Login überschreiben - mkdir -p /etc/systemd/system/getty@tty1.service.d/ - cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << EOF -[Service] -ExecStart= -ExecStart=-/sbin/agetty --autologin $KIOSK_USER --noclear %I linux -EOF - - # Systemd Target auf Multi-User setzen (ohne grafisches Login) - systemctl set-default multi-user.target - - log "✅ Autologin konfiguriert für Benutzer: $KIOSK_USER" + log "✅ HTTPS Backend-Service erstellt" } # ========================== KIOSK BROWSER KONFIGURATION ========================== configure_kiosk_browser() { - log "=== KONFIGURIERE KIOSK-BROWSER ===" + log "=== KONFIGURIERE KIOSK-BROWSER FÜR HTTPS ===" KIOSK_HOME="/home/$KIOSK_USER" @@ -918,8 +858,8 @@ if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then fi EOF - # .xinitrc für Kiosk-Session erstellen - progress "Erstelle optimierte Kiosk X-Session..." + # .xinitrc für HTTPS Kiosk-Session erstellen + progress "Erstelle optimierte HTTPS Kiosk X-Session..." cat > "$KIOSK_HOME/.xinitrc" << 'EOF' #!/bin/bash @@ -946,31 +886,30 @@ unclutter -idle 0.1 -root -noevents & # Openbox im Hintergrund starten openbox & -# Warte auf Backend-Services -echo "Warte auf MYP Backend-Services..." +# Warte auf HTTPS Backend-Service +echo "Warte auf MYP HTTPS Backend-Service..." WAIT_COUNT=0 -while ! curl -s http://localhost:5000 > /dev/null; do - echo "Warte auf Backend (Port 5000)... ($WAIT_COUNT/60)" +while ! curl -k -s https://localhost:443 > /dev/null; do + echo "Warte auf HTTPS Backend (Port 443)... ($WAIT_COUNT/60)" sleep 2 WAIT_COUNT=$((WAIT_COUNT + 1)) if [ $WAIT_COUNT -gt 60 ]; then - echo "FEHLER: Backend nach 120s nicht erreichbar!" + echo "FEHLER: HTTPS Backend nach 120s nicht erreichbar!" break fi done -echo "Backend erreichbar - starte Kiosk-Browser..." +echo "HTTPS Backend erreichbar - starte Kiosk-Browser..." -# Browser-Erkennung und -Start mit optimierten Vollbild-Flags +# Browser-Erkennung und -Start mit HTTPS-optimierten Vollbild-Flags if command -v chromium >/dev/null 2>&1; then - echo "Starte Chromium Browser mit Auflösung ${WIDTH}x${HEIGHT}..." + echo "Starte Chromium Browser mit HTTPS und Auflösung ${WIDTH}x${HEIGHT}..." exec chromium \ --kiosk \ --no-sandbox \ --disable-infobars \ --disable-session-crashed-bubble \ --disable-restore-session-state \ - --disable-web-security \ --disable-features=TranslateUI \ --disable-extensions \ --disable-plugins \ @@ -1000,16 +939,21 @@ if command -v chromium >/dev/null 2>&1; then --disable-features=VizDisplayCompositor \ --enable-features=OverlayScrollbar \ --hide-scrollbars \ - http://localhost:5000 + --ignore-certificate-errors \ + --ignore-ssl-errors \ + --ignore-certificate-errors-spki-list \ + --disable-web-security \ + --allow-running-insecure-content \ + --unsafely-treat-insecure-origin-as-secure=https://localhost:443 \ + https://localhost:443 elif command -v chromium-browser >/dev/null 2>&1; then - echo "Starte Chromium-Browser mit Auflösung ${WIDTH}x${HEIGHT}..." + echo "Starte Chromium-Browser mit HTTPS und Auflösung ${WIDTH}x${HEIGHT}..." exec chromium-browser \ --kiosk \ --no-sandbox \ --disable-infobars \ --disable-session-crashed-bubble \ --disable-restore-session-state \ - --disable-web-security \ --disable-features=TranslateUI \ --disable-extensions \ --disable-plugins \ @@ -1039,18 +983,26 @@ elif command -v chromium-browser >/dev/null 2>&1; then --disable-features=VizDisplayCompositor \ --enable-features=OverlayScrollbar \ --hide-scrollbars \ - http://localhost:5000 + --ignore-certificate-errors \ + --ignore-ssl-errors \ + --ignore-certificate-errors-spki-list \ + --disable-web-security \ + --allow-running-insecure-content \ + --unsafely-treat-insecure-origin-as-secure=https://localhost:443 \ + https://localhost:443 elif command -v firefox-esr >/dev/null 2>&1; then - echo "Starte Firefox ESR mit Auflösung ${WIDTH}x${HEIGHT}..." - # Firefox-Profil für Kiosk erstellen + echo "Starte Firefox ESR mit HTTPS und Auflösung ${WIDTH}x${HEIGHT}..." + # Firefox-Profil für HTTPS Kiosk erstellen mkdir -p /home/kiosk/.mozilla/firefox/kiosk.default cat > /home/kiosk/.mozilla/firefox/kiosk.default/user.js << FIREFOXEOF user_pref("browser.shell.checkDefaultBrowser", false); -user_pref("browser.startup.homepage", "http://localhost:5000"); +user_pref("browser.startup.homepage", "https://localhost:443"); user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true); user_pref("browser.tabs.warnOnClose", false); user_pref("browser.sessionstore.resume_from_crash", false); user_pref("security.tls.insecure_fallback_hosts", "localhost"); +user_pref("security.mixed_content.block_active_content", false); +user_pref("security.mixed_content.block_display_content", false); user_pref("browser.cache.disk.enable", false); user_pref("browser.cache.memory.enable", true); user_pref("browser.cache.offline.enable", false); @@ -1059,6 +1011,8 @@ user_pref("browser.fullscreen.autohide", true); user_pref("dom.disable_open_during_load", false); user_pref("privacy.popups.disable_from_plugins", 0); user_pref("dom.popup_maximum", 0); +user_pref("security.cert_pinning.enforcement_level", 0); +user_pref("security.tls.unrestricted_rc4_fallback", true); FIREFOXEOF # Firefox CSS für randlosen Vollbildmodus @@ -1082,7 +1036,7 @@ FIREFOXCSS --width=${WIDTH} \ --height=${HEIGHT} \ --profile /home/kiosk/.mozilla/firefox/kiosk.default \ - http://localhost:5000 + https://localhost:443 else echo "FEHLER: Kein Browser verfügbar!" exit 1 @@ -1093,12 +1047,73 @@ EOF chmod +x "$KIOSK_HOME/.xinitrc" chown -R "$KIOSK_USER:$KIOSK_USER" "$KIOSK_HOME" - log "✅ Kiosk-Browser mit optimiertem Vollbildmodus konfiguriert" + log "✅ Kiosk-Browser mit HTTPS-Vollbildmodus konfiguriert" +} + +# ========================== APP.PY SSL-UNTERSTÜTZUNG PRÜFEN ========================== +verify_app_ssl_support() { + log "=== PRÜFE APP.PY SSL-UNTERSTÜTZUNG ===" + + if [ ! -f "$APP_DIR/app.py" ]; then + error "app.py nicht gefunden in $APP_DIR" + fi + + # Prüfe ob get_ssl_context Funktion existiert + if grep -q "def get_ssl_context" "$APP_DIR/app.py"; then + log "✅ get_ssl_context Funktion bereits vorhanden in app.py" + else + progress "Füge SSL-Unterstützung zu app.py hinzu..." + + # Backup der originalen app.py + cp "$APP_DIR/app.py" "$APP_DIR/app.py.backup.$(date +%s)" + + # SSL-Funktion am Ende der Datei hinzufügen (vor dem if __name__ == '__main__' Block) + cat >> "$APP_DIR/app.py" << 'EOF' + +def get_ssl_context(): + """ + SSL-Kontext für HTTPS-Server erstellen + Verwendet localhost-Zertifikate falls verfügbar + """ + import os + + ssl_cert_path = '/opt/myp/certs/localhost/localhost.crt' + ssl_key_path = '/opt/myp/certs/localhost/localhost.key' + + if os.path.exists(ssl_cert_path) and os.path.exists(ssl_key_path): + try: + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + context.load_cert_chain(ssl_cert_path, ssl_key_path) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + print(f"✅ SSL-Kontext erstellt mit: {ssl_cert_path}") + return context + except Exception as e: + print(f"❌ Fehler beim Erstellen des SSL-Kontexts: {e}") + return None + else: + print(f"❌ SSL-Zertifikate nicht gefunden:") + print(f" Cert: {ssl_cert_path}") + print(f" Key: {ssl_key_path}") + return None + +EOF + + log "✅ SSL-Unterstützung zu app.py hinzugefügt" + fi + + # Prüfe ob SSL-Imports vorhanden sind + if ! grep -q "import ssl" "$APP_DIR/app.py"; then + progress "Füge SSL-Import zu app.py hinzu..." + sed -i '1i import ssl' "$APP_DIR/app.py" + fi + + log "✅ app.py SSL-Unterstützung verifiziert" } # ========================== PRODUKTIONS-KIOSK SETUP ========================== setup_production_kiosk() { - log "=== RICHTE PRODUKTIONS-KIOSK-MODUS EIN ===" + log "=== RICHTE PRODUKTIONS-KIOSK-MODUS MIT HTTPS EIN ===" # 1. System-Abhängigkeiten installieren install_system_dependencies @@ -1161,6 +1176,7 @@ setup_production_kiosk() { mkdir -p "$APP_DIR/logs/auth" mkdir -p "$APP_DIR/logs/errors" mkdir -p "$APP_DIR/uploads/temp" + mkdir -p "$APP_DIR/certs/localhost" # Berechtigungen setzen chown -R root:root "$APP_DIR" @@ -1168,65 +1184,70 @@ setup_production_kiosk() { chmod 750 "$APP_DIR/database" chmod 750 "$APP_DIR/logs" chmod 755 "$APP_DIR/uploads" + chmod 750 "$APP_DIR/certs" - # 6. Backend-Services erstellen - create_backend_services + # 6. App.py SSL-Unterstützung prüfen und hinzufügen + verify_app_ssl_support - # 7. Autologin konfigurieren + # 7. SSL-Zertifikate für localhost erstellen + create_localhost_ssl_certificates + + # 8. Backend-Service erstellen + create_backend_service + + # 9. Autologin konfigurieren configure_autologin - # 8. Kiosk-Browser konfigurieren + # 10. Kiosk-Browser konfigurieren configure_kiosk_browser - # 9. Services aktivieren und starten + # 11. Service aktivieren und starten progress "Lade Systemd-Konfiguration neu..." systemctl daemon-reload || error "Systemd Reload fehlgeschlagen" - progress "Aktiviere alle Backend-Services..." - systemctl enable "$SERVICE_NAME_KIOSK.service" || error "Kiosk-Service Enable fehlgeschlagen" - systemctl enable "$SERVICE_NAME_HTTPS.service" || error "HTTPS-Service Enable fehlgeschlagen" - systemctl enable "$SERVICE_NAME_HTTP.service" || error "HTTP-Service Enable fehlgeschlagen" + progress "Aktiviere HTTPS Backend-Service..." + systemctl enable "$SERVICE_NAME.service" || error "HTTPS-Service Enable fehlgeschlagen" - progress "Starte alle Backend-Services..." - systemctl start "$SERVICE_NAME_KIOSK.service" || error "Kiosk-Service Start fehlgeschlagen" - systemctl start "$SERVICE_NAME_HTTPS.service" || warning "HTTPS-Service Start fehlgeschlagen (SSL möglicherweise nicht konfiguriert)" - systemctl start "$SERVICE_NAME_HTTP.service" || error "HTTP-Service Start fehlgeschlagen" + progress "Starte HTTPS Backend-Service..." + systemctl start "$SERVICE_NAME.service" || error "HTTPS-Service Start fehlgeschlagen" # Service-Status prüfen sleep 5 info "=== SERVICE-STATUS ===" - for service in "$SERVICE_NAME_KIOSK" "$SERVICE_NAME_HTTPS" "$SERVICE_NAME_HTTP"; do - if systemctl is-active --quiet "$service.service"; then - log "✅ $service Service läuft erfolgreich" - else - warning "⚠️ $service Service läuft nicht - prüfen Sie die Logs: journalctl -u $service -f" - fi - done + if systemctl is-active --quiet "$SERVICE_NAME.service"; then + log "✅ $SERVICE_NAME Service läuft erfolgreich" + else + warning "⚠️ $SERVICE_NAME Service läuft nicht - prüfen Sie die Logs: journalctl -u $SERVICE_NAME -f" + fi # Backend-Tests - progress "Teste Backend-Erreichbarkeit..." + progress "Teste HTTPS Backend-Erreichbarkeit..." sleep 3 - if curl -s http://localhost:5000 > /dev/null 2>&1; then - log "✅ Port 5000 (Kiosk) erreichbar" - else - warning "⚠️ Port 5000 (Kiosk) nicht erreichbar" - fi - - if curl -s http://localhost:80 > /dev/null 2>&1; then - log "✅ Port 80 (HTTP) erreichbar" - else - warning "⚠️ Port 80 (HTTP) nicht erreichbar" - fi - if curl -k -s https://localhost:443 > /dev/null 2>&1; then log "✅ Port 443 (HTTPS) erreichbar" else - warning "⚠️ Port 443 (HTTPS) nicht erreichbar (SSL möglicherweise nicht konfiguriert)" + warning "⚠️ Port 443 (HTTPS) nicht erreichbar" + # Zusätzliche Debug-Information + progress "Versuche SSL-Zertifikat zu testen..." + if openssl s_client -connect localhost:443 -servername localhost < /dev/null 2>/dev/null | grep -q "CONNECTED"; then + log "✅ SSL-Verbindung funktioniert" + else + warning "⚠️ SSL-Verbindung fehlgeschlagen" + fi fi - log "✅ PRODUKTIONS-KIOSK-MODUS ERFOLGREICH EINGERICHTET" + # SSL-Zertifikat Status + if [ -f "$APP_DIR/certs/localhost/localhost.crt" ]; then + log "✅ SSL-Zertifikat vorhanden: $APP_DIR/certs/localhost/localhost.crt" + CERT_EXPIRY=$(openssl x509 -in "$APP_DIR/certs/localhost/localhost.crt" -noout -enddate | cut -d= -f2) + log "📅 Zertifikat läuft ab: $CERT_EXPIRY" + else + warning "⚠️ SSL-Zertifikat nicht gefunden" + fi + + log "✅ PRODUKTIONS-KIOSK-MODUS MIT HTTPS ERFOLGREICH EINGERICHTET" log "" log "🚀 WICHTIG: NEUSTART ERFORDERLICH!" log " sudo reboot" @@ -1234,30 +1255,36 @@ setup_production_kiosk() { log "📊 NACH DEM NEUSTART:" log " • Automatischer Login als Benutzer: $KIOSK_USER" log " • Automatischer X-Start und Chromium-Kiosk" - log " • Backend läuft auf 3 Ports:" - log " - http://localhost:5000 (Kiosk-Anzeige)" - log " - http://localhost:80 (HTTP-API)" - log " - https://localhost:443 (HTTPS-API)" + log " • Backend läuft auf HTTPS:" + log " - https://localhost:443 (Kiosk-Anzeige mit SSL)" + log " - https://0.0.0.0:443 (Netzwerk-Zugriff)" + log "" + log "🔐 SSL-ZERTIFIKATE:" + log " • Self-Signed Zertifikat für localhost erstellt" + log " • Chromium akzeptiert Zertifikat automatisch" + log " • Zertifikat im System CA-Store installiert" log "" log "🔧 SERVICE-BEFEHLE:" - log " • Status: sudo systemctl status myp-{kiosk,https,http}" - log " • Logs: sudo journalctl -u myp-kiosk -f" - log " • Restart: sudo systemctl restart myp-{kiosk,https,http}" + log " • Status: sudo systemctl status $SERVICE_NAME" + log " • Logs: sudo journalctl -u $SERVICE_NAME -f" + log " • Restart: sudo systemctl restart $SERVICE_NAME" + log " • SSL-Test: curl -k https://localhost:443" log "" - warning "🔄 FÜHRE JETZT 'sudo reboot' AUS, UM DEN KIOSK-MODUS ZU AKTIVIEREN!" + warning "🔄 FÜHRE JETZT 'sudo reboot' AUS, UM DEN HTTPS-KIOSK-MODUS ZU AKTIVIEREN!" } # ========================== HAUPTMENÜ ========================== show_menu() { clear echo -e "${BLUE}=================================================================${NC}" - echo -e "${GREEN} $APP_NAME - VOLLSTÄNDIGER KIOSK-INSTALLER${NC}" + echo -e "${GREEN} $APP_NAME - HTTPS KIOSK-INSTALLER${NC}" echo -e "${BLUE}=================================================================${NC}" echo "" echo -e "${YELLOW}Aktuelles Verzeichnis:${NC} $CURRENT_DIR" echo -e "${YELLOW}Systemzeit:${NC} $(date)" echo -e "${YELLOW}Zielverzeichnis:${NC} $APP_DIR" echo -e "${YELLOW}Kiosk-Benutzer:${NC} $KIOSK_USER" + echo -e "${YELLOW}HTTPS-Service:${NC} $SERVICE_NAME" echo "" echo -e "${PURPLE}Wählen Sie eine Option:${NC}" echo "" @@ -1265,17 +1292,21 @@ show_menu() { echo -e " → Installiert Python 3, pip und alle benötigten Pakete" echo -e " → Verwendet: pip install --break-system-packages" echo -e " → Mercedes SSL-Zertifikate werden konfiguriert" + echo -e " → Node.js und npm für Frontend-Build" echo "" - echo -e "${GREEN}2)${NC} VOLLSTÄNDIGER KIOSK-MODUS installieren" + echo -e "${GREEN}2)${NC} VOLLSTÄNDIGER HTTPS KIOSK-MODUS installieren" echo -e " → ${RED}ENTFERNT ALLE DESKTOP-ENVIRONMENTS!${NC}" echo -e " → Installiert minimale X11-Umgebung" - echo -e " → Erstellt 3 Backend-Services (Port 5000, 80, 443)" - echo -e " → Konfiguriert Autologin und Kiosk-Browser" + echo -e " → Erstellt Self-Signed SSL-Zertifikate für localhost" + echo -e " → Erstellt HTTPS Backend-Service (Port 443)" + echo -e " → Konfiguriert Autologin und HTTPS Kiosk-Browser" + echo -e " → Browser öffnet: ${BLUE}https://localhost:443${NC}" echo -e " → ${YELLOW}NEUSTART ERFORDERLICH!${NC}" echo "" echo -e "${RED}0)${NC} Beenden" echo "" - echo -e "${RED}⚠️ WARNUNG: Option 2 macht Raspberry Pi zu reinem Kiosk-System!${NC}" + echo -e "${RED}⚠️ WARNUNG: Option 2 macht Raspberry Pi zu reinem HTTPS-Kiosk-System!${NC}" + echo -e "${GREEN}🔐 HTTPS: Automatische SSL-Zertifikat-Generierung für localhost${NC}" echo -e "${BLUE}=================================================================${NC}" echo -n "Ihre Wahl [0-2]: " } @@ -1290,11 +1321,12 @@ main() { mkdir -p "$(dirname "$INSTALL_LOG")" touch "$INSTALL_LOG" - log "=== MYP VOLLSTÄNDIGER KIOSK-INSTALLER GESTARTET ===" + log "=== MYP HTTPS KIOSK-INSTALLER GESTARTET ===" log "Arbeitsverzeichnis: $CURRENT_DIR" log "Zielverzeichnis: $APP_DIR" - log "Kiosk-Services: $SERVICE_NAME_KIOSK, $SERVICE_NAME_HTTPS, $SERVICE_NAME_HTTP" + log "HTTPS-Service: $SERVICE_NAME" log "Kiosk-Benutzer: $KIOSK_USER" + log "SSL-Zertifikate: $APP_DIR/certs/localhost/" log "System: $(uname -a)" log "Debian-Version: $(cat /etc/debian_version 2>/dev/null || echo 'Unbekannt')" @@ -1315,19 +1347,21 @@ main() { 2) clear echo -e "${RED}⚠️ WARNUNG: Sie sind dabei, alle Desktop-Environments zu entfernen!${NC}" - echo -e "${YELLOW}Der Raspberry Pi wird zu einem reinen Kiosk-System umgebaut.${NC}" - echo -e "${BLUE}Nach der Installation startet automatisch der Kiosk-Browser.${NC}" + echo -e "${YELLOW}Der Raspberry Pi wird zu einem reinen HTTPS-Kiosk-System umgebaut.${NC}" + echo -e "${BLUE}Nach der Installation startet automatisch der HTTPS-Kiosk-Browser.${NC}" + echo -e "${GREEN}SSL-Zertifikate für localhost werden automatisch generiert.${NC}" echo "" echo -n "Sind Sie sicher? [ja/NEIN]: " read -r confirm if [ "$confirm" = "ja" ] || [ "$confirm" = "JA" ]; then clear - log "=== OPTION 2: VOLLSTÄNDIGER KIOSK-MODUS ===" + log "=== OPTION 2: VOLLSTÄNDIGER HTTPS KIOSK-MODUS ===" setup_production_kiosk echo "" - echo -e "${GREEN}✅ KIOSK-MODUS ERFOLGREICH EINGERICHTET!${NC}" + echo -e "${GREEN}✅ HTTPS KIOSK-MODUS ERFOLGREICH EINGERICHTET!${NC}" echo -e "${RED}🔄 NEUSTART JETZT ERFORDERLICH: sudo reboot${NC}" + echo -e "${BLUE}🔐 HTTPS-URL: https://localhost:443${NC}" echo -e "${YELLOW}Drücken Sie Enter, um fortzufahren...${NC}" read -r else @@ -1336,7 +1370,7 @@ main() { fi ;; 0) - log "=== INSTALLER BEENDET ===" + log "=== HTTPS KIOSK-INSTALLER BEENDET ===" echo -e "${GREEN}Auf Wiedersehen!${NC}" echo -e "${BLUE}Log-Datei: $INSTALL_LOG${NC}" exit 0 diff --git a/backend/logs/app/app.log b/backend/logs/app/app.log index 451333ea..7089a2a4 100644 --- a/backend/logs/app/app.log +++ b/backend/logs/app/app.log @@ -84263,3 +84263,149 @@ WHERE printers.active = 1 AND printers.status = ?) AS anon_1] 2025-06-01 02:28:10 - myp.printer_monitor - INFO - 🔍 Teste IP 5/6: 192.168.0.102 2025-06-01 02:28:16 - myp.printer_monitor - INFO - 🔍 Teste IP 6/6: 192.168.0.105 2025-06-01 02:28:22 - myp.printer_monitor - INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 36.0s +2025-06-01 02:28:30 - myp.app - INFO - Optimierte SQLite-Engine erstellt: /mnt/database/myp.db +2025-06-01 02:28:30 - myp.printer_monitor - INFO - 🖨️ Drucker-Monitor initialisiert +2025-06-01 02:28:30 - myp.printer_monitor - INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-06-01 02:28:30 - myp.database - INFO - Datenbank-Wartungs-Scheduler gestartet +2025-06-01 02:28:30 - myp.backup - INFO - BackupManager initialisiert (minimal implementation) +2025-06-01 02:28:30 - myp.analytics - INFO - 📈 Analytics Engine initialisiert +2025-06-01 02:28:30 - myp.dashboard - INFO - Dashboard-Background-Worker gestartet +2025-06-01 02:28:30 - myp.app - INFO - SQLite für Produktionsumgebung konfiguriert (WAL-Modus, Cache, Optimierungen) +2025-06-01 02:28:30 - myp.email_notification - INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) +2025-06-01 02:28:30 - myp.maintenance - INFO - Wartungs-Scheduler gestartet +2025-06-01 02:28:30 - myp.multi_location - INFO - Standard-Standort erstellt +2025-06-01 02:28:30 - myp.dashboard - INFO - Dashboard-Background-Worker gestartet +2025-06-01 02:28:30 - myp.maintenance - INFO - Wartungs-Scheduler gestartet +2025-06-01 02:28:30 - myp.multi_location - INFO - Standard-Standort erstellt +2025-06-01 02:28:30 - myp.dashboard - INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) +2025-06-01 02:28:30 - myp.dashboard - INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) +2025-06-01 02:28:30 - myp.security - INFO - 🔒 Security System initialisiert +2025-06-01 02:28:30 - myp.permissions - INFO - 🔐 Permission Template Helpers registriert +2025-06-01 02:28:30 - myp.app - INFO - ================================================== +2025-06-01 02:28:30 - myp.app - INFO - 🚀 MYP (Manage Your Printers) wird gestartet... +2025-06-01 02:28:30 - myp.app - INFO - 📂 Log-Verzeichnis: /mnt/logs +2025-06-01 02:28:30 - myp.app - INFO - 📊 Log-Level: INFO +2025-06-01 02:28:30 - myp.app - INFO - 💻 Betriebssystem: Linux 6.1.0-37-amd64 +2025-06-01 02:28:30 - myp.app - INFO - 🌐 Hostname: debian +2025-06-01 02:28:30 - myp.app - INFO - 📅 Startzeit: 01.06.2025 02:28:30 +2025-06-01 02:28:30 - myp.app - INFO - ================================================== +2025-06-01 02:28:31 - myp.app - INFO - 🔄 Starte Datenbank-Setup und Migrationen... +2025-06-01 02:28:31 - myp.app - INFO - Datenbank mit Optimierungen initialisiert +2025-06-01 02:28:31 - myp.app - INFO - ✅ JobOrder-Tabelle bereits vorhanden +2025-06-01 02:28:31 - myp.app - INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-06-01 02:28:31 - myp.app - INFO - ✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen +2025-06-01 02:28:31 - myp.app - INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-06-01 02:28:31 - myp.printer_monitor - INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-06-01 02:28:31 - myp.printer_monitor - WARNING - ⚠️ Keine aktiven Drucker zur Initialisierung gefunden +2025-06-01 02:28:31 - myp.app - INFO - ℹ️ Keine Drucker zur Initialisierung gefunden +2025-06-01 02:28:31 - myp.app - INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-06-01 02:28:31 - myp.app - INFO - Job-Scheduler gestartet +2025-06-01 02:28:31 - myp.app - INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-06-01 02:28:31 - werkzeug - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://10.0.2.15:5000 +2025-06-01 02:28:31 - werkzeug - INFO - Press CTRL+C to quit +2025-06-01 02:28:31 - werkzeug - INFO - * Restarting with watchdog (inotify) +2025-06-01 02:28:31 - myp.app - INFO - Optimierte SQLite-Engine erstellt: /mnt/database/myp.db +2025-06-01 02:28:31 - myp.printer_monitor - INFO - 🖨️ Drucker-Monitor initialisiert +2025-06-01 02:28:31 - myp.printer_monitor - INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-06-01 02:28:31 - myp.database - INFO - Datenbank-Wartungs-Scheduler gestartet +2025-06-01 02:28:32 - myp.backup - INFO - BackupManager initialisiert (minimal implementation) +2025-06-01 02:28:32 - myp.analytics - INFO - 📈 Analytics Engine initialisiert +2025-06-01 02:28:32 - myp.dashboard - INFO - Dashboard-Background-Worker gestartet +2025-06-01 02:28:32 - myp.app - INFO - SQLite für Produktionsumgebung konfiguriert (WAL-Modus, Cache, Optimierungen) +2025-06-01 02:28:32 - myp.email_notification - INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) +2025-06-01 02:28:32 - myp.maintenance - INFO - Wartungs-Scheduler gestartet +2025-06-01 02:28:32 - myp.printer_monitor - INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-06-01 02:28:32 - myp.printer_monitor - INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-06-01 02:28:32 - myp.multi_location - INFO - Standard-Standort erstellt +2025-06-01 02:28:32 - myp.printer_monitor - INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-06-01 02:28:32 - myp.dashboard - INFO - Dashboard-Background-Worker gestartet +2025-06-01 02:28:32 - myp.maintenance - INFO - Wartungs-Scheduler gestartet +2025-06-01 02:28:32 - myp.multi_location - INFO - Standard-Standort erstellt +2025-06-01 02:28:32 - myp.dashboard - INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) +2025-06-01 02:28:32 - myp.dashboard - INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) +2025-06-01 02:28:32 - myp.security - INFO - 🔒 Security System initialisiert +2025-06-01 02:28:32 - myp.permissions - INFO - 🔐 Permission Template Helpers registriert +2025-06-01 02:28:32 - myp.app - INFO - ================================================== +2025-06-01 02:28:32 - myp.app - INFO - 🚀 MYP (Manage Your Printers) wird gestartet... +2025-06-01 02:28:32 - myp.app - INFO - 📂 Log-Verzeichnis: /mnt/logs +2025-06-01 02:28:32 - myp.app - INFO - 📊 Log-Level: INFO +2025-06-01 02:28:32 - myp.app - INFO - 💻 Betriebssystem: Linux 6.1.0-37-amd64 +2025-06-01 02:28:32 - myp.app - INFO - 🌐 Hostname: debian +2025-06-01 02:28:32 - myp.app - INFO - 📅 Startzeit: 01.06.2025 02:28:32 +2025-06-01 02:28:32 - myp.app - INFO - ================================================== +2025-06-01 02:28:32 - myp.app - INFO - 🔄 Starte Datenbank-Setup und Migrationen... +2025-06-01 02:28:32 - myp.app - INFO - Datenbank mit Optimierungen initialisiert +2025-06-01 02:28:32 - myp.app - INFO - ✅ JobOrder-Tabelle bereits vorhanden +2025-06-01 02:28:32 - myp.app - INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-06-01 02:28:32 - myp.app - INFO - ✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen +2025-06-01 02:28:32 - myp.app - INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-06-01 02:28:32 - myp.printer_monitor - INFO - 🚀 Starte Steckdosen-Initialisierung beim Programmstart... +2025-06-01 02:28:32 - myp.printer_monitor - WARNING - ⚠️ Keine aktiven Drucker zur Initialisierung gefunden +2025-06-01 02:28:32 - myp.app - INFO - ℹ️ Keine Drucker zur Initialisierung gefunden +2025-06-01 02:28:32 - myp.app - INFO - 🔄 Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-06-01 02:28:32 - myp.app - INFO - Job-Scheduler gestartet +2025-06-01 02:28:32 - myp.app - INFO - Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-06-01 02:28:32 - werkzeug - WARNING - * Debugger is active! +2025-06-01 02:28:32 - werkzeug - INFO - * Debugger PIN: 829-619-842 +2025-06-01 02:28:33 - myp.printer_monitor - INFO - 🔍 Starte automatische Tapo-Steckdosenerkennung... +2025-06-01 02:28:33 - myp.printer_monitor - INFO - 🔄 Teste 6 Standard-IPs aus der Konfiguration +2025-06-01 02:28:33 - myp.printer_monitor - INFO - 🔍 Teste IP 1/6: 192.168.0.103 +2025-06-01 02:28:34 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:28:34 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:28:34 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:28:34 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:28:35 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:28:35 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... +2025-06-01 02:28:35 - myp.database_cleanup - INFO - 🧹 Starte umfassendes Datenbank-Cleanup... +2025-06-01 02:28:35 - myp.database_cleanup - INFO - 📝 Schritt 1: Schließe alle Datenbankverbindungen... +2025-06-01 02:28:35 - myp.database_cleanup - INFO - 🔄 Schließe alle aktiven Datenbankverbindungen... +2025-06-01 02:28:35 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:28:35 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:28:35 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:28:35 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:28:35 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:28:35 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... +2025-06-01 02:28:35 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:28:35 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... +2025-06-01 02:28:35 - myp.database_cleanup - INFO - 🧹 Starte umfassendes Datenbank-Cleanup... +2025-06-01 02:28:35 - myp.database_cleanup - INFO - 📝 Schritt 1: Schließe alle Datenbankverbindungen... +2025-06-01 02:28:35 - myp.database_cleanup - INFO - 🔄 Schließe alle aktiven Datenbankverbindungen... +2025-06-01 02:28:35 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:28:35 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:28:35 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:28:35 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:28:35 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... +2025-06-01 02:28:35 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:28:35 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:28:35 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... +2025-06-01 02:28:38 - myp.printer_monitor - INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-06-01 02:28:39 - myp.printer_monitor - INFO - 🔍 Teste IP 2/6: 192.168.0.104 +2025-06-01 02:28:45 - myp.printer_monitor - INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-06-01 02:28:46 - myp.printer_monitor - INFO - 🔍 Teste IP 3/6: 192.168.0.100 +2025-06-01 02:28:51 - myp.printer_monitor - INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-06-01 02:28:52 - myp.printer_monitor - INFO - 🔍 Teste IP 4/6: 192.168.0.101 +2025-06-01 02:28:59 - myp.printer_monitor - INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-06-01 02:28:59 - myp.printer_monitor - INFO - 🔍 Teste IP 5/6: 192.168.0.102 +2025-06-01 02:29:05 - myp.printer_monitor - INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-06-01 02:29:07 - myp.printer_monitor - INFO - 🔍 Teste IP 6/6: 192.168.0.105 +2025-06-01 02:29:12 - myp.printer_monitor - INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 40.3s +2025-06-01 02:29:13 - myp.printer_monitor - INFO - ✅ Steckdosen-Erkennung abgeschlossen: 0/6 Steckdosen gefunden in 39.7s +2025-06-01 02:31:17 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:31:17 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:31:17 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:31:17 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:31:17 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:31:17 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... +2025-06-01 02:31:17 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:31:17 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... +2025-06-01 02:31:17 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:31:17 - myp.app - WARNING - 🛑 Signal 2 empfangen - fahre System herunter... +2025-06-01 02:31:17 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:31:17 - myp.app - INFO - 🔄 Beende Queue Manager... +2025-06-01 02:31:17 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:31:17 - myp.app - INFO - Job-Scheduler gestoppt +2025-06-01 02:31:17 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... +2025-06-01 02:31:17 - myp.app - INFO - 💾 Führe robustes Datenbank-Cleanup durch... diff --git a/backend/logs/scheduler/scheduler.log b/backend/logs/scheduler/scheduler.log index ad807a00..2f963a4e 100644 --- a/backend/logs/scheduler/scheduler.log +++ b/backend/logs/scheduler/scheduler.log @@ -2790,3 +2790,20 @@ 2025-06-01 02:27:45 - myp.scheduler - INFO - Scheduler gestartet 2025-06-01 02:27:48 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True 2025-06-01 02:28:07 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-06-01 02:28:30 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-06-01 02:28:31 - myp.scheduler - INFO - Scheduler-Thread gestartet +2025-06-01 02:28:31 - myp.scheduler - INFO - Scheduler gestartet +2025-06-01 02:28:31 - myp.scheduler - INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-06-01 02:28:32 - myp.scheduler - INFO - Scheduler-Thread gestartet +2025-06-01 02:28:32 - myp.scheduler - INFO - Scheduler gestartet +2025-06-01 02:28:35 - myp.scheduler - INFO - Scheduler-Thread beendet +2025-06-01 02:28:35 - myp.scheduler - INFO - Scheduler gestoppt +2025-06-01 02:28:35 - myp.scheduler - WARNING - Scheduler läuft nicht +2025-06-01 02:28:35 - myp.scheduler - INFO - Scheduler-Thread beendet +2025-06-01 02:28:35 - myp.scheduler - INFO - Scheduler gestoppt +2025-06-01 02:28:35 - myp.scheduler - WARNING - Scheduler läuft nicht +2025-06-01 02:28:35 - myp.scheduler - WARNING - Scheduler läuft nicht +2025-06-01 02:31:17 - myp.scheduler - WARNING - Scheduler läuft nicht +2025-06-01 02:31:17 - myp.scheduler - WARNING - Scheduler läuft nicht +2025-06-01 02:31:17 - myp.scheduler - WARNING - Scheduler läuft nicht +2025-06-01 02:31:17 - myp.scheduler - WARNING - Scheduler läuft nicht diff --git a/backend/utils/__pycache__/shutdown_manager.cpython-313.pyc b/backend/utils/__pycache__/shutdown_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a87a20e3307e09e72db0e7d7a8a4aa5203f0bf0 GIT binary patch literal 21661 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 PrinterQueueManager: return _queue_manager_instance def start_queue_manager(): - """Startet den globalen Queue-Manager.""" - manager = get_queue_manager() - manager.start() - return manager + """Startet den globalen Queue-Manager sicher und ohne Signal-Handler-Interferenzen.""" + global _queue_manager_instance + with _queue_manager_lock: + if _queue_manager_instance is not None: + queue_logger.warning("Queue-Manager läuft bereits") + return _queue_manager_instance + + try: + queue_logger.info("🚀 Initialisiere neuen Queue-Manager...") + + # Prüfe ob zentraler Shutdown-Manager verfügbar ist + register_signals = True + try: + from utils.shutdown_manager import is_shutdown_requested + if is_shutdown_requested is not None: + queue_logger.info("🔄 Zentrale Shutdown-Verwaltung erkannt - deaktiviere lokale Signal-Handler") + register_signals = False + except ImportError: + queue_logger.debug("Kein zentraler Shutdown-Manager verfügbar - verwende lokale Signal-Handler") + + # Erstelle Queue-Manager ohne Signal-Handler wenn zentraler Manager vorhanden + _queue_manager_instance = PrinterQueueManager(register_signal_handlers=register_signals) + _queue_manager_instance.start() + + queue_logger.info("✅ Queue-Manager erfolgreich gestartet") + return _queue_manager_instance + + except Exception as e: + queue_logger.error(f"❌ Fehler beim Starten des Queue-Managers: {str(e)}") + _queue_manager_instance = None + raise def stop_queue_manager(): """Stoppt den globalen Queue-Manager definitiv und sicher.""" diff --git a/backend/utils/shutdown_manager.py b/backend/utils/shutdown_manager.py new file mode 100644 index 00000000..f70a9e85 --- /dev/null +++ b/backend/utils/shutdown_manager.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +""" +Zentraler Shutdown-Manager für robuste Anwendungsbeendigung +Koordiniert alle Cleanup-Operationen mit Timeouts und Fehlerbehandlung +""" + +import os +import sys +import signal +import threading +import time +import atexit +from typing import List, Callable, Dict, Any, Optional +from datetime import datetime +from contextlib import contextmanager +import logging + +# Logging-Setup mit Fallback +try: + from utils.logging_config import get_logger + shutdown_logger = get_logger("shutdown_manager") +except ImportError: + logging.basicConfig(level=logging.INFO) + shutdown_logger = logging.getLogger("shutdown_manager") + + +class ShutdownManager: + """ + Zentraler Manager für ordnungsgemäße Anwendungsbeendigung. + Koordiniert alle Cleanup-Operationen mit Timeouts und Prioritäten. + """ + + def __init__(self, timeout: int = 30): + self.timeout = timeout + self.is_shutting_down = False + self.shutdown_started = None + self.cleanup_functions: List[Dict[str, Any]] = [] + self.components: Dict[str, Any] = {} + self._lock = threading.Lock() + self._signal_handlers_registered = False + + # Shutdown-Ereignis für Thread-Koordination + self.shutdown_event = threading.Event() + + # Registriere grundlegende Signal-Handler + self._register_signal_handlers() + + shutdown_logger.info("🔧 Shutdown-Manager initialisiert") + + def _register_signal_handlers(self): + """Registriert plattformspezifische Signal-Handler""" + if self._signal_handlers_registered: + return + + try: + if os.name == 'nt': # Windows + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + if hasattr(signal, 'SIGBREAK'): + signal.signal(signal.SIGBREAK, self._signal_handler) + else: # Unix/Linux + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + signal.signal(signal.SIGHUP, self._signal_handler) + + self._signal_handlers_registered = True + shutdown_logger.debug("✅ Signal-Handler registriert") + + except Exception as e: + shutdown_logger.warning(f"⚠️ Signal-Handler konnten nicht registriert werden: {e}") + + def _signal_handler(self, signum, frame): + """Zentraler Signal-Handler für ordnungsgemäße Beendigung""" + if not self.is_shutting_down: + shutdown_logger.warning(f"🛑 Signal {signum} empfangen - starte koordiniertes Shutdown") + self.shutdown() + + def register_component(self, name: str, component: Any, stop_method: str = "stop"): + """ + Registriert eine Komponente für ordnungsgemäße Beendigung + + Args: + name: Name der Komponente + component: Komponenten-Objekt + stop_method: Name der Stopp-Methode (Standard: "stop") + """ + with self._lock: + self.components[name] = { + 'component': component, + 'stop_method': stop_method, + 'priority': 1 # Standard-Priorität + } + shutdown_logger.debug(f"📝 Komponente '{name}' registriert") + + def register_cleanup_function(self, + func: Callable, + name: str, + priority: int = 1, + timeout: int = 10, + args: tuple = (), + kwargs: dict = None): + """ + Registriert eine Cleanup-Funktion + + Args: + func: Cleanup-Funktion + name: Beschreibender Name + priority: Priorität (1=hoch, 3=niedrig) + timeout: Timeout in Sekunden + args: Funktions-Argumente + kwargs: Funktions-Keyword-Argumente + """ + if kwargs is None: + kwargs = {} + + cleanup_item = { + 'function': func, + 'name': name, + 'priority': priority, + 'timeout': timeout, + 'args': args, + 'kwargs': kwargs + } + + with self._lock: + self.cleanup_functions.append(cleanup_item) + # Nach Priorität sortieren (1 = höchste Priorität zuerst) + self.cleanup_functions.sort(key=lambda x: x['priority']) + + shutdown_logger.debug(f"📝 Cleanup-Funktion '{name}' registriert (Priorität: {priority})") + + def register_queue_manager(self, queue_manager_module): + """Registriert den Queue Manager für ordnungsgemäße Beendigung""" + try: + stop_func = getattr(queue_manager_module, 'stop_queue_manager', None) + if stop_func: + self.register_cleanup_function( + func=stop_func, + name="Queue Manager", + priority=1, # Hohe Priorität + timeout=15 + ) + shutdown_logger.debug("✅ Queue Manager registriert") + else: + shutdown_logger.warning("⚠️ Queue Manager stop_queue_manager Funktion nicht gefunden") + except Exception as e: + shutdown_logger.error(f"❌ Fehler beim Registrieren des Queue Managers: {e}") + + def register_scheduler(self, scheduler, scheduler_enabled: bool = True): + """Registriert den Job-Scheduler für ordnungsgemäße Beendigung""" + if not scheduler_enabled or not scheduler: + shutdown_logger.debug("Scheduler nicht registriert (deaktiviert oder nicht vorhanden)") + return + + def stop_scheduler(): + try: + if hasattr(scheduler, 'shutdown'): + shutdown_logger.info("🔄 Beende Scheduler mit shutdown()...") + scheduler.shutdown(wait=True) + elif hasattr(scheduler, 'stop'): + shutdown_logger.info("🔄 Beende Scheduler mit stop()...") + scheduler.stop() + else: + shutdown_logger.warning("⚠️ Scheduler hat keine bekannte Stop-Methode") + + shutdown_logger.info("✅ Scheduler erfolgreich gestoppt") + except Exception as e: + shutdown_logger.error(f"❌ Fehler beim Stoppen des Schedulers: {e}") + + self.register_cleanup_function( + func=stop_scheduler, + name="Job Scheduler", + priority=1, # Hohe Priorität + timeout=10 + ) + + def register_database_cleanup(self): + """Registriert Datenbank-Cleanup mit robuster Fehlerbehandlung""" + def safe_database_cleanup(): + try: + shutdown_logger.info("💾 Führe sicheres Datenbank-Cleanup durch...") + + # Versuche den neuen DatabaseCleanupManager zu verwenden + try: + from utils.database_cleanup import safe_database_cleanup + + result = safe_database_cleanup(force_mode_switch=False) # Kein riskantes Mode-Switching + + if result.get("success", False): + shutdown_logger.info(f"✅ Datenbank-Cleanup erfolgreich: {', '.join(result.get('operations', []))}") + else: + shutdown_logger.warning(f"⚠️ Datenbank-Cleanup mit Problemen: {', '.join(result.get('errors', []))}") + + except ImportError: + shutdown_logger.info("Fallback: Verwende einfaches WAL-Checkpoint...") + + # Einfacher Fallback nur mit WAL-Checkpoint + try: + from models import create_optimized_engine + from sqlalchemy import text + + engine = create_optimized_engine() + + with engine.connect() as conn: + result = conn.execute(text("PRAGMA wal_checkpoint(PASSIVE)")).fetchone() + if result and result[1] > 0: + shutdown_logger.info(f"WAL-Checkpoint: {result[1]} Seiten übertragen") + conn.commit() + + engine.dispose() + shutdown_logger.info("✅ Einfaches Datenbank-Cleanup abgeschlossen") + + except Exception as db_error: + shutdown_logger.error(f"❌ Fehler beim einfachen Datenbank-Cleanup: {db_error}") + + except Exception as cleanup_error: + shutdown_logger.error(f"❌ Fehler beim Datenbank-Cleanup: {cleanup_error}") + + self.register_cleanup_function( + func=safe_database_cleanup, + name="Datenbank Cleanup", + priority=3, # Niedrige Priorität (am Ende) + timeout=20 + ) + + def register_windows_thread_manager(self): + """Registriert Windows Thread Manager falls verfügbar""" + try: + if os.name == 'nt': + from utils.windows_fixes import get_windows_thread_manager + + thread_manager = get_windows_thread_manager() + if thread_manager: + self.register_cleanup_function( + func=thread_manager.shutdown_all, + name="Windows Thread Manager", + priority=2, + timeout=15 + ) + shutdown_logger.debug("✅ Windows Thread Manager registriert") + except Exception as e: + shutdown_logger.debug(f"Windows Thread Manager nicht verfügbar: {e}") + + @contextmanager + def cleanup_timeout(self, timeout: int, operation_name: str): + """Context Manager für Timeout-überwachte Operationen""" + start_time = time.time() + success = False + + try: + yield + success = True + except Exception as e: + elapsed = time.time() - start_time + shutdown_logger.error(f"❌ Fehler bei {operation_name} nach {elapsed:.1f}s: {e}") + raise + finally: + elapsed = time.time() - start_time + if success: + shutdown_logger.debug(f"✅ {operation_name} abgeschlossen in {elapsed:.1f}s") + elif elapsed >= timeout: + shutdown_logger.warning(f"⏱️ {operation_name} Timeout nach {elapsed:.1f}s") + + def shutdown(self, exit_code: int = 0): + """ + Führt koordiniertes Shutdown aller registrierten Komponenten durch + + Args: + exit_code: Exit-Code für sys.exit() + """ + if self.is_shutting_down: + shutdown_logger.debug("Shutdown bereits in Bearbeitung") + return + + with self._lock: + if self.is_shutting_down: + return + + self.is_shutting_down = True + self.shutdown_started = datetime.now() + + shutdown_logger.info("🔄 Starte koordiniertes System-Shutdown...") + self.shutdown_event.set() + + total_start = time.time() + + try: + # 1. Komponenten stoppen (nach Priorität) + self._shutdown_components() + + # 2. Cleanup-Funktionen ausführen (nach Priorität) + self._execute_cleanup_functions() + + total_elapsed = time.time() - total_start + shutdown_logger.info(f"✅ Koordiniertes Shutdown abgeschlossen in {total_elapsed:.1f}s") + + except Exception as e: + shutdown_logger.error(f"❌ Fehler beim koordinierten Shutdown: {e}") + finally: + shutdown_logger.info("🏁 System wird beendet...") + sys.exit(exit_code) + + def _shutdown_components(self): + """Stoppt alle registrierten Komponenten""" + if not self.components: + return + + shutdown_logger.info(f"🔄 Stoppe {len(self.components)} registrierte Komponenten...") + + for name, config in self.components.items(): + try: + component = config['component'] + stop_method = config['stop_method'] + + if hasattr(component, stop_method): + with self.cleanup_timeout(10, f"Komponente {name}"): + method = getattr(component, stop_method) + method() + shutdown_logger.debug(f"✅ Komponente '{name}' gestoppt") + else: + shutdown_logger.warning(f"⚠️ Komponente '{name}' hat keine '{stop_method}' Methode") + + except Exception as e: + shutdown_logger.error(f"❌ Fehler beim Stoppen der Komponente '{name}': {e}") + + def _execute_cleanup_functions(self): + """Führt alle registrierten Cleanup-Funktionen aus""" + if not self.cleanup_functions: + return + + shutdown_logger.info(f"🧹 Führe {len(self.cleanup_functions)} Cleanup-Funktionen aus...") + + for cleanup_item in self.cleanup_functions: + try: + func = cleanup_item['function'] + name = cleanup_item['name'] + timeout = cleanup_item['timeout'] + args = cleanup_item['args'] + kwargs = cleanup_item['kwargs'] + + shutdown_logger.debug(f"🔄 Führe Cleanup aus: {name}") + + with self.cleanup_timeout(timeout, f"Cleanup {name}"): + func(*args, **kwargs) + shutdown_logger.debug(f"✅ Cleanup '{name}' abgeschlossen") + + except Exception as e: + shutdown_logger.error(f"❌ Fehler bei Cleanup '{cleanup_item['name']}': {e}") + # Weiter mit nächstem Cleanup + continue + + def is_shutdown_requested(self) -> bool: + """Prüft ob Shutdown angefordert wurde""" + return self.shutdown_event.is_set() + + def get_shutdown_time(self) -> Optional[datetime]: + """Gibt den Zeitpunkt des Shutdown-Starts zurück""" + return self.shutdown_started + + def force_shutdown(self, exit_code: int = 1): + """Erzwingt sofortiges Shutdown ohne Cleanup""" + shutdown_logger.warning("🚨 Erzwinge sofortiges Shutdown ohne Cleanup!") + sys.exit(exit_code) + + +# Globaler Shutdown-Manager +_shutdown_manager: Optional[ShutdownManager] = None +_manager_lock = threading.Lock() + + +def get_shutdown_manager(timeout: int = 30) -> ShutdownManager: + """ + Singleton-Pattern für globalen Shutdown-Manager + + Args: + timeout: Gesamttimeout für Shutdown-Operationen + + Returns: + ShutdownManager: Globaler Shutdown-Manager + """ + global _shutdown_manager + + with _manager_lock: + if _shutdown_manager is None: + _shutdown_manager = ShutdownManager(timeout=timeout) + + # Registriere atexit-Handler als Fallback + atexit.register(_shutdown_manager.shutdown) + + return _shutdown_manager + + +def register_for_shutdown(component_or_function, + name: str, + component_stop_method: str = "stop", + priority: int = 1, + timeout: int = 10): + """ + Vereinfachte Registrierung für Shutdown + + Args: + component_or_function: Komponente mit Stop-Methode oder Cleanup-Funktion + name: Beschreibender Name + component_stop_method: Name der Stop-Methode für Komponenten + priority: Priorität (1=hoch, 3=niedrig) + timeout: Timeout in Sekunden + """ + manager = get_shutdown_manager() + + if callable(component_or_function): + # Es ist eine Funktion + manager.register_cleanup_function( + func=component_or_function, + name=name, + priority=priority, + timeout=timeout + ) + else: + # Es ist eine Komponente + manager.register_component( + name=name, + component=component_or_function, + stop_method=component_stop_method + ) + + +def shutdown_application(exit_code: int = 0): + """ + Startet koordiniertes Shutdown der Anwendung + + Args: + exit_code: Exit-Code für sys.exit() + """ + manager = get_shutdown_manager() + manager.shutdown(exit_code) + + +def is_shutdown_requested() -> bool: + """Prüft ob Shutdown angefordert wurde""" + if _shutdown_manager: + return _shutdown_manager.is_shutdown_requested() + return False \ No newline at end of file