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 97257d90..00000000 Binary files a/backend/database/myp.db-shm and /dev/null differ diff --git a/backend/database/myp.db-wal b/backend/database/myp.db-wal deleted file mode 100644 index a6f8e039..00000000 Binary files a/backend/database/myp.db-wal and /dev/null differ 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 00000000..1a87a20e Binary files /dev/null and b/backend/utils/__pycache__/shutdown_manager.cpython-313.pyc differ diff --git a/backend/utils/queue_manager.py b/backend/utils/queue_manager.py index 6ac216d4..67bd11b1 100644 --- a/backend/utils/queue_manager.py +++ b/backend/utils/queue_manager.py @@ -81,7 +81,7 @@ class PrinterQueueManager: Verbesserte Version mit ordnungsgemäßem Thread-Management für Windows. """ - def __init__(self): + def __init__(self, register_signal_handlers: bool = True): self.is_running = False self.monitor_thread = None self.shutdown_event = threading.Event() # Sauberes Shutdown-Signal @@ -89,65 +89,109 @@ class PrinterQueueManager: self.last_status_cache = {} # Cache für letzten bekannten Status self.notification_cooldown = {} # Verhindert Spam-Benachrichtigungen self._lock = threading.Lock() # Thread-Sicherheit + self._signal_handlers_registered = False - # Windows-spezifische Signal-Handler registrieren - if os.name == 'nt': + # Signal-Handler nur registrieren wenn explizit gewünscht + # (Verhindert Interferenzen mit zentralem Shutdown-Manager) + if register_signal_handlers and os.name == 'nt': + self._register_signal_handlers() + + def _register_signal_handlers(self): + """Windows-spezifische Signal-Handler registrieren (nur wenn gewünscht)""" + if self._signal_handlers_registered: + return + + try: + # Prüfe ob bereits zentrale Signal-Handler existieren + try: + from utils.shutdown_manager import is_shutdown_requested + if is_shutdown_requested is not None: + queue_logger.info("🔄 Zentrale Signal-Handler erkannt - deaktiviere lokale Handler") + return + except ImportError: + pass # Kein zentraler Manager verfügbar, verwende lokale Handler + signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) + self._signal_handlers_registered = True + queue_logger.debug("✅ Lokale Signal-Handler für Queue Manager registriert") + + except Exception as e: + queue_logger.warning(f"⚠️ Lokale Signal-Handler konnten nicht registriert werden: {e}") def _signal_handler(self, signum, frame): - """Signal-Handler für ordnungsgemäßes Shutdown.""" + """Signal-Handler für ordnungsgemäßes Shutdown (nur als Fallback).""" queue_logger.warning(f"🛑 Signal {signum} empfangen - stoppe Queue Manager...") self.stop() def start(self): - """Startet den Queue-Manager mit verbessertem Thread-Management.""" - with self._lock: - if not self.is_running: - self.is_running = True - self.shutdown_event.clear() - self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=False) - self.monitor_thread.name = "PrinterQueueMonitor" - - # Windows Thread-Manager verwenden falls verfügbar - if os.name == 'nt' and get_windows_thread_manager: - try: - thread_manager = get_windows_thread_manager() - thread_manager.register_thread(self.monitor_thread) - thread_manager.register_cleanup_function(self.stop) - queue_logger.debug("✅ Queue Manager bei Windows Thread-Manager registriert") - except Exception as e: - queue_logger.warning(f"⚠️ Windows Thread-Manager nicht verfügbar: {str(e)}") - - self.monitor_thread.start() - queue_logger.info("✅ Printer Queue Manager erfolgreich gestartet") - - def stop(self): - """Stoppt den Queue-Manager ordnungsgemäß.""" + """Startet den Queue-Manager mit verbessertem Shutdown-Handling.""" with self._lock: if self.is_running: - queue_logger.info("🔄 Beende Queue Manager...") - self.is_running = False - self.shutdown_event.set() + queue_logger.warning("Queue-Manager läuft bereits") + return self + + queue_logger.info("🚀 Starte Printer Queue Manager...") + self.is_running = True + self.shutdown_event.clear() + + # Monitor-Thread mit Daemon-Flag für automatische Beendigung + self.monitor_thread = threading.Thread( + target=self._monitor_loop, + name="PrinterQueueMonitor", + daemon=True # Automatische Beendigung bei Programm-Ende + ) + self.monitor_thread.start() + + queue_logger.info("✅ Printer Queue Manager gestartet") + return self + + def stop(self): + """Stoppt den Queue-Manager ordnungsgemäß mit verbessertem Timeout-Handling.""" + with self._lock: + if not self.is_running: + queue_logger.debug("Queue-Manager ist bereits gestoppt") + return - if self.monitor_thread and self.monitor_thread.is_alive(): - queue_logger.debug("⏳ Warte auf Thread-Beendigung...") - self.monitor_thread.join(timeout=10) + queue_logger.info("🔄 Beende Queue Manager...") + self.is_running = False + self.shutdown_event.set() + + if self.monitor_thread and self.monitor_thread.is_alive(): + queue_logger.debug("⏳ Warte auf Thread-Beendigung...") + + # Verbessertes Timeout-Handling + try: + self.monitor_thread.join(timeout=5.0) # Reduziertes Timeout if self.monitor_thread.is_alive(): - queue_logger.warning("⚠️ Thread konnte nicht ordnungsgemäß beendet werden") + queue_logger.warning("⚠️ Thread konnte nicht in 5s beendet werden - setze als Daemon") + # Thread als Daemon markieren für automatische Beendigung + self.monitor_thread.daemon = True else: queue_logger.info("✅ Monitor-Thread erfolgreich beendet") - - self.monitor_thread = None - queue_logger.info("❌ Printer Queue Manager gestoppt") - + + except Exception as e: + queue_logger.error(f"❌ Fehler beim Thread-Join: {e}") + + self.monitor_thread = None + queue_logger.info("❌ Printer Queue Manager gestoppt") + def _monitor_loop(self): """Hauptschleife für die Überwachung der Drucker mit verbessertem Shutdown-Handling.""" queue_logger.info(f"🔄 Queue-Überwachung gestartet (Intervall: {self.check_interval} Sekunden)") while self.is_running and not self.shutdown_event.is_set(): try: + # Prüfe auf zentrales Shutdown-Signal + try: + from utils.shutdown_manager import is_shutdown_requested + if is_shutdown_requested(): + queue_logger.info("🛑 Zentrales Shutdown-Signal empfangen - beende Monitor-Loop") + break + except ImportError: + pass # Kein zentraler Manager verfügbar + self._check_waiting_jobs() # Verwende Event.wait() statt time.sleep() für unterbrechbares Warten @@ -379,10 +423,37 @@ def get_queue_manager() -> 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