""" Timer-Manager für Countdown-Zähler mit Force-Quit-Funktionalität Dieses Modul verwaltet System-Timer für verschiedene Anwendungsfälle: - Kiosk-Timer für automatische Session-Beendigung - Job-Timer für Druckaufträge mit Timeout - Session-Timer für Benutzerinaktivität - Wartungs-Timer für geplante System-Shutdowns Autor: System Erstellt: 2025 """ import threading import time import json import logging from datetime import datetime, timedelta from typing import Dict, List, Optional, Callable, Any from enum import Enum from contextlib import contextmanager from models import SystemTimer, get_db_session, get_cached_session from utils.logging_config import get_logger logger = get_logger("timer_manager") class TimerType(Enum): """Verfügbare Timer-Typen""" KIOSK = "kiosk" SESSION = "session" JOB = "job" SYSTEM = "system" MAINTENANCE = "maintenance" class ForceQuitAction(Enum): """Verfügbare Force-Quit-Aktionen""" LOGOUT = "logout" RESTART = "restart" SHUTDOWN = "shutdown" CUSTOM = "custom" class TimerStatus(Enum): """Timer-Status-Werte""" STOPPED = "stopped" RUNNING = "running" PAUSED = "paused" EXPIRED = "expired" FORCE_QUIT = "force_quit" class TimerManager: """ Zentraler Timer-Manager für alle System-Timer. Verwaltet Timer-Instanzen und führt automatische Cleanup-Operationen durch. """ def __init__(self): self._timers: Dict[str, SystemTimer] = {} self._timer_callbacks: Dict[str, List[Callable]] = {} self._force_quit_handlers: Dict[str, Callable] = {} self._background_thread: Optional[threading.Thread] = None self._shutdown_flag = threading.Event() self._update_interval = 1.0 # Sekunden zwischen Updates # Standard Force-Quit-Handler registrieren self._register_default_handlers() # Background-Thread für Timer-Updates starten self._start_background_thread() logger.info("Timer-Manager initialisiert") def _register_default_handlers(self): """Registriert Standard-Handler für Force-Quit-Aktionen""" def logout_handler(timer: SystemTimer) -> bool: """Standard-Handler für Logout-Aktion""" try: logger.info(f"Logout-Handler für Timer '{timer.name}' ausgeführt") # Hier würde der tatsächliche Logout implementiert werden # Das wird in app.py über die API-Endpunkte gemacht return True except Exception as e: logger.error(f"Fehler im Logout-Handler: {str(e)}") return False def restart_handler(timer: SystemTimer) -> bool: """Standard-Handler für System-Restart""" try: logger.warning(f"System-Restart durch Timer '{timer.name}' ausgelöst") # Implementierung würde über System-API erfolgen return True except Exception as e: logger.error(f"Fehler im Restart-Handler: {str(e)}") return False def shutdown_handler(timer: SystemTimer) -> bool: """Standard-Handler für System-Shutdown""" try: logger.warning(f"System-Shutdown durch Timer '{timer.name}' ausgelöst") # Implementierung würde über System-API erfolgen return True except Exception as e: logger.error(f"Fehler im Shutdown-Handler: {str(e)}") return False # Handler registrieren self._force_quit_handlers[ForceQuitAction.LOGOUT.value] = logout_handler self._force_quit_handlers[ForceQuitAction.RESTART.value] = restart_handler self._force_quit_handlers[ForceQuitAction.SHUTDOWN.value] = shutdown_handler def _start_background_thread(self): """Startet den Background-Thread für Timer-Updates""" if self._background_thread is None or not self._background_thread.is_alive(): self._background_thread = threading.Thread( target=self._background_worker, name="TimerManager-Background", daemon=True ) self._background_thread.start() logger.debug("Background-Thread für Timer-Updates gestartet") def _background_worker(self): """Background-Worker für kontinuierliche Timer-Updates""" logger.debug("Timer-Manager Background-Worker gestartet") while not self._shutdown_flag.is_set(): try: self._update_all_timers() self._process_expired_timers() # Warte bis zum nächsten Update self._shutdown_flag.wait(self._update_interval) except Exception as e: logger.error(f"Fehler im Timer-Background-Worker: {str(e)}") time.sleep(5) # Kurze Pause bei Fehlern logger.debug("Timer-Manager Background-Worker beendet") def _update_all_timers(self): """Aktualisiert alle Timer aus der Datenbank""" try: with get_cached_session() as session: # Lade alle aktiven Timer aus der Datenbank db_timers = session.query(SystemTimer).filter( SystemTimer.status.in_([TimerStatus.RUNNING.value, TimerStatus.PAUSED.value]) ).all() # Update lokale Timer-Cache current_timer_names = set(self._timers.keys()) db_timer_names = {timer.name for timer in db_timers} # Entferne Timer die nicht mehr in der DB sind for name in current_timer_names - db_timer_names: if name in self._timers: del self._timers[name] logger.debug(f"Timer '{name}' aus lokalem Cache entfernt") # Aktualisiere/füge Timer hinzu for timer in db_timers: self._timers[timer.name] = timer # Callback-Funktionen aufrufen wenn verfügbar if timer.name in self._timer_callbacks: for callback in self._timer_callbacks[timer.name]: try: callback(timer) except Exception as e: logger.error(f"Fehler in Timer-Callback für '{timer.name}': {str(e)}") except Exception as e: logger.error(f"Fehler beim Update der Timer: {str(e)}") def _process_expired_timers(self): """Verarbeitet abgelaufene Timer und führt Force-Quit-Aktionen aus""" try: expired_timers = SystemTimer.get_expired_timers() for timer in expired_timers: try: logger.warning(f"Timer '{timer.name}' ist abgelaufen - führe Force-Quit aus") # Force-Quit-Aktion ausführen success = self._execute_force_quit(timer) if success: # Timer als abgelaufen markieren with get_cached_session() as session: db_timer = session.query(SystemTimer).filter( SystemTimer.id == timer.id ).first() if db_timer: db_timer.status = TimerStatus.EXPIRED.value db_timer.updated_at = datetime.now() session.commit() except Exception as e: logger.error(f"Fehler beim Verarbeiten des abgelaufenen Timers '{timer.name}': {str(e)}") except Exception as e: logger.error(f"Fehler beim Verarbeiten abgelaufener Timer: {str(e)}") def _execute_force_quit(self, timer: SystemTimer) -> bool: """Führt die Force-Quit-Aktion für einen Timer aus""" try: action = timer.force_quit_action # Custom-Endpoint prüfen if action == ForceQuitAction.CUSTOM.value and timer.custom_action_endpoint: return self._execute_custom_action(timer) # Standard-Handler verwenden if action in self._force_quit_handlers: handler = self._force_quit_handlers[action] return handler(timer) logger.warning(f"Unbekannte Force-Quit-Aktion: {action}") return False except Exception as e: logger.error(f"Fehler beim Ausführen der Force-Quit-Aktion für Timer '{timer.name}': {str(e)}") return False def _execute_custom_action(self, timer: SystemTimer) -> bool: """Führt eine benutzerdefinierte Force-Quit-Aktion aus""" try: # Hier würde ein HTTP-Request an den Custom-Endpoint gemacht werden # Das wird über die Flask-App-Routen implementiert logger.info(f"Custom-Action für Timer '{timer.name}': {timer.custom_action_endpoint}") return True except Exception as e: logger.error(f"Fehler bei Custom-Action für Timer '{timer.name}': {str(e)}") return False def create_timer(self, name: str, timer_type: TimerType, duration_seconds: int, force_quit_action: ForceQuitAction = ForceQuitAction.LOGOUT, auto_start: bool = False, **kwargs) -> Optional[SystemTimer]: """ Erstellt einen neuen Timer. Args: name: Eindeutiger Name des Timers timer_type: Typ des Timers duration_seconds: Dauer in Sekunden force_quit_action: Aktion bei Force-Quit auto_start: Automatisch starten **kwargs: Zusätzliche Timer-Konfiguration Returns: SystemTimer-Instanz oder None bei Fehler """ try: with get_cached_session() as session: # Prüfe ob Timer bereits existiert existing = session.query(SystemTimer).filter( SystemTimer.name == name ).first() if existing: logger.warning(f"Timer '{name}' existiert bereits") return existing # Neuen Timer erstellen timer = SystemTimer( name=name, timer_type=timer_type.value, duration_seconds=duration_seconds, remaining_seconds=duration_seconds, target_timestamp=datetime.now() + timedelta(seconds=duration_seconds), force_quit_action=force_quit_action.value, auto_start=auto_start, **kwargs ) session.add(timer) session.commit() # Zu lokalem Cache hinzufügen self._timers[name] = timer if auto_start: timer.start_timer() logger.info(f"Timer '{name}' erstellt - Typ: {timer_type.value}, Dauer: {duration_seconds}s") return timer except Exception as e: logger.error(f"Fehler beim Erstellen des Timers '{name}': {str(e)}") return None def get_timer(self, name: str) -> Optional[SystemTimer]: """ Holt einen Timer anhand des Namens. Args: name: Name des Timers Returns: SystemTimer-Instanz oder None """ try: # Erst aus lokalem Cache prüfen if name in self._timers: return self._timers[name] # Aus Datenbank laden timer = SystemTimer.get_by_name(name) if timer: self._timers[name] = timer return timer except Exception as e: logger.error(f"Fehler beim Laden des Timers '{name}': {str(e)}") return None def start_timer(self, name: str) -> bool: """Startet einen Timer""" try: timer = self.get_timer(name) if not timer: logger.error(f"Timer '{name}' nicht gefunden") return False success = timer.start_timer() if success: with get_cached_session() as session: # Timer in Datenbank aktualisieren db_timer = session.merge(timer) session.commit() logger.info(f"Timer '{name}' gestartet") return success except Exception as e: logger.error(f"Fehler beim Starten des Timers '{name}': {str(e)}") return False def pause_timer(self, name: str) -> bool: """Pausiert einen Timer""" try: timer = self.get_timer(name) if not timer: logger.error(f"Timer '{name}' nicht gefunden") return False success = timer.pause_timer() if success: with get_cached_session() as session: db_timer = session.merge(timer) session.commit() logger.info(f"Timer '{name}' pausiert") return success except Exception as e: logger.error(f"Fehler beim Pausieren des Timers '{name}': {str(e)}") return False def stop_timer(self, name: str) -> bool: """Stoppt einen Timer""" try: timer = self.get_timer(name) if not timer: logger.error(f"Timer '{name}' nicht gefunden") return False success = timer.stop_timer() if success: with get_cached_session() as session: db_timer = session.merge(timer) session.commit() logger.info(f"Timer '{name}' gestoppt") return success except Exception as e: logger.error(f"Fehler beim Stoppen des Timers '{name}': {str(e)}") return False def reset_timer(self, name: str) -> bool: """Setzt einen Timer zurück""" try: timer = self.get_timer(name) if not timer: logger.error(f"Timer '{name}' nicht gefunden") return False success = timer.reset_timer() if success: with get_cached_session() as session: db_timer = session.merge(timer) session.commit() logger.info(f"Timer '{name}' zurückgesetzt") return success except Exception as e: logger.error(f"Fehler beim Zurücksetzen des Timers '{name}': {str(e)}") return False def extend_timer(self, name: str, additional_seconds: int) -> bool: """Verlängert einen Timer""" try: timer = self.get_timer(name) if not timer: logger.error(f"Timer '{name}' nicht gefunden") return False success = timer.extend_timer(additional_seconds) if success: with get_cached_session() as session: db_timer = session.merge(timer) session.commit() logger.info(f"Timer '{name}' um {additional_seconds} Sekunden verlängert") return success except Exception as e: logger.error(f"Fehler beim Verlängern des Timers '{name}': {str(e)}") return False def delete_timer(self, name: str) -> bool: """Löscht einen Timer""" try: with get_cached_session() as session: timer = session.query(SystemTimer).filter( SystemTimer.name == name ).first() if not timer: logger.error(f"Timer '{name}' nicht gefunden") return False session.delete(timer) session.commit() # Aus lokalem Cache entfernen if name in self._timers: del self._timers[name] # Callbacks entfernen if name in self._timer_callbacks: del self._timer_callbacks[name] logger.info(f"Timer '{name}' gelöscht") return True except Exception as e: logger.error(f"Fehler beim Löschen des Timers '{name}': {str(e)}") return False def register_callback(self, timer_name: str, callback: Callable[[SystemTimer], None]): """ Registriert eine Callback-Funktion für Timer-Updates. Args: timer_name: Name des Timers callback: Callback-Funktion die bei Updates aufgerufen wird """ if timer_name not in self._timer_callbacks: self._timer_callbacks[timer_name] = [] self._timer_callbacks[timer_name].append(callback) logger.debug(f"Callback für Timer '{timer_name}' registriert") def register_force_quit_handler(self, action: str, handler: Callable[[SystemTimer], bool]): """ Registriert einen benutzerdefinierten Force-Quit-Handler. Args: action: Name der Aktion handler: Handler-Funktion """ self._force_quit_handlers[action] = handler logger.debug(f"Force-Quit-Handler für Aktion '{action}' registriert") def get_all_timers(self) -> List[SystemTimer]: """Gibt alle Timer zurück""" try: with get_cached_session() as session: timers = session.query(SystemTimer).all() return timers except Exception as e: logger.error(f"Fehler beim Laden aller Timer: {str(e)}") return [] def get_timers_by_type(self, timer_type: TimerType) -> List[SystemTimer]: """Gibt alle Timer eines bestimmten Typs zurück""" try: return SystemTimer.get_by_type(timer_type.value) except Exception as e: logger.error(f"Fehler beim Laden der Timer vom Typ '{timer_type.value}': {str(e)}") return [] def get_running_timers(self) -> List[SystemTimer]: """Gibt alle laufenden Timer zurück""" try: return SystemTimer.get_running_timers() except Exception as e: logger.error(f"Fehler beim Laden der laufenden Timer: {str(e)}") return [] def create_kiosk_timer(self, duration_minutes: int = 30, auto_start: bool = True) -> Optional[SystemTimer]: """ Erstellt einen Standard-Kiosk-Timer. Args: duration_minutes: Timer-Dauer in Minuten auto_start: Automatisch starten Returns: SystemTimer-Instanz oder None """ return self.create_timer( name="kiosk_session", timer_type=TimerType.KIOSK, duration_seconds=duration_minutes * 60, force_quit_action=ForceQuitAction.LOGOUT, auto_start=auto_start, force_quit_warning_seconds=30, show_warning=True, warning_message="Kiosk-Session läuft ab. Bitte speichern Sie Ihre Arbeit." ) def create_session_timer(self, user_id: int, duration_minutes: int = 120, auto_start: bool = True) -> Optional[SystemTimer]: """ Erstellt einen Session-Timer für einen Benutzer. Args: user_id: Benutzer-ID duration_minutes: Timer-Dauer in Minuten auto_start: Automatisch starten Returns: SystemTimer-Instanz oder None """ return self.create_timer( name=f"session_{user_id}", timer_type=TimerType.SESSION, duration_seconds=duration_minutes * 60, force_quit_action=ForceQuitAction.LOGOUT, auto_start=auto_start, context_id=user_id, force_quit_warning_seconds=60, show_warning=True, warning_message="Ihre Session läuft ab. Aktivität erforderlich." ) def update_session_activity(self, user_id: int) -> bool: """ Aktualisiert die Aktivität eines Session-Timers. Args: user_id: Benutzer-ID Returns: True wenn erfolgreich """ try: timer = self.get_timer(f"session_{user_id}") if timer and timer.timer_type == TimerType.SESSION.value: success = timer.update_activity() if success: with get_cached_session() as session: db_timer = session.merge(timer) session.commit() return success return False except Exception as e: logger.error(f"Fehler beim Aktualisieren der Session-Aktivität für User {user_id}: {str(e)}") return False def shutdown(self): """Beendet den Timer-Manager sauber""" logger.info("Timer-Manager wird heruntergefahren...") self._shutdown_flag.set() if self._background_thread and self._background_thread.is_alive(): self._background_thread.join(timeout=5) self._timers.clear() self._timer_callbacks.clear() logger.info("Timer-Manager heruntergefahren") # Globale Timer-Manager-Instanz _timer_manager: Optional[TimerManager] = None def get_timer_manager() -> TimerManager: """ Gibt die globale Timer-Manager-Instanz zurück. Thread-sicher mit Lazy Loading. """ global _timer_manager if _timer_manager is None: _timer_manager = TimerManager() return _timer_manager def init_timer_manager() -> TimerManager: """ Initialisiert den Timer-Manager explizit. Sollte beim App-Start aufgerufen werden. """ return get_timer_manager() def shutdown_timer_manager(): """ Beendet den Timer-Manager sauber. Sollte beim App-Shutdown aufgerufen werden. """ global _timer_manager if _timer_manager: _timer_manager.shutdown() _timer_manager = None # Convenience-Funktionen für häufige Timer-Operationen def create_kiosk_timer(duration_minutes: int = 30, auto_start: bool = True) -> Optional[SystemTimer]: """Erstellt einen Kiosk-Timer""" return get_timer_manager().create_kiosk_timer(duration_minutes, auto_start) def create_session_timer(user_id: int, duration_minutes: int = 120) -> Optional[SystemTimer]: """Erstellt einen Session-Timer""" return get_timer_manager().create_session_timer(user_id, duration_minutes) def start_timer(name: str) -> bool: """Startet einen Timer""" return get_timer_manager().start_timer(name) def pause_timer(name: str) -> bool: """Pausiert einen Timer""" return get_timer_manager().pause_timer(name) def stop_timer(name: str) -> bool: """Stoppt einen Timer""" return get_timer_manager().stop_timer(name) def reset_timer(name: str) -> bool: """Setzt einen Timer zurück""" return get_timer_manager().reset_timer(name) def extend_timer(name: str, additional_seconds: int) -> bool: """Verlängert einen Timer""" return get_timer_manager().extend_timer(name, additional_seconds) def get_timer_status(name: str) -> Optional[Dict[str, Any]]: """Gibt den Status eines Timers zurück""" timer = get_timer_manager().get_timer(name) return timer.to_dict() if timer else None def update_session_activity(user_id: int) -> bool: """Aktualisiert Session-Aktivität""" return get_timer_manager().update_session_activity(user_id)