🎉 Feature: Implement MASSIVE KONSOLIDIERUNG PLAN in backend utils
This commit is contained in:
822
backend/utils/core_system.py
Normal file
822
backend/utils/core_system.py
Normal file
@ -0,0 +1,822 @@
|
||||
#!/usr/bin/env python3.11
|
||||
"""
|
||||
Core System Management - Massive Konsolidierung
|
||||
==============================================
|
||||
|
||||
Konsolidiert alle System-Management-Funktionalitäten in einer Datei:
|
||||
- System Control (system_control.py)
|
||||
- Shutdown Manager (shutdown_manager.py)
|
||||
- Watchdog Manager (watchdog_manager.py)
|
||||
- Windows Fixes (windows_fixes.py)
|
||||
- Error Recovery (error_recovery.py)
|
||||
- Timeout Force Quit Manager (timeout_force_quit_manager.py)
|
||||
|
||||
Migration: 6 Dateien → 1 Datei
|
||||
Autor: MYP Team - Massive Konsolidierung für IHK-Projektarbeit
|
||||
Datum: 2025-06-09
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import threading
|
||||
import subprocess
|
||||
import platform
|
||||
import traceback
|
||||
import shutil
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional, Tuple, Union, Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
# ===== UNIFIED LOGGER =====
|
||||
core_logger = get_logger("core_system")
|
||||
|
||||
# ===== ENUMS =====
|
||||
|
||||
class SystemOperation(Enum):
|
||||
"""Verfügbare System-Operationen"""
|
||||
RESTART = "restart"
|
||||
SHUTDOWN = "shutdown"
|
||||
KIOSK_RESTART = "kiosk_restart"
|
||||
KIOSK_ENABLE = "kiosk_enable"
|
||||
KIOSK_DISABLE = "kiosk_disable"
|
||||
SERVICE_RESTART = "service_restart"
|
||||
EMERGENCY_STOP = "emergency_stop"
|
||||
|
||||
class ErrorSeverity(Enum):
|
||||
"""Schweregrade von Fehlern"""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
class RecoveryAction(Enum):
|
||||
"""Verfügbare Recovery-Aktionen"""
|
||||
LOG_ONLY = "log_only"
|
||||
RESTART_SERVICE = "restart_service"
|
||||
RESTART_COMPONENT = "restart_component"
|
||||
CLEAR_CACHE = "clear_cache"
|
||||
RESET_DATABASE = "reset_database"
|
||||
RESTART_SYSTEM = "restart_system"
|
||||
EMERGENCY_STOP = "emergency_stop"
|
||||
|
||||
# ===== DATA CLASSES =====
|
||||
|
||||
@dataclass
|
||||
class ErrorPattern:
|
||||
"""Definiert ein Fehlermuster und zugehörige Recovery-Aktionen"""
|
||||
name: str
|
||||
patterns: List[str] # Regex-Patterns
|
||||
severity: ErrorSeverity
|
||||
actions: List[RecoveryAction]
|
||||
max_occurrences: int = 3
|
||||
time_window: int = 300 # Sekunden
|
||||
escalation_actions: List[RecoveryAction] = None
|
||||
description: str = ""
|
||||
|
||||
@dataclass
|
||||
class ErrorOccurrence:
|
||||
"""Einzelnes Auftreten eines Fehlers"""
|
||||
timestamp: datetime
|
||||
pattern_name: str
|
||||
error_message: str
|
||||
severity: ErrorSeverity
|
||||
context: Dict[str, Any] = None
|
||||
recovery_attempted: List[RecoveryAction] = None
|
||||
recovery_successful: bool = False
|
||||
|
||||
# ===== CORE SYSTEM MANAGER =====
|
||||
|
||||
class CoreSystemManager:
|
||||
"""
|
||||
Zentraler System-Manager für alle kritischen System-Operationen.
|
||||
Konsolidiert System Control, Shutdown Management, Error Recovery und Windows-Fixes.
|
||||
"""
|
||||
|
||||
def __init__(self, timeout: int = 30):
|
||||
self.timeout = timeout
|
||||
self.shutdown_requested = False
|
||||
self.shutdown_time = None
|
||||
self.components = {}
|
||||
self.cleanup_functions = []
|
||||
self.pending_operations = {}
|
||||
self.operation_history = []
|
||||
|
||||
# Error Recovery
|
||||
self.error_patterns = {}
|
||||
self.error_occurrences = []
|
||||
self.recovery_handlers = {}
|
||||
self.monitoring_active = False
|
||||
self.monitoring_thread = None
|
||||
|
||||
# Windows Thread Management
|
||||
self.registered_threads = []
|
||||
self.cleanup_callbacks = []
|
||||
|
||||
self._init_default_patterns()
|
||||
self._init_recovery_handlers()
|
||||
self._register_signal_handlers()
|
||||
self._apply_platform_fixes()
|
||||
|
||||
core_logger.info("🚀 Core System Manager initialisiert")
|
||||
|
||||
# ===== SYSTEM CONTROL =====
|
||||
|
||||
def is_safe_to_operate(self) -> Tuple[bool, str]:
|
||||
"""Prüft, ob es sicher ist, System-Operationen durchzuführen"""
|
||||
try:
|
||||
from models import get_cached_session, Job
|
||||
|
||||
with get_cached_session() as session:
|
||||
# Aktive Jobs prüfen
|
||||
active_jobs = session.query(Job).filter(
|
||||
Job.status.in_(['printing', 'paused'])
|
||||
).count()
|
||||
|
||||
if active_jobs > 0:
|
||||
return False, f"{active_jobs} aktive Jobs laufen noch"
|
||||
|
||||
# System-Load prüfen
|
||||
if hasattr(os, 'getloadavg'):
|
||||
load = os.getloadavg()[0]
|
||||
if load > 2.0:
|
||||
return False, f"System-Load zu hoch: {load:.1f}"
|
||||
|
||||
return True, "System bereit für Operationen"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"Sicherheitsprüfung fehlgeschlagen: {str(e)}"
|
||||
|
||||
def schedule_operation(self, operation: SystemOperation, delay_seconds: int = None,
|
||||
user_id: str = None, reason: str = None, force: bool = False) -> Dict[str, Any]:
|
||||
"""Plant eine System-Operation"""
|
||||
operation_id = f"{operation.value}_{int(time.time())}"
|
||||
|
||||
if not force:
|
||||
safe, message = self.is_safe_to_operate()
|
||||
if not safe:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f"Operation nicht sicher: {message}",
|
||||
'operation_id': None
|
||||
}
|
||||
|
||||
operation_data = {
|
||||
'id': operation_id,
|
||||
'operation': operation,
|
||||
'scheduled_at': datetime.now(),
|
||||
'execute_at': datetime.now() + timedelta(seconds=delay_seconds or 5),
|
||||
'user_id': user_id,
|
||||
'reason': reason or f"Geplante {operation.value}",
|
||||
'force': force,
|
||||
'status': 'scheduled'
|
||||
}
|
||||
|
||||
self.pending_operations[operation_id] = operation_data
|
||||
|
||||
if delay_seconds and delay_seconds > 0:
|
||||
# Verzögerte Ausführung
|
||||
timer = threading.Timer(delay_seconds, self._execute_delayed_operation, [operation_id])
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
operation_data['timer'] = timer
|
||||
else:
|
||||
# Sofortige Ausführung
|
||||
self._execute_delayed_operation(operation_id)
|
||||
|
||||
core_logger.info(f"System-Operation geplant: {operation.value} (ID: {operation_id})")
|
||||
return {
|
||||
'success': True,
|
||||
'message': f"Operation {operation.value} geplant",
|
||||
'operation_id': operation_id,
|
||||
'execute_at': operation_data['execute_at'].isoformat()
|
||||
}
|
||||
|
||||
def _execute_delayed_operation(self, operation_id: str):
|
||||
"""Führt eine verzögerte Operation aus"""
|
||||
if operation_id not in self.pending_operations:
|
||||
return
|
||||
|
||||
operation_data = self.pending_operations[operation_id]
|
||||
operation = operation_data['operation']
|
||||
|
||||
try:
|
||||
operation_data['status'] = 'executing'
|
||||
operation_data['started_at'] = datetime.now()
|
||||
|
||||
core_logger.info(f"Führe System-Operation aus: {operation.value}")
|
||||
|
||||
result = self._execute_operation(operation, operation_data)
|
||||
operation_data.update(result)
|
||||
|
||||
except Exception as e:
|
||||
operation_data['status'] = 'failed'
|
||||
operation_data['error'] = str(e)
|
||||
core_logger.error(f"System-Operation fehlgeschlagen: {operation.value} - {str(e)}")
|
||||
finally:
|
||||
operation_data['completed_at'] = datetime.now()
|
||||
self._move_to_history(operation_id)
|
||||
|
||||
def _execute_operation(self, operation: SystemOperation, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Führt die eigentliche System-Operation aus"""
|
||||
if operation == SystemOperation.RESTART:
|
||||
return self._restart_system(operation_data)
|
||||
elif operation == SystemOperation.SHUTDOWN:
|
||||
return self._shutdown_system(operation_data)
|
||||
elif operation == SystemOperation.KIOSK_RESTART:
|
||||
return self._restart_kiosk(operation_data)
|
||||
elif operation == SystemOperation.EMERGENCY_STOP:
|
||||
return self._emergency_stop(operation_data)
|
||||
else:
|
||||
return {'status': 'failed', 'message': f'Unbekannte Operation: {operation.value}'}
|
||||
|
||||
def _restart_system(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Startet das System neu"""
|
||||
try:
|
||||
self._cleanup_before_restart()
|
||||
core_logger.info("System-Neustart wird eingeleitet...")
|
||||
|
||||
if platform.system() == "Windows":
|
||||
subprocess.run(['shutdown', '/r', '/t', '5'], check=True)
|
||||
else:
|
||||
subprocess.run(['sudo', 'reboot'], check=True)
|
||||
|
||||
return {'status': 'success', 'message': 'System-Neustart eingeleitet'}
|
||||
except Exception as e:
|
||||
return {'status': 'failed', 'message': f'Neustart fehlgeschlagen: {str(e)}'}
|
||||
|
||||
def _shutdown_system(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Fährt das System herunter"""
|
||||
try:
|
||||
self._cleanup_before_restart()
|
||||
core_logger.info("System-Herunterfahren wird eingeleitet...")
|
||||
|
||||
if platform.system() == "Windows":
|
||||
subprocess.run(['shutdown', '/s', '/t', '5'], check=True)
|
||||
else:
|
||||
subprocess.run(['sudo', 'shutdown', 'now'], check=True)
|
||||
|
||||
return {'status': 'success', 'message': 'System-Herunterfahren eingeleitet'}
|
||||
except Exception as e:
|
||||
return {'status': 'failed', 'message': f'Herunterfahren fehlgeschlagen: {str(e)}'}
|
||||
|
||||
def _emergency_stop(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Führt einen Notfall-Stop durch"""
|
||||
try:
|
||||
core_logger.critical("NOTFALL-STOP eingeleitet!")
|
||||
self._force_shutdown_all_threads()
|
||||
self._cleanup_before_restart()
|
||||
os._exit(1)
|
||||
except Exception as e:
|
||||
return {'status': 'failed', 'message': f'Notfall-Stop fehlgeschlagen: {str(e)}'}
|
||||
|
||||
def _cleanup_before_restart(self):
|
||||
"""Bereinigung vor Neustart/Herunterfahren"""
|
||||
try:
|
||||
# Database WAL cleanup
|
||||
from utils.database_core import database_service
|
||||
database_service.cleanup.perform_wal_checkpoint()
|
||||
|
||||
# Clear caches
|
||||
self._clear_caches()
|
||||
|
||||
# Stop all registered components
|
||||
self._shutdown_components()
|
||||
|
||||
core_logger.info("Bereinigung vor Neustart abgeschlossen")
|
||||
except Exception as e:
|
||||
core_logger.error(f"Fehler bei Bereinigung: {str(e)}")
|
||||
|
||||
def _clear_caches(self):
|
||||
"""Löscht System-Caches"""
|
||||
try:
|
||||
cache_dirs = ['/tmp/myp_cache', '/var/cache/myp']
|
||||
for cache_dir in cache_dirs:
|
||||
if os.path.exists(cache_dir):
|
||||
shutil.rmtree(cache_dir, ignore_errors=True)
|
||||
except Exception as e:
|
||||
core_logger.warning(f"Cache-Bereinigung teilweise fehlgeschlagen: {str(e)}")
|
||||
|
||||
# ===== SHUTDOWN MANAGEMENT =====
|
||||
|
||||
def register_component(self, name: str, component: Any, stop_method: str = "stop"):
|
||||
"""Registriert eine Komponente für ordnungsgemäßes Herunterfahren"""
|
||||
self.components[name] = {
|
||||
'component': component,
|
||||
'stop_method': stop_method,
|
||||
'priority': 1
|
||||
}
|
||||
core_logger.debug(f"Komponente '{name}' für Shutdown registriert")
|
||||
|
||||
def register_cleanup_function(self, func: Callable, name: str, priority: int = 1,
|
||||
timeout: int = 10, args: tuple = (), kwargs: dict = None):
|
||||
"""Registriert eine Bereinigungsfunktion"""
|
||||
self.cleanup_functions.append({
|
||||
'function': func,
|
||||
'name': name,
|
||||
'priority': priority,
|
||||
'timeout': timeout,
|
||||
'args': args or (),
|
||||
'kwargs': kwargs or {}
|
||||
})
|
||||
core_logger.debug(f"Cleanup-Funktion '{name}' registriert")
|
||||
|
||||
def shutdown(self, exit_code: int = 0):
|
||||
"""Führt ordnungsgemäßes Herunterfahren durch"""
|
||||
if self.shutdown_requested:
|
||||
return
|
||||
|
||||
self.shutdown_requested = True
|
||||
self.shutdown_time = datetime.now()
|
||||
|
||||
core_logger.info("🔄 Ordnungsgemäßes Herunterfahren wird eingeleitet...")
|
||||
|
||||
self._shutdown_components()
|
||||
self._execute_cleanup_functions()
|
||||
|
||||
core_logger.info("✅ Herunterfahren abgeschlossen")
|
||||
sys.exit(exit_code)
|
||||
|
||||
def _shutdown_components(self):
|
||||
"""Fährt alle registrierten Komponenten herunter"""
|
||||
for name, component_info in self.components.items():
|
||||
try:
|
||||
component = component_info['component']
|
||||
stop_method = component_info['stop_method']
|
||||
|
||||
if hasattr(component, stop_method):
|
||||
getattr(component, stop_method)()
|
||||
core_logger.debug(f"Komponente '{name}' erfolgreich heruntergefahren")
|
||||
else:
|
||||
core_logger.warning(f"Komponente '{name}' hat keine '{stop_method}' Methode")
|
||||
|
||||
except Exception as e:
|
||||
core_logger.error(f"Fehler beim Herunterfahren von '{name}': {str(e)}")
|
||||
|
||||
def _execute_cleanup_functions(self):
|
||||
"""Führt alle Cleanup-Funktionen aus"""
|
||||
# Nach Priorität sortieren
|
||||
sorted_functions = sorted(self.cleanup_functions, key=lambda x: x['priority'])
|
||||
|
||||
for cleanup_info in sorted_functions:
|
||||
try:
|
||||
with self.cleanup_timeout(cleanup_info['timeout'], cleanup_info['name']):
|
||||
cleanup_info['function'](*cleanup_info['args'], **cleanup_info['kwargs'])
|
||||
core_logger.debug(f"Cleanup '{cleanup_info['name']}' erfolgreich")
|
||||
|
||||
except Exception as e:
|
||||
core_logger.error(f"Cleanup '{cleanup_info['name']}' fehlgeschlagen: {str(e)}")
|
||||
|
||||
@contextmanager
|
||||
def cleanup_timeout(self, timeout: int, operation_name: str):
|
||||
"""Context Manager für Timeout bei Cleanup-Operationen"""
|
||||
def timeout_handler(signum, frame):
|
||||
raise TimeoutError(f"Timeout bei {operation_name}")
|
||||
|
||||
if platform.system() != "Windows":
|
||||
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm(timeout)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if platform.system() != "Windows":
|
||||
signal.alarm(0)
|
||||
signal.signal(signal.SIGALRM, old_handler)
|
||||
|
||||
# ===== ERROR RECOVERY =====
|
||||
|
||||
def _init_default_patterns(self):
|
||||
"""Initialisiert Standard-Fehlermuster"""
|
||||
patterns = [
|
||||
ErrorPattern(
|
||||
name="database_lock",
|
||||
patterns=[r"database.*locked", r"sqlite.*busy"],
|
||||
severity=ErrorSeverity.HIGH,
|
||||
actions=[RecoveryAction.CLEAR_CACHE, RecoveryAction.RESTART_COMPONENT],
|
||||
description="Datenbank-Sperren"
|
||||
),
|
||||
ErrorPattern(
|
||||
name="memory_exhaustion",
|
||||
patterns=[r"out of memory", r"memory.*exhausted"],
|
||||
severity=ErrorSeverity.CRITICAL,
|
||||
actions=[RecoveryAction.RESTART_SYSTEM],
|
||||
description="Speicher-Erschöpfung"
|
||||
),
|
||||
ErrorPattern(
|
||||
name="network_timeout",
|
||||
patterns=[r"timeout.*network", r"connection.*timed out"],
|
||||
severity=ErrorSeverity.MEDIUM,
|
||||
actions=[RecoveryAction.RESTART_SERVICE],
|
||||
description="Netzwerk-Timeouts"
|
||||
)
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
self.error_patterns[pattern.name] = pattern
|
||||
|
||||
def _init_recovery_handlers(self):
|
||||
"""Initialisiert Recovery-Handler"""
|
||||
self.recovery_handlers = {
|
||||
RecoveryAction.LOG_ONLY: self._handle_log_only,
|
||||
RecoveryAction.RESTART_SERVICE: self._handle_restart_service,
|
||||
RecoveryAction.RESTART_COMPONENT: self._handle_restart_component,
|
||||
RecoveryAction.CLEAR_CACHE: self._handle_clear_cache,
|
||||
RecoveryAction.RESET_DATABASE: self._handle_reset_database,
|
||||
RecoveryAction.RESTART_SYSTEM: self._handle_restart_system,
|
||||
RecoveryAction.EMERGENCY_STOP: self._handle_emergency_stop
|
||||
}
|
||||
|
||||
def start_error_monitoring(self):
|
||||
"""Startet Error-Monitoring"""
|
||||
if self.monitoring_active:
|
||||
return
|
||||
|
||||
self.monitoring_active = True
|
||||
self.monitoring_thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||
self.monitoring_thread.start()
|
||||
core_logger.info("Error-Monitoring gestartet")
|
||||
|
||||
def stop_error_monitoring(self):
|
||||
"""Stoppt Error-Monitoring"""
|
||||
self.monitoring_active = False
|
||||
if self.monitoring_thread:
|
||||
self.monitoring_thread.join(timeout=5)
|
||||
core_logger.info("Error-Monitoring gestoppt")
|
||||
|
||||
def _monitor_loop(self):
|
||||
"""Haupt-Monitoring-Schleife"""
|
||||
while self.monitoring_active:
|
||||
try:
|
||||
self._check_log_files()
|
||||
self._check_system_metrics()
|
||||
self._cleanup_old_entries()
|
||||
time.sleep(30) # Alle 30 Sekunden prüfen
|
||||
except Exception as e:
|
||||
core_logger.error(f"Monitoring-Fehler: {str(e)}")
|
||||
time.sleep(60) # Bei Fehlern länger warten
|
||||
|
||||
def _check_log_files(self):
|
||||
"""Überprüft Log-Dateien auf Fehlermuster"""
|
||||
try:
|
||||
from utils.settings import LOG_DIR
|
||||
|
||||
if not os.path.exists(LOG_DIR):
|
||||
return
|
||||
|
||||
for log_subdir in os.listdir(LOG_DIR):
|
||||
log_file = os.path.join(LOG_DIR, log_subdir, f"{log_subdir}.log")
|
||||
|
||||
if os.path.exists(log_file):
|
||||
# Nur die letzten 100 Zeilen lesen
|
||||
with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
lines = f.readlines()[-100:]
|
||||
|
||||
for line in lines:
|
||||
self._analyze_log_line(line, log_subdir)
|
||||
|
||||
except Exception as e:
|
||||
core_logger.debug(f"Log-Datei-Prüfung fehlgeschlagen: {str(e)}")
|
||||
|
||||
def _analyze_log_line(self, line: str, source: str):
|
||||
"""Analysiert eine Log-Zeile auf Fehlermuster"""
|
||||
import re
|
||||
|
||||
for pattern_name, pattern in self.error_patterns.items():
|
||||
for regex in pattern.patterns:
|
||||
if re.search(regex, line, re.IGNORECASE):
|
||||
self._handle_error_detection(pattern_name, line, {'source': source})
|
||||
return
|
||||
|
||||
def _handle_error_detection(self, pattern_name: str, error_message: str, context: Dict[str, Any] = None):
|
||||
"""Behandelt erkannte Fehler"""
|
||||
pattern = self.error_patterns[pattern_name]
|
||||
|
||||
# Prüfen, ob zu viele Fehler in kurzer Zeit aufgetreten sind
|
||||
recent_count = self._count_recent_occurrences(pattern_name, pattern.time_window)
|
||||
|
||||
occurrence = ErrorOccurrence(
|
||||
timestamp=datetime.now(),
|
||||
pattern_name=pattern_name,
|
||||
error_message=error_message,
|
||||
severity=pattern.severity,
|
||||
context=context or {}
|
||||
)
|
||||
|
||||
self.error_occurrences.append(occurrence)
|
||||
|
||||
if recent_count >= pattern.max_occurrences:
|
||||
# Eskalation
|
||||
actions = pattern.escalation_actions or [RecoveryAction.EMERGENCY_STOP]
|
||||
core_logger.critical(f"Fehler-Eskalation für {pattern_name}: {recent_count} Vorkommen")
|
||||
else:
|
||||
actions = pattern.actions
|
||||
|
||||
self._execute_recovery_actions(occurrence, actions)
|
||||
|
||||
def _count_recent_occurrences(self, pattern_name: str, time_window: int) -> int:
|
||||
"""Zählt kürzliche Vorkommen eines Fehlermusters"""
|
||||
cutoff_time = datetime.now() - timedelta(seconds=time_window)
|
||||
return len([occ for occ in self.error_occurrences
|
||||
if occ.pattern_name == pattern_name and occ.timestamp > cutoff_time])
|
||||
|
||||
def _execute_recovery_actions(self, occurrence: ErrorOccurrence, actions: List[RecoveryAction]):
|
||||
"""Führt Recovery-Aktionen aus"""
|
||||
for action in actions:
|
||||
if action in self.recovery_handlers:
|
||||
try:
|
||||
success = self.recovery_handlers[action](occurrence)
|
||||
occurrence.recovery_attempted.append(action)
|
||||
if success:
|
||||
occurrence.recovery_successful = True
|
||||
break
|
||||
except Exception as e:
|
||||
core_logger.error(f"Recovery-Aktion {action.value} fehlgeschlagen: {str(e)}")
|
||||
|
||||
def _handle_log_only(self, occurrence: ErrorOccurrence) -> bool:
|
||||
"""Recovery: Nur Logging"""
|
||||
core_logger.warning(f"Fehler erkannt ({occurrence.pattern_name}): {occurrence.error_message}")
|
||||
return True
|
||||
|
||||
def _handle_restart_service(self, occurrence: ErrorOccurrence) -> bool:
|
||||
"""Recovery: Service-Neustart"""
|
||||
try:
|
||||
service_name = occurrence.context.get('service_name', 'myp-https')
|
||||
subprocess.run(['sudo', 'systemctl', 'restart', service_name], check=True)
|
||||
core_logger.info(f"Service {service_name} neu gestartet")
|
||||
return True
|
||||
except Exception as e:
|
||||
core_logger.error(f"Service-Neustart fehlgeschlagen: {str(e)}")
|
||||
return False
|
||||
|
||||
def _handle_clear_cache(self, occurrence: ErrorOccurrence) -> bool:
|
||||
"""Recovery: Cache leeren"""
|
||||
try:
|
||||
self._clear_caches()
|
||||
core_logger.info("Cache erfolgreich geleert")
|
||||
return True
|
||||
except Exception as e:
|
||||
core_logger.error(f"Cache-Bereinigung fehlgeschlagen: {str(e)}")
|
||||
return False
|
||||
|
||||
def _handle_restart_system(self, occurrence: ErrorOccurrence) -> bool:
|
||||
"""Recovery: System-Neustart"""
|
||||
try:
|
||||
self.schedule_operation(SystemOperation.RESTART, delay_seconds=30,
|
||||
reason=f"Error Recovery: {occurrence.pattern_name}", force=True)
|
||||
return True
|
||||
except Exception as e:
|
||||
core_logger.error(f"System-Neustart fehlgeschlagen: {str(e)}")
|
||||
return False
|
||||
|
||||
def _handle_emergency_stop(self, occurrence: ErrorOccurrence) -> bool:
|
||||
"""Recovery: Notfall-Stop"""
|
||||
core_logger.critical(f"NOTFALL-STOP wegen: {occurrence.pattern_name}")
|
||||
self._emergency_stop({})
|
||||
return True
|
||||
|
||||
# ===== WINDOWS-SPEZIFISCHE FIXES =====
|
||||
|
||||
def _apply_platform_fixes(self):
|
||||
"""Wendet plattformspezifische Fixes an"""
|
||||
if platform.system() == "Windows":
|
||||
self._apply_windows_fixes()
|
||||
else:
|
||||
self._apply_unix_fixes()
|
||||
|
||||
def _apply_windows_fixes(self):
|
||||
"""Windows-spezifische Fixes"""
|
||||
try:
|
||||
# UTF-8 Encoding
|
||||
import locale
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, 'German_Germany.1252')
|
||||
except:
|
||||
pass
|
||||
|
||||
# Socket-Fixes
|
||||
import socket
|
||||
if hasattr(socket, 'SO_REUSEADDR'):
|
||||
original_bind = socket.socket.bind
|
||||
def windows_bind_with_reuse(self, address):
|
||||
self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
return original_bind(self, address)
|
||||
socket.socket.bind = windows_bind_with_reuse
|
||||
|
||||
core_logger.info("Windows-Fixes angewendet")
|
||||
except Exception as e:
|
||||
core_logger.warning(f"Windows-Fixes teilweise fehlgeschlagen: {str(e)}")
|
||||
|
||||
def _apply_unix_fixes(self):
|
||||
"""Unix-spezifische Optimierungen"""
|
||||
try:
|
||||
# Umgebungsvariablen setzen
|
||||
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
|
||||
core_logger.debug("Unix-Optimierungen angewendet")
|
||||
except Exception as e:
|
||||
core_logger.warning(f"Unix-Optimierungen fehlgeschlagen: {str(e)}")
|
||||
|
||||
# ===== THREAD MANAGEMENT =====
|
||||
|
||||
def register_thread(self, thread: threading.Thread):
|
||||
"""Registriert einen Thread für sauberes Shutdown"""
|
||||
self.registered_threads.append(thread)
|
||||
core_logger.debug(f"Thread registriert: {thread.name}")
|
||||
|
||||
def register_cleanup_callback(self, func: Callable):
|
||||
"""Registriert Callback für Cleanup"""
|
||||
self.cleanup_callbacks.append(func)
|
||||
|
||||
def _force_shutdown_all_threads(self):
|
||||
"""Erzwingt Shutdown aller registrierten Threads"""
|
||||
for thread in self.registered_threads:
|
||||
if thread.is_alive():
|
||||
try:
|
||||
# Versuche graceful shutdown
|
||||
if hasattr(thread, 'stop'):
|
||||
thread.stop()
|
||||
|
||||
thread.join(timeout=2)
|
||||
|
||||
if thread.is_alive():
|
||||
core_logger.warning(f"Thread {thread.name} reagiert nicht - erzwinge Beendigung")
|
||||
|
||||
except Exception as e:
|
||||
core_logger.error(f"Fehler beim Thread-Shutdown: {str(e)}")
|
||||
|
||||
# Cleanup-Callbacks ausführen
|
||||
for callback in self.cleanup_callbacks:
|
||||
try:
|
||||
callback()
|
||||
except Exception as e:
|
||||
core_logger.error(f"Cleanup-Callback fehlgeschlagen: {str(e)}")
|
||||
|
||||
# ===== SIGNAL HANDLING =====
|
||||
|
||||
def _register_signal_handlers(self):
|
||||
"""Registriert Signal-Handler für verschiedene Signale"""
|
||||
if platform.system() != "Windows":
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
signal.signal(signal.SIGHUP, self._signal_handler)
|
||||
else:
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
|
||||
def _signal_handler(self, signum, frame):
|
||||
"""Handler für System-Signale"""
|
||||
signal_names = {2: 'SIGINT', 15: 'SIGTERM', 1: 'SIGHUP'}
|
||||
signal_name = signal_names.get(signum, f'Signal {signum}')
|
||||
|
||||
core_logger.info(f"📡 {signal_name} empfangen - leite ordnungsgemäßes Herunterfahren ein")
|
||||
self.shutdown(0)
|
||||
|
||||
def _cleanup_old_entries(self):
|
||||
"""Bereinigt alte Error-Occurrences"""
|
||||
cutoff_time = datetime.now() - timedelta(hours=24)
|
||||
self.error_occurrences = [
|
||||
occ for occ in self.error_occurrences
|
||||
if occ.timestamp > cutoff_time
|
||||
]
|
||||
|
||||
def _move_to_history(self, operation_id: str):
|
||||
"""Verschiebt Operation in Historie"""
|
||||
if operation_id in self.pending_operations:
|
||||
operation = self.pending_operations.pop(operation_id)
|
||||
self.operation_history.append(operation)
|
||||
|
||||
# Nur die letzten 50 Operationen behalten
|
||||
self.operation_history = self.operation_history[-50:]
|
||||
|
||||
# ===== STATUS & MONITORING =====
|
||||
|
||||
def get_system_status(self) -> Dict[str, Any]:
|
||||
"""Gibt umfassenden System-Status zurück"""
|
||||
return {
|
||||
'shutdown_requested': self.shutdown_requested,
|
||||
'shutdown_time': self.shutdown_time.isoformat() if self.shutdown_time else None,
|
||||
'registered_components': len(self.components),
|
||||
'cleanup_functions': len(self.cleanup_functions),
|
||||
'pending_operations': len(self.pending_operations),
|
||||
'error_monitoring_active': self.monitoring_active,
|
||||
'error_patterns': len(self.error_patterns),
|
||||
'recent_errors': len([occ for occ in self.error_occurrences
|
||||
if occ.timestamp > datetime.now() - timedelta(hours=1)]),
|
||||
'registered_threads': len(self.registered_threads),
|
||||
'platform': platform.system(),
|
||||
'platform_fixes_applied': True
|
||||
}
|
||||
|
||||
# ===== GLOBALE INSTANZ =====
|
||||
|
||||
# Singleton-Pattern für Core System Manager
|
||||
_core_system_manager = None
|
||||
|
||||
def get_core_system_manager(timeout: int = 30) -> CoreSystemManager:
|
||||
"""Gibt die globale CoreSystemManager-Instanz zurück"""
|
||||
global _core_system_manager
|
||||
if _core_system_manager is None:
|
||||
_core_system_manager = CoreSystemManager(timeout)
|
||||
return _core_system_manager
|
||||
|
||||
# ===== CONVENIENCE FUNCTIONS =====
|
||||
|
||||
def schedule_system_restart(delay_seconds: int = 60, user_id: str = None,
|
||||
reason: str = None, force: bool = False) -> Dict[str, Any]:
|
||||
"""Convenience-Funktion für System-Neustart"""
|
||||
manager = get_core_system_manager()
|
||||
return manager.schedule_operation(SystemOperation.RESTART, delay_seconds, user_id, reason, force)
|
||||
|
||||
def schedule_system_shutdown(delay_seconds: int = 30, user_id: str = None,
|
||||
reason: str = None, force: bool = False) -> Dict[str, Any]:
|
||||
"""Convenience-Funktion für System-Herunterfahren"""
|
||||
manager = get_core_system_manager()
|
||||
return manager.schedule_operation(SystemOperation.SHUTDOWN, delay_seconds, user_id, reason, force)
|
||||
|
||||
def register_for_shutdown(component_or_function, name: str,
|
||||
component_stop_method: str = "stop", priority: int = 1,
|
||||
timeout: int = 10):
|
||||
"""Registriert Komponente oder Funktion für Shutdown"""
|
||||
manager = get_core_system_manager()
|
||||
|
||||
if callable(component_or_function):
|
||||
manager.register_cleanup_function(
|
||||
component_or_function, name, priority, timeout
|
||||
)
|
||||
else:
|
||||
manager.register_component(name, component_or_function, component_stop_method)
|
||||
|
||||
def shutdown_application(exit_code: int = 0):
|
||||
"""Fährt die Anwendung ordnungsgemäß herunter"""
|
||||
manager = get_core_system_manager()
|
||||
manager.shutdown(exit_code)
|
||||
|
||||
def start_error_monitoring():
|
||||
"""Startet Error-Monitoring"""
|
||||
manager = get_core_system_manager()
|
||||
manager.start_error_monitoring()
|
||||
|
||||
def stop_error_monitoring():
|
||||
"""Stoppt Error-Monitoring"""
|
||||
manager = get_core_system_manager()
|
||||
manager.stop_error_monitoring()
|
||||
|
||||
def get_system_status() -> Dict[str, Any]:
|
||||
"""Gibt System-Status zurück"""
|
||||
manager = get_core_system_manager()
|
||||
return manager.get_system_status()
|
||||
|
||||
def is_shutdown_requested() -> bool:
|
||||
"""Prüft, ob Shutdown angefordert wurde"""
|
||||
manager = get_core_system_manager()
|
||||
return manager.shutdown_requested
|
||||
|
||||
# ===== INITIALIZATION =====
|
||||
|
||||
# Auto-Initialisierung beim Import
|
||||
core_logger.info("🔧 Core System Manager wird initialisiert...")
|
||||
|
||||
# Legacy-Kompatibilität: Alte Funktionen automatisch verfügbar machen
|
||||
def apply_all_windows_fixes():
|
||||
"""Legacy-Kompatibilität für Windows-Fixes"""
|
||||
manager = get_core_system_manager()
|
||||
if platform.system() == "Windows":
|
||||
manager._apply_windows_fixes()
|
||||
|
||||
def fix_windows_socket_issues():
|
||||
"""Legacy-Kompatibilität für Socket-Fixes"""
|
||||
apply_all_windows_fixes()
|
||||
|
||||
# CLI Interface
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
manager = get_core_system_manager()
|
||||
|
||||
if command == "status":
|
||||
status = manager.get_system_status()
|
||||
print("=== Core System Status ===")
|
||||
for key, value in status.items():
|
||||
print(f"{key}: {value}")
|
||||
elif command == "restart":
|
||||
result = schedule_system_restart(delay_seconds=10)
|
||||
print(f"Restart scheduled: {result}")
|
||||
elif command == "monitor":
|
||||
print("Starting error monitoring...")
|
||||
start_error_monitoring()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping monitoring...")
|
||||
stop_error_monitoring()
|
||||
else:
|
||||
print("Available commands: status, restart, monitor")
|
||||
else:
|
||||
print("Core System Manager - Available commands: status, restart, monitor")
|
509
backend/utils/hardware_integration.py
Normal file
509
backend/utils/hardware_integration.py
Normal file
@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env python3.11
|
||||
"""
|
||||
Hardware Integration - Konsolidierte Hardware-Steuerung
|
||||
====================================================
|
||||
|
||||
Konsolidiert alle Hardware-Integration-Funktionalitäten:
|
||||
- TP-Link Tapo Controller (tapo_controller.py)
|
||||
- Printer Monitor (printer_monitor.py)
|
||||
|
||||
Migration: 2 Dateien → 1 Datei
|
||||
Autor: MYP Team - Konsolidierung für IHK-Projektarbeit
|
||||
Datum: 2025-06-09
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import socket
|
||||
import subprocess
|
||||
import platform
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
# Logger
|
||||
hardware_logger = get_logger("hardware_integration")
|
||||
|
||||
# ===== TAPO SMART PLUG CONTROLLER =====
|
||||
|
||||
class TapoController:
|
||||
"""TP-Link Tapo Smart Plug Controller"""
|
||||
|
||||
def __init__(self):
|
||||
self.default_username = "till.tomczak@mercedes-benz.com"
|
||||
self.default_password = "744563017196A"
|
||||
hardware_logger.info("🔌 Tapo Controller initialisiert")
|
||||
|
||||
def toggle_plug(self, ip: str, state: bool, username: str = None, password: str = None) -> bool:
|
||||
"""Schaltet eine Tapo-Steckdose ein oder aus"""
|
||||
try:
|
||||
from PyP100 import PyP110
|
||||
|
||||
username = username or self.default_username
|
||||
password = password or self.default_password
|
||||
|
||||
p110 = PyP110.P110(ip, username, password)
|
||||
p110.handshake()
|
||||
p110.login()
|
||||
|
||||
if state:
|
||||
p110.turnOn()
|
||||
hardware_logger.info(f"✅ Tapo {ip} eingeschaltet")
|
||||
else:
|
||||
p110.turnOff()
|
||||
hardware_logger.info(f"❌ Tapo {ip} ausgeschaltet")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Tapo-Steuerung fehlgeschlagen {ip}: {str(e)}")
|
||||
return False
|
||||
|
||||
def check_outlet_status(self, ip: str, username: str = None, password: str = None,
|
||||
printer_id: int = None) -> Tuple[bool, str]:
|
||||
"""Prüft den Status einer Tapo-Steckdose"""
|
||||
try:
|
||||
from PyP100 import PyP110
|
||||
|
||||
username = username or self.default_username
|
||||
password = password or self.default_password
|
||||
|
||||
p110 = PyP110.P110(ip, username, password)
|
||||
p110.handshake()
|
||||
p110.login()
|
||||
|
||||
device_info = p110.getDeviceInfo()
|
||||
is_on = device_info.get('device_on', False)
|
||||
status = "Ein" if is_on else "Aus"
|
||||
|
||||
return is_on, status
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Tapo-Status-Prüfung fehlgeschlagen {ip}: {str(e)}")
|
||||
return False, "Fehler"
|
||||
|
||||
# ===== PRINTER MONITOR =====
|
||||
|
||||
class PrinterMonitor:
|
||||
"""3D-Drucker Monitoring System"""
|
||||
|
||||
def __init__(self):
|
||||
self.tapo_controller = TapoController()
|
||||
self.status_cache = {}
|
||||
self.cache_timeout = timedelta(minutes=2)
|
||||
self.last_cache_update = None
|
||||
hardware_logger.info("🖨️ Printer Monitor initialisiert")
|
||||
|
||||
def get_live_printer_status(self, use_session_cache: bool = True) -> Dict[int, Dict]:
|
||||
"""Holt Live-Status aller Drucker"""
|
||||
try:
|
||||
# Cache-Prüfung
|
||||
if (use_session_cache and self.last_cache_update and
|
||||
datetime.now() - self.last_cache_update < self.cache_timeout):
|
||||
return self.status_cache
|
||||
|
||||
# Fresh status fetch
|
||||
status = self._fetch_live_printer_status()
|
||||
|
||||
# Cache aktualisieren
|
||||
self.status_cache = status
|
||||
self.last_cache_update = datetime.now()
|
||||
|
||||
return status
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Live-Status-Abfrage fehlgeschlagen: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _fetch_live_printer_status(self) -> Dict[int, Dict]:
|
||||
"""Holt tatsächlichen Live-Status"""
|
||||
try:
|
||||
from models import get_cached_session, Printer
|
||||
|
||||
status_dict = {}
|
||||
|
||||
with get_cached_session() as session:
|
||||
printers = session.query(Printer).filter(Printer.active == True).all()
|
||||
|
||||
for printer in printers:
|
||||
status = self._check_single_printer_status(printer)
|
||||
status_dict[printer.id] = status
|
||||
|
||||
hardware_logger.debug(f"Status für {len(status_dict)} Drucker abgerufen")
|
||||
return status_dict
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Drucker-Status-Abfrage fehlgeschlagen: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _check_single_printer_status(self, printer) -> Dict:
|
||||
"""Prüft den Status eines einzelnen Druckers"""
|
||||
status = {
|
||||
'printer_id': printer.id,
|
||||
'name': printer.name,
|
||||
'ip_address': printer.ip_address,
|
||||
'plug_ip': printer.plug_ip,
|
||||
'ping_status': 'unknown',
|
||||
'outlet_status': 'unknown',
|
||||
'outlet_on': False,
|
||||
'last_checked': datetime.now(),
|
||||
'error_message': None
|
||||
}
|
||||
|
||||
try:
|
||||
# Ping-Test
|
||||
if printer.ip_address:
|
||||
ping_ok = self._ping_address(printer.ip_address)
|
||||
status['ping_status'] = 'online' if ping_ok else 'offline'
|
||||
|
||||
# Outlet-Status prüfen
|
||||
if printer.plug_ip:
|
||||
outlet_on, outlet_status = self.tapo_controller.check_outlet_status(
|
||||
printer.plug_ip, printer.plug_username, printer.plug_password, printer.id
|
||||
)
|
||||
status['outlet_on'] = outlet_on
|
||||
status['outlet_status'] = outlet_status
|
||||
|
||||
# Gesamtstatus bestimmen
|
||||
if status['ping_status'] == 'online' and status['outlet_on']:
|
||||
status['overall_status'] = 'online'
|
||||
elif status['outlet_on']:
|
||||
status['overall_status'] = 'booting'
|
||||
else:
|
||||
status['overall_status'] = 'offline'
|
||||
|
||||
except Exception as e:
|
||||
status['error_message'] = str(e)
|
||||
status['overall_status'] = 'error'
|
||||
hardware_logger.error(f"Drucker-Status-Prüfung fehlgeschlagen {printer.name}: {str(e)}")
|
||||
|
||||
return status
|
||||
|
||||
def _ping_address(self, ip_address: str, timeout: int = 3) -> bool:
|
||||
"""Pingt eine IP-Adresse an"""
|
||||
try:
|
||||
if platform.system().lower() == "windows":
|
||||
result = subprocess.run(
|
||||
['ping', '-n', '1', '-w', str(timeout * 1000), ip_address],
|
||||
capture_output=True, text=True, timeout=timeout + 2,
|
||||
encoding='utf-8', errors='replace'
|
||||
)
|
||||
else:
|
||||
result = subprocess.run(
|
||||
['ping', '-c', '1', '-W', str(timeout), ip_address],
|
||||
capture_output=True, text=True, timeout=timeout + 2,
|
||||
encoding='utf-8', errors='replace'
|
||||
)
|
||||
|
||||
return result.returncode == 0
|
||||
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
except Exception as e:
|
||||
hardware_logger.debug(f"Ping fehlgeschlagen {ip_address}: {str(e)}")
|
||||
return False
|
||||
|
||||
def initialize_all_outlets_on_startup(self) -> Dict[str, bool]:
|
||||
"""Initialisiert alle Outlets beim Systemstart"""
|
||||
try:
|
||||
from models import get_cached_session, Printer
|
||||
|
||||
results = {}
|
||||
|
||||
with get_cached_session() as session:
|
||||
printers = session.query(Printer).filter(
|
||||
Printer.active == True,
|
||||
Printer.plug_ip.isnot(None)
|
||||
).all()
|
||||
|
||||
hardware_logger.info(f"Initialisiere {len(printers)} Drucker-Outlets...")
|
||||
|
||||
for printer in printers:
|
||||
try:
|
||||
# Outlet ausschalten als Standardzustand
|
||||
success = self.tapo_controller.toggle_plug(
|
||||
printer.plug_ip, False,
|
||||
printer.plug_username, printer.plug_password
|
||||
)
|
||||
results[printer.name] = success
|
||||
|
||||
if success:
|
||||
hardware_logger.info(f"✅ {printer.name} Outlet initialisiert (AUS)")
|
||||
else:
|
||||
hardware_logger.warning(f"⚠️ {printer.name} Outlet-Initialisierung fehlgeschlagen")
|
||||
|
||||
except Exception as e:
|
||||
results[printer.name] = False
|
||||
hardware_logger.error(f"❌ {printer.name} Outlet-Fehler: {str(e)}")
|
||||
|
||||
success_count = sum(1 for success in results.values() if success)
|
||||
hardware_logger.info(f"Outlet-Initialisierung: {success_count}/{len(results)} erfolgreich")
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Outlet-Initialisierung fehlgeschlagen: {str(e)}")
|
||||
return {}
|
||||
|
||||
def get_printer_summary(self) -> Dict[str, int]:
|
||||
"""Gibt eine Zusammenfassung des Drucker-Status zurück"""
|
||||
try:
|
||||
status_dict = self.get_live_printer_status()
|
||||
|
||||
summary = {
|
||||
'total': len(status_dict),
|
||||
'online': 0,
|
||||
'offline': 0,
|
||||
'booting': 0,
|
||||
'error': 0,
|
||||
'outlets_on': 0,
|
||||
'outlets_off': 0
|
||||
}
|
||||
|
||||
for printer_status in status_dict.values():
|
||||
overall_status = printer_status.get('overall_status', 'unknown')
|
||||
|
||||
if overall_status in summary:
|
||||
summary[overall_status] += 1
|
||||
|
||||
if printer_status.get('outlet_on'):
|
||||
summary['outlets_on'] += 1
|
||||
else:
|
||||
summary['outlets_off'] += 1
|
||||
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Drucker-Zusammenfassung fehlgeschlagen: {str(e)}")
|
||||
return {'total': 0, 'online': 0, 'offline': 0, 'error': 1}
|
||||
|
||||
def auto_discover_tapo_outlets(self) -> Dict[str, bool]:
|
||||
"""Automatische Erkennung von Tapo-Outlets im Netzwerk"""
|
||||
try:
|
||||
discovered = {}
|
||||
|
||||
# Standard-IP-Bereiche scannen
|
||||
base_ips = ["192.168.0.", "192.168.1.", "192.168.100."]
|
||||
|
||||
for base_ip in base_ips:
|
||||
for i in range(100, 110): # Scan 100-109
|
||||
ip = f"{base_ip}{i}"
|
||||
|
||||
try:
|
||||
# Schneller TCP-Port-Test
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(1)
|
||||
result = sock.connect_ex((ip, 80))
|
||||
sock.close()
|
||||
|
||||
if result == 0:
|
||||
# Versuche Tapo-Verbindung
|
||||
is_on, status = self.tapo_controller.check_outlet_status(ip)
|
||||
if status != "Fehler":
|
||||
discovered[ip] = True
|
||||
hardware_logger.info(f"🔍 Tapo-Outlet entdeckt: {ip}")
|
||||
|
||||
except Exception:
|
||||
pass # Ignoriere Fehler beim Scannen
|
||||
|
||||
hardware_logger.info(f"Auto-Discovery: {len(discovered)} Tapo-Outlets gefunden")
|
||||
return discovered
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Auto-Discovery fehlgeschlagen: {str(e)}")
|
||||
return {}
|
||||
|
||||
def clear_all_caches(self):
|
||||
"""Löscht alle Caches"""
|
||||
self.status_cache.clear()
|
||||
self.last_cache_update = None
|
||||
hardware_logger.info("Alle Hardware-Caches geleert")
|
||||
|
||||
# ===== HARDWARE INTEGRATION MANAGER =====
|
||||
|
||||
class HardwareIntegrationManager:
|
||||
"""Zentraler Manager für alle Hardware-Integrationen"""
|
||||
|
||||
def __init__(self):
|
||||
self.tapo_controller = TapoController()
|
||||
self.printer_monitor = PrinterMonitor()
|
||||
self.monitoring_active = False
|
||||
self.monitoring_thread = None
|
||||
hardware_logger.info("🔧 Hardware Integration Manager initialisiert")
|
||||
|
||||
def start_monitoring(self):
|
||||
"""Startet Hardware-Monitoring"""
|
||||
if self.monitoring_active:
|
||||
return
|
||||
|
||||
self.monitoring_active = True
|
||||
self.monitoring_thread = threading.Thread(target=self._monitoring_loop, daemon=True)
|
||||
self.monitoring_thread.start()
|
||||
hardware_logger.info("Hardware-Monitoring gestartet")
|
||||
|
||||
def stop_monitoring(self):
|
||||
"""Stoppt Hardware-Monitoring"""
|
||||
self.monitoring_active = False
|
||||
if self.monitoring_thread:
|
||||
self.monitoring_thread.join(timeout=5)
|
||||
hardware_logger.info("Hardware-Monitoring gestoppt")
|
||||
|
||||
def _monitoring_loop(self):
|
||||
"""Hauptschleife für Hardware-Monitoring"""
|
||||
while self.monitoring_active:
|
||||
try:
|
||||
# Drucker-Status aktualisieren
|
||||
self.printer_monitor.get_live_printer_status(use_session_cache=False)
|
||||
|
||||
# 60 Sekunden warten
|
||||
for _ in range(60):
|
||||
if not self.monitoring_active:
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Hardware-Monitoring-Fehler: {str(e)}")
|
||||
time.sleep(30) # Bei Fehlern länger warten
|
||||
|
||||
def get_hardware_status(self) -> Dict[str, Any]:
|
||||
"""Gibt umfassenden Hardware-Status zurück"""
|
||||
try:
|
||||
printer_summary = self.printer_monitor.get_printer_summary()
|
||||
|
||||
return {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'monitoring_active': self.monitoring_active,
|
||||
'printer_summary': printer_summary,
|
||||
'tapo_controller_active': True,
|
||||
'cache_status': {
|
||||
'last_update': self.printer_monitor.last_cache_update.isoformat()
|
||||
if self.printer_monitor.last_cache_update else None,
|
||||
'cached_printers': len(self.printer_monitor.status_cache)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Hardware-Status-Abfrage fehlgeschlagen: {str(e)}")
|
||||
return {'error': str(e), 'timestamp': datetime.now().isoformat()}
|
||||
|
||||
def initialize_system(self) -> Dict[str, Any]:
|
||||
"""Initialisiert das gesamte Hardware-System"""
|
||||
results = {
|
||||
'outlet_initialization': {},
|
||||
'auto_discovery': {},
|
||||
'monitoring_started': False
|
||||
}
|
||||
|
||||
try:
|
||||
# Outlets initialisieren
|
||||
results['outlet_initialization'] = self.printer_monitor.initialize_all_outlets_on_startup()
|
||||
|
||||
# Auto-Discovery
|
||||
results['auto_discovery'] = self.printer_monitor.auto_discover_tapo_outlets()
|
||||
|
||||
# Monitoring starten
|
||||
self.start_monitoring()
|
||||
results['monitoring_started'] = True
|
||||
|
||||
hardware_logger.info("Hardware-System erfolgreich initialisiert")
|
||||
|
||||
except Exception as e:
|
||||
hardware_logger.error(f"Hardware-System-Initialisierung fehlgeschlagen: {str(e)}")
|
||||
results['error'] = str(e)
|
||||
|
||||
return results
|
||||
|
||||
# ===== GLOBALE INSTANZ =====
|
||||
|
||||
# Singleton für Hardware Integration Manager
|
||||
_hardware_manager = None
|
||||
|
||||
def get_hardware_manager() -> HardwareIntegrationManager:
|
||||
"""Gibt die globale Hardware-Manager-Instanz zurück"""
|
||||
global _hardware_manager
|
||||
if _hardware_manager is None:
|
||||
_hardware_manager = HardwareIntegrationManager()
|
||||
return _hardware_manager
|
||||
|
||||
# ===== CONVENIENCE FUNCTIONS =====
|
||||
|
||||
def toggle_plug(ip: str, state: bool, username: str = None, password: str = None) -> bool:
|
||||
"""Convenience-Funktion für Tapo-Steuerung"""
|
||||
manager = get_hardware_manager()
|
||||
return manager.tapo_controller.toggle_plug(ip, state, username, password)
|
||||
|
||||
def get_printer_status(printer_id: int = None) -> Dict:
|
||||
"""Convenience-Funktion für Drucker-Status"""
|
||||
manager = get_hardware_manager()
|
||||
all_status = manager.printer_monitor.get_live_printer_status()
|
||||
|
||||
if printer_id:
|
||||
return all_status.get(printer_id, {})
|
||||
return all_status
|
||||
|
||||
def get_hardware_summary() -> Dict[str, Any]:
|
||||
"""Convenience-Funktion für Hardware-Zusammenfassung"""
|
||||
manager = get_hardware_manager()
|
||||
return manager.get_hardware_status()
|
||||
|
||||
def initialize_hardware_system() -> Dict[str, Any]:
|
||||
"""Convenience-Funktion für Hardware-Initialisierung"""
|
||||
manager = get_hardware_manager()
|
||||
return manager.initialize_system()
|
||||
|
||||
# Legacy-Kompatibilität
|
||||
def check_outlet_status(ip: str, username: str = None, password: str = None,
|
||||
printer_id: int = None) -> Tuple[bool, str]:
|
||||
"""Legacy-Kompatibilität für Outlet-Status"""
|
||||
manager = get_hardware_manager()
|
||||
return manager.tapo_controller.check_outlet_status(ip, username, password, printer_id)
|
||||
|
||||
def auto_discover_tapo_outlets() -> Dict[str, bool]:
|
||||
"""Legacy-Kompatibilität für Auto-Discovery"""
|
||||
manager = get_hardware_manager()
|
||||
return manager.printer_monitor.auto_discover_tapo_outlets()
|
||||
|
||||
def initialize_all_outlets() -> Dict[str, bool]:
|
||||
"""Legacy-Kompatibilität für Outlet-Initialisierung"""
|
||||
manager = get_hardware_manager()
|
||||
return manager.printer_monitor.initialize_all_outlets_on_startup()
|
||||
|
||||
# CLI Interface
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
manager = get_hardware_manager()
|
||||
|
||||
if command == "status":
|
||||
status = manager.get_hardware_status()
|
||||
print("=== Hardware Status ===")
|
||||
for key, value in status.items():
|
||||
print(f"{key}: {value}")
|
||||
|
||||
elif command == "discover":
|
||||
print("Starte Auto-Discovery...")
|
||||
discovered = manager.printer_monitor.auto_discover_tapo_outlets()
|
||||
print(f"Gefundene Outlets: {discovered}")
|
||||
|
||||
elif command == "init":
|
||||
print("Initialisiere Hardware-System...")
|
||||
results = manager.initialize_system()
|
||||
print(f"Initialisierung: {results}")
|
||||
|
||||
elif command == "monitor":
|
||||
print("Starte Hardware-Monitoring...")
|
||||
manager.start_monitoring()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(10)
|
||||
status = manager.get_hardware_status()
|
||||
print(f"Status: {status['printer_summary']}")
|
||||
except KeyboardInterrupt:
|
||||
print("Stoppe Monitoring...")
|
||||
manager.stop_monitoring()
|
||||
else:
|
||||
print("Available commands: status, discover, init, monitor")
|
||||
else:
|
||||
print("Hardware Integration Manager - Available commands: status, discover, init, monitor")
|
@ -1,442 +0,0 @@
|
||||
#!/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
|
@ -1,658 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Robuste System-Control-Funktionen für wartungsfreien Produktionsbetrieb
|
||||
Bietet sichere Restart-, Shutdown- und Kiosk-Verwaltungsfunktionen
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import signal
|
||||
import psutil
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
|
||||
# Logging-Setup
|
||||
try:
|
||||
from utils.logging_config import get_logger
|
||||
system_logger = get_logger("system_control")
|
||||
except ImportError:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
system_logger = logging.getLogger("system_control")
|
||||
|
||||
|
||||
class SystemOperation(Enum):
|
||||
"""Verfügbare System-Operationen"""
|
||||
RESTART = "restart"
|
||||
SHUTDOWN = "shutdown"
|
||||
KIOSK_RESTART = "kiosk_restart"
|
||||
KIOSK_ENABLE = "kiosk_enable"
|
||||
KIOSK_DISABLE = "kiosk_disable"
|
||||
SERVICE_RESTART = "service_restart"
|
||||
EMERGENCY_STOP = "emergency_stop"
|
||||
|
||||
|
||||
class SystemControlManager:
|
||||
"""
|
||||
Zentraler Manager für alle System-Control-Operationen.
|
||||
Bietet sichere und robuste Funktionen für wartungsfreien Betrieb.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_windows = os.name == 'nt'
|
||||
self.pending_operations: Dict[str, Dict] = {}
|
||||
self.operation_history: List[Dict] = []
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# Konfiguration
|
||||
self.config = {
|
||||
"restart_delay": 60, # Sekunden
|
||||
"shutdown_delay": 30, # Sekunden
|
||||
"kiosk_restart_delay": 10, # Sekunden
|
||||
"max_operation_history": 100,
|
||||
"safety_checks": True,
|
||||
"require_confirmation": True
|
||||
}
|
||||
|
||||
# Service-Namen für verschiedene Plattformen
|
||||
self.services = {
|
||||
"https": "myp-https.service",
|
||||
"kiosk": "myp-kiosk.service",
|
||||
"watchdog": "kiosk-watchdog.service"
|
||||
}
|
||||
|
||||
system_logger.info("🔧 System-Control-Manager initialisiert")
|
||||
|
||||
def is_safe_to_operate(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
Prüft ob System-Operationen sicher ausgeführt werden können.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (is_safe, reason)
|
||||
"""
|
||||
try:
|
||||
# Prüfe Systemlast
|
||||
load_avg = psutil.getloadavg()[0] if hasattr(psutil, 'getloadavg') else 0
|
||||
if load_avg > 2.0:
|
||||
return False, f"Hohe Systemlast: {load_avg:.2f}"
|
||||
|
||||
# Prüfe verfügbaren Speicher
|
||||
memory = psutil.virtual_memory()
|
||||
if memory.percent > 90:
|
||||
return False, f"Wenig verfügbarer Speicher: {memory.percent:.1f}% belegt"
|
||||
|
||||
# Prüfe aktive Drucker-Jobs
|
||||
try:
|
||||
from models import get_db_session, Job
|
||||
db_session = get_db_session()
|
||||
active_jobs = db_session.query(Job).filter(
|
||||
Job.status.in_(["printing", "queued", "preparing"])
|
||||
).count()
|
||||
db_session.close()
|
||||
|
||||
if active_jobs > 0:
|
||||
return False, f"Aktive Druckjobs: {active_jobs}"
|
||||
except Exception as e:
|
||||
system_logger.warning(f"Job-Prüfung fehlgeschlagen: {e}")
|
||||
|
||||
# Prüfe kritische Prozesse
|
||||
critical_processes = ["chromium", "firefox", "python"]
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']):
|
||||
try:
|
||||
if any(crit in proc.info['name'].lower() for crit in critical_processes):
|
||||
if proc.info['cpu_percent'] > 80:
|
||||
return False, f"Kritischer Prozess unter hoher Last: {proc.info['name']}"
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
return True, "System ist sicher für Operationen"
|
||||
|
||||
except Exception as e:
|
||||
system_logger.error(f"Fehler bei Sicherheitsprüfung: {e}")
|
||||
return False, f"Sicherheitsprüfung fehlgeschlagen: {e}"
|
||||
|
||||
def schedule_operation(self,
|
||||
operation: SystemOperation,
|
||||
delay_seconds: int = None,
|
||||
user_id: str = None,
|
||||
reason: str = None,
|
||||
force: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Plant eine System-Operation mit Verzögerung.
|
||||
|
||||
Args:
|
||||
operation: Art der Operation
|
||||
delay_seconds: Verzögerung in Sekunden (None = Standard)
|
||||
user_id: ID des anfragenden Benutzers
|
||||
reason: Grund für die Operation
|
||||
force: Sicherheitsprüfungen überspringen
|
||||
|
||||
Returns:
|
||||
Dict mit Operation-Details
|
||||
"""
|
||||
with self.lock:
|
||||
# Sicherheitsprüfung (außer bei Force)
|
||||
if not force and self.config["safety_checks"]:
|
||||
is_safe, safety_reason = self.is_safe_to_operate()
|
||||
if not is_safe:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Operation abgelehnt: {safety_reason}",
|
||||
"safety_check": False
|
||||
}
|
||||
|
||||
# Standard-Verzögerung setzen
|
||||
if delay_seconds is None:
|
||||
delay_seconds = {
|
||||
SystemOperation.RESTART: self.config["restart_delay"],
|
||||
SystemOperation.SHUTDOWN: self.config["shutdown_delay"],
|
||||
SystemOperation.KIOSK_RESTART: self.config["kiosk_restart_delay"],
|
||||
SystemOperation.KIOSK_ENABLE: 5,
|
||||
SystemOperation.KIOSK_DISABLE: 5,
|
||||
SystemOperation.SERVICE_RESTART: 10,
|
||||
SystemOperation.EMERGENCY_STOP: 0
|
||||
}.get(operation, 30)
|
||||
|
||||
# Operations-ID generieren
|
||||
operation_id = f"{operation.value}_{int(time.time())}"
|
||||
scheduled_time = datetime.now() + timedelta(seconds=delay_seconds)
|
||||
|
||||
# Operation speichern
|
||||
operation_data = {
|
||||
"id": operation_id,
|
||||
"operation": operation.value,
|
||||
"scheduled_time": scheduled_time,
|
||||
"delay_seconds": delay_seconds,
|
||||
"user_id": user_id,
|
||||
"reason": reason or "Keine Begründung angegeben",
|
||||
"force": force,
|
||||
"created_at": datetime.now(),
|
||||
"status": "scheduled"
|
||||
}
|
||||
|
||||
self.pending_operations[operation_id] = operation_data
|
||||
|
||||
# Operation in separatem Thread ausführen
|
||||
thread = threading.Thread(
|
||||
target=self._execute_delayed_operation,
|
||||
args=(operation_id,),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
system_logger.info(f"🕐 Operation geplant: {operation.value} in {delay_seconds}s")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"operation_id": operation_id,
|
||||
"scheduled_time": scheduled_time.isoformat(),
|
||||
"delay_seconds": delay_seconds,
|
||||
"message": f"Operation '{operation.value}' geplant für {scheduled_time.strftime('%H:%M:%S')}"
|
||||
}
|
||||
|
||||
def _execute_delayed_operation(self, operation_id: str):
|
||||
"""
|
||||
Führt geplante Operation nach Verzögerung aus.
|
||||
|
||||
Args:
|
||||
operation_id: ID der auszuführenden Operation
|
||||
"""
|
||||
try:
|
||||
operation_data = self.pending_operations.get(operation_id)
|
||||
if not operation_data:
|
||||
return
|
||||
|
||||
# Warten bis zur geplanten Zeit
|
||||
scheduled_time = operation_data["scheduled_time"]
|
||||
wait_time = (scheduled_time - datetime.now()).total_seconds()
|
||||
|
||||
if wait_time > 0:
|
||||
time.sleep(wait_time)
|
||||
|
||||
# Status aktualisieren
|
||||
operation_data["status"] = "executing"
|
||||
operation_data["executed_at"] = datetime.now()
|
||||
|
||||
# Operation ausführen
|
||||
operation = SystemOperation(operation_data["operation"])
|
||||
result = self._execute_operation(operation, operation_data)
|
||||
|
||||
# Ergebnis speichern
|
||||
operation_data["result"] = result
|
||||
operation_data["status"] = "completed" if result.get("success") else "failed"
|
||||
operation_data["completed_at"] = datetime.now()
|
||||
|
||||
# In Historie verschieben
|
||||
self._move_to_history(operation_id)
|
||||
|
||||
except Exception as e:
|
||||
system_logger.error(f"Fehler bei verzögerter Operation {operation_id}: {e}")
|
||||
if operation_id in self.pending_operations:
|
||||
self.pending_operations[operation_id]["status"] = "error"
|
||||
self.pending_operations[operation_id]["error"] = str(e)
|
||||
self._move_to_history(operation_id)
|
||||
|
||||
def _execute_operation(self, operation: SystemOperation, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""
|
||||
Führt die eigentliche System-Operation aus.
|
||||
|
||||
Args:
|
||||
operation: Art der Operation
|
||||
operation_data: Operation-Daten
|
||||
|
||||
Returns:
|
||||
Dict mit Ergebnis
|
||||
"""
|
||||
try:
|
||||
system_logger.info(f"▶️ Führe Operation aus: {operation.value}")
|
||||
|
||||
if operation == SystemOperation.RESTART:
|
||||
return self._restart_system(operation_data)
|
||||
elif operation == SystemOperation.SHUTDOWN:
|
||||
return self._shutdown_system(operation_data)
|
||||
elif operation == SystemOperation.KIOSK_RESTART:
|
||||
return self._restart_kiosk(operation_data)
|
||||
elif operation == SystemOperation.KIOSK_ENABLE:
|
||||
return self._enable_kiosk(operation_data)
|
||||
elif operation == SystemOperation.KIOSK_DISABLE:
|
||||
return self._disable_kiosk(operation_data)
|
||||
elif operation == SystemOperation.SERVICE_RESTART:
|
||||
return self._restart_services(operation_data)
|
||||
elif operation == SystemOperation.EMERGENCY_STOP:
|
||||
return self._emergency_stop(operation_data)
|
||||
else:
|
||||
return {"success": False, "error": f"Unbekannte Operation: {operation.value}"}
|
||||
|
||||
except Exception as e:
|
||||
system_logger.error(f"Fehler bei Operation {operation.value}: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def _restart_system(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Startet das System neu."""
|
||||
try:
|
||||
system_logger.warning("🔄 System-Neustart wird ausgeführt...")
|
||||
|
||||
# Cleanup vor Neustart
|
||||
self._cleanup_before_restart()
|
||||
|
||||
# System-Neustart je nach Plattform
|
||||
if self.is_windows:
|
||||
subprocess.run(["shutdown", "/r", "/t", "0"], check=True)
|
||||
else:
|
||||
subprocess.run(["sudo", "systemctl", "reboot"], check=True)
|
||||
|
||||
return {"success": True, "message": "System-Neustart initiiert"}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {"success": False, "error": f"Neustart fehlgeschlagen: {e}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Unerwarteter Fehler: {e}"}
|
||||
|
||||
def _shutdown_system(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Fährt das System herunter."""
|
||||
try:
|
||||
system_logger.warning("🛑 System-Shutdown wird ausgeführt...")
|
||||
|
||||
# Cleanup vor Shutdown
|
||||
self._cleanup_before_restart()
|
||||
|
||||
# System-Shutdown je nach Plattform
|
||||
if self.is_windows:
|
||||
subprocess.run(["shutdown", "/s", "/t", "0"], check=True)
|
||||
else:
|
||||
subprocess.run(["sudo", "systemctl", "poweroff"], check=True)
|
||||
|
||||
return {"success": True, "message": "System-Shutdown initiiert"}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {"success": False, "error": f"Shutdown fehlgeschlagen: {e}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Unerwarteter Fehler: {e}"}
|
||||
|
||||
def _restart_kiosk(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Startet nur den Kiosk-Modus neu."""
|
||||
try:
|
||||
system_logger.info("🖥️ Kiosk-Neustart wird ausgeführt...")
|
||||
|
||||
success_count = 0
|
||||
errors = []
|
||||
|
||||
# Kiosk-Service neustarten
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", "restart", self.services["kiosk"]],
|
||||
check=True, timeout=30)
|
||||
success_count += 1
|
||||
system_logger.info("✅ Kiosk-Service neugestartet")
|
||||
except Exception as e:
|
||||
errors.append(f"Kiosk-Service: {e}")
|
||||
|
||||
# Watchdog-Service neustarten (falls vorhanden)
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", "restart", self.services["watchdog"]],
|
||||
check=True, timeout=30)
|
||||
success_count += 1
|
||||
system_logger.info("✅ Watchdog-Service neugestartet")
|
||||
except Exception as e:
|
||||
errors.append(f"Watchdog-Service: {e}")
|
||||
|
||||
# X11-Session neustarten
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", "restart", "getty@tty1.service"],
|
||||
check=True, timeout=30)
|
||||
success_count += 1
|
||||
system_logger.info("✅ X11-Session neugestartet")
|
||||
except Exception as e:
|
||||
errors.append(f"X11-Session: {e}")
|
||||
|
||||
if success_count > 0:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Kiosk neugestartet ({success_count} Services)",
|
||||
"errors": errors if errors else None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Alle Kiosk-Neustarts fehlgeschlagen",
|
||||
"details": errors
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Kiosk-Neustart fehlgeschlagen: {e}"}
|
||||
|
||||
def _enable_kiosk(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Aktiviert den Kiosk-Modus."""
|
||||
try:
|
||||
system_logger.info("🖥️ Kiosk-Modus wird aktiviert...")
|
||||
|
||||
# Kiosk-Service aktivieren und starten
|
||||
subprocess.run(["sudo", "systemctl", "enable", self.services["kiosk"]],
|
||||
check=True, timeout=30)
|
||||
subprocess.run(["sudo", "systemctl", "start", self.services["kiosk"]],
|
||||
check=True, timeout=30)
|
||||
|
||||
# Watchdog aktivieren
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", "enable", self.services["watchdog"]],
|
||||
check=True, timeout=30)
|
||||
subprocess.run(["sudo", "systemctl", "start", self.services["watchdog"]],
|
||||
check=True, timeout=30)
|
||||
except Exception as e:
|
||||
system_logger.warning(f"Watchdog-Aktivierung fehlgeschlagen: {e}")
|
||||
|
||||
return {"success": True, "message": "Kiosk-Modus aktiviert"}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {"success": False, "error": f"Kiosk-Aktivierung fehlgeschlagen: {e}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Unerwarteter Fehler: {e}"}
|
||||
|
||||
def _disable_kiosk(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Deaktiviert den Kiosk-Modus."""
|
||||
try:
|
||||
system_logger.info("🖥️ Kiosk-Modus wird deaktiviert...")
|
||||
|
||||
# Kiosk-Service stoppen und deaktivieren
|
||||
subprocess.run(["sudo", "systemctl", "stop", self.services["kiosk"]],
|
||||
check=True, timeout=30)
|
||||
subprocess.run(["sudo", "systemctl", "disable", self.services["kiosk"]],
|
||||
check=True, timeout=30)
|
||||
|
||||
# Watchdog stoppen
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", "stop", self.services["watchdog"]],
|
||||
check=True, timeout=30)
|
||||
subprocess.run(["sudo", "systemctl", "disable", self.services["watchdog"]],
|
||||
check=True, timeout=30)
|
||||
except Exception as e:
|
||||
system_logger.warning(f"Watchdog-Deaktivierung fehlgeschlagen: {e}")
|
||||
|
||||
return {"success": True, "message": "Kiosk-Modus deaktiviert"}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {"success": False, "error": f"Kiosk-Deaktivierung fehlgeschlagen: {e}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Unerwarteter Fehler: {e}"}
|
||||
|
||||
def _restart_services(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Startet wichtige Services neu."""
|
||||
try:
|
||||
system_logger.info("🔄 Services werden neugestartet...")
|
||||
|
||||
success_count = 0
|
||||
errors = []
|
||||
|
||||
# HTTPS-Service neustarten
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", "restart", self.services["https"]],
|
||||
check=True, timeout=60)
|
||||
success_count += 1
|
||||
system_logger.info("✅ HTTPS-Service neugestartet")
|
||||
except Exception as e:
|
||||
errors.append(f"HTTPS-Service: {e}")
|
||||
|
||||
# NetworkManager neustarten (falls nötig)
|
||||
try:
|
||||
subprocess.run(["sudo", "systemctl", "restart", "NetworkManager"],
|
||||
check=True, timeout=30)
|
||||
success_count += 1
|
||||
system_logger.info("✅ NetworkManager neugestartet")
|
||||
except Exception as e:
|
||||
errors.append(f"NetworkManager: {e}")
|
||||
|
||||
if success_count > 0:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Services neugestartet ({success_count})",
|
||||
"errors": errors if errors else None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Alle Service-Neustarts fehlgeschlagen",
|
||||
"details": errors
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Service-Neustart fehlgeschlagen: {e}"}
|
||||
|
||||
def _emergency_stop(self, operation_data: Dict) -> Dict[str, Any]:
|
||||
"""Notfall-Stopp aller Services."""
|
||||
try:
|
||||
system_logger.warning("🚨 Notfall-Stopp wird ausgeführt...")
|
||||
|
||||
# Flask-App stoppen
|
||||
try:
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
except Exception as e:
|
||||
system_logger.error(f"Flask-Stopp fehlgeschlagen: {e}")
|
||||
|
||||
return {"success": True, "message": "Notfall-Stopp initiiert"}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Notfall-Stopp fehlgeschlagen: {e}"}
|
||||
|
||||
def _cleanup_before_restart(self):
|
||||
"""Führt Cleanup-Operationen vor Neustart/Shutdown aus."""
|
||||
try:
|
||||
system_logger.info("🧹 Cleanup vor Neustart/Shutdown...")
|
||||
|
||||
# Shutdown-Manager verwenden falls verfügbar
|
||||
try:
|
||||
from utils.shutdown_manager import get_shutdown_manager
|
||||
shutdown_manager = get_shutdown_manager()
|
||||
shutdown_manager.shutdown(exit_code=0)
|
||||
except ImportError:
|
||||
system_logger.warning("Shutdown-Manager nicht verfügbar")
|
||||
|
||||
# Datenbank-Cleanup
|
||||
try:
|
||||
from utils.database_cleanup import safe_database_cleanup
|
||||
safe_database_cleanup(force_mode_switch=False)
|
||||
except ImportError:
|
||||
system_logger.warning("Database-Cleanup nicht verfügbar")
|
||||
|
||||
# Cache leeren
|
||||
self._clear_caches()
|
||||
|
||||
except Exception as e:
|
||||
system_logger.error(f"Cleanup fehlgeschlagen: {e}")
|
||||
|
||||
def _clear_caches(self):
|
||||
"""Leert alle Caches."""
|
||||
try:
|
||||
# User-Cache leeren
|
||||
from app import clear_user_cache, clear_printer_status_cache
|
||||
clear_user_cache()
|
||||
clear_printer_status_cache()
|
||||
|
||||
# System-Cache leeren
|
||||
if not self.is_windows:
|
||||
subprocess.run(["sudo", "sync"], timeout=10)
|
||||
subprocess.run(["sudo", "echo", "3", ">", "/proc/sys/vm/drop_caches"],
|
||||
shell=True, timeout=10)
|
||||
|
||||
except Exception as e:
|
||||
system_logger.warning(f"Cache-Clearing fehlgeschlagen: {e}")
|
||||
|
||||
def _move_to_history(self, operation_id: str):
|
||||
"""Verschiebt abgeschlossene Operation in Historie."""
|
||||
with self.lock:
|
||||
if operation_id in self.pending_operations:
|
||||
operation_data = self.pending_operations.pop(operation_id)
|
||||
self.operation_history.append(operation_data)
|
||||
|
||||
# Historie begrenzen
|
||||
if len(self.operation_history) > self.config["max_operation_history"]:
|
||||
self.operation_history = self.operation_history[-self.config["max_operation_history"]:]
|
||||
|
||||
def cancel_operation(self, operation_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Bricht geplante Operation ab.
|
||||
|
||||
Args:
|
||||
operation_id: ID der abzubrechenden Operation
|
||||
|
||||
Returns:
|
||||
Dict mit Ergebnis
|
||||
"""
|
||||
with self.lock:
|
||||
if operation_id not in self.pending_operations:
|
||||
return {"success": False, "error": "Operation nicht gefunden"}
|
||||
|
||||
operation_data = self.pending_operations[operation_id]
|
||||
if operation_data["status"] == "executing":
|
||||
return {"success": False, "error": "Operation bereits in Ausführung"}
|
||||
|
||||
operation_data["status"] = "cancelled"
|
||||
operation_data["cancelled_at"] = datetime.now()
|
||||
self._move_to_history(operation_id)
|
||||
|
||||
system_logger.info(f"❌ Operation abgebrochen: {operation_id}")
|
||||
|
||||
return {"success": True, "message": "Operation erfolgreich abgebrochen"}
|
||||
|
||||
def get_pending_operations(self) -> List[Dict]:
|
||||
"""Gibt alle geplanten Operationen zurück."""
|
||||
with self.lock:
|
||||
return list(self.pending_operations.values())
|
||||
|
||||
def get_operation_history(self, limit: int = 20) -> List[Dict]:
|
||||
"""Gibt Operation-Historie zurück."""
|
||||
with self.lock:
|
||||
return self.operation_history[-limit:] if limit else self.operation_history
|
||||
|
||||
def get_system_status(self) -> Dict[str, Any]:
|
||||
"""Gibt aktuellen System-Status zurück."""
|
||||
try:
|
||||
# Service-Status prüfen
|
||||
service_status = {}
|
||||
for name, service in self.services.items():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["sudo", "systemctl", "is-active", service],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
service_status[name] = result.stdout.strip()
|
||||
except Exception as e:
|
||||
service_status[name] = f"error: {e}"
|
||||
|
||||
# System-Metriken
|
||||
memory = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage('/')
|
||||
|
||||
# Aktive Operations
|
||||
pending_ops = len(self.pending_operations)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"services": service_status,
|
||||
"system_metrics": {
|
||||
"memory_percent": memory.percent,
|
||||
"memory_available_gb": memory.available / (1024**3),
|
||||
"disk_percent": disk.percent,
|
||||
"disk_free_gb": disk.free / (1024**3),
|
||||
"load_average": psutil.getloadavg()[0] if hasattr(psutil, 'getloadavg') else 0
|
||||
},
|
||||
"operations": {
|
||||
"pending": pending_ops,
|
||||
"history_count": len(self.operation_history)
|
||||
},
|
||||
"is_safe": self.is_safe_to_operate()[0]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
# Globaler System-Control-Manager
|
||||
_system_control_manager: Optional[SystemControlManager] = None
|
||||
_control_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_system_control_manager() -> SystemControlManager:
|
||||
"""
|
||||
Singleton-Pattern für globalen System-Control-Manager.
|
||||
|
||||
Returns:
|
||||
SystemControlManager: Globaler System-Control-Manager
|
||||
"""
|
||||
global _system_control_manager
|
||||
|
||||
with _control_lock:
|
||||
if _system_control_manager is None:
|
||||
_system_control_manager = SystemControlManager()
|
||||
return _system_control_manager
|
||||
|
||||
|
||||
# Convenience-Funktionen
|
||||
def schedule_system_restart(delay_seconds: int = 60, user_id: str = None, reason: str = None, force: bool = False) -> Dict[str, Any]:
|
||||
"""Plant System-Neustart."""
|
||||
manager = get_system_control_manager()
|
||||
return manager.schedule_operation(SystemOperation.RESTART, delay_seconds, user_id, reason, force)
|
||||
|
||||
|
||||
def schedule_system_shutdown(delay_seconds: int = 30, user_id: str = None, reason: str = None, force: bool = False) -> Dict[str, Any]:
|
||||
"""Plant System-Shutdown."""
|
||||
manager = get_system_control_manager()
|
||||
return manager.schedule_operation(SystemOperation.SHUTDOWN, delay_seconds, user_id, reason, force)
|
||||
|
||||
|
||||
def restart_kiosk(delay_seconds: int = 10, user_id: str = None, reason: str = None) -> Dict[str, Any]:
|
||||
"""Plant Kiosk-Neustart."""
|
||||
manager = get_system_control_manager()
|
||||
return manager.schedule_operation(SystemOperation.KIOSK_RESTART, delay_seconds, user_id, reason)
|
||||
|
||||
|
||||
def get_system_status() -> Dict[str, Any]:
|
||||
"""Gibt System-Status zurück."""
|
||||
manager = get_system_control_manager()
|
||||
return manager.get_system_status()
|
Reference in New Issue
Block a user